♻️ refactor: move getHubModeSystemPrompt to shared package

- 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
This commit is contained in:
Vaayne 2026-01-07 00:23:04 +08:00
parent e698965120
commit 894b2fd487
7 changed files with 197 additions and 160 deletions

View File

@ -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": {

View File

@ -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 || ''
)

View File

@ -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"
}
}

View File

@ -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<string>()
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}
`
}

View File

@ -0,0 +1 @@
export { getHubModeSystemPrompt, type ToolInfo } from './hubModePrompt'

View File

@ -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:

View File

@ -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<string>()
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'