fix(merge): PR 集成 - fix merge 上传状态与易支付校验

在 #544、#545、#546 的原作者 merge commit 之后独立修复:保留上传 Promise、处理重复上传与复制失败、格式对齐,以及易支付网关、订单证据、签名、通知幂等与安全关单校验。同步重建六平台插件产物和包。

验证:前端 1832 tests、build/typecheck、lint、相关格式检查通过;财务 SQLite、支付、插件 RPC 与产物专项通过。后端全量出现一项未改动的素材删除 SQLite 表锁失败,单独复跑 3 次通过,保留全量未通过记录。浏览器与真实商户验收待完成。#543 因迁移历史与能力限制回退暂缓。
This commit is contained in:
ddCat
2026-09-20 05:18:16 +08:00
parent 9c22ea1d76
commit 3e35f7d687
20 changed files with 398 additions and 117 deletions

View File

@@ -10,14 +10,7 @@ import { Area, CartesianGrid, ComposedChart, Legend, Line, ResponsiveContainer,
import { useSearchParams } from "react-router";
import { ListToolbar, PaginationBar, AdminDataTable, AdminExportButton, AdminFilterChip, AdminStatusBadge, AdminTableEmpty, type AdminStatusTone } from "./admin-ui";
import {
exportAdminAnalytics,
getAdminAnalytics,
listAdminUsers,
type AdminReferenceData,
type AdminAnalytics,
type AnalyticsFilters,
} from "@/services/api/auth";
import { exportAdminAnalytics, getAdminAnalytics, listAdminUsers, type AdminReferenceData, type AdminAnalytics, type AnalyticsFilters } from "@/services/api/auth";
import { analyticsFinanceColumns, formatCredits, formatFinanceCost, formatFinanceMargin } from "./analytics-finance";
type Props = {
@@ -300,11 +293,26 @@ export default function AnalyticsPanel({ users, channels }: Props) {
<AnalyticsHealthCard
icon={<CircleDollarSign className="size-4" />}
label="费用统计(积分)"
value={finance ? <dl className="admin-analytics-finance-values">
<div><dt>收入</dt><dd>{formatCredits(finance.revenueMicrocredits)}</dd></div>
<div><dt>成本</dt><dd>{formatFinanceCost(finance)}</dd></div>
<div><dt>利润</dt><dd>{formatCredits(finance.profitMicrocredits)}</dd></div>
</dl> : "--"}
value={
finance ? (
<dl className="admin-analytics-finance-values">
<div>
<dt>收入</dt>
<dd>{formatCredits(finance.revenueMicrocredits)}</dd>
</div>
<div>
<dt>成本</dt>
<dd>{formatFinanceCost(finance)}</dd>
</div>
<div>
<dt>利润</dt>
<dd>{formatCredits(finance.profitMicrocredits)}</dd>
</div>
</dl>
) : (
"--"
)
}
detail={finance ? `已结算 ${finance.settledOrders} 笔 · 利润率 ${formatFinanceMargin(finance.profitMargin)}` : undefined}
tone={finance && (finance.costedOrders < finance.settledOrders || (finance.profitMicrocredits ?? 0) < 0) ? "warning" : "neutral"}
/>
@@ -390,7 +398,15 @@ export default function AnalyticsPanel({ users, channels }: Props) {
icon={<CircleDollarSign className="size-4" />}
label="成本覆盖订单"
value={finance ? `${finance.costedOrders}/${finance.settledOrders}` : "--"}
description={!finance ? "财务统计尚未返回" : finance.settledOrders ? (finance.costedOrders === finance.settledOrders ? "已结算订单成本完整" : `${finance.settledOrders - finance.costedOrders} 笔缺少成本快照或有效用量`) : "当前范围暂无已结算订单"}
description={
!finance
? "财务统计尚未返回"
: finance.settledOrders
? finance.costedOrders === finance.settledOrders
? "已结算订单成本完整"
: `${finance.settledOrders - finance.costedOrders} 笔缺少成本快照或有效用量`
: "当前范围暂无已结算订单"
}
tone={finance && finance.costedOrders < finance.settledOrders ? "warning" : "neutral"}
onClick={() => openAnalysis("models")}
/>
@@ -449,7 +465,6 @@ export default function AnalyticsPanel({ users, channels }: Props) {
]}
/>
</section>
</div>
);
}

View File

@@ -322,7 +322,7 @@ export default function AdminPluginsPage() {
/>
}
/>
<UploadPluginModal open={uploadOpen} onClose={() => setUploadOpen(false)} onUpload={(file) => void upload(file)} />
<UploadPluginModal open={uploadOpen} onClose={() => setUploadOpen(false)} onUpload={upload} />
</AdminPageFrame>
);
}

View File

@@ -1,6 +1,6 @@
import { Modal, Upload } from "antd";
import { App, Modal, Upload } from "antd";
import { CloudUpload, FileText, ShieldCheck } from "lucide-react";
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import type { RegisteredPlugin } from "@/lib/plugins/plugin-types";
@@ -15,26 +15,27 @@ type UploadPluginModalProps = {
onUpload: (file: File) => void | Promise<void>;
};
const MINIMAL_MANIFEST_SNIPPET = `{
"apiVersion": "yingce.plugin/v1",
"id": "my-custom-plugin",
"name": "我的自定义插件",
"version": "1.0.0",
"author": "Developer",
"description": "通过统一清单扩展平台功能",
"permissions": ["generation.run", "media.read"],
"contributes": {
"providers": [],
"workflows": []
}
}`;
const MANIFEST_EXAMPLE = pluginDevelopmentGuideMarkdown.match(/```json\s*([\s\S]*?)```/)?.[1].trim();
export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModalProps) {
const { message } = App.useApp();
const [activeTab, setActiveTab] = useState<"install" | "guide">("install");
const [isDraggingPlugin, setIsDraggingPlugin] = useState(false);
const [uploading, setUploading] = useState(false);
const [copied, setCopied] = useState(false);
const dragDepth = useRef(0);
const uploadInFlight = useRef(false);
const copyTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => {
if (!open) {
setActiveTab("install");
setIsDraggingPlugin(false);
setCopied(false);
dragDepth.current = 0;
}
return () => clearTimeout(copyTimer.current);
}, [open]);
const handlePluginDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
@@ -58,19 +59,30 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
};
const handleExecuteUpload = async (file: File) => {
if (uploadInFlight.current) return;
uploadInFlight.current = true;
setUploading(true);
try {
await onUpload(file);
} catch (error) {
message.error(error instanceof Error ? error.message : "安装插件失败");
} finally {
uploadInFlight.current = false;
setUploading(false);
}
};
const copySnippet = () => {
void navigator.clipboard?.writeText(MINIMAL_MANIFEST_SNIPPET).then(() => {
const copySnippet = async () => {
try {
if (!MANIFEST_EXAMPLE) throw new Error("开发规范中缺少清单示例");
if (!navigator.clipboard) throw new Error("当前环境不支持剪贴板,请从开发规范中手动复制");
await navigator.clipboard.writeText(MANIFEST_EXAMPLE);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
clearTimeout(copyTimer.current);
copyTimer.current = setTimeout(() => setCopied(false), 2000);
} catch (error) {
message.error(error instanceof Error ? error.message : "复制失败,请手动复制");
}
};
const isCompact = activeTab === "install";
@@ -83,37 +95,25 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
centered
footer={null}
destroyOnHidden
closable={!uploading}
mask={{ closable: !uploading }}
onCancel={uploading ? undefined : onClose}
styles={{ body: { maxHeight: "min(84vh, 900px)", overflowY: "auto", overscrollBehavior: "contain", padding: "16px 20px 24px" } }}
>
<div className="plugin-upload-tabs-bar">
<div className="plugin-upload-tabs-nav">
<button
type="button"
className={`plugin-upload-tab-btn ${activeTab === "install" ? "is-active" : ""}`}
onClick={() => setActiveTab("install")}
>
<button type="button" className={`plugin-upload-tab-btn ${activeTab === "install" ? "is-active" : ""}`} aria-pressed={activeTab === "install"} onClick={() => setActiveTab("install")}>
<CloudUpload className="size-4" />
<span>安装插件包</span>
</button>
<button
type="button"
className={`plugin-upload-tab-btn ${activeTab === "guide" ? "is-active" : ""}`}
onClick={() => setActiveTab("guide")}
>
<button type="button" className={`plugin-upload-tab-btn ${activeTab === "guide" ? "is-active" : ""}`} aria-pressed={activeTab === "guide"} onClick={() => setActiveTab("guide")}>
<FileText className="size-4" />
<span>开发规范与示例</span>
</button>
</div>
{activeTab === "guide" ? (
<button
type="button"
className="plugin-upload-copy-btn"
onClick={copySnippet}
title="复制最小 manifest.json 配置模板"
>
<span>{copied ? "已复制清单" : "复制最小清单"}</span>
<button type="button" className="plugin-upload-copy-btn" onClick={() => void copySnippet()} title="复制开发规范中的 manifest.json 配置示例">
<span>{copied ? "已复制清单" : "复制清单示例"}</span>
</button>
) : (
<div className="plugin-upload-badge">
@@ -126,7 +126,9 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
{activeTab === "install" ? (
<div className="plugin-upload-install-view">
<div className="plugin-upload-panel-heading">
<span className="plugin-upload-panel-icon"><CloudUpload className="size-5" /></span>
<span className="plugin-upload-panel-icon">
<CloudUpload className="size-5" />
</span>
<div>
<h2>安装插件包</h2>
<p>选择统一站点插件包,安装后会立即进入插件中心。</p>
@@ -204,13 +206,15 @@ export function PluginDetailsModal({ plugin, restoreFocus, onClose }: PluginDeta
return (
<Modal
className="workspace-modal workspace-modal-wide plugin-details-modal"
title={plugin ? (
<div className="plugin-details-title">
<FileText className="size-4" />
<span>{plugin.manifest.name}</span>
<span className="plugin-version">v{plugin.manifest.version}</span>
</div>
) : null}
title={
plugin ? (
<div className="plugin-details-title">
<FileText className="size-4" />
<span>{plugin.manifest.name}</span>
<span className="plugin-version">v{plugin.manifest.version}</span>
</div>
) : null
}
open={Boolean(plugin)}
centered
footer={null}

View File

@@ -4,12 +4,24 @@ import { analyticsFinanceColumns } from "../src/pages/admin/components/analytics
function renderFinanceCells(finance?: AnalyticsFinance | null) {
const row: AdminAnalytics["models"][number] = {
model: "example", capability: "text", tasks: 1, requests: 1, uniqueUsers: 1,
taskSuccessRate: 100, requestSuccessRate: 100, p50DurationMs: 100, p95DurationMs: 100,
inputTokens: 10, outputTokens: 10, cachedTokens: 0, usageAvailable: true,
mediaCount: 0, videoSeconds: 0, ...(finance === undefined ? {} : { finance }),
model: "example",
capability: "text",
tasks: 1,
requests: 1,
uniqueUsers: 1,
taskSuccessRate: 100,
requestSuccessRate: 100,
p50DurationMs: 100,
p95DurationMs: 100,
inputTokens: 10,
outputTokens: 10,
cachedTokens: 0,
usageAvailable: true,
mediaCount: 0,
videoSeconds: 0,
...(finance === undefined ? {} : { finance }),
};
return analyticsFinanceColumns.map((column) => "render" in column ? column.render?.(undefined, row, 0) : undefined);
return analyticsFinanceColumns.map((column) => ("render" in column ? column.render?.(undefined, row, 0) : undefined));
}
test("model finance cells tolerate missing and null backend finance", () => {
@@ -18,16 +30,13 @@ test("model finance cells tolerate missing and null backend finance", () => {
});
test("model finance cells preserve real zero values and unknown margins", () => {
expect(renderFinanceCells({ settledOrders: 1, costedOrders: 1, revenueMicrocredits: 0, costMicrocredits: 0, profitMicrocredits: 0, profitMargin: null }))
.toEqual(["0", "0", "0", "--"]);
expect(renderFinanceCells({ settledOrders: 1, costedOrders: 1, revenueMicrocredits: 0, costMicrocredits: 0, profitMicrocredits: 0, profitMargin: null })).toEqual(["0", "0", "0", "--"]);
});
test("model finance cells show partial costs without inventing profits", () => {
expect(renderFinanceCells({ settledOrders: 2, costedOrders: 1, revenueMicrocredits: 1500000, costMicrocredits: 250000, profitMicrocredits: null, profitMargin: null }))
.toEqual(["1.5", "0.25(部分)", "--", "--"]);
expect(renderFinanceCells({ settledOrders: 2, costedOrders: 1, revenueMicrocredits: 1500000, costMicrocredits: 250000, profitMicrocredits: null, profitMargin: null })).toEqual(["1.5", "0.25(部分)", "--", "--"]);
});
test("model finance cells display known losses", () => {
expect(renderFinanceCells({ settledOrders: 1, costedOrders: 1, revenueMicrocredits: 1000000, costMicrocredits: 1500000, profitMicrocredits: -500000, profitMargin: -50 }))
.toEqual(["1", "1.5", "-0.5", "-50.0%"]);
expect(renderFinanceCells({ settledOrders: 1, costedOrders: 1, revenueMicrocredits: 1000000, costMicrocredits: 1500000, profitMicrocredits: -500000, profitMargin: -50 })).toEqual(["1", "1.5", "-0.5", "-50.0%"]);
});

View File

@@ -0,0 +1,43 @@
import { expect, test } from "bun:test";
import { registerPlugin, unregisterPlugin } from "../src/lib/plugins/plugin-registry";
import type { PluginManifest } from "../src/lib/plugins/plugin-types";
test("plugin upload caller preserves the installation promise", async () => {
const page = await Bun.file(new URL("../src/pages/admin/plugins/plugins-page.tsx", import.meta.url)).text();
expect(page).toContain("onUpload={upload}");
expect(page).not.toContain("onUpload={(file) => void upload(file)}");
});
test("plugin upload guards concurrent events and exposes installation failures", async () => {
const modal = await Bun.file(new URL("../src/pages/plugins/plugin-documentation-modals.tsx", import.meta.url)).text();
expect(modal).toContain("if (uploadInFlight.current) return;");
expect(modal.indexOf("uploadInFlight.current = true")).toBeLessThan(modal.indexOf("await onUpload(file)"));
expect(modal).toContain("uploadInFlight.current = false");
expect(modal).toContain("closable={!uploading}");
expect(modal).toContain('message.error(error instanceof Error ? error.message : "安装插件失败")');
expect(modal).toContain('aria-pressed={activeTab === "install"}');
expect(modal).toContain('aria-pressed={activeTab === "guide"}');
expect(modal).toContain("if (!navigator.clipboard) throw new Error");
expect(modal).toContain("clearTimeout(copyTimer.current)");
});
test("copied guide manifest is accepted by the real plugin registry", async () => {
const guide = await Bun.file(new URL("../src/pages/plugins/plugin-development-guide.md", import.meta.url)).text();
const example = guide.match(/```json\s*([\s\S]*?)```/)?.[1].trim();
expect(example).toBeTruthy();
const manifest = { ...(JSON.parse(example!) as PluginManifest), id: "merge-test-manifest-example" };
try {
expect(() => registerPlugin({ manifest })).not.toThrow();
} finally {
unregisterPlugin(manifest.id);
}
});
test("reduced motion preserves modal completion events", async () => {
const css = await Bun.file(new URL("../src/pages/admin/theme/admin-chrome.css", import.meta.url)).text();
const reducedMotion = css.slice(css.lastIndexOf("@media (prefers-reduced-motion: reduce)"));
expect(reducedMotion).toContain("animation-duration: 0.01ms !important");
expect(reducedMotion).toContain("transition-duration: 0.01ms !important");
expect(reducedMotion).not.toContain("animation: none");
expect(reducedMotion).not.toContain("transition: none");
});