dsh-plugin-tool-management 0.14.0 → 0.16.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/CHANGELOG.md +585 -519
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +409 -365
- package/lib/audit-log.js +157 -0
- package/lib/client.js +3915 -2545
- package/lib/compat/patch-dialect.js +3 -1
- package/lib/compat/preset-reach.js +37 -6
- package/lib/compat/probe.js +281 -11
- package/lib/context-inject.js +5 -3
- package/lib/index.js +236 -30
- package/lib/mcp/loader-token.js +53 -28
- package/lib/mcp/manager.js +311 -9
- package/lib/memories/archive-engine.js +70 -2
- package/lib/memories/archive.js +8 -1
- package/lib/memories/index-io.js +6 -0
- package/lib/memories/service.js +9 -0
- package/lib/op-registry.js +18 -4
- package/lib/ops/compat.js +121 -35
- package/lib/ops/scene-records.js +34 -0
- package/lib/ops/scene-sync.js +12 -1
- package/lib/ops/sessions.js +23 -8
- package/lib/ops/snapshot.js +596 -0
- package/lib/scenes/candidates.js +10 -5
- package/lib/sessions/bridge.js +5 -0
- package/lib/sessions/workspace.js +17 -5
- package/lib/skills/core.js +12 -3
- package/lib/tools/deps.js +1 -1
- package/lib/tools/table.js +5 -1
- package/package.json +113 -108
package/lib/ops/compat.js
CHANGED
|
@@ -64,6 +64,9 @@ export function buildCompatOps(deps) {
|
|
|
64
64
|
host: { version: assessment.identity.version, modules: assessment.identity.modules },
|
|
65
65
|
sameAsHost: assessment.identity.sameAsHost,
|
|
66
66
|
unverified: assessment.identity.unverified,
|
|
67
|
+
// 身份校验的说明(目前只有「宿主在 app.asar 里」一种):不是阻塞项,界面据此
|
|
68
|
+
// 解释那一排「无法比较」,而不是留给用户一句「两份拷贝,去 --fix」。
|
|
69
|
+
notes: assessment.identity.notes,
|
|
67
70
|
findings,
|
|
68
71
|
degraded,
|
|
69
72
|
blockers: assessment.identity.blockers,
|
|
@@ -129,15 +132,35 @@ export function buildCompatOps(deps) {
|
|
|
129
132
|
const domainsOn = INJECT_DOMAIN_KEYS.filter((key) => settings.domains[key] !== false);
|
|
130
133
|
const token = await deps.readTokenState();
|
|
131
134
|
const lockedScenes = await deps.lockedSceneNames();
|
|
132
|
-
const disabledTools = deps.
|
|
135
|
+
const disabledTools = deps.disabledToolSummary();
|
|
133
136
|
const routeOf = (operation) => assessment === undefined ? undefined : routeFor(assessment, operation);
|
|
134
|
-
/** 一行一个功能点:状态三层合成,`detail` 写明依据。 */
|
|
135
137
|
const rows = [];
|
|
136
|
-
const push = (
|
|
137
|
-
|
|
138
|
+
const push = (row) => rows.push(row);
|
|
139
|
+
/** note 降级行的公共字段:透传 note 的 detailKey / params,没有结构化键就只带原文。 */
|
|
140
|
+
const noteFields = (note) => note === undefined ? {}
|
|
141
|
+
: { ...(note.detailKey === undefined ? {} : { detailKey: note.detailKey }), ...(note.params === undefined ? {} : { params: note.params }) };
|
|
142
|
+
/** 路由判定行的公共字段:可用带 via;不可用带缺失能力的 id + 原始标签(客户端出各语言标签)。 */
|
|
143
|
+
const routeFields = (decision) => {
|
|
144
|
+
if (decision === undefined)
|
|
145
|
+
return {};
|
|
146
|
+
return decision.via === 'none'
|
|
147
|
+
? { refusals: decision.refusals.map((item) => ({ id: item.id, label: item.label })) }
|
|
148
|
+
: { via: decision.via };
|
|
149
|
+
};
|
|
150
|
+
const routeDetail = (decision) => decision === undefined ? '能力探测不可用(归档服务未挂载)'
|
|
151
|
+
: decision.via === 'none' ? '宿主缺少该路径所需能力:' + decision.refusals.map((item) => item.label).join('、')
|
|
152
|
+
: `可用(路径:${decision.via})`;
|
|
153
|
+
// 前置项:插件挂载(这一行本身说明 inject 都解析了 —— 否则 apply 根本不会跑);
|
|
138
154
|
// 启动期那段 `!!js` 表达式若抛错,插件同样不会挂上(它的 try/catch 就是为了不抛)。
|
|
139
155
|
const mountNote = notes.get('cordis-original-symbol');
|
|
140
|
-
push(
|
|
156
|
+
push({
|
|
157
|
+
key: 'mount', label: '插件挂载', tab: 'compat',
|
|
158
|
+
state: mountNote === undefined ? 'ok' : 'degraded',
|
|
159
|
+
detail: mountNote === undefined
|
|
160
|
+
? `${deps.injectServiceCount} 个 inject 服务全部解析,插件已挂载(详见 doctor 的挂载心跳)`
|
|
161
|
+
: mountNote.detail,
|
|
162
|
+
...(mountNote === undefined ? { params: { count: deps.injectServiceCount } } : noteFields(mountNote)),
|
|
163
|
+
});
|
|
141
164
|
// 装配层:有上报就是降级,附上报里的原因;没有就是正常。
|
|
142
165
|
const assemblyRows = [
|
|
143
166
|
['patch-write-guard', '宿主配置写入(补丁校验)', 'mcp'],
|
|
@@ -149,52 +172,115 @@ export function buildCompatOps(deps) {
|
|
|
149
172
|
];
|
|
150
173
|
for (const [noteId, label, tab] of assemblyRows) {
|
|
151
174
|
const note = notes.get(noteId);
|
|
152
|
-
push(
|
|
175
|
+
push({
|
|
176
|
+
key: noteId, label, tab, state: note === undefined ? 'ok' : 'degraded',
|
|
177
|
+
detail: note === undefined ? '装配正常,无降级上报' : note.detail,
|
|
178
|
+
...noteFields(note),
|
|
179
|
+
});
|
|
153
180
|
}
|
|
154
181
|
// 宿主能力层:删除 / 归档 / 恢复 / 批量各自由路由判定回答(与服务端执行同源)。
|
|
155
182
|
const deleteRoute = routeOf('delete');
|
|
156
|
-
push(
|
|
157
|
-
:
|
|
158
|
-
|
|
183
|
+
push({
|
|
184
|
+
key: 'session-delete', label: '会话删除', tab: 'sessions',
|
|
185
|
+
state: deleteRoute === undefined ? 'unknown' : deleteRoute.via === 'none' ? 'unavailable' : 'ok',
|
|
186
|
+
detail: routeDetail(deleteRoute), ...routeFields(deleteRoute),
|
|
187
|
+
});
|
|
159
188
|
for (const [operation, label] of [['archive', '归档'], ['unarchive', '恢复'], ['batch', '批量操作'], ['list', '历史列表']]) {
|
|
160
189
|
const decision = routeOf(operation);
|
|
161
|
-
push(
|
|
162
|
-
:
|
|
163
|
-
|
|
190
|
+
push({
|
|
191
|
+
key: 'session-' + operation, label: `会话${label}`, tab: 'sessions',
|
|
192
|
+
state: decision === undefined ? 'unknown' : decision.via === 'none' ? 'unavailable' : 'ok',
|
|
193
|
+
detail: routeDetail(decision), ...routeFields(decision),
|
|
194
|
+
});
|
|
164
195
|
}
|
|
165
196
|
// 用户配置层:注入域 / 令牌 / 场景锁定 / 停用工具数。
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
push(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
197
|
+
// 分母必须取自 INJECT_DOMAIN_KEYS:0.14.0 场景与记忆分家后域从 5 个变 6 个,
|
|
198
|
+
// 写死的「/5」把 6/6 报成 6/5(2026-09-28 用户实测)。
|
|
199
|
+
const domainTotal = INJECT_DOMAIN_KEYS.length;
|
|
200
|
+
push({
|
|
201
|
+
key: 'injection-domains', label: '注入域开关', tab: 'compat',
|
|
202
|
+
state: domainsOn.length === 0 ? 'disabled' : 'ok',
|
|
203
|
+
detail: domainsOn.length === 0 ? `${domainTotal} 个注入域全部关闭(用户设置)` : `${domainsOn.length}/${domainTotal} 个域开启:${domainsOn.join('、')}`,
|
|
204
|
+
domains: domainsOn, count: domainsOn.length, total: domainTotal,
|
|
205
|
+
});
|
|
206
|
+
push({
|
|
207
|
+
key: 'token', label: '访问令牌', tab: 'compat',
|
|
208
|
+
state: !token.active ? 'disabled' : token.accepted ? 'ok' : 'locked',
|
|
209
|
+
detail: !token.active ? '未启用(宿主没配令牌,或令牌功能被关掉)'
|
|
210
|
+
: token.accepted ? '已生效且本次启动已通过验证' : '已生效,本次启动尚未验证:写操作与对话会被拦住,到本页下方填写令牌',
|
|
211
|
+
});
|
|
212
|
+
push({
|
|
213
|
+
key: 'scene-lock', label: '场景锁定', tab: 'scenes',
|
|
214
|
+
state: lockedScenes.length === 0 ? 'ok' : 'locked-scene',
|
|
215
|
+
detail: lockedScenes.length === 0 ? '无锁定场景' : `锁定中:${lockedScenes.join('、')}(写门禁按锁定场景生效)`,
|
|
216
|
+
scenes: lockedScenes,
|
|
217
|
+
});
|
|
218
|
+
// 停用是用户配置或场景收窄的**事实**,不是故障:功能本身的好坏由上面「工具表可见性」
|
|
219
|
+
// 装配行报(`mcp-tool-visibility` 降级 = 拦截/摘除没装上),这一行只如实陈述停用构成,
|
|
220
|
+
// 状态恒为 ok —— 否则"只开了几台 MCP"的用户会一直看到琥珀,以为出了问题
|
|
221
|
+
// (2026-09-28 用户裁定:琥珀只留给功能出问题)。
|
|
222
|
+
// 「N 个工具」也说不清整台停用:一条 `*` 通配背后是那台服务器的全部工具,
|
|
223
|
+
// 按条目数报会把 6 台整台停用读成"6 个工具"。两个口径分开说:台数只数 `*`,工具数只数单独停用的条目。
|
|
224
|
+
const disabledPart = disabledTools.servers > 0 && disabledTools.tools > 0
|
|
225
|
+
? `${disabledTools.servers} 台服务器整台停用 + ${disabledTools.tools} 个工具单独停用`
|
|
226
|
+
: disabledTools.servers > 0
|
|
227
|
+
? `${disabledTools.servers} 台服务器整台停用`
|
|
228
|
+
: disabledTools.tools > 0
|
|
229
|
+
? `${disabledTools.tools} 个工具单独停用`
|
|
230
|
+
: '没有停用的工具';
|
|
231
|
+
push({
|
|
232
|
+
key: 'mcp-tools', label: 'MCP 工具停用', tab: 'mcp', state: 'ok',
|
|
233
|
+
detail: disabledPart + (disabledTools.servers + disabledTools.tools === 0 ? '' : '(执行拦截 + 可见性摘除)'),
|
|
234
|
+
servers: disabledTools.servers, tools: disabledTools.tools,
|
|
235
|
+
});
|
|
236
|
+
// 工具表按每个请求付钱:这一行回答"这一轮实际发出去多少"。关掉(无论出厂默认还是
|
|
237
|
+
// 用户自己关)都是**配置事实**而非故障 —— 状态恒为 ok,琥珀只留给功能出问题
|
|
238
|
+
// (口径与 MCP 工具停用一致,2026-09-28 用户裁定)。出厂默认关掉的那几条单说:
|
|
239
|
+
// 那是插件替用户做的一个可逆选择,与用户自己关的分开计数。
|
|
240
|
+
// 说明只保留一句关了多少:省多少 tok、去哪开关、面板不受影响、skill_manager_read
|
|
241
|
+
// 让位这些次级信息都不上功能总览(正常行一行读完,2026-09-28 用户反馈);
|
|
242
|
+
// 逐会话的让位差额(index.ts 的 CARRIER_DUPLICATES)在别处自有交代。
|
|
178
243
|
const table = deps.toolTableReport();
|
|
179
|
-
const carrierNote = ';官方 `skill` 工具在场的会话,`skill_manager_read` 还会自动让位一份';
|
|
180
244
|
const userOff = table.hiddenCount - table.defaultHiddenCount;
|
|
181
245
|
const offPart = table.hiddenCount === 0
|
|
182
|
-
? `${table.totalCount}
|
|
246
|
+
? `${table.totalCount} 个工具全部下发`
|
|
183
247
|
: (userOff === 0
|
|
184
248
|
? `出厂默认关掉 ${table.defaultHiddenCount}/${table.totalCount} 个`
|
|
185
|
-
: `关掉 ${table.hiddenCount}/${table.totalCount} 个(含出厂默认 ${table.defaultHiddenCount} 个)`)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
249
|
+
: `关掉 ${table.hiddenCount}/${table.totalCount} 个(含出厂默认 ${table.defaultHiddenCount} 个)`);
|
|
250
|
+
push({
|
|
251
|
+
key: 'tool-table', label: '模型工具表', tab: 'compat', state: 'ok', detail: offPart,
|
|
252
|
+
hidden: table.hiddenCount, total: table.totalCount, defaults: table.defaultHiddenCount,
|
|
253
|
+
});
|
|
254
|
+
const nativeNote = notes.get('workspace.delete-native');
|
|
255
|
+
push({
|
|
256
|
+
key: 'native-delete', label: '宿主原生删除入口', tab: 'compat',
|
|
257
|
+
state: nativeNote ? 'partial' : 'ok',
|
|
258
|
+
detail: nativeNote?.detail ?? '宿主未提供原生删除入口(本插件自有完整序列)',
|
|
259
|
+
...noteFields(nativeNote),
|
|
260
|
+
});
|
|
189
261
|
// 身份 / 版本:与 compat-status 同一份数据。
|
|
190
262
|
const identity = assessment?.identity;
|
|
191
263
|
const identityIssue = identity !== undefined && (identity.blockers.length > 0 || Object.values(identity.sameAsHost).some((same) => same === false));
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
264
|
+
// 归档宿主(桌面版):不是阻塞项,但"模块与宿主同源"这句话对它不成立(同版本、
|
|
265
|
+
// 不同文件),所以总览那一行要换句话讲,而不是继续报一句善意的假话。
|
|
266
|
+
const asarHost = identity?.notes.find((note) => note.kind === 'asar-host');
|
|
267
|
+
push({
|
|
268
|
+
key: 'host-identity', label: '宿主身份与模块', tab: 'compat',
|
|
269
|
+
state: assessment === undefined ? 'unknown' : identityIssue ? 'degraded' : 'ok',
|
|
270
|
+
detail: assessment === undefined ? '能力探测不可用(归档服务未挂载)'
|
|
271
|
+
: identityIssue ? '存在阻塞项:' + (identity?.blockers ?? []).join(';')
|
|
272
|
+
: asarHost !== undefined
|
|
273
|
+
? `官方包与宿主同版本,但宿主跑在 ${asarHost.asarName} 里(插件在归档外)`
|
|
274
|
+
: `模块与宿主同源(宿主 DSH ${identity?.version ?? '?'})`,
|
|
275
|
+
...(assessment === undefined ? {} : {
|
|
276
|
+
version: identity?.version ?? '?',
|
|
277
|
+
blockers: identity?.blockers ?? [],
|
|
278
|
+
...(asarHost === undefined ? {} : { asarHost: true, asarName: asarHost.asarName }),
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
195
281
|
// 无独立装配点的域:没有上报就是正常(如实说明依据是"没有降级上报")。
|
|
196
282
|
for (const [key, label, tab] of [['memory', '记忆', 'memory'], ['prompts', '提示词', 'prompts'], ['subagents', '子智能体', 'subagents'], ['scenes', '场景档案', 'scenes']]) {
|
|
197
|
-
push(key, label, tab, 'ok', '无降级上报(装配失败会出现在这里)');
|
|
283
|
+
push({ key, label, tab, state: 'ok', detail: '无降级上报(装配失败会出现在这里)' });
|
|
198
284
|
}
|
|
199
285
|
return { ok: true, rows, generatedAt: Date.now() };
|
|
200
286
|
}
|
|
@@ -230,7 +316,7 @@ export function buildCompatOps(deps) {
|
|
|
230
316
|
...(r.failed.length ? { failedCount: r.failed.length, failedNames: r.failed } : {}),
|
|
231
317
|
};
|
|
232
318
|
},
|
|
233
|
-
//
|
|
319
|
+
// 注入实况(只读):最近活跃会话里模型**真正看到**的各域文本 + 那一段对话的投递统计。
|
|
234
320
|
// 回答"勾了开关到底送没送到"——界面配置与实际注入不一致时,这里一眼可见。
|
|
235
321
|
'injection-live': async () => {
|
|
236
322
|
const live = deps.getContextInjectorLive();
|
package/lib/ops/scene-records.js
CHANGED
|
@@ -108,6 +108,18 @@ export function buildSceneRecordOps(rc) {
|
|
|
108
108
|
if (name === GLOBAL_SCENE)
|
|
109
109
|
next.label = GLOBAL_SCENE_LABEL;
|
|
110
110
|
index.scenes[name] = next;
|
|
111
|
+
// 模型工具表方案绑定(0.15.0 用户裁定:入口改到「修改场景」表单):绑定存**档案**,
|
|
112
|
+
// 只在真的绑了东西时才建档案存根 —— 不绑就保持「没有档案」的原语义
|
|
113
|
+
//(没有档案与有空档案在进入场景的预览提示上不等价)。
|
|
114
|
+
const toolTablePreset = args && args.toolTablePreset !== undefined ? String(args.toolTablePreset).trim() : '';
|
|
115
|
+
if (toolTablePreset.length > 64) {
|
|
116
|
+
return fail('error.rules.invalidGroup', `工具表方案名过长(≤64 字符)`);
|
|
117
|
+
}
|
|
118
|
+
if (toolTablePreset !== '') {
|
|
119
|
+
if (!index.archives)
|
|
120
|
+
index.archives = {};
|
|
121
|
+
index.archives[name] = { ...(index.archives[name] || {}), toolTablePreset };
|
|
122
|
+
}
|
|
111
123
|
await writeIndex(stateDir, index);
|
|
112
124
|
invalidateSnapshot();
|
|
113
125
|
return { ok: true, scene: sceneRecordOf(name, next), ...(collapsed ? { collapsedActive: true } : {}) };
|
|
@@ -158,6 +170,28 @@ export function buildSceneRecordOps(rc) {
|
|
|
158
170
|
else
|
|
159
171
|
next.prompt = prompt;
|
|
160
172
|
}
|
|
173
|
+
if (args && args.toolTablePreset !== undefined) {
|
|
174
|
+
// 模型工具表方案绑定(0.15.0 用户裁定:入口改到「修改场景」表单):绑定存**档案**,
|
|
175
|
+
// 记录与档案同在一份索引 JSON,这一次读-改-写顺带落掉。''= 解绑(删字段;档案因此
|
|
176
|
+
// 变空时整条删掉,与档案保存「空档案不落条目」的约定一致);不传这个参数 = 完全不碰
|
|
177
|
+
// 这一域(旧调用方兼容)。绑了不存在的方案在这里不硬失败 —— 引擎在进出场景时按
|
|
178
|
+
// stale 丢弃并报告,和档案保存对悬空绑定的处理同口径。
|
|
179
|
+
const toolTablePreset = String(args.toolTablePreset).trim();
|
|
180
|
+
if (toolTablePreset.length > 64) {
|
|
181
|
+
return fail('error.rules.invalidGroup', `工具表方案名过长(≤64 字符)`);
|
|
182
|
+
}
|
|
183
|
+
if (!index.archives)
|
|
184
|
+
index.archives = {};
|
|
185
|
+
const archive = { ...((index.archives && index.archives[name]) || {}) };
|
|
186
|
+
if (toolTablePreset === '')
|
|
187
|
+
delete archive.toolTablePreset;
|
|
188
|
+
else
|
|
189
|
+
archive.toolTablePreset = toolTablePreset;
|
|
190
|
+
if (Object.keys(archive).length > 0)
|
|
191
|
+
index.archives[name] = archive;
|
|
192
|
+
else
|
|
193
|
+
delete index.archives[name];
|
|
194
|
+
}
|
|
161
195
|
if (args && args.label !== undefined && name !== GLOBAL_SCENE) {
|
|
162
196
|
const label = String(args.label).trim();
|
|
163
197
|
if (label === '')
|
package/lib/ops/scene-sync.js
CHANGED
|
@@ -10,7 +10,18 @@ export function buildSceneSyncOps(deps) {
|
|
|
10
10
|
// 的 `agentsMd` 字段(`applied` / `restored` / `unchanged` / `error`)。
|
|
11
11
|
// 组装时放在 memoriesService.ops 的 spread **之后**,显式覆盖同名 op。
|
|
12
12
|
'rules-set-active': async (args) => deps.withAgentsMdSync(await deps.rulesOps['rules-set-active'](args)),
|
|
13
|
-
'rules-update-scene': async (args) =>
|
|
13
|
+
'rules-update-scene': async (args) => {
|
|
14
|
+
const res = await deps.rulesOps['rules-update-scene'](args);
|
|
15
|
+
// 工具表方案绑定改动(0.15.0):改的是**当前启用场景**且这次真的带了这一参数时即时生效;
|
|
16
|
+
// 改名在场景处于模式中时会被拦(rulesUpdateScene 的 sceneInMode),所以这里 args.name 就是生效场景的名字。
|
|
17
|
+
if (res && res.ok !== false && deps.onToolTablePresetChanged && args && args.toolTablePreset !== undefined) {
|
|
18
|
+
try {
|
|
19
|
+
await deps.onToolTablePresetChanged(String(args.name || ''), String(args.toolTablePreset));
|
|
20
|
+
}
|
|
21
|
+
catch { /* 即时生效失败不拦保存;下次进/退场景会对齐 */ }
|
|
22
|
+
}
|
|
23
|
+
return deps.withAgentsMdSync(res);
|
|
24
|
+
},
|
|
14
25
|
'rules-create-scene': async (args) => deps.withAgentsMdSync(await deps.rulesOps['rules-create-scene'](args)),
|
|
15
26
|
'rules-remove-scene': async (args) => deps.withAgentsMdSync(await deps.rulesOps['rules-remove-scene'](args)),
|
|
16
27
|
};
|
package/lib/ops/sessions.js
CHANGED
|
@@ -54,7 +54,27 @@ export function buildSessionOps(deps) {
|
|
|
54
54
|
? (await registry.archivedSessionDetails()).items
|
|
55
55
|
: (await registry.archivedSessionMetadata()).items;
|
|
56
56
|
const archived = new Set(archivedItems.map((i) => i.sessionId));
|
|
57
|
+
// 官方契约(dsh-session-projection-cache 源码):cachedSnapshot(meta, keys?) 两参,
|
|
58
|
+
// keys 缺省 = 全部 wire 键;keys 传非可迭代值会在 viewCheckpoint 里 new Set 抛错。
|
|
59
|
+
// 旧格式代际的缓存记录过不了 cachedSnapshot 的身份校验,官方另有只认标题的
|
|
60
|
+
// cachedPredecessorTitle 提示面(api-session-controller 的列表路径正是两者相或)。
|
|
57
61
|
const cache = deps.host('sessionProjectionCache');
|
|
62
|
+
const titleOf = (header) => {
|
|
63
|
+
if (!cache)
|
|
64
|
+
return undefined;
|
|
65
|
+
try {
|
|
66
|
+
const snap = typeof cache.cachedSnapshot === 'function' ? cache.cachedSnapshot(header, ['title']) : undefined;
|
|
67
|
+
const current = snap && snap.values ? snap.values.title : undefined;
|
|
68
|
+
if (typeof current === 'string')
|
|
69
|
+
return current;
|
|
70
|
+
const legacy = typeof cache.cachedPredecessorTitle === 'function' ? cache.cachedPredecessorTitle(header) : undefined;
|
|
71
|
+
const legacyTitle = legacy && legacy.values ? legacy.values.title : undefined;
|
|
72
|
+
return typeof legacyTitle === 'string' ? legacyTitle : undefined;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
58
78
|
const items = [];
|
|
59
79
|
for (const h of headers) {
|
|
60
80
|
const sessionId = h && typeof h.id === 'string' ? h.id : undefined;
|
|
@@ -71,14 +91,9 @@ export function buildSessionOps(deps) {
|
|
|
71
91
|
item.cwd = h.cwd;
|
|
72
92
|
if (typeof h.createdAt === 'number' && Number.isFinite(h.createdAt))
|
|
73
93
|
item.createdAt = h.createdAt;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (snap && snap.values && typeof snap.values.title === 'string')
|
|
78
|
-
item.title = snap.values.title;
|
|
79
|
-
}
|
|
80
|
-
catch { /* title best-effort */ }
|
|
81
|
-
}
|
|
94
|
+
const title = titleOf(h);
|
|
95
|
+
if (title !== undefined)
|
|
96
|
+
item.title = title;
|
|
82
97
|
// 工作区目录可能已被删除/移动(孤儿会话):best-effort 标记,供导出选择时识别。
|
|
83
98
|
if (item.cwd) {
|
|
84
99
|
try {
|