refactor(*): 平台架构 - 拆分后端领域模块并同步插件协议与模型能力

This commit is contained in:
ddCat
2026-09-11 14:02:49 +08:00
parent 916043fb5a
commit 6e5cbc8b05
644 changed files with 62731 additions and 2320 deletions

View File

@@ -49,7 +49,7 @@ export function ApiLogDetailDrawer({ logId, onClose, onLogUpdated }: { logId: st
};
return (
<Drawer title="请求详情" open={Boolean(logId)} onClose={onClose} size="min(920px, 100vw)" destroyOnHidden rootClassName="admin-drawer">
<Drawer title="请求详情" open={Boolean(logId)} onClose={onClose} width="min(1200px, 90vw)" destroyOnHidden rootClassName="admin-drawer">
{loading ? <Skeleton active paragraph={{ rows: 12 }} /> : log ? <LogDetail log={log} querying={querying} onQueryProviderTask={queryProviderTask} /> : <EmptyState size="compact" title="没有请求详情" />}
</Drawer>
);
@@ -64,27 +64,64 @@ function LogDetail({ log, querying, onQueryProviderTask }: { log: ApiCallLog; qu
["状态", <AdminStatusBadge label={failed ? "失败" : processing ? "处理中" : "成功"} tone={failed ? "error" : processing ? "warning" : "success"} />],
[
"用户",
<span>
{log.userDisplayName || log.userAccount || "未知用户"}
{log.userAccount ? <span className="ml-2 text-foreground/45">@{log.userAccount}</span> : null}
</span>,
<div>
<div className="font-medium text-foreground/90">{log.userDisplayName || log.userAccount || "未知用户"}</div>
{log.userAccount ? <div className="mt-0.5 text-xs text-foreground/50">@{log.userAccount}</div> : null}
</div>,
],
[
"渠道 / 模型",
<div>
<div className="text-foreground/85">{log.channelName || "未记录渠道"}</div>
<div className="mt-0.5 font-mono text-xs text-foreground/55" title={log.model}>
{log.model || "未识别模型"}
</div>
</div>,
],
["渠道 / 模型", `${log.channelName || "未记录渠道"} / ${log.model || "未识别模型"}`],
["能力", capabilityText(log.capability)],
["请求阶段", requestKindText(log.requestKind)],
["计费属性", log.billable ? "计费调用" : "不计费"],
["总耗时", formatDuration(log.durationMs)],
["视频轮询", log.capability === "video" ? `${log.pollCount || 0} 次` : "--"],
["Token", log.usageAvailable ? `${log.inputTokens} 输入 / ${log.outputTokens} 输出 / ${log.cachedTokens} 缓存` : "未返回"],
["总耗时", <span className="tabular-nums">{formatDuration(log.durationMs)}</span>],
["视频轮询", log.capability === "video" ? <span className="tabular-nums">{log.pollCount || 0} 次</span> : <span className="text-foreground/35">--</span>],
[
"Token 用量",
log.usageAvailable ? (
<div className="space-y-1 font-mono text-sm tabular-nums">
<div>
<span className="text-foreground/55">输入</span> <span className="ml-2 text-foreground/85">{log.inputTokens.toLocaleString()}</span>
</div>
<div>
<span className="text-foreground/55">输出</span> <span className="ml-2 text-foreground/85">{log.outputTokens.toLocaleString()}</span>
</div>
{log.cachedTokens > 0 ? (
<div>
<span className="text-foreground/55">缓存</span> <span className="ml-2 text-foreground/85">{log.cachedTokens.toLocaleString()}</span>
</div>
) : null}
</div>
) : (
<span className="text-foreground/35">未返回</span>
),
],
["积分计费", billingText(log)],
["上游成本", log.costAvailable ? `${log.currency || "USD"} ${(log.estimatedCostMicros / 1_000_000).toFixed(6)}` : "未配置成本"],
["错误信息", [log.errorCode, log.error].filter(Boolean).join(" · ") || "--"],
["方法与路径", `${log.method} ${log.path}`],
["请求 Content-Type", log.requestContentType || "--"],
["HTTP 状态", String(log.statusCode || "--")],
["任务 ID", log.taskId || "--"],
["供应商任务 ID", log.providerRequestId || "--"],
["上游地址", log.upstreamUrl || "--"],
["上游成本", log.costAvailable ? <span className="font-mono tabular-nums">{log.currency || "USD"} {(log.estimatedCostMicros / 1_000_000).toFixed(6)}</span> : <span className="text-foreground/35">未配置成本</span>],
[
"错误信息",
[log.errorCode, log.error].filter(Boolean).length > 0 ? (
<div>
{log.errorCode ? <div className="mb-1 font-mono text-sm font-medium text-red-500">{log.errorCode}</div> : null}
{log.error ? <div className="text-sm text-foreground/75">{log.error}</div> : null}
</div>
) : (
<span className="text-foreground/35">--</span>
),
],
["方法与路径", <code className="text-sm text-foreground/80">{log.method} {log.path}</code>],
["请求 Content-Type", log.requestContentType ? <code className="text-sm text-foreground/70">{log.requestContentType}</code> : <span className="text-foreground/35">--</span>],
["HTTP 状态", log.statusCode ? <span className="tabular-nums">{log.statusCode}</span> : <span className="text-foreground/35">--</span>],
["任务 ID", log.taskId ? <code className="font-mono text-sm text-foreground/70">{log.taskId}</code> : <span className="text-foreground/35">--</span>],
["供应商任务 ID", log.providerRequestId ? <code className="font-mono text-sm text-foreground/70">{log.providerRequestId}</code> : <span className="text-foreground/35">--</span>],
["上游地址", log.upstreamUrl ? <code className="break-all text-sm text-foreground/70">{log.upstreamUrl}</code> : <span className="text-foreground/35">--</span>],
].map(([label, children], index) => ({ key: String(index), label, children }));
const canQueryProviderTask = log.capability === "video" && log.taskStatus === "failed" && Boolean(log.taskId && log.providerRequestId);
@@ -98,9 +135,9 @@ function LogDetail({ log, querying, onQueryProviderTask }: { log: ApiCallLog; qu
</Button>
</div>
) : null}
<Descriptions bordered size="small" column={{ xs: 1, sm: 1, md: 2 }} items={items} />
<Descriptions bordered size="middle" column={{ xs: 1, sm: 1, md: 1, lg: 2, xl: 2 }} labelStyle={{ width: "160px", fontWeight: 500 }} items={items} />
<section>
<div className="mb-2 text-sm font-semibold text-foreground/85">原始报文</div>
<div className="mb-3 text-base font-semibold text-foreground/90">原始报文</div>
<Tabs
items={[
{ key: "request", label: "请求报文", children: <PayloadPanel value={log.requestBody} empty="该请求未记录请求报文" /> },

View File

@@ -4,7 +4,7 @@ import { Callout } from "@/components/ui/product/callout";
import { RotateCcw } from "lucide-react";
import type { ReactNode } from "react";
import type { CapabilitySpec, OptionConstraint } from "@/services/api/logical-models";
import type { CapabilityImageSize, CapabilitySpec, OptionConstraint } from "@/services/api/logical-models";
import type { ChannelModel } from "@/services/api/wallet";
import { STANDARD_IMAGE_SIZE_VALUES } from "@/lib/model-capabilities";
@@ -106,6 +106,7 @@ export function capabilitySpecFromChannelModel(item?: ChannelModel): CapabilityS
transparentBackground: { values: image.transparentBackground.supported ? [false, true] : [false] },
count: { min: 1, max: image.maxOutputs, step: 1 },
}),
imageSize: image.size.parameter === "none" ? undefined : { parameter: image.size.parameter, allowCustom: image.size.allowCustom, presets: image.size.presets },
};
}
if (capability === "video") {
@@ -175,9 +176,22 @@ export function mergeCapabilitySpecs(capability: CapabilityKind, specs: Capabili
}
result.options![definition.name] = preferredSourceConstraint(constraints);
}
result.imageSize = mergeImageSize(matching);
return result;
}
function mergeImageSize(specs: CapabilitySpec[]): CapabilityImageSize | undefined {
const parts = specs.map((item) => item.imageSize).filter((item): item is CapabilityImageSize => Boolean(item));
if (!parts.length) return undefined;
const parameters = [...new Set(parts.map((item) => item.parameter).filter((value): value is NonNullable<CapabilityImageSize["parameter"]> => Boolean(value)))];
const presets = [...new Map(parts.flatMap((item) => item.presets || []).map((preset) => [`${preset.tier}:${preset.ratio}:${preset.size}`, preset])).values()];
return {
parameter: parameters.includes("aspect_ratio") ? "aspect_ratio" : parameters[0],
allowCustom: parts.some((item) => item.allowCustom),
presets,
};
}
/**
* 旧版本把“允许自定义”保存成只有 `*` 的能力范围。
* 管理端编辑时用当前供应线路的标准值补回展示范围,保存后即可完成数据修复。
@@ -191,7 +205,7 @@ export function normalizeCapabilitySpecForSources(value: CapabilitySpec | undefi
if (!sourceConstraint || !isWildcardConstraint(constraint) || !sourceConstraint.values) continue;
options[name] = { values: uniqueScalars([...(sourceConstraint.values || []), ...(constraint.values || [])]) };
}
return { ...value, options };
return { ...value, options, imageSize: value.imageSize?.presets?.length ? value.imageSize : source.imageSize || value.imageSize };
}
export function capabilitySourceError(capability: CapabilityKind, sourceSpecs: CapabilitySpec[], value?: CapabilitySpec) {

View File

@@ -2519,6 +2519,7 @@ function InfiniteCanvasPage() {
node={dialogNode}
viewport={viewport}
containerRef={containerRef}
allowOverflow={dialogNode.type !== CanvasNodeType.Config}
dragOffset={dragPreview?.nodeIds.has(dialogNode.id) ? { x: dragPreview.x, y: dragPreview.y } : null}
isDragging={isNodeDragging && Boolean(dragPreview?.nodeIds.has(dialogNode.id))}
>

View File

@@ -1,4 +1,4 @@
import { ArrowUpRight, UsersRound } from "lucide-react";
import { ArrowUpRight, BadgeCheck, UsersRound } from "lucide-react";
import { CometCard } from "@/components/ui/aceternity/comet-card";
@@ -13,20 +13,43 @@ export function WelcomeContributorsCard() {
<section className="welcome-contributors" aria-labelledby="welcome-contributors-title">
<CometCard containerClassName="welcome-contributors-perspective" className="welcome-contributors-card" rotateDepth={3.5} translateDepth={3}>
<div className="welcome-contributors-heading">
<span className="welcome-contributors-kicker"><UsersRound size={15} />Contributors</span>
<span className="welcome-contributors-kicker">
<UsersRound size={15} />
Contributors
</span>
<div>
<h3 id="welcome-contributors-title">一起把故事搬上银幕</h3>
<p>{welcomeContributors.length} 位创作者参与产品、开发、测试与社区建设</p>
</div>
<a href={githubContributors} target="_blank" rel="noreferrer" aria-label="在 GitHub README 查看贡献者清单">
查看清单<ArrowUpRight size={14} />
查看清单
<ArrowUpRight size={14} />
</a>
</div>
<ul className="welcome-contributors-list" aria-label="项目贡献者">
{welcomeContributors.map((contributor) => (
<li key={contributor.name} title={contributor.signature || contributor.name}>
<li
key={contributor.name}
className={contributor.isFounder ? "is-founder" : undefined}
title={contributor.signature || contributor.name}
aria-label={[contributor.name, contributor.wechat && `微信:${contributor.wechat}`, contributor.email].filter(Boolean).join(",")}
>
<img src={contributor.avatar} alt="" loading="lazy" />
<span>{contributor.name}</span>
{contributor.isFounder ? (
<div className="welcome-contributor-content">
<div className="welcome-contributor-name-row">
<span className="welcome-contributor-name">{contributor.name}</span>
<span className="welcome-contributor-badge">
<BadgeCheck size={13} />
项目发起者
</span>
</div>
<p className="welcome-contributor-meta">{[contributor.wechat && `微信:${contributor.wechat}`, contributor.email].filter(Boolean).join(" · ")}</p>
<p className="welcome-contributor-signature">{contributor.signature}</p>
</div>
) : (
<span>{contributor.name}</span>
)}
</li>
))}
</ul>

View File

@@ -10,6 +10,9 @@ export type WelcomeContributor = {
name: string;
avatar: string;
signature: string;
email?: string;
wechat?: string;
isFounder?: boolean;
};
function plainText(value: string) {
@@ -29,16 +32,33 @@ function parseContributors(markdown: string): WelcomeContributor[] {
.split("\n")
.filter((line) => /^\|\s*<img\s/i.test(line))
.flatMap((line) => {
const columns = line.slice(1, line.lastIndexOf("|")).split("|").map((column) => column.trim());
const columns = line
.slice(1, line.lastIndexOf("|"))
.split("|")
.map((column) => column.trim());
const image = columns[0]?.match(/src="([^"]+)"[^>]*alt="([^"]*)"/i);
const name = plainText(columns[1]?.split(/<br\s*\/?>/i)[0] ?? image?.[2] ?? "");
const nameColumn = columns[1] ?? "";
const name = plainText(nameColumn.split(/<br\s*\/?>/i)[0] ?? image?.[2] ?? "");
const avatarPath = image?.[1];
if (!avatarPath || !name) return [];
const avatar = contributorAvatarModules[`../../../../${avatarPath}`];
if (!avatar) return [];
return [{ name, avatar, signature: plainText(columns[3] ?? "") }];
const nameDetails = plainText(nameColumn);
const email = plainText(columns[2] ?? "");
const wechat = nameDetails.match(/微信\s*[::]\s*([^·]+)/)?.[1]?.trim();
return [
{
name,
avatar,
signature: plainText(columns[3] ?? ""),
email: email || undefined,
wechat,
isFounder: nameDetails.includes("项目发起者"),
},
];
});
}

View File

@@ -143,7 +143,8 @@ function WelcomeExperience({ look, brandName, onLookChange }: { look: WelcomeLoo
<div className="welcome-workbench-caption"><p>{active.detail}</p></div>
</section>
<section className="welcome-ending"><h2>你的故事,<br />现在开始。</h2><Button className="welcome-primary" type="primary" size="large" href="/create" icon={<ArrowRight size={20} />} iconPlacement="end">开始创作</Button><a className="welcome-source" href={github} target="_blank" rel="noreferrer"><Code2 size={16} />GitHub<ArrowUpRight size={14} /></a><WelcomeContributorsCard /></section>
<section className="welcome-ending"><h2>你的故事,<br />现在开始。</h2>
<WelcomeContributorsCard /></section>
</main>
<footer className="welcome-footer"><a href="/welcome">{brandName}</a><span>开源 AI 影视创作工作台</span><a href={`${github}/blob/main/LICENSE`} target="_blank" rel="noreferrer">Open Source · MIT License<ArrowUpRight size={12} /></a></footer>
{look.credit && <div className="welcome-media-credit"><a href={`/welcome/credits.html#${look.id}`} target="_blank" rel="noreferrer">{look.credit} · 署名与许可<ArrowUpRight size={12} /></a></div>}

File diff suppressed because it is too large Load Diff