# dsh-hindsight-daemon [English](README.md) | **简体中文** 给 [DSH Desktop](https://github.com/deepseek-ai) 用的 **Hindsight 本地记忆 daemon 生命周期插件**。 DSH 启动时自动把 Hindsight daemon 拉起来,DSH 退出时把它连同它依赖的嵌入式 PostgreSQL 一起停掉 —— **不产生任何常驻进程**,也不需要开机自启项、计划任务或后台监视器。 安装完成后在「设置 → Hindsight 记忆」里有一个独立页面,可以看状态、装依赖、启停服务、改配置。 > 支持 **Windows 10/11** 与 **Linux**(x86_64)。Windows 用 PowerShell 脚本安装, > Linux 用 `scripts/install.sh`;两端的进程收尾分别落在 `taskkill` 与 `pg_ctl` / `pkill` 上。 ## Features - **生命周期即语义**:DSH 在 = daemon 在,DSH 关 = 进程全清。不装计划任务、不常驻监视器。 - **三条退出通道**:Cordis disposer、`process.on('exit')`、信号钩子,命中任意一条就能干净收尾, 覆盖 Windows 桌面端与 Linux CLI 两种完全不同的退出保证。 - **独立设置页**:服务状态与启停、环境检测、一键装依赖、当前记忆占用统计、 多仓库(profile)新建/切换/删除、模型服务配置与连通性测试。 - **跨平台**:Windows 与 Linux 同一套逻辑,只在 `lib/core/platform.js` 里分叉。 - **零运行时依赖**:配置读写、设置页渲染、HTTP 路由全部自己实现,不绑定 DSH 内部包版本。 - **自保护**:插件自身初始化失败只会降级(没有设置页),绝不让 DSH 起不来。 ## 为什么需要这个插件 Hindsight 的 coding-agent 集成有三种模式:`cloud`、`self-hosted`、`daemon`。 选 `daemon` 时,记忆存在本机(嵌入式 PostgreSQL + pg0),只有「抽取事实」这一步会调用 你自己的 LLM —— 数据不出机器,也不需要任何云端账号。 但官方插件在 `daemon` 模式下有两个限制,导致它无法自己把 daemon 拉起来: 1. **它只认进程环境变量。** `detectLlm()` 读的是 DSH 进程的 `process.env` (`HINDSIGHT_API_LLM_PROVIDER` 或 `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / …), 读不到 `~/.hindsight/profiles/.env` 里已经配好的值。检测不到就直接放弃, 日志里留下 `daemon mode needs an LLM for fact extraction`。 2. **它只等 12 秒。** 会话启动时 `seedIfCold` 用 `DAEMON_WAIT_SESSION_START_MS = 12e3` 等待, 且**每个进程只尝试一次**。而 daemon 冷启动要 ~30 秒(Python 侧 `import torch` + `import sentence_transformers` 就要 12~13 秒),根本等不到。 这个插件把「拉起 daemon」这件事从官方插件手里接过来:**在 DSH 自己的 Cordis 树里** 做,时机更早、不占用任何额外进程,而且退出时能干净收尾。 ## How it works ``` DSH 启动 └─ Cordis 加载本插件 (lib/index.js → apply) ├─ 注册 /hindsight-daemon/api/* 路由(设置页用) └─ 后台 spawn: hindsight-embed -p daemon start └─ daemon (pythonw) 监听 127.0.0.1:9077 └─ pg0 拉起嵌入式 PostgreSQL (:5432) DSH 退出(三条通道,命中任意一条就够) ├─ ① Cordis disposer:桌面端 main.js 的 before-quit 会 dispose 整棵树(宽限 5 秒) ├─ ② process 'exit' 钩子:进程正常结束 / 被信号终止且运行时有 signal-exit 时 └─ ③ SIGTERM / SIGHUP / SIGINT 钩子:宿主没注册信号处理器时兜底接管 └─ 清理动作(全程同步,信号钩子的约束): ├─ 掐掉 detached 的 hindsight-embed CLI ├─ 按端口找到 daemon PID 并杀掉 └─ pg_ctl stop -m fast -D ← 优雅关库,下次不用回放 WAL ``` 三条通道都只是入口,真正干活的是同一段幂等清理(`shared.closed` 只放行一次)。 实测:`apply()` 返回耗时约 **2 ms**(不阻塞 DSH 启动),清理约 **500–600 ms**, 远在 5 秒宽限之内。冷启动到健康约 **20 秒**(Linux,模型与依赖已就绪时)。 为什么需要三条通道:**不同宿主给的退出保证不一样**。 - Windows 桌面端会先拦下退出再 dispose 树,通道 ① 就够。 - Linux 上的 `dsh` CLI **不注册任何信号处理器**(实测 `process.listenerCount('SIGTERM')` 为 0), 被 `kill` 时进程直接终止,通道 ① 完全没机会跑。 - 但真实进程里往往已经有别的库(如 `signal-exit`)注册了信号监听器, 它会在终止前触发 Node 的 `exit` 事件 —— 所以通道 ② 才是 Linux 上实际生效的那条。 通道 ③ 只在「确实没人管这个信号」时接管,避免抢跑打断宿主的异步收尾。 清理必须显式做三件事,原因都在实测里踩过: - `hindsight-embed daemon start` 这个 CLI 是 **detached** 的,daemon 被杀后它会把 daemon 再拉起来 —— 所以第一步先掐 CLI。 - `hindsight-embed daemon stop` **不会停 PostgreSQL**,而且实测要 4 秒,贴着 5 秒宽限,不安全 —— 所以改按端口 PID 强杀 daemon,再用 `pg_ctl stop -m fast` 单独关库(约 160 ms)。 更细的进程拓扑、通道取舍与调试方法见 [docs/architecture.md](docs/architecture.md)。 ## 环境要求 | 项目 | 要求 | | --- | --- | | 系统 | Windows 10 / 11,或 Linux(x86_64) | | DSH Desktop | 2.0.x(依赖 `apply()` 返回函数即 disposer 的 Cordis 4 语义) | | Node.js | ≥ 22(DSH 自身要求;`scripts/install-deps.mjs` 也用它) | | uv | 由安装流程检查;缺失时会提示如何安装 | | 磁盘 | 约 3 GB(Python 依赖 1.5 GB + 本地模型 1.2 GB + 数据库) | | 模型服务 | 一个 OpenAI 兼容的 LLM 接口(只用于抽取事实) | ## 安装 ### 方式一:一键脚本(推荐) ```bash git clone https://github.com/FYTXY/dsh-hindsight-daemon.git cd dsh-hindsight-daemon ``` **Windows**(PowerShell): ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\install.ps1 ``` `install.ps1` 会追加 insert 块,并接着调用 `scripts/install-deps.mjs` 安装 Python 依赖与 两个本地模型(想跳过就加 `-SkipDeps`,改在设置页里点「一键安装」)。 可用的参数: ```powershell -Profile <名> DSH profile 名,默认 desktop -DshHome <路径> DSH home 目录,默认 $env:DSH_HOME 或 %USERPROFILE%\.dsh -SkipDeps 只挂插件,依赖留到设置页里点「一键安装」 ``` **Linux**: ```bash chmod +x ./scripts/install.sh ./scripts/install.sh # 默认装进 web profile ./scripts/install.sh --profile desktop # 或指定 profile ``` `install.sh` 只做一件事:在 profile 的 `cordis.patch.yml` 里追加 insert 块(写的是本仓库 `lib/index.js` 的绝对路径,所以 `git pull` 升级后立即生效)。依赖同样留给设置页。 两个脚本都是幂等的:重复执行不会写入第二个 insert 块。 装完**重启 DSH**,然后打开 **设置 → Hindsight 记忆**,填入你自己的模型服务。 ### 方式二:手动挂载 把插件目录放到任意位置,然后在 `/profiles//cordis.patch.yml` 末尾追加: ```yaml - insert: - id: hindsight-daemon name: '/absolute/path/to/dsh-hindsight-daemon/lib/index.js' ``` 注意 `name` 用**正斜杠**(Windows 上也写 `C:/path/to/...`),指向 `lib/index.js`。之后在设置页点「一键安装」补齐依赖。 ### 方式三:npm ```powershell dsh plugin --profile desktop add dsh-hindsight-daemon ``` `package.json` 里带了 `dsh.bundle.patch`,安装后由包自带的 `cordis.patch.yml` 自动挂载。 > 三种方式**只能选一种**。重复挂载会让 `/hindsight-daemon/api` 前缀路由注册两次, > DSH 启动时报 `webserver: duplicate prefix route`。 ## 设置页 **设置 → Hindsight 记忆**(导航里排在靠后的位置)包含六块: > **每个按钮都有明确反馈**:点击后按钮文字立刻变成 `保存中…` / `启动中…` / `切换中…` 这类进行中状态并禁用,避免重复点击;操作结果紧跟在底部 **保存配置 / 重新探测** 两个按钮下方 —— 成功是绿色回执(如「配置已保存」),5 秒后自动消失;失败是红色错误,一直保留到下一次操作成功。 ### 服务状态 显示阶段(`已停止` / `启动中` / `运行中` / `安装中` / `异常`)、地址与已运行时长, 并提供 **启动** / **停止** 按钮。每 2 秒刷新一次。 ### 环境检测 分别显示 `uv`、`hindsight-embed`、`PostgreSQL` 与数据库目录的就绪状态。 任何一项缺失都可以用一键安装补齐。探测优先级:**设置里填的路径 > 环境变量 > PATH > 常见安装位置**。 ### 一键安装 幂等,可以重复点。依次执行五个阶段: | 阶段 | 做什么 | | --- | --- | | `check` | 定位 `uv` | | `packages` | `uv tool install hindsight-embed --with hindsight-api --force` | | `profile` | 创建 / 对齐 `hindsight-embed` 的 profile(写环境变量) | | `models` | 用 venv 里的 Python 通过 `huggingface_hub` 下载 embedding 与 reranker | | `verify` | `import torch, sentence_transformers, hindsight_api` 自检 | `--with hindsight-api` 这一步很关键:把 API 装进**同一个 venv**,daemon 启动时才能直接找到 `hindsight-api.exe`;否则它会退回 `uvx hindsight-api@`,在临时环境里重新下载一遍 600 MB。 ### 当前记忆 **只读**,回答「记忆到底存在哪」:数据库目录、占用空间,以及每个记忆库的节点 / 关联 / 文档数。 一个代码仓库对应一个记忆库,按仓库名自动命名,不同项目之间互不可见。这些信息不能在这里修改 —— 位置由下面的「高级」决定,想换地方见「文件都放在哪」。 ### 记忆仓库(Profile) 列出本机所有 profile,标明哪个正在使用,并可以直接新建、切换、删除。 一个 profile 就是一间独立的仓库:自己的数据库、自己的 env(含网关密钥)、自己的日志。 新建只写 env 文件,**不建数据库、不启进程** —— 等它被启用、daemon 第一次启动时才建出来, 所以新建本身不占磁盘。启用某个仓库会停掉当前 daemon 再按新仓库拉起(约 30 秒), 原来的仓库原封不动,随时能切回来。 名字只能包含字母、数字、点、下划线和连字符,以字母或数字开头,最多 32 个字符。 只允许切到已经存在的 profile —— 手输一个拼错的名字会静默建出空库,比直接报错更难察觉。 **删除**:每个仓库右侧(「启用」之后)有删除按钮,点一下会先弹确认框,确认后才会真的删。 删除是**不可恢复**的,会连数据库目录、env 文件(含网关密钥)、日志和登记表条目一起清掉; 正在使用的那一个按钮是灰的 —— 想删它,先把别的仓库切成当前使用。 删除不会碰其他仓库,也不会停 daemon。 ### 模型服务 | 字段 | 说明 | | --- | --- | | Provider | 目前走 OpenAI 兼容协议,选 `openai` 即可 | | 模型名 | 抽取事实用的模型,例如 `gpt-4o-mini` | | Base URL | 兼容网关地址,例如 `https://api.example.com/v1` | | API Key | 落盘明文保存;设置页只回显圆点(`••••`)表示已设置,留空保存表示不修改 | 填完 `Base URL` / `模型名` / `API Key` 之后,点 **测试连通性** 会立刻用当前草稿发一次最小请求, 直接告诉你通不通、失败原因、耗时和接口实际回显的模型名 —— 不必等到写记忆时才发现配错。 密钥留空时自动回退到已保存的那一份,所以「刚填完还没保存就想测」也能用。 测试只读,不写配置、不影响正在运行的 daemon。 ### 高级 | 字段 | 说明 | | --- | --- | | Profile 名 | **只读**,显示当前使用的仓库。要换仓库请用上面的「记忆仓库(Profile)」列表 —— 直接改名等于指向一个全新的空库:旧记忆不会被删除,但也不会跟过来 | | 记忆库命名模板 | 留空 = 每个代码仓库自动一个库,模板为 `coding-agent::{gitProject}`。填了会当成字面模板 —— 填 `work` 就是所有项目共用 `work` 这一个库 | | 本地端口 | daemon 监听端口,默认 `9077`,改动需重启 DSH | | HF 镜像 | 默认 `https://hf-mirror.com`,只影响首次下载模型,装好后不再起作用 | | PyPI 镜像 | 默认清华源,只影响安装依赖,装好后不再起作用 | 前两项决定记忆存在哪,后三项只影响下载与监听 —— 所以界面上把危险项排在前并单独加了警示。 #### 记忆库到底叫什么 上游插件按模板算库名,默认模板是 `coding-agent::{gitProject}`,`{gitProject}` 取 **git 仓库根目录名**: | 当前工作区 | 推导出的库名 | | --- | --- | | `~/projects/my-app`(git 仓库) | `coding-agent::my-app` | | `~/scratch/notes`(不是 git 仓库) | `coding-agent::notes`(取目录名) | 可用占位符:`{gitProject}`(git 仓库根目录名,worktree 会追到主仓库)、`{project}`(当前目录名,不追仓库根)、`{harness}`、`{channel}`、`{user}`。 所以换一个工作区目录,只要它是另一个 git 仓库,就会自动写进另一个库 —— 互不串味,不需要任何配置。库里没东西就是还没在这个仓库里聊过天。 页面底部合并展示插件日志与 daemon 日志(各取尾部),排错时先看这里。 ## 文件都放在哪 | 内容 | 路径 | | --- | --- | | 插件配置 | `/hindsight-daemon/config.json` | | profile 环境变量 | `~/.hindsight/profiles/.env` | | hindsight 全局配置 | `~/.hindsight/coding-agent.json` | | daemon 日志 | `~/.hindsight/profiles/.log` | | 数据库 | `~/.pg0/instances/hindsight-embed-/data` | | Python 运行时 | `/hindsight-embed`(默认 `~/.local/share/uv/tools`,Windows 为 `%LOCALAPPDATA%\uv\tools`) | | 本地模型 | `$HF_HOME`,未设置则在 `~/.cache/huggingface` | 上表用 `~` 表示用户主目录:Windows 上是 `%USERPROFILE%`(`C:\Users\<你>`),Linux 上是 `$HOME`。 ## 卸载 ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\uninstall.ps1 ``` ```bash chmod +x ./scripts/uninstall.sh ./scripts/uninstall.sh # 默认从 web profile 移除 ./scripts/uninstall.sh --profile desktop # 或指定 profile ``` 只从 `cordis.patch.yml` 里移除 insert 块(同样会备份)。重启 DSH 后插件不再加载。 Python 依赖、模型与数据库都保留,方便之后重新挂上;要彻底清理请按上面表格手动删除对应目录。 ## 常见问题 **首次启动很慢?** 正常。首次要下 1.5 GB 依赖 + 1.2 GB 模型,之后冷启动约 30 秒。装好依赖和模型后, 每次启动就只是 Python 导入的开销。 **日志里出现 `daemon mode needs an LLM for fact extraction`?** 这是官方 `@vectorize-io/hindsight-coding-agents` 插件在报怨它自己检测不到 LLM —— 它只看 DSH 进程的环境变量。本插件不依赖那条路径,看到这条可以忽略,只要 daemon 状态是「运行中」即可。 **依赖安装到一半失败?** 装依赖对网络比较敏感。默认已经走清华 PyPI 镜像和 hf-mirror;如果在公司网络里,把 「PyPI 镜像」和「HF 镜像」改成内网地址,或留空走官方源。装到一半失败可以直接重试, 已下载的轮子有缓存。 **端口 9077 被占用?** 改「本地端口」并重启 DSH。插件启动前会先探测 `/health`,如果端口上已经有一个健康的 daemon(可能是你自己手动起的),会直接复用而不是再拉一个。 **改了端口 / Profile 名没生效?** 这两项要**重启 DSH** 才生效(要重建 profile 与全局配置)。 **DSH 非正常退出(崩溃 / 强杀)后 daemon 还在?** 说明清理通道都没跑到(例如被 `SIGKILL`,任何语言都拦不住)。手动收一次即可: ```powershell # Windows taskkill /F /IM hindsight-embed.exe /T Get-NetTCPConnection -LocalPort 9077 -State Listen | ForEach-Object { taskkill /F /PID $_.OwningProcess /T } & "$env:USERPROFILE\.pg0\installation\*\bin\pg_ctl.exe" stop -m fast -D "$env:USERPROFILE\.pg0\instances\hindsight-embed-\data" ``` ```bash # Linux pkill -f hindsight-embed pgrep -f "hindsight" | xargs -r kill -9 # 或按端口: ss -ltnp | grep 9077 "$HOME/.pg0/installation/"*/bin/pg_ctl stop -m fast -D "$HOME/.pg0/instances/hindsight-embed-/data" ``` 下次启动 DSH 时如果数据库没干净关闭,PostgreSQL 会自己回放 WAL 恢复,不会丢数据。 **想看清退出路径到底哪条通道生效?** 设置环境变量 `DSH_HINDSIGHT_DEBUG=1` 再启动 DSH,插件会把钩子注册与清理决策写进 `~/.dsh/hindsight-daemon/debug.log`: ``` 退出钩子已注册 pid=58065 SIGTERM=2 SIGINT=2 exit=3 收到 SIGTERM,listeners=2 pid=58065 cleanup(exit) enabled=true stopOnExit=true busy=null ``` `listeners` 数量很关键:大于 1 说明宿主/依赖已经有人处理这个信号,信号钩子会让位, 真正的清理走 `exit` 通道;等于 1 时才是信号钩子自己接管。 **升级插件后行为没变?** 多半是加载了**另一份旧副本**。DSH 的 profile 有两条挂载路子 —— `dsh.profile.bundles`(按包名从 `node_modules` 解析)和 profile 自己的 `cordis.patch.yml` (可以写绝对路径)。如果 `cordis.patch.yml` 里留着指向源码目录的绝对路径,它会覆盖 bundle 那条,于是 `dsh plugin add` 装的新版本根本没被加载。检查: ```bash grep -rn "hindsight" ~/.dsh/profiles//cordis.patch.yml ``` 推荐只留一种:把 `name` 写成包名 `dsh-hindsight-daemon`,让版本跟着 `dsh plugin add` 走。 ## 开发 零运行时依赖,不需要 `npm install`: ```bash npm test # node --test,全套一条命令 npm run check # 对所有入口做 node --check npm run pack:dry # 检查打包白名单 ``` 测试全部不依赖 DSH 与真实 daemon:它们把 `HOME` / `USERPROFILE` / `DSH_HOME` 都指到临时 目录,再用最小化的 hooks 实现把浏览器半边跑起来。改动约定见 [CONTRIBUTING.md](CONTRIBUTING.md)。 单独跑某个文件: ```bash node --test tests/units.test.mjs node --test tests/packaging.test.mjs ``` 在 DSH 之外调试安装流程: ```bash node scripts/install-deps.mjs ``` ## 目录结构 ``` lib/ index.js Cordis 入口:什么时候挂接口、什么时候拉起、什么时候收摊 lifecycle.js 生命周期:探测 → 启动 → 停止 → 安装 → 保存配置 api.js /hindsight-daemon/api/* 路由 + Host 白名单 config.js 配置读写(原子写、密钥脱敏、扁平化转换) client.js 浏览器半边:设置页 core/ paths.js 磁盘探测:uv / hindsight-embed / pg_ctl / 数据目录 daemon.js 健康探测、按端口找 PID、spawn、三段式关闭 profile.js profile 创建与 env 行级补丁、全局配置写入 profiles.js profile 列举 / 新建 / 删除(含目录大小缓存) installer.js 五阶段安装(流式读取子进程输出) platform.js 平台分叉:可执行名、venv 目录、进程控制 llmTest.js 连通性测试用的最小请求 memoryInfo.js 数据库目录占用与记忆库统计 status.js 状态与日志环形缓冲 scripts/ install.ps1 / install.sh 一键安装(写 cordis.patch.yml) uninstall.ps1 / uninstall.sh 从 cordis.patch.yml 移除 install-deps.mjs 在 DSH 之外执行依赖与模型安装 tests/ node:test 用例(不需要 DSH) docs/architecture.md 进程拓扑、三条退出通道、取舍理由 ``` 设计上刻意**零运行时依赖**:配置自己读写,设置页自己渲染,不引 `schemastery` / `dsh-shared` / `dsh-api-gateway`。原因是这些包在公开 npm 上的版本(`0.0.1-rc.1` 系列) 与本机 DSH 内置版本(`0.2.0-rc.2`)不一致,绑定它们会让插件在 DSH 升级后随时崩掉。 ## 相关文档 - [docs/architecture.md](docs/architecture.md) — 进程拓扑、三条退出通道与设计取舍 - [CHANGELOG.md](CHANGELOG.md) — 版本变更 - [CONTRIBUTING.md](CONTRIBUTING.md) — 开发约定与测试规范 - [SECURITY.md](SECURITY.md) — 安全边界与漏洞上报 ## 许可 [MIT](LICENSE)