65 KiB
title, description, type, version, file, author, date, tags, license, status
| title | description | type | version | file | author | date | tags | license | status | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Arabica Srpint2 开发指南:多维思维框架引擎与意图拆解 | 基于 Sprint 2 架构设计,指导开发者搭建支持 MCP Prompts 原语的提示词策略 Server,实现多维思维框架库、意图拆解工具及结构化输出约束,完成从单一工具到多框架引擎的升级。 | Development Guide | v1.0.0 (Arabica) - Sprint 2 | arabica-sprint2-development-specification.md | Gitconomy Research-郭晧 | 2026-03-06 |
|
CC BY-SA 4.0 | Active |
Arabica Sprint 2 开发指南:多维思维框架引擎与意图拆解
1. 模块概览与架构设计
Sprint 2 的核心是构建一个支持 MCP Prompts 原语的多维思维框架引擎,同时保留 Sprint 1 的工具与资源能力。整体采用经典的三层架构:
- 接入层:
app.ts负责初始化 MCP 服务器并注册所有原语。 - 控制层:
controllers/接收请求,校验参数,调用服务层。 - 服务层:
services/实现业务逻辑(框架加载、意图拆解、文件操作)。 - 模型层:
models/存放框架定义 JSON、角色配置、Zod 校验模式。
下图展示了模块间的静态关系:
graph TD
A[app.ts] --> B[promptsController]
A --> C[toolsController]
A --> D[resource 注册]
B --> E[promptService]
C --> F[intentService]
C --> G[resourceService]
C --> H[schema 校验]
E --> I[框架 JSON 文件]
E --> J[personas.json]
G --> K[本地 Markdown 文件]
F --> L[生成检索词]
2. 函数列表和调用逻辑关系
2.1 完成函数列表
以下是项目中所有显式定义的函数,按文件分组,并附简要说明。
1. app.ts(入口文件)
| 函数名 | 参数 | 描述 | 异步 |
|---|---|---|---|
start |
无 | 初始化 MCP 服务器,连接 STDIO 传输层 | ✅ |
2. resourceService.ts(资源服务)
| 函数名 | 参数 | 描述 | 异步 |
|---|---|---|---|
listObsidianNotes |
无 | 列出知识库目录下所有 .md 文件 |
✅ |
readObsidianNote |
filename: string |
读取指定笔记文件内容,含路径安全校验 | ✅ |
saveNote |
filename: string, content: string |
将笔记内容保存到知识库目录 | ✅ |
3. promptService.ts(提示词服务)
| 函数名 | 参数 | 描述 | 异步 |
|---|---|---|---|
loadFrameworks |
无 | 从文件系统加载所有框架 JSON 并缓存 | ✅ |
listFrameworks |
无 | 返回所有框架的元信息(不含模板) | ✅ |
getFramework |
name: string, args: Record<string, string> |
根据框架名称和参数组装完整消息序列 | ✅ |
4. intentService.ts(意图拆解服务)
| 函数名 | 参数 | 描述 | 异步 |
|---|---|---|---|
generateSearchQueries |
query: string |
将用户查询拆解为 3~5 个检索词 | ❌ |
5. toolsController.ts(工具控制器)
| 函数名 | 参数 | 描述 | 异步 |
|---|---|---|---|
handleToolCall |
toolName: string, params: any |
统一入口,根据工具名分发到具体处理函数 | ✅ |
handleGenerateSearchQueries |
params: any |
处理 generate_search_queries 工具调用 |
✅ |
handleListLocalNotes |
无 | 处理 list_local_notes 工具调用 |
✅ |
handleReadLocalNote |
params: any |
处理 read_local_note 工具调用 |
✅ |
handleSaveNote |
params: any |
处理 save_note 工具调用 |
✅ |
6. promptsController.ts(提示词控制器)
| 函数名 | 参数 | 描述 | 异步 |
|---|---|---|---|
handlePromptsList |
无 | 处理 prompts/list 请求,返回可用框架列表 |
✅ |
handlePromptsGet |
name: string, args: Record<string, string> |
处理 prompts/get 请求,返回具体框架消息 |
✅ |
补充说明:
- 上述列表仅包含项目代码中显式定义的命名函数。
app.ts中通过server.prompt/server.tool/server.resource注册的匿名回调函数未列入,因为它们仅作为 MCP 原语的处理器,内部直接调用对应的控制器函数(如handlePromptsGet、handleToolCall等),本身不包含独立业务逻辑。- 所有 Zod 校验模式定义在
schemas.ts中,为对象而非函数,故未列出。 - JSON 配置文件(如
personas.json、框架定义文件)不包含函数,故未列出。
2.2 整体函数调用关系图
下图展示了从 MCP 原语注册到具体功能实现的全链路函数调用关系,涵盖接入层、控制器、服务层及数据存储之间的交互:
graph TD
subgraph "接入层 app.ts"
A1["server.prompt (scqa/5whys/...)"] -->|调用| B1[handlePromptsGet]
A2["server.tool (generate_search_queries/...)"] -->|调用| B2[handleToolCall]
A3["server.resource (local-notes)"] -->|list 回调| C1[listObsidianNotes]
A3 -->|read 回调| C2[readObsidianNote]
end
subgraph "控制器"
B1[handlePromptsGet] -->|调用| D1[getFramework]
B2[handleToolCall] -->|根据名称分发| E1[handleGenerateSearchQueries]
B2 --> E2[handleListLocalNotes]
B2 --> E3[handleReadLocalNote]
B2 --> E4[handleSaveNote]
end
subgraph "服务层"
D1[getFramework] -->|读取框架定义| F1[loadFrameworks]
F1 -->|读取| G1[(frameworks/*.json)]
D1 -->|获取角色提示| G2[(personas.json)]
E1[handleGenerateSearchQueries] -->|调用| H1[generateSearchQueries]
E2[handleListLocalNotes] -->|调用| H2[listObsidianNotes]
E3[handleReadLocalNote] -->|调用| H3[readObsidianNote]
E4[handleSaveNote] -->|调用| H4[saveNote]
H2 -->|读取目录| I1[(本地知识库)]
H3 -->|读取文件| I1
H4 -->|写入文件| I1
end
subgraph "工具函数"
H1[generateSearchQueries] -->|返回| J1[检索词数组]
end
C1[listObsidianNotes] --> I1
C2[readObsidianNote] --> I1
3. 核心业务代码实现
3.1 接入层
3.1.1 模块职责与设计目标
职责
接入层(app.ts)是整个 MCP 服务器的入口,负责:
- 初始化
McpServer实例,配置服务器元信息。 - 注册所有 MCP 原语(Prompts、Tools、Resources),将外部请求映射到内部控制器。
- 启动 STDIO 传输层,等待客户端(如 Cherry Studio)连接。
设计目标
- 声明式注册:通过 SDK 提供的
server.prompt、server.tool、server.resource方法,清晰定义每个原语的名称、参数模式和处理函数。 - 解耦与控制:处理函数仅做基础参数转换和结果格式化,业务逻辑委托给控制器,保持接入层轻量。
- 健壮性:对返回的消息进行清洗(如过滤非法 role、补全缺失字段),确保符合 MCP 协议规范。
- 可扩展性:新增框架或工具只需添加对应注册代码,无需修改现有结构。
3.1.2 代码实现与解析
src/app.ts
/**
* Project Caffeine v0.1.1
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { handlePromptsGet } from './controllers/promptsController';
import { handleToolCall } from './controllers/toolsController';
import { listObsidianNotes, readObsidianNote } from './services/resourceService';
// ==========================================
// 初始化 MCP Server
// ==========================================
const server = new McpServer({
name: 'Project-Caffeine-S2-Prompt-Strategy',
version: '2.0.0'
});
// ==========================================
// 注册 Prompts 原语(多维思维框架模板)
// ==========================================
/**
* 处理 SCQA 框架的 prompts/get 请求。
* 调用 handlePromptsGet 获取消息序列,并进行清洗(过滤非法 role、补全 annotations/_meta)。
* @param args - 包含 situation, complication, question, answer 的对象
* @param extra - MCP 额外上下文(未使用)
* @returns 符合 MCP 规范的 prompt 响应对象
*/
server.prompt(
'scqa', // 示例:SCQA 框架
{
situation: z.string().describe('情境'),
complication: z.string().describe('复杂性'),
question: z.string().describe('问题'),
answer: z.string().describe('答案')
},
async (args, extra) => {
const result = await handlePromptsGet('scqa', args as Record<string, string>);
return {
...result,
messages: Array.isArray(result.messages)
? result.messages
.filter((msg: any) => msg.role === "user" || msg.role === "assistant")
.map((msg: any) => ({
...msg,
// Optionally ensure content has required structure
content: {
...msg.content,
// Add default annotations/_meta if missing
annotations: msg.content.annotations ?? undefined,
_meta: msg.content._meta ?? undefined
}
}))
: []
};
}
);
/**
* 处理 5 Whys 框架的 prompts/get 请求。
* 调用 handlePromptsGet 获取消息序列,并进行清洗。
* @param args - 包含 problem 的对象
* @returns 符合 MCP 规范的 prompt 响应对象
*/
server.prompt(
'5whys', // 5 Whys 框架
{ problem: z.string().describe('需要分析的问题或现象') },
async (args) => {
const result = await handlePromptsGet('5whys', args);
return {
...result,
messages: Array.isArray(result.messages)
? result.messages
.filter((msg: any) => msg.role === "user" || msg.role === "assistant")
.map((msg: any) => ({
...msg,
content: {
...msg.content,
annotations: msg.content.annotations ?? undefined,
_meta: msg.content._meta ?? undefined
}
}))
: []
};
}
);
/**
* 处理 5W3H 框架的 prompts/get 请求。
* 调用 handlePromptsGet 获取消息序列,并进行清洗。
* @param args - 包含 topic 的对象
* @returns 符合 MCP 规范的 prompt 响应对象
*/
server.prompt(
'5w3h', // 5W3H 框架
{ topic: z.string().describe('需要分析的主题') },
async (args) => {
const result = await handlePromptsGet('5w3h', args);
return {
...result,
messages: Array.isArray(result.messages)
? result.messages
.filter((msg: any) => msg.role === "user" || msg.role === "assistant")
.map((msg: any) => ({
...msg,
content: {
...msg.content,
annotations: msg.content.annotations ?? undefined,
_meta: msg.content._meta ?? undefined
}
}))
: []
};
}
);
/**
* 处理 SWOT 框架的 prompts/get 请求。
* 调用 handlePromptsGet 获取消息序列,并进行清洗。
* @param args - 包含 entity 的对象
* @returns 符合 MCP 规范的 prompt 响应对象
*/
server.prompt(
'swot', // SWOT 框架
{ entity: z.string().describe('分析对象(企业、项目等)') },
async (args) => {
const result = await handlePromptsGet('swot', args);
return {
...result,
messages: Array.isArray(result.messages)
? result.messages
.filter((msg: any) => msg.role === "user" || msg.role === "assistant")
.map((msg: any) => ({
...msg,
content: {
...msg.content,
annotations: msg.content.annotations ?? undefined,
_meta: msg.content._meta ?? undefined
}
}))
: []
};
}
);
/**
* 处理 PESTLE 框架的 prompts/get 请求。
* 调用 handlePromptsGet 获取消息序列,并进行清洗。
* @param args - 包含 domain 的对象
* @returns 符合 MCP 规范的 prompt 响应对象
*/
server.prompt(
'pestle', // PESTLE 框架
{ domain: z.string().describe('行业或领域') },
async (args) => {
const result = await handlePromptsGet('pestle', args);
return {
...result,
messages: Array.isArray(result.messages)
? result.messages
.filter((msg: any) => msg.role === "user" || msg.role === "assistant")
.map((msg: any) => ({
...msg,
content: {
...msg.content,
annotations: msg.content.annotations ?? undefined,
_meta: msg.content._meta ?? undefined
}
}))
: []
};
}
);
// ==========================================
// 注册工具:generate_search_queries
// ==========================================
/**
* 处理 generate_search_queries 工具调用。
* 调用 handleToolCall 获取结果,并确保返回的每个 content 项具有 type: "text"。
* @param args - 包含 query 的对象
* @param extra - MCP 额外上下文(未使用)
* @returns MCP 工具响应对象
*/
server.tool(
'generate_search_queries',
{ query: z.string().describe('用户的原始查询语句') },
async (args, extra) => {
const result = await handleToolCall('generate_search_queries', args);
// Ensure each content item has type: "text" (not a generic string)
return {
...result,
content: result.content.map((item: any) => ({
...item,
type: "text"
}))
};
}
);
// =====================================================
// 注册工具:list_local_notes, read_local_note, save_note
// =====================================================
/**
* 处理 list_local_notes 工具调用。
* 调用 handleToolCall 获取笔记列表,并标准化 content 格式。
* @param args - 空对象(无需参数)
* @returns MCP 工具响应对象,包含笔记列表文本
*/
server.tool(
'list_local_notes',
{},
async (args) => {
const result = await handleToolCall('list_local_notes', args);
return {
...result,
content: result.content.map((item: any) => ({
type: 'text',
text: item.text
}))
};
}
);
/**
* 处理 read_local_note 工具调用。
* 调用 handleToolCall 读取指定笔记内容,并标准化 content 格式。
* @param args - 包含 filename 的对象
* @returns MCP 工具响应对象,包含笔记内容文本
*/
server.tool(
'read_local_note',
{ filename: z.string().describe('需要读取的笔记文件名,必须包含 .md 后缀') },
async (args) => {
const result = await handleToolCall('read_local_note', args);
return {
...result,
content: result.content.map((item: any) => ({
type: 'text',
text: item.text
}))
};
}
);
/**
* 处理 save_note 工具调用。
* 调用 handleToolCall 保存笔记到本地知识库,并标准化 content 格式。
* @param args - 包含 filename 和 content 的对象
* @returns MCP 工具响应对象,包含保存结果信息
*/
server.tool(
'save_note',
{
filename: z.string().describe('笔记文件名,必须以 .md 结尾'),
content: z.string().describe('笔记内容(Markdown 格式)')
},
async (args) => {
const result = await handleToolCall('save_note', args);
return {
...result,
content: result.content.map((item: any) => ({
type: 'text',
text: item.text
}))
};
}
);
// ==========================================
// 注册 Resources 原语(暴露本地笔记供客户端勾选)
// ==========================================
// 使用 ResourceTemplate 注册动态资源
server.resource(
"local-notes", // 资源名称
new ResourceTemplate("note://local/{filename}", {
// 实现列表功能:返回所有可用的笔记资源
list: async () => {
try {
const notes = await listObsidianNotes();
return {
resources: notes.map(filename => ({
name: filename,
uri: `note://local/${encodeURIComponent(filename)}`,
mimeType: "text/markdown",
description: `本地笔记: ${filename}`
}))
};
} catch (error: any) {
console.error('[Resources] 列出资源失败:', error);
return { resources: [] };
}
}
}),
/**
* 处理资源读取请求:根据 URI 中的 filename 参数读取对应笔记内容。
* @param uri - 请求的 URI 对象
* @param params - 包含从 URI 提取的 filename 参数
* @returns 符合 MCP 规范的资源内容对象
* @throws 当读取失败时抛出错误
*/
async (uri, { filename }) => {
try {
// filename 参数由 ResourceTemplate 自动从 URI 中提取
const filenameStr = Array.isArray(filename) ? filename[0] : filename;
const decodedFilename = decodeURIComponent(filenameStr);
const content = await readObsidianNote(decodedFilename);
return {
contents: [{
uri: uri.href,
mimeType: "text/markdown",
text: content
}]
};
} catch (error: any) {
// 错误处理:返回错误信息
throw new Error(`读取笔记失败: ${error.message}`);
}
}
);
// ==========================================
// 启动 STDIO 传输层
// ==========================================
/**
* 启动 MCP 服务器,连接 STDIO 传输层。
* 该函数创建 StdioServerTransport 实例并连接到 server。
* @returns {Promise<void>} 无返回值
*/
async function start() {
console.error('[S2] 正在启动 MCP Server (Prompts + 检索词工具)...');
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('[S2] MCP Server 已就绪,等待 Cherry Studio 连接');
}
start().catch((err) => {
console.error('[S2] 服务器启动失败:', err);
process.exit(1);
});
关键解析:
-
初始化:
new McpServer({ name, version })创建服务器实例。 -
Prompts 注册:每个框架对应一个
server.prompt调用。- 参数模式使用
zod定义,并附带describe供客户端展示。 - 异步处理函数中调用
handlePromptsGet获取消息序列,随后对messages进行清洗:- 仅保留
role为"user"或"assistant"的消息(移除可能的系统消息)。 - 确保
content对象包含annotations和_meta字段(即使为undefined),避免协议校验失败。
- 仅保留
- 参数模式使用
-
Tools 注册:类似 Prompts,但处理函数调用
handleToolCall,并对返回的content数组统一设置type: "text"。 -
Resources 注册:使用
ResourceTemplate实现动态资源。list回调:调用listObsidianNotes获取所有笔记文件名,组装为资源列表。- 读取回调:从 URI 中提取
filename,解码后调用readObsidianNote,返回文件内容。
-
启动:
start()函数创建StdioServerTransport并连接到服务器,输出就绪日志。
3.1.3 核心函数详解
接入层包含两类核心函数:匿名回调函数(由 SDK 在收到请求时触发)和 启动函数 start。以下分别说明其逻辑、调用关系与数据流。
3.1.3.1 Prompts 回调处理流程(以 scqa 为例)
- 逻辑流程图
flowchart TD
A[收到 prompts/get 请求] --> B[调用 scqa 回调]
B --> C[提取 args 参数]
C --> D["调用 handlePromptsGet('scqa', args)"]
D --> E[从 promptService 获取消息序列]
E --> F[过滤 messages:仅保留 user/assistant]
F --> G[补全每个 content 的 annotations/_meta]
G --> H[返回格式化后的响应]
- 调用序列图
sequenceDiagram
participant Client as Cherry Studio
participant App as app.ts (scqa回调)
participant Ctrl as promptsController
participant Svc as promptService
Client->>App: prompts/get?name=scqa&args={...}
App->>Ctrl: handlePromptsGet('scqa', args)
Ctrl->>Svc: getFramework('scqa', args)
Svc-->>Ctrl: messages 数组
Ctrl-->>App: 原始消息序列
App->>App: 清洗消息(过滤/补全)
App-->>Client: 符合 MCP 规范的响应
- 关系与数据流
- 输入:框架名称
scqa和参数对象(如{ situation, complication, ... })。 - 输出:包含
messages数组的响应,每个消息对象结构为{ role, content: { type, text, annotations, _meta } }。 - 依赖:
promptsController.handlePromptsGet→promptService.getFramework→ 框架 JSON 文件。
3.1.3.2 Tools 回调处理流程(以 save_note 为例)
- 逻辑流程图
flowchart TD
A[收到 prompts/get 请求] --> B[调用 scqa 回调]
B --> C[提取 args 参数]
C --> D["调用 handlePromptsGet('scqa', args)"]
D --> E[从 promptService 获取消息序列]
E --> F[过滤 messages:仅保留 user 和 assistant]
F --> G[补全每个 content 的 annotations 和 _meta]
G --> H[返回格式化后的响应]
- 调用序列图
sequenceDiagram
participant Client as Cherry Studio
participant App as app.ts (scqa回调)
participant Ctrl as promptsController
participant Svc as promptService
Client->>App: prompts/get?name=scqa&args={...}
App->>Ctrl: handlePromptsGet('scqa', args)
Ctrl->>Svc: getFramework('scqa', args)
Svc-->>Ctrl: messages 数组
Ctrl-->>App: 原始消息序列
App->>App: 清洗消息(过滤/补全)
App-->>Client: 符合 MCP 规范的响应
关系与数据流
- 输入:工具名称
save_note和参数{ filename, content }。 - 输出:包含
content数组的响应,每个元素为{ type: 'text', text: string }。 - 依赖:
toolsController.handleToolCall→ 具体处理函数 →resourceService.saveNote→ 本地文件系统。
3.1.3.3 Resources 回调处理流程
- 资源列表回调
flowchart TD
A[收到 resources/list 请求] --> B[触发 resourceTemplate.list]
B --> C[调用 listObsidianNotes]
C --> D[获取 .md 文件列表]
D --> E[组装 resources 数组(name, uri, mimeType)]
E --> F[返回资源列表]
- 资源读取回调
flowchart TD
A[收到 resources/read 请求] --> B[触发 resource 读取回调]
B --> C[从 URI 提取 filename 参数]
C --> D[解码 filename]
D --> E[调用 readObsidianNote]
E --> F[读取文件内容]
F --> G[返回 contents 数组]
- 调用序列图
sequenceDiagram
participant Client as Cherry Studio
participant App as app.ts (resource回调)
participant Svc as resourceService
Client->>App: resources/read?uri=note://local/example.md
App->>App: 提取 filename=example.md
App->>Svc: readObsidianNote('example.md')
Svc-->>App: 文件内容
App-->>Client: 资源内容响应
关系与数据流
- 输入:资源 URI(如
note://local/example.md)。 - 输出:
contents数组,包含uri、mimeType、text。 - 依赖:
resourceService.listObsidianNotes/readObsidianNote→ 本地文件系统
3.1.3.4 启动函数 start
- 逻辑流程图
flowchart TD
A["调用 start()"] --> B[创建 StdioServerTransport 实例]
B --> C["server.connect(transport)"]
C --> D[输出就绪日志]
D --> E[等待客户端连接]
调用关系
start被立即执行,捕获错误并退出进程。
3.1.4 未来优化
潜在优化点:
- 自动注册:当前框架和工具需要手动编写注册代码。未来可通过读取目录下的 JSON 文件,动态生成注册逻辑,实现零代码扩展。
- 消息清洗复用:多个 prompt 回调中存在重复的清洗代码,可提取为独立工具函数(如
sanitizeMessages)。 - 错误处理增强:资源回调中直接抛出错误,MCP 客户端可能无法友好显示。可返回标准错误响应格式。
- 性能监控:在回调中加入耗时日志,便于定位性能瓶颈。
- 配置化:服务器名称、版本等信息可从环境变量或配置文件读取,提高部署灵活性。
3.2 控制层
3.2.1 模块职责与设计目标
职责
控制层位于接入层与服务层之间,负责:
- 接收并路由请求:根据原语类型(prompts/tools)和名称,将请求分发给对应的业务处理函数。
- 参数校验与转换:对客户端传入的参数进行格式校验(使用 Zod schema),确保数据合法性。
- 调用服务层:将校验后的参数传递给服务层函数,执行核心业务逻辑。
- 结果格式化:将服务层返回的数据组装成 MCP 协议要求的响应格式,统一添加必要字段(如
type: "text")。 - 错误处理:捕获服务层抛出的异常,转换为标准错误响应返回给客户端。
设计目标
- 职责单一:每个控制器函数只处理一类请求,逻辑清晰。
- 统一入口:
handleToolCall作为所有工具调用的统一入口,减少接入层重复代码。 - 防御性编程:通过 Zod 校验确保输入合法性,避免脏数据流入服务层。
- 错误友好:所有异常均被捕获并格式化为包含错误信息的 MCP 响应,客户端可正常显示。
- 易于扩展:新增工具只需在
handleToolCall中添加 case 分支,并实现对应的处理函数。
3.2.2 代码实现与解析
src/controllers/promptsController.ts
/**
* Project Caffeine v0.1.1
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
import { listFrameworks, getFramework } from '../services/promptService';
/**
* 处理 prompts/list 请求,返回所有可用思维框架的元信息列表。
*
* 该函数从服务层获取所有框架的概要信息(名称、描述、参数定义),
* 并将其转换为 MCP prompts/list 响应所要求的格式。
*
* @returns {Promise<{ prompts: Array<{ name: string, description: string, arguments: Array<{ name: string, description: string, required: boolean }> }> }>}
* 符合 MCP 规范的 prompts 列表,每个 prompt 包含名称、描述及参数列表。
*
* @throws 不会直接抛出异常,服务层错误已在 listFrameworks 内部处理并返回空数组。
*/
export async function handlePromptsList() {
const frameworks = await listFrameworks();
return {
prompts: frameworks.map(f => ({
name: f.name,
description: f.description,
arguments: f.parameters.map(p => ({
name: p.name,
description: p.description,
required: p.required
}))
}))
};
}
/**
* 处理 prompts/get 请求,获取指定思维框架的完整提示词消息序列。
*
* 该函数根据框架名称和用户传入的参数,调用服务层组装包含系统提示、
* few-shot 示例和当前用户请求的 messages 数组,并返回符合 MCP 规范的响应。
*
* @param {string} name - 框架名称(如 "scqa", "swot" 等)
* @param {Record<string, string>} args - 用户传入的参数键值对,用于填充模板中的变量
*
* @returns {Promise<{ description: string, messages: Array<{ role: string, content: { type: string, text: string } }> }>}
* 包含描述信息和消息序列的响应对象,可直接用于 MCP prompts/get 响应。
*
* @throws {Error} 当框架不存在或服务层组装失败时,抛出错误(将被上层捕获并返回给客户端)。
*/
export async function handlePromptsGet(name: string, args: Record<string, string>) {
try {
const result = await getFramework(name, args || {});
return {
description: `框架: ${name}`,
messages: result.messages
};
} catch (error: any) {
throw new Error(`获取框架失败: ${error.message}`);
}
}
关键解析:
-
handlePromptsList:调用服务层listFrameworks获取所有框架的元信息,然后将其转换为 MCPprompts/list响应格式。每个框架包含name、description和arguments数组(参数名称、描述、是否必需)。 -
handlePromptsGet:接收框架名称和参数对象,调用getFramework获取完整消息序列。若成功,返回包含description和messages的对象;若失败,抛出新错误(错误信息将被接入层捕获并返回给客户端)。 -
错误处理:这里直接抛出异常,由上层(接入层回调)捕获。也可以选择返回标准错误响应,但当前设计倾向于抛出,以便接入层统一处理。
src/controllers/toolsController.ts
/**
* Project Caffeine v0.1.1
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
import { generateSearchQueries } from '../services/intentService';
import { listObsidianNotes, readObsidianNote, saveNote } from '../services/resourceService';
import { z } from 'zod';
import { generateSearchQueriesSchema, saveNoteSchema } from '../models/schemas';
/**
* 统一工具调用处理入口。
*
* 根据工具名称分发到对应的具体处理函数,并对未知工具返回错误响应。
*
* @param toolName - 工具名称,支持 'generate_search_queries'、'list_local_notes'、'read_local_note'、'save_note'
* @param params - 工具参数对象,具体结构取决于工具
* @returns MCP 工具响应格式,包含 content 数组,可能带有 isError 标记
*/
export async function handleToolCall(toolName: string, params: any) {
switch (toolName) {
case 'generate_search_queries':
return handleGenerateSearchQueries(params);
case 'list_local_notes':
return handleListLocalNotes();
case 'read_local_note':
return handleReadLocalNote(params);
case 'save_note':
return handleSaveNote(params);
default:
return {
content: [{ type: 'text', text: `未知工具: ${toolName}` }],
isError: true
};
}
}
/**
* 处理检索词生成工具 (generate_search_queries)。
*
* 该函数接收用户原始查询语句,通过 intentService 生成 3~5 个专业检索词,
* 并以 JSON 字符串形式返回。
*
* @param params - 工具参数对象,应包含 query 字段
* @param params.query - 用户的原始查询语句
* @returns MCP 工具响应,成功时 content 包含 JSON 格式的检索词数组;失败时 content 包含错误信息且 isError 为 true
*/
async function handleGenerateSearchQueries(params: any) {
// 使用集中管理的 schema 进行参数校验
const parseResult = generateSearchQueriesSchema.safeParse(params);
if (!parseResult.success) {
return {
content: [{ type: 'text', text: `参数错误: ${parseResult.error.message}` }],
isError: true
};
}
const { query } = parseResult.data;
try {
const queries = generateSearchQueries(query);
return {
content: [{ type: 'text', text: JSON.stringify(queries, null, 2) }]
};
} catch (error: any) {
console.error('[ToolsController] generate_search_queries 失败:', error);
return {
content: [{ type: 'text', text: `执行失败: ${error.message}` }],
isError: true
};
}
}
/**
* 处理列出本地笔记工具 (list_local_notes)。
*
* 调用 resourceService 获取知识库中所有 Markdown 笔记的文件名,
* 并以文本列表形式返回。
*
* @returns MCP 工具响应,成功时 content 包含笔记列表文本;失败时 content 包含错误信息且 isError 为 true
*/
async function handleListLocalNotes() {
try {
const notes = await listObsidianNotes();
return {
content: [{
type: 'text',
text: notes.length > 0 ? `找到了以下笔记:\n${notes.join('\n')}` : '未找到笔记。'
}]
};
} catch (error: any) {
console.error('[ToolsController] list_local_notes 失败:', error);
return {
content: [{ type: 'text', text: `执行失败: ${error.message}` }],
isError: true
};
}
}
/**
* 处理读取本地笔记工具 (read_local_note)。
*
* 接收文件名参数,调用 resourceService 读取对应笔记内容并返回。
*
* @param params - 工具参数对象,应包含 filename 字段
* @param params.filename - 要读取的笔记文件名(必须包含 .md 后缀)
* @returns MCP 工具响应,成功时 content 包含笔记内容;失败时 content 包含错误信息且 isError 为 true
*/
async function handleReadLocalNote(params: any) {
const schema = z.object({
filename: z.string().min(1, '文件名不能为空').includes('.md', { message: '文件名必须包含 .md 后缀' })
});
const parseResult = schema.safeParse(params);
if (!parseResult.success) {
return {
content: [{ type: 'text', text: `参数错误: ${parseResult.error.message}` }],
isError: true
};
}
const { filename } = parseResult.data;
try {
const content = await readObsidianNote(filename);
return {
content: [{ type: 'text', text: content }]
};
} catch (error: any) {
console.error('[ToolsController] read_local_note 失败:', error);
return {
content: [{ type: 'text', text: `读取失败: ${error.message}` }],
isError: true
};
}
}
/**
* 处理保存笔记工具 (save_note)。
*
* 接收文件名和内容参数,调用 resourceService 将笔记保存到本地知识库。
*
* @param params - 工具参数对象,应包含 filename 和 content 字段
* @param params.filename - 笔记文件名(必须以 .md 结尾)
* @param params.content - 笔记内容(Markdown 格式)
* @returns MCP 工具响应,成功时 content 包含保存成功信息;失败时 content 包含错误信息且 isError 为 true
*/
async function handleSaveNote(params: any) {
const parseResult = saveNoteSchema.safeParse(params);
if (!parseResult.success) {
return {
content: [{ type: 'text', text: `参数错误: ${parseResult.error.message}` }],
isError: true
};
}
const { filename, content } = parseResult.data;
try {
const message = await saveNote(filename, content);
return {
content: [{ type: 'text', text: message }]
};
} catch (error: any) {
console.error('[ToolsController] save_note 失败:', error);
return {
content: [{ type: 'text', text: `保存失败: ${error.message}` }],
isError: true
};
}
}
关键解析:
-
统一入口
handleToolCall:根据toolName分发到具体处理函数。若工具名不匹配,返回包含错误信息的响应,并标记isError: true。 -
参数校验:
generate_search_queries和save_note使用集中定义的 Zod schema(位于schemas.ts),确保校验规则统一且可复用。read_local_note临时定义了一个 schema,但也可提取到schemas.ts中以便复用。
-
错误处理:每个处理函数内部均使用
try-catch捕获服务层异常,并返回格式化的错误响应(包含isError: true)。这样即使服务层出错,客户端也能收到明确提示。 -
返回格式:所有成功的响应均包含
content数组,每个元素为{ type: 'text', text: string },符合 MCP 工具响应规范。
3.2.3 核心函数详解
3.2.3.1 handlePromptsList
- 逻辑流程图
flowchart TD
A[收到 prompts/list 请求] --> B[调用 listFrameworks]
B --> C[获取框架元信息列表]
C --> D[遍历框架,转换为 prompts 格式]
D --> E["返回 { prompts: [...] }"]
- 调用序列图
sequenceDiagram
participant App as app.ts (prompts/list 回调)
participant Ctrl as promptsController
participant Svc as promptService
App->>Ctrl: handlePromptsList()
Ctrl->>Svc: listFrameworks()
Svc-->>Ctrl: 框架元信息数组
Ctrl->>Ctrl: 格式转换
Ctrl-->>App: 标准 prompts 列表
App-->>Client: MCP 响应
- 关系与数据流
- 输入:无(
prompts/list请求不带参数)。 - 输出:符合 MCP 规范的
prompts列表,每个 prompt 包含名称、描述、参数定义。 - 依赖:
promptService.listFrameworks→ 框架 JSON 文件。
3.2.3.2 handlePromptsGet
- 逻辑流程图
flowchart TD
A[收到 prompts/get 请求] --> B[接收 name 和 args]
B --> C["调用 getFramework(name, args)"]
C --> D{是否成功?}
D -- 是 --> E["组装响应 { description, messages }"]
D -- 否 --> F[抛出错误]
E --> G[返回响应]
- 调用序列图
sequenceDiagram
participant App as app.ts (prompts/get 回调)
participant Ctrl as promptsController
participant Svc as promptService
App->>Ctrl: handlePromptsGet(name, args)
Ctrl->>Svc: getFramework(name, args)
Svc-->>Ctrl: 消息序列
Ctrl-->>App: 格式化后的响应
App-->>Client: MCP 响应
- 关系与数据流
- 输入:框架名称
name(如"scqa")和参数字典args。 - 输出:包含
description和messages的对象。 - 依赖:
promptService.getFramework→ 框架 JSON + 角色 JSON。
3.2.3.3 handleToolCall 与工具处理函数
以 save_note 工具为例,展示统一入口和具体处理函数的协作。
- 逻辑流程图(
handleToolCall+handleSaveNote)
flowchart TD
A[收到 tool/call 请求] --> B["调用 handleToolCall(toolName, params)"]
B --> C{根据 toolName 分发}
C -- save_note --> D["调用 handleSaveNote(params)"]
C -- 其他工具 --> E[其他处理函数]
D --> F[使用 saveNoteSchema 校验参数]
F --> G{校验通过?}
G -- 否 --> H[返回参数错误响应]
G -- 是 --> I[调用 saveNote 服务]
I --> J{服务执行是否成功?}
J -- 是 --> K[返回成功响应]
J -- 否 --> L[返回错误响应]
H --> M[返回响应给接入层]
K --> M
L --> M
- 调用序列图(save_note 工具)
sequenceDiagram
participant App as app.ts (save_note 回调)
participant Ctrl as toolsController
participant Svc as resourceService
App->>Ctrl: handleToolCall('save_note', params)
Ctrl->>Ctrl: 分发到 handleSaveNote
Ctrl->>Ctrl: 参数校验 (saveNoteSchema)
Ctrl->>Svc: saveNote(filename, content)
Svc-->>Ctrl: 保存结果消息
Ctrl-->>App: content 数组
App-->>Client: MCP 工具响应
- 关系与数据流
- 输入:工具名称和参数对象(如
{ filename: "note.md", content: "# Title" })。 - 输出:MCP 工具响应,包含
content数组,可能包含isError: true表示错误。 - 依赖:
resourceService.saveNote→ 本地文件系统;Zod schema 用于校验。
其他工具处理函数(如 handleGenerateSearchQueries)具有相似的结构,仅调用的服务和校验规则不同。
3.2.4 未来优化
潜在优化点:
- 校验规则集中管理:
read_local_note的校验 schema 目前定义在函数内部,可移至schemas.ts中统一管理,便于复用和维护。 - 错误处理统一化:各工具处理函数中的错误响应格式一致,但存在少量重复代码(如
content: [{ type: 'text', text: ... }])。可提取一个工具函数createErrorResponse(message: string)。 - 日志增强:在
handleToolCall入口和每个处理函数中添加结构化日志(如工具名称、参数、执行耗时),便于监控和调试。 - 参数校验增强:当前仅做基本类型和格式校验,未来可增加业务规则校验(如文件名不得包含特殊字符、内容大小限制)。
- 支持批量操作:某些工具(如
list_local_notes)已支持返回列表,未来可考虑分页或过滤功能,避免返回过多数据。 - 自动化测试:为每个控制函数编写单元测试,模拟服务层行为,确保错误处理和格式转换正确。
3.3 服务层
3.3.1 模块职责与设计目标
职责
服务层位于控制层之下,封装核心业务逻辑,独立于 MCP 协议细节。主要包括三大服务模块:
-
promptService.ts- 加载并缓存所有思维框架的 JSON 定义。
- 根据框架名称和用户参数,组装完整的提示词消息序列(包括系统提示、Few-Shot 示例、当前用户请求)。
- 支持通过角色 ID 引用
personas.json中的系统提示,实现角色与框架的解耦。
-
intentService.ts- 实现意图拆解功能,将用户的自然语言查询转换为专业检索词列表。
- 核心算法:清洗文本、分词、去重、补全、截取,生成 3~5 个检索词。
-
resourceService.ts- 封装对本地知识库(Markdown 文件夹)的文件操作。
- 提供笔记列表、内容读取、笔记保存功能。
- 实现严格的安全校验,防止路径遍历攻击。
设计目标
- 高内聚低耦合:每个服务聚焦单一职责,对外提供清晰接口,不依赖上层(控制器)的调用方式。
- 缓存优化:
promptService缓存框架定义,避免重复读盘。 - 安全优先:
resourceService在文件操作前进行路径校验,确保操作仅限于指定目录。 - 易于测试:服务层函数不涉及 MCP 响应格式,纯业务逻辑,便于单元测试。
- 可扩展:新增框架只需在
models/frameworks/下添加 JSON 文件,服务层自动加载;新增文件操作只需在resourceService中扩展。
3.3.2 代码实现与解析
src/services/promptsService.ts
/**
* Project Caffeine v0.1.1
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
import fs from 'fs/promises';
import path from 'path';
import personas from '../models/personas/personas.json';
// ==========================================
// 类型定义
// ==========================================
/**
* 思维框架的定义结构,与框架 JSON 文件中的字段一一对应。
*/
export interface Framework {
name: string;
description: string;
parameters: Array<{ name: string; description: string; required: boolean }>;
template: string;
systemPrompt?: string;
persona?: string; // 引用的角色 ID
examples?: Array<{ // Few-Shot 示例
input: Record<string, string>; // 示例输入参数
output: string; // 期望输出(Markdown 格式)
}>;
}
/**
* MCP Prompts 原语所要求的消息序列格式。
*/
export type PromptResult = {
messages: Array<{
role: 'system' | 'user' | 'assistant';
content: { type: 'text'; text: string };
}>;
};
// ==========================================
// 框架缓存与加载
// ==========================================
/** 框架定义文件存放的目录路径 */
const FRAMEWORKS_DIR = path.join(__dirname, '../models/frameworks');
/** 框架缓存,避免重复读取文件系统 */
let frameworksCache: Framework[] | null = null;
/**
* 从文件系统加载所有框架 JSON 文件。
*
* 该函数会读取 FRAMEWORKS_DIR 下所有 .json 文件,解析为 Framework 对象,
* 并存入缓存。首次调用后,后续调用直接返回缓存数据。
*
* @returns {Promise<Framework[]>} 框架对象数组,若加载失败则返回空数组
*/
async function loadFrameworks(): Promise<Framework[]> {
if (frameworksCache) return frameworksCache;
try {
const files = await fs.readdir(FRAMEWORKS_DIR);
const jsonFiles = files.filter(f => f.endsWith('.json'));
const frameworks = await Promise.all(
jsonFiles.map(async file => {
const content = await fs.readFile(path.join(FRAMEWORKS_DIR, file), 'utf-8');
return JSON.parse(content) as Framework;
})
);
frameworksCache = frameworks;
return frameworks;
} catch (error) {
console.error('[PromptService] 加载框架失败:', error);
return [];
}
}
// ==========================================
// 公开 API
// ==========================================
/**
* 列出所有可用框架的元信息(不含模板、系统提示词和示例)。
*
* @returns {Promise<Array<Omit<Framework, 'template' | 'systemPrompt' | 'examples'>>>}
* 框架元信息列表,每个框架包含名称、描述和参数列表
*/
export async function listFrameworks(): Promise<Array<Omit<Framework, 'template' | 'systemPrompt' | 'examples'>>> {
const frameworks = await loadFrameworks();
return frameworks.map(({ name, description, parameters }) => ({
name,
description,
parameters
}));
}
/**
* 获取指定框架的完整提示词消息序列。
*
* 该函数根据框架名称查找对应的框架定义,结合用户传入的参数,
* 构建包含系统提示、Few-Shot 示例和当前用户请求的消息数组。
* 系统提示优先使用框架关联的角色(persona),若未定义则使用框架自带的 systemPrompt。
*
* @param {string} name - 框架名称,需与框架 JSON 文件中的 name 字段一致
* @param {Record<string, string>} args - 用户传入的参数键值对,用于填充模板中的 {{param}} 占位符
* @returns {Promise<PromptResult>} 符合 MCP 规范的消息序列对象
* @throws {Error} 当指定名称的框架不存在时抛出错误
*/
export async function getFramework(name: string, args: Record<string, string>): Promise<PromptResult> {
const frameworks = await loadFrameworks();
const framework = frameworks.find(f => f.name === name);
if (!framework) {
throw new Error(`框架 "${name}" 不存在`);
}
// ==========================================
// 确定系统提示词(优先使用角色矩阵)
// ==========================================
let systemPrompt = framework.systemPrompt || '';
if (framework.persona) {
const persona = personas.find(p => p.id === framework.persona);
if (persona) {
systemPrompt = persona.systemPrompt;
}
}
// ==========================================
// 构建消息数组
// ==========================================
const messages: PromptResult['messages'] = [];
// 1. 系统消息
if (systemPrompt) {
messages.push({
role: 'system',
content: { type: 'text', text: systemPrompt }
});
}
// 2. Few-Shot 示例(如果存在)
if (framework.examples && Array.isArray(framework.examples)) {
for (const example of framework.examples) {
// 构建示例用户输入:将 example.input 中的参数填充到模板中
let exampleUserContent = framework.template;
for (const [key, value] of Object.entries(example.input)) {
exampleUserContent = exampleUserContent.replace(new RegExp(`{{${key}}}`, 'g'), value);
}
messages.push({
role: 'user',
content: { type: 'text', text: exampleUserContent }
});
// 添加示例助手输出
messages.push({
role: 'assistant',
content: { type: 'text', text: example.output }
});
}
}
// 3. 当前用户请求
let currentUserContent = framework.template;
for (const [key, value] of Object.entries(args)) {
currentUserContent = currentUserContent.replace(new RegExp(`{{${key}}}`, 'g'), value);
}
messages.push({
role: 'user',
content: { type: 'text', text: currentUserContent }
});
return { messages };
}
关键解析:
-
类型定义:
Framework接口与框架 JSON 的结构严格对应,确保类型安全;PromptResult定义 MCP 所期望的消息序列格式。 -
框架加载与缓存:
loadFrameworks首次调用时读取FRAMEWORKS_DIR下所有.json文件,解析后缓存到frameworksCache,后续调用直接返回缓存,提升性能。 -
listFrameworks:对外暴露框架的元信息(名称、描述、参数),隐藏模板和示例等内部数据,保护知识产权并减少网络传输。 -
getFramework:核心组装逻辑:- 根据
name查找框架,若不存在则抛出错误。 - 确定系统提示词:优先使用框架关联的
persona从personas.json中获取,若没有则回退到框架自带的systemPrompt。 - 构建消息序列:
- 首先插入系统消息(如果有)。
- 然后遍历
examples,将示例的input填充到模板生成用户消息,并将示例的output作为助手消息插入(实现 Few-Shot 学习)。 - 最后将当前用户参数填充到模板,插入用户消息。
- 返回符合
PromptResult格式的对象。
- 根据
src/services/intentServices/ts
/**
* Project Caffeine v0.1.1
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
/**
* 将用户的自然语言查询拆解为专业检索词列表
* @param query 用户原始查询字符串
* @returns 去重后的检索词数组(3~5 个)
*/
export function generateSearchQueries(query: string): string[] {
if (!query || query.trim().length === 0) {
return ['通用研究主题'];
}
// 1. 去除常见标点符号,替换为空格
const cleaned = query.replace(/[,,。??、;;]/g, ' ');
// 2. 按空白字符分割,过滤掉长度小于 2 的词(避免单字噪音)
const words = cleaned.split(/\s+/).filter(word => word.length >= 2);
// 3. 去重
const uniqueWords = [...new Set(words)];
// 4. 若不足 3 个,补充基于原查询的扩展词
while (uniqueWords.length < 3) {
uniqueWords.push(`${query} 相关研究`);
}
// 5. 截取前 5 个返回
return uniqueWords.slice(0, 5);
}
关键解析:
- 输入为空:返回一个默认检索词
['通用研究主题'],保证下游工具(如搜索引擎)能够运行。 - 文本清洗:使用正则替换常见中文/英文标点为空格,避免标点干扰分词。
- 分词与过滤:按空白分割,过滤掉长度小于 2 的词(如“的”、“是”等停用词可被自然过滤)。
- 去重:使用
Set去除重复词,减少冗余。 - 补全机制:若有效词少于 3 个,循环追加
"${query} 相关研究",确保至少返回 3 个检索词。 - 数量限制:截取前 5 个返回,避免过多检索词导致下游超时或噪音。
src/services/resourcesServices.ts
/**
* Project Caffeine v0.1.1
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
import fs from 'fs/promises';
import path from 'path';
/**
* 本地知识库的根目录路径。
*
* 该目录存放所有 Markdown 笔记文件,所有文件操作均限定在此目录内,
* 以防止路径遍历攻击。
*
* @constant {string}
*/
const OBSIDIAN_VAULT_PATH = '/home/wguo/Downloads/MyVault'; // 【⚠️ 重要配置】请修改为你电脑上真实的 Markdown 笔记文件夹绝对路径!
/**
* 列出知识库中所有 Markdown 笔记的文件名。
*
* 该函数读取 OBSIDIAN_VAULT_PATH 目录下的所有文件,过滤出以 .md 结尾
* (不区分大小写)的文件,并返回文件名列表。若目录不存在或无权限访问,
* 则返回空数组并打印错误日志。
*
* @returns {Promise<string[]>} 包含所有笔记文件名的数组,若失败则返回空数组。
*/
export async function listObsidianNotes(): Promise<string[]> {
try {
const files = await fs.readdir(OBSIDIAN_VAULT_PATH);
return files.filter(file => file.toLowerCase().endsWith('.md'));
} catch (error: any) {
console.error(`[Project Caffeine] 无法读取知识库目录: ${error.message}`);
return [];
}
}
/**
* 读取指定笔记文件的完整内容。
*
* 该函数首先对文件名进行安全校验,确保文件位于知识库目录内,
* 防止路径遍历攻击。校验通过后,读取文件内容并返回。
*
* @param {string} filename - 要读取的笔记文件名(必须包含 .md 后缀)
* @returns {Promise<string>} 笔记文件的文本内容
* @throws {Error} 当文件名导致路径越界时抛出安全警告
* @throws {Error} 当文件不存在或无权限读取时抛出错误
*/
export async function readObsidianNote(filename: string): Promise<string> {
const targetPath = path.resolve(OBSIDIAN_VAULT_PATH, filename);
const safeVaultPath = path.resolve(OBSIDIAN_VAULT_PATH);
// 核心防御:防止大模型通过传入 "../../" 读取系统敏感文件
if (!targetPath.startsWith(safeVaultPath)) {
throw new Error(`安全警告:越权访问拦截!禁止读取目录外的文件: ${filename}`);
}
try {
const content = await fs.readFile(targetPath, 'utf-8');
return content;
} catch (error: any) {
throw new Error(`无法读取笔记 [${filename}]: 文件可能不存在或无权限。`);
}
}
/**
* 保存笔记到本地知识库。
*
* 该函数将内容写入指定文件,执行以下校验和操作:
* 1. 验证文件名是否以 .md 结尾。
* 2. 验证文件路径是否在知识库目录内,防止路径遍历攻击。
* 3. 确保知识库目录存在(若不存在则自动创建)。
* 4. 将内容写入文件。
*
* @param {string} filename - 笔记文件名(必须以 .md 结尾)
* @param {string} content - 笔记内容(Markdown 格式)
* @returns {Promise<string>} 保存成功的提示信息,包含文件绝对路径
* @throws {Error} 当文件名不以 .md 结尾时抛出错误
* @throws {Error} 当文件名导致路径越界时抛出错误
* @throws {Error} 当目录创建失败或文件写入失败时抛出错误
*/
export async function saveNote(filename: string, content: string): Promise<string> {
// 1. 验证文件名是否以 .md 结尾
if (!filename.endsWith('.md')) {
throw new Error('文件名必须以 .md 结尾');
}
// 2. 防止路径遍历攻击:解析绝对路径,并检查是否在 NOTES_DIR 下
const fullPath = path.resolve(OBSIDIAN_VAULT_PATH, filename);
const relative = path.relative(OBSIDIAN_VAULT_PATH, fullPath);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
throw new Error('无效的文件名,不允许访问上层目录');
}
// 3. 确保目标目录存在(可选,如果 NOTES_DIR 必须存在则可跳过)
await fs.mkdir(OBSIDIAN_VAULT_PATH, { recursive: true });
// 4. 写入文件
await fs.writeFile(fullPath, content, 'utf-8');
return `笔记已保存至: ${fullPath}`;
}
关键解析:
-
常量配置:
OBSIDIAN_VAULT_PATH为本地知识库的绝对路径,需用户手动修改。 -
listObsidianNotes:读取目录,过滤出.md文件(不区分大小写)。若目录不存在或无权限,返回空数组并打印错误日志,避免服务崩溃。 -
readObsidianNote:- 先使用
path.resolve获取目标文件的绝对路径。 - 安全校验:检查目标路径是否以知识库路径开头,防止通过
../等相对路径访问外部文件。 - 若校验通过,读取文件内容;若失败,抛出明确错误。
- 先使用
-
saveNote:- 验证文件名是否以
.md结尾。 - 安全校验:使用
path.relative计算相对路径,检查是否越界(relative.startsWith('..')或path.isAbsolute(relative))。 - 确保知识库目录存在(
mkdir带recursive选项,若目录已存则不报错)。 - 写入文件,返回成功信息。
- 验证文件名是否以
3.3.3 核心函数详解
3.3.3.1 promptService.loadFrameworks
- 逻辑流程图
flowchart TD
A[调用 loadFrameworks] --> B{缓存存在?}
B -- 是 --> C[返回缓存]
B -- 否 --> D[读取 FRAMEWORKS_DIR]
D --> E[过滤出 .json 文件]
E --> F[并发读取所有 JSON 文件]
F --> G[解析为 Framework 对象]
G --> H[存入缓存]
H --> I[返回框架数组]
- 调用序列图
sequenceDiagram
participant Caller as 调用方(如 listFrameworks)
participant Svc as promptService
participant FS as 文件系统
Caller->>Svc: loadFrameworks()
Svc->>Svc: 检查缓存
alt 缓存未命中
Svc->>FS: readdir(FRAMEWORKS_DIR)
FS-->>Svc: 文件列表
loop 每个 .json 文件
Svc->>FS: readFile(file)
FS-->>Svc: JSON 内容
Svc->>Svc: JSON.parse
end
Svc->>Svc: 存入缓存
end
Svc-->>Caller: Framework[]
- 关系与数据流
- 输入:无。
- 输出:
Framework[]数组。 - 依赖:文件系统读取操作。
- 缓存策略:使用模块级变量
frameworksCache,在服务生命周期内有效,避免重复读盘。
3.3.3.2 promptService.getFramework
- 逻辑流程图
flowchart TD
A["调用 getFramework(name, args)"] --> B[加载所有框架]
B --> C[查找名称为 name 的框架]
C --> D{框架存在?}
D -- 否 --> E[抛出错误]
D -- 是 --> F[确定系统提示]
F --> G[初始化 messages 数组]
G --> H["添加系统消息(如有)"]
H --> I{有 examples?}
I -- 是 --> J[遍历 examples]
J --> K[填充示例 input 到模板]
K --> L[添加 user 消息]
L --> M[添加 assistant 消息]
M --> J
I -- 结束 --> N[填充当前 args 到模板]
N --> O[添加当前 user 消息]
O --> P["返回 { messages }"]
- 调用序列图
sequenceDiagram
participant Ctrl as promptsController
participant Svc as promptService
participant FS as 文件系统
participant Personas as personas.json
Ctrl->>Svc: getFramework('pestle', { domain: 'AI芯片' })
Svc->>Svc: loadFrameworks()
Svc-->>Svc: 框架数组
Svc->>Svc: 查找 pestle 框架
Svc->>Personas: 根据 persona 查找角色
Personas-->>Svc: 角色定义
Svc->>Svc: 组装 messages
Svc-->>Ctrl: PromptResult
- 关系与数据流
- 输入:框架名称
name,参数字典args。 - 输出:
PromptResult对象(包含messages数组)。 - 依赖:
loadFrameworks返回的框架定义,personas.json角色配置。 - 核心逻辑:模板填充(正则替换
{{key}}),Few-Shot 示例插入,系统提示选择。
3.3.3.3 intentService.generateSearchQueries
- 逻辑流程图
flowchart TD
A[输入 query] --> B{query 为空?}
B -- 是 --> C["返回 ['通用研究主题']"]
B -- 否 --> D[清洗标点]
D --> E[按空格分割,过滤长度<2的词]
E --> F[去重]
F --> G{长度 < 3?}
G -- 是 --> H["循环补全 '${query} 相关研究'"]
H --> I[截取前5个]
G -- 否 --> I
I --> J[返回检索词数组]
- 调用序列图
sequenceDiagram
participant Ctrl as toolsController
participant Svc as intentService
Ctrl->>Svc: generateSearchQueries('AI芯片市场趋势2026')
Svc->>Svc: 清洗、分词、去重
Svc-->>Ctrl: ['AI芯片','市场趋势','2026','AI芯片市场趋势2026 相关研究']
- 关系与数据流
- 输入:用户查询字符串。
- 输出:字符串数组(检索词)。
- 核心算法:基于规则的简单 NLP,不依赖外部库,轻量高效。
3.3.3.4 resourceService.listObsidianNotes
- 逻辑流程图
flowchart TD
A[调用 listObsidianNotes] --> B[读取 OBSIDIAN_VAULT_PATH 目录]
B --> C{读取成功?}
C -- 是 --> D[过滤 .md 文件]
D --> E[返回文件名数组]
C -- 否 --> F[打印错误日志]
F --> G[返回空数组]
- 调用序列图
sequenceDiagram
participant Ctrl as toolsController / resource回调
participant Svc as resourceService
participant FS as 文件系统
Ctrl->>Svc: listObsidianNotes()
Svc->>FS: readdir(OBSIDIAN_VAULT_PATH)
FS-->>Svc: 文件列表
Svc->>Svc: 过滤 .md
Svc-->>Ctrl: string[]
- 关系与数据流
- 输入:无。
- 输出:Markdown 文件名列表。
- 错误处理:若目录不可读,返回空数组并记录日志,不中断服务。
3.3.3.5 resourceService.readObsidianNote
- 逻辑流程图
flowchart TD
A[输入 filename] --> B[拼接目标路径 targetPath]
B --> C[获取安全路径 safeVaultPath]
C --> D{targetPath 以 safeVaultPath 开头?}
D -- 否 --> E[抛出安全异常]
D -- 是 --> F[读取文件]
F --> G{读取成功?}
G -- 是 --> H[返回文件内容]
G -- 否 --> I[抛出读取异常]
- 调用序列图
sequenceDiagram
participant Ctrl as toolsController / resource回调
participant Svc as resourceService
participant FS as 文件系统
Ctrl->>Svc: readObsidianNote('example.md')
Svc->>Svc: 路径安全校验
alt 校验通过
Svc->>FS: readFile(targetPath)
FS-->>Svc: 文件内容
Svc-->>Ctrl: content
else 校验失败
Svc-->>Ctrl: throw Error
end
- 关系与数据流
- 输入:文件名(如
"example.md")。 - 输出:文件内容字符串。
- 安全机制:必须确保文件在知识库目录内,防止路径遍历攻击。
- 错误处理:校验失败或读取失败均抛出具体错误,由调用方处理。
3.3.3.6 resourceService.saveNote
- 逻辑流程图
flowchart TD
A[输入 filename, content] --> B{文件名以 .md 结尾?}
B -- 否 --> C[抛出文件名错误]
B -- 是 --> D[拼接完整路径 fullPath]
D --> E[计算相对路径 relative]
E --> F{relative 以 '..' 开头 或 为绝对路径?}
F -- 是 --> G[抛出越界错误]
F -- 否 --> H[确保目录存在]
H --> I[写入文件]
I --> J[返回成功信息]
- 调用序列图
sequenceDiagram
participant Ctrl as toolsController
participant Svc as resourceService
participant FS as 文件系统
Ctrl->>Svc: saveNote('note.md', '# Hello')
Svc->>Svc: 校验后缀
Svc->>Svc: 路径安全校验
alt 校验通过
Svc->>FS: mkdir(OBSIDIAN_VAULT_PATH, { recursive: true })
Svc->>FS: writeFile(fullPath, content)
FS-->>Svc: 写入完成
Svc-->>Ctrl: 成功消息
else 校验失败
Svc-->>Ctrl: throw Error
end
- 关系与数据流
- 输入:文件名、内容。
- 输出:成功提示字符串。
- 安全机制:双重校验(后缀 + 路径越界)。
- 目录创建:自动创建知识库目录(若不存在),避免因目录缺失导致写入失败。
3.3.4 未来迭代优化
-
promptService迭代优化:- 支持动态角色:目前角色配置在
personas.json中静态定义,未来可支持从数据库或 API 动态获取,实现多租户定制。 - 模板引擎升级:当前使用简单的字符串替换,若模板复杂可考虑引入轻量模板引擎(如 Handlebars),支持条件判断、循环等。
- 示例缓存:若示例数量巨大,可将示例内容独立存储,按需加载,减少内存占用。
- 支持动态角色:目前角色配置在
-
intentService迭代优化:- 引入 NLP 模型:当前规则简单,可集成词向量或 LLM 生成更精准的检索词,但需权衡性能。
- 同义词扩展:对核心词汇添加同义词,提高检索召回率。
- 配置化停用词:将停用词表提取为配置文件,便于根据领域调整。
-
resourceService迭代优化- 支持子目录:当前仅支持平铺的笔记目录,可扩展为递归遍历子文件夹。
- 文件变更监听:通过
fs.watch实现实时更新资源列表,避免每次 list 都读盘。 - 大文件流式处理:若笔记文件极大,读取时可能占用大量内存,可考虑流式读取或分块返回。
- 文件类型扩展:除
.md外,支持更多文档格式(如.txt、.pdf),需增加文件解析逻辑。
-
通用优化:
- 错误码标准化:服务层抛出的错误可携带错误码(如
ERR_FILE_NOT_FOUND),便于控制器统一映射。 - 性能监控:在关键服务函数中添加耗时日志,帮助定位性能瓶颈。
- 单元测试:为每个服务编写测试用例,模拟文件系统、缓存等,确保逻辑正确性。
- 错误码标准化:服务层抛出的错误可携带错误码(如
许可声明
本文档采用 知识共享署名-相同方式共享 4.0 国际许可协议 (CC BY-SA 4.0) 进行许可,© 2025-2026 Gitconomy Research.