Files
Project-Caffeine/projects/arabica/docs/deisgn/arabica-srpint2-development-specification.md
2026-03-06 14:29:25 +08:00

65 KiB
Raw Blame History

title, description, type, version, file, author, date, tags, license, status
title description type version file author date tags license status
Arabica Srpint2 开发指南:多维思维框架引擎与意图拆解 基于 Sprint 2 架构设计,指导开发者搭建支持 MCP Prompts 原语的提示词策略 Server实现多维思维框架库、意图拆解工具及结构化输出约束完成从单一工具到多框架引擎的升级。 Development Guide v1.0.0 (Arabica) - Sprint 2 arabica-sprint2-development-specification.md Gitconomy Research-郭晧 2026-03-06
Project Caffeine
MCP Server
Sprint 2
Prompt Strategy
Prompts
思维框架
意图拆解
Node.js
CC BY-SA 4.0 Active

Arabica Sprint 2 开发指南:多维思维框架引擎与意图拆解

1. 模块概览与架构设计

Sprint 2 的核心是构建一个支持 MCP Prompts 原语的多维思维框架引擎,同时保留 Sprint 1 的工具与资源能力。整体采用经典的三层架构:

  • 接入层app.ts 负责初始化 MCP 服务器并注册所有原语。
  • 控制层controllers/ 接收请求,校验参数,调用服务层。
  • 服务层services/ 实现业务逻辑(框架加载、意图拆解、文件操作)。
  • 模型层models/ 存放框架定义 JSON、角色配置、Zod 校验模式。

下图展示了模块间的静态关系:

graph TD
    A[app.ts] --> B[promptsController]
    A --> C[toolsController]
    A --> D[resource 注册]

    B --> E[promptService]
    C --> F[intentService]
    C --> G[resourceService]
    C --> H[schema 校验]

    E --> I[框架 JSON 文件]
    E --> J[personas.json]
    G --> K[本地 Markdown 文件]
    F --> L[生成检索词]

2. 函数列表和调用逻辑关系

2.1 完成函数列表

以下是项目中所有显式定义的函数,按文件分组,并附简要说明。

1. app.ts(入口文件)

函数名 参数 描述 异步
start 初始化 MCP 服务器,连接 STDIO 传输层

2. resourceService.ts(资源服务)

函数名 参数 描述 异步
listObsidianNotes 列出知识库目录下所有 .md 文件
readObsidianNote filename: string 读取指定笔记文件内容,含路径安全校验
saveNote filename: string, content: string 将笔记内容保存到知识库目录

3. promptService.ts(提示词服务)

函数名 参数 描述 异步
loadFrameworks 从文件系统加载所有框架 JSON 并缓存
listFrameworks 返回所有框架的元信息(不含模板)
getFramework name: string, args: Record<string, string> 根据框架名称和参数组装完整消息序列

4. intentService.ts(意图拆解服务)

函数名 参数 描述 异步
generateSearchQueries query: string 将用户查询拆解为 3~5 个检索词

5. toolsController.ts(工具控制器)

函数名 参数 描述 异步
handleToolCall toolName: string, params: any 统一入口,根据工具名分发到具体处理函数
handleGenerateSearchQueries params: any 处理 generate_search_queries 工具调用
handleListLocalNotes 处理 list_local_notes 工具调用
handleReadLocalNote params: any 处理 read_local_note 工具调用
handleSaveNote params: any 处理 save_note 工具调用

6. promptsController.ts(提示词控制器)

函数名 参数 描述 异步
handlePromptsList 处理 prompts/list 请求,返回可用框架列表
handlePromptsGet name: string, args: Record<string, string> 处理 prompts/get 请求,返回具体框架消息

补充说明

  • 上述列表仅包含项目代码中显式定义的命名函数。
  • app.ts 中通过 server.prompt / server.tool / server.resource 注册的匿名回调函数未列入,因为它们仅作为 MCP 原语的处理器,内部直接调用对应的控制器函数(如 handlePromptsGethandleToolCall 等),本身不包含独立业务逻辑。
  • 所有 Zod 校验模式定义在 schemas.ts 中,为对象而非函数,故未列出。
  • JSON 配置文件(如 personas.json、框架定义文件)不包含函数,故未列出。

2.2 整体函数调用关系图

下图展示了从 MCP 原语注册到具体功能实现的全链路函数调用关系,涵盖接入层、控制器、服务层及数据存储之间的交互:

graph TD
    subgraph "接入层 app.ts"
        A1["server.prompt (scqa/5whys/...)"] -->|调用| B1[handlePromptsGet]
        A2["server.tool (generate_search_queries/...)"] -->|调用| B2[handleToolCall]
        A3["server.resource (local-notes)"] -->|list 回调| C1[listObsidianNotes]
        A3 -->|read 回调| C2[readObsidianNote]
    end

    subgraph "控制器"
        B1[handlePromptsGet] -->|调用| D1[getFramework]
        B2[handleToolCall] -->|根据名称分发| E1[handleGenerateSearchQueries]
        B2 --> E2[handleListLocalNotes]
        B2 --> E3[handleReadLocalNote]
        B2 --> E4[handleSaveNote]
    end

    subgraph "服务层"
        D1[getFramework] -->|读取框架定义| F1[loadFrameworks]
        F1 -->|读取| G1[(frameworks/*.json)]
        D1 -->|获取角色提示| G2[(personas.json)]

        E1[handleGenerateSearchQueries] -->|调用| H1[generateSearchQueries]
        E2[handleListLocalNotes] -->|调用| H2[listObsidianNotes]
        E3[handleReadLocalNote] -->|调用| H3[readObsidianNote]
        E4[handleSaveNote] -->|调用| H4[saveNote]

        H2 -->|读取目录| I1[(本地知识库)]
        H3 -->|读取文件| I1
        H4 -->|写入文件| I1
    end

    subgraph "工具函数"
        H1[generateSearchQueries] -->|返回| J1[检索词数组]
    end

    C1[listObsidianNotes] --> I1
    C2[readObsidianNote] --> I1

3. 核心业务代码实现

3.1 接入层

3.1.1 模块职责与设计目标

职责

接入层(app.ts)是整个 MCP 服务器的入口,负责:

  • 初始化 McpServer 实例,配置服务器元信息。
  • 注册所有 MCP 原语Prompts、Tools、Resources将外部请求映射到内部控制器。
  • 启动 STDIO 传输层,等待客户端(如 Cherry Studio连接。

设计目标

  1. 声明式注册:通过 SDK 提供的 server.promptserver.toolserver.resource 方法,清晰定义每个原语的名称、参数模式和处理函数。
  2. 解耦与控制:处理函数仅做基础参数转换和结果格式化,业务逻辑委托给控制器,保持接入层轻量。
  3. 健壮性:对返回的消息进行清洗(如过滤非法 role、补全缺失字段确保符合 MCP 协议规范。
  4. 可扩展性:新增框架或工具只需添加对应注册代码,无需修改现有结构。

3.1.2 代码实现与解析

src/app.ts

/**
 * Project Caffeine v0.1.1
 * Copyright (c) 2025-2026 Gitconomy Research
 *
 * SPDX-License-Identifier: MIT
 *
 * Contributors:
 * - 郭晧 <guohao@gitconomy.org> (Initial Author)
 */
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { handlePromptsGet } from './controllers/promptsController';
import { handleToolCall } from './controllers/toolsController';
import { listObsidianNotes, readObsidianNote } from './services/resourceService';

// ==========================================
// 初始化 MCP Server
// ==========================================
const server = new McpServer({
  name: 'Project-Caffeine-S2-Prompt-Strategy',
  version: '2.0.0'
});

// ==========================================
// 注册 Prompts 原语(多维思维框架模板)
// ==========================================

/**
 * 处理 SCQA 框架的 prompts/get 请求。
 * 调用 handlePromptsGet 获取消息序列,并进行清洗(过滤非法 role、补全 annotations/_meta * @param args - 包含 situation, complication, question, answer 的对象
 * @param extra - MCP 额外上下文(未使用)
 * @returns 符合 MCP 规范的 prompt 响应对象
 */

server.prompt(
  'scqa',  // 示例SCQA 框架
  {
    situation: z.string().describe('情境'),
    complication: z.string().describe('复杂性'),
    question: z.string().describe('问题'),
    answer: z.string().describe('答案')
  },
  async (args, extra) => {
    const result = await handlePromptsGet('scqa', args as Record<string, string>);
    return {
      ...result,
      messages: Array.isArray(result.messages)
        ? result.messages
            .filter((msg: any) => msg.role === "user" || msg.role === "assistant")
            .map((msg: any) => ({
              ...msg,
              // Optionally ensure content has required structure
              content: {
                ...msg.content,
                // Add default annotations/_meta if missing
                annotations: msg.content.annotations ?? undefined,
                _meta: msg.content._meta ?? undefined
              }
            }))
        : []
    };
  }
);

/**
 * 处理 5 Whys 框架的 prompts/get 请求。
 * 调用 handlePromptsGet 获取消息序列,并进行清洗。
 * @param args - 包含 problem 的对象
 * @returns 符合 MCP 规范的 prompt 响应对象
 */

server.prompt(
  '5whys', // 5 Whys 框架
  { problem: z.string().describe('需要分析的问题或现象') },
  async (args) => {
    const result = await handlePromptsGet('5whys', args);
    return {
      ...result,
      messages: Array.isArray(result.messages)
        ? result.messages
            .filter((msg: any) => msg.role === "user" || msg.role === "assistant")
            .map((msg: any) => ({
              ...msg,
              content: {
                ...msg.content,
                annotations: msg.content.annotations ?? undefined,
                _meta: msg.content._meta ?? undefined
              }
            }))
        : []
    };
  }
);

/**
 * 处理 5W3H 框架的 prompts/get 请求。
 * 调用 handlePromptsGet 获取消息序列,并进行清洗。
 * @param args - 包含 topic 的对象
 * @returns 符合 MCP 规范的 prompt 响应对象
 */

server.prompt(
  '5w3h', // 5W3H 框架
  { topic: z.string().describe('需要分析的主题') },
  async (args) => {
    const result = await handlePromptsGet('5w3h', args);
    return {
      ...result,
      messages: Array.isArray(result.messages)
        ? result.messages
            .filter((msg: any) => msg.role === "user" || msg.role === "assistant")
            .map((msg: any) => ({
              ...msg,
              content: {
                ...msg.content,
                annotations: msg.content.annotations ?? undefined,
                _meta: msg.content._meta ?? undefined
              }
            }))
        : []
    };
  }
);

/**
 * 处理 SWOT 框架的 prompts/get 请求。
 * 调用 handlePromptsGet 获取消息序列,并进行清洗。
 * @param args - 包含 entity 的对象
 * @returns 符合 MCP 规范的 prompt 响应对象
 */

server.prompt(
  'swot', // SWOT 框架
  { entity: z.string().describe('分析对象(企业、项目等)') },
  async (args) => {
    const result = await handlePromptsGet('swot', args);
    return {
      ...result,
      messages: Array.isArray(result.messages)
        ? result.messages
            .filter((msg: any) => msg.role === "user" || msg.role === "assistant")
            .map((msg: any) => ({
              ...msg,
              content: {
                ...msg.content,
                annotations: msg.content.annotations ?? undefined,
                _meta: msg.content._meta ?? undefined
              }
            }))
        : []
    };
  }
);

/**
 * 处理 PESTLE 框架的 prompts/get 请求。
 * 调用 handlePromptsGet 获取消息序列,并进行清洗。
 * @param args - 包含 domain 的对象
 * @returns 符合 MCP 规范的 prompt 响应对象
 */

server.prompt(
  'pestle', // PESTLE 框架
  { domain: z.string().describe('行业或领域') },
  async (args) => {
    const result = await handlePromptsGet('pestle', args);
    return {
      ...result,
      messages: Array.isArray(result.messages)
        ? result.messages
            .filter((msg: any) => msg.role === "user" || msg.role === "assistant")
            .map((msg: any) => ({
              ...msg,
              content: {
                ...msg.content,
                annotations: msg.content.annotations ?? undefined,
                _meta: msg.content._meta ?? undefined
              }
            }))
        : []
    };
  }
);

// ==========================================
// 注册工具generate_search_queries
// ==========================================

/**
 * 处理 generate_search_queries 工具调用。
 * 调用 handleToolCall 获取结果,并确保返回的每个 content 项具有 type: "text"。
 * @param args - 包含 query 的对象
 * @param extra - MCP 额外上下文(未使用)
 * @returns MCP 工具响应对象
 */

server.tool(
  'generate_search_queries',
  { query: z.string().describe('用户的原始查询语句') },
  async (args, extra) => {
    const result = await handleToolCall('generate_search_queries', args);
    // Ensure each content item has type: "text" (not a generic string)
    return {
      ...result,
      content: result.content.map((item: any) => ({
        ...item,
        type: "text"
      }))
    };
  }
);

// =====================================================
// 注册工具list_local_notes, read_local_note, save_note
// =====================================================

/**
 * 处理 list_local_notes 工具调用。
 * 调用 handleToolCall 获取笔记列表,并标准化 content 格式。
 * @param args - 空对象(无需参数)
 * @returns MCP 工具响应对象,包含笔记列表文本
 */

server.tool(
  'list_local_notes',
  {},
  async (args) => {
    const result = await handleToolCall('list_local_notes', args);
    return {
      ...result,
      content: result.content.map((item: any) => ({
        type: 'text',
        text: item.text
      }))
    };
  }
);


/**
 * 处理 read_local_note 工具调用。
 * 调用 handleToolCall 读取指定笔记内容,并标准化 content 格式。
 * @param args - 包含 filename 的对象
 * @returns MCP 工具响应对象,包含笔记内容文本
 */

server.tool(
  'read_local_note',
  { filename: z.string().describe('需要读取的笔记文件名,必须包含 .md 后缀') },
  async (args) => {
    const result = await handleToolCall('read_local_note', args);
    return {
      ...result,
      content: result.content.map((item: any) => ({
        type: 'text',
        text: item.text
      }))
    };
  }
);

/**
 * 处理 save_note 工具调用。
 * 调用 handleToolCall 保存笔记到本地知识库,并标准化 content 格式。
 * @param args - 包含 filename 和 content 的对象
 * @returns MCP 工具响应对象,包含保存结果信息
 */

server.tool(
  'save_note',
  {
    filename: z.string().describe('笔记文件名,必须以 .md 结尾'),
    content: z.string().describe('笔记内容Markdown 格式)')
  },
  async (args) => {
    const result = await handleToolCall('save_note', args);
    return {
      ...result,
      content: result.content.map((item: any) => ({
        type: 'text',
        text: item.text
      }))
    };
  }
);

// ==========================================
// 注册 Resources 原语(暴露本地笔记供客户端勾选)
// ==========================================

// 使用 ResourceTemplate 注册动态资源
server.resource(
  "local-notes",  // 资源名称
  new ResourceTemplate("note://local/{filename}", {
    // 实现列表功能:返回所有可用的笔记资源
    list: async () => {
      try {
        const notes = await listObsidianNotes();
        return {
          resources: notes.map(filename => ({
            name: filename,
            uri: `note://local/${encodeURIComponent(filename)}`,
            mimeType: "text/markdown",
            description: `本地笔记: ${filename}`
          }))
        };
      } catch (error: any) {
        console.error('[Resources] 列出资源失败:', error);
        return { resources: [] };
      }
    }
  }),

    /**
   * 处理资源读取请求:根据 URI 中的 filename 参数读取对应笔记内容。
   * @param uri - 请求的 URI 对象
   * @param params - 包含从 URI 提取的 filename 参数
   * @returns 符合 MCP 规范的资源内容对象
   * @throws 当读取失败时抛出错误
   */

  async (uri, { filename }) => {
    try {
      // filename 参数由 ResourceTemplate 自动从 URI 中提取
      const filenameStr = Array.isArray(filename) ? filename[0] : filename;
      const decodedFilename = decodeURIComponent(filenameStr);
      const content = await readObsidianNote(decodedFilename);

      return {
        contents: [{
          uri: uri.href,
          mimeType: "text/markdown",
          text: content
        }]
      };
    } catch (error: any) {
      // 错误处理:返回错误信息
      throw new Error(`读取笔记失败: ${error.message}`);
    }
  }
);

// ==========================================
// 启动 STDIO 传输层
// ==========================================


/**
 * 启动 MCP 服务器,连接 STDIO 传输层。
 * 该函数创建 StdioServerTransport 实例并连接到 server。
 * @returns {Promise<void>} 无返回值
 */

async function start() {
  console.error('[S2] 正在启动 MCP Server (Prompts + 检索词工具)...');
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('[S2] MCP Server 已就绪,等待 Cherry Studio 连接');
}

start().catch((err) => {
  console.error('[S2] 服务器启动失败:', err);
  process.exit(1);
});

关键解析:

  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. 逻辑流程图
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[返回格式化后的响应]
  1. 调用序列图
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 规范的响应
  1. 关系与数据流
  • 输入:框架名称 scqa 和参数对象(如 { situation, complication, ... })。
  • 输出:包含 messages 数组的响应,每个消息对象结构为 { role, content: { type, text, annotations, _meta } }
  • 依赖:promptsController.handlePromptsGetpromptService.getFramework → 框架 JSON 文件。
3.1.3.2 Tools 回调处理流程(以 save_note 为例)
  1. 逻辑流程图
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[返回格式化后的响应]
  1. 调用序列图
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. 资源列表回调
flowchart TD
    A[收到 resources/list 请求] --> B[触发 resourceTemplate.list]
    B --> C[调用 listObsidianNotes]
    C --> D[获取 .md 文件列表]
    D --> E[组装 resources 数组name, uri, mimeType]
    E --> F[返回资源列表]
  1. 资源读取回调
flowchart TD
    A[收到 resources/read 请求] --> B[触发 resource 读取回调]
    B --> C[从 URI 提取 filename 参数]
    C --> D[解码 filename]
    D --> E[调用 readObsidianNote]
    E --> F[读取文件内容]
    F --> G[返回 contents 数组]
  1. 调用序列图
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: 资源内容响应

关系与数据流

  • 输入:资源 URInote://local/example.md)。
  • 输出:contents 数组,包含 urimimeTypetext
  • 依赖:resourceService.listObsidianNotes / readObsidianNote → 本地文件系统
3.1.3.4 启动函数 start
  1. 逻辑流程图
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
/**
 * 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 响应格式。每个框架包含 namedescriptionarguments 数组(参数名称、描述、是否必需)。

  • handlePromptsGet:接收框架名称和参数对象,调用 getFramework 获取完整消息序列。若成功,返回包含 descriptionmessages 的对象;若失败,抛出新错误(错误信息将被接入层捕获并返回给客户端)。

  • 错误处理:这里直接抛出异常,由上层(接入层回调)捕获。也可以选择返回标准错误响应,但当前设计倾向于抛出,以便接入层统一处理。

  1. src/controllers/toolsController.ts
/**
 * Project Caffeine v0.1.1
 * Copyright (c) 2025-2026 Gitconomy Research
 *
 * SPDX-License-Identifier: MIT
 *
 * Contributors:
 * - 郭晧 <guohao@gitconomy.org> (Initial Author)
 */
import { generateSearchQueries } from '../services/intentService';
import { listObsidianNotes, readObsidianNote, saveNote } from '../services/resourceService';
import { z } from 'zod';
import { generateSearchQueriesSchema, saveNoteSchema } from '../models/schemas';

/**
 * 统一工具调用处理入口。
 *
 * 根据工具名称分发到对应的具体处理函数,并对未知工具返回错误响应。
 *
 * @param toolName - 工具名称,支持 'generate_search_queries'、'list_local_notes'、'read_local_note'、'save_note'
 * @param params - 工具参数对象,具体结构取决于工具
 * @returns MCP 工具响应格式,包含 content 数组,可能带有 isError 标记
 */

export async function handleToolCall(toolName: string, params: any) {
  switch (toolName) {
    case 'generate_search_queries':
      return handleGenerateSearchQueries(params);
    case 'list_local_notes':
      return handleListLocalNotes();
    case 'read_local_note':
      return handleReadLocalNote(params);
    case 'save_note':
      return handleSaveNote(params);
    default:
      return {
        content: [{ type: 'text', text: `未知工具: ${toolName}` }],
        isError: true
      };
  }
}

/**
 * 处理检索词生成工具 (generate_search_queries)。
 *
 * 该函数接收用户原始查询语句,通过 intentService 生成 3~5 个专业检索词,
 * 并以 JSON 字符串形式返回。
 *
 * @param params - 工具参数对象,应包含 query 字段
 * @param params.query - 用户的原始查询语句
 * @returns MCP 工具响应,成功时 content 包含 JSON 格式的检索词数组;失败时 content 包含错误信息且 isError 为 true
 */

async function handleGenerateSearchQueries(params: any) {
  // 使用集中管理的 schema 进行参数校验
  const parseResult = generateSearchQueriesSchema.safeParse(params);
  if (!parseResult.success) {
    return {
      content: [{ type: 'text', text: `参数错误: ${parseResult.error.message}` }],
      isError: true
    };
  }

  const { query } = parseResult.data;

  try {
    const queries = generateSearchQueries(query);
    return {
      content: [{ type: 'text', text: JSON.stringify(queries, null, 2) }]
    };
  } catch (error: any) {
    console.error('[ToolsController] generate_search_queries 失败:', error);
    return {
      content: [{ type: 'text', text: `执行失败: ${error.message}` }],
      isError: true
    };
  }
}

/**
 * 处理列出本地笔记工具 (list_local_notes)。
 *
 * 调用 resourceService 获取知识库中所有 Markdown 笔记的文件名,
 * 并以文本列表形式返回。
 *
 * @returns MCP 工具响应,成功时 content 包含笔记列表文本;失败时 content 包含错误信息且 isError 为 true
 */

async function handleListLocalNotes() {
  try {
    const notes = await listObsidianNotes();
    return {
      content: [{
        type: 'text',
        text: notes.length > 0 ? `找到了以下笔记:\n${notes.join('\n')}` : '未找到笔记。'
      }]
    };
  } catch (error: any) {
    console.error('[ToolsController] list_local_notes 失败:', error);
    return {
      content: [{ type: 'text', text: `执行失败: ${error.message}` }],
      isError: true
    };
  }
}

/**
 * 处理读取本地笔记工具 (read_local_note)。
 *
 * 接收文件名参数,调用 resourceService 读取对应笔记内容并返回。
 *
 * @param params - 工具参数对象,应包含 filename 字段
 * @param params.filename - 要读取的笔记文件名(必须包含 .md 后缀)
 * @returns MCP 工具响应,成功时 content 包含笔记内容;失败时 content 包含错误信息且 isError 为 true
 */

async function handleReadLocalNote(params: any) {
  const schema = z.object({
    filename: z.string().min(1, '文件名不能为空').includes('.md', { message: '文件名必须包含 .md 后缀' })
  });

  const parseResult = schema.safeParse(params);
  if (!parseResult.success) {
    return {
      content: [{ type: 'text', text: `参数错误: ${parseResult.error.message}` }],
      isError: true
    };
  }

  const { filename } = parseResult.data;

  try {
    const content = await readObsidianNote(filename);
    return {
      content: [{ type: 'text', text: content }]
    };
  } catch (error: any) {
    console.error('[ToolsController] read_local_note 失败:', error);
    return {
      content: [{ type: 'text', text: `读取失败: ${error.message}` }],
      isError: true
    };
  }
}

/**
 * 处理保存笔记工具 (save_note)。
 *
 * 接收文件名和内容参数,调用 resourceService 将笔记保存到本地知识库。
 *
 * @param params - 工具参数对象,应包含 filename 和 content 字段
 * @param params.filename - 笔记文件名(必须以 .md 结尾)
 * @param params.content - 笔记内容Markdown 格式)
 * @returns MCP 工具响应,成功时 content 包含保存成功信息;失败时 content 包含错误信息且 isError 为 true
 */

async function handleSaveNote(params: any) {
  const parseResult = saveNoteSchema.safeParse(params);
  if (!parseResult.success) {
    return {
      content: [{ type: 'text', text: `参数错误: ${parseResult.error.message}` }],
      isError: true
    };
  }

  const { filename, content } = parseResult.data;

  try {
    const message = await saveNote(filename, content);
    return {
      content: [{ type: 'text', text: message }]
    };
  } catch (error: any) {
    console.error('[ToolsController] save_note 失败:', error);
    return {
      content: [{ type: 'text', text: `保存失败: ${error.message}` }],
      isError: true
    };
  }
}

关键解析

  • 统一入口 handleToolCall:根据 toolName 分发到具体处理函数。若工具名不匹配,返回包含错误信息的响应,并标记 isError: true

  • 参数校验

    • generate_search_queriessave_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. 逻辑流程图
flowchart TD
    A[收到 prompts/list 请求] --> B[调用 listFrameworks]
    B --> C[获取框架元信息列表]
    C --> D[遍历框架,转换为 prompts 格式]
    D --> E["返回 { prompts: [...] }"]
  1. 调用序列图
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 响应
  1. 关系与数据流
  • 输入:无(prompts/list 请求不带参数)。
  • 输出:符合 MCP 规范的 prompts 列表,每个 prompt 包含名称、描述、参数定义。
  • 依赖promptService.listFrameworks → 框架 JSON 文件。

3.2.3.2 handlePromptsGet

  1. 逻辑流程图
flowchart TD
    A[收到 prompts/get 请求] --> B[接收 name 和 args]
    B --> C["调用 getFramework(name, args)"]
    C --> D{是否成功?}
    D -- 是 --> E["组装响应 { description, messages }"]
    D -- 否 --> F[抛出错误]
    E --> G[返回响应]
  1. 调用序列图
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 响应
  1. 关系与数据流
  • 输入:框架名称 name(如 "scqa")和参数字典 args
  • 输出:包含 descriptionmessages 的对象。
  • 依赖promptService.getFramework → 框架 JSON + 角色 JSON。

3.2.3.3 handleToolCall 与工具处理函数

save_note 工具为例,展示统一入口和具体处理函数的协作。

  1. 逻辑流程图(handleToolCall + handleSaveNote
flowchart TD
    A[收到 tool/call 请求] --> B["调用 handleToolCall(toolName, params)"]
    B --> C{根据 toolName 分发}
    C -- save_note --> D["调用 handleSaveNote(params)"]
    C -- 其他工具 --> E[其他处理函数]
    D --> F[使用 saveNoteSchema 校验参数]
    F --> G{校验通过?}
    G -- 否 --> H[返回参数错误响应]
    G -- 是 --> I[调用 saveNote 服务]
    I --> J{服务执行是否成功?}
    J -- 是 --> K[返回成功响应]
    J -- 否 --> L[返回错误响应]
    H --> M[返回响应给接入层]
    K --> M
    L --> M
  1. 调用序列图save_note 工具)
sequenceDiagram
    participant App as app.ts (save_note 回调)
    participant Ctrl as toolsController
    participant Svc as resourceService
    App->>Ctrl: handleToolCall('save_note', params)
    Ctrl->>Ctrl: 分发到 handleSaveNote
    Ctrl->>Ctrl: 参数校验 (saveNoteSchema)
    Ctrl->>Svc: saveNote(filename, content)
    Svc-->>Ctrl: 保存结果消息
    Ctrl-->>App: content 数组
    App-->>Client: MCP 工具响应
  1. 关系与数据流
  • 输入:工具名称和参数对象(如 { 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
/**
 * 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 查找框架,若不存在则抛出错误。
    • 确定系统提示词:优先使用框架关联的 personapersonas.json 中获取,若没有则回退到框架自带的 systemPrompt
    • 构建消息序列:
      • 首先插入系统消息(如果有)。
      • 然后遍历 examples,将示例的 input 填充到模板生成用户消息,并将示例的 output 作为助手消息插入(实现 Few-Shot 学习)。
      • 最后将当前用户参数填充到模板,插入用户消息。
    • 返回符合 PromptResult 格式的对象。
  1. src/services/intentServices/ts
/**
 * Project Caffeine v0.1.1
 * Copyright (c) 2025-2026 Gitconomy Research
 *
 * SPDX-License-Identifier: MIT
 *
 * Contributors:
 * - 郭晧 <guohao@gitconomy.org> (Initial Author)
 */

/**
 * 将用户的自然语言查询拆解为专业检索词列表
 * @param query 用户原始查询字符串
 * @returns 去重后的检索词数组3~5 个)
 */

export function generateSearchQueries(query: string): string[] {
  if (!query || query.trim().length === 0) {
    return ['通用研究主题'];
  }

  // 1. 去除常见标点符号,替换为空格
  const cleaned = query.replace(/[,。??、;;]/g, ' ');

  // 2. 按空白字符分割,过滤掉长度小于 2 的词(避免单字噪音)
  const words = cleaned.split(/\s+/).filter(word => word.length >= 2);

  // 3. 去重
  const uniqueWords = [...new Set(words)];

  // 4. 若不足 3 个,补充基于原查询的扩展词
  while (uniqueWords.length < 3) {
    uniqueWords.push(`${query} 相关研究`);
  }

  // 5. 截取前 5 个返回
  return uniqueWords.slice(0, 5);
}

关键解析

  • 输入为空:返回一个默认检索词 ['通用研究主题'],保证下游工具(如搜索引擎)能够运行。
  • 文本清洗:使用正则替换常见中文/英文标点为空格,避免标点干扰分词。
  • 分词与过滤:按空白分割,过滤掉长度小于 2 的词(如“的”、“是”等停用词可被自然过滤)。
  • 去重:使用 Set 去除重复词,减少冗余。
  • 补全机制:若有效词少于 3 个,循环追加 "${query} 相关研究",确保至少返回 3 个检索词。
  • 数量限制:截取前 5 个返回,避免过多检索词导致下游超时或噪音。
  1. src/services/resourcesServices.ts
/**
 * Project Caffeine v0.1.1
 * Copyright (c) 2025-2026 Gitconomy Research
 *
 * SPDX-License-Identifier: MIT
 *
 * Contributors:
 * - 郭晧 <guohao@gitconomy.org> (Initial Author)
 */

import fs from 'fs/promises';
import path from 'path';

/**
 * 本地知识库的根目录路径。
 *
 * 该目录存放所有 Markdown 笔记文件,所有文件操作均限定在此目录内,
 * 以防止路径遍历攻击。
 *
 * @constant {string}
 */


const OBSIDIAN_VAULT_PATH = '/home/wguo/Downloads/MyVault'; // 【⚠️ 重要配置】请修改为你电脑上真实的 Markdown 笔记文件夹绝对路径!

/**
 * 列出知识库中所有 Markdown 笔记的文件名。
 *
 * 该函数读取 OBSIDIAN_VAULT_PATH 目录下的所有文件,过滤出以 .md 结尾
 * (不区分大小写)的文件,并返回文件名列表。若目录不存在或无权限访问,
 * 则返回空数组并打印错误日志。
 *
 * @returns {Promise<string[]>} 包含所有笔记文件名的数组,若失败则返回空数组。
 */

export async function listObsidianNotes(): Promise<string[]> {
    try {
        const files = await fs.readdir(OBSIDIAN_VAULT_PATH);
        return files.filter(file => file.toLowerCase().endsWith('.md'));
    } catch (error: any) {
        console.error(`[Project Caffeine] 无法读取知识库目录: ${error.message}`);
        return [];
    }
}

/**
 * 读取指定笔记文件的完整内容。
 *
 * 该函数首先对文件名进行安全校验,确保文件位于知识库目录内,
 * 防止路径遍历攻击。校验通过后,读取文件内容并返回。
 *
 * @param {string} filename - 要读取的笔记文件名(必须包含 .md 后缀)
 * @returns {Promise<string>} 笔记文件的文本内容
 * @throws {Error} 当文件名导致路径越界时抛出安全警告
 * @throws {Error} 当文件不存在或无权限读取时抛出错误
 */

export async function readObsidianNote(filename: string): Promise<string> {
    const targetPath = path.resolve(OBSIDIAN_VAULT_PATH, filename);
    const safeVaultPath = path.resolve(OBSIDIAN_VAULT_PATH);

    // 核心防御:防止大模型通过传入 "../../" 读取系统敏感文件
    if (!targetPath.startsWith(safeVaultPath)) {
        throw new Error(`安全警告:越权访问拦截!禁止读取目录外的文件: ${filename}`);
    }

    try {
        const content = await fs.readFile(targetPath, 'utf-8');
        return content;
    } catch (error: any) {
        throw new Error(`无法读取笔记 [${filename}]: 文件可能不存在或无权限。`);
    }
}

/**
 * 保存笔记到本地知识库。
 *
 * 该函数将内容写入指定文件,执行以下校验和操作:
 * 1. 验证文件名是否以 .md 结尾。
 * 2. 验证文件路径是否在知识库目录内,防止路径遍历攻击。
 * 3. 确保知识库目录存在(若不存在则自动创建)。
 * 4. 将内容写入文件。
 *
 * @param {string} filename - 笔记文件名(必须以 .md 结尾)
 * @param {string} content - 笔记内容Markdown 格式)
 * @returns {Promise<string>} 保存成功的提示信息,包含文件绝对路径
 * @throws {Error} 当文件名不以 .md 结尾时抛出错误
 * @throws {Error} 当文件名导致路径越界时抛出错误
 * @throws {Error} 当目录创建失败或文件写入失败时抛出错误
 */

export async function saveNote(filename: string, content: string): Promise<string> {
  // 1. 验证文件名是否以 .md 结尾
  if (!filename.endsWith('.md')) {
    throw new Error('文件名必须以 .md 结尾');
  }

  // 2. 防止路径遍历攻击:解析绝对路径,并检查是否在 NOTES_DIR 下
  const fullPath = path.resolve(OBSIDIAN_VAULT_PATH, filename);
  const relative = path.relative(OBSIDIAN_VAULT_PATH, fullPath);
  if (relative.startsWith('..') || path.isAbsolute(relative)) {
    throw new Error('无效的文件名,不允许访问上层目录');
  }

  // 3. 确保目标目录存在(可选,如果 NOTES_DIR 必须存在则可跳过)
  await fs.mkdir(OBSIDIAN_VAULT_PATH, { recursive: true });

  // 4. 写入文件
  await fs.writeFile(fullPath, content, 'utf-8');
  return `笔记已保存至: ${fullPath}`;
}

关键解析

  • 常量配置OBSIDIAN_VAULT_PATH 为本地知识库的绝对路径,需用户手动修改。

  • listObsidianNotes:读取目录,过滤出 .md 文件(不区分大小写)。若目录不存在或无权限,返回空数组并打印错误日志,避免服务崩溃。

  • readObsidianNote

    • 先使用 path.resolve 获取目标文件的绝对路径。
    • 安全校验:检查目标路径是否以知识库路径开头,防止通过 ../ 等相对路径访问外部文件。
    • 若校验通过,读取文件内容;若失败,抛出明确错误。
  • saveNote

    • 验证文件名是否以 .md 结尾。
    • 安全校验:使用 path.relative 计算相对路径,检查是否越界(relative.startsWith('..')path.isAbsolute(relative))。
    • 确保知识库目录存在(mkdirrecursive 选项,若目录已存则不报错)。
    • 写入文件,返回成功信息。

3.3.3 核心函数详解

3.3.3.1 promptService.loadFrameworks
  1. 逻辑流程图
flowchart TD
    A[调用 loadFrameworks] --> B{缓存存在?}
    B -- 是 --> C[返回缓存]
    B -- 否 --> D[读取 FRAMEWORKS_DIR]
    D --> E[过滤出 .json 文件]
    E --> F[并发读取所有 JSON 文件]
    F --> G[解析为 Framework 对象]
    G --> H[存入缓存]
    H --> I[返回框架数组]
  1. 调用序列图
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[]
  1. 关系与数据流
  • 输入:无。
  • 输出Framework[] 数组。
  • 依赖:文件系统读取操作。
  • 缓存策略:使用模块级变量 frameworksCache,在服务生命周期内有效,避免重复读盘。
3.3.3.2 promptService.getFramework
  1. 逻辑流程图
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 }"]
  1. 调用序列图
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
  1. 关系与数据流
  • 输入:框架名称 name,参数字典 args
  • 输出PromptResult 对象(包含 messages 数组)。
  • 依赖loadFrameworks 返回的框架定义,personas.json 角色配置。
  • 核心逻辑:模板填充(正则替换 {{key}}Few-Shot 示例插入,系统提示选择。
3.3.3.3 intentService.generateSearchQueries
  1. 逻辑流程图
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[返回检索词数组]
  1. 调用序列图
sequenceDiagram
    participant Ctrl as toolsController
    participant Svc as intentService
    Ctrl->>Svc: generateSearchQueries('AI芯片市场趋势2026')
    Svc->>Svc: 清洗、分词、去重
    Svc-->>Ctrl: ['AI芯片','市场趋势','2026','AI芯片市场趋势2026 相关研究']
  1. 关系与数据流
  • 输入:用户查询字符串。
  • 输出:字符串数组(检索词)。
  • 核心算法:基于规则的简单 NLP不依赖外部库轻量高效。
3.3.3.4 resourceService.listObsidianNotes
  1. 逻辑流程图
flowchart TD
    A[调用 listObsidianNotes] --> B[读取 OBSIDIAN_VAULT_PATH 目录]
    B --> C{读取成功?}
    C -- 是 --> D[过滤 .md 文件]
    D --> E[返回文件名数组]
    C -- 否 --> F[打印错误日志]
    F --> G[返回空数组]
  1. 调用序列图
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[]
  1. 关系与数据流
  • 输入:无。
  • 输出Markdown 文件名列表。
  • 错误处理:若目录不可读,返回空数组并记录日志,不中断服务。
3.3.3.5 resourceService.readObsidianNote
  1. 逻辑流程图
flowchart TD
    A[输入 filename] --> B[拼接目标路径 targetPath]
    B --> C[获取安全路径 safeVaultPath]
    C --> D{targetPath 以 safeVaultPath 开头?}
    D -- 否 --> E[抛出安全异常]
    D -- 是 --> F[读取文件]
    F --> G{读取成功?}
    G -- 是 --> H[返回文件内容]
    G -- 否 --> I[抛出读取异常]
  1. 调用序列图
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
  1. 关系与数据流
  • 输入:文件名(如 "example.md")。
  • 输出:文件内容字符串。
  • 安全机制:必须确保文件在知识库目录内,防止路径遍历攻击。
  • 错误处理:校验失败或读取失败均抛出具体错误,由调用方处理。
3.3.3.6 resourceService.saveNote
  1. 逻辑流程图
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[返回成功信息]
  1. 调用序列图
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
  1. 关系与数据流
  • 输入:文件名、内容。
  • 输出:成功提示字符串。
  • 安全机制:双重校验(后缀 + 路径越界)。
  • 目录创建:自动创建知识库目录(若不存在),避免因目录缺失导致写入失败。
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.