Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2574dfe3d7 | |||
| 136fb15397 | |||
| 4e97e706fd | |||
| 30536ad15f | |||
| e78cc05299 | |||
| b88be66e7f | |||
| 1a9196a63a | |||
| 4dfcb6fc8a | |||
| e351e93200 | |||
| 117b9354eb | |||
| 446514dd06 | |||
| 85a174bcb5 | |||
| 560a7baddc | |||
| 4f7f67a278 | |||
| 3963d9ae2f |
+7
-1
@@ -374,6 +374,7 @@ function App() {
|
|||||||
})));
|
})));
|
||||||
|
|
||||||
const [ecommerceEverMounted, setEcommerceEverMounted] = useState(false);
|
const [ecommerceEverMounted, setEcommerceEverMounted] = useState(false);
|
||||||
|
const [workbenchResetToken, setWorkbenchResetToken] = useState(0);
|
||||||
const isEcommerceActive = activeView === "ecommerce" || activeView === "ecommerceHub";
|
const isEcommerceActive = activeView === "ecommerce" || activeView === "ecommerceHub";
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEcommerceActive && !ecommerceEverMounted) setEcommerceEverMounted(true);
|
if (isEcommerceActive && !ecommerceEverMounted) setEcommerceEverMounted(true);
|
||||||
@@ -459,6 +460,9 @@ function App() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSetView = useCallback((view: WebViewKey) => {
|
const handleSetView = useCallback((view: WebViewKey) => {
|
||||||
|
if (view === "workbench" && Boolean(session)) {
|
||||||
|
setWorkbenchResetToken((token) => token + 1);
|
||||||
|
}
|
||||||
window.location.hash = `/${view}`;
|
window.location.hash = `/${view}`;
|
||||||
setView(view);
|
setView(view);
|
||||||
if (view !== "login") {
|
if (view !== "login") {
|
||||||
@@ -467,7 +471,7 @@ function App() {
|
|||||||
if (isWorkspaceView(view)) {
|
if (isWorkspaceView(view)) {
|
||||||
setWorkspaceExpanded(true);
|
setWorkspaceExpanded(true);
|
||||||
}
|
}
|
||||||
}, [setView, setWorkspaceExpanded]);
|
}, [session, setView, setWorkspaceExpanded]);
|
||||||
|
|
||||||
const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => {
|
const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => {
|
||||||
clearAllUserStorage();
|
clearAllUserStorage();
|
||||||
@@ -1313,11 +1317,13 @@ function App() {
|
|||||||
case "workbench":
|
case "workbench":
|
||||||
return (
|
return (
|
||||||
<WorkbenchPage
|
<WorkbenchPage
|
||||||
|
key={`workbench-${workbenchResetToken}`}
|
||||||
isAuthenticated={Boolean(session)}
|
isAuthenticated={Boolean(session)}
|
||||||
session={session}
|
session={session}
|
||||||
onRequireLogin={handleRequireTaskLogin}
|
onRequireLogin={handleRequireTaskLogin}
|
||||||
onOpenResultInCanvas={handleOpenResultInCanvas}
|
onOpenResultInCanvas={handleOpenResultInCanvas}
|
||||||
onRefreshUsage={refreshUsage}
|
onRefreshUsage={refreshUsage}
|
||||||
|
resetToken={workbenchResetToken}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "home":
|
case "home":
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
isRecord,
|
isRecord,
|
||||||
readJsonResponse,
|
readJsonResponse,
|
||||||
serverRequest,
|
serverRequest,
|
||||||
|
isServerRequestError,
|
||||||
throwResponseError,
|
throwResponseError,
|
||||||
} from "./serverConnection";
|
} from "./serverConnection";
|
||||||
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
|
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
|
||||||
@@ -247,6 +248,46 @@ let taskHistoryRouteMissing = false;
|
|||||||
const TASK_SUBMIT_TIMEOUT_MS = 90_000;
|
const TASK_SUBMIT_TIMEOUT_MS = 90_000;
|
||||||
const TASK_STATUS_TIMEOUT_MS = 20_000;
|
const TASK_STATUS_TIMEOUT_MS = 20_000;
|
||||||
const NON_RETRYING_REQUEST = { maxRetries: 0 };
|
const NON_RETRYING_REQUEST = { maxRetries: 0 };
|
||||||
|
const PENDING_CANCEL_TASKS_KEY = "omniai:pending-task-cancellations";
|
||||||
|
|
||||||
|
function readPendingCancelTaskIds(): string[] {
|
||||||
|
if (typeof window === "undefined") return [];
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(PENDING_CANCEL_TASKS_KEY);
|
||||||
|
const parsed = raw ? JSON.parse(raw) : [];
|
||||||
|
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === "string" && id.trim().length > 0) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePendingCancelTaskIds(taskIds: string[]): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
const uniqueIds = Array.from(new Set(taskIds.filter(Boolean)));
|
||||||
|
if (uniqueIds.length) {
|
||||||
|
window.localStorage.setItem(PENDING_CANCEL_TASKS_KEY, JSON.stringify(uniqueIds));
|
||||||
|
} else {
|
||||||
|
window.localStorage.removeItem(PENDING_CANCEL_TASKS_KEY);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Pending cancellation recovery is best-effort.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function markTaskCancelPending(taskId: string): void {
|
||||||
|
writePendingCancelTaskIds([...readPendingCancelTaskIds(), taskId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPendingTaskCancel(taskId: string): void {
|
||||||
|
writePendingCancelTaskIds(readPendingCancelTaskIds().filter((id) => id !== taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRetryTaskCancel(error: unknown): boolean {
|
||||||
|
if (!isServerRequestError(error)) return true;
|
||||||
|
const status = error.status;
|
||||||
|
return status === 429 || status === undefined || status >= 500;
|
||||||
|
}
|
||||||
|
|
||||||
export const aiGenerationClient = {
|
export const aiGenerationClient = {
|
||||||
async createImageTask(input: ImageGenInput): Promise<ImageTaskCreateResponse> {
|
async createImageTask(input: ImageGenInput): Promise<ImageTaskCreateResponse> {
|
||||||
@@ -335,18 +376,48 @@ export const aiGenerationClient = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async cancelTask(taskId: string): Promise<void> {
|
async cancelTask(taskId: string): Promise<void> {
|
||||||
|
markTaskCancelPending(taskId);
|
||||||
try {
|
try {
|
||||||
await serverRequest<void>(`ai/tasks/${taskId}/cancel`, {
|
await serverRequest<void>(`ai/tasks/${taskId}/cancel`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||||
fallbackMessage: "Task cancel failed",
|
fallbackMessage: "Task cancel failed",
|
||||||
});
|
});
|
||||||
|
clearPendingTaskCancel(taskId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isOptionalApiRouteMissing(error)) return;
|
if (isOptionalApiRouteMissing(error) || !shouldRetryTaskCancel(error)) {
|
||||||
|
clearPendingTaskCancel(taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cancelTaskOnUnload(taskId: string): void {
|
||||||
|
markTaskCancelPending(taskId);
|
||||||
|
const url = buildApiUrl(`ai/tasks/${encodeURIComponent(taskId)}/cancel`);
|
||||||
|
const headers = buildAuthHeaders();
|
||||||
|
const body = JSON.stringify({ reason: "page_unload" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
void fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
credentials: "include",
|
||||||
|
keepalive: true,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Page unload cancellation is best-effort.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
flushPendingTaskCancellations(): void {
|
||||||
|
readPendingCancelTaskIds().forEach((taskId) => {
|
||||||
|
this.cancelTask(taskId).catch(() => {});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async getTaskStatus(taskId: string): Promise<AiTaskStatus> {
|
async getTaskStatus(taskId: string): Promise<AiTaskStatus> {
|
||||||
return serverRequest<AiTaskStatus>(`ai/tasks/${taskId}`, {
|
return serverRequest<AiTaskStatus>(`ai/tasks/${taskId}`, {
|
||||||
timeoutMs: TASK_STATUS_TIMEOUT_MS,
|
timeoutMs: TASK_STATUS_TIMEOUT_MS,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ function getEffectiveLimit(): number {
|
|||||||
return userMaxConcurrency ?? DEFAULT_MAX_ACTIVE_GENERATION_TASKS;
|
return userMaxConcurrency ?? DEFAULT_MAX_ACTIVE_GENERATION_TASKS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getEffectiveGenerationLimit(): number {
|
||||||
|
return getEffectiveLimit();
|
||||||
|
}
|
||||||
|
|
||||||
export function getGenerationUserKey(userId?: string | number | null): string {
|
export function getGenerationUserKey(userId?: string | number | null): string {
|
||||||
return userId === undefined || userId === null || userId === "" ? "anonymous" : String(userId);
|
return userId === undefined || userId === null || userId === "" ? "anonymous" : String(userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -396,7 +396,6 @@ function CanvasPage({
|
|||||||
const canvasUploadInputRef = useRef<HTMLInputElement>(null);
|
const canvasUploadInputRef = useRef<HTMLInputElement>(null);
|
||||||
const imageNodeInputRef = useRef<HTMLInputElement>(null);
|
const imageNodeInputRef = useRef<HTMLInputElement>(null);
|
||||||
const canvasRef = useRef<HTMLElement>(null);
|
const canvasRef = useRef<HTMLElement>(null);
|
||||||
const videoGenerationInFlightRef = useRef(new Set<string>());
|
|
||||||
const canvasReferenceUploadPromisesRef = useRef(new Map<string, Promise<string | null>>());
|
const canvasReferenceUploadPromisesRef = useRef(new Map<string, Promise<string | null>>());
|
||||||
const canvasDragCounterRef = useRef(0);
|
const canvasDragCounterRef = useRef(0);
|
||||||
const [isCanvasDragging, setIsCanvasDragging] = useState(false);
|
const [isCanvasDragging, setIsCanvasDragging] = useState(false);
|
||||||
@@ -417,7 +416,7 @@ function CanvasPage({
|
|||||||
const {
|
const {
|
||||||
textGenerationState, imageGenerationState, videoGenerationState,
|
textGenerationState, imageGenerationState, videoGenerationState,
|
||||||
generationToast, setGenerationToast,
|
generationToast, setGenerationToast,
|
||||||
imageGenerationInFlightRef, textGenerationInFlightRef, textGenerationAbortControllersRef,
|
imageGenerationInFlightRef, videoGenerationInFlightRef, textGenerationInFlightRef, textGenerationAbortControllersRef,
|
||||||
canvasGenKeepaliveRestoredRef,
|
canvasGenKeepaliveRestoredRef,
|
||||||
setTextGenerationStatus, setImageGenerationStatus, setVideoGenerationStatus,
|
setTextGenerationStatus, setImageGenerationStatus, setVideoGenerationStatus,
|
||||||
restoreKeepaliveTasks, resetGenerationState,
|
restoreKeepaliveTasks, resetGenerationState,
|
||||||
@@ -1887,13 +1886,14 @@ function CanvasPage({
|
|||||||
setVideoGenerationStatus(nodeId, { status: "submitting", message: "正在提交视频生成", progress: 8 });
|
setVideoGenerationStatus(nodeId, { status: "submitting", message: "正在提交视频生成", progress: 8 });
|
||||||
setGenerationToast("视频正在生成");
|
setGenerationToast("视频正在生成");
|
||||||
|
|
||||||
|
let task: Awaited<ReturnType<typeof onCreateTask>> | null = null;
|
||||||
try {
|
try {
|
||||||
const referenceUrls = await resolveConnectedImageReferenceUrls("video", nodeId);
|
const referenceUrls = await resolveConnectedImageReferenceUrls("video", nodeId);
|
||||||
if (videoNode.videoMode === "img2video" && referenceUrls.length === 0) {
|
if (videoNode.videoMode === "img2video" && referenceUrls.length === 0) {
|
||||||
throw new Error("图生视频需要先连接至少一个可用的图片节点");
|
throw new Error("图生视频需要先连接至少一个可用的图片节点");
|
||||||
}
|
}
|
||||||
let requestModel = resolveVideoRequestModel({ model, referenceUrls });
|
let requestModel = resolveVideoRequestModel({ model, referenceUrls });
|
||||||
const task = await onCreateTask({
|
task = await onCreateTask({
|
||||||
title: videoNode.title || "视频节点生成",
|
title: videoNode.title || "视频节点生成",
|
||||||
type: "video",
|
type: "video",
|
||||||
prompt: prompt || "根据参考图片生成视频",
|
prompt: prompt || "根据参考图片生成视频",
|
||||||
@@ -1916,10 +1916,12 @@ function CanvasPage({
|
|||||||
if (task.status === "completed" && !task.outputUrl) {
|
if (task.status === "completed" && !task.outputUrl) {
|
||||||
throw new Error("视频生成任务已完成,但服务器没有返回结果地址,请稍后重试");
|
throw new Error("视频生成任务已完成,但服务器没有返回结果地址,请稍后重试");
|
||||||
}
|
}
|
||||||
|
const taskId = task.id;
|
||||||
|
addCanvasGenKeepalive(taskId, nodeId, "video", projectId || "");
|
||||||
setVideoGenerationStatus(nodeId, { status: "running", message: "视频生成中", progress: Math.max(18, Number(task.progress || 0)) });
|
setVideoGenerationStatus(nodeId, { status: "running", message: "视频生成中", progress: Math.max(18, Number(task.progress || 0)) });
|
||||||
const outputUrl =
|
const outputUrl =
|
||||||
task.outputUrl ||
|
task.outputUrl ||
|
||||||
(await waitForImageTaskResult(task.id, (status) => {
|
(await waitForVideoTaskResult(taskId, (status) => {
|
||||||
const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0)));
|
const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0)));
|
||||||
const statusLabel =
|
const statusLabel =
|
||||||
status.status === "pending"
|
status.status === "pending"
|
||||||
@@ -1932,11 +1934,12 @@ function CanvasPage({
|
|||||||
setVideoGenerationStatus(nodeId, { status: "running", message: statusLabel, progress });
|
setVideoGenerationStatus(nodeId, { status: "running", message: statusLabel, progress });
|
||||||
}));
|
}));
|
||||||
setVideoGenerationStatus(nodeId, { status: "success", message: "视频生成完成", progress: 100 });
|
setVideoGenerationStatus(nodeId, { status: "success", message: "视频生成完成", progress: 100 });
|
||||||
|
removeCanvasGenKeepalive(taskId);
|
||||||
const immediateAssetRef = createCanvasAssetRefFromGeneratedResult({
|
const immediateAssetRef = createCanvasAssetRefFromGeneratedResult({
|
||||||
url: outputUrl,
|
url: outputUrl,
|
||||||
mediaType: "video/mp4",
|
mediaType: "video/mp4",
|
||||||
resultType: "video",
|
resultType: "video",
|
||||||
taskId: task.id,
|
taskId,
|
||||||
originalUrl: outputUrl,
|
originalUrl: outputUrl,
|
||||||
});
|
});
|
||||||
setVideoNodes((currentNodes) =>
|
setVideoNodes((currentNodes) =>
|
||||||
@@ -1947,7 +1950,7 @@ function CanvasPage({
|
|||||||
videoUrl: outputUrl,
|
videoUrl: outputUrl,
|
||||||
assetRef: immediateAssetRef,
|
assetRef: immediateAssetRef,
|
||||||
taskRef: {
|
taskRef: {
|
||||||
taskId: task.id,
|
taskId,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
resultUrl: outputUrl,
|
resultUrl: outputUrl,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
@@ -1961,7 +1964,7 @@ function CanvasPage({
|
|||||||
url: outputUrl,
|
url: outputUrl,
|
||||||
mediaType: "video/mp4",
|
mediaType: "video/mp4",
|
||||||
resultType: "video",
|
resultType: "video",
|
||||||
taskId: task.id,
|
taskId,
|
||||||
originalUrl: outputUrl,
|
originalUrl: outputUrl,
|
||||||
});
|
});
|
||||||
await delay(420);
|
await delay(420);
|
||||||
@@ -1974,7 +1977,7 @@ function CanvasPage({
|
|||||||
videoUrl: assetRef.url,
|
videoUrl: assetRef.url,
|
||||||
assetRef,
|
assetRef,
|
||||||
taskRef: {
|
taskRef: {
|
||||||
taskId: task.id,
|
taskId,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
resultUrl: assetRef.url,
|
resultUrl: assetRef.url,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
@@ -1991,6 +1994,7 @@ function CanvasPage({
|
|||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
videoGenerationInFlightRef.current.delete(nodeId);
|
videoGenerationInFlightRef.current.delete(nodeId);
|
||||||
|
if (task?.id) removeCanvasGenKeepalive(task.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
CanvasVideoGenerationState,
|
CanvasVideoGenerationState,
|
||||||
CanvasVideoNode,
|
CanvasVideoNode,
|
||||||
} from "./canvasTypes";
|
} from "./canvasTypes";
|
||||||
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||||
import { createCanvasAssetRefFromGeneratedResult, persistCanvasGeneratedResultAsset } from "./canvasAssetPersistence";
|
import { createCanvasAssetRefFromGeneratedResult, persistCanvasGeneratedResultAsset } from "./canvasAssetPersistence";
|
||||||
import { waitForImageTaskResult, waitForVideoTaskResult } from "./canvasUtils";
|
import { waitForImageTaskResult, waitForVideoTaskResult } from "./canvasUtils";
|
||||||
|
|
||||||
@@ -41,6 +42,13 @@ export function removeCanvasGenKeepalive(taskId: string): void {
|
|||||||
saveCanvasGenKeepalive(loadCanvasGenKeepalive().filter((e) => e.taskId !== taskId));
|
saveCanvasGenKeepalive(loadCanvasGenKeepalive().filter((e) => e.taskId !== taskId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cancelCanvasGenKeepaliveOnUnload(): void {
|
||||||
|
const entries = loadCanvasGenKeepalive();
|
||||||
|
if (!entries.length) return;
|
||||||
|
entries.forEach((entry) => aiGenerationClient.cancelTaskOnUnload(entry.taskId));
|
||||||
|
saveCanvasGenKeepalive([]);
|
||||||
|
}
|
||||||
|
|
||||||
export interface UseCanvasGenerationParams {
|
export interface UseCanvasGenerationParams {
|
||||||
setImageNodes: Dispatch<SetStateAction<CanvasImageNode[]>>;
|
setImageNodes: Dispatch<SetStateAction<CanvasImageNode[]>>;
|
||||||
setVideoNodes: Dispatch<SetStateAction<CanvasVideoNode[]>>;
|
setVideoNodes: Dispatch<SetStateAction<CanvasVideoNode[]>>;
|
||||||
@@ -55,6 +63,7 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
|
|||||||
const [generationToast, setGenerationToast] = useState<string | null>(null);
|
const [generationToast, setGenerationToast] = useState<string | null>(null);
|
||||||
|
|
||||||
const imageGenerationInFlightRef = useRef(new Set<string>());
|
const imageGenerationInFlightRef = useRef(new Set<string>());
|
||||||
|
const videoGenerationInFlightRef = useRef(new Set<string>());
|
||||||
const textGenerationInFlightRef = useRef(new Set<string>());
|
const textGenerationInFlightRef = useRef(new Set<string>());
|
||||||
const textGenerationAbortControllersRef = useRef(new Map<string, AbortController>());
|
const textGenerationAbortControllersRef = useRef(new Map<string, AbortController>());
|
||||||
const canvasGenKeepaliveRestoredRef = useRef(false);
|
const canvasGenKeepaliveRestoredRef = useRef(false);
|
||||||
@@ -125,7 +134,7 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
|
|||||||
imageGenerationInFlightRef.current.delete(entry.nodeId);
|
imageGenerationInFlightRef.current.delete(entry.nodeId);
|
||||||
});
|
});
|
||||||
} else if (entry.nodeKind === "video") {
|
} else if (entry.nodeKind === "video") {
|
||||||
imageGenerationInFlightRef.current.add(entry.nodeId);
|
videoGenerationInFlightRef.current.add(entry.nodeId);
|
||||||
setVideoGenerationStatus(entry.nodeId, { status: "running", message: "正在恢复视频生成", progress: 20 });
|
setVideoGenerationStatus(entry.nodeId, { status: "running", message: "正在恢复视频生成", progress: 20 });
|
||||||
void waitForVideoTaskResult(entry.taskId, (status) => {
|
void waitForVideoTaskResult(entry.taskId, (status) => {
|
||||||
const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0)));
|
const progress = Math.max(18, Math.min(status.status === "completed" ? 100 : 96, Math.trunc(status.progress || 0)));
|
||||||
@@ -154,7 +163,7 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
|
|||||||
removeCanvasGenKeepalive(entry.taskId);
|
removeCanvasGenKeepalive(entry.taskId);
|
||||||
setVideoGenerationStatus(entry.nodeId, { status: "error", message: "视频生成失败" });
|
setVideoGenerationStatus(entry.nodeId, { status: "error", message: "视频生成失败" });
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
imageGenerationInFlightRef.current.delete(entry.nodeId);
|
videoGenerationInFlightRef.current.delete(entry.nodeId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,11 +174,36 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
|
|||||||
textGenerationAbortControllersRef.current.clear();
|
textGenerationAbortControllersRef.current.clear();
|
||||||
textGenerationInFlightRef.current.clear();
|
textGenerationInFlightRef.current.clear();
|
||||||
imageGenerationInFlightRef.current.clear();
|
imageGenerationInFlightRef.current.clear();
|
||||||
|
videoGenerationInFlightRef.current.clear();
|
||||||
setTextGenerationState({});
|
setTextGenerationState({});
|
||||||
setImageGenerationState({});
|
setImageGenerationState({});
|
||||||
setVideoGenerationState({});
|
setVideoGenerationState({});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePageHide = () => {
|
||||||
|
cancelCanvasGenKeepaliveOnUnload();
|
||||||
|
textGenerationAbortControllersRef.current.forEach((controller) => controller.abort());
|
||||||
|
textGenerationAbortControllersRef.current.clear();
|
||||||
|
textGenerationInFlightRef.current.clear();
|
||||||
|
imageGenerationInFlightRef.current.clear();
|
||||||
|
videoGenerationInFlightRef.current.clear();
|
||||||
|
setTextGenerationState({});
|
||||||
|
setImageGenerationState({});
|
||||||
|
setVideoGenerationState({});
|
||||||
|
};
|
||||||
|
const handleOnline = () => {
|
||||||
|
aiGenerationClient.flushPendingTaskCancellations();
|
||||||
|
};
|
||||||
|
window.addEventListener("pagehide", handlePageHide);
|
||||||
|
window.addEventListener("online", handleOnline);
|
||||||
|
aiGenerationClient.flushPendingTaskCancellations();
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pagehide", handlePageHide);
|
||||||
|
window.removeEventListener("online", handleOnline);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
textGenerationState,
|
textGenerationState,
|
||||||
imageGenerationState,
|
imageGenerationState,
|
||||||
@@ -177,6 +211,7 @@ export function useCanvasGeneration(params: UseCanvasGenerationParams) {
|
|||||||
generationToast,
|
generationToast,
|
||||||
setGenerationToast,
|
setGenerationToast,
|
||||||
imageGenerationInFlightRef,
|
imageGenerationInFlightRef,
|
||||||
|
videoGenerationInFlightRef,
|
||||||
textGenerationInFlightRef,
|
textGenerationInFlightRef,
|
||||||
textGenerationAbortControllersRef,
|
textGenerationAbortControllersRef,
|
||||||
canvasGenKeepaliveRestoredRef,
|
canvasGenKeepaliveRestoredRef,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -7,61 +7,213 @@ interface CompliancePageProps {
|
|||||||
kind: ComplianceKind;
|
kind: ComplianceKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
const companyName = "OmniAI";
|
const companyName = "南京万物可爱文化传媒有限公司";
|
||||||
|
const platformName = "Omniai平台";
|
||||||
|
const contactEmail = "system@omniai.net.cn";
|
||||||
const contactPhone = "15155073618";
|
const contactPhone = "15155073618";
|
||||||
const address = "江苏省南京市江北新区扬子江数字视听产业园9栋A楼501";
|
const address = "江苏省南京市江北新区扬子江数字视听产业园9栋A楼9A-501";
|
||||||
|
const icpRecord = "苏ICP备2026021747号-1";
|
||||||
|
const effectiveDate = "2026年06月8日";
|
||||||
|
|
||||||
const agreementSections = [
|
const privacyPolicyText = `
|
||||||
{
|
隐私政策
|
||||||
title: "服务范围",
|
更新日期:2026年06月8日
|
||||||
body: "平台提供 AI 图片、视频、脚本、数字人及相关创作辅助服务。具体功能、模型能力、消耗规则以页面展示和平台公告为准。",
|
生效日期:2026年06月8日
|
||||||
},
|
欢迎您使用本平台服务!
|
||||||
{
|
南京万物可爱文化传媒有限公司(以下简称“我们”或“平台”)是Omniai平台的运营者。我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与/或服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》(以下简称“本政策”)向您说明,在您使用我们的产品与/或服务时,我们如何收集、使用、保存、共享和转让您的个人信息,以及您所享有的个人信息权利。
|
||||||
title: "账号与使用",
|
本政策与您所使用的我们的产品与/或服务息息相关,请您务必仔细阅读并确认您已经充分理解本政策的内容。一旦您开始使用我们的产品与/或服务,即表示您已充分理解并同意本政策。
|
||||||
body: "用户应保证注册信息真实有效,妥善保管账号与登录凭证,不得出租、转让账号或以自动化方式恶意占用平台资源。",
|
本政策将帮助您了解以下内容:
|
||||||
},
|
一、本政策的适用范围
|
||||||
{
|
二、我们如何收集和使用您的个人信息
|
||||||
title: "内容合规",
|
三、我们如何使用Cookie和同类技术
|
||||||
body: "用户不得上传、生成、发布违法违规、侵权、涉政敏感、暴恐、色情、赌博、诈骗或侵犯他人合法权益的内容。平台有权对违规内容采取删除、限制功能、封禁账号等措施。",
|
四、我们如何共享、转让、公开披露您的个人信息
|
||||||
},
|
五、我们如何保护和存储您的个人信息
|
||||||
{
|
六、您如何管理您的个人信息
|
||||||
title: "积分与付费",
|
七、我们如何保护未成年人的个人信息
|
||||||
body: "积分仅限平台内消费,不支持提现、转让或折现。充值、套餐、赠送积分的有效期、消耗顺序和退费规则以充值页面展示为准。",
|
八、本政策如何更新
|
||||||
},
|
九、其他声明与责任限制
|
||||||
{
|
十、如何联系我们
|
||||||
title: "责任限制",
|
一、本政策的适用范围
|
||||||
body: "AI 生成结果可能存在偏差,用户应自行审核输出内容并承担使用后果。因不可抗力、第三方服务异常、网络故障造成的服务中断,平台将在合理范围内修复。",
|
1. 本政策适用于您通过我们的网站、客户端应用程序、小程序以及其他技术形态(包括但不限于SDK、API等方式)访问和使用我们的产品与/或服务。
|
||||||
},
|
2. 本政策不适用于以下情况:
|
||||||
];
|
(1)我们的产品与/或服务中包含的或链接至第三方提供的信息与/或服务(包括任何第三方应用、网站、产品、服务等)。这些服务由第三方负责运营,您使用该服务适用第三方另行向您说明的个人信息处理规则。
|
||||||
|
(2)其他非我们向您提供的产品与/或服务。
|
||||||
|
(3)特别提示:用户间交易。您通过平台社区与其他用户进行作品、素材、版权等交易时,您主动向交易对方提供的个人信息(如联系方式、交付地址等)不适用本政策。我们建议您在交易过程中谨慎保护您的个人信息,并充分了解交易对方的信用状况。
|
||||||
|
二、我们如何收集和使用您的个人信息
|
||||||
|
我们会遵循合法、正当、必要、诚信的原则,基于本政策所述的目的,收集和使用您的个人信息。如果我们将您的个人信息用于本政策未载明的其他用途,我们将以合理方式另行向您告知并征得您的同意。
|
||||||
|
(一)注册、登录与认证
|
||||||
|
1. 必要信息:当您注册平台账号时,您需要提供您的手机号码及验证码/登录密码。手机号码是履行国家法律法规对网络实名制(真实身份信息认证)要求所需的必要信息,若您不提供,您将无法完成注册,仅能使用浏览、搜索等基本功能。
|
||||||
|
2. 企业用户:如您以企业身份注册,您还需提供企业名称、营业执照、法定代表人/联系人信息等,以便我们核验您的企业身份,为您提供企业级服务。
|
||||||
|
3. 第三方账号登录:您可以使用我们认可的第三方账号(如微信、QQ等)进行登录。我们会收集您在该第三方账号下的唯一标识、昵称、头像等信息,用于创建您在本平台的账号。
|
||||||
|
4. 账号信息完善:您可以选择填写或修改您的昵称、头像、个人简介、所在地区等信息。此类信息非必要,但有助于提升您的社区交互体验。
|
||||||
|
(二)浏览、搜索与内容发布
|
||||||
|
1. 内容浏览:当您浏览平台上的模型、素材、作品、文章等内容时,我们会收集您的设备信息、日志信息,包括IP地址、浏览器类型、操作系统版本、访问时间、点击记录、浏览记录、下载记录等。此类信息用于为您提供内容展示、优化推荐算法以及保障服务安全稳定运行。
|
||||||
|
2. 搜索功能:当您使用搜索服务时,我们会收集您的搜索关键字信息,用于向您展示搜索结果。
|
||||||
|
3. 内容发布与社区交互:当您在平台社区上传模型、发布作品、发表评论、点赞、收藏、分享时,我们会收集您主动发布的内容(包括但不限于文字、图片、视频、模型文件等)。您发布的内容中会展示您的昵称、头像等信息。
|
||||||
|
(三)SaaS软件服务
|
||||||
|
1. 使用SaaS工具:当您使用我们提供的SaaS软件工具(如图片编辑、模型训练、设计工具等)时,您主动上传、输入或导入的内容及指令将被收集,以便为您提供服务。
|
||||||
|
2. 生成内容:您通过SaaS工具生成的内容,其所有权归您所有。我们会保存该生成内容的记录,以便您进行查看、下载、管理或再次编辑。
|
||||||
|
3. 模型优化:在经过安全加密技术处理、严格去标识化且无法重新识别特定个人的前提下,我们可能会将您使用SaaS软件过程中产生的脱敏数据用于模型优化和改进服务。您可以通过平台设置选择是否允许我们将您的内容用于此目的。
|
||||||
|
(四)付费订阅与交易服务
|
||||||
|
1. 会员订阅:当您购买会员服务(软件订阅费)或充值积分时,我们会收集您的订单信息(包括商品/服务名称、金额、交易时间)和支付信息。支付信息(如银行卡号、第三方支付账号)由第三方支付机构直接收集,我们不会获取您完整的支付敏感信息。
|
||||||
|
2. 积分消耗:当您使用积分消耗大模型算力或兑换服务时,我们会记录您的积分变动情况。
|
||||||
|
3. 用户间交易:
|
||||||
|
(1)当您作为卖方(发布作品/素材进行售卖)时,我们需要收集您的实名认证信息(个人或企业)、收款账户信息(银行卡号或第三方支付账号),以便向您结算交易款项。
|
||||||
|
(2)当您作为买方(购买作品/素材)时,我们需要收集您的订单信息,并可能向卖方提供您的平台账号信息,以便卖方完成交付。您的真实联系方式(如手机号、地址)不会直接提供给卖方,除非您主动通过平台沟通工具披露。
|
||||||
|
(五)运营与安全保障
|
||||||
|
为了维护相关产品或服务的正常稳定运行,保护您或其他用户或公众的安全及合法利益,我们会收集如下必要信息:
|
||||||
|
1. 设备信息:包括设备型号、操作系统版本、唯一设备标识符、IP地址、MAC地址、WLAN接入点、蓝牙、基站、软件版本号、网络接入方式/类型/状态等。
|
||||||
|
2. 日志信息:包括您的操作日志、服务日志,例如您对平台功能的点击、使用情况、崩溃数据、异常信息等。
|
||||||
|
3. 应用信息:用于预防恶意程序、保障运营质量,我们会收集安装的应用列表、软件列表或正在运行的进程信息。
|
||||||
|
4. IP归属地:根据相关法律法规要求,我们可能会在您的个人主页、作品发布等页面展示您的IP地址归属地信息。
|
||||||
|
(六)系统权限
|
||||||
|
为实现特定功能,我们可能会向您申请相机/摄像头、相册/存储、麦克风、通知、剪贴板等系统权限。您可以在设备设置中自主选择开启或关闭这些权限。关闭权限将可能导致对应功能无法使用,但不影响其他功能。
|
||||||
|
(七)征得授权同意的例外
|
||||||
|
根据相关法律法规,以下情形中收集您的个人信息无需征得您的授权同意:为订立、履行您作为一方当事人的合同所必需;为履行法定职责或者法定义务所必需;为应对突发公共卫生事件,或者紧急情况下为保护自然人的生命健康和财产安全所必需;为公共利益实施新闻报道、舆论监督等行为,在合理的范围内处理个人信息;依照法律规定在合理的范围内处理您自行公开或者其他已经合法公开的个人信息;法律、行政法规规定的其他情形。
|
||||||
|
三、我们如何使用Cookie和同类技术
|
||||||
|
1. Cookie:为使您获得更轻松的访问体验,我们可能会使用Cookie技术收集和存储您的登录状态、浏览偏好、使用习惯等信息。您可以根据自己的偏好管理或删除Cookie,但请注意,禁用Cookie可能会导致您无法正常使用平台的某些功能。
|
||||||
|
2. 同类技术:我们可能会使用网站信标、像素标签等其他同类技术,用于分析您对我们服务的使用情况、评估广告效果等。
|
||||||
|
四、我们如何共享、转让、公开披露您的个人信息
|
||||||
|
(一)共享
|
||||||
|
我们不会与任何公司、组织和个人共享您的个人信息,但获得您的明确同意、履行法定义务、与授权合作伙伴共享、用户间交易、关联公司共享等情况除外。授权合作伙伴包括支付服务提供商、实名认证服务商、云服务提供商、推送服务提供商、安全与风控服务商等,我们仅共享实现目的所必要的信息。
|
||||||
|
(二)转让
|
||||||
|
我们不会将您的个人信息转让给任何公司、组织和个人,但获得您的明确同意后,或在涉及合并、收购、破产清算等情形时除外。如涉及个人信息转让,我们会要求新的持有您个人信息的公司、组织继续受本政策约束,否则将要求其重新向您征求授权同意。
|
||||||
|
(三)公开披露
|
||||||
|
我们不会公开披露您的个人信息,但获得您的明确同意后、基于法律司法或行政程序要求、对违规账号或侵权行为进行必要公示等情况除外。
|
||||||
|
(四)共享、转让、公开披露的例外
|
||||||
|
在为订立或履行合同所必需、履行法定义务所必需、应对紧急情况、公共利益新闻报道或舆论监督、处理您自行公开或其他已合法公开信息、法律行政法规规定的其他情形中,共享、转让、公开披露您的个人信息无需事先征得您的授权同意。
|
||||||
|
五、我们如何保护和存储您的个人信息
|
||||||
|
1. 存储地点:我们在中华人民共和国境内运营中收集和产生的个人信息,将存储在中华人民共和国境内。我们不会将您的个人信息传输至境外。
|
||||||
|
2. 存储期限:我们仅在为实现本政策所述目的所必需的最短期限内保留您的个人信息,除非法律法规有更强的存留要求。超出存储期限后,我们将对您的个人信息进行删除或匿名化处理。
|
||||||
|
3. 保护措施:我们采用SSL加密、数据脱敏、访问控制、入侵检测等符合行业标准的安全技术措施,保护您的个人信息免遭泄露、篡改、毁损或未经授权的访问。我们建立数据安全管理制度,与员工签署保密协议,定期开展安全审计。一旦发生个人信息安全事件,我们将依法及时告知您,并向监管部门报告。
|
||||||
|
六、您如何管理您的个人信息
|
||||||
|
您可以通过平台账号功能或联系我们,访问、更正、补充、删除您的个人信息,撤回同意或申请注销账号。账号注销后,我们将删除或匿名化处理您的个人信息(法律法规另有规定的除外),您将无法再使用该账号登录平台。对于合理的请求,我们原则上不收取费用;对于无端重复、超出合理限度的请求,我们可能会拒绝或收取合理成本费用。
|
||||||
|
七、我们如何保护未成年人的个人信息
|
||||||
|
1. 平台主要面向成年人。如您为18周岁以下的未成年人,应在您的父母或其他监护人监护、指导下共同阅读并同意本政策。
|
||||||
|
2. 如您为14周岁以下的儿童,请务必在监护人明确同意的前提下使用我们的服务。我们将只会在法律法规允许、监护人明确同意或保护儿童所必要的情况下收集、使用儿童的个人信息。
|
||||||
|
3. 如我们发现未经监护人同意收集了儿童个人信息,我们将尽快删除相关数据。
|
||||||
|
八、本政策如何更新
|
||||||
|
我们可能会根据法律法规变化、产品功能调整或业务发展需要适时修订本政策。本政策更新后,我们会在平台显著位置发布更新版本,并在生效前通过公告、站内信、弹窗等方式通知您。如您继续使用我们的服务,即表示您同意受修订后的本政策约束。
|
||||||
|
九、其他声明与责任限制
|
||||||
|
为提供更好的服务,我们的产品中可能包含第三方SDK。第三方SDK可能会收集您的设备信息、网络信息等,具体请查阅我们在平台公示的《第三方信息共享清单》。本政策适用中华人民共和国大陆地区法律。因本政策引起的任何争议,双方应友好协商解决;协商不成的,任何一方均有权将争议提交至本平台运营主体所在地有管辖权的人民法院诉讼解决。
|
||||||
|
十、如何联系我们
|
||||||
|
如您对本政策有任何疑问、意见或建议,或需要进行投诉、举报,您可以通过以下方式与我们联系:
|
||||||
|
客服邮箱:system@omniai.net.cn
|
||||||
|
客服电话:15155073618
|
||||||
|
邮寄地址:江苏省南京市江北新区扬子江数字视听产业园9栋A楼9A-501
|
||||||
|
我们将在15个工作日内回复您的请求。
|
||||||
|
`.trim();
|
||||||
|
|
||||||
const privacySections = [
|
const agreementText = `
|
||||||
{
|
Omniai平台用户协议
|
||||||
title: "收集的信息",
|
更新日期:2026年06月8日
|
||||||
body: "我们会收集账号信息、登录状态、联系方式、创作输入、生成结果、用量记录、设备与网络日志,用于提供服务、安全审计和问题排查。",
|
生效日期:2026年06月8日
|
||||||
},
|
欢迎您使用本平台服务。
|
||||||
{
|
本平台向您提供SaaS软件服务、社区交流、素材及作品交易等相关服务。欢迎您与南京万物可爱文化传媒有限公司(以下简称“我们”或“平台”)共同签署本《用户服务协议》(下称“本协议”),并使用Omniai平台服务!
|
||||||
title: "Cookie 与本地存储",
|
在您注册成为平台用户前,请您务必审慎阅读、充分理解本协议的全部内容,特别是免除或限制责任、法律适用和争议解决条款。如您对本协议内容有任何疑问,可通过平台客服进行咨询。如您未满18周岁,请在法定监护人陪同下仔细阅读并充分理解本协议,并征得监护人的同意后使用本平台服务。
|
||||||
body: "我们使用 Cookie、localStorage 和 sessionStorage 保存登录状态、偏好设置、Cookie 同意状态、创作草稿和断点续传数据。",
|
当您点击“同意”本协议、完成注册程序,或实际开始使用平台服务时,即表示您已充分阅读、理解并接受本协议的全部内容,并与我们达成一致,本协议即对您产生法律约束力。如您不同意本协议的任何条款,请立即停止注册或使用行为。
|
||||||
},
|
我们可能不时修改本协议及相关平台规则,并通过网站公告、站内信等方式进行通知。若您在本协议修改后继续使用服务,即视为您已接受修改后的协议。
|
||||||
{
|
第一章 定义
|
||||||
title: "信息使用",
|
1. 本平台:指由南京万物可爱文化传媒有限公司拥有并运营的,向用户提供SaaS软件工具、社区互动、内容上传、展示、分享、付费下载及交易等功能的网站、客户端应用程序及其他技术服务平台。
|
||||||
body: "信息用于身份验证、生成任务处理、资产管理、积分计费、客服支持、风控合规、服务优化和法律法规要求的备案审计。",
|
2. SaaS服务:指我们基于软件即服务模式,向您提供的在线软件工具及相关技术服务,您可能需要支付软件订阅费(会员费)后方可使用全部或部分功能。
|
||||||
},
|
3. 社区服务:指我们在平台上为用户提供的,用于发布、展示、交流、分享及交易作品、素材、版权等内容的空间与功能。
|
||||||
{
|
4. 平台规则:指我们已经发布或后续可能发布、修改的与本平台相关的所有协议、政策、活动规则、公告、说明、站内信通知等,以及《隐私政策》等,均构成本协议不可分割的组成部分。
|
||||||
title: "第三方处理",
|
5. 用户:指使用本平台服务的任何自然人或组织,包括企业用户与个人用户,合称“您”。
|
||||||
body: "为完成 AI 生成、对象存储、短信邮件、支付或错误监控,我们可能向必要的第三方服务提供最小范围数据,并要求其按约定保护数据安全。",
|
6. 内容:指用户通过本平台上传、发布、生成、展示、交易的全部信息,包括但不限于模型、软件、素材、图片、视频、音频、文字、代码、设计图、评论等。
|
||||||
},
|
第二章 账号的注册、使用与管理
|
||||||
{
|
1. 账号注册
|
||||||
title: "用户权利",
|
(1)您在使用本平台服务前,需通过实名手机号或我们认可的第三方账号进行注册。企业用户还应提供真实、有效的企业营业执照等信息。您在注册或使用Omniai平台服务时可能需要提供一些必要的信息,为保证您享用的平台服务安全有效且不断优化,您同意授权我们对您的必要个人信息进行验证和合理使用。您须保证所填写及提供的资料真实、准确、完整、合法有效。
|
||||||
body: "你可以通过平台账号功能或联系方式申请访问、更正、删除个人信息,或撤回非必要授权。法律法规另有要求的记录可能需按规定保留。",
|
(2)您注册成功的账号仅限您本人/本企业自身正当使用,禁止以任何形式赠与、借用、出租、转让、售卖或授权他人使用该账号。
|
||||||
},
|
(3)特别提示:禁止恶意注册。您不得恶意批量注册账号,不得利用多个账号或其他技术手段实施干扰平台运营、规避平台规则、获取不当利益的行为。一经发现,我们有权立即冻结或收回相关账号,并追究您的违约责任。
|
||||||
];
|
(4)如您提供任何违法、不实或我们认为不适合的资料,或我们有理由怀疑您的行为属于恶意操作,我们有权暂停或终止您的账号。
|
||||||
|
(5)我们及Omniai平台无须对任何用户的任何登记资料承担任何责任,包括但不限于鉴别、核实任何登记资料的真实性、正确性、完整性、适用性及是否为最新资料的责任。
|
||||||
|
2. 账号安全
|
||||||
|
(1)您应对账号及密码/验证码的安全性负完全责任,并对该账号下进行的所有活动承担责任。您应高度重视对账号与验证码的保密,在任何情况下不向他人透露账号及验证码。您的账号遭到未获授权的使用,或者发生其它任何安全问题时,您应立即通知我们。
|
||||||
|
(2)我们发现或合理认为使用者并非账号初始注册人,为保障账号安全,有权立即暂停或终止提供服务,并永久禁用该账号。
|
||||||
|
3. 账号注销与回收
|
||||||
|
(1)您可以按照平台公示的方式申请注销账号。账号注销后,我们将对账号内信息进行删除或匿名化处理,法律法规另有规定的除外。
|
||||||
|
(2)如您的账号连续六个月以上未登录,我们有权冻结、收回或者注销该账号。
|
||||||
|
第三章 平台服务与使用规范
|
||||||
|
1. 服务内容
|
||||||
|
(1)SaaS软件服务:我们向您提供在线SaaS软件工具,您可按需选择免费版或付费版(会员费)。使用部分高级功能或消耗大模型算力时,可能需要消耗积分,积分可通过充值等方式获得。
|
||||||
|
(2)社区服务:平台提供作品、素材、版权的上传、展示、付费下载、互动交流等服务。用户间可通过平台进行相关版权或素材的交易。
|
||||||
|
(3)交易手续费:为维持平台运营,对于用户间通过平台达成的交易,我们可能会向卖方或买方收取一定比例的交易手续费,具体费率以平台页面公示为准。
|
||||||
|
2. 用户行为规范:严禁侵权与违法内容
|
||||||
|
(1)您承诺,您在使用平台SaaS软件创作、或上传、发布、交易的所有内容,均由您原创或已获得合法、完整的授权,不存在侵犯任何第三方知识产权、肖像权、名誉权、隐私权等合法权益的情形。
|
||||||
|
(2)高风险警示:您不得使用本平台创作、上传、发布、传播任何违反中华人民共和国法律法规、社会主义制度、国家利益、社会公序良俗或包含淫秽、色情、暴力、赌博、恐怖、侮辱、诽谤、虚假信息、扰乱社会秩序等任何不良内容的信息或作品。
|
||||||
|
(3)您不得利用本平台进行任何危害网络安全的行为,包括但不限于使用插件、外挂、爬虫、病毒、反向工程、干扰系统正常运行等。
|
||||||
|
(4)您不得利用本平台实施洗钱、套现、诈骗、赌博等违法活动。
|
||||||
|
3. 禁止恶意“薅羊毛”与不正当竞争
|
||||||
|
您不得通过批量注册账号、使用外挂程序、虚构交易、虚假评价、套取平台补贴或奖励等方式,获取不当商业利益或损害平台及其他用户的权益。一经发现,平台有权扣划不当得利、限制或封禁账号、追究违约责任。
|
||||||
|
AI内容生成特别提示:您理解并同意,本平台提供的SaaS软件中可能包含基于人工智能技术的内容生成功能。鉴于AI技术的局限性,生成内容可能存在不确定性或不准确性。您应自行对生成内容进行审核和判断,并对其使用承担全部责任。我们不对生成内容的合法性、准确性、完整性、不侵权性作任何保证。
|
||||||
|
第四章 知识产权
|
||||||
|
1. 平台的知识产权
|
||||||
|
本平台软件、代码、界面设计、商标、标识等知识产权归我们所有,未经书面许可,您不得进行复制、修改、反向编译或用于任何商业目的。
|
||||||
|
2. 您的内容的知识产权
|
||||||
|
(1)权利归属:您通过本平台上传、发布的内容及使用SaaS软件独立创作生成的内容,其知识产权归您或原权利人所有。您对您的内容承担全部责任。
|
||||||
|
(2)授权许可:为了使您的内容能够在平台上进行展示、传播、交易,并为了我们能够持续改进SaaS软件和社区服务,您授予我们一项全球范围内、免费的、非排他性的、可分许可的权利,允许我们在平台及相关业务中使用、复制、修改、改编、分发您公开发布的内容。如果您不希望我们使用您的内容用于模型训练或改进,您可以通过平台设置或联系客服关闭此选项。对于您设置为非公开或仅用于交易的内容,我们将采取更严格的保护措施。
|
||||||
|
(3)交易授权:您通过平台社区进行作品、素材、版权的付费下载或交易,即授予购买方一项按照交易约定范围使用该内容的许可。您作为卖方,应清晰标识授权范围,并对授权内容的真实性和合法性承担全部责任。
|
||||||
|
3. 侵权内容处理
|
||||||
|
我们尊重他人知识产权,并已建立侵权投诉处理机制。任何第三方认为平台上的内容侵犯其合法权益的,可按照平台公示的投诉渠道提交书面通知及初步侵权证据。我们将在收到合格通知后,依法采取删除、屏蔽、断开链接等必要措施,并通知相关用户。对于重复侵权、恶意侵权或情节严重的用户,我们有权直接终止向其提供服务,并永久封禁其账号。
|
||||||
|
第五章 社区交易与责任限制
|
||||||
|
1. 交易主体与责任承担
|
||||||
|
(1)您充分理解并同意,本平台仅为用户提供作品、素材、版权的信息发布、展示、下载、支付结算等交易技术支持服务。平台不是交易合同的任何一方当事人,也并非作品的卖方或买方。
|
||||||
|
(2)核心免责声明:平台对于用户发布、展示、交易的内容的合法性、真实性、准确性、完整性、安全性、质量、是否侵权以及交易的履行等,不承担任何事先审查或担保责任。您与其他用户之间因内容交易产生的任何纠纷,均由交易双方自行协商解决,或通过司法、行政途径解决。
|
||||||
|
(3)如我们收到司法机关或行政机关的有效法律文书,或认为确有必要时,我们可以采取临时性措施以维护各方权益。
|
||||||
|
2. 社区内容的管理权利
|
||||||
|
我们有权依据法律法规、本协议及平台规则,对平台社区内的内容进行主动巡查。如发现或收到举报证明内容涉嫌侵权、违法或违反本协议,我们有权在不事先通知的情况下,直接删除、屏蔽相关内容,并对相关用户采取警示、限制功能、暂停服务或封禁账号等措施。
|
||||||
|
第六章 费用与支付
|
||||||
|
1. 软件订阅费(会员费):您购买会员服务时,应按照平台页面公示的价格和期限支付费用。会员服务为虚拟产品,一经开通,除法律另有规定或平台规则另有说明外,原则上不予退款。
|
||||||
|
2. 积分充值:积分用于消耗平台算力或兑换特定服务。充值后积分有效期以平台公示为准。平台有权根据运营情况调整积分获取或消耗规则,但会提前公告。
|
||||||
|
3. 交易手续费:平台有权就用户间通过平台达成的交易收取手续费,费率在交易前明确告知。平台有权根据市场情况调整手续费率,通过公告方式通知后生效。
|
||||||
|
4. 税费:您使用本平台所获得的收入(如售卖素材所得),根据中国法律规定,您应自行申报并缴纳相关税费。平台按照法律规定履行代扣代缴义务(如适用)。
|
||||||
|
第七章 违约与处理
|
||||||
|
1. 违约认定:发生违反本协议或平台规则、侵犯第三方权益、恶意批量注册、虚假交易、套取利益或其他违反法律法规并造成损害的行为,视为您违约。
|
||||||
|
2. 处理措施:我们有权独立判断并采取警告、拒绝发布、删除内容、限制使用功能、暂停服务、扣划不当得利、冻结或永久封禁账号、追究赔偿等一项或多项措施。
|
||||||
|
3. 赔偿责任:如因您的行为导致我们或第三方遭受任何损失,您应足额赔偿。
|
||||||
|
第八章 免责声明与责任限制
|
||||||
|
1. 服务现状:我们的服务按“现状”和“可得到”的状态提供,我们不作出任何明示或暗示的保证,包括但不限于服务无间断、无错误、安全可靠、适用于特定目的等。
|
||||||
|
2. AI生成内容免责:鉴于人工智能技术的局限性,我们无法保证通过SaaS软件生成内容的真实性、准确性、独创性及不侵权性。您应当对生成内容自行加以判断和验证,并承担使用该等内容所产生的全部风险与责任。
|
||||||
|
3. 不可抗力及第三方原因:因自然灾害、战争、政府行为、电力或通讯故障、第三方攻击等不可抗力或非我们故意或重大过失的原因导致的损失,我们不承担责任。
|
||||||
|
4. 服务中断与变更:我们可能会对平台进行升级、维护或修改,由此导致的服务中断或功能变更,我们将提前通过合理方式通知您。但我们不对因上述必要维护导致的任何损失承担责任,法律另有强制性规定的除外。
|
||||||
|
5. 第三方链接与内容:平台可能包含指向第三方网站或资源的链接。我们不对这些第三方网站或资源的可用性、内容、产品或服务承担任何责任。您访问第三方网站或使用第三方服务所产生的一切风险由您自行承担。
|
||||||
|
6. 责任上限:在法律允许的最大范围内,我们对于您因使用平台服务而遭受的任何间接、附带、特殊、惩罚性损失,即使已被告知可能发生该等损失,也不承担任何责任。我们的全部赔偿责任总额,不超过您在过去十二个月内向平台支付的费用总额。
|
||||||
|
第九章 个人信息保护
|
||||||
|
1. 我们非常重视您的个人信息保护。我们将按照平台公布的《隐私政策》收集、使用、存储、共享和保护您的个人信息。请您务必仔细阅读《隐私政策》。
|
||||||
|
2. 您同意我们根据《隐私政策》以及相关法律法规的要求处理您的个人信息。
|
||||||
|
3. 您知悉并同意,为方便您使用Omniai相关服务,我们有权在遵守法律法规的前提下,以明示的方式获取、使用、储存和分享您的个人信息。我们不会在未经您授权时,公开、编辑或透露您的个人信息及您保存在Omniai的非公开内容。
|
||||||
|
4. 您知悉并同意,我们有权通过cookie等技术收集您的产品或服务使用、行为数据,并在经过数据脱敏使之不再指向或关联到您个人身份信息时,自由使用脱敏后的纯商业数据。
|
||||||
|
第十章 协议的修改与终止
|
||||||
|
1. 协议的修改:我们有权根据法律法规变化或业务需要修改本协议。修改后的协议将在平台公示或以其他方式通知您。如您不接受修改后的协议,您应停止使用平台服务;如您继续使用,则视为同意接受修改后的协议。
|
||||||
|
2. 协议的终止:您可以申请注销账号终止本协议。我们有权在您违约或出现其他法定、约定情形时终止本协议。协议终止后,您仍需对协议终止前的行为承担责任。
|
||||||
|
第十一章 法律适用与争议解决
|
||||||
|
1. 本协议的订立、生效、履行、解释及争议解决,均适用中华人民共和国大陆地区法律。
|
||||||
|
2. 凡因本协议引起的或与本协议有关的任何争议,双方应首先友好协商解决;协商不成的,任何一方均有权将争议提交至本平台运营主体所在地南京市浦口区有管辖权的人民法院通过诉讼解决。
|
||||||
|
第十二章 其他
|
||||||
|
1. 本协议条款标题仅为方便阅读而设,不影响条款含义的解释。
|
||||||
|
2. 本协议任何条款被认定为无效或不可执行,不影响其他条款的效力。
|
||||||
|
3. 如您对本协议有任何疑问、意见或建议,或需要投诉举报,您可以通过system@omniai.net.cn与我们联系。
|
||||||
|
4. 兜底免责声明:在法律法规允许的最大范围内,本协议未明确列明的、或因超出我们合理预见或控制范围所产生的任何直接或间接损失、责任或风险,我们均不承担责任,除非相关法律另有强制性规定。您理解并同意,使用本平台服务的风险由您自行承担,我们仅以普通注意义务对平台进行管理,不对任何未明确约定的商业成果或安全性作出保证。
|
||||||
|
(以下无正文)
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
function getDocumentLines(text: string) {
|
||||||
|
return text.split(/\n+/).map((line) => line.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLineClassName(line: string, index: number) {
|
||||||
|
if (index === 0) return "compliance-document__title";
|
||||||
|
if (/^(第[一二三四五六七八九十]+章|[一二三四五六七八九十]+、)/.test(line)) return "compliance-document__heading";
|
||||||
|
if (/^([一二三四五六七八九十]+)/.test(line)) return "compliance-document__subheading";
|
||||||
|
if (/^[0-9]+\./.test(line) || /^([0-9]+)/.test(line) || /^·/.test(line)) return "compliance-document__clause";
|
||||||
|
return "compliance-document__paragraph";
|
||||||
|
}
|
||||||
|
|
||||||
export default function CompliancePage({ kind }: CompliancePageProps) {
|
export default function CompliancePage({ kind }: CompliancePageProps) {
|
||||||
const isPrivacy = kind === "privacy";
|
const isPrivacy = kind === "privacy";
|
||||||
const sections = isPrivacy ? privacySections : agreementSections;
|
|
||||||
const title = isPrivacy ? "隐私政策" : "用户协议";
|
const title = isPrivacy ? "隐私政策" : "用户协议";
|
||||||
const Icon = isPrivacy ? SafetyOutlined : FileTextOutlined;
|
const Icon = isPrivacy ? SafetyOutlined : FileTextOutlined;
|
||||||
|
const lines = getDocumentLines(isPrivacy ? privacyPolicyText : agreementText);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="compliance-page">
|
<section className="compliance-page">
|
||||||
@@ -71,27 +223,26 @@ export default function CompliancePage({ kind }: CompliancePageProps) {
|
|||||||
<div>
|
<div>
|
||||||
<span className="compliance-hero__eyebrow">合规文件</span>
|
<span className="compliance-hero__eyebrow">合规文件</span>
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
<p>{companyName} 平台服务合规说明。更新日期:2026 年 6 月 3 日。</p>
|
<p>{companyName}({platformName})服务合规说明。生效日期:{effectiveDate}</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="compliance-card">
|
<article className="compliance-card compliance-document">
|
||||||
{sections.map((section, index) => (
|
{lines.map((line, index) => {
|
||||||
<article key={section.title} className="compliance-section">
|
const className = getLineClassName(line, index);
|
||||||
<span>{String(index + 1).padStart(2, "0")}</span>
|
if (className === "compliance-document__title") return <h2 key={`${index}-${line}`} className={className}>{line}</h2>;
|
||||||
<div>
|
if (className === "compliance-document__heading") return <h3 key={`${index}-${line}`} className={className}>{line}</h3>;
|
||||||
<h2>{section.title}</h2>
|
if (className === "compliance-document__subheading") return <h4 key={`${index}-${line}`} className={className}>{line}</h4>;
|
||||||
<p>{section.body}</p>
|
return <p key={`${index}-${line}`} className={className}>{line}</p>;
|
||||||
</div>
|
})}
|
||||||
</article>
|
</article>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer className="compliance-contact">
|
<footer className="compliance-contact">
|
||||||
<strong>联系我们</strong>
|
<strong>联系我们</strong>
|
||||||
|
<span>邮箱:{contactEmail}</span>
|
||||||
<span>地址:{address}</span>
|
<span>地址:{address}</span>
|
||||||
<span>电话:{contactPhone}</span>
|
<span>电话:{contactPhone}</span>
|
||||||
<span>备案号:苏ICP备2026021747号-1</span>
|
<span>备案号:{icpRecord}</span>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
import "../../styles/pages/workbench.css";
|
import "../../styles/pages/workbench.css";
|
||||||
import type { WebGenerationPreviewTask, WebUserSession } from "../../types";
|
import type { WebGenerationPreviewTask, WebUserSession } from "../../types";
|
||||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||||
import { claimGenerationSlot, getActiveGenerationTaskCount, getGenerationUserKey, releaseGenerationSlot } from "../../api/generationConcurrency";
|
import { claimGenerationSlot, getActiveGenerationTaskCount, getEffectiveGenerationLimit, getGenerationUserKey, releaseGenerationSlot } from "../../api/generationConcurrency";
|
||||||
import { preUploadReference, resolvePreUploadedUrl } from "../../api/referenceUploadService";
|
import { preUploadReference, resolvePreUploadedUrl } from "../../api/referenceUploadService";
|
||||||
import { assetClient } from "../../api/assetClient";
|
import { assetClient } from "../../api/assetClient";
|
||||||
import { communityClient } from "../../api/communityClient";
|
import { communityClient } from "../../api/communityClient";
|
||||||
@@ -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,
|
||||||
@@ -201,6 +202,7 @@ interface WorkbenchPageProps {
|
|||||||
onRequireLogin: (input: CreatePreviewTaskInput) => void;
|
onRequireLogin: (input: CreatePreviewTaskInput) => void;
|
||||||
onOpenResultInCanvas?: (payload: import("./workbenchConstants").WorkbenchResultActionPayload) => void;
|
onOpenResultInCanvas?: (payload: import("./workbenchConstants").WorkbenchResultActionPayload) => void;
|
||||||
onRefreshUsage?: () => void;
|
onRefreshUsage?: () => void;
|
||||||
|
resetToken?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Component ───────────────────────────────────────────────────────────
|
// ─── Component ───────────────────────────────────────────────────────────
|
||||||
@@ -220,12 +222,19 @@ 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,
|
||||||
onRequireLogin,
|
onRequireLogin,
|
||||||
onOpenResultInCanvas,
|
onOpenResultInCanvas,
|
||||||
onRefreshUsage,
|
onRefreshUsage,
|
||||||
|
resetToken,
|
||||||
}: WorkbenchPageProps) {
|
}: WorkbenchPageProps) {
|
||||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const referenceInputRef = useRef<HTMLInputElement | null>(null);
|
const referenceInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
@@ -244,10 +253,11 @@ function WorkbenchPage({
|
|||||||
const activeConversationIdRef = useRef<number | null>(null);
|
const activeConversationIdRef = useRef<number | null>(null);
|
||||||
const messagesRef = useRef<ChatMessage[]>([]);
|
const messagesRef = useRef<ChatMessage[]>([]);
|
||||||
const conversationMessagesCacheRef = useRef<Map<number, ChatMessage[]>>(new Map());
|
const conversationMessagesCacheRef = useRef<Map<number, ChatMessage[]>>(new Map());
|
||||||
const skipConversationAutoSelectRef = useRef(false);
|
const skipConversationAutoSelectRef = useRef(Boolean(resetToken));
|
||||||
const keepaliveTasksRef = useRef<Record<string, WorkbenchKeepaliveTask>>(readStoredKeepaliveTasks());
|
const keepaliveTasksRef = useRef<Record<string, WorkbenchKeepaliveTask>>(readStoredKeepaliveTasks());
|
||||||
const taskAbortControllersRef = useRef<Map<string, AbortController>>(new Map());
|
const taskAbortControllersRef = useRef<Map<string, AbortController>>(new Map());
|
||||||
const lastScrollTopRef = useRef(0);
|
const lastScrollTopRef = useRef(0);
|
||||||
|
const scrollActionHintTimerRef = useRef<number | null>(null);
|
||||||
const shouldFollowNewMessagesRef = useRef(true);
|
const shouldFollowNewMessagesRef = useRef(true);
|
||||||
const pendingScrollToLatestRef = useRef(true);
|
const pendingScrollToLatestRef = useRef(true);
|
||||||
const genTracker = useGenerationTasks({ sourceView: "workbench" });
|
const genTracker = useGenerationTasks({ sourceView: "workbench" });
|
||||||
@@ -256,7 +266,7 @@ function WorkbenchPage({
|
|||||||
|
|
||||||
const [activeMode, setActiveMode] = useState<WorkbenchMode>("video");
|
const [activeMode, setActiveMode] = useState<WorkbenchMode>("video");
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>(() => readStoredMessages());
|
const [messages, setMessages] = useState<ChatMessage[]>(() => (resetToken ? [] : readStoredMessages()));
|
||||||
const [promptHistory, setPromptHistory] = useState<string[]>(() => readStoredPromptHistory());
|
const [promptHistory, setPromptHistory] = useState<string[]>(() => readStoredPromptHistory());
|
||||||
const [toolbarMenuId, setToolbarMenuId] = useState<ToolbarMenuId>(null);
|
const [toolbarMenuId, setToolbarMenuId] = useState<ToolbarMenuId>(null);
|
||||||
const [referenceItems, setReferenceItems] = useState<ReferenceItem[]>([]);
|
const [referenceItems, setReferenceItems] = useState<ReferenceItem[]>([]);
|
||||||
@@ -279,7 +289,7 @@ function WorkbenchPage({
|
|||||||
const [projectError, setProjectError] = useState<string | null>(null);
|
const [projectError, setProjectError] = useState<string | null>(null);
|
||||||
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
|
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
|
||||||
const [activeConversationId, setActiveConversationId] = useState<number | null>(() =>
|
const [activeConversationId, setActiveConversationId] = useState<number | null>(() =>
|
||||||
readStoredActiveConversationId(readStoredMessages()),
|
resetToken ? null : readStoredActiveConversationId(readStoredMessages()),
|
||||||
);
|
);
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
||||||
const [deleteDialog, setDeleteDialog] = useState<DeleteDialogState | null>(null);
|
const [deleteDialog, setDeleteDialog] = useState<DeleteDialogState | null>(null);
|
||||||
@@ -289,7 +299,9 @@ function WorkbenchPage({
|
|||||||
const [promptSelectionRange, setPromptSelectionRange] = useState({ start: 0, end: 0 });
|
const [promptSelectionRange, setPromptSelectionRange] = useState({ start: 0, end: 0 });
|
||||||
const [mentionActiveIndex, setMentionActiveIndex] = useState(0);
|
const [mentionActiveIndex, setMentionActiveIndex] = useState(0);
|
||||||
const [composerHidden, setComposerHidden] = useState(false);
|
const [composerHidden, setComposerHidden] = useState(false);
|
||||||
|
const [scrollActionHint, setScrollActionHint] = useState<"top" | "bottom" | null>(null);
|
||||||
const [workspaceStarted, setWorkspaceStarted] = useState(false);
|
const [workspaceStarted, setWorkspaceStarted] = useState(false);
|
||||||
|
const lastResetTokenRef = useRef(resetToken);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
activeConversationIdRef.current = activeConversationId;
|
activeConversationIdRef.current = activeConversationId;
|
||||||
@@ -415,7 +427,6 @@ function WorkbenchPage({
|
|||||||
const toolTheme = MODE_META[activeMode];
|
const toolTheme = MODE_META[activeMode];
|
||||||
const workbenchAccent = "#00ff88";
|
const workbenchAccent = "#00ff88";
|
||||||
const hasConversationRecords = activeConversationId !== null || messages.length > 0;
|
const hasConversationRecords = activeConversationId !== null || messages.length > 0;
|
||||||
const hasActivatedWorkspace = workspaceStarted || isGenerating || hasConversationRecords;
|
|
||||||
const referenceCount = referenceItems.length;
|
const referenceCount = referenceItems.length;
|
||||||
const activeVideoModelValue = toHappyHorseDisplayModel(videoModel);
|
const activeVideoModelValue = toHappyHorseDisplayModel(videoModel);
|
||||||
const activeModelValue =
|
const activeModelValue =
|
||||||
@@ -443,6 +454,7 @@ function WorkbenchPage({
|
|||||||
[conversations],
|
[conversations],
|
||||||
);
|
);
|
||||||
const hasSidebarRecords = conversationRecords.length > 0;
|
const hasSidebarRecords = conversationRecords.length > 0;
|
||||||
|
const hasActivatedWorkspace = workspaceStarted || isGenerating || hasConversationRecords;
|
||||||
|
|
||||||
const activeConversationTitle = useMemo(() => {
|
const activeConversationTitle = useMemo(() => {
|
||||||
if (!activeConversationId) return "";
|
if (!activeConversationId) return "";
|
||||||
@@ -459,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";
|
||||||
@@ -496,6 +569,31 @@ function WorkbenchPage({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const hideScrollActionHint = useCallback(() => {
|
||||||
|
if (scrollActionHintTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(scrollActionHintTimerRef.current);
|
||||||
|
scrollActionHintTimerRef.current = null;
|
||||||
|
}
|
||||||
|
setScrollActionHint(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showScrollActionHint = useCallback((direction: "top" | "bottom") => {
|
||||||
|
if (scrollActionHintTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(scrollActionHintTimerRef.current);
|
||||||
|
}
|
||||||
|
setScrollActionHint(direction);
|
||||||
|
scrollActionHintTimerRef.current = window.setTimeout(() => {
|
||||||
|
setScrollActionHint(null);
|
||||||
|
scrollActionHintTimerRef.current = null;
|
||||||
|
}, 1400);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
if (scrollActionHintTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(scrollActionHintTimerRef.current);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const imageSettingGroups = useMemo<WorkbenchFieldGroup[]>(
|
const imageSettingGroups = useMemo<WorkbenchFieldGroup[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -890,6 +988,54 @@ function WorkbenchPage({
|
|||||||
persistKeepaliveTasks(rest);
|
persistKeepaliveTasks(rest);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const releaseKeepaliveTaskLocally = useCallback((taskId: string, options?: { cancelServer?: boolean }) => {
|
||||||
|
const task = keepaliveTasksRef.current[taskId];
|
||||||
|
taskAbortControllersRef.current.get(taskId)?.abort();
|
||||||
|
taskAbortControllersRef.current.delete(taskId);
|
||||||
|
removeKeepaliveTask(taskId);
|
||||||
|
if (task && options?.cancelServer) {
|
||||||
|
aiGenerationClient.cancelTask(task.taskId).catch(() => {});
|
||||||
|
}
|
||||||
|
syncActiveGenerationUi();
|
||||||
|
}, [syncActiveGenerationUi]);
|
||||||
|
|
||||||
|
const releaseKeepaliveTaskAfterNetworkLoss = useCallback((task: WorkbenchKeepaliveTask, progress: number) => {
|
||||||
|
const latestTask = {
|
||||||
|
...task,
|
||||||
|
progress,
|
||||||
|
statusLabel: "网络中断,已释放提交按钮",
|
||||||
|
};
|
||||||
|
void patchConversationMessage(task.conversationId, task.assistantMessageId, {
|
||||||
|
status: "failed",
|
||||||
|
taskProgress: Math.max(progress, 100),
|
||||||
|
taskStatusLabel: "网络中断",
|
||||||
|
body: "网络中断,当前任务已停止等待并释放提交按钮。请确认网络恢复后重新提交任务。",
|
||||||
|
});
|
||||||
|
upsertKeepaliveTask(latestTask);
|
||||||
|
releaseKeepaliveTaskLocally(task.taskId, { cancelServer: true });
|
||||||
|
if (activeConversationIdRef.current === task.conversationId) {
|
||||||
|
setIsGenerating(false);
|
||||||
|
setGenerationStatus("网络中断,已释放提交按钮");
|
||||||
|
setGenerationProgress(0);
|
||||||
|
}
|
||||||
|
}, [patchConversationMessage, releaseKeepaliveTaskLocally]);
|
||||||
|
|
||||||
|
const cancelActiveKeepaliveTasksOnPageExit = useCallback(() => {
|
||||||
|
const tasks = Object.values(keepaliveTasksRef.current);
|
||||||
|
if (!tasks.length) return;
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
taskAbortControllersRef.current.get(task.taskId)?.abort();
|
||||||
|
taskAbortControllersRef.current.delete(task.taskId);
|
||||||
|
releaseGenerationSlot(task.concurrencySlotId);
|
||||||
|
aiGenerationClient.cancelTaskOnUnload(task.taskId);
|
||||||
|
});
|
||||||
|
keepaliveTasksRef.current = {};
|
||||||
|
persistKeepaliveTasks({});
|
||||||
|
setIsGenerating(false);
|
||||||
|
setGenerationStatus("已释放未完成任务");
|
||||||
|
setGenerationProgress(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const runKeepalivePoll = useCallback(
|
const runKeepalivePoll = useCallback(
|
||||||
(task: WorkbenchKeepaliveTask) => {
|
(task: WorkbenchKeepaliveTask) => {
|
||||||
if (taskAbortControllersRef.current.has(task.taskId)) return;
|
if (taskAbortControllersRef.current.has(task.taskId)) return;
|
||||||
@@ -916,6 +1062,10 @@ function WorkbenchPage({
|
|||||||
if (abortController.signal.aborted) return;
|
if (abortController.signal.aborted) return;
|
||||||
if (attempt > 0) await sleep(3000);
|
if (attempt > 0) await sleep(3000);
|
||||||
if (abortController.signal.aborted) return;
|
if (abortController.signal.aborted) return;
|
||||||
|
if (typeof navigator !== "undefined" && navigator.onLine === false) {
|
||||||
|
releaseKeepaliveTaskAfterNetworkLoss(task, lastKnownProgress);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let status;
|
let status;
|
||||||
try {
|
try {
|
||||||
@@ -929,7 +1079,8 @@ function WorkbenchPage({
|
|||||||
taskProgress: 100,
|
taskProgress: 100,
|
||||||
taskStatusLabel: "任务异常",
|
taskStatusLabel: "任务异常",
|
||||||
});
|
});
|
||||||
removeKeepaliveTask(task.taskId);
|
releaseKeepaliveTaskLocally(task.taskId, { cancelServer: true });
|
||||||
|
onRefreshUsage?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -1128,9 +1279,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;
|
||||||
@@ -1151,6 +1308,24 @@ function WorkbenchPage({
|
|||||||
};
|
};
|
||||||
}, [runKeepalivePoll]);
|
}, [runKeepalivePoll]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePageHide = () => {
|
||||||
|
cancelActiveKeepaliveTasksOnPageExit();
|
||||||
|
};
|
||||||
|
const handleOnline = () => {
|
||||||
|
Object.values(keepaliveTasksRef.current).forEach((task) => runKeepalivePoll(task));
|
||||||
|
syncActiveGenerationUi();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("pagehide", handlePageHide);
|
||||||
|
window.addEventListener("online", handleOnline);
|
||||||
|
aiGenerationClient.flushPendingTaskCancellations();
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pagehide", handlePageHide);
|
||||||
|
window.removeEventListener("online", handleOnline);
|
||||||
|
};
|
||||||
|
}, [cancelActiveKeepaliveTasksOnPageExit, runKeepalivePoll, syncActiveGenerationUi]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
persistPromptHistory(promptHistory);
|
persistPromptHistory(promptHistory);
|
||||||
}, [promptHistory]);
|
}, [promptHistory]);
|
||||||
@@ -1266,6 +1441,12 @@ function WorkbenchPage({
|
|||||||
activeConversationIdRef.current = null;
|
activeConversationIdRef.current = null;
|
||||||
}, [syncActiveGenerationUi]);
|
}, [syncActiveGenerationUi]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resetToken === undefined || lastResetTokenRef.current === resetToken) return;
|
||||||
|
lastResetTokenRef.current = resetToken;
|
||||||
|
handleNewConversation();
|
||||||
|
}, [handleNewConversation, resetToken]);
|
||||||
|
|
||||||
const handleSelectProject = useCallback((id: string) => {
|
const handleSelectProject = useCallback((id: string) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
handleNewConversation();
|
handleNewConversation();
|
||||||
@@ -1420,6 +1601,7 @@ function WorkbenchPage({
|
|||||||
const atBottom = top + surface.clientHeight >= surface.scrollHeight - edgeThreshold;
|
const atBottom = top + surface.clientHeight >= surface.scrollHeight - edgeThreshold;
|
||||||
shouldFollowNewMessagesRef.current = atBottom;
|
shouldFollowNewMessagesRef.current = atBottom;
|
||||||
setComposerHidden(!(atTop || atBottom));
|
setComposerHidden(!(atTop || atBottom));
|
||||||
|
hideScrollActionHint();
|
||||||
lastScrollTopRef.current = top;
|
lastScrollTopRef.current = top;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1434,24 +1616,27 @@ function WorkbenchPage({
|
|||||||
shouldFollowNewMessagesRef.current = atBottom;
|
shouldFollowNewMessagesRef.current = atBottom;
|
||||||
if (atTop || atBottom) {
|
if (atTop || atBottom) {
|
||||||
setComposerHidden(false);
|
setComposerHidden(false);
|
||||||
|
hideScrollActionHint();
|
||||||
} else if (Math.abs(delta) > scrollDeltaThreshold) {
|
} else if (Math.abs(delta) > scrollDeltaThreshold) {
|
||||||
setComposerHidden(true);
|
setComposerHidden(true);
|
||||||
|
showScrollActionHint(delta < 0 ? "top" : "bottom");
|
||||||
}
|
}
|
||||||
lastScrollTopRef.current = top;
|
lastScrollTopRef.current = top;
|
||||||
};
|
};
|
||||||
|
|
||||||
surface.addEventListener("scroll", handleScroll, { passive: true });
|
surface.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
return () => surface.removeEventListener("scroll", handleScroll);
|
return () => surface.removeEventListener("scroll", handleScroll);
|
||||||
}, [hasActivatedWorkspace]);
|
}, [hasActivatedWorkspace, hideScrollActionHint, showScrollActionHint]);
|
||||||
|
|
||||||
const scrollMessagesSurface = useCallback((direction: "top" | "bottom") => {
|
const scrollMessagesSurface = useCallback((direction: "top" | "bottom") => {
|
||||||
const surface = messagesSurfaceRef.current;
|
const surface = messagesSurfaceRef.current;
|
||||||
if (!surface) return;
|
if (!surface) return;
|
||||||
|
|
||||||
const top = direction === "top" ? 0 : surface.scrollHeight;
|
const top = direction === "top" ? 0 : surface.scrollHeight;
|
||||||
|
hideScrollActionHint();
|
||||||
setComposerHidden(false);
|
setComposerHidden(false);
|
||||||
surface.scrollTo({ top, behavior: "smooth" });
|
surface.scrollTo({ top, behavior: "smooth" });
|
||||||
}, []);
|
}, [hideScrollActionHint]);
|
||||||
|
|
||||||
const closeToolbarMenus = () => setToolbarMenuId(null);
|
const closeToolbarMenus = () => setToolbarMenuId(null);
|
||||||
const toggleToolbarMenu = (menuId: Exclude<ToolbarMenuId, null>) => {
|
const toggleToolbarMenu = (menuId: Exclude<ToolbarMenuId, null>) => {
|
||||||
@@ -1771,7 +1956,7 @@ function WorkbenchPage({
|
|||||||
const trimmedPrompt = (promptOverride ?? inputValue).trim();
|
const trimmedPrompt = (promptOverride ?? inputValue).trim();
|
||||||
if (!trimmedPrompt) return;
|
if (!trimmedPrompt) return;
|
||||||
const userKey = getGenerationUserKey(session?.user.id);
|
const userKey = getGenerationUserKey(session?.user.id);
|
||||||
if (activeMode !== "chat" && getActiveGenerationTaskCount(userKey) >= 3) return;
|
if (activeMode !== "chat" && getActiveGenerationTaskCount(userKey) >= getEffectiveGenerationLimit()) return;
|
||||||
setReferencePreviewOpen(false);
|
setReferencePreviewOpen(false);
|
||||||
|
|
||||||
let conversationId = activeConversationIdRef.current ?? activeConversationId;
|
let conversationId = activeConversationIdRef.current ?? activeConversationId;
|
||||||
@@ -2250,8 +2435,11 @@ function WorkbenchPage({
|
|||||||
setProjectError("仅支持对视频结果进行超分");
|
setProjectError("仅支持对视频结果进行超分");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3) {
|
const userKey = getGenerationUserKey(session?.user.id);
|
||||||
setProjectError(`当前已有 ${getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id))} 个任务进行中(上限3个),请等待任一任务完成后再提交新任务`);
|
const activeCount = getActiveGenerationTaskCount(userKey);
|
||||||
|
const limit = getEffectiveGenerationLimit();
|
||||||
|
if (activeCount >= limit) {
|
||||||
|
setProjectError(`当前已有 ${activeCount} 个任务进行中(上限${limit}个),请等待任一任务完成后再提交新任务`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
@@ -2372,8 +2560,11 @@ function WorkbenchPage({
|
|||||||
setProjectError("仅支持对图片结果进行超分");
|
setProjectError("仅支持对图片结果进行超分");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3) {
|
const userKey = getGenerationUserKey(session?.user.id);
|
||||||
setProjectError(`当前已有 ${getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id))} 个任务进行中(上限3个),请等待任一任务完成后再提交新任务`);
|
const activeCount = getActiveGenerationTaskCount(userKey);
|
||||||
|
const limit = getEffectiveGenerationLimit();
|
||||||
|
if (activeCount >= limit) {
|
||||||
|
setProjectError(`当前已有 ${activeCount} 个任务进行中(上限${limit}个),请等待任一任务完成后再提交新任务`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
@@ -2545,7 +2736,16 @@ function WorkbenchPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendDisabled = !inputValue.trim() || (activeMode !== "chat" && getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3);
|
const activeGenerationCount = getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id));
|
||||||
|
const activeGenerationLimit = getEffectiveGenerationLimit();
|
||||||
|
const generationLimitReached = activeMode !== "chat" && activeGenerationCount >= activeGenerationLimit;
|
||||||
|
const promptIsEmpty = !inputValue.trim();
|
||||||
|
const sendDisabled = promptIsEmpty || generationLimitReached;
|
||||||
|
const sendButtonTitle = promptIsEmpty
|
||||||
|
? "输入内容后可发送"
|
||||||
|
: generationLimitReached
|
||||||
|
? `当前已有 ${activeGenerationCount} 个任务进行中(上限 ${activeGenerationLimit} 个),请等待任一任务完成`
|
||||||
|
: billingEstimate.title;
|
||||||
|
|
||||||
const suggestedPrompts = [
|
const suggestedPrompts = [
|
||||||
{ text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode },
|
{ text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode },
|
||||||
@@ -2810,7 +3010,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")}
|
||||||
@@ -2882,10 +3082,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,
|
||||||
@@ -2933,9 +3138,29 @@ function WorkbenchPage({
|
|||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const renderPromptCaseOverlay = () =>
|
const renderPromptCaseOverlay = () => {
|
||||||
selectedPromptCase ? (
|
if (!selectedPromptCase) return null;
|
||||||
<div className="wb-prompt-case-modal" role="dialog" aria-modal="true" aria-labelledby="wb-prompt-case-title">
|
|
||||||
|
const measuredRatio = promptCaseMeasuredRatios[selectedPromptCase.id];
|
||||||
|
const ratioParts = selectedPromptCase.ratio.replace(/\s+/g, "").split(":").map(Number);
|
||||||
|
const declaredRatio =
|
||||||
|
ratioParts.length === 2 && ratioParts[0] > 0 && ratioParts[1] > 0
|
||||||
|
? ratioParts[0] / ratioParts[1]
|
||||||
|
: null;
|
||||||
|
const caseRatio =
|
||||||
|
typeof measuredRatio === "number" && Number.isFinite(measuredRatio) && measuredRatio > 0
|
||||||
|
? measuredRatio
|
||||||
|
: declaredRatio;
|
||||||
|
const copyLength = `${selectedPromptCase.summary} ${selectedPromptCase.prompt}`.length;
|
||||||
|
const modalClassName = [
|
||||||
|
"wb-prompt-case-modal",
|
||||||
|
caseRatio && caseRatio < 0.72 ? "is-tall-media" : "",
|
||||||
|
caseRatio && caseRatio >= 0.72 && caseRatio < 1 ? "is-portrait-media" : "",
|
||||||
|
copyLength > 260 ? "is-long-copy" : "",
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={modalClassName} role="dialog" aria-modal="true" aria-labelledby="wb-prompt-case-title">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="wb-prompt-case-modal__backdrop"
|
className="wb-prompt-case-modal__backdrop"
|
||||||
@@ -2944,7 +3169,11 @@ function WorkbenchPage({
|
|||||||
/>
|
/>
|
||||||
<section className="wb-prompt-case-modal__panel">
|
<section className="wb-prompt-case-modal__panel">
|
||||||
<div className="wb-prompt-case-modal__media">
|
<div className="wb-prompt-case-modal__media">
|
||||||
<img src={selectedPromptCase.imageUrl} alt={selectedPromptCase.title} />
|
<img
|
||||||
|
src={selectedPromptCase.imageUrl}
|
||||||
|
alt={selectedPromptCase.title}
|
||||||
|
onLoad={(event) => handlePromptCaseImageLoad(selectedPromptCase.id, event)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<aside className="wb-prompt-case-modal__sidebar">
|
<aside className="wb-prompt-case-modal__sidebar">
|
||||||
<button
|
<button
|
||||||
@@ -2984,7 +3213,8 @@ function WorkbenchPage({
|
|||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (!hasActivatedWorkspace) {
|
if (!hasActivatedWorkspace) {
|
||||||
return (
|
return (
|
||||||
@@ -3079,8 +3309,8 @@ function WorkbenchPage({
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{renderConversationSidebar()}
|
||||||
</div>
|
</div>
|
||||||
{renderConversationSidebar()}
|
|
||||||
{renderMessagePreviewOverlay()}
|
{renderMessagePreviewOverlay()}
|
||||||
{renderPromptCaseOverlay()}
|
{renderPromptCaseOverlay()}
|
||||||
{renderDeleteDialog()}
|
{renderDeleteDialog()}
|
||||||
@@ -3166,11 +3396,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}
|
||||||
@@ -3227,10 +3452,10 @@ function WorkbenchPage({
|
|||||||
{renderComposerToolbar(false, isGenerating)}
|
{renderComposerToolbar(false, isGenerating)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div className="wb-chat-scroll-actions" aria-label="聊天滚动">
|
<div className={`wb-chat-scroll-actions${scrollActionHint ? ` is-showing-${scrollActionHint}` : ""}`} aria-label="聊天滚动">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="wb-chat-scroll-actions__button"
|
className="wb-chat-scroll-actions__button wb-chat-scroll-actions__button--top"
|
||||||
title="返回聊天顶部"
|
title="返回聊天顶部"
|
||||||
aria-label="返回聊天顶部"
|
aria-label="返回聊天顶部"
|
||||||
onClick={() => scrollMessagesSurface("top")}
|
onClick={() => scrollMessagesSurface("top")}
|
||||||
@@ -3239,7 +3464,7 @@ function WorkbenchPage({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="wb-chat-scroll-actions__button"
|
className="wb-chat-scroll-actions__button wb-chat-scroll-actions__button--bottom"
|
||||||
title="到达聊天底部"
|
title="到达聊天底部"
|
||||||
aria-label="到达聊天底部"
|
aria-label="到达聊天底部"
|
||||||
onClick={() => scrollMessagesSurface("bottom")}
|
onClick={() => scrollMessagesSurface("bottom")}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -788,6 +788,65 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compliance-document {
|
||||||
|
gap: 0;
|
||||||
|
padding: 30px 34px 34px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compliance-document__title,
|
||||||
|
.compliance-document__heading,
|
||||||
|
.compliance-document__subheading,
|
||||||
|
.compliance-document__paragraph,
|
||||||
|
.compliance-document__clause {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
color: var(--fg-body);
|
||||||
|
letter-spacing: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compliance-document__title {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
font-size: clamp(24px, 3vw, 32px);
|
||||||
|
line-height: 1.22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compliance-document__heading {
|
||||||
|
margin: 26px 0 12px;
|
||||||
|
padding-top: 4px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compliance-document__subheading {
|
||||||
|
margin: 18px 0 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compliance-document__paragraph,
|
||||||
|
.compliance-document__clause {
|
||||||
|
color: var(--fg-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compliance-document__paragraph + .compliance-document__paragraph,
|
||||||
|
.compliance-document__clause + .compliance-document__clause,
|
||||||
|
.compliance-document__paragraph + .compliance-document__clause,
|
||||||
|
.compliance-document__clause + .compliance-document__paragraph {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compliance-document__clause {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.compliance-section {
|
.compliance-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 52px minmax(0, 1fr);
|
grid-template-columns: 52px minmax(0, 1fr);
|
||||||
@@ -892,4 +951,12 @@
|
|||||||
.compliance-section {
|
.compliance-section {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compliance-document {
|
||||||
|
padding: 22px 18px 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compliance-document__clause {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
@@ -10837,8 +10845,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
.web-shell[data-ui-theme="dark-green"] {
|
||||||
|
--dg-mobile-nav-height: 58px;
|
||||||
|
--dg-mobile-nav-gap: 12px;
|
||||||
|
--dg-mobile-nav-space: calc(var(--dg-mobile-nav-height) + var(--dg-mobile-nav-gap));
|
||||||
|
}
|
||||||
|
|
||||||
|
.web-shell[data-ui-theme="dark-green"] .web-topbar {
|
||||||
|
z-index: 72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.web-shell[data-ui-theme="dark-green"] .web-shell__content,
|
||||||
|
.web-shell[data-ui-theme="dark-green"] .web-shell__page {
|
||||||
|
min-height: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.web-shell[data-ui-theme="dark-green"] .web-shell__page {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.web-shell[data-ui-theme="dark-green"]:not([data-view="home"]):not([data-view="login"]):not([data-view="workbench"]):not([data-view="agent"]):not([data-view="avatarConsole"]) .web-shell__page {
|
||||||
|
padding-top: var(--dg-mobile-nav-space);
|
||||||
|
}
|
||||||
|
|
||||||
|
.web-shell[data-ui-theme="dark-green"] .profile-popover {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(56px + var(--dg-mobile-nav-space) + env(safe-area-inset-top, 0px));
|
||||||
|
right: 12px;
|
||||||
|
z-index: 120;
|
||||||
|
width: min(288px, calc(100vw - 24px));
|
||||||
|
max-height: calc(100svh - 56px - var(--dg-mobile-nav-space) - 24px);
|
||||||
|
overflow-y: auto;
|
||||||
|
transform-origin: top right;
|
||||||
|
}
|
||||||
|
|
||||||
.web-shell[data-ui-theme="dark-green"] .floating-nav {
|
.web-shell[data-ui-theme="dark-green"] .floating-nav {
|
||||||
top: calc(56px + env(safe-area-inset-top, 0px));
|
top: calc(56px + env(safe-area-inset-top, 0px));
|
||||||
|
z-index: 50;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
|
|||||||
@@ -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