dsh-session-ops 0.1.4 → 0.2.0

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/lib/index.js CHANGED
@@ -43,57 +43,24 @@
43
43
  *
44
44
  * @module dsh-session-ops
45
45
  */
46
-
47
- import z from '@deepseek-ai/schemastery'
48
- import {
49
- LOG_LINE_LEVEL_FIRST,
50
- LOG_LINE_TIME_FIRST,
51
- createFileLogger,
52
- resolveVolatileConfig,
53
- } from './store/host.js'
54
- import {
55
- ADMIN_API_PREFIX,
56
- createSessionAdmin,
57
- isInside,
58
- isTrustedRequest as isTrustedAdminRequest,
59
- mountAdmin,
60
- } from './admin.js'
61
- import {
62
- MOVE_API_PREFIX,
63
- isTrustedRequest as isTrustedMoveRequest,
64
- mountForkMembershipRepair,
65
- mountMove,
66
- sweepForkChildrenOnBoot,
67
- } from './move.js'
68
-
69
- export {
70
- ADMIN_API_PREFIX,
71
- MOVE_API_PREFIX,
72
- createSessionAdmin,
73
- isInside,
74
- isTrustedAdminRequest,
75
- isTrustedMoveRequest,
76
- mountAdmin,
77
- mountForkMembershipRepair,
78
- mountMove,
79
- sweepForkChildrenOnBoot,
80
- }
81
-
46
+ import z from '@deepseek-ai/schemastery';
47
+ import { LOG_LINE_LEVEL_FIRST, LOG_LINE_TIME_FIRST, createFileLogger, resolveVolatileConfig, } from './store/host.js';
48
+ import { ADMIN_API_PREFIX, createSessionAdmin, isInside, isTrustedRequest as isTrustedAdminRequest, mountAdmin, } from './admin.js';
49
+ import { MOVE_API_PREFIX, isTrustedRequest as isTrustedMoveRequest, mountForkMembershipRepair, mountMove, sweepForkChildrenOnBoot, } from './move.js';
50
+ import { messageOf } from './store/util.js';
51
+ export { ADMIN_API_PREFIX, MOVE_API_PREFIX, createSessionAdmin, isInside, isTrustedAdminRequest, isTrustedMoveRequest, mountAdmin, mountForkMembershipRepair, mountMove, sweepForkChildrenOnBoot, };
82
52
  /**
83
53
  * 搬迁半边原来的公开名(`dsh-session-move` 的 `API_PREFIX = '/session-move/api'`)。
84
54
  *
85
55
  * 合并后两半各有一个前缀,明确的名字是 {@link ADMIN_API_PREFIX} / {@link MOVE_API_PREFIX};
86
56
  * 这个别名留着,是为了让按 `dsh-session-move/lib/index.js` 写过 import 的脚本/测试不改也能跑。
87
57
  */
88
- export const API_PREFIX = MOVE_API_PREFIX
89
-
58
+ export const API_PREFIX = MOVE_API_PREFIX;
90
59
  // ---------------------------------------------------------------------------
91
60
  // 插件元信息
92
61
  // ---------------------------------------------------------------------------
93
-
94
62
  /** Cordis 插件名(profile 行 id 是 `session-toolbox`,两者刻意不同名)。 */
95
- export const name = 'dsh-session-ops'
96
-
63
+ export const name = 'dsh-session-ops';
97
64
  /**
98
65
  * 依赖的服务。**这不是可选的**:cordis 里没有声明 inject 就拿不到服务,
99
66
  * `ctx.webServer` / `ctx.tools` / `ctx.sessions` / `ctx.sessionPersistence` 会静默是
@@ -103,15 +70,12 @@ export const name = 'dsh-session-ops'
103
70
  * `sessionPersistence`(会话根与写入句柄)。`workspaceRegistry` / `webServer` / `tools`
104
71
  * 两边都要。
105
72
  */
106
- export const inject = ['webServer', 'tools', 'sessions', 'sessionPersistence', 'workspaceRegistry']
107
-
73
+ export const inject = ['webServer', 'tools', 'sessions', 'sessionPersistence', 'workspaceRegistry'];
108
74
  /** 设置命名空间;与 client 半边 `settings.section` 的 id 必须一致。 */
109
- export const SETTINGS_NS = 'session-toolbox'
110
-
75
+ export const SETTINGS_NS = 'session-toolbox';
111
76
  // ---------------------------------------------------------------------------
112
77
  // 配置(两半合并成一份 schema)
113
78
  // ---------------------------------------------------------------------------
114
-
115
79
  /**
116
80
  * 插件配置(两份原配置的**并集**,键名与默认值见 {@link module:dsh-session-ops} 的表)。
117
81
  *
@@ -127,82 +91,81 @@ export const SETTINGS_NS = 'session-toolbox'
127
91
  * `description` 会随 schema 进入表单描述,是界面文案的权威来源。
128
92
  */
129
93
  export const Config = z.object({
130
- /** 总开关(两半共用)。关掉后两条路由与两个工具都不注册。改它需要重挂本行(非 volatile)。 */
131
- enabled: z.boolean().default(true),
132
- /** 会话根目录;留空则从 `sessionPersistence.root` 推断(jsonl 后端会暴露它)。搬迁半边用。 */
133
- root: z.string().default(''),
134
- /**
135
- * 删除半边的额外信任 authority(`host` 或 `host:port`,按 `Host` 头解析)。
136
- * 原 `dsh-session-admin` 的 `allowHosts`。默认空——只信 loopback。
137
- */
138
- adminAllowHosts: z.array(String).default([]),
139
- /**
140
- * 搬迁半边的额外信任 authority(`host` 或 `host:port`,按 socket 的 `remoteAddress`
141
- * 精确匹配)。原 `dsh-session-move` 的 `allowHosts`。默认空——只信 loopback。
142
- */
143
- moveAllowHosts: z.array(String).default([]),
144
- /** 是否允许删「属于某个工作区」的会话。默认否——工作区会话通常还想留。 */
145
- allowWorkspaceSessions: z.boolean().default(false)
146
- .description('允许删除「属于某个工作区」的会话。默认否——工作区会话通常还想留。')
147
- .volatile(),
148
- /** 是否允许删仍然挂在 Host 进程里的会话(正在浏览/正在跑)。默认否。 */
149
- allowLiveSessions: z.boolean().default(false)
150
- .description('允许删除仍然挂在 Host 进程里的会话(正在浏览或正在跑)。默认否。')
151
- .volatile(),
152
- /** 一次调用最多删多少个,防手滑。 */
153
- maxBatch: z.number().default(200)
154
- .description('一次调用最多删多少个会话,防手滑。默认 200。')
155
- .volatile(),
156
- /**
157
- * 是否在会话标题栏显示「删除当前会话」的垃圾桶图标。**默认开启。**
158
- *
159
- * 形态刻意是「图标 + 二次确认」而不是带字面标签的按钮:标签会招来点击,而这里
160
- * 删的正是你正在用的会话。图标不占字面位置,误点最多弹出一个确认框,离真正删除
161
- * 还隔着一步。清理由右侧「会话管理」面板或设置页承担,那是显式挑选动作。
162
- *
163
- * 这是个 volatile 旋钮:浏览器半边在挂载时问一次 Host(`config` 接口),所以
164
- * 改完**刷新页面**就生效——不需要重启 dsh web,也不需要重挂本插件。
165
- */
166
- headerDeleteButton: z.boolean().default(true)
167
- .description('在会话标题栏显示「删除当前会话」的垃圾桶图标。默认开启;关掉后清理由「会话管理」面板/设置页承担。改完刷新页面生效。')
168
- .volatile(),
169
- /** 是否拒绝搬"正在跑一轮"的会话。默认拒绝——搬运期间它的写入句柄可能落盘。 */
170
- refuseRunning: z.boolean().default(true)
171
- .description('拒绝搬迁「正在跑一轮」的会话。默认开启——搬运期间它的写入句柄可能落盘。')
172
- .volatile(),
173
- /** `list` 一次最多返回多少条会话(浏览器半边还要筛选)。 */
174
- maxSessions: z.number().default(2000)
175
- .description('`list` 一次最多返回多少条会话(浏览器半边还要筛选)。默认 2000。')
176
- .volatile(),
177
- /**
178
- * 项目作用域记忆的条目要不要跟着会话走。**默认 off**。
179
- *
180
- * 项目记忆是**工作区共享**的(一次写入、那个工作区里所有会话都读),所以把一条
181
- * 会话的条目拖到另一个工作区会改变那边的记忆视图——只有"这条会话本来就该在目标
182
- * 工作区"(纠正错放)时才该这么做:
183
- * - `off` :什么都不做。报告里仍给出**只读预览**(若开启会复制哪几条)。
184
- * - `copy` :把该会话产出的条目(frontmatter `session:` 精确匹配)复制到目标
185
- * 工作区,按文件名去重,源工作区不动。
186
- * - `move` :同上,但复制后从源工作区移除(不可逆,谨慎)。
187
- */
188
- memoryFollow: z.string().default('off')
189
- .description('项目记忆跟随:off 只给只读预览(默认 / 推荐);copy 把本会话产出的条目复制到目标工作区;move 复制后从源移除(不可逆)。')
190
- .volatile(),
191
- /**
192
- * 启动时是否做一轮「fork 子会话漏挂工作区」的定向扫描。
193
- *
194
- * 创建期的 `session/created` 补挂只挡住「Host fork 时就没挂」。记账写进去之后
195
- * 又被抹掉的那一类(同机两个 Host 共用 `$DSH_HOME` 时,另一个进程用旧内存快照
196
- * 重写 `storages/workspace.json`)只有启动期扫描能补回来,所以默认开启。
197
- *
198
- * 扫描范围收得很窄:只认带 `parentSession` 的普通 fork 子会话(排除 `origin:
199
- * subagent` 与 `delegationDepth > 0`),且父会话已被某个工作区记账、子会话
200
- * `cwd` 规范化后等于该工作区路径。刻意 ungrouped 的普通会话/迁移会话不碰。
201
- */
202
- repairForkChildrenOnBoot: z.boolean().default(true)
203
- .description('启动时补挂「fork 子会话漏挂工作区」:只处理父会话已入册、cwd 与工作区一致的普通 fork 子会话。默认开启;挂载期读取,改它需要重挂本行。'),
204
- })
205
-
94
+ /** 总开关(两半共用)。关掉后两条路由与两个工具都不注册。改它需要重挂本行(非 volatile)。 */
95
+ enabled: z.boolean().default(true),
96
+ /** 会话根目录;留空则从 `sessionPersistence.root` 推断(jsonl 后端会暴露它)。搬迁半边用。 */
97
+ root: z.string().default(''),
98
+ /**
99
+ * 删除半边的额外信任 authority(`host` 或 `host:port`,按 `Host` 头解析)。
100
+ * 原 `dsh-session-admin` 的 `allowHosts`。默认空——只信 loopback。
101
+ */
102
+ adminAllowHosts: z.array(String).default([]),
103
+ /**
104
+ * 搬迁半边的额外信任 authority(`host` 或 `host:port`,按 socket 的 `remoteAddress`
105
+ * 精确匹配)。原 `dsh-session-move` 的 `allowHosts`。默认空——只信 loopback。
106
+ */
107
+ moveAllowHosts: z.array(String).default([]),
108
+ /** 是否允许删「属于某个工作区」的会话。默认否——工作区会话通常还想留。 */
109
+ allowWorkspaceSessions: z.boolean().default(false)
110
+ .description('允许删除「属于某个工作区」的会话。默认否——工作区会话通常还想留。')
111
+ .volatile(),
112
+ /** 是否允许删仍然挂在 Host 进程里的会话(正在浏览/正在跑)。默认否。 */
113
+ allowLiveSessions: z.boolean().default(false)
114
+ .description('允许删除仍然挂在 Host 进程里的会话(正在浏览或正在跑)。默认否。')
115
+ .volatile(),
116
+ /** 一次调用最多删多少个,防手滑。 */
117
+ maxBatch: z.number().default(200)
118
+ .description('一次调用最多删多少个会话,防手滑。默认 200。')
119
+ .volatile(),
120
+ /**
121
+ * 是否在会话标题栏显示「删除当前会话」的垃圾桶图标。**默认开启。**
122
+ *
123
+ * 形态刻意是「图标 + 二次确认」而不是带字面标签的按钮:标签会招来点击,而这里
124
+ * 删的正是你正在用的会话。图标不占字面位置,误点最多弹出一个确认框,离真正删除
125
+ * 还隔着一步。清理由右侧「会话管理」面板或设置页承担,那是显式挑选动作。
126
+ *
127
+ * 这是个 volatile 旋钮:浏览器半边在挂载时问一次 Host(`config` 接口),所以
128
+ * 改完**刷新页面**就生效——不需要重启 dsh web,也不需要重挂本插件。
129
+ */
130
+ headerDeleteButton: z.boolean().default(true)
131
+ .description('在会话标题栏显示「删除当前会话」的垃圾桶图标。默认开启;关掉后清理由「会话管理」面板/设置页承担。改完刷新页面生效。')
132
+ .volatile(),
133
+ /** 是否拒绝搬"正在跑一轮"的会话。默认拒绝——搬运期间它的写入句柄可能落盘。 */
134
+ refuseRunning: z.boolean().default(true)
135
+ .description('拒绝搬迁「正在跑一轮」的会话。默认开启——搬运期间它的写入句柄可能落盘。')
136
+ .volatile(),
137
+ /** `list` 一次最多返回多少条会话(浏览器半边还要筛选)。 */
138
+ maxSessions: z.number().default(2000)
139
+ .description('`list` 一次最多返回多少条会话(浏览器半边还要筛选)。默认 2000。')
140
+ .volatile(),
141
+ /**
142
+ * 项目作用域记忆的条目要不要跟着会话走。**默认 off**。
143
+ *
144
+ * 项目记忆是**工作区共享**的(一次写入、那个工作区里所有会话都读),所以把一条
145
+ * 会话的条目拖到另一个工作区会改变那边的记忆视图——只有"这条会话本来就该在目标
146
+ * 工作区"(纠正错放)时才该这么做:
147
+ * - `off` :什么都不做。报告里仍给出**只读预览**(若开启会复制哪几条)。
148
+ * - `copy` :把该会话产出的条目(frontmatter `session:` 精确匹配)复制到目标
149
+ * 工作区,按文件名去重,源工作区不动。
150
+ * - `move` :同上,但复制后从源工作区移除(不可逆,谨慎)。
151
+ */
152
+ memoryFollow: z.string().default('off')
153
+ .description('项目记忆跟随:off 只给只读预览(默认 / 推荐);copy 把本会话产出的条目复制到目标工作区;move 复制后从源移除(不可逆)。')
154
+ .volatile(),
155
+ /**
156
+ * 启动时是否做一轮「fork 子会话漏挂工作区」的定向扫描。
157
+ *
158
+ * 创建期的 `session/created` 补挂只挡住「Host fork 时就没挂」。记账写进去之后
159
+ * 又被抹掉的那一类(同机两个 Host 共用 `$DSH_HOME` 时,另一个进程用旧内存快照
160
+ * 重写 `storages/workspace.json`)只有启动期扫描能补回来,所以默认开启。
161
+ *
162
+ * 扫描范围收得很窄:只认带 `parentSession` 的普通 fork 子会话(排除 `origin:
163
+ * subagent` 与 `delegationDepth > 0`),且父会话已被某个工作区记账、子会话
164
+ * `cwd` 规范化后等于该工作区路径。刻意 ungrouped 的普通会话/迁移会话不碰。
165
+ */
166
+ repairForkChildrenOnBoot: z.boolean().default(true)
167
+ .description('启动时补挂「fork 子会话漏挂工作区」:只处理父会话已入册、cwd 与工作区一致的普通 fork 子会话。默认开启;挂载期读取,改它需要重挂本行。'),
168
+ });
206
169
  /**
207
170
  * 解包 entry 配置里的 `.volatile()` 活引用,并交给 schema 校验补默认值。
208
171
  *
@@ -213,13 +176,10 @@ export const Config = z.object({
213
176
  * @returns 普通值配置。
214
177
  */
215
178
  export function resolveConfig(raw = {}) {
216
- return resolveVolatileConfig(Config, raw)
179
+ // 解包后的普通对象在这里收敛成 {@link SessionOpsConfig}:schema 的默认值保证了
180
+ // 每个键都在,但 schemastery 的输出类型不携带这些静态事实,只能就地断言一次。
181
+ return resolveVolatileConfig(Config, raw);
217
182
  }
218
-
219
- // ---------------------------------------------------------------------------
220
- // 插件入口
221
- // ---------------------------------------------------------------------------
222
-
223
183
  /**
224
184
  * 挂载插件:两条路由 + 两个工具。
225
185
  *
@@ -234,47 +194,47 @@ export function resolveConfig(raw = {}) {
234
194
  * @param rawConfig - 部署层配置。
235
195
  * @returns 无。
236
196
  */
237
- export function apply(ctx, rawConfig) {
238
- /**
239
- * 当前生效的配置。
240
- *
241
- * 对象**身份保持不变**:volatile 字段更新时只把新值 `Object.assign` 进来,于是两个
242
- * 半边里所有已经捕获了 `config` 的闭包(HTTP 方法表、工具、信任栅栏、日志)自动
243
- * 看到最新值,不需要重挂,也不需要额外的 getter 管道。
244
- */
245
- const config = { ...resolveConfig(rawConfig) }
246
- // 两个半边各自的文件日志(行格式是各自原有的排障产物,见 lib/store/host.js)。
247
- const adminLog = createFileLogger(ctx, { name: 'dsh-session-admin', formatLine: LOG_LINE_LEVEL_FIRST })
248
- const moveLog = createFileLogger(ctx, { name: 'dsh-session-move', formatLine: LOG_LINE_TIME_FIRST })
249
- const logs = [adminLog, moveLog]
250
- // volatile 字段写盘后 Loader 原地换引用并补发这个事件:重读一次就能拿到新值。
251
- ctx.on('loader/volatile-update', () => {
252
- try {
253
- Object.assign(config, resolveConfig(rawConfig))
254
- } catch (error) {
255
- // 表单写盘前会先校验候选值,正常路径不会走到这里;真出错也不能把插件带崩。
256
- for (const log of logs) log('warn', `config refresh failed: ${String(error)}`)
197
+ export function apply(ctx, rawConfig = {}) {
198
+ /**
199
+ * 当前生效的配置。
200
+ *
201
+ * 对象**身份保持不变**:volatile 字段更新时只把新值 `Object.assign` 进来,于是两个
202
+ * 半边里所有已经捕获了 `config` 的闭包(HTTP 方法表、工具、信任栅栏、日志)自动
203
+ * 看到最新值,不需要重挂,也不需要额外的 getter 管道。
204
+ */
205
+ const config = { ...resolveConfig(rawConfig) };
206
+ // 两个半边各自的文件日志(行格式是各自原有的排障产物,见 lib/store/host.js)。
207
+ const adminLog = createFileLogger(ctx, { name: 'dsh-session-admin', formatLine: LOG_LINE_LEVEL_FIRST });
208
+ const moveLog = createFileLogger(ctx, { name: 'dsh-session-move', formatLine: LOG_LINE_TIME_FIRST });
209
+ const logs = [adminLog, moveLog];
210
+ // volatile 字段写盘后 Loader 原地换引用并补发这个事件:重读一次就能拿到新值。
211
+ ctx.on('loader/volatile-update', () => {
212
+ try {
213
+ Object.assign(config, resolveConfig(rawConfig));
214
+ }
215
+ catch (error) {
216
+ // 表单写盘前会先校验候选值,正常路径不会走到这里;真出错也不能把插件带崩。
217
+ for (const log of logs)
218
+ log('warn', `config refresh failed: ${String(error)}`);
219
+ }
220
+ });
221
+ if (config.enabled === false) {
222
+ for (const log of logs)
223
+ log('info', 'disabled by config; nothing registered');
224
+ return;
225
+ }
226
+ mountAdmin(ctx, config, adminLog);
227
+ mountMove(ctx, config, moveLog);
228
+ // fork 子会话漏挂工作区(实测):创建期监听 + 启动期定向扫描;只补记账,不改 Host 源码。
229
+ // 扫描是 fire-and-forget,但绝不留下未处理的 rejection——自愈路径不能影响插件其余部分。
230
+ const forkSweep = mountForkMembershipRepair(ctx, config, moveLog);
231
+ if (forkSweep !== undefined) {
232
+ forkSweep.catch((error) => moveLog('warn', `membership sweep failed: ${messageOf(error)}`));
257
233
  }
258
- })
259
- if (config.enabled === false) {
260
- for (const log of logs) log('info', 'disabled by config; nothing registered')
261
- return
262
- }
263
-
264
- mountAdmin(ctx, config, adminLog)
265
- mountMove(ctx, config, moveLog)
266
- // fork 子会话漏挂工作区(实测):创建期监听 + 启动期定向扫描;只补记账,不改 Host 源码。
267
- // 扫描是 fire-and-forget,但绝不留下未处理的 rejection——自愈路径不能影响插件其余部分。
268
- const forkSweep = mountForkMembershipRepair(ctx, config, moveLog)
269
- if (forkSweep !== undefined) {
270
- forkSweep.catch((error) => moveLog('warn', `membership sweep failed: ${String(error?.message ?? error)}`))
271
- }
272
234
  }
273
-
274
235
  // ---------------------------------------------------------------------------
275
236
  // 兼容再导出
276
237
  // ---------------------------------------------------------------------------
277
-
278
238
  /**
279
239
  * 会话存储原语全量再导出(`lib/store/` 的五个模块):
280
240
  * `dshHome`、`encodeSegment`、`readSessionHeader`、`hasZstd`、`decodeProjectKey` 等。
@@ -283,7 +243,6 @@ export function apply(ctx, rawConfig) {
283
243
  * 外部按包名导入的脚本(如 dsh-open-in-dsh)只改包名即可,不必换符号名。
284
244
  * 需要显式分层时也可以用子路径 `dsh-session-ops/store`(见 package.json exports)。
285
245
  */
286
- export * from './store/index.js'
287
-
246
+ export * from './store/index.js';
288
247
  // 2026-09-25:本文件是 dsh-session-admin + dsh-session-move 的合并入口
289
248
  // (本包就是这两个插件的合并产物;会话存储知识收在 `lib/store/`)。