mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 23:59:45 +08:00
♻️ 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:
parent
e698965120
commit
894b2fd487
@ -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": {
|
||||
|
||||
@ -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 || ''
|
||||
)
|
||||
|
||||
16
packages/shared/package.json
Normal file
16
packages/shared/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
159
packages/shared/prompts/hubModePrompt.ts
Normal file
159
packages/shared/prompts/hubModePrompt.ts
Normal 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}
|
||||
`
|
||||
}
|
||||
1
packages/shared/prompts/index.ts
Normal file
1
packages/shared/prompts/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { getHubModeSystemPrompt, type ToolInfo } from './hubModePrompt'
|
||||
@ -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:
|
||||
|
||||
@ -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'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user