Codex/generation task reliability #27

Merged
stringadmin merged 10 commits from codex/generation-task-reliability into master 2026-06-08 10:31:08 +00:00
17 changed files with 206 additions and 29 deletions
+4
View File
@@ -2,6 +2,7 @@ import { serverRequest } from "./serverConnection";
export interface BetaApplicationInput { export interface BetaApplicationInput {
name: string; name: string;
email: string;
phone: string; phone: string;
wechat: string; wechat: string;
industry: string; industry: string;
@@ -16,6 +17,7 @@ export interface BetaApplicationInput {
wantFeature: string[]; wantFeature: string[];
selfStatement: string; selfStatement: string;
signature: string; signature: string;
applicationDate: string;
agreeRules: boolean; agreeRules: boolean;
} }
@@ -72,6 +74,7 @@ function normalizeApplication(raw: unknown): BetaApplicationItem {
userId: readNumberOrNull(item.userId), userId: readNumberOrNull(item.userId),
username: readNullableString(item.username), username: readNullableString(item.username),
name: readString(item.name), name: readString(item.name),
email: readString(item.email),
phone: readString(item.phone), phone: readString(item.phone),
wechat: readString(item.wechat), wechat: readString(item.wechat),
industry: readString(item.industry), industry: readString(item.industry),
@@ -86,6 +89,7 @@ function normalizeApplication(raw: unknown): BetaApplicationItem {
wantFeature: readStringArray(item.wantFeature), wantFeature: readStringArray(item.wantFeature),
selfStatement: readString(item.selfStatement), selfStatement: readString(item.selfStatement),
signature: readString(item.signature), signature: readString(item.signature),
applicationDate: readString(item.applicationDate),
agreeRules: item.agreeRules === true, agreeRules: item.agreeRules === true,
status: normalizeStatus(item.status), status: normalizeStatus(item.status),
inviteCode: readNullableString(item.inviteCode), inviteCode: readNullableString(item.inviteCode),
+6 -1
View File
@@ -38,9 +38,14 @@ function normalizeModelOption(raw: unknown): ModelCapabilityOption | null {
const enabled = raw.enabled === undefined ? status !== "maintenance" && status !== "disabled" : Boolean(raw.enabled); const enabled = raw.enabled === undefined ? status !== "maintenance" && status !== "disabled" : Boolean(raw.enabled);
if (!enabled) return null; if (!enabled) return null;
const label = toStringValue(raw.label ?? raw.displayName ?? raw.display_name ?? raw.name, value);
return { return {
value, value,
label: toStringValue(raw.label ?? raw.displayName ?? raw.display_name ?? raw.name, value), label:
value === "wan2.7-image-pro"
? label.replace(/\s*4k\b/i, "").trim() || "wan 2.7 Pro"
: label,
description: toStringValue(raw.description) || undefined, description: toStringValue(raw.description) || undefined,
badge: toStringValue(raw.badge) || undefined, badge: toStringValue(raw.badge) || undefined,
enabled, enabled,
+12
View File
@@ -248,6 +248,17 @@ function isNonAuthErrorCode(code: string | undefined): boolean {
].includes(code); ].includes(code);
} }
function isAuthFailureResponse(status: number, payload: unknown): boolean {
if (status === 401) return true;
if (status !== 403) return false;
const code = getPayloadCode(payload);
if (code === "SESSION_REPLACED" || code === "TOKEN_EXPIRED" || code === "ACCOUNT_DISABLED") return true;
const message = getPayloadMessage(payload) || "";
return /账号已禁用|登录已过期|登录状态|session|token|企业信息不存在/i.test(message);
}
function notifySessionExpired(status: number, response: Response, payload: unknown): void { function notifySessionExpired(status: number, response: Response, payload: unknown): void {
if (status !== 401 && status !== 403) return; if (status !== 401 && status !== 403) return;
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
@@ -263,6 +274,7 @@ function notifySessionExpired(status: number, response: Response, payload: unkno
// Non-auth 403 errors (enterprise model access, insufficient balance) must // Non-auth 403 errors (enterprise model access, insufficient balance) must
// not trigger session expiry. // not trigger session expiry.
if (status === 403 && isNonAuthErrorCode(getPayloadCode(payload))) return; if (status === 403 && isNonAuthErrorCode(getPayloadCode(payload))) return;
if (!isAuthFailureResponse(status, payload)) return;
const now = Date.now(); const now = Date.now();
if (now - lastSessionExpiredEventAt < 1500) return; if (now - lastSessionExpiredEventAt < 1500) return;
+12 -6
View File
@@ -10,6 +10,7 @@ interface BetaApplicationModalProps {
/* ── Form state ── */ /* ── Form state ── */
interface BetaFormData { interface BetaFormData {
name: string; name: string;
email: string;
phone: string; phone: string;
wechat: string; wechat: string;
industry: string; industry: string;
@@ -24,11 +25,13 @@ interface BetaFormData {
wantFeature: string[]; wantFeature: string[];
selfStatement: string; selfStatement: string;
signature: string; signature: string;
applicationDate: string;
agreeRules: boolean; agreeRules: boolean;
} }
const INITIAL_FORM: BetaFormData = { const INITIAL_FORM: BetaFormData = {
name: "", name: "",
email: "",
phone: "", phone: "",
wechat: "", wechat: "",
industry: "", industry: "",
@@ -43,6 +46,7 @@ const INITIAL_FORM: BetaFormData = {
wantFeature: [], wantFeature: [],
selfStatement: "", selfStatement: "",
signature: "", signature: "",
applicationDate: "",
agreeRules: false, agreeRules: false,
}; };
@@ -156,10 +160,12 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
const validate = () => { const validate = () => {
if (!form.name.trim()) return "请填写姓名 / 常用昵称"; if (!form.name.trim()) return "请填写姓名 / 常用昵称";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) return "请填写用于接收内测码的有效邮箱";
if (!form.phone.trim()) return "请填写联系手机号码"; if (!form.phone.trim()) return "请填写联系手机号码";
if (!form.wechat.trim()) return "请填写微信账号"; if (!form.wechat.trim()) return "请填写微信账号";
if (!form.selfStatement.trim()) return "请填写申请自述"; if (!form.selfStatement.trim()) return "请填写申请自述";
if (!form.signature.trim()) return "请填写申请人确认签字"; if (!form.signature.trim()) return "请填写申请人确认签字";
if (!form.applicationDate.trim()) return "请填写申请日期";
if (!form.agreeRules) return "请先阅读并同意内测规则"; if (!form.agreeRules) return "请先阅读并同意内测规则";
return null; return null;
}; };
@@ -178,6 +184,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
await betaApplicationClient.submit({ await betaApplicationClient.submit({
...form, ...form,
name: form.name.trim(), name: form.name.trim(),
email: form.email.trim(),
phone: form.phone.trim(), phone: form.phone.trim(),
wechat: form.wechat.trim(), wechat: form.wechat.trim(),
industry: form.industry.trim(), industry: form.industry.trim(),
@@ -190,9 +197,10 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
feedbackWilling: form.feedbackWilling.trim(), feedbackWilling: form.feedbackWilling.trim(),
selfStatement: form.selfStatement.trim(), selfStatement: form.selfStatement.trim(),
signature: form.signature.trim(), signature: form.signature.trim(),
applicationDate: form.applicationDate.trim(),
}); });
setForm(INITIAL_FORM); setForm(INITIAL_FORM);
setMessage({ tone: "success", text: "申请已提交,请留意站内通知。" }); setMessage({ tone: "success", text: "申请已提交,请留意预留邮箱中的审核结果。" });
} catch (error) { } catch (error) {
setMessage({ tone: "error", text: error instanceof Error ? error.message : "提交内测申请失败" }); setMessage({ tone: "error", text: error instanceof Error ? error.message : "提交内测申请失败" });
} finally { } finally {
@@ -229,6 +237,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
<h3 className="beta-doc-section__title"></h3> <h3 className="beta-doc-section__title"></h3>
<div className="beta-doc-grid"> <div className="beta-doc-grid">
<TextField label="姓名 / 常用昵称" value={form.name} onChange={(v) => update("name", v)} /> <TextField label="姓名 / 常用昵称" value={form.name} onChange={(v) => update("name", v)} />
<TextField label="接收内测码邮箱" value={form.email} onChange={(v) => update("email", v)} placeholder="审核通过后内测码将发送到此邮箱" />
<TextField label="联系手机号码" value={form.phone} onChange={(v) => update("phone", v)} /> <TextField label="联系手机号码" value={form.phone} onChange={(v) => update("phone", v)} />
<TextField label="微信账号" value={form.wechat} onChange={(v) => update("wechat", v)} /> <TextField label="微信账号" value={form.wechat} onChange={(v) => update("wechat", v)} />
<TextField label="所在行业 / 职业" value={form.industry} onChange={(v) => update("industry", v)} /> <TextField label="所在行业 / 职业" value={form.industry} onChange={(v) => update("industry", v)} />
@@ -297,7 +306,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
<li> <strong>500 50,000 </strong>使</li> <li> <strong>500 50,000 </strong>使</li>
<li>线</li> <li>线</li>
<li></li> <li></li>
<li> <strong>48 </strong> </li> <li> <strong>48 </strong> </li>
<li>线</li> <li>线</li>
</ol> </ol>
@@ -312,10 +321,7 @@ const BetaApplicationModal = ({ open, onClose }: BetaApplicationModalProps) => {
<div className="beta-doc-grid beta-doc-grid--two"> <div className="beta-doc-grid beta-doc-grid--two">
<TextField label="申请人确认签字" value={form.signature} onChange={(v) => update("signature", v)} placeholder="请签署姓名" /> <TextField label="申请人确认签字" value={form.signature} onChange={(v) => update("signature", v)} placeholder="请签署姓名" />
<div className="beta-text-field"> <TextField label="申请填写日期" value={form.applicationDate} onChange={(v) => update("applicationDate", v)} placeholder="例如:2026年6月8日" />
<span className="beta-text-field__label"></span>
<input type="text" className="beta-text-field__input" value="2026年 月 日" readOnly />
</div>
</div> </div>
</section> </section>
@@ -214,6 +214,7 @@ export default function BetaApplicationsPage({ session, onOpenLogin }: BetaAppli
<h3></h3> <h3></h3>
<div className="beta-admin-field-grid"> <div className="beta-admin-field-grid">
<DetailField label="姓名 / 常用昵称" value={valueOrEmpty(selectedApplication.name)} /> <DetailField label="姓名 / 常用昵称" value={valueOrEmpty(selectedApplication.name)} />
<DetailField label="接收内测码邮箱" value={valueOrEmpty(selectedApplication.email)} />
<DetailField label="联系手机号码" value={valueOrEmpty(selectedApplication.phone)} /> <DetailField label="联系手机号码" value={valueOrEmpty(selectedApplication.phone)} />
<DetailField label="微信账号" value={valueOrEmpty(selectedApplication.wechat)} /> <DetailField label="微信账号" value={valueOrEmpty(selectedApplication.wechat)} />
<DetailField label="所在行业 / 职业" value={valueOrEmpty(selectedApplication.industry)} /> <DetailField label="所在行业 / 职业" value={valueOrEmpty(selectedApplication.industry)} />
@@ -248,6 +249,7 @@ export default function BetaApplicationsPage({ session, onOpenLogin }: BetaAppli
<p className="beta-admin-statement">{selectedApplication.selfStatement || "未填写"}</p> <p className="beta-admin-statement">{selectedApplication.selfStatement || "未填写"}</p>
<div className="beta-admin-field-grid"> <div className="beta-admin-field-grid">
<DetailField label="申请人确认签字" value={valueOrEmpty(selectedApplication.signature)} /> <DetailField label="申请人确认签字" value={valueOrEmpty(selectedApplication.signature)} />
<DetailField label="申请填写日期" value={valueOrEmpty(selectedApplication.applicationDate)} />
<DetailField label="同意规则" value={selectedApplication.agreeRules ? "已同意" : "未同意"} /> <DetailField label="同意规则" value={selectedApplication.agreeRules ? "已同意" : "未同意"} />
<DetailField label="IP" value={valueOrEmpty(selectedApplication.ipAddress)} /> <DetailField label="IP" value={valueOrEmpty(selectedApplication.ipAddress)} />
<DetailField label="客户端" value={valueOrEmpty(selectedApplication.userAgent)} wide /> <DetailField label="客户端" value={valueOrEmpty(selectedApplication.userAgent)} wide />
@@ -10,6 +10,7 @@ import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react";
import { aiGenerationClient } from "../../api/aiGenerationClient"; import { aiGenerationClient } from "../../api/aiGenerationClient";
import { communityClient } from "../../api/communityClient"; import { communityClient } from "../../api/communityClient";
import WorkspacePageShell from "../../components/WorkspacePageShell"; import WorkspacePageShell from "../../components/WorkspacePageShell";
import "../../styles/pages/compliance.css";
import type { WebCanvasWorkflow, WebUserSession } from "../../types"; import type { WebCanvasWorkflow, WebUserSession } from "../../types";
import { getWorkflowCoverUrl, isCanvasWorkflow } from "../community/communityCaseUtils"; import { getWorkflowCoverUrl, isCanvasWorkflow } from "../community/communityCaseUtils";
import { canManageCommunityCases } from "./communityPermissions"; import { canManageCommunityCases } from "./communityPermissions";
@@ -11,6 +11,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { communityClient, type ServerCommunityCase } from "../../api/communityClient"; import { communityClient, type ServerCommunityCase } from "../../api/communityClient";
import { reportClient, type AdminReportItem } from "../../api/reportClient"; import { reportClient, type AdminReportItem } from "../../api/reportClient";
import WorkspacePageShell from "../../components/WorkspacePageShell"; import WorkspacePageShell from "../../components/WorkspacePageShell";
import "../../styles/pages/compliance.css";
import type { WebUserSession } from "../../types"; import type { WebUserSession } from "../../types";
import { canManageCommunityCases, canReviewCommunity } from "./communityPermissions"; import { canManageCommunityCases, canReviewCommunity } from "./communityPermissions";
+1
View File
@@ -2,6 +2,7 @@ import { CheckCircleOutlined, FlagOutlined, MailOutlined, PhoneOutlined } from "
import { useEffect, useState, type FormEvent } from "react"; import { useEffect, useState, type FormEvent } from "react";
import { publicConfigClient, type WebPublicConfig } from "../../api/publicConfigClient"; import { publicConfigClient, type WebPublicConfig } from "../../api/publicConfigClient";
import { reportClient, type ReportInput } from "../../api/reportClient"; import { reportClient, type ReportInput } from "../../api/reportClient";
import "../../styles/pages/compliance.css";
type SubmitState = "idle" | "loading" | "success" | "error"; type SubmitState = "idle" | "loading" | "success" | "error";
+94 -12
View File
@@ -67,7 +67,6 @@ import { downloadResultAsset } from "./workbenchDownload";
import { translateTaskError } from "../../utils/translateTaskError"; import { translateTaskError } from "../../utils/translateTaskError";
import { import {
buildLocalTimeoutMessage, buildLocalTimeoutMessage,
formatTextTokenUsage,
getTaskTimeoutPolicy, getTaskTimeoutPolicy,
isTaskLocallyTimedOut, isTaskLocallyTimedOut,
} from "../../utils/taskLifecycle"; } from "../../utils/taskLifecycle";
@@ -79,10 +78,12 @@ import {
import { isViduModel } from "../../utils/viduRouting"; import { isViduModel } from "../../utils/viduRouting";
import { isPixverseModel } from "../../utils/pixverseRouting"; import { isPixverseModel } from "../../utils/pixverseRouting";
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel"; import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
import { ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy"; import { calculateEnterpriseVideoCredits, ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
import { import {
getImageQualityOptions, getImageQualityOptions,
getImageQualityOptionsForContext,
getDefaultImageQuality, getDefaultImageQuality,
getDefaultImageQualityForContext,
getVideoQualityOptions, getVideoQualityOptions,
getDefaultVideoQuality, getDefaultVideoQuality,
getVideoQualityLabel, getVideoQualityLabel,
@@ -221,6 +222,12 @@ const MODE_ICONS: Record<WorkbenchMode, ReactNode> = {
video: <VideoCameraOutlined />, video: <VideoCameraOutlined />,
}; };
function formatCreditValue(value: number): string {
if (!Number.isFinite(value)) return "-";
if (value >= 100) return Math.round(value).toLocaleString("zh-CN");
return Number(value.toFixed(2)).toString();
}
function WorkbenchPage({ function WorkbenchPage({
isAuthenticated, isAuthenticated,
session, session,
@@ -464,11 +471,72 @@ function WorkbenchPage({
setSidebarCollapsed(!hasSidebarRecords); setSidebarCollapsed(!hasSidebarRecords);
}, [hasSidebarRecords]); }, [hasSidebarRecords]);
const imageQualityOptions = useMemo(() => getImageQualityOptions(imageModel), [imageModel]); const hasImageReferences = activeMode === "image" && referenceItems.some((item) => item.kind === "image");
const isImageGridMode = activeMode === "image" && imageGridMode !== "single";
const imageQualityContext = useMemo(
() => ({
hasReferenceImages: hasImageReferences,
isGridMode: isImageGridMode,
}),
[hasImageReferences, isImageGridMode],
);
const imageQualityOptions = useMemo(
() => getImageQualityOptionsForContext(imageModel, imageQualityContext),
[imageModel, imageQualityContext],
);
const imageGridModeOptions = useMemo(
() =>
String(imageModel || "").toLowerCase().startsWith("wan2.7-")
? GRID_MODE_OPTIONS.filter((option) => option.value !== "grid-25")
: GRID_MODE_OPTIONS,
[imageModel],
);
const videoQualityOptions = getVideoQualityOptions(videoModel); const videoQualityOptions = getVideoQualityOptions(videoModel);
const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality); const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality);
const imageSettingsSummary = `${imageRatio} / ${imageQuality}`; const imageSettingsSummary = `${imageRatio} / ${imageQuality}`;
const billingEstimate = useMemo(() => {
if (activeMode === "image") {
return {
label: "预计 20 积分",
title: `图片生成按任务计费:${activeModel}${imageSettingsSummary},预计 20 积分`,
};
}
if (activeMode === "video") {
try {
const durationSeconds = Math.max(1, Math.ceil(Number(videoDuration) || 1));
const credits = calculateEnterpriseVideoCredits({
model: activeModelValue,
resolution: videoQuality,
durationSeconds,
muted: false,
hasReferenceVideo: referenceItems.some((item) => item.kind === "video"),
});
return {
label: `预计 ${formatCreditValue(credits)} 积分`,
title: `${activeModel}${videoQualityLabel}${durationSeconds} 秒,预计 ${formatCreditValue(credits)} 积分`,
};
} catch {
return {
label: "计费以提交后为准",
title: "当前模型的预估计费暂不可用,实际扣费以服务端结算为准",
};
}
}
return {
label: "按 Token 结算",
title: "文本对话按输入、输出 Token 实际用量结算,完成后显示本次积分",
};
}, [
activeMode,
activeModel,
activeModelValue,
imageSettingsSummary,
referenceItems,
videoDuration,
videoQuality,
videoQualityLabel,
]);
const composerPlaceholder = const composerPlaceholder =
referenceItems.length > 0 ? `${toolTheme.placeholder},可输入 @ 引用参考内容` : toolTheme.placeholder; referenceItems.length > 0 ? `${toolTheme.placeholder},可输入 @ 引用参考内容` : toolTheme.placeholder;
const dropdownDirection = hasActivatedWorkspace ? "up" : "down"; const dropdownDirection = hasActivatedWorkspace ? "up" : "down";
@@ -1158,9 +1226,15 @@ function WorkbenchPage({
useEffect(() => { useEffect(() => {
if (!imageQualityOptions.some((option) => option.value === imageQuality)) { if (!imageQualityOptions.some((option) => option.value === imageQuality)) {
setImageQuality(getDefaultImageQuality(imageModel)); setImageQuality(getDefaultImageQualityForContext(imageModel, imageQualityContext));
} }
}, [imageModel, imageQuality, imageQualityOptions]); }, [imageModel, imageQuality, imageQualityContext, imageQualityOptions]);
useEffect(() => {
if (!imageGridModeOptions.some((option) => option.value === imageGridMode)) {
setImageGridMode("single");
}
}, [imageGridMode, imageGridModeOptions]);
useEffect(() => { useEffect(() => {
if (activeMode !== "video" || videoFrameMode !== "start-end" || referenceItems.length <= 2) return; if (activeMode !== "video" || videoFrameMode !== "start-end" || referenceItems.length <= 2) return;
@@ -2585,7 +2659,15 @@ function WorkbenchPage({
} }
}; };
const sendDisabled = !inputValue.trim() || (activeMode !== "chat" && getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3); const activeGenerationCount = getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id));
const generationLimitReached = activeMode !== "chat" && activeGenerationCount >= 3;
const promptIsEmpty = !inputValue.trim();
const sendDisabled = promptIsEmpty || generationLimitReached;
const sendButtonTitle = promptIsEmpty
? "输入内容后可发送"
: generationLimitReached
? `当前已有 ${activeGenerationCount} 个任务进行中,请等待任一任务完成`
: billingEstimate.title;
const suggestedPrompts = [ const suggestedPrompts = [
{ text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode }, { text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode },
@@ -2850,7 +2932,7 @@ function WorkbenchPage({
<SelectChip <SelectChip
chipId="image-grid-mode" chipId="image-grid-mode"
value={imageGridMode} value={imageGridMode}
options={GRID_MODE_OPTIONS} options={imageGridModeOptions}
disabled={disabled} disabled={disabled}
isOpen={toolbarMenuId === "image-grid-mode"} isOpen={toolbarMenuId === "image-grid-mode"}
onToggle={() => toggleToolbarMenu("image-grid-mode")} onToggle={() => toggleToolbarMenu("image-grid-mode")}
@@ -2922,10 +3004,15 @@ function WorkbenchPage({
)} )}
</div> </div>
<div className="wb-composer__toolbar-right"> <div className="wb-composer__toolbar-right">
<span className="wb-composer__billing-estimate" title={billingEstimate.title}>
{billingEstimate.label}
</span>
<button <button
type="button" type="button"
className={`wb-composer__send-primary${isGenerating ? " is-loading" : ""}`} className={`wb-composer__send-primary${isGenerating ? " is-loading" : ""}`}
disabled={sendDisabled || isGenerating} disabled={sendDisabled || isGenerating}
title={isGenerating ? "任务处理中" : sendButtonTitle}
aria-label={isGenerating ? "任务处理中" : sendButtonTitle}
onClick={() => { onClick={() => {
if (getCachedRole() === "admin") console.log("[ai/workbench-send-click]", { if (getCachedRole() === "admin") console.log("[ai/workbench-send-click]", {
mode: activeMode, mode: activeMode,
@@ -3231,11 +3318,6 @@ function WorkbenchPage({
<span>{message.taskStatusLabel || generationStatus}</span> <span>{message.taskStatusLabel || generationStatus}</span>
</div> </div>
)} )}
{message.role === "assistant" && message.mode === "chat" && message.status === "completed" && (
<div className="ai-chat-task-billing-note">
{formatTextTokenUsage(message.taskUsage)}
</div>
)}
{(message.resultUrl || (message.result && message.status !== "thinking")) && ( {(message.resultUrl || (message.result && message.status !== "thinking")) && (
<ResultCard <ResultCard
message={message} message={message}
+1 -1
View File
@@ -231,7 +231,7 @@ export const MODE_OPTIONS: WorkbenchOption[] = (Object.keys(MODE_META) as Workbe
})); }));
export const IMAGE_MODEL_OPTIONS: WorkbenchOption[] = [ export const IMAGE_MODEL_OPTIONS: WorkbenchOption[] = [
{ value: "wan2.7-image-pro", label: "wan 2.7 Pro 4K" }, { value: "wan2.7-image-pro", label: "wan 2.7 Pro" },
{ value: "wan2.7-image", label: "wan 2.7" }, { value: "wan2.7-image", label: "wan 2.7" },
{ value: "gpt-image-2", label: "omni-GPT" }, { value: "gpt-image-2", label: "omni-GPT" },
{ value: "gpt-image-2-vip", label: "omni-GPT VIP" }, { value: "gpt-image-2-vip", label: "omni-GPT VIP" },
+2 -2
View File
@@ -33,7 +33,7 @@ const initialState: SessionState = {
loginPromptOpen: false, loginPromptOpen: false,
pendingAction: null, pendingAction: null,
sessionReplacedOpen: false, sessionReplacedOpen: false,
sessionReplacedMessage: '您的账号已在其他设备登录,此设备的登录状态已失效。', sessionReplacedMessage: '当前账号已在其他设备登录,此设备的登录状态已失效。',
}; };
export const useSessionStore = create<SessionState & SessionActions>((set) => ({ export const useSessionStore = create<SessionState & SessionActions>((set) => ({
@@ -55,7 +55,7 @@ export const useSessionStore = create<SessionState & SessionActions>((set) => ({
showSessionReplaced: (message) => set({ showSessionReplaced: (message) => set({
sessionReplacedOpen: true, sessionReplacedOpen: true,
sessionReplacedMessage: message || '您的账号已在其他设备登录(最多同时 2 台设备),此设备的登录状态已失效。', sessionReplacedMessage: message || '当前账号已在其他设备登录,此设备的登录状态已失效。',
}), }),
hideSessionReplaced: () => set({ sessionReplacedOpen: false }), hideSessionReplaced: () => set({ sessionReplacedOpen: false }),
+19 -1
View File
@@ -3,7 +3,11 @@
flex-direction: column; flex-direction: column;
gap: 18px; gap: 18px;
width: min(1180px, calc(100vw - 48px)); width: min(1180px, calc(100vw - 48px));
margin: 0 auto;
height: 100%;
min-height: 0;
padding: 24px; padding: 24px;
overflow: hidden;
} }
.beta-admin-toolbar { .beta-admin-toolbar {
@@ -90,6 +94,8 @@
display: grid; display: grid;
grid-template-columns: 300px minmax(0, 1fr); grid-template-columns: 300px minmax(0, 1fr);
gap: 16px; gap: 16px;
flex: 1;
min-height: 0;
align-items: start; align-items: start;
} }
@@ -97,7 +103,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
max-height: calc(100vh - 220px); max-height: 100%;
overflow: auto; overflow: auto;
padding-right: 4px; padding-right: 4px;
} }
@@ -174,6 +180,10 @@
flex-direction: column; flex-direction: column;
gap: 14px; gap: 14px;
min-width: 0; min-width: 0;
max-height: 100%;
min-height: 0;
overflow: auto;
padding-right: 4px;
} }
.beta-admin-detail__header, .beta-admin-detail__header,
@@ -376,6 +386,7 @@
.beta-admin-page__inner { .beta-admin-page__inner {
width: min(100%, calc(100vw - 24px)); width: min(100%, calc(100vw - 24px));
padding: 16px 12px; padding: 16px 12px;
overflow: auto;
} }
.beta-admin-toolbar, .beta-admin-toolbar,
@@ -385,11 +396,18 @@
.beta-admin-layout { .beta-admin-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
overflow: visible;
} }
.beta-admin-list { .beta-admin-list {
max-height: none; max-height: none;
} }
.beta-admin-detail {
max-height: none;
overflow: visible;
padding-right: 0;
}
} }
@media (max-width: 640px) { @media (max-width: 640px) {
+15
View File
@@ -11915,6 +11915,21 @@
gap: 6px; gap: 6px;
} }
.wb-composer__billing-estimate {
max-width: 138px;
padding: 6px 9px;
border: 2px solid #111;
background: #fffbe8;
color: #111;
font-size: 12px;
font-weight: 800;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-shadow: 2px 2px 0 #111;
}
.wb-composer__send-primary { .wb-composer__send-primary {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
+8
View File
@@ -1794,6 +1794,14 @@
box-shadow: none; box-shadow: none;
} }
.web-shell[data-ui-theme="dark-green"] .wb-composer__billing-estimate {
border: 1px solid var(--border-default);
border-radius: 999px;
background: var(--bg-elevated);
color: var(--fg-body);
box-shadow: none;
}
.web-shell[data-ui-theme="dark-green"] .wb-composer__send-primary:hover:not(:disabled) { .web-shell[data-ui-theme="dark-green"] .wb-composer__send-primary:hover:not(:disabled) {
background: var(--accent-hover); background: var(--accent-hover);
color: var(--dg-button-text); color: var(--dg-button-text);
+4 -3
View File
@@ -40,6 +40,7 @@ export const ENTERPRISE_VIDEO_RESOLUTION_OPTIONS = [
export const ENTERPRISE_DEFAULT_VIDEO_MODEL = HAPPY_HORSE_UI_MODEL; export const ENTERPRISE_DEFAULT_VIDEO_MODEL = HAPPY_HORSE_UI_MODEL;
export const ENTERPRISE_DEFAULT_VIDEO_RESOLUTION = "1080P"; export const ENTERPRISE_DEFAULT_VIDEO_RESOLUTION = "1080P";
const CREDITS_PER_CNY = 100;
export interface EnterpriseVideoPricingInput { export interface EnterpriseVideoPricingInput {
model: string; model: string;
@@ -74,11 +75,11 @@ export function getEnterpriseVideoCreditRate(input: EnterpriseVideoPricingInput)
} }
if (model.includes("vidu")) { if (model.includes("vidu")) {
return resolution === "720P" ? 0.4 : 0.8; return resolution === "720P" ? 0.6 : 1.0;
} }
if (model.includes("pixverse")) { if (model.includes("pixverse")) {
return resolution === "720P" ? 0.4 : 0.8; return resolution === "720P" ? 0.6 : 1.0;
} }
if (model.includes("kling")) { if (model.includes("kling")) {
@@ -94,5 +95,5 @@ export function getEnterpriseVideoCreditRate(input: EnterpriseVideoPricingInput)
export function calculateEnterpriseVideoCredits(input: EnterpriseVideoPricingInput): number { export function calculateEnterpriseVideoCredits(input: EnterpriseVideoPricingInput): number {
const duration = Math.max(1, Math.ceil(Number(input.durationSeconds) || 1)); const duration = Math.max(1, Math.ceil(Number(input.durationSeconds) || 1));
return Number((getEnterpriseVideoCreditRate(input) * duration).toFixed(2)); return Number((getEnterpriseVideoCreditRate(input) * duration * CREDITS_PER_CNY).toFixed(2));
} }
+19
View File
@@ -25,11 +25,30 @@ export function getImageQualityOptions(model: string): CanvasOption[] {
: imageQualityOptions.filter((option) => option.value !== "4K"); : imageQualityOptions.filter((option) => option.value !== "4K");
} }
export function getImageQualityOptionsForContext(
model: string,
context?: { hasReferenceImages?: boolean; isGridMode?: boolean },
): CanvasOption[] {
const options = getImageQualityOptions(model);
const shouldLimitTo2K =
String(model || "").toLowerCase() === "wan2.7-image-pro" &&
(context?.hasReferenceImages || context?.isGridMode);
return shouldLimitTo2K ? options.filter((option) => option.value !== "4K") : options;
}
export function getDefaultImageQuality(model: string): string { export function getDefaultImageQuality(model: string): string {
const options = getImageQualityOptions(model); const options = getImageQualityOptions(model);
return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K"; return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K";
} }
export function getDefaultImageQualityForContext(
model: string,
context?: { hasReferenceImages?: boolean; isGridMode?: boolean },
): string {
const options = getImageQualityOptionsForContext(model, context);
return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K";
}
// ─── Video quality ──────────────────────────────────────────────────────────── // ─── Video quality ────────────────────────────────────────────────────────────
function normalizeVideoModel(model: string): string { function normalizeVideoModel(model: string): string {
+5 -3
View File
@@ -32,8 +32,10 @@ export interface TextTokenUsage {
totalTokens?: number; totalTokens?: number;
} }
export const TEXT_INPUT_CREDITS_PER_MILLION = 2; const CREDITS_PER_CNY = 100;
export const TEXT_OUTPUT_CREDITS_PER_MILLION = 5;
export const TEXT_INPUT_CREDITS_PER_MILLION = 2 * CREDITS_PER_CNY;
export const TEXT_OUTPUT_CREDITS_PER_MILLION = 5 * CREDITS_PER_CNY;
const IMAGE_TIMEOUT_POLICY: TaskTimeoutPolicy = { const IMAGE_TIMEOUT_POLICY: TaskTimeoutPolicy = {
submitTimeoutMs: 90_000, submitTimeoutMs: 90_000,
@@ -151,7 +153,7 @@ export function estimateTextTokenCredits(usage: TextTokenUsage): number {
} }
export function formatTextTokenUsage(usage?: TextTokenUsage | null): string { export function formatTextTokenUsage(usage?: TextTokenUsage | null): string {
const rule = "文本计费规则:输入 Token 每百万 2 积分,输出 Token 每百万 5 积分,实际以服务端结算为准。"; const rule = "文本计费规则:输入 Token 每百万 200 积分,输出 Token 每百万 500 积分,实际以服务端结算为准。";
if (!usage) return rule; if (!usage) return rule;
const promptTokens = Math.max(0, Number(usage.promptTokens || 0)); const promptTokens = Math.max(0, Number(usage.promptTokens || 0));
const completionTokens = Math.max(0, Number(usage.completionTokens || 0)); const completionTokens = Math.max(0, Number(usage.completionTokens || 0));