production runtime and mock removal

This commit is contained in:
inman committed 2026-08-17 23:44:14 +08:00
1 parent 2ef3da7af5
commit 6480e503eb
55 files changed
+581 -540

No files matched your search

+22 -9
View File
@@ -52,6 +52,7 @@ export function AssetManager({ initialView = "assets", initialTaskId }: AssetMan
}
return map;
}, [jobs]);
const outputAssetIds = useMemo(() => new Set(assets.map((asset) => asset.id)), [assets]);
const visibleAssets = pageItems(assets, assetPage, ASSET_PAGE_SIZE);
const visibleJobs = pageItems(jobs, jobPage, TASK_PAGE_SIZE);
@@ -262,10 +263,10 @@ export function AssetManager({ initialView = "assets", initialTaskId }: AssetMan
<div className="job-summary">
<h3>{job.prompt?.slice(0, 90) || capabilityLabel(job.capability)}</h3>
<p className="muted">{capabilityLabel(job.capability)} / {job.reqKey}</p>
<p className="muted compact-hint">{durationLabel(job, durationNow)} / 输入 {job.inputAssetIds.length || job.inputUrls.length} 个,输出 {job.outputAssetIds.length} 个 / {billingLabel(job)}</p>
<p className="muted compact-hint">{durationLabel(job, durationNow)} / 输入 {job.inputAssetIds.length || job.inputUrls.length} 个,输出 {resolvedOutputCount(job, outputAssetIds)} 个 / {billingLabel(job)}</p>
</div>
<div className="toolbar job-actions">
<span className={`status ${job.status}`}>{statusLabel(job.status)}</span>
<span className={`status ${taskStatusClass(job, outputAssetIds)}`}>{statusLabel(job.status, hasResolvedOutput(job, outputAssetIds))}</span>
<button className="button mini-button" type="button" aria-expanded={expandedJobId === job.id} onClick={() => setExpandedJobId(expandedJobId === job.id ? null : job.id)}>
<Info size={15} />
{expandedJobId === job.id ? "收起" : "详情"}
@@ -278,7 +279,7 @@ export function AssetManager({ initialView = "assets", initialTaskId }: AssetMan
</button>
</div>
</div>
{expandedJobId === job.id ? <JobDetails job={job} now={durationNow} /> : null}
{expandedJobId === job.id ? <JobDetails job={job} now={durationNow} outputAssetIds={outputAssetIds} /> : null}
</article>
))}
{!jobs.length ? <div className="callout">暂无生成任务。提交图片或视频生成后会保留任务历史。</div> : null}
@@ -314,13 +315,13 @@ export function AssetManager({ initialView = "assets", initialTaskId }: AssetMan
);
}
function JobDetails({ job, now }: { job: GenerationJob; now: number }) {
function JobDetails({ job, now, outputAssetIds }: { job: GenerationJob; now: number; outputAssetIds: Set<string> }) {
return (
<div className="job-detail-panel">
<dl className="job-detail-grid">
<div>
<dt>状态</dt>
<dd><span className={`status ${job.status}`}>{statusLabel(job.status)}</span></dd>
<dd><span className={`status ${taskStatusClass(job, outputAssetIds)}`}>{statusLabel(job.status, hasResolvedOutput(job, outputAssetIds))}</span></dd>
</div>
<div>
<dt>耗时</dt>
@@ -352,7 +353,7 @@ function JobDetails({ job, now }: { job: GenerationJob; now: number }) {
</div>
<div>
<dt>输入/输出</dt>
<dd>{job.inputAssetIds.length || job.inputUrls.length} / {job.outputAssetIds.length}</dd>
<dd>{job.inputAssetIds.length || job.inputUrls.length} / {resolvedOutputCount(job, outputAssetIds)}</dd>
</div>
<div>
<dt>扣费</dt>
@@ -456,10 +457,22 @@ function sourceLabel(source: Asset["source"]) {
return "外部";
}
function statusLabel(status: GenerationJob["status"]) {
function hasResolvedOutput(job: GenerationJob, outputAssetIds: Set<string>) {
return job.outputAssetIds.some((assetId) => outputAssetIds.has(assetId));
}
function resolvedOutputCount(job: GenerationJob, outputAssetIds: Set<string>) {
return job.outputAssetIds.filter((assetId) => outputAssetIds.has(assetId)).length;
}
function taskStatusClass(job: GenerationJob, outputAssetIds: Set<string>) {
return job.status === "succeeded" && !hasResolvedOutput(job, outputAssetIds) ? "syncing" : job.status;
}
function statusLabel(status: GenerationJob["status"], hasOutput = true) {
if (status === "queued") return "排队中";
if (status === "running") return "生成中";
if (status === "succeeded") return "已完成";
if (status === "succeeded") return hasOutput ? "已完成" : "结果同步中";
if (status === "failed") return "失败";
if (status === "expired") return "已过期";
if (status === "cancelled") return "已取消";
@@ -485,7 +498,7 @@ function capabilityLabel(capability?: GenerationJob["capability"]) {
}
function billingLabel(job: GenerationJob) {
if (!job.billing) return job.provider === "mock" ? "免计费" : "未计费";
if (!job.billing) return job.provider === "mock" ? "历史任务(未计费)" : "未计费";
if (job.billing.quotaExempt) return `超管费用 ${formatBillingAmount(job.billing.amountFen)}(不计额度)`;
if (job.billing.status === "refunded") return `已退款 ${formatBillingAmount(job.billing.amountFen)}`;
if (job.billing.status === "pending") return `预计 ${formatBillingAmount(job.billing.amountFen)}`;
+31 -13
View File
@@ -150,6 +150,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
const [busy, setBusy] = useState(false);
const [billingQuote, setBillingQuote] = useState<BillingQuote | null>(null);
const [billingQuoteLoading, setBillingQuoteLoading] = useState(false);
const [billingQuoteError, setBillingQuoteError] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
@@ -307,10 +308,12 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
const timer = window.setTimeout(async () => {
if (!prompt.trim() || missingMaterialPlaceholders.length) {
if (active) setBillingQuote(null);
if (active) setBillingQuoteError(null);
if (active) setBillingQuoteLoading(false);
return;
}
setBillingQuoteLoading(true);
setBillingQuoteError(null);
try {
const response = await fetch("/api/billing/quote", {
method: "POST",
@@ -319,11 +322,18 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
});
const payload = await response.json().catch(() => ({}));
if (active) {
setBillingQuote(response.ok && payload.quote ? payload.quote as BillingQuote : null);
if (response.ok && payload.quote) {
setBillingQuote(payload.quote as BillingQuote);
setBillingQuoteError(null);
} else {
setBillingQuote(null);
setBillingQuoteError(typeof payload.error === "string" ? payload.error : "计费估算暂不可用,请检查服务配置");
}
}
} catch {
if (active) {
setBillingQuote(null);
setBillingQuoteError("计费估算暂不可用,请检查后端服务配置");
}
} finally {
if (active) setBillingQuoteLoading(false);
@@ -1167,7 +1177,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
</>
)}
</div>
<BillingEstimate quote={billingQuote} loading={billingQuoteLoading} />
<BillingEstimate quote={billingQuote} loading={billingQuoteLoading} error={billingQuoteError} />
</div>
</div>
{templateEditorOpen ? (
@@ -1425,7 +1435,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
<div className="create-task-body">
<h3 title={taskName(job)}>{taskName(job)}</h3>
<div className="create-task-meta">
<span className={`status ${job.status}`}>{statusLabel(job.status)}</span>
<span className={`status ${taskStatusClass(job, taskAssetById)}`}>{statusLabel(job.status, Boolean(firstOutputAsset(job, taskAssetById)))}</span>
<span>{durationLabel(job, durationNow)}</span>
<span className="billing-task-cost">{billingLabel(job)}</span>
</div>
@@ -1477,10 +1487,11 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
function renderTaskThumbnail(job: GenerationJob, assetById: Map<string, Asset>) {
const asset = job.outputAssetIds.map((assetId) => assetById.get(assetId)).find((item): item is Asset => Boolean(item));
if (!asset) {
const awaitingOutput = job.status === "succeeded";
return (
<div className={clsx("create-task-thumb create-task-thumb-placeholder", isPendingTask(job) && "generating")} aria-hidden="true">
<div className={clsx("create-task-thumb create-task-thumb-placeholder", (isPendingTask(job) || awaitingOutput) && "generating")} aria-hidden="true">
{isPendingTask(job) ? <Loader2 className="spin" size={18} /> : <ImageIcon size={18} />}
<span>{isPendingTask(job) ? "生成中" : "无结果"}</span>
<span>{isPendingTask(job) ? "生成中" : awaitingOutput ? "结果同步中" : "无结果"}</span>
</div>
);
}
@@ -1532,7 +1543,7 @@ function TaskDetailModal({
<span className="task-detail-kicker">任务详情</span>
<h2 id="task-detail-title" title={taskName(job)}>{taskName(job)}</h2>
<div className="task-detail-subline">
<span className={`status ${job.status}`}>{statusLabel(job.status)}</span>
<span className={`status ${taskStatusClass(job, assets)}`}>{statusLabel(job.status, outputAssets.length > 0)}</span>
<span>{capabilityLabel(job.capability)}</span>
<span>{durationLabel(job, now)}</span>
</div>
@@ -1666,20 +1677,23 @@ function firstOutputAsset(job: GenerationJob, assets: Map<string, Asset>) {
function BillingEstimate({
quote,
loading
loading,
error
}: {
quote: BillingQuote | null;
loading: boolean;
error: string | null;
}) {
return (
<aside className={clsx("billing-estimate", loading && "is-loading")} aria-live="polite" aria-label={quote?.quotaExempt ? "本次预计费用(超级管理员不计额度)" : "本次预计消耗额度"}>
<aside className={clsx("billing-estimate", loading && "is-loading", error && "has-error")} aria-live="polite" aria-label={quote?.quotaExempt ? "本次预计费用(超级管理员不计额度)" : "本次预计消耗额度"}>
<div className="billing-estimate-head">
<span className="billing-estimate-icon" aria-hidden="true"><CircleDollarSign size={16} /></span>
<span>{quote?.quotaExempt ? "本次预计费用(不计额度)" : "本次预计消耗额度"}</span>
</div>
<strong className="billing-estimate-amount">
{loading ? "…" : quote ? formatBillingAmount(quote.amountFen) : "—"}
{loading ? "…" : error ? "暂不可用" : quote ? formatBillingAmount(quote.amountFen) : "—"}
</strong>
{error ? <span className="billing-estimate-error">{error}</span> : null}
</aside>
);
}
@@ -1767,7 +1781,7 @@ function taskEngineLabel(value: string) {
if (value === "evolink") return "Image2";
if (value === "bailian") return "阿里云百炼";
if (value === "seedance") return "Seedance";
if (value === "mock") return "系统生成";
if (value === "mock") return "历史任务";
return value;
}
@@ -1805,10 +1819,14 @@ function taskName(job: GenerationJob) {
return text ? text : capabilityLabel(job.capability);
}
function statusLabel(status: GenerationJob["status"]) {
function taskStatusClass(job: GenerationJob, assets: Map<string, Asset>) {
return job.status === "succeeded" && !firstOutputAsset(job, assets) ? "syncing" : job.status;
}
function statusLabel(status: GenerationJob["status"], hasOutput = true) {
if (status === "queued") return "排队中";
if (status === "running") return "生成中";
if (status === "succeeded") return "已完成";
if (status === "succeeded") return hasOutput ? "已完成" : "结果同步中";
if (status === "failed") return "失败";
if (status === "expired") return "已过期";
if (status === "cancelled") return "已取消";
@@ -1844,7 +1862,7 @@ function formatDateTime(value: string) {
}
function billingLabel(job: GenerationJob) {
if (!job.billing) return job.provider === "mock" ? "免计费" : "未计费";
if (!job.billing) return job.provider === "mock" ? "历史任务(未计费)" : "未计费";
if (job.billing.quotaExempt) return `超管费用 ${formatBillingAmount(job.billing.amountFen)}(不计额度)`;
if (job.billing.status === "refunded") return `已退款 ${formatBillingAmount(job.billing.amountFen)}`;
if (job.billing.status === "pending") return `预计 ${formatBillingAmount(job.billing.amountFen)}`;