feat: release v0.81.0 with full pending changes

This commit is contained in:
coso
2026-03-08 07:53:37 +08:00
parent 7516671610
commit 2888352a00
181 changed files with 32791 additions and 2237 deletions
+10
View File
@@ -739,6 +739,14 @@ export interface AsterProviderConfig {
base_url?: string;
}
export interface AutoContinueRequestPayload {
enabled: boolean;
fast_mode_enabled: boolean;
continuation_length: number;
sensitivity: number;
source?: string;
}
/**
* Aster 会话信息(匹配后端 SessionInfo 结构)
*/
@@ -827,6 +835,7 @@ export async function sendAsterMessageStream(
providerConfig?: AsterProviderConfig,
executionStrategy?: AsterExecutionStrategy,
webSearch?: boolean,
autoContinue?: AutoContinueRequestPayload,
): Promise<void> {
const resolvedWorkspaceId = requireWorkspaceId(workspaceId);
@@ -840,6 +849,7 @@ export async function sendAsterMessageStream(
workspace_id: resolvedWorkspaceId,
execution_strategy: executionStrategy,
web_search: webSearch,
auto_continue: autoContinue,
},
});
}
+11
View File
@@ -0,0 +1,11 @@
import { safeInvoke } from "@/lib/dev-bridge/safeInvoke";
export async function saveExportedDocument(
filePath: string,
content: string,
): Promise<void> {
await safeInvoke("save_exported_document", {
filePath,
content,
});
}
+51
View File
@@ -26,6 +26,46 @@ export interface AgentRun {
updated_at: string;
}
export interface ThemeWorkbenchRunTodoItem {
run_id: string;
execution_id?: string | null;
session_id?: string | null;
artifact_paths?: string[];
title: string;
gate_key?: "topic_select" | "write_mode" | "publish_confirm" | null;
status: AgentRunStatus;
source: AgentRunSource | string;
source_ref: string | null;
started_at: string;
}
export interface ThemeWorkbenchRunTerminalItem {
run_id: string;
execution_id?: string | null;
session_id?: string | null;
artifact_paths?: string[];
title: string;
gate_key?: "topic_select" | "write_mode" | "publish_confirm" | null;
status: AgentRunStatus;
source: AgentRunSource | string;
source_ref: string | null;
started_at: string;
finished_at: string | null;
}
export interface ThemeWorkbenchRunState {
run_state: "idle" | "auto_running";
current_gate_key?:
| "idle"
| "topic_select"
| "write_mode"
| "publish_confirm"
| null;
queue_items: ThemeWorkbenchRunTodoItem[];
latest_terminal: ThemeWorkbenchRunTerminalItem | null;
updated_at: string;
}
export async function executionRunList(
limit: number = 50,
offset: number = 0,
@@ -38,3 +78,14 @@ export async function executionRunGet(
): Promise<AgentRun | null> {
return await safeInvoke("execution_run_get", { runId });
}
export async function executionRunGetThemeWorkbenchState(
sessionId: string,
limit: number = 3,
): Promise<ThemeWorkbenchRunState> {
return await safeInvoke("execution_run_get_theme_workbench_state", {
sessionId,
session_id: sessionId,
limit,
});
}
+22
View File
@@ -188,6 +188,21 @@ export interface ContentDetail extends ContentListItem {
session_id?: string;
}
export interface ThemeWorkbenchVersionState {
id: string;
created_at: number;
description?: string;
status?: "in_progress" | "pending" | "merged" | "candidate";
is_current: boolean;
}
export interface ThemeWorkbenchDocumentState {
content_id: string;
current_version_id: string;
version_count: number;
versions: ThemeWorkbenchVersionState[];
}
/** 创建项目请求 */
export interface CreateProjectRequest {
name: string;
@@ -326,6 +341,13 @@ export async function getContent(id: string): Promise<ContentDetail | null> {
return invoke("content_get", { id });
}
/** 获取主题工作台文稿版本状态(后端解析 content.metadata) */
export async function getThemeWorkbenchDocumentState(
id: string,
): Promise<ThemeWorkbenchDocumentState | null> {
return invoke("content_get_theme_workbench_document_state", { id });
}
/** 获取项目的内容列表 */
export async function listContents(
projectId: string,
+101
View File
@@ -170,6 +170,41 @@ export async function readFile(
});
}
/**
* 解析会话文件绝对路径
*/
export async function resolveFilePath(
sessionId: string,
fileName: string,
): Promise<string> {
return safeInvoke<string>("session_files_resolve_file_path", {
sessionId,
fileName,
});
}
/**
* 在 Finder/文件管理器中定位会话文件
*/
export async function revealFileInFinder(
sessionId: string,
fileName: string,
): Promise<void> {
const path = await resolveFilePath(sessionId, fileName);
await safeInvoke("reveal_in_finder", { path });
}
/**
* 使用系统默认应用打开会话文件
*/
export async function openFileWithDefaultApp(
sessionId: string,
fileName: string,
): Promise<void> {
const path = await resolveFilePath(sessionId, fileName);
await safeInvoke("open_with_default_app", { path });
}
/**
* 删除会话文件
*/
@@ -207,3 +242,69 @@ export async function cleanupExpired(maxAgeDays?: number): Promise<number> {
export async function cleanupEmpty(): Promise<number> {
return safeInvoke<number>("session_files_cleanup_empty");
}
// ============================================================================
// 图片上传 API
// ============================================================================
/**
* 上传图片到会话
* @param sessionId 会话 ID
* @param filePath 本地图片文件路径
* @returns 图片在会话中的访问路径
*/
export async function uploadImageToSession(
sessionId: string,
filePath: string,
): Promise<string> {
return safeInvoke<string>("upload_image_to_session", {
sessionId,
filePath,
});
}
/**
* 从会话中读取图片(返回 base64 编码)
* @param sessionId 会话 ID
* @param fileName 文件名
* @returns base64 编码的图片数据
*/
export async function readImageFromSession(
sessionId: string,
fileName: string,
): Promise<string> {
return safeInvoke<string>("read_image_from_session", {
sessionId,
fileName,
});
}
// ============================================================================
// 文档导入 API
// ============================================================================
/**
* 导入文档内容
* @param filePath 本地文档文件路径
* @returns 文档的文本内容
*/
export async function importDocument(filePath: string): Promise<string> {
return safeInvoke<string>("import_document", { filePath });
}
/**
* 导入文档并保存到会话
* @param sessionId 会话 ID
* @param filePath 本地文档文件路径
* @returns [文档内容, 保存的文件名]
*/
export async function importDocumentToSession(
sessionId: string,
filePath: string,
): Promise<[string, string]> {
return safeInvoke<[string, string]>("import_document_to_session", {
sessionId,
filePath,
});
}
+25
View File
@@ -16,6 +16,11 @@ import {
getWorkspaceRepairHistory,
type WorkspaceRepairRecord,
} from "@/lib/workspaceHealthTelemetry";
import { getActiveContentTarget } from "@/lib/activeContentTarget";
import {
getThemeWorkbenchDocumentState,
type ThemeWorkbenchDocumentState,
} from "@/lib/api/project";
export interface CrashDiagnosticPayload {
generated_at: string;
@@ -32,6 +37,7 @@ export interface CrashDiagnosticPayload {
invoke_error_buffer?: InvokeErrorBufferEntry[];
persisted_log_tail?: LogEntry[];
workspace_repair_history?: WorkspaceRepairRecord[];
theme_workbench_document_state?: ThemeWorkbenchDocumentState | null;
diagnostic_collection_notes?: string[];
}
@@ -113,6 +119,7 @@ interface BuildCrashDiagnosticPayloadParams {
maxInvokeErrors?: number;
maxPersistedLogs?: number;
maxWorkspaceRepairs?: number;
themeWorkbenchDocumentState?: ThemeWorkbenchDocumentState | null;
}
export function buildCrashDiagnosticPayload(
@@ -130,6 +137,7 @@ export function buildCrashDiagnosticPayload(
maxInvokeErrors = 40,
maxPersistedLogs = 200,
maxWorkspaceRepairs = 50,
themeWorkbenchDocumentState = null,
} = params;
return {
@@ -152,12 +160,27 @@ export function buildCrashDiagnosticPayload(
invoke_error_buffer: getInvokeErrorBuffer(maxInvokeErrors),
persisted_log_tail: persistedLogTail.slice(-maxPersistedLogs),
workspace_repair_history: getWorkspaceRepairHistory(maxWorkspaceRepairs),
theme_workbench_document_state: themeWorkbenchDocumentState,
diagnostic_collection_notes: collectionNotes.filter((item) =>
typeof item === "string" && item.trim().length > 0
),
};
}
export async function collectThemeWorkbenchDocumentStateForDiagnostic(): Promise<ThemeWorkbenchDocumentState | null> {
const activeTarget = getActiveContentTarget();
if (!activeTarget?.contentId || activeTarget.canvasType !== "document") {
return null;
}
try {
return await getThemeWorkbenchDocumentState(activeTarget.contentId);
} catch (error) {
console.warn("[crashDiagnostic] 获取主题工作台文稿状态失败:", error);
return null;
}
}
export async function copyCrashDiagnosticToClipboard(
payload: CrashDiagnosticPayload,
): Promise<void> {
@@ -248,6 +271,7 @@ function buildDiagnosticSummary(payload: CrashDiagnosticPayload): string {
const invokeErrorCount = payload.invoke_error_buffer?.length ?? 0;
const persistedLogCount = payload.persisted_log_tail?.length ?? 0;
const workspaceRepairCount = payload.workspace_repair_history?.length ?? 0;
const versionCount = payload.theme_workbench_document_state?.version_count ?? 0;
const dsnConfigured = payload.crash_reporting.dsn ? "是" : "否";
return [
`- 版本:${payload.app_version}`,
@@ -259,6 +283,7 @@ function buildDiagnosticSummary(payload: CrashDiagnosticPayload): string {
`- 命令调用失败缓存条数:${invokeErrorCount}`,
`- 持久化日志尾部行数:${persistedLogCount}`,
`- Workspace 自动修复记录条数:${workspaceRepairCount}`,
`- 主题工作台文稿版本数:${versionCount}`,
`- 崩溃上报已启用:${payload.crash_reporting.enabled ? "是" : "否"}(DSN 已配置:${dsnConfigured})`,
].join("\n");
}
+35
View File
@@ -0,0 +1,35 @@
export const DOCUMENT_EDITOR_FOCUS_EVENT = "proxycast:document-editor-focus";
export interface DocumentEditorFocusDetail {
focused: boolean;
}
export function emitDocumentEditorFocus(focused: boolean): void {
if (typeof window === "undefined") {
return;
}
window.dispatchEvent(
new CustomEvent<DocumentEditorFocusDetail>(DOCUMENT_EDITOR_FOCUS_EVENT, {
detail: { focused },
}),
);
}
export function subscribeDocumentEditorFocus(
callback: (focused: boolean) => void,
): () => void {
if (typeof window === "undefined") {
return () => {};
}
const handle = (event: Event) => {
const customEvent = event as CustomEvent<DocumentEditorFocusDetail>;
callback(Boolean(customEvent.detail?.focused));
};
window.addEventListener(DOCUMENT_EDITOR_FOCUS_EVENT, handle);
return () => {
window.removeEventListener(DOCUMENT_EDITOR_FOCUS_EVENT, handle);
};
}
+55
View File
@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import {
findImageProviderById,
findImageProviderForSelection,
getImageModelsForProvider,
isImageProvider,
pickImageModelBySelection,
type ImageProviderCandidate,
} from "./imageGeneration";
interface MockProvider extends ImageProviderCandidate {
name: string;
}
const providers: MockProvider[] = [
{ id: "new-api", type: "openai", name: "OpenAI 兼容" },
{ id: "doubao-image", type: "openai", name: "即梦" },
{ id: "kling", type: "openai", name: "可灵" },
];
describe("imageGeneration", () => {
it("应识别图片 Provider", () => {
expect(isImageProvider("new-api", "openai")).toBe(true);
expect(isImageProvider("tts-only", "audio")).toBe(false);
});
it("应按项目配置优先匹配指定 Provider", () => {
expect(findImageProviderById(providers, "doubao-image")?.name).toBe("即梦");
expect(findImageProviderById(providers, "missing-provider")).toBeNull();
});
it("应按预设模型偏好自动选择 Provider", () => {
expect(findImageProviderForSelection(providers, "basic")?.id).toBe("new-api");
expect(findImageProviderForSelection(providers, "jimeng")?.id).toBe(
"doubao-image",
);
expect(findImageProviderForSelection(providers, "kling")?.id).toBe("kling");
});
it("应按预设模型偏好自动选择模型", () => {
expect(pickImageModelBySelection([], "basic")).toBe("gpt-image-1");
expect(
pickImageModelBySelection(["flux-pro", "gpt-image-1"], "basic"),
).toBe("gpt-image-1");
expect(pickImageModelBySelection([], "jimeng")).toBe("seedream-3.0");
});
it("应解析 Provider 可用模型列表", () => {
expect(getImageModelsForProvider("new-api", "openai")[0]?.id).toBe("dall-e-3");
expect(
getImageModelsForProvider("custom-provider", "openai", ["gpt-image-1"])[0]
?.id,
).toBe("gpt-image-1");
});
});
+149
View File
@@ -0,0 +1,149 @@
/**
* @file 图片生成 Provider 选择辅助
* @description 统一图片 Provider 的识别、优先级选择与模型自动匹配
* @module lib/imageGeneration
*/
import {
IMAGE_GEN_MODELS,
type ImageGenModel,
} from "@/components/image-gen/types";
export type ImageModelPreset = "basic" | "jimeng" | "kling";
export interface ImageProviderCandidate {
id: string;
type: string;
}
export interface ImageProviderModelCandidate extends ImageProviderCandidate {
customModels?: string[];
}
function normalizeProviderSignature(providerId: string, providerType: string): string {
return `${providerId}:${providerType}`.toLowerCase();
}
export function isImageProvider(providerId: string, providerType: string): boolean {
const normalized = normalizeProviderSignature(providerId, providerType);
return (
normalized.includes("new-api") ||
normalized.includes("openai") ||
normalized.includes("doubao") ||
normalized.includes("volc") ||
normalized.includes("dashscope") ||
normalized.includes("alibaba") ||
normalized.includes("qwen") ||
normalized.includes("kling") ||
normalized.includes("minimax") ||
normalized.includes("hailuo") ||
normalized.includes("image")
);
}
export function findImageProviderById<T extends ImageProviderCandidate>(
providers: T[],
providerId?: string | null,
): T | null {
const normalizedProviderId = providerId?.trim().toLowerCase();
if (!normalizedProviderId) {
return null;
}
return (
providers.find((provider) => provider.id.trim().toLowerCase() === normalizedProviderId) ?? null
);
}
export function findImageProviderForSelection<T extends ImageProviderCandidate>(
providers: T[],
modelType: ImageModelPreset,
): T | null {
const preferredKeywords: string[] =
modelType === "jimeng"
? ["doubao", "volc"]
: modelType === "kling"
? ["kling", "minimax", "hailuo"]
: ["new-api", "openai", "dashscope", "alibaba", "qwen"];
for (const keyword of preferredKeywords) {
const matched = providers.find((provider) =>
normalizeProviderSignature(provider.id, provider.type).includes(keyword),
);
if (matched) {
return matched;
}
}
return providers[0] ?? null;
}
export function pickImageModelBySelection(
models: string[],
modelType: ImageModelPreset,
): string {
if (models.length === 0) {
return modelType === "jimeng"
? "seedream-3.0"
: modelType === "kling"
? "kling-2.6"
: "gpt-image-1";
}
const normalizedModels = models.map((model) => model.toLowerCase());
const priorities =
modelType === "jimeng"
? ["jimeng", "seedream", "doubao", "volc", "pro"]
: modelType === "kling"
? ["kling", "hailuo", "minimax"]
: ["gpt-image", "flux", "sd", "image", "wanx", "seedream"];
for (const keyword of priorities) {
const index = normalizedModels.findIndex((model) => model.includes(keyword));
if (index >= 0) {
return models[index] ?? models[0] ?? "gpt-image-1";
}
}
return models[0] ?? "gpt-image-1";
}
export function getImageModelsForProvider(
providerId: string,
providerType: string,
customModels?: string[],
): ImageGenModel[] {
if (customModels && customModels.length > 0) {
return customModels.map((modelId) => ({
id: modelId,
name: modelId,
supportedSizes: [
"1024x1024",
"768x1344",
"1344x768",
"1792x1024",
"1024x1792",
],
}));
}
if (IMAGE_GEN_MODELS[providerId]) {
return IMAGE_GEN_MODELS[providerId] ?? [];
}
if (IMAGE_GEN_MODELS[providerType]) {
return IMAGE_GEN_MODELS[providerType] ?? [];
}
return [];
}
export function getImageModelIdsForProvider(
providerId: string,
providerType: string,
customModels?: string[],
): string[] {
return getImageModelsForProvider(providerId, providerType, customModels).map(
(model) => model.id,
);
}
+118
View File
@@ -0,0 +1,118 @@
import { describe, expect, it } from "vitest";
import {
buildPersistedMediaGenerationPreference,
findMediaProviderById,
findTtsProviderForSelection,
findVideoProviderForSelection,
getTtsModelsForProvider,
getVideoModelsForProvider,
hasMediaGenerationPreferenceOverride,
isTtsProvider,
isVideoProvider,
pickTtsModel,
pickVideoModelByVersion,
resolveMediaGenerationPreference,
} from "./mediaGeneration";
const providers = [
{ id: "doubao-video", type: "openai", customModels: [] },
{ id: "openai-tts", type: "openai", customModels: ["gpt-4o-mini-tts"] },
{ id: "qwen-video", type: "openai", customModels: [] },
];
describe("mediaGeneration", () => {
it("应按 ID 查找媒体 Provider", () => {
expect(findMediaProviderById(providers, "doubao-video")?.id).toBe(
"doubao-video",
);
expect(findMediaProviderById(providers, "missing")).toBeNull();
});
it("应识别视频和语音 Provider", () => {
expect(isVideoProvider("doubao-video")).toBe(true);
expect(isVideoProvider("openai-tts")).toBe(false);
expect(isTtsProvider("openai-tts", "openai")).toBe(true);
expect(isTtsProvider("runway-video", "video")).toBe(false);
});
it("应解析视频 Provider 模型并按版本挑选", () => {
expect(getVideoModelsForProvider("doubao-video")[0]).toBe(
"seedance-1-5-pro-251215",
);
expect(
pickVideoModelByVersion(
["seedance-1-5-pro-251215", "seedance-1-5-lite-250428"],
"v2-1-master",
),
).toBe("seedance-1-5-pro-251215");
});
it("应解析语音 Provider 模型并挑选 TTS 模型", () => {
expect(getTtsModelsForProvider()[0]).toBe("gpt-4o-mini-tts");
expect(pickTtsModel(["gpt-4o-mini-tts", "gpt-image-1"])).toBe(
"gpt-4o-mini-tts",
);
});
it("应按默认策略选择视频与语音 Provider", () => {
expect(findVideoProviderForSelection(providers, "jimeng")?.id).toBe(
"doubao-video",
);
expect(findTtsProviderForSelection([providers[1]])?.id).toBe("openai-tts");
});
it("应优先使用项目覆盖,否则回退到全局默认", () => {
expect(
resolveMediaGenerationPreference(
{
preferredProviderId: "project-provider",
preferredModelId: "project-model",
allowFallback: false,
},
{
preferredProviderId: "global-provider",
preferredModelId: "global-model",
allowFallback: true,
},
),
).toEqual({
preferredProviderId: "project-provider",
preferredModelId: "project-model",
allowFallback: false,
source: "project",
});
expect(
resolveMediaGenerationPreference(undefined, {
preferredProviderId: "global-provider",
preferredModelId: "global-model",
allowFallback: false,
}),
).toEqual({
preferredProviderId: "global-provider",
preferredModelId: "global-model",
allowFallback: false,
source: "global",
});
});
it("应在无有效覆盖时不持久化默认值", () => {
expect(hasMediaGenerationPreferenceOverride(undefined)).toBe(false);
expect(
buildPersistedMediaGenerationPreference({
preferredProviderId: "",
preferredModelId: "model-only",
allowFallback: true,
}),
).toBeUndefined();
expect(
buildPersistedMediaGenerationPreference({
allowFallback: false,
}),
).toEqual({
preferredProviderId: undefined,
preferredModelId: undefined,
allowFallback: false,
});
});
});
+292
View File
@@ -0,0 +1,292 @@
/**
* @file 视频与语音生成选择辅助
* @description 统一视频 / 语音 Provider 识别、模型解析与默认选择策略
* @module lib/mediaGeneration
*/
export interface MediaProviderCandidate {
id: string;
type?: string;
customModels?: string[];
}
export interface MediaGenerationPreference {
preferredProviderId?: string;
preferredModelId?: string;
allowFallback?: boolean;
}
export interface MediaGenerationDefaults {
image?: MediaGenerationPreference;
video?: MediaGenerationPreference;
voice?: MediaGenerationPreference;
}
export interface ResolvedMediaGenerationPreference {
preferredProviderId?: string;
preferredModelId?: string;
allowFallback: boolean;
source: "project" | "global" | "auto";
}
export type VideoModelPreset = "keling" | "jimeng" | "wan-2-5";
export type VideoModelVersion = "v2-1-master" | "v2" | "v1-6";
const VIDEO_MODEL_PRESETS: Record<string, string[]> = {
doubao: ["seedance-1-5-pro-251215", "seedance-1-5-lite-250428"],
volcengine: ["seedance-1-5-pro-251215", "seedance-1-5-lite-250428"],
dashscope: ["wanx2.1-t2v-turbo", "wanx2.1-kf2v-plus"],
alibaba: ["wanx2.1-t2v-turbo", "wanx2.1-kf2v-plus"],
qwen: ["wanx2.1-t2v-turbo", "wanx2.1-kf2v-plus"],
sora: ["sora-2", "sora-2-pro"],
openai: ["sora-2", "sora-2-pro"],
veo: ["veo-3.1"],
google: ["veo-3.1"],
vertex: ["veo-3.1"],
kling: ["kling-2.6"],
minimax: ["minimax-hailuo-2.3", "minimax-hailuo-02"],
hailuo: ["minimax-hailuo-2.3", "minimax-hailuo-02"],
runway: ["runway-gen-4-turbo"],
};
function normalizeMaybeString(value?: string | null): string | undefined {
const normalized = value?.trim();
return normalized ? normalized : undefined;
}
export function normalizeMediaGenerationPreference(
preference?: MediaGenerationPreference | null,
): MediaGenerationPreference {
return {
preferredProviderId: normalizeMaybeString(preference?.preferredProviderId),
preferredModelId: normalizeMaybeString(preference?.preferredModelId),
allowFallback: preference?.allowFallback,
};
}
export function hasMediaGenerationPreferenceOverride(
preference?: MediaGenerationPreference | null,
): boolean {
const normalized = normalizeMediaGenerationPreference(preference);
return Boolean(
normalized.preferredProviderId ||
normalized.preferredModelId ||
normalized.allowFallback === false,
);
}
export function buildPersistedMediaGenerationPreference(
preference?: MediaGenerationPreference | null,
): MediaGenerationPreference | undefined {
const normalized = normalizeMediaGenerationPreference(preference);
if (!normalized.preferredProviderId) {
normalized.preferredModelId = undefined;
}
if (!hasMediaGenerationPreferenceOverride(normalized)) {
return undefined;
}
return {
preferredProviderId: normalized.preferredProviderId,
preferredModelId: normalized.preferredModelId,
allowFallback: normalized.allowFallback ?? true,
};
}
export function resolveMediaGenerationPreference(
projectPreference?: MediaGenerationPreference | null,
globalPreference?: MediaGenerationPreference | null,
): ResolvedMediaGenerationPreference {
const normalizedProject =
normalizeMediaGenerationPreference(projectPreference);
const normalizedGlobal = normalizeMediaGenerationPreference(globalPreference);
const projectSelectsModel = Boolean(
normalizedProject.preferredProviderId || normalizedProject.preferredModelId,
);
const globalSelectsModel = Boolean(
normalizedGlobal.preferredProviderId || normalizedGlobal.preferredModelId,
);
const preferredProviderId =
normalizedProject.preferredProviderId ??
normalizedGlobal.preferredProviderId;
const preferredModelId = normalizedProject.preferredProviderId
? normalizedProject.preferredModelId
: (normalizedProject.preferredModelId ?? normalizedGlobal.preferredModelId);
return {
preferredProviderId,
preferredModelId,
allowFallback:
normalizedProject.allowFallback ?? normalizedGlobal.allowFallback ?? true,
source: projectSelectsModel
? "project"
: globalSelectsModel
? "global"
: "auto",
};
}
export function findMediaProviderById<T extends MediaProviderCandidate>(
providers: T[],
providerId?: string | null,
): T | null {
const normalizedProviderId = providerId?.trim().toLowerCase();
if (!normalizedProviderId) {
return null;
}
return (
providers.find(
(provider) => provider.id.trim().toLowerCase() === normalizedProviderId,
) ?? null
);
}
export function isVideoProvider(providerId: string): boolean {
const normalized = providerId.toLowerCase();
return (
normalized.includes("doubao") ||
normalized.includes("volc") ||
normalized.includes("dashscope") ||
normalized.includes("alibaba") ||
normalized.includes("qwen") ||
normalized.includes("video") ||
normalized.includes("runway") ||
normalized.includes("minimax") ||
normalized.includes("kling") ||
normalized.includes("sora") ||
normalized.includes("veo")
);
}
export function getVideoModelsForProvider(
providerId: string,
customModels?: string[],
): string[] {
if (customModels && customModels.length > 0) {
return customModels;
}
const normalizedId = providerId.toLowerCase();
for (const [key, models] of Object.entries(VIDEO_MODEL_PRESETS)) {
if (normalizedId.includes(key)) {
return models;
}
}
return [];
}
export function findVideoProviderForSelection<T extends MediaProviderCandidate>(
providers: T[],
modelType: VideoModelPreset,
): T | null {
const preferredKeywords: string[] =
modelType === "keling"
? ["kling", "hailuo", "minimax"]
: modelType === "jimeng"
? ["doubao", "volc"]
: ["dashscope", "alibaba", "qwen"];
for (const keyword of preferredKeywords) {
const matched = providers.find((provider) =>
provider.id.toLowerCase().includes(keyword),
);
if (matched) {
return matched;
}
}
return providers[0] ?? null;
}
export function pickVideoModelByVersion(
models: string[],
version: VideoModelVersion,
): string {
if (models.length === 0) {
return "";
}
const normalizedModels = models.map((model) => model.toLowerCase());
const priorities =
version === "v2-1-master"
? ["2.1", "master", "pro", "turbo"]
: version === "v2"
? ["v2", "2.0", "pro", "turbo"]
: ["1.6", "v1-6", "lite", "1.5"];
for (const keyword of priorities) {
const index = normalizedModels.findIndex((model) =>
model.includes(keyword),
);
if (index >= 0) {
return models[index] ?? models[0] ?? "";
}
}
return models[0] ?? "";
}
export function isTtsProvider(
providerId: string,
providerType: string,
): boolean {
const normalized = `${providerId}:${providerType}`.toLowerCase();
return (
normalized.includes("openai") ||
normalized.includes("new-api") ||
normalized.includes("azure") ||
normalized.includes("google") ||
normalized.includes("voice") ||
normalized.includes("tts")
);
}
export function findTtsProviderForSelection<T extends MediaProviderCandidate>(
providers: T[],
): T | null {
const preferredKeywords = ["openai", "new-api", "azure", "google", "tts"];
for (const keyword of preferredKeywords) {
const matched = providers.find((provider) =>
`${provider.id}:${provider.type ?? ""}`.toLowerCase().includes(keyword),
);
if (matched) {
return matched;
}
}
return providers[0] ?? null;
}
export function getTtsModelsForProvider(customModels?: string[]): string[] {
if (customModels && customModels.length > 0) {
return customModels;
}
return ["gpt-4o-mini-tts"];
}
export function pickTtsModel(models: string[]): string {
if (models.length === 0) {
return "gpt-4o-mini-tts";
}
const normalized = models.map((model) => model.toLowerCase());
const preferredKeywords = ["tts", "speech", "audio", "gpt-4o-mini-tts"];
for (const keyword of preferredKeywords) {
const index = normalized.findIndex((model) => model.includes(keyword));
if (index >= 0) {
return models[index] ?? "gpt-4o-mini-tts";
}
}
const nonImageModel = models.find(
(model) => !model.toLowerCase().includes("image"),
);
if (nonImageModel) {
return nonImageModel;
}
return "gpt-4o-mini-tts";
}
+52
View File
@@ -0,0 +1,52 @@
export const EDITOR_PERF_DEBUG_KEY = "proxycast:editor-perf-debug";
export const INPUT_LATENCY_DEBUG_KEY = "proxycast:input-latency-debug";
interface RenderPerfPayload {
[key: string]: string | number | boolean | null | undefined;
}
export function isEditorPerfDebugEnabled(): boolean {
return isDebugFlagEnabled(EDITOR_PERF_DEBUG_KEY);
}
export function isInputLatencyDebugEnabled(): boolean {
return isDebugFlagEnabled(INPUT_LATENCY_DEBUG_KEY);
}
export function isDebugFlagEnabled(key: string): boolean {
if (typeof window === "undefined") {
return false;
}
try {
return window.localStorage.getItem(key) === "1";
} catch {
return false;
}
}
export function logRenderPerf(
component: string,
renderCount: number,
sinceLastCommitMs: number | null,
payload: RenderPerfPayload,
): void {
if (!isEditorPerfDebugEnabled()) {
return;
}
const shouldLog =
renderCount <= 5 ||
renderCount % 20 === 0 ||
(sinceLastCommitMs !== null && sinceLastCommitMs < 8);
if (!shouldLog) {
return;
}
console.debug(
`[RenderPerf] ${component}`,
JSON.stringify({
renderCount,
sinceLastCommitMs:
sinceLastCommitMs === null ? null : Number(sinceLastCommitMs.toFixed(2)),
...payload,
}),
);
}
+8
View File
@@ -1053,6 +1053,14 @@ const defaultMocks: Record<string, any> = {
generate_content_creator_tasks: () => 0,
execution_run_list: () => [],
execution_run_get: () => null,
execution_run_get_theme_workbench_state: () => ({
run_state: "idle",
current_gate_key: "idle",
queue_items: [],
latest_terminal: null,
updated_at: new Date().toISOString(),
}),
content_get_theme_workbench_document_state: () => null,
// Workspace 相关
workspace_list: () => [],