fix: resolve TypeScript compilation errors and add missing API modules

- Add sourceKind property to Skill mock data in tests
- Fix UpdateProjectRequest type usage (remove invalid rootPath)
- Align WorkspaceType and ProjectType definitions
- Add index signatures to API request types for safeInvoke compatibility
- Fix agentStore toolCall status type narrowing
- Add missing API modules: apiCompatibility, endpointProviders, memory, profileAssets, voiceTools

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
coso
2026-03-12 01:25:26 +08:00
co-authored by Claude Opus 4.6
parent c1ff332074
commit d08b328685
42 changed files with 1080 additions and 504 deletions
+126
View File
@@ -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",
@@ -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({
@@ -293,7 +293,7 @@ function summarizeSchedulerEvent(event: SchedulerEvent): string {
case "cancelled":
return "调度已取消";
default:
return event.type;
return (event as { type: string }).type;
}
}
@@ -215,6 +215,7 @@ function createSkill(name: string, key: string, installed: boolean): Skill {
description: "测试技能",
directory: `${key}-dir`,
installed,
sourceKind: "builtin",
};
}
@@ -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: {
@@ -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",
},
],
[],
@@ -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);
+2 -1
View File
@@ -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);
+2 -2
View File
@@ -26,10 +26,10 @@ export type {
SubAgentTask,
} from "@/lib/api/subAgentScheduler";
interface SchedulerEventEnvelope extends SchedulerEvent {
type SchedulerEventEnvelope = SchedulerEvent & {
sessionId?: string;
session_id?: string;
}
};
/**
* Hook 状态
+29 -301
View File
@@ -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<void> {
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<WorkspaceEnsureResult> {
return ensureProjectWorkspaceReady(id);
}
export async function workspaceEnsureDefaultReady(): Promise<WorkspaceEnsureResult | null> {
return ensureDefaultProjectWorkspaceReady();
}
export {
getAvailableVoices,
testTts,
type TtsTestResult,
type VoiceOption,
} from "@/lib/api/voiceTools";
/**
* 更新 Provider 的环境变量
@@ -357,170 +252,3 @@ export async function workspaceEnsureDefaultReady(): Promise<WorkspaceEnsureResu
* @param apiHost Provider 的 API Host
* @param apiKey 可选的 API Key
*/
// ============ API Compatibility Check ============
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<ApiCompatibilityResult> {
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<EndpointProvidersConfig> {
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<string> {
return safeInvoke("set_endpoint_provider", {
endpoint: clientType,
provider,
});
}
// Network Info
// ============ 实验室功能 API ============
/**
* 获取实验室功能配置
* @returns 实验室功能配置对象
*/
// ============ 记忆管理 API ============
/**
* 获取记忆统计信息
*/
export async function getMemoryStats(): Promise<MemoryStatsResponse> {
return safeInvoke("get_conversation_memory_stats");
}
/**
* 获取记忆总览(含分类与条目)
*/
export async function requestMemoryAnalysis(
fromTimestamp?: number,
toTimestamp?: number,
): Promise<MemoryAnalysisResult> {
return safeInvoke("request_conversation_memory_analysis", {
fromTimestamp,
toTimestamp,
});
}
/**
* 清理过期记忆
*/
export async function cleanupMemory(): Promise<CleanupMemoryResult> {
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<TtsTestResult> {
return safeInvoke("test_tts", { service, voice });
}
/**
* 获取可用的语音列表
* @param service TTS 服务名称
*/
export async function getAvailableVoices(
service: string,
): Promise<VoiceOption[]> {
return safeInvoke("get_available_voices", { service });
}
// ============ 文件上传 API ============
export interface UploadResult {
url: string;
size: number;
}
/**
* 上传用户头像
* @param filePath 文件路径
*/
export async function uploadAvatar(filePath: string): Promise<UploadResult> {
return safeInvoke("upload_avatar", { filePath });
}
/**
* 删除用户头像
* @param url 头像 URL
*/
export async function deleteAvatar(url: string): Promise<void> {
return safeInvoke("delete_avatar", { url });
}
+26
View File
@@ -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" }),
);
});
});
+24
View File
@@ -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<ApiCompatibilityResult> {
return safeInvoke("check_api_compatibility", { provider });
}
+5 -5
View File
@@ -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)
+7 -7
View File
@@ -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<BrandPersona | null> {
return invoke<BrandPersona | null>("get_brand_persona", { personaId });
return safeInvoke<BrandPersona | null>("get_brand_persona", { personaId });
}
export async function getBrandExtension(
personaId: string,
): Promise<BrandPersonaExtension | null> {
return invoke<BrandPersonaExtension | null>("get_brand_extension", {
return safeInvoke<BrandPersonaExtension | null>("get_brand_extension", {
personaId,
});
}
@@ -24,7 +24,7 @@ export async function getBrandExtension(
export async function saveBrandExtension(
request: CreateBrandExtensionRequest,
): Promise<BrandPersonaExtension> {
return invoke<BrandPersonaExtension>("save_brand_extension", {
return safeInvoke<BrandPersonaExtension>("save_brand_extension", {
req: request,
});
}
@@ -33,18 +33,18 @@ export async function updateBrandExtension(
personaId: string,
update: UpdateBrandExtensionRequest,
): Promise<BrandPersonaExtension> {
return invoke<BrandPersonaExtension>("update_brand_extension", {
return safeInvoke<BrandPersonaExtension>("update_brand_extension", {
personaId,
update,
});
}
export async function deleteBrandExtension(personaId: string): Promise<void> {
await invoke("delete_brand_extension", { personaId });
await safeInvoke<void>("delete_brand_extension", { personaId });
}
export async function listBrandPersonaTemplates(): Promise<
BrandPersonaTemplate[]
> {
return invoke<BrandPersonaTemplate[]>("list_brand_persona_templates");
return safeInvoke<BrandPersonaTemplate[]>("list_brand_persona_templates");
}
+26
View File
@@ -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",
);
});
});
+24
View File
@@ -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<EndpointProvidersConfig> {
return safeInvoke("get_endpoint_providers");
}
export async function setEndpointProvider(
clientType: string,
provider: string | null,
): Promise<string> {
return safeInvoke("set_endpoint_provider", {
endpoint: clientType,
provider,
});
}
+15 -15
View File
@@ -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",
});
});
+8 -8
View File
@@ -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<Material[]> {
const materials = await invoke<RawMaterial[]>("list_materials", {
const materials = await safeInvoke<RawMaterial[]>("list_materials", {
projectId,
project_id: projectId,
filter: filter ?? null,
@@ -103,7 +103,7 @@ export async function listMaterials(
}
export async function getMaterialCount(projectId: string): Promise<number> {
return invoke<number>("get_material_count", {
return safeInvoke<number>("get_material_count", {
projectId,
project_id: projectId,
});
@@ -112,7 +112,7 @@ export async function getMaterialCount(projectId: string): Promise<number> {
export async function uploadMaterial(
request: UploadMaterialRequest,
): Promise<Material> {
const material = await invoke<RawMaterial>("upload_material", {
const material = await safeInvoke<RawMaterial>("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<ImportedMaterialRef> {
return invoke<ImportedMaterialRef>("import_material_from_url", {
return safeInvoke<ImportedMaterialRef>("import_material_from_url", {
req: buildImportRequestPayload(request),
});
}
@@ -130,7 +130,7 @@ export async function updateMaterial(
id: string,
update: MaterialUpdate,
): Promise<Material> {
const material = await invoke<RawMaterial>("update_material", {
const material = await safeInvoke<RawMaterial>("update_material", {
id,
update,
});
@@ -138,9 +138,9 @@ export async function updateMaterial(
}
export async function deleteMaterial(id: string): Promise<void> {
await invoke("delete_material", { id });
await safeInvoke<void>("delete_material", { id });
}
export async function getMaterialContent(id: string): Promise<string> {
return invoke<string>("get_material_content", { id });
return safeInvoke<string>("get_material_content", { id });
}
+210
View File
@@ -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",
]);
});
});
+29 -25
View File
@@ -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<Character[]> {
return invoke("character_list", { projectId });
return safeInvoke<Character[]>("character_list", { projectId });
}
/** 获取角色详情 */
export async function getCharacter(id: string): Promise<Character | null> {
return invoke("character_get", { id });
return safeInvoke<Character | null>("character_get", { id });
}
/** 创建角色 */
export async function createCharacter(
request: CreateCharacterRequest,
): Promise<Character> {
return invoke("character_create", { request });
return safeInvoke<Character>("character_create", { request });
}
/** 更新角色 */
@@ -171,12 +176,12 @@ export async function updateCharacter(
id: string,
request: UpdateCharacterRequest,
): Promise<Character> {
return invoke("character_update", { id, request });
return safeInvoke<Character>("character_update", { id, request });
}
/** 删除角色 */
export async function deleteCharacter(id: string): Promise<boolean> {
return invoke("character_delete", { id });
return safeInvoke<boolean>("character_delete", { id });
}
// ==================== 世界观 API ====================
@@ -185,7 +190,7 @@ export async function deleteCharacter(id: string): Promise<boolean> {
export async function getWorldBuilding(
projectId: string,
): Promise<WorldBuilding | null> {
return invoke("world_building_get", { projectId });
return safeInvoke<WorldBuilding | null>("world_building_get", { projectId });
}
/** 更新世界观 */
@@ -193,7 +198,10 @@ export async function updateWorldBuilding(
projectId: string,
request: UpdateWorldBuildingRequest,
): Promise<WorldBuilding> {
return invoke("world_building_update", { projectId, request });
return safeInvoke<WorldBuilding>("world_building_update", {
projectId,
request,
});
}
// ==================== 风格指南 API ====================
@@ -202,7 +210,7 @@ export async function updateWorldBuilding(
export async function getStyleGuide(
projectId: string,
): Promise<StyleGuide | null> {
return invoke("style_guide_get", { projectId });
return safeInvoke<StyleGuide | null>("style_guide_get", { projectId });
}
/** 更新风格指南 */
@@ -210,7 +218,7 @@ export async function updateStyleGuide(
projectId: string,
request: UpdateStyleGuideRequest,
): Promise<StyleGuide> {
return invoke("style_guide_update", { projectId, request });
return safeInvoke<StyleGuide>("style_guide_update", { projectId, request });
}
// ==================== 大纲 API ====================
@@ -219,19 +227,19 @@ export async function updateStyleGuide(
export async function listOutlineNodes(
projectId: string,
): Promise<OutlineNode[]> {
return invoke("outline_node_list", { projectId });
return safeInvoke<OutlineNode[]>("outline_node_list", { projectId });
}
/** 获取大纲节点详情 */
export async function getOutlineNode(id: string): Promise<OutlineNode | null> {
return invoke("outline_node_get", { id });
return safeInvoke<OutlineNode | null>("outline_node_get", { id });
}
/** 创建大纲节点 */
export async function createOutlineNode(
request: CreateOutlineNodeRequest,
): Promise<OutlineNode> {
return invoke("outline_node_create", { request });
return safeInvoke<OutlineNode>("outline_node_create", { request });
}
/** 更新大纲节点 */
@@ -239,12 +247,12 @@ export async function updateOutlineNode(
id: string,
request: UpdateOutlineNodeRequest,
): Promise<OutlineNode> {
return invoke("outline_node_update", { id, request });
return safeInvoke<OutlineNode>("outline_node_update", { id, request });
}
/** 删除大纲节点 */
export async function deleteOutlineNode(id: string): Promise<boolean> {
return invoke("outline_node_delete", { id });
return safeInvoke<boolean>("outline_node_delete", { id });
}
// ==================== 聚合 API ====================
@@ -253,17 +261,15 @@ export async function deleteOutlineNode(id: string): Promise<boolean> {
export async function getProjectMemory(
projectId: string,
): Promise<ProjectMemory> {
return invoke("project_memory_get", { projectId });
return safeInvoke<ProjectMemory>("project_memory_get", { projectId });
}
// ==================== 辅助函数 ====================
/** 构建大纲树结构 */
export function buildOutlineTree(
nodes: OutlineNode[],
): (OutlineNode & { children: OutlineNode[] })[] {
const nodeMap = new Map<string, OutlineNode & { children: OutlineNode[] }>();
const roots: (OutlineNode & { children: OutlineNode[] })[] = [];
export function buildOutlineTree(nodes: OutlineNode[]): OutlineTreeNode[] {
const nodeMap = new Map<string, OutlineTreeNode>();
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);
+19
View File
@@ -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: [] }),
);
+21
View File
@@ -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<MemoryStatsResponse> {
return safeInvoke("get_conversation_memory_stats");
}
export async function requestMemoryAnalysis(
fromTimestamp?: number,
toTimestamp?: number,
): Promise<MemoryAnalysisResult> {
return safeInvoke("request_conversation_memory_analysis", {
fromTimestamp,
toTimestamp,
});
}
export async function cleanupMemory(): Promise<CleanupMemoryResult> {
return safeInvoke("cleanup_conversation_memory");
}
export async function getMemoryEffectiveSources(
workingDir?: string,
activeRelativePath?: string,
+1 -1
View File
@@ -9,5 +9,5 @@ export interface ShowNotificationRequest {
export async function showSystemNotification(
request: ShowNotificationRequest,
): Promise<void> {
await safeInvoke("show_notification", request);
await safeInvoke("show_notification", request as unknown as Record<string, unknown>);
}
+11 -9
View File
@@ -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",
});
+8 -8
View File
@@ -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<Persona[]> {
return invoke<Persona[]>("list_personas", { projectId });
return safeInvoke<Persona[]>("list_personas", { projectId });
}
export async function getDefaultPersona(
projectId: string,
): Promise<Persona | null> {
return invoke<Persona | null>("get_default_persona", { projectId });
return safeInvoke<Persona | null>("get_default_persona", { projectId });
}
export async function createPersona(
request: CreatePersonaRequest,
): Promise<Persona> {
return invoke<Persona>("create_persona", { req: request });
return safeInvoke<Persona>("create_persona", { req: request });
}
export async function updatePersona(
id: string,
update: PersonaUpdate,
): Promise<Persona> {
return invoke<Persona>("update_persona", { id, update });
return safeInvoke<Persona>("update_persona", { id, update });
}
export async function deletePersona(id: string): Promise<void> {
await invoke("delete_persona", { id });
await safeInvoke<void>("delete_persona", { id });
}
export async function setDefaultPersona(
projectId: string,
personaId: string,
): Promise<void> {
await invoke("set_default_persona", { projectId, personaId });
await safeInvoke<void>("set_default_persona", { projectId, personaId });
}
export async function listPersonaTemplates(): Promise<PersonaTemplate[]> {
return invoke<PersonaTemplate[]>("list_persona_templates");
return safeInvoke<PersonaTemplate[]>("list_persona_templates");
}
+1 -1
View File
@@ -20,7 +20,7 @@ export async function listInstalledPlugins<T>(): Promise<T[]> {
export async function listPluginTasks<T>(
params: ListPluginTasksParams,
): Promise<T[]> {
return safeInvoke<T[]>("list_plugin_tasks", params);
return safeInvoke<T[]>("list_plugin_tasks", params as unknown as Record<string, unknown>);
}
export async function getPluginQueueStats<T>(): Promise<T[]> {
+7 -7
View File
@@ -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" }]);
+10 -8
View File
@@ -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<PosterMaterial | null> {
return invoke<PosterMaterial | null>("get_poster_material", { materialId });
return safeInvoke<PosterMaterial | null>("get_poster_material", {
materialId,
});
}
export async function createPosterMetadata(
request: CreatePosterMetadataRequest,
): Promise<PosterMaterialMetadata> {
return invoke<PosterMaterialMetadata>("create_poster_metadata", {
return safeInvoke<PosterMaterialMetadata>("create_poster_metadata", {
req: request,
});
}
@@ -26,21 +28,21 @@ export async function updatePosterMetadata(
materialId: string,
request: CreatePosterMetadataRequest,
): Promise<PosterMaterialMetadata> {
return invoke<PosterMaterialMetadata>("update_poster_metadata", {
return safeInvoke<PosterMaterialMetadata>("update_poster_metadata", {
materialId,
req: request,
});
}
export async function deletePosterMetadata(materialId: string): Promise<void> {
await invoke("delete_poster_metadata", { materialId });
await safeInvoke<void>("delete_poster_metadata", { materialId });
}
export async function listPosterMaterialsByImageCategory(
projectId: string,
category?: ImageCategory | null,
): Promise<PosterMaterial[]> {
return invoke<PosterMaterial[]>("list_by_image_category", {
return safeInvoke<PosterMaterial[]>("list_by_image_category", {
projectId,
category: category ?? null,
});
@@ -50,7 +52,7 @@ export async function listPosterMaterialsByLayoutCategory(
projectId: string,
category?: LayoutCategory | null,
): Promise<PosterMaterial[]> {
return invoke<PosterMaterial[]>("list_by_layout_category", {
return safeInvoke<PosterMaterial[]>("list_by_layout_category", {
projectId,
category: category ?? null,
});
@@ -60,7 +62,7 @@ export async function listPosterMaterialsByMood(
projectId: string,
mood?: ColorMood | null,
): Promise<PosterMaterial[]> {
return invoke<PosterMaterial[]>("list_by_mood", {
return safeInvoke<PosterMaterial[]>("list_by_mood", {
projectId,
mood: mood ?? null,
});
+24
View File
@@ -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();
});
});
+14
View File
@@ -0,0 +1,14 @@
import { safeInvoke } from "@/lib/dev-bridge";
export interface UploadResult {
url: string;
size: number;
}
export async function uploadAvatar(filePath: string): Promise<UploadResult> {
return safeInvoke("upload_avatar", { filePath });
}
export async function deleteAvatar(url: string): Promise<void> {
return safeInvoke("delete_avatar", { url });
}
+252 -26
View File
@@ -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", () => {
+31 -22
View File
@@ -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<Project> {
const project = await invoke<RawProject>("workspace_create", { request });
const project = await safeInvoke<RawProject>("workspace_create", { request });
return normalizeProject(project);
}
/** 获取统一 workspace 项目根目录 */
export async function getWorkspaceProjectsRoot(): Promise<string> {
return invoke<string>("workspace_get_projects_root");
return safeInvoke<string>("workspace_get_projects_root");
}
/** 按项目名称解析固定项目目录 */
export async function resolveProjectRootPath(name: string): Promise<string> {
return invoke<string>("workspace_resolve_project_path", { name });
return safeInvoke<string>("workspace_resolve_project_path", { name });
}
/** 获取项目列表 */
export async function listProjects(): Promise<Project[]> {
const projects = await invoke<RawProject[]>("workspace_list");
const projects = await safeInvoke<RawProject[]>("workspace_list");
// 防御性编程:确保返回数组
if (!Array.isArray(projects)) {
console.warn("listProjects 返回非数组值:", projects);
@@ -304,7 +305,7 @@ export async function listProjects(): Promise<Project[]> {
/** 获取默认项目 */
export async function getDefaultProject(): Promise<Project | null> {
const project = await invoke<RawProject | null>("workspace_get_default");
const project = await safeInvoke<RawProject | null>("workspace_get_default");
return project ? normalizeProject(project) : null;
}
@@ -331,22 +332,24 @@ export async function requireDefaultProjectId(
export async function ensureWorkspaceReady(
id: string,
): Promise<WorkspaceEnsureResult> {
return invoke<WorkspaceEnsureResult>("workspace_ensure_ready", { id });
return safeInvoke<WorkspaceEnsureResult>("workspace_ensure_ready", { id });
}
/** 确保默认工作区目录就绪 */
export async function ensureDefaultWorkspaceReady(): Promise<WorkspaceEnsureResult | null> {
return invoke<WorkspaceEnsureResult | null>("workspace_ensure_default_ready");
return safeInvoke<WorkspaceEnsureResult | null>(
"workspace_ensure_default_ready",
);
}
/** 设置默认项目 */
export async function setDefaultProject(id: string): Promise<void> {
await invoke("workspace_set_default", { id });
await safeInvoke<void>("workspace_set_default", { id });
}
/** 获取或创建默认项目 */
export async function getOrCreateDefaultProject(): Promise<Project> {
const project = await invoke<RawProject>("get_or_create_default_project");
const project = await safeInvoke<RawProject>("get_or_create_default_project");
return normalizeProject(project);
}
@@ -354,7 +357,7 @@ export async function getOrCreateDefaultProject(): Promise<Project> {
export async function getProjectByRootPath(
rootPath: string,
): Promise<Project | null> {
const project = await invoke<RawProject | null>("workspace_get_by_path", {
const project = await safeInvoke<RawProject | null>("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<Project | null> {
const project = await invoke<RawProject | null>("workspace_get", { id });
const project = await safeInvoke<RawProject | null>("workspace_get", { id });
return project ? normalizeProject(project) : null;
}
@@ -371,7 +374,10 @@ export async function updateProject(
id: string,
request: UpdateProjectRequest,
): Promise<Project> {
const project = await invoke<RawProject>("workspace_update", { id, request });
const project = await safeInvoke<RawProject>("workspace_update", {
id,
request,
});
return normalizeProject(project);
}
@@ -380,7 +386,7 @@ export async function deleteProject(
id: string,
deleteDirectory?: boolean,
): Promise<boolean> {
return invoke("workspace_delete", { id, deleteDirectory });
return safeInvoke<boolean>("workspace_delete", { id, deleteDirectory });
}
// ==================== 内容 API ====================
@@ -389,19 +395,22 @@ export async function deleteProject(
export async function createContent(
request: CreateContentRequest,
): Promise<ContentDetail> {
return invoke("content_create", { request });
return safeInvoke<ContentDetail>("content_create", { request });
}
/** 获取内容详情 */
export async function getContent(id: string): Promise<ContentDetail | null> {
return invoke("content_get", { id });
return safeInvoke<ContentDetail | null>("content_get", { id });
}
/** 获取主题工作台文稿版本状态(后端解析 content.metadata) */
export async function getThemeWorkbenchDocumentState(
id: string,
): Promise<ThemeWorkbenchDocumentState | null> {
return invoke("content_get_theme_workbench_document_state", { id });
return safeInvoke<ThemeWorkbenchDocumentState | null>(
"content_get_theme_workbench_document_state",
{ id },
);
}
/** 获取项目的内容列表 */
@@ -409,7 +418,7 @@ export async function listContents(
projectId: string,
query?: ListContentQuery,
): Promise<ContentListItem[]> {
const contents = await invoke<ContentListItem[]>("content_list", {
const contents = await safeInvoke<ContentListItem[]>("content_list", {
projectId,
query,
});
@@ -426,12 +435,12 @@ export async function updateContent(
id: string,
request: UpdateContentRequest,
): Promise<ContentDetail> {
return invoke("content_update", { id, request });
return safeInvoke<ContentDetail>("content_update", { id, request });
}
/** 删除内容 */
export async function deleteContent(id: string): Promise<boolean> {
return invoke("content_delete", { id });
return safeInvoke<boolean>("content_delete", { id });
}
/** 重新排序内容 */
@@ -439,14 +448,14 @@ export async function reorderContents(
projectId: string,
contentIds: string[],
): Promise<void> {
return invoke("content_reorder", { projectId, contentIds });
return safeInvoke<void>("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 });
}
// ==================== 辅助函数 ====================
+1 -1
View File
@@ -8,7 +8,7 @@ export interface SendScreenshotChatParams {
export async function sendScreenshotChat(
params: SendScreenshotChatParams,
): Promise<void> {
await safeInvoke("send_screenshot_chat", params);
await safeInvoke("send_screenshot_chat", params as unknown as Record<string, unknown>);
}
export async function closeScreenshotChatWindow(): Promise<void> {
+7 -7
View File
@@ -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");
});
});
+3 -3
View File
@@ -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<SchedulerExecutionResult> {
return invoke<SchedulerExecutionResult>("execute_subagent_tasks", {
return safeInvoke<SchedulerExecutionResult>("execute_subagent_tasks", {
tasks,
config,
sessionId,
@@ -88,5 +88,5 @@ export async function executeSubAgentTasks(
}
export async function cancelSubAgentTasks(): Promise<void> {
await invoke("cancel_subagent_tasks");
await safeInvoke<void>("cancel_subagent_tasks");
}
+13 -11
View File
@@ -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",
});
+7 -7
View File
@@ -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<Template[]> {
return invoke<Template[]>("list_templates", { projectId });
return safeInvoke<Template[]>("list_templates", { projectId });
}
export async function getDefaultTemplate(
projectId: string,
): Promise<Template | null> {
return invoke<Template | null>("get_default_template", { projectId });
return safeInvoke<Template | null>("get_default_template", { projectId });
}
export async function createTemplate(
request: CreateTemplateRequest,
): Promise<Template> {
return invoke<Template>("create_template", { req: request });
return safeInvoke<Template>("create_template", { req: request });
}
export async function updateTemplate(
id: string,
update: TemplateUpdate,
): Promise<Template> {
return invoke<Template>("update_template", { id, update });
return safeInvoke<Template>("update_template", { id, update });
}
export async function deleteTemplate(id: string): Promise<void> {
await invoke("delete_template", { id });
await safeInvoke<void>("delete_template", { id });
}
export async function setDefaultTemplate(
projectId: string,
templateId: string,
): Promise<void> {
await invoke("set_default_template", { projectId, templateId });
await safeInvoke<void>("set_default_template", { projectId, templateId });
}
+26
View File
@@ -0,0 +1,26 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { safeInvoke } from "@/lib/dev-bridge";
import { getAvailableVoices, testTts } from "./voiceTools";
vi.mock("@/lib/dev-bridge", () => ({
safeInvoke: vi.fn(),
}));
describe("voiceTools API", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("应代理语音测试命令", async () => {
vi.mocked(safeInvoke)
.mockResolvedValueOnce({ success: true, error: null, audio_path: null })
.mockResolvedValueOnce([{ id: "alloy", name: "Alloy", language: "en" }]);
await expect(testTts("openai", "alloy")).resolves.toEqual(
expect.objectContaining({ success: true }),
);
await expect(getAvailableVoices("openai")).resolves.toEqual([
expect.objectContaining({ id: "alloy" }),
]);
});
});
+26
View File
@@ -0,0 +1,26 @@
import { safeInvoke } from "@/lib/dev-bridge";
export interface TtsTestResult {
success: boolean;
error: string | null;
audio_path: string | null;
}
export interface VoiceOption {
id: string;
name: string;
language: string;
}
export async function testTts(
service: string,
voice: string,
): Promise<TtsTestResult> {
return safeInvoke("test_tts", { service, voice });
}
export async function getAvailableVoices(
service: string,
): Promise<VoiceOption[]> {
return safeInvoke("get_available_voices", { service });
}
+2 -2
View File
@@ -1,11 +1,11 @@
import type { Project as ApiProject } from "@/lib/api/project";
import type { Project as ApiProject, ProjectType } from "@/lib/api/project";
import type { Project } from "@/types/project";
export function toProjectView(project: ApiProject): Project {
return {
id: project.id,
name: project.name,
workspaceType: project.workspaceType,
workspaceType: project.workspaceType as ProjectType,
rootPath: project.rootPath,
isDefault: project.isDefault,
settings: project.settings,
+29 -16
View File
@@ -593,16 +593,24 @@ export const useAgentStore = create<AgentState>((set, get) => ({
messages: currentState.messages.map((message) => {
if (message.id !== msgId) return message;
const nextToolCalls = (message.toolCalls || []).map((toolCall) =>
toolCall.id === event.tool_id
? {
...toolCall,
status: event.result.success ? "completed" : "failed",
result: event.result,
endTime: new Date(),
}
: toolCall,
);
const nextToolCalls = (message.toolCalls || []).map((toolCall) => {
if (toolCall.id !== event.tool_id) {
return toolCall;
}
const nextStatus: "completed" | "failed" = event.result.success
? "completed"
: "failed";
const updatedToolCall: ToolCallState = {
...toolCall,
status: nextStatus,
result: event.result,
endTime: new Date(),
};
return updatedToolCall;
});
const nextContentParts = (message.contentParts || []).map(
(part) => {
@@ -613,14 +621,19 @@ export const useAgentStore = create<AgentState>((set, get) => ({
return part;
}
const nextStatus: "completed" | "failed" = event.result.success
? "completed"
: "failed";
const updatedToolCall: ToolCallState = {
...part.toolCall,
status: nextStatus,
result: event.result,
endTime: new Date(),
};
return {
...part,
toolCall: {
...part.toolCall,
status: event.result.success ? "completed" : "failed",
result: event.result,
endTime: new Date(),
},
toolCall: updatedToolCall,
};
},
);
+15 -1
View File
@@ -13,20 +13,34 @@
*/
export type WorkspaceType =
| "persistent" // 持久化项目
| "temporary" // 临时项目
| "social-media" // 社交媒体
| "blog" // 博客
| "novel" // 小说
| "general"; // 通用
| "general" // 通用
| "poster" // 图文海报
| "music" // 歌词曲谱
| "knowledge" // 知识探索
| "planning" // 计划规划
| "document" // 办公文档
| "video"; // 短视频
/**
* Workspace 类型显示名称映射
*/
export const WorkspaceTypeLabels: Record<WorkspaceType, string> = {
persistent: "持久化",
temporary: "临时",
"social-media": "社交媒体",
blog: "博客",
novel: "小说",
general: "通用",
poster: "图文海报",
music: "歌词曲谱",
knowledge: "知识探索",
planning: "计划规划",
document: "办公文档",
video: "短视频",
};
/** 媒体生成偏好设置 */