mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-12 00:49:14 +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": "^2.0.0",
|
||||||
"@ai-sdk/provider-utils": "^3.0.17",
|
"@ai-sdk/provider-utils": "^3.0.17",
|
||||||
"@ai-sdk/xai": "^2.0.36",
|
"@ai-sdk/xai": "^2.0.36",
|
||||||
|
"@cherrystudio/shared": "workspace:*",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
* 为不支持原生 Function Call 的模型提供 prompt 方式的工具调用
|
* 为不支持原生 Function Call 的模型提供 prompt 方式的工具调用
|
||||||
* 内置默认逻辑,支持自定义覆盖
|
* 内置默认逻辑,支持自定义覆盖
|
||||||
*/
|
*/
|
||||||
|
import { getHubModeSystemPrompt, type ToolInfo } from '@cherrystudio/shared/prompts'
|
||||||
import type { TextStreamPart, ToolSet } from 'ai'
|
import type { TextStreamPart, ToolSet } from 'ai'
|
||||||
|
|
||||||
import { definePlugin } from '../../index'
|
import { definePlugin } from '../../index'
|
||||||
@ -12,6 +13,16 @@ import { type TagConfig, TagExtractor } from './tagExtraction'
|
|||||||
import { ToolExecutor } from './ToolExecutor'
|
import { ToolExecutor } from './ToolExecutor'
|
||||||
import type { PromptToolUseConfig, ToolUseResult } from './type'
|
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 (availableTools === null) return userSystemPrompt
|
||||||
|
|
||||||
if (mcpMode == 'auto') {
|
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 }}',
|
'{{ USER_SYSTEM_PROMPT }}',
|
||||||
userSystemPrompt || ''
|
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':
|
'@cherrystudio/ai-sdk-provider':
|
||||||
specifier: ^0.1.3
|
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)
|
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:
|
ai:
|
||||||
specifier: ^5.0.26
|
specifier: ^5.0.26
|
||||||
version: 5.0.117(zod@4.3.4)
|
version: 5.0.117(zod@4.3.4)
|
||||||
@ -1228,6 +1231,8 @@ importers:
|
|||||||
specifier: ^0.13.3
|
specifier: ^0.13.3
|
||||||
version: 0.13.5(@typescript/native-preview@7.0.0-dev.20260104.1)(typescript@5.9.2)
|
version: 0.13.5(@typescript/native-preview@7.0.0-dev.20260104.1)(typescript@5.9.2)
|
||||||
|
|
||||||
|
packages/shared: {}
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
7zip-bin@5.2.0:
|
7zip-bin@5.2.0:
|
||||||
|
|||||||
@ -1,159 +1,2 @@
|
|||||||
import { generateMcpToolFunctionName } from '@shared/mcp'
|
// Re-export from shared package for backward compatibility
|
||||||
|
export { getHubModeSystemPrompt, type ToolInfo } from '@shared/prompts'
|
||||||
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}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user