<!--
---
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<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 原语注册到具体功能实现的全链路函数调用关系，涵盖接入层、控制器、服务层及数据存储之间的交互：

```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:
 * - 郭晧 <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);
});
```

**关键解析：**

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
graph TD
    A[收到 prompts/get 请求] --> B[调用 scqa 回调]
    B --> C[提取 args 参数]
    C --> D[调用 handlePromptsGet]
    D --> E[从 promptService 获取消息序列]
    E --> F[过滤 messages]
    F --> G[补全 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
graph TD
    A[收到 prompts/get 请求] --> B[调用 scqa 回调]
    B --> C[提取 args 参数]
    C --> D[调用 handlePromptsGet]
    D --> E[从 promptService 获取消息序列]
    E --> F[过滤 messages 保留 user assistant]
    F --> G[补全 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
graph 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:
 * - 郭晧 <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` 获取所有框架的元信息，然后将其转换为 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:
 * - 郭晧 <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`

1. **逻辑流程图**

```mermaid
graph 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
graph 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
graph 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:
 * - 郭晧 <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` 格式的对象。

2. **`src/services/intentServices/ts`**

```typescript
/**
 * 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 个返回，避免过多检索词导致下游超时或噪音。

3. **`src/services/resourcesServices.ts`**

```typescript
/**
 * 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`

1. **逻辑流程图**

```mermaid
graph 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
graph 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
graph 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
graph 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
graph 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
graph 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.
