mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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: [] }),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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" }]);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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 });
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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" }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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: "短视频",
|
||||
};
|
||||
|
||||
/** 媒体生成偏好设置 */
|
||||
|
||||
Reference in New Issue
Block a user