git:20261031.a2c8fce to git:20261101.41ab48b

1 added, 1 removed. Audit B to B.

# 观澜 (GuānLán) — 设计文档
> 《孟子·尽心上》"观水有术,必观其澜"
>
> 在信息的汪洋中洞察脉络与趋势。
---
## 1. 项目目标
观澜是 [Karpathy LLM Wiki 模式](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) 的一个实现:让 LLM Agent **增量地构建并持续维护一个结构化、互相链接的知识 wiki**,而不是每次提问都从原始文档里临时检索(传统 RAG)。知识被"编译"一次后持续保鲜,随每一篇新资料、每一次提问而复利增长。
**演进路线**
1. **个人版(先做)** —— 以 **Skills** 的形式接入 Agent,纯文件(Markdown),通过 **Agentao CLI + 极薄的 `guanlan` 包装器** 跑 ingest/query。这是 MVP,也是验证理念的最小闭环,**不依赖任何后端服务**。
- **MVP 只承诺 Agentao + `guanlan` wrapper。** 设计落点是 Agentao 专有机制(`AGENTAO.md`、skill discovery、agent shell 调用脚本)与观澜自己的确定性门禁。其他 Agent CLI(Claude Code / Codex 等)因 wiki 是纯 markdown 而**可能适配**,但不进入 MVP 设计承诺。
- Web UI 是 MVP 之后(P4)叠加的**可选宿主层**,不改变"纯文件"的本质——它只是 `ingest` 与问答的另一个入口。无 Web UI 时,整套东西用 CLI 一样跑通。
2. **企业版(后续)** —— 在个人版之上演进,支持**大数据量**、**多租户权限**、**7×24 自动富化**(技术栈不预先绑定,见 §6)。
**技术底座**
- **Powered by Agentao** —— 受治理的 Agent 运行时(Python,可嵌入 / CLI / ACP server)。观澜不自己造 Agent 内核,而是把"知识 wiki 的维护逻辑"做成 Agentao 的 **Skill + Python 脚本**(MVP,CLI 即可跑);自定义 Tool 注入则留作 P4 之后的按需项。进程内嵌入在 P4 首次落地:**写** `ingest` 仍走 `agentao run` 子进程,**Web 问答**(含单轮与多轮)走只读进程内嵌入(见 [`P4-Web宿主.md`](P4-Web宿主.md))。
- **观澜 Web 宿主(P4,可选)** —— 一个复用同一文件库的可选本地 Web 宿主,给 CLI 之外加一个图形入口(FastAPI/uvicorn 薄表现层)。**读写分线**:唯一写作业 `ingest` 复用 P2 子进程 + 单写者门禁;**所有问答**(一次性单轮与多轮对话)因 agentao CLI 无 session resume 而统一走进程内嵌入 `Agentao`(默认只读、token 流式,无 `/api/query`)。它不是 MVP 前置依赖;技术细节见 [`P4-Web宿主.md`](P4-Web宿主.md) 与**附录 A**。
### 核心设计原则
1. **markdown 始终是唯一事实来源(source of truth)。** 整个知识库就是一组本地 markdown 文件;任何数据库 / 向量索引 / 图谱 / 缓存都是**可从 markdown 完全、幂等重建的派生物**,绝不反向成为权威。git 只用于观澜项目源码与模板版本管理;用户 wiki 数据默认不纳入观澜 git,是否另行版本化由用户自己决定。
2. **Agent 全权拥有 wiki 层,人不直接写 wiki。** 人负责投喂资料、提问、给方向;记账(摘要、交叉引用、归档)全交给 Agent。
3. **`raw/` 只读不可变。** Agent 只读原始资料,永不修改,保证事实可追溯。
4. **确定性优先,LLM 调用统一走 Agentao。** 能用脚本确定性完成的(结构检查、wikilink 解析、格式转换)不调 LLM;需 LLM 的步骤统一经 Agentao runtime 治理。
---
## 2. 核心理念(Karpathy LLM Wiki)
传统 RAG 在每次查询时检索原始文档片段,LLM 每次都要"从零重新发现"知识,没有积累。观澜的不同之处:**wiki 是一个持久、复利的产物**——交叉引用已经建好,矛盾已经被标记,综述已经反映了你读过的一切。
三层架构(来自原始 pattern,见 [`../llm-wiki/llm-wiki.md`](../llm-wiki/llm-wiki.md)):
| 层 | 内容 | 谁来写 | 可变性 |
|----|------|--------|--------|
| **Raw sources(原始资料)** | 文章、论文、图片、数据文件 | 用户投喂 | 只读,事实来源,Agent 永不修改 |
| **Wiki(知识层)** | LLM 生成的 markdown 页面:摘要、实体页、概念页、综述 | Agent 全权拥有 | Agent 创建/更新/维护交叉引用 |
| **Schema(配置/规约)** | 告诉 Agent wiki 如何组织、约定、工作流 | 人与 Agent 共同演进 | 本库 schema 在根级 `SCHEMA.md`(跟数据走)+ 维护引擎在 skill;`AGENTAO.md` 为行为约定层(详见 §4.3) |
三类核心操作:
- **Ingest(摄入)**:用户投放新资料 → Agent 阅读、抽取要点、写摘要页、更新 index、修订相关实体/概念页、追加 log。一篇资料可能触及 10–15 个页面。
- **Query(查询)**:对 wiki 提问 → Agent 先读 index 定位相关页 → 综合出带引用的答案。**好的答案可以回填进 wiki**(`syntheses/`),让探索也复利。
- **Lint(体检)**:周期性健康检查 —— 结构类(孤儿页、断链、缺失的实体页/交叉引用)先做;语义类(矛盾、过期论断、可补的资料缺口,需 LLM)后续再加(见 §4.4 / §8)。
> 核心洞察:"维护知识库最累的不是阅读或思考,而是记账(bookkeeping)。" LLM 不会厌烦、不会忘记更新交叉引用、能一次改 15 个文件——维护成本趋近于零,wiki 才能持续保鲜。
---
## 3. 参考实现对比
我们研究了三个同源实现,观澜各取所长:
| 项目 | 形态 | 技术栈 | 观澜借鉴什么 |
|------|------|--------|--------------|
| **[SamurAIGPT/llm-wiki-agent](../llm-wiki/llm-wiki-agent)** | 纯 Agent Skill(无后端) | Markdown + `CLAUDE.md` + slash commands + Python `tools/` | **个人版的直接蓝本**:wiki 目录结构、5 个工作流、wikilink graph 构建思路、frontmatter/wikilink 约定、CJK 支持 |
| **[nashsu/llm_wiki](../llm-wiki/llm_wiki)** | 跨平台桌面应用 | Tauri(Rust) + React + LanceDB | 产品化思路:知识图谱可视化、向量+关键词混合检索、Web Clipper、本地 HTTP API + Agent Skill |
| **[garrytan/gbrain](../llm-wiki/gbrain)** | 生产级常驻 daemon | TypeScript/Bun + PGLite/Postgres+pgvector | **企业版蓝本**:类型化知识图谱(零 LLM 调用抽边)、混合检索+重排、OAuth 多租户 source 隔离、7×24 dream cycle |
**观澜的差异化定位**:不重造桌面应用(nashsu),而是把 wiki 维护逻辑做成 **Agentao 原生 Skill**,复用 Agentao 的运行时治理(权限、可观测、MCP);个人版即 llm-wiki-agent 的"Agentao 化 + 增强"(CLI/Skill 优先),企业版参考 gbrain 的问题域与经验。
---
## 4. 个人版设计(MVP)
### 4.1 形态
**MVP = 纯文件 + Skill + 极薄包装器,无后端服务。** 整个知识库就是一个本地目录里的 markdown 文件,Agentao runtime 通过 Skill 读写它;`guanlan` CLI 只负责 init、调用 Agentao、运行确定性门禁与输出结果,不承载业务智能。
**维护引擎与知识库分离。** 观澜的 skill + 脚本(维护引擎)由**观澜项目/安装包**提供并安装一次,**不复制进每个知识库**;用户知识库目录只放数据与本库配置。`guanlan init` 在空目录生成最小模板。
### 4.2 目录结构
用户知识库目录(`guanlan init` 生成的最小模板;默认不纳入观澜项目 git):
```
my-wiki/ # 用户的知识库(普通工作目录;可由用户自行版本化)
├── AGENTAO.md # ← Agent 行为约束 + 指针(指向 guanlan-wiki skill 与 SCHEMA.md)
├── SCHEMA.md # ← 第三层 Schema:本库领域/主题/启用页面类型/本库约定(与 raw/、wiki/ 平级;排除出 index/graph/lint)
├── raw/ # 第一层:原始资料(只读,事实来源)
├── wiki/ # 第二层:Agent 全权生成的知识层(纯页面,无 config 混入)
│ ├── index.md # 全量页面目录,每次 ingest 更新
│ ├── log.md # append-only 时间线:## [YYYY-MM-DD] <op> | <title>
│ ├── overview.md # 跨资料的活体综述
│ ├── sources/ entities/ concepts/ syntheses/ # 各类页面(自动创建)
└── graph/ # 确定性 wikilink graph 产物(graph.json + graph.html,P3)
```
> 维护引擎在**观澜项目仓库**里(不复制进用户库):`skills/guanlan-wiki/`(`SKILL.md` + `references/{conventions,heal,audit}.md`);**确定性校验 `guanlan check` 在 `guanlan` 包内、不在 skill**(§4.6 / 决策1),P3 的 health/lint/graph **同样落包内**(`guanlan health/lint/graph`,见 [`P3-健康与图谱.md`](P3-健康与图谱.md) 决策P3-1)。两种运行方式,**各只对应一条路径,不混用**:
>
> - **开发期 = 仓库根即 sample wiki**:`working_directory` 直接设为观澜仓库根,仓库根同时放一份 sample wiki(`raw/`、`wiki/`、`SCHEMA.md`、`AGENTAO.md`)。此时 `skills/guanlan-wiki/` 正好命中 Agentao 的 repo-root 发现路径(`<wd>/skills/`),wiki 数据也在同一目录——免安装、改完即生效。
> - **外部真实 wiki = 全局安装**:把 skill 装入 `~/.agentao/skills/guanlan-wiki/`(cwd 无关),再以用户库为 `working_directory` 运行。用户库(`guanlan init` 生成)保持最小、只含数据与本库配置。
>
> 不存在"从外部 wiki 目录发现观澜仓库 skills/"的情形——外部 wiki 一律走全局安装,避免引入额外路径机制。
观澜项目仓库结构(引擎源码;开发期仓库根兼作 sample wiki):
```
guanlan/ # 观澜项目仓库(本仓库);开发期 working_directory 即此
├── skills/guanlan-wiki/ # ← 维护引擎(repo-root 路径,开发期自动发现)
│ ├── SKILL.md # 共用规则 + ingest/query 工作流
│ └── references/ # conventions.md(默认约定)、heal.md、audit.md(低频工作流,按需读)
│ # P2 无 scripts/:guanlan check 在包内(决策1);P3 health/lint/graph 同落包内(决策P3-1)
├── guanlan/ # ← 包:cli / init / runtime / gate / check / ingest / query(P2)
├── examples/ # ← guanlan init 模板(入库,供参考)
│ ├── AGENTAO.md
│ └── SCHEMA.md
├── AGENTAO.md SCHEMA.md # ← 开发期由 examples/ 拷入的 sample 配置(可能含本机路径/开发者私有设置,.gitignore,不入库)
├── raw/ wiki/ graph/ # ← sample 数据与派生(仅开发用,.gitignore,不提交远程)
└── docs/DESIGN.md
```
### 4.3 Schema / 约定放在哪里
Schema 是 Karpathy 三层之一,与 `raw/`、`wiki/` 平级,因此落为**仓库根的一等文件 `SCHEMA.md`**——不塞进 `wiki/`(会污染 Agent 全权生成的内容层),也不塞进 `AGENTAO.md`(厂商相关、每 session 全量入上下文)。三类约定按职责分家:
- **`SCHEMA.md`(仓库根)** —— **本库约定**:领域/主题、启用的页面类型、本库自定义规则、演进中的论点。每库不同、人与 Agent co-evolve、随 markdown 文件走、自描述。ingest/query 时读,且与 `index.md`/`log.md`/`overview.md` 一样**排除出 index/graph/lint 扫描**(config 非 content)。
- **`AGENTAO.md`(仓库根)** —— **Agent 行为约束**:角色 + 硬约束(永不改 `raw/`、markdown 是事实来源、页面必带 frontmatter、术语转 `[[wikilink]]`、query 答案必引来源)+ 运行时偏好 + 指针("遵循 `guanlan-wiki` skill;本库 schema 见 `SCHEMA.md`")。只放精简硬规则(每 session 自动入上下文),大块可复用约定下沉 skill。
- **`guanlan-wiki/` skill(观澜安装包提供,不在用户库内)** —— **维护引擎**:`SKILL.md`(共用规则与 ingest/query 工作流)+ `references/heal.md`、`references/audit.md`(低频工作流,运行时先读)+ `references/conventions.md`(通用默认约定:页面类型/frontmatter/命名/index·log 格式/模板,按需载入,`SCHEMA.md` 可覆盖)。wiki-无关、可复用、可独立升级。**P2 不含 `scripts/`**:确定性校验经包内 `guanlan check`(§4.6 / `P2-最小闭环.md` 决策1);skill 只 shell 调它。
一句话:**本库约定 → `SCHEMA.md`;行为约束 → `AGENTAO.md`;工作流 → skill;写操作门禁 → `guanlan` wrapper。** 知识库本体(`SCHEMA.md` + `wiki/`)agent-无关、可移植;MVP 宿主只承诺 Agentao + `guanlan` wrapper,未来可适配其他 Agent CLI(见 §1)。
### 4.4 工作流
**MVP 最小闭环(P1–P2)= init + ingest(仅 `.md`)+ query + 基础校验。** 这是"能在 CLI 里稳定摄入和查询的文件型知识库"的最小集;health / lint / `graph.html` 是 P3 的增强,不是 MVP 核心。
> **P2 的实现级细化见 [`P2-最小闭环.md`](P2-最小闭环.md)**:模块落点、`raw/` 快照与 check 的数据结构/契约、退出码、prompt、Agentao 集成与测试计划,以及对本文若干处的具体化决策(§4.6 check 落点、§4.7 快照算法等)。
| 工作流 | 触发 | 步骤要点 | 阶段 |
|--------|------|----------|------|
| **init** | `guanlan init` | 在空目录生成最小模板:`AGENTAO.md` / `SCHEMA.md` / `raw/` / `wiki/`(含 index·log·overview) | P1 |
| **ingest** | `guanlan ingest raw/x.md` | wrapper 调用 Agentao + skill:读 `.md` 源→读 index/overview→写 source 页→更新 index/overview→建/更新实体&概念页→标记矛盾(格式见 §4.5)→追加 log→wrapper 强制跑门禁。**单轮优先,必要时分步**(不承诺固定调用次数;一篇资料可能触及多页) | P2 |
| **query** | `guanlan query "…"` | wrapper 调用 Agentao + skill:读 index 定位→读相关页→综合带 `[[页]]` 引用的答案;`index.md` + 2-gram 粗召回不中时,Agent 可扫相关目录或请用户补关键词(graceful fallback)。默认只读;如要回填 `syntheses/`,走显式 `guanlan query --backfill "…"` 并同样跑门禁 | P2 |
| **基础校验** | ingest / query backfill 收尾**强制**运行 / `guanlan check` | 独立 `guanlan check` 只做 frontmatter + 断链 + `sources` 解析;ingest / `--backfill` 的 wrapper 门禁在此之上**额外**做 `raw/` 调用前后快照比对(独立 check 无"调用前"基线,故不含此项)。**零 LLM**,见 §4.7 | P2 |
| **health** | `health` | 结构检查:空页/桩页、index 与磁盘同步、source 页在 `raw/` 有无原文。**零 LLM** | P3 |
| **lint** | `lint` | **只做结构 lint**:孤儿页、断链、缺失实体页。**零 LLM** | P3 |
| **graph** | `build graph` | **只做确定性**:解析 `[[wikilink]]`→边,输出 `graph.json` + 自包含 `graph.html`。**零 LLM** | P3 |
> LLM 只用于 **ingest / query**(单轮优先、必要时分步);其余工作流全部零 LLM。
>
> **MVP ingest 只承诺 `.md`**,多格式推到 **P5**。**语义 lint**(矛盾/过期/资料缺口,需 LLM)、**graph 增强**(LLM 推断边、Louvain、增量缓存)、**图感知语义 lint** 均**不进 P3**,作为后续优化项(见 §8)。P3 的 `guanlan lint` 本身就是**图感知的确定性结构 lint**(孤儿/断链/缺失实体,复用 wikilink graph,零 LLM);先用确定性的 health + 结构 lint + wikilink graph 快速闭环,避免过早引入 LLM 成本。
### 4.5 数据约定
**页面 frontmatter(统一)**
```yaml
---
title: '页面标题' # 字符串值用单引号(决策7:根除双引号嵌套的 YAML 解析失败)
type: source | entity | concept | synthesis
tags: []
sources: [] # 支撑本页的 source slug 列表
last_updated: YYYY-MM-DD
---
```
- **命名**:source 用 `kebab-case`(同源文件名);entity/concept 用 `TitleCase.md`。
- **wikilink**:正文用 `[[PageName]]`,大小写不敏感按 stem 解析;ingest 后校验断链。
- **index.md**:按 Overview / Sources / Entities / Concepts / Syntheses 分区,每行 `- [标题](路径) — 一句话`(Overview 区仅链到 `overview.md`,不重复其正文)。
- **log.md**:`## [YYYY-MM-DD] <op> | <title>`,可被 `grep "^## \[" wiki/log.md | tail` 解析。
- **CJK**:query 对中文用 2-gram 滑窗匹配(参考 llm-wiki-agent 的 `examples/cjk-showcase`);粗召回不足时走上述 query 的目录扫描/补词兜底,按需升级路径见 §8(**不优先上分词**)。
- **冲突与待核标记(卖点核心,必须有确定格式)**:ingest 时若 LLM 发现新资料与既有页冲突,或新资料有影响论断的具体缺口,**就地**在相关页维护一节 `## ⚠️ 资料冲突与待核`(v0.1.30 前称 `## ⚠️ 矛盾与存疑`),每条一行:
`- <断言 A>([[source-a]])↔ <断言 B>([[source-b]])— <open|resolved> · <备注>`(冲突,双边)或
`- <待核断言>([[source]])— <open|resolved> · 待核 · <证据缺口及需核对内容>`(待核,单边;只收影响论断可信度或引用的具体缺口);
并向 `log.md` 追加一条(标题表示有冲突或待核记录,有无未决事项以 `open` 条目为准,不另存 frontmatter 布尔)。**ingest 只做"发现即就地标记"**;系统性复检与状态流转(open→resolved、过期论断、跨页矛盾发现)属语义 lint,P3 之后再加(见 §4.4 注 / §8)——P3 的确定性 health/lint **不统计矛盾**。
### 4.6 工具(确定性脚本,Python 3.12;P2 的 check 在 `guanlan` 包内)
- **确定性脚本(零 LLM)**:check(**P2 基础校验**:frontmatter + 断链 + `sources` 解析;`raw/` 不变性由 wrapper 快照比对兜底,见 §4.7,不在脚本里巡检)、health(**P3** 结构检查:空页/桩页、index 同步、source 页原文)、lint(**P3** 图感知结构 lint:孤儿/断链/缺失实体)、graph(**P3** 确定性 wikilink graph + 自包含 `graph.html`)。P2 只交付 check,避免把 P3 的 health/lint 提前塞进 P2;P3 三者落点与契约见 [`P3-健康与图谱.md`](P3-健康与图谱.md)。
- **check 的唯一实现落在 `guanlan` 包内(`guanlan/check.py`),暴露为 `guanlan check` 子命令与 `python -m guanlan.check`**;wrapper 门禁直接 import 同一函数,skill 指示 agent shell 调 `guanlan check`。不在 skill `scripts/` 下另写一份——否则开发期(repo `skills/`)与安装期(`~/.agentao/skills/`)skill 路径不同会逼 wrapper 解析该路径并引入双实现分叉。理由与契约见 [`P2-最小闭环.md`](P2-最小闭环.md) §10 决策1。
- **ingest / query 的 LLM 部分不写成脚本** —— 由 `guanlan-wiki` skill + `agentao run` 子进程完成(P2 路线,见 §5.1 / `P2-最小闭环.md` §4);in-process 嵌入(`Agentao.chat()` / `LLMClient`)留待 P4。脚本只做确定性工作。(结构 lint 为零 LLM 的结构检查,属 P3;语义 lint 以后再加,见 §8。)
- **推到 P5(已实现)**:多格式转换 adapter(统一接口 `convert(file) -> md`)。**P5.2 `guanlan convert` 已落地**——改为复用既有 `pdf-to-markdown` skill(MinerU→marker→pypdf 分层兜底)当后端引擎(非 markitdown),把多格式转成 `raw/*.md` + `origin` provenance,复用 `.md` 单格式 ingest 不动;脚本零 LLM、无新依赖,见 [`P5.2-多格式摄入.md`](P5.2-多格式摄入.md)。MVP ingest 仍只吃 `.md`。
> 关键决策:**LLM 调用统一走 Agentao runtime**,脚本保持零 LLM。这样 provider、密钥、权限、可观测都由 Agentao 治理,观澜不重复造轮子。
### 4.7 Wrapper 与确定性门禁
需要一个真实但很薄的 `guanlan ingest/query` 包装器。原因是:Agent prompt 只能提示"收尾要校验",不能保证每次都执行;frontmatter、断链这些约束应由确定性代码兜底。wrapper 是 MVP 的控制边界,不是后端服务,也不替代 Agentao 的 LLM 能力。
- **`guanlan ingest <raw-file>` 与 `guanlan query --backfill` 是唯二写入口。** wrapper 经 `agentao run` + `guanlan-wiki` skill 让 Agent 写 `wiki/`,收尾强制运行 `guanlan check`:通过则保留本次 wiki 改动;失败则报告失败项,**`wiki/` 改动原样留在磁盘供检查**,由用户手动修正或删除(单用户 MVP 可接受)。需要一键撤销者,可自行把 `wiki/` 纳入 git(可选,非 MVP 机制)。
- **`guanlan query` 默认只读。** 普通 query 不写 wiki,只输出答案;只有显式 `--backfill` 才允许写 `wiki/syntheses/`,并与 ingest 使用同一套门禁。
- **`raw/` 不变性由 wrapper 确定性快照兜底,不押在权限配置上。** `ingest` / `--backfill` 在调用 Agentao **前**对 `raw/` 取一次快照(**P2 落地为:按相对路径建键的内容 SHA256**,见 [`P2-最小闭环.md`](P2-最小闭环.md) §5.1 / §10 决策4——个人级数据量上 SHA256 成本可忽略,且不受 mtime 漂移/伪造影响),收尾再比对;任何增、删、改、重命名都判门禁失败并报告。此检查自包含、可测,不依赖 Agentao 权限规则的实参键名细节,也能拦住 shell 里 `mv`/`cp`/`rm`/`python` 等绕过 `write_file` 的写入——而单条用户级 deny 规则做不到这点。`AGENTAO.md` 仍声明"永不改 `raw/`"作为 Agent 软约束;用户若愿意,也可在 `~/.agentao/permissions.json` 额外加 deny 规则作纵深防御,但 MVP 的**硬约束是 wrapper 快照**,不是手工权限配置。
- **门禁内容保持确定性且最小。** P2 门禁 = `guanlan check`(frontmatter + wikilink 断链 + `sources` 解析)+ `raw/` 快照比对两部分;均零 LLM、不做语义判断。**写门禁额外保证既有页源不回退**(半阶段 **P2.1**,已实现):复用同一处 `raw/` 写前快照再取一份每页指纹,写后确定性比对——既有页 `sources` 只增不减(覆盖丢源 → `sources.dropped` 阻断 + 回喂自愈),正文腰斩级骤缩出非阻断 `body.shrank` 警告;合并逻辑仍归 Agent(工具只验不变量),细化见 [`P2.1-摄入写入纪律.md`](P2.1-摄入写入纪律.md)。
- **不用 git commit 表达原子性。** wiki 数据默认不进观澜项目 git,MVP 也不要求用户知识库是 git 仓库。若用户自行把 wiki 放进 git,`guanlan` 可后续加可选 `--commit`,但这不是 P1–P2 的一致性前提。
> **wiki 数据不进观澜项目 git。** `raw/`、`wiki/`、`graph/` 以及开发期由 `examples/` 拷入仓库根的 sample `AGENTAO.md`/`SCHEMA.md`(可能含本机路径/开发者私有设置)都默认 `.gitignore`、仅本地;观澜项目仓库只版本化引擎源码(`skills/guanlan-wiki/`)、文档(`docs/`)与 init 模板(`examples/AGENTAO.md`、`examples/SCHEMA.md`)。用户如果想给自己的 wiki 建 git 仓库,可以自行开启,但这不是 MVP 的一致性机制。
### 4.8 分发与发布(PyPI / CI)
观澜以 wheel 形式发到 PyPI,安装即得 `guanlan` CLI 与随包携带的维护引擎 skill;细化操作手册见 [`发布到-PyPI.md`](发布到-PyPI.md)。
- **发布名 ≠ 导入名。** PyPI **发布名是 `guanlan-wiki`**(裸名 `guanlan` 已被一个无关项目占用);**导入名与 CLI 仍是 `guanlan`**(`pyproject.toml` 的 `packages = ["guanlan"]` 与 `[project.scripts] guanlan` 不变)。装:`pip install guanlan-wiki`;可选 Web 宿主:`pip install 'guanlan-wiki[web]'`(`web` extra 带 fastapi/uvicorn/markdown,核心安装不背,见 §5.2 / [`P4-Web宿主.md`](P4-Web宿主.md) 决策P4-2)。
- **构建(hatchling)把模板与引擎一并打进 wheel。** `[tool.hatch.build.targets.wheel.force-include]` 在构建期把 `examples/{AGENTAO.md,SCHEMA.md,wiki}` 拷为 `guanlan/_templates/`、把 `skills/guanlan-wiki/` 拷为 `guanlan/_skill/guanlan-wiki/`——故 **`examples/` 是 init 模板的单一事实源**(开发期 `init` 直接读仓库根 `examples/`,安装态读包内 `_templates/`,见 `guanlan/init.py:_templates_dir`),维护引擎 skill 随 wheel 携带、运行时幂等装入 `~/.agentao/skills/`(与 §8「skill 分发」一致)。Web 前端静态资源(`guanlan/web/static/*`)随 `packages` 自动入包。
- **发布走 GitHub Actions Trusted Publishing(OIDC,零 token)。** 推 `v*` tag 触发 `.github/workflows/release.yml`:`uv build` → `twine check` → `pypa/gh-action-pypi-publish` 经 OIDC 上传(仓库不存任何 API token,附 PyPI 数字签名);gated 在 `pypi` environment,需先在 PyPI 配 pending publisher。另有 `.github/workflows/ci.yml`:push `main` / 对 `main` 发 PR 时跑 `ruff check` + 全量 `pytest`(全程离线、无需 API key——测试用 fake runner / monkeypatch 不打真实 LLM)。
- **版本遵循 PEP 440。** 两次发布之间 `main` 的 `version` 保持 `X.Y.Z.devN`(标记非发布态、`pip install` 默认装不到);发布时去掉 `.devN`、打 `vX.Y.Z` tag 触发上传。
---
## 5. Agentao 集成
Agentao 是 Python 的**受治理 Agent 运行时**(约束 / 连接 / 可观测)。MVP 通过 `guanlan` wrapper **以 `agentao run` 子进程**执行 ingest/query(非进程内嵌入);in-process 库嵌入(`Agentao(...)`)留待 P4。此分界与 `agentao` 的 `docs/EMBED_FOR_AGENTS.md` §0 一致("跑 prompt、拿退出码"走 `agentao run`,非嵌入),细节见 [`P2-最小闭环.md`](P2-最小闭环.md) §4。
### 5.1 MVP(CLI)集成
MVP 使用 Agentao 的文件约定 + skill discovery,并增加一个只负责编排和门禁的 `guanlan` wrapper:
- **`guanlan-wiki` skill** —— 工作流指令里让 agent 直接 shell 调用确定性脚本(如 `guanlan check`)。**激活方式**:skill 被发现后默认**不自动激活**(实测构造后 active 为空),故 `guanlan` wrapper 经 `agentao run --skill guanlan-wiki` **显式激活**(不依赖 `description` 触发,保证工作流稳定入上下文);`description` 触发仅用于 wrapper 之外的交互式直接调用(用户在 agent 里自然语言说"摄入…"时)。发现方式(Agentao skill 路径见 `agentao/skills/manager.py`),开发与外部各一条、不混用:
- **开发期**:`working_directory` = 观澜仓库根,skill 在 `skills/guanlan-wiki/`,命中 repo-root 路径(`<wd>/skills/`,优先级最高)自动发现,**免安装**;仓库根兼作 sample wiki(见 §4.2)。
- **外部真实 wiki**:skill 装在**全局 `~/.agentao/skills/guanlan-wiki/`**(cwd 无关),以用户库为 `working_directory`。用户库保持最小、不含 skill。**安装态下 wrapper 自动兜底**:skill 随 wheel 携带(`guanlan/_skill/`),ingest/query 前若发现路径里都没有 skill 就**幂等装入全局**(与 Agentao 自带 skill 的 bootstrap 同理),也可显式 `guanlan install-skill [--force]`。落点见 `guanlan/skill.py`。
- **`guanlan` wrapper** —— `init` 生成模板;ingest/query 经 `agentao run --skill guanlan-wiki --permission-mode <read-only|workspace-write> --interaction-policy reject --format json` 驱动(prompt 经 stdin 的 run spec 传入;显式激活 skill、设权限姿态、非交互、解析 `RunResult.final_text`)。`ingest / query --backfill` 收尾强制门禁:跑 check(frontmatter + 断链 + `sources` 解析)并比对调用前后的 `raw/` 快照,两项全过才算成功;`query` 默认只读(且以 `read-only` 姿态纵深防御)。
- **`AGENTAO.md`**(用户库根,自动读取):行为约束 + 指针(→ skill 与 `SCHEMA.md`)。
- **`.agentao/*.json`(可选)**:`guanlan init` **默认不生成**;仅当用户需要 MCP、运行模式等再自行添加。
- **权限(可选纵深防御)**:`raw/` 不变性由 wrapper 快照兜底(§4.7),不依赖权限配置;如需额外防御,可在 Agentao **用户级** `~/.agentao/permissions.json` 加 deny(项目级 `.agentao/permissions.json` 会被 Agentao 忽略并打 warning,故仓库内固化权限无效)。
### 5.2 P4 Web 宿主(可选)
P4 给同一文件库加一个**可选的本地 Web 图形入口**。**实现级细化见 [`P4-Web宿主.md`](P4-Web宿主.md)**——它把 P4 收敛为"薄 HTTP(FastAPI/uvicorn)表现层 + 复用既有命令",并采**读写分线**:唯一写作业 `ingest` 仍走 `agentao run` 子进程 + 单写者门禁(P2 不动);**所有问答**(一次性单轮与多轮对话)因 agentao CLI 无 session resume,统一进程内嵌入 `Agentao`(`.arun`、默认只读、token 流式,逐一化解 P2 §4.3 嵌入四坑),Web 不设 `/api/query`。仍**不做** Tool 注入、不做可写工作会话、不做会话落盘(见该文决策P4-1 修订 / P4-8 与下文附录 A)。Web UI 与嵌入式细节**另见附录 A**,均不属于 MVP。
---
## 6. 企业版演进(方向,暂不展开)
企业版在个人版跑通后再开**单独的设计文档**详述,并在那时选型。此处只锁定三条方向性原则(参考 [gbrain](../llm-wiki/gbrain) 的问题域与经验,不破坏个人版前提),**不预先绑定任何技术栈或路线**:
1. **markdown 仍是唯一事实来源**——任何派生检索 / 索引层都只是可从 markdown 幂等重建的产物;git 仍只是可选版本化手段。
2. **检索随规模升级、权限按 source 隔离**——index-only 撑不住时再引入更强检索;多租户按 source 作用域隔离 + 信任边界(本地全权 / 远程 scoped)。
3. **维护尽量自动化**——把 health/lint/富化做成后台定时任务,让 wiki 自保鲜。
> 具体技术选型(存储/向量库、检索与重排、权限协议、自动富化等)留待企业版设计文档评估,gbrain 有成熟实现可参考。
---
## 7. 里程碑
**个人版(P)**
| 阶段 | 目标 | 交付 |
|------|------|------|
| **P1 — 骨架** | init + Schema 落地 | 观澜仓库 `skills/guanlan-wiki/`(开发期 repo-root 自动发现);`guanlan init` 生成最小模板(`AGENTAO.md` 含 skill 指针 / `SCHEMA.md` / `raw/` / `wiki/` 含 index·log·overview);wiki 数据默认 `.gitignore` |
| **P2 — 最小闭环** | wrapper + ingest + query + 基础校验 | `guanlan ingest/query/check`;`.md` ingest;source/entity/concept 页生成;wikilink + frontmatter + `sources` + 断链校验 + `raw/` 快照比对(见 §4.7);query 带引用;纯 CLI。**实现级细化见 [`P2-最小闭环.md`](P2-最小闭环.md)**;半阶段 **P2.1**(已实现)写门禁加既有页源不回退 `sources.dropped`(阻断+自愈)+ 正文骤缩 `body.shrank`(警告),见 [`P2.1-摄入写入纪律.md`](P2.1-摄入写入纪律.md) |
| **P3 — 健康与图谱** | health + 结构 lint + 确定性 graph | `guanlan health/lint/graph`:结构健康检查(桩页/index 同步)、结构 lint(孤儿/断链/缺失实体,零 LLM)、确定性 wikilink graph + 自包含 `graph.html`。**实现级细化见 [`P3-健康与图谱.md`](P3-健康与图谱.md)**;半阶段 P3.1–P3.6 已落(别名/heal/规范标题/reindex;**P3.5 图谱分析** 确定性 Louvain 社区 + hub/bridge/silo 拓扑 lint、**P3.6 图论桥与割点** 确定性迭代 Tarjan 割边/割点,均零 LLM)见 [`P3.5-图谱分析.md`](P3.5-图谱分析.md)、[`P3.6-图论桥与割点.md`](P3.6-图论桥与割点.md);**P3.8 链接归一**(wikilink 解析期 fold 消变体 `_`↔`-`/NFKC、零正文改写、零 LLM;**已实现**)见 [`P3.8-链接归一.md`](P3.8-链接归一.md);**P3.10 页型↔目录一致性**(`health` 两类 schema 漂移 advisory:`type_dir_mismatch`/`uncharted_page`、不解析 SCHEMA.md、零 LLM;**已实现**)见 [`P3.10-页型目录一致性.md`](P3.10-页型目录一致性.md);**P3.7 语义审计**(新开 `audit` 命令:确定性粗筛→LLM 门禁内复核两层、复刻 heal,首次把 LLM 引入维护线;拱心石 `raw_digest` provenance 由 wrapper 门禁后直写 source 页(决策P3.7-3a / Option A);**决策齐备、待实现**)见 [`P3.7-语义审计.md`](P3.7-语义审计.md) |
- | **P4 — 可选宿主层(Web + MCP + IM)** | 复用同一文件库的本地宿主入口(浏览器 / Agent / 聊天窗口) | `guanlan web`:浏览 wiki + 触发 `ingest` + **与 agent 单轮/多轮问答** + 跑 check/health/lint + 看 graph;FastAPI/uvicorn 薄表现层,写作业走子进程、所有问答走只读进程内嵌入(默认只读、token 流式)。**实现级细化见 [`P4-Web宿主.md`](P4-Web宿主.md)**;半阶段 P4.1–P4.9 已落。**P4.10 MCP 宿主**(第二种传输:FastMCP **只读 stdio**,把**既有只读核心**暴露给任意 Agent——七个只读工具复用 P5.0/P5.1 `search_result_dict` 检索单一归口、新抽 `report_dict` 体检/lint 信封、CLI query 只读路径,`page`/`path` 统一为相对库根带 `wiki/` 前缀同口径;**不碰 `chat.py`**(客户端自持对话、零服务端会话)、镜像 P4.9 reader **KB 零写**、stdout 即 JSON-RPC 帧须洁净;**已实现**:`guanlan mcp` + `guanlan/mcp/` 子包,extra `guanlan-wiki[mcp]`)见 [`P4.10-MCP宿主.md`](P4.10-MCP宿主.md);**P4.13 Web mermaid 渲染**(纯前端表现层增强:把 wiki 页 / 对话答案 / 暂存区预览里用户或 LLM 手写的 ` ```mermaid ` 围栏块在浏览器内渲染成图——**服务端零改**(`fenced_code` 已把图源保留为 `code.language-mermaid` 转义文本,`render.py` 字节不动、`_EscapeHtmlExtension` 转义姿态不破)、前端 **vendored** mermaid 运行时(非 CDN、**自包含 UMD 单文件** `mermaid.min.js` 经注入 `<script>` 懒加载,归类「内容渲染器」与已 vendored 的 `markdown` 同位阶、对决策P4-3 有界破例)、`securityLevel:'strict'`+`htmlLabels:false` 安全闸(「内容→客户端 HTML」首次破例的信任边界,不承诺产物绝对可信、CSP 列独立加固)、优雅降级保留源码(markdown 仍唯一事实来源);`enhanceMermaid` 挂 `paintPage`/staging `paint()` 重绘函数内令切语言/切预览模式重增强;**已实现**:`static/vendor/mermaid.min.js` + `static/mermaid_enhance.js` + 四注入点接线,真浏览器冒烟已过)见 [`P4.13-Web-mermaid渲染.md`](P4.13-Web-mermaid渲染.md);**P4.14 Web 数学/化学/代码渲染**(承 P4.13 同骨架:把 `$…$`/`$$…$$`/`\(…\)`/`\[…\]` 数学、mhchem `$\ce{}$` 化学、` ```X ` 代码块在浏览器内渲染成富呈现——**服务端零改**(代码块同 mermaid 用 `code.language-X` 转义文本载体;数学**无围栏载体**、作普通文本穿过、落成转义文本节点,**新机制** KaTeX `renderMathInElement` 遍历文本节点排版,`render.py` 字节不动、转义姿态不破)、前端 **vendored** KaTeX 0.16.22 家族(`katex`+`mhchem`+`auto-render`+css+20 字体,归「内容渲染器」对决策P4-3 有界破例)+ highlight.js 11.10.0 common、**安全闸** KaTeX `trust:false`(默认即安全的白名单铰链,禁 `\href`/`\html*`;**取 KaTeX 而否 MathJax** 因后者需脆弱的全量包减法黑名单)+ 不共享 `macros`(跨容器宏隔离)+ highlight v11 转义契约,不承诺产物绝对可信、CSP 列独立加固、失败保留源码;P4.13 单钩子泛化为 `enhanceContent` 编排器、四注入点改接;**已实现**:`static/vendor/katex/` + `vendor/highlight/` + `math_enhance.js`/`code_enhance.js`/`content_enhance.js` + 四注入点接线 + `app.css` hljs 昼澜主题,真浏览器冒烟 16/16 已过含注入/宏不跨容器/懒加载运行时验证)见 [`P4.14-Web数学化学代码渲染.md`](P4.14-Web数学化学代码渲染.md);**P4.15 Web 工具确认 / `ask_user` 人在环**(把 P4.5 可写会话里**静默自动放行**的 ASK 决策补成**浏览器内人确认**:workspace-write 下带操作符 shell / `requires_confirmation` 工具经 SSE 弹 `confirm_request`、阻塞 turn 等用户点 允许 / 本会话起自动放行 / 拒绝、回传布尔;模型 `ask_user` 同机制弹 `ask_request`、收答案串。确认/提问回调在 arun 的 executor 线程阻塞、`call_soon_threadsafe` 推帧 + `queue.Queue` 收应答、端点瞬返不取写锁;**超时**(`--confirm-timeout`,默认 120s)+ 停止 / 断线一律**默认拒绝**兜「人走开→写锁永占」(断线先 `request_stop` trip 令牌再取消外层);气泡②「本会话起自动放行」取 CLI 第 2 项**安全版**——只切本会话 `confirm_mode=auto`、姿态仍 workspace-write、层①②③ 不动、`raw/`/`AGENTAO.md` 仍硬只读、**非 full-access**、可逆;与层①②③**正交**:**「人点允许 ≠ 绕过 immutable」**,确认只决定 ASK 工具跑不跑、不开新写路径;pending 记 `kind` 跨类拒(`/answer` 打到 confirm pending → 409,杜绝非空串被当 truthy `True`);`--confirm {ask,auto}` 默认 ask;**已实现**:`conversation.py` 回调 + 线程桥、`app.py` 三端点、`cli.py` 旗标、前端确认/提问气泡,`tests/test_web.py` 20 例 + `scripts/smoke_p415.py` 真浏览器冒烟 9/9)见 [`P4.15-Web工具确认.md`](P4.15-Web工具确认.md);**P4.16 Web 长任务目标与自动续跑 `/goal`**(在浏览器对话框设一个长任务目标,由**宿主拥有**的 `while goal.is_active` 外层循环反复驱动 `Conversation.turn()`、每轮注入续跑 prompt、host 记时间/轮数预算、撞预算跑恰好一次收尾轮;agent 仅经注入的 `update_goal` 工具自报 `complete`/`blocked`、用户经 `/goal [<目标>\|show\|pause\|resume\|edit\|budget\|clear]` 控制。**最大化复用 agentao 已落地原语** `cli.goal_state.GoalState`/`cli.duration.parse_duration`/`tools.goal.UpdateGoalTool`(依赖上提至 `agentao[cli]>=0.4.13`),guanlan 只建 Web 编排层:per-conversation `.agentao/goals/<id>.json` sidecar(`os.replace` 原子写)、async `run_goal` 续跑循环(每内层轮 `ensure_future + asyncio.shield`、`_goal_lock`+`_GoalProxy` 串行 GoalState、`_goal_io_lock` 串行落盘 + `_goal_deleted` 防删后 resurrect)、`POST /api/chat/{id}/goal{,/budget,/pause,/resume,/edit,/clear,/run}` 端点(`/run` 走 SSE 续跑流 + 轮边界帧 + 多气泡 + goal 横幅)、前端 `/goal` 斜杠支线、`guanlan web --goal {on,off}`(reader 强制关);**不放宽任何边界**(goal 每内层可写轮照走 P4.5 层①②③+check+撤销+单写者写锁逐轮取放、与 P4.15 confirm/ask 自动组合、进程内至多一个活跃可写 goal、停止断整 goal/断线收 paused/重启 active 显示可恢复不自动续)、`--turns`(外层续跑轮)⊥ `max_iterations`(内层工具循环)、不走 `force_continue`、零 agentao 内核改动;设计三轮 + 实现后 xhigh 工作流 + 多轮 codex 评审收敛至零 finding(`_in_goal`/预订零泄漏、中断轮墙钟计入、删会话竞态串行、冷会话懒恢复、孤儿 sidecar 可清);**已实现**:`web/goal_io.py` + `conversation.py`/`conversation_store.py`/`app.py`/`server.py`/`cli.py` + `static/{chat.js,i18n.js,app.css}`,`tests/test_web.py` goal 套件 + 真浏览器冒烟 `scripts/smoke_p416.py` 5/5)见 [`P4.16-Web目标续跑.md`](P4.16-Web目标续跑.md);**P4.17 MCP 远程传输**(给 P4.10 stdio-only MCP 宿主补第二种传输:官方 **Streamable HTTP**(spec 2025-03-26 起、取代 legacy HTTP+SSE),`serve_mcp` 加 `--transport {stdio,http}` 分支暴露**同一套只读工具**(`tools.py` 零改、`build_mcp` 仅加 `allow_ask` 注册门;传输/绑定/鉴权接线全在 `serve_mcp`);**安全默认**——默认仍绑 `127.0.0.1`(沿用决策P4-4)、非环回强制 bearer token(最小常量时间比对、不引 OAuth)否则拒启、`ask` over http 默认关(`allow_ask=(transport=='stdio' or --allow-ask)`:stdio 永远七工具、http 默认六、`--allow-ask` 才七,避成本/DoS 放大)、无状态 `stateless_http` + DNS-rebinding 防护、TLS 外置;完整 OAuth(MCP 2025-06-18)/多租户 source 级 scoping 显式推 E2;已实测装的 mcp SDK 支持 `transport="streamable-http"` 且自带 uvicorn/starlette 故 `[mcp]` extra 与 SDK 下界均零依赖变更;编号辨析:属 P4 宿主家族兄弟半阶段(非 P4.10.1 补漏 / 非新里程碑 / 非直接 E2);**已实现**:`guanlan mcp --transport http` + `guanlan/mcp/server.py` http 分支(`_http_security`/`_BearerTokenMiddleware`/`_build_http_app`/`_serve_http`)+ `cli.py` 六旗标,`tests/test_mcp.py` P4.17 套件含真 uvicorn 端到端往返)见 [`P4.17-MCP远程传输.md`](P4.17-MCP远程传输.md);**P4.18 MCP 2.0 迁移**(底座从官方 SDK v1 `mcp.server.fastmcp.FastMCP` 迁到 **v2 `mcp.server.mcpserver.MCPServer`**,对齐协议修订 **`2026-07-28`**:**等价迁移**——命令契约/工具集/只读契约/退出码逐字保留(决策P4.18-1),用户可见变化恰好两处:**依赖契约**——`[mcp]` extra 硬切 `mcp>=2,<3`(v1.x 转只收安全修复的维护态、v2 删掉整个 `fastmcp` 包无 alias,故不做双大版本 shim,决策P4.18-2),**并连带把核心依赖 `agentao[cli]` 下限抬到 `>=0.4.17`**——agentao 的 mcp 1.x/2.x 探针兼容层是 0.4.17 才有的,不抬则「pip 认为下限已满足、只把 mcp 抬到 2.x」会让 `guanlan mcp` 照常工作而 ingest/query/Web 问答全死在 agentao 侧 Tool schema 上(决策P4.18-10)与 `initialize.serverInfo.version` 改报 guanlan 自身版本(v1 曾回所装 SDK 版本、v2 默认空串,决策P4.18-12);http 姿态从 `mcp.settings` 后置赋值改走 `streamable_http_app(...)` kwargs(v2 已无这些 Settings 字段,决策P4.18-4);**协议红利**——无状态核心/`server/discover`/标准错误码白拿(我们本就 `stateless_http` + 零服务端会话),`Mcp-Method`/`Mcp-Name` 路由头留给反代限流(服务端不强制),**cache hints 与 tasks 扩展显式不做**(前者只覆盖 list/read 类方法、救不了大 payload;后者 SDK 2.0.0 无现成实现且与无状态冲突,决策P4.18-8);**旧客户端不掉线**已在 stdio/http 双传输实测(`initialize` 2025-06-18 正常协商,客户端首帧决定 era、同一连接不得混 era,决策P4.18-7);测试面:in-memory 改 `Client(mcp, mode="legacy")` 保住 v1 的真 JSON-RPC 覆盖(v2 默认 mode 走 `DirectDispatcher` 无帧,决策P4.18-11)+ 新增默认 mode 平价用例 + **真 stdio 子进程逐帧洁净断言**(决策P4.18-6,此前只有 in-memory 推断);**已实现**:`guanlan/mcp/{server,tools,__init__}.py` + `cli.py` 版本口径提示 + `pyproject.toml` extra,`tests/test_mcp.py` 63 例全绿)见 [`P4.18-MCP2.0迁移.md`](P4.18-MCP2.0迁移.md);**P4.19 Web MCP 配置诊断**(**方向与 P4.10/P4.17/P4.18 相反**:那三个是观澜作 MCP **服务端**,本半阶段诊断的是观澜作 MCP **客户端**被注入的外部 server——agentao 的 `build_from_environment` 在调用方不传 `mcp_registry=` 时**默认注入** `FileBackedMCPRegistry`(读 `<kb>/.agentao/mcp.json` + `~/.agentao/mcp.json`),而观澜的 CLI 子进程与 Web 嵌入**都吃这个默认**,故外部 MCP 工具早已在注入 ingest/query/heal/audit/Web 问答,用户却在观澜里看不到任何痕迹;本半阶段只补这份**可见性**——`GET /api/mcp` 如实展示配置 + `POST /api/mcp/check` **用户显式点一次**连接检查,**零 LLM、零策略、零管理、不写 KB、不执行任何外部工具**(只 `initialize` + `tools/list`),**不改变注入行为**;措辞按决策P4.19-8 钉死为「当前磁盘配置的解析结果、已有会话需新建后生效」(MCP 是会话构造期一次性加载);两条轨——生效集合取上游 `load_mcp_config` 归一结果、诊断另读原文(上游对坏 JSON **静默返回 `{}`**,只信它就只会显示「没有配置」而非「配置写坏了」);脱敏覆盖到**连接错误文本**(`NonMcpEndpointError` 会把完整 URL 写进消息,只脱敏端点等于没脱敏);单飞 + 并发 409 不排队、不设端点级墙钟(`to_thread` 线程不可取消,沿用上游 per-server startup timeout)、reader 下两端点**不注册**(理由是**有外部副作用**而非写 KB)、缺 `[mcp]` extra 回 501 + 安装指引;**已实现**:`web/mcpdiag.py` + `app.py` 两端点 + `static/{reports.js,index.html,i18n.js,chat.js,app.css}`,`tests/test_web_mcpdiag.py` 16 例含真 stdio 端到端(拿 `guanlan mcp` 当被测上游)与两条脱敏反向用例)见 [`P4.19-Web-MCP诊断.md`](P4.19-Web-MCP诊断.md);**P4.20 Web flint 数据图表渲染**(承 P4.13/P4.14 同骨架的**第三类内容渲染器**:把 wiki 页 / 对话答案里的 ` ```flint ` 围栏块([flint](https://github.com/microsoft/flint-chart) 语义图表规格 JSON)在浏览器内**编译**成 ECharts option 并渲染成 SVG 图表(37 种图型)——**服务端零改**(`render.py` 字节不动、转义姿态不破)、前端 **vendored** `flint-chart@0.4.1`(自包含单文件 **ESM**,经动态 `import()` 加载,本仓唯一走 ESM 的 vendored 资产,**仍无 npm、无构建**)+ `echarts@6.1.0`(自包含 UMD),P4.14 的 `enhanceContent` 编排器令**七个注入点零改动**;**上游触发条件**是观澜经 MCP 接入用户的结构化数据库后有了可视化需求——Agent 取数 → 把结果行写成数据**整体内联**的块 → 浏览器成图,**拒 `data.url`、渲染器永不出网**,故图 100% 可从那段 markdown 重建(原则 1 在本相位的形态)、`check`/`graph`/`search` 原样复用,图是**快照**不做渲染时查库/自动刷新/交互下钻;**安全闸**入口白名单(只五个顶层键)+ flint **生成式**编译(按模板生成、不 merge 用户私货,三条走私路径实测零残留)+ **键域**出口断言(不扫值域——合法数据里就有 URL 串)+ `renderer:'svg'` + `tooltip.renderMode:'richText'`(tooltip 不走 HTML 通道),**输入是纯 JSON、没有函数**故 ECharts 回调注入面天然不存在(这是选 flint 而非让模型直接写 ECharts option 的核心安全理由);**资源闸**(结构化库接入独有:输入是查询结果而非人写的短文本)块源 64 K 字符 / 1000 行 / 画布不过大,超限**不渲染、保留源码 + 徽标、不静默截断**,且 flint **自己**超预算整批丢类别时同样拒渲(读 `_warnings`);**总原则「能让 flint 自己算的绝不在事后改」**——按栏宽收窄走 `canvasSize` **重编译**而非事后改画布(flint 把 `legend.left` 等烤成绝对像素,缩画布会把图例挤出视口);ECharts 是**前端第一个有状态渲染器**,须显式 `dispose` 脱树实例;配套**独立**作者 skill `flint-chart-author`(模型不会自发写 flint,没有作者侧供给渲染器就是「通了电没有灯」;作图与 wiki 维护正交,故不进 `guanlan-wiki`,沿 `pdf-to-markdown` 先例);**已实现**:`static/vendor/flint/echarts.js` + `vendor/echarts.min.js` + `static/flint_enhance.js` + 编排器一行接线 + `skills/flint-chart-author/`,`tests/test_web.py` 16 例 + 真浏览器冒烟 `scripts/smoke_p420.py` 18/18(含 **37 图型全集扫描**——闸子不只要「拦得住」还要「不乱拒」,该扫描一次揪出四例误杀,已设为常驻场景 + 升级复核项))见 [`P4.20-Web-flint图表渲染.md`](P4.20-Web-flint图表渲染.md);**P4.21 IM 宿主 `guanlan im`**(P4 的**第三种传输**:Web 是浏览器里的人、MCP 是 Agent 客户端,IM 是**聊天窗口里的人**——同一套只读会话逻辑 `Conversation.turn()`、同一零写契约、同一零业务智能。形态是「**一个平台无关的核心 + 若干薄适配器**」而非「某一个平台的宿主」:核心永不写 `if platform ==`,一切分派只读 `AdapterCaps`;v1 两个适配器**恰好互补**(个人微信不能编辑、只能整条发;飞书能原地改写)——核心一行未改即驱动两者,这就是分界的验收标准。**新增的信任线是身份与受众面**(P4.11 管提示词注入、P4.17 管网络传输,两条都不覆盖它:一个零监听端口的进程,照样能经**出站**长连把整库答案送进一个 200 人的群),故默认 deny + 空白名单拒启,群聊放行 = `chat ∈ allow_chats` **AND** (`allow_all` **OR** `user ∈ allow_users`) **AND** `mentioned_me`(`--allow-all-users` **只**旁路用户名单、**永不**旁路群名单);身份可信度**分档**——企业系强档、个人微信弱档(iLink 标识不可枚举、不可反查),弱档只适合小圈子,且新增 `guanlan im-identify`(零 LLM、**绝不回复**、限时、把完整 ID 打到终端)解开「白名单要 ID、拿 ID 要看日志、日志脱敏」的死锁。会话 TTL 与腾容量**归宿主自持的 `SessionRegistry` 独占**(`store.idle_ttl=None`)——已核实 `store.get()` 命中即刷新 `last_active`、回收只在 `create`/`restore` 触发、顶满是**抛错而非逐出**,两套 TTL 并存反而造出一个谁都不负责的空档;过期语义靠**墓碑**(`(None, last_seen)`)保住。停机纪律:收流 task 可 `cancel()`(只阻塞在 I/O),问答 turn **绝不可以**——`arun` 收到 `CancelledError` 是置令牌后**立刻 re-raise、不等线程**,取消它是**把「还没停」伪装成「已经停了」**;故只 `request_stop()` 并**无限期**等真实收尾,30 秒只打 WARNING。**并如实声明优雅停机没有绝对时间上界**——模型侧的 `read=600` 是**每次读**的空闲上限而非整轮墙钟,外部 MCP 那条链上出站写入 / legacy SSE 传输进入 / stdio 传输进入 / 断连清理**都没有人管**(`--mcp-request-timeout` 只收住「写完之后等 `tools/call` 回话」那一段),**唯一的绝对保证是第二次中断信号的 `os._exit(5)`**(那条路上连 `logging` 都不许碰);总 deadline 记为 agentao 上游诉求。长答案契约是**「不静默截断」而非「绝不截断」**(三条原要求数学上不可同时满足),末片是显式截断告示且**不得称「完整版」**——IM 会话 `persist=False`,被舍弃的内容没存在任何地方;**已实现**:`guanlan/im/` 子包(contract/intake/session/reply/delivery/state/server/mcp_bound + 两个适配器)+ `guanlan im` / `im-login` / `im-identify` 三个子命令 + `[im-weixin]`/`[im-feishu]` 两个 extra,`tests/test_im{,_weixin,_feishu}.py` 136 例)见 [`P4.21-IM宿主.md`](P4.21-IM宿主.md);**P4.22 IM 可点引用**(给 P4.21 补最后一公里:答案里的 `[[某页]]` 在飞书里**可以点**、点完机器人把那一页发回来——**不是新宿主、不开监听端口、不引入任何写路径**,回调走已建好的那条 WS 长连故决策P4.21-21 无监听面继续成立、`/page` 只读盘故决策P4.21-8 KB 零字节写继续成立。三件事:**① `/page <名字>`**(所有平台、零 LLM,复用 P3.8 单一 owner 归口,故精确 stem / 别名(P3.1)/ 安全 fold variant(P3.8)三种写法都认——IM 侧另写一套按名查找就是给这个系统开第五个口径;未命中降级为 top-3 检索;截断告示**不附 Web 入口**,因为 Web 至今没有按名开页的路由、给出去仍是死链);**② 可点动作的平台无关表达**(`Action` 携带的是**核心生成的命令文本**而非"页面名"或"动作类型",适配器只把这句话塞进按钮、回调时原样取回——**它不知道 `/page` 是什么**;`AdapterCaps` 加 `supports_actions`/`max_actions`,与 `typing()` 完全对称,个人微信恒关、一个字不变);**③ 点击 = 一条合成的入站消息**(枢纽:回调经 `adapter.inbound()` **原路**回到既有流水线,五道闸全复用、**`server.py` 零改**、核心零平台分支,新增动作类型时适配器同样零改)。**红线的闸在接收端而非构造端**——`InboundMessage` 新增事实字段 `origin`,核心据此把合成消息能跑的命令收窄到 `ACTION_COMMANDS`(v1 只有 `page`,`/help`/`/new` 都是零 LLM 却**不在**其中:前者是噪音、后者会清用户上下文),因为 `classify()` 把**未知**斜杠命令当普通提问送进 LLM,只校验"以 `/` 开头"挡不住一次不该发生的 LLM 花费。**授权面**:`chat_type` 只认**真实入站消息见过**的会话(适配器自持 `(tenant, chat_id) → chat_type` 事实缓存、未命中 fail-closed)——飞书卡片可被转发,"卡片出现在哪里"不构成授权依据;`allow_chats` 一个字都不进适配器(静态断言守着),键的两半都取 `map_event` 同源字段(取错 tenant 的后果不是报错、是**间歇性"会话已过期"**)。**卡片只承载页面名、且只来自已确认送达的正文片**(`split_for` 超限会丢后缀,从完整答案抽会把正文里根本没出现过的页面名印上群内公开可见的卡片);出按钮前先解析一次,断链引用不出按钮(不承诺一个不存在的东西);超限**显式告示**、`[[原文]]` 始终保留故手打 `/page` 永远是退路。**CARD 帧兼容层**:`lark-oapi` 的 WS 客户端对 CARD 帧**直接 return**,而只有 EVENT 分支才走到卡片回调注册表,而"回调以哪种帧下发"证据**矛盾**(Go SDK 注释 vs 上游 issue #126 的真机 200340)——实现**不选边**,子类化 WS 客户端把 CARD 帧头就地改写成 EVENT 再委回父类,**纯粹的超集**(走 EVENT 时一次都不触发),探不到私有接缝即降级告警不拒启 + 真 SDK 形状探针,上游修好即可整段删除;**已实现**:`guanlan/im/pageview.py` + contract/reply/intake/delivery 扩展 + 飞书卡片与回调 + CARD 帧兼容层,`tests/test_im{,_feishu}.py` P4.22 套件;**帧类型 / 卡片 2.0 组件键形状 / 转发点击表现仍待真机核对**,见该文 §9)见 [`P4.22-IM可点引用.md`](P4.22-IM可点引用.md);**P4.24 MCP 技能发布**(issue #81:`guanlan mcp` 除工具外再经 MCP Skills 扩展(`io.modelcontextprotocol/skills`)发布**一份静态只读 skill** `guanlan-query`,把「`search` → `read_page` → 顺链 `read_page(name=…)` → 只据所读页作答并引用、库里没有就明说」这套查询流程交给客户端的技能发现、经用户同意激活——**不发** `guanlan-wiki`(它的写工作流依赖本地文件与写门禁,发到只读服务端等于许诺服务端没有的能力)。经 SDK v2 的 `Extension` 接缝挂 `skills/list`/`skills/get`(只在现代协议修订上应答,握手时代客户端只看到工具)+ 唯一资源 `skill://guanlan-query/SKILL.md`(不注册模板,任何其他 URI 一律 -32602);**清单与所发内容是同一份字节**(起服读一次原始字节,size/sha256/文本/frontmatter 全由它派生,不用会剥 BOM、转行尾的 `FileResource`);skill 文件放包内 `guanlan/mcp/published_skills/` 而非仓库根 `skills/`(后者是 Agentao 发现路径,开发态会被当成本地 skill);两种传输都发布、沿用同一道 token 闸、无旗标,坏文件降级不拒启。前置一步:`read_page` 新增可选 `name`,沿用 P3.8 单一 owner 归口按页名顺链(`path` 路由字节等价、越界照旧拒绝;入参须注解为纯 `str`,否则 SDK 会先 JSON 预解析把 `[[2024]]` 变成列表)。**已实现**:`guanlan/mcp/skills.py` + `published_skills/` + `tools.py`/`server.py`,`tests/test_mcp_skills.py` 41 例(含用 agentao 0.5.10 自己的条目/摘要/frontmatter 校验、真 stdio 子进程与真 HTTP 上的现代 era 客户端往返);**真机验收通过**(agentao 0.5.10 交互式激活 + 带引用作答,2026-10-04))见 [`P4.24-MCP技能发布.md`](P4.24-MCP技能发布.md);**P4.25 Web 思考过程显示**(纯表现层:agentao 早已发出 `THINKING` 推理事件、Web 嵌入此前在旁听口丢弃,现经 `transport.subscribe` 转成 SSE `thinking{text}` 帧,前端显示为答案气泡前默认折叠的**纯文本**块——推理里的 `[[链接]]` 是「考虑过」不是「引用了」,不渲染;**不计时**(模型端不流式给推理时上游答完才整段补发、可能晚于正文)、无调用分段、尾部 64 K 上限、`reasoning_only` 自动展开、顶栏开关只影响本浏览器;**不落盘、不回放**(推理不是事实来源,盘上载体恢复时还会被清),IM/MCP 不跟进,子 Agent 推理上游本就不转给父级;**已实现**:`conversation.py` 一个分支 + `chat.js` `handleThinking` 两流共用,契约用例走真调用路径覆盖流式与整段回退,真浏览器冒烟 `scripts/smoke_p425.py` 9/9)见 [`P4.25-Web思考过程.md`](P4.25-Web思考过程.md);**P4.26 Web 子 Agent 进展跟踪**(纯表现层:子 Agent 运行时在答案气泡前一行显示名称、最近观测的第 n/max 轮与最近调用的工具,结束时显示终态、轮数、工具次数与耗时;transport 旁听口把 `AGENT_START` / 子 `TOOL_START` / `AGENT_END` **无状态**投影成 SSE `subagent` 帧,前端按 agentao 0.5.17 的 `child_task_id`(观澜提的 jin-bo/agentao#578)配对,故依赖下限抬到 `>=0.5.17`;任务原文、参数、失败说明、token 用量不上线;每气泡至多 20 行、超出计数;不计时、不落盘、不回放,IM 不跟进;同版上游的子会话 HostEvent 转播不接(所需字段 transport 已全);**已实现**:`conversation.py` `_subagent_frame` + `chat.js` `handleSubagent` 两流共用,契约用例覆盖四种结局与父→子全链路,真浏览器冒烟 `scripts/smoke_p426.py` 8/8)见 [`P4.26-Web子Agent进展跟踪.md`](P4.26-Web子Agent进展跟踪.md) |
+ | **P4 — 可选宿主层(Web + MCP + IM)** | 复用同一文件库的本地宿主入口(浏览器 / Agent / 聊天窗口) | `guanlan web`:浏览 wiki + 触发 `ingest` + **与 agent 单轮/多轮问答** + 跑 check/health/lint + 看 graph;FastAPI/uvicorn 薄表现层,写作业走子进程、所有问答走只读进程内嵌入(默认只读、token 流式)。**实现级细化见 [`P4-Web宿主.md`](P4-Web宿主.md)**;半阶段 P4.1–P4.9 已落。**P4.10 MCP 宿主**(第二种传输:FastMCP **只读 stdio**,把**既有只读核心**暴露给任意 Agent——七个只读工具复用 P5.0/P5.1 `search_result_dict` 检索单一归口、新抽 `report_dict` 体检/lint 信封、CLI query 只读路径,`page`/`path` 统一为相对库根带 `wiki/` 前缀同口径;**不碰 `chat.py`**(客户端自持对话、零服务端会话)、镜像 P4.9 reader **KB 零写**、stdout 即 JSON-RPC 帧须洁净;**已实现**:`guanlan mcp` + `guanlan/mcp/` 子包,extra `guanlan-wiki[mcp]`)见 [`P4.10-MCP宿主.md`](P4.10-MCP宿主.md);**P4.13 Web mermaid 渲染**(纯前端表现层增强:把 wiki 页 / 对话答案 / 暂存区预览里用户或 LLM 手写的 ` ```mermaid ` 围栏块在浏览器内渲染成图——**服务端零改**(`fenced_code` 已把图源保留为 `code.language-mermaid` 转义文本,`render.py` 字节不动、`_EscapeHtmlExtension` 转义姿态不破)、前端 **vendored** mermaid 运行时(非 CDN、**自包含 UMD 单文件** `mermaid.min.js` 经注入 `<script>` 懒加载,归类「内容渲染器」与已 vendored 的 `markdown` 同位阶、对决策P4-3 有界破例)、`securityLevel:'strict'`+`htmlLabels:false` 安全闸(「内容→客户端 HTML」首次破例的信任边界,不承诺产物绝对可信、CSP 列独立加固)、优雅降级保留源码(markdown 仍唯一事实来源);`enhanceMermaid` 挂 `paintPage`/staging `paint()` 重绘函数内令切语言/切预览模式重增强;**已实现**:`static/vendor/mermaid.min.js` + `static/mermaid_enhance.js` + 四注入点接线,真浏览器冒烟已过)见 [`P4.13-Web-mermaid渲染.md`](P4.13-Web-mermaid渲染.md);**P4.14 Web 数学/化学/代码渲染**(承 P4.13 同骨架:把 `$…$`/`$$…$$`/`\(…\)`/`\[…\]` 数学、mhchem `$\ce{}$` 化学、` ```X ` 代码块在浏览器内渲染成富呈现——**服务端零改**(代码块同 mermaid 用 `code.language-X` 转义文本载体;数学**无围栏载体**、作普通文本穿过、落成转义文本节点,**新机制** KaTeX `renderMathInElement` 遍历文本节点排版,`render.py` 字节不动、转义姿态不破)、前端 **vendored** KaTeX 0.16.22 家族(`katex`+`mhchem`+`auto-render`+css+20 字体,归「内容渲染器」对决策P4-3 有界破例)+ highlight.js 11.10.0 common、**安全闸** KaTeX `trust:false`(默认即安全的白名单铰链,禁 `\href`/`\html*`;**取 KaTeX 而否 MathJax** 因后者需脆弱的全量包减法黑名单)+ 不共享 `macros`(跨容器宏隔离)+ highlight v11 转义契约,不承诺产物绝对可信、CSP 列独立加固、失败保留源码;P4.13 单钩子泛化为 `enhanceContent` 编排器、四注入点改接;**已实现**:`static/vendor/katex/` + `vendor/highlight/` + `math_enhance.js`/`code_enhance.js`/`content_enhance.js` + 四注入点接线 + `app.css` hljs 昼澜主题,真浏览器冒烟 16/16 已过含注入/宏不跨容器/懒加载运行时验证)见 [`P4.14-Web数学化学代码渲染.md`](P4.14-Web数学化学代码渲染.md);**P4.15 Web 工具确认 / `ask_user` 人在环**(把 P4.5 可写会话里**静默自动放行**的 ASK 决策补成**浏览器内人确认**:workspace-write 下带操作符 shell / `requires_confirmation` 工具经 SSE 弹 `confirm_request`、阻塞 turn 等用户点 允许 / 本会话起自动放行 / 拒绝、回传布尔;模型 `ask_user` 同机制弹 `ask_request`、收答案串。确认/提问回调在 arun 的 executor 线程阻塞、`call_soon_threadsafe` 推帧 + `queue.Queue` 收应答、端点瞬返不取写锁;**超时**(`--confirm-timeout`,默认 120s)+ 停止 / 断线一律**默认拒绝**兜「人走开→写锁永占」(断线先 `request_stop` trip 令牌再取消外层);气泡②「本会话起自动放行」取 CLI 第 2 项**安全版**——只切本会话 `confirm_mode=auto`、姿态仍 workspace-write、层①②③ 不动、`raw/`/`AGENTAO.md` 仍硬只读、**非 full-access**、可逆;与层①②③**正交**:**「人点允许 ≠ 绕过 immutable」**,确认只决定 ASK 工具跑不跑、不开新写路径;pending 记 `kind` 跨类拒(`/answer` 打到 confirm pending → 409,杜绝非空串被当 truthy `True`);`--confirm {ask,auto}` 默认 ask;**已实现**:`conversation.py` 回调 + 线程桥、`app.py` 三端点、`cli.py` 旗标、前端确认/提问气泡,`tests/test_web.py` 20 例 + `scripts/smoke_p415.py` 真浏览器冒烟 9/9)见 [`P4.15-Web工具确认.md`](P4.15-Web工具确认.md);**P4.16 Web 长任务目标与自动续跑 `/goal`**(在浏览器对话框设一个长任务目标,由**宿主拥有**的 `while goal.is_active` 外层循环反复驱动 `Conversation.turn()`、每轮注入续跑 prompt、host 记时间/轮数预算、撞预算跑恰好一次收尾轮;agent 仅经注入的 `update_goal` 工具自报 `complete`/`blocked`、用户经 `/goal [<目标>\|show\|pause\|resume\|edit\|budget\|clear]` 控制。**最大化复用 agentao 已落地原语** `cli.goal_state.GoalState`/`cli.duration.parse_duration`/`tools.goal.UpdateGoalTool`(依赖上提至 `agentao[cli]>=0.4.13`),guanlan 只建 Web 编排层:per-conversation `.agentao/goals/<id>.json` sidecar(`os.replace` 原子写)、async `run_goal` 续跑循环(每内层轮 `ensure_future + asyncio.shield`、`_goal_lock`+`_GoalProxy` 串行 GoalState、`_goal_io_lock` 串行落盘 + `_goal_deleted` 防删后 resurrect)、`POST /api/chat/{id}/goal{,/budget,/pause,/resume,/edit,/clear,/run}` 端点(`/run` 走 SSE 续跑流 + 轮边界帧 + 多气泡 + goal 横幅)、前端 `/goal` 斜杠支线、`guanlan web --goal {on,off}`(reader 强制关);**不放宽任何边界**(goal 每内层可写轮照走 P4.5 层①②③+check+撤销+单写者写锁逐轮取放、与 P4.15 confirm/ask 自动组合、进程内至多一个活跃可写 goal、停止断整 goal/断线收 paused/重启 active 显示可恢复不自动续)、`--turns`(外层续跑轮)⊥ `max_iterations`(内层工具循环)、不走 `force_continue`、零 agentao 内核改动;设计三轮 + 实现后 xhigh 工作流 + 多轮 codex 评审收敛至零 finding(`_in_goal`/预订零泄漏、中断轮墙钟计入、删会话竞态串行、冷会话懒恢复、孤儿 sidecar 可清);**已实现**:`web/goal_io.py` + `conversation.py`/`conversation_store.py`/`app.py`/`server.py`/`cli.py` + `static/{chat.js,i18n.js,app.css}`,`tests/test_web.py` goal 套件 + 真浏览器冒烟 `scripts/smoke_p416.py` 5/5)见 [`P4.16-Web目标续跑.md`](P4.16-Web目标续跑.md);**P4.17 MCP 远程传输**(给 P4.10 stdio-only MCP 宿主补第二种传输:官方 **Streamable HTTP**(spec 2025-03-26 起、取代 legacy HTTP+SSE),`serve_mcp` 加 `--transport {stdio,http}` 分支暴露**同一套只读工具**(`tools.py` 零改、`build_mcp` 仅加 `allow_ask` 注册门;传输/绑定/鉴权接线全在 `serve_mcp`);**安全默认**——默认仍绑 `127.0.0.1`(沿用决策P4-4)、非环回强制 bearer token(最小常量时间比对、不引 OAuth)否则拒启、`ask` over http 默认关(`allow_ask=(transport=='stdio' or --allow-ask)`:stdio 永远七工具、http 默认六、`--allow-ask` 才七,避成本/DoS 放大)、无状态 `stateless_http` + DNS-rebinding 防护、TLS 外置;完整 OAuth(MCP 2025-06-18)/多租户 source 级 scoping 显式推 E2;已实测装的 mcp SDK 支持 `transport="streamable-http"` 且自带 uvicorn/starlette 故 `[mcp]` extra 与 SDK 下界均零依赖变更;编号辨析:属 P4 宿主家族兄弟半阶段(非 P4.10.1 补漏 / 非新里程碑 / 非直接 E2);**已实现**:`guanlan mcp --transport http` + `guanlan/mcp/server.py` http 分支(`_http_security`/`_BearerTokenMiddleware`/`_build_http_app`/`_serve_http`)+ `cli.py` 六旗标,`tests/test_mcp.py` P4.17 套件含真 uvicorn 端到端往返)见 [`P4.17-MCP远程传输.md`](P4.17-MCP远程传输.md);**P4.18 MCP 2.0 迁移**(底座从官方 SDK v1 `mcp.server.fastmcp.FastMCP` 迁到 **v2 `mcp.server.mcpserver.MCPServer`**,对齐协议修订 **`2026-07-28`**:**等价迁移**——命令契约/工具集/只读契约/退出码逐字保留(决策P4.18-1),用户可见变化恰好两处:**依赖契约**——`[mcp]` extra 硬切 `mcp>=2,<3`(v1.x 转只收安全修复的维护态、v2 删掉整个 `fastmcp` 包无 alias,故不做双大版本 shim,决策P4.18-2),**并连带把核心依赖 `agentao[cli]` 下限抬到 `>=0.4.17`**——agentao 的 mcp 1.x/2.x 探针兼容层是 0.4.17 才有的,不抬则「pip 认为下限已满足、只把 mcp 抬到 2.x」会让 `guanlan mcp` 照常工作而 ingest/query/Web 问答全死在 agentao 侧 Tool schema 上(决策P4.18-10)与 `initialize.serverInfo.version` 改报 guanlan 自身版本(v1 曾回所装 SDK 版本、v2 默认空串,决策P4.18-12);http 姿态从 `mcp.settings` 后置赋值改走 `streamable_http_app(...)` kwargs(v2 已无这些 Settings 字段,决策P4.18-4);**协议红利**——无状态核心/`server/discover`/标准错误码白拿(我们本就 `stateless_http` + 零服务端会话),`Mcp-Method`/`Mcp-Name` 路由头留给反代限流(服务端不强制),**cache hints 与 tasks 扩展显式不做**(前者只覆盖 list/read 类方法、救不了大 payload;后者 SDK 2.0.0 无现成实现且与无状态冲突,决策P4.18-8);**旧客户端不掉线**已在 stdio/http 双传输实测(`initialize` 2025-06-18 正常协商,客户端首帧决定 era、同一连接不得混 era,决策P4.18-7);测试面:in-memory 改 `Client(mcp, mode="legacy")` 保住 v1 的真 JSON-RPC 覆盖(v2 默认 mode 走 `DirectDispatcher` 无帧,决策P4.18-11)+ 新增默认 mode 平价用例 + **真 stdio 子进程逐帧洁净断言**(决策P4.18-6,此前只有 in-memory 推断);**已实现**:`guanlan/mcp/{server,tools,__init__}.py` + `cli.py` 版本口径提示 + `pyproject.toml` extra,`tests/test_mcp.py` 63 例全绿)见 [`P4.18-MCP2.0迁移.md`](P4.18-MCP2.0迁移.md);**P4.19 Web MCP 配置诊断**(**方向与 P4.10/P4.17/P4.18 相反**:那三个是观澜作 MCP **服务端**,本半阶段诊断的是观澜作 MCP **客户端**被注入的外部 server——agentao 的 `build_from_environment` 在调用方不传 `mcp_registry=` 时**默认注入** `FileBackedMCPRegistry`(读 `<kb>/.agentao/mcp.json` + `~/.agentao/mcp.json`),而观澜的 CLI 子进程与 Web 嵌入**都吃这个默认**,故外部 MCP 工具早已在注入 ingest/query/heal/audit/Web 问答,用户却在观澜里看不到任何痕迹;本半阶段只补这份**可见性**——`GET /api/mcp` 如实展示配置 + `POST /api/mcp/check` **用户显式点一次**连接检查,**零 LLM、零策略、零管理、不写 KB、不执行任何外部工具**(只 `initialize` + `tools/list`),**不改变注入行为**;措辞按决策P4.19-8 钉死为「当前磁盘配置的解析结果、已有会话需新建后生效」(MCP 是会话构造期一次性加载);两条轨——生效集合取上游 `load_mcp_config` 归一结果、诊断另读原文(上游对坏 JSON **静默返回 `{}`**,只信它就只会显示「没有配置」而非「配置写坏了」);脱敏覆盖到**连接错误文本**(`NonMcpEndpointError` 会把完整 URL 写进消息,只脱敏端点等于没脱敏);单飞 + 并发 409 不排队、不设端点级墙钟(`to_thread` 线程不可取消,沿用上游 per-server startup timeout)、reader 下两端点**不注册**(理由是**有外部副作用**而非写 KB)、缺 `[mcp]` extra 回 501 + 安装指引;**已实现**:`web/mcpdiag.py` + `app.py` 两端点 + `static/{reports.js,index.html,i18n.js,chat.js,app.css}`,`tests/test_web_mcpdiag.py` 16 例含真 stdio 端到端(拿 `guanlan mcp` 当被测上游)与两条脱敏反向用例)见 [`P4.19-Web-MCP诊断.md`](P4.19-Web-MCP诊断.md);**P4.20 Web flint 数据图表渲染**(承 P4.13/P4.14 同骨架的**第三类内容渲染器**:把 wiki 页 / 对话答案里的 ` ```flint ` 围栏块([flint](https://github.com/microsoft/flint-chart) 语义图表规格 JSON)在浏览器内**编译**成 ECharts option 并渲染成 SVG 图表(37 种图型)——**服务端零改**(`render.py` 字节不动、转义姿态不破)、前端 **vendored** `flint-chart@0.4.1`(自包含单文件 **ESM**,经动态 `import()` 加载,本仓唯一走 ESM 的 vendored 资产,**仍无 npm、无构建**)+ `echarts@6.1.0`(自包含 UMD),P4.14 的 `enhanceContent` 编排器令**七个注入点零改动**;**上游触发条件**是观澜经 MCP 接入用户的结构化数据库后有了可视化需求——Agent 取数 → 把结果行写成数据**整体内联**的块 → 浏览器成图,**拒 `data.url`、渲染器永不出网**,故图 100% 可从那段 markdown 重建(原则 1 在本相位的形态)、`check`/`graph`/`search` 原样复用,图是**快照**不做渲染时查库/自动刷新/交互下钻;**安全闸**入口白名单(只五个顶层键)+ flint **生成式**编译(按模板生成、不 merge 用户私货,三条走私路径实测零残留)+ **键域**出口断言(不扫值域——合法数据里就有 URL 串)+ `renderer:'svg'` + `tooltip.renderMode:'richText'`(tooltip 不走 HTML 通道),**输入是纯 JSON、没有函数**故 ECharts 回调注入面天然不存在(这是选 flint 而非让模型直接写 ECharts option 的核心安全理由);**资源闸**(结构化库接入独有:输入是查询结果而非人写的短文本)块源 64 K 字符 / 1000 行 / 画布不过大,超限**不渲染、保留源码 + 徽标、不静默截断**,且 flint **自己**超预算整批丢类别时同样拒渲(读 `_warnings`);**总原则「能让 flint 自己算的绝不在事后改」**——按栏宽收窄走 `canvasSize` **重编译**而非事后改画布(flint 把 `legend.left` 等烤成绝对像素,缩画布会把图例挤出视口);ECharts 是**前端第一个有状态渲染器**,须显式 `dispose` 脱树实例;配套**独立**作者 skill `flint-chart-author`(模型不会自发写 flint,没有作者侧供给渲染器就是「通了电没有灯」;作图与 wiki 维护正交,故不进 `guanlan-wiki`,沿 `pdf-to-markdown` 先例);**已实现**:`static/vendor/flint/echarts.js` + `vendor/echarts.min.js` + `static/flint_enhance.js` + 编排器一行接线 + `skills/flint-chart-author/`,`tests/test_web.py` 16 例 + 真浏览器冒烟 `scripts/smoke_p420.py` 18/18(含 **37 图型全集扫描**——闸子不只要「拦得住」还要「不乱拒」,该扫描一次揪出四例误杀,已设为常驻场景 + 升级复核项))见 [`P4.20-Web-flint图表渲染.md`](P4.20-Web-flint图表渲染.md);**P4.21 IM 宿主 `guanlan im`**(P4 的**第三种传输**:Web 是浏览器里的人、MCP 是 Agent 客户端,IM 是**聊天窗口里的人**——同一套只读会话逻辑 `Conversation.turn()`、同一零写契约、同一零业务智能。形态是「**一个平台无关的核心 + 若干薄适配器**」而非「某一个平台的宿主」:核心永不写 `if platform ==`,一切分派只读 `AdapterCaps`;v1 两个适配器**恰好互补**(个人微信不能编辑、只能整条发;飞书能原地改写)——核心一行未改即驱动两者,这就是分界的验收标准。**新增的信任线是身份与受众面**(P4.11 管提示词注入、P4.17 管网络传输,两条都不覆盖它:一个零监听端口的进程,照样能经**出站**长连把整库答案送进一个 200 人的群),故默认 deny + 空白名单拒启,群聊放行 = `chat ∈ allow_chats` **AND** (`allow_all` **OR** `user ∈ allow_users`) **AND** `mentioned_me`(`--allow-all-users` **只**旁路用户名单、**永不**旁路群名单);身份可信度**分档**——企业系强档、个人微信弱档(iLink 标识不可枚举、不可反查),弱档只适合小圈子,且新增 `guanlan im-identify`(零 LLM、**绝不回复**、限时、把完整 ID 打到终端)解开「白名单要 ID、拿 ID 要看日志、日志脱敏」的死锁。会话 TTL 与腾容量**归宿主自持的 `SessionRegistry` 独占**(`store.idle_ttl=None`)——已核实 `store.get()` 命中即刷新 `last_active`、回收只在 `create`/`restore` 触发、顶满是**抛错而非逐出**,两套 TTL 并存反而造出一个谁都不负责的空档;过期语义靠**墓碑**(`(None, last_seen)`)保住。停机纪律:收流 task 可 `cancel()`(只阻塞在 I/O),问答 turn **绝不可以**——`arun` 收到 `CancelledError` 是置令牌后**立刻 re-raise、不等线程**,取消它是**把「还没停」伪装成「已经停了」**;故只 `request_stop()` 并**无限期**等真实收尾,30 秒只打 WARNING。**并如实声明优雅停机没有绝对时间上界**——模型侧的 `read=600` 是**每次读**的空闲上限而非整轮墙钟,外部 MCP 那条链上出站写入 / legacy SSE 传输进入 / stdio 传输进入 / 断连清理**都没有人管**(`--mcp-request-timeout` 只收住「写完之后等 `tools/call` 回话」那一段),**唯一的绝对保证是第二次中断信号的 `os._exit(5)`**(那条路上连 `logging` 都不许碰);总 deadline 记为 agentao 上游诉求。长答案契约是**「不静默截断」而非「绝不截断」**(三条原要求数学上不可同时满足),末片是显式截断告示且**不得称「完整版」**——IM 会话 `persist=False`,被舍弃的内容没存在任何地方;**已实现**:`guanlan/im/` 子包(contract/intake/session/reply/delivery/state/server/mcp_bound + 两个适配器)+ `guanlan im` / `im-login` / `im-identify` 三个子命令 + `[im-weixin]`/`[im-feishu]` 两个 extra,`tests/test_im{,_weixin,_feishu}.py` 136 例)见 [`P4.21-IM宿主.md`](P4.21-IM宿主.md);**P4.22 IM 可点引用**(给 P4.21 补最后一公里:答案里的 `[[某页]]` 在飞书里**可以点**、点完机器人把那一页发回来——**不是新宿主、不开监听端口、不引入任何写路径**,回调走已建好的那条 WS 长连故决策P4.21-21 无监听面继续成立、`/page` 只读盘故决策P4.21-8 KB 零字节写继续成立。三件事:**① `/page <名字>`**(所有平台、零 LLM,复用 P3.8 单一 owner 归口,故精确 stem / 别名(P3.1)/ 安全 fold variant(P3.8)三种写法都认——IM 侧另写一套按名查找就是给这个系统开第五个口径;未命中降级为 top-3 检索;截断告示**不附 Web 入口**,因为 Web 至今没有按名开页的路由、给出去仍是死链);**② 可点动作的平台无关表达**(`Action` 携带的是**核心生成的命令文本**而非"页面名"或"动作类型",适配器只把这句话塞进按钮、回调时原样取回——**它不知道 `/page` 是什么**;`AdapterCaps` 加 `supports_actions`/`max_actions`,与 `typing()` 完全对称,个人微信恒关、一个字不变);**③ 点击 = 一条合成的入站消息**(枢纽:回调经 `adapter.inbound()` **原路**回到既有流水线,五道闸全复用、**`server.py` 零改**、核心零平台分支,新增动作类型时适配器同样零改)。**红线的闸在接收端而非构造端**——`InboundMessage` 新增事实字段 `origin`,核心据此把合成消息能跑的命令收窄到 `ACTION_COMMANDS`(v1 只有 `page`,`/help`/`/new` 都是零 LLM 却**不在**其中:前者是噪音、后者会清用户上下文),因为 `classify()` 把**未知**斜杠命令当普通提问送进 LLM,只校验"以 `/` 开头"挡不住一次不该发生的 LLM 花费。**授权面**:`chat_type` 只认**真实入站消息见过**的会话(适配器自持 `(tenant, chat_id) → chat_type` 事实缓存、未命中 fail-closed)——飞书卡片可被转发,"卡片出现在哪里"不构成授权依据;`allow_chats` 一个字都不进适配器(静态断言守着),键的两半都取 `map_event` 同源字段(取错 tenant 的后果不是报错、是**间歇性"会话已过期"**)。**卡片只承载页面名、且只来自已确认送达的正文片**(`split_for` 超限会丢后缀,从完整答案抽会把正文里根本没出现过的页面名印上群内公开可见的卡片);出按钮前先解析一次,断链引用不出按钮(不承诺一个不存在的东西);超限**显式告示**、`[[原文]]` 始终保留故手打 `/page` 永远是退路。**CARD 帧兼容层**:`lark-oapi` 的 WS 客户端对 CARD 帧**直接 return**,而只有 EVENT 分支才走到卡片回调注册表,而"回调以哪种帧下发"证据**矛盾**(Go SDK 注释 vs 上游 issue #126 的真机 200340)——实现**不选边**,子类化 WS 客户端把 CARD 帧头就地改写成 EVENT 再委回父类,**纯粹的超集**(走 EVENT 时一次都不触发),探不到私有接缝即降级告警不拒启 + 真 SDK 形状探针,上游修好即可整段删除;**已实现**:`guanlan/im/pageview.py` + contract/reply/intake/delivery 扩展 + 飞书卡片与回调 + CARD 帧兼容层,`tests/test_im{,_feishu}.py` P4.22 套件;**帧类型 / 卡片 2.0 组件键形状 / 转发点击表现仍待真机核对**,见该文 §9)见 [`P4.22-IM可点引用.md`](P4.22-IM可点引用.md);**P4.24 MCP 技能发布**(issue #81:`guanlan mcp` 除工具外再经 MCP Skills 扩展(`io.modelcontextprotocol/skills`)发布**一份静态只读 skill** `guanlan-query`,把「`search` → `read_page` → 顺链 `read_page(name=…)` → 只据所读页作答并引用、库里没有就明说」这套查询流程交给客户端的技能发现、经用户同意激活——**不发** `guanlan-wiki`(它的写工作流依赖本地文件与写门禁,发到只读服务端等于许诺服务端没有的能力)。经 SDK v2 的 `Extension` 接缝挂 `skills/list`/`skills/get`(只在现代协议修订上应答,握手时代客户端只看到工具)+ 唯一资源 `skill://guanlan-query/SKILL.md`(不注册模板,任何其他 URI 一律 -32602);**清单与所发内容是同一份字节**(起服读一次原始字节,size/sha256/文本/frontmatter 全由它派生,不用会剥 BOM、转行尾的 `FileResource`);skill 文件放包内 `guanlan/mcp/published_skills/` 而非仓库根 `skills/`(后者是 Agentao 发现路径,开发态会被当成本地 skill);两种传输都发布、沿用同一道 token 闸、无旗标,坏文件降级不拒启。前置一步:`read_page` 新增可选 `name`,沿用 P3.8 单一 owner 归口按页名顺链(`path` 路由字节等价、越界照旧拒绝;入参须注解为纯 `str`,否则 SDK 会先 JSON 预解析把 `[[2024]]` 变成列表)。**已实现**:`guanlan/mcp/skills.py` + `published_skills/` + `tools.py`/`server.py`,`tests/test_mcp_skills.py` 41 例(含用 agentao 0.5.10 自己的条目/摘要/frontmatter 校验、真 stdio 子进程与真 HTTP 上的现代 era 客户端往返);**真机验收通过**(agentao 0.5.10 交互式激活 + 带引用作答,2026-10-04))见 [`P4.24-MCP技能发布.md`](P4.24-MCP技能发布.md);**P4.25 Web 思考过程显示**(纯表现层:agentao 早已发出 `THINKING` 推理事件、Web 嵌入此前在旁听口丢弃,现经 `transport.subscribe` 转成 SSE `thinking{text}` 帧,前端显示为答案气泡前默认折叠的**纯文本**块——推理里的 `[[链接]]` 是「考虑过」不是「引用了」,不渲染;**不计时**(模型端不流式给推理时上游答完才整段补发、可能晚于正文)、无调用分段、尾部 64 K 上限、`reasoning_only` 自动展开、顶栏开关只影响本浏览器;**不落盘、不回放**(推理不是事实来源,盘上载体恢复时还会被清),IM/MCP 不跟进,子 Agent 推理上游本就不转给父级;**已实现**:`conversation.py` 一个分支 + `chat.js` `handleThinking` 两流共用,契约用例走真调用路径覆盖流式与整段回退,真浏览器冒烟 `scripts/smoke_p425.py` 9/9)见 [`P4.25-Web思考过程.md`](P4.25-Web思考过程.md);**P4.26 Web 子 Agent 进展跟踪**(纯表现层:子 Agent 运行时在答案气泡前一行显示名称、最近观测的第 n/max 轮与最近调用的工具,结束时显示终态、轮数、工具次数与耗时;transport 旁听口把 `AGENT_START` / 子 `TOOL_START` / `AGENT_END` **无状态**投影成 SSE `subagent` 帧,前端按 agentao 0.5.17 的 `child_task_id`(观澜提的 jin-bo/agentao#578)配对,故依赖下限抬到 `>=0.5.17`;任务原文、参数、失败说明、token 用量不上线;每气泡至多 20 行、超出计数;不计时、不落盘、不回放,IM 不跟进;同版上游的子会话 HostEvent 转播不接(所需字段 transport 已全);**已实现**:`conversation.py` `_subagent_frame` + `chat.js` `handleSubagent` 两流共用,契约用例覆盖四种结局与父→子全链路,真浏览器冒烟 `scripts/smoke_p426.py` 8/8)见 [`P4.26-Web子Agent进展跟踪.md`](P4.26-Web子Agent进展跟踪.md);**P4.27 Web 图可视化**(兑现决策P3-7「图形化留 P4 Web UI」:只读端点 `GET /api/graph` 内存建图、服务端按视图裁剪聚合、不写 `graph/`、reader 下也注册;三个视图 社区总览 → 社区展开 → 单页一跳邻域,不画全图(真实库实测全图是散点或毛球);社区按锚页寻址、每视图 300 页上限显式告示、标题纯文本渲染;复用 P4.20 已内置的 ECharts、懒加载抽成公共加载器;`graph.json`/`graph.html`/`guanlan graph` 不变;**已实现**)见 [`P4.27-Web图可视化.md`](P4.27-Web图可视化.md) |
| **P5 — 语料规模化与自动化(多格式 + 检索)** | 多格式摄入、检索、定时同步 | **P5.0 检索层** `guanlan search`(确定性 BM25 + CJK 2-gram,零 LLM;E1 检索升级的零基建先行片;**已实现**)见 [`P5.0-检索层.md`](P5.0-检索层.md);**P5.1 Web 检索接入**(P5.0 内核接入长驻 Web 进程:`GET /api/search` + 只读 `guanlan_search` 宿主工具 + 长驻 `CorpusCache`;**已实现**)见 [`P5.1-Web检索接入.md`](P5.1-Web检索接入.md);**P5.2 多格式摄入** `guanlan convert`(复用 `pdf-to-markdown` skill 分层后端把 PDF/DOCX/… 转成 `raw/*.md` + `origin`,复用 `.md` ingest 不动、复用 `rawio` 写归口,脚本零 LLM、无新依赖;补 CLI 那个洞,Web 那半 P4.6 已落;**已实现**)见 [`P5.2-多格式摄入.md`](P5.2-多格式摄入.md);**P5.2.1 转换图片随源落盘**(P5.2 补漏半阶段:图片随转换文件一并落 `raw/images/<slug>/<slug>-N.ext`、重写 markdown 引用使 `raw/<slug>.md` 自洽,引擎无关、仍零 LLM、仍只写 `raw/`、`rawio` 一字不改;**已实现**)见 [`P5.2.1-图片落盘.md`](P5.2.1-图片落盘.md);**P5.3 检索 backlink 重排**(P5.0 BM25 召回叠一道确定性文档先验:命中页按入链数 `1+W·ln(1+c)` 温和乘性上浮、`compute_backlinks` 复用 `build_graph` 解析口径不另起第二套、CLI/Web/chat/MCP 四处同名次、热路 `CorpusCache` 按语料签名 memo、零 LLM 零写盘、收录门槛判在 boost 之前故召回集与纯 BM25 逐页一致;**已实现**)见 [`P5.3-检索backlink重排.md`](P5.3-检索backlink重排.md);**P5.4 检索冷启动性能**(`CorpusCache` 懒构建——首搜一次付清 `build_corpus`+`build_graph` 两笔全库扫描 ~1 分钟、压在用户关键路径上,2 次起 `(mtime_ns,size)` 署名 memo 命中即瞬时;**已实现**:候选①启动预热——长驻 Web/MCP 起服时 daemon 线程 `CorpusCache.prewarm` 把冷算移出关键路径 + 推荐(a) 按库根分桶 singleflight 构建锁令预热与并发首搜经同一把锁合流、不双重冷扫(决策P5.4-1/2,零新派生物/旗标/退出码/契约,守 P5.0 stateless 边界);候选②profile ③拆 corpus/backlink ④backlink 增量 ⑤持久化索引[E1] 均未做)见 [`P5.4-检索冷启动性能.md`](P5.4-检索冷启动性能.md);nightly health/lint |
**企业版(E,方向;个人版跑通后另立设计文档详述,见 §6)**
| 阶段 | 目标 | 方向 |
|------|------|------|
| **E1 — 存储与检索升级** | 大数据量 | markdown 仍是事实来源;index-only 撑不住时引入更强检索(选型另文)。**零基建先行片**(确定性 BM25 轻量检索)已先行规划为 **P5.0**(见 [`P5.0-检索层.md`](P5.0-检索层.md));**向量 / 重排 / 持久化索引**仍属本阶段(选型另文) |
| **E2 — 多租户权限** | 权限隔离 | source 级作用域 + 信任边界(本地全权 / 远程 scoped)。**会话分租先行**([`P4.9-只读多会话.md`](P4.9-只读多会话.md):只读多用户 / 能力 UUID 隔离 / 无用户管理,作 E2 前哨在 P4 半阶段落地);只读本地 **MCP 宿主**([`P4.10-MCP宿主.md`](P4.10-MCP宿主.md))作"远程 / scoped MCP"前哨,其 **Streamable HTTP 传输前哨** 已实现为 **P4.17**(localhost-first + 单 bearer-token 闸、无状态 + DNS-rebinding 防护;完整 OAuth/多租户 source 级 scoping 仍属本阶段)见 [`P4.17-MCP远程传输.md`](P4.17-MCP远程传输.md);**鉴权口径随协议演进**——P4.18 起底座为 MCP `2026-07-28`,其授权硬化(RFC 9207 `iss` 校验、DCR 废弃转 CIMD、凭据绑定 issuer)**全在 OAuth 面、仍归本阶段**,P4.18 本身不碰(见 [`P4.18-MCP2.0迁移.md`](P4.18-MCP2.0迁移.md) §5);**source 级作用域后续**(真正的 E2,动数据模型、排 P 轨之后);**IM 宿主([`P4.21-IM宿主.md`](P4.21-IM宿主.md))作身份面前哨**——它是 P4.9 会话分租模型的第二个宿主,**不动数据模型**(所有获准用户看同一个整库只读视图,隔离的只是各自的会话历史),并新引入一条正交的信任线:**提问者是谁、答案发给谁看见**;且身份可信度**分档**(企业系强档 / 个人微信弱档,弱档 ID 不可枚举不可反查、不得冒充强档)。source 级作用域仍属本阶段 |
| **E3 — 自动富化** | wiki 自保鲜 | health/lint/富化做成后台定时任务 |
---
## 8. 待定问题 / 风险
- **LLM 调用归口**:脚本保持**零 LLM**;ingest/query 的 LLM 部分由 `guanlan-wiki` skill 驱动 Agentao 完成(不让脚本自带 litellm / 自管密钥,也不把 LLM 能力反向塞进 Tool 接口)。语义 lint 后续再加。
- **wrapper 必要性**:MVP 需要真实的 `guanlan ingest/query`,但只做编排与确定性门禁;不做独立后端,不自管模型,不把 ingest/query 逻辑写成脱离 Agentao 的大脚本。
- **skill 分发**:开发期用观澜仓库 `skills/guanlan-wiki/`(repo-root 自动发现,免安装,见 §5.1);安装态下 skill 随 wheel 携带,wrapper 在 ingest/query 前**幂等装入全局 `~/.agentao/skills/`**(也可 `guanlan install-skill`),故 P2 既能在仓库内、也能在外部安装库跑通(落点 `guanlan/skill.py`)。分发渠道已落地:以 `guanlan-wiki` 发到 PyPI、skill 随 wheel 携带(见 §4.8)。
- **个人版 → 企业版的数据迁移**:markdown 为事实来源的前提下,索引重建应可幂等、增量(SHA256 缓存)。
- **CJK 检索质量(按需升级,不预先加机制)**:MVP 用 `index.md` + 2-gram 粗召回,召回不中时 Agent 扫目录或请用户补关键词兜底;后续仅在漏召回确有实证时再评估 aliases 或轻量检索,分词不优先(逐级增强备选见 `docs/backlog/notes/cjk-retrieval-enhancements.md`)。**别名已落 P3.1**;**轻量检索已细化为 P5.0**(确定性 BM25 + 2-gram,整页正文召回,见 [`P5.0-检索层.md`](P5.0-检索层.md));**检索增强线第④档 backlink 重排已落 P5.3**(命中页按入链数做确定性文档先验温和乘性上浮、复用 `build_graph` 解析口径、零 LLM 查询期算不落派生物,见 [`P5.3-检索backlink重排.md`](P5.3-检索backlink重排.md));向量仍留 E1、分词仍不优先。**检索性能线**:长驻 Web 进程首搜 ~1 分钟(`CorpusCache` 懒构建、冷算两笔全库扫描)已落 **P5.4 检索冷启动性能**(**已实现**:启动预热把冷算移出用户关键路径 + singleflight 构建锁令预热/并发首搜合流,守 P5.0 stateless 边界、零契约变更),见 [`P5.4-检索冷启动性能.md`](P5.4-检索冷启动性能.md)。
- **多格式摄入(已细化为 P5.2 + P5.2.1)**:PDF/DOCX/… → `raw/*.md` 的 adapter 已落 `guanlan convert`——复用 `pdf-to-markdown` skill 的 MinerU→marker→pypdf 分层后端、宿主侧零 LLM 写 `raw/`、复用 `rawio` 写归口,见 [`P5.2-多格式摄入.md`](P5.2-多格式摄入.md);转换抽出的图片随源落盘(`raw/images/<slug>/<slug>-N.ext`、重写引用使 `raw/<slug>.md` 自洽,引擎无关)已落 **P5.2.1**,见 [`P5.2.1-图片落盘.md`](P5.2.1-图片落盘.md)。**仍推后**:多格式**自动解析管线 / nightly 自动转换富化**(属 E3)、**高保真 OCR 多 backend 选型评估**。Web 转换接入降为 P4.6 的机会性优化(非阶段),MCP 明确不做 convert(写 `raw/` 与 P4.10 只读相悖)。
- **语义 lint(P3 之后)**:矛盾、过期论断、资料缺口等需 LLM 的检查,在结构 lint 跑通后再加;成本较高,应可按需触发而非每次 lint 全量跑。
- **graph 增强(P3 之后)**:在确定性 wikilink graph 之上是否加 LLM 推断边(`INFERRED`/`AMBIGUOUS`)、Louvain 社区检测、图感知**语义** lint、SHA256 增量缓存——成本/收益待评估,需可关闭与采样策略。(P3 的 `guanlan lint` 已是图感知的**确定性结构** lint,不在此推后项内。)其中 **Louvain 社区 + 拓扑 lint(hub/bridge/silo)的零 LLM 那半已细化为 P3.5**(见 [`P3.5-图谱分析.md`](P3.5-图谱分析.md),确定性、保 graph 可重建);**LLM 推断边**(非确定性、破坏 graph 可重建性)与 SHA256 增量缓存仍推后、需另开。
---
## 9. 参考
- Karpathy LLM Wiki 原始 pattern:[`../llm-wiki/llm-wiki.md`](../llm-wiki/llm-wiki.md) · [gist](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f)
- 纯 Skill 实现(个人版蓝本):[`../llm-wiki/llm-wiki-agent`](../llm-wiki/llm-wiki-agent) · [github](https://github.com/SamurAIGPT/llm-wiki-agent)
- 桌面应用实现:[`../llm-wiki/llm_wiki`](../llm-wiki/llm_wiki) · [github](https://github.com/nashsu/llm_wiki)
- 生产级 daemon(企业版蓝本):[`../llm-wiki/gbrain`](../llm-wiki/gbrain) · [github](https://github.com/garrytan/gbrain)
- Agentao 运行时:受治理的 Agent 运行时(Python,可嵌入 / CLI / ACP server);核心 API、ACP 协议、配置与示例见其自带文档
---
## 附录 A — P4 Web 宿主(非 MVP)
P4 可选:一个本地 Web host,复用同一 wiki 目录,给 CLI 之外加图形入口。**实现方案已细化为 [`P4-Web宿主.md`](P4-Web宿主.md)**,关键形态:**FastAPI/uvicorn(ASGI)薄表现层 + 自包含静态前端**把既有命令搬进浏览器,仅监听 `127.0.0.1`、单用户、强制 `workers=1`。**读写分线**:唯一写作业 `ingest` 复用 P2 `agentao run` 子进程 + 单写者门禁;**所有问答**(一次性单轮与多轮对话)因子进程无 session resume 而统一进程内嵌入 `Agentao`(`.arun`、默认只读、回调→token 流式,化解 P2 §4.3 四坑),Web 不设 `/api/query`。选 ASGI 是押注流式/多用户的"向前赌",并以工程约束补偿 async 与阻塞负载不匹配(决策P4-2)。
**仍不在 P4**:Tool 注入、把写(`ingest`)迁到嵌入、可写多轮工作会话、仓库内权限编程注入、跨会话**写**并发、ACP 备选——作为后续按需项,真要做时另开实现方案文档(参考 Agentao 的示例与嵌入式开发指南)。
(**Tool 注入的事实校正**:可写多轮工作会话已在 P4.5 落地;而"Tool 注入"经 P4.19 核实**并非"尚未开始"而是"早已默认开着"**——agentao 在调用方不传 `mcp_registry=` 时默认注入 `FileBackedMCPRegistry`,读 `<kb>/.agentao/mcp.json` + `~/.agentao/mcp.json`,观澜的 CLI 子进程与 Web 嵌入都吃这个默认。**P4.19 只补可见性、不建治理**(见 [`P4.19-Web-MCP诊断.md`](P4.19-Web-MCP诊断.md));注入方向的准入/白名单/只读断言仍**不立相位**,已核实的源码结论存放在 [`backlog/notes/mcp客户端注入-未排期.md`](backlog/notes/mcp客户端注入-未排期.md)。)