目标:把 v1.0 的"能存能搜的三层 KB"升级成"会自己长大的知识系统"—— ① 每天自动抓取并总结成信息日报;② 粘贴任意链接(公众号 / 网站 / B站 / YouTube) 一键转成带出处的 Markdown 知识;③ 所有知识用 index 织成链接图,memory 通过 link 调取。
参考:docs/PLAN_KNOWLEDGE_BASE_v1.0.md(已 shipped #235–#242)、 src/kb/、src/mail/(日报调度的现成范式)、src/prompt.ts。
Status: 提案(待评审) · 日期: 2026-07-23 · Author: Claude (for Oratis) 本文含现状 review(含 1 个已确认 bug)+ 完整设计 + 正反方辩论 + 分阶段 PR 计划。
| # | 能力 | 一句话 | 核心新模块 |
|---|---|---|---|
| K1 | 链接 → Markdown | 粘贴任意链接 → 带出处 frontmatter 的 Layer-1 source | src/kb/ingest/ |
| K2 | 抓取 + 信息日报 | 一份 watchlist → 每日拉取 → 分类总结 → 日报(推送 + 落库) | src/kb/feeds/ |
| K3 | 索引链接图 | [[slug]] 真正被解析成图;index 变 MOC;memory 用 link 调取 |
src/kb/links.ts |
三者不是三个独立功能,而是一条流水线:K2 发现 → K1 摄取 → K3 织网 → idle 蒸馏(v1.0 已有)→ 长成 wiki。K3 是黏合剂,也是最该先做的(见 §7 排序理由)。
| 层 | 代码 | 状态 |
|---|---|---|
| 存储 | src/kb/store.ts | frontmatter 读写、addSource / writeWiki / removeEntry、withFileLock + git commit、每次写入重建 index.md |
| 路径与越狱防护 | src/kb/paths.ts | ~/.lisa/kb/{sources,wiki},entryFile() 用 assertSafeSlug 做唯一收口 |
| 检索 | src/kb/search.ts | TF-IDF + mtime 指纹缓存 |
| 工具 | src/kb/tool.ts | kb_search / kb_read / kb_list / kb_add / kb_write |
| 常驻上下文 | src/prompt.ts:158 | SCHEMA.md(4KB 上限)+ index.md(2.6KB 上限)注入系统提示;getPromptFingerprint 含 KB 目录 → 会话内热重载 |
| 自主蒸馏 | src/idle/runner.ts:148 | idle 时读 index,提示 Lisa 用 kb_write 把新 source 蒸馏进 wiki |
| Web | src/web/server.ts:2567 + lisa-client.ts:2670 | /api/kb{,/search,/entry,/add,/remove} + Knowledge 视图 + 聊天选中消息存入 KB |
| 权限子集 | src/tools/registry.ts:204 | kb_add/kb_write remote-blocked、autonomous-allowed |
G1 · 没有摄取通道。 现有 web_fetch 只做有损纯文本——
htmlToText(web_fetch.ts:115)把所有标签、标题层级、
列表、代码块、链接、图片全部抹平成一坨文本。它能防 SSRF(fetchFollowingSafeRedirects
逐跳校验,值得复用),但产出不是 Markdown、没有元数据、没有去重、也不落库。
"粘贴链接变知识"目前是零。
G2 · 没有 KB 的定时任务。 全仓唯一的每日调度是 mail
(server.ts:606 的 30 分钟轮询 + isDigestDue 纯函数 +
重启补跑),KB 侧没有任何定时器、没有 watchlist、没有"昨天到今天新增了什么"的概念。
G3 · slug 铸造是纯 ASCII 的(对中文内容是硬伤)。
normalizeSlug(src/soul/slug.ts)执行
replace(/[^a-z0-9]+/g, "-") —— 一个中文标题会被整条剃成空串,addSource
随即回退到 entry-<timestamp>。今天每一条中文来源都会拿到一个无意义的时间戳
slug,而本次要灌进来的正是公众号 / B 站这类中文内容。必须先修。
G4 · index.md 是平铺 TOC,[[slug]] 没人解析。
regenerateIndexLocked(store.ts:192)输出的是
"标题 + 前 100 字"的两段列表。SCHEMA.md 里写着"用 [[slug]] 互链"
(src/kb/schema.ts:20),但全仓没有任何代码读它——
没有反向链接、没有孤儿检测、没有图。sources: frontmatter 记了出处却从不被查询。
G5 · 中文检索实际上是坏的(已实测确认)。 两处 tokenizer
(kb/search.ts:45 与 memory/vector.ts:212)
逻辑相同:把非 [a-z0-9一-鿿\s] 换成空格后按空白切分。中文不带空格,所以
文档 "这篇公众号文章讲的是知识库的设计" → tokens: ["这篇公众号文章讲的是知识库的设计"] ← 一整条
查询 "知识库" → tokens: ["知识库"]
交集 = ∅ → 命中 0
也就是说中文文档只能被"一字不差的整段查询"命中,等于不可检索。 (英文不受影响。)灌入中文内容前这是必修项——见 K3-a。
URL ─► normalize ─► adapter.fetch ─► extract ─► toMarkdown ─► provenance frontmatter
│
dedupe(hash) ◄────────────────────────────────┤
│ │
├─ 已存在 → 返回既有 slug(除非 force) │
└─ 新 → addSource()(Layer 1)───────┘
│
└─►(可选 distill:true) 立即触发一次 kb_write 蒸馏
保持 v1.0 的核心不变量:摄取只写 Layer 1,原样保真;蒸馏是 Layer 2 的活
(v1.0 §D5 的结论,这里继续遵守)。distill:true 只是把 idle 里那一步提前触发。
新目录 src/kb/ingest/:
| 文件 | 职责 |
|---|---|
index.ts |
ingestUrl(url, opts) 编排 + adapter 路由 |
adapters/{generic,wechat,bilibili,youtube,rssitem}.ts |
每站一个提取器 |
readability.ts |
正文抽取(打分选主节点,Readability 精简版) |
html-to-md.ts |
HTML → Markdown(标题/列表/代码/表格/引用/链接/图片) |
provenance.ts |
canonical URL、hash、frontmatter 字段 |
| 适配器 | 匹配 | 能拿到 | 可靠性 | 降级路径 |
|---|---|---|---|---|
| generic | 其余 http(s) | 标题/作者/发布时间(og:/JSON-LD)+ 正文 MD | 高 | 抽正文失败 → 全页 MD |
| wechat 公众号 | mp.weixin.qq.com/s* |
#js_content 正文、og:title、js_name 公众号名、发布时间、data-src 图片 |
中高(单篇文章页服务端可取;频控时会跳验证页) | 命中验证页 → 明确报错并提示"用手机分享→粘贴正文" |
| bilibili | bilibili.com/video/*、b23.tv |
标题/UP主/简介/时长/分P(x/web-interface/view,公开) |
元数据高 / 字幕低 | 字幕需 SESSDATA(用户可选填)→ 无则 yt-dlp(若装了)→ 再无则仅元数据 + 简介 |
| youtube | youtube.com/watch*、youtu.be |
元数据(oEmbed,稳定)+ 字幕(InnerTube player → captionTracks → fmt=json3) |
元数据高 / 字幕中低 | PoToken / exp=xpe 会返回空体 → yt-dlp → 仅元数据 |
| rss-item | 来自 K2 | 条目正文(content:encoded)或回落 generic |
高 | — |
| paste | 无 URL | 用户直接贴的正文 | — | — |
关于视频字幕,必须写在明面上:这是一个持续会坏的依赖。
YouTube 2025–2026 上线了 PO Token 机制,youtube-transcript-api 为此新增了
PoTokenRequired 异常;部分视频的 captionTracks.baseUrl 带 &exp=xpe,
即使带 cookie 也返回 200 空体;云厂商 IP 段还会被直接限流。B 站方面,
/x/player/v2 自 2022 起字幕需要登录态(SESSDATA),社区已确认未登录取不到。
所以设计上不把字幕当作成功条件:拿不到就落"元数据 + 简介 + 显式的
transcript: unavailable(reason)",并在返回值里告诉用户可以补贴文稿。
分层顺序固定为 内建 API → yt-dlp(可选外部二进制,PATH 上有才用)→ 仅元数据。
(进一步可选:yt-dlp 下音频 → 复用 src/voice/transcribe.ts
的 Whisper/Scribe 转写。成本高,默认关,LISA_KB_ASR=1 才开。)
不做的事(写进文档避免以后跑偏):不做公众号历史文章批量爬取、不做需要登录态的 批量抓取、不内置任何账号 cookie。用户自己填的 cookie 只用于他自己的内容。
见 §6 D1。结论:自建 ~350 行零依赖版本,但把 extract(html, url) → {title, meta, markdown}
定成接口,后续可替换 @mozilla/readability + turndown + linkedom 或外部 reader,
不改调用方。理由:本仓运行时依赖只有 5 个(package.json),
jsdom 体量与 linkedom 的兼容性坑都不值得为一个可自控的 350 行付出。
新增 src/kb/slug.ts:
export function kbSlug(input: { title: string; url?: string; date?: string }): string {
const ascii = normalizeSlug(input.title); // 复用现有 soft cleaner
if (ascii.length >= 3) return ascii; // 英文标题走原路
const d = (input.date ?? new Date().toISOString()).slice(0, 10);
const h = sha256(input.url ?? input.title).slice(0, 8); // 稳定、可去重
return `${d}-${h}`; // 例:2026-07-23-9f3ac1de
}slug 保持 ASCII(真标题永远在 frontmatter 的 title:)。理由见 §6 D6:
macOS 的 NFD/NFC 归一化、git 的 core.precomposeunicode、URL 编码,三者叠加会让
中文文件名在跨设备同步时变成难查的幽灵 bug;而 slug 只是内部标识符,可读性由标题承担。
当前 serializeEntry(store.ts:90)字段是硬编码的。
加一个 extra?: Record<string, string> 透传,摄取写入:
---
title: 某公众号文章标题
tags: [ai, 阅读]
created: 2026-07-23T10:12:00.000Z
origin: web # ← 关键:标记"外部不可信内容",见 §8
url: https://mp.weixin.qq.com/s/xxxx
site: mp.weixin.qq.com
author: 某某公众号
published: 2026-07-21
lang: zh
hash: 9f3ac1de… # sha256(canonicalUrl),去重键
via: paste | brief | share # 怎么进来的
---hash 落在 frontmatter,同时维护 kb/.ingested.json(hash → slug 映射,
损坏可从 sources 全量重建)。默认同 URL 不重复写,直接返回既有 slug;
force:true 时写新条目并加 supersedes: <old-slug>。这样"来源不可变"
(v1.0 §10)与"文章会更新"两个诉求都不破。
| 入口 | 形态 |
|---|---|
| 工具 | kb_ingest(url, title?, tags?, distill?) |
| CLI | lisa kb add <url> [--tag x] [--distill](新 lisa kb 子命令,照 src/cli/mail.ts 的形状) |
| Web · Knowledge 视图 | 顶部一个"粘贴链接"输入框 → 进度 → 成功后直接打开该条 |
| Web · 聊天 | 用户消息里检测到裸 URL → 气泡下方出现"存入知识库"小按钮(复用已有的 /api/kb/add 交互模式,lisa-client.ts:1337) |
| 移动端分享 | iOS/Mac 分享面板 → POST /api/kb/ingest。本次不做,但 HTTP 接口按能被分享面板直接调用来设计 |
整体照抄 mail 模块的形状(它已经在线上跑了一年半的同一个问题:定时拉取 → 分类 → 日报 → 推送),差别只在数据源。能复用的绝不重写。
文件不存在或 feeds 为空 → 整个能力完全惰性(与 mail 未连账号时一致,
server.ts:588 的判据)。
RSS/Atom 是主干,理由是它零依赖可解析、且覆盖面最广:
YouTube 频道仍开放 feeds/videos.xml?channel_id=;绝大多数博客/新闻站有 feed。
公众号和 B 站没有官方 feed —— 诚实的答案是支持 RSSHub 兼容 URL
(用户自建或公共实例),而不是我们去逆向它们的私有接口。这既是工程上最稳的,
也是唯一不踩 ToS 的路。kind 里保留 site(无 feed 站点的轻量首页 diff)作为兜底,
但默认不启用(噪声大)。
| 步骤 | mail 的实现 | KB 版 |
|---|---|---|
| 是否该跑 | isDigestDue(last, now, hour) 纯函数(mail/scheduler.ts:19) |
isBriefDue(...) 同签名 |
| 定时 | 30 分钟 setInterval + 20 秒重启补跑(server.ts:606) |
同 |
| 拉取 | IMAP 增量(seen-UID) | 各 feed 增量(lastSeenId + 发布时间) |
| 分类 | classifyMail 批量小模型 → category + importance |
classifyItems 同形状:{category, importance 0–3, oneLine} |
| 成品 | buildDigest 纯函数 → DailyDigest |
buildBrief 纯函数 → DailyBrief |
| 投递 | pushBridge.onMailDigest + broadcast(idle_message, source:'mail') |
onKbBrief + source:'kb' |
| 落盘 | ~/.lisa/mail/digests/<date>.json |
同时落 ~/.lisa/kb/feeds/<date>.json(结构化)和 sources/brief-<date>.md(Layer-1 知识) |
最后一行是关键区别:日报本身就是一条知识。它进 Layer 1 之后,
自动获得检索(kb_search)、链接(K3 把它链到当天摄取的每篇文章)、
以及 idle 蒸馏("这周我读了什么" 自然长成一张 wiki 页)。
mail 的 digest 只是通知,KB 的 brief 是语料。
# 信息日报 · 2026-07-23
12 条新内容 · 3 条值得读。
## 值得读
- **[标题]** — 一句话说清它讲了什么。为什么值得你读:… `[[2026-07-23-9f3ac1de]]` ↗原文
## 其余(按主题)
### AI
- 标题 — 一句话 ↗
### 工程
…"值得读"的判定复用 mail 的 importance 分级思路,但排序信号换成个人化的: 标签匹配 watchlist 权重 + 与 KB 现有 wiki 页的 TF-IDF 相似度(读者是谁,KB 已经知道了)
- 与
MEMORY.md/USER.md里的兴趣。这是 LISA 相对通用 RSS 阅读器的真正差异点。
Top-N 自动全文摄取(默认 N=3,可配 0 关闭):只有"值得读"的条目才走 K1 拿全文, 其余只留标题+一句话。这样每天的 token 与磁盘开销都是有界的。
抄 heartbeat 的做法(heartbeat/config.ts:20 的
budgetTokens + DEFAULT_HEARTBEAT_BUDGET_TOKENS):feeds.json 的 budgetTokens
默认 120k/天,超了就跳过剩余条目并记日志而非静默丢弃。分类走一次批量调用,
不是一条一个请求。
不新增调度器——注册成一条 heartbeat 任务即可(用户自己的
~/.lisa/heartbeat.json 保留全量工具集)。周日跑:"读过去 7 份 brief +
本周新增 sources,写一页 wiki/weekly-<date>"。这条天然接上 Reve/idle 的既有循环。
新 src/kb/links.ts,输入全部条目,输出:
interface KbGraph {
forward: Map<string, string[]>; // slug → 它指向谁([[slug]] + sources: + url 引用)
back: Map<string, string[]>; // slug → 谁指向它 ← v1.0 完全没有
orphans: string[]; // 无入无出
hubs: { slug: string; score: number }[]; // 按 入度×新鲜度 排序
tags: Map<string, string[]>;
}边的三个来源:正文里的 [[slug]] / [[slug|显示文本]]、wiki 页的 sources: frontmatter
(v1.0 已经在写,只是没人读)、source 的 url 与 brief 的引用。
缓存策略直接复用 search.ts 那套 mtime+size 指纹(search.ts:75),
KB 不变时零成本。
约束没变:注入系统提示时被砍到 2.6KB(prompt.ts:165)。 所以 KB 一大,平铺列表就会被从中间截断——恰恰是最有价值的枢纽页被切掉。改成:
# 知识库索引
_31 wiki · 214 sources · 更新于 2026-07-23_
## 枢纽(按被引用次数)
- **OAuth 与 PKCE** (`oauth`) ↔7 · #auth #security — 一句话主旨
- **LISA 架构** (`lisa-arch`) ↔5 · #project — …
## 按主题
#ai(12) #auth(4) #reading(9) …
## 最近摄取
- 2026-07-23 · 某公众号文章标题 (`2026-07-23-9f3ac1de`) ← 只给标题,不给正文摘录(§8)
_3 条孤儿页待整理:… (idle 时处理)_排序由 hubs(入度×新鲜度)决定,截断从尾部发生,枢纽永远在前 2.6KB 内。
同时产出 kb/index.json 给程序用(Web 图视图 / 工具),index.md 只服务人和提示词。
kb_read返回值末尾追加**被引用:** [[a]] [[b]](反向链接是"顺藤摸瓜"的关键)。kb_read接受[[slug]]原样输入(模型经常直接把 wikilink 抄进参数)。- 新工具
kb_links(slug)→ 前向/反向/相关(共享标签 + TF-IDF 近邻)。 - Web Knowledge 视图:条目页底部加"被引用/引用"两列;先做列表,不做 d3 力导向图 (§6 D5:图好看但对导航的边际价值远低于反向链接列表)。
这是三条需求里最容易做偏的一条,说清机制:
- memory 里存 link,不存内容。
MEMORY.md只有 4KB 上限 (memory/store.ts:5),本来就装不下知识。 约定 memory 条目可以写[[kb:oauth]],例如- 用户在做 OAuth 迁移,细节见 [[kb:oauth]]。 - 常驻提示里 link 自解释。 组装系统提示时,把 memory 行里的
[[kb:slug]]就地补上标题 →[[kb:oauth]](OAuth 与 PKCE)。Lisa 因此在看到 memory 的同一瞬间 就知道该不该展开,而不是先猜一次。成本是几十字节。 - 展开靠既有工具。
kb_read('oauth')就是展开动作,不需要新机制。 - 反向自动建立。 idle 蒸馏(v1.0 已有的那一步)写完 wiki 页后,若该主题在
memory 里已有条目而没有 link,就补一条
[[kb:slug]]进去。 这一步才是"memory 调取 link"真正闭环的地方——不靠用户手写。 - 自动互链(保守版)。 蒸馏时扫描新页正文里出现的已有页面标题全词匹配,
转成
[[slug]]。只做全词、只做标题、只在 wiki 层做——宁可漏不可错, 错的互链会污染图并让 hubs 排序失真。
tokenize() 加 CJK 二元组:遇到连续 CJK 字符串时,既产出整串、也产出所有 2-gram。
"知识库的设计" → ["知识库的设计", "知识", "识库", "库的", "的设", "设计"]
查询 "知识库" → ["知识库", "知识", "识库"] → 命中 ✓
二元组是中文检索里"零依赖 / 无词典 / 召回优先"的标准做法,且对 IDF 友好。 两处都要改(kb/search.ts:45、 memory/vector.ts:212),因为 memory_search 面对的 中文对话记录有一模一样的问题。改完索引缓存指纹会自然失效,无需迁移。
~/.lisa/kb/
├── SCHEMA.md # v1.0(补充摄取/日报/互链三段工作流说明)
├── index.md # v1.0 → MOC(K3)
├── index.json # 新:链接图(K3)
├── .ingested.json # 新:hash → slug 去重表(K1)
├── feeds.json # 新:watchlist + briefHour + budget(K2)
├── feeds/<date>.json # 新:结构化日报(K2)
├── sources/ # v1.0;新增 brief-<date>.md 与摄取来的文章
└── wiki/ # v1.0
| 工具 | 权限 | 说明 |
|---|---|---|
kb_ingest(url,…) |
remote-blocked;autonomous 受限(§8) | K1 |
kb_brief(date?) |
read-only, remote-safe | 读某天日报 |
kb_feeds(action,…) |
remote-blocked | 管 watchlist |
kb_links(slug) |
read-only, remote-safe | K3 |
| HTTP | 用途 |
|---|---|
POST /api/kb/ingest |
粘贴链接 / 分享面板 |
GET /api/kb/brief?date= |
日报 |
GET,POST /api/kb/feeds |
watchlist CRUD |
GET /api/kb/graph |
index.json |
- 依赖派:
@mozilla/readability + turndown是业界标准,2026 年的横评里 Readability 84%、Jina Reader 81%,成熟度高于任何自建。 - 远程派:
r.jina.ai前缀即用,零维护。 - 自建派(采纳):远程 reader 把用户每一篇要读的东西都发给第三方,
直接违背 LISA 的"100% 本地"承诺(v1.0 §10),出局。npm 派需要一个 DOM
(
jsdom体积大、linkedom与部分抽取器不兼容),而本仓只有 5 个运行时依赖。 自建 350 行拿到 80% 的效果,且公众号/B站这些站点本来就要写专用适配器—— 真正决定质量的是适配器,不是通用抽取器。 - 共识:自建 + 明确的
extract()接口,把升级成本压到"换一个实现"。
- 正:Karpathy 三层的根基就是 Layer 1 不可变。
- 反:同一个 URL 明天内容就变了,硬拒绝会让用户困惑。
- 共识(采纳):默认按 hash 去重、返回既有 slug;
force时新增条目并用supersedes:串起来。不可变性未破,历史可追。
- 正:Lisa 主动读、主动补充知识,才是"会长大的知识库"。
- 反:
AUTONOMOUS_BLOCKED_TOOL_NAMES的注释已经点名了这个风险 (registry.ts:146)——"web_fetch 内容里的间接注入 可能种下恶意 desire"。而 KB 比那更危险一步:摄取内容会进index.md, 而index.md每一轮都注入系统提示。这是一条从"任意网页"到"常驻提示词"的直通路。 - 共识(采纳,三重收口):
- 自主运行只能摄取 watchlist 里的 URL(用户自己配的域),不能摄取模型自选的任意 URL;
origin: web的 source 在index.md里只出标题、不出正文摘录(§4.2 已体现)—— 切断"网页正文 → 系统提示"的通路;kb_read返回外部来源时用不可信内容围栏包裹,正文里的指令一律当数据。 用户手动粘贴的链接不受限制(那是用户的明确意图)。
- 正:定时对外发网络请求,形态上接近 mail(mail 有
mailconsent)。 - 反:consent 框架管的是敏感的环境采集(屏幕/麦克风/剪贴板/邮件内容) (consent/store.ts:31)。抓取的是用户自己填进 watchlist 的公开 URL,既不涉他人隐私也不涉本机环境;多一个开关是仪式感不是安全。
- 共识(采纳):不新增 consent signal。
feeds.json缺失/为空即完全惰性, 这已经是与 mail 完全同构的"默认关"。但 UI 上必须显示"每日 N 点会去拉这 M 个源"。
- 正:平铺简单、可预期。
- 反:2.6KB 硬截断意味着 KB 一大就随机丢内容,而丢掉的可能正是枢纽页。
- 共识(采纳):按
入度×新鲜度排序的 MOC,截断只发生在尾部。 代价是要维护图(K3 本来就要做)。
- 正(中文 slug):
wiki/知识库.md一眼可读,assertSafeSlug也允许非 ASCII。 - 反:macOS 存储用 NFD、Linux/git 用 NFC,跨设备同步会出现"看起来一样但打不开"的文件;
URL 里还要 percent-encode。而 slug 是内部标识符,可读性由
title:承担。 - 共识(采纳):ASCII slug(
<日期>-<8位hash>),标题保留原文。 顺带修掉 G3 —— 今天中文标题拿到的是无意义时间戳,改完至少日期+稳定哈希可去重。
- 正(单独):结构化、好渲染、不污染 sources。
- 反:日报不进 KB 就搜不到、链不上、蒸馏不到,等于每天生产完就扔。
- 共识(采纳):两份都写——
feeds/<date>.json给 UI,sources/brief-<date>.md给知识系统。冗余几 KB 换来"日报本身是语料"。
- 正(自己抓):不依赖外部服务。
- 反:公众号无公开列表接口、B 站空间接口已 wbi 签名且随时会变, 自建等于长期维护一个反爬军备竞赛,且踩 ToS。
- 共识(采纳):单篇文章的摄取自己做(K1,用户给了链接就是明确授权), 订阅发现交给 RSSHub 兼容 URL(K2)。这条分界线同时是工程上和合规上的最优点。
每个 PR 独立可合、typecheck + test 全绿。预计从 #278 起。
| PR | 内容 | 关键测试 |
|---|---|---|
| K-A | 本文档(设计定稿) | — |
| K-B · 地基 | ① CJK 二元组分词(kb/search.ts + memory/vector.ts);② kb/slug.ts 中文 slug;③ store.ts frontmatter extra 透传 |
中文查询能命中中文文档(G5 回归);中文标题产出稳定 slug(G3);frontmatter 往返稳定 |
| K-C · 链接图 | kb/links.ts(前向/反向/孤儿/枢纽)+ index.json + index.md 升级为 MOC + kb_links 工具 |
[[slug]]/sources: 双向解析;截断后枢纽仍在;孤儿检出 |
| K-D · memory ⇄ link | [[kb:slug]] 在系统提示里补标题;kb_read 接受 wikilink;idle 蒸馏回写 link;kb_read 附反向链接 |
提示里 link 带标题;蒸馏后 memory 出现 link |
| K-E · 摄取引擎 | readability.ts + html-to-md.ts + provenance.ts + generic 适配器 + kb_ingest 工具 + 去重 |
HTML→MD 结构保真(标题/列表/代码/表格);SSRF 复用测试;同 URL 去重 |
| K-F · 站点适配器 | wechat / bilibili / youtube / b23 短链 + yt-dlp 可选分层 + 降级路径 |
用离线 HTML 固件测提取(不打真实网络);字幕缺失时的降级断言 |
| K-G · 摄取界面 | POST /api/kb/ingest、Knowledge 视图粘贴框、聊天 URL "存入知识库"按钮、lisa kb add <url> CLI |
HTTP 契约测试;快照测试(lisa-html-snapshot.test.ts) |
| K-H · 日报 | kb/feeds/(RSS/Atom 解析、增量、分类、buildBrief 纯函数、isBriefDue)+ server 定时器 + push + brief 落 Layer 1 + Brief 视图 |
纯函数全覆盖(照 mail/digest.test.ts);空 watchlist 完全惰性;预算超限跳过并记日志 |
| K-I · 收尾 | SCHEMA.md 补三段工作流;kb_ingest 自主受限(D3 三重收口);周报 heartbeat 模板;本文状态改 shipped |
自主子集里 kb_ingest 受 watchlist 限制;origin: web 不进 index 摘录 |
排序理由:K-B/K-C/K-D 先行看着"不出活",但它们是另外两条的地基—— 在中文检索坏着、slug 是时间戳、链接没人解析的状态下先灌内容,等于制造 一堆事后要迁移的数据。先修管道,再开闸。 最小可用切片是 K-B + K-E + K-G(粘贴链接就能用), K-H 单独就能交付日报,K-C/K-D 让整个东西从"文件堆"变成"知识网"。
- SSRF:所有出站抓取走既有的
fetchFollowingSafeRedirects(web_fetch.ts:75,逐跳校验私网/回环),不另起 fetch。 - 提示注入(最需要盯的面):外部内容 →
index.md→ 系统提示是一条真实通路。 三重收口见 D3:自主摄取限 watchlist、origin: web不进索引摘录、kb_read外部内容加不可信围栏。 - 越狱:摄取仍然只经
addSource()→entryFile()→assertSafeSlug()这条唯一收口 (paths.ts:48),不新增写路径。 - 凭据:B 站
SESSDATA之类由用户自愿填入feeds.json,0600,不进 git、不进提示词、 不随 KB 仓库提交(kb/.gitignore排除feeds.json)。 - ToS 与礼貌:尊重
robots.txt、每站串行 + 退避、真实 UA 标识Lisa/…; 不做登录态批量抓取;不做公众号历史全量爬取。 - 成本:日报每天一次批量分类调用 + 至多 N 篇全文;
budgetTokens硬闸门; 磁盘上每篇文章几十 KB,且 KB 有独立 git 仓(v1.0 §D4)不会拖累 soul。 - 本地性:全部落
~/.lisa/kb/,除了抓取目标站点本身,不向任何第三方发送内容 (这条直接否掉了远程 reader 方案,见 D1)。
开放问题
- 图片怎么办?公众号图片有 referer 校验,外链在 KB 里大概率显示不出来。
选项:(a)只留链接(本次默认);(b)下载到
kb/assets/(占空间但离线可读)。倾向 (b) 但放到 K-F 之后单独评估。 sources/会长很大之后,listEntries()每次全量读盘重解析(store.ts:163) 会变慢——需要一个元数据缓存。过千条再做,别提前优化。- 语义检索:v1.0 就留了口子(复用 memory/embedding.ts 的 可选 Ollama 嵌入)。CJK 二元组修完后先看 TF-IDF 够不够,不够再上。
- 日报的"值得读"判定要不要让用户反馈(👍/👎)来调权重?这会把它从静态规则变成 会学习的东西——很诱人,但先跑两周静态版本看真实噪声率再定。
非目标(本轮明确不做)
- 公众号/B站的账号级批量爬取与历史归档。
- 浏览器扩展 / 分享面板客户端(HTTP 接口预留,客户端另开)。
- 知识图谱可视化(力导向图)——先做反向链接列表。
- 多人协作 / KB 共享——KB 是单用户本地资产。
摄取可行性部分基于以下公开资料(2026-07 核对):
- YouTube 字幕/PO Token 现状:youtube-transcript-api releases、issue #592
exp=xpe空响应、YouTube timedtext endpoint、SkipTheWatch: API not working - B 站字幕需登录:bilibili-API-collect issue #778、播放器接口文档
- 公众号正文结构(
#js_content/data-src):codex 抓取公众号 skill、Python 抓取公众号文章内容 - 提取器横评(Web2MD 94% / Trafilatura 87% / Readability 84% / Jina 81%):Best Web to Markdown Tools 2026、Readability 抽取实践
{ "feeds": [ { "id": "hn", "kind": "rss", "url": "https://news.ycombinator.com/rss", "tags": ["tech"], "max": 10 }, { "id": "karpathy","kind": "youtube-channel", "url": "…/feeds/videos.xml?channel_id=UC…", "tags": ["ai"] }, { "id": "某公众号", "kind": "rss", "url": "https://rsshub.example/wechat/…", "tags": ["读物"] } ], "briefHour": 8, "budgetTokens": 120000 }