用户原话档案(needs-archive)
把项目用户自己在对话里提交的原话按主题存进项目内的 .needs/ 目录,开发中途需要核对「当初到底怎么说的」时,按主题或关键词取回原文。
- 插件名:
needs-archive - 包名:
@local/needs-archive,版本0.1.0 - 类型:Host-only Cordis bundle(没有前端面板),一个 profile 一份
- 目标环境:DeepSeek Harness
0.2.2-rc.1 - 数据位置:项目根目录下的
.needs/(不是用户目录、不是全局存储)
它做什么
DSH 把用户提交的消息以 user/message 事件写入会话。本插件订阅 session/event,只挑出其中作者本人写的文本:
event.type === 'user/message',且data.role === 'user'data.source.kind === 'user':goal 轮次、compaction 摘要、agent-instructions、working-directory 等插件注入的消息同样以user/message出现,但它们的source.kind不是'user',一律不碰- 作者会话判定:子代理的初始任务 prompt 也是
user/message且source.kind === 'user'(写它的是委派它的 agent),所以还要看会话 header——origin: 'subagent'、存在parentSession或delegationDepth > 0时整会话排除(lib/session-scope.js) - 只取
content里type: 'text'的块;单块原样使用,多块之间用一个换行符连接,没有文本块则跳过
命中的消息交给启发式分类器判断「记不记、记成哪种、归到哪个主题」,然后以逐字节原文写入:
<project>/.needs/<domain>/<topic>/<YYYY-MM-DD>-<短标题>-<hash8>.md
同时注册两个工具:
needs_query:按主题/关键词回查原话,返回项目相对路径 + 原文 + 元数据needs_archive:补录自动捕获没归档的、插件加载后已观察到的消息(例如被启发式跳过),可手动指定域/主题/标题/kind(只影响元数据,原文不变)
可选地,当项目已有档案时,插件会向模型上下文注入一小段提示:需要精确原话时可以回查档案。这只是提示,不是工作流步骤。
边界(它不做什么)
- 不是工作流引擎,不强制 agent 先查档案,也不改变任何任务的执行顺序。
- 档案是历史引用:后一条记录是更新的说法,但档案里的文字不覆盖当前指令,也不构成任何授权。
- 不总结、不改写、不翻译、不脱敏后保存。命中的密钥与明显个人信息会被直接跳过(不存),而不是存一份打码版。
- 不修改、不删除旧记录。更正和撤回都是新增一条记录,原文件保留。
- 不跨项目共享。
.needs/属于它所在的项目;两个项目各存各的。 - 不归档子代理会话。委派的子代理初始任务是
user/message且source.kind也是user,但那是委派它的 agent 写的;会话 header 带子代理标记(origin: 'subagent'、parentSession或delegationDepth > 0)时,整会话不喂档案,needs_archive也会直接报错。 - 不做模型分类。分类是关键词与句式启发式,会分错;可用
needs_archive的domain/topic覆盖元数据,或直接手工整理目录。
工作流程
用户提交消息
└─ DSH 提交 user/message 事件
└─ 插件:作者会话判定(子代理/子会话整体不归档)
└─ 来源过滤(source.kind === 'user',仅文本块)
└─ 只读检查(read-only 会话跳过)
└─ 项目根解析(显式 projectRoot → 最近 .git → 最近 .needs → 当前 .dsh)
└─ 分类器:跳过(寒暄/确认/过短/一次性操作/密钥/个人信息/明确不记)
或归档(statement / supplement / correction / retraction)
└─ 正文 sha256 查重(同一项目只存一份)
└─ 经 ctx.fs + 会话 sandbox 策略写入 .needs/
写入遇到只读会话直接跳过;项目根无法确定时本轮暂缓(deferred),不落盘。
快速开始
前置条件:DSH 0.2.2-rc.1 正在运行,插件目录已在工作区(<workspace>\needs-archive)。插件不依赖 npm 包、没有构建步骤。
安装(任选一种,不要手改 profile 的 package.json 或组合包列表):
- GUI:侧栏「插件」→「添加插件」→ 输入插件目录的绝对路径(例如
C:\Users\<you>\Documents\deepseek-harness\default-workspace\needs-archive)→ 安装。安装成功后该包出现在「已安装插件」里。 - 让会话里的 agent 调
plugin_manager:action: install_bundle,target传同一个绝对目录。返回application: applied表示已生效;安装需要批准时按提示确认。
安装后,在任意一个含 .git(或已有 .needs/,或配置了 projectRoot)的项目里新建会话,发一条真正的需求,例如:
侧边栏宽度固定在 240px,不要自适应;窄屏下保持 240px 并出现横向滚动。
提交后,项目里应出现类似文件:
<project>/.needs/inbox/unclassified/2026-11-11-侧边栏宽度固定在-240px-不要自适应-窄屏下-d8e76885.md
没配 domains 时域是 inbox、主题是 unclassified;文件名里的短标题来自正文标题(保留汉字,最多 24 个码点,标点与空格转成 -),标题全文保留在 frontmatter 里。
之后问一句「我当初侧边栏是怎么说的?」,agent 会调用 needs_query,返回那条原文与路径。
安装
前置检查
| 项目 | 要求 |
|---|---|
| DSH 版本 | 0.2.2-rc.1(插件只使用 Host 服务 tools / fs / systemPrompt / workingDirectory,sandboxPolicy 可选) |
| Node | 随 DSH 内置的运行时即可;本地跑测试需要 Node 24 及以上 |
| 目录 | 插件目录必须包含 package.json、cordis.patch.yml、index.js |
| 权限 | 安装会写入当前 profile,需允许该操作 |
安装步骤
- 确认插件目录的绝对路径。
- 用上面「快速开始」里的 GUI 或
plugin_manager方式安装。本地目录安装会以link:方式登记到 profile 的package.json(由插件管理器写入,不要自己改),并默认启用。 - 判读结果:
install_bundle返回里application为applied、warnings为空或与你无关;插件页的「已安装插件」出现@local/needs-archive,其组合包详情里有一行 id 为needs-archive且在启用状态。安装接口的返回与插件页状态才是判据,不要用进程列表或页面是否刷新来判断。 - 在新会话里确认工具存在(
needs_query/needs_archive),并确认插件启动日志(插件用 Host logger,前缀needs-archive:;当前日志级别下可能看不到ready行)。
插件代码更新之后
本地目录是链接安装,profile 里指向的就是工作区这份代码。实测(2026-11-11):改完 index.js / lib/** 后必须重启 DSH 才能载入新的模块代际;plugin_manager 禁用/启用插件行只会重挂入口,不会刷新已加载的 ESM 模块缓存(旧模块会继续跑)。headless 每次都是新进程,不受此影响。
配置改动:桌面 profile 启用了 HMR,改 profile patch 后可能热重载;这一点没有单独实测,改完建议重启确认。
配置速查
所有字段都可省略,省略即用默认值。完整字段说明、边界与示例见 docs/configuration.md。
| 字段 | 类型 | 默认值 | 一句话作用 |
|---|---|---|---|
enabled |
boolean | true |
总开关;false 时插件不监听、不注册工具 |
minLength |
integer | 2 |
自动归档的最短字符数(按 Unicode 码点;0–1000) |
domains |
array | [] |
关键词 → 域目录映射,每项 { name, keywords, defaultTopic? } |
defaultDomain |
string | inbox |
没有命中任何域关键词时使用的域目录 |
secretPatterns |
array | [] |
额外的密钥匹配模式,命中即跳过 |
skipPatterns |
array | [] |
额外的「永不归档」匹配模式,命中即跳过 |
promptMode |
string | auto |
上下文提示策略:auto / always / never |
maxQueryResults |
integer | 20 |
needs_query 不传 limit 时的默认返回上限(1–500) |
projectRoot |
string | null | null |
显式项目根,给非 Git 项目或无 .git 的目录用 |
配置写在你自己的 profile patch 层($env:DSH_HOME\profiles\<profile>\cordis.patch.yml)里,对 needs-archive 行做 override。注意:按 id 定位的 override 会把该行的整个 config 替换掉,不会与之前的值深度合并,因此这一行里要写全你想保留的字段。
- id: needs-archive
name: "@local/needs-archive"
config:
enabled: true
domains:
- name: ui
keywords: [侧边栏, sidebar, 按钮, 布局]
defaultTopic: sidebar
- name: api
keywords: [接口, api, 端点]
defaultTopic: api-design
promptMode: auto
插件没有声明配置 schema,也没有前端配置页,所以当前版本不会在插件页出现配置表单;改配置就是改上面这段 patch。不合法的字段会在启动时回落到默认值,并在日志里以 needs-archive: config: … 给出告警。
使用
三类操作,详见 docs/usage.md:
- 自动归档:插件加载后,每条提交的人写消息都会被评估,不需要调用工具。
- 回查:让 agent 调
needs_query,或直接打开.needs/下的 Markdown 文件。 - 手动补录:对插件加载后、被自动分类跳过的已观察消息,或想显式指定主题/kind 的记录,调
needs_archive。
回查样例:
needs_query(query: "侧边栏")
needs_query(domain: "ui", topic: "sidebar")
needs_query(query: "240px", kind: "correction")
文件格式
每条记录是一个 Markdown 文件:YAML frontmatter(下文 --- 开闭)+ 一份逐字节原文。
---
format: 1
id: "0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0"
sha256: "…64 位小写十六进制…"
created: "2026-11-11T02:00:01.000Z"
domain: "ui"
topic: "sidebar"
subtopic: null
title: "侧边栏交互"
kind: "statement"
related: []
session: "session-a"
message: "msg-1"
source: "user"
pluginVersion: "0.1.0"
---
## 侧边栏交互
希望侧边栏默认是**收起**的,鼠标悬停时才展开;移动端不要跟随页面滚动。
- 展开时保留上次选中的 tab
- 收起动画 120ms,别用弹跳
- 键盘聚焦时展开
(正文对应 scripts/verify.mjs 场景 1 实际写入的内容,逐字节一致;id / sha256 / session 等为示例值。注意示例里「收起动画」那一行末尾保留了原文的两个空格。)
| frontmatter 字段 | 含义 |
|---|---|
format |
记录格式版本,当前为 1;没有 frontmatter 的手写文件按 0 处理 |
id |
记录 UUID |
sha256 |
正文的 sha256,用于跨文件查重 |
created |
归档时间,ISO 8601(UTC) |
domain / topic / subtopic |
目录归属;subtopic 目前恒为 null(保留字段) |
title |
从正文首个一、二级标题或首行提取的标题,最多 40 个码点 |
kind |
statement / supplement / correction / retraction |
related |
关联记录,目前恒为 [](保留字段) |
session / message |
来源会话 id 与消息 id,可为空 |
source |
固定 user |
pluginVersion |
写入该记录的插件版本 |
正文规则:renderRecord 只做 frontmatter + "---\n" + body,不追加结尾换行、不 trim、不转换 CRLF/LF;解析时以 frontmatter 之后第一个独立 --- 行作为分隔,因此正文本身以 --- 开头也能原样还原。
目录示例(配置了 ui 域与 sidebar 主题;文件名为同一规则的实际产出):
<project>/.needs/
├── ui/
│ └── sidebar/
│ ├── 2026-11-11-侧边栏交互-7df0b132.md # statement:原始需求
│ ├── 2026-11-11-另外侧边栏还要支持折叠-d1736793.md # supplement:补充
│ └── 2026-11-11-侧边栏改成-280px-240-太窄了-2cd32ae6.md # correction:更正
└── inbox/
└── unclassified/
└── 2026-11-11-上面那条不算-撤回-82913bbe.md # 没命中关键词的撤回
文件名规则:<YYYY-MM-DD>-<短标题>-<hash8>.md。
YYYY-MM-DD是本机本地日历日期(frontmatter 里的created才是 UTC 时间戳);- 短标题来自标题,保留英文字母、数字、汉字(U+4E00–U+9FFF)与
-;标点、空格、emoji 等转成-,折叠重复分隔符,最长 24 个码点,空则用record; hash8是正文 sha256 的前 8 位,更换日期不会改变它;- 目录段(domain / topic / subtopic)仍限定小写 ASCII,中文不会出现在目录名里。
.needs/.needs.lock 是写入时的临时排他锁,正常结束即删除;异常残留超过 30 秒会被回收,不要手工编辑它。
已知限制(0.2.2-rc.1 / 插件 0.1.0)
以下是按源码能确认、且在当前版本上成立的行为,不是待办清单:
- 只捕获插件加载之后提交的消息。 插件不会回放历史会话,也不会重新触发
session/event。插件加载前的消息无法补录(没有被观察,事件也不会重放);needs_archive只能补本会话插件加载后观察到的最近 8 条人写消息。 - 没有 GUI 面板。 只有工具与
.needs/文件;没有列表页、搜索页、编辑页。 - 依赖
session/event的 live append。 会话恢复、导出导入、重放等路径不会重新触发归档。好在同一正文重复落盘会被去重,因此不会产生重复文件。 - 只读会话跳过。 会话的 sandbox 策略为
read-only时自动归档直接跳过;即使写入请求到达文件系统,也会被沙箱拒绝。没有 sandbox 后端的环境(策略解析为undefined)按普通写入处理。 - 分类是启发式的,不调模型。 域与主题来自
domains关键词匹配,kind 来自句式(另外/补充→ supplement,改成/不是…而是→ correction,撤回/上面那条不算→ retraction)。关键词没命中就落到inbox/unclassified;中文标题且没配defaultTopic时主题同样是unclassified,只保留文件名里的中文标题。 - 项目根必须有明确归属。 判定顺序:显式
projectRoot→ 最近的含.git祖先(文件或目录都算)→ 最近的已存在.needs/祖先 → 当前目录含.dsh/。都不满足时本轮deferred,什么都不写。非 Git 项目要么配projectRoot,要么先在项目里建.needs/。 - 主题沿用只在进程内有效。 更正/补充会沿用本会话「上一条已归档记录」的 domain/topic,这份记忆只存在内存里;插件重载或 DSH 重启后丢失,之后按关键词重新分类,可能落进
inbox/unclassified。跨会话的更正不保证落在同一主题。 - 同一正文在一个项目里只存一份。 之后再说一遍完全相同的话不会产生第二条记录(
duplicate),也就没有新的时间戳。要保留新的说法,正文必须不同。 - 撤回不删除原件。 撤回只新增一条
retraction记录;原记录仍在,回查时按时间顺序同时看到。归档文件的删除只能手工做。 - 没有配置表单。 插件未声明
Configschema、没有前端,配置只能写在 profile patch 的 override 行;非法值回落到默认并告警。 - 子代理/子会话不归档。 判定依据是会话 header:
origin === 'subagent'、存在parentSession,或delegationDepth > 0时,该会话的自动归档静默跳过、上下文提示不注入、needs_archive直接报错;needs_query仍可在子代理里查询。用户自己 fork 的会话没有这些标记,照常归档。该守卫是源码行为;已安装但仍在运行的 desktop 进程要重启 DSH 后才载入(见「插件代码更新之后」)。
验证
本地离线验证与真实 DSH 加载步骤都在 docs/verification.md。两个命令(cwd 位于 needs-archive):
node --test
node scripts/verify.mjs
node --test 当前工作树通过(94 个用例,0 失败,Node v24.13.0,2026-11-11);node scripts/verify.mjs 5/5 通过;真实 DSH 已分项验证:desktop profile 验证了安装、工具注册、真实 needs_query 与条件提示注入;headless 新进程验证了提交 → 落盘、同 session 更正 → 第二份记录、只读会话不落盘。受限之处(headless 无模型凭据;desktop 运行中进程为安装时模块代际,需重启才载入最新源码)写在 docs/verification.md。
卸载
用插件管理器卸载,不要手改 profile:
- GUI:「插件」→「已安装插件」→ 该组合包 → 移除。
- 或让 agent 调
plugin_manager:action: remove_bundle,target: '@local/needs-archive'。
卸载后两个工具消失,插件不再监听消息;已经写入的 .needs/ 不会被删除,它们仍是普通 Markdown 文件,可以直接阅读或备份。需要清理由你自行删除项目里的 .needs/(如果项目已把档案纳入 Git,还要一并处理版本库)。
目录与文件
needs-archive/
├── index.js # Host 入口:事件监听、上下文提示、两个工具
├── cordis.patch.yml # 组合包 patch(插入一行 needs-archive)
├── package.json # @local/needs-archive,声明 dsh.bundle.patch
├── locale/{en,zh}.json # 插件管理器里的标题/描述
├── icon.svg # 插件图标
├── lib/
│ ├── dsh-io.js # ctx.fs 实现存储 I/O 端口 + 按会话策略执行
│ ├── guidance.js # 上下文提示与工具描述文案
│ ├── session-scope.js # 作者会话判定(子代理/子会话不喂档案)
│ └── core/ # 纯 Node 核心(不依赖任何 @deepseek-ai/* 包)
│ ├── config.js # 配置默认值与容错归一化
│ ├── classify.js # 跳过规则、kind 判定、域/主题选择
│ ├── source.js # 只取 source.kind === 'user' 的文本块
│ ├── project.js # 项目根解析、文件名与路径安全
│ ├── format.js # frontmatter 渲染/解析、sha256
│ ├── io.js # 存储 I/O 端口契约(附带 node:fs 实现供单测)
│ ├── store.js # 去重、锁、原子写、查询
│ └── engine.js # 编排层(可注入依赖,单测直接用)
├── scripts/verify.mjs # 端到端离线验证脚本
├── test/*.test.mjs # node:test 单测
└── docs/ # 本目录:配置、使用、验证
No comments yet. Be the first to write one.