19 Commits
Author SHA1 Message Date
pyh fee7799d7e 把清理临时文件从通用设置移到插件页 2026-09-30 16:41:03 +08:00
pyh da8e5e43a4 refactor!: align with the DSH plugin conventions and drop the npm publish workflow
Follow the cordis-plugin-development references instead of the ad-hoc choices
the first version made.

Client half:
- register the row button under this package's own id instead of shadowing the
  shipped `archive` action at a lower priority, so the official hover buttons
  keep their cells and archiving stays where the harness put it
- drop the synthetic `pointerout` dispatched at a `[data-row-key]` ancestor:
  a plugin does not read or drive another package's DOM, so the tooltip is now
  positioned from its own button alone
- keep the self-rendered primitives, the `--dsw-*` token-only styling, and the
  modal focus/Escape behavior, and document why

Host half:
- resolve a conversation's descendants with `sessionQuery.traceSession()`
  instead of listing every stored header and re-deriving `parentSession` edges
- decide liveness from the traced `SessionRecord.live` flag, which removes the
  `agents` and `sessions` dependencies from `inject`
- build the orphan-sweep corpus from `sessionQuery.listSessions()`, which
  already merges live and persisted sessions
- document that `sessionPersistence.locate()` is a JSONL-backend diagnostic
  hook, not part of the seam, and keep probing it explicitly
- record only tree roots in the deferred-deletion ledger: the activation sweep
  removes a root with everything under it, so a separately recorded child was
  deleted twice or needed a pass that never runs
- return the activation's sweep promise from `apply()` so a test can await it

Manifest and docs:
- version 2.0.0, `private`, `dsh.manifestVersion`, and `engines`
- delete the Gitea npm publish workflow and every npm-publishing task: the
  package is distributed only through the Git repository and tags
- rewrite the README around the current install paths and the plugin's limits
2026-09-30 15:30:39 +08:00
pyh acaca33c49 ci: publish with the token that actually works
The job token belongs to the gitea-actions pseudo user, which is not an organisation member, and Gitea grants package write only to members of the owning organisation; v1.0.2 proved it with E401 on the job token and a successful publish on the PAT. The step now prefers NPM_TOKEN and only falls back to the job token, printing its identity when it does.
2026-09-30 14:31:41 +08:00
pyh 1a9165fa4f release: 1.0.2, and learn whether the built-in job token can publish
publish / npm (push) Successful in 5s
The publish step now tries the built-in Gitea job token first, prints which user it belongs to, falls back to NPM_TOKEN when the registry rejects it, and reports which token actually published. A 409 keeps meaning 'already published', so a re-run stays harmless.
2026-09-30 14:29:02 +08:00
pyh c842cd0118 docs(ci): describe the job token honestly instead of promising it
The first run failed with E401 while using the built-in token and before the workflow declared permissions, so nothing here proves the job token cannot publish. The header now says a write:package token is what makes publishing reliable, that the job token depends on the instance's Actions token settings, and that permissions: packages: write is what keeps a restricted token from being read-only.
2026-09-30 14:14:55 +08:00
pyh 0a452bd21b docs: mention the semver ref form for following 1.x
pnpm accepts #semver:^1.0.1 on a git address and resolves it against the repository tags; verified against this repository, where it selected the v1.0.1 commit.
2026-09-30 14:00:26 +08:00
pyh d09920363f docs: say which install field takes the git address
The dialog has two inputs: the spec field takes a package name, a git address or a local path, while the install source only decides which registry a package name is looked up in. Git and tarball specs are fetched directly, so the source can stay on its default; the previous wording left that open and the address ended up in the wrong field.
2026-09-30 13:54:51 +08:00
pyh b9445cbf4d docs: install from the git address or a tarball, and say why the npm source cannot work
Gitea's npm registry returns a fixed field set, so the dsh field never reaches the installer's registry preflight and the install is refused with 'declares no dsh.bundle'. The tarball and the git address carry the real package.json, so they are the routes that work.
2026-09-30 12:03:03 +08:00
pyh dbf6138df2 release: 1.0.1
publish / npm (push) Successful in 3s
Ships the rewritten README inside the package and pins the documented install specs to this version.
2026-09-30 11:54:43 +08:00
pyh 105e86edd5 docs: rewrite the README around the plugin itself
Only the features, the behaviour boundaries, the three ways to install it, and a precise compatibility statement remain; the release pipeline, the file layout and the local-directory install method are gone. The install section now mirrors how a published DSH plugin is described: npm package plus custom registry, a pinned git URL, or a downloaded tgz.
2026-09-30 11:46:17 +08:00
pyh ec8aadedb7 docs: record what the release workflow actually depends on
The publish section now says why the workflow avoids external actions, that a repeated version is a skip rather than a failure, and that an npm registry line pointing elsewhere produces a misleading 'version already exists'.
2026-09-30 11:37:25 +08:00
pyh 06889ab3ec ci: point the default registry at Gitea and recognise npm's duplicate-version error
Without setup-node nothing wrote the default registry, so npm compared versions against registry.npmjs.org and refused with 'cannot publish over the previously published versions'. The step now sets the default registry too, and treats both that message and Gitea's 409 as an idempotent skip.
2026-09-30 11:35:43 +08:00
pyh a3fbe602ff ci: drop the external actions so the runner never needs github.com
Runs 11 and 14 died on 'dial tcp 20.205.243.166:443: i/o timeout' while act_runner cloned actions/checkout from GitHub. The workflow now fetches its own ref with git and relies on the node already present in the runner image, so it only touches this instance.
2026-09-30 11:33:51 +08:00
pyh 3a806f84ab ci: treat an already-published version as a skip, not a failure
The registry check through npm view did not see the existing version, so publish ran into E409. The registry itself is the authority here: a 409 now reports 'already exists, skipping' and exits zero, while every other error still fails the step.
2026-09-30 11:31:42 +08:00
pyh f4e107782a ci: make a repeated release a no-op and drop the misleading diagnostics
Gitea does not implement the npm whoami endpoint and npm refuses to read an auth token back, so both lines only produced error-looking output. The run now checks the registry first and skips publishing a version that already exists, which also makes a manual re-run safe.
2026-09-30 11:28:57 +08:00
pyh 733f0ba456 ci: fall back to a PAT and self-diagnose the publish step
publish / npm (push) Failing after 31s
The first tagged run failed inside the publish step within a second, which is the shape of an auth rejection rather than a build problem. The step now prefers a repository NPM_TOKEN secret when one exists, prints the effective registry and a masked token, and runs npm whoami so a re-run names the cause instead of just failing.
2026-09-30 11:05:33 +08:00
pyh 37468d6ac9 feat!: publish as @dsh-plugin/session-delete with an automated Gitea npm release
publish / npm (push) Failing after 1m49s
The registry owner is the scope the package must publish under, so the name moves from @local/ to @dsh-plugin/ in all three places that must agree: package.json, cordis.patch.yml, and the client module id. `private` goes away because npm refuses to publish such a package, and publishConfig pins the registry to this organization's npm endpoint.

The tag workflow publishes whatever version the pushed v* tag names, using the built-in Gitea job token, and refuses a tag that disagrees with package.json.
2026-09-30 11:00:39 +08:00
pyh 29060db8dc chore: set the copyright holder to pyh 2026-09-30 10:57:21 +08:00
pyh 39a39adfec feat: permanently delete a conversation, sweep orphaned spill files 2026-09-30 10:43:38 +08:00
6 changed files with 54 additions and 245 deletions
-10
View File
@@ -1,10 +0,0 @@
# <emoji> <type>[(scope)][!]: <中文描述,一句话,不加句号>
# 一条提交信息只有这一行,不写正文,也不写脚注。
# 破坏性变更只用 ! 标记,例如:♻️ refactor!: 改掉了某某行为
# 需要交代背景时,把说明写进 README.md、CONTRIBUTING.md 或代码注释。
#
# 类型与 emoji:feat ✨ / fix 🐛 / docs 📝 / refactor ♻️ / perf ⚡️ /
# test ✅ / build 📦️ / ci 💚 / chore 🔧 / revert ⏪️
# 例:✨ feat(client): 会话行悬停区加删除按钮
# 例:🐛 fix(host): 延迟删除台账只记录根会话
# 例:📝 docs(ci): 安装说明只保留 Git 标签与本地 tgz
-94
View File
@@ -1,94 +0,0 @@
# 提交信息规范
本仓库的提交信息遵循 [Conventional Commits](https://www.conventionalcommits.org/) 的**首行**结构,并在行首加一个 emoji 做视觉标记。**描述用中文**,类型关键字、作用域保持英文。
## 格式
**一条提交信息只有一行**,没有正文,也没有脚注:
```text
<emoji> <type>[(scope)][!]: <中文描述>
```
- `<emoji>`:下面表格里该类型对应的 emoji,后面跟**一个空格**。emoji 是必需的,且必须与本行类型一致——`📝 docs:` 对,`✨ docs:` 不对。
- `<type>`:小写英文关键字,见下表。
- `(scope)`:可选,改动范围。用 `()` 包住一个简短英文标识,例如 `docs(ci)`、`fix(client)`、`feat(host)`。
- `!`:可选,紧跟在 type 或 scope 之后、冒号之前,表示破坏性变更。
- `<描述>`:中文,一句话,讲清这次改动做了什么;不换行、不用列表、不加句号。宽度不超过 72(中文字符按 2 个宽度算)。
**不要写正文,也不要写脚注。** 连 `Refs:` / `Closes:` / `BREAKING CHANGE:` 这类尾注也不写——需要交代的"为什么""影响面""迁移注意事项",写进 [README.md](README.md) 或本文件,或写进代码注释,让说明跟着代码走,而不是埋在 `git log` 里。
**破坏性变更只靠 `!` 标记**(例如 `♻️ refactor!: …`)。这一条是对 Conventional Commits 的有意偏离:它要求破坏性变更必须有 `BREAKING CHANGE:` 脚注,本仓库不写脚注,所以判断破坏性变更以 `!` 为准。
如果确实需要多行说明,说明这次改动不该只用一个提交标题交代——先补文档,再提交。
## 类型与 emoji
| 类型 | emoji | 码位 | 用途 |
| --- | --- | --- | --- |
| `feat` | ✨ | `U+2728` | 新增功能 |
| `fix` | 🐛 | `U+1F41B` | 修复缺陷 |
| `docs` | 📝 | `U+1F4DD` | 只改文档 |
| `refactor` | ♻️ | `U+267B U+FE0F` | 重构,行为不变 |
| `perf` | ⚡️ | `U+26A1 U+FE0F` | 性能优化 |
| `test` | ✅ | `U+2705` | 变更测试 |
| `build` | 📦️ | `U+1F4E6 U+FE0F` | 构建系统或依赖 |
| `ci` | 💚 | `U+1F49A` | CI 配置 |
| `chore` | 🔧 | `U+1F527` | 杂项,以上都不属于时用 |
| `revert` | ⏪️ | `U+23EA U+FE0F` | 回滚某次提交,正文写 `Refs: <被回滚的提交>` |
**复制上表里的 emoji,不要手敲。** 带 `U+FE0F` 的那五个是「基础字符 + 变体选择符」两个码位,手敲时很容易只打出前一个字符;对工具来说 `♻`(`U+267B`)和 `♻️`(`U+267B U+FE0F`)是两个不同的字符串,比对会失败。写规范或写脚本时,比较 emoji 前先做一次 NFC 归一化,或把两边都按 `\uFE0F?` 处理。
## 示例
每条都是一整条提交信息,就这一行:
```text
✨ feat(client): 会话行悬停区加删除按钮
```
```text
🐛 fix(host): 延迟删除台账只记录根会话
```
带作用域与破坏性标记(破坏性变更**没有**脚注,只看 `!`):
```text
♻️ refactor(client)!: 行内按钮改用自己的槽位 id
```
```text
📝 docs: 安装说明改为只走 Git 标签与本地 tgz
```
## 工具链提示
emoji 在行首是本仓库的有意选择,视觉上整齐。代价是:以类型前缀开头的解析工具(`commitlint`、`semantic-release`、`release-please` 等)会读不到类型。如果以后要接这类工具,有两条路:
1. 把 emoji 挪到冒号之后(`feat: ✨ 描述`),类型回到行首,规范其余部分不变;
2. 保留行首 emoji,给工具写一个剥掉开头 emoji 与空格的预处理。可用的正则(`\p{Extended_Pictographic}` 已覆盖带与不带变体选择符两种写法):
```text
^(?:\p{Extended_Pictographic}\uFE0F?\s+)?(?<type>[a-z]+)(?:\((?<scope>[^)]*)\))?(?<breaking>!)?:\s(?<desc>.+)$
```
## 自查
`scripts/check-commit-log.mjs` 会按本规范检查现有提交,只报告、不拦提交。除了首行格式,它还会报出**任何带正文或脚注的提交**:
```bash
node scripts/check-commit-log.mjs # 最近 10 条
node scripts/check-commit-log.mjs all # 全部
```
它从本文件的类型表里读 emoji 对照表,所以改了表不用改脚本。规范之前的提交不符合是正常的。
## 启用提交模板(可选)
`.gitmessage` 是提交信息模板。启用后每次 `git commit` 会带上前缀提示:
```bash
git config commit.template .gitmessage
```
模板整份都是 `#` 开头的注释,git 会自动丢弃,所以直接 `git commit -m` 也不受影响。只对当前仓库生效;换机器要重新执行一次。
+28 -22
View File
@@ -2,13 +2,13 @@
DeepSeek Harness(DSH)插件:给会话加上**真正不可逆的删除**,并清理已删除会话遗留的临时文件。 DeepSeek Harness(DSH)插件:给会话加上**真正不可逆的删除**,并清理已删除会话遗留的临时文件。
DSH 原本只能归档会话——归档只是把会话从列表里移开,日志仍然留在磁盘上,越堆越多。这个插件补上的正是这一块:两处删除入口,以及插件页上的临时文件清理。 DSH 原本只能归档会话——归档只是把会话从列表里移开,日志仍然留在磁盘上(DSH 官方的 JSONL 会话存储后端明确写着「没有任何东西会删除会话文件,日志会一直堆在 root 下直到被外部移除」,会话存储契约里也没有删除 API)。这个插件补上的正是这一块:两处删除入口,以及设置页里的一次性孤儿文件清理。
## 功能 ## 功能
### 1. 删除会话 ### 1. 删除会话
两个入口,行为一致,都会先弹出确认弹窗(Esc 关闭、Tab 焦点圈定、打开时焦点进入、关闭后焦点归位): 两个入口,行为一致,都会先弹出确认弹窗(复刻系统 Modal 的样式与键盘行为:Esc 关闭、Tab 焦点圈定、打开时焦点进入、关闭后焦点归位):
- 会话行右侧 **⋯** 菜单 → **删除会话** - 会话行右侧 **⋯** 菜单 → **删除会话**
- 会话行悬停时的 🗑 按钮(在官方的「归档」「置顶」之前,是悬停区的第一个按钮) - 会话行悬停时的 🗑 按钮(在官方的「归档」「置顶」之前,是悬停区的第一个按钮)
@@ -17,35 +17,37 @@ DSH 原本只能归档会话——归档只是把会话从列表里移开,日
| 对象 | 处理方式 | | 对象 | 处理方式 |
| --- | --- | | --- | --- |
| 会话日志 | 整个删除 | | 会话日志目录 | 整个删除,仅保留 `session.lock` |
| 子智能体会话 | 递归找出它的全部子会话,连同日志与临时文件一并删除 | | 子智能体会话 | 按血缘递归收集,连同日志与溢出文件一并删除 |
| 工具输出的溢出临时文件 | 删除该会话(含子会话)对应的目录 | | 工具输出溢出文件(spill) | 删除该会话(含子会话)对应的 spill 目录 |
| 工作区登记项 | 解除绑定、取消置顶、取消归档 | | 工作区登记项 | 解除绑定、取消置顶、取消归档 |
| 会话列表 | 该行立即消失 | | 客户端列表 | 广播 `api-session/removed`,列表行立即消失 |
有意**不**删除: 有意**不**处理:
- **附件**:同一份内容可能被多个会话共用,DSH 没有引用计数,跟着一个会话删除会破坏别的会话; - **附件对象**(`$DSH_HOME/attachments/v1/objects`):按内容 sha256 寻址,同一份内容可能被多个会话共享,而 DSH 没有引用计数,跟着一个会话删除会破坏别的会话;
- **派生缓存**:由日志生成,读取时带会话身份校验,不会串到别的会话。 - **投影缓存**(`$DSH_HOME/storages/session_projcache`):由日志派生的缓存,读取时带生命周期身份校验,永远不会串到别的会话。
### 2. 清理临时文件 ### 2. 清理临时文件
**设置 → 插件 → 会话彻底删除**,打开插件页就能看到。它清理的是**孤儿**临时文件——那些属于已经不存在会话的工具输出溢出目录。判定偏保守: **设置 → 插件 → 会话彻底删除**(点会话彻底删除那一行右侧的配置按钮),清理 spill 后端留下的**孤儿**目录。判定偏保守:
1. 只认 DSH 生成溢出目录时的固定命名(`session-` 加会话 id 哈希的前 12 位),形状不符的一概不动; 1. 目录名必须精确等于 `session-<sha256(会话 id) 前 12 位>`——这是 spill 后端唯一会生成的形状;
2. 该名字不属于任何已知会话(磁盘上的与正在运行的都算); 2. 该名字不属于任何已知会话(`sessionQuery.listSessions()` 已经把存量会话与内存中正在运行的会话合并成同一份语料,所以「已知」只需要一个来源);
3. 只在 DSH 的临时目录范围内操作; 3. 只在 spill 的 root 内操作(后端当前 root + 系统临时目录下的 `dsh-spill*`);
4. 符号链接一律跳过、不跟随;单项失败只记警告。 4. 符号链接一律跳过、不跟随;单项失败只记警告。
结果直接显示在页面上(清理了几个目录、几个文件、释放多少空间),可反复点击。 结果直接显示在该页(清理了几个目录、几个文件、释放多少空间),可反复点击。
这个页面占的是 `plugins.row.config` 槽位——和官方插件「终端」的配置页是同一个位置。注册了这个槽位的 key(`<包名>#<行 id>`)之后,插件行就会长出配置入口,所以本插件不声明 `Config` 也能有自己的页面。
## 行为与边界 ## 行为与边界
- **仍在运行的会话**无法被插件从内存里移除:它会先被归档并停止,日志与临时文件在**下次启动 DSH 时**删除(界面会明确提示);在此之前取消归档就会取消这次删除。 - **仍在运行的会话**无法被插件从内存里移除:它会先被归档并停止,日志与溢出文件在**下次启动 DSH 时**删除(界面会明确提示);在此之前取消归档就会取消这次删除。
- 删除**不可恢复**,但**不是安全擦除**:按普通文件删除处理,底层介质上可能有残留。 - 删除**不可恢复**,但**不是安全擦除**:按普通文件删除处理,底层介质上可能有残留。
- 工作区里的代码文件不受影响。 - 工作区里的代码文件不受影响。
- 临时文件被删除后,fork 出来的会话日志里若仍保留指向它的路径文本,那些路径会失效。 - 溢出文件被删除后,fork 出来的会话日志里若仍保留指向它的路径文本,那些路径会失效。
- 指针停在会话行上时,删除按钮的气泡可能和会话行自带的悬停预览卡同时出现。 - 行内按钮的悬停气泡由按钮自身定位。指针停在会话行上时,行自己的悬停预览卡可能和这个气泡同时出现——插件不会去动宿主的行 DOM 来避免这一点(那是插件不该依赖的自有渲染结构)。若观感不能接受,可把气泡改为只在键盘 focus 时显示:删掉 `DeleteSessionRowButton` 里的 `onMouseEnter: show` / `onMouseLeave: hide` 两行即可。
## 安装 ## 安装
@@ -59,24 +61,28 @@ DSH 的 **插件 → 添加插件** 里有两个输入,含义不同:
在上面的输入框里填: 在上面的输入框里填:
```text ```text
https://gitea.iwake.top/dsh-plugin/session-delete.git#v2.0.2 https://gitea.iwake.top/dsh-plugin/session-delete.git#v2.0.0
``` ```
`#` 后面可以跟标签或提交,用来锁定版本;不写则取默认分支。仓库是公开的,不需要凭据,也不需要改安装源。 `#` 后面可以跟标签或提交,用来锁定版本;不写则取默认分支。仓库是公开的,不需要凭据,也不需要改安装源。
想跟随 2.x 的最新版本,可以把后缀写成 `#semver:^2.0.2`——以后发了新版本,移除旧版本再装一次就能拿到。 想跟随 2.x 的最新版本,可以把后缀写成 `#semver:^2.0.0`——pnpm 会按仓库里的标签挑最高的 2.x,以后发了 v2.0.1,移除旧版本再装一次就能拿到。
### 2. 本地 tgz ### 2. 本地 tgz
把仓库打包成 tgz(得到 `dsh-plugin-session-delete-2.0.2.tgz`),把它在运行 DSH 那台机器上的绝对路径填进上面的输入框。 把仓库打包成 tgz(例如在仓库目录执行 `npm pack`,得到 `dsh-plugin-session-delete-2.0.0.tgz`),把它在运行 DSH 那台机器上的绝对路径填进上面的输入框。
### 升级 ### 升级
DSH 暂不支持插件自动更新,升级就是用新标签重装一次(建议先移除旧版本)。装好后刷新页面即可生效。 DSH 暂不支持插件自动更新,升级就是用新标签重装一次(建议先移除旧版本)。安装或升级后:**宿主半变更需要重启 DSH**(完全退出再打开,仅关闭窗口不一定退出进程);只改浏览器半时刷新页面即可。
## 兼容性 ## 兼容性
基于 **DeepSeek Harness 0.2.0-rc.2** 编写,其它版本与平台组合未逐一验证。 基于 **DeepSeek Harness 0.2.0-rc.2** 编写。DSH 仍在演进,其它版本与平台组合未逐一验证。
- **浏览器半**不 import 任何 Harness 客户端包(上游规范如此要求:这些包随时可能变化,渲染抛错会让整个插槽条目变空白),而是复刻 `dsh-client-ui-primitives` 的 markup、CSS 声明、图标 path 数据与键盘行为,只保留 `--dsw-*` 主题 token 引用。它**不读写宿主的 DOM**,也不占用官方槽位:行内按钮用自己的 id 与官方「归档」「置顶」并列,不是顶替。上游若改动这些内部结构,界面细节可能失配(例如悬停气泡不再出现),但不会导致插件加载失败。
- **宿主半**只依赖稳定的服务契约:`sessionQuery`(`traceSession` 取血缘、`listSessions` 取语料)、`sessionPersistence`(含其 JSONL 后端的 `locate()` 诊断钩子,用于定位会话目录)、`workspaceRegistry`、`connection.fetch`(注册受认证的 exact 路由),以及 spill 后端的目录规则。
- **跨半边通信**走 `connection.fetch` 注册的 `POST /api/plugin/session-delete/delete` 与 `.../orphans` 两条路由,浏览器半用文档相对路径请求(支持挂载前缀)。这是静态客户端包与宿主半通信的正规通道;`host.call` 属于动态客户端运行器,不适用于包形式的插件。
## License ## License
+24 -26
View File
@@ -8,14 +8,11 @@
* *
* The temporary-file sweep is the plugin's other operation, and it lives on * The temporary-file sweep is the plugin's other operation, and it lives on
* this plugin's own page under Settings → Plugins: the entry registers into * this plugin's own page under Settings → Plugins: the entry registers into
* `plugins.bundle.config` under this package's name, the page-level seat the * `plugins.row.config` under the `<package name>#<row id>` key the manager
* manager renders on a bundle's page between its description and its rows. * dispatches, which also gives the row its configure control. That is the same
* * seat a bundled plugin's configuration form occupies, so the plugin's only
* That page-level seat is deliberate, not the row-level `plugins.row.config` * control sits beside everything else the manager says about it instead of
* one. The row seat grows a configure control on the row listed under * taking a row in General settings.
* 「包含的组件」 and nothing else, so it parks this plugin's one and only control
* one click deeper than the page the user just opened; the page seat puts it
* where the page is read.
* *
* Two client-authoring rules shape this file: * Two client-authoring rules shape this file:
* *
@@ -47,13 +44,15 @@ window.__ModuleLoader__.load({
/** Between the shipped `archive` (100) and `pin` (200) hover buttons. */ /** Between the shipped `archive` (100) and `pin` (200) hover buttons. */
const ROW_ACTION_ORDER = 150 const ROW_ACTION_ORDER = 150
/** /**
* The key `plugins.bundle.config` is dispatched under: the manager keys a * The key `plugins.row.config` is dispatched under: the manager composes it
* bundle's own configuration by its npm package name and renders the entry * as `<package name>#<row id>`, with the row id read from this bundle's own
* on that bundle's page, between the description and the rows. The value is * patch. Both halves are literals from `package.json` and
* the literal from `package.json` — a typo costs the section silently, so * `cordis.patch.yml`; a typo costs the configure control silently, so they
* it is the only place renaming has to reach. * are the only two places renaming has to reach.
*/ */
const PACKAGE = '@dsh-plugin/session-delete' const PACKAGE = '@dsh-plugin/session-delete'
const ROW_ID = 'session-delete'
const CONFIG_KEY = `${PACKAGE}#${ROW_ID}`
const TOOLTIP_DELAY_MS = 500 const TOOLTIP_DELAY_MS = 500
const TOOLTIP_GAP = 8 const TOOLTIP_GAP = 8
/** Document base captured at bundle registration, before any routing. */ /** Document base captured at bundle registration, before any routing. */
@@ -332,12 +331,12 @@ window.__ModuleLoader__.load({
* This plugin's page under Settings → Plugins: sweep spilled tool output * This plugin's page under Settings → Plugins: sweep spilled tool output
* whose conversation no longer exists. * whose conversation no longer exists.
* *
* The owner dispatches the `page` view only: a bundle's configuration has * The owner asks every `plugins.row.config` entry for two views. `summary`
* no one-line form, because the page header already prints this package's * is the one-liner on the row, and this plugin's manifest description
* title and description from the manifest. So the section opens with the * already fills that seat, so `summary` renders nothing and the page keeps
* operation's own name and the rule it applies — what the header cannot say * the text the manager read from `locale/*.json`. `page` is the section the
* — and the guard below keeps rendering nothing if a `summary` view ever * manager renders with its own heading, breadcrumb, and title; copy,
* arrives. * control, and result belong to this entry.
*/ */
function TempCleanupPanel(props) { function TempCleanupPanel(props) {
const t = translate(props.t) const t = translate(props.t)
@@ -651,13 +650,12 @@ window.__ModuleLoader__.load({
}), }),
}, DeleteSessionDialog)) }, DeleteSessionDialog))
// The cleanup control belongs on this plugin's own page itself, not // The cleanup control belongs on this plugin's own page: registering
// behind the component row listed under 「包含的组件」: // this cell key is what makes the row grow a configure control, and the
// `plugins.bundle.config` keyed by this package's name is the seat the // manager renders the entry for both its summary and page views.
// manager renders between the page's description and its rows. ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({ name: 'plugins.row.config',
name: 'plugins.bundle.config', key: CONFIG_KEY,
key: PACKAGE,
locale: NS, locale: NS,
inject: () => ({ cleanOrphans }), inject: () => ({ cleanOrphans }),
}, TempCleanupPanel)) }, TempCleanupPanel))
+2 -5
View File
@@ -1,9 +1,9 @@
{ {
"name": "@dsh-plugin/session-delete", "name": "@dsh-plugin/session-delete",
"version": "2.0.2", "version": "2.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Permanently delete a conversation: stop it, remove its session log, subagent logs, and spilled tool output, and drop every registry reference. The plugin's own page under Settings → Plugins can sweep orphaned temporary files.", "description": "Permanently delete a conversation: stop it, remove its session log, subagent logs, and spilled tool output, and drop every registry reference. Settings → General can sweep orphaned temporary files left by conversations that are gone.",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -28,9 +28,6 @@
"icon.svg", "icon.svg",
"locale/*.json", "locale/*.json",
"README.md", "README.md",
"CONTRIBUTING.md",
".gitmessage",
"scripts/**/*.mjs",
"LICENSE" "LICENSE"
], ],
"engines": { "engines": {
-88
View File
@@ -1,88 +0,0 @@
// Checks existing commits against the CONTRIBUTING.md spec. The spec allows one
// line only, so a subject must both parse (emoji + type + Chinese description)
// and be the whole message: any body or footer is reported too.
//
// This is a reporting tool, not a hook: it never blocks a commit.
//
// Usage:
// node scripts/check-commit-log.mjs # newest 10 commits
// node scripts/check-commit-log.mjs 50 # newest 50 commits
// node scripts/check-commit-log.mjs all # every commit
// DSD_SPEC=path/to/CONTRIBUTING.md node scripts/check-commit-log.mjs
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const specPath = process.env.DSD_SPEC ?? resolve(repoRoot, 'CONTRIBUTING.md')
const doc = readFileSync(specPath, 'utf8')
const count = process.argv[2] ?? '10'
/** The regex the spec documents for a subject line. */
const SUBJECT = new RegExp(
'^(?:\\p{Extended_Pictographic}\\uFE0F?\\s+)?' +
'(?<type>[a-z]+)' +
'(?:\\((?<scope>[^)]*)\\))?' +
'(?<breaking>!)?' +
':\\s(?<desc>.+)$',
'u',
)
// The spec's table is the single source of truth for the type → emoji mapping,
// so a table edit cannot drift from what this script checks.
const emojiOfType = new Map()
for (const row of doc.split('\n').filter((line) => /^\| `[a-z]+` \| /.test(line))) {
const cells = row.split('|').map((cell) => cell.trim())
emojiOfType.set(cells[1].replaceAll('`', ''), cells[2])
}
if (emojiOfType.size === 0) throw new Error(`${specPath} has no type table to read`)
/** Strip the variation selector, so '♻' and '♻️' compare equal. */
const base = (text) => text.replace(/\uFE0F/g, '')
/** Display width, counting anything above U+2000 as double. */
const widthOf = (text) => [...text].reduce((n, c) => n + (c.codePointAt(0) > 0x2000 ? 2 : 1), 0)
// %B is the raw message body, so a record can hold several lines: a record ends
// at the next hash that starts a line, which is what this splits on.
const range = count === 'all' ? [] : [`-${Number(count)}`]
const raw = execFileSync('git', ['log', ...range, '--format=%h%x09%B%x00'], {
encoding: 'utf8',
cwd: repoRoot,
})
const commits = raw.split('\0').map((chunk) => chunk.replace(/^\n+/, '')).filter((chunk) => chunk.trim() !== '')
let problems = 0
for (const chunk of commits) {
const tab = chunk.indexOf('\t')
const hash = chunk.slice(0, tab)
const message = chunk.slice(tab + 1).replace(/\n+$/, '')
const lines = message.split('\n')
const subject = lines[0]
const extra = lines.slice(1).filter((line) => line.trim() !== '')
const notes = []
const match = SUBJECT.exec(subject)
if (match === null) {
notes.push('首行缺少类型前缀,或整体格式不是 <emoji> <type>[(scope)][!]: <描述>')
} else {
const expected = emojiOfType.get(match.groups.type)
const lead = [...subject][0]
if (expected === undefined) notes.push(`类型不在规范表里:${match.groups.type}`)
else if (base(lead) !== base(expected)) notes.push(`类型 ${match.groups.type} 的 emoji 应为 ${expected},实际是 ${lead}`)
const width = widthOf(subject)
if (width > 72) notes.push(`首行宽度 ${width},超过 72`)
if (/[。.]$/.test(subject)) notes.push('首行结尾不应有句号')
}
if (extra.length > 0) notes.push(`带了 ${extra.length} 行正文或脚注,规范要求只有一行`)
if (notes.length > 0) problems += 1
const mark = notes.length === 0 ? 'ok ' : 'FAIL'
console.log(` ${mark} ${hash} ${notes.length === 0 ? '' : notes.join(';') + ' — '}${subject}`)
}
console.log(problems === 0
? `\n检查了 ${commits.length} 条提交,全部符合规范`
: `\n检查了 ${commits.length} 条提交,其中 ${problems} 条不符合规范(规范文件:${specPath})`)
process.exit(problems === 0 ? 0 : 1)