This commit is contained in:
gzkoala
2026-03-11 14:14:06 +08:00
9043 changed files with 1407507 additions and 492 deletions

View File

@@ -3,10 +3,10 @@
title: Project Caffeine Changelog (更新日志)
description: 记录 Project Caffeine 项目的所有显著更改、版本迭代与发布历史,遵循 Keep a Changelog 与语义化版本规范。
type: Changelog
version: v0.1.1 (Arabica) - Sprint 2
version: v0.0.3 (Arabica) - Sprint 3
file: CHANGELOG.md
author: Gitconomy Research-郭晧
date: 2026-03-07
date: 2026-03-11
tags:
- Project Caffeine
- Changelog
@@ -24,7 +24,43 @@ status: Active
---
## [0.1.1] - 2026-03-06
## [0.0.3] - 2026-03-10
### Added (新增)
- **意图驱动路由引擎**:全面重构系统提示词,确立四大核心意图映射(文献查询、框架分析、保存内容、本地笔记分析),使大模型能够根据自然语言智能调度专属工具。
- **学术文献检索接入**:新增 `search_arxiv` 原子工具与 `arxivService.ts`,支持基于提取的核心关键字调用 Arxiv API实时获取学术论文并结构化输出。
- **结果落盘持久化**:优化 `save_note` 工具,允许大模型在获取用户明确授权后,将生成的长篇分析报告或检索结果一键保存为本地 Markdown 文件。
- **内部模板获取工具**:新增 `fetch_framework_template` 后台工具,专供大模型内部读取 SWOT、SCQA 等 JSON 框架配置,不再向客户端 UI 暴露。
### Changed (变更)
- **系统架构降维升级**系统架构从“UI 按钮/提示词驱动Prompt-driven”彻底转向“自然语言/工具驱动Tool-driven”。
- **参数宽容度放宽 (Parameter Tolerance)**:将 `save_note` 等工具的核心负载参数类型校验由严格的 `z.string()` 放宽至 `z.any()`,并在底层 `toolsController.ts` 中引入自动序列化机制(`JSON.stringify`),以承接大模型输出的畸形对象。
### Deprecated (废弃)
- **MCP Prompts 原语暴露**:废弃了向客户端 UI 直接暴露的 `prompts/list``prompts/get` 接口,改由大模型在后台自主调用工具获取模板。
### Removed (移除)
- 移除了 Sprint 2 时期遗留的 5 个硬编码 `server.prompt` 注册代码。
### Fixed (修复)
- **大模型 JSON 转义崩溃**:彻底修复了因大模型未能正确转义超长 Markdown 文本和嵌套引号,导致底层触发 `AI_JSONParseError` 并中断工具调用的核心故障。
- **无限循环调用死锁 (Infinite Tool Call Loop)**:修复了大模型在读取原始 JSON 框架文件后由于“困惑”而导致的工具循环调用崩溃;通过在 `toolsController.ts` 中剥离 JSON 外壳,并向大模型强制注入“🛑立即停止调用任何工具”的底层防呆指令解决。
- **TypeScript 强类型推断错误**:修复了 MCP SDK 中因 `type` 字段推断为泛用 `string` 而引发的编译报错,通过显式声明 `type: "text" as const` 解决。
### Security (安全)
- **强指令防越权写入**:在系统提示词与底层逻辑中双重加固防线,设定【绝对红线】,严禁大模型在未经主动提问并获取用户明确(如“是”、“保存”)授权前,私自调用 `save_note` 执行写盘操作。
---
## [0.0.2] - 2026-03-06
### Added (新增)
@@ -59,7 +95,7 @@ _(无)_
---
## [0.1.0] - 2026-03-03
## [0.0.1] - 2026-03-03
### Added (新增)

View File

@@ -108,7 +108,7 @@ status: Active
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| [`v0.0.1`](./projects/arabica/src/sprint1/README.md) | 部署基于 Node.js 的开发环境,验证 MCP 协议组件间通讯、大语言模型推理等基本运行环境。<br> | [Arabicat Sprint1系统设计文档](./projects/arabica/docs/design/arabica-sprint1-architecture-specification.md) | [Arabicat Sprint1系统开发文档](./projects/arabica/docs/design/arabica-sprint1-development-specification.md) |
| [`v0.0.2`](./projects/arabica/src/sprint2/README.md) | 基于 Sprint 1 原型,扩展为支持 MCP Prompts 原语的多框架引擎,实现意图拆解工具与本地知识库集成,构建模块化、可扩展的提示词策略服务器。 | [Arabica Sprint2系统设计文档](./projects/arabica/docs/design/arabica-sprint2-architecture-specification.md) | [Arabica Sprint2系统开发文档](./projects/arabica/docs/design/arabica-sprint2-development-specification.md) |
| [`v0.0.3`](./projects/arabica/src/sprint3/README.md) | 构建文献查询 Server集成学术 API 实现基础外围检索能力,并开发双轨制数据落盘模块,将离散的 JSON 数据转换为带有标准 YAML 元数据的本地化 Markdown 文件。 | [Arabica Sprint3系统设计文档](./projects/arabica/docs/design/arabica-sprint3-architecture-specification.md) | [Arabica Sprint3系统开发文档](./projects/arabica/docs/design/arabica-sprint3-devekopment-specification.md) |
| [`v0.0.3`](./projects/arabica/src/sprint3/README.md) | 构建文献查询 Server集成学术 API 实现基础外围检索能力,并开发双轨制数据落盘模块,将离散的 JSON 数据转换为带有标准 YAML 元数据的本地化 Markdown 文件。 | [Arabica Sprint3系统设计文档](./projects/arabica/docs/design/arabica-sprint3-architecture-specification.md) | [Arabica Sprint3系统开发文档](./projects/arabica/docs/design/arabica-sprint3-development-specification.md) |
---

View File

@@ -1,480 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1320 1200" width="100%" height="100%">
<!--
================================================================================
图表名称Arabica Sprint 3 架构图 (Arabica Sprint 3 Architecture)
文件命名arabica-sprint3-architecture-design.svg
用途:聚焦外围学术检索与双轨制数据落盘模块的实现。
版本v1.0.0 (Arabica) - Sprint 3
作者Gitconomy Research-郭晧
SPDX-License-Identifier: MIT & CC-BY-SA-4.0
创建日期2026-03-08
================================================================================
-->
<!-- 头部注释:图表背景声明 -->
<!-- Background: Solid White -->
<rect width="100%" height="100%" fill="#FFFFFF" />
<defs>
<style>
:root {
/* 语义色定义 */
--c-cloud-blue: #0099FF;
--c-cloud-blue-light: rgba(0, 153, 255, 0.08);
--c-local-green: #009900;
--c-local-green-light: rgba(0, 153, 0, 0.08);
--c-risk-amber: #F97316;
--c-risk-amber-light: rgba(249, 115, 22, 0.08);
--c-gov-blue: #3B82F6;
--c-gov-blue-light: rgba(59, 130, 246, 0.08);
--c-neutral-gray: #475569;
--c-neutral-gray-light: #B2B2B2;
/* 步骤高亮色 */
--c-step-red: #EF4444;
}
/* 字体降级机制 */
.font-sans { font-family: 'Noto Sans', 'Helvetica Neue', Arial, sans-serif; }
.font-mono { font-family: 'JetBrains Mono', Consolas, 'Courier New', monospace; }
/* 文本层级 */
.card-title { font-size: 14px; font-weight: bold; fill: #FFFFFF; }
.card-text { font-size: 12px; fill: var(--c-neutral-gray); }
.text-code { font-size: 13px; font-weight: bold; fill: #0F172A; }
.label-text { font-size: 12px; font-weight: bold; fill: var(--c-neutral-gray); }
.step-text { font-size: 13px; font-weight: bold; fill: #FFFFFF; }
</style>
<!-- 箭头标记 -->
<marker id="arrow-gray" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<path d="M 0 0 L 8 4 L 0 8 Z" fill="var(--c-neutral-gray)" />
</marker>
<marker id="arrow-green" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<path d="M 0 0 L 8 4 L 0 8 Z" fill="var(--c-local-green)" />
</marker>
<marker id="arrow-blue" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<path d="M 0 0 L 8 4 L 0 8 Z" fill="var(--c-cloud-blue)" />
</marker>
</defs>
<!--
===========================================================================
顶层标题块 (Title Block)
===========================================================================
-->
<g id="title-block" transform="translate(660, 40)">
<text y="0" text-anchor="middle" class="font-mono" font-size="12" fill="var(--c-neutral-gray)" letter-spacing="1">FIG-02</text>
<text y="30" text-anchor="middle" class="font-sans" font-weight="bold" font-size="24" fill="#000000">Arabica Sprint 3 系统开发组件架构图</text>
<text y="55" text-anchor="middle" class="font-mono" font-size="14" fill="var(--c-neutral-gray)">架构图 &gt; Arabica Sprint 3 &gt; 文献检索与标准化落盘工作流</text>
<!-- Context Indicator: Y=70 -->
<rect x="-30" y="70" width="60" height="3" fill="var(--c-cloud-blue)" />
</g>
<!--
===========================================================================
架构图主体内容 (Diagram Content)
整体下移80单位保证距离标题拥有 50 单位以上的空间
===========================================================================
-->
<g id="diagram-content" transform="translate(0, 80)">
<!--
===========================================================================
物理边界与容器 (Boundaries)
===========================================================================
-->
<g id="boundaries" transform="translate(0, 240)">
<!-- Node.js MCP Server Runtime Boundary -->
<rect x="270" y="0" width="690" height="620" rx="12" fill="var(--c-local-green-light)" stroke="var(--c-local-green)" stroke-dasharray="4 4" stroke-width="2" />
<rect x="270" y="0" width="690" height="36" fill="var(--c-local-green)" opacity="0.1" rx="12"/>
<text x="290" y="23" class="font-mono" font-size="14" font-weight="bold" fill="var(--c-local-green)">📦 MCP Server RuntimeSprint 3)</text>
</g>
<!-- Public Internet Boundary (Cloud) - 距MCP服务器恰好保持30单位间距 -->
<g id="cloud-boundary" transform="translate(990, 80)">
<rect x="0" y="0" width="320" height="280" rx="12" fill="var(--c-cloud-blue-light)" stroke="var(--c-cloud-blue)" stroke-dasharray="4 4" stroke-width="2" />
<rect x="0" y="0" width="320" height="36" fill="var(--c-cloud-blue)" opacity="0.1" rx="12"/>
<text x="20" y="23" class="font-mono" font-size="14" font-weight="bold" fill="var(--c-cloud-blue)">☁️ Cloud</text>
</g>
<!--
===========================================================================
核心连线与数据流向 (Connections)
===========================================================================
-->
<g id="connections" fill="none" stroke-width="2">
<!-- User <-> Cherry Studio -->
<path d="M 380 110 L 462 110" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<path d="M 470 140 L 388 140" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<!-- Cherry Studio <-> Remote LLM -->
<path d="M 740 130 L 1112 130" stroke="var(--c-cloud-blue)" stroke-dasharray="4 2" marker-end="url(#arrow-blue)" />
<path d="M 1120 150 L 748 150" stroke="var(--c-cloud-blue)" stroke-dasharray="4 2" marker-end="url(#arrow-blue)" />
<!-- Cherry Studio <-> app.ts (stdio) -->
<path d="M 600 170 L 600 272" stroke="var(--c-local-green)" marker-end="url(#arrow-green)" />
<path d="M 620 280 L 620 178" stroke="var(--c-local-green)" marker-end="url(#arrow-green)" />
<!-- app.ts -> Controllers -->
<path d="M 580 350 L 580 370 L 365 370 L 365 395" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<path d="M 610 350 L 610 395" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<path d="M 640 350 L 640 370 L 820 370 L 820 395" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<!-- toolsController -> Services -->
<!-- to literatureService -->
<path d="M 550 470 L 550 505" stroke="var(--c-local-green)" marker-end="url(#arrow-green)" />
<!-- to storageService -->
<path d="M 660 470 L 660 615" stroke="var(--c-local-green)" marker-end="url(#arrow-green)" />
<!-- resourcesController -> resourceService -->
<path d="M 820 470 L 820 505" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<!-- literatureService <-> Academic APIs (Cloud) -->
<path d="M 720 550 L 1150 550 L 1150 335" stroke="var(--c-cloud-blue)" stroke-dasharray="4 2" marker-end="url(#arrow-blue)" />
<path d="M 1130 330 L 1130 570 L 728 570" stroke="var(--c-cloud-blue)" stroke-dasharray="4 2" marker-end="url(#arrow-blue)" />
<!-- storageService -> yamlHelper -->
<path d="M 700 650 L 745 650" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<!-- storageService -> Local Vault (垂直向下落盘至正下方) -->
<path d="M 580 690 L 580 850 L 600 850 L 600 885" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<!-- resourceService -> Local Vault (绕开卡片落盘) -->
<path d="M 820 580 L 820 600 L 940 600 L 940 850 L 630 850 L 630 885" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<!-- API Clients & Schemas dotted dependencies -->
<path d="M 550 580 L 550 725" stroke="var(--c-neutral-gray)" stroke-dasharray="2 2" marker-end="url(#arrow-gray)" />
<path d="M 660 690 L 660 725" stroke="var(--c-neutral-gray)" stroke-dasharray="2 2" marker-end="url(#arrow-gray)" />
</g>
<!--
===========================================================================
路径标签说明 (Path Labels)
===========================================================================
-->
<g id="path-labels" class="font-sans label-text">
<text x="425" y="95" text-anchor="middle">Query</text>
<text x="425" y="160" text-anchor="middle">Report</text>
<text x="920" y="120" text-anchor="middle" fill="var(--c-cloud-blue)">Reasoning / Tool Calls</text>
<text x="920" y="165" text-anchor="middle" fill="var(--c-cloud-blue)">LLM Result</text>
<!-- stdio Link -->
<text x="590" y="220" text-anchor="end" fill="var(--c-local-green)">tools/call</text>
<text x="630" y="220" text-anchor="start" fill="var(--c-local-green)">Result JSON</text>
<rect x="580" y="230" width="60" height="20" rx="4" fill="#FFFFFF" stroke="var(--c-local-green)" />
<text x="610" y="245" text-anchor="middle" class="font-mono" font-size="11" fill="var(--c-local-green)">stdio</text>
<!-- Network / Vault Labels -->
<text x="1050" y="540" text-anchor="middle" fill="var(--c-cloud-blue)">HTTP GET (Search)</text>
<text x="1050" y="585" text-anchor="middle" fill="var(--c-cloud-blue)">Metadata JSON</text>
<text x="590" y="810" text-anchor="start" class="font-mono" font-size="11" fill="var(--c-neutral-gray)">fs.writeFileSync()</text>
</g>
<!--
===========================================================================
架构节点卡片 (Node Cards)
===========================================================================
-->
<g id="nodes">
<!-- 1. User Node -->
<g transform="translate(260, 95)">
<rect x="0" y="0" width="110" height="60" rx="8" fill="#F8FAFC" stroke="var(--c-neutral-gray)" stroke-width="2" />
<circle cx="55" cy="22" r="10" fill="var(--c-neutral-gray)" />
<path d="M 35 50 C 35 35, 75 35, 75 50" fill="none" stroke="var(--c-neutral-gray)" stroke-width="3" stroke-linecap="round"/>
<text x="55" y="75" text-anchor="middle" class="font-sans label-text">User / 用户</text>
</g>
<!-- 2. Cherry Studio -->
<g transform="translate(480, 80)">
<rect x="0" y="0" width="250" height="90" rx="8" fill="var(--c-gov-blue-light)" stroke="var(--c-gov-blue)" stroke-width="2" />
<rect x="0" y="0" width="250" height="30" fill="var(--c-gov-blue)" rx="8" />
<rect x="0" y="20" width="250" height="10" fill="var(--c-gov-blue)" />
<text x="125" y="20" text-anchor="middle" class="font-sans card-title">Cherry Studio (MCP Client)</text>
<text x="125" y="55" text-anchor="middle" class="font-sans card-text" font-weight="bold">大模型对话与调度宿主</text>
<text x="125" y="75" text-anchor="middle" class="font-mono card-text" font-size="11">Initiates stdio subprocess</text>
</g>
<!-- 3. Remote LLM Brain -->
<g transform="translate(1140, 145)">
<polygon points="0,-40 40,-20 40,20 0,40 -40,20 -40,-20" fill="#FFFFFF" stroke="var(--c-cloud-blue)" stroke-width="3" />
<text x="0" y="-5" text-anchor="middle" class="font-mono card-text" fill="var(--c-cloud-blue)" font-size="14" font-weight="bold">LLM</text>
<text x="0" y="15" text-anchor="middle" class="font-mono card-text" font-size="10" fill="var(--c-cloud-blue)">(Remote)</text>
</g>
<!-- 4. Academic APIs -->
<g transform="translate(1020, 260)">
<rect x="0" y="0" width="240" height="65" rx="8" fill="#FFFFFF" stroke="var(--c-cloud-blue)" stroke-width="3" />
<rect x="0" y="0" width="240" height="26" fill="var(--c-cloud-blue)" rx="8" />
<rect x="0" y="20" width="240" height="6" fill="var(--c-cloud-blue)" />
<text x="120" y="18" text-anchor="middle" class="font-sans card-title">Academic APIs</text>
<text x="120" y="42" text-anchor="middle" class="font-mono text-code" fill="var(--c-cloud-blue)" font-size="12">arXiv API</text>
<text x="120" y="56" text-anchor="middle" class="font-mono text-code" fill="var(--c-cloud-blue)" font-size="12">Semantic Scholar API</text>
</g>
<!-- 5. app.ts -->
<g transform="translate(530, 280)">
<rect x="0" y="0" width="160" height="70" rx="6" fill="#FFFFFF" stroke="var(--c-local-green)" stroke-width="2" />
<rect x="0" y="0" width="160" height="26" fill="var(--c-local-green)" rx="6" />
<rect x="0" y="20" width="160" height="6" fill="var(--c-local-green)" />
<text x="80" y="18" text-anchor="middle" class="font-mono card-title">app.ts</text>
<text x="80" y="45" text-anchor="middle" class="font-sans text-code" font-size="12">McpServer 注册原语</text>
<text x="80" y="62" text-anchor="middle" class="font-mono card-text" font-size="10">Stdio Transport</text>
</g>
<!-- 6. Controllers -->
<!-- promptsController -->
<g transform="translate(290, 400)">
<rect x="0" y="0" width="150" height="70" rx="6" fill="#FFFFFF" stroke="var(--c-risk-amber)" stroke-width="2"/>
<rect x="0" y="0" width="150" height="26" fill="var(--c-risk-amber)" rx="6"/>
<rect x="0" y="20" width="150" height="6" fill="var(--c-risk-amber)" />
<text x="75" y="18" text-anchor="middle" class="font-mono card-title">promptsController</text>
<text x="75" y="45" text-anchor="middle" class="font-sans card-text">处理 prompts/get</text>
</g>
<!-- toolsController (Sprint 3 Focus) -->
<g transform="translate(460, 400)">
<rect x="0" y="0" width="240" height="70" rx="6" fill="#FFFFFF" stroke="var(--c-risk-amber)" stroke-width="2" />
<rect x="0" y="0" width="240" height="26" fill="var(--c-risk-amber)" rx="6" />
<rect x="0" y="20" width="240" height="6" fill="var(--c-risk-amber)" />
<text x="120" y="18" text-anchor="middle" class="font-mono card-title">toolsController</text>
<text x="120" y="44" text-anchor="middle" class="font-sans card-text" font-size="11">search_academic_literature</text>
<text x="120" y="60" text-anchor="middle" class="font-sans card-text" font-size="11">save_literature_to_vault</text>
</g>
<!-- resourcesController -->
<g transform="translate(720, 400)">
<rect x="0" y="0" width="200" height="70" rx="6" fill="#FFFFFF" stroke="var(--c-risk-amber)" stroke-width="2" />
<rect x="0" y="0" width="200" height="26" fill="var(--c-risk-amber)" rx="6" />
<rect x="0" y="20" width="200" height="6" fill="var(--c-risk-amber)" />
<text x="100" y="18" text-anchor="middle" class="font-mono card-title">resourcesController</text>
<text x="100" y="44" text-anchor="middle" class="font-sans card-text">处理 resources/read 分发</text>
<text x="100" y="60" text-anchor="middle" class="font-sans card-text">含 literature:// 协议支持</text>
</g>
<!-- 7. Services Row 1 -->
<!-- promptService & intentService (S2 Stacked) -->
<g transform="translate(290, 510)">
<rect x="0" y="0" width="150" height="160" rx="6" fill="#FFFFFF" stroke="var(--c-local-green)" stroke-width="2" opacity="0.6"/>
<rect x="0" y="0" width="150" height="26" fill="var(--c-local-green)" rx="6" opacity="0.6"/>
<text x="75" y="18" text-anchor="middle" class="font-mono card-title">S2 Strategy Services</text>
<rect x="10" y="40" width="130" height="45" rx="4" fill="var(--c-local-green-light)" />
<text x="75" y="58" text-anchor="middle" class="font-mono text-code" font-size="11">promptService</text>
<text x="75" y="75" text-anchor="middle" class="font-sans card-text" font-size="10">多维思维框架管理</text>
<rect x="10" y="95" width="130" height="45" rx="4" fill="var(--c-local-green-light)" />
<text x="75" y="113" text-anchor="middle" class="font-mono text-code" font-size="11">intentService</text>
<text x="75" y="130" text-anchor="middle" class="font-sans card-text" font-size="10">意图拆解生成检索词</text>
</g>
<!-- literatureService -->
<g transform="translate(460, 510)">
<rect x="0" y="0" width="240" height="70" rx="6" fill="#FFFFFF" stroke="var(--c-local-green)" stroke-width="2" />
<rect x="0" y="0" width="240" height="26" fill="var(--c-local-green)" rx="6" />
<rect x="0" y="20" width="240" height="6" fill="var(--c-local-green)" />
<text x="120" y="18" text-anchor="middle" class="font-mono card-title">literatureService.ts (New)</text>
<text x="120" y="45" text-anchor="middle" class="font-sans text处理 prompts/ge-code" font-size="12" fill="var(--c-local-green)">并发调用学术API及去重</text>
<text x="120" y="62" text-anchor="middle" class="font-sans card-text" font-size="11">向大模型返回标准化 JSON 数组</text>
</g>
<!-- resourceService -->
<g transform="translate(720, 510)">
<rect x="0" y="0" width="200" height="70" rx="6" fill="#FFFFFF" stroke="var(--c-local-green)" stroke-width="2" />
<rect x="0" y="0" width="200" height="26" fill="var(--c-local-green)" rx="6" />
<rect x="0" y="20" width="200" height="6" fill="var(--c-local-green)" />
<text x="100" y="18" text-anchor="middle" class="font-mono card-title">resourceService.ts</text>
<text x="100" y="45" text-anchor="middle" class="font-sans text-code" font-size="12">文件系统读取交互</text>
<text x="100" y="62" text-anchor="middle" class="font-sans card-text" font-size="11">读取 Vault 中的文献卡片</text>
</g>
<!-- 8. Services Row 2 -->
<!-- storageService -->
<g transform="translate(460, 620)">
<rect x="0" y="0" width="240" height="70" rx="6" fill="#FFFFFF" stroke="var(--c-local-green)" stroke-width="2" />
<rect x="0" y="0" width="240" height="26" fill="var(--c-local-green)" rx="6" />
<rect x="0" y="20" width="240" height="6" fill="var(--c-local-green)" />
<text x="120" y="18" text-anchor="middle" class="font-mono card-title">storageService.ts (New)</text>
<text x="120" y="45" text-anchor="middle" class="font-sans text-code" font-size="12" fill="var(--c-local-green)">JSON 转 Markdown+YAML</text>
<text x="120" y="62" text-anchor="middle" class="font-sans card-text" font-size="11">路径安全校验与双轨数据落盘</text>
</g>
<!-- yamlHelper.js -->
<g transform="translate(750, 625)">
<rect x="0" y="0" width="170" height="50" rx="6" fill="#F8FAFC" stroke="var(--c-neutral-gray)" stroke-width="1.5" />
<text x="85" y="20" text-anchor="middle" class="font-mono text-code" font-size="12">utils/yamlHelper.js</text>
<text x="85" y="40" text-anchor="middle" class="font-sans card-text" font-size="11">生成 Frontmatter 文本</text>
</g>
<!-- 9. Utils & Models -->
<g transform="translate(460, 730)">
<rect x="0" y="0" width="115" height="55" rx="6" fill="#F8FAFC" stroke="var(--c-neutral-gray)" stroke-width="1.5" />
<text x="57" y="22" text-anchor="middle" class="font-mono text-code" font-size="11">apiClients.js</text>
<text x="57" y="42" text-anchor="middle" class="font-sans card-text" font-size="11">Axios / 重试</text>
</g>
<g transform="translate(585, 730)">
<rect x="0" y="0" width="115" height="55" rx="6" fill="#F8FAFC" stroke="var(--c-neutral-gray)" stroke-width="1.5" />
<text x="57" y="18" text-anchor="middle" class="font-mono text-code" font-size="11">schemas.js</text>
<text x="57" y="34" text-anchor="middle" class="font-mono card-text" font-size="10">literatureSchema</text>
<text x="57" y="48" text-anchor="middle" class="font-sans card-text" font-size="10">Zod强校验标准</text>
</g>
<!-- 10. External Resources (Local Vault 位于MCP中心正下方) -->
<g transform="translate(565, 890)">
<path d="M 0 15 A 50 15 0 1 0 100 15 V 60 A 50 15 0 1 1 0 60 Z" fill="var(--c-local-green-light)" stroke="var(--c-neutral-gray)" stroke-width="2" stroke-dasharray="2 2" />
<ellipse cx="50" cy="15" rx="50" ry="15" fill="var(--c-local-green-light)" stroke="var(--c-neutral-gray)" stroke-width="2" stroke-dasharray="2 2" />
<text x="50" y="48" text-anchor="middle" class="font-sans card-text" fill="var(--c-neutral-gray)" font-size="13" font-weight="bold">Local Vault</text>
<text x="50" y="65" text-anchor="middle" class="font-mono card-text" fill="var(--c-neutral-gray)" font-size="10">.md 卡片落盘</text>
</g>
</g>
<!--
===========================================================================
步骤侧边栏与流程图注 (Execution Steps)
===========================================================================
-->
<g id="execution-steps">
<!-- 图内红圈标记 -->
<g transform="translate(420, 110)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">1</text>
</g>
<g transform="translate(820, 130)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">2</text>
</g>
<g transform="translate(610, 365)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">3</text>
</g>
<g transform="translate(970, 550)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">4</text>
</g>
<g transform="translate(1150, 420)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">5</text>
</g>
<g transform="translate(660, 490)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">6</text>
</g>
<g transform="translate(715, 650)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">7</text>
</g>
<g transform="translate(580, 820)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">8</text>
</g>
<g transform="translate(420, 140)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">9</text>
</g>
<!-- 步骤流程侧边栏 -->
<g transform="translate(10, 320)">
<rect x="0" y="0" width="250" height="250" rx="8" fill="#FEF2F2" stroke="var(--c-step-red)" stroke-width="1.5" />
<text x="125" y="25" text-anchor="middle" class="font-sans" font-size="14" font-weight="bold" fill="var(--c-step-red)">Sprint 3 文献检索落盘工作流</text>
<line x1="15" y1="35" x2="235" y2="35" stroke="var(--c-step-red)" stroke-dasharray="2 2" />
<g transform="translate(15, 55)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">1</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">User: 输入研究主题进行查询</text>
</g>
<g transform="translate(15, 75)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">2</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">LLM: 判断并决定检索相关学术文献</text>
</g>
<g transform="translate(15, 95)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">3</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">Client: 调用 search_academic 工具</text>
</g>
<g transform="translate(15, 115)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">4</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">S1: 并发请求 arXiv / Sem. Scholar</text>
</g>
<g transform="translate(15, 135)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">5</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">API: 返回聚合的 JSON 文献元数据</text>
</g>
<g transform="translate(15, 155)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">6</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">LLM: 分析后调用 save_literature</text>
</g>
<g transform="translate(15, 175)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">7</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">S1: 将 JSON 转为 MD + YAML 前缀</text>
</g>
<g transform="translate(15, 195)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">8</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">S1: 安全写入指定的本地 Vault 目录</text>
</g>
<g transform="translate(15, 215)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">9</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">LLM: 生成带本地文献引用的最终报告</text>
</g>
</g>
</g>
</g>
<!--
===========================================================================
底部图例与注释块 (Legend & Key Block)
===========================================================================
-->
<g id="legend" transform="translate(660, 1090)">
<!-- 统一的半透明背景容器 -->
<rect x="-480" y="-16" width="960" height="40" rx="4" fill="rgba(255, 255, 255, 0.9)" stroke="var(--c-neutral-gray)" stroke-width="0.5"/>
<g transform="translate(-460, 0)">
<!-- 图例引导文本 -->
<text x="0" y="8" class="font-sans" font-size="13" font-weight="bold" fill="var(--c-neutral-gray)">组件与语义对照:</text>
<!-- 图例项 1 -->
<g transform="translate(130, 0)">
<rect x="-6" y="-4" width="12" height="12" rx="2" fill="var(--c-gov-blue)" />
<text x="12" y="8" class="font-sans" font-size="12" fill="var(--c-neutral-gray)">宿主客户端架构</text>
</g>
<!-- 图例项 2 -->
<g transform="translate(280, 0)">
<rect x="-6" y="-4" width="12" height="12" rx="2" fill="var(--c-risk-amber)" />
<text x="12" y="8" class="font-sans" font-size="12" fill="var(--c-neutral-gray)">路由分发控制器 (Controllers)</text>
</g>
<!-- 图例项 3 -->
<g transform="translate(520, 0)">
<rect x="-6" y="-4" width="12" height="12" rx="2" fill="var(--c-local-green)" />
<text x="12" y="8" class="font-sans" font-size="12" fill="var(--c-neutral-gray)">核心引擎与本地执行层 (Sprint 3核心)</text>
</g>
<!-- 图例项 4 -->
<g transform="translate(790, 0)">
<rect x="-6" y="-4" width="12" height="12" rx="2" fill="var(--c-cloud-blue)" />
<text x="12" y="8" class="font-sans" font-size="12" fill="var(--c-neutral-gray)">远程云端 API 依赖</text>
</g>
</g>
</g>
<!--
===========================================================================
版权许可声明 (License)
===========================================================================
-->
<text x="660" y="1150" text-anchor="middle" class="font-sans" font-size="11" fill="var(--c-neutral-gray)">
本作品采用 CC-BY-SA 4.0 进行许可,© 2025-2026 Gitconomy Research社区
</text>
</svg>

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,445 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="140 0 1100 1200" width="100%" height="100%">
<!--
================================================================================
图表名称Arabica Sprint 3 架构图 (Arabica Sprint 3 Architecture)
文件命名arabica-sprint3-architecture-design.svg
用途:展示 Project Caffeine 在 Sprint 3 中的意图路由、学术检索接入与高容错防死循环落盘机制。
版本v1.0.0 (Arabica) - Sprint 3
作者Gitconomy Research-郭晧
SPDX-License-Identifier: MIT & CC-BY-SA-4.0
创建日期2026-03-11
================================================================================
-->
<!-- 头部注释:图表背景声明 -->
<!-- Background: Solid White -->
<rect width="100%" height="100%" fill="#FFFFFF" />
<defs>
<style>
:root {
/* 语义色定义 */
--c-cloud-blue: #0099FF;
--c-cloud-blue-light: rgba(0, 153, 255, 0.08);
--c-local-green: #009900;
--c-local-green-light: rgba(0, 153, 0, 0.08);
--c-risk-amber: #F97316;
--c-risk-amber-light: rgba(249, 115, 22, 0.08);
--c-gov-blue: #3B82F6;
--c-gov-blue-light: rgba(59, 130, 246, 0.08);
--c-neutral-gray: #475569;
--c-neutral-gray-light: #B2B2B2;
/* 步骤高亮色 */
--c-step-red: #EF4444;
}
/* 字体降级机制 */
.font-sans { font-family: 'Noto Sans', 'Helvetica Neue', Arial, sans-serif; }
.font-mono { font-family: 'JetBrains Mono', Consolas, 'Courier New', monospace; }
/* 文本层级 */
.card-title { font-size: 15px; font-weight: bold; fill: #FFFFFF; }
.card-text { font-size: 13px; fill: var(--c-neutral-gray); }
.text-code { font-size: 13px; font-weight: bold; fill: #0F172A; }
.label-text { font-size: 12px; font-weight: bold; fill: var(--c-neutral-gray); }
.step-text { font-size: 13px; font-weight: bold; fill: #FFFFFF; }
.alert-text { font-size: 12px; font-weight: bold; fill: #DC2626; } /* 红色警示文字 */
.amber-text { font-size: 12px; font-weight: bold; fill: #D97706; } /* 琥珀色强调文字 */
</style>
<!-- 箭头标记:尺寸缩小三分之一以更精致呈现 -->
<marker id="arrow-gray" markerWidth="4" markerHeight="4" refX="3.5" refY="2" orient="auto">
<path d="M 0 0 L 4 2 L 0 4 Z" fill="var(--c-neutral-gray)" />
</marker>
<marker id="arrow-green" markerWidth="4" markerHeight="4" refX="3.5" refY="2" orient="auto">
<path d="M 0 0 L 4 2 L 0 4 Z" fill="var(--c-local-green)" />
</marker>
<marker id="arrow-blue" markerWidth="4" markerHeight="4" refX="3.5" refY="2" orient="auto">
<path d="M 0 0 L 4 2 L 0 4 Z" fill="var(--c-cloud-blue)" />
</marker>
<marker id="arrow-amber" markerWidth="4" markerHeight="4" refX="3.5" refY="2" orient="auto">
<path d="M 0 0 L 4 2 L 0 4 Z" fill="var(--c-risk-amber)" />
</marker>
</defs>
<!--
===========================================================================
顶层标题块 (Title Block)
===========================================================================
-->
<g id="title-block" transform="translate(640, 40)">
<text y="0" text-anchor="middle" class="font-mono" font-size="12" fill="var(--c-neutral-gray)" letter-spacing="1">FIG-02</text>
<text y="30" text-anchor="middle" class="font-sans" font-weight="bold" font-size="24" fill="#000000">Arabica Sprint 3 系统开发组件架构图</text>
<text y="55" text-anchor="middle" class="font-mono" font-size="14" fill="var(--c-neutral-gray)">架构图 &gt; Arabica Sprint 3 &gt; 意图路由、学术检索与高容错落盘机制</text>
<!-- Context Indicator: Y=70 -->
<rect x="-30" y="70" width="60" height="3" fill="var(--c-cloud-blue)" />
</g>
<!--
===========================================================================
架构图主体内容 (Diagram Content)
===========================================================================
-->
<g id="diagram-content" transform="translate(0, 80)">
<!-- 物理边界与容器 -->
<g id="boundaries" transform="translate(0, 240)">
<!-- Node.js MCP Server Runtime Boundary -->
<rect x="290" y="0" width="710" height="560" rx="12" fill="var(--c-local-green-light)" stroke="var(--c-local-green)" stroke-dasharray="4 4" stroke-width="2" />
<rect x="290" y="0" width="710" height="36" fill="var(--c-local-green)" opacity="0.1" rx="12"/>
<text x="310" y="23" class="font-mono" font-size="14" font-weight="bold" fill="var(--c-local-green)">📦 MCP Server RuntimeSprint3) </text>
</g>
<!--
===========================================================================
核心连线与数据流向 (Connections)
===========================================================================
-->
<g id="connections" fill="none" stroke-width="2">
<!-- User to Cherry Studio -->
<path d="M 400 110 L 482 110" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<path d="M 490 140 L 408 140" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<!-- Cherry Studio to Remote LLM -->
<path d="M 760 110 L 892 110" stroke="var(--c-cloud-blue)" stroke-dasharray="4 2" marker-end="url(#arrow-blue)" />
<path d="M 900 140 L 768 140" stroke="var(--c-cloud-blue)" stroke-dasharray="4 2" marker-end="url(#arrow-blue)" />
<!-- stdio Transport -->
<path d="M 600 170 L 600 292" stroke="var(--c-local-green)" marker-end="url(#arrow-green)" />
<path d="M 640 300 L 640 178" stroke="var(--c-local-green)" marker-end="url(#arrow-green)" />
<!-- app.ts to Controllers -->
<!-- To toolsController (Router Brain) -->
<path d="M 560 370 L 560 395 L 420 395 L 420 422" stroke="var(--c-risk-amber)" stroke-width="2.5" marker-end="url(#arrow-amber)" />
<!-- To resourcesController -->
<path d="M 680 370 L 680 395 L 770 395 L 770 422" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<!-- toolsController (Router) to Services -->
<path d="M 410 495 L 410 532" stroke="var(--c-local-green)" marker-end="url(#arrow-green)" />
<path d="M 580 495 L 580 515 L 770 515 L 770 532" stroke="var(--c-local-green)" marker-end="url(#arrow-green)" />
<!-- resourcesController to resourceService -->
<path d="M 770 495 L 770 532" stroke="var(--c-neutral-gray)" marker-end="url(#arrow-gray)" />
<!-- arxivService to External arXiv API (Network) 穿越边界线 -->
<path d="M 320 580 L 220 580" stroke="var(--c-cloud-blue)" stroke-dasharray="4 4" marker-end="url(#arrow-blue)" />
<!-- resourceService to Local Vault (File I/O) -->
<path d="M 770 620 L 770 672" stroke="var(--c-local-green)" marker-end="url(#arrow-green)" />
<!-- direct read Frameworks from toolsController (Bypass) -->
<path d="M 560 495 L 560 652" stroke="var(--c-risk-amber)" stroke-dasharray="2 2" marker-end="url(#arrow-amber)" opacity="0.6"/>
</g>
<!--
===========================================================================
路径标签说明 (Path Labels)
===========================================================================
-->
<g id="path-labels" class="font-sans label-text">
<!-- Top Level -->
<text x="445" y="95" text-anchor="middle">Query</text>
<text x="445" y="160" text-anchor="middle">Report / Confirm</text>
<text x="830" y="95" text-anchor="middle" fill="var(--c-cloud-blue)">Routing Intent</text>
<text x="830" y="160" text-anchor="middle" fill="var(--c-cloud-blue)">LLM Result / Ask</text>
<!-- stdio Link -->
<text x="585" y="210" text-anchor="end" fill="var(--c-local-green)">tools/call</text>
<text x="655" y="210" text-anchor="start" fill="var(--c-local-green)">Result / Brake</text>
<text x="700" y="415" text-anchor="start" fill="var(--c-neutral-gray)">resources/read</text>
<rect x="585" y="250" width="70" height="20" rx="4" fill="#FFFFFF" stroke="var(--c-local-green)" />
<text x="620" y="264" text-anchor="middle" class="font-mono" font-size="11" fill="var(--c-local-green)">stdio</text>
<!-- Internal Labels -->
<text x="420" y="515" text-anchor="start" fill="var(--c-local-green)" font-size="11">search_arxiv</text>
<text x="650" y="510" text-anchor="start" fill="var(--c-local-green)" font-size="11">save_note (z.any容错)</text>
<text x="570" y="590" text-anchor="start" fill="var(--c-risk-amber)" font-size="10">fetch_framework</text>
</g>
<!--
===========================================================================
架构节点卡片 (Node Cards)
===========================================================================
-->
<g id="nodes">
<!-- 1. User Node -->
<g transform="translate(280, 95)">
<rect x="0" y="0" width="110" height="60" rx="8" fill="#F8FAFC" stroke="var(--c-neutral-gray)" stroke-width="2" />
<circle cx="55" cy="22" r="10" fill="var(--c-neutral-gray)" />
<path d="M 35 50 C 35 35, 75 35, 75 50" fill="none" stroke="var(--c-neutral-gray)" stroke-width="3" stroke-linecap="round"/>
<text x="55" y="75" text-anchor="middle" class="font-sans label-text">User / 用户</text>
</g>
<!-- 2. Cherry Studio -->
<g transform="translate(490, 80)">
<rect x="0" y="0" width="260" height="90" rx="8" fill="var(--c-gov-blue-light)" stroke="var(--c-gov-blue)" stroke-width="2" />
<rect x="0" y="0" width="260" height="30" fill="var(--c-gov-blue)" rx="8" />
<rect x="0" y="20" width="260" height="10" fill="var(--c-gov-blue)" />
<text x="130" y="20" text-anchor="middle" class="font-sans card-title">Cherry Studio (MCP Client)</text>
<text x="130" y="55" text-anchor="middle" class="font-sans card-text" font-weight="bold">大模型交互与意图传递中枢</text>
<text x="130" y="75" text-anchor="middle" class="font-mono card-text" font-size="11">Initiates stdio subprocess</text>
</g>
<!-- 3. Remote LLM Brain -->
<g transform="translate(970, 120)">
<polygon points="0,-45 40,-20 40,20 0,45 -40,20 -40,-20" fill="var(--c-cloud-blue-light)" stroke="var(--c-cloud-blue)" stroke-width="3" />
<text x="0" y="-5" text-anchor="middle" class="font-mono card-text" fill="var(--c-cloud-blue)" font-size="14">LLM</text>
<text x="0" y="15" text-anchor="middle" class="font-mono card-text" font-size="10" fill="var(--c-cloud-blue)">(Agent)</text>
</g>
<!-- 4. app.ts -->
<g transform="translate(520, 300)">
<rect x="0" y="0" width="200" height="70" rx="6" fill="#FFFFFF" stroke="var(--c-local-green)" stroke-width="2" />
<rect x="0" y="0" width="200" height="26" fill="var(--c-local-green)" rx="6" />
<rect x="0" y="20" width="200" height="6" fill="var(--c-local-green)" />
<text x="100" y="18" text-anchor="middle" class="font-mono card-title">app.ts</text>
<text x="100" y="45" text-anchor="middle" class="font-sans text-code" font-size="12">McpServer 注册入口</text>
<text x="100" y="62" text-anchor="middle" class="font-mono card-text" font-size="10">Tools &amp; Resources (stdio)</text>
</g>
<!-- 5. toolsController.ts (Expanded Central Router) -->
<g transform="translate(320, 430)">
<rect x="0" y="0" width="280" height="65" rx="6" fill="var(--c-risk-amber-light)" stroke="var(--c-risk-amber)" stroke-width="2.5" />
<rect x="0" y="0" width="280" height="26" fill="var(--c-risk-amber)" rx="6" />
<rect x="0" y="20" width="280" height="6" fill="var(--c-risk-amber)" />
<text x="140" y="18" text-anchor="middle" class="font-mono card-title">toolsController</text>
<text x="140" y="45" text-anchor="middle" class="font-sans card-text" font-weight="bold">统一调度与防呆:截获意图请求并分发</text>
<text x="140" y="58" text-anchor="middle" class="font-sans alert-text" font-size="11">自动剥离框架 JSON 外壳,注入【🛑停止】指令</text>
</g>
<!-- 6. resourcesController.ts -->
<g transform="translate(630, 430)">
<rect x="0" y="0" width="280" height="65" rx="6" fill="#FFFFFF" stroke="var(--c-neutral-gray-light)" stroke-width="2" />
<rect x="0" y="0" width="280" height="26" fill="var(--c-neutral-gray-light)" rx="6" />
<rect x="0" y="20" width="280" height="6" fill="var(--c-neutral-gray-light)" />
<text x="140" y="18" text-anchor="middle" class="font-mono card-title">resourcesController</text>
<text x="140" y="45" text-anchor="middle" class="font-sans card-text">解析 resource/read 请求</text>
<text x="140" y="58" text-anchor="middle" class="font-sans card-text" font-size="11">扩展 literature:// 协议</text>
</g>
<!-- 7. arxivService.ts -->
<g transform="translate(320, 540)">
<rect x="0" y="0" width="180" height="80" rx="6" fill="#FFFFFF" stroke="var(--c-local-green)" stroke-width="2" />
<rect x="0" y="0" width="180" height="26" fill="var(--c-local-green)" rx="6" />
<rect x="0" y="20" width="180" height="6" fill="var(--c-local-green)" />
<text x="90" y="18" text-anchor="middle" class="font-mono card-title">arxivService.ts</text>
<text x="90" y="45" text-anchor="middle" class="font-sans text-code" font-size="12">外部学术检索网关</text>
<text x="90" y="60" text-anchor="middle" class="font-sans card-text" font-size="11">解析底层复杂 XML 结构</text>
<text x="90" y="73" text-anchor="middle" class="font-sans card-text" font-size="11">转化为标准 Markdown 列表</text>
</g>
<!-- 8. resourceService.ts -->
<g transform="translate(665, 540)">
<rect x="0" y="0" width="210" height="80" rx="6" fill="#FFFFFF" stroke="var(--c-local-green)" stroke-width="2" />
<rect x="0" y="0" width="210" height="26" fill="var(--c-local-green)" rx="6" />
<rect x="0" y="20" width="210" height="6" fill="var(--c-local-green)" />
<text x="105" y="18" text-anchor="middle" class="font-mono card-title">resourceService.ts</text>
<text x="105" y="45" text-anchor="middle" class="font-sans text-code" font-size="12">处理长文本落盘 (save_note)</text>
<rect x="25" y="52" width="160" height="20" rx="3" fill="var(--c-risk-amber-light)" />
<text x="105" y="66" text-anchor="middle" class="font-sans amber-text" font-size="11">拦截 z.any() 畸形入参并序列化</text>
</g>
<!-- 9. External arXiv API -->
<g transform="translate(0, 580)">
<polygon points="30,-24 190,-24 220,0 190,24 30,24 0,0" fill="var(--c-cloud-blue-light)" stroke="var(--c-cloud-blue)" stroke-width="2" stroke-dasharray="4 2" />
<!-- arXiv 学术网站图标 (学士帽/文档云) -->
<g transform="translate(35, -10)">
<polygon points="12,0 2,5 12,10 22,5" fill="none" stroke="var(--c-cloud-blue)" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M 5 6.5 L 5 12 Q 12 16 19 12 L 19 6.5" fill="none" stroke="var(--c-cloud-blue)" stroke-width="1.5"/>
<polyline points="12,5 22,5 22,11" fill="none" stroke="var(--c-cloud-blue)" stroke-width="1.5" stroke-linejoin="round"/>
<circle cx="22" cy="12" r="1.5" fill="var(--c-cloud-blue)"/>
</g>
<text x="130" y="4" text-anchor="middle" class="font-sans card-text" fill="var(--c-cloud-blue)" font-weight="bold">External arXiv API</text>
</g>
<!-- 10. models/frameworks -->
<g transform="translate(470, 660)">
<rect x="0" y="0" width="180" height="60" rx="6" fill="#F8FAFC" stroke="var(--c-neutral-gray)" stroke-width="1.5" stroke-dasharray="4 2" />
<path d="M 0 20 L 180 20" stroke="var(--c-neutral-gray)" stroke-width="1.5" stroke-dasharray="4 2" />
<text x="90" y="14" text-anchor="middle" class="font-mono text-code" font-size="11">models/frameworks/</text>
<text x="90" y="38" text-anchor="middle" class="font-mono card-text" font-size="11">静态分析框架模板</text>
<text x="90" y="52" text-anchor="middle" class="font-mono card-text" font-size="11">(swot.json, scqa.json)</text>
</g>
<!-- 11. Local Vault -->
<g transform="translate(695, 680)">
<path d="M 0 15 A 75 15 0 1 0 150 15 V 45 A 75 15 0 1 1 0 45 Z" fill="var(--c-local-green-light)" stroke="var(--c-neutral-gray)" stroke-width="2" />
<ellipse cx="75" cy="15" rx="75" ry="15" fill="var(--c-local-green-light)" stroke="var(--c-neutral-gray)" stroke-width="2" />
<text x="75" y="45" text-anchor="middle" class="font-sans card-text" fill="var(--c-neutral-gray)" font-size="12" font-weight="bold">Obsidian Vault</text>
</g>
<!-- 12. schemas.ts (Zod Validation) -->
<g transform="translate(695, 755)">
<rect x="0" y="0" width="150" height="24" rx="4" fill="#F8FAFC" stroke="var(--c-neutral-gray)" stroke-width="1" />
<text x="75" y="16" text-anchor="middle" class="font-mono text-code" font-size="11">schemas.ts: z.any()</text>
</g>
</g>
<!--
===========================================================================
步骤侧边栏与流程图注 (Execution Steps) - 点落连线上
===========================================================================
-->
<g id="execution-steps">
<!-- 1. User input -->
<g transform="translate(440, 110)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">1</text>
</g>
<!-- 2. LLM routes search -->
<g transform="translate(826, 110)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">2</text>
</g>
<!-- 3. arXiv fetch (下移至跨界线上) -->
<g transform="translate(270, 580)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">3</text>
</g>
<!-- 4. LLM routes framework -->
<g transform="translate(600, 210)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">4</text>
</g>
<!-- 5. tools strips and brakes -->
<g transform="translate(560, 580)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">5</text>
</g>
<!-- 6. LLM stops tool, outputs & asks -->
<g transform="translate(826, 140)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">6</text>
</g>
<!-- 7. User confirms -->
<g transform="translate(440, 140)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">7</text>
</g>
<!-- 8. z.any fallback -->
<g transform="translate(675, 602)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">8</text>
</g>
<!-- 9. Write to vault -->
<g transform="translate(770, 646)">
<circle cx="0" cy="0" r="10" fill="var(--c-step-red)" />
<text x="0" y="4.5" text-anchor="middle" class="font-sans step-text">9</text>
</g>
</g>
</g>
<!--
===========================================================================
底部步骤流程说明面板 (Sprint 3 执行步骤流)
===========================================================================
-->
<g transform="translate(290, 910)">
<rect x="0" y="0" width="710" height="160" rx="8" fill="#FEF2F2" stroke="var(--c-step-red)" stroke-width="1.5" />
<text x="355" y="25" text-anchor="middle" class="font-sans" font-size="14" font-weight="bold" fill="var(--c-step-red)">Sprint 3 执行步骤流</text>
<line x1="15" y1="35" x2="695" y2="35" stroke="var(--c-step-red)" stroke-dasharray="2 2" />
<!-- Column 1 (Steps 1-5) -->
<g transform="translate(30, 55)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">1</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">用户输入包含检索与分析的复合查询</text>
</g>
<g transform="translate(30, 75)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">2</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">大模型识别意图1: 请求 search_arxiv</text>
</g>
<g transform="translate(30, 95)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">3</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">arxivService 请求外网并解析格式化</text>
</g>
<g transform="translate(30, 115)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">4</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">大模型识别意图2: 请求分析框架模板</text>
</g>
<g transform="translate(30, 135)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">5</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">Tools 剥离 JSON注入🛑【刹车指令】</text>
</g>
<!-- Column 2 (Steps 6-9) -->
<g transform="translate(380, 55)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">6</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">模型工具被截断,输出报告并询问保存</text>
</g>
<g transform="translate(380, 75)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">7</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">用户确认保存触发意图3: save_note</text>
</g>
<g transform="translate(380, 95)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">8</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">底层 z.any() 宽容拦截长文本并序列化</text>
</g>
<g transform="translate(380, 115)">
<circle cx="6" cy="-4" r="7" fill="var(--c-step-red)" />
<text x="6" y="0" text-anchor="middle" class="font-sans step-text" font-size="10">9</text>
<text x="20" y="0" class="font-sans card-text" fill="#000000">resourceService 安全写入知识库落盘</text>
</g>
</g>
<!--
===========================================================================
底部图例与注释块 (Legend & Key Block)
===========================================================================
-->
<g id="legend" transform="translate(640, 1110)">
<!-- 统一的半透明背景容器 -->
<rect x="-440" y="-16" width="880" height="40" rx="4" fill="rgba(255, 255, 255, 0.9)" stroke="var(--c-neutral-gray)" stroke-width="0.5"/>
<g transform="translate(-420, 0)">
<!-- 图例引导文本 -->
<text x="0" y="8" class="font-sans" font-size="13" font-weight="bold" fill="var(--c-neutral-gray)">组件与语义对照:</text>
<!-- 图例项 1 -->
<g transform="translate(130, 0)">
<rect x="-6" y="-4" width="12" height="12" rx="2" fill="var(--c-gov-blue)" />
<text x="12" y="8" class="font-sans" font-size="12" fill="var(--c-neutral-gray)">宿主客户端</text>
</g>
<!-- 图例项 2 -->
<g transform="translate(240, 0)">
<rect x="-6" y="-4" width="12" height="12" rx="2" fill="var(--c-risk-amber)" />
<text x="12" y="8" class="font-sans" font-size="12" fill="var(--c-neutral-gray)">防火墙路由 (防死循环)</text>
</g>
<!-- 图例项 3 -->
<g transform="translate(430, 0)">
<rect x="-6" y="-4" width="12" height="12" rx="2" fill="var(--c-local-green)" />
<text x="12" y="8" class="font-sans" font-size="12" fill="var(--c-neutral-gray)">底层服务引擎与资源落盘</text>
</g>
<!-- 图例项 4 -->
<g transform="translate(640, 0)">
<rect x="-6" y="-4" width="12" height="12" rx="2" fill="var(--c-cloud-blue)" />
<text x="12" y="8" class="font-sans" font-size="12" fill="var(--c-neutral-gray)">外部云端/网络依赖</text>
</g>
</g>
</g>
<!--
===========================================================================
版权许可声明 (License)
===========================================================================
-->
<text x="640" y="1170" text-anchor="middle" class="font-sans" font-size="11" fill="var(--c-neutral-gray)">
本作品采用 CC-BY-SA 4.0 进行许可,© 2025-2026 Gitconomy Research社区
</text>
</svg>

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200" width="100%" height="100%">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 970" width="100%" height="100%">
<!--
================================================================================
图表名称:个人级研究助手智能体 - 开发框架与技术栈架构图

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,88 @@
<!--
---
title: "Project Caffeine项目命名规则指南"
description: "详细阐述 Project Caffeine萃取者计划的核心命名隐喻、A-Z 咖啡图谱版本号规范及其在 Git 与工程化中的配置标准"
type: "Guide"
file: "project-caffeine-version-name-convention-guide.md"
version: "v1.0.0 (Arabica)"
author: "Gitconomy Research-郭晧"
date: 2026-02-28
tags:
- Project Caffeine
- Name Convention
- A-Z版本迭代
- Project Management
license: "CC BY-SA 4.0"
status: "Active"
---
-->
# Project Caffeine项目版本命名规则指南
## 1. 项目代号深度解析:为什么是 "Project Caffeine"
本科研助手智能体系统旨在解决海量信息检索、深度推理与结构化报告生成的痛点。我们将项目正式定名为 **Project Caffeine萃取者计划**,因为**咖啡的精细萃取过程,完美隐喻了本系统在编写科研报告时,大语言模型执行一步步深度推理与提纯的完整工作流**。
具体而言,咖啡萃取与科研推理存在以下高度的相似之处:
1. **生豆采摘与筛选 对应 “全网数据抓取”** 面对互联网上杂乱无章的海量原始数据(生豆),我们的“文献查询 MCP Server”就像经验丰富的采摘者自动连接 arXiv、PubMed 等外部 API 获取生豆,并过滤掉无效的视觉杂音。
2. **精细研磨 对应 “语义分块 (Semantic Chunking)”** 咖啡豆必须经过精确刻度的研磨才能释放风味。面对导致 Token 超载的超长篇 PDF 论文,系统在本地完成文本解析与“向量切割(语义分块)”,将长文本研磨成大模型能够完美吸收的知识颗粒。
3. **高压萃取 对应 “CoT 多步链式推理”** 意式咖啡机的高压水流穿过咖啡粉饼的瞬间即是核心风味物质被提取的过程。这等同于系统的“CoT 推理 MCP Server”强制大模型加载经典思维框架如 SCQA、SWOT 分析、思维链)。模型在极高的数据密度下进行交叉比对、事实冲突验证与逻辑推演,一步步滤去幻觉与水分。
4. **最终成杯 对应 “结构化科研报告”** 正如经过滤纸或把手滴落的纯净咖啡液,系统最终“萃取”出的是极具学术深度的研究洞察,并将其沉淀为带有 YAML 元数据的高质量 Markdown 文件无缝接入用户的本地知识管理图谱PKM
此外MCP 协议本身被定义为解决“N×M 整合难题”的标准接口(犹如科技界的 USB-C 接口)。在我们的项目命名中,**MCP 协议就是那个标准化的咖啡机手柄Portafilter**,它以统一的规格兼容了各种不同的外部数据(咖啡豆)与宿主模型(咖啡机)。
--------------------------------------------------------------------------------
## 2. 版本号命名规范A-Z 咖啡图谱
为了让系统的演进路径清晰且充满极客浪漫主义,**系统的版本号Release Version将严格采用以英文字母 A 到 Z 顺序开头的、与咖啡相关的英文单词进行命名**。每一个代号都将映射该版本核心突破的技术特征。
以下是规划中的 A-Z 核心版本迭代序列:
- **v1.x (A)**: Arabica (阿拉比卡 - 基础高品质咖啡树种)
- **v2.x (B)**: Bourbon (波旁 - 阿拉比卡经典优质变种)
- **v3.x (C)**: Cortado (可塔朵 - 浓缩与牛奶等比例混合的平衡咖啡)
- **v4.x (D)**: Drip (滴滤/手冲 - 依靠重力慢速萃取的方式)
- **v5.x (E)**: Espresso (意式浓缩 - 高压快速萃取的咖啡精华)
- **v6.x (F)**: Flat White (馥芮白/平白 - 奶泡更薄的浓缩奶咖)
- **v7.x (G)**: Geisha (瑰夏 - 具有极其复杂花果香的顶级咖啡品种)
- **v8.x (H)**: Honey Process (蜜处理 - 保留果胶层进行干燥的处理法)
- **v9.x (I)**: Ibrik (土耳其咖啡壶 - 最古老的不过滤煮沸法器具)
- **v10.x (J)**: Java (爪哇 - 印尼著名产区,也常作为咖啡的代名词)
- **v11.x (K)**: Kona (科纳 - 夏威夷顶级咖啡产区)
- **v12.x (L)**: Liberica (赖比瑞卡 - 世界第三大原生咖啡树种,风味独特)
- **v13.x (M)**: Macchiato (玛奇朵 - 意为“印记”,仅加少许奶泡的浓缩)
- **v14.x (N)**: Nitro (氮气冷萃咖啡 - 注入氮气带来绵密气泡口感的冷萃)
- **v15.x (O)**: Obata (奥巴塔 - 抗病虫害的优良阿拉比卡杂交品种)
- **v16.x (P)**: Peaberry (圆豆 - 单粒果实变异,风味更集中)
- **v17.x (Q)**: Q-Grader (咖啡品鉴师 - 国际认证的咖啡质量分级专家)
- **v18.x (R)**: Robusta (罗布斯塔 - 醇度厚重、抗虫害能力强的原生树种)
- **v19.x (S)**: Syphon (虹吸壶 - 利用水蒸气压力原理的冲煮器具)
- **v20.x (T)**: Typica (铁皮卡 - 最古老、最经典的阿拉比卡原生变种)
- **v21.x (U)**: Unwashed (日晒处理/非水洗 - 保留完整果实晒干的最古老处理法)
- **v22.x (V)**: V60 (V60滤杯 - 具有60度锥角的经典手冲器具)
- **v23.x (W)**: Washed (水洗处理 - 去除果肉与果胶后洗净风干的处理法)
- **v24.x (X)**: Xalapa (哈拉帕 - 墨西哥知名高海拔精品咖啡产区)
- **v25.x (Y)**: Yirgacheffe (耶加雪菲 - 以明亮柑橘花香闻名的埃塞俄比亚产区)
- **v26.x (Z)**: Zambia (赞比亚 - 极具潜力的非洲高海拔精品咖啡产地)
---
## 3. 分支与工程配置规范
为了将该命名规则严格落实到开发环节,团队需遵循以下工程配置标准:
1. **环境变量注入**:在工程的 `package.json` 及系统级 `.env` 文件中,需显式声明代号。
2. **Git 分支管理**:所有的 Release 发布分支需携带字母代号后缀,例如:`release/v1.0-arabica`, `release/v2.1-bourbon`
3. **架构图注规范**:根据《图形即代码设计规范指南》,所有的系统架构 SVG 拓扑图中,右上角及面包屑导航的注释必须带有当前版本的咖啡代号,例如 `Release: v1.0.0 (Arabica)`
---
## 许可声明
本文档采用 **知识共享署名--相同方式共享 4.0 国际许可协议 (CC BY--SA 4.0)** 进行许可,© 2025-2026 Gitconomy Research.

View File

@@ -0,0 +1,214 @@
<!--
---
title: Arabica Sprint3 系统架构设计说明
description: Project Caffeine 文献查询功能系统架构设设计,聚焦外围学术检索与双轨制数据落盘模块的实现。
type: Architecture Design
version: v1.0.0 (Arabica) - Sprint 3
file: arabica-sprint3-architecture-specification.md
author: Gitconomy Research-郭晧
date: 2026-03-11
tags:
- Project Caffeine
- MCP Server
- Sprint 3
- Literature Search
- PKM
- Node.js
license: CC BY-SA 4.0
status: Active
---
-->
# Arabica Sprint3 系统架构设计说明
## 1. Sprint3 设计概述
**Sprint 2** 完成了多维思维框架引擎的建设(基于 Prompt 原语并实现了对本地知识库Obsidian的基本操作。
**Sprint 3** 的核心目标是在现有本地知识管理能力之上,增强外部学术检索、实现大模型意图路由,并彻底解决大模型在工具调用中的幻觉与崩溃问题。本次迭代将系统从“被动提供 Prompt 的工具箱”升级为“能够听懂自然语言意图并主动调度工具的高级智能体Agent”。
**核心架构升级与关键功能**
* **意图驱动路由**:删除了向客户端 UI 暴露的 Prompt 原语,将底层收口至 `toolsController`。通过系统提示词定义 4 大意图(文献检索、框架分析、保存内容、本地笔记分析),让大模型自主路由,实现全自然语言对话交互。
* **学术文献检索接入**:集成 arXiv API实现 `search_arxiv` 工具,支持提取核心关键字进行检索,并自动格式化为大模型易读的 Markdown 文本。
* **高容错落盘机制**:重构 `save_note` 工具,通过将强类型校验放宽至 `z.any()` 结合底层 `JSON.stringify` 自动序列化,彻底解决了大模型生成长文本时的 JSON 转义崩溃问题。
* **抗死循环框架引擎**:将静态框架 JSON 的读取封装为后台私有工具 `fetch_framework_template`。通过底层剥离 JSON 外壳并强行注入“🛑立即停止调用工具”的指令,解决大模型读取复杂 JSON 后的死循环调用幻觉。
---
## 2. 功能实现步骤说明
典型用户查询与文献检索流程(参见图 3-1
```mermaid
sequenceDiagram
participant User as 用户
participant Client as Cherry Studio (MCP Client)
participant LLM as 大模型
participant Tools as Tools Controller (路由中心)
participant Arxiv as arXiv Service
participant Vault as Resource Service (本地知识库)
User->>Client: "用 SWOT 框架分析一下量子计算,并查查最新文献"
Client->>LLM: 传递自然语言意图
%% 意图 1查询文献
LLM->>Client: 识别意图1请求调用 search_arxiv
Client->>Tools: handleToolCall('search_arxiv', {query: 'quantum computing'})
Tools->>Arxiv: 发起 API 检索
Arxiv-->>Tools: 返回文献元数据
Tools-->>Client: 返回格式化文献列表
%% 意图 2框架分析
LLM->>Client: 识别意图2请求调用 fetch_framework_template
Client->>Tools: handleToolCall('fetch_framework_template', {framework_name: 'swot'})
Tools-->>Client: 返回纯文本 Prompt 并注入【强制刹车指令】
%% 大模型思考与输出
LLM->>Client: 停止调用工具,直接在聊天框输出分析报告
LLM->>Client: 报告末尾主动询问:"是否需要保存?"
%% 意图 3保存落盘
User->>Client: "是的,保存下来"
Client->>LLM: 传递确认指令
LLM->>Client: 识别意图3请求调用 save_note
Client->>Tools: handleToolCall('save_note', {filename: '...', content: <长文本或JSON对象>})
Tools->>Vault: 自动容错序列化并写入 Markdown
Vault-->>Tools: 写入成功
Tools-->>Client: 返回成功提示
LLM-->>User: 告知已保存至本地笔记
```
*图 3-1Sprint 3 核心工作流——文献检索与标准化落盘*
上述时序图展示了系统在 Sprint 3 架构下,如何通过“意图识别 + 底层容错”处理一次包含多个动作的复杂用户指令。具体步骤解析如下:
1. **多重意图触发**:用户在客户端输入包含复合诉求的自然语言指令(例如:“*用 SWOT 框架分析一下量子计算,并查查最新文献*”)。
2. **意图 1学术检索执行**:大模型首先解析出“查找文献”的意图,自主生成检索词并调用 `search_arxiv` 工具。`toolsController` 充当路由网关,将请求分发给 arXiv 服务,拉取真实的学术数据,并将晦涩的 XML 格式化为清晰的 Markdown 列表返回给大模型。
3. **意图 2框架读取与防呆拦截执行**大模型紧接着解析出“SWOT 分析”意图,调用后台私有工具 `fetch_framework_template`。此时,`toolsController` 读取本地 JSON 配置,**主动剥离 JSON 外壳**,提取出纯文本 Prompt并在头部强行拼接**“🛑立即停止调用任何工具”**的最高优先级刹车指令。
4. **大模型思考与安全输出**:大模型接收到“刹车指令”后,其工具调用的“冲动”被成功阻断(避免了因反复读取 JSON 导致的无限死循环幻觉)。大模型转为文本生成模式,直接在聊天框为用户输出详尽的分析报告。报告输出完毕后,大模型严格遵从系统纪律,主动向用户发起确认:“*是否需要保存?*”
5. **意图 3授权落盘与容错兜底执行**:用户回复确认(如:“*是的,保存下来*”)。大模型获取授权,识别出保存意图,调用 `save_note` 工具并将上万字的分析报告作为参数传入。即使大模型在传参时发生了引号转义错误或误传了 JSON 对象,`toolsController` 底层的 **z.any() 容错与自动序列化机制** 也会完美兜底,确保长文本安全无损地写入本地 Obsidian 知识库。
6. **链路闭环**:本地文件写入成功,系统通过大模型向用户反馈最终的成功状态,本次复杂交互圆满结束。
---
## 3. 系统组件架构设计
### 3.1 核心模块职责映射
| 模块 / 服务 | 职责说明 |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`app.ts`** | MCP Server 主入口。负责注册 `search_arxiv``fetch_framework_template``save_note``list_local_notes``read_local_note` 五大工具。**关键设计:**将 `user_input``payload` 参数类型设定为 `z.any()` 以规避底层 `AI_JSONParseError`。 |
| **`toolsController.ts`** | **大脑与防火墙**。接收工具请求。负责处理大模型的畸形传参(如对象序列化)、剥离 JSON 框架外壳、并在返回给大模型前拼接最高优先级的系统指令。 |
| **`arxivService.ts`** | 学术检索服务。负责与外部数据库通信,提取标题、摘要、链接等核心信息。 |
| **`resourceService.ts`** | 本地文件系统网关。负责安全的本地 Markdown 文件读写与列表扫描。 |
### 3.2 基于 Sprint 3 的项目目录结构
Sprint 3 移除了繁杂的独立 Controller将逻辑高度内聚形成了以 `toolsController.ts` 为核心的分发架构:
```
project-caffeine/
├── src/
│ ├── controllers/
│ │ ├── toolsController.ts # (修改)【核心路由】处理 5 大工具的分发、防呆与容错处理
│ │ └── resourcesController.ts # (修改)增加对 literature:// 资源的支持
│ ├── services/
│ │ ├── resourceService.ts # (修改)扩展支持 literature 协议的文件读写与扫描
│ │ └── arxivService.ts # (新增)【核心】封装 arXiv API 调用与 XML 解析
│ ├── models/
│ │ ├── schemas.ts # (修改)增加新工具的输入输出 Zod 校验
│ │ └── frameworks/ # (不变)思维框架定义 (静态 JSON 模板)
│ └── app.ts # 修改MCP Server 主入口,注册新增的 Tools 和 Resources
├── tsconfig.json # 修改TypeScript 编译与输出配置调整
└── package.json # (修改)新增第三方依赖(如 axios, zod, fast-xml-parser 等)
```
*表 3-1Sprint 3 新增/修改文件说明*
| **文件/目录** | **状态** | **功能说明** |
| ---------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `src/controllers/toolsController.ts` | **修改** | **【核心路由大脑】** 统一接管 5 大工具的分发。负责拦截大模型的畸形传参并执行兜底序列化JSON 容错);负责读取并剥离本地框架 JSON 外壳,注入“🛑立即停止调用工具”的强指令,防止大模型陷入死循环。 |
| `src/controllers/resourcesController.ts` | **修改** | **【资源端点】** 增加对 `literature://` 协议的支持,使大模型可以通过统一接口读取已落盘的学术文献 Markdown 文件。 |
| `src/services/arxivService.ts` | **新增** | **【学术网关】** 负责与外部 arXiv 数据库通信。封装 HTTP 请求,解析复杂的 XML 响应格式,提取标题、摘要、链接等核心元数据,并返回大模型友好的 Markdown 纯文本列表。 |
| `src/services/resourceService.ts` | **修改** | **【本地系统交互】** 扩展现有的文件操作能力。负责安全的本地 Markdown 文件读取与写入,并在底层确保路径防穿越安全。 |
| `src/models/schemas.ts` | **修改** | **【安全防线】** 增加新工具的强类型参数校验。针对大模型易错的落盘操作,将 `save_note` 的入参校验适度放宽至 `z.any()` 以配合 Controller 层的容错。 |
| `src/models/frameworks/` | **不变** | 存放 SWOT、SCQA 等多维思维框架的静态 JSON 配置模板。 |
| `src/app.ts` | **修改** | **【MCP 注册中心】** 实例化 MCP Server统一定义 `server.tool``server.resource`。显式声明 `type: "text" as const` 以解决 TypeScript 的强类型推断编译报错。 |
| `package.json` | **修改** | 新增处理 HTTP 请求和数据解析的核心依赖,配置 `npm run build``npm run test` 等关键工程化脚本。 |
### 3.3 系统模块架构图
*图 3-2Sprint 3 组件架构与数据流*
![Sprint3组件架构图](./../../../../docs/assets/images/arabica-srpint3-architecture-design.svg)
- **控制器层**`toolsController` 接收工具调用请求,分发给对应的服务;`resourcesController` 处理资源读取请求(可复用原有逻辑,仅需扩展协议类型)。
- **服务层**
- `literatureService`:调用 `apiClients` 并发请求 arXiv 和 Semantic Scholar对返回结果进行字段映射、去重、排序输出标准化的文献 JSON 数组。
- `storageService`:接收文献 JSON 和可选标签,调用 `yamlHelper` 生成 YAML Frontmatter组合 Markdown 正文(默认为文献摘要),写入文件,返回文件绝对路径。
- `resourceService`:扩展现有功能,支持读取 `literature://` 协议下的 Markdown 文件(实际仍从同一知识库目录读取)。
- **工具层**`apiClients` 封装 HTTP 客户端,处理速率限制和错误重试;`yamlHelper` 负责将 JSON 字段转换为符合 YAML 规范的字符串。
- **模型层**`literatureSchema.js` 定义文献的标准结构,`schemas.js` 集成新工具的 Zod 校验。
---
## 4. MCP 标准通信接口设计 (Tools)
### 4.1 意图 1学术文献查询 (`search_arxiv`)
* **描述**:通过核心关键字检索 arXiv 学术库。
* **输入 Schema**`{ query: z.string() }`
* **容错与输出**:在 Service 层自动过滤无效结果,将复杂的 XML 解析为带有 Markdown 排版的纯文本列表(标题、链接、摘要),防止大模型处理复杂结构时产生幻觉。
### 4.2 意图 2框架模板获取 (`fetch_framework_template`)
* **描述**:供大模型内部读取 SWOT、SCQA 等分析模板。不对外暴露 UI。
* **输入 Schema**`{ framework_name: z.string() }`
* **核心逻辑**
1. 读取本地静态 JSON 配置。
2. **剥离外壳**:提取出纯文本的 `system``user` prompt丢弃 JSON 结构。
3. **强制刹车**:在返回的开头拼接 `【🛑立即停止调用任何工具,请直接在聊天框输出...】`,阻断大模型的“工具调用死循环”。
### 4.3 意图 3内容落盘保存 (`save_note`)
* **描述**:将大模型生成的长文本或检索结果保存到本地 Obsidian 知识库。
* **输入 Schema**`{ filename: z.string(), content: z.any() }`
* **核心逻辑**:大模型极易在长文本转义时出错。系统允许传入 `z.any()`(即接收 JSON Object若发现传入的是对象而非字符串`toolsController` 会自动在底层执行 `JSON.stringify(content, null, 2)` 进行兜底序列化。
### 4.4 意图 4本地笔记分析 (`list_local_notes` & `read_local_note`)
* **描述**:继承 Sprint 2 的能力,大模型可主动列出本地笔记清单,并根据需要读取单篇或多篇笔记的内容以进行综合总结。
---
## 5. 安全、边界与质量保障
### 5.1 防越权落盘 (Authorization Boundary)
在系统级提示词System Prompt中设置了**“绝对红线”**:严禁大模型在没有主动询问并获得用户明确同意(如回复“保存”或“是”)的情况下,私自调用 `save_note` 工具。写盘动作必须由用户自然语言最终确认。
### 5.2 强类型收敛与 TS 错误阻断
针对 MCP SDK 要求 `content` 数组内的 `type` 必须为严格字面量 `"text"` 的限制,在 `app.ts` 中通过 `type: "text" as const` 进行显式断言,消除了编译期的类型推断模糊问题。
### 5.3 自动化测试覆盖
Sprint 3 引入了基于 Jest 的自动化测试套件:
* `arxivService.test.ts`:通过 Mock `global.fetch`,测试 XML 解析的健壮性和网络异常处理。
* `toolsController.test.ts`:深度覆盖工具的路由分发、`save_note` 的 JSON 畸形对象兜底序列化逻辑,以及 `fetch_framework_template` 是否成功注入了防死循环的“刹车指令”。
---
## 6. 总结
Sprint 3 在 Sprint 2 的本地知识管理基础上,通过新增文献检索与标准化存储模块,显著增强了系统的外部知识获取能力。我们通过将“决策权上交给大模型(意图识别)”**与**“容错权下放给底层代码参数宽容与刹车指令”相结合解决了大语言模型在多工具、长文本复杂场景下的状态机崩溃难题为后续更复杂的学术分析Sprint 4 & 5奠定了基础。
---
## 许可声明
本文档采用 **知识共享署名--相同方式共享 4.0 国际许可协议 (CC BY--SA 4.0)** 进行许可,© 2025-2026 Gitconomy Research.

View File

@@ -0,0 +1,291 @@
<!--
---
title: Arabica Sprint3 开发指南:意图驱动路由与高可用容错架构
description: 基于 Sprint 3 架构设计,指导开发者搭建全自然语言意图驱动的 MCP Server实现学术文献检索、本地知识库无缝对接并在底层构建抗死循环与强容错机制。
type: Development Guide
version: v1.0.0 (Arabica) - Sprint 3
file: arabica-sprint3-development-specification.md
author: Gitconomy Research-郭晧
date: 2026-03-11
tags:
- Project Caffeine
- MCP Server
- Sprint 3
- Intent-Driven
- Fault Tolerance
- arXiv API
- Node.js
license: CC BY-SA 4.0
status: Active
---
-->
# Arabica Sprint 3 开发指南
## 1. 模块概览与架构设计
Sprint 3 的核心是完成系统的“智能升维”:不再采用向客户端 UI 暴露的 Prompts 原语将大模型升级为拥有完整上下文的高级智能体Agent。通过意图识别系统自动路由至对应的工具Tools并在底层彻底解决大模型长文本转义崩溃与工具死循环问题。
整体架构精简为高度内聚的三层模型:
- **接入层**`app.ts` 负责初始化 MCP 服务器,注册 5 大核心工具,并放宽参数类型(`z.any()`)以承接畸形负载。
- **控制层(核心大脑)**`toolsController.ts` 充当路由网关与防火墙处理参数兜底序列化、JSON 框架外壳剥离及强指令注入。
- **服务层**`arxivService.ts`(学术网关)与 `resourceService.ts`(本地存储网关)处理具体的外部/内部 I/O。
<br>
下图展示了模块间的静态关系:
```mermaid
graph TD
A[app.ts 接入层] --> B[toolsController 核心路由]
A --> C[resource 资源注册]
B --> D[arxivService 学术检索]
B --> E[resourceService 本地文件操作]
B --> F[本地 frameworks JSON]
D --> G[arXiv API]
E --> H[本地 Markdown 知识库]
C --> E
````
---
## 2. 函数列表和调用逻辑关系
## 2.1 完成函数列表
以下是 Sprint 3 项目中所有显式定义的核心函数按文件分组。_(注Sprint 2 中的 `intentService.ts`、`promptsController.ts` 和 `promptService.ts` 已在本次迭代中删除)_
#### 1. `app.ts`(入口文件)
|**函数名**|**参数**|**描述**|**异步**|
|---|---|---|---|
|`start`|无|初始化 MCP 服务器,连接 STDIO 传输层|✅|
#### 2. `toolsController.ts`(工具控制器 & 路由大脑)
|**函数名**|**参数**|**描述**|**异步**|
|---|---|---|---|
|`handleToolCall`|`toolName: string, params: any`|统一入口,根据工具名分发到具体处理函数|✅|
|`handleSearchArxiv`|`params: any`|处理 `search_arxiv`,调用外部 API 并格式化结果|✅|
|`handleFetchFramework`|`params: any`|处理 `fetch_framework_template`,剥离 JSON 注入刹车指令|✅|
|`handleSaveNote`|`params: any`|处理 `save_note`,执行 JSON 对象容错与序列化|✅|
|`handleListLocalNotes`|无|处理 `list_local_notes`,返回本地笔记清单|✅|
|`handleReadLocalNote`|`params: any`|处理 `read_local_note`,读取具体笔记|✅|
#### 3. `arxivService.ts`(学术检索服务)
|**函数名**|**参数**|**描述**|**异步**|
|---|---|---|---|
|`searchArxiv`|`query: string, maxResults: number`|发起 HTTP 请求,解析 arXiv XML 返回标准化文献数组|✅|
#### 4. `resourceService.ts`(资源服务 - 维持 S2 核心)
|**函数名**|**参数**|**描述**|**异步**|
|---|---|---|---|
|`listObsidianNotes`|无|列出知识库目录下所有 `.md` 文件|✅|
|`readObsidianNote`|`filename: string`|读取指定笔记文件内容,含路径防穿越校验|✅|
|`saveNote`|`filename: string, content: string`|将内容安全写入本地知识库|✅|
## 2.2 整体函数调用关系图
代码段
```mermaid
graph TD
subgraph "接入层 app.ts"
A1["server.tool (search_arxiv)"] -->|分发| B1[handleToolCall]
A2["server.tool (fetch_framework)"] -->|分发| B1
A3["server.tool (save_note)"] -->|分发| B1
A4["server.resource (local-notes)"] -->|list/read 回调| C1[resourceService]
end
subgraph "控制器 (防火墙与路由) toolsController"
B1 -->|匹配意图 1| D1[handleSearchArxiv]
B1 -->|匹配意图 2| D2[handleFetchFramework]
B1 -->|匹配意图 3| D3[handleSaveNote]
end
subgraph "服务层"
D1 -->|发起网络请求| E1[arxivService.searchArxiv]
D2 -->|读取本地配置| E2[(frameworks/*.json)]
D3 -->|容错兜底后写盘| E3[resourceService.saveNote]
end
```
---
## 3. 核心业务代码实现
## 3.1 接入层 (`app.ts`)
#### 3.1.1 模块职责与设计目标
- **类型收敛与编译修复**:通过显式断言 `type: "text" as const`,解决 TypeScript 无法推断 MCP 严格字面量类型的编译报错。
- **宽容参数输入**:将容易引发大模型转义崩溃的长文本参数(如 `user_input``content`)从 `z.string()` 放宽至 `z.any()`,将容错权下放给 Controller 层。
#### 3.1.2 核心代码解析
```typescript
// ==========================================
// 意图 3: 保存笔记工具 (高容错版)
// ==========================================
server.tool(
'save_note',
{
filename: z.string().describe('生成的文件名,必须以 .md 结尾'),
// 💡 关键容错:放宽校验,接纳大模型误传的 JSON Object
content: z.any().describe('需要保存的完整内容文本支持传入JSON对象。')
},
async (args) => {
const result = await handleToolCall('save_note', args);
return {
isError: result.isError,
// 💡 显式字面量断言,修复 TS 报错
content: result.content.map((item: any) => ({ type: "text" as const, text: item.text }))
};
}
);
```
关键解析:
- 放宽类型校验 (z.any()):大语言模型在生成包含大量 Markdown 语法的长篇报告时,极易因未闭合的引号或换行符导致底层的 JSON RPC 解析崩溃AI_JSONParseError。将其由严格的 z.string() 放宽为 z.any(),使得系统允许大模型即使错误地传入了一个嵌套 JSON 对象,也能顺利进入 Controller 层。
- 字面量断言 (as const)MCP SDK 在强类型下强制要求返回的 content 数组内元素 type 必须为特指的字面量 "text",使用 as const 断言完美消除了 TypeScript 宽泛类型推断带来的编译时报错。
## 3.2 控制层 (`toolsController.ts`)
#### 3.2.1 模块职责与设计目标
控制层在 Sprint 3 中化身为**“智能体的大脑与护栏”**。
1. **抗死循环Anti-Loop**:处理 `fetch_framework_template` 时,拦截原始 JSON 结构,提取纯文本,并强行拼接业务指令阻断模型乱调工具。
2. **长文本容错Fallback Serialization**:处理 `save_note` 时,拦截畸形的 JSON 入参并自动序列化,确保平滑写盘。
#### 3.2.2 核心逻辑流程与代码
##### 3.2.2.1 `handleFetchFramework` (抗死循环剥离器)
1. **调用序列图**
```mermaid
sequenceDiagram
participant LLM as 大模型
participant Ctrl as toolsController
participant FS as 本地文件系统
LLM->>Ctrl: 请求工具 fetch_framework (SWOT)
Ctrl->>FS: 读取 swot.json
FS-->>Ctrl: 原始 JSON 字符串
Ctrl->>Ctrl: JSON.parse() 提取 system / user prompt
Ctrl->>Ctrl: 组装纯文本,并在头部拼接【🛑立即停止调用工具】
Ctrl-->>LLM: 返回纯文本强指令
LLM->>LLM: 遵守指令,停止工具调用,开始输出分析文本
```
2. **核心代码**
```typescript
async function handleFetchFramework(params: any) {
const name = params.framework_name?.toLowerCase();
// ... 校验框架有效性并读取 JSON 文件 ...
const parsedData = JSON.parse(fileContent);
// 提取出真正的 Prompt 文本,绝对不给大模型看 JSON 壳子
const sysMsg = parsedData.messages?.find((m: any) => m.role === 'system')?.content?.text || '';
const userMsg = parsedData.messages?.filter((m: any) => m.role === 'user').pop()?.content?.text || '';
// 组装极强约束的系统指令,强制大模型刹车
const returnText = `【🛑 最高优先级的底层执行指令 🛑】
你已经成功获取了 ${name.toUpperCase()} 框架的模板。现在,请你**立即停止调用任何工具**!绝对不要重复调用 fetch_framework_template
请直接在聊天框中为用户输出最终的分析报告。
【系统设定】:\n${sysMsg}\n
【分析结构要求】:\n${userMsg}\n
⚠️ 最终要求:报告输出完毕后,你必须向用户提问:“分析完毕,是否需要将此分析结果保存成笔记?”`;
return { content: [{ type: 'text', text: returnText }] };
}
```
关键解析:
- JSON 外壳剥离:如果直接将带有 {{variable}} 等变量占位符和嵌套结构的原始 JSON 返回给大模型,模型极大概率会产生“幻觉”,认为这是需要它进一步填空后重新发起请求的数据结构。提取内部纯文本能够大幅降低模型的理解成本。
- 强制刹车指令 (🛑 立即停止...):这是解决大模型陷入“工具无限死循环”的核心护城河。通过在返回文本首部注入最高优先级的底层指令,能够强制打断其内部的 Tool-Call 冲动,迫使其状态机切回文本生成模式。
##### 3.2.2.2 `handleSaveNote` (兜底序列化)
**核心代码**
```typescript
async function handleSaveNote(params: any) {
if (!params.filename || !params.content) {
throw new Error("保存失败:缺少 filename 或 content 参数");
}
// 💡 核心容错逻辑:帮大模型擦屁股处理奇葩格式
let contentToSave = '';
if (typeof params.content === 'string') {
contentToSave = params.content;
} else {
// 若大模型传了深度嵌套的对象,在底层帮它转成带缩进的字符串
contentToSave = JSON.stringify(params.content, null, 2);
}
const message = await saveNote(params.filename, contentToSave);
return { content: [{ type: 'text', text: message }] };
}
```
关键解析:
- 隐式类型转换(兜底机制):完美承接了接入层中放开的 z.any() 校验。当大模型发生幻觉,自作主张地将整篇分析报告包装成了类似 { "title": "...", "data": "..." } 的对象传入时,这段底层逻辑会悄无声息地帮它执行 JSON.stringify(..., null, 2),完成序列化。
- 业务平滑流转:通过这种代码层的宽容处理,避免了仅仅因为一个参数类型错误就让模型前序耗费大量 Token 思考和生成的长篇内容全部作废,极大提升了工程鲁棒性。
## 3.3 服务层 (`arxivService.ts`)
#### 3.3.1 模块职责
专注于与 arXiv 官方 API 通信。其核心使命是将原本极其复杂的学术 XML 标签(包含海量对大模型无用的元数据)**清洗、降维**为大模型能够轻松理解的极简 Markdown 列表。
#### 3.3.2 核心逻辑流
```mermaid
sequenceDiagram
participant Ctrl as toolsController
participant Svc as arxivService
participant API as arXiv API
Ctrl->>Svc: searchArxiv('quantum', 5)
Svc->>API: fetch('[http://export.arxiv.org/api/query?search_query=all:quantum&max_results=5](http://export.arxiv.org/api/query?search_query=all:quantum&max_results=5)')
API-->>Svc: XML 格式的学术数据
Svc->>Svc: 解析 XML提取 entry (title, id, summary)
Svc->>Svc: 清洗换行符,映射为统一的 Literature Object
Svc-->>Ctrl: 返回极简对象数组
```
---
## 4. 测试与质量保障 (Testing & QA)
Sprint 3 引入了 Jest 自动化测试套件,重点关注工具控制层的路由与容错能力:
1. **网络拦截与 Mock**:在 `arxivService.test.ts` 中,必须通过 `global.fetch = jest.fn()` 拦截真实网络请求,确保测试在离线环境下秒级通过。
2. **容错机制断言**:在 `toolsController.test.ts` 中,编写专门针对 `save_note` 传入 JSON Object 的断言Assertion验证 `resourceService.saveNote` 被调用时接收到的是否为序列化后的字符串。
3. **刹车指令探针**:测试 `fetch_framework_template` 时,断言返回的 `content.text` 必须 `toContain('立即停止调用任何工具')`,确保防死锁底座生效。
---
## 许可声明
本文档采用 **知识共享署名-相同方式共享 4.0 国际许可协议 (CC BY-SA 4.0)** 进行许可,© 2025-2026 Gitconomy Research.

View File

@@ -0,0 +1,319 @@
<!--
---
title: Arabica Sprint 3 代码单元测试样例
description: 为 Arabica Sprint 3 核心服务与控制器层提供标准 Jest 测试用例示例,涵盖文献检索、工具分发、容错保存及本地笔记操作场景。
type: System Development Guide
version: v1.0.0 (Arabica) - Sprint 3
file: arabica-sprint3- code-unit-test-specification.md
author: Gitconomy Research-郭晧
date: 2026-03-11
tags:
- Project Caffeine
- Sprint 3
- Jest
- Unit Testing
license: CC BY-SA 4.0
status: Active
---
-->
# Arabica Sprint 3 代码单元测试样例
## 1. 测试架构概览
在 Sprint 3 中系统彻底从“提示词Prompt驱动”转向了“工具Tool驱动”。我们的测试重点也应该从原来的 Prompts 转移到**核心服务层Services和工具控制层Controllers**。
当前 Sprint 3 真实需要进行单元测试的核心文件如下:
1. **`src/services/arxivService.ts`**:测试连接 Arxiv API 并解析 XML 的能力。
2. **`src/services/resourceService.ts`**:测试本地笔记的读取、扫描和保存能力。
3. **`src/controllers/toolsController.ts`**:【测试重点】测试五大核心工具(文献、框架、存笔记、查笔记、读笔记)的路由分发和容错机制。
---
## 2. 核心测试场景设计与示例
为了让您的 Jest 测试跑通,以下是针对 Sprint 3 真实文件的测试用例编写指南及代码示例。您可以在 `src/services/__test__/``src/controllers/__test__/` 目录下创建或修改对应的 `.test.ts` 文件。
### 场景 A测试文献检索服务 (`arxivService.test.ts`)
**目标**:验证 `searchArxiv` 函数是否能正确发起 HTTP 请求,并从 Arxiv 复杂的 XML 中提取出干净的标题和摘要。
```typescript
// 文件路径: src/services/__test__/arxivService.test.ts
import { searchArxiv } from '../arxivService';
// Mock 全局的 fetch 函数以防真实发起网络请求
global.fetch = jest.fn();
describe('Arxiv Service', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('应该成功解析 Arxiv 的 XML 并返回文献数组', async () => {
const mockXml = `
<feed>
<entry>
<id>http://arxiv.org/abs/1234.5678</id>
<title>Quantum Machine Learning</title>
<summary>This is a summary of QML.</summary>
</entry>
</feed>
`;
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue(mockXml)
});
const results = await searchArxiv('Quantum', 1);
expect(results).toHaveLength(1);
expect(results[0].title).toBe('Quantum Machine Learning');
expect(results[0].id).toBe('http://arxiv.org/abs/1234.5678');
});
it('当网络请求失败时应该抛出错误', async () => {
(global.fetch as jest.Mock).mockResolvedValue({
ok: false,
status: 500
});
await expect(searchArxiv('Error', 1)).rejects.toThrow('Arxiv API 响应错误: 500');
});
});
```
### 场景 B测试工具分发与框架剥离 (`toolsController.test.ts`)
**目标**:测试 `handleToolCall` 是否正确将参数分发给对应函数,**特别要测试 `fetch_framework_template` 是否成功拦截了原始 JSON 并加上了“刹车指令”**。
```typescript
// 文件路径: src/controllers/__test__/toolsController.test.ts
import { handleToolCall } from '../toolsController';
import * as fs from 'fs';
// Mock 文件系统,防止测试时去真实读取本地 JSON
jest.mock('fs');
describe('Tools Controller - Sprint 3', () => {
it('当调用未知工具时,应返回 isError 为 true', async () => {
const result = await handleToolCall('unknown_tool', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('未知工具');
});
it('fetch_framework_template 应该正确解析 JSON 并附带刹车指令', async () => {
// 伪造一个框架 JSON 文件内容
const mockFrameworkJson = JSON.stringify({
messages: [
{ role: 'system', content: { text: 'Mock系统指令' } },
{ role: 'user', content: { text: 'Mock用户模板' } }
]
});
(fs.readFileSync as jest.Mock).mockReturnValue(mockFrameworkJson);
const result = await handleToolCall('fetch_framework_template', { framework_name: 'swot' });
expect(result.isError).toBeUndefined(); // 不应报错
const outputText = result.content[0].text;
// 断言是否包含了防止死循环的强指令
expect(outputText).toContain('立即停止调用任何工具');
// 断言 JSON 壳子被剥离,提取出了纯文本
expect(outputText).toContain('Mock系统指令');
expect(outputText).toContain('Mock用户模板');
});
});
```
### 场景 C测试容错版保存工具 (`resourceService.test.ts` 相关逻辑)
**目标**:在 `toolsController` 层测试 `save_note`,验证当大模型错误地传入 JSON 对象而不是字符串时,系统能否自动将其序列化以避免崩溃。
```typescript
import { handleToolCall } from '../../controllers/toolsController'';
import * as resourceService from '../../services/resourceService';
import * as arxivService from '../../services/arxivService';
import * as fs from 'fs';
// ====================================================
// 1. 全局 Mock 外部依赖 (防止测试时发生真实读写和网络请求)
// ====================================================
jest.mock('fs');
jest.mock('../../services/resourceService', () => ({
saveNote: jest.fn().mockResolvedValue(''),
listObsidianNotes: jest.fn().mockResolvedValue(['test-note.md', 'ai-trend.md']),
readObsidianNote: jest.fn().mockResolvedValue('')
}));
jest.mock('../../services/arxivService', () => ({
searchArxiv: jest.fn().mockResolvedValue([
{ id: 'http://arxiv.org/abs/1234', title: 'Test Paper', summary: 'Mock Summary' }
])
}));
describe('Tools Controller - Sprint 3 (意图驱动与容错机制)', () => {
beforeEach(() => {
// 每次测试前清理 Mock 调用记录
jest.clearAllMocks();
});
// ====================================================
// 测试: 未知工具防呆
// ====================================================
it('当调用未知工具时,应返回 isError 为 true 的友好提示', async () => {
const result = await handleToolCall('unknown_tool', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('未知工具: unknown_tool');
});
// ====================================================
// 测试: 意图 3 - 保存笔记工具 (重点测试大模型容错能力)
// ====================================================
describe('Tool: save_note (容错版保存工具)', () => {
it('当大模型老老实实传入【纯文本字符串】时,应直接正常保存', async () => {
const result = await handleToolCall('save_note', { filename: 'output.md', content: '这是一段完美的Markdown文本' });
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toBe('保存成功');
expect(resourceService.saveNote).toHaveBeenCalledWith('output.md', '这是一段完美的Markdown文本');
});
it('🚨 容错测试:当大模型错误地传入【深度嵌套的 JSON 对象】时,应自动将其序列化,而不是崩溃', async () => {
// 模拟大模型发生幻觉,把整个分析对象当成了参数传进来
const mockJsonObject = {
title: "行业分析报告",
data: { trend: "上升", keywords: ["AI", "Quantum"] }
};
const result = await handleToolCall('save_note', { filename: 'report.md', content: mockJsonObject });
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toBe('保存成功');
// 期望底层调用保存时Controller 已经非常聪明地把 JSON 对象转换为了带缩进的字符串
const expectedString = JSON.stringify(mockJsonObject, null, 2);
expect(resourceService.saveNote).toHaveBeenCalledWith('report.md', expectedString);
});
it('当缺少必要参数时,应优雅地返回错误信息', async () => {
const result = await handleToolCall('save_note', { filename: 'error.md' }); // 故意漏掉 content
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('缺少 filename 或 content 参数');
});
});
// ====================================================
// 测试: 意图 2 - 获取思维框架模板
// ====================================================
describe('Tool: fetch_framework_template (抗死循环剥离器)', () => {
it('应该正确读取 JSON 文件,剥离外壳,并【强制附加刹车指令】防止大模型死循环', async () => {
// 伪造一个框架 JSON 文件内容
const mockFrameworkJson = JSON.stringify({
messages: [
{ role: 'system', content: { text: '我是系统架构师' } },
{ role: 'user', content: { text: '请按 SWOT 分析化验' } }
]
});
(fs.readFileSync as jest.Mock).mockReturnValue(mockFrameworkJson);
const result = await handleToolCall('fetch_framework_template', { framework_name: 'swot' });
expect(result.isError).toBeUndefined();
const outputText = result.content[0].text;
// 断言:必须包含这句护身符,阻止大模型不断重复调工具
expect(outputText).toContain('立即停止调用任何工具');
// 断言JSON 外壳已经被剥离,直接透出了内部指导文字
expect(outputText).toContain('我是系统架构师');
expect(outputText).toContain('请按 SWOT 分析化验');
});
});
// ====================================================
// 测试: 意图 1 - 文献检索
// ====================================================
describe('Tool: search_arxiv', () => {
it('应该正确调用服务并格式化返回带 Markdown 语法的文献列表', async () => {
const result = await handleToolCall('search_arxiv', { query: 'AI' });
expect(result.isError).toBeUndefined();
expect(arxivService.searchArxiv).toHaveBeenCalledWith('AI', 5);
const outputText = result.content[0].text;
expect(outputText).toContain('找到了关于 "AI" 的相关文献');
expect(outputText).toContain('Test Paper');
expect(outputText).toContain('http://arxiv.org/abs/1234');
});
});
// ====================================================
// 测试: 意图 4 - 本地笔记相关
// ====================================================
describe('Local Notes Tools', () => {
it('list_local_notes 应该正确返回笔记列表', async () => {
const result = await handleToolCall('list_local_notes', {});
expect(result.content[0].text).toContain('test-note.md');
expect(result.content[0].text).toContain('ai-trend.md');
});
it('read_local_note 应该正确读取具体笔记内容', async () => {
const result = await handleToolCall('read_local_note', { filename: 'test-note.md' });
expect(resourceService.readObsidianNote).toHaveBeenCalledWith('test-note.md');
expect(result.content[0].text).toContain('这是模拟的本地笔记内容');
});
});
});
```
---
## 3. 测试执行说明
在您配置好上述的测试文件后,请在终端使用项目中 `package.json` 原有配置的命令执行测试:
1. **执行基础测试**
```bash
npm run test
```
2. **执行并查看覆盖率Coverage**
```bash
npm run test:coverage
```
执行完成后,请检查终端输出的表格,重点关注 `arxivService.ts` 和 `toolsController.ts` 的 **Stmts (语句覆盖率)** 和 **Branch (分支覆盖率)** 是否达到预期(推荐 > 80%)。
3. **运行单个测试文件**
如果您只想跑 `toolsController.test.ts` 这一个文件,可以使用以下命令:
```bash
# 方式 A (推荐,使用 npx 直接调用 jest)
npx jest src/controllers/__test__/toolsController.test.ts
# 方式 B (使用 npm 脚本透传参数,注意中间的 `--`)
npm run test -- src/controllers/__test__/toolsController.test.ts
```
4. **监听模式**
在写测试或改 Bug 时,我们经常需要修改完代码后自动重新跑那个报错的测试文件。您可以加上 `--watch` 参数:
```bash
npx jest src/controllers/__test__/toolsController.test.ts --watch
```
这样您每次保存代码Jest 就会自动为您重新运行这个文件的测试,极大提升开发效率!
---
## 许可声明
本文档采用 **知识共享署名--相同方式共享 4.0 国际许可协议 (CC BY--SA 4.0)** 进行许可,© 2025-2026 Gitconomy Research。

View File

@@ -0,0 +1,177 @@
<!--
---
title: Arabica v0.0.3 MCP Inspector 测试指南
description: 指导开发人员使用官方 MCP Inspector 工具对 Project Caffeine v0.0.3 版本的核心原语Tools、Resources进行标准化的交互联调与测试。
type: Test Guide
version: v0.0.3 (Arabica)
file: arabica-v0.0.3-mcp-inspector-test-guide.md
author: Gitconomy Research-郭晧
date: 2026-03-10
tags:
- Project Caffeine
- MCP
- MCP Inspector
- 测试指南
license: CC BY-SA 4.0
status: Active
---
-->
# Project Caffeine Arabica v0.0.3 MCP Inspector 测试指南
## 1. 测试目的的概述
本文档旨在为开发人员提供一份标准化的操作指南,使用官方的 **MCP Inspector** 工具对 Project Caffeine (v0.0.3) 的底层协议接口进行独立测试。通过脱离具体的大模型客户端(如 Claude Desktop我们可以直接验证系统暴露的 Tools工具与 Resources资源原语的逻辑正确性、参数校验机制以及边缘异常处理能力。
## 2. 测试环境的准备
### 2.1 配置系统的环境变量
在启动测试之前,必须确保本地环境中的配置项已正确挂载。请在项目根目录的 `src/.env` 文件中检查以下关键路径,确保其指向本地真实的测试用例目录:
```bash
# 必须配置为本地真实存在的目录,且具备读写权限
OBSIDIAN_VAULT_PATH=/真实的/本地/测试目录/MyVault
LITERATURE_STORAGE_PATH=/真实的/本地/测试目录/MyVault
````
## 2.2 构建并启动检查器
Project Caffeine 的服务端运行依赖于编译后的 JavaScript 代码。请在终端中依次执行以下命令,完成项目的构建并通过 `npx` 启动官方的 MCP Inspector
Bash
```bash
# 1. 编译 TypeScript 源码
npm run build
# 2. 启动 MCP Inspector 并挂载编译后的入口文件
npx @modelcontextprotocol/inspector node dist/app.js
```
启动成功后,终端将输出一个本地调试地址(通常为 `http://localhost:5173`)。请在浏览器中打开该地址,进入 MCP Inspector 的可视化调试界面。
## 3. 核心原语的测试流程
在 MCP Inspector 的 Web 界面中,您将看到系统成功连接的标识 `Project-Caffeine-Arabica-Intent-Mode (v0.0.3)`。接下来,请依次对以下核心功能模块进行拨测。
## 3.1 执行文献检索工具测试 (Tools)
系统通过 `search_arxiv` 工具提供外部学术数据的抓取能力。
1. **定位工具**:在左侧面板选择 **Tools** 选项卡,在列表中找到并选中 `search_arxiv`
2. **输入参数**:在参数输入框中填入合法的 JSON 负载:
JSON
```bash
{
"query": "Large Language Models in Healthcare"
}
```
3. **发起调用**:点击 **Run Tool** 按钮。
4. **验证结果**
- **预期成功**:右侧响应区应返回状态 `isError: false`,且 `content` 数组中包含格式化好的文献标题、链接与摘要列表。
- **预期异常**:若输入空的 `query`,系统应触发 Zod 校验拦截,提示“需要检索的学术核心关键字”。
## 3.2 执行思维框架获取测试 (Tools)
系统将静态思维模板通过 `fetch_framework_template` 工具暴露给大模型。
1. **定位工具**:在 Tools 列表中选中 `fetch_framework_template`。
2. **输入参数**
JSON
```bash
{
"framework_name": "scqa"
}
```
3. **发起调用**:点击 **Run Tool** 按钮。
4. **验证结果**
- **预期成功**:响应文本中应包含“【🛑 最高优先级的底层执行指令 🛑】”以及 SCQA 框架的系统设定与结构要求。
- **边缘测试**:尝试输入未注册的框架(如 `abcd`),系统应返回软拦截提示:“未找到框架 abcd。系统支持的框架有: swot, scqa, pestle, 5w3h, 5whys”。
## 3.3 执行双轨制落盘与读取测试 (Tools)
验证系统对本地文件系统的 I/O 控制与路径安全防范。
1. **保存笔记测试 (`save_note`)**
- 构造参数:`{ "filename": "inspector_test.md", "content": "## 测试数据\n这是由 Inspector 写入的测试内容。" }`
- 执行后,检查本地 `OBSIDIAN_VAULT_PATH` 目录下是否成功生成了 `inspector_test.md` 文件。
2. **防穿越安全红线测试 (`save_note`)**
- 构造参数:`{ "filename": "../hacked.md", "content": "危险载荷" }`
- **验证预期**:系统必须返回 `isError: true`,并明确提示“无效的文件名,不允许访问上层目录”。
3. **列出笔记测试 (`list_local_notes`)**
- 直接运行该工具(无需参数)。系统应返回刚才创建的 `inspector_test.md`。
4. **读取笔记测试 (`read_local_note`)**
- 构造参数:`{ "filename": "inspector_test.md" }`。
- 系统应正确返回刚写入的 Markdown 文本内容。
## 3.4 验证本地静态资源挂载 (Resources)
Resources 原语用于将本地知识库作为被动只读资源暴露,以便大模型在对话上下文中作为附件引用。
1. **获取资源列表**
- 在左侧面板切换至 **Resources** 选项卡。
- 点击 **List Resources**,系统应返回由 `note://local/` 协议开头的资源列表,且列表中应包含 `note://local/inspector_test.md`。
2. **读取特定资源**
- 选中 `note://local/inspector_test.md`。
- 点击 **Read Resource** 按钮。
- **验证结果**:响应区应成功解码 URI 参数,并返回该文件的原始内容文本。
## 4. 常见问题与排错方案
## 4.1 解决连接超时的异常
如果 MCP Inspector 在启动后长时间处于 `Connecting...` 状态,请排查以下可能:
- **检查编译产物**:确认 `npm run build` 是否成功执行,`dist/app.js` 文件是否存在。
- **检查端口占用**MCP Inspector 默认使用 `5173` 端口,若端口被占用,可通过环境变量更换端口。
## 4.2 解决本地路径挂载失败的异常
当调用 `save_note` 抛出文件写入失败时:
- **检查环境配置**:确认 `.env` 文件中的 `OBSIDIAN_VAULT_PATH` 路径是否拼写正确,且必须为绝对路径。
- **检查目录权限**:确保当前运行 Node.js 进程的用户对该绝对路径具有完全读写权限。
---
## 许可声明
本文档采用 **知识共享署名--相同方式共享 4.0 国际许可协议 (CC BY--SA 4.0)** 进行许可,© 2025-2026 Gitconomy Research。

View File

@@ -18,7 +18,7 @@ license: "CC BY-SA 4.0"
-->
# Project Caffeine - Arabica Sprint1 QuickStart
## 1. Sprint1 版本`0.0.1`核心特性
## 1. Arabica v0.0.1(Sprint1)版本核心特性
- **零网络开销通信**:作为本地集成版本,本系统采用 `stdio` 传输协议,利用同一台机器上本地进程间的 stdin 和 stdout 管道进行直接通信,实现零网络传输开销。
@@ -97,7 +97,7 @@ Sprint1 采用纯本地 `stdio` 架构,推荐使用 VS Code 配合客户端进
- **名称**: `ProjectCaffeine-Sprint1`
- **Command**: `node`
- **Args**: `[--inspect=9230", /你的实际克隆路径/Project-Caffeine/projects/arabica/sprint1/dist/app.js]` _(⚠️ 必须为编译后的 js 文件绝对路径,且 `--inspect` 需放在首位以开启调试)_
- **Args**: `[--inspect=9229", /你的实际克隆路径/Project-Caffeine/projects/arabica/sprint1/dist/app.js]` _(⚠️ 必须为编译后的 js 文件绝对路径,且 `--inspect` 需放在首位以开启调试)_
或者通过导入json
@@ -112,7 +112,7 @@ Sprint1 采用纯本地 `stdio` 架构,推荐使用 VS Code 配合客户端进
"baseUrl": "",
"command": "node",
"args": [
"--inspect=9230",
"--inspect=9229",
"/home/wguo/Downloads/Project-Caffeine/projects/arabica/sprint2/dist/app.js"
],
"env": {}

View File

@@ -20,7 +20,7 @@ license: CC BY-SA 4.0
-->
# Project Caffeine - Arabica Sprint2 QuickStart
## 1. Sprint2 `0.0.2` 版本核心特性
## 1. Arabica v0.0.2(Spint2) 版本核心特性
- **多维思维框架引擎**:从单一工具扩展为完整的 Prompts 原语支持,内置 **SCQA、5Whys、5W3H、SWOT、PESTLE** 六大经典思维框架。每个框架均包含角色化系统提示、参数模板及 Few-Shot 示例,强制约束大模型的思考路径,输出高质量结构化分析。
@@ -126,7 +126,7 @@ const OBSIDIAN_VAULT_PATH = '/your/actual/vault/path';
2. 添加新的 Server 配置:
- **名称**`ProjectCaffeine-Sprint2`
- **Command**`node`
- **Args**`[--inspect=9230, /home/wguo/Downloads/Project-Caffeine/projects/arabica/sprint2/dist/app.js]`
- **Args**`[--inspect=9229, /home/wguo/Downloads/Project-Caffeine/projects/arabica/sprint2/dist/app.js]`
`--inspect` 端口可自定义,用于 VS Code 调试)
或者通过导入json
@@ -142,7 +142,7 @@ const OBSIDIAN_VAULT_PATH = '/your/actual/vault/path';
"baseUrl": "",
"command": "node",
"args": [
"--inspect=9230",
"--inspect=9229",
"/home/wguo/Downloads/Project-Caffeine/projects/arabica/sprint2/dist/app.js"
],
"env": {}

View File

@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "🍒 附加到 Cherry Studio (MCP 联调)",
"port": 9229,
"restart": true,
"skipFiles": ["<node_internals>/**"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
}
]
}

View File

@@ -0,0 +1,153 @@
<!--
---
title: "Project Caffeine - Arabica v0.0.3 QuickStart"
description: "提供 Arabica v0.0.3 (Sprint 3) 版本的快速上手指南,包括核心特性、环境配置、工具接口及客户端接入步骤。"
type: "README"
version: "v1.0.0 (Arabica) - Sprint 3"
file: "README.md"
author: "Gitconomy Research-郭晧"
date: "2026-03-11"
tags:
- "Project Caffeine"
- "Arabica"
- "Sprint 3"
- "QuickStart"
- "MCP"
- "Tools"
- "Cherry Studio"
license: "CC BY-SA 4.0"
status: "Active"
---
-->
# Project Caffeine - Arabica v0.0.3 QuickStart
# 1. Arabica v0.0.3Sprint3) 版本核心特性
`arabica v0.0.3` (Sprint 3) 版本的 5 大核心特性:
- **全自动意图驱动路由**:系统架构发生“智能升维”,全面废弃了向客户端 UI 暴露的 Prompts提示词原语。大模型现在通过完整的系统提示词上下文自主理解用户意图查询文献、调用框架分析、保存内容并在后台全自动调度 `search_arxiv``fetch_framework_template` 等专用工具,实现了**全自然语言驱动**的交互体验。
- **学术文献检索与解析**:新增 `search_arxiv` 原子工具,无缝集成 arXiv 官方 API。系统底层自动处理了极其复杂的 XML 解析与数据清洗工作,将杂乱的学术元数据(标题、摘要、链接)按需提取,并转化为大模型极度易读的 Markdown 纯文本列表,极大提升了系统的专业研究检索能力。
- **抗死循环框架引擎**:针对大模型在读取复杂 JSON 思维框架模板时极易陷入“工具疯狂调用死循环”的痛点,系统进行了底层防呆重构。将框架获取下放为私有后台工具 `fetch_framework_template`,在代码底层**主动剥离 JSON 外壳**,并强制向大模型注入“🛑 立即停止调用工具,直接输出报告”的最高优先级系统指令,确保大模型稳定输出分析。
- **高容错落盘兜底机制** 重构了 `save_note` 工具彻底解决了大模型在保存上万字长篇分析报告时频发的“JSON 转义崩溃AI_JSONParseError”问题。系统将工具的入参校验宽容度放宽至 `z.any()`,并在 Controller 底层通过 `JSON.stringify` 实施自动序列化兜底,确保无论大模型传参格式多混乱,都能 100% 安全平滑地写入本地 Obsidian 知识库。
- **坚固的越权防线与沙箱隔离** 在系统提示词与底层逻辑中设定了**“绝对红线”严禁大模型在未经用户明确自然语言授权如主动回复“保存”或“是的”私自执行任何写盘操作。同时系统完整保留并强化了本地知识库读写过程中的路径防穿越Path Traversal安全机制。
---
## 2. 克隆仓库与获取分支代码
**📌 重要说明**`arabica v0.0.3` 的迭代代码位于独立特性分支 `feature-arabica-sprint-1`。克隆时请指定该分支:
```bash
# 直接克隆指定的 feature 分支
git clone -b feature-arabica-sprint-1 https://gitlink.org.cn/Gitconomy/Project-Caffeine.git
# 进入 Sprint3 的独立工作目录
cd Project-Caffeine/projects/arabica/sprint3
# 安装 Node.js 项目依赖
npm install
```
---
## 3. 环境与路径配置
打开 `src/services/resourceService.ts`,将 `OBSIDIAN_VAULT_PATH` 变量修改为您本机真实的 Markdown 笔记文件夹绝对路径。
---
## 4. 编译与工作流说明
项目采用 TypeScript 开发,运行前必须将源码编译为 `.js` 文件。
```bash
npm build
```
基于 MCP 的 `stdio` 架构,本程序**不需要**手动启动独立后台服务。请根据使用场景选择工作流:
- **🟢 日常使用 (生产模式)**
您**不需要**在终端里输入任何启动命令。只需在 Cherry Studio 等客户端中配置好 `dist/app.js` 的绝对路径并开启开关,客户端会自动唤起并托管 Node 进程。
- **🛠️ 开发与调试 (实时监听模式)**
如需修改源码并配合 VS Code 断点调试,请保持终端运行:
代码保存后自动编译,在 Cherry Studio 中将 Server 开关关闭再打开,即可应用最新代码。
---
## 5. Sprint3 核心工具 (Tools) 接口
本服务端向支持 MCP 的大语言模型暴露以下 5 个高度内聚的核心工具:
_(注Sprint 2 中的 MCP Prompts 原语已在本次迭代中废弃,功能收口至 `fetch_framework_template` 工具中实现智能化调度。)_
---
## 6. 客户端接入联调(以 Cherry Studio 为例)
1. 打开 Cherry Studio进入 **设置 → MCP**
2. 添加新的 Server 配置:
- **名称**`ProjectCaffeine-Sprint3`
- **Command**`node`
- **Args**`[--inspect=9229, /您的绝对路径/Project-Caffeine/projects/arabica/sprint3/dist/app.js]`
`--inspect` 端口可自定义,用于 VS Code 调试)
**💡 [必看] 系统提示词配置指南**
为了让大模型完美理解并调度 Sprint 3 的意图引擎,**您必须**在 Cherry Studio 的“助手”设置中,将“系统提示词”设定为 Sprint 3 专属的**意图映射规则**。这是激活全自动路由的关键。
```markdown
# Role
你是一个基于意图识别的智能研究助手。你拥有多个专用工具,必须严格根据用户的自然语言意图来选择相应的动作,绝不能自作主张越权操作。
# Intent & Action Mapping (意图与动作映射)
## 意图 1查询 / 检索学术文献
- **触发条件**:用户明确表示想要检索、查询论文或文献。
- **执行动作**:调用 `search_arxiv` 进行检索 -> 清晰排版展示结果 -> 主动询问:“是否需要将以上文献搜索结果保存到本地?”
## 意图 2框架分析
- **触发条件**:用户要求使用思维框架(如 SWOT, SCQA, PESTLE 等)进行分析。
- **执行动作**:调用 `fetch_framework_template` 获取模板 -> 按模板输出详细分析 -> 主动询问:“分析完毕,是否需要将此分析结果保存成笔记?”
## 意图 3保存内容写盘
- **触发条件**:用户明确回复“保存”、“需要”等确认指令。
- **执行动作**:调用 `save_note` 工具 -> 传入刚刚生成的全文内容并保存 -> 告知成功。
## 意图 4本地笔记分析
- **触发条件**:用户要求查询、分析或结合本地的笔记内容。
- **执行动作**
1. 调用 `list_local_notes` 获取本地笔记列表。
2. 根据文件名,调用 `read_local_note` 读取匹配笔记的具体内容。
3. 基于读取的内容为用户输出综合分析报告。
4. 主动询问:“分析完毕,是否需要将此综合分析结果保存到本地?”
# 🚫 绝对红线 (核心禁忌)
1. **禁止越权保存**:绝不允许在没有主动询问并获得用户明确同意的情况下,私自调用 `save_note` 工具!
2. **禁止瞎编框架**:进行框架分析前,必须先调用工具获取框架模板。
```
---
## 7. Sprint3 文档
| **版本** | **开发目标** | **设计文档** | 开发文档 |
| ----------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| [`v0.0.3`](./README.md) | 构建文献查询 Server集成学术 API 实现基础外围检索能力,并开发双轨制数据落盘模块,将离散的 JSON 数据转换为带有标准 YAML 元数据的本地化 Markdown 文件。 | [Arabica Sprint3系统设计文档](./../../docs/design/arabica-sprint3-architecture-specification.md) | [Arabica Sprint3系统开发文档](./../../docs/design/arabica-sprint3-development-specification.md) |
---
## 许可声明
本文档采用 **知识共享署名-相同方式共享 4.0 国际许可协议 (CC BY-SA 4.0)** 进行许可,© 2025-2026 Gitconomy Research.

View File

@@ -0,0 +1,138 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Project Caffeine
* 单元测试: app.ts (接入层)
*/
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const promptsController_1 = require("../controllers/promptsController");
const toolsController_1 = require("../controllers/toolsController");
const resourceService = __importStar(require("../services/resourceService"));
// 1. 全局 Mock MCP SDK 和传输层,防止测试时真实启动服务器
jest.mock('@modelcontextprotocol/sdk/server/mcp.js');
jest.mock('@modelcontextprotocol/sdk/server/stdio.js');
jest.mock('../controllers/promptsController');
jest.mock('../controllers/toolsController');
jest.mock('../services/resourceService');
describe('app.ts 接入层逻辑测试', () => {
let mockServerInstance;
beforeEach(() => {
jest.clearAllMocks();
// 2. 模拟 McpServer 实例的行为
mockServerInstance = {
prompt: jest.fn(),
tool: jest.fn(),
resource: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
};
// 让 McpServer 构造函数返回这个 mock 实例
mcp_js_1.McpServer.mockImplementation(() => mockServerInstance);
// 拦截 console 输出以保持测试日志整洁
jest.spyOn(console, 'error').mockImplementation(() => { });
// 核心:使用 isolateModules 重新 require app.ts确保顶层注册代码被执行
jest.isolateModules(() => {
require('../app');
});
});
describe('Prompts 逻辑验证 (思维框架)', () => {
it('验证参数自动补全:漏传可选参数时应当补全为 "无"', async () => {
// 提取 scqa 注册时的回调函数 (第三个参数)
const scqaCall = mockServerInstance.prompt.mock.calls.find((c) => c[0] === 'scqa');
expect(scqaCall).toBeDefined();
const handler = scqaCall[2];
// 模拟控制器返回一个标准消息
promptsController_1.handlePromptsGet.mockResolvedValueOnce({
messages: [{ role: 'user', content: { text: 'test' } }]
});
// 触发回调:只传入必填的 situation不传 context 和 objective
await handler({ situation: '当前的背景' });
// 验证app.ts 应该调用控制器并补全了参数
expect(promptsController_1.handlePromptsGet).toHaveBeenCalledWith('scqa', {
situation: '当前的背景',
context: '无', // 自动补全逻辑
objective: '无' // 自动补全逻辑
});
});
it('验证消息角色过滤:应当只保留 user 和 assistant 消息', async () => {
const handler = mockServerInstance.prompt.mock.calls.find((c) => c[0] === 'scqa')[2];
// 模拟控制器返回包含 system 消息的序列
promptsController_1.handlePromptsGet.mockResolvedValueOnce({
messages: [
{ role: 'system', content: { type: 'text', text: '指令' } },
{ role: 'user', content: { type: 'text', text: '问题' } },
{ role: 'assistant', content: { type: 'text', text: '回答' } }
]
});
const result = await handler({ situation: 's' });
// 验证system 消息应该被 filter 过滤掉
expect(result.messages).toHaveLength(2);
expect(result.messages[0].role).toBe('user');
expect(result.messages[1].role).toBe('assistant');
// 验证元数据补全
expect(result.messages[0].content._meta).toBeUndefined(); // 应按代码逻辑设为 undefined
});
});
describe('Tools 逻辑验证 (执行工具)', () => {
it('应当将控制器返回的结果正确映射为 type: "text" 格式', async () => {
const toolCall = mockServerInstance.tool.mock.calls.find((c) => c[0] === 'save_note');
const handler = toolCall[2];
// 模拟控制器返回原始文本内容
toolsController_1.handleToolCall.mockResolvedValueOnce({
content: [{ text: '保存成功' }]
});
const result = await handler({ filename: 'a.md', content: 'c' });
// 验证app.ts 应该通过 .map 补全了 type: "text"
expect(result.content[0]).toEqual({
type: 'text',
text: '保存成功'
});
});
});
describe('Resources 逻辑验证 (静态资源)', () => {
it('验证资源读取:应当从 note:// URI 中解码文件名并调用服务层', async () => {
// 寻找 resource 注册时的读取处理函数 (第三个参数)
const readHandler = mockServerInstance.resource.mock.calls[0][2];
const mockUri = { href: 'note://local/%E6%B5%8B%E8%AF%95.md' }; // "测试.md" 的编码
const mockParams = { filename: '%E6%B5%8B%E8%AF%95.md' };
resourceService.readObsidianNote.mockResolvedValueOnce('# 笔记内容');
const result = await readHandler(mockUri, mockParams);
// 验证URI 是否被正确 decodeURIComponent 解码为 "测试.md"
expect(resourceService.readObsidianNote).toHaveBeenCalledWith('测试.md');
expect(result.contents[0].text).toBe('# 笔记内容');
});
});
});
//# sourceMappingURL=app.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"app.test.js","sourceRoot":"","sources":["../../src/__test__/app.test.ts"],"names":[],"mappings":"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"}

113
projects/arabica/src/sprint3/dist/app.js vendored Normal file
View File

@@ -0,0 +1,113 @@
"use strict";
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
Object.defineProperty(exports, "__esModule", { value: true });
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const zod_1 = require("zod");
const toolsController_1 = require("./controllers/toolsController");
const resourceService_1 = require("./services/resourceService");
const server = new mcp_js_1.McpServer({
name: 'Project-Caffeine-Arabica-Intent-Mode',
version: '0.0.3'
});
/**
* 注册文献搜索工具 (search_arxiv)
* 用于通过 arXiv API 检索相关学术文献。
*/
server.tool('search_arxiv', { query: zod_1.z.string().describe('需要检索的学术核心关键字') }, async (args) => {
const result = await (0, toolsController_1.handleToolCall)('search_arxiv', args);
return { isError: result.isError, content: result.content.map((item) => ({ type: "text", text: item.text })) };
});
/**
* 注册框架模板获取工具 (fetch_framework_template)
* 用于获取指定的经典思维框架模板(如 swot, scqa 等)。
*/
server.tool('fetch_framework_template', { framework_name: zod_1.z.string().describe('框架名称,必须是 swot, scqa, pestle, 5w3h, 5whys 之一') }, async (args) => {
const result = await (0, toolsController_1.handleToolCall)('fetch_framework_template', args);
return { isError: result.isError, content: result.content.map((item) => ({ type: "text", text: item.text })) };
});
/**
* 注册保存笔记工具 (save_note)
* 用于将分析结果或文献内容保存到本地知识库中。
*/
server.tool('save_note', {
filename: zod_1.z.string().describe('生成的文件名,必须以 .md 结尾'),
content: zod_1.z.any().describe('需要保存的完整内容文本支持传入JSON对象。')
}, async (args) => {
const result = await (0, toolsController_1.handleToolCall)('save_note', args);
return { isError: result.isError, content: result.content.map((item) => ({ type: "text", text: item.text })) };
});
/**
* 注册列出本地笔记工具 (list_local_notes)
* 用于扫描并返回本地知识库中所有 Markdown 格式的笔记文件名。
*/
server.tool('list_local_notes', {}, async (args) => {
const result = await (0, toolsController_1.handleToolCall)('list_local_notes', args);
return { isError: result.isError, content: result.content.map((item) => ({ type: "text", text: item.text })) };
});
/**
* 注册读取本地笔记工具 (read_local_note)
* 用于读取指定的本地 Markdown 笔记内容。
*/
server.tool('read_local_note', { filename: zod_1.z.string().describe('需要读取的笔记文件名,必须包含 .md 后缀') }, async (args) => {
const result = await (0, toolsController_1.handleToolCall)('read_local_note', args);
return { isError: result.isError, content: result.content.map((item) => ({ type: "text", text: item.text })) };
});
// ==========================================
// 注册资源Resources保持 UI 附件功能
// ==========================================
/**
* 注册本地笔记资源 (local-notes)
* 将本地 Obsidian 知识库作为静态资源暴露给客户端,支持列表展示与指定 URI 读取。
*/
server.resource("local-notes", new mcp_js_1.ResourceTemplate("note://local/{filename}", {
list: async () => {
try {
const notes = await (0, resourceService_1.listObsidianNotes)();
return {
resources: notes.map(filename => ({
name: filename,
uri: `note://local/${encodeURIComponent(filename)}`,
mimeType: "text/markdown",
description: `本地笔记: ${filename}`
}))
};
}
catch (error) {
return { resources: [] };
}
}
}), async (uri, { filename }) => {
try {
const filenameStr = Array.isArray(filename) ? filename[0] : filename;
const content = await (0, resourceService_1.readObsidianNote)(decodeURIComponent(filenameStr));
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: content }] };
}
catch (error) {
throw new Error(`读取笔记失败: ${error.message}`);
}
});
/**
* 启动 MCP Server
* 建立 stdio 传输层并开始监听客户端连接。
* * @returns {Promise<void>}
*/
async function start() {
console.error('正在启动 MCP Server (意图识别模式 + 本地笔记)...');
const transport = new stdio_js_1.StdioServerTransport();
await server.connect(transport);
console.error('MCP Server 已就绪');
}
start().catch((err) => {
console.error('服务器启动失败:', err);
process.exit(1);
});
//# sourceMappingURL=app.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAEH,oEAAsF;AACtF,wEAAiF;AACjF,6BAAwB;AACxB,mEAA+D;AAC/D,gEAAiF;AAEjF,MAAM,MAAM,GAAG,IAAI,kBAAS,CAAC;IAC3B,IAAI,EAAE,sCAAsC;IAC5C,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,IAAI,CACT,cAAc,EACd,EAAE,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,EAC9C,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,MAAM,GAAG,MAAM,IAAA,gCAAc,EAAC,cAAc,EAAE,IAAI,CAAC,CAAC;IAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;AAC/H,CAAC,CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,IAAI,CACT,0BAA0B,EAC1B,EAAE,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,6CAA6C,CAAC,EAAE,EACtF,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,MAAM,GAAG,MAAM,IAAA,gCAAc,EAAC,0BAA0B,EAAE,IAAI,CAAC,CAAC;IACtE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;AAC/H,CAAC,CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,IAAI,CACT,WAAW,EACX;IACE,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC;IAClD,OAAO,EAAE,OAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;CACrD,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,MAAM,GAAG,MAAM,IAAA,gCAAc,EAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IACvD,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;AAC/H,CAAC,CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,IAAI,CACT,kBAAkB,EAClB,EAAE,EACF,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,MAAM,GAAG,MAAM,IAAA,gCAAc,EAAC,kBAAkB,EAAE,IAAI,CAAC,CAAC;IAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;AAC/H,CAAC,CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,IAAI,CACT,iBAAiB,EACjB,EAAE,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,wBAAwB,CAAC,EAAE,EAC3D,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,MAAM,GAAG,MAAM,IAAA,gCAAc,EAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;IAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;AAC/H,CAAC,CACF,CAAC;AAEF,6CAA6C;AAC7C,4BAA4B;AAC5B,6CAA6C;AAE7C;;;GAGG;AACH,MAAM,CAAC,QAAQ,CACb,aAAa,EACb,IAAI,yBAAgB,CAAC,yBAAyB,EAAE;IAC9C,IAAI,EAAE,KAAK,IAAI,EAAE;QACf,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,IAAA,mCAAiB,GAAE,CAAC;YACxC,OAAO;gBACL,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;oBAChC,IAAI,EAAE,QAAQ;oBACd,GAAG,EAAE,gBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE;oBACnD,QAAQ,EAAE,eAAe;oBACzB,WAAW,EAAE,SAAS,QAAQ,EAAE;iBACjC,CAAC,CAAC;aACJ,CAAC;QACJ,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;QAC3B,CAAC;IACH,CAAC;CACF,CAAC,EACF,KAAK,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;IAC1B,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACrE,MAAM,OAAO,GAAG,MAAM,IAAA,kCAAgB,EAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC,CAAC;QACxE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IACrF,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,WAAW,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC,CACF,CAAC;AAEF;;;;GAIG;AACH,KAAK,UAAU,KAAK;IAClB,OAAO,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACpD,MAAM,SAAS,GAAG,IAAI,+BAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;AAClC,CAAC;AAED,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACpB,OAAO,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}

View File

@@ -0,0 +1,2 @@
"use strict";
//# sourceMappingURL=app.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"app.test.js","sourceRoot":"","sources":["../src/app.test.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,30 @@
"use strict";
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const dotenv_1 = __importDefault(require("dotenv"));
const path_1 = __importDefault(require("path"));
// 加载环境变量
dotenv_1.default.config();
// 从环境变量读取配置,提供合理的默认值
const config = {
obsidianVaultPath: process.env.OBSIDIAN_VAULT_PATH || path_1.default.join(process.env.HOME || '/home/wguo', 'Downloads/MyVault'),
literatureStoragePath: process.env.LITERATURE_STORAGE_PATH || process.env.OBSIDIAN_VAULT_PATH || path_1.default.join(process.env.HOME || '/home/wguo', 'Downloads/MyVault'),
};
// 输出调试信息到 stderr不影响 MCP 通信)
console.error('[Config] OBSIDIAN_VAULT_PATH from env:', process.env.OBSIDIAN_VAULT_PATH);
console.error('[Config] LITERATURE_STORAGE_PATH from env:', process.env.LITERATURE_STORAGE_PATH);
console.error('[Config] final obsidianVaultPath:', config.obsidianVaultPath);
console.error('[Config] final literatureStoragePath:', config.literatureStoragePath);
exports.default = config;
//# sourceMappingURL=config.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;AAEH,oDAA4B;AAC5B,gDAAwB;AAExB,SAAS;AACT,gBAAM,CAAC,MAAM,EAAE,CAAC;AAUhB,qBAAqB;AACrB,MAAM,MAAM,GAAW;IACrB,iBAAiB,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,cAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,YAAY,EAAE,mBAAmB,CAAC;IACtH,qBAAqB,EAAE,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,cAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,YAAY,EAAE,mBAAmB,CAAC;CAClK,CAAC;AAEF,6BAA6B;AAC7B,OAAO,CAAC,KAAK,CAAC,wCAAwC,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;AACzF,OAAO,CAAC,KAAK,CAAC,4CAA4C,EAAE,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;AACjG,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAC7E,OAAO,CAAC,KAAK,CAAC,uCAAuC,EAAE,MAAM,CAAC,qBAAqB,CAAC,CAAC;AAErF,kBAAe,MAAM,CAAC"}

View File

@@ -0,0 +1,153 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
// src/controllers/__tests__/promptsController.test.ts
const promptsController_1 = require("../promptsController");
const promptService = __importStar(require("../../services/promptService"));
// 模拟 promptService 模块
jest.mock('../../services/promptService');
// 类型断言,便于 TypeScript 识别模拟函数的类型
const mockedPromptService = jest.mocked(promptService);
describe('promptsController', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('handlePromptsList', () => {
it('应该正确将服务层返回的框架列表映射为 prompts/list 响应格式', async () => {
// 模拟服务层返回的数据
const mockFrameworks = [
{
name: 'scqa',
description: 'SCQA 框架',
parameters: [
{ name: 'situation', description: '情境', required: true },
{ name: 'complication', description: '复杂性', required: true }
],
template: '...', // 这些字段不会被 list 使用,但模拟时可省略
systemPrompt: '...'
},
{
name: 'swot',
description: 'SWOT 分析',
parameters: [
{ name: 'entity', description: '分析对象', required: true }
],
template: '...'
}
];
mockedPromptService.listFrameworks.mockResolvedValue(mockFrameworks);
const result = await (0, promptsController_1.handlePromptsList)();
// 验证返回结构符合 MCP prompts/list 规范
expect(result).toHaveProperty('prompts');
expect(Array.isArray(result.prompts)).toBe(true);
expect(result.prompts).toHaveLength(2);
// 验证每个 prompt 的字段映射正确
expect(result.prompts[0]).toEqual({
name: 'scqa',
description: 'SCQA 框架',
arguments: [
{ name: 'situation', description: '情境', required: true },
{ name: 'complication', description: '复杂性', required: true }
]
});
expect(result.prompts[1]).toEqual({
name: 'swot',
description: 'SWOT 分析',
arguments: [
{ name: 'entity', description: '分析对象', required: true }
]
});
// 验证服务层被调用一次
expect(mockedPromptService.listFrameworks).toHaveBeenCalledTimes(1);
});
it('当服务层返回空数组时,应该返回空列表', async () => {
mockedPromptService.listFrameworks.mockResolvedValue([]);
const result = await (0, promptsController_1.handlePromptsList)();
expect(result).toEqual({ prompts: [] });
expect(mockedPromptService.listFrameworks).toHaveBeenCalledTimes(1);
});
it('当服务层抛出异常时,应该传播异常(由调用方处理)', async () => {
const error = new Error('加载框架失败');
mockedPromptService.listFrameworks.mockRejectedValue(error);
await expect((0, promptsController_1.handlePromptsList)()).rejects.toThrow('加载框架失败');
});
});
describe('handlePromptsGet', () => {
const mockName = 'scqa';
const mockArgs = { situation: '市场增长放缓' };
it('当服务层成功返回时,应该返回包含 description 和 messages 的响应', async () => {
const mockServiceResult = {
messages: [
{
role: 'system',
content: { type: "text", text: '你是一名战略顾问' }
},
{
role: 'user',
content: { type: "text", text: '请分析情境:市场增长放缓' }
}
]
};
mockedPromptService.getFramework.mockResolvedValue(mockServiceResult);
const result = await (0, promptsController_1.handlePromptsGet)(mockName, mockArgs);
// 验证返回结构
expect(result).toHaveProperty('description', '框架: scqa');
expect(result).toHaveProperty('messages');
expect(result.messages).toEqual(mockServiceResult.messages);
// 验证服务层被正确调用
expect(mockedPromptService.getFramework).toHaveBeenCalledWith(mockName, mockArgs);
});
it('当 args 为 undefined 时,应传入空对象给服务层', async () => {
const mockServiceResult = { messages: [] };
mockedPromptService.getFramework.mockResolvedValue(mockServiceResult);
// 调用时第二个参数为 undefined
await (0, promptsController_1.handlePromptsGet)(mockName, undefined);
expect(mockedPromptService.getFramework).toHaveBeenCalledWith(mockName, {} // 预期被转换为空对象
);
});
it('当服务层抛出错误时,应重新抛出错误,并包装错误信息', async () => {
const serviceError = new Error('框架不存在');
mockedPromptService.getFramework.mockRejectedValue(serviceError);
// 验证抛出的错误包含原始信息
await expect((0, promptsController_1.handlePromptsGet)(mockName, mockArgs)).rejects.toThrow('获取框架失败: 框架不存在');
});
it('当服务层抛出非 Error 类型时,也应正确处理', async () => {
// 模拟服务层抛出一个字符串
mockedPromptService.getFramework.mockRejectedValue('some string error');
await expect((0, promptsController_1.handlePromptsGet)(mockName, mockArgs)).rejects.toThrow('获取框架失败: some string error');
});
});
});
//# sourceMappingURL=promptsController.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"promptsController.test.js","sourceRoot":"","sources":["../../../src/controllers/__test__/promptsController.test.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,153 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
// src/controllers/__tests__/promptsController.test.ts
const promptsController_1 = require("../promptsController");
const promptService = __importStar(require("../../services/promptService"));
// 模拟 promptService 模块
jest.mock('../../services/promptService');
// 类型断言,便于 TypeScript 识别模拟函数的类型
const mockedPromptService = jest.mocked(promptService);
describe('promptsController', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('handlePromptsList', () => {
it('应该正确将服务层返回的框架列表映射为 prompts/list 响应格式', async () => {
// 模拟服务层返回的数据
const mockFrameworks = [
{
name: 'scqa',
description: 'SCQA 框架',
parameters: [
{ name: 'situation', description: '情境', required: true },
{ name: 'complication', description: '复杂性', required: true }
],
template: '...', // 这些字段不会被 list 使用,但模拟时可省略
systemPrompt: '...'
},
{
name: 'swot',
description: 'SWOT 分析',
parameters: [
{ name: 'entity', description: '分析对象', required: true }
],
template: '...'
}
];
mockedPromptService.listFrameworks.mockResolvedValue(mockFrameworks);
const result = await (0, promptsController_1.handlePromptsList)();
// 验证返回结构符合 MCP prompts/list 规范
expect(result).toHaveProperty('prompts');
expect(Array.isArray(result.prompts)).toBe(true);
expect(result.prompts).toHaveLength(2);
// 验证每个 prompt 的字段映射正确
expect(result.prompts[0]).toEqual({
name: 'scqa',
description: 'SCQA 框架',
arguments: [
{ name: 'situation', description: '情境', required: true },
{ name: 'complication', description: '复杂性', required: true }
]
});
expect(result.prompts[1]).toEqual({
name: 'swot',
description: 'SWOT 分析',
arguments: [
{ name: 'entity', description: '分析对象', required: true }
]
});
// 验证服务层被调用一次
expect(mockedPromptService.listFrameworks).toHaveBeenCalledTimes(1);
});
it('当服务层返回空数组时,应该返回空列表', async () => {
mockedPromptService.listFrameworks.mockResolvedValue([]);
const result = await (0, promptsController_1.handlePromptsList)();
expect(result).toEqual({ prompts: [] });
expect(mockedPromptService.listFrameworks).toHaveBeenCalledTimes(1);
});
it('当服务层抛出异常时,应该传播异常(由调用方处理)', async () => {
const error = new Error('加载框架失败');
mockedPromptService.listFrameworks.mockRejectedValue(error);
await expect((0, promptsController_1.handlePromptsList)()).rejects.toThrow('加载框架失败');
});
});
describe('handlePromptsGet', () => {
const mockName = 'scqa';
const mockArgs = { situation: '市场增长放缓' };
it('当服务层成功返回时,应该返回包含 description 和 messages 的响应', async () => {
const mockServiceResult = {
messages: [
{
role: 'system',
content: { type: "text", text: '你是一名战略顾问' }
},
{
role: 'user',
content: { type: "text", text: '请分析情境:市场增长放缓' }
}
]
};
mockedPromptService.getFramework.mockResolvedValue(mockServiceResult);
const result = await (0, promptsController_1.handlePromptsGet)(mockName, mockArgs);
// 验证返回结构
expect(result).toHaveProperty('description', '框架: scqa');
expect(result).toHaveProperty('messages');
expect(result.messages).toEqual(mockServiceResult.messages);
// 验证服务层被正确调用
expect(mockedPromptService.getFramework).toHaveBeenCalledWith(mockName, mockArgs);
});
it('当 args 为 undefined 时,应传入空对象给服务层', async () => {
const mockServiceResult = { messages: [] };
mockedPromptService.getFramework.mockResolvedValue(mockServiceResult);
// 调用时第二个参数为 undefined
await (0, promptsController_1.handlePromptsGet)(mockName, undefined);
expect(mockedPromptService.getFramework).toHaveBeenCalledWith(mockName, {} // 预期被转换为空对象
);
});
it('当服务层抛出错误时,应重新抛出错误,并包装错误信息', async () => {
const serviceError = new Error('框架不存在');
mockedPromptService.getFramework.mockRejectedValue(serviceError);
// 验证抛出的错误包含原始信息
await expect((0, promptsController_1.handlePromptsGet)(mockName, mockArgs)).rejects.toThrow('获取框架失败: 框架不存在');
});
it('当服务层抛出非 Error 类型时,也应正确处理', async () => {
// 模拟服务层抛出一个字符串
mockedPromptService.getFramework.mockRejectedValue('some string error');
await expect((0, promptsController_1.handlePromptsGet)(mockName, mockArgs)).rejects.toThrow('获取框架失败: some string error');
});
});
});
//# sourceMappingURL=promtsController.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"promtsController.test.js","sourceRoot":"","sources":["../../../src/controllers/__test__/promtsController.test.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,215 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
// src/controllers/__tests__/toolsController.test.ts
const toolsController_1 = require("../toolsController");
const intentService = __importStar(require("../../services/intentService"));
const resourceService = __importStar(require("../../services/resourceService"));
// 模拟所有依赖的服务
jest.mock('../../services/intentService');
jest.mock('../../services/resourceService');
// 类型断言,方便 TypeScript 识别模拟函数
const mockedIntentService = jest.mocked(intentService);
const mockedResourceService = jest.mocked(resourceService);
describe('toolsController', () => {
beforeEach(() => {
jest.clearAllMocks();
// 可选:模拟 console.error 避免测试输出干扰
jest.spyOn(console, 'error').mockImplementation(() => { });
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('handleToolCall', () => {
describe('未知工具名称', () => {
it('应返回包含未知工具错误信息的响应', async () => {
const result = await (0, toolsController_1.handleToolCall)('unknown_tool', {});
expect(result).toEqual({
content: [{ type: 'text', text: '未知工具: unknown_tool' }],
isError: true
});
});
});
describe('generate_search_queries 工具', () => {
const toolName = 'generate_search_queries';
const validParams = { query: '新能源汽车电池回收' };
it('参数校验失败时,应返回参数错误响应', async () => {
// 传入缺少 query 的参数
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
// 确保服务层未被调用
expect(mockedIntentService.generateSearchQueries).not.toHaveBeenCalled();
});
it('服务层成功执行时,应返回检索词数组的 JSON 字符串', async () => {
const mockQueries = ['动力电池回收', '锂离子再生', '环保法规'];
mockedIntentService.generateSearchQueries.mockReturnValue(mockQueries);
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBeUndefined(); // 成功时不设置 isError
expect(result.content).toEqual([
{ type: 'text', text: JSON.stringify(mockQueries, null, 2) }
]);
expect(mockedIntentService.generateSearchQueries).toHaveBeenCalledWith(validParams.query);
});
it('服务层抛出异常时,应返回执行失败响应', async () => {
const error = new Error('意图拆解失败');
mockedIntentService.generateSearchQueries.mockImplementation(() => {
throw error;
});
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('执行失败: 意图拆解失败');
});
it('当服务层抛出非 Error 类型时,应返回包含字符串化信息的失败响应', async () => {
// 模拟抛出一个字符串
mockedIntentService.generateSearchQueries.mockImplementation(() => {
throw 'some string error';
});
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('执行失败: some string error');
});
});
describe('list_local_notes 工具', () => {
const toolName = 'list_local_notes';
it('服务层成功返回笔记列表时,应返回文本列表', async () => {
const mockNotes = ['note1.md', 'note2.md', 'note3.md'];
mockedResourceService.listObsidianNotes.mockResolvedValue(mockNotes);
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toContain('找到了以下笔记');
expect(result.content[0].text).toContain('note1.md\nnote2.md\nnote3.md');
});
it('服务层返回空列表时,应返回“未找到笔记”', async () => {
mockedResourceService.listObsidianNotes.mockResolvedValue([]);
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.content[0].text).toBe('未找到笔记。');
});
it('服务层抛出异常时,应返回执行失败响应', async () => {
const error = new Error('目录不可读');
mockedResourceService.listObsidianNotes.mockRejectedValue(error);
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('执行失败: 目录不可读');
});
it('服务层抛出非 Error 类型时,应返回字符串化信息的失败响应', async () => {
mockedResourceService.listObsidianNotes.mockRejectedValue('权限错误');
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('执行失败: 权限错误');
});
});
describe('read_local_note 工具', () => {
const toolName = 'read_local_note';
const validParams = { filename: 'test.md' };
it('参数校验失败(缺少 filename应返回参数错误响应', async () => {
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
expect(result.content[0].text).toContain('文件名不能为空');
expect(mockedResourceService.readObsidianNote).not.toHaveBeenCalled();
});
it('参数校验失败filename 不含 .md应返回参数错误响应', async () => {
const result = await (0, toolsController_1.handleToolCall)(toolName, { filename: 'test.txt' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
expect(result.content[0].text).toContain('文件名必须包含 .md 后缀');
});
it('服务层成功读取笔记时,应返回笔记内容', async () => {
const mockContent = '# 测试笔记\n这是一段内容。';
mockedResourceService.readObsidianNote.mockResolvedValue(mockContent);
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBeUndefined();
expect(result.content).toEqual([{ type: 'text', text: mockContent }]);
expect(mockedResourceService.readObsidianNote).toHaveBeenCalledWith(validParams.filename);
});
it('服务层抛出异常时,应返回读取失败响应', async () => {
const error = new Error('文件不存在');
mockedResourceService.readObsidianNote.mockRejectedValue(error);
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('读取失败: 文件不存在');
});
it('服务层抛出非 Error 类型时,应返回字符串化信息的失败响应', async () => {
mockedResourceService.readObsidianNote.mockRejectedValue('权限不足');
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('读取失败: 权限不足');
});
});
describe('save_note 工具', () => {
const toolName = 'save_note';
const validParams = { filename: 'new.md', content: '# 新笔记' };
it('参数校验失败(缺少 filename应返回参数错误响应', async () => {
const result = await (0, toolsController_1.handleToolCall)(toolName, { content: '# 内容' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
expect(result.content[0].text).toContain('filename'); // 错误信息应提及 filename
expect(mockedResourceService.saveNote).not.toHaveBeenCalled();
});
it('参数校验失败filename 不含 .md应返回参数错误响应', async () => {
const result = await (0, toolsController_1.handleToolCall)(toolName, {
filename: 'new.txt',
content: '# 内容'
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
expect(result.content[0].text).toContain('.md');
});
it('服务层成功保存笔记时,应返回成功信息', async () => {
const mockMessage = '笔记已保存至: /path/new.md';
mockedResourceService.saveNote.mockResolvedValue(mockMessage);
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBeUndefined();
expect(result.content).toEqual([{ type: 'text', text: mockMessage }]);
expect(mockedResourceService.saveNote).toHaveBeenCalledWith(validParams.filename, validParams.content);
});
it('服务层抛出异常时,应返回保存失败响应', async () => {
const error = new Error('磁盘空间不足');
mockedResourceService.saveNote.mockRejectedValue(error);
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('保存失败: 磁盘空间不足');
});
it('服务层抛出非 Error 类型时,应返回字符串化信息的失败响应', async () => {
mockedResourceService.saveNote.mockRejectedValue('写入错误');
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('保存失败: 写入错误');
});
});
});
});
//# sourceMappingURL=toolsControll.test.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,65 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const toolsController_1 = require("../toolsController");
const fs = __importStar(require("fs"));
// Mock 文件系统,防止测试时去真实读取本地 JSON
jest.mock('fs');
describe('Tools Controller - Sprint 3', () => {
it('当调用未知工具时,应返回 isError 为 true', async () => {
const result = await (0, toolsController_1.handleToolCall)('unknown_tool', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('未知工具');
});
it('fetch_framework_template 应该正确解析 JSON 并附带刹车指令', async () => {
// 伪造一个框架 JSON 文件内容
const mockFrameworkJson = JSON.stringify({
messages: [
{ role: 'system', content: { text: 'Mock系统指令' } },
{ role: 'user', content: { text: 'Mock用户模板' } }
]
});
fs.readFileSync.mockReturnValue(mockFrameworkJson);
const result = await (0, toolsController_1.handleToolCall)('fetch_framework_template', { framework_name: 'swot' });
expect(result.isError).toBeUndefined(); // 不应报错
const outputText = result.content[0].text;
// 断言是否包含了防止死循环的强指令
expect(outputText).toContain('立即停止调用任何工具');
// 断言 JSON 壳子被剥离,提取出了纯文本
expect(outputText).toContain('Mock系统指令');
expect(outputText).toContain('Mock用户模板');
});
});
//# sourceMappingURL=toolsController.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"toolsController.test.js","sourceRoot":"","sources":["../../../src/controllers/__test__/toolsController.test.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wDAAoD;AACpD,uCAAyB;AAGzB,8BAA8B;AAC9B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEhB,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAE3C,EAAE,CAAC,6BAA6B,EAAE,KAAK,IAAI,EAAE;QAC3C,MAAM,MAAM,GAAG,MAAM,IAAA,gCAAc,EAAC,cAAc,EAAE,EAAE,CAAC,CAAC;QACxD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,mBAAmB;QACnB,MAAM,iBAAiB,GAAG,IAAI,CAAC,SAAS,CAAC;YACvC,QAAQ,EAAE;gBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE;gBACjD,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE;aAChD;SACF,CAAC,CAAC;QACF,EAAE,CAAC,YAA0B,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAC;QAElE,MAAM,MAAM,GAAG,MAAM,IAAA,gCAAc,EAAC,0BAA0B,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC,CAAC;QAE5F,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,OAAO;QAC/C,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAE1C,mBAAmB;QACnB,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QAC3C,wBAAwB;QACxB,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACzC,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}

View File

@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handlePromptsList = handlePromptsList;
exports.handlePromptsGet = handlePromptsGet;
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
const promptService_1 = require("../services/promptService");
/**
* 处理 prompts/list 请求,返回所有可用思维框架的元信息列表。
*
* @returns {Promise<{ prompts: Array<{ name: string, description: string, arguments: Array<{ name: string, description: string, required: boolean }> }> }>}
* 符合 MCP 规范的 prompts 列表
*/
async function handlePromptsList() {
const frameworks = await (0, promptService_1.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 请求,获取指定思维框架的完整提示词消息序列。
*
* @param {string} name - 框架名称
* @param {Record<string, string>} args - 用户传入的参数
* @returns {Promise<{ description: string, messages: Array<{ role: string, content: { type: string, text: string } }> }>}
* 包含描述信息和消息序列的响应对象
* @throws {Error} 当框架不存在或服务层组装失败时抛出错误
*/
async function handlePromptsGet(name, args) {
try {
const result = await (0, promptService_1.getFramework)(name, args || {});
return {
description: `框架: ${name}`,
messages: result.messages
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`获取框架失败: ${errorMessage}`);
}
}
//# sourceMappingURL=promptsController.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"promptsController.js","sourceRoot":"","sources":["../../src/controllers/promptsController.ts"],"names":[],"mappings":";;AAiBA,8CAaC;AAWD,4CAWC;AApDD;;;;;;;;GAQG;AACH,6DAAyE;AAEzE;;;;;GAKG;AACI,KAAK,UAAU,iBAAiB;IACrC,MAAM,UAAU,GAAG,MAAM,IAAA,8BAAc,GAAE,CAAC;IAC1C,OAAO;QACL,OAAO,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC5B,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,SAAS,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;gBAChC,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,QAAQ,EAAE,CAAC,CAAC,QAAQ;aACrB,CAAC,CAAC;SACJ,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACI,KAAK,UAAU,gBAAgB,CAAC,IAAY,EAAE,IAA4B;IAC/E,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAA,4BAAY,EAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;QACpD,OAAO;YACL,WAAW,EAAE,OAAO,IAAI,EAAE;YAC1B,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAC;IACJ,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,MAAM,YAAY,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,WAAW,YAAY,EAAE,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC"}

View File

@@ -0,0 +1,112 @@
"use strict";
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.listObsidianNotes = listObsidianNotes;
exports.readObsidianNote = readObsidianNote;
exports.saveNote = saveNote;
exports.searchNotes = searchNotes;
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
/** 本地知识库根目录(请修改为实际路径) */
const OBSIDIAN_VAULT_PATH = '/home/wguo/Downloads/MyVault';
/**
* 列出知识库中所有 Markdown 笔记的文件名。
* @returns {Promise<string[]>} 笔记文件名数组
*/
async function listObsidianNotes() {
try {
const files = await promises_1.default.readdir(OBSIDIAN_VAULT_PATH);
return files.filter(file => file.toLowerCase().endsWith('.md'));
}
catch (error) {
console.error(`[Project Caffeine] 无法读取知识库目录: ${error.message}`);
return [];
}
}
/**
* 读取指定笔记文件的完整内容。
* @param filename - 笔记文件名(必须包含 .md 后缀)
* @returns {Promise<string>} 文件内容
* @throws 安全警告或文件读取错误
*/
async function readObsidianNote(filename) {
const targetPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH, filename);
const safeVaultPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH);
if (!targetPath.startsWith(safeVaultPath)) {
throw new Error(`安全警告:越权访问拦截!禁止读取目录外的文件: ${filename}`);
}
try {
return await promises_1.default.readFile(targetPath, 'utf-8');
}
catch (error) {
throw new Error(`无法读取笔记 [${filename}]: 文件可能不存在或无权限。`);
}
}
/**
* 保存笔记到本地知识库。
* @param filename - 笔记文件名(必须以 .md 结尾)
* @param content - 笔记内容Markdown 格式)
* @returns {Promise<string>} 保存成功的提示信息
* @throws 参数校验失败或文件写入错误
*/
async function saveNote(filename, content) {
if (!filename.endsWith('.md')) {
throw new Error('文件名必须以 .md 结尾');
}
const fullPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH, filename);
const relative = path_1.default.relative(OBSIDIAN_VAULT_PATH, fullPath);
if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
throw new Error('无效的文件名,不允许访问上层目录');
}
await promises_1.default.mkdir(OBSIDIAN_VAULT_PATH, { recursive: true });
await promises_1.default.writeFile(fullPath, content, 'utf-8');
return `笔记已保存至: ${fullPath}`;
}
/**
* 在笔记内容中搜索关键词(忽略大小写)。
* @param keywords - 关键词数组
* @returns 匹配的笔记列表每个包含文件名、URI 和上下文片段
*/
async function searchNotes(keywords) {
const files = await listObsidianNotes();
const results = [];
for (const file of files) {
try {
const content = await readObsidianNote(file);
const lowerContent = content.toLowerCase();
if (keywords.every(kw => lowerContent.includes(kw.toLowerCase()))) {
const snippets = [];
for (const kw of keywords) {
const index = lowerContent.indexOf(kw.toLowerCase());
if (index !== -1) {
const start = Math.max(0, index - 30);
const end = Math.min(content.length, index + kw.length + 30);
let snippet = content.substring(start, end);
if (start > 0)
snippet = '...' + snippet;
if (end < content.length)
snippet = snippet + '...';
snippets.push(snippet);
}
}
results.push({ filename: file, snippets });
}
}
catch (e) {
// 忽略读取失败的文件
}
}
return results.slice(0, 5);
}
//# sourceMappingURL=resourcesController.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"resourcesController.js","sourceRoot":"","sources":["../../src/controllers/resourcesController.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,189 @@
"use strict";
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleToolCall = handleToolCall;
const resourceService_1 = require("../services/resourceService");
const arxivService_1 = require("../services/arxivService");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/**
* MCP Tools 调用的统一路由分发处理器。
* * @param {string} toolName - 客户端调用的工具名称
* @param {any} params - 工具请求的参数对象
* @returns {Promise<{ content: Array<{ type: string, text: string }>, isError?: boolean }>} MCP 规范的返回结构
*/
async function handleToolCall(toolName, params) {
switch (toolName) {
case 'search_arxiv':
return await handleSearchArxiv(params);
case 'save_note':
return await handleSaveNote(params);
case 'fetch_framework_template':
return await handleFetchFramework(params);
case 'list_local_notes':
return await handleListLocalNotes();
case 'read_local_note':
return await handleReadLocalNote(params);
default:
return { content: [{ type: 'text', text: `未知工具: ${toolName}` }], isError: true };
}
}
/**
* 处理文献查询工具请求
* * @param {any} params - 包含 query (检索关键字) 的参数对象
* @returns {Promise<{ content: Array<{ type: string, text: string }>, isError?: boolean }>}
*/
async function handleSearchArxiv(params) {
try {
const query = params.query;
if (!query)
throw new Error("缺少检索关键字");
const results = await (0, arxivService_1.searchArxiv)(query, 5);
if (results.length === 0) {
return { content: [{ type: 'text', text: `未检索到关于 "${query}" 的相关文献。` }] };
}
let textOutput = `找到了关于 "${query}" 的相关文献:\n\n`;
results.forEach((r, i) => {
textOutput += `### [${i + 1}] ${r.title}\n**链接:** ${r.id}\n**摘要:** ${r.summary}\n\n`;
});
return { content: [{ type: 'text', text: textOutput }] };
}
catch (error) {
return { content: [{ type: 'text', text: `执行检索失败: ${error.message}` }], isError: true };
}
}
/**
* 处理获取思维框架工具请求
* * @param {any} params - 包含 framework_name 的参数对象
* @returns {Promise<{ content: Array<{ type: string, text: string }>, isError?: boolean }>}
*/
async function handleFetchFramework(params) {
try {
const name = params.framework_name?.toLowerCase();
const valid = ['swot', 'scqa', 'pestle', '5w3h', '5whys'];
if (!valid.includes(name)) {
return { content: [{ type: 'text', text: `未找到框架 ${name}。系统支持的框架有: ${valid.join(', ')}` }] };
}
const filePath = path.join(__dirname, `../models/frameworks/${name}.json`);
const fileContent = fs.readFileSync(filePath, 'utf-8');
const parsedData = JSON.parse(fileContent);
const sysMsg = parsedData.messages?.find((m) => m.role === 'system')?.content?.text || '你是一个专业分析师。';
const userMsg = parsedData.messages?.filter((m) => m.role === 'user').pop()?.content?.text || '请进行详细分析。';
const returnText = `【🛑 最高优先级的底层执行指令 🛑】
你已经成功获取了 ${name.toUpperCase()} 框架的模板。现在,请你**立即停止调用任何工具**!绝对不要重复调用 fetch_framework_template
请直接在聊天框中为用户输出最终的分析报告。
【你的角色与设定】:
${sysMsg}
【你的分析结构要求】:
根据用户刚才提出的问题,严格按照以下结构输出内容(请自行理解并填充结构中的关键变量):
${userMsg}
⚠️ 最终要求:报告输出完毕后,你必须在末尾向用户提问:“分析完毕,是否需要将此分析结果保存成笔记?”`;
return { content: [{ type: 'text', text: returnText }] };
}
catch (error) {
return { content: [{ type: 'text', text: `读取框架失败: ${error.message}` }], isError: true };
}
}
/**
* 处理保存笔记工具请求
* * @param {any} params - 包含 filename 和 content 的参数对象
* @returns {Promise<{ content: Array<{ type: string, text: string }>, isError?: boolean }>}
*/
async function handleSaveNote(params) {
try {
if (!params.filename || !params.content) {
throw new Error("保存失败:缺少 filename 或 content 参数");
}
let contentToSave = '';
if (typeof params.content === 'string') {
contentToSave = params.content;
}
else {
contentToSave = JSON.stringify(params.content, null, 2);
}
const message = await (0, resourceService_1.saveNote)(params.filename, contentToSave);
return { content: [{ type: 'text', text: message }] };
}
catch (error) {
return { content: [{ type: 'text', text: `保存失败: ${error.message}` }], isError: true };
}
}
/**
* 处理列出本地笔记请求
* * @returns {Promise<{ content: Array<{ type: string, text: string }>, isError?: boolean }>}
*/
async function handleListLocalNotes() {
try {
const notes = await (0, resourceService_1.listObsidianNotes)();
if (notes.length === 0) {
return { content: [{ type: 'text', text: '当前本地知识库中未找到笔记。' }] };
}
return { content: [{ type: 'text', text: `找到了以下本地笔记文件:\n${notes.join('\n')}` }] };
}
catch (error) {
return { content: [{ type: 'text', text: `扫描本地笔记失败: ${error.message}` }], isError: true };
}
}
/**
* 处理读取本地笔记请求
* * @param {any} params - 包含 filename (笔记名称) 的参数对象
* @returns {Promise<{ content: Array<{ type: string, text: string }>, isError?: boolean }>}
*/
async function handleReadLocalNote(params) {
try {
const filename = params.filename;
if (!filename)
throw new Error("缺少 filename 参数");
const content = await (0, resourceService_1.readObsidianNote)(filename);
return { content: [{ type: 'text', text: `笔记 ${filename} 的内容如下:\n\n${content}` }] };
}
catch (error) {
return { content: [{ type: 'text', text: `读取笔记失败: ${error.message}` }], isError: true };
}
}
//# sourceMappingURL=toolsController.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"toolsController.js","sourceRoot":"","sources":["../../src/controllers/toolsController.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,192 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleWorkflowRouter = handleWorkflowRouter;
/**
* Project Caffeine - Workflow Router
* 采用硬编码状态机,彻底杜绝大模型跳步幻觉
*/
const resourceService_1 = require("../services/resourceService");
const intentService_1 = require("../services/intentService");
const arxivService_1 = require("../services/arxivService");
const promptService_1 = require("../services/promptService");
// 内存级会话状态(单用户本地服务适用)
let sessionState = {};
async function handleWorkflowRouter(currentStep, userInput, payload) {
const input = userInput.trim();
const inputUpper = input.toUpperCase();
try {
switch (currentStep) {
// ==========================================
// 状态 0: 初始意图确认
// ==========================================
case 'init':
sessionState = {}; // 重置状态
return {
next_step: 'wait_for_path',
message_to_user: "👋 欢迎使用 Project Caffeine 智能向导。\n\n请问您希望进行哪种操作直接回复字母即可\n**A.** 思维框架分析 (如 SWOT, SCQA 等)\n**B.** 学术文献查询 (Arxiv 检索)\n**C.** 本地笔记分析"
};
// ==========================================
// 状态 1: 分支选择
// ==========================================
case 'wait_for_path':
if (inputUpper === 'A' || inputUpper.includes('A')) {
return {
next_step: 'wait_for_framework',
message_to_user: "🧠 您选择了 **思维框架分析**。\n请问您希望使用哪个框架\n支持SWOT, SCQA, PESTLE, 5W3H, 5WHYS"
};
}
else if (inputUpper === 'B' || inputUpper.includes('B')) {
return {
next_step: 'wait_for_keyword',
message_to_user: "📚 您选择了 **学术文献查询**。\n请简要描述您想研究的领域或直接输入核心关键词"
};
}
else if (inputUpper === 'C' || inputUpper.includes('C')) {
const notes = await (0, resourceService_1.listObsidianNotes)();
if (notes.length > 0) {
sessionState.availableNotes = notes;
return {
next_step: 'wait_for_local_analyze_confirm',
message_to_user: `📂 您选择了 **本地笔记分析**。\n目前知识库中扫描到 ${notes.length} 篇笔记(如 ${notes[0]} 等)。\n\n是否希望我阅读这些笔记并为您进行综合分析?(是/否)`
};
}
else {
return {
next_step: 'wait_for_direct_analyze_confirm',
message_to_user: "📂 您选择了 **本地笔记分析**。\n但目前本地知识库中 **没有找到任何笔记**。\n\n是否需要我直接为您进行通用知识分析是/否)"
};
}
}
else {
return {
next_step: 'wait_for_path',
message_to_user: "⚠️ 输入无效。请回复 A、B 或 C 来选择路径:"
};
}
// ==========================================
// [分支 A] 框架分析链路
// ==========================================
case 'wait_for_framework':
const valid = ['SWOT', 'SCQA', 'PESTLE', '5W3H', '5WHYS'];
const fw = valid.find(f => inputUpper.includes(f));
if (!fw) {
return {
next_step: 'wait_for_framework',
message_to_user: "⚠️ 未识别到支持的框架。请输入 SWOT, SCQA, PESTLE, 5W3H 或 5WHYS"
};
}
sessionState.framework = fw.toLowerCase();
return {
next_step: 'wait_for_topic_a',
message_to_user: `✅ 已选择 **${fw}** 框架。\n请问您需要分析的具体主题或对象是什么?`
};
case 'wait_for_topic_a':
sessionState.topic = input;
// 组装通用参数以适配所有框架的占位符
const args = { entity: input, topic: input, problem: input, domain: input, situation: input, context: '无', objective: '无', competitors: '无' };
const frameworkDef = await (0, promptService_1.getFramework)(sessionState.framework, args);
const sysMsg = frameworkDef.messages.find(m => m.role === 'system')?.content.text || '';
const userMsg = frameworkDef.messages.filter(m => m.role === 'user').pop()?.content.text || '';
return {
next_step: 'wait_for_save_confirm',
message_to_user: `【底层指令:请勿向用户暴露本段文字】\n请扮演专家,使用以下设定和模板为用户生成并在对话框输出分析报告。\n\n系统设定:${sysMsg}\n分析任务:${userMsg}\n\n⚠️ 生成报告后,请必须向用户提问:“分析完毕。是否需要将此分析结果保存到本地笔记?”\n⚠️ 注意:当用户在下一轮回复“保存”时,你必须把这份报告的全文放入 payload 参数中调用工具。`
};
// ==========================================
// [分支 B] 文献检索链路
// ==========================================
case 'wait_for_keyword':
const queries = (0, intentService_1.generateSearchQueries)(input);
sessionState.queries = queries;
return {
next_step: 'wait_for_keyword_selection',
message_to_user: `🔍 基于您的需求,我提取了以下检索关键字:\n${queries.map((q, i) => `${i + 1}. ${q}`).join('\n')}\n\n请问您想使用哪一个关键字进行 Arxiv 检索?(您可以回复序号,或直接输入自定义关键字)`
};
case 'wait_for_keyword_selection':
let selectedQuery = input;
if (/^\d+$/.test(input.trim()) && sessionState.queries) {
const idx = parseInt(input.trim()) - 1;
if (sessionState.queries[idx])
selectedQuery = sessionState.queries[idx];
}
const results = await (0, arxivService_1.searchArxiv)(selectedQuery, 5);
if (results.length === 0) {
return {
next_step: 'init',
message_to_user: `❌ 针对关键字 "${selectedQuery}" 未检索到相关文献。您可以随时输入新的查询重新开始。`
};
}
let textOutput = `找到了关于 "${selectedQuery}" 的相关文献:\n\n`;
results.forEach((r, i) => {
textOutput += `### [${i + 1}] ${r.title}\n**链接:** ${r.id}\n**摘要:** ${r.summary}\n\n`;
});
return {
next_step: 'wait_for_save_confirm',
message_to_user: `【底层指令:请勿暴露此标记】\n请将以下检索结果原封不动地展示给用户:\n\n${textOutput}\n\n⚠️ 展示完毕后,请向用户提问:“是否需要将以上文献记录保存到本地笔记?”\n⚠️ 注意:当用户回复“保存”时,把这批检索结果放入 payload 传回。`
};
// ==========================================
// [分支 C] 本地笔记分析链路
// ==========================================
case 'wait_for_local_analyze_confirm':
if (input.includes('是') || input.toLowerCase() === 'y' || input.includes('保存') || input.includes('分析')) {
const notesToRead = sessionState.availableNotes.slice(0, 3); // 限制前3篇防超载
let combinedContent = '';
for (const note of notesToRead) {
combinedContent += `\n\n--- 笔记:${note} ---\n${await (0, resourceService_1.readObsidianNote)(note)}`;
}
return {
next_step: 'wait_for_save_confirm',
message_to_user: `【底层指令】请基于以下本地笔记内容,提取核心观点并为用户生成并在对话框输出一份综合分析摘要:\n${combinedContent}\n\n⚠️ 生成摘要后提问:“分析完毕。是否需要将此综合分析保存到本地?”并在下次获批后将摘要写入 payload。`
};
}
else {
return { next_step: 'init', message_to_user: "好的,已取消分析。本次交互结束。" };
}
case 'wait_for_direct_analyze_confirm':
if (input.includes('是') || input.toLowerCase() === 'y' || input.includes('分析')) {
return {
next_step: 'wait_for_save_confirm',
message_to_user: `【底层指令】请根据用户最初的需求,直接运用你的专业知识为用户提供一份详细的分析报告。\n⚠️ 报告生成后提问:“分析完毕。是否保存结果?”并在下次获批后将结果写入 payload。`
};
}
else {
return { next_step: 'init', message_to_user: "好的,已取消分析。本次交互结束。" };
}
// ==========================================
// 公共保存出口 (收口防呆机制)
// ==========================================
case 'wait_for_save_confirm':
if (input.includes('是') || input.includes('保存') || input.toLowerCase() === 'y' || input.includes('好')) {
if (!payload || payload.trim() === '') {
return {
next_step: 'init',
message_to_user: "⚠️ 系统内部错误:模型未能回传需要保存的内容文本。保存失败,交互结束。"
};
}
const filename = `Caffeine_Output_${new Date().getTime()}.md`;
const saveResult = await (0, resourceService_1.saveNote)(filename, payload);
return {
next_step: 'init',
message_to_user: `🎉 操作成功!\n${saveResult}\n\n本次交互已圆满结束。您可以随时发送任何新消息来唤起菜单。`
};
}
else {
return {
next_step: 'init',
message_to_user: "好的,结果将不会被保存。本次交互结束。"
};
}
default:
return {
next_step: 'init',
message_to_user: "状态机已重置。请输入任何文字重新唤起菜单。"
};
}
}
catch (error) {
return {
next_step: 'init',
message_to_user: `⚠️ 系统运行遇到错误:${error.message}\n状态机已重置,请重新开始。`
};
}
}
//# sourceMappingURL=workflowController.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,50 @@
{
"name": "5w3h",
"description": "5W3H 分析法:从 What、Why、Who、When、Where、How、How much、How feel 八个维度全面结构化拆解问题,并基于全景拆解得出行动洞察。",
"persona": "structured_thinker",
"parameters": [
{
"name": "topic",
"description": "需要分析的核心主题、事件或问题",
"required": true
},
{
"name": "context",
"description": "该主题发生的特定背景、前置条件或行业环境(可选)",
"required": false
},
{
"name": "objective",
"description": "期望通过此次分析达成的核心目标(可选)",
"required": false
}
],
"template": "请使用 5W3H 框架分析以下主题:\n核心主题 (Topic){{topic}}\n背景信息 (Context){{context}}\n分析目标 (Objective){{objective}}\n\n请依次从 What(是什么)、Why(为什么)、Who(谁)、When(何时)、Where(何地)、How(如何做)、How much(多少)、How feel(感受如何) 八个维度展开结构化深度拆解,并在最后给出可执行的行动洞察。\n\n输出格式要求请严格按以下 JSON 格式输出你的分析结果,不要包含任何 Markdown 代码块标记(如反引号包裹的 json直接输出纯 JSON 文本:\n{\n \"what\": \"描述问题的本质、核心现象或定义的准确陈述\",\n \"why\": \"分析现象产生的原因、背景动因或深层目的\",\n \"who\": \"识别涉及的核心主体、执行者、利益相关者或受众角色\",\n \"when\": \"明确事件发生的时间节点、生命周期、频率或关键时间窗\",\n \"where\": \"界定问题发生的空间范围、物理地点、系统模块或应用场景\",\n \"how\": \"探讨解决问题的途径、落地机制、实施过程或运转模式\",\n \"howMuch\": \"量化指标,如影响规模、涉及成本、人员数量、收益等核心数据\",\n \"howFeel\": \"描述各利益相关方的直观感受、情绪反馈、满意度或体验评价\",\n \"actionableInsights\": [\n \"基于上述8个维度的全景拆解提炼出的第1条具体、可落地的行动指导建议\",\n \"第2条可落地的行动指导建议\"\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\"推导此拆解结构的关键前提假设\"]\n }\n}",
"systemPrompt": "你是一位擅长运用 5W3H 框架进行结构化思考的分析师,能够全面且精确地覆盖事件的八个维度,确保分析的系统性,并能由繁入简提炼行动洞察。你必须严格遵守 JSON 格式输出规范,确保数据可直接被下游程序结构化解析。",
"examples": [
{
"input": {
"topic": "公司推行远程办公政策",
"context": "疫情后复工,但员工普遍期望保留一定的弹性工作时间",
"objective": "评估该政策的影响面,并为平稳落地提供建议"
},
"output": "{\n \"what\": \"允许员工定期或永久性在公司以外(主要为家中)完成工作任务的正式制度与新型工作模式。\",\n \"why\": \"顺应混合办公模式普及的趋势,提升员工满意度与工作生活平衡,减少通勤消耗,并降低公司办公场地的物理运营成本。\",\n \"who\": \"直接主体为全体适岗员工管理主体为各级管理者与HR支持主体为IT部门间接利益相关者包括外部客户与供应商。\",\n \"when\": \"计划于2024年Q3开始小范围试点Q4全面推广包含前期准备、中期过渡、后期稳定三个完整阶段。\",\n \"where\": \"适用于总部及各分支机构的非现场操作岗位,实际工作地点延伸至员工家中或符合保密要求的远程协作空间。\",\n \"how\": \"通过发布政策文档、发放远程软硬件工具包、开展管理者赋能培训以及将考核标准从工时导向强力转向成果导向OKR来落地。\",\n \"howMuch\": \"预计覆盖全公司60%约800名员工初期软硬件投入约200万长期预计每年节省150万场地费员工日均节省通勤1.5小时。\",\n \"howFeel\": \"员工普遍期待且满意度高但部分中层管理者存在强烈的失控感与焦虑IT部门面临短期内支持压力暴增的疲惫感。\",\n \"actionableInsights\": [\n \"针对管理者的失控感HR需在Q3试点前组织专项的异步协同与在线目标管理培训。\",\n \"IT部门需提前建立远程支持工单系统及自助排障知识库以缓解全员推广初期的咨询并发压力。\"\n ],\n \"metadata\": {\n \"confidenceScore\": 0.88,\n \"assumptions\": [\n \"公司核心业务流程已基本实现数字化,支持云端协作\",\n \"员工居家环境具备基础的网络办公条件\"\n ]\n }\n}"
},
{
"input": {
"topic": "城市共享单车乱停乱放问题",
"context": "市中心早晚高峰期间,大量单车堵塞地铁口和人行盲道",
"objective": "全面拆解现象成因,为城市管理部门提供治理抓手"
},
"output": "{\n \"what\": \"共享单车使用结束后,用户未按规定停放至指定电子围栏或物理区域,导致违规占用盲道、消防通道及绿化带的现象。\",\n \"why\": \"用户追求极致便利的惰性心理;企业为抢占市场超量投放且电子围栏技术精度不足;政府缺乏实时动态的联合监管与企业惩处机制。\",\n \"who\": \"违规主体为部分单车用户;管理责任方为单车运营企业及城市街道城管部门;受损主体为普通路人、视障群体及社区居民。\",\n \"when\": \"乱停放现象全天候存在但早晚潮汐通勤高峰期后上午9点、下午7点及周末节假日的商圈周边尤为严重且集中。\",\n \"where\": \"高度集中于地铁出入口、大型商圈外围、老旧写字楼周边以及高校门口等短途接驳需求极其旺盛且停车资源匮乏的区域。\",\n \"how\": \"需通过升级高精度北斗定位强制入栏结算、城管与企业建立动态配额联动扣分机制、以及用户信用分与骑行费率挂钩等综合手段治理。\",\n \"howMuch\": \"某一线城市日均需额外清理2万辆违停单车耗费500名专职人员年隐性财政支出超2000万元且占该市出行投诉总量的30%。\",\n \"howFeel\": \"普通市民对出行受阻感到强烈反感与愤怒;企业面对高昂运维搬运成本感到无奈;基层城管面临极大管理压力与反复回潮的疲劳感。\",\n \"actionableInsights\": [\n \"交警与城管部门应联合约谈头部企业,推行‘按违停率扣减下季度投放配额’的强力商业杠杆机制。\",\n \"在地铁口等潮汐热点区域,引入带有物理锁孔的立体停车架,从物理空间上规范停放形态并增加单点容量。\"\n ],\n \"metadata\": {\n \"confidenceScore\": 0.92,\n \"assumptions\": [\n \"北斗高精度定位芯片在主流共享单车上的普及率已达到技术要求\",\n \"政府具备获取各企业实时车辆定位数据的权限\"\n ]\n }\n}"
},
{
"input": {
"topic": "某餐饮品牌推出植物肉新品的市场反响",
"context": "品牌希望迎合年轻人的低碳饮食趋势,在核心门店试点推出了植物肉汉堡",
"objective": "评估首月市场反馈,决定是否向二三线城市全面推广"
},
"output": "{\n \"what\": \"餐饮品牌推出以大豆、豌豆等植物蛋白为原料、模拟真肉口感和外观的植物基汉堡及相关配套轻食餐品。\",\n \"why\": \"为迎合全球健康饮食、低碳环保的新消费趋势打造品牌差异化标签并重点吸引年轻Z世代和弹性素食人群。\",\n \"who\": \"目标受众为年轻白领、健身达人及环保主义者;执行方包括内部研发、市场营销团队及外部植物基供应链头部供应商。\",\n \"when\": \"2024年3月配合春季健康季节点首发上市目前处于上市满首月的市场反馈收集、数据盘点与复盘期。\",\n \"where\": \"首批在一线城市北上广深的50家核心商圈门店及线上主流外卖平台同步推出暂未下沉至二三线城市门店。\",\n \"how\": \"通过KOL探店种草、推出尝鲜价折扣套餐、在门店布置碳足迹减排科普展板等线上线下全渠道营销组合拳推向市场。\",\n \"howMuch\": \"首月总销量达5万份占总营收8%客单价较常规产品溢价15%营销推广费用约200万获取全网曝光超1000万次。\",\n \"howFeel\": \"60%尝鲜用户对口感表示满意并愿复购30%认为价格偏高且有‘科技与狠活’的过度加工顾虑,环保组织则给予了高度赞誉。\",\n \"actionableInsights\": [\n \"针对过度加工的顾虑需在后续营销中公开并强调植物肉的清洁配料表突出0胆固醇、高蛋白的健康属性。\",\n \"暂缓向二三线城市全面推广考虑到30%用户对价格敏感,建议先优化供应链,推出‘半份植物肉+半份蔬菜’的高性价比平替沙拉碗进行下沉测试。\"\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\n \"一线城市消费者对植物基食品的溢价接受度和概念认知度显著高于下沉市场\"\n ]\n }\n}"
}
]
}

View File

@@ -0,0 +1,50 @@
{
"name": "5whys",
"description": "5 Whys 分析法:通过连续追问五次“为什么”,剥开表层现象,探究并锁定导致问题发生的根本原因 (Root Cause),并推导解决方案。",
"persona": "root_cause_analyst",
"parameters": [
{
"name": "problem",
"description": "需要分析的核心问题、故障或不良现象",
"required": true
},
{
"name": "context",
"description": "问题发生的背景信息、前置条件或受影响的范围(可选)",
"required": false
},
{
"name": "goal",
"description": "期望通过解决此问题达成的最终目标(可选)",
"required": false
}
],
"template": "请使用 5 Whys 分析法探究以下问题的根本原因:\n核心问题 (Problem){{problem}}\n背景信息 (Context){{context}}\n改善目标 (Goal){{goal}}\n\n请严格按照 5 Whys 的逻辑,从表象问题出发,连续进行 5 次有逻辑因果关系的追问,直到找到根本原因,并给出针对根本原因的解决方案。\n\n输出格式要求请严格按以下 JSON 格式输出你的分析结果,不要包含任何 Markdown 代码块标记(如反引号包裹的 json直接输出纯 JSON 文本:\n{\n \"problemStatement\": \"清晰界定当前面临的问题\",\n \"whys\": [\n {\n \"level\": 1,\n \"question\": \"为什么会出现上述问题?\",\n \"answer\": \"第一层直接原因\"\n },\n {\n \"level\": 2,\n \"question\": \"基于上一个回答,进一步追问为什么?\",\n \"answer\": \"第二层深层原因\"\n },\n {\n \"level\": 3,\n \"question\": \"继续追问为什么?\",\n \"answer\": \"第三层原因\"\n },\n {\n \"level\": 4,\n \"question\": \"继续追问为什么?\",\n \"answer\": \"第四层原因\"\n },\n {\n \"level\": 5,\n \"question\": \"追问根本机制或系统性缺陷?\",\n \"answer\": \"第五层根本原因\"\n }\n ],\n \"rootCause\": \"综合上述推导,用一句话总结最终的根本原因(通常是系统、流程或管理机制的问题)\",\n \"actionableSolutions\": [\n {\n \"action\": \"针对根本原因的具体整改措施\",\n \"owner\": \"负责执行的角色或部门\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\"推导此因果链条的关键假设\"]\n }\n}",
"systemPrompt": "你是一位擅长根因分析的问题解决专家,能够通过系统性追问深入挖掘问题的根本原因。你必须严格遵守 JSON 格式输出规范,确保推导链条清晰且可直接被下游程序结构化解析。",
"examples": [
{
"input": {
"problem": "某互联网公司近期新注册用户留存率大幅下降",
"context": "公司主打年轻人的社交APP近期刚投入了大量买量预算但次日留存跌破了警戒线",
"goal": "止住留存率下滑趋势,提升新用户的早期活跃度"
},
"output": "{\n \"problemStatement\": \"公司投入大量买量预算后,新注册用户的次日留存率大幅下跌,未达到预期活跃度。\",\n \"whys\": [\n {\n \"level\": 1,\n \"question\": \"为什么近期新用户留存率大幅下降?\",\n \"answer\": \"因为大量新用户在注册后的一小时内就卸载了APP或停止了活跃。\"\n },\n {\n \"level\": 2,\n \"question\": \"为什么新用户在一小时内就停止活跃?\",\n \"answer\": \"因为他们觉得新用户引导流程过于复杂繁琐,在第三步就放弃了。\"\n },\n {\n \"level\": 3,\n \"question\": \"为什么新用户引导流程复杂繁琐?\",\n \"answer\": \"因为引导步骤多达五步,且强制要求填写过多个人真实信息(如行业、真实头像等)才能进入主界面。\"\n },\n {\n \"level\": 4,\n \"question\": \"为什么引导步骤设计为五步且强制索要信息?\",\n \"answer\": \"因为产品和运营团队希望在注册漏斗最前端收集最全面的用户画像,以便后续做精准算法推荐。\"\n },\n {\n \"level\": 5,\n \"question\": \"为什么产品团队为了收集画像而牺牲了基础的注册转化体验?\",\n \"answer\": \"因为内部KPI考核导向存在偏差设计决策基于内部业务诉求且上线前缺乏可用性测试和A/B测试验证。\"\n }\n ],\n \"rootCause\": \"产品决策机制存在缺陷以内部数据收集目标凌驾于用户体验之上且关键路径改版缺乏科学的A/B测试验证闭环。\",\n \"actionableSolutions\": [\n {\n \"action\": \"立即回滚或精简注册流程,将部分非核心信息的收集后置到用户深度体验产品后\",\n \"owner\": \"产品团队\"\n },\n {\n \"action\": \"建立核心链路改版必须通过A/B测试且留存率不降才能全量发布的流程红线\",\n \"owner\": \"质量与数据分析团队\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.90,\n \"assumptions\": [\n \"新买量的渠道质量与历史渠道一致,非渠道刷量导致\",\n \"留存率的下降与最近一次版本的注册流程改版在时间点上高度重合\"\n ]\n }\n}"
},
{
"input": {
"problem": "某制造工厂生产线设备故障率近期异常上升",
"context": "生产旺季即将到来订单压力极大当前设备停机导致产能出现20%的缺口",
"goal": "迅速降低设备故障率,恢复并保障生产旺季的产能供应"
},
"output": "{\n \"problemStatement\": \"生产旺季前夕,工厂核心生产线设备故障率异常上升,导致停机频发并造成严重产能缺口。\",\n \"whys\": [\n {\n \"level\": 1,\n \"question\": \"为什么设备故障率近期异常上升?\",\n \"answer\": \"因为核心设备的传动轴承等关键部件磨损速度异常加快,导致频繁卡死。\"\n },\n {\n \"level\": 2,\n \"question\": \"为什么关键部件磨损速度异常加快?\",\n \"answer\": \"因为传动部件的润滑严重不足,导致物理摩擦急剧增大。\"\n },\n {\n \"level\": 3,\n \"question\": \"为什么会发生润滑严重不足的情况?\",\n \"answer\": \"因为设备的自动润滑系统出现了堵塞,未能按预定计划加注润滑油。\"\n },\n {\n \"level\": 4,\n \"question\": \"为什么自动润滑系统堵塞没有被及时发现和处理?\",\n \"answer\": \"因为一线维护人员在日常巡检中忽略了对自动润滑系统的检查,误以为它既然是‘自动’的就不需要人工干预。\"\n },\n {\n \"level\": 5,\n \"question\": \"为什么维护人员会产生这种误解并忽略检查?\",\n \"answer\": \"因为设备维护的标准作业程序 (SOP) 文件存在漏洞,未将‘自动润滑系统巡检’明确写入日常检查表,导致责任和标准缺失。\"\n }\n ],\n \"rootCause\": \"设备维护流程与SOP存在管理盲区未覆盖自动化附件的日常监督导致预防性维护失效。\",\n \"actionableSolutions\": [\n {\n \"action\": \"立即全面清理自动润滑系统并更换受损轴承,恢复生产线运转\",\n \"owner\": \"设备维修组\"\n },\n {\n \"action\": \"修订全厂设备点检SOP将所有自动化辅助系统纳入每日必检项并对维修人员进行全员培训\",\n \"owner\": \"工程与设备管理部\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.95,\n \"assumptions\": [\n \"润滑油的标号和品质符合设备要求,非油品质量问题\",\n \"设备并未处于超负荷设计极限运行状态\"\n ]\n }\n}"
},
{
"input": {
"problem": "高校图书馆自习室“占座”现象严重,引发大量投诉",
"context": "期末考试临近,学生对学习空间需求暴增。大量座位上放着书本但长时间无人使用,引发学生间的矛盾",
"goal": "提高图书馆座位的实际利用率,平息学生群体的不满情绪"
},
"output": "{\n \"problemStatement\": \"期末复习高峰期,图书馆自习室存在大量‘物品占座但无人使用’的现象,导致实际资源浪费并引发学生强烈不满。\",\n \"whys\": [\n {\n \"level\": 1,\n \"question\": \"为什么自习室‘占座’现象如此严重?\",\n \"answer\": \"因为很多学生为了确保自己随时有座位,习惯用书本占位后去上课或吃饭,且长时间不返回。\"\n },\n {\n \"level\": 2,\n \"question\": \"为什么学生能够肆无忌惮地长时间占座而不被制止?\",\n \"answer\": \"因为现场没有有效的制止机制,管理员巡查频率极低,且其他学生不敢私自清理他人物品。\"\n },\n {\n \"level\": 3,\n \"question\": \"为什么管理员巡查频率低且不主动清理?\",\n \"answer\": \"因为管理员人手不足,更重要的是,缺乏专门针对‘占座超时’的具体判定标准和管理授权。\"\n },\n {\n \"level\": 4,\n \"question\": \"为什么图书馆一直没有制定占座的判定标准和管理制度?\",\n \"answer\": \"因为图书馆管理层此前一直认为占座是‘学生道德和自觉性’问题,不愿采取强硬的管理手段引发可能的冲突。\"\n },\n {\n \"level\": 5,\n \"question\": \"为什么管理层会将管理责任推给‘自觉’,而不积极介入?\",\n \"answer\": \"因为图书馆缺乏有效的数据监控手段和学生意见反馈渠道,管理层与学生真实痛点脱节,未意识到资源错配的严重性。\"\n }\n ],\n \"rootCause\": \"图书馆管理层服务意识与数字化管理手段双重滞后,未建立科学的空间资源分配规则与违规惩处机制。\",\n \"actionableSolutions\": [\n {\n \"action\": \"紧急出台《自习室防占座管理规定》明确离座超过45分钟即视为放弃并安排专人定期清理滞留物品\",\n \"owner\": \"图书馆馆长办公室\"\n },\n {\n \"action\": \"加快引入基于微信小程序的‘座位预约与扫码签到系统’,用技术手段实现座位资源的动态分配与黑名单机制\",\n \"owner\": \"图书馆信息技术部\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.88,\n \"assumptions\": [\n \"图书馆整体座位数在绝对数量上确实无法满足期末全体学生的同时自习需求,属于存量博弈\",\n \"学生群体对引入公平的预约机制具有较高的接受度\"\n ]\n }\n}"
}
]
}

View File

@@ -0,0 +1,50 @@
{
"name": "pestle",
"description": "PESTLE 宏观环境分析:从政治(P)、经济(E)、社会(S)、技术(T)、法律(L)、环境(E)六个维度,全面结构化评估目标行业或领域的外部宏观环境。",
"persona": "macro_environment_analyst",
"parameters": [
{
"name": "domain",
"description": "需要分析的具体行业、市场或业务领域",
"required": true
},
{
"name": "region",
"description": "目标地域范围(如:全球、中国、北美、特定省市等,可选)",
"required": false
},
{
"name": "timeframe",
"description": "分析的时间跨度当前现状、未来3-5年等可选",
"required": false
}
],
"template": "请使用 PESTLE 框架分析以下行业/领域的宏观环境:\n目标行业/领域 (Domain){{domain}}\n地域范围 (Region){{region}}\n时间跨度 (Timeframe){{timeframe}}\n\n请依次从政治、经济、社会、技术、法律、环境六个维度展开深度评估并给出战略建议。\n\n输出格式要求请严格按以下 JSON 格式输出你的分析结果,不要包含任何 Markdown 代码块标记(如反引号包裹的 json直接输出纯 JSON 文本:\n{\n \"political\": {\n \"analysis\": \"政治因素分析,如政府政策、稳定性、贸易导向等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"economic\": {\n \"analysis\": \"经济因素分析,如经济增长、汇率、消费能力等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"social\": {\n \"analysis\": \"社会因素分析,如人口结构、文化趋势、生活方式等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"technological\": {\n \"analysis\": \"技术因素分析,如技术创新、研发周期、自动化替代等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"legal\": {\n \"analysis\": \"法律因素分析,如劳动法、数据安全、知识产权等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"environmental\": {\n \"analysis\": \"环境因素分析如气候变化、ESG标准、碳排放政策等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"strategicAdvice\": [\n {\n \"strategy\": \"基于上述宏观环境得出的具体战略建议\",\n \"focusArea\": \"研发|市场|合规|供应链等\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\"推导该分析所基于的关键宏观假设\"]\n }\n}",
"systemPrompt": "你是一位宏观环境分析专家,擅长运用 PESTLE 框架评估行业外部环境,能够结合具体数据和发展趋势进行深入洞察。你必须严格遵守 JSON 格式输出规范,确保数据可直接被下游程序结构化解析。",
"examples": [
{
"input": {
"domain": "人工智能芯片行业",
"region": "中国",
"timeframe": "未来3-5年"
},
"output": "{\n \"political\": {\n \"analysis\": \"中国政府将AI芯片列为国家战略新兴产业通过大基金等提供支持。同时外部大国对华芯片出口管制持续收紧倒逼国内加速自主可控。政府东数西算工程也为国产芯片创造了庞大的算力底座应用场景。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"economic\": {\n \"analysis\": \"AI大模型带来的算力需求旺盛市场规模年增速预计超30%。然而,宏观经济弱复苏可能导致部分下游行业资本开支收缩,且先进制程的代工成本持续高企,对芯片设计企业的现金流带来挑战。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Neutral\"\n },\n \"social\": {\n \"analysis\": \"全社会数字化转型加速AI应用向制造、医疗等实体经济渗透。公众对数据安全和算法伦理关注度上升。此外高校相关专业扩招使得底层架构研发人才供给逐渐增加但高端领军人才依然稀缺。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Positive\"\n },\n \"technological\": {\n \"analysis\": \"GPU仍占主导但ASIC、类脑芯片等新架构不断涌现。先进封装Chiplet成为绕开先进制程封锁、提升性能的关键途径。国内在推理芯片端已接近国际水平但在训练芯片端仍存代差。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"legal\": {\n \"analysis\": \"《数据安全法》等法规对芯片底层的数据处理合规提出要求。知识产权保护力度加大专利战可能频发。海外实体清单限制了部分EDA工具和半导体IP的获取带来法律和合规断供风险。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Negative\"\n },\n \"environmental\": {\n \"analysis\": \"芯片制造是高耗能产业双碳目标下政府对晶圆厂能耗监管趋严。同时AI数据中心对算力芯片的能效比(PPA)要求极高低功耗设计成为核心竞争力符合ESG投资趋势。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Positive\"\n },\n \"strategicAdvice\": [\n {\n \"strategy\": \"加大基于Chiplet架构和RISC-V开源指令集的研发投入对冲先进制程和商业IP断供的风险。\",\n \"focusArea\": \"研发\"\n },\n {\n \"strategy\": \"紧贴‘东数西算’等政府新基建项目,聚焦信创市场进行国产化替代销售。\",\n \"focusArea\": \"市场\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.88,\n \"assumptions\": [\n \"全球地缘政治博弈在未来3-5年内不会显著缓和\",\n \"国内算力需求将持续保持指数级增长\"\n ]\n }\n}"
},
{
"input": {
"domain": "新能源汽车电池回收行业",
"region": "欧洲",
"timeframe": "当前至2030年"
},
"output": "{\n \"political\": {\n \"analysis\": \"欧盟致力于实现‘欧洲绿色协议’目标,高度依赖海外关键矿物(锂、钴、镍)。通过立法推动本土电池供应链闭环,以减少对亚洲材料的依赖,政治支持力度空前。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"economic\": {\n \"analysis\": \"由于原始矿产资源价格波动巨大回收材料的经济价值凸显。预计到2030年欧洲将有大量初代电动车电池退役催生百亿欧元级别的回收市场吸引大量风险投资和传统车企资本涌入。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"social\": {\n \"analysis\": \"欧洲民众环保意识极强,对循环经济接受度高。消费者在购车时越来越关注产品的全生命周期碳足迹,社会舆论对不可回收的高污染电池持强烈抵制态度。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Positive\"\n },\n \"technological\": {\n \"analysis\": \"湿法冶金和火法冶金技术正面临优化,以提高金属回收率并降低能耗。电池直接修复与梯次利用(如降级用于储能)的检测和筛选技术是当前的研发热点,自动化拆解技术急需突破。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Neutral\"\n },\n \"legal\": {\n \"analysis\": \"新《欧盟电池法》强制规定了新电池中必须包含最低比例的回收金属如16%的钴、6%的锂等),并要求实施‘电池护照’制度。法规极其严苛,对企业的合规溯源能力提出巨大挑战。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"environmental\": {\n \"analysis\": \"电池回收本身旨在解决环境问题但回收工艺尤其是传统火法可能产生二次污染和高碳排放。行业必须采用清洁能源驱动的低碳回收工艺以符合欧盟严苛的ESG环保审核。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Neutral\"\n },\n \"strategicAdvice\": [\n {\n \"strategy\": \"加快建立基于区块链的‘电池护照’数据平台,实现全链条数据追踪,满足欧盟合规要求。\",\n \"focusArea\": \"合规与数字化\"\n },\n {\n \"strategy\": \"与欧洲本土大型车企建立早期回收联盟锁定未来5年的退役电池废料来源。\",\n \"focusArea\": \"供应链\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.92,\n \"assumptions\": [\n \"新《欧盟电池法》将按预期时间表严格执行不倒退\",\n \"新能源车保有量达到预期规模,提供充足的退役电池\"\n ]\n }\n}"
},
{
"input": {
"domain": "在线职业教育",
"region": "全球(聚焦新兴市场)",
"timeframe": "当前现状"
},
"output": "{\n \"political\": {\n \"analysis\": \"多国政府为缓解结构性失业,出台政策鼓励成人技能重塑。部分新兴市场政府提供财政补贴支持数字化技能培训,但同时对跨境在线教育内容和数据出境的审查也在趋严。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Neutral\"\n },\n \"economic\": {\n \"analysis\": \"全球经济增长放缓导致就业压力增大职场人逆周期自我提升投资意愿增强。然而新兴市场用户支付能力有限平台需要探索B2B2C通过企业采购或微支付等灵活的商业模式。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"social\": {\n \"analysis\": \"终身学习理念普及斜杠青年和自由职业者群体扩大。年轻一代习惯碎片化、移动化学习。由于AI对基础文职工作的冲击社会对软技能和复合型技术技能的培训需求急剧上升。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"technological\": {\n \"analysis\": \"AIGC技术正在重塑在线教育实现课程内容的自动化生成、个性化自适应学习路径以及7x24小时的AI虚拟导师辅导大幅降低了教研和辅导的人力成本。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"legal\": {\n \"analysis\": \"面临不同国家的隐私保护法如GDPR合规挑战。AI生成内容的版权归属问题尚不明确。此外部分国家对颁发职业资格证书的线上机构设有严格的准入资质限制。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Negative\"\n },\n \"environmental\": {\n \"analysis\": \"在线教育作为无纸化、零通勤的低碳行业契合ESG理念。云服务器带来的碳排放虽然存在但总体环境负面影响极小。\",\n \"impactLevel\": \"Low\",\n \"trend\": \"Neutral\"\n },\n \"strategicAdvice\": [\n {\n \"strategy\": \"全面集成AIGC能力推出AI私教功能以低成本在下沉市场和新兴市场提供个性化体验。\",\n \"focusArea\": \"研发与产品\"\n },\n {\n \"strategy\": \"加强与目标市场本地企业HR部门的合作从单纯卖课向培训+推荐就业’闭环服务转型。\",\n \"focusArea\": \"市场与销售\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\n \"AI技术能够有效替代部分基础人工辅导工作\",\n \"新兴市场互联网基础设施足以支持流畅的视频与AI互动\"\n ]\n }\n}"
}
]
}

View File

@@ -0,0 +1,50 @@
{
"name": "scqa",
"description": "SCQA 架构分析:基于情境(Situation)与冲突(Complication),提炼核心问题(Question)并输出结构化的战略级答案(Answer)与行动计划。",
"persona": "scqa_facilitator",
"parameters": [
{
"name": "situation",
"description": "当前的客观背景或情境描述",
"required": true
},
{
"name": "context",
"description": "补充的约束条件、行业背景或已有资源(可选)",
"required": false
},
{
"name": "objective",
"description": "期望达成的最终业务目标(可选)",
"required": false
}
],
"template": "请使用 SCQA 架构分析以下情境:\n情境 (Situation){{situation}}\n补充背景/约束 (Context){{context}}\n期望目标 (Objective){{objective}}\n\n请依次构建\n1. 复杂化 (Complication):指出情境中存在的矛盾或挑战。\n2. 问题 (Question):基于复杂化提炼出核心问题。\n3. 答案 (Answer):提出解决问题的初步方案或分析路径。\n\n输出格式要求请严格按以下 JSON 格式输出你的分析结果,不要包含任何 Markdown 代码块标记(如反引号包裹的 json直接输出纯 JSON 文本:\n{\n \"situation\": \"客观陈述背景,说明当前处于什么状态\",\n \"complication\": \"指出打破现状的冲突点、矛盾或面临的挑战\",\n \"question\": \"基于冲突提炼出一个或多个亟待解决的核心疑问\",\n \"answer\": \"提出解决问题的战略性回答或核心逻辑\",\n \"actionItems\": [\n {\n \"task\": \"具体的执行步骤描述\",\n \"owner\": \"建议的负责角色或部门\",\n \"priority\": \"High|Medium|Low\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\"推导该方案所基于的关键假设\"]\n }\n}",
"systemPrompt": "你是一位资深战略分析师,擅长运用 SCQA 框架进行逻辑严谨的业务拆解。你必须严格遵守 JSON 格式输出规范,确保数据可直接被程序解析。",
"examples": [
{
"input": {
"situation": "一家传统零售企业过去三年线上销售额年均增长仅 3%,而行业平均水平为 15%。",
"context": "公司拥有300家实体店网络和稳定供应链但IT部门仅能维护ERP缺乏数字营销经验。",
"objective": "在两年内将线上销售增速提升至行业平均水平,且不影响实体店利润。"
},
"output": "{\n \"situation\": \"企业拥有300家实体店和成熟供应链但在电商崛起背景下过去三年线上销售年均复合增长率仅为3%远落后于行业15%的均值。\",\n \"complication\": \"企业意识到需数字化转型并设定了高增长目标但线上线下渠道割裂且现有IT团队缺乏数字营销与用户运营的基因导致转型难以落地。\",\n \"question\": \"在缺乏原生数字团队和资源受限的情况下,如何快速弥补线上运营短板,实现线上线下融合(OMO)以拉动业绩增长?\",\n \"answer\": \"采取‘借力与重构’并行的 OMO 战略:短期内引入成熟的第三方电商中台与代运营团队快速起步,中期在内部成立独立核算的数字化事业部,打通底层会员与库存数据。\",\n \"actionItems\": [\n {\n \"task\": \"引入外部数字化中台SaaS服务实现库存、会员、营销三通\",\n \"owner\": \"CIO & 采购部\",\n \"priority\": \"High\"\n },\n {\n \"task\": \"成立独立数字化事业部按全域GMV而非单渠道进行绩效考核\",\n \"owner\": \"CEO & HR\",\n \"priority\": \"High\"\n },\n {\n \"task\": \"在一线城市挑选20家门店作为店仓一体化敏捷试点\",\n \"owner\": \"数字化事业部 & 运营部\",\n \"priority\": \"Medium\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\n \"现有实体店网络具备转化为前置仓的空间和物流基础\",\n \"管理层愿意为独立数字化事业部下放决策权\"\n ]\n }\n}"
},
{
"input": {
"situation": "某三甲医院近期患者满意度调查得分持续下降,尤其在挂号、候诊环节抱怨最多。",
"context": "医院两年前已推行微信预约挂号目前线上预约率达60%,但现场仍有大量不会使用智能手机的老年患者,与预约患者混流。",
"objective": "在不增加医生工作负荷的前提下将患者平均候诊时间缩短30%并提升整体满意度至90分以上。"
},
"output": "{\n \"situation\": \"医院日均门诊量巨大虽已推行线上预约且预约率达60%,但近期患者满意度调查中挂号和候诊环节得分大幅下降。\",\n \"complication\": \"线上预约未实现精准分时,且与大量依赖现场挂号的老年患者混流;同时信息系统割裂导致护士手动核对效率低,加剧了现场拥堵和医患沟通不足的矛盾。\",\n \"question\": \"在医疗资源和医生工作量固定的限制下,如何通过技术手段与流程重构,精准分流并缩短患者候诊时间,挽回患者满意度?\",\n \"answer\": \"推行‘精准预约+分时段报到’的精细化排班体系,同时针对老年群体保留专属服务通道,打通院内信息系统实现全流程自动化叫号。\",\n \"actionItems\": [\n {\n \"task\": \"升级预约系统将预约窗口缩短至15分钟实行提前扫码报到制\",\n \"owner\": \"信息科 & 门诊办\",\n \"priority\": \"High\"\n },\n {\n \"task\": \"设立2个老年人无手机通道窗口及志愿者协助机制\",\n \"owner\": \"客服中心\",\n \"priority\": \"High\"\n },\n {\n \"task\": \"对接HIS与叫号系统实现自动化排队和诊前科普内容推送\",\n \"owner\": \"信息科\",\n \"priority\": \"Medium\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.90,\n \"assumptions\": [\n \"各科室医生看诊速度可通过历史数据进行合理均值化预估\",\n \"绝大部分中青年患者能较快适应分时段报到制度\"\n ]\n }\n}"
},
{
"input": {
"situation": "某知识付费平台“课程完课率”不足 10%,用户购买课程后往往只学几节课就放弃。",
"context": "平台主打职场技能提升,课程质量高评分好,但学习模式以单向录播视频为主,缺乏社群互动和外部学习督促机制。",
"objective": "通过产品机制和轻度运营手段将课程完课率提升至30%以上,带动二次复购。"
},
"output": "{\n \"situation\": \"知识付费平台坐拥大量活跃用户和高分优质课程但核心指标完课率仅不足10%,用户极易流失。\",\n \"complication\": \"内容优质但缺乏互动与反馈机制,用户仅凭自律难以坚持;加之无社群督促,学习过程枯燥孤立,导致冲动购买后长期搁置。\",\n \"question\": \"如何在不大幅增加人工服务成本的前提下,利用产品机制激发用户内在学习动力,突破完课率瓶颈?\",\n \"answer\": \"引入‘游戏化学习路径’与‘社群轻运营’双螺旋模式:通过将课程拆解为微任务并匹配荣誉体系降低学习门槛,同时通过系统自动匹配学习小组营造同侪压力。\",\n \"actionItems\": [\n {\n \"task\": \"重构播放器体验课程按15分钟微任务拆包增加签到与勋章奖励\",\n \"owner\": \"产品部 & 研发部\",\n \"priority\": \"High\"\n },\n {\n \"task\": \"开发学伴匹配功能系统自动为同进度用户组建3-5人微信或站内互助小组\",\n \"owner\": \"产品部 & 运营部\",\n \"priority\": \"High\"\n },\n {\n \"task\": \"建立触发式自动化干预策略针对超过3天未学习的用户自动推送关怀提醒\",\n \"owner\": \"用户运营部\",\n \"priority\": \"Medium\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.88,\n \"assumptions\": [\n \"现有的录播课程内容结构上允许被切分为短小精悍的微任务模块\",\n \"用户对于虚拟成就(勋章/积分)和同伴压力有积极的响应反馈\"\n ]\n }\n}"
}
]
}

View File

@@ -0,0 +1,50 @@
{
"name": "swot",
"description": "SWOT 分析:从内部优势(S)、内部劣势(W)、外部机会(O)、外部威胁(T)四个维度全面评估分析对象并推导交叉战略方案如SO/ST/WO/WT。",
"persona": "strategy_advisor",
"parameters": [
{
"name": "entity",
"description": "分析对象(如企业、产品、项目、个人等)",
"required": true
},
{
"name": "context",
"description": "补充的行业背景、当前阶段或面临的核心挑战(可选)",
"required": false
},
{
"name": "competitors",
"description": "主要竞争对手或对标对象(可选)",
"required": false
}
],
"template": "请使用 SWOT 框架分析以下对象:\n分析对象 (Entity){{entity}}\n背景信息 (Context){{context}}\n主要竞争对手 (Competitors){{competitors}}\n\n请依次从内部优势、内部劣势、外部机会、外部威胁四个维度展开评估并结合 SWOT 交叉矩阵给出战略建议。\n\n输出格式要求请严格按以下 JSON 格式输出你的分析结果,不要包含任何 Markdown 代码块标记(如反引号包裹的 json直接输出纯 JSON 文本:\n{\n \"strengths\": [\n { \"factor\": \"优势点名称\", \"reasoning\": \"简要论证\", \"impactLevel\": \"High|Medium|Low\" }\n ],\n \"weaknesses\": [\n { \"factor\": \"劣势点名称\", \"reasoning\": \"简要论证\", \"impactLevel\": \"High|Medium|Low\" }\n ],\n \"opportunities\": [\n { \"factor\": \"机会点名称\", \"reasoning\": \"简要论证\", \"urgency\": \"High|Medium|Low\" }\n ],\n \"threats\": [\n { \"factor\": \"威胁点名称\", \"reasoning\": \"简要论证\", \"severity\": \"High|Medium|Low\" }\n ],\n \"actionableStrategies\": [\n {\n \"strategyType\": \"SO增长战略 | ST防御战略 | WO扭转战略 | WT生存战略\",\n \"description\": \"结合具体优劣势和外部环境得出的具体行动建议\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\"推导该分析所基于的关键假设\"]\n }\n}",
"systemPrompt": "你是一位资深的战略分析顾问,擅长使用 SWOT 框架进行竞争态势分析。你必须清晰区分内外部因素,并通过交叉分析推导可执行的战略。你必须严格遵守 JSON 格式输出规范,确保数据可直接被下游程序结构化解析。",
"examples": [
{
"input": {
"entity": "一家专注于新能源汽车电池研发的初创公司",
"context": "A轮融资仅够支撑18个月急需实现技术向商业的转化并打入头部整车厂供应链",
"competitors": "宁德时代、LG新能源等传统电池巨头"
},
"output": "{\n \"strengths\": [\n { \"factor\": \"核心技术领先\", \"reasoning\": \"创始团队拥有5项固态电池核心专利实验室能量密度比市面主流产品高30%。\", \"impactLevel\": \"High\" },\n { \"factor\": \"轻资产与高灵活性\", \"reasoning\": \"采用研发+技术授权模式,决策链路短,无需背负沉重的重资产建厂包袱。\", \"impactLevel\": \"Medium\" }\n ],\n \"weaknesses\": [\n { \"factor\": \"资金链脆弱\", \"reasoning\": \"A轮资金仅能维持18个月的研发抗风险能力弱难以支撑长周期的商业化试错。\", \"impactLevel\": \"High\" },\n { \"factor\": \"缺乏量产与品控经验\", \"reasoning\": \"团队主要为科研背景,中试到规模化量产的技术转化能力未经验证。\", \"impactLevel\": \"High\" }\n ],\n \"opportunities\": [\n { \"factor\": \"高端车型对高能量密度电池渴求\", \"reasoning\": \"整车厂为缓解里程焦虑,对下一代固态电池技术存在巨大的潜在订单需求。\", \"urgency\": \"High\" },\n { \"factor\": \"车企供应链‘去中心化’趋势\", \"reasoning\": \"整车厂不愿被单一巨头(如宁德时代)绑定,积极扶持‘二供’和‘三供’。\", \"urgency\": \"Medium\" }\n ],\n \"threats\": [\n { \"factor\": \"巨头挤压与技术路线封锁\", \"reasoning\": \"传统巨头已投入巨资研发固态电池,初创公司面临巨大的资本与专利壁垒。\", \"severity\": \"High\" },\n { \"factor\": \"原材料价格剧烈波动\", \"reasoning\": \"上游关键矿石材料价格波动易导致研发和中试成本失控。\", \"severity\": \"Medium\" }\n ],\n \"actionableStrategies\": [\n {\n \"strategyType\": \"ST防御战略\",\n \"description\": \"凭借核心专利S选择与头部整车厂建立独家联合研发实验室通过技术入股或预付款形式绑定客户抵御传统电池巨头的资本挤压T缓解资金压力。\"\n },\n {\n \"strategyType\": \"WO扭转战略\",\n \"description\": \"利用车企扶持二供的意愿O引入具备制造业经验的外部高管或与成熟代工厂合资弥补团队缺乏量产经验的短板W。\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.88,\n \"assumptions\": [\n \"固态电池技术在未来两年内能够达到量产前夕的中试标准\",\n \"资本市场对固态电池赛道依然保持较高估值期待\"\n ]\n }\n}"
},
{
"input": {
"entity": "个人职业转型:从技术开发岗转向产品经理岗",
"context": "30岁5年后端开发经验公司内部有产品线扩张的转岗机会",
"competitors": "外部经验丰富的产品经理候选人、内部更年轻的转岗者"
},
"output": "{\n \"strengths\": [\n { \"factor\": \"技术底层理解深\", \"reasoning\": \"5年开发经验使得对系统架构、边界条件及开发成本有精准预判不易被技术团队忽悠。\", \"impactLevel\": \"High\" },\n { \"factor\": \"业务熟悉度高\", \"reasoning\": \"作为内部员工,熟悉公司现有产品线历史债务及组织协作文化,上手成本极低。\", \"impactLevel\": \"High\" }\n ],\n \"weaknesses\": [\n { \"factor\": \"缺乏用户洞察经验\", \"reasoning\": \"长期面对机器和代码,缺乏系统的竞品分析、用户访谈及交互设计思维训练。\", \"impactLevel\": \"High\" },\n { \"factor\": \"沟通习惯偏向确定性\", \"reasoning\": \"习惯以确定性的技术视角看待问题,需适应产品早期阶段高度模糊和频繁变更的沟通场景。\", \"impactLevel\": \"Medium\" }\n ],\n \"opportunities\": [\n { \"factor\": \"公司内部业务扩张\", \"reasoning\": \"公司正进行中台化或技术驱动型产品线扩张,亟需懂业务逻辑的技术型产品经理。\", \"urgency\": \"High\" },\n { \"factor\": \"AI与大模型浪潮\", \"reasoning\": \"AI产品的设计越来越需要懂算法和接口边界的复合型人才这是纯业务型PM的软肋。\", \"urgency\": \"Medium\" }\n ],\n \"threats\": [\n { \"factor\": \"年龄与试错成本\", \"reasoning\": \"30岁转岗意味着放弃原有技术积累的溢价且一旦转型失败难以退回原岗。\", \"severity\": \"High\" },\n { \"factor\": \"外部熟练工竞争\", \"reasoning\": \"行业内涌现大量自带成熟产品方法论的外部候选人,竞争激烈。\", \"severity\": \"High\" }\n ],\n \"actionableStrategies\": [\n {\n \"strategyType\": \"SO增长战略\",\n \"description\": \"利用自身深厚的技术背景和对内部系统的熟悉S主动申请主导偏底层逻辑或技术中台类的产品线O形成差异化竞争。\"\n },\n {\n \"strategyType\": \"WO扭转战略\",\n \"description\": \"在内部寻找资深业务型PM作为导师并在日常工作中主动承担原型绘制和用户调研任务O快速补齐需求分析短板W。\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\n \"公司内部拥有较为宽容的转岗试用机制\",\n \"目标产品线确实属于技术逻辑较重的领域\"\n ]\n }\n}"
},
{
"input": {
"entity": "某地方性连锁超市",
"context": "在本市拥有40家门店主打生鲜当前正面临客流严重下滑的危机",
"competitors": "美团优选、多多买菜等全国性社区团购平台"
},
"output": "{\n \"strengths\": [\n { \"factor\": \"本地供应链深耕\", \"reasoning\": \"与本地农户长期合作,生鲜直采品质可控,生鲜品类的信任度高于纯线上平台。\", \"impactLevel\": \"High\" },\n { \"factor\": \"高密度的前置网点\", \"reasoning\": \"40家门店深入社区腹地具备开展最后一公里即时零售的天然地理优势。\", \"impactLevel\": \"Medium\" }\n ],\n \"weaknesses\": [\n { \"factor\": \"数字化运营能力弱\", \"reasoning\": \"缺乏精准的用户画像与私域流量池,营销手段依赖传统的纸质海报,触达率极低。\", \"impactLevel\": \"High\" },\n { \"factor\": \"规模采购成本劣势\", \"reasoning\": \"标品(日化、零食)的采购体量远不及全国性互联网巨头,价格竞争处于下风。\", \"impactLevel\": \"High\" }\n ],\n \"opportunities\": [\n { \"factor\": \"即时零售(即时配送)红利\", \"reasoning\": \"消费者对‘半小时达’的即时性需求增加,而社区团购通常是‘次日达’。\", \"urgency\": \"High\" },\n { \"factor\": \"适老化消费场景构建\", \"reasoning\": \"老龄化趋势下,老年人依然偏好实体店挑拣生鲜的体验和人际交流。\", \"urgency\": \"Medium\" }\n ],\n \"threats\": [\n { \"factor\": \"巨头低价倾销抢夺客流\", \"reasoning\": \"社区团购利用资本优势持续高额补贴,导致价格敏感型顾客大量流失。\", \"severity\": \"High\" },\n { \"factor\": \"运营成本刚性上升\", \"reasoning\": \"实体门店租金、人工成本逐年攀升,持续挤压本已微薄的零售利润。\", \"severity\": \"High\" }\n ],\n \"actionableStrategies\": [\n {\n \"strategyType\": \"ST防御战略\",\n \"description\": \"避开与互联网巨头在标品上的价格战T利用生鲜直采优势S主打高品质、即买即得的差异化生鲜体验巩固基本盘。\"\n },\n {\n \"strategyType\": \"WO扭转战略\",\n \"description\": \"借势即时零售平台(如入驻美团/饿了么O将实体店转化为前置仓弥补自身数字化与配送能力的短板W拓展年轻客群。\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.90,\n \"assumptions\": [\n \"超市有一定资金支撑初期接入第三方即时配送平台的改造成本\",\n \"本地核心消费群体仍愿为高品质生鲜支付一定的溢价\"\n ]\n }\n}"
}
]
}

View File

@@ -0,0 +1,28 @@
"use strict";
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.literatureSchema = void 0;
const zod_1 = require("zod");
/**
* 文献标准化 JSON 的 Zod 模型
*/
exports.literatureSchema = zod_1.z.object({
title: zod_1.z.string().min(1, '标题不能为空'),
authors: zod_1.z.array(zod_1.z.string()).min(1, '作者列表不能为空'),
abstract: zod_1.z.string().optional(),
link: zod_1.z.string().url('链接格式不正确').optional(),
publication_year: zod_1.z.number().int().min(1900).optional(),
citation_count: zod_1.z.number().int().min(0).optional(),
source: zod_1.z.string().min(1, '来源不能为空'),
doi: zod_1.z.string().optional(),
keywords: zod_1.z.array(zod_1.z.string()).optional(),
});
//# sourceMappingURL=literatureSchema.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"literatureSchema.js","sourceRoot":"","sources":["../../src/models/literatureSchema.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AAEH,6BAAwB;AAExB;;GAEG;AACU,QAAA,gBAAgB,GAAG,OAAC,CAAC,MAAM,CAAC;IACvC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC;IAClC,OAAO,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC;IAC/C,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE;IAC1C,gBAAgB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;IACvD,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAClD,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC;IACnC,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC1B,QAAQ,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;CACzC,CAAC,CAAC"}

View File

@@ -0,0 +1,32 @@
[
{
"id": "strategy_advisor",
"name": "战略分析顾问",
"description": "擅长竞争态势分析,能够清晰区分内外部因素并提出客观见解。",
"systemPrompt": "你是一位资深的战略分析顾问,擅长使用 SWOT 框架进行竞争态势分析,能够清晰区分内外部因素并提出客观见解。"
},
{
"id": "root_cause_analyst",
"name": "根因分析专家",
"description": "擅长通过系统性追问深入挖掘问题的根本原因。",
"systemPrompt": "你是一位擅长根因分析的问题解决专家,能够通过系统性追问深入挖掘问题的根本原因。"
},
{
"id": "macro_environment_analyst",
"name": "宏观环境分析专家",
"description": "擅长运用 PESTLE 框架评估行业外部环境。",
"systemPrompt": "你是一位宏观环境分析专家,擅长运用 PESTLE 框架评估行业外部环境,能够结合具体数据和发展趋势进行深入洞察。"
},
{
"id": "structured_thinker",
"name": "结构化思维分析师",
"description": "擅长运用 5W3H 框架进行系统性拆解。",
"systemPrompt": "你是一位擅长运用 5W3H 框架进行结构化思考的分析师,能够全面覆盖问题的各个维度,确保分析的系统性和深度。"
},
{
"id": "scqa_facilitator",
"name": "SCQA 引导师",
"description": "擅长运用 SCQA 框架构建逻辑严谨的问题分析报告。",
"systemPrompt": "你是一位资深战略分析师,擅长运用 SCQA 框架构建逻辑严谨的问题分析报告。"
}
]

View File

@@ -0,0 +1,48 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.searchArxivSchema = exports.saveNoteSchema = exports.readNoteSchema = exports.generateSearchQueriesSchema = void 0;
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
const zod_1 = require("zod");
/**
* 检索词生成工具的校验模式。
* 使用 .min(1) 确保查询语句不为空。
*/
exports.generateSearchQueriesSchema = zod_1.z.object({
query: zod_1.z.string().min(1, '查询语句不能为空'),
});
/**
* 读取笔记工具的校验模式。
* 对应 toolsController 中的 handleReadLocalNote。
*/
exports.readNoteSchema = zod_1.z.object({
filename: zod_1.z.string()
.min(1, '文件名不能为空')
.refine((name) => name.toLowerCase().endsWith('.md'), { message: '文件名必须以 .md 结尾' }),
});
/**
* 保存笔记工具的校验模式。
* 对应 toolsController 中的 handleSaveNote。
*/
exports.saveNoteSchema = zod_1.z.object({
filename: zod_1.z.string()
.min(1, '文件名不能为空')
.refine((name) => name.toLowerCase().endsWith('.md'), { message: '文件名必须以 .md 结尾' }),
content: zod_1.z.string().min(1, '内容不能为空'),
});
/**
* 文献检索工具的校验模式。
* 对应 toolsController 中的 handleSearchArxiv。
*/
exports.searchArxivSchema = zod_1.z.object({
query: zod_1.z.string().min(1, '检索词不能为空'),
maxResults: zod_1.z.number().optional().default(5).describe('最大返回数量'),
});
//# sourceMappingURL=schemas.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../src/models/schemas.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,6BAAwB;AAExB;;;GAGG;AACU,QAAA,2BAA2B,GAAG,OAAC,CAAC,MAAM,CAAC;IAClD,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC;CACrC,CAAC,CAAC;AAEH;;;GAGG;AACU,QAAA,cAAc,GAAG,OAAC,CAAC,MAAM,CAAC;IACrC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE;SACjB,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC;SACjB,MAAM,CACL,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,EAC5C,EAAE,OAAO,EAAE,eAAe,EAAE,CAC7B;CACJ,CAAC,CAAC;AAEH;;;GAGG;AACU,QAAA,cAAc,GAAG,OAAC,CAAC,MAAM,CAAC;IACrC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE;SACjB,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC;SACjB,MAAM,CACL,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,EAC5C,EAAE,OAAO,EAAE,eAAe,EAAE,CAC7B;IACH,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC;CACrC,CAAC,CAAC;AAEH;;;GAGG;AACU,QAAA,iBAAiB,GAAG,OAAC,CAAC,MAAM,CAAC;IACxC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC;IACnC,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;CAChE,CAAC,CAAC"}

View File

@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const arxivService_1 = require("../arxivService");
// Mock 全局的 fetch 函数以防真实发起网络请求
global.fetch = jest.fn();
describe('Arxiv Service', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('应该成功解析 Arxiv 的 XML 并返回文献数组', async () => {
const mockXml = `
<feed>
<entry>
<id>http://arxiv.org/abs/1234.5678</id>
<title>Quantum Machine Learning</title>
<summary>This is a summary of QML.</summary>
</entry>
</feed>
`;
global.fetch.mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue(mockXml)
});
const results = await (0, arxivService_1.searchArxiv)('Quantum', 1);
expect(results).toHaveLength(1);
expect(results[0].title).toBe('Quantum Machine Learning');
expect(results[0].id).toBe('http://arxiv.org/abs/1234.5678');
});
it('当网络请求失败时应该抛出错误', async () => {
global.fetch.mockResolvedValue({
ok: false,
status: 500
});
await expect((0, arxivService_1.searchArxiv)('Error', 1)).rejects.toThrow('Arxiv API 响应错误: 500');
});
});
//# sourceMappingURL=arxivService.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"arxivService.test.js","sourceRoot":"","sources":["../../../src/services/__test__/arxivService.test.ts"],"names":[],"mappings":";;AAAA,kDAA8C;AAE9C,8BAA8B;AAC9B,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC;AAEzB,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;QAC1C,MAAM,OAAO,GAAG;;;;;;;;KAQf,CAAC;QACD,MAAM,CAAC,KAAmB,CAAC,iBAAiB,CAAC;YAC5C,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC;SAC3C,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,MAAM,IAAA,0BAAW,EAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QAChD,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QAC1D,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gBAAgB,EAAE,KAAK,IAAI,EAAE;QAC7B,MAAM,CAAC,KAAmB,CAAC,iBAAiB,CAAC;YAC5C,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;SACZ,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,IAAA,0BAAW,EAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}

View File

@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const arxivService_1 = require("../arxivService");
// Mock 全局的 fetch 函数以防真实发起网络请求
global.fetch = jest.fn();
describe('Arxiv Service', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('应该成功解析 Arxiv 的 XML 并返回文献数组', async () => {
const mockXml = `
<feed>
<entry>
<id>http://arxiv.org/abs/1234.5678</id>
<title>Quantum Machine Learning</title>
<summary>This is a summary of QML.</summary>
</entry>
</feed>
`;
global.fetch.mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue(mockXml)
});
const results = await (0, arxivService_1.searchArxiv)('Quantum', 1);
expect(results).toHaveLength(1);
expect(results[0].title).toBe('Quantum Machine Learning');
expect(results[0].id).toBe('http://arxiv.org/abs/1234.5678');
});
it('当网络请求失败时应该抛出错误', async () => {
global.fetch.mockResolvedValue({
ok: false,
status: 500
});
await expect((0, arxivService_1.searchArxiv)('Error', 1)).rejects.toThrow('Arxiv API 响应错误: 500');
});
});
//# sourceMappingURL=arxivService.test.ts.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"arxivService.test.ts.js","sourceRoot":"","sources":["../../../src/services/__test__/arxivService.test.ts.ts"],"names":[],"mappings":";;AAAA,kDAA8C;AAE9C,8BAA8B;AAC9B,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC;AAEzB,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;QAC1C,MAAM,OAAO,GAAG;;;;;;;;KAQf,CAAC;QACD,MAAM,CAAC,KAAmB,CAAC,iBAAiB,CAAC;YAC5C,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC;SAC3C,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,MAAM,IAAA,0BAAW,EAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QAChD,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QAC1D,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gBAAgB,EAAE,KAAK,IAAI,EAAE;QAC7B,MAAM,CAAC,KAAmB,CAAC,iBAAiB,CAAC;YAC5C,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;SACZ,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,IAAA,0BAAW,EAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}

View File

@@ -0,0 +1,58 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Project Caffeine
* 单元测试: intentService.ts
*/
const intentService_1 = require("../intentService");
describe('intentService -> generateSearchQueries', () => {
it('异常流:当输入为空字符串或全空格时,应当返回默认的后备检索词', () => {
// 验证空字符串
expect((0, intentService_1.generateSearchQueries)('')).toEqual(['通用研究主题']);
// 验证全空格
expect((0, intentService_1.generateSearchQueries)(' ')).toEqual(['通用研究主题']);
// 验证 null/undefined (通过 any 强制绕过 ts 检查,确保防御性编程生效)
expect((0, intentService_1.generateSearchQueries)(null)).toEqual(['通用研究主题']);
});
it('正常流:当输入包含多个有效关键词时,应当正确分词、去重并返回', () => {
// 包含重复的 "人工智能"
const query = '人工智能 机器学习 深度学习 人工智能';
const result = (0, intentService_1.generateSearchQueries)(query);
// 验证去重逻辑
expect(result).toEqual(['人工智能', '机器学习', '深度学习']);
expect(result).toHaveLength(3);
});
it('边界测试:当输入包含各种中英文标点符号时,应当正确替换为空格并分词', () => {
const query = 'AI芯片市场趋势2026;未来发展、产业格局';
const result = (0, intentService_1.generateSearchQueries)(query);
// 验证所有的标点都被视为了分隔符
expect(result).toEqual(['AI芯片', '市场趋势', '2026', '未来发展', '产业格局']);
});
it('边界测试:当有效检索词少于 3 个时,应当触发补全机制', () => {
const query = '量子计算';
const result = (0, intentService_1.generateSearchQueries)(query);
// 验证数量是否被补全到了 3 个
expect(result).toHaveLength(3);
// 验证第一项是有效词本身
expect(result[0]).toBe('量子计算');
// 验证后两项是被 "${query} 相关研究" 占位补全的
expect(result[1]).toBe('量子计算 相关研究');
expect(result[2]).toBe('量子计算 相关研究');
});
it('边界测试:当有效检索词超过 5 个时,应当执行截取操作', () => {
const query = '苹果 香蕉 橘子 葡萄 西瓜 芒果 樱桃';
const result = (0, intentService_1.generateSearchQueries)(query);
// 验证长度严格限制在 5
expect(result).toHaveLength(5);
// 验证截取的是前 5 个有效词
expect(result).toEqual(['苹果', '香蕉', '橘子', '葡萄', '西瓜']);
});
it('边界测试:应当自动过滤掉长度小于 2 的无意义单字(如停用词)', () => {
// "论"、"的"、"与" 长度均为 1应该被抛弃
const query = '论 AI 的 发展 与 IT 行业';
const result = (0, intentService_1.generateSearchQueries)(query);
// 验证单字被正确过滤
expect(result).toEqual(['AI', '发展', 'IT', '行业']);
});
});
//# sourceMappingURL=intentService.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"intentService.test.js","sourceRoot":"","sources":["../../../src/services/__test__/intentService.test.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,2 @@
"use strict";
//# sourceMappingURL=literatureService.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"literatureService.test.js","sourceRoot":"","sources":["../../../src/services/__test__/literatureService.test.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,144 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
// 1. 全局 Mock fs/promises 模块
jest.mock('fs/promises');
// 2. 全局 Mock personas.json提供测试专用的角色系统提示词
jest.mock('../../models/personas/personas.json', () => [
{ id: 'test_advisor', name: '测试顾问', systemPrompt: '这是一条测试专用的系统提示词' }
], { virtual: true });
describe('promptService', () => {
// 使用 any 替代 typeof import避免部分环境中未深度配置 ts-jest 导致的 Babel 解析报错
let promptService;
// 用于承载每次重置后的全新 fs mock 实例
let fsMock;
beforeEach(async () => {
jest.clearAllMocks(); // 清理 mock 的调用统计
jest.resetModules(); // 核心:重置模块注册表,清空 promptService 内部的 frameworksCache 状态
// 【关键修复】在 resetModules 之后,必须重新 require mock 的 fs 模块!
// 否则 promptService 内部引用的 fs 和这里外部的 fs 不是同一个实例,导致 mock 返回值失效并引发 TypeError。
fsMock = require('fs/promises');
promptService = await Promise.resolve().then(() => __importStar(require('../promptService')));
});
// 用于测试的伪造思维框架 JSON 数据
const mockFrameworkJson = {
name: "test_framework",
description: "用于测试的伪造框架",
parameters: [{ name: "topic", description: "测试主题", required: true }],
template: "请分析这个主题:{{topic}}",
persona: "test_advisor",
examples: [
{
input: { topic: "人工智能" },
output: "人工智能的分析结果"
}
]
};
describe('listFrameworks', () => {
it('应当成功读取并返回框架的元数据(且必须剔除 template 等内部字段)', async () => {
// Given: 使用重新获取的 fsMock 模拟文件系统的返回
fsMock.readdir.mockResolvedValueOnce(['test_framework.json']);
fsMock.readFile.mockResolvedValueOnce(JSON.stringify(mockFrameworkJson));
// When: 调用服务
const result = await promptService.listFrameworks();
// Then: 断言文件系统调用次数及返回值格式
expect(fsMock.readdir).toHaveBeenCalledTimes(1);
expect(fsMock.readFile).toHaveBeenCalledTimes(1);
expect(result).toHaveLength(1);
// 验证返回的元数据对象是否符合预期
expect(result[0]).toEqual({
name: "test_framework",
description: "用于测试的伪造框架",
parameters: [{ name: "topic", description: "测试主题", required: true }]
});
// 断言安全红线:不应该将模板内容暴露在 list 接口中
expect(result[0].template).toBeUndefined();
});
it('异常流:当框架目录读取失败时,应当安全捕获错误并返回空数组', async () => {
// 【新增优化】拦截并静音 console.error避免预期的错误日志污染终端测试面板
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
// 模拟磁盘异常
fsMock.readdir.mockRejectedValueOnce(new Error('目录不存在或无权限'));
const result = await promptService.listFrameworks();
// 验证业务逻辑:服务不会崩溃,而是返回容错的空数组
expect(result).toEqual([]);
// 验证系统确实捕获并打印了预期的错误
expect(consoleSpy).toHaveBeenCalledWith('[PromptService] 加载框架失败:', expect.any(Error));
// 测试完毕后恢复 console.error 的正常行为
consoleSpy.mockRestore();
});
});
describe('getFramework', () => {
beforeEach(() => {
// 为 getFramework 的测试统一挂载成功的 Mock 返回
fsMock.readdir.mockResolvedValue(['test_framework.json']);
fsMock.readFile.mockResolvedValue(JSON.stringify(mockFrameworkJson));
});
it('异常流:当请求不存在的框架名称时,应当抛出明确的错误', async () => {
await expect(promptService.getFramework('unknown_framework', {}))
.rejects.toThrow('框架 "unknown_framework" 不存在');
});
it('核心逻辑:应当正确解析角色,并组装 System、Few-Shot 及 Current User 消息序列', async () => {
// When: 请求框架,并传入动态参数
const result = await promptService.getFramework('test_framework', { topic: '云计算' });
const messages = result.messages;
// Then: 应当精确包含 4 条消息
// (1x System, 1x Example User, 1x Example Assistant, 1x Current User)
expect(messages).toHaveLength(4);
// 1. 验证 System 提示词 (是否成功匹配并读取到了 Mock 的 persona 数据)
expect(messages[0].role).toBe('system');
expect(messages[0].content.text).toBe('这是一条测试专用的系统提示词');
// 2. 验证 Few-Shot 示例中的 User 消息 (模板占位符是否被示例 input 正确替换)
expect(messages[1].role).toBe('user');
expect(messages[1].content.text).toBe('请分析这个主题:人工智能');
// 3. 验证 Few-Shot 示例中的 Assistant 消息
expect(messages[2].role).toBe('assistant');
expect(messages[2].content.text).toBe('人工智能的分析结果');
// 4. 验证当前用户请求 ({{topic}} 占位符是否被传入的 '云计算' 正确替换)
expect(messages[3].role).toBe('user');
expect(messages[3].content.text).toBe('请分析这个主题:云计算');
});
it('边界情况:如果多次调用 getFramework验证内部框架缓存 (frameworksCache) 是否生效', async () => {
// 第一次调用,会触发读盘
await promptService.getFramework('test_framework', { topic: '测试1' });
// 第二次调用,应该直接命中内部的 frameworksCache 缓存
await promptService.getFramework('test_framework', { topic: '测试2' });
// 断言:整个生命周期中,读盘操作应只有 1 次
expect(fsMock.readdir).toHaveBeenCalledTimes(1);
expect(fsMock.readFile).toHaveBeenCalledTimes(1);
});
});
});
//# sourceMappingURL=promptService.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"promptService.test.js","sourceRoot":"","sources":["../../../src/services/__test__/promptService.test.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,107 @@
"use strict";
/**
* Project Caffeine v0.1.1
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.listObsidianNotes = listObsidianNotes;
exports.readObsidianNote = readObsidianNote;
exports.saveNote = saveNote;
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
/**
* 本地知识库的根目录路径。
*
* 该目录存放所有 Markdown 笔记文件,所有文件操作均限定在此目录内,
* 以防止路径遍历攻击。
*
* @constant {string}
*/
const OBSIDIAN_VAULT_PATH = '/home/wguo/Downloads/MyVault'; // 【⚠️ 重要配置】请修改为你电脑上真实的 Markdown 笔记文件夹绝对路径!
/**
* 列出知识库中所有 Markdown 笔记的文件名。
*
* 该函数读取 OBSIDIAN_VAULT_PATH 目录下的所有文件,过滤出以 .md 结尾
* (不区分大小写)的文件,并返回文件名列表。若目录不存在或无权限访问,
* 则返回空数组并打印错误日志。
*
* @returns {Promise<string[]>} 包含所有笔记文件名的数组,若失败则返回空数组。
*/
async function listObsidianNotes() {
try {
const files = await promises_1.default.readdir(OBSIDIAN_VAULT_PATH);
return files.filter(file => file.toLowerCase().endsWith('.md'));
}
catch (error) {
console.error(`[Project Caffeine] 无法读取知识库目录: ${error.message}`);
return [];
}
}
/**
* 读取指定笔记文件的完整内容。
*
* 该函数首先对文件名进行安全校验,确保文件位于知识库目录内,
* 防止路径遍历攻击。校验通过后,读取文件内容并返回。
*
* @param {string} filename - 要读取的笔记文件名(必须包含 .md 后缀)
* @returns {Promise<string>} 笔记文件的文本内容
* @throws {Error} 当文件名导致路径越界时抛出安全警告
* @throws {Error} 当文件不存在或无权限读取时抛出错误
*/
async function readObsidianNote(filename) {
const targetPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH, filename);
const safeVaultPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH);
// 核心防御:防止大模型通过传入 "../../" 读取系统敏感文件
if (!targetPath.startsWith(safeVaultPath)) {
throw new Error(`安全警告:越权访问拦截!禁止读取目录外的文件: ${filename}`);
}
try {
const content = await promises_1.default.readFile(targetPath, 'utf-8');
return content;
}
catch (error) {
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} 当目录创建失败或文件写入失败时抛出错误
*/
async function saveNote(filename, content) {
// 1. 验证文件名是否以 .md 结尾
if (!filename.endsWith('.md')) {
throw new Error('文件名必须以 .md 结尾');
}
// 2. 防止路径遍历攻击:解析绝对路径,并检查是否在 NOTES_DIR 下
const fullPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH, filename);
const relative = path_1.default.relative(OBSIDIAN_VAULT_PATH, fullPath);
if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
throw new Error('无效的文件名,不允许访问上层目录');
}
// 3. 确保目标目录存在(可选,如果 NOTES_DIR 必须存在则可跳过)
await promises_1.default.mkdir(OBSIDIAN_VAULT_PATH, { recursive: true });
// 4. 写入文件
await promises_1.default.writeFile(fullPath, content, 'utf-8');
return `笔记已保存至: ${fullPath}`;
}
//# sourceMappingURL=resourceService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"resourceService.js","sourceRoot":"","sources":["../../../src/services/__test__/resourceService.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;AA2BH,8CAQC;AAcD,4CAeC;AAmBD,4BAmBC;AApGD,2DAA6B;AAC7B,gDAAwB;AAExB;;;;;;;GAOG;AAGH,MAAM,mBAAmB,GAAG,8BAA8B,CAAC,CAAC,2CAA2C;AAEvG;;;;;;;;GAQG;AAEI,KAAK,UAAU,iBAAiB;IACnC,IAAI,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,kBAAE,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACpD,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IACpE,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,iCAAiC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAChE,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AAEI,KAAK,UAAU,gBAAgB,CAAC,QAAgB;IACnD,MAAM,UAAU,GAAG,cAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;IAC/D,MAAM,aAAa,GAAG,cAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAExD,mCAAmC;IACnC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,kBAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACvD,OAAO,OAAO,CAAC;IACnB,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,WAAW,QAAQ,iBAAiB,CAAC,CAAC;IAC1D,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AAEI,KAAK,UAAU,QAAQ,CAAC,QAAgB,EAAE,OAAe;IAC9D,qBAAqB;IACrB,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACnC,CAAC;IAED,wCAAwC;IACxC,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,cAAI,CAAC,QAAQ,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;IAC9D,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,cAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;IACtC,CAAC;IAED,wCAAwC;IACxC,MAAM,kBAAE,CAAC,KAAK,CAAC,mBAAmB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEzD,UAAU;IACV,MAAM,kBAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC/C,OAAO,WAAW,QAAQ,EAAE,CAAC;AAC/B,CAAC"}

View File

@@ -0,0 +1,153 @@
"use strict";
/**
* Project Caffeine - Sprint 3
* 测试意图识别模式下的核心工具分发器 (Tools Controller)
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const toolsController_1 = require("../../controllers/toolsController");
const resourceService = __importStar(require("../../services/resourceService"));
const arxivService = __importStar(require("../../services/arxivService"));
const fs = __importStar(require("fs"));
// ====================================================
// 1. 全局 Mock 外部依赖 (防止测试时发生真实读写和网络请求)
// ====================================================
jest.mock('fs');
jest.mock('../../services/resourceService', () => ({
saveNote: jest.fn().mockResolvedValue('保存成功'),
listObsidianNotes: jest.fn().mockResolvedValue(['test-note.md', 'ai-trend.md']),
readObsidianNote: jest.fn().mockResolvedValue('这是模拟的本地笔记内容')
}));
jest.mock('../../services/arxivService', () => ({
searchArxiv: jest.fn().mockResolvedValue([
{ id: 'http://arxiv.org/abs/1234', title: 'Test Paper', summary: 'Mock Summary' }
])
}));
describe('Tools Controller - Sprint 3 (意图驱动与容错机制)', () => {
beforeEach(() => {
// 每次测试前清理 Mock 调用记录
jest.clearAllMocks();
});
// ====================================================
// 测试: 未知工具防呆
// ====================================================
it('当调用未知工具时,应返回 isError 为 true 的友好提示', async () => {
const result = await (0, toolsController_1.handleToolCall)('unknown_tool', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('未知工具: unknown_tool');
});
// ====================================================
// 测试: 意图 3 - 保存笔记工具 (重点测试大模型容错能力)
// ====================================================
describe('Tool: save_note (容错版保存工具)', () => {
it('当大模型老老实实传入【纯文本字符串】时,应直接正常保存', async () => {
const result = await (0, toolsController_1.handleToolCall)('save_note', { filename: 'output.md', content: '这是一段完美的Markdown文本' });
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toBe('保存成功');
expect(resourceService.saveNote).toHaveBeenCalledWith('output.md', '这是一段完美的Markdown文本');
});
it('🚨 容错测试:当大模型错误地传入【深度嵌套的 JSON 对象】时,应自动将其序列化,而不是崩溃', async () => {
// 模拟大模型发生幻觉,把整个分析对象当成了参数传进来
const mockJsonObject = {
title: "行业分析报告",
data: { trend: "上升", keywords: ["AI", "Quantum"] }
};
const result = await (0, toolsController_1.handleToolCall)('save_note', { filename: 'report.md', content: mockJsonObject });
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toBe('保存成功');
// 期望底层调用保存时Controller 已经非常聪明地把 JSON 对象转换为了带缩进的字符串
const expectedString = JSON.stringify(mockJsonObject, null, 2);
expect(resourceService.saveNote).toHaveBeenCalledWith('report.md', expectedString);
});
it('当缺少必要参数时,应优雅地返回错误信息', async () => {
const result = await (0, toolsController_1.handleToolCall)('save_note', { filename: 'error.md' }); // 故意漏掉 content
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('缺少 filename 或 content 参数');
});
});
// ====================================================
// 测试: 意图 2 - 获取思维框架模板
// ====================================================
describe('Tool: fetch_framework_template (抗死循环剥离器)', () => {
it('应该正确读取 JSON 文件,剥离外壳,并【强制附加刹车指令】防止大模型死循环', async () => {
// 伪造一个框架 JSON 文件内容
const mockFrameworkJson = JSON.stringify({
messages: [
{ role: 'system', content: { text: '我是系统架构师' } },
{ role: 'user', content: { text: '请按 SWOT 分析化验' } }
]
});
fs.readFileSync.mockReturnValue(mockFrameworkJson);
const result = await (0, toolsController_1.handleToolCall)('fetch_framework_template', { framework_name: 'swot' });
expect(result.isError).toBeUndefined();
const outputText = result.content[0].text;
// 断言:必须包含这句护身符,阻止大模型不断重复调工具
expect(outputText).toContain('立即停止调用任何工具');
// 断言JSON 外壳已经被剥离,直接透出了内部指导文字
expect(outputText).toContain('我是系统架构师');
expect(outputText).toContain('请按 SWOT 分析化验');
});
});
// ====================================================
// 测试: 意图 1 - 文献检索
// ====================================================
describe('Tool: search_arxiv', () => {
it('应该正确调用服务并格式化返回带 Markdown 语法的文献列表', async () => {
const result = await (0, toolsController_1.handleToolCall)('search_arxiv', { query: 'AI' });
expect(result.isError).toBeUndefined();
expect(arxivService.searchArxiv).toHaveBeenCalledWith('AI', 5);
const outputText = result.content[0].text;
expect(outputText).toContain('找到了关于 "AI" 的相关文献');
expect(outputText).toContain('Test Paper');
expect(outputText).toContain('http://arxiv.org/abs/1234');
});
});
// ====================================================
// 测试: 意图 4 - 本地笔记相关
// ====================================================
describe('Local Notes Tools', () => {
it('list_local_notes 应该正确返回笔记列表', async () => {
const result = await (0, toolsController_1.handleToolCall)('list_local_notes', {});
expect(result.content[0].text).toContain('test-note.md');
expect(result.content[0].text).toContain('ai-trend.md');
});
it('read_local_note 应该正确读取具体笔记内容', async () => {
const result = await (0, toolsController_1.handleToolCall)('read_local_note', { filename: 'test-note.md' });
expect(resourceService.readObsidianNote).toHaveBeenCalledWith('test-note.md');
expect(result.content[0].text).toContain('这是模拟的本地笔记内容');
});
});
});
//# sourceMappingURL=resourceService.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"resourceService.test.js","sourceRoot":"","sources":["../../../src/services/__test__/resourceService.test.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,45 @@
"use strict";
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.searchArxiv = searchArxiv;
/**
* 连接 Arxiv API 进行文献检索。
* * @param {string} query - 检索关键字
* @param {number} [maxResults=5] - 最大返回数量,默认为 5
* @returns {Promise<Array<{ id: string, title: string, summary: string }>>} 包含文献 ID、标题和摘要的数组
* @throws {Error} 若网络请求或 XML 解析失败则抛出错误
*/
async function searchArxiv(query, maxResults = 5) {
const url = `http://export.arxiv.org/api/query?search_query=all:${encodeURIComponent(query)}&start=0&max_results=${maxResults}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Arxiv API 响应错误: ${response.status}`);
}
const xmlData = await response.text();
// 使用正则提取 entry 节点(轻量级解析,避免引入繁重的 XML 解析库)
const entries = xmlData.split('<entry>').slice(1);
return entries.map(entry => {
const titleMatch = entry.match(/<title>([\s\S]*?)<\/title>/);
const summaryMatch = entry.match(/<summary>([\s\S]*?)<\/summary>/);
const idMatch = entry.match(/<id>([\s\S]*?)<\/id>/);
return {
id: idMatch ? idMatch[1].trim() : '未知链接',
title: titleMatch ? titleMatch[1].replace(/\s+/g, ' ').trim() : '无标题',
summary: summaryMatch ? summaryMatch[1].replace(/\s+/g, ' ').trim() : '无摘要'
};
});
}
catch (error) {
throw new Error(`获取文献失败: ${error.message}`);
}
}
//# sourceMappingURL=arxivService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"arxivService.js","sourceRoot":"","sources":["../../src/services/arxivService.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AASH,kCA2BC;AAlCD;;;;;;GAMG;AACI,KAAK,UAAU,WAAW,CAAC,KAAa,EAAE,aAAqB,CAAC;IACrE,MAAM,GAAG,GAAG,sDAAsD,kBAAkB,CAAC,KAAK,CAAC,wBAAwB,UAAU,EAAE,CAAC;IAEhI,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,mBAAmB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QACxD,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACtC,yCAAyC;QACzC,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAElD,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE;YACzB,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;YAC7D,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;YACnE,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;YAEpD,OAAO;gBACL,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM;gBACxC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK;gBACrE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK;aAC5E,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,WAAW,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC"}

View File

@@ -0,0 +1,46 @@
"use strict";
/**
* 意图分析服务:根据用户查询生成交互选项(硬编码)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzeIntent = analyzeIntent;
function analyzeIntent(query) {
const lowerQuery = query.toLowerCase();
// 检测是否包含具体框架名称(支持常见写法)
const frameworkMatch = lowerQuery.match(/\b(scqa|5whys|5w3h|swot|pestle)\b/);
const hasFramework = !!frameworkMatch;
// 检测是否包含明确的检索意图
const hasSearch = /论文|文献|检索|查找|找|paper|article|survey|literature|search/.test(lowerQuery);
let options = [];
let message = '';
if (hasFramework) {
// 用户指定了框架,直接提供分析选项
const framework = frameworkMatch[0].toLowerCase(); // 转为小写以匹配 prompt 名称
options = [{
id: 'A',
action: 'use_prompt', // 特殊标记,指示模型调用 prompt
params: { query, framework }
}];
message = `检测到您希望使用 ${framework} 框架进行分析。我将为您调用对应的 prompt 工具。请回复 A 确认开始分析。`;
}
else if (hasSearch) {
// 用户明确想检索
options = [{
id: 'B',
action: 'search_academic_literature',
params: { query, limit: 5 }
}];
message = `检测到您希望检索文献。请回复 B 确认检索。`;
}
else {
// 模糊查询,提供三个标准选项
options = [
{ id: 'A', action: 'use_prompt', params: { query } },
{ id: 'B', action: 'search_academic_literature', params: { query, limit: 5 } },
{ id: 'C', action: 'generate_search_queries', params: { query } }
];
message = `您希望我如何帮助您?请选择:\nA. 使用思维框架进行分析(可指定框架,如 SWOT、5Whys 等)\nB. 直接检索相关学术文献\nC. 先生成英文搜索关键词,再决定如何检索\n请回复 A、B 或 C。`;
}
return { message, options };
}
//# sourceMappingURL=intentAnalysisService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"intentAnalysisService.js","sourceRoot":"","sources":["../../src/services/intentAnalysisService.ts"],"names":[],"mappings":";AAAA;;GAEG;;AAEH,sCAyCC;AAzCD,SAAgB,aAAa,CAAC,KAAa;IACzC,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAEvC,uBAAuB;IACvB,MAAM,cAAc,GAAG,UAAU,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;IAC7E,MAAM,YAAY,GAAG,CAAC,CAAC,cAAc,CAAC;IAEtC,gBAAgB;IAChB,MAAM,SAAS,GAAG,sDAAsD,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAE1F,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,OAAO,GAAG,EAAE,CAAC;IAEjB,IAAI,YAAY,EAAE,CAAC;QACjB,mBAAmB;QACnB,MAAM,SAAS,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,oBAAoB;QACvE,OAAO,GAAG,CAAC;gBACT,EAAE,EAAE,GAAG;gBACP,MAAM,EAAE,YAAY,EAAE,qBAAqB;gBAC3C,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE;aAC7B,CAAC,CAAC;QACH,OAAO,GAAG,YAAY,SAAS,2CAA2C,CAAC;IAC7E,CAAC;SAAM,IAAI,SAAS,EAAE,CAAC;QACrB,UAAU;QACV,OAAO,GAAG,CAAC;gBACT,EAAE,EAAE,GAAG;gBACP,MAAM,EAAE,4BAA4B;gBACpC,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE;aAC5B,CAAC,CAAC;QACH,OAAO,GAAG,wBAAwB,CAAC;IACrC,CAAC;SAAM,CAAC;QACN,gBAAgB;QAChB,OAAO,GAAG;YACR,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE;YACpD,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,4BAA4B,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;YAC9E,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,yBAAyB,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE;SAClE,CAAC;QACF,OAAO,GAAG,yGAAyG,CAAC;IACtH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC"}

View File

@@ -0,0 +1,36 @@
"use strict";
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateSearchQueries = generateSearchQueries;
/**
* 将用户的自然语言查询拆解为专业检索词列表。
*
* @param {string} query - 用户原始查询字符串
* @returns {string[]} 去重后的检索词数组3~5 个)
*/
function generateSearchQueries(query) {
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);
}
//# sourceMappingURL=intentService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"intentService.js","sourceRoot":"","sources":["../../src/services/intentService.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAQH,sDAqBC;AA3BD;;;;;GAKG;AACH,SAAgB,qBAAqB,CAAC,KAAa;IACjD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpB,CAAC;IAED,oBAAoB;IACpB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;IAElD,kCAAkC;IAClC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;IAEpE,QAAQ;IACR,MAAM,WAAW,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;IAExC,yBAAyB;IACzB,OAAO,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9B,WAAW,CAAC,IAAI,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,eAAe;IACf,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACjC,CAAC"}

View File

@@ -0,0 +1,17 @@
"use strict";
/**
* Project Caffeine v0.1.1
* 文献检索服务(仅 arXiv
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveLiteratureToVault = void 0;
exports.searchAcademicLiterature = searchAcademicLiterature;
const apiClients_1 = require("../utils/apiClients");
const storageService_1 = require("../services/storageService");
Object.defineProperty(exports, "saveLiteratureToVault", { enumerable: true, get: function () { return storageService_1.saveLiteratureAsMarkdown; } });
async function searchAcademicLiterature(query, limit = 10, yearFrom, yearTo) {
const arxivResults = await (0, apiClients_1.fetchArXivPapers)(query, limit, yearFrom, yearTo);
// 如果后续需要添加其他源,可以在这里扩展,目前直接返回 arXiv 结果
return arxivResults.slice(0, limit);
}
//# sourceMappingURL=literatureService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"literatureService.js","sourceRoot":"","sources":["../../src/services/literatureService.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAMH,4DASC;AAbD,oDAAuD;AAEvD,+DAAsE;AAcjC,sGAd5B,yCAAwB,OAcyB;AAZnD,KAAK,UAAU,wBAAwB,CAC5C,KAAa,EACb,QAAgB,EAAE,EAClB,QAAiB,EACjB,MAAe;IAEf,MAAM,YAAY,GAAG,MAAM,IAAA,6BAAgB,EAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC5E,sCAAsC;IACtC,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AACtC,CAAC"}

View File

@@ -0,0 +1,105 @@
"use strict";
/**
* Project Caffeine v0.0.3
* 意图规划服务:分析用户查询,生成研究计划
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectFramework = detectFramework;
exports.extractSearchQuery = extractSearchQuery;
exports.planResearchApproach = planResearchApproach;
// 框架关键词映射表(仅保留专有标识)
const FRAMEWORK_KEYWORDS = {
scqa: ['scqa', 'SCQA'],
'5whys': ['5whys', '5 why', '5why', '五问法', '五问', '根因分析', 'root cause'],
'5w3h': ['5w3h', '5W3H', '五W三H'],
swot: ['swot', 'SWOT', '优势劣势', '机会威胁'],
pestle: ['pestle', 'PESTLE', '宏观环境', '政治经济']
};
/**
* 检测用户查询中是否包含特定框架的关键词,返回匹配的框架名称(第一个匹配的)
*/
function detectFramework(query) {
const lowerQuery = query.toLowerCase();
for (const [framework, keywords] of Object.entries(FRAMEWORK_KEYWORDS)) {
if (keywords.some(keyword => lowerQuery.includes(keyword.toLowerCase()))) {
return framework;
}
}
return null;
}
/**
* 提取可能的检索关键词简单策略原样返回后续可由LLM优化
*/
function extractSearchQuery(query) {
return query;
}
/**
* 分析用户查询,生成研究计划
*/
function planResearchApproach(query) {
const lowerQuery = query.toLowerCase();
const analysisKeywords = ['分析', '评估', '评价', '剖析', '诊断', '拆解', '研究', '探讨'];
const containsAnalysis = analysisKeywords.some(keyword => lowerQuery.includes(keyword));
const searchKeywords = ['论文', '文献', '研究', '最新进展', '综述', 'survey', 'paper', 'article', 'publication', '文献检索', '查找'];
const containsSearch = searchKeywords.some(keyword => lowerQuery.includes(keyword));
const detectedFramework = detectFramework(query);
const steps = [];
if (containsAnalysis || detectedFramework) {
let framework = detectedFramework || 'scqa';
let frameworkDescription = '';
const frameworkDescriptions = {
scqa: '使用 SCQA 框架对问题进行结构化分析',
'5whys': '使用 5 Whys 框架进行根因分析',
'5w3h': '使用 5W3H 框架全面拆解问题',
swot: '使用 SWOT 框架分析优势、劣势、机会、威胁',
pestle: '使用 PESTLE 框架分析宏观环境'
};
frameworkDescription = frameworkDescriptions[framework] || '使用思维框架分析问题';
const params = {};
if (framework === 'scqa') {
params.situation = query;
}
else if (framework === '5whys') {
params.problem = query;
}
else if (framework === '5w3h') {
params.topic = query;
}
else if (framework === 'swot') {
params.entity = query;
}
else if (framework === 'pestle') {
params.domain = query;
}
steps.push({
type: 'prompt',
tool: framework,
params,
description: frameworkDescription
});
}
if (containsSearch || (!containsAnalysis && !detectedFramework)) {
steps.push({
type: 'search',
tool: 'search_academic_literature',
params: {
query: extractSearchQuery(query),
limit: 5
},
description: '检索相关学术文献'
});
}
if (steps.length === 0) {
steps.push({
type: 'search',
tool: 'search_academic_literature',
params: {
query: extractSearchQuery(query),
limit: 5
},
description: '检索相关学术文献'
});
}
return { steps };
}
//# sourceMappingURL=planningService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"planningService.js","sourceRoot":"","sources":["../../src/services/planningService.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,154 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.listFrameworks = listFrameworks;
exports.getFramework = getFramework;
exports.getFrameworkTemplate = getFrameworkTemplate;
/**
* Project Caffeine v0.0.4
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
/** 框架定义文件存放的目录路径 */
const FRAMEWORKS_DIR = path_1.default.join(__dirname, '../models/frameworks');
/** 角色矩阵文件路径 */
const PERSONAS_PATH = path_1.default.join(__dirname, '../models/personas/personas.json');
/** 框架缓存,避免重复读取文件系统 */
let frameworksCache = null;
/** 角色缓存 */
let personasCache = null;
/**
* 加载角色矩阵personas.json
*
* @returns {Promise<any[]>} 角色数组,若加载失败则返回空数组
*/
async function loadPersonas() {
if (personasCache)
return personasCache;
try {
const content = await promises_1.default.readFile(PERSONAS_PATH, 'utf-8');
personasCache = JSON.parse(content);
return personasCache;
}
catch (error) {
console.error('[PromptService] 加载角色矩阵失败:', error);
return [];
}
}
/**
* 从文件系统加载所有框架 JSON 文件。
*
* @returns {Promise<Framework[]>} 框架对象数组,若加载失败则返回空数组
*/
async function loadFrameworks() {
if (frameworksCache)
return frameworksCache;
try {
const files = await promises_1.default.readdir(FRAMEWORKS_DIR);
const jsonFiles = files.filter(f => f.endsWith('.json'));
const frameworks = await Promise.all(jsonFiles.map(async (file) => {
const content = await promises_1.default.readFile(path_1.default.join(FRAMEWORKS_DIR, file), 'utf-8');
return JSON.parse(content);
}));
frameworksCache = frameworks;
return frameworks;
}
catch (error) {
console.error('[PromptService] 加载框架失败:', error);
return [];
}
}
/**
* 列出所有可用框架的元信息(不含模板、系统提示词和示例)。
*
* @returns {Promise<Array<Omit<Framework, 'template' | 'systemPrompt' | 'examples'>>>}
* 框架元信息列表,每个框架包含名称、描述和参数列表
*/
async function listFrameworks() {
const frameworks = await loadFrameworks();
return frameworks.map(({ name, description, parameters }) => ({
name,
description,
parameters
}));
}
/**
* 获取指定框架的完整提示词消息序列。
*
* @param {string} name - 框架名称
* @param {Record<string, string>} args - 用户传入的参数键值对
* @returns {Promise<PromptResult>} 符合 MCP 规范的消息序列对象
* @throws {Error} 当指定名称的框架不存在时抛出错误
*/
async function getFramework(name, args) {
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 personas = await loadPersonas();
const persona = personas.find(p => p.id === framework.persona);
if (persona) {
systemPrompt = persona.systemPrompt;
}
}
const messages = [];
if (systemPrompt) {
messages.push({
role: 'system',
content: { type: 'text', text: systemPrompt }
});
}
if (framework.examples && Array.isArray(framework.examples)) {
for (const example of framework.examples) {
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 }
});
}
}
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 };
}
/**
* 获取指定框架的完整定义(包括模板、示例、系统提示等)。
*
* @param {string} name - 框架名称
* @returns {Promise<Framework>} 完整的 Framework 对象
* @throws {Error} 如果框架不存在则抛出错误
*/
async function getFrameworkTemplate(name) {
const frameworks = await loadFrameworks();
const framework = frameworks.find(f => f.name === name);
if (!framework) {
throw new Error(`框架 "${name}" 不存在`);
}
return framework;
}
//# sourceMappingURL=promptService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"promptService.js","sourceRoot":"","sources":["../../src/services/promptService.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,64 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.researchAssistant = researchAssistant;
const literatureService_1 = require("./literatureService");
const promptService_1 = require("./promptService");
const planningService_1 = require("./planningService"); // 复用之前的逻辑
async function researchAssistant(query, maxPapers) {
const lowerQuery = query.toLowerCase();
let resultParts = [];
// 检测是否包含分析意图
const analysisKeywords = ['分析', '评估', 'swot', '5whys', 'scqa', 'pestle', '5w3h', '根因', '优势劣势'];
const hasAnalysis = analysisKeywords.some(kw => lowerQuery.includes(kw));
// 检测是否包含检索意图
const searchKeywords = ['论文', '文献', '检索', '查找', '找', 'paper', 'article', 'survey'];
const hasSearch = searchKeywords.some(kw => lowerQuery.includes(kw));
// 检测具体框架
const framework = (0, planningService_1.detectFramework)(query);
// 1. 如果需要分析,调用对应框架
if (hasAnalysis || framework) {
try {
const frameworkName = framework || 'scqa'; // 默认SCQA
const args = {};
// 根据框架填充参数
if (frameworkName === 'scqa')
args.situation = query;
else if (frameworkName === '5whys')
args.problem = query;
else if (frameworkName === '5w3h')
args.topic = query;
else if (frameworkName === 'swot')
args.entity = query;
else if (frameworkName === 'pestle')
args.domain = query;
const promptResult = await (0, promptService_1.getFramework)(frameworkName, args);
const analysisText = promptResult.messages
.filter(m => m.role === 'assistant')
.map(m => m.content.text)
.join('\n\n');
resultParts.push(`## 分析结果 (${frameworkName})\n\n${analysisText}`);
}
catch (error) {
resultParts.push(`## 分析失败\n\n${error}`);
}
}
// 2. 如果需要检索,调用文献检索
if (hasSearch || !hasAnalysis) { // 若没有分析则默认检索
try {
const searchQuery = (0, planningService_1.extractSearchQuery)(query); // 可复用之前的关键词提取逻辑
const papers = await (0, literatureService_1.searchAcademicLiterature)(searchQuery, maxPapers);
if (papers.length === 0) {
resultParts.push(`## 文献检索\n\n未找到相关文献。`);
}
else {
const papersList = papers.map(p => `- **${p.title}** (${p.publication_year})\n ${p.authors.join(', ')}\n [链接](${p.link})`).join('\n\n');
resultParts.push(`## 相关文献\n\n${papersList}`);
}
}
catch (error) {
resultParts.push(`## 文献检索失败\n\n${error}`);
}
}
return resultParts.join('\n\n---\n\n');
}
//# sourceMappingURL=researchAssistantService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"researchAssistantService.js","sourceRoot":"","sources":["../../src/services/researchAssistantService.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,124 @@
"use strict";
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.listObsidianNotes = listObsidianNotes;
exports.readObsidianNote = readObsidianNote;
exports.saveNote = saveNote;
exports.searchNotes = searchNotes;
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
/**
* 本地知识库的根目录路径。
* @constant {string}
*/
const OBSIDIAN_VAULT_PATH = '/home/wguo/Downloads/MyVault'; // 【⚠️ 重要配置】请修改为你电脑上真实的 Markdown 笔记文件夹绝对路径!
/**
* 列出知识库中所有 Markdown 笔记的文件名。
*
* @returns {Promise<string[]>} 包含所有笔记文件名的数组,若失败则返回空数组
*/
async function listObsidianNotes() {
try {
const files = await promises_1.default.readdir(OBSIDIAN_VAULT_PATH);
return files.filter(file => file.toLowerCase().endsWith('.md'));
}
catch (error) {
console.error(`[Project Caffeine] 无法读取知识库目录: ${error.message}`);
return [];
}
}
/**
* 读取指定笔记文件的完整内容。
*
* @param {string} filename - 要读取的笔记文件名(必须包含 .md 后缀)
* @returns {Promise<string>} 笔记文件的文本内容
* @throws {Error} 当文件名导致路径越界时抛出安全警告
* @throws {Error} 当文件不存在或无权限读取时抛出错误
*/
async function readObsidianNote(filename) {
const targetPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH, filename);
const safeVaultPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH);
// 核心防御:防止大模型通过传入 "../../" 读取系统敏感文件
if (!targetPath.startsWith(safeVaultPath)) {
throw new Error(`安全警告:越权访问拦截!禁止读取目录外的文件: ${filename}`);
}
try {
return await promises_1.default.readFile(targetPath, 'utf-8');
}
catch (error) {
throw new Error(`无法读取笔记 [${filename}]: 文件可能不存在或无权限。`);
}
}
/**
* 保存笔记到本地知识库。
*
* @param {string} filename - 笔记文件名(必须以 .md 结尾)
* @param {string} content - 笔记内容Markdown 格式)
* @returns {Promise<string>} 保存成功的提示信息,包含文件绝对路径
* @throws {Error} 当文件名不以 .md 结尾时抛出错误
* @throws {Error} 当文件名导致路径越界时抛出错误
* @throws {Error} 当目录创建失败或文件写入失败时抛出错误
*/
async function saveNote(filename, content) {
if (!filename.endsWith('.md')) {
throw new Error('文件名必须以 .md 结尾');
}
const fullPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH, filename);
const relative = path_1.default.relative(OBSIDIAN_VAULT_PATH, fullPath);
if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
throw new Error('无效的文件名,不允许访问上层目录');
}
await promises_1.default.mkdir(OBSIDIAN_VAULT_PATH, { recursive: true });
await promises_1.default.writeFile(fullPath, content, 'utf-8');
return `笔记已保存至: ${fullPath}`;
}
/**
* 在笔记内容中搜索关键词(忽略大小写)。
*
* @param {string[]} keywords - 关键词数组
* @returns {Promise<Array<{ filename: string; snippets: string[] }>>} 匹配的笔记列表,
* 每个包含文件名和上下文片段最多5条
*/
async function searchNotes(keywords) {
const files = await listObsidianNotes();
const results = [];
for (const file of files) {
try {
const content = await readObsidianNote(file);
const lowerContent = content.toLowerCase();
if (keywords.every(kw => lowerContent.includes(kw.toLowerCase()))) {
const snippets = [];
for (const kw of keywords) {
const index = lowerContent.indexOf(kw.toLowerCase());
if (index !== -1) {
const start = Math.max(0, index - 30);
const end = Math.min(content.length, index + kw.length + 30);
let snippet = content.substring(start, end);
if (start > 0)
snippet = '...' + snippet;
if (end < content.length)
snippet = snippet + '...';
snippets.push(snippet);
}
}
results.push({ filename: file, snippets });
}
}
catch (e) {
// 忽略读取失败的文件(如权限问题)
}
}
return results.slice(0, 5);
}
//# sourceMappingURL=resourceService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"resourceService.js","sourceRoot":"","sources":["../../src/services/resourceService.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,2 @@
"use strict";
//# sourceMappingURL=sessionMagement.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"sessionMagement.js","sourceRoot":"","sources":["../../src/services/sessionMagement.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,158 @@
"use strict";
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*
* 会话管理器:维护每个用户会话的状态,支持多轮交互流程控制。
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.sessionManager = exports.SessionManager = exports.SessionState = void 0;
/**
* 会话状态枚举
*/
var SessionState;
(function (SessionState) {
/** 等待用户选择意图 */
SessionState["AWAITING_INTENT"] = "AWAITING_INTENT";
/** 等待用户选择具体框架 */
SessionState["AWAITING_FRAMEWORK_SELECTION"] = "AWAITING_FRAMEWORK_SELECTION";
/** 等待用户提供框架参数 */
SessionState["AWAITING_FRAMEWORK_PARAMETERS"] = "AWAITING_FRAMEWORK_PARAMETERS";
/** 等待用户确认关键词 */
SessionState["AWAITING_KEYWORD_CONFIRMATION"] = "AWAITING_KEYWORD_CONFIRMATION";
/** 等待用户选择是否基于笔记分析 */
SessionState["AWAITING_NOTE_ANALYSIS_CONFIRMATION"] = "AWAITING_NOTE_ANALYSIS_CONFIRMATION";
/** 等待用户指定要读取的笔记 */
SessionState["AWAITING_NOTE_SELECTION"] = "AWAITING_NOTE_SELECTION";
/** 等待用户确认保存 */
SessionState["AWAITING_SAVE_CONFIRMATION"] = "AWAITING_SAVE_CONFIRMATION";
/** 空闲状态 */
SessionState["IDLE"] = "IDLE";
})(SessionState || (exports.SessionState = SessionState = {}));
/**
* 会话管理器类
*/
class SessionManager {
/**
* 创建会话管理器实例
* @param {number} timeout - 会话超时时间(毫秒),默认 1800000 (30分钟)
*/
constructor(timeout = 30 * 60 * 1000) {
this.sessions = new Map();
this.timeout = timeout;
}
/**
* 创建新会话
* @param {string} sessionId - 会话ID
* @returns {Session} 创建的会话对象
*/
createSession(sessionId) {
const session = {
id: sessionId,
state: SessionState.AWAITING_INTENT,
data: {},
lastUpdated: Date.now(),
};
this.sessions.set(sessionId, session);
return session;
}
/**
* 获取会话,如果不存在则创建
* @param {string} sessionId - 会话ID
* @returns {Session} 会话对象
*/
getOrCreateSession(sessionId) {
this.cleanupExpiredSessions();
let session = this.sessions.get(sessionId);
if (!session) {
session = this.createSession(sessionId);
}
else {
session.lastUpdated = Date.now();
}
return session;
}
/**
* 获取会话状态
* @param {string} sessionId - 会话ID
* @returns {SessionState | undefined} 会话状态,如果会话不存在则返回 undefined
*/
getSessionState(sessionId) {
return this.sessions.get(sessionId)?.state;
}
/**
* 更新会话状态
* @param {string} sessionId - 会话ID
* @param {SessionState} newState - 新状态
* @param {Partial<SessionData>} data - 要更新的数据(可选)
* @returns {boolean} 是否更新成功
*/
updateSession(sessionId, newState, data) {
const session = this.sessions.get(sessionId);
if (!session)
return false;
session.state = newState;
if (data) {
session.data = { ...session.data, ...data };
}
session.lastUpdated = Date.now();
return true;
}
/**
* 获取会话数据
* @param {string} sessionId - 会话ID
* @returns {SessionData | undefined} 会话数据
*/
getSessionData(sessionId) {
return this.sessions.get(sessionId)?.data;
}
/**
* 更新会话数据
* @param {string} sessionId - 会话ID
* @param {Partial<SessionData>} data - 要更新的数据
* @returns {boolean} 是否更新成功
*/
updateSessionData(sessionId, data) {
const session = this.sessions.get(sessionId);
if (!session)
return false;
session.data = { ...session.data, ...data };
session.lastUpdated = Date.now();
return true;
}
/**
* 删除会话
* @param {string} sessionId - 会话ID
* @returns {boolean} 是否删除成功
*/
deleteSession(sessionId) {
return this.sessions.delete(sessionId);
}
/**
* 清理过期会话
*/
cleanupExpiredSessions() {
const now = Date.now();
for (const [id, session] of this.sessions.entries()) {
if (now - session.lastUpdated > this.timeout) {
this.sessions.delete(id);
}
}
}
/**
* 获取所有会话数量(用于监控)
* @returns {number} 会话数量
*/
getSessionCount() {
return this.sessions.size;
}
}
exports.SessionManager = SessionManager;
// 导出默认实例(单例)
exports.sessionManager = new SessionManager();
//# sourceMappingURL=sessionManager.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"sessionManager.js","sourceRoot":"","sources":["../../src/services/sessionManager.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,57 @@
"use strict";
/**
* Project Caffeine v0.0.3
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveLiteratureAsMarkdown = saveLiteratureAsMarkdown;
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const yamlHelper_1 = require("../utils/yamlHelper");
const config_1 = __importDefault(require("../config/config"));
/**
* 将文献对象保存为 Markdown 文件(含 YAML Frontmatter
* @param literature 符合 Literature 接口的文献对象
* @param customFilename 可选的自定义文件名(不含扩展名)
* @param additionalTags 可选附加标签数组
* @returns 保存的文件的绝对路径
* @throws 当文件名非法或路径越界时抛出错误
*/
async function saveLiteratureAsMarkdown(literature, customFilename, additionalTags = []) {
const storageRoot = config_1.default.literatureStoragePath;
await promises_1.default.mkdir(storageRoot, { recursive: true });
let filename;
if (customFilename) {
if (!/^[a-zA-Z0-9_-]+$/.test(customFilename)) {
throw new Error('自定义文件名只能包含字母、数字、连字符和下划线');
}
filename = `${customFilename}.md`;
}
else {
const base = literature.title
.toLowerCase()
.replace(/[^\w\s]/g, '')
.replace(/\s+/g, '-')
.substring(0, 80);
filename = `${base}-${literature.publication_year || 'nodate'}.md`;
}
const fullPath = path_1.default.resolve(storageRoot, filename);
const relative = path_1.default.relative(storageRoot, fullPath);
if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
throw new Error('无效的文件名,不允许访问上层目录');
}
const yaml = (0, yamlHelper_1.generateYamlFrontmatter)(literature, additionalTags);
const markdownBody = literature.abstract || '暂无摘要。';
const content = `${yaml}\n\n${markdownBody}`;
await promises_1.default.writeFile(fullPath, content, 'utf-8');
return fullPath;
}
//# sourceMappingURL=storageService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"storageService.js","sourceRoot":"","sources":["../../src/services/storageService.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;AAgBH,4DAmCC;AAjDD,2DAA6B;AAC7B,gDAAwB;AACxB,oDAA8D;AAE9D,8DAAsC;AAEtC;;;;;;;GAOG;AACI,KAAK,UAAU,wBAAwB,CAC5C,UAAsB,EACtB,cAAuB,EACvB,iBAA2B,EAAE;IAE7B,MAAM,WAAW,GAAG,gBAAM,CAAC,qBAAqB,CAAC;IACjD,MAAM,kBAAE,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEjD,IAAI,QAAgB,CAAC;IACrB,IAAI,cAAc,EAAE,CAAC;QACnB,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;YAC7C,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7C,CAAC;QACD,QAAQ,GAAG,GAAG,cAAc,KAAK,CAAC;IACpC,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK;aAC1B,WAAW,EAAE;aACb,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;aACvB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;aACpB,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACpB,QAAQ,GAAG,GAAG,IAAI,IAAI,UAAU,CAAC,gBAAgB,IAAI,QAAQ,KAAK,CAAC;IACrE,CAAC;IAED,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,cAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACtD,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,cAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,IAAI,GAAG,IAAA,oCAAuB,EAAC,UAAU,EAAE,cAAc,CAAC,CAAC;IACjE,MAAM,YAAY,GAAG,UAAU,CAAC,QAAQ,IAAI,OAAO,CAAC;IACpD,MAAM,OAAO,GAAG,GAAG,IAAI,OAAO,YAAY,EAAE,CAAC;IAC7C,MAAM,kBAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAE/C,OAAO,QAAQ,CAAC;AAClB,CAAC"}

View File

@@ -0,0 +1,93 @@
"use strict";
// src/utils/apiClients.ts
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchArXivPapers = fetchArXivPapers;
const axios_1 = __importDefault(require("axios"));
function createClient(baseURL, rateLimitMs = 1000) {
const client = axios_1.default.create({ baseURL, timeout: 10000 });
let lastRequestTime = 0;
client.interceptors.request.use(async (config) => {
const now = Date.now();
const timeSinceLast = now - lastRequestTime;
if (timeSinceLast < rateLimitMs) {
await new Promise(resolve => setTimeout(resolve, rateLimitMs - timeSinceLast));
}
lastRequestTime = Date.now();
return config;
});
return client;
}
const arxivClient = createClient('http://export.arxiv.org/api/', 1000);
/**
* 构建 arXiv 搜索查询字符串
* - 如果查询包含布尔运算符 (AND, OR, NOT) 或引号,则原样传递(支持高级语法)
* - 否则,将空格分隔的词用 OR 连接,以扩大匹配范围
*/
function buildArxivQuery(rawQuery) {
// 去除首尾空格
const trimmed = rawQuery.trim();
if (!trimmed)
return '';
// 检测是否包含高级语法AND/OR/NOT 或双引号
const hasAdvanced = /\b(AND|OR|NOT)\b|"/.test(trimmed);
if (hasAdvanced) {
return trimmed; // 原样返回,交给 arXiv 解析
}
// 普通查询:将空格拆分为词,用 OR 连接
const words = trimmed.split(/\s+/).filter(w => w.length > 0);
if (words.length === 1) {
return `all:${words[0]}`;
}
return words.map(w => `all:${w}`).join(' OR ');
}
async function fetchArXivPapers(query, limit, yearFrom, yearTo) {
try {
const arxivQuery = buildArxivQuery(query);
if (!arxivQuery)
return [];
// 多取一些结果,因为 OR 可能返回较多论文,确保年份过滤后仍有足够数量
const params = {
search_query: arxivQuery,
start: 0,
max_results: limit * 2,
sortBy: 'lastUpdatedDate',
sortOrder: 'descending',
};
const response = await arxivClient.get('query', { params });
const xml = response.data;
const entries = xml.split('<entry>').slice(1);
const papers = [];
for (const entry of entries) {
const titleMatch = entry.match(/<title>(.*?)<\/title>/);
const authorMatches = [...entry.matchAll(/<author><name>(.*?)<\/name><\/author>/g)];
const summaryMatch = entry.match(/<summary>(.*?)<\/summary>/s);
const idMatch = entry.match(/<id>(.*?)<\/id>/);
const publishedMatch = entry.match(/<published>(.*?)<\/published>/);
const year = publishedMatch ? new Date(publishedMatch[1]).getFullYear() : undefined;
if (yearFrom && year && year < yearFrom)
continue;
if (yearTo && year && year > yearTo)
continue;
papers.push({
title: titleMatch ? titleMatch[1].trim() : '无标题',
authors: authorMatches.map(m => m[1].trim()),
abstract: summaryMatch ? summaryMatch[1].trim() : undefined,
link: idMatch ? idMatch[1].trim() : undefined,
publication_year: year,
citation_count: 0,
source: 'arXiv',
doi: undefined,
});
}
// 返回前 limit 条
return papers.slice(0, limit);
}
catch (error) {
console.error('[apiClients] arXiv 请求失败:', error);
return [];
}
}
//# sourceMappingURL=apiClients.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"apiClients.js","sourceRoot":"","sources":["../../src/utils/apiClients.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,42 @@
"use strict";
/**
* Project Caffeine v0.1.1
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateYamlFrontmatter = generateYamlFrontmatter;
const yaml_1 = __importDefault(require("yaml"));
/**
* 根据文献对象和附加标签生成 YAML Frontmatter 字符串。
* @param literature 文献对象
* @param additionalTags 附加标签
* @returns 包含 YAML 文档的字符串(以 "---\n" 包围)
*/
function generateYamlFrontmatter(literature, additionalTags = []) {
const frontmatter = {
title: literature.title,
authors: literature.authors,
abstract: literature.abstract,
publication_year: literature.publication_year,
citation_count: literature.citation_count,
source: literature.source,
doi: literature.doi,
date_saved: new Date().toISOString().split('T')[0],
};
const tags = [...(literature.keywords || []), ...additionalTags];
if (tags.length > 0) {
frontmatter.tags = tags;
}
const cleanFrontmatter = Object.fromEntries(Object.entries(frontmatter).filter(([_, v]) => v !== undefined && v !== null));
const yamlStr = yaml_1.default.stringify(cleanFrontmatter, { indent: 2 });
return `---\n${yamlStr}---`;
}
//# sourceMappingURL=yamlHelper.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"yamlHelper.js","sourceRoot":"","sources":["../../src/utils/yamlHelper.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;AAWH,0DAuBC;AA/BD,gDAAwB;AAExB;;;;;GAKG;AACH,SAAgB,uBAAuB,CAAC,UAAsB,EAAE,iBAA2B,EAAE;IAC3F,MAAM,WAAW,GAAwB;QACvC,KAAK,EAAE,UAAU,CAAC,KAAK;QACvB,OAAO,EAAE,UAAU,CAAC,OAAO;QAC3B,QAAQ,EAAE,UAAU,CAAC,QAAQ;QAC7B,gBAAgB,EAAE,UAAU,CAAC,gBAAgB;QAC7C,cAAc,EAAE,UAAU,CAAC,cAAc;QACzC,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,GAAG,EAAE,UAAU,CAAC,GAAG;QACnB,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KACnD,CAAC;IAEF,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,GAAG,cAAc,CAAC,CAAC;IACjE,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,WAAW,CAAC,IAAI,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED,MAAM,gBAAgB,GAAG,MAAM,CAAC,WAAW,CACzC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI,CAAC,CAC9E,CAAC;IAEF,MAAM,OAAO,GAAG,cAAI,CAAC,SAAS,CAAC,gBAAgB,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;IAChE,OAAO,QAAQ,OAAO,KAAK,CAAC;AAC9B,CAAC"}

View File

@@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['/node_modules/', '/dist/'] // 忽略 dist 目录!
};

View File

@@ -0,0 +1 @@
../baseline-browser-mapping/dist/cli.cjs

View File

@@ -0,0 +1 @@
../browserslist/cli.js

View File

@@ -0,0 +1 @@
../copyfiles/copyfiles

1
projects/arabica/src/sprint3/node_modules/.bin/copyup generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../copyfiles/copyfiles

View File

@@ -0,0 +1 @@
../create-jest/bin/create-jest.js

1
projects/arabica/src/sprint3/node_modules/.bin/esparse generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../esprima/bin/esparse.js

View File

@@ -0,0 +1 @@
../esprima/bin/esvalidate.js

View File

@@ -0,0 +1 @@
../handlebars/bin/handlebars

View File

@@ -0,0 +1 @@
../import-local/fixtures/cli.js

1
projects/arabica/src/sprint3/node_modules/.bin/jest generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../jest/bin/jest.js

Some files were not shown because too many files have changed in this diff Show More