diff --git a/eslint.config.js b/eslint.config.js index f6bd4cea9..bd1bc75a0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -125,6 +125,56 @@ const generalChatRestrictedPaths = [ message: "日志相关能力已迁移到 @/lib/api/logs,请不要继续从 useTauri 聚合层引入。", }, + { + name: "@/hooks/useTauri", + importNames: [ + "checkApiCompatibility", + "ApiCheckResult", + "ApiCompatibilityResult", + ], + message: + "API 兼容性检查能力已迁移到 @/lib/api/apiCompatibility,请不要继续从 useTauri 聚合层引入。", + }, + { + name: "@/hooks/useTauri", + importNames: [ + "getEndpointProviders", + "setEndpointProvider", + "EndpointProvidersConfig", + ], + message: + "端点 Provider 配置能力已迁移到 @/lib/api/endpointProviders,请不要继续从 useTauri 聚合层引入。", + }, + { + name: "@/hooks/useTauri", + importNames: [ + "getMemoryStats", + "requestMemoryAnalysis", + "cleanupMemory", + "CleanupMemoryResult", + "MemoryAnalysisResult", + "MemoryStatsResponse", + ], + message: + "记忆分析/清理能力已迁移到 @/lib/api/memoryRuntime,请不要继续从 useTauri 聚合层引入。", + }, + { + name: "@/hooks/useTauri", + importNames: [ + "testTts", + "getAvailableVoices", + "TtsTestResult", + "VoiceOption", + ], + message: + "语音测试能力已迁移到 @/lib/api/voiceTools,请不要继续从 useTauri 聚合层引入。", + }, + { + name: "@/hooks/useTauri", + importNames: ["uploadAvatar", "deleteAvatar", "UploadResult"], + message: + "头像上传/删除能力已迁移到 @/lib/api/profileAssets,请不要继续从 useTauri 聚合层引入。", + }, { name: "@/hooks/useTauri", importNames: [ @@ -414,6 +464,14 @@ const projectGatewayCommandSelectors = [ "workspace_delete", "workspace_ensure_ready", "get_or_create_default_project", + "content_create", + "content_get", + "content_get_theme_workbench_document_state", + "content_list", + "content_update", + "content_delete", + "content_reorder", + "content_stats", ].map((command) => ({ selector: `CallExpression[callee.name='safeInvoke'][arguments.0.value='${command}'], CallExpression[callee.name='invoke'][arguments.0.value='${command}']`, message: @@ -656,6 +714,9 @@ const experimentalFeaturesCommandSelectors = [ })); const memoryRuntimeCommandSelectors = [ + "get_conversation_memory_stats", + "request_conversation_memory_analysis", + "cleanup_conversation_memory", "get_conversation_memory_overview", "memory_get_effective_sources", "memory_get_auto_index", @@ -667,6 +728,61 @@ const memoryRuntimeCommandSelectors = [ "记忆运行时相关后端命令请统一通过 `src/lib/api/memoryRuntime.ts` 暴露的网关函数调用,避免继续在其他模块中直接拼接命令名。", })); +const projectMemoryCommandSelectors = [ + "character_list", + "character_get", + "character_create", + "character_update", + "character_delete", + "world_building_get", + "world_building_update", + "style_guide_get", + "style_guide_update", + "outline_node_list", + "outline_node_get", + "outline_node_create", + "outline_node_update", + "outline_node_delete", + "project_memory_get", +].map((command) => ({ + selector: `CallExpression[callee.name='safeInvoke'][arguments.0.value='${command}'], CallExpression[callee.name='invoke'][arguments.0.value='${command}']`, + message: + "项目记忆 CRUD 相关后端命令请统一通过 `src/lib/api/memory.ts` 暴露的网关函数调用,避免继续在其他模块中直接拼接命令名。", +})); + +const apiCompatibilityCommandSelectors = ["check_api_compatibility"].map( + (command) => ({ + selector: `CallExpression[callee.name='safeInvoke'][arguments.0.value='${command}'], CallExpression[callee.name='invoke'][arguments.0.value='${command}']`, + message: + "API 兼容性检查命令请统一通过 `src/lib/api/apiCompatibility.ts` 暴露的网关函数调用,避免继续在其他模块中直接拼接命令名。", + }), +); + +const endpointProvidersCommandSelectors = [ + "get_endpoint_providers", + "set_endpoint_provider", +].map((command) => ({ + selector: `CallExpression[callee.name='safeInvoke'][arguments.0.value='${command}'], CallExpression[callee.name='invoke'][arguments.0.value='${command}']`, + message: + "端点 Provider 配置命令请统一通过 `src/lib/api/endpointProviders.ts` 暴露的网关函数调用,避免继续在其他模块中直接拼接命令名。", +})); + +const voiceToolsCommandSelectors = ["test_tts", "get_available_voices"].map( + (command) => ({ + selector: `CallExpression[callee.name='safeInvoke'][arguments.0.value='${command}'], CallExpression[callee.name='invoke'][arguments.0.value='${command}']`, + message: + "语音测试命令请统一通过 `src/lib/api/voiceTools.ts` 暴露的网关函数调用,避免继续在其他模块中直接拼接命令名。", + }), +); + +const profileAssetsCommandSelectors = ["upload_avatar", "delete_avatar"].map( + (command) => ({ + selector: `CallExpression[callee.name='safeInvoke'][arguments.0.value='${command}'], CallExpression[callee.name='invoke'][arguments.0.value='${command}']`, + message: + "头像资产命令请统一通过 `src/lib/api/profileAssets.ts` 暴露的网关函数调用,避免继续在其他模块中直接拼接命令名。", + }), +); + const modelCatalogCommandSelectors = ["get_available_models"].map( (command) => ({ selector: `CallExpression[callee.name='safeInvoke'][arguments.0.value='${command}'], CallExpression[callee.name='invoke'][arguments.0.value='${command}']`, @@ -812,10 +928,15 @@ export default [ ...channelsRuntimeCommandSelectors, ...experimentalFeaturesCommandSelectors, ...memoryRuntimeCommandSelectors, + ...projectMemoryCommandSelectors, + ...apiCompatibilityCommandSelectors, + ...endpointProvidersCommandSelectors, ...modelCatalogCommandSelectors, + ...profileAssetsCommandSelectors, ...usageStatsCommandSelectors, ...providerRuntimeCommandSelectors, ...serverToolsCommandSelectors, + ...voiceToolsCommandSelectors, ], "@typescript-eslint/no-unused-vars": [ "error", @@ -870,6 +991,7 @@ export default [ "src/lib/api/posterMaterials.ts", "src/lib/api/subAgentScheduler.ts", "src/lib/api/fileSystem.ts", + "src/lib/api/memory.ts", "src/lib/api/plugins.ts", "src/lib/api/pluginUI.ts", "src/lib/api/fileBrowser.ts", @@ -881,14 +1003,18 @@ export default [ "src/lib/api/terminal.ts", "src/lib/api/serverRuntime.ts", "src/lib/api/logs.ts", + "src/lib/api/apiCompatibility.ts", "src/lib/api/appConfig.ts", "src/lib/api/channelsRuntime.ts", + "src/lib/api/endpointProviders.ts", "src/lib/api/experimentalFeatures.ts", "src/lib/api/memoryRuntime.ts", "src/lib/api/modelCatalog.ts", + "src/lib/api/profileAssets.ts", "src/lib/api/usageStats.ts", "src/lib/api/providerRuntime.ts", "src/lib/api/serverTools.ts", + "src/lib/api/voiceTools.ts", ], rules: { "no-restricted-syntax": "off", diff --git a/src/components/agent/chat/components/EmptyState.test.tsx b/src/components/agent/chat/components/EmptyState.test.tsx index 1ecdfa220..c5ab964f5 100644 --- a/src/components/agent/chat/components/EmptyState.test.tsx +++ b/src/components/agent/chat/components/EmptyState.test.tsx @@ -204,6 +204,7 @@ describe("EmptyState", () => { description: "desc", directory: "skill-a", installed: true, + sourceKind: "builtin", }, ]; const setInput = vi.fn<(value: string) => void>(); @@ -246,6 +247,7 @@ describe("EmptyState", () => { description: "desc", directory: "canvas-design", installed: true, + sourceKind: "builtin", }; const container = renderEmptyState({ @@ -393,6 +395,7 @@ describe("EmptyState", () => { description: "desc", directory: "custom-social-skill", installed: true, + sourceKind: "builtin", }; const container = renderEmptyState({ diff --git a/src/components/agent/chat/components/HarnessStatusPanel.tsx b/src/components/agent/chat/components/HarnessStatusPanel.tsx index 606bed165..67bb5e725 100644 --- a/src/components/agent/chat/components/HarnessStatusPanel.tsx +++ b/src/components/agent/chat/components/HarnessStatusPanel.tsx @@ -293,7 +293,7 @@ function summarizeSchedulerEvent(event: SchedulerEvent): string { case "cancelled": return "调度已取消"; default: - return event.type; + return (event as { type: string }).type; } } diff --git a/src/components/agent/chat/components/Inputbar/components/CharacterMention.test.tsx b/src/components/agent/chat/components/Inputbar/components/CharacterMention.test.tsx index 5d66b8ef1..0608ff985 100644 --- a/src/components/agent/chat/components/Inputbar/components/CharacterMention.test.tsx +++ b/src/components/agent/chat/components/Inputbar/components/CharacterMention.test.tsx @@ -215,6 +215,7 @@ function createSkill(name: string, key: string, installed: boolean): Skill { description: "测试技能", directory: `${key}-dir`, installed, + sourceKind: "builtin", }; } diff --git a/src/components/agent/chat/components/ThemeWorkbenchSkillsPanel.test.tsx b/src/components/agent/chat/components/ThemeWorkbenchSkillsPanel.test.tsx index ec69707da..6b8daec33 100644 --- a/src/components/agent/chat/components/ThemeWorkbenchSkillsPanel.test.tsx +++ b/src/components/agent/chat/components/ThemeWorkbenchSkillsPanel.test.tsx @@ -41,6 +41,7 @@ function renderPanel( description: "社媒文案与封面生成", directory: "social_post_with_cover", installed: true, + sourceKind: "builtin", }, { key: "research", @@ -48,6 +49,7 @@ function renderPanel( description: "信息检索与趋势分析", directory: "research", installed: true, + sourceKind: "builtin", }, { key: "typesetting", @@ -55,6 +57,7 @@ function renderPanel( description: "主稿排版与润色", directory: "typesetting", installed: true, + sourceKind: "builtin", }, ], currentGate: { diff --git a/src/components/agent/chat/components/ThemeWorkbenchSkillsPanel.tsx b/src/components/agent/chat/components/ThemeWorkbenchSkillsPanel.tsx index 8a41f8fc8..76fec1f34 100644 --- a/src/components/agent/chat/components/ThemeWorkbenchSkillsPanel.tsx +++ b/src/components/agent/chat/components/ThemeWorkbenchSkillsPanel.tsx @@ -453,6 +453,7 @@ export function ThemeWorkbenchSkillsPanel({ description: "社媒主稿与封面图生成", directory: "social_post_with_cover", installed: true, + sourceKind: "builtin", }, { key: "cover_generate", @@ -460,6 +461,7 @@ export function ThemeWorkbenchSkillsPanel({ description: "封面图生成", directory: "cover_generate", installed: true, + sourceKind: "builtin", }, { key: "research", @@ -467,6 +469,7 @@ export function ThemeWorkbenchSkillsPanel({ description: "信息检索与趋势分析", directory: "research", installed: true, + sourceKind: "builtin", }, { key: "typesetting", @@ -474,6 +477,7 @@ export function ThemeWorkbenchSkillsPanel({ description: "主稿排版与润色", directory: "typesetting", installed: true, + sourceKind: "builtin", }, ], [], diff --git a/src/components/projects/memory/OutlinePanel.tsx b/src/components/projects/memory/OutlinePanel.tsx index c995f8eba..5913971c5 100644 --- a/src/components/projects/memory/OutlinePanel.tsx +++ b/src/components/projects/memory/OutlinePanel.tsx @@ -38,14 +38,15 @@ import { } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; import { - OutlineNode, CreateOutlineNodeRequest, + OutlineNode, + OutlineTreeNode, UpdateOutlineNodeRequest, - listOutlineNodes, - createOutlineNode, - updateOutlineNode, - deleteOutlineNode, buildOutlineTree, + createOutlineNode, + deleteOutlineNode, + listOutlineNodes, + updateOutlineNode, } from "@/lib/api/memory"; import { toast } from "sonner"; @@ -53,8 +54,6 @@ interface OutlinePanelProps { projectId: string; } -type OutlineTreeNode = OutlineNode & { children: OutlineTreeNode[] }; - interface NodeFormData { title: string; content: string; @@ -82,8 +81,7 @@ export function OutlinePanel({ projectId }: OutlinePanelProps) { try { const list = await listOutlineNodes(projectId); setNodes(list); - const treeData = buildOutlineTree(list) as OutlineTreeNode[]; - setTree(treeData); + setTree(buildOutlineTree(list)); // 默认展开所有节点 const allIds = new Set(list.map((n) => n.id)); setExpandedNodes(allIds); diff --git a/src/hooks/useProjects.ts b/src/hooks/useProjects.ts index 6e070cbef..362a2ef17 100644 --- a/src/hooks/useProjects.ts +++ b/src/hooks/useProjects.ts @@ -21,6 +21,7 @@ import { listProjects, resolveProjectRootPath, updateProject, + type ProjectType, } from "@/lib/api/project"; import { toProjectView } from "@/lib/projectView"; import { recordWorkspaceRepair } from "@/lib/workspaceHealthTelemetry"; @@ -140,7 +141,7 @@ export function useProjects(): UseProjectsReturn { const project = await createProject({ name: request.name, rootPath, - workspaceType: request.workspaceType, + workspaceType: request.workspaceType as ProjectType, }); await refresh(); return toProjectView(project); diff --git a/src/hooks/useSubAgentScheduler.ts b/src/hooks/useSubAgentScheduler.ts index 321d77d55..5fc82f023 100644 --- a/src/hooks/useSubAgentScheduler.ts +++ b/src/hooks/useSubAgentScheduler.ts @@ -26,10 +26,10 @@ export type { SubAgentTask, } from "@/lib/api/subAgentScheduler"; -interface SchedulerEventEnvelope extends SchedulerEvent { +type SchedulerEventEnvelope = SchedulerEvent & { sessionId?: string; session_id?: string; -} +}; /** * Hook 状态 diff --git a/src/hooks/useTauri.ts b/src/hooks/useTauri.ts index 7b734926b..4264fb379 100644 --- a/src/hooks/useTauri.ts +++ b/src/hooks/useTauri.ts @@ -1,112 +1,19 @@ -// 使用共享的 safeInvoke -import { - ensureDefaultWorkspaceReady as ensureDefaultProjectWorkspaceReady, - ensureWorkspaceReady as ensureProjectWorkspaceReady, +export { + checkApiCompatibility, + type ApiCheckResult, + type ApiCompatibilityResult, +} from "@/lib/api/apiCompatibility"; +export { + getEndpointProviders, + setEndpointProvider, + type EndpointProvidersConfig, +} from "@/lib/api/endpointProviders"; +export { revealPathInFinder as revealInFinder } from "@/lib/api/fileSystem"; +export { + ensureDefaultWorkspaceReady as workspaceEnsureDefaultReady, + ensureWorkspaceReady as workspaceEnsureReady, + type WorkspaceEnsureResult, } from "@/lib/api/project"; -import { revealPathInFinder } from "@/lib/api/fileSystem"; -import type { - AssistantConfig, - ChatAppearanceConfig, - Config, - ContentCreatorConfig, - CrashReportingConfig, - EnvironmentConfig, - EnvironmentPreview, - EnvironmentPreviewEntry, - EnvironmentVariableOverride, - ImageGenConfig, - MultiSearchConfig, - MultiSearchEngineEntryConfig, - NavigationConfig, - QuotaExceededConfig, - RemoteManagementConfig, - ResponseCacheConfig, - ShellImportPreview, - TlsConfig, - UserProfile, - VoiceConfig, -} from "@/lib/api/appConfig"; -import type { - ChannelsConfig, - CloudflaredInstallResult, - CloudflaredInstallStatus, - CloudflareTunnelConfig, - DiscordAccountConfig, - DiscordActionsConfig, - DiscordAgentComponentsConfig, - DiscordAutoPresenceConfig, - DiscordBotConfig, - DiscordChannelConfig, - DiscordExecApprovalsConfig, - DiscordGatewayAccountStatus, - DiscordGatewayStatus, - DiscordGuildConfig, - DiscordIntentsConfig, - DiscordProbeResult, - DiscordThreadBindingsConfig, - DiscordUiComponentsConfig, - DiscordUiConfig, - DiscordVoiceAutoJoinConfig, - DiscordVoiceConfig, - FeishuAccountConfig, - FeishuBotConfig, - FeishuGatewayAccountStatus, - FeishuGatewayStatus, - FeishuGroupConfig, - FeishuProbeResult, - GatewayChannelStatusResponse, - GatewayConfig, - GatewayTunnelConfig, - GatewayTunnelCreateResponse, - GatewayTunnelProbeResult, - GatewayTunnelStatus, - GatewayTunnelSyncWebhookResponse, - TelegramBotConfig, - TelegramGatewayAccountStatus, - TelegramGatewayStatus, - TelegramProbeResult, -} from "@/lib/api/channelsRuntime"; -import type { - ExperimentalFeatures, - SmartInputConfig, - ToolCallingConfig, -} from "@/lib/api/experimentalFeatures"; -import type { - AutoMemoryIndexItem, - AutoMemoryIndexResponse, - CleanupMemoryResult, - EffectiveMemorySource, - EffectiveMemorySourcesResponse, - MemoryAnalysisResult, - MemoryAutoConfig, - MemoryAutoToggleResponse, - MemoryCategoryStat, - MemoryConfig, - MemoryEntryPreview, - MemoryOverviewResponse, - MemoryProfileConfig, - MemoryResolveConfig, - MemorySourcesConfig, - MemoryStatsResponse, -} from "@/lib/api/memoryRuntime"; -import type { - AmpConfig, - AmpModelMapping, - ApiKeyEntry, - CheckResult, - ClaudeCustomStatus, - CredentialEntry, - CredentialPoolConfig, - EnvVariable, - GeminiApiKeyEntry, - GeminiCredentialStatus, - IFlowCredentialEntry, - KiroCredentialStatus, - OpenAICustomStatus, - QwenCredentialStatus, - VertexApiKeyEntry, - VertexModelAlias, -} from "@/lib/api/providerRuntime"; export { checkAndReloadCredentials, checkAndReloadGeminiCredentials, @@ -170,6 +77,11 @@ export { startServer, stopServer, } from "@/lib/api/serverRuntime"; +export { + deleteAvatar, + type UploadResult, + uploadAvatar, +} from "@/lib/api/profileAssets"; export { getConfig, getDefaultProvider, @@ -207,9 +119,12 @@ export { getPersistedLogsTail, } from "@/lib/api/logs"; export { + cleanupMemory, + getMemoryStats, getMemoryAutoIndex, getMemoryEffectiveSources, getMemoryOverview, + requestMemoryAnalysis, toggleMemoryAuto, updateMemoryAutoNote, } from "@/lib/api/memoryRuntime"; @@ -320,32 +235,12 @@ export type { WindowsStartupDiagnostics, } from "@/lib/api/serverRuntime"; export type { LogEntry } from "@/lib/api/logs"; -import { safeInvoke } from "@/lib/dev-bridge"; - -export async function revealInFinder(path: string): Promise { - return revealPathInFinder(path); -} - -export interface WorkspaceEnsureResult { - workspaceId: string; - rootPath: string; - existed: boolean; - created: boolean; - repaired: boolean; - relocated?: boolean; - previousRootPath?: string | null; - warning?: string | null; -} - -export async function workspaceEnsureReady( - id: string, -): Promise { - return ensureProjectWorkspaceReady(id); -} - -export async function workspaceEnsureDefaultReady(): Promise { - return ensureDefaultProjectWorkspaceReady(); -} +export { + getAvailableVoices, + testTts, + type TtsTestResult, + type VoiceOption, +} from "@/lib/api/voiceTools"; /** * 更新 Provider 的环境变量 @@ -357,170 +252,3 @@ export async function workspaceEnsureDefaultReady(): Promise { - return safeInvoke("check_api_compatibility", { provider }); -} - -// ============ Endpoint Provider Configuration ============ - -/** - * 端点 Provider 配置 - * 为不同客户端类型配置不同的 LLM Provider - */ -export interface EndpointProvidersConfig { - /** Cursor 客户端使用的 Provider */ - cursor?: string | null; - /** Claude Code 客户端使用的 Provider */ - claude_code?: string | null; - /** Codex 客户端使用的 Provider */ - codex?: string | null; - /** Windsurf 客户端使用的 Provider */ - windsurf?: string | null; - /** Kiro 客户端使用的 Provider */ - kiro?: string | null; - /** 其他客户端使用的 Provider */ - other?: string | null; -} - -/** - * 获取端点 Provider 配置 - * @returns 端点 Provider 配置对象 - */ -export async function getEndpointProviders(): Promise { - return safeInvoke("get_endpoint_providers"); -} - -/** - * 设置端点 Provider 配置 - * @param clientType 客户端类型 (cursor, claude_code, codex, windsurf, kiro, other) - * @param provider Provider 名称,传 null 表示使用默认 Provider - * @returns 设置后的 Provider 名称 - */ -export async function setEndpointProvider( - clientType: string, - provider: string | null, -): Promise { - return safeInvoke("set_endpoint_provider", { - endpoint: clientType, - provider, - }); -} - -// Network Info -// ============ 实验室功能 API ============ - -/** - * 获取实验室功能配置 - * @returns 实验室功能配置对象 - */ -// ============ 记忆管理 API ============ - -/** - * 获取记忆统计信息 - */ -export async function getMemoryStats(): Promise { - return safeInvoke("get_conversation_memory_stats"); -} - -/** - * 获取记忆总览(含分类与条目) - */ -export async function requestMemoryAnalysis( - fromTimestamp?: number, - toTimestamp?: number, -): Promise { - return safeInvoke("request_conversation_memory_analysis", { - fromTimestamp, - toTimestamp, - }); -} - -/** - * 清理过期记忆 - */ -export async function cleanupMemory(): Promise { - return safeInvoke("cleanup_conversation_memory"); -} - -/** - * 获取记忆来源解析结果 - */ -// ============ 语音测试 API ============ - -export interface TtsTestResult { - success: boolean; - error: string | null; - audio_path: string | null; -} - -export interface VoiceOption { - id: string; - name: string; - language: string; -} - -/** - * 测试 TTS 语音合成 - * @param service TTS 服务名称 - * @param voice 语音 ID - */ -export async function testTts( - service: string, - voice: string, -): Promise { - return safeInvoke("test_tts", { service, voice }); -} - -/** - * 获取可用的语音列表 - * @param service TTS 服务名称 - */ -export async function getAvailableVoices( - service: string, -): Promise { - return safeInvoke("get_available_voices", { service }); -} - -// ============ 文件上传 API ============ - -export interface UploadResult { - url: string; - size: number; -} - -/** - * 上传用户头像 - * @param filePath 文件路径 - */ -export async function uploadAvatar(filePath: string): Promise { - return safeInvoke("upload_avatar", { filePath }); -} - -/** - * 删除用户头像 - * @param url 头像 URL - */ -export async function deleteAvatar(url: string): Promise { - return safeInvoke("delete_avatar", { url }); -} diff --git a/src/lib/api/apiCompatibility.test.ts b/src/lib/api/apiCompatibility.test.ts new file mode 100644 index 000000000..1a8e7db16 --- /dev/null +++ b/src/lib/api/apiCompatibility.test.ts @@ -0,0 +1,26 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { safeInvoke } from "@/lib/dev-bridge"; +import { checkApiCompatibility } from "./apiCompatibility"; + +vi.mock("@/lib/dev-bridge", () => ({ + safeInvoke: vi.fn(), +})); + +describe("apiCompatibility API", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("应代理兼容性检查命令", async () => { + vi.mocked(safeInvoke).mockResolvedValueOnce({ + provider: "openai", + overall_status: "ok", + results: [], + warnings: [], + }); + + await expect(checkApiCompatibility("openai")).resolves.toEqual( + expect.objectContaining({ overall_status: "ok" }), + ); + }); +}); diff --git a/src/lib/api/apiCompatibility.ts b/src/lib/api/apiCompatibility.ts new file mode 100644 index 000000000..117a9604e --- /dev/null +++ b/src/lib/api/apiCompatibility.ts @@ -0,0 +1,24 @@ +import { safeInvoke } from "@/lib/dev-bridge"; + +export interface ApiCheckResult { + model: string; + available: boolean; + status: number; + error_type: string | null; + error_message: string | null; + time_ms: number; +} + +export interface ApiCompatibilityResult { + provider: string; + overall_status: string; + checked_at: string; + results: ApiCheckResult[]; + warnings: string[]; +} + +export async function checkApiCompatibility( + provider: string, +): Promise { + return safeInvoke("check_api_compatibility", { provider }); +} diff --git a/src/lib/api/brandPersona.test.ts b/src/lib/api/brandPersona.test.ts index 979d6b633..a1b2ac1fb 100644 --- a/src/lib/api/brandPersona.test.ts +++ b/src/lib/api/brandPersona.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import { deleteBrandExtension, getBrandExtension, @@ -9,8 +9,8 @@ import { updateBrandExtension, } from "./brandPersona"; -vi.mock("@tauri-apps/api/core", () => ({ - invoke: vi.fn(), +vi.mock("@/lib/dev-bridge", () => ({ + safeInvoke: vi.fn(), })); describe("brandPersona API", () => { @@ -19,7 +19,7 @@ describe("brandPersona API", () => { }); it("应获取品牌人设与扩展", async () => { - vi.mocked(invoke) + vi.mocked(safeInvoke) .mockResolvedValueOnce({ id: "persona-1", name: "品牌人设" }) .mockResolvedValueOnce({ personaId: "persona-1", brandTone: "专业" }); @@ -32,7 +32,7 @@ describe("brandPersona API", () => { }); it("应代理品牌扩展写操作与模板列表", async () => { - vi.mocked(invoke) + vi.mocked(safeInvoke) .mockResolvedValueOnce({ personaId: "persona-2" }) .mockResolvedValueOnce({ personaId: "persona-2" }) .mockResolvedValueOnce(undefined) diff --git a/src/lib/api/brandPersona.ts b/src/lib/api/brandPersona.ts index aab55f887..dcb589e44 100644 --- a/src/lib/api/brandPersona.ts +++ b/src/lib/api/brandPersona.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import type { BrandPersona, BrandPersonaExtension, @@ -10,13 +10,13 @@ import type { export async function getBrandPersona( personaId: string, ): Promise { - return invoke("get_brand_persona", { personaId }); + return safeInvoke("get_brand_persona", { personaId }); } export async function getBrandExtension( personaId: string, ): Promise { - return invoke("get_brand_extension", { + return safeInvoke("get_brand_extension", { personaId, }); } @@ -24,7 +24,7 @@ export async function getBrandExtension( export async function saveBrandExtension( request: CreateBrandExtensionRequest, ): Promise { - return invoke("save_brand_extension", { + return safeInvoke("save_brand_extension", { req: request, }); } @@ -33,18 +33,18 @@ export async function updateBrandExtension( personaId: string, update: UpdateBrandExtensionRequest, ): Promise { - return invoke("update_brand_extension", { + return safeInvoke("update_brand_extension", { personaId, update, }); } export async function deleteBrandExtension(personaId: string): Promise { - await invoke("delete_brand_extension", { personaId }); + await safeInvoke("delete_brand_extension", { personaId }); } export async function listBrandPersonaTemplates(): Promise< BrandPersonaTemplate[] > { - return invoke("list_brand_persona_templates"); + return safeInvoke("list_brand_persona_templates"); } diff --git a/src/lib/api/endpointProviders.test.ts b/src/lib/api/endpointProviders.test.ts new file mode 100644 index 000000000..671f33df6 --- /dev/null +++ b/src/lib/api/endpointProviders.test.ts @@ -0,0 +1,26 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { safeInvoke } from "@/lib/dev-bridge"; +import { getEndpointProviders, setEndpointProvider } from "./endpointProviders"; + +vi.mock("@/lib/dev-bridge", () => ({ + safeInvoke: vi.fn(), +})); + +describe("endpointProviders API", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("应代理端点 Provider 配置读取与保存", async () => { + vi.mocked(safeInvoke) + .mockResolvedValueOnce({ cursor: "openai" }) + .mockResolvedValueOnce("claude"); + + await expect(getEndpointProviders()).resolves.toEqual( + expect.objectContaining({ cursor: "openai" }), + ); + await expect(setEndpointProvider("cursor", "claude")).resolves.toBe( + "claude", + ); + }); +}); diff --git a/src/lib/api/endpointProviders.ts b/src/lib/api/endpointProviders.ts new file mode 100644 index 000000000..3f8705b59 --- /dev/null +++ b/src/lib/api/endpointProviders.ts @@ -0,0 +1,24 @@ +import { safeInvoke } from "@/lib/dev-bridge"; + +export interface EndpointProvidersConfig { + cursor?: string | null; + claude_code?: string | null; + codex?: string | null; + windsurf?: string | null; + kiro?: string | null; + other?: string | null; +} + +export async function getEndpointProviders(): Promise { + return safeInvoke("get_endpoint_providers"); +} + +export async function setEndpointProvider( + clientType: string, + provider: string | null, +): Promise { + return safeInvoke("set_endpoint_provider", { + endpoint: clientType, + provider, + }); +} diff --git a/src/lib/api/materials.test.ts b/src/lib/api/materials.test.ts index d04c8b07a..a8445b678 100644 --- a/src/lib/api/materials.test.ts +++ b/src/lib/api/materials.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import { deleteMaterial, getMaterialContent, @@ -11,8 +11,8 @@ import { uploadMaterial, } from "./materials"; -vi.mock("@tauri-apps/api/core", () => ({ - invoke: vi.fn(), +vi.mock("@/lib/dev-bridge", () => ({ + safeInvoke: vi.fn(), })); describe("materials API", () => { @@ -48,7 +48,7 @@ describe("materials API", () => { }); it("listMaterials 应返回规范化后的素材数组", async () => { - vi.mocked(invoke).mockResolvedValueOnce([ + vi.mocked(safeInvoke).mockResolvedValueOnce([ { id: "m1", project_id: "project-1", @@ -66,7 +66,7 @@ describe("materials API", () => { }), ]); - expect(invoke).toHaveBeenCalledWith("list_materials", { + expect(safeInvoke).toHaveBeenCalledWith("list_materials", { projectId: "project-1", project_id: "project-1", filter: null, @@ -74,17 +74,17 @@ describe("materials API", () => { }); it("getMaterialCount 应调用统计命令", async () => { - vi.mocked(invoke).mockResolvedValueOnce(3); + vi.mocked(safeInvoke).mockResolvedValueOnce(3); await expect(getMaterialCount("project-2")).resolves.toBe(3); - expect(invoke).toHaveBeenCalledWith("get_material_count", { + expect(safeInvoke).toHaveBeenCalledWith("get_material_count", { projectId: "project-2", project_id: "project-2", }); }); it("uploadMaterial 应发送兼容字段并规范化返回值", async () => { - vi.mocked(invoke).mockResolvedValueOnce({ + vi.mocked(safeInvoke).mockResolvedValueOnce({ id: "m2", project_id: "project-3", material_type: "image", @@ -108,7 +108,7 @@ describe("materials API", () => { }), ); - expect(invoke).toHaveBeenCalledWith("upload_material", { + expect(safeInvoke).toHaveBeenCalledWith("upload_material", { req: expect.objectContaining({ projectId: "project-3", project_id: "project-3", @@ -119,7 +119,7 @@ describe("materials API", () => { }); it("importMaterialFromUrl 应统一走网关请求格式", async () => { - vi.mocked(invoke).mockResolvedValueOnce({ id: "m3" }); + vi.mocked(safeInvoke).mockResolvedValueOnce({ id: "m3" }); await expect( importMaterialFromUrl({ @@ -131,7 +131,7 @@ describe("materials API", () => { }), ).resolves.toEqual({ id: "m3" }); - expect(invoke).toHaveBeenCalledWith("import_material_from_url", { + expect(safeInvoke).toHaveBeenCalledWith("import_material_from_url", { req: expect.objectContaining({ projectId: "project-4", project_id: "project-4", @@ -141,7 +141,7 @@ describe("materials API", () => { }); it("updateMaterial / deleteMaterial / getMaterialContent 应代理到对应命令", async () => { - vi.mocked(invoke) + vi.mocked(safeInvoke) .mockResolvedValueOnce({ id: "m4", projectId: "project-5", @@ -160,14 +160,14 @@ describe("materials API", () => { await expect(deleteMaterial("m4")).resolves.toBeUndefined(); await expect(getMaterialContent("m4")).resolves.toBe("hello"); - expect(invoke).toHaveBeenNthCalledWith(1, "update_material", { + expect(safeInvoke).toHaveBeenNthCalledWith(1, "update_material", { id: "m4", update: { name: "new-name" }, }); - expect(invoke).toHaveBeenNthCalledWith(2, "delete_material", { + expect(safeInvoke).toHaveBeenNthCalledWith(2, "delete_material", { id: "m4", }); - expect(invoke).toHaveBeenNthCalledWith(3, "get_material_content", { + expect(safeInvoke).toHaveBeenNthCalledWith(3, "get_material_content", { id: "m4", }); }); diff --git a/src/lib/api/materials.ts b/src/lib/api/materials.ts index d1681a18c..a6d680dae 100644 --- a/src/lib/api/materials.ts +++ b/src/lib/api/materials.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import type { Material, MaterialFilter, @@ -88,7 +88,7 @@ export async function listMaterials( projectId: string, filter?: MaterialFilter | null, ): Promise { - const materials = await invoke("list_materials", { + const materials = await safeInvoke("list_materials", { projectId, project_id: projectId, filter: filter ?? null, @@ -103,7 +103,7 @@ export async function listMaterials( } export async function getMaterialCount(projectId: string): Promise { - return invoke("get_material_count", { + return safeInvoke("get_material_count", { projectId, project_id: projectId, }); @@ -112,7 +112,7 @@ export async function getMaterialCount(projectId: string): Promise { export async function uploadMaterial( request: UploadMaterialRequest, ): Promise { - const material = await invoke("upload_material", { + const material = await safeInvoke("upload_material", { req: buildUploadRequestPayload(request), }); return normalizeMaterial(material, request.projectId); @@ -121,7 +121,7 @@ export async function uploadMaterial( export async function importMaterialFromUrl( request: ImportMaterialFromUrlRequest, ): Promise { - return invoke("import_material_from_url", { + return safeInvoke("import_material_from_url", { req: buildImportRequestPayload(request), }); } @@ -130,7 +130,7 @@ export async function updateMaterial( id: string, update: MaterialUpdate, ): Promise { - const material = await invoke("update_material", { + const material = await safeInvoke("update_material", { id, update, }); @@ -138,9 +138,9 @@ export async function updateMaterial( } export async function deleteMaterial(id: string): Promise { - await invoke("delete_material", { id }); + await safeInvoke("delete_material", { id }); } export async function getMaterialContent(id: string): Promise { - return invoke("get_material_content", { id }); + return safeInvoke("get_material_content", { id }); } diff --git a/src/lib/api/memory.test.ts b/src/lib/api/memory.test.ts new file mode 100644 index 000000000..97612c0a9 --- /dev/null +++ b/src/lib/api/memory.test.ts @@ -0,0 +1,210 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { safeInvoke } from "@/lib/dev-bridge"; +import { + buildOutlineTree, + createCharacter, + createOutlineNode, + deleteCharacter, + deleteOutlineNode, + getCharacter, + getOutlineNode, + getProjectMemory, + getStyleGuide, + getWorldBuilding, + listCharacters, + listOutlineNodes, + updateCharacter, + updateOutlineNode, + updateStyleGuide, + updateWorldBuilding, +} from "./memory"; + +vi.mock("@/lib/dev-bridge", () => ({ + safeInvoke: vi.fn(), +})); + +describe("memory API", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("应代理角色 CRUD 命令", async () => { + vi.mocked(safeInvoke) + .mockResolvedValueOnce([{ id: "c1", name: "角色1" }]) + .mockResolvedValueOnce({ id: "c1", name: "角色1" }) + .mockResolvedValueOnce({ id: "c2", name: "角色2" }) + .mockResolvedValueOnce({ id: "c2", name: "角色2-更新" }) + .mockResolvedValueOnce(true); + + await expect(listCharacters("project-1")).resolves.toEqual([ + expect.objectContaining({ id: "c1" }), + ]); + await expect(getCharacter("c1")).resolves.toEqual( + expect.objectContaining({ id: "c1" }), + ); + await expect( + createCharacter({ project_id: "project-1", name: "角色2" }), + ).resolves.toEqual(expect.objectContaining({ id: "c2" })); + await expect( + updateCharacter("c2", { name: "角色2-更新" }), + ).resolves.toEqual(expect.objectContaining({ id: "c2" })); + await expect(deleteCharacter("c2")).resolves.toBe(true); + + expect(safeInvoke).toHaveBeenNthCalledWith(1, "character_list", { + projectId: "project-1", + }); + expect(safeInvoke).toHaveBeenNthCalledWith(2, "character_get", { + id: "c1", + }); + expect(safeInvoke).toHaveBeenNthCalledWith(3, "character_create", { + request: { project_id: "project-1", name: "角色2" }, + }); + expect(safeInvoke).toHaveBeenNthCalledWith(4, "character_update", { + id: "c2", + request: { name: "角色2-更新" }, + }); + expect(safeInvoke).toHaveBeenNthCalledWith(5, "character_delete", { + id: "c2", + }); + }); + + it("应代理世界观与风格指南命令", async () => { + vi.mocked(safeInvoke) + .mockResolvedValueOnce({ project_id: "project-1", description: "世界观" }) + .mockResolvedValueOnce({ + project_id: "project-1", + description: "更新后的世界观", + }) + .mockResolvedValueOnce({ project_id: "project-1", style: "克制" }) + .mockResolvedValueOnce({ project_id: "project-1", style: "冷静" }); + + await expect(getWorldBuilding("project-1")).resolves.toEqual( + expect.objectContaining({ description: "世界观" }), + ); + await expect( + updateWorldBuilding("project-1", { description: "更新后的世界观" }), + ).resolves.toEqual( + expect.objectContaining({ description: "更新后的世界观" }), + ); + await expect(getStyleGuide("project-1")).resolves.toEqual( + expect.objectContaining({ style: "克制" }), + ); + await expect( + updateStyleGuide("project-1", { style: "冷静" }), + ).resolves.toEqual(expect.objectContaining({ style: "冷静" })); + + expect(safeInvoke).toHaveBeenNthCalledWith(1, "world_building_get", { + projectId: "project-1", + }); + expect(safeInvoke).toHaveBeenNthCalledWith(2, "world_building_update", { + projectId: "project-1", + request: { description: "更新后的世界观" }, + }); + expect(safeInvoke).toHaveBeenNthCalledWith(3, "style_guide_get", { + projectId: "project-1", + }); + expect(safeInvoke).toHaveBeenNthCalledWith(4, "style_guide_update", { + projectId: "project-1", + request: { style: "冷静" }, + }); + }); + + it("应代理大纲与项目记忆命令", async () => { + vi.mocked(safeInvoke) + .mockResolvedValueOnce([{ id: "n1", title: "第一章", order: 1 }]) + .mockResolvedValueOnce({ id: "n1", title: "第一章", order: 1 }) + .mockResolvedValueOnce({ id: "n2", title: "第二章", order: 2 }) + .mockResolvedValueOnce({ id: "n2", title: "第二章-修订", order: 2 }) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce({ + characters: [], + outline: [], + }); + + await expect(listOutlineNodes("project-1")).resolves.toEqual([ + expect.objectContaining({ id: "n1" }), + ]); + await expect(getOutlineNode("n1")).resolves.toEqual( + expect.objectContaining({ id: "n1" }), + ); + await expect( + createOutlineNode({ project_id: "project-1", title: "第二章" }), + ).resolves.toEqual(expect.objectContaining({ id: "n2" })); + await expect( + updateOutlineNode("n2", { title: "第二章-修订" }), + ).resolves.toEqual(expect.objectContaining({ id: "n2" })); + await expect(deleteOutlineNode("n2")).resolves.toBe(true); + await expect(getProjectMemory("project-1")).resolves.toEqual( + expect.objectContaining({ characters: [], outline: [] }), + ); + + expect(safeInvoke).toHaveBeenNthCalledWith(1, "outline_node_list", { + projectId: "project-1", + }); + expect(safeInvoke).toHaveBeenNthCalledWith(2, "outline_node_get", { + id: "n1", + }); + expect(safeInvoke).toHaveBeenNthCalledWith(3, "outline_node_create", { + request: { project_id: "project-1", title: "第二章" }, + }); + expect(safeInvoke).toHaveBeenNthCalledWith(4, "outline_node_update", { + id: "n2", + request: { title: "第二章-修订" }, + }); + expect(safeInvoke).toHaveBeenNthCalledWith(5, "outline_node_delete", { + id: "n2", + }); + expect(safeInvoke).toHaveBeenNthCalledWith(6, "project_memory_get", { + projectId: "project-1", + }); + }); + + it("应按父子关系和顺序构建大纲树", () => { + const tree = buildOutlineTree([ + { + id: "child-2", + project_id: "p1", + parent_id: "root-1", + title: "子节点 2", + order: 2, + expanded: true, + created_at: "", + updated_at: "", + }, + { + id: "root-1", + project_id: "p1", + title: "根节点 1", + order: 2, + expanded: true, + created_at: "", + updated_at: "", + }, + { + id: "child-1", + project_id: "p1", + parent_id: "root-1", + title: "子节点 1", + order: 1, + expanded: true, + created_at: "", + updated_at: "", + }, + { + id: "root-0", + project_id: "p1", + title: "根节点 0", + order: 1, + expanded: true, + created_at: "", + updated_at: "", + }, + ]); + + expect(tree.map((node) => node.id)).toEqual(["root-0", "root-1"]); + expect(tree[1].children.map((node) => node.id)).toEqual([ + "child-1", + "child-2", + ]); + }); +}); diff --git a/src/lib/api/memory.ts b/src/lib/api/memory.ts index dbbe32788..42cc660f8 100644 --- a/src/lib/api/memory.ts +++ b/src/lib/api/memory.ts @@ -4,7 +4,7 @@ * 提供角色、世界观、风格指南、大纲的 CRUD 操作 */ -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; // ==================== 类型定义 ==================== @@ -118,6 +118,11 @@ export interface OutlineNode { updated_at: string; } +/** 大纲树节点 */ +export interface OutlineTreeNode extends OutlineNode { + children: OutlineTreeNode[]; +} + /** 创建大纲节点请求 */ export interface CreateOutlineNodeRequest { project_id: string; @@ -151,19 +156,19 @@ export interface ProjectMemory { /** 获取角色列表 */ export async function listCharacters(projectId: string): Promise { - return invoke("character_list", { projectId }); + return safeInvoke("character_list", { projectId }); } /** 获取角色详情 */ export async function getCharacter(id: string): Promise { - return invoke("character_get", { id }); + return safeInvoke("character_get", { id }); } /** 创建角色 */ export async function createCharacter( request: CreateCharacterRequest, ): Promise { - return invoke("character_create", { request }); + return safeInvoke("character_create", { request }); } /** 更新角色 */ @@ -171,12 +176,12 @@ export async function updateCharacter( id: string, request: UpdateCharacterRequest, ): Promise { - return invoke("character_update", { id, request }); + return safeInvoke("character_update", { id, request }); } /** 删除角色 */ export async function deleteCharacter(id: string): Promise { - return invoke("character_delete", { id }); + return safeInvoke("character_delete", { id }); } // ==================== 世界观 API ==================== @@ -185,7 +190,7 @@ export async function deleteCharacter(id: string): Promise { export async function getWorldBuilding( projectId: string, ): Promise { - return invoke("world_building_get", { projectId }); + return safeInvoke("world_building_get", { projectId }); } /** 更新世界观 */ @@ -193,7 +198,10 @@ export async function updateWorldBuilding( projectId: string, request: UpdateWorldBuildingRequest, ): Promise { - return invoke("world_building_update", { projectId, request }); + return safeInvoke("world_building_update", { + projectId, + request, + }); } // ==================== 风格指南 API ==================== @@ -202,7 +210,7 @@ export async function updateWorldBuilding( export async function getStyleGuide( projectId: string, ): Promise { - return invoke("style_guide_get", { projectId }); + return safeInvoke("style_guide_get", { projectId }); } /** 更新风格指南 */ @@ -210,7 +218,7 @@ export async function updateStyleGuide( projectId: string, request: UpdateStyleGuideRequest, ): Promise { - return invoke("style_guide_update", { projectId, request }); + return safeInvoke("style_guide_update", { projectId, request }); } // ==================== 大纲 API ==================== @@ -219,19 +227,19 @@ export async function updateStyleGuide( export async function listOutlineNodes( projectId: string, ): Promise { - return invoke("outline_node_list", { projectId }); + return safeInvoke("outline_node_list", { projectId }); } /** 获取大纲节点详情 */ export async function getOutlineNode(id: string): Promise { - return invoke("outline_node_get", { id }); + return safeInvoke("outline_node_get", { id }); } /** 创建大纲节点 */ export async function createOutlineNode( request: CreateOutlineNodeRequest, ): Promise { - return invoke("outline_node_create", { request }); + return safeInvoke("outline_node_create", { request }); } /** 更新大纲节点 */ @@ -239,12 +247,12 @@ export async function updateOutlineNode( id: string, request: UpdateOutlineNodeRequest, ): Promise { - return invoke("outline_node_update", { id, request }); + return safeInvoke("outline_node_update", { id, request }); } /** 删除大纲节点 */ export async function deleteOutlineNode(id: string): Promise { - return invoke("outline_node_delete", { id }); + return safeInvoke("outline_node_delete", { id }); } // ==================== 聚合 API ==================== @@ -253,17 +261,15 @@ export async function deleteOutlineNode(id: string): Promise { export async function getProjectMemory( projectId: string, ): Promise { - return invoke("project_memory_get", { projectId }); + return safeInvoke("project_memory_get", { projectId }); } // ==================== 辅助函数 ==================== /** 构建大纲树结构 */ -export function buildOutlineTree( - nodes: OutlineNode[], -): (OutlineNode & { children: OutlineNode[] })[] { - const nodeMap = new Map(); - const roots: (OutlineNode & { children: OutlineNode[] })[] = []; +export function buildOutlineTree(nodes: OutlineNode[]): OutlineTreeNode[] { + const nodeMap = new Map(); + const roots: OutlineTreeNode[] = []; // 初始化所有节点 nodes.forEach((node) => { @@ -286,11 +292,9 @@ export function buildOutlineTree( }); // 按 order 排序 - const sortByOrder = ( - items: (OutlineNode & { children: OutlineNode[] })[], - ) => { + const sortByOrder = (items: OutlineTreeNode[]) => { items.sort((a, b) => a.order - b.order); - items.forEach((item) => sortByOrder(item.children as any)); + items.forEach((item) => sortByOrder(item.children)); }; sortByOrder(roots); diff --git a/src/lib/api/memoryRuntime.test.ts b/src/lib/api/memoryRuntime.test.ts index 3498405c5..e1c80cc76 100644 --- a/src/lib/api/memoryRuntime.test.ts +++ b/src/lib/api/memoryRuntime.test.ts @@ -1,9 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { safeInvoke } from "@/lib/dev-bridge"; import { + cleanupMemory, getMemoryAutoIndex, getMemoryEffectiveSources, getMemoryOverview, + getMemoryStats, + requestMemoryAnalysis, toggleMemoryAuto, updateMemoryAutoNote, } from "./memoryRuntime"; @@ -19,10 +22,26 @@ describe("memoryRuntime API", () => { it("应代理记忆查询命令", async () => { vi.mocked(safeInvoke) + .mockResolvedValueOnce({ + total_entries: 1, + storage_used: 2, + memory_count: 3, + }) + .mockResolvedValueOnce({ analyzed_sessions: 1 }) + .mockResolvedValueOnce({ cleaned_entries: 1, freed_space: 2 }) .mockResolvedValueOnce({ stats: {}, categories: [], entries: [] }) .mockResolvedValueOnce({ sources: [] }) .mockResolvedValueOnce({ items: [] }); + await expect(getMemoryStats()).resolves.toEqual( + expect.objectContaining({ total_entries: 1 }), + ); + await expect(requestMemoryAnalysis()).resolves.toEqual( + expect.objectContaining({ analyzed_sessions: 1 }), + ); + await expect(cleanupMemory()).resolves.toEqual( + expect.objectContaining({ cleaned_entries: 1 }), + ); await expect(getMemoryOverview(200)).resolves.toEqual( expect.objectContaining({ entries: [] }), ); diff --git a/src/lib/api/memoryRuntime.ts b/src/lib/api/memoryRuntime.ts index 83e746591..77382e778 100644 --- a/src/lib/api/memoryRuntime.ts +++ b/src/lib/api/memoryRuntime.ts @@ -1,8 +1,11 @@ import { safeInvoke } from "@/lib/dev-bridge"; import type { AutoMemoryIndexResponse, + CleanupMemoryResult, EffectiveMemorySourcesResponse, + MemoryAnalysisResult, MemoryAutoToggleResponse, + MemoryStatsResponse, MemoryOverviewResponse, } from "./memoryRuntimeTypes"; @@ -31,6 +34,24 @@ export async function getMemoryOverview( return safeInvoke("get_conversation_memory_overview", { limit }); } +export async function getMemoryStats(): Promise { + return safeInvoke("get_conversation_memory_stats"); +} + +export async function requestMemoryAnalysis( + fromTimestamp?: number, + toTimestamp?: number, +): Promise { + return safeInvoke("request_conversation_memory_analysis", { + fromTimestamp, + toTimestamp, + }); +} + +export async function cleanupMemory(): Promise { + return safeInvoke("cleanup_conversation_memory"); +} + export async function getMemoryEffectiveSources( workingDir?: string, activeRelativePath?: string, diff --git a/src/lib/api/notification.ts b/src/lib/api/notification.ts index 404eeca0d..85558b1ee 100644 --- a/src/lib/api/notification.ts +++ b/src/lib/api/notification.ts @@ -9,5 +9,5 @@ export interface ShowNotificationRequest { export async function showSystemNotification( request: ShowNotificationRequest, ): Promise { - await safeInvoke("show_notification", request); + await safeInvoke("show_notification", request as unknown as Record); } diff --git a/src/lib/api/personas.test.ts b/src/lib/api/personas.test.ts index a532a9766..25f3f7a71 100644 --- a/src/lib/api/personas.test.ts +++ b/src/lib/api/personas.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import { createPersona, deletePersona, @@ -10,8 +10,8 @@ import { updatePersona, } from "./personas"; -vi.mock("@tauri-apps/api/core", () => ({ - invoke: vi.fn(), +vi.mock("@/lib/dev-bridge", () => ({ + safeInvoke: vi.fn(), })); describe("personas API", () => { @@ -20,7 +20,7 @@ describe("personas API", () => { }); it("应获取人设列表、默认人设和模板列表", async () => { - vi.mocked(invoke) + vi.mocked(safeInvoke) .mockResolvedValueOnce([{ id: "p1", name: "人设 1" }]) .mockResolvedValueOnce({ id: "p1", name: "人设 1" }) .mockResolvedValueOnce([{ id: "pt1", name: "模板人设" }]); @@ -37,7 +37,7 @@ describe("personas API", () => { }); it("应代理人设写操作", async () => { - vi.mocked(invoke) + vi.mocked(safeInvoke) .mockResolvedValueOnce({ id: "p2", name: "人设 2" }) .mockResolvedValueOnce({ id: "p2", name: "人设 2-更新" }) .mockResolvedValueOnce(undefined) @@ -56,15 +56,17 @@ describe("personas API", () => { await expect(deletePersona("p2")).resolves.toBeUndefined(); await expect(setDefaultPersona("project-2", "p2")).resolves.toBeUndefined(); - expect(invoke).toHaveBeenNthCalledWith(1, "create_persona", { + expect(safeInvoke).toHaveBeenNthCalledWith(1, "create_persona", { req: expect.objectContaining({ projectId: "project-2" }), }); - expect(invoke).toHaveBeenNthCalledWith(2, "update_persona", { + expect(safeInvoke).toHaveBeenNthCalledWith(2, "update_persona", { id: "p2", update: { tone: "克制" }, }); - expect(invoke).toHaveBeenNthCalledWith(3, "delete_persona", { id: "p2" }); - expect(invoke).toHaveBeenNthCalledWith(4, "set_default_persona", { + expect(safeInvoke).toHaveBeenNthCalledWith(3, "delete_persona", { + id: "p2", + }); + expect(safeInvoke).toHaveBeenNthCalledWith(4, "set_default_persona", { projectId: "project-2", personaId: "p2", }); diff --git a/src/lib/api/personas.ts b/src/lib/api/personas.ts index 9dc5830c9..be87ceb2e 100644 --- a/src/lib/api/personas.ts +++ b/src/lib/api/personas.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import type { CreatePersonaRequest, Persona, @@ -7,39 +7,39 @@ import type { } from "@/types/persona"; export async function listPersonas(projectId: string): Promise { - return invoke("list_personas", { projectId }); + return safeInvoke("list_personas", { projectId }); } export async function getDefaultPersona( projectId: string, ): Promise { - return invoke("get_default_persona", { projectId }); + return safeInvoke("get_default_persona", { projectId }); } export async function createPersona( request: CreatePersonaRequest, ): Promise { - return invoke("create_persona", { req: request }); + return safeInvoke("create_persona", { req: request }); } export async function updatePersona( id: string, update: PersonaUpdate, ): Promise { - return invoke("update_persona", { id, update }); + return safeInvoke("update_persona", { id, update }); } export async function deletePersona(id: string): Promise { - await invoke("delete_persona", { id }); + await safeInvoke("delete_persona", { id }); } export async function setDefaultPersona( projectId: string, personaId: string, ): Promise { - await invoke("set_default_persona", { projectId, personaId }); + await safeInvoke("set_default_persona", { projectId, personaId }); } export async function listPersonaTemplates(): Promise { - return invoke("list_persona_templates"); + return safeInvoke("list_persona_templates"); } diff --git a/src/lib/api/plugins.ts b/src/lib/api/plugins.ts index 33965ef7f..b598c1f32 100644 --- a/src/lib/api/plugins.ts +++ b/src/lib/api/plugins.ts @@ -20,7 +20,7 @@ export async function listInstalledPlugins(): Promise { export async function listPluginTasks( params: ListPluginTasksParams, ): Promise { - return safeInvoke("list_plugin_tasks", params); + return safeInvoke("list_plugin_tasks", params as unknown as Record); } export async function getPluginQueueStats(): Promise { diff --git a/src/lib/api/posterMaterials.test.ts b/src/lib/api/posterMaterials.test.ts index c1533e4fa..60dfdc235 100644 --- a/src/lib/api/posterMaterials.test.ts +++ b/src/lib/api/posterMaterials.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import { createPosterMetadata, deletePosterMetadata, @@ -10,8 +10,8 @@ import { updatePosterMetadata, } from "./posterMaterials"; -vi.mock("@tauri-apps/api/core", () => ({ - invoke: vi.fn(), +vi.mock("@/lib/dev-bridge", () => ({ + safeInvoke: vi.fn(), })); describe("posterMaterials API", () => { @@ -20,18 +20,18 @@ describe("posterMaterials API", () => { }); it("应获取单个海报素材", async () => { - vi.mocked(invoke).mockResolvedValueOnce({ id: "m1", type: "image" }); + vi.mocked(safeInvoke).mockResolvedValueOnce({ id: "m1", type: "image" }); await expect(getPosterMaterial("m1")).resolves.toEqual( expect.objectContaining({ id: "m1" }), ); - expect(invoke).toHaveBeenCalledWith("get_poster_material", { + expect(safeInvoke).toHaveBeenCalledWith("get_poster_material", { materialId: "m1", }); }); it("应代理海报素材元数据写操作", async () => { - vi.mocked(invoke) + vi.mocked(safeInvoke) .mockResolvedValueOnce({ materialId: "m2" }) .mockResolvedValueOnce({ materialId: "m2" }) .mockResolvedValueOnce(undefined); @@ -52,7 +52,7 @@ describe("posterMaterials API", () => { }); it("应代理不同维度的海报素材查询", async () => { - vi.mocked(invoke) + vi.mocked(safeInvoke) .mockResolvedValueOnce([{ id: "img-1" }]) .mockResolvedValueOnce([{ id: "layout-1" }]) .mockResolvedValueOnce([{ id: "color-1" }]); diff --git a/src/lib/api/posterMaterials.ts b/src/lib/api/posterMaterials.ts index 6db732b05..e962a39e7 100644 --- a/src/lib/api/posterMaterials.ts +++ b/src/lib/api/posterMaterials.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import type { ColorMood, CreatePosterMetadataRequest, @@ -11,13 +11,15 @@ import type { export async function getPosterMaterial( materialId: string, ): Promise { - return invoke("get_poster_material", { materialId }); + return safeInvoke("get_poster_material", { + materialId, + }); } export async function createPosterMetadata( request: CreatePosterMetadataRequest, ): Promise { - return invoke("create_poster_metadata", { + return safeInvoke("create_poster_metadata", { req: request, }); } @@ -26,21 +28,21 @@ export async function updatePosterMetadata( materialId: string, request: CreatePosterMetadataRequest, ): Promise { - return invoke("update_poster_metadata", { + return safeInvoke("update_poster_metadata", { materialId, req: request, }); } export async function deletePosterMetadata(materialId: string): Promise { - await invoke("delete_poster_metadata", { materialId }); + await safeInvoke("delete_poster_metadata", { materialId }); } export async function listPosterMaterialsByImageCategory( projectId: string, category?: ImageCategory | null, ): Promise { - return invoke("list_by_image_category", { + return safeInvoke("list_by_image_category", { projectId, category: category ?? null, }); @@ -50,7 +52,7 @@ export async function listPosterMaterialsByLayoutCategory( projectId: string, category?: LayoutCategory | null, ): Promise { - return invoke("list_by_layout_category", { + return safeInvoke("list_by_layout_category", { projectId, category: category ?? null, }); @@ -60,7 +62,7 @@ export async function listPosterMaterialsByMood( projectId: string, mood?: ColorMood | null, ): Promise { - return invoke("list_by_mood", { + return safeInvoke("list_by_mood", { projectId, mood: mood ?? null, }); diff --git a/src/lib/api/profileAssets.test.ts b/src/lib/api/profileAssets.test.ts new file mode 100644 index 000000000..24355b0c7 --- /dev/null +++ b/src/lib/api/profileAssets.test.ts @@ -0,0 +1,24 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { safeInvoke } from "@/lib/dev-bridge"; +import { deleteAvatar, uploadAvatar } from "./profileAssets"; + +vi.mock("@/lib/dev-bridge", () => ({ + safeInvoke: vi.fn(), +})); + +describe("profileAssets API", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("应代理头像上传与删除命令", async () => { + vi.mocked(safeInvoke) + .mockResolvedValueOnce({ url: "/avatar.png", size: 123 }) + .mockResolvedValueOnce(undefined); + + await expect(uploadAvatar("/tmp/avatar.png")).resolves.toEqual( + expect.objectContaining({ url: "/avatar.png" }), + ); + await expect(deleteAvatar("/avatar.png")).resolves.toBeUndefined(); + }); +}); diff --git a/src/lib/api/profileAssets.ts b/src/lib/api/profileAssets.ts new file mode 100644 index 000000000..b22299ee0 --- /dev/null +++ b/src/lib/api/profileAssets.ts @@ -0,0 +1,14 @@ +import { safeInvoke } from "@/lib/dev-bridge"; + +export interface UploadResult { + url: string; + size: number; +} + +export async function uploadAvatar(filePath: string): Promise { + return safeInvoke("upload_avatar", { filePath }); +} + +export async function deleteAvatar(url: string): Promise { + return safeInvoke("delete_avatar", { url }); +} diff --git a/src/lib/api/project.test.ts b/src/lib/api/project.test.ts index b3fd9b6d8..68b6427bf 100644 --- a/src/lib/api/project.test.ts +++ b/src/lib/api/project.test.ts @@ -5,17 +5,31 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import { + createContent, + createProject, + deleteContent, + deleteProject, ensureWorkspaceReady, ensureDefaultWorkspaceReady, + getContent, + getContentStats, getWorkspaceProjectsRoot, + getOrCreateDefaultProject, resolveProjectRootPath, getProjectByRootPath, getDefaultProject, + getProject, + getThemeWorkbenchDocumentState, + listContents, + listProjects, requireDefaultProject, requireDefaultProjectId, + reorderContents, setDefaultProject, + updateContent, + updateProject, isUserProjectType, getProjectTypeLabel, getProjectTypeIcon, @@ -35,8 +49,8 @@ import { type ContentStatus, } from "./project"; -vi.mock("@tauri-apps/api/core", () => ({ - invoke: vi.fn(), +vi.mock("@/lib/dev-bridge", () => ({ + safeInvoke: vi.fn(), })); // ============================================================================ @@ -50,44 +64,50 @@ describe("项目管理 API", () => { }); it("应该调用命令获取 workspace 根目录", async () => { - vi.mocked(invoke).mockResolvedValueOnce( + vi.mocked(safeInvoke).mockResolvedValueOnce( "/Users/test/.proxycast/projects", ); const root = await getWorkspaceProjectsRoot(); expect(root).toBe("/Users/test/.proxycast/projects"); - expect(invoke).toHaveBeenCalledWith("workspace_get_projects_root"); + expect(safeInvoke).toHaveBeenCalledWith("workspace_get_projects_root"); }); it("应该调用命令解析项目目录", async () => { - vi.mocked(invoke).mockResolvedValueOnce( + vi.mocked(safeInvoke).mockResolvedValueOnce( "/Users/test/.proxycast/projects/MyProject", ); const path = await resolveProjectRootPath("MyProject"); expect(path).toBe("/Users/test/.proxycast/projects/MyProject"); - expect(invoke).toHaveBeenCalledWith("workspace_resolve_project_path", { - name: "MyProject", - }); + expect(safeInvoke).toHaveBeenCalledWith( + "workspace_resolve_project_path", + { + name: "MyProject", + }, + ); }); it("应该将空名称传给后端统一处理", async () => { - vi.mocked(invoke).mockResolvedValueOnce( + vi.mocked(safeInvoke).mockResolvedValueOnce( "/Users/test/.proxycast/projects/未命名项目", ); const path = await resolveProjectRootPath(" "); expect(path).toBe("/Users/test/.proxycast/projects/未命名项目"); - expect(invoke).toHaveBeenCalledWith("workspace_resolve_project_path", { - name: " ", - }); + expect(safeInvoke).toHaveBeenCalledWith( + "workspace_resolve_project_path", + { + name: " ", + }, + ); }); it("应该调用命令按路径获取项目", async () => { - vi.mocked(invoke).mockResolvedValueOnce({ + vi.mocked(safeInvoke).mockResolvedValueOnce({ id: "p1", name: "测试项目", workspace_type: "general", @@ -100,13 +120,13 @@ describe("项目管理 API", () => { expect(project?.id).toBe("p1"); expect(project?.rootPath).toBe("/Users/test/.proxycast/projects/demo"); - expect(invoke).toHaveBeenCalledWith("workspace_get_by_path", { + expect(safeInvoke).toHaveBeenCalledWith("workspace_get_by_path", { rootPath: "/Users/test/.proxycast/projects/demo", }); }); it("按路径查询不存在项目时应该返回 null", async () => { - vi.mocked(invoke).mockResolvedValueOnce(null); + vi.mocked(safeInvoke).mockResolvedValueOnce(null); const project = await getProjectByRootPath( "/Users/test/.proxycast/projects/missing", @@ -116,7 +136,7 @@ describe("项目管理 API", () => { }); it("应该获取并标准化默认项目", async () => { - vi.mocked(invoke).mockResolvedValueOnce({ + vi.mocked(safeInvoke).mockResolvedValueOnce({ id: "default-1", name: "默认项目", workspace_type: "general", @@ -135,11 +155,11 @@ describe("项目管理 API", () => { isDefault: true, }), ); - expect(invoke).toHaveBeenCalledWith("workspace_get_default"); + expect(safeInvoke).toHaveBeenCalledWith("workspace_get_default"); }); it("requireDefaultProject 缺失默认项目时应抛指定错误", async () => { - vi.mocked(invoke).mockResolvedValueOnce(null); + vi.mocked(safeInvoke).mockResolvedValueOnce(null); await expect(requireDefaultProject("请先创建默认项目")).rejects.toThrow( "请先创建默认项目", @@ -147,7 +167,7 @@ describe("项目管理 API", () => { }); it("requireDefaultProjectId 应返回默认项目 ID", async () => { - vi.mocked(invoke).mockResolvedValueOnce({ + vi.mocked(safeInvoke).mockResolvedValueOnce({ id: "default-2", name: "默认项目 2", }); @@ -156,7 +176,7 @@ describe("项目管理 API", () => { }); it("应该调用命令确保默认项目目录就绪", async () => { - vi.mocked(invoke).mockResolvedValueOnce({ + vi.mocked(safeInvoke).mockResolvedValueOnce({ workspaceId: "default-3", rootPath: "/tmp/default-3", existed: true, @@ -171,26 +191,232 @@ describe("项目管理 API", () => { created: false, repaired: true, }); - expect(invoke).toHaveBeenCalledWith("workspace_ensure_ready", { + expect(safeInvoke).toHaveBeenCalledWith("workspace_ensure_ready", { id: "default-3", }); }); it("应该调用命令确保默认项目目录就绪并支持空返回", async () => { - vi.mocked(invoke).mockResolvedValueOnce(null); + vi.mocked(safeInvoke).mockResolvedValueOnce(null); await expect(ensureDefaultWorkspaceReady()).resolves.toBeNull(); - expect(invoke).toHaveBeenCalledWith("workspace_ensure_default_ready"); + expect(safeInvoke).toHaveBeenCalledWith("workspace_ensure_default_ready"); }); it("应该调用命令设置默认项目", async () => { - vi.mocked(invoke).mockResolvedValueOnce(undefined); + vi.mocked(safeInvoke).mockResolvedValueOnce(undefined); await expect(setDefaultProject("default-4")).resolves.toBeUndefined(); - expect(invoke).toHaveBeenCalledWith("workspace_set_default", { + expect(safeInvoke).toHaveBeenCalledWith("workspace_set_default", { id: "default-4", }); }); + + it("应该代理项目 CRUD 相关命令", async () => { + vi.mocked(safeInvoke) + .mockResolvedValueOnce({ + id: "project-1", + name: "项目 1", + workspace_type: "general", + root_path: "/tmp/project-1", + }) + .mockResolvedValueOnce([ + { + id: "project-1", + name: "项目 1", + workspace_type: "general", + root_path: "/tmp/project-1", + }, + ]) + .mockResolvedValueOnce({ + id: "default-5", + name: "默认项目 5", + workspace_type: "general", + root_path: "/tmp/default-5", + }) + .mockResolvedValueOnce({ + id: "project-1", + name: "项目 1", + workspace_type: "general", + root_path: "/tmp/project-1", + }) + .mockResolvedValueOnce({ + id: "project-1", + name: "项目 1-更新", + workspace_type: "general", + root_path: "/tmp/project-1", + }) + .mockResolvedValueOnce(true); + + await expect( + createProject({ + name: "项目 1", + rootPath: "/tmp/project-1", + workspaceType: "general", + }), + ).resolves.toEqual(expect.objectContaining({ id: "project-1" })); + await expect(listProjects()).resolves.toEqual([ + expect.objectContaining({ id: "project-1" }), + ]); + await expect(getOrCreateDefaultProject()).resolves.toEqual( + expect.objectContaining({ id: "default-5" }), + ); + await expect(getProject("project-1")).resolves.toEqual( + expect.objectContaining({ id: "project-1" }), + ); + await expect( + updateProject("project-1", { name: "项目 1-更新" }), + ).resolves.toEqual(expect.objectContaining({ name: "项目 1-更新" })); + await expect(deleteProject("project-1", true)).resolves.toBe(true); + + expect(safeInvoke).toHaveBeenNthCalledWith(1, "workspace_create", { + request: { + name: "项目 1", + rootPath: "/tmp/project-1", + workspaceType: "general", + }, + }); + expect(safeInvoke).toHaveBeenNthCalledWith(2, "workspace_list"); + expect(safeInvoke).toHaveBeenNthCalledWith( + 3, + "get_or_create_default_project", + ); + expect(safeInvoke).toHaveBeenNthCalledWith(4, "workspace_get", { + id: "project-1", + }); + expect(safeInvoke).toHaveBeenNthCalledWith(5, "workspace_update", { + id: "project-1", + request: { name: "项目 1-更新" }, + }); + expect(safeInvoke).toHaveBeenNthCalledWith(6, "workspace_delete", { + id: "project-1", + deleteDirectory: true, + }); + }); + + it("应该代理内容相关命令", async () => { + vi.mocked(safeInvoke) + .mockResolvedValueOnce({ + id: "content-1", + project_id: "project-1", + title: "第一章", + content_type: "chapter", + status: "draft", + order: 1, + word_count: 10, + created_at: 1, + updated_at: 2, + body: "内容", + }) + .mockResolvedValueOnce({ + id: "content-1", + project_id: "project-1", + title: "第一章", + content_type: "chapter", + status: "draft", + order: 1, + word_count: 10, + created_at: 1, + updated_at: 2, + body: "内容", + }) + .mockResolvedValueOnce({ + content_id: "content-1", + current_version_id: "v1", + version_count: 1, + versions: [], + }) + .mockResolvedValueOnce([ + { + id: "content-1", + project_id: "project-1", + title: "第一章", + content_type: "chapter", + status: "draft", + order: 1, + word_count: 10, + created_at: 1, + updated_at: 2, + }, + ]) + .mockResolvedValueOnce({ + id: "content-1", + project_id: "project-1", + title: "第一章-修订", + content_type: "chapter", + status: "completed", + order: 1, + word_count: 20, + created_at: 1, + updated_at: 3, + body: "内容", + }) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce([1, 2, 3]); + + await expect( + createContent({ + project_id: "project-1", + title: "第一章", + content_type: "chapter", + }), + ).resolves.toEqual(expect.objectContaining({ id: "content-1" })); + await expect(getContent("content-1")).resolves.toEqual( + expect.objectContaining({ id: "content-1" }), + ); + await expect( + getThemeWorkbenchDocumentState("content-1"), + ).resolves.toEqual(expect.objectContaining({ current_version_id: "v1" })); + await expect( + listContents("project-1", { content_type: "chapter" }), + ).resolves.toEqual([expect.objectContaining({ id: "content-1" })]); + await expect( + updateContent("content-1", { + title: "第一章-修订", + status: "completed", + }), + ).resolves.toEqual(expect.objectContaining({ title: "第一章-修订" })); + await expect(deleteContent("content-1")).resolves.toBe(true); + await expect( + reorderContents("project-1", ["content-1"]), + ).resolves.toBeUndefined(); + await expect(getContentStats("project-1")).resolves.toEqual([1, 2, 3]); + + expect(safeInvoke).toHaveBeenNthCalledWith(1, "content_create", { + request: { + project_id: "project-1", + title: "第一章", + content_type: "chapter", + }, + }); + expect(safeInvoke).toHaveBeenNthCalledWith(2, "content_get", { + id: "content-1", + }); + expect(safeInvoke).toHaveBeenNthCalledWith( + 3, + "content_get_theme_workbench_document_state", + { id: "content-1" }, + ); + expect(safeInvoke).toHaveBeenNthCalledWith(4, "content_list", { + projectId: "project-1", + query: { content_type: "chapter" }, + }); + expect(safeInvoke).toHaveBeenNthCalledWith(5, "content_update", { + id: "content-1", + request: { title: "第一章-修订", status: "completed" }, + }); + expect(safeInvoke).toHaveBeenNthCalledWith(6, "content_delete", { + id: "content-1", + }); + expect(safeInvoke).toHaveBeenNthCalledWith(7, "content_reorder", { + projectId: "project-1", + contentIds: ["content-1"], + }); + expect(safeInvoke).toHaveBeenNthCalledWith(8, "content_stats", { + projectId: "project-1", + }); + }); }); describe("isUserProjectType", () => { diff --git a/src/lib/api/project.ts b/src/lib/api/project.ts index 248a2812a..5dfb1b389 100644 --- a/src/lib/api/project.ts +++ b/src/lib/api/project.ts @@ -4,7 +4,7 @@ * 提供项目(Project)和内容(Content)的 CRUD 操作 */ -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import type { WorkspaceSettings } from "@/types/workspace"; // ==================== 类型定义 ==================== @@ -219,6 +219,7 @@ export interface CreateProjectRequest { /** 更新项目请求 */ export interface UpdateProjectRequest { name?: string; + rootPath?: string; settings?: WorkspaceSettings; icon?: string; color?: string; @@ -277,23 +278,23 @@ export interface ListContentQuery { export async function createProject( request: CreateProjectRequest, ): Promise { - const project = await invoke("workspace_create", { request }); + const project = await safeInvoke("workspace_create", { request }); return normalizeProject(project); } /** 获取统一 workspace 项目根目录 */ export async function getWorkspaceProjectsRoot(): Promise { - return invoke("workspace_get_projects_root"); + return safeInvoke("workspace_get_projects_root"); } /** 按项目名称解析固定项目目录 */ export async function resolveProjectRootPath(name: string): Promise { - return invoke("workspace_resolve_project_path", { name }); + return safeInvoke("workspace_resolve_project_path", { name }); } /** 获取项目列表 */ export async function listProjects(): Promise { - const projects = await invoke("workspace_list"); + const projects = await safeInvoke("workspace_list"); // 防御性编程:确保返回数组 if (!Array.isArray(projects)) { console.warn("listProjects 返回非数组值:", projects); @@ -304,7 +305,7 @@ export async function listProjects(): Promise { /** 获取默认项目 */ export async function getDefaultProject(): Promise { - const project = await invoke("workspace_get_default"); + const project = await safeInvoke("workspace_get_default"); return project ? normalizeProject(project) : null; } @@ -331,22 +332,24 @@ export async function requireDefaultProjectId( export async function ensureWorkspaceReady( id: string, ): Promise { - return invoke("workspace_ensure_ready", { id }); + return safeInvoke("workspace_ensure_ready", { id }); } /** 确保默认工作区目录就绪 */ export async function ensureDefaultWorkspaceReady(): Promise { - return invoke("workspace_ensure_default_ready"); + return safeInvoke( + "workspace_ensure_default_ready", + ); } /** 设置默认项目 */ export async function setDefaultProject(id: string): Promise { - await invoke("workspace_set_default", { id }); + await safeInvoke("workspace_set_default", { id }); } /** 获取或创建默认项目 */ export async function getOrCreateDefaultProject(): Promise { - const project = await invoke("get_or_create_default_project"); + const project = await safeInvoke("get_or_create_default_project"); return normalizeProject(project); } @@ -354,7 +357,7 @@ export async function getOrCreateDefaultProject(): Promise { export async function getProjectByRootPath( rootPath: string, ): Promise { - const project = await invoke("workspace_get_by_path", { + const project = await safeInvoke("workspace_get_by_path", { rootPath, }); return project ? normalizeProject(project) : null; @@ -362,7 +365,7 @@ export async function getProjectByRootPath( /** 获取项目详情 */ export async function getProject(id: string): Promise { - const project = await invoke("workspace_get", { id }); + const project = await safeInvoke("workspace_get", { id }); return project ? normalizeProject(project) : null; } @@ -371,7 +374,10 @@ export async function updateProject( id: string, request: UpdateProjectRequest, ): Promise { - const project = await invoke("workspace_update", { id, request }); + const project = await safeInvoke("workspace_update", { + id, + request, + }); return normalizeProject(project); } @@ -380,7 +386,7 @@ export async function deleteProject( id: string, deleteDirectory?: boolean, ): Promise { - return invoke("workspace_delete", { id, deleteDirectory }); + return safeInvoke("workspace_delete", { id, deleteDirectory }); } // ==================== 内容 API ==================== @@ -389,19 +395,22 @@ export async function deleteProject( export async function createContent( request: CreateContentRequest, ): Promise { - return invoke("content_create", { request }); + return safeInvoke("content_create", { request }); } /** 获取内容详情 */ export async function getContent(id: string): Promise { - return invoke("content_get", { id }); + return safeInvoke("content_get", { id }); } /** 获取主题工作台文稿版本状态(后端解析 content.metadata) */ export async function getThemeWorkbenchDocumentState( id: string, ): Promise { - return invoke("content_get_theme_workbench_document_state", { id }); + return safeInvoke( + "content_get_theme_workbench_document_state", + { id }, + ); } /** 获取项目的内容列表 */ @@ -409,7 +418,7 @@ export async function listContents( projectId: string, query?: ListContentQuery, ): Promise { - const contents = await invoke("content_list", { + const contents = await safeInvoke("content_list", { projectId, query, }); @@ -426,12 +435,12 @@ export async function updateContent( id: string, request: UpdateContentRequest, ): Promise { - return invoke("content_update", { id, request }); + return safeInvoke("content_update", { id, request }); } /** 删除内容 */ export async function deleteContent(id: string): Promise { - return invoke("content_delete", { id }); + return safeInvoke("content_delete", { id }); } /** 重新排序内容 */ @@ -439,14 +448,14 @@ export async function reorderContents( projectId: string, contentIds: string[], ): Promise { - return invoke("content_reorder", { projectId, contentIds }); + return safeInvoke("content_reorder", { projectId, contentIds }); } /** 获取项目内容统计 */ export async function getContentStats( projectId: string, ): Promise<[number, number, number]> { - return invoke("content_stats", { projectId }); + return safeInvoke<[number, number, number]>("content_stats", { projectId }); } // ==================== 辅助函数 ==================== diff --git a/src/lib/api/screenshotChat.ts b/src/lib/api/screenshotChat.ts index 29c8e89b4..aa8489f47 100644 --- a/src/lib/api/screenshotChat.ts +++ b/src/lib/api/screenshotChat.ts @@ -8,7 +8,7 @@ export interface SendScreenshotChatParams { export async function sendScreenshotChat( params: SendScreenshotChatParams, ): Promise { - await safeInvoke("send_screenshot_chat", params); + await safeInvoke("send_screenshot_chat", params as unknown as Record); } export async function closeScreenshotChatWindow(): Promise { diff --git a/src/lib/api/subAgentScheduler.test.ts b/src/lib/api/subAgentScheduler.test.ts index 6eb6ee4ef..0238c4913 100644 --- a/src/lib/api/subAgentScheduler.test.ts +++ b/src/lib/api/subAgentScheduler.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import { cancelSubAgentTasks, executeSubAgentTasks } from "./subAgentScheduler"; -vi.mock("@tauri-apps/api/core", () => ({ - invoke: vi.fn(), +vi.mock("@/lib/dev-bridge", () => ({ + safeInvoke: vi.fn(), })); describe("subAgentScheduler API", () => { @@ -12,7 +12,7 @@ describe("subAgentScheduler API", () => { }); it("应透传执行任务请求", async () => { - vi.mocked(invoke).mockResolvedValueOnce({ + vi.mocked(safeInvoke).mockResolvedValueOnce({ success: true, results: [], totalDurationMs: 0, @@ -34,7 +34,7 @@ describe("subAgentScheduler API", () => { ), ).resolves.toEqual(expect.objectContaining({ success: true })); - expect(invoke).toHaveBeenCalledWith("execute_subagent_tasks", { + expect(safeInvoke).toHaveBeenCalledWith("execute_subagent_tasks", { tasks: [{ id: "task-1", taskType: "code", prompt: "检查入口" }], config: { maxConcurrency: 2 }, sessionId: "session-1", @@ -42,9 +42,9 @@ describe("subAgentScheduler API", () => { }); it("应代理取消任务", async () => { - vi.mocked(invoke).mockResolvedValueOnce(undefined); + vi.mocked(safeInvoke).mockResolvedValueOnce(undefined); await expect(cancelSubAgentTasks()).resolves.toBeUndefined(); - expect(invoke).toHaveBeenCalledWith("cancel_subagent_tasks"); + expect(safeInvoke).toHaveBeenCalledWith("cancel_subagent_tasks"); }); }); diff --git a/src/lib/api/subAgentScheduler.ts b/src/lib/api/subAgentScheduler.ts index 6817b8a4b..53c3e2921 100644 --- a/src/lib/api/subAgentScheduler.ts +++ b/src/lib/api/subAgentScheduler.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; export interface SubAgentTask { id: string; @@ -80,7 +80,7 @@ export async function executeSubAgentTasks( config?: SchedulerConfig, sessionId?: string | null, ): Promise { - return invoke("execute_subagent_tasks", { + return safeInvoke("execute_subagent_tasks", { tasks, config, sessionId, @@ -88,5 +88,5 @@ export async function executeSubAgentTasks( } export async function cancelSubAgentTasks(): Promise { - await invoke("cancel_subagent_tasks"); + await safeInvoke("cancel_subagent_tasks"); } diff --git a/src/lib/api/templates.test.ts b/src/lib/api/templates.test.ts index 790a4ad4e..8e64ca878 100644 --- a/src/lib/api/templates.test.ts +++ b/src/lib/api/templates.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import { createTemplate, deleteTemplate, @@ -9,8 +9,8 @@ import { updateTemplate, } from "./templates"; -vi.mock("@tauri-apps/api/core", () => ({ - invoke: vi.fn(), +vi.mock("@/lib/dev-bridge", () => ({ + safeInvoke: vi.fn(), })); describe("templates API", () => { @@ -19,7 +19,7 @@ describe("templates API", () => { }); it("应获取模板列表和默认模板", async () => { - vi.mocked(invoke) + vi.mocked(safeInvoke) .mockResolvedValueOnce([{ id: "t1", name: "模板 1" }]) .mockResolvedValueOnce({ id: "t1", name: "模板 1" }); @@ -30,16 +30,16 @@ describe("templates API", () => { expect.objectContaining({ id: "t1" }), ); - expect(invoke).toHaveBeenNthCalledWith(1, "list_templates", { + expect(safeInvoke).toHaveBeenNthCalledWith(1, "list_templates", { projectId: "project-1", }); - expect(invoke).toHaveBeenNthCalledWith(2, "get_default_template", { + expect(safeInvoke).toHaveBeenNthCalledWith(2, "get_default_template", { projectId: "project-1", }); }); it("应代理模板写操作", async () => { - vi.mocked(invoke) + vi.mocked(safeInvoke) .mockResolvedValueOnce({ id: "t2", name: "模板 2" }) .mockResolvedValueOnce({ id: "t2", name: "模板 2-更新" }) .mockResolvedValueOnce(undefined) @@ -60,15 +60,17 @@ describe("templates API", () => { setDefaultTemplate("project-2", "t2"), ).resolves.toBeUndefined(); - expect(invoke).toHaveBeenNthCalledWith(1, "create_template", { + expect(safeInvoke).toHaveBeenNthCalledWith(1, "create_template", { req: expect.objectContaining({ projectId: "project-2" }), }); - expect(invoke).toHaveBeenNthCalledWith(2, "update_template", { + expect(safeInvoke).toHaveBeenNthCalledWith(2, "update_template", { id: "t2", update: { name: "模板 2-更新" }, }); - expect(invoke).toHaveBeenNthCalledWith(3, "delete_template", { id: "t2" }); - expect(invoke).toHaveBeenNthCalledWith(4, "set_default_template", { + expect(safeInvoke).toHaveBeenNthCalledWith(3, "delete_template", { + id: "t2", + }); + expect(safeInvoke).toHaveBeenNthCalledWith(4, "set_default_template", { projectId: "project-2", templateId: "t2", }); diff --git a/src/lib/api/templates.ts b/src/lib/api/templates.ts index 9f268b5d2..904c8ecdc 100644 --- a/src/lib/api/templates.ts +++ b/src/lib/api/templates.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { safeInvoke } from "@/lib/dev-bridge"; import type { CreateTemplateRequest, Template, @@ -6,35 +6,35 @@ import type { } from "@/types/template"; export async function listTemplates(projectId: string): Promise { - return invoke("list_templates", { projectId }); + return safeInvoke("list_templates", { projectId }); } export async function getDefaultTemplate( projectId: string, ): Promise