Merge pull request 'Codex/generation task reliability' (#27) from codex/generation-task-reliability into master
Reviewed-on: #27
This commit was merged in pull request #27.
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user