🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

自动交接 S3K926/dsh-auto-handoff

DeepSeek Harnessv0.2.0
S3K92615 天前有提交已提供中文文档扫描:含敏感能力

DSH 插件:上下文到阈值时生成交接包并切到新会话。已弃用 2026-10-17:换会话整条退役——续航交给 DSH…

33.6/100
综合分
5
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/17 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

自动兼容检查未通过:仓库 package.json 标记 private,未发布到 npm,需从源码安装。以下命令仍可尝试,但可能失败。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add S3K926/dsh-auto-handoff
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:需留意
是什么
dsh 原生插件 · chat
装得上吗
实装验证未通过(unknown),装前请到仓库确认最近更新与 issue
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 15 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
⚠︎ 实装验证未通过(unknown · 2026/10/26) ——可能是验证环境差异,装前建议到 GitHub 仓库确认最近更新与 issue。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装

仓库 package.json 标记 private,未发布到 npm,需从源码安装

展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-auto-handoff @ 0.2.0
✓Node 引擎未声明 engines.node
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/28 20:42:58

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件发起外部网络请求
查看扫描证据(共 120 处,展示前 92 条)
  • 发起网络请求
    index.js:340
    const response = await fetch(url, {
  • 写入本地文件
    index.js:357
    mkdirSync(dir, { recursive: true });
  • 写入本地文件
    index.js:358
    writeFileSync(join(dir, 'pending.json'), `${JSON.stringify({
  • 读取本地文件
    index.js:545
    const data = JSON.parse(readFileSync(join(stateDir, BOOT_TASK_FILE), 'utf8'));
  • 写入本地文件
    index.js:556
    mkdirSync(stateDir, { recursive: true });
  • 删除本地文件
    index.js:559
    try { rmSync(join(stateDir, BOOT_TASK_FILE), { force: true }); } catch { }
  • 写入本地文件
    index.js:562
    writeFileSync(join(stateDir, BOOT_TASK_FILE),
  • 读取本地文件
    index.js:920
    if (!existsSync(file)) return false;
  • 删除本地文件
    index.js:922
    rmSync(file, { force: true });
  • 发起网络请求
    client.js:102
    const response = await fetch(ROUTE_PENDING, { headers: { accept: 'application/json' } });
  • 发起网络请求
    client.js:167
    await fetch(ROUTE_ACK, {
  • 发起网络请求
    client.js:330
    await fetch(ROUTE_DRAFT_REPORT, {
  • 发起网络请求
    client.js:347
    const response = await fetch(ROUTE_PENDING, { headers: { accept: 'application/json' } });
  • 发起网络请求
    client.js:354
    await fetch(ROUTE_FIRE_ACK, { method: 'POST' }).catch(() => {});
  • 读取本地文件
    handoff-core.js:88
    if (typeof dir !== 'string' || dir.trim() === '' || !existsSync(dir)) return false;
  • 读取本地文件
    handoff-core.js:90
    if (!statSync(dir).isDirectory()) return false;
  • 读取本地文件
    handoff-core.js:94
    return ROOT_MARKERS.every((marker) => existsSync(resolve(join(dir, marker))));
  • 读取本地文件
    handoff-core.js:146
    return readFileSync(file, 'utf8').replace(/^ FEFF/, '');
  • 读取本地文件
    handoff-core.js:152
    const read = (file) => (existsSync(file) ? readText(file) : null);
  • 写入本地文件
    handoff-core.js:339
    mkdirSync(dirname(file), { recursive: true });
  • 写入本地文件
    handoff-core.js:342
    writeFileSync(tmp, toCRLF(text), 'utf8');
  • 写入本地文件
    handoff-core.js:343
    renameSync(tmp, file);
  • 删除本地文件
    handoff-core.js:345
    try { rmSync(tmp, { force: true }); } catch { }
  • 读取本地文件
    handoff-core.js:355
    if (!existsSync(file)) return null;
  • 写入本地文件
    handoff-core.js:358
    mkdirSync(dirname(target), { recursive: true });
  • 写入本地文件
    handoff-core.js:359
    writeFileSync(target, readFileSync(file));
  • 写入本地文件
    handoff-state.js:98
    mkdirSync(tmpdir(), { recursive: true });
  • 写入本地文件
    handoff-state.js:99
    writeFileSync(LOG_FILE, `${text}\n`, { encoding: 'utf8', flag: 'a' });
  • 读取本地文件
    handoff-plan.js:38
    const data = JSON.parse(readFileSync(join(stateDir, PRINT_FILE), 'utf8'));
  • 写入本地文件
    handoff-plan.js:49
    mkdirSync(stateDir, { recursive: true });
  • 写入本地文件
    handoff-plan.js:51
    writeFileSync(join(stateDir, PRINT_FILE), `${JSON.stringify({ at: new Date().toISOString(), prints }, null, 2)}\n`, 'utf8');
  • 写入本地文件
    switch-store.js:55
    mkdirSync(dir === undefined || dir === null ? pendingDir() : dir, { recursive: true });
  • 写入本地文件
    switch-store.js:56
    appendFileSync(logPath(dir), `[${new Date().toISOString()}] ${line}\n`, 'utf8');
  • 写入本地文件
    switch-store.js:79
    writeFileSync(tmp, text, 'utf8');
  • 写入本地文件
    switch-store.js:81
    renameSync(tmp, file);
  • 删除本地文件
    switch-store.js:84
    rmSync(tmp, { force: true });
  • 读取本地文件
    switch-store.js:139
    const raw = readFileSync(budgetFile(dir), 'utf8');
  • 读取本地文件
    switch-store.js:233
    raw = readFileSync(file, 'utf8');
  • 写入本地文件
    switch-store.js:266
    mkdirSync(dir === undefined || dir === null ? pendingDir() : dir, { recursive: true });
  • 写入本地文件
    switch-session-selftest.js:158
    writeFileSync(file, '{ 这不是 JSON', 'utf8');
  • 读取本地文件
    switch-session-selftest.js:160
    const logText1 = readFileSync(logFile, 'utf8');
  • 读取本地文件
    switch-session-selftest.js:167
    const logText2 = readFileSync(logFile, 'utf8');
  • 删除本地文件
    switch-session-selftest.js:221
    rmSync(budgetFile(dir), { force: true });
  • 删除本地文件
    switch-session-selftest.js:242
    rmSync(budgetFile(dir), { force: true });
  • 写入本地文件
    switch-session-selftest.js:274
    writeFileSync(budgetFile(dir), '{ 这不是 JSON', 'utf8');
  • 写入本地文件
    switch-session-selftest.js:277
    writeFileSync(budgetFile(dir), JSON.stringify({ records: [{ at: Date.now() - 10 * 24 * 60 * 60 * 1000, sourceSession: 'session-old', seq: 1, handoffCh
  • 删除本地文件
    switch-session-selftest.js:285
    rmSync(budgetFile(dir), { force: true });
  • 删除本地文件
    switch-session-selftest.js:289
    rmSync(budgetFile(dir), { force: true });
  • 删除本地文件
    switch-session-selftest.js:315
    rmSync(budgetFile(dir), { force: true });
  • 删除本地文件
    switch-session-selftest.js:327
    rmSync(budgetFile(dir), { force: true });
  • 写入本地文件
    test-archive-off.mjs:46
    mkdirSync(join(root, '生长'), { recursive: true });
  • 写入本地文件
    test-archive-off.mjs:47
    writeFileSync(join(root, '日记.md'), DIARY, 'utf8');
  • 写入本地文件
    test-archive-off.mjs:48
    writeFileSync(join(root, '生长', '状态.md'), STATE, 'utf8');
  • 写入本地文件
    test-archive-off.mjs:49
    writeFileSync(join(root, '生长', '生长记录.md'), GROWTH, 'utf8');
  • 读取本地文件
    test-archive-off.mjs:53
    diary: readFileSync(join(root, '日记.md'), 'utf8'),
  • 读取本地文件
    test-archive-off.mjs:54
    state: readFileSync(join(root, '生长', '状态.md'), 'utf8'),
  • 读取本地文件
    test-archive-off.mjs:55
    growth: readFileSync(join(root, '生长', '生长记录.md'), 'utf8'),
  • 删除本地文件递归
    test-archive-off.mjs:97
    rmSync(root1, { recursive: true, force: true });
  • 删除本地文件递归
    test-archive-off.mjs:98
    rmSync(root2, { recursive: true, force: true });
  • 读取本地文件
    test-boot-tasks.mjs:34
    ok('文件真的落地了', existsSync(file));
  • 读取本地文件
    test-boot-tasks.mjs:36
    ok('传空数组会清掉文件', writeBootTasks(dir, []) === true && !existsSync(file));
  • 读取本地文件
    test-boot-tasks.mjs:51
    ok('消费后队列文件也被清掉', !existsSync(file));
  • 读取本地文件
    test-boot-tasks.mjs:64
    const raw = readFileSync(file, 'utf8');
  • 删除本地文件递归
    test-boot-tasks.mjs:66
    rmSync(dir, { recursive: true, force: true });
  • 写入本地文件
    test-fire-now.mjs:49
    writeFileSync(file, '');
  • 读取本地文件
    test-fire-now.mjs:52
    ok('文件被删掉(不会被下一轮重复举手)', existsSync(file) === false);
  • 写入本地文件
    test-fire-now.mjs:56
    writeFileSync(file, '');
  • 写入本地文件
    test-fire-now.mjs:58
    writeFileSync(file, '');
  • 写入本地文件
    test-fire-now.mjs:61
    writeFileSync(file, '');
  • 读取本地文件
    test-fire-now.mjs:64
    ok('被挡住时文件照样清掉(避免卡成死循环)', existsSync(file) === false);
  • 删除本地文件递归
    test-fire-now.mjs:73
    rmSync(dir, { recursive: true, force: true });
  • 读取本地文件
    test-handoff-sandbox.mjs:27
    const bytes = (file) => (existsSync(file) ? statSync(file).size : -1);
  • 读取本地文件
    test-handoff-sandbox.mjs:27
    const bytes = (file) => (existsSync(file) ? statSync(file).size : -1);
  • 读取本地文件
    test-handoff-sandbox.mjs:28
    const hashOf = (file) => (existsSync(file) ? sha(readFileSync(file, 'utf8')) : '(缺)');
  • 读取本地文件
    test-handoff-sandbox.mjs:28
    const hashOf = (file) => (existsSync(file) ? sha(readFileSync(file, 'utf8')) : '(缺)');
  • 写入本地文件
    test-handoff-sandbox.mjs:32
    mkdirSync(join(root, '生长'), { recursive: true });
  • 写入本地文件
    test-handoff-sandbox.mjs:33
    writeFileSync(join(root, '身份.md'), crlf('# 身份(沙盘假件)\n\n这份档案只活在临时目录里,跑完就删。\n'), 'utf8');
  • 写入本地文件
    test-handoff-sandbox.mjs:34
    writeFileSync(join(root, '日记.md'), crlf(DIARY_FIXTURE), 'utf8');
  • 写入本地文件
    test-handoff-sandbox.mjs:35
    writeFileSync(join(root, '生长', '状态.md'), crlf(STATE_FIXTURE), 'utf8');
  • 写入本地文件
    test-handoff-sandbox.mjs:36
    writeFileSync(join(root, '生长', '生长记录.md'), crlf(GROWTH_FIXTURE), 'utf8');
  • 写入本地文件
    test-handoff-sandbox.mjs:200
    mkdirSync(outside, { recursive: true });
  • 读取本地文件
    test-handoff-sandbox.mjs:210
    const before = Object.fromEntries(Object.entries(paths).filter(([k]) => k !== 'backups').map(([k, f]) => [k, readFileSync(f, 'utf8')]));
  • 读取本地文件
    test-handoff-sandbox.mjs:230
    const afterDry = Object.fromEntries(Object.keys(before).map((k) => [k, sha(readFileSync(paths[k], 'utf8'))]));
  • 请求外部主机sandbox
    test-handoff-sandbox.mjs:390
    const healthBody = host ? await (await host.routes.get('/api/handoff/health').fetch(new Request('http://sandbox/api/handoff/health'))).json() : {};
  • 发起网络请求
    test-handoff-sandbox.mjs:390
    const healthBody = host ? await (await host.routes.get('/api/handoff/health').fetch(new Request('http://sandbox/api/handoff/health'))).json() : {};
  • 请求外部主机sandbox
    test-handoff-sandbox.mjs:393
    const pendingBody = host ? await (await host.routes.get('/api/handoff/pending').fetch(new Request('http://sandbox/api/handoff/pending'))).json() : {};
  • 发起网络请求
    test-handoff-sandbox.mjs:393
    const pendingBody = host ? await (await host.routes.get('/api/handoff/pending').fetch(new Request('http://sandbox/api/handoff/pending'))).json() : {};
  • 请求外部主机sandbox
    test-handoff-sandbox.mjs:395
    const ackNoPending = host ? await host.routes.get('/api/handoff/ack').fetch(new Request('http://sandbox/api/handoff/ack', { method: 'POST', body: '{}'
  • 发起网络请求
    test-handoff-sandbox.mjs:395
    const ackNoPending = host ? await host.routes.get('/api/handoff/ack').fetch(new Request('http://sandbox/api/handoff/ack', { method: 'POST', body: '{}'
  • 删除本地文件递归
    test-handoff-sandbox.mjs:476
    rmSync(base, { recursive: true, force: true });
  • 读取本地文件
    test-summary-options.mjs:9
    const before = (() => { try { return statSync(LOG).size; } catch { return 0; } })();
  • 写入本地文件
    test-summary-options.mjs:51
    try { if (before > 0) truncateSync(LOG, before); } catch { }
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
涉及外部主机:sandbox

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 上下文达阈值自动写档案
  • 生成交接包
  • 多智能体协作

DeepSeek Harness 插件:多智能体协作(主控/子代理/技能)+ 会话管理 + 会话切换(含 dsh-session-switch 命令)

README

由 DeepSeek 最新模型翻译生成
⚠ 2026-09-28 合并记录:本插件现在也管「会话那一摊」 起因是一次「合插件」的需求 ——dsh-session-switch(建新会话、发交接包、写 pending、供界面切过去)整个搬进了本包,作为 switch-session.js 与 switch-store.js 两个模块: * 内部握手改成直调(runSwitchDirect()),不再是 POST http://127.0.0.1:3080/api/session/switch 那条网络往返; * 那四条 HTTP 路由照旧注册(/api/session/switch|pending|ack|health),自检、手工 curl、老调用方都还认; * 状态目录不变:~/.dsh/session-switch/(DSH_SESSION_SWITCH_DIR 可覆盖); * 会话那一摊要的七个宿主服务改成运行时注入:缺谁只让这一摊缺席,档案那一摊照常(合并前是「整包卡 pending」); * 自检多了一个口:DSH_SESSION_SWITCH_DIR=/tmp/ss node index.js --switch-selftest(原样 19 项)。 撤下前的完整副本在 plugin-src/归档-合并前-20260927-232845/。
展开完整文档(README 剩余 1 节)
⚠ 2026-09-28 合并记录:本插件现在也管「会话那一摊」

起因是一次「合插件」的需求 ——dsh-session-switch(建新会话、发交接包、写 pending、供界面切过去)整个搬进了本包,作为 switch-session.js 与 switch-store.js 两个模块:

* 内部握手改成直调(runSwitchDirect()),不再是 POST http://127.0.0.1:3080/api/session/switch 那条网络往返;
* 那四条 HTTP 路由照旧注册(/api/session/switch|pending|ack|health),自检、手工 curl、老调用方都还认;
* 状态目录不变:~/.dsh/session-switch/(DSH_SESSION_SWITCH_DIR 可覆盖);
* 会话那一摊要的七个宿主服务改成运行时注入:缺谁只让这一摊缺席,档案那一摊照常(合并前是「整包卡 pending」);
* 自检多了一个口:DSH_SESSION_SWITCH_DIR=/tmp/ss node index.js --switch-selftest(原样 19 项)。

撤下前的完整副本在 plugin-src/归档-合并前-20260927-232845/。

dsh-auto-handoff

DSH(DeepSeek Harness)插件:上下文占用到阈值时自动交接。

到阈值那一刻,它做这两件事:

1. 拼一份限长的交接包(默认 1200 字,只有它进新会话),请会话那一摊(2026-09-28 已并入本包的 switch-session.js)开新会话、把交接包当第一条消息送进去;
2. 记一笔幂等指纹到自己状态目录下的 handoff-fingerprints.json(跨重启有效,封顶 50 条)。

⚠️ 2026-10-16 起:默认 不写档案

以前它每交接一次就往日记里塞一条「上下文到阈值,插件自动做了一次交接」的流水账(外加状态一节、生长记录一行),一天能塞好几条、删了又长;接手那边还会被提示「再补一条」。现在默认 archiveOnHandoff: false:

* 交接只交接 —— 一个档案字节都不碰;
* 幂等指纹不再寄生在日记条目上,改落 ~/.dsh/session-switch/handoff-fingerprints.json;
* 日记/状态/生长记录只在人点「日记」或跑「收尾」时才更新;
* 想回到旧行为(写日记 + 状态一节 + 生长记录一行)就把 archiveOnHandoff 设成 true。

到阈值时的默认动作也变了:autoTrigger: true 时它不自己切会话,而是给 AI 塞一条【自动收尾提示】(先做收尾,做完写 fire-now 举手换会话)—— 免得在人正看着屏幕的时候把对话切走。

本插件不自己建会话。 建会话、切界面、归档原会话都是会话那一摊(已并入本包的 switch-session.js)的活;两者只靠一个 HTTP 调用握手,叫不动就走降级(把完整待接写到约定位置,由浏览器那半边自己切)。

它管什么 / 不管什么

| | 本插件 | dsh-session-switch |
| 管什么 | 交接包 + 待接记录(archiveOnHandoff: true 时才写日记 / 状态 / 生长记录) | 会话:建、切、归档 |
| 触发 | 上下文到阈值自动;或手动打演练关键词 | 收到 /api/session/switch 才动 |

记忆的看 / 编 / 导入 / 导出是另一个插件(记忆面板)的事,本插件不碰。

安装

1. 把这个目录放进你的 DSH 插件源目录(本机示例:~/.dsh/plugin-src/dsh-auto-handoff)。
2. 把 cordis.patch.example.yml 里那一条按你的情况改好,插进 profile 的 cordis.patch.yml;或者用 Plugin Manager 的 install_bundle 把它当 bundle 装进 profile。
3. 重启宿主。实测:装 bundle、改 bundle 内代码、改 config 都认重启(HMR 不可靠)。

唯一的必填项是档案根:留空则依次尝试

config.root → 环境变量 DSH_MEMORY_ROOT → ~/.dsh/memory → 从当前目录逐级向上找

认根靠特征文件:身份.md + 日记.md + 生长\状态.md 三者齐备才算一份档案; 认不到就不动作(绝不拿一个猜出来的根去写真档案)。

配置

| 键 | 默认 | 说明 |
| root | '' | 档案根;留空按上面的顺序找 |
| triggerRatio | 0.65 | 占用到这个比例触发交接 |
| minTurns | 3 | 会话太短不触发 |
| cooldownMs | 180000 | 冷却 3 分钟;过了冷却还能再交一次(不是「一会话一次」) |
| handoffMaxChars | 1200 | 交接包上限 |
| dryRun | true | 首启只算不写:把「本来会写什么」记日志,一个字节都不落盘 |
| archiveOnHandoff | false | 交接要不要写档案:false = 只出交接包(默认、推荐);true = 还写日记一条 + 状态一节 + 生长记录一行 |
| autoTrigger | false | true = 到阈值不自己切会话,而是给 AI 塞一条「该收尾了」的提示(见上文 2026-10-16 说明) |
| device | 【PC】 | 日记标题与状态节里的设备标签(archiveOnHandoff: true 时才用得上) |
| drillKeyword | #交接演练 | 手动演练关键词(只在用户消息里整句匹配) |
| gatewayBase | http://127.0.0.1:3080 | dsh-session-switch 的地址 |
| legacyDevice / legacyBoundary | '' | 旧条目设备归属判据;没配就完全不推断 |
占用率的取数口径:优先会话投影 contextPressure(跟页面上那个百分比同源),拿不到再退回 ctx.tokenMeter.measure() ÷ request/context().contextWindow; 两个都拿不到就返回「不动作」 —— 不许瞎猜分母。

手动演练

在对话里整句打 #交接演练:

- 只写演练副本根(sandboxRoot),真档案一个字节不碰;
- 没配 sandboxRoot 就只做一次干跑,日志里能看到「本来会写什么」;
- sandboxRoot 落在真档案根里面时硬拒(那样「演练」就是真写)。

Host 侧路由

前缀 /api/handoff/,五条各自注册一次(requestBody 只能是 buffered,写别的会静默失败)。

| 路由 | 方法 | 用途 |
| pending | GET | 给客户端轮询的待接(完整交接包 + sourceSession + newSessionId) |
| take | GET/POST | 与 pending 同义(取一份待接) |
| ack | POST | 客户端切完回报,标记「已处理」 |
| health | GET | 一眼看配置与管道:阈值 / 冷却 / 上限 / 档案根 / gateway |
| status | GET | 最近 20 行诊断日志 + 上次判定 |
| draft-report | POST | 客户端上报输入框草稿状态(「别打断正在打字的人」用) |
| fire | POST | AI 举手请求换会话(只认「收尾」口令 + 每小时 ≤2/每天 ≤8) |
| fire-ack | POST | 客户端替用户打完「换会话」后回来清标志 |

客户端半边(client.js)只做「轮询 + 调导航/归档」:不注册 slot、不写 DOM,出错只 console,绝不抛 —— 客户端入口抛错的代价是「整个页面打不开」。

「收尾后自动换会话」怎么举手(文件信令)

POST /api/handoff/fire 那条路要过宿主鉴权(实测在本机回 401,本机 curl 也一样),所以 AI 举手改用写一个文件:

: > ~/.dsh/session-switch/fire-now # 路径 = <状态目录>/fire-now,内容随便

- 插件每 1 秒看一次这个文件 → 删掉 → 设 fireRequested;
- 客户端本来就在轮询 /pending,看到 fire 就替用户打一条「换会话」,于是走既有那条触发路径(建新会话、送交接包、切界面),打完回 fire-ack 清标志;
- 闸没松:仍然受 每小时 ≤2/每天 ≤8 限制;fire 那条 HTTP 路由还额外要求「最近一条用户消息就是『收尾』」。
- 典型用法:AI 跑完收尾(日记 → 备份 → 体检)写一下这个文件,人就自动被带到新会话。

写档案的护栏(只在 archiveOnHandoff: true 时用得上)

默认不写档案(见上文),所以这一节只在把 archiveOnHandoff 打开时生效。写盘一律四条:

1. 改前备份(整份复制到 <根>\归档-旧版本与记录\写入备份\,.bak-YYYYMMDD-HHMMSS);
2. 只改定位处(序号 +1、total_count +1、last_updated、unfinished,别的一字不动);
3. 写完复核(读回来逐字比对 + 「旧内容只能是新内容的前缀/只差一处」的形状复核);
4. 外部改动拒写(指纹比对:我们读过之后被改过 → 拒写)。

外加:

- 幂等:同一份交接重复跑不追加第二条 —— archiveOnHandoff: false 时指纹写在 <状态目录>/handoff-fingerprints.json,true 时写在日记条目里的注释标记;
- 写临时文件再 rename,不赌覆盖式写入;
- 行尾/编码:全文件 CRLF、UTF-8 无 BOM。

自检

node index.js --selftest # 在临时目录造假档案,跑全套回归(每项打实际输出)
node test-handoff-sandbox.mjs # 同一套,只打结论
node test-handoff-sandbox.mjs --verbose

自检口在模块层(不是 apply 里)—— 独立跑 node index.js 时 cordis 不会调 apply,写在里面会一声不吭。

已知边界

- 它不保证「交接的内容是这一轮的全文」:交接包是压缩后的接手起点,不是复制品—— 新会话一开就吃满上下文等于把坑搬过去。
- 日记正文里的 event_description 由「最近说过的话 + 动过的工具」拼成;没给 userMood 时那一栏会照实留白,不替你编心情。
- 跨设备没做:手机侧没有同一套 Web 客户端,切会话要另想办法。
- legacyDevice / legacyBoundary 只是判据:本插件不会去改写任何旧条目。

License

MIT
预览分享卡片长什么样 ↓S3K926/dsh-auto-handoff
分享 · DeepSeek Harness Hub
S3K926/dsh-auto-handoff

DSH 插件:上下文到阈值时生成交接包并切到新会话。已弃用 2026-10-17:换会话整条退役——续航交给 DSH…

回到顶部