refactor(*): 代码质量治理 - 收敛接口客户端、模型职责与异步计费巡检

This commit is contained in:
ddCat
2026-08-06 16:36:51 +08:00
parent 9ca9dd9df6
commit b3199c5e7e
42 changed files with 3051 additions and 2271 deletions

View File

@@ -55,21 +55,21 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
const [paramsExpanded, setParamsExpanded] = useState(false); // #98 决策2:B区参数区折叠状态(手风琴)
const generationCount = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
const priceChannel = resolveModelChannel(config, config.model);
const credits = requestCreditCost({ channelMode: priceChannel.scope === "system" ? "remote" : "local", modelCosts: priceChannel.modelCosts, model: modelOptionName(config.model), count: mode === "image" ? generationCount : 1, seconds: mode === "video" ? config.videoSeconds : 1 });
const credits = requestCreditCost({
channelMode: priceChannel.scope === "system" ? "remote" : "local",
modelCosts: priceChannel.modelCosts,
model: modelOptionName(config.model),
count: mode === "image" ? generationCount : 1,
seconds: mode === "video" ? config.videoSeconds : 1,
});
const activeReferenceCount = mentionReferences.filter((item) => item.active && item.kind !== "skill").length;
const videoFrameOptions = mentionReferences
.filter((item) => item.active && item.kind === "image")
.map((item) => ({ nodeId: item.nodeId, label: item.label, title: item.title, previewUrl: item.previewUrl }));
const videoFrameOptions = mentionReferences.filter((item) => item.active && item.kind === "image").map((item) => ({ nodeId: item.nodeId, label: item.label, title: item.title, previewUrl: item.previewUrl }));
const darkSurface = themeName === "dark";
const monochromeAccent = theme.node.activeStroke;
const shellBorder = darkSurface ? "rgba(255,255,255,.08)" : "rgba(15,23,42,.08)";
const insetBorder = darkSurface ? "rgba(255,255,255,.06)" : "rgba(15,23,42,.07)";
const shellSurface = darkSurface
? theme.canvas.background
: "rgba(250,251,252,.97)";
const composerSurface = darkSurface
? theme.canvas.background
: "rgba(15,23,42,.025)";
const shellSurface = darkSurface ? theme.canvas.background : "rgba(250,251,252,.97)";
const composerSurface = darkSurface ? theme.canvas.background : "rgba(15,23,42,.025)";
const controlSurface = darkSurface ? theme.canvas.background : "rgba(15,23,42,.045)";
const controlsSurface = darkSurface ? controlSurface : "transparent";
const shellShadow = darkSurface ? `0 22px 60px ${theme.spatial.shadow}, 0 2px 8px rgba(0,0,0,.22)` : "0 12px 32px rgba(15,23,42,.10), 0 1px 2px rgba(15,23,42,.06)";
@@ -82,7 +82,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
const canExpandPrompt = mode === "image" || mode === "video";
const isPortraitTexture = mode === "image" && Boolean(node.metadata?.portraitTexture);
const updatePromptContentHeight = useCallback((height: number) => {
setPromptContentHeight((current) => Math.abs(current - height) < 1 ? current : height);
setPromptContentHeight((current) => (Math.abs(current - height) < 1 ? current : height));
}, []);
useEffect(() => {
@@ -141,11 +141,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
const renderComposerHeader = (expanded: boolean) => (
<div className="flex min-w-0 items-center gap-1 px-0.5">
{isPortraitTexture ? (
<CanvasPortraitTexturePopover
value={node.metadata?.portraitTexture}
placement={expanded ? "topRight" : "topLeft"}
onChange={(portraitTexture) => onConfigChange(node.id, { portraitTexture })}
/>
<CanvasPortraitTexturePopover value={node.metadata?.portraitTexture} placement={expanded ? "topRight" : "topLeft"} onChange={(portraitTexture) => onConfigChange(node.id, { portraitTexture })} />
) : (
<div className="flex h-6 min-w-0 items-center gap-1 rounded-md px-1.5 transition-colors hover:bg-white/[.04]" style={{ background: controlSurface }}>
<span className="grid size-3.5 shrink-0 place-items-center" style={{ color: monochromeAccent }}>
@@ -154,16 +150,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
<span className="truncate text-[var(--fs-tiny)] font-medium">{modeDisplayName(mode)}创作</span>
</div>
)}
{!simpleMode ? (
<CanvasPresetPicker
mode={mode}
skillReferences={skillReferences}
open={expanded ? expandedPresetOpen : presetOpen}
onOpenChange={expanded ? setExpandedPresetOpen : setPresetOpen}
onSelect={applyPreset}
dense
/>
) : null}
{!simpleMode ? <CanvasPresetPicker mode={mode} skillReferences={skillReferences} open={expanded ? expandedPresetOpen : presetOpen} onOpenChange={expanded ? setExpandedPresetOpen : setPresetOpen} onSelect={applyPreset} dense /> : null}
<div className="ml-auto flex shrink-0 items-center justify-end gap-1">
{activeReferenceCount ? <ComposerPill theme={theme} borderColor={insetBorder} icon={<Boxes className="size-2.5" />} label={`已连接 ${activeReferenceCount} 个`} /> : null}
{!expanded && canExpandPrompt ? (
@@ -183,59 +170,86 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
</div>
);
const renderComposerControls = (expanded: boolean) => simpleMode ? (
<div className="flex min-w-0 items-center justify-between gap-2 p-1" style={{ background: controlsSurface }}>
<span className="min-w-0 truncate px-2 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>
{activeReferenceCount ? `已连接 ${activeReferenceCount} 个素材` : "将使用默认模型与参数"}
</span>
<Button
type="text"
className="!inline-flex !h-8 shrink-0 !items-center !gap-1 !rounded-full !px-2.5 !text-[var(--fs-tiny)] !font-medium"
danger={isRunning}
disabled={isSubmitDisabled}
style={{ background: isSubmitDisabled ? theme.toolbar.itemHover : isRunning ? theme.accent.danger : monochromeAccent, color: isSubmitDisabled ? theme.node.faint : isRunning ? "#ffffff" : theme.canvas.background, boxShadow: isSubmitDisabled ? "none" : `0 8px 20px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.18)` }}
onClick={() => (isRunning ? onStop(node.id) : expanded ? submitExpandedPrompt() : submit())}
aria-label={isRunning ? "停止生成" : "生成"}
>
{isRunning ? <Square className="size-2.5 fill-current" /> : <ArrowUp className="size-3" />}
{isRunning ? "停止" : "生成"}
</Button>
</div>
) : (
<div className="flex min-w-0 items-center justify-between gap-0.5 p-1" style={{ background: controlsSurface }}>
<div className={`${expanded ? "max-w-[320px]" : mode === "image" || mode === "video" ? "max-w-[240px]" : "max-w-[174px]"} min-w-[104px] flex-1`}>
<ModelPicker className="!h-7 !w-full !min-w-0 !text-[var(--fs-tiny)] !font-normal [&_img]:!size-3 [&_.lucide]:!size-3" fullWidth config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability={mode} onMissingConfig={() => navigateToSettings({ continueCreation: true })} showSelectedPrice={false} />
</div>
<div className="ml-auto flex min-w-0 shrink-0 items-center gap-1">
{mode === "image" ? (
<CanvasImageSettingsPopover
config={config}
placement={expanded ? "topRight" : "topLeft"}
buttonClassName="!h-7 !w-[146px] !justify-start !rounded-full !border-0 !px-2.5 !text-[var(--fs-tiny)] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3"
onConfigChange={(key, value) => onConfigChange(node.id, key === "count" ? { count: Number(value) || 1 } : { [key]: value })}
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
onOpenChange={expanded ? undefined : onImageSettingsOpenChange}
/>
) : mode === "video" ? (
<CanvasVideoSettingsPopover config={config} buttonClassName="!h-7 !w-[144px] !justify-start !rounded-full !border-0 !px-2.5 !text-[var(--fs-tiny)] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3" onConfigChange={(key, value) => onConfigChange(node.id, videoConfigPatch(key, value))} />
) : mode === "audio" ? (
<CanvasAudioSettingsPopover config={config} buttonClassName="!h-7 !w-[146px] !justify-start !rounded-full !border-0 !px-2.5 !text-[var(--fs-tiny)] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3" onConfigChange={(key, value) => onConfigChange(node.id, audioConfigPatch(key, value))} />
) : null}
{creditsEnabled ? <GenerationCostBadge credits={credits} theme={theme} /> : null}
const renderComposerControls = (expanded: boolean) =>
simpleMode ? (
<div className="flex min-w-0 items-center justify-between gap-2 p-1" style={{ background: controlsSurface }}>
<span className="min-w-0 truncate px-2 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>
{activeReferenceCount ? `已连接 ${activeReferenceCount} 个素材` : "将使用默认模型与参数"}
</span>
<Button
type="text"
className="!inline-flex !h-8 !w-8 shrink-0 !items-center !justify-center !rounded-full !border !p-0 transition hover:!-translate-y-px hover:!brightness-110 motion-reduce:hover:!translate-y-0"
className="!inline-flex !h-8 shrink-0 !items-center !gap-1 !rounded-full !px-2.5 !text-[var(--fs-tiny)] !font-medium"
danger={isRunning}
disabled={isSubmitDisabled}
style={{ background: isSubmitDisabled ? theme.toolbar.itemHover : isRunning ? theme.accent.danger : monochromeAccent, borderColor: isSubmitDisabled ? insetBorder : monochromeAccent, color: isSubmitDisabled ? theme.node.faint : theme.canvas.background, boxShadow: isSubmitDisabled ? "none" : `0 8px 20px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.18)` }}
style={{
background: isSubmitDisabled ? theme.toolbar.itemHover : isRunning ? theme.accent.danger : monochromeAccent,
color: isSubmitDisabled ? theme.node.faint : isRunning ? "#ffffff" : theme.canvas.background,
boxShadow: isSubmitDisabled ? "none" : `0 8px 20px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.18)`,
}}
onClick={() => (isRunning ? onStop(node.id) : expanded ? submitExpandedPrompt() : submit())}
aria-label={isRunning ? "停止生成" : "生成"}
>
{isRunning ? <Square className="size-2.5 fill-current" /> : <ArrowUp className="size-3" />}
{isRunning ? "停止" : "生成"}
</Button>
</div>
</div>
);
) : (
<div className="flex min-w-0 items-center justify-between gap-0.5 p-1" style={{ background: controlsSurface }}>
<div className={`${expanded ? "max-w-[var(--panel-width-compact)]" : mode === "image" || mode === "video" ? "max-w-[240px]" : "max-w-[174px]"} min-w-[104px] flex-1`}>
<ModelPicker
className="!h-7 !w-full !min-w-0 !text-[var(--fs-tiny)] !font-normal [&_img]:!size-3 [&_.lucide]:!size-3"
fullWidth
config={config}
value={config.model}
onChange={(model) => onConfigChange(node.id, { model })}
capability={mode}
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
showSelectedPrice={false}
/>
</div>
<div className="ml-auto flex min-w-0 shrink-0 items-center gap-1">
{mode === "image" ? (
<CanvasImageSettingsPopover
config={config}
placement={expanded ? "topRight" : "topLeft"}
buttonClassName="!h-7 !w-[146px] !justify-start !rounded-full !border-0 !px-2.5 !text-[var(--fs-tiny)] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3"
onConfigChange={(key, value) => onConfigChange(node.id, key === "count" ? { count: Number(value) || 1 } : { [key]: value })}
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
onOpenChange={expanded ? undefined : onImageSettingsOpenChange}
/>
) : mode === "video" ? (
<CanvasVideoSettingsPopover
config={config}
buttonClassName="!h-7 !w-[144px] !justify-start !rounded-full !border-0 !px-2.5 !text-[var(--fs-tiny)] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3"
onConfigChange={(key, value) => onConfigChange(node.id, videoConfigPatch(key, value))}
/>
) : mode === "audio" ? (
<CanvasAudioSettingsPopover
config={config}
buttonClassName="!h-7 !w-[146px] !justify-start !rounded-full !border-0 !px-2.5 !text-[var(--fs-tiny)] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3"
onConfigChange={(key, value) => onConfigChange(node.id, audioConfigPatch(key, value))}
/>
) : null}
{creditsEnabled ? <GenerationCostBadge credits={credits} theme={theme} /> : null}
<Button
type="text"
className="!inline-flex !h-8 !w-8 shrink-0 !items-center !justify-center !rounded-full !border !p-0 transition hover:!-translate-y-px hover:!brightness-110 motion-reduce:hover:!translate-y-0"
danger={isRunning}
disabled={isSubmitDisabled}
style={{
background: isSubmitDisabled ? theme.toolbar.itemHover : isRunning ? theme.accent.danger : monochromeAccent,
borderColor: isSubmitDisabled ? insetBorder : monochromeAccent,
color: isSubmitDisabled ? theme.node.faint : theme.canvas.background,
boxShadow: isSubmitDisabled ? "none" : `0 8px 20px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.18)`,
}}
onClick={() => (isRunning ? onStop(node.id) : expanded ? submitExpandedPrompt() : submit())}
aria-label={isRunning ? "停止生成" : "生成"}
>
{isRunning ? <Square className="size-2.5 fill-current" /> : <ArrowUp className="size-3" />}
</Button>
</div>
</div>
);
return (
<div
@@ -297,15 +311,18 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
centered
width={760}
destroyOnHidden
onCancel={() => { setExpandedPresetOpen(false); setExpandedPromptOpen(false); }}
styles={{ container: { display: "flex", height: "min(440px, calc(100vh - 40px))", flexDirection: "column", borderRadius: 12, border: `1px solid ${shellBorder}`, padding: 0, overflow: "hidden", background: shellSurface, boxShadow: modalShadow }, body: { minHeight: 0, flex: 1, padding: 0 } }}
onCancel={() => {
setExpandedPresetOpen(false);
setExpandedPromptOpen(false);
}}
styles={{
container: { display: "flex", height: "min(440px, calc(100vh - 40px))", flexDirection: "column", borderRadius: 12, border: `1px solid ${shellBorder}`, padding: 0, overflow: "hidden", background: shellSurface, boxShadow: modalShadow },
body: { minHeight: 0, flex: 1, padding: 0 },
}}
>
<div className="flex h-full min-h-0 flex-col gap-2.5 p-3" style={{ color: theme.node.text }}>
<div className="shrink-0 pr-8">{renderComposerHeader(true)}</div>
<div
className="flex min-h-[240px] flex-1 flex-col overflow-hidden rounded-xl outline-none ring-0 focus-within:outline-none focus-within:ring-0"
style={{ background: composerSurface, boxShadow: composerShadow }}
>
<div className="flex min-h-[240px] flex-1 flex-col overflow-hidden rounded-xl outline-none ring-0 focus-within:outline-none focus-within:ring-0" style={{ background: composerSurface, boxShadow: composerShadow }}>
<ConnectedReferenceShelf references={mentionReferences} theme={theme} onInsert={insertPromptReference} />
<CanvasResourceMentionTextarea
value={prompt}
@@ -332,10 +349,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
function ComposerPill({ theme, borderColor, icon, label }: { theme: CanvasTheme; borderColor: string; icon: ReactNode; label: string }) {
return (
<span
className="inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-1.5 text-[var(--fs-micro)] font-medium transition hover:brightness-125"
style={{ background: theme.canvas.background, borderColor, color: theme.node.activeStroke }}
>
<span className="inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-1.5 text-[var(--fs-micro)] font-medium transition hover:brightness-125" style={{ background: theme.canvas.background, borderColor, color: theme.node.activeStroke }}>
{icon}
{label}
</span>
@@ -376,7 +390,9 @@ function ConnectedReferenceShelf({ references, theme, onInsert }: { references:
<ReferenceThumbnail reference={reference} />
</span>
<span className="absolute left-0.5 top-0.5 grid size-3.5 place-items-center rounded-full bg-black/65 text-[var(--fs-micro)] font-semibold text-white backdrop-blur-sm">{index + 1}</span>
<span className="absolute bottom-0.5 right-0.5 grid size-3.5 place-items-center rounded-full bg-black/65 text-white backdrop-blur-sm"><AtSign className="size-2" /></span>
<span className="absolute bottom-0.5 right-0.5 grid size-3.5 place-items-center rounded-full bg-black/65 text-white backdrop-blur-sm">
<AtSign className="size-2" />
</span>
</button>
))}
</div>
@@ -399,7 +415,12 @@ function ReferenceThumbnail({ reference }: { reference: CanvasResourceReference
function GenerationCostBadge({ credits, theme }: { credits: number | null; theme: CanvasTheme }) {
if (credits === null) return null;
return (
<span className="canvas-generation-cost-badge inline-flex h-7 shrink-0 items-center gap-1 rounded-full px-2 text-[var(--fs-micro)] font-semibold tabular-nums" style={{ background: theme.accent.primarySoft, color: theme.accent.primary }} title={`预计消耗 ${credits.toLocaleString()} 积分`} aria-label={`预计消耗 ${credits.toLocaleString()} 积分`}>
<span
className="canvas-generation-cost-badge inline-flex h-7 shrink-0 items-center gap-1 rounded-full px-2 text-[var(--fs-micro)] font-semibold tabular-nums"
style={{ background: theme.accent.primarySoft, color: theme.accent.primary }}
title={`预计消耗 ${credits.toLocaleString()} 积分`}
aria-label={`预计消耗 ${credits.toLocaleString()} 积分`}
>
<CreditSymbol className="text-[var(--fs-label)]" />
{credits.toLocaleString()}
</span>