diff --git a/projects/arabica/docs/deisgn/arabica-srpint2-development-specification.md b/projects/arabica/docs/deisgn/arabica-srpint2-development-specification.md new file mode 100644 index 0000000..3785c23 --- /dev/null +++ b/projects/arabica/docs/deisgn/arabica-srpint2-development-specification.md @@ -0,0 +1,1851 @@ +--- +title: Arabica Srpint2 开发指南:多维思维框架引擎与意图拆解 +description: 基于 Sprint 2 架构设计,指导开发者搭建支持 MCP Prompts 原语的提示词策略 Server,实现多维思维框架库、意图拆解工具及结构化输出约束,完成从单一工具到多框架引擎的升级。 +type: Development Guide +version: v1.0.0 (Arabica) - Sprint 2 +file: arabica-sprint2-development-specification.md +author: Gitconomy Research-郭晧 +date: 2026-03-06 +tags: + - Project Caffeine + - MCP Server + - Sprint 2 + - Prompt Strategy + - Prompts + - 思维框架 + - 意图拆解 + - Node.js +license: CC BY-SA 4.0 +status: Active +--- +# Arabica Sprint 2 开发指南:多维思维框架引擎与意图拆解 + +## 1. 模块概览与架构设计 + +Sprint 2 的核心是构建一个支持 **MCP Prompts 原语**的多维思维框架引擎,同时保留 Sprint 1 的工具与资源能力。整体采用经典的三层架构: + +- **接入层**:`app.ts` 负责初始化 MCP 服务器并注册所有原语。 +- **控制层**:`controllers/` 接收请求,校验参数,调用服务层。 +- **服务层**:`services/` 实现业务逻辑(框架加载、意图拆解、文件操作)。 +- **模型层**:`models/` 存放框架定义 JSON、角色配置、Zod 校验模式。 + +下图展示了模块间的静态关系: + +```mermaid +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` | 根据框架名称和参数组装完整消息序列 | ✅ | +#### `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` | 处理 `prompts/get` 请求,返回具体框架消息 | ✅ | + +--- +**补充说明**: + +- 上述列表仅包含项目代码中显式定义的命名函数。 +- `app.ts` 中通过 `server.prompt` / `server.tool` / `server.resource` 注册的匿名回调函数未列入,因为它们仅作为 MCP 原语的处理器,内部直接调用对应的控制器函数(如 `handlePromptsGet`、`handleToolCall` 等),本身不包含独立业务逻辑。 +- 所有 Zod 校验模式定义在 `schemas.ts` 中,为对象而非函数,故未列出。 +- JSON 配置文件(如 `personas.json`、框架定义文件)不包含函数,故未列出。 + +### 2.2 整体函数调用关系图 + +下图展示了从 MCP 原语注册到具体功能实现的全链路函数调用关系,涵盖接入层、控制器、服务层及数据存储之间的交互: + +```mermaid +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)连接。 + +**设计目标** + +1. **声明式注册**:通过 SDK 提供的 `server.prompt`、`server.tool`、`server.resource` 方法,清晰定义每个原语的名称、参数模式和处理函数。 +2. **解耦与控制**:处理函数仅做基础参数转换和结果格式化,业务逻辑委托给控制器,保持接入层轻量。 +3. **健壮性**:对返回的消息进行清洗(如过滤非法 role、补全缺失字段),确保符合 MCP 协议规范。 +4. **可扩展性**:新增框架或工具只需添加对应注册代码,无需修改现有结构。 + +#### 3.1.2 代码实现与解析 + +**`src/app.ts`** + +```typescript +/** + * Project Caffeine v0.1.1 + * Copyright (c) 2025-2026 Gitconomy Research + * + * SPDX-License-Identifier: MIT + * + * Contributors: + * - 郭晧 (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); + 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} 无返回值 + */ + +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); +}); +``` + +**关键解析:** + +1. **初始化**:`new McpServer({ name, version })` 创建服务器实例。 + +2. **Prompts 注册**:每个框架对应一个 `server.prompt` 调用。 + - 参数模式使用 `zod` 定义,并附带 `describe` 供客户端展示。 + - 异步处理函数中调用 `handlePromptsGet` 获取消息序列,随后对 `messages` 进行清洗: + - 仅保留 `role` 为 `"user"` 或 `"assistant"` 的消息(移除可能的系统消息)。 + - 确保 `content` 对象包含 `annotations` 和 `_meta` 字段(即使为 `undefined`),避免协议校验失败。 + +3. **Tools 注册**:类似 Prompts,但处理函数调用 `handleToolCall`,并对返回的 `content` 数组统一设置 `type: "text"`。 + +4. **Resources 注册**:使用 `ResourceTemplate` 实现动态资源。 + - `list` 回调:调用 `listObsidianNotes` 获取所有笔记文件名,组装为资源列表。 + - 读取回调:从 URI 中提取 `filename`,解码后调用 `readObsidianNote`,返回文件内容。 + +5. **启动**:`start()` 函数创建 `StdioServerTransport` 并连接到服务器,输出就绪日志。 + +#### 3.1.3 核心函数详解 + +接入层包含两类核心函数:**匿名回调函数**(由 SDK 在收到请求时触发)和 **启动函数** `start`。以下分别说明其逻辑、调用关系与数据流。 + +##### 3.1.3.1 Prompts 回调处理流程(以 scqa 为例) + +1. **逻辑流程图** + +```mermaid +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[返回格式化后的响应] +``` + +2. **调用序列图** + +```mermaid +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 规范的响应 +``` + +3. **关系与数据流** + +- 输入:框架名称 `scqa` 和参数对象(如 `{ situation, complication, ... }`)。 +- 输出:包含 `messages` 数组的响应,每个消息对象结构为 `{ role, content: { type, text, annotations, _meta } }`。 +- 依赖:`promptsController.handlePromptsGet` → `promptService.getFramework` → 框架 JSON 文件。 + +##### 3.1.3.2 Tools 回调处理流程(以 save_note 为例) + +1. **逻辑流程图** + +```mermaid +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[返回格式化后的响应] +``` + +2. **调用序列图** + +```mermaid +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 回调处理流程 + +1. **资源列表回调** + +```mermaid +flowchart TD + A[收到 resources/list 请求] --> B[触发 resourceTemplate.list] + B --> C[调用 listObsidianNotes] + C --> D[获取 .md 文件列表] + D --> E[组装 resources 数组(name, uri, mimeType)] + E --> F[返回资源列表] +``` + +2. **资源读取回调** + +```mermaid +flowchart TD + A[收到 resources/read 请求] --> B[触发 resource 读取回调] + B --> C[从 URI 提取 filename 参数] + C --> D[解码 filename] + D --> E[调用 readObsidianNote] + E --> F[读取文件内容] + F --> G[返回 contents 数组] +``` + +3. **调用序列图** + +```mermaid +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` + +1. **逻辑流程图** + +```mermaid +flowchart TD + A["调用 start()"] --> B[创建 StdioServerTransport 实例] + B --> C["server.connect(transport)"] + C --> D[输出就绪日志] + D --> E[等待客户端连接] +``` + +**调用关系** + +- `start` 被立即执行,捕获错误并退出进程。 + +#### 3.1.4 未来优化 + +**潜在优化点**: + +1. **自动注册**:当前框架和工具需要手动编写注册代码。未来可通过读取目录下的 JSON 文件,动态生成注册逻辑,实现零代码扩展。 +2. **消息清洗复用**:多个 prompt 回调中存在重复的清洗代码,可提取为独立工具函数(如 `sanitizeMessages`)。 +3. **错误处理增强**:资源回调中直接抛出错误,MCP 客户端可能无法友好显示。可返回标准错误响应格式。 +4. **性能监控**:在回调中加入耗时日志,便于定位性能瓶颈。 +5. **配置化**:服务器名称、版本等信息可从环境变量或配置文件读取,提高部署灵活性。 + +### 3.2 控制层 + +### 3.2.1 模块职责与设计目标 + +**职责** + +控制层位于接入层与服务层之间,负责: + +- **接收并路由请求**:根据原语类型(prompts/tools)和名称,将请求分发给对应的业务处理函数。 +- **参数校验与转换**:对客户端传入的参数进行格式校验(使用 Zod schema),确保数据合法性。 +- **调用服务层**:将校验后的参数传递给服务层函数,执行核心业务逻辑。 +- **结果格式化**:将服务层返回的数据组装成 MCP 协议要求的响应格式,统一添加必要字段(如 `type: "text"`)。 +- **错误处理**:捕获服务层抛出的异常,转换为标准错误响应返回给客户端。 + + +**设计目标** + +1. **职责单一**:每个控制器函数只处理一类请求,逻辑清晰。 +2. **统一入口**:`handleToolCall` 作为所有工具调用的统一入口,减少接入层重复代码。 +3. **防御性编程**:通过 Zod 校验确保输入合法性,避免脏数据流入服务层。 +4. **错误友好**:所有异常均被捕获并格式化为包含错误信息的 MCP 响应,客户端可正常显示。 +5. **易于扩展**:新增工具只需在 `handleToolCall` 中添加 case 分支,并实现对应的处理函数。 + +### 3.2.2 代码实现与解析 + +1. **`src/controllers/promptsController.ts`** + +```typescript +/** + * Project Caffeine v0.1.1 + * Copyright (c) 2025-2026 Gitconomy Research + * + * SPDX-License-Identifier: MIT + * + * Contributors: + * - 郭晧 (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} 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) { + try { + const result = await getFramework(name, args || {}); + return { + description: `框架: ${name}`, + messages: result.messages + }; + } catch (error: any) { + throw new Error(`获取框架失败: ${error.message}`); + } +} +``` + +**关键解析**: + +- **`handlePromptsList`**:调用服务层 `listFrameworks` 获取所有框架的元信息,然后将其转换为 MCP `prompts/list` 响应格式。每个框架包含 `name`、`description` 和 `arguments` 数组(参数名称、描述、是否必需)。 + +- **`handlePromptsGet`**:接收框架名称和参数对象,调用 `getFramework` 获取完整消息序列。若成功,返回包含 `description` 和 `messages` 的对象;若失败,抛出新错误(错误信息将被接入层捕获并返回给客户端)。 + +- **错误处理**:这里直接抛出异常,由上层(接入层回调)捕获。也可以选择返回标准错误响应,但当前设计倾向于抛出,以便接入层统一处理。 + +2. **`src/controllers/toolsController.ts`** + +```typescript +/** + * Project Caffeine v0.1.1 + * Copyright (c) 2025-2026 Gitconomy Research + * + * SPDX-License-Identifier: MIT + * + * Contributors: + * - 郭晧 (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` + +1. **逻辑流程图** + +```mermaid +flowchart TD + A[收到 prompts/list 请求] --> B[调用 listFrameworks] + B --> C[获取框架元信息列表] + C --> D[遍历框架,转换为 prompts 格式] + D --> E["返回 { prompts: [...] }"] +``` + +2. **调用序列图** + +```mermaid +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 响应 +``` + +3. **关系与数据流** + +- **输入**:无(`prompts/list` 请求不带参数)。 +- **输出**:符合 MCP 规范的 `prompts` 列表,每个 prompt 包含名称、描述、参数定义。 +- **依赖**:`promptService.listFrameworks` → 框架 JSON 文件。 + +#### 3.2.3.2 `handlePromptsGet` + +1. **逻辑流程图** + +```mermaid +flowchart TD + A[收到 prompts/get 请求] --> B[接收 name 和 args] + B --> C["调用 getFramework(name, args)"] + C --> D{是否成功?} + D -- 是 --> E["组装响应 { description, messages }"] + D -- 否 --> F[抛出错误] + E --> G[返回响应] +``` + + +2. **调用序列图** + +```mermaid +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 响应 +``` + + +3. **关系与数据流** + +- **输入**:框架名称 `name`(如 `"scqa"`)和参数字典 `args`。 +- **输出**:包含 `description` 和 `messages` 的对象。 +- **依赖**:`promptService.getFramework` → 框架 JSON + 角色 JSON。 + +#### 3.2.3.3 `handleToolCall` 与工具处理函数 + +以 `save_note` 工具为例,展示统一入口和具体处理函数的协作。 + +1. **逻辑流程图(`handleToolCall` + `handleSaveNote`)** + +```mermaid +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 +``` + +2. **调用序列图(save_note 工具)** + +```mermaid +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 工具响应 +``` + +3. **关系与数据流** + +- **输入**:工具名称和参数对象(如 `{ filename: "note.md", content: "# Title" }`)。 +- **输出**:MCP 工具响应,包含 `content` 数组,可能包含 `isError: true` 表示错误。 +- **依赖**:`resourceService.saveNote` → 本地文件系统;Zod schema 用于校验。 + + +其他工具处理函数(如 `handleGenerateSearchQueries`)具有相似的结构,仅调用的服务和校验规则不同。 +### 3.2.4 未来优化 + +**潜在优化点**: + +1. **校验规则集中管理**:`read_local_note` 的校验 schema 目前定义在函数内部,可移至 `schemas.ts` 中统一管理,便于复用和维护。 +2. **错误处理统一化**:各工具处理函数中的错误响应格式一致,但存在少量重复代码(如 `content: [{ type: 'text', text: ... }]`)。可提取一个工具函数 `createErrorResponse(message: string)`。 +3. **日志增强**:在 `handleToolCall` 入口和每个处理函数中添加结构化日志(如工具名称、参数、执行耗时),便于监控和调试。 +4. **参数校验增强**:当前仅做基本类型和格式校验,未来可增加业务规则校验(如文件名不得包含特殊字符、内容大小限制)。 +5. **支持批量操作**:某些工具(如 `list_local_notes`)已支持返回列表,未来可考虑分页或过滤功能,避免返回过多数据。 +6. **自动化测试**:为每个控制函数编写单元测试,模拟服务层行为,确保错误处理和格式转换正确。 + +### 3.3 服务层 + +#### 3.3.1 模块职责与设计目标 + +**职责** + +服务层位于控制层之下,封装核心业务逻辑,独立于 MCP 协议细节。主要包括三大服务模块: + +1. **`promptService.ts`** + - 加载并缓存所有思维框架的 JSON 定义。 + - 根据框架名称和用户参数,组装完整的提示词消息序列(包括系统提示、Few-Shot 示例、当前用户请求)。 + - 支持通过角色 ID 引用 `personas.json` 中的系统提示,实现角色与框架的解耦。 + +2. **`intentService.ts`** + + - 实现意图拆解功能,将用户的自然语言查询转换为专业检索词列表。 + - 核心算法:清洗文本、分词、去重、补全、截取,生成 3~5 个检索词。 + +3. **`resourceService.ts`** + + - 封装对本地知识库(Markdown 文件夹)的文件操作。 + - 提供笔记列表、内容读取、笔记保存功能。 + - 实现严格的安全校验,防止路径遍历攻击。 + +**设计目标** + +1. **高内聚低耦合**:每个服务聚焦单一职责,对外提供清晰接口,不依赖上层(控制器)的调用方式。 +2. **缓存优化**:`promptService` 缓存框架定义,避免重复读盘。 +3. **安全优先**:`resourceService` 在文件操作前进行路径校验,确保操作仅限于指定目录。 +4. **易于测试**:服务层函数不涉及 MCP 响应格式,纯业务逻辑,便于单元测试。 +5. **可扩展**:新增框架只需在 `models/frameworks/` 下添加 JSON 文件,服务层自动加载;新增文件操作只需在 `resourceService` 中扩展。 + +#### 3.3.2 代码实现与解析 + +1. **`src/services/promptsService.ts`** + +```typescript +/** + * Project Caffeine v0.1.1 + * Copyright (c) 2025-2026 Gitconomy Research + * + * SPDX-License-Identifier: MIT + * + * Contributors: + * - 郭晧 (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; // 示例输入参数 + 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} 框架对象数组,若加载失败则返回空数组 + */ + +async function loadFrameworks(): Promise { + 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>>} + * 框架元信息列表,每个框架包含名称、描述和参数列表 + */ + +export async function listFrameworks(): Promise>> { + const frameworks = await loadFrameworks(); + return frameworks.map(({ name, description, parameters }) => ({ + name, + description, + parameters + })); +} + +/** + * 获取指定框架的完整提示词消息序列。 + * + * 该函数根据框架名称查找对应的框架定义,结合用户传入的参数, + * 构建包含系统提示、Few-Shot 示例和当前用户请求的消息数组。 + * 系统提示优先使用框架关联的角色(persona),若未定义则使用框架自带的 systemPrompt。 + * + * @param {string} name - 框架名称,需与框架 JSON 文件中的 name 字段一致 + * @param {Record} args - 用户传入的参数键值对,用于填充模板中的 {{param}} 占位符 + * @returns {Promise} 符合 MCP 规范的消息序列对象 + * @throws {Error} 当指定名称的框架不存在时抛出错误 + */ + +export async function getFramework(name: string, args: Record): Promise { + 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` 格式的对象。 + +2. **`src/services/intentServices/ts`** + +```typescript +/** + * Project Caffeine v0.1.1 + * Copyright (c) 2025-2026 Gitconomy Research + * + * SPDX-License-Identifier: MIT + * + * Contributors: + * - 郭晧 (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 个返回,避免过多检索词导致下游超时或噪音。 + +3. **`src/services/resourcesServices.ts`** + +```typescript +/** + * Project Caffeine v0.1.1 + * Copyright (c) 2025-2026 Gitconomy Research + * + * SPDX-License-Identifier: MIT + * + * Contributors: + * - 郭晧 (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} 包含所有笔记文件名的数组,若失败则返回空数组。 + */ + +export async function listObsidianNotes(): Promise { + 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} 笔记文件的文本内容 + * @throws {Error} 当文件名导致路径越界时抛出安全警告 + * @throws {Error} 当文件不存在或无权限读取时抛出错误 + */ + +export async function readObsidianNote(filename: string): Promise { + 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} 保存成功的提示信息,包含文件绝对路径 + * @throws {Error} 当文件名不以 .md 结尾时抛出错误 + * @throws {Error} 当文件名导致路径越界时抛出错误 + * @throws {Error} 当目录创建失败或文件写入失败时抛出错误 + */ + +export async function saveNote(filename: string, content: string): Promise { + // 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` + +1. **逻辑流程图** + +```mermaid +flowchart TD + A[调用 loadFrameworks] --> B{缓存存在?} + B -- 是 --> C[返回缓存] + B -- 否 --> D[读取 FRAMEWORKS_DIR] + D --> E[过滤出 .json 文件] + E --> F[并发读取所有 JSON 文件] + F --> G[解析为 Framework 对象] + G --> H[存入缓存] + H --> I[返回框架数组] +``` + + +2. **调用序列图** + +```mermaid +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[] +``` + + +3. **关系与数据流** + +- **输入**:无。 +- **输出**:`Framework[]` 数组。 +- **依赖**:文件系统读取操作。 +- **缓存策略**:使用模块级变量 `frameworksCache`,在服务生命周期内有效,避免重复读盘。 + +###### 3.3.3.2 `promptService.getFramework` + +1. **逻辑流程图** + +```mermaid +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 }"] +``` + + +2. **调用序列图** + +```mermaid +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 +``` + +3. **关系与数据流** + +- **输入**:框架名称 `name`,参数字典 `args`。 +- **输出**:`PromptResult` 对象(包含 `messages` 数组)。 +- **依赖**:`loadFrameworks` 返回的框架定义,`personas.json` 角色配置。 +- **核心逻辑**:模板填充(正则替换 `{{key}}`),Few-Shot 示例插入,系统提示选择。 + +##### 3.3.3.3 `intentService.generateSearchQueries` + +1. **逻辑流程图** + +```mermaid +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[返回检索词数组] +``` + +3. **调用序列图** + +```mermaid +sequenceDiagram + participant Ctrl as toolsController + participant Svc as intentService + Ctrl->>Svc: generateSearchQueries('AI芯片市场趋势2026') + Svc->>Svc: 清洗、分词、去重 + Svc-->>Ctrl: ['AI芯片','市场趋势','2026','AI芯片市场趋势2026 相关研究'] +``` + +3. **关系与数据流** + +- **输入**:用户查询字符串。 +- **输出**:字符串数组(检索词)。 +- **核心算法**:基于规则的简单 NLP,不依赖外部库,轻量高效。 + +##### 3.3.3.4 `resourceService.listObsidianNotes` + +1. **逻辑流程图** + +```mermaid +flowchart TD + A[调用 listObsidianNotes] --> B[读取 OBSIDIAN_VAULT_PATH 目录] + B --> C{读取成功?} + C -- 是 --> D[过滤 .md 文件] + D --> E[返回文件名数组] + C -- 否 --> F[打印错误日志] + F --> G[返回空数组] +``` + +2. **调用序列图** + +```mermaid +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[] +``` + + +3. **关系与数据流** + +- **输入**:无。 +- **输出**:Markdown 文件名列表。 +- **错误处理**:若目录不可读,返回空数组并记录日志,不中断服务。 + +##### 3.3.3.5 `resourceService.readObsidianNote` + +1. **逻辑流程图** + +```mermaid +flowchart TD + A[输入 filename] --> B[拼接目标路径 targetPath] + B --> C[获取安全路径 safeVaultPath] + C --> D{targetPath 以 safeVaultPath 开头?} + D -- 否 --> E[抛出安全异常] + D -- 是 --> F[读取文件] + F --> G{读取成功?} + G -- 是 --> H[返回文件内容] + G -- 否 --> I[抛出读取异常] +``` + +2. **调用序列图** + +```mermaid +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 +``` + +3. **关系与数据流** + +- **输入**:文件名(如 `"example.md"`)。 +- **输出**:文件内容字符串。 +- **安全机制**:必须确保文件在知识库目录内,防止路径遍历攻击。 +- **错误处理**:校验失败或读取失败均抛出具体错误,由调用方处理。 + +##### 3.3.3.6 `resourceService.saveNote` + +1. **逻辑流程图** + +```mermaid +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[返回成功信息] +``` + +2. **调用序列图** + +```mermaid +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.3.4 未来迭代优化 + +1. **`promptService` 迭代优化**: + - **支持动态角色**:目前角色配置在 `personas.json` 中静态定义,未来可支持从数据库或 API 动态获取,实现多租户定制。 + - **模板引擎升级**:当前使用简单的字符串替换,若模板复杂可考虑引入轻量模板引擎(如 Handlebars),支持条件判断、循环等。 + - **示例缓存**:若示例数量巨大,可将示例内容独立存储,按需加载,减少内存占用。 + +2. **`intentService` 迭代优化**: + - **引入 NLP 模型**:当前规则简单,可集成词向量或 LLM 生成更精准的检索词,但需权衡性能。 + - **同义词扩展**:对核心词汇添加同义词,提高检索召回率。 + - **配置化停用词**:将停用词表提取为配置文件,便于根据领域调整。 + +3. **`resourceService` 迭代优化** + - **支持子目录**:当前仅支持平铺的笔记目录,可扩展为递归遍历子文件夹。 + - **文件变更监听**:通过 `fs.watch` 实现实时更新资源列表,避免每次 list 都读盘。 + - **大文件流式处理**:若笔记文件极大,读取时可能占用大量内存,可考虑流式读取或分块返回。 + - **文件类型扩展**:除 `.md` 外,支持更多文档格式(如 `.txt`、`.pdf`),需增加文件解析逻辑。 + +4. **通用优化**: + - **错误码标准化**:服务层抛出的错误可携带错误码(如 `ERR_FILE_NOT_FOUND`),便于控制器统一映射。 + - **性能监控**:在关键服务函数中添加耗时日志,帮助定位性能瓶颈。 + - **单元测试**:为每个服务编写测试用例,模拟文件系统、缓存等,确保逻辑正确性。 + +--- + +## 许可声明 + +本文档采用 **知识共享署名-相同方式共享 4.0 国际许可协议 (CC BY-SA 4.0)** 进行许可,© 2025-2026 Gitconomy Research.