From 894b2fd487363ae516a0f0d2bbd4d857a0e612ef Mon Sep 17 00:00:00 2001 From: Vaayne Date: Wed, 7 Jan 2026 00:23:04 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20move=20getHubM?= =?UTF-8?q?odeSystemPrompt=20to=20shared=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create @cherrystudio/shared workspace package with exports - Move getHubModeSystemPrompt and ToolInfo to packages/shared/prompts - Add @cherrystudio/shared dependency to @cherrystudio/ai-core - Update promptToolUsePlugin to import from shared package - Update renderer prompts-code-mode.ts to re-export from shared - Add toolSetToToolInfoArray converter for type compatibility --- packages/aiCore/package.json | 1 + .../toolUsePlugin/promptToolUsePlugin.ts | 14 +- packages/shared/package.json | 16 ++ packages/shared/prompts/hubModePrompt.ts | 159 +++++++++++++++++ packages/shared/prompts/index.ts | 1 + pnpm-lock.yaml | 5 + src/renderer/src/config/prompts-code-mode.ts | 161 +----------------- 7 files changed, 197 insertions(+), 160 deletions(-) create mode 100644 packages/shared/package.json create mode 100644 packages/shared/prompts/hubModePrompt.ts create mode 100644 packages/shared/prompts/index.ts diff --git a/packages/aiCore/package.json b/packages/aiCore/package.json index 5f8321ff73..0d13b34a91 100644 --- a/packages/aiCore/package.json +++ b/packages/aiCore/package.json @@ -46,6 +46,7 @@ "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.17", "@ai-sdk/xai": "^2.0.36", + "@cherrystudio/shared": "workspace:*", "zod": "^4.1.5" }, "devDependencies": { diff --git a/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts b/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts index 11a69a3354..1465737821 100644 --- a/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts +++ b/packages/aiCore/src/core/plugins/built-in/toolUsePlugin/promptToolUsePlugin.ts @@ -3,6 +3,7 @@ * 为不支持原生 Function Call 的模型提供 prompt 方式的工具调用 * 内置默认逻辑,支持自定义覆盖 */ +import { getHubModeSystemPrompt, type ToolInfo } from '@cherrystudio/shared/prompts' import type { TextStreamPart, ToolSet } from 'ai' import { definePlugin } from '../../index' @@ -12,6 +13,16 @@ import { type TagConfig, TagExtractor } from './tagExtraction' import { ToolExecutor } from './ToolExecutor' import type { PromptToolUseConfig, ToolUseResult } from './type' +/** + * Convert ToolSet to ToolInfo array for hub mode system prompt + */ +function toolSetToToolInfoArray(tools: ToolSet): ToolInfo[] { + return Object.entries(tools).map(([name, tool]) => ({ + name, + description: tool.description || '' + })) +} + /** * 工具使用标签配置 */ @@ -182,7 +193,8 @@ function defaultBuildSystemPrompt(userSystemPrompt: string, tools: ToolSet, mcpM if (availableTools === null) return userSystemPrompt if (mcpMode == 'auto') { - return DEFAULT_SYSTEM_PROMPT.replace('{{ TOOLS_INFO }}', getHubModeSystemPrompt(tools)).replace( + const toolInfoArray = toolSetToToolInfoArray(tools) + return DEFAULT_SYSTEM_PROMPT.replace('{{ TOOLS_INFO }}', getHubModeSystemPrompt(toolInfoArray)).replace( '{{ USER_SYSTEM_PROMPT }}', userSystemPrompt || '' ) diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000000..254591fde6 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,16 @@ +{ + "name": "@cherrystudio/shared", + "version": "1.0.0", + "description": "Cherry Studio Shared Utilities", + "main": "index.ts", + "types": "index.ts", + "scripts": {}, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": "./index.ts", + "./mcp": "./mcp.ts", + "./utils": "./utils.ts", + "./prompts": "./prompts/index.ts" + } +} diff --git a/packages/shared/prompts/hubModePrompt.ts b/packages/shared/prompts/hubModePrompt.ts new file mode 100644 index 0000000000..4a8dfd95df --- /dev/null +++ b/packages/shared/prompts/hubModePrompt.ts @@ -0,0 +1,159 @@ +import { generateMcpToolFunctionName } from '../mcp' + +export type ToolInfo = { + name: string + serverName?: string + description?: string +} + +/** + * Hub Mode System Prompt - For native MCP tool calling + * Used when model supports native function calling via MCP protocol + */ +const HUB_MODE_SYSTEM_PROMPT_BASE = ` +## Hub MCP Tools – Code Execution Mode + +You can discover and call MCP tools through the hub server using two meta-tools: **search** and **exec**. + +### Critical Rules (Read First) + +1. You MUST explicitly \`return\` the final value from your \`exec\` code. If you do not return a value, the result will be \`undefined\`. +2. All MCP tools are async functions. Always call them as \`await ToolName(params)\`. +3. Use the exact function names and parameter shapes returned by \`search\`. +4. You CANNOT call \`search\` or \`exec\` from inside \`exec\` code—use them only as MCP tools. +5. \`console.log\` output is NOT the result. Logs are separate; the final answer must come from \`return\`. + +### Workflow + +1. Call \`search\` with relevant keywords to discover tools. +2. Read the returned JavaScript function declarations and JSDoc to understand names and parameters. +3. Call \`exec\` with JavaScript code that uses the discovered tools and ends with an explicit \`return\`. +4. Use the \`exec\` result as your answer. + +### What \`search\` Does + +- Input: keyword string (comma-separated for OR-matching), plus optional \`limit\`. +- Output: JavaScript async function declarations with JSDoc showing exact function names, parameters, and return types. + +### What \`exec\` Does + +- Runs JavaScript code in an isolated async context (wrapped as \`(async () => { your code })())\`. +- All discovered tools are exposed as async functions: \`await ToolName(params)\`. +- Available helpers: + - \`parallel(...promises)\` → \`Promise.all(promises)\` + - \`settle(...promises)\` → \`Promise.allSettled(promises)\` + - \`console.log/info/warn/error/debug\` +- Returns JSON with: \`result\` (your returned value), \`logs\` (optional), \`error\` (optional), \`isError\` (optional). + +### Example: Single Tool Call + +\`\`\`javascript +// Step 1: search({ query: "browser,fetch" }) +// Step 2: exec with: +const page = await CherryBrowser_fetch({ url: "https://example.com" }) +return page +\`\`\` + +### Example: Multiple Tools with Parallel + +\`\`\`javascript +const [forecast, time] = await parallel( + Weather_getForecast({ city: "Paris" }), + Time_getLocalTime({ city: "Paris" }) +) +return { city: "Paris", forecast, time } +\`\`\` + +### Example: Handle Partial Failures with Settle + +\`\`\`javascript +const results = await settle( + Weather_getForecast({ city: "Paris" }), + Weather_getForecast({ city: "Tokyo" }) +) +const successful = results.filter(r => r.status === "fulfilled").map(r => r.value) +return { results, successful } +\`\`\` + +### Example: Error Handling + +\`\`\`javascript +try { + const user = await User_lookup({ email: "user@example.com" }) + return { found: true, user } +} catch (error) { + return { found: false, error: String(error) } +} +\`\`\` + +### Common Mistakes to Avoid + +❌ **Forgetting to return** (result will be \`undefined\`): +\`\`\`javascript +const data = await SomeTool({ id: "123" }) +// Missing return! +\`\`\` + +✅ **Always return**: +\`\`\`javascript +const data = await SomeTool({ id: "123" }) +return data +\`\`\` + +❌ **Only logging, not returning**: +\`\`\`javascript +const data = await SomeTool({ id: "123" }) +console.log(data) // Logs are NOT the result! +\`\`\` + +❌ **Missing await**: +\`\`\`javascript +const data = SomeTool({ id: "123" }) // Returns Promise, not value! +return data +\`\`\` + +❌ **Awaiting before parallel**: +\`\`\`javascript +await parallel(await ToolA(), await ToolB()) // Wrong: runs sequentially +\`\`\` + +✅ **Pass promises directly to parallel**: +\`\`\`javascript +await parallel(ToolA(), ToolB()) // Correct: runs in parallel +\`\`\` + +### Best Practices + +- Always call \`search\` first to discover tools and confirm signatures. +- Always use an explicit \`return\` at the end of \`exec\` code. +- Use \`parallel\` for independent operations that can run at the same time. +- Use \`settle\` when some calls may fail but you still want partial results. +- Prefer a single \`exec\` call for multi-step flows. +- Treat \`console.*\` as debugging only, never as the primary result. +` + +function buildToolsSection(tools: ToolInfo[]): string { + const existingNames = new Set() + return tools + .map((t) => { + const functionName = generateMcpToolFunctionName(t.serverName, t.name, existingNames) + const desc = t.description || '' + const normalizedDesc = desc.replace(/\s+/g, ' ').trim() + const truncatedDesc = normalizedDesc.length > 50 ? `${normalizedDesc.slice(0, 50)}...` : normalizedDesc + return `- ${functionName}: ${truncatedDesc}` + }) + .join('\n') +} + +export function getHubModeSystemPrompt(tools: ToolInfo[] = []): string { + if (tools.length === 0) { + return '' + } + + const toolsSection = buildToolsSection(tools) + + return `${HUB_MODE_SYSTEM_PROMPT_BASE} +## Discoverable Tools (use search to get full signatures) +${toolsSection} +` +} diff --git a/packages/shared/prompts/index.ts b/packages/shared/prompts/index.ts new file mode 100644 index 0000000000..a1d12c6570 --- /dev/null +++ b/packages/shared/prompts/index.ts @@ -0,0 +1 @@ +export { getHubModeSystemPrompt, type ToolInfo } from './hubModePrompt' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e93839f943..66eac8e192 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1184,6 +1184,9 @@ importers: '@cherrystudio/ai-sdk-provider': specifier: ^0.1.3 version: 0.1.3(@ai-sdk/anthropic@2.0.57(zod@4.3.4))(@ai-sdk/google@2.0.49(patch_hash=279e9d43f675e4b979b32b78954dd37acc3026aa36ae2dd7701b5bad2f061522)(zod@4.3.4))(@ai-sdk/openai@2.0.85(patch_hash=f2077f4759520d1de69b164dfd8adca1a9ace9de667e35cb0e55e812ce2ac13b)(zod@4.3.4))(ai@5.0.117(zod@4.3.4))(zod@4.3.4) + '@cherrystudio/shared': + specifier: workspace:* + version: link:../shared ai: specifier: ^5.0.26 version: 5.0.117(zod@4.3.4) @@ -1228,6 +1231,8 @@ importers: specifier: ^0.13.3 version: 0.13.5(@typescript/native-preview@7.0.0-dev.20260104.1)(typescript@5.9.2) + packages/shared: {} + packages: 7zip-bin@5.2.0: diff --git a/src/renderer/src/config/prompts-code-mode.ts b/src/renderer/src/config/prompts-code-mode.ts index a2b3592c09..e0a2f185a7 100644 --- a/src/renderer/src/config/prompts-code-mode.ts +++ b/src/renderer/src/config/prompts-code-mode.ts @@ -1,159 +1,2 @@ -import { generateMcpToolFunctionName } from '@shared/mcp' - -export interface ToolInfo { - name: string - serverName?: string - description?: string -} - -/** - * Hub Mode System Prompt - For native MCP tool calling - * Used when model supports native function calling via MCP protocol - */ -const HUB_MODE_SYSTEM_PROMPT_BASE = ` -## Hub MCP Tools – Code Execution Mode - -You can discover and call MCP tools through the hub server using two meta-tools: **search** and **exec**. - -### Critical Rules (Read First) - -1. You MUST explicitly \`return\` the final value from your \`exec\` code. If you do not return a value, the result will be \`undefined\`. -2. All MCP tools are async functions. Always call them as \`await ToolName(params)\`. -3. Use the exact function names and parameter shapes returned by \`search\`. -4. You CANNOT call \`search\` or \`exec\` from inside \`exec\` code—use them only as MCP tools. -5. \`console.log\` output is NOT the result. Logs are separate; the final answer must come from \`return\`. - -### Workflow - -1. Call \`search\` with relevant keywords to discover tools. -2. Read the returned JavaScript function declarations and JSDoc to understand names and parameters. -3. Call \`exec\` with JavaScript code that uses the discovered tools and ends with an explicit \`return\`. -4. Use the \`exec\` result as your answer. - -### What \`search\` Does - -- Input: keyword string (comma-separated for OR-matching), plus optional \`limit\`. -- Output: JavaScript async function declarations with JSDoc showing exact function names, parameters, and return types. - -### What \`exec\` Does - -- Runs JavaScript code in an isolated async context (wrapped as \`(async () => { your code })())\`. -- All discovered tools are exposed as async functions: \`await ToolName(params)\`. -- Available helpers: - - \`parallel(...promises)\` → \`Promise.all(promises)\` - - \`settle(...promises)\` → \`Promise.allSettled(promises)\` - - \`console.log/info/warn/error/debug\` -- Returns JSON with: \`result\` (your returned value), \`logs\` (optional), \`error\` (optional), \`isError\` (optional). - -### Example: Single Tool Call - -\`\`\`javascript -// Step 1: search({ query: "browser,fetch" }) -// Step 2: exec with: -const page = await CherryBrowser_fetch({ url: "https://example.com" }) -return page -\`\`\` - -### Example: Multiple Tools with Parallel - -\`\`\`javascript -const [forecast, time] = await parallel( - Weather_getForecast({ city: "Paris" }), - Time_getLocalTime({ city: "Paris" }) -) -return { city: "Paris", forecast, time } -\`\`\` - -### Example: Handle Partial Failures with Settle - -\`\`\`javascript -const results = await settle( - Weather_getForecast({ city: "Paris" }), - Weather_getForecast({ city: "Tokyo" }) -) -const successful = results.filter(r => r.status === "fulfilled").map(r => r.value) -return { results, successful } -\`\`\` - -### Example: Error Handling - -\`\`\`javascript -try { - const user = await User_lookup({ email: "user@example.com" }) - return { found: true, user } -} catch (error) { - return { found: false, error: String(error) } -} -\`\`\` - -### Common Mistakes to Avoid - -❌ **Forgetting to return** (result will be \`undefined\`): -\`\`\`javascript -const data = await SomeTool({ id: "123" }) -// Missing return! -\`\`\` - -✅ **Always return**: -\`\`\`javascript -const data = await SomeTool({ id: "123" }) -return data -\`\`\` - -❌ **Only logging, not returning**: -\`\`\`javascript -const data = await SomeTool({ id: "123" }) -console.log(data) // Logs are NOT the result! -\`\`\` - -❌ **Missing await**: -\`\`\`javascript -const data = SomeTool({ id: "123" }) // Returns Promise, not value! -return data -\`\`\` - -❌ **Awaiting before parallel**: -\`\`\`javascript -await parallel(await ToolA(), await ToolB()) // Wrong: runs sequentially -\`\`\` - -✅ **Pass promises directly to parallel**: -\`\`\`javascript -await parallel(ToolA(), ToolB()) // Correct: runs in parallel -\`\`\` - -### Best Practices - -- Always call \`search\` first to discover tools and confirm signatures. -- Always use an explicit \`return\` at the end of \`exec\` code. -- Use \`parallel\` for independent operations that can run at the same time. -- Use \`settle\` when some calls may fail but you still want partial results. -- Prefer a single \`exec\` call for multi-step flows. -- Treat \`console.*\` as debugging only, never as the primary result. -` - -function buildToolsSection(tools: ToolInfo[]): string { - const existingNames = new Set() - return tools - .map((t) => { - const functionName = generateMcpToolFunctionName(t.serverName, t.name, existingNames) - const desc = t.description || '' - const normalizedDesc = desc.replace(/\s+/g, ' ').trim() - const truncatedDesc = normalizedDesc.length > 50 ? `${normalizedDesc.slice(0, 50)}...` : normalizedDesc - return `- ${functionName}: ${truncatedDesc}` - }) - .join('\n') -} - -export function getHubModeSystemPrompt(tools: ToolInfo[] = []): string { - if (tools.length === 0) { - return '' - } - - const toolsSection = buildToolsSection(tools) - - return `${HUB_MODE_SYSTEM_PROMPT_BASE} -## Discoverable Tools (use search to get full signatures) -${toolsSection} -` -} +// Re-export from shared package for backward compatibility +export { getHubModeSystemPrompt, type ToolInfo } from '@shared/prompts'