dsh-session-manager 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.zh.md CHANGED
@@ -1,166 +1,137 @@
1
- # dsh-session-manager — DeepSeek Harness 会话管理器
2
-
3
- [English](README.md) | 中文
4
-
5
- [![npm version](https://img.shields.io/npm/v/dsh-session-manager)](https://www.npmjs.com/package/dsh-session-manager)
6
- [![GitHub](https://img.shields.io/badge/GitHub-仓库-blue)](https://github.com/hkkz9522/dsh-session-manager)
7
- [![CI](https://github.com/hkkz9522/dsh-session-manager/actions/workflows/ci.yml/badge.svg)](https://github.com/hkkz9522/dsh-session-manager/actions/workflows/ci.yml)
8
- [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
9
-
10
- DSH Web 会话管理:删除、归档、跨工作区移动、迁移预设;收藏、待看、搜索、排序、设置优先级、添加(手动/半自动)标签和备注。欢迎至 GitHub 提意见。
11
-
12
- ## 功能
13
-
14
- ### 会话生命周期
15
-
16
- - **归档 / 移出归档**会话。
17
- - **删除会话**:不可逆操作的二次确认。subagent 会话和临时空白会话占位会被拒绝。
18
- - **移动至工作区**:保留历史、标题、归档状态和派生会话关系,同时把会话的 `cwd` 更新为目标工作区。移动会就地更新 live writer 的 header,使挂起的工具调用继续落到新路径。
19
- - **迁移 Agent 预设**:按需修改。典型工况:当原预设被改名或删除,导致会话无法恢复时,可修复该会话。迁移会就地重写最后一条 `agent-preset/selected` 事件(若从未记录选择事件则修改会话 header),不会改写历史消息。
20
-
21
- ### 会话管理窗口(侧边栏)
22
-
23
- - 浏览未归档和已归档会话、切换工作区、在列表中筛选 / 排序 / 搜索。
24
- - 直接从某一行打开会话,或点击标签直接按该标签筛选。
25
- - 每行操作:**打开**、**归档 / 移出归档**、**移动**、**迁移预设**、**删除**。
26
- - 各弹窗(移动 / 迁移预设 / 删除 / 面板本身)在触发按钮再次点击时会切换关闭,与标题栏原生按钮的行为一致。
27
-
28
- ### 搜索、筛选和排序
29
-
30
- - 不区分大小写的标题和会话 ID 搜索,自动去除首尾空格;不读取聊天历史。
31
- - 工作区下拉(全部 / 未分组 / 具体工作区)可与归档状态筛选(全部 / 未归档 / 已归档)叠加。
32
- - 可叠加收藏 / 待回看、标签和优先级筛选;排序支持最近更新(默认)、最早更新、最新创建、最早创建以及**优先级(1 → 5)**。
33
- - 显示匹配 / 总数,并提供"重置筛选"。这些控件只影响管理窗口,不改变会话归属、归档状态或原生侧边栏顺序。
34
- - 工作区加载失败时可重试,标题 / ID 搜索和更新时间排序仍可使用。
35
-
36
- ### 收藏、待回看、标签、备注与优先级
37
-
38
- - **标题栏**(当前会话)和 **管理窗口**(每一行)都提供收藏 / 待回看 / **标签 / 备注** / 优先级操作入口。
39
- - 收藏是长期标记,待回看是手动提醒;不会随归档或会话结束自动清除。
40
- - 优先级下拉:**1 最高、2 高、3 普通、4 低、5 最低**,**默认 3(普通)**;管理行 / 标题栏始终显示 P1–P5 徽标。旧数据中的 `null` 优先级归一化为 3。
41
- - 标签:每个会话最多 20 个,每个最多 32 字符;英文 `,` 与中文 `,` 都是分隔符,首尾空白被去除,重复标签按大小写不敏感合并。
42
- - 备注:最多 2000 字符的多行纯文本。
43
- - 标签、备注、AI 粘贴三个输入框使用相同的 `sm-noteInput` 样式与 `rows: 3` 高度(60px min-height),三个字段在视觉上对齐。
44
- - "标签 / 备注" 编辑窗口内还提供 **复制 Prompt** / **导入** 两个按钮,用于 AI 辅助整理(见下)。
45
- - 标记明文保存在 DSH home 下的 `dsh-session-manager/annotations.v1.json`,按会话 ID 关联;不写入 JSONL/Zstd 历史,也不自动发送给模型。移动 / 迁移预设会保留标记;删除会话后会清理对应标记(清理失败会单独提示)。
46
- - 标题栏和管理窗口实时共享状态;同源浏览器标签页通过 `BroadcastChannel` 通知同步,重新获得焦点或打开管理窗口也会刷新数据。
47
- - 保存采用原子写入并使用跨进程锁;版本冲突时保留草稿,要求显式"载入最新内容"。保存失败不会关闭编辑窗口或丢弃草稿。
48
- - 异常退出遗留的 `annotations.v1.lock` 不会被自动强行删除;应在确认没有进程写入后再处理。
49
-
50
- ### AI 整理(手动、可选)
51
-
52
- **标签/备注** 编辑窗口在标准"取消 / 保存标记"按钮之外,还多了两个按钮,位于粘贴输入框上方。两者都不会自动调用模型,是否发送完全由你决定:
53
-
54
- - **复制 Prompt** / **Copy Prompt**:把结构化 Prompt(中文或英文,跟随当前界面语言)复制到剪贴板。粘贴到当前对话中,要求模型按本插件的限制生成标签 / 备注 / 优先级(最多 20 个标签、每个 ≤ 32 字符、备注 ≤ 2000 字符、优先级 1–5 默认 3)。
55
- - **导入** / **Import**:读取剪贴板,提取首个 JSON 对象(可识别 Markdown 代码块、对话包裹、智能引号、孤立反斜杠和开头 BOM),按相同规则校验后填入编辑窗口。如果当前有未保存的修改,会先询问是否覆盖再继续。超限的标签会被丢弃、超长的备注会被截断,所有调整都会在状态消息中列出,确认后再保存。如果仍然解析失败,错误信息会附带每一次修复尝试中 `JSON.parse` 给出的具体位置(原始 / 修复引号反斜杠 / 扫描对象 / 扫描对象+修复),方便定位坏掉的字符。
56
-
57
- Prompt 模板和导入解析逻辑位于 `lib/clipboard-parser.js`,直接打包进客户端,无需额外构建步骤,也不会发起任何网络请求。
58
-
59
- ### 当前会话标题栏
60
-
61
- 标题栏右侧提供:
62
-
63
- - **归档 / 移出归档** 当前会话。
64
- - **移动至工作区**,弹窗选择目标工作区。
65
- - 红色的 **删除会话** 按钮,带确认。
66
-
67
- 同样的按钮在管理窗口每一行也可用。
68
-
69
- ## UI 入口
70
-
71
- - **会话标题右侧**:归档 / 移出归档、移动至工作区、删除会话。
72
- - **侧边栏底部 → 会话管理**:查看全部会话(含归档会话)并操作每一条会话。
73
-
74
- ## Agent 预设迁移
75
-
76
- 1. 打开**会话管理**。
77
- 2. 找到目标会话,点击**迁移预设**。
78
- 3. 从当前可用的预设中选择目标预设并确认。
79
-
80
- 例如:当会话无法恢复,报错表明原 Agent 预设不存在时(例如删掉了 `router-standard`),可以使用迁移功能。
81
-
82
- 插件会读取最后一条 `agent-preset/selected` 事件中的有效预设(若不存在则读取会话 header)。冷会话会重写最后一条选择事件;从未记录选择事件时修改 header。正常的 live session 通过 `Session.append()` 追加选择事件,再通过 `SessionStore.flush()` 刷到磁盘;api-gateway 的聊天面板在下次事件折叠时即可看到新预设。
83
-
84
- > 迁移预设只会修改会话元数据,不会改写历史消息、文件或当前工作区。
85
-
86
- ## 安装
87
-
88
- 本插件已收录于 [dsh-market](https://github.com/dsh-market/dsh-market) 和 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin),可以通过 DSH 应用内的**插件市场**直接搜索安装。
89
-
90
- ### 从 dsh-market 安装
91
-
92
- ```powershell
93
- dsh plugin --profile web add npm:dsh-session-manager
94
- ```
95
-
96
- ### 从 GitHub 安装
97
-
98
- ```powershell
99
- dsh plugin --profile web add github:hkkz9522/dsh-session-manager
100
- ```
101
-
102
- 安装后重启 DSH Web。若浏览器仍加载旧的客户端代码,请使用 `Ctrl+Shift+R` 强制刷新。
103
-
104
- ### 本地开发 / 运行时注入
105
-
106
- ```text
107
- dev_inject_plugin {"dir": "<本仓库的绝对路径>"}
108
- ```
109
-
110
- ## 安全与行为说明
111
-
112
- - **删除不可恢复**,因此界面始终要求确认。删除前校验会话 ID、目录边界和工件 header;不允许通过路径穿越、符号链接或 junction 操作其他目录。
113
- - 移动和迁移预设保留 live session / agent;删除才会取消运行并释放会话。移动会更新保存的 cwd 和 live writer 的 header。
114
- - 会话管理列表隐藏 subagent 会话,移动接口也拒绝 subagent;尚未落盘的空白会话不能跨工作区移动。
115
- - 冷会话重写保留原工件格式,不强制升级 v2 → v3。损坏或截断的 Zstd 日志、缺少完整尾行的 JSONL 会拒绝移动/重写,不会把部分历史当作完整日志保存。
116
- - 迁移预设按“备份 → 发布 → 回滚”分阶段处理。如果回滚失败,会保留恢复文件并在错误中报告路径;不要删除这些文件。
117
- - 启动扫描不完整时跳过工作区归属修复;完整扫描也不会清除仍在内存中或扫描期间新加入的会话。
118
- - 同一会话的插件写操作按顺序执行;请求体限制为 64 KiB。该队列不替代 DSH 自身的持久化写入协调。
119
-
120
- ## 兼容性
121
-
122
- | 插件版本 | 已验证 DSH 版本 |
123
- | ------ | ------------- |
124
- | 0.5.1 | v0.1.6-alpha.2 |
125
- | 0.4.11 | v0.1.5-rc.2 |
126
- | 0.4.10 | v0.1.5-rc.1 |
127
- | 0.4.9 | v0.1.5-rc.1 |
128
- | 0.4.7 | v0.1.5-rc.1 |
129
- | 0.4.4 | 0.1.3-alpha.2 |
130
- | 0.4.1 | 0.1.3-alpha.2 |
131
- | 0.4.0 | v0.1.2-rc.1 |
132
- | 0.1.2 | v0.1.0-rc.7 |
133
- | 0.1.1 | v0.1.0-rc.7 |
134
- | 0.1.0 | v0.1.0-rc.7 |
135
-
136
- 运行时要求 Node.js 22.15+(22.x)或 24+,以提供内置 Zstd 支持。
137
-
138
- 本插件是 Cordis 插件,peer dependency 为 `cordis: ">=4.0.0-rc <5"`。
139
-
140
- ## 开发
141
-
142
- - `lib/index.js` 是 host 端 ESM 插件,`lib/client.js` 是 Web 客户端 bundle,无需构建步骤。
143
- - 提交修改前请运行:
144
-
145
- ```powershell
146
- npm run check
147
- npm test
148
- npm run check:package
149
- git diff --check
150
- ```
151
-
152
- 测试使用隔离临时目录和真实插件入口,不操作真实会话。CI 在 Windows/Linux、Node 22.15.0/24 上执行相同检查。
153
-
154
- 可选集成检查:在 DSH Web 已运行的测试环境中执行 `node scripts/smoke-test.mjs`;它会请求实际服务,不属于默认单元测试。
155
-
156
- 发布记录见 [CHANGELOG.md](CHANGELOG.md)。
157
-
158
- ## 致谢
159
-
160
- 感谢每一位安装和使用 dsh-session-manager 的用户,也感谢提交 Issue 与 Pull Request 帮助改进本插件的朋友们。
161
-
162
- 本插件已被 [dsh-market](https://github.com/dsh-market/dsh-market) 和 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 收录。欢迎提出修改意见。
163
-
164
- ## 开源许可
165
-
166
- [MIT](LICENSE)
1
+ # dsh-session-manager — DeepSeek Harness 会话管理器
2
+
3
+ [English](README.md) | 中文
4
+
5
+ [![npm version](https://img.shields.io/npm/v/dsh-session-manager)](https://www.npmjs.com/package/dsh-session-manager)
6
+ [![GitHub](https://img.shields.io/badge/GitHub-仓库-blue)](https://github.com/hkkz9522/dsh-session-manager)
7
+ [![CI](https://github.com/hkkz9522/dsh-session-manager/actions/workflows/ci.yml/badge.svg)](https://github.com/hkkz9522/dsh-session-manager/actions/workflows/ci.yml)
8
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
9
+
10
+ ## 0 简介
11
+
12
+ DSH Web 会话管理:删除、归档、跨工作区移动、迁移预设;收藏、待看、搜索、排序、设置优先级、添加标签和备注;批量处理。欢迎至 GitHub 提意见。
13
+
14
+ ## 1 功能
15
+
16
+ ### 1.1 会话生命周期管理
17
+
18
+ - **删除**:不可逆操作,UI 始终要求二次确认。subagent 会话和尚未落盘的空白会话占位不能删除。
19
+ - **归档 / 移出归档**:把会话移出或移回主列表,不删除磁盘内容。
20
+ - **移动至工作区**:跨工作区移动时保留历史、标题、归档状态和派生会话关系,同时把 `cwd` 重写为目标工作区,并就地更新 live writer 的 header,使挂起的工具调用继续落到新路径。
21
+ - **迁移 Agent 预设**:原预设被改名或删除导致会话无法恢复时,可修复该会话。迁移会就地重写最后一条 `agent-preset/selected` 事件(若从未记录则修改会话 header),不改动历史消息。
22
+
23
+ ### 1.2 会话快捷管理
24
+
25
+ - **收藏 / 待回看**:长期标记和手动提醒;不会随归档或会话结束自动清除。
26
+ - **搜索**:按标题、会话 ID、备注、标签不区分大小写匹配,自动去除首尾空格,不读取聊天历史。
27
+ - **筛选与排序**:工作区(全部 / 未分组 / 具体)与归档状态(全部 / 未归档 / 已归档)可叠加;可叠加收藏 / 待回看、标签和优先级筛选;排序支持最近更新(默认)、最早更新、最新创建、最早创建以及**优先级(1 → 5)**。
28
+ - **优先级**:下拉 **1 最高、2 高、3 普通、4 低、5 最低**,**默认 3(普通)**;旧数据中的 `null` 归一化为 3。
29
+ - **标签 / 备注**:每会话最多 20 个标签(每个 ≤ 32 字符)、备注最多 2000 字符。英文 `,` 与中文 `,` 都是分隔符,首尾空白被去除,重复标签按大小写不敏感合并。
30
+ - **AI 整理(手动、可选)**:标签 / 备注编辑窗口内的 **复制 Prompt** 把结构化提示复制到剪贴板,**导入** 解析剪贴板 JSON(可识别 Markdown 代码块、对话包裹、智能引号、孤立反斜杠和开头 BOM),按相同规则校验后填入字段;两者都不会自动调用模型。
31
+ - 标记保存在 `dsh-session-manager/annotations.v1.json`,按会话 ID 关联;同源浏览器标签页通过 `BroadcastChannel` 同步;保存可跨进程崩溃恢复,版本冲突会提示"载入最新内容"。
32
+
33
+ ### 1.3 批量处理
34
+
35
+ - **批量模式入口**:在会话管理窗口顶部点击 **批量处理** 切换按钮,行左侧出现复选框,工具栏出现 **全选当前筛选 / 清空选择** 和 **批量按钮区**;退出批量模式会清空当前选中。
36
+ - **批量按钮区** 列出所有批量动作:
37
+ - **标记切换**:**归档 / 取消归档 / 收藏 / 取消收藏 / 待回看 / 取消待看**。
38
+ - **变更操作**:**添加标签 / 清空标签 / 设置优先级 / 移动至工作区 / 迁移预设 / 删除会话**。
39
+ - **执行流程**:非破坏性操作(归档 / 取消归档 / 收藏 / 取消收藏 / 待回看 / 取消待看 / 添加标签 / 清空标签 / 设置优先级 / 移动至工作区 / 迁移预设)立即执行,结果按会话逐条展示在 **结果对话框** 的 **成功 / 失败 / 跳过** 分组里,并提供 **重试失败项** 一键把失败 ID 重新加入选中;破坏性操作(**删除会话**)先弹 **预览对话框** 列出受影响的会话,再显示进度条,最后给出逐条结果。
40
+
41
+ ## 2 UI入口
42
+
43
+ ### 2.1 标题栏入口
44
+
45
+ 标题栏右侧对**当前会话**提供:**归档 / 移出归档**、**标签 / 备注**、**移动至工作区**、**删除会话**。
46
+
47
+ ### 2.2 会话管理入口与界面
48
+
49
+ 从 DSH 侧边栏底部进入 **会话管理**,可浏览全部会话、切换工作区、按标题 / ID / 备注 / 标签搜索、应用筛选与排序,并对每条会话执行 **打开 / 归档 / 移出归档 / 标签 / 备注 / 移动 / 迁移预设 / 删除** 操作。窗口顶部承载工作区选择、归档筛选、收藏 / 待回看、标签、优先级筛选与排序控件,以及匹配 / 总数计数和"重置筛选"。
50
+
51
+ ### 2.3 批量管理入口
52
+
53
+ 会话管理窗口顶部的 **批量处理** 按钮即是入口:点一下进入批量模式,行左侧出现复选框,工具栏出现 **全选当前筛选 / 清空选择** 和 **批量按钮区**;再点一次退出批量模式。
54
+
55
+ ## 3 安装
56
+
57
+ ### 3.1 从官方插件管理入口安装
58
+
59
+ 进入 DSH 应用内的 **插件管理**,搜索 `dsh-session-manager` 并安装。
60
+
61
+ ### 3.2 从 dsh-market 安装
62
+
63
+ ```powershell
64
+ dsh plugin --profile web add npm:dsh-session-manager
65
+ ```
66
+
67
+ ### 3.3 从 GitHub 安装
68
+
69
+ ```powershell
70
+ dsh plugin --profile web add github:hkkz9522/dsh-session-manager
71
+ ```
72
+
73
+ 安装后重启 DSH Web;若浏览器仍加载旧的客户端代码,请使用 `Ctrl+Shift+R` 强制刷新。
74
+
75
+ ### 3.4 本地开发 / 运行时注入
76
+
77
+ ```text
78
+ dev_inject_plugin {"dir": "<本仓库的绝对路径>"}
79
+ ```
80
+
81
+ ## 4 安全说明
82
+
83
+ - **删除不可恢复**,UI 始终要求二次确认;删除前校验会话 ID、目录边界和工件 header,不允许通过路径穿越、符号链接或 junction 操作其他目录。
84
+ - 移动和迁移预设保留 live session / agent;只有删除才会取消运行并释放会话。移动会更新保存的 cwd 和 live writer 的 header。
85
+ - 会话管理列表隐藏 subagent 会话,移动接口也拒绝 subagent;尚未落盘的空白会话不能跨工作区移动。
86
+ - 冷会话重写保留原工件格式(V1 / V2 / V3 / V4 都可读,绝不强制升级)。损坏或截断的 Zstd 日志、缺少完整尾行的 JSONL 会拒绝移动 / 重写,不会把部分历史当作完整日志保存。
87
+ - 迁移预设按"备份 → 发布 → 回滚"分阶段处理。如果回滚失败,会保留恢复文件并在错误中报告路径;不要删除这些文件。
88
+ - 启动扫描不完整时跳过工作区归属修复;完整扫描也不会清除仍在内存中或扫描期间新加入的会话。
89
+ - 同一会话的插件写操作按顺序执行;请求体限制为 64 KiB。该队列不替代 DSH 自身的持久化写入协调。
90
+
91
+ ## 5 兼容性
92
+
93
+ | 插件版本 | 已验证 DSH 版本 |
94
+ | ------ | ------------- |
95
+ | 0.5.2 | v0.1.7-rc.1 |
96
+ | 0.5.1 | v0.1.6-alpha.2 |
97
+ | 0.4.11 | v0.1.5-rc.2 |
98
+ | 0.4.10 | v0.1.5-rc.1 |
99
+ | 0.4.9 | v0.1.5-rc.1 |
100
+ | 0.4.7 | v0.1.5-rc.1 |
101
+ | 0.4.6 | 0.1.3-alpha.2 |
102
+ | 0.4.4 | 0.1.3-alpha.2 |
103
+ | 0.4.1 | 0.1.3-alpha.2 |
104
+ | 0.4.0 | v0.1.2-rc.1 |
105
+ | 0.1.2 | v0.1.0-rc.7 |
106
+ | 0.1.1 | v0.1.0-rc.7 |
107
+ | 0.1.0 | v0.1.0-rc.7 |
108
+
109
+ 运行时要求 Node.js 22.15+(22.x)或 24+,以提供内置 Zstd 支持。
110
+
111
+ 本插件是 Cordis 插件,peer dependency 为 `cordis: ">=4.0.0-rc <5"`。
112
+
113
+ ## 6 开发
114
+
115
+ - `lib/index.js` 是 host 端 ESM 插件,`lib/client.js` 是 Web 客户端 bundle,无需构建步骤。
116
+ - 提交修改前请运行:
117
+
118
+ ```powershell
119
+ npm run check
120
+ npm test
121
+ npm run check:package
122
+ git diff --check
123
+ ```
124
+
125
+ 测试使用隔离临时目录和真实插件入口,不操作真实会话。CI 在 Windows / Linux、Node 22.15.0 / 24 上执行相同检查。
126
+
127
+ 可选集成检查:在 DSH Web 已运行的测试环境中执行 `node scripts/smoke-test.mjs`;它会请求实际服务,不属于默认单元测试。
128
+
129
+ 发布记录见 [CHANGELOG.md](CHANGELOG.md)。
130
+
131
+ ## 7 致谢
132
+
133
+ 感谢每一位安装和使用 dsh-session-manager 的用户,也感谢提交 Issue 与 Pull Request 帮助改进本插件的朋友们。本插件已被 [dsh-market](https://github.com/dsh-market/dsh-market) 和 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 收录。欢迎提出修改意见。
134
+
135
+ ## 8 开源许可
136
+
137
+ [MIT](LICENSE)
@@ -142,7 +142,47 @@ export function createAnnotationStore(directory, { io = fs, lockWaitMs = 1500 }
142
142
  return next;
143
143
  }));
144
144
  },
145
- async remove(id) {
145
+ batchUpdate(ids, patch, expectedRevisions) {
146
+ const normalized = normalizeAnnotationPatch(patch);
147
+ const idList = Array.from(new Set((Array.isArray(ids) ? ids : []).filter(id => typeof id === 'string' && id.trim() !== '').map(id => id.trim())));
148
+ if (idList.length === 0) throw fail('sessionIds 必须是非空数组');
149
+ if (idList.length > 200) throw fail('单次批量最多 200 条');
150
+ for (const id of idList) assertSessionId(id);
151
+ return enqueue(() => withLock(async () => {
152
+ const state = await read();
153
+ const nextSessions = { ...state.sessions };
154
+ const items = [];
155
+ let success = 0, failed = 0, skipped = 0;
156
+ for (const id of idList) {
157
+ try {
158
+ const previous = Object.hasOwn(nextSessions, id) ? nextSessions[id] : DEFAULT_ANNOTATION;
159
+ const expected = expectedRevisions instanceof Map ? expectedRevisions.get(id) : undefined;
160
+ if (expected !== undefined) {
161
+ if (!Number.isSafeInteger(expected) || expected < 0) throw fail('标记版本号无效');
162
+ if (expected !== previous.revision) throw fail('标记已被其他窗口修改,请载入最新内容后再保存', 'annotation-conflict');
163
+ }
164
+ if (Object.keys(normalized).every(key => JSON.stringify(previous[key]) === JSON.stringify(normalized[key]))) {
165
+ items.push({ sessionId: id, status: 'success', annotation: { ...previous }, unchanged: true });
166
+ success++;
167
+ continue;
168
+ }
169
+ const next = { ...previous, ...normalized, revision: previous.revision + 1, updatedAt: Date.now() };
170
+ if (!Number.isSafeInteger(next.revision)) throw fail('标记版本号已超出范围', 'annotations-unavailable');
171
+ nextSessions[id] = next;
172
+ items.push({ sessionId: id, status: 'success', annotation: next });
173
+ success++;
174
+ } catch (error) {
175
+ const code = error && typeof error.code === 'string' ? error.code : undefined;
176
+ items.push({ sessionId: id, status: 'failed', code, error: error && error.message ? error.message : String(error) });
177
+ failed++;
178
+ }
179
+ }
180
+ const anyChanged = items.some(item => item.status === 'success' && !item.unchanged);
181
+ if (anyChanged) await publish({ version: 1, sessions: nextSessions });
182
+ return { summary: { total: idList.length, success, failed, skipped }, items };
183
+ }));
184
+ },
185
+ async remove(id) {
146
186
  assertSessionId(id);
147
187
  // A plugin that has never used annotations should not create a directory
148
188
  // just because an ordinary session was deleted.