fix(*): 质量检查 - 修复预演保存、增量同步与缺失资源恢复
This commit is contained in:
@@ -8,13 +8,7 @@ import { resolveCompatibleModel, type ModelRequirements } from "@/lib/model-sele
|
||||
import type { AgentMediaSettings } from "@/services/api/agent";
|
||||
import { PUBLIC_MODEL_CATALOG_ID, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
|
||||
export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabled, onChange }: {
|
||||
initial: AgentVideoApproval;
|
||||
value?: AgentMediaSettings;
|
||||
theme: CanvasTheme;
|
||||
disabled: boolean;
|
||||
onChange: (value: AgentMediaSettings) => void;
|
||||
}) {
|
||||
export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabled, onChange }: { initial: AgentVideoApproval; value?: AgentMediaSettings; theme: CanvasTheme; disabled: boolean; onChange: (value: AgentMediaSettings) => void }) {
|
||||
const effective = useEffectiveConfig();
|
||||
const config = useMemo(() => ({ ...effective, channels: effective.channels.filter((channel) => channel.scope === "system" || channel.id === PUBLIC_MODEL_CATALOG_ID) }), [effective]);
|
||||
const settings = value || initial;
|
||||
@@ -42,7 +36,15 @@ export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabl
|
||||
const profile = modelCapabilityConfigFor(config, next).video;
|
||||
if (!profile) return;
|
||||
const normalized = normalizeVideoValue(profile, { seconds: duration, ratio: settings.size, resolution: settings.quality });
|
||||
onChange({ ...agentApprovalModelSelection(config, next), size: normalized.ratio, quality: normalized.resolution, durationSeconds: Number(normalized.seconds), videoGenerateAudio: settings.videoGenerateAudio, videoWatermark: settings.videoWatermark, videoArkPrivateAssetUpload: settings.videoArkPrivateAssetUpload });
|
||||
onChange({
|
||||
...agentApprovalModelSelection(config, next),
|
||||
size: normalized.ratio,
|
||||
quality: normalized.resolution,
|
||||
durationSeconds: Number(normalized.seconds),
|
||||
videoGenerateAudio: settings.videoGenerateAudio,
|
||||
videoWatermark: settings.videoWatermark,
|
||||
videoArkPrivateAssetUpload: settings.videoArkPrivateAssetUpload,
|
||||
});
|
||||
};
|
||||
|
||||
const changeOption = (key: "vquality" | "size" | "videoSeconds" | "videoGenerateAudio" | "videoWatermark" | "videoArkPrivateAssetUpload", next: string) => {
|
||||
@@ -75,12 +77,32 @@ export function CanvasAgentVideoApprovalSettings({ initial, value, theme, disabl
|
||||
return (
|
||||
<fieldset disabled={disabled} className="min-w-0 space-y-3 border-0 p-0" data-canvas-no-zoom data-canvas-wheel-scroll aria-label="视频生成设置">
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs" style={{ color: theme.node.muted }}>生成模型</div>
|
||||
<div className="text-xs" style={{ color: theme.node.muted }}>
|
||||
生成模型
|
||||
</div>
|
||||
<ModelPicker config={config} capability="video" value={model} onChange={changeModel} requirements={requirements} variant="creation" fullWidth showOptionPrices placeholder="选择生成模型" popoverClassName="agent-model-picker-popover" />
|
||||
</div>
|
||||
{model ? <VideoSettingsPanel config={{ ...config, model, size: settings.size, videoSeconds: duration, vquality: settings.quality, videoGenerateAudio: audio, videoWatermark: watermark, videoArkPrivateAssetUpload: privateAssetUpload }} onConfigChange={changeOption} theme={theme} showTitle={false} className="min-w-0 space-y-3" /> : <p className="text-xs" style={{ color: theme.node.muted }}>当前模型不在可选目录中,可重新选择;提交时将重新校验模型与规格。</p>}
|
||||
<p className="text-xs" style={{ color: theme.node.text }}>本次规格:{settings.size}{settings.quality ? ` · ${settings.quality}` : ""}{settings.durationSeconds ? ` · ${settings.durationSeconds} 秒` : ""}</p>
|
||||
<p className="text-xs" style={{ color: theme.node.muted }}>修改仅用于本次生成,费用按最终模型和规格计算。</p>
|
||||
{model ? (
|
||||
<VideoSettingsPanel
|
||||
config={{ ...config, model, size: settings.size, videoSeconds: duration, vquality: settings.quality, videoGenerateAudio: audio, videoWatermark: watermark, videoArkPrivateAssetUpload: privateAssetUpload }}
|
||||
onConfigChange={changeOption}
|
||||
theme={theme}
|
||||
showTitle={false}
|
||||
className="min-w-0 space-y-3"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-xs" style={{ color: theme.node.muted }}>
|
||||
当前模型不在可选目录中,可重新选择;提交时将重新校验模型与规格。
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs" style={{ color: theme.node.text }}>
|
||||
本次规格:{settings.size}
|
||||
{settings.quality ? ` · ${settings.quality}` : ""}
|
||||
{settings.durationSeconds ? ` · ${settings.durationSeconds} 秒` : ""}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: theme.node.muted }}>
|
||||
修改仅用于本次生成,费用按最终模型和规格计算。
|
||||
</p>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -571,7 +571,11 @@ export function ApprovalCard({
|
||||
无法确认具体目标,继续前请重新读取画布。
|
||||
</div>
|
||||
)}
|
||||
{imageApproval ? <CanvasAgentImageApprovalSettings initial={imageApproval} value={mediaSettings} onChange={setMediaSettings} theme={theme} disabled={submitting} /> : videoApproval ? <CanvasAgentVideoApprovalSettings initial={videoApproval} value={mediaSettings} onChange={setMediaSettings} theme={theme} disabled={submitting} /> : null}
|
||||
{imageApproval ? (
|
||||
<CanvasAgentImageApprovalSettings initial={imageApproval} value={mediaSettings} onChange={setMediaSettings} theme={theme} disabled={submitting} />
|
||||
) : videoApproval ? (
|
||||
<CanvasAgentVideoApprovalSettings initial={videoApproval} value={mediaSettings} onChange={setMediaSettings} theme={theme} disabled={submitting} />
|
||||
) : null}
|
||||
<button type="button" className="canvas-agent-approval-reason-toggle" aria-expanded={showReason} onClick={() => setShowReason((value) => !value)} disabled={submitting}>
|
||||
{showReason ? "收起拒绝理由" : "填写拒绝理由(可选)"}
|
||||
</button>
|
||||
|
||||
@@ -79,7 +79,7 @@ export function CanvasPrevisWorkbench({
|
||||
onDeleteImageNode: (nodeId: string) => void;
|
||||
onFlush?: () => void | Promise<void>;
|
||||
}) {
|
||||
const { message } = App.useApp();
|
||||
const { message, modal } = App.useApp();
|
||||
const theme = canvasThemes[useActiveTheme()];
|
||||
const effectiveCanvasId = useMemo(() => {
|
||||
const explicit = canvasId?.trim();
|
||||
@@ -241,6 +241,7 @@ export function CanvasPrevisWorkbench({
|
||||
resetWorkbench();
|
||||
}, [open, resetWorkbench, scene, writeDraft]);
|
||||
|
||||
// 打开会话时检查合法本地恢复候选
|
||||
// 父级项目收到 Agent 的预演台语义补丁后,scene.id 不变但 updatedAt 会变化;
|
||||
// 不能只按 scene.id 忽略,否则打开着的工作台永远看不到 Agent 的修改。
|
||||
useEffect(() => {
|
||||
@@ -257,7 +258,7 @@ export function CanvasPrevisWorkbench({
|
||||
setSelectedBone(null);
|
||||
setPlayhead(0);
|
||||
message.info("Agent 已更新预演场景");
|
||||
}, [message, open, scene, setPlayhead, setSelectedBone, setSelectedLightId, setSelectedObjectId, writeDraft]);
|
||||
}, [message, modal, open, scene, writeDraft]);
|
||||
|
||||
const activeShot = draft?.shots?.find((item) => item.id === draft.activeShotId) || draft?.shots?.[0] || null;
|
||||
const activeCamera = draft?.cameras?.find((item) => item.id === activeShot?.cameraId) || draft?.cameras?.[0] || null;
|
||||
@@ -355,6 +356,7 @@ export function CanvasPrevisWorkbench({
|
||||
[writeAndPublish],
|
||||
);
|
||||
|
||||
// 打开会话时检查合法本地恢复候选之后,挂载暂存手势生命周期。
|
||||
// 暂存手势的终止生命周期:常驻安装,非活跃时 end 为空操作。
|
||||
useEffect(
|
||||
() =>
|
||||
@@ -466,7 +468,14 @@ export function CanvasPrevisWorkbench({
|
||||
return;
|
||||
}
|
||||
recordPrevisDiagnostic("PREVIS_CLOSE_BLOCKED", { sceneId: draftRef.current?.id, saveOutcome: "stay", revision: saveController.progress.revision });
|
||||
message.error(decision.message);
|
||||
modal.confirm({
|
||||
title: "保存未完成",
|
||||
content: decision.message,
|
||||
okText: "放弃修改并关闭",
|
||||
cancelText: "留在预演台",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: onClose,
|
||||
});
|
||||
closingRef.current = false;
|
||||
})();
|
||||
};
|
||||
@@ -1246,6 +1255,12 @@ export function CanvasPrevisWorkbench({
|
||||
<span className={`previs-desk-save ${saveStatusClass}`} aria-live="polite">
|
||||
{saveIndicator.label}
|
||||
</span>
|
||||
{saveController.progress.status === "error" ? (
|
||||
<button type="button" className="previs-desk-header-action" onClick={() => void saveController.retrySave()}>
|
||||
<Save className="size-3.5" />
|
||||
重试保存
|
||||
</button>
|
||||
) : null}
|
||||
{onboardingScope ? (
|
||||
<button type="button" className="previs-desk-icon-button" aria-label="重新开始引导" title="重新开始引导" onClick={() => setOnboardingRestartSignal((value) => value + 1)}>
|
||||
<Lightbulb className="size-4" />
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { PrevisScene } from "@/types/previs";
|
||||
export type PrevisSaveController = {
|
||||
progress: PrevisSaveProgress;
|
||||
commitScene: (scene: PrevisScene) => void;
|
||||
retrySave: () => Promise<boolean>;
|
||||
prepareClose: () => Promise<PrevisCloseDecision>;
|
||||
handlePageHide: () => Promise<boolean>;
|
||||
};
|
||||
@@ -59,7 +60,10 @@ export function usePrevisSaveCoordinator(input: { sceneId: string | null; initia
|
||||
coordinatorRef.current = null;
|
||||
const pending = coordinator.getSnapshot();
|
||||
if (pending.confirmedRevision < pending.revision) {
|
||||
void coordinator.flushLatest().catch(() => undefined).finally(() => coordinator.dispose());
|
||||
void coordinator
|
||||
.flushLatest()
|
||||
.catch(() => undefined)
|
||||
.finally(() => coordinator.dispose());
|
||||
return;
|
||||
}
|
||||
coordinator.dispose();
|
||||
@@ -79,6 +83,14 @@ export function usePrevisSaveCoordinator(input: { sceneId: string | null; initia
|
||||
[syncProgress],
|
||||
);
|
||||
|
||||
const retrySave = useCallback(async () => {
|
||||
const coordinator = coordinatorRef.current;
|
||||
if (!coordinator) return false;
|
||||
const success = await coordinator.flushLatest();
|
||||
syncProgress();
|
||||
return success;
|
||||
}, [syncProgress]);
|
||||
|
||||
const prepareClose = useCallback(async () => {
|
||||
const coordinator = coordinatorRef.current;
|
||||
if (!coordinator) return "close" as PrevisCloseDecision;
|
||||
@@ -107,5 +119,5 @@ export function usePrevisSaveCoordinator(input: { sceneId: string | null; initia
|
||||
}
|
||||
}, [syncProgress]);
|
||||
|
||||
return { progress, commitScene, prepareClose, handlePageHide };
|
||||
return { progress, commitScene, retrySave, prepareClose, handlePageHide };
|
||||
}
|
||||
|
||||
@@ -45,8 +45,6 @@ export function commitCanvasGenerationResult(current: CanvasNodeData[], before:
|
||||
const existingIds = new Set(current.map((node) => node.id));
|
||||
return [
|
||||
...committedWithAdditional,
|
||||
...additionalNodes
|
||||
.filter((node) => !existingIds.has(node.id))
|
||||
.map((node) => ({ ...node, position: { x: node.position.x + merged.position.x - result.position.x, y: node.position.y + merged.position.y - result.position.y } })),
|
||||
...additionalNodes.filter((node) => !existingIds.has(node.id)).map((node) => ({ ...node, position: { x: node.position.x + merged.position.x - result.position.x, y: node.position.y + merged.position.y - result.position.y } })),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -383,16 +383,16 @@ async function buildGenerationTaskNodeResults(node: CanvasNodeData, task: Genera
|
||||
const id = `${node.id}:task:${task.id}:image:${index}`;
|
||||
const existing = nodes.find((item) => item.id === id);
|
||||
const child = await buildGenerationTaskNodeResult(
|
||||
existing
|
||||
? { ...node, ...existing, id, title: existing.title, position: existing.position, width: existing.width, height: existing.height }
|
||||
: { ...node, id, title: `${node.title} · ${index + 1}`, parentId: undefined },
|
||||
existing ? { ...node, ...existing, id, title: existing.title, position: existing.position, width: existing.width, height: existing.height } : { ...node, id, title: `${node.title} · ${index + 1}`, parentId: undefined },
|
||||
task,
|
||||
nodes,
|
||||
index,
|
||||
);
|
||||
children.push({
|
||||
...child,
|
||||
...(existing ? { title: existing.title, position: existing.position, width: existing.metadata?.manualSize ? existing.width : child.width, height: existing.metadata?.manualSize ? existing.height : child.height } : { position: imageGenerationChildPosition(resultNode.position, resultNode.width, child, index) }),
|
||||
...(existing
|
||||
? { title: existing.title, position: existing.position, width: existing.metadata?.manualSize ? existing.width : child.width, height: existing.metadata?.manualSize ? existing.height : child.height }
|
||||
: { position: imageGenerationChildPosition(resultNode.position, resultNode.width, child, index) }),
|
||||
metadata: {
|
||||
...child.metadata,
|
||||
assetId: task.outputs?.find((output) => output.outputIndex === index)?.materializedAssetId,
|
||||
|
||||
@@ -15,6 +15,7 @@ export { isDirectSystemModel } from "@/lib/model-selection";
|
||||
export function modelChannelLabel(config: AiConfig, value: string) {
|
||||
const channel = resolveModelChannel(config, value);
|
||||
const cost = channel.modelCosts?.find((item) => item.model === modelOptionName(value));
|
||||
if (channel.scope === "system") return cost?.channelLabel?.trim() || "系统模型";
|
||||
return cost?.channelLabel?.trim() || channel.name || "未命名渠道";
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ export function groupModelsForPicker(config: AiConfig, options: string[]): Model
|
||||
const key = JSON.stringify(["channel", channel.id]);
|
||||
groups.set(key, {
|
||||
key,
|
||||
label: channel.name || "未命名渠道",
|
||||
label: channel.scope === "system" ? "系统模型" : channel.name || "未命名渠道",
|
||||
icon: modelIcon(config, otherModels[0]),
|
||||
scope: channel.id === PUBLIC_MODEL_CATALOG_ID ? "" : "我的模型",
|
||||
kind: "channel",
|
||||
|
||||
@@ -68,8 +68,8 @@ export function ModelDefaultGrid({ config, onChange }: { config: AiConfig; onCha
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-xs font-semibold">{modelDisplayName(config, model)}</span>
|
||||
<span className="mt-1 flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1 text-[var(--fs-tiny)] text-foreground/45">
|
||||
<span className="max-w-full truncate">{channel.name || "未命名渠道"}</span>
|
||||
<span className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1 text-[var(--fs-tiny)] text-foreground/45">
|
||||
<span className="max-w-full truncate">{channel.scope === "system" ? "系统模型" : channel.name || "未命名渠道"}</span>
|
||||
<span className="model-default-option-scope">{channel.scope === "system" ? "系统" : "自定义"}</span>
|
||||
{creditsEnabled && cost ? <span className="model-default-price">{formatPrice(cost.billingMode === "token" ? (cost.outputTokenPriceMicrocredits || 0) : cost.unitPriceMicrocredits)} /{cost.billingMode === "token" ? "百万 Token" : cost.billingMode === "per_second" ? "秒" : "次"}</span> : null}
|
||||
</span>
|
||||
|
||||
@@ -20,8 +20,11 @@ export function createAgentCanvasSync(options: Options) {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const schedule = () => {
|
||||
if (disposed || running || timer || (!patches.length && !needsRefresh)) return;
|
||||
const wait = patches.length ? options.batchMs ?? 40 : Math.max(options.batchMs ?? 40, (options.refreshIntervalMs ?? 1000) - (Date.now() - lastRefresh));
|
||||
timer = setTimeout(() => { timer = undefined; void flush(); }, wait);
|
||||
const wait = patches.length ? (options.batchMs ?? 40) : Math.max(options.batchMs ?? 40, (options.refreshIntervalMs ?? 1000) - (Date.now() - lastRefresh));
|
||||
timer = setTimeout(() => {
|
||||
timer = undefined;
|
||||
void flush();
|
||||
}, wait);
|
||||
};
|
||||
const flush = async () => {
|
||||
if (disposed || running) return;
|
||||
@@ -30,8 +33,11 @@ export function createAgentCanvasSync(options: Options) {
|
||||
patches = [];
|
||||
try {
|
||||
if (batch.length) {
|
||||
try { await options.applyPatches(batch); }
|
||||
catch { needsRefresh = true; }
|
||||
try {
|
||||
await options.applyPatches(batch);
|
||||
} catch {
|
||||
needsRefresh = true;
|
||||
}
|
||||
}
|
||||
if (!disposed && needsRefresh && Date.now() - lastRefresh >= (options.refreshIntervalMs ?? 1000)) {
|
||||
needsRefresh = false;
|
||||
@@ -51,7 +57,15 @@ export function createAgentCanvasSync(options: Options) {
|
||||
const patch = event.payload.canvasPatch as AgentCanvasPatch | undefined;
|
||||
if (patch && patch.canvasId === options.canvasId) {
|
||||
patches.push(patch);
|
||||
} else if (event.type === "canvas_updated" || event.type === "canvas_undone" || event.type === "generation_task_created" || event.type === "tool_failed" || event.type === "run_failed" || (event.type === "tool_completed" && ["canvas_apply_ops", "generate_media"].includes(String(event.payload.toolName)))) {
|
||||
} else if (
|
||||
!patches.length &&
|
||||
(event.type === "canvas_updated" ||
|
||||
event.type === "canvas_undone" ||
|
||||
event.type === "generation_task_created" ||
|
||||
event.type === "tool_failed" ||
|
||||
event.type === "run_failed" ||
|
||||
(event.type === "tool_completed" && ["canvas_apply_ops", "generate_media"].includes(String(event.payload.toolName))))
|
||||
) {
|
||||
// A patch may be dropped even after earlier patches succeeded.
|
||||
// Keep the snapshot fallback available for every meaningful task
|
||||
// boundary; refreshes remain rate-limited below.
|
||||
@@ -59,7 +73,16 @@ export function createAgentCanvasSync(options: Options) {
|
||||
}
|
||||
schedule();
|
||||
},
|
||||
reconcile() { if (!disposed) { needsRefresh = true; schedule(); } },
|
||||
dispose() { disposed = true; if (timer) clearTimeout(timer); patches = []; },
|
||||
reconcile() {
|
||||
if (!disposed) {
|
||||
needsRefresh = true;
|
||||
schedule();
|
||||
}
|
||||
},
|
||||
dispose() {
|
||||
disposed = true;
|
||||
if (timer) clearTimeout(timer);
|
||||
patches = [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -456,7 +456,10 @@ async function flushResourceAccessBatch() {
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await http.post<{ items: ResourceAccessBatchItem[] }>("/resources/access", entries.map((entry) => entry.payload));
|
||||
const data = await http.post<{ items: ResourceAccessBatchItem[] }>(
|
||||
"/resources/access",
|
||||
entries.map((entry) => entry.payload),
|
||||
);
|
||||
await Promise.all(
|
||||
entries.map(async (entry, index) => {
|
||||
try {
|
||||
|
||||
70
web/src/services/canvas-missing-resources.ts
Normal file
70
web/src/services/canvas-missing-resources.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
||||
|
||||
const RESOURCE_KEY = /^resource:([^/]+)$/;
|
||||
const RESOURCE_URL = /\/api\/resources\/([^/]+)\/file(?:[/?#]|$)/;
|
||||
const LOCATOR_KEY = /(?:storage|resource|preview|poster|thumbnail|content|url|locator|asset)/i;
|
||||
|
||||
function resourceId(value: string) {
|
||||
const storageMatch = value.match(RESOURCE_KEY);
|
||||
if (storageMatch) return storageMatch[1];
|
||||
return value.match(RESOURCE_URL)?.[1] || "";
|
||||
}
|
||||
|
||||
function isMediaLocatorKey(key: string) {
|
||||
return LOCATOR_KEY.test(key);
|
||||
}
|
||||
|
||||
function collect(value: unknown, ids: Set<string>, key = "") {
|
||||
if (typeof value === "string") {
|
||||
if (isMediaLocatorKey(key)) {
|
||||
const id = resourceId(value);
|
||||
if (id) ids.add(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => collect(item, ids, key));
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== "object") return;
|
||||
Object.entries(value).forEach(([childKey, childValue]) => collect(childValue, ids, childKey));
|
||||
}
|
||||
|
||||
export function canvasResourceIds(value: unknown) {
|
||||
const ids = new Set<string>();
|
||||
collect(value, ids);
|
||||
return ids;
|
||||
}
|
||||
|
||||
function clear(value: JsonValue, missing: Set<string>, serverReported: boolean, key = ""): JsonValue | undefined {
|
||||
if (Array.isArray(value)) return value.map((item) => clear(item, missing, serverReported, key)).filter((item): item is JsonValue => item !== undefined);
|
||||
if (!value || typeof value !== "object") {
|
||||
if (typeof value === "string" && isMediaLocatorKey(key) && missing.has(resourceId(value))) return undefined;
|
||||
return value;
|
||||
}
|
||||
|
||||
const entries = Object.entries(value);
|
||||
const invalidMedia =
|
||||
!serverReported &&
|
||||
entries.some(([childKey, childValue]) => {
|
||||
if (typeof childValue !== "string" || !isMediaLocatorKey(childKey)) return false;
|
||||
return missing.has(resourceId(childValue));
|
||||
});
|
||||
const output: Record<string, JsonValue> = {};
|
||||
for (const [childKey, childValue] of entries) {
|
||||
if (childKey === "assetId" && invalidMedia && !serverReported) continue;
|
||||
if (serverReported && childKey !== "assetId" && isMediaLocatorKey(childKey)) continue;
|
||||
if (typeof childValue === "string" && isMediaLocatorKey(childKey) && missing.has(resourceId(childValue))) continue;
|
||||
if (childKey === "status" && childValue === "success" && invalidMedia) {
|
||||
output[childKey] = "idle";
|
||||
continue;
|
||||
}
|
||||
const next = clear(childValue, missing, serverReported, childKey);
|
||||
if (next !== undefined) output[childKey] = next;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function clearMissingCanvasResources<T>(value: T, missingIds: Set<string>, _scope = "", serverReported = false): T {
|
||||
return clear(value as unknown as JsonValue, missingIds, serverReported) as T;
|
||||
}
|
||||
@@ -362,7 +362,7 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
createModelChannel({
|
||||
...channel,
|
||||
id: channel.id || `system-${index + 1}`,
|
||||
name: channel.name || `系统渠道 ${index + 1}`,
|
||||
name: "系统模型",
|
||||
scope: "system",
|
||||
apiKey: channel.apiKey || "system",
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user