Skip to content
dsh-market Browse plugins GitHub 中文

mikugui/dsh-session-eater

Session cleanup for the DSH sidebar: drag a conversation row onto the balance widget's whale and it eats the row — the session directory goes to a recycle bin, not to rm. Deletion asks one short question first (how many tokens it burned, and that prompt can be turned off in Settings). Undo from the persistent toast or the 12-entry log in Settings. Clicking the whale jumps straight to the plugin's Settings, where you can swap the avatar, drag the mouth into place and rewrite all 17 UI strings; blank, currently-open and running sessions are refused in place with a reason.

Stars ★ 0 Category Sessions & Messages Listed 2026-09-21

Install

Inside DeepSeek Harness, with dsh-market

dsh plugin --profile web add dshmarket

Or from the command line

dsh plugin --profile web add "https://github.com/mikugui/dsh-session-eater/releases/latest/download/dsh-session-eater.tgz"

Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.

Screenshots

README

This plugin publishes its README in Chinese only.

把不要的会话拖到侧栏底部的小胖鱼嘴边,它张嘴吃掉 —— 会话目录移入回收站,不是 rm。

  • 删除前确认:只问一句「消耗了 1.2M token,真的给我吃吗?」,可在设置里关掉
  • 撤销两条路:回执上的「撤销」+ 设置里 12 条持久台账(跨刷新保留)
  • 点图标直达设置:点侧栏那条药丸(或 Tab + 回车)= 打开「设置 → 会话喂鱼」
  • 17 段界面文案全可改,留空=不显示;形象、大小、嘴位、动画也能调
  • 吃不了的会当场说清:空白会话 / 正在聊的 / 正在跑的,就地变红拒绝
  • 零运行时依赖、无构建步骤

喂鱼


怎么用

  1. 侧边栏最底部(「设置」上面)有一条 🐟 想吃大白饭 药丸。 点它(或按回车/空格)= 直接打开「设置 → 会话喂鱼」,不用先点设置再找分区。 文案都能改,见下面的「设置 → 文案」。

  2. 抓住任意一条会话行往它那边拖 —— 会话列表下缘会展开一大块虚线投放区(默认写着"拖到这里丢掉")。

  3. 拖到鱼头上:它张开嘴开始啃(投放区变绿实线,文案变"啊啊啊")。

  4. 松手 —— 先在会话列表里问一句,只问一句:不列会话名、也不算轮数账:

    删除前确认

    消耗了 1.2M token,真的给我吃吗?
                               [取消] [删除]
    

    确认期间鱼张着嘴不动、脑袋慢慢点、腮红亮着 —— 一副"快给我吃"的渴望表情 (跟悬停时那种咀嚼 dse-chew 明确区分:这里是静止张开)。

    点删除才真吃掉(列表里那一行立刻消失,底部弹出「嗝~已吃掉:<标题> [撤销]」); 点取消或按 Esc 就当什么都没发生,一个请求都不发。默认焦点在取消上,回车不会误删。

    token 数来自内核自己的会话投影缓存(storages/session_projcache/sessions/<id>.json 里的 tokenUsage.totals),跟界面同源,不用解那个 5MB 的多帧 zstd。 读不到时(比如空白会话)会退回不带数字的「真的给我吃吗?」。 这一路只读内核的盘上文件,不读任何其他插件的状态、也不调用它们的接口 —— 所以别人装了哪些插件都不影响这里的数字(读不到就退回不带数字的询问语)。

    嫌每次都要确认,可以在 设置 → 删除前确认 里把开关关掉 —— 那就回到"松手即删"。 四段文案(两条询问语、确定、取消)都能改;带用量那条里的 {tokens} 会被换成实际数字, 留空则退回不带数字的那条。

上面引号里的都是出厂文案,全部可以在设置里改掉或清空(清空 = 那段文字不显示)。

撤销有两条路,任选:

  1. 回执上的「撤销」 —— 带撤销的回执不会自动消失(右边有 ✕ 才关), 所以不用再担心"4 秒没看到就没了":

    回执上的撤销

  2. 设置 → 会话喂鱼 → 最近吃掉 —— 一份持久台账(存在浏览器本地,最多 12 条), 每条都带「撤销」,回执关掉之后也能从这里捞:

    最近吃掉

早先药丸行右侧还有一个「撤销最近吃掉的一条」按钮,已移除:它读的就是上面那份台账, 而台账本来要跨刷新保留,于是刷新/重启后那个按钮一直挂在那儿,看着像个"删不掉的按钮"。 撤销现在只走上面两条路。

点撤销后宿主按回收站里那份 manifest 把会话目录搬回原来的分桶,页面自动刷新后回到列表。 即使一直没撤销,数据也只是被 移动 到回收站,不是 rm:

%DSH_HOME%\session-eater-trash\<时间戳>-<sessionId>\

想手动捞回来:把该目录搬回 %DSH_HOME%\sessions\<原分桶>\<sessionId>\ 再刷新即可。 浏览器里还能用 __dshSessionEater.lastEaten() 看台账。

会被拒绝的三种情况

拒绝是就地给出的,不靠屏幕底部那条容易被忽略的回执:

  • 拖到鱼头上还没松手,投放区就变红、那行字直接写出原因(「别喂正在聊的这个会话」), 同时鱼闭着嘴(不摆出要吃的张嘴样子)。
  • 松手时整块晃两下、停在红色状态约 1.8 秒,底部再补一条回执(双通道)。

具体哪三种:

  • 空白会话(列表里的「新会话」,没有任何用户消息):DSH 会为工作区按需保持一个空白会话, 所以把空白的「新会话」喂掉之后,刷新/新建时它会以新的 session id 再冒出来 —— 看起来就像"删了没生效"。插件现在直接拒绝并说明原因。
  • 正在聊的这个会话:客户端拦下,写明「别喂正在聊的这个会话」。
  • 正在跑的会话:宿主还握着它的写句柄,返回 409;客户端认出这个原因码后也走上面同一套就地拒绝 (红色投放区 + 「会话还在跑,先停下来再喂」+ 鱼闭嘴摇头,1.8 秒后自动归位), 不再落进「没吃下去」那种"像是插件坏了"的失败回执。

这三条文案都能在 设置 → 文案 里改(拒绝:空白会话 / 拒绝:正在聊的会话 / 拒绝:会话还在跑), 关掉确认弹窗也不影响它们的醒目程度。

换句话说:真正有内容的对话删除后不会回来(删除是"移进回收站",不是 rm)。 刷新后又看到的那个「新会话」,是一个新 id 的空白会话,不是被删的那条。 想确认的话跑 node tests/inspect-sessions.mjs,它会按帧解开日志告诉你每条会话有几条用户消息。


设置:设置 → 会话喂鱼

💡 快捷入口:点侧栏底部那条药丸(或按回车/空格)直接跳到这一页。

实现上没有走内核 API —— 设置面板的开合状态是 SettingsRoot 内部的 React state, 内核只把 openSection 下发给 onboarding 步骤,普通设置分区拿不到。所以这里是 DOM 层实现:先点 [data-slot='sidebar.settings'] button(稳定契约),等面板挂载后 再点文案等于 会话喂鱼 的导航项。刻意不绑类名 —— 构建产物的类名是哈希的 (实测 VOzbGW_trigger / VOzbGW_navCell),客户端一升级就全变; 回归测试在 tests/verify-settings.mjs 第 8 项。

设置

  • 形象:跟随挂件(用余额挂件当前的角色图)或 自定义。自定义可以选择文件 / 直接把图拖进预览框 / 填图片网址, 挑完自动切到自定义模式,不用先切模式再挑图。上传的图会等比缩到 ≤512px 再编码成 WebP,免得撑爆本地存储。
  • 预览与嘴的位置:预览框里显示的嘴就是最终效果,点一下或拖动就能把嘴挪到你的图上正确的位置(绿圈是嘴心)。 画嘴 三档:自动(只有默认立绘才画,因为只有它量过)、开(一直画,自定义图用它)、关(不画)。 还有 嘴大小 与 回到默认位置。
  • 大小:投放区形象边长(56–220px)、底部药丸头像边长(16–48px)。
  • 删除前确认:一个开关,没有别的。开着(默认)时松手不会直接删, 而是在会话列表里弹出确认框;关掉=回到"松手即删"。
  • 动画:咀嚼周期(0.2–1.6s)、两颊粉红开关、点头咬合开关。
  • 文案 / 最近吃掉:这两个区块很长,默认为折叠状态,点标题展开(标题右侧会给 17 段 / 2 条 这样的摘要,不展开也知道里面有多少东西)。收起时正文根本不渲染, 所以设置页一眼能看全。展开状态记在配置里,刷新后保持。
  • 文案:界面上出现的每一句话都能改,留空 = 那段文字不显示(药丸只剩图标、投放区只剩鱼、 回执只剩会话标题和按钮)。

文案设置

17 段可改文案与出厂值:

设置里的名字 出现位置 出厂值
空闲时 药丸上的字(平时显示的那个) 想吃大白饭
开始拖拽时 已经抓起会话、还没到鱼头上 松手喂鱼
拖到鱼头上时 悬停时药丸与投放区的提示 啊啊啊
正在吃时 松手之后、请求返回之前 咔嚓…
投放区提示 还没拖到鱼上时,投放区里的那行字 拖到这里丢掉
吃完回执 后面自动接上会话标题 嗝~已吃掉
失败回执 后面自动接上错误信息 没吃下去
回执上的撤销 回执里那个撤销按钮的字(留空则不显示撤销) 撤销
撤销成功提示 点完撤销之后的回执 已吐出来,刷新后回到列表
确认弹窗标题 读不到用量时的那句询问 真的给我吃吗?
确认弹窗标题(带用量) {tokens} 换成实际用量;留空则退回上一条 消耗了 {tokens} token,真的给我吃吗?
确认弹窗:确定 真删那个按钮(留空回落到「删除」) 删除
确认弹窗:取消 算了那个按钮(留空回落到「取消」) 取消
拒绝:正在聊的会话 拖的正是当前打开的会话时 别喂正在聊的这个会话
拒绝:会话还在跑 宿主回 409(会话正在运行)时 会话还在跑,先停下来再喂
拒绝:空白会话 拖的是没有内容的「新会话」时 空白会话不用清理 —— …
悬浮说明 鼠标停在药丸上的小提示(title) 把不要的会话拖到鱼头上…

改完立刻生效(药丸/投放区会马上换字),不用刷新。旁边的 恢复默认文案 只重置文案, 底部那个 恢复全部默认 会连形象、尺寸、动画一起重置。

设置页自己在导航里的名字(「会话喂鱼」)故意不可改 —— 否则把它清空就再也找不到这个设置页了。

  • 最近吃掉:被吃掉的会话台账(最多 12 条,存在浏览器本地),每条都能一键撤销 —— 回执关掉之后也能从这里捞。现在这是唯一"撤历史"的入口(侧边栏那个按钮已移除)。
  • 恢复全部默认:一键回到出厂设置(含形象、嘴位与全部文案)。

配置存在 localStorage["dsh-session-eater/config"],改完立刻生效、刷新不丢、不用重启。 代价是它只属于这个浏览器 —— 换浏览器/换端口要重设一次。写入失败(图片太大超配额)时面板底部会给出提示。


实现要点

会话列表由官方 @deepseek-ai/dsh-client-ui-workspace 渲染,会话行本来就是 draggable 的,而且 dragstart 会把 sessionId 写进 dataTransfer 的 text/plain。 所以这个插件完全不改官方组件:

半边 干什么
lib/client.js 在 sidebar.footer.action 席位注册药丸;文档级监听 dragstart/dragend/drop;拖拽期间在会话列表下缘开一块 position: fixed 投放区;悬停时给形象挂上 data-over 触发张嘴 + 咀嚼动画;松手后 POST /delete,并渲染可撤销回执
lib/index.js 注册 /dsh-session-eater/{delete,restore,whale.png,status};真正执行删除

为什么自己实现删除

内核 0.1.5-rc.2 没有可用的会话删除 RPC: workspace/deleteSession 与 workspace/unarchiveSession 只在 @deepseek-ai/dsh-api-workspace-controller/lib/typert.host.js 的 typert 清单里被声明, 宿主侧的 WorkspaceController 里并没有对应方法,调用只会失败。

所以 /delete 用内核已有的公开能力自己走完:

  1. ctx.workspaceRegistry 里找到持有该会话的工作区 → workspace.detachSession(id)(工作区本身保留);
  2. 若在归档集合里 → registry.unarchiveSession(id)(顺带清掉指向已消失会话的陈旧归档项);
  3. 会话目录 rename 进回收站,并在桶里写一份 .dsh-session-eater.json 记住原始路径(撤销按它搬回); 移不动会直接报错(code: move-failed)而不是假装成功 —— 静默失败会让用户以为删掉了、 刷新却又出现,这是最糟的失败模式;
  4. 删掉 storages/session_projcache/sessions/<id>.json,避免重启后从投影缓存里复活一行;
  5. ctx.emit("api-session/removed", id) —— 这个事件在 dsh-api-remotes 的转发白名单里, 浏览器侧的 session controller 会据此把该行从会话列表摘掉,无需刷新; 客户端拿到 200 后还会再调一次本地的同款入口(sessions.handleSessionRemoved)兜底 —— 因为如果该会话在宿主进程里还是"活的",宿主可能继续把它写进 baseline。

排查工具

node tests/inspect-sessions.mjs

只读列出现有会话 / 回收站 / 投影缓存 / 工作区账目,并告诉你每条会话有几条用户消息(= 是不是空白会话)。 它按 zstd 魔数切帧逐帧解压 —— session.v3.jsonl.zstd 是多帧追加格式(本机当前会话 900+ 帧), 而 Node 的 zstdDecompressSync / createZstdDecompress 只给第一帧,直接解会误判成"只有 1 个事件"。

浏览器侧还有一个诊断桥(和 dsh-session-manager 的 window.__dshSessionManager 一个路子):

__dshSessionEater.version
__dshSessionEater.sessionsProbe()          // 当前会话 id、各会话的 blank 标记、快照字段
__dshSessionEater.isBlank('session-xxx')   // 某个会话是不是空白会话
__dshSessionEater.current()
__dshSessionEater.config() / setConfig({ size: 140 }) / resetConfig()

形象从哪来

默认直接用余额挂件的路由 /dsh-whale/image.png(所以你在挂件里换了角色,鱼头也跟着换); 取不到时回落到本包自带的 assets/whale-fallback.png。

嘴怎么画的

嘴是手绘 SVG,不是贴一个椭圆色块:

  • 唇线用原画的描边色 #16264f(直接从立绘上取的),口腔是暗梅红渐变,舌头粉色 + 一点白高光, 整张嘴再加一点 drop-shadow 让它"嵌"进脸里而不是浮在脸上;
  • 口腔 = 唇线路径绕偏下的支点缩 0.84 —— 于是上唇天然比下唇厚,和原画线稿一致;
  • 外层 SVG 是 16.5% × 14% 的扁框且 preserveAspectRatio="none",近似圆的路径会自然摊成横向的"啊"口。

口腔坐标源自对 610×610 原始立绘的实测:嘴心约在 x 52% / y 72.5%。 自定义图不是正方形时,object-fit: contain 会留边,所以嘴的位置是先换算到图片实际显示矩形再折算回框内的 (见 displayRect() / mouthBox()),否则嘴会飘到黑边上。 换了自定义角色/图标时在设置页拖一下就能对准。

咀嚼动画

嘴和脑袋同一时长(0.54s)、同一 easing,所以读起来是"一口咬下去"而不是两个独立动作:

  • 张嘴 = 下颚往下掉(translateY 正值 + 纵向拉伸),闭嘴 = 下颚往上收(负值 + 压成一条唇线);
  • 脑袋方向相反 —— 合嘴时低头咬下、张嘴时抬头,形成咬合感;
  • 一个周期咬两下;两颊的粉红(原画本来就有腮红,这里只做很轻的加成)跟着同一节奏脉动。

系统开了「减少动态效果」时动画自动关闭,嘴保持在张开状态。

想调动画就 node tests/filmstrip.mjs 10 —— 它把一整个咀嚼周期按相位冻结成一张 docs/chew-cycle.png,好不好看一眼就知道。

想把这段动画导出来做视频片头/封面用 node tests/loop.mjs [帧数](默认 18 帧 = 每帧 30ms):

产物 说明
docs/chew-loop.gif 原速循环 0.54s,透明背景
docs/chew-loop-slow.gif 3 倍慢 1.62s,做片头更好读
docs/chew-loop-dark.gif 深色底(#0e1322)可直接拖进剪辑软件
docs/chew-loop.webp 8 位 alpha,叠背景不会出 GIF 的锯齿边
<工作区>/_loop-frames/ 透明 PNG 序列,剪映/PR/AE 都能吃(不入库)

它和 filmstrip.mjs 同一套手法(拖拽武装 → 用负 animation-delay 把动画冻在周期里的 不同相位),区别是逐帧单独截图。两个坑记在这:omitBackground 只去掉页面默认白底, 应用自己的背景层得用 visibility:hidden 藏掉;html/body 自己的 background 会直接画在 canvas 上,必须显式设成透明 —— 否则导出的帧全是"看起来透明、其实贴了一层底色"。


文件

dsh-session-eater/
├─ package.json          # dsh.bundle.patch + dsh.client 双半边声明
├─ cordis.patch.yml      # bundle 层的 loader insert
├─ lib/
│  ├─ index.js           # 宿主半边(路由 + 删除/撤销)
│  └─ client.js          # 客户端半边(药丸 + 投放区 + 嘴 + 设置页 + 配置存储)
├─ assets/whale-fallback.png
├─ tests/
│  ├─ lib.mjs            # token / puppeteer / 断言工具
│  ├─ verify-host.mjs    # 宿主契约测试(合成会话,不碰真数据)
│  ├─ verify-client.mjs  # 拖拽/张嘴/回执端到端(fetch 打桩,不可能删东西)
│  ├─ verify-settings.mjs# 设置页端到端(上传图片、校准嘴位、尺寸、文案、持久化、恢复默认)
│  ├─ inspect-sessions.mjs # 只读取证:会话/回收站/缓存/账目 + 每条的空白判定
│  ├─ peek-localstorage.mjs # 只读取证:直接从 Edge/WebView2 的 LevelDB 里读你存的配置
│  ├─ fixtures/          # 测试用图(故意做成非正方形,专门验 contain 换算)
│  ├─ filmstrip.mjs      # 把咀嚼周期按相位冻结成 contact sheet(调动画用)
│  ├─ loop.mjs           # 导出循环动图(GIF/WebP + 透明 PNG 序列,做视频片头用)
│  ├─ verify-market-entry.mjs # 投稿前自检:对照市场 CI 规则核 entry
│  └─ hero.mjs           # 生成 README 效果图
└─ docs/
   ├─ eating.png         # 真实尺寸下的投放区
   ├─ settings.png       # 设置页
   ├─ settings-text.png  # 设置页的「文案」区块
   ├─ settings-eaten.png # 设置页的「最近吃掉」区块
   ├─ toast-undo.png     # 吃掉后的回执与撤销按钮
   ├─ chew-cycle.png     # 咀嚼周期 10 帧分解
   └─ chew-loop.gif      # 咀嚼循环动图(透明背景;loop.mjs 生成)

安装 / 卸载

从 GitHub 安装(推荐,不用克隆):

dsh plugin --profile web add github:mikugui/dsh-session-eater

装完 dsh web 会自动把本包加进 dsh.profile.bundles,重启 dsh web 即生效。 想升级就 dsh plugin --profile web update dsh-session-eater。

⚠️ github: 安装要靠 pnpm + 全局可用的 git。如果机器上只装了 GitHub Desktop (它自带的 git 不进 PATH),先补上 PATH 再装,例如:

$env:PATH += ";$env:LOCALAPPDATA\GitHubDesktop\app-3.6.6\resources\app\git\cmd"

或者干脆装个 Git for Windows,或者改用下面的本地目录 / tgz 方式安装。

从本地目录安装:

dsh plugin --profile web add "link:D:\path\to\dsh-session-eater"
# 或从 npm 包
dsh plugin --profile web add "D:\path\to\dsh-session-eater-0.4.0.tgz"

整个插件零运行时依赖(宿主半边只用 node 内置模块,客户端半边只 require 宿主已提供的 react / react/jsx-runtime),所以不需要任何额外安装步骤。

本机当前的实际装法(bundle 层)

这台机器上已经装好并且在跑了,走的是标准 bundle 层(package.json 的 dsh.profile.bundles + pnpm link:),用户 patch 层里不要再放同名 insert:

// %DSH_HOME%\profiles\web\package.json
"dependencies": { "dsh-session-eater": "link:D:/deepseek harness/dsh-session-eater" },
"dsh": { "profile": { "bundles": [ "...", "dsh-session-eater" ] } }

安装 / 卸载都走 CLI,别手改 YAML:

dsh plugin --profile web add "link:D:\deepseek harness\dsh-session-eater"
dsh plugin --profile web remove dsh-session-eater

⚠️ 不要同时在用户 patch 层(profiles\web\cordis.patch.yml)里再 insert 一次。 bundle 层和用户 patch 层会各插一遍同一个 id,loader 直接抛 duplicate loader entry id: dsh-session-eater 把 boot 打崩 (2026-09-21 实际踩过,只能手改 YAML 才救得回来)。二选一,且以 bundle 层为准。

⚠️ 改了宿主侧 lib/index.js 必须重启 dsh web 才生效。 bundle 层不做热重载;patchReload: live 只监听 patch 层文件,不会重新 import 模块。 客户端半边(lib/client.js)不受影响:dsh-client-hmr 在轮询 bundle,改完浏览器 会自动热重载。想确认宿主半边的版本,看 /dsh-session-eater/status 返回的 version 字段。

🧭 改完 patch / 依赖后自检一次——重复 id 会在这里立刻暴露,而不是等到启动时崩:

dsh web --dump-config > $env:TEMP\dump.yml
Select-String $env:TEMP\dump.yml -Pattern "^- id: " |
  ForEach-Object { $_.Line.Trim() } | Group-Object | Where-Object Count -gt 1

测试与调参工具

这些脚本都对着正在运行的 dsh web 说话,不需要重启,也不会碰任何真实对话:

cd "D:\deepseek harness\dsh-session-eater"
node tests/verify-host.mjs      # 合成会话目录 → 验 /delete 契约与回收站
node tests/verify-client.mjs    # 拖拽/张嘴/回执(fetch 打桩)
node tests/verify-settings.mjs  # 设置页:上传图、按 contain 校准嘴位、尺寸、持久化、恢复默认
node tests/inspect-sessions.mjs # 只读取证:会话/回收站/缓存/账目(排查"删了又回来"用)
node tests/peek-localstorage.mjs # 只读取证:从 Edge/WebView2 的 LevelDB 读你存的配置
node tests/filmstrip.mjs 10     # 咀嚼周期 10 帧分解 → docs/chew-cycle.png(调动画用)
node tests/hero.mjs             # 重出 README 效果图 → docs/eating.png
  • 宿主测试:自己造 session-eater-selftest-* 目录,验完清理干净。
  • 客户端测试:页面里把 window.fetch 换成桩,/dsh-session-eater/* 只记账不外发, 所以物理上不可能删掉你的会话;拖拽用真 DragEvent + DataTransfer 合成。
  • 设置测试:上传一张非正方形图,验证嘴是按要求换算到 contain 后的显示矩形而不是正方形框; 它在一次性的 headless 浏览器里跑,不会动你正在用的浏览器里的配置。

依赖探测顺序:DSH_TEST_BROWSER → Edge → Chrome。 puppeteer-core 直接复用 profile 里已有的那份,不额外装东西。


维护者:怎么发版

scripts/ 下有几个脚本,全程走 GitHub REST API,不依赖 git (网络受限、或机器上根本没装 git 时尤其有用):

# 0) 改仓库页面上那句「简介」(Description / topics):正文写在脚本顶部的 ABOUT 里
node scripts/update-meta.mjs --dry-run      # 只看新旧文案
node scripts/update-meta.mjs                # 真写(幂等)

# 1) 推代码:把当前目录作为一个提交追加到远端 main 之上
node scripts/publish-github.mjs --dry-run   # 先看会推哪些文件
node scripts/publish-github.mjs             # 真推

# 2) 发 Release:打 tag、写 release notes(自动取 CHANGELOG 里对应段落)、上传附件
node scripts/release-github.mjs --dry-run
node scripts/release-github.mjs             # 附件自动找上一层的 *-<version>.tgz / *.zip

# 3) 投稿到插件市场(精选目录 awesome-dsh-plugin:一个 PR 加一个 yml)
node scripts/submit-market.mjs --dry-run    # 离线:打印计划与待提交的 entry 内容
node scripts/submit-market.mjs              # 加 topic + 补无版本号 tarball + fork + 建分支 + 开 PR

# 打包(发 Release 前先做)
npm pack --pack-destination ..
Compress-Archive -Path .\* -DestinationPath ..\dsh-session-eater-<version>.zip

投稿到市场的硬性条件(CI 会逐项检查,写在 scripts/market-entry.yml 的注释与 ../市场投稿材料.md 里):package.json 必须有 dsh.bundle(只有 dsh.client 会被拒)、 仓库根要有 cordis.patch.yml、仓库创建满 1 天、仓库带 dsh-plugin topic。 submit-market.mjs 会先查仓库年龄,不满 1 天直接拒绝提交并告诉你可以提的时间。 tarball: 建议指向不带版本号的附件(releases/latest/download/<name>.tgz)—— 带版本号的文件名会在下次发版后 404,脚本会自动补上传这个附件。

凭据读取顺序:环境变量 GITHUB_TOKEN / GITHUB_OWNER → 工作区根目录的 .github-token。 后者是两行文本(第 1 行 token,第 2 行用户名可选,# 开头是注释),建议存成 UTF-8 带 BOM, 这样记事本打开不乱码。脚本会用正则从整行里抠出令牌本体,前后多粘了 ghp_ / github_ 之类 前缀也能认出来;并会先做形状预检(classic 必须是 ghp_ + 36 字符)再动网络。

⚠️ 令牌请用 classic + 只勾 repo。fine-grained 令牌建不了仓库 (POST /user/repos 会回 403 Resource not accessible by personal access token)。

两个实测踩过的坑(脚本已经各自兜住,但值得知道)

  1. latest/download/<name>.tgz 的名字不能立刻复用。附件名是仓库级唯一的,发新版时脚本会 把它从旧 Release 上摘下来再传到新 Release —— 但 GitHub 的删除不是立刻生效的, 刚摘完就传会回 422 already_exists(v0.6.0 实测:Release 都发出去了,这个附件还空着)。 现在脚本会带重试(最多 6 次 × 10 秒,每次重试前再摘一遍)。
  2. 投稿用的 fork 会过期,而过期的 fork 会让 PR 变 dirty。fork 落后上游之后:
    • 直接拿上游 main 的 sha 在 fork 上建 ref 会 404 Not Found(那个提交不在 fork 的对象库里);
    • 退用 fork 自己的 main 又能建 ref,但那个 base 里还没有我们的条目文件, 于是新分支"新增"了一个上游已存在的文件 → add/add 冲突,PR 合不了。 脚本会先试 merge-upstream 同步 fork;本机这条必然失败,因为上游带 .github/workflows/build-site.yml,而 classic 令牌没有 workflow 权限 (422 refusing to allow a Personal Access Token to create or update workflow)。 失败后脚本会退到「上游最后一次改过本条目文件的提交」当 base:它一定含这个文件 (diff 只剩我们这次的改动),而且是上游 main 的干净祖先(behind=0)。 想彻底省掉这个回退,给令牌补一个 workflow 勾选即可(令牌字符串不变,不用重新粘)。

发版清单

  1. 改 package.json 的 version
  2. 在 CHANGELOG.md 顶部加 ## <version> 段落(会被自动当作 release notes)
  3. npm pack + Compress-Archive 出两个产物
  4. node scripts/publish-github.mjs
  5. node scripts/release-github.mjs
  6. 删掉 .github-token,并去 GitHub 设置里把这个令牌 Revoke

关于提交历史:publish-github.mjs 是在远端 head 之上建提交的,所以它产生的 SHA 和本地 git commit 出来的不同(同内容、不同历史)。要么以后统一用脚本推,要么在网络正常时先 git fetch origin && git reset --hard origin/main 把本地对齐,再用普通 git push。

不需要令牌的发版路径(日常推荐)

只要 git 能推(凭据交给 Git Credential Manager / GitHub Desktop 管),发版可以完全不碰 API:

# 1) 改 package.json 的 version,在 CHANGELOG.md 顶部加 ## <version> 段落
# 2) 打包
npm pack --pack-destination ..
Compress-Archive -Path .\* -DestinationPath ..\dsh-session-eater-<version>.zip
# 3) 提交与 tag 一起推
git add -A
git commit -m "release: v<version>"
git tag -a v<version> -m "dsh-session-eater v<version>"
git push --follow-tags
  1. 网页上发 Release:仓库页右侧 Releases → Draft a new release → 选刚推上去的 tag → 正文粘 CHANGELOG.md 里那段 → 把上面两个产物拖进附件区 → Publish release。

两个脚本的 --dry-run 都是纯离线的(不校验凭据、不发网络请求),可以先用它确认 要推 / 要传哪些文件。release-github.mjs --dry-run 还会顺便打印它准备用的 release notes。

网络提醒(在受限网络里实测得到):github.com:443 会时通时断 —— 同一台机器上出现过 git push / curl 连续 21 秒超时(Failed to connect to github.com port 443),而 api.github.com 全程稳定。所以在这种环境里优先用上面那两个 API 脚本, git push 只作为网络窗口好时的补充。判断方法很直接:

curl.exe -4 -s -o NUL -w "%{http_code}`n" -m 20 https://github.com/     # 000 = 不通
Invoke-RestMethod https://api.github.com/rate_limit                    # 有响应 = 通

排障:删不掉 / 拖了没反应 / 界面样式裸奔

按这个顺序查,绝大多数情况第 1 步就好了:

  1. 先刷新页面(F5)。 客户端半边由 dsh-client-hmr 热重载,宿主半边改动要重启 dsh web。 如果你在重启前就开着页面,标签页里跑的可能还是旧 bundle —— 现象正是"拖进去没反应、 会话删不掉",而且回收站里一条新记录都没有(请求根本没发出去)。 (实测踩过一次:dsh web 重启后旧标签页就这么僵着,刷新即好。)

    同一个根因还会让界面"裸奔":插件注入的 <style> 是挂在 ctx.effect 的清理函数上的, 热重载时会被摘掉;如果那次重载没跑完,页面就一直没样式 —— 于是布局退回默认的块级流淌。 实测过别人的插件栽在这:表情包选择面板从 4 列网格 变成 774×1800 的一列竖排 (.mp-grid 的 display 从 flex 变回 block),刷新即恢复。本插件已经加了样式自愈 (MutationObserver 盯 <head>,样式不在就补一次),所以不刷也应该没事,但别的插件不一定有。

  2. 看宿主半边活着没:

    curl http://127.0.0.1:3080/dsh-session-eater/status
    

    返回 {"ok":true,...,"version":"0.4.1"} 就正常。version 直接读自 package.json, 所以"装的到底是哪一版"一眼能看出来,不会骗人。

  3. 看日志有没有 loader 报错(%TEMP%\dsh-web.log):

    • duplicate loader entry id: dsh-session-eater → 插件被注册了两次。它只能出现在一处:profile package.json 的 dsh.profile.bundles,或者 profile cordis.patch.yml 里的 insert:,不能两边都写。 (插件管理器每次装/卸插件都会按 dependencies 重组 bundles,很容易把两边都写上; 踩过一次,loader 直接起不来。)
    • 其它 plugin tree failed to load → 先修 patch / bundles,再重启。

删除失败时回执会带原因(例如「没吃下去:HTTP 404」,或宿主返回的 session not found / move-failed),把那一行贴出来就能定位。


已知取舍

  • 只处理左侧会话列表里的常规会话行;工作区行(projectRow)拖不动,也不会被吃。
  • 空白会话(还没落盘的)也能吃:磁盘上没有目录时就只摘账目 + 广播移除。
  • 回收站不自动清理,长期用可以自己定期清 %DSH_HOME%\session-eater-trash。
  • 配置只存在本浏览器(localStorage),不跨浏览器/端口同步 —— 想跨端同步得把配置搬到宿主侧 (新增宿主路由 + 落盘),而宿主代码改动需要重启 dsh web 才生效,所以这版先做客户端持久化。
  • 自定义图标建议用透明背景 PNG(嘴是画在图片上的,不透明底的方图会看出边界)。

Content from the project README on GitHub ↗

Comments

Comments live in GitHub Discussions. Sign in with GitHub to post or react.