dsh-session-conductor 1.3.5 → 1.3.7
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.md +13 -8
- package/lib/apply.js +7 -0
- package/lib/client-parts/apply.js +18 -4
- package/lib/client-parts/components/panel.js +55 -2
- package/lib/client-parts/foundation/bootstrap.js +7 -1
- package/lib/client.js +80 -7
- package/lib/features/memory-recall/index.js +12 -2
- package/lib/i18n/en.json +1 -0
- package/lib/i18n/zh.json +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,22 +137,27 @@ allowBuilds:
|
|
|
137
137
|
|
|
138
138
|
### 真实后端预览页
|
|
139
139
|
|
|
140
|
-
改 UI 后免重启查看效果 —— **预览能力已移出本插件**,统一用独立插件 `dsh-useful-tools
|
|
140
|
+
改 UI 后免重启查看效果 —— **预览能力已移出本插件**,统一用独立插件 `dsh-useful-tools`(**0.1.11**)的 `preview` / `minihost` 工具。
|
|
141
141
|
|
|
142
|
-
**最短三行(实测可用;本仓库没有 `assets/preview.spec.mjs`
|
|
142
|
+
**最短三行(实测可用;本仓库没有 `assets/preview.spec.mjs` 也能出真实界面)**:
|
|
143
143
|
|
|
144
144
|
```
|
|
145
|
-
minihost { action: "start", plugin: "<插件根>", port: 31177
|
|
146
|
-
preview { action: "gen", plugin: "<插件根>" }
|
|
147
|
-
preview { action: "serve", file: "<插件根>/assets/preview.html", backend: "http://127.0.0.1:31177" }
|
|
145
|
+
minihost { action: "start", plugin: "<插件根>", port: 31177, services: "<插件根>/test/api/helpers/fake-services.mjs" }
|
|
146
|
+
preview { action: "gen", plugin: "<插件根>" }
|
|
147
|
+
preview { action: "serve", file: "<插件根>/assets/preview.html", backend: "http://127.0.0.1:31177" }
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
停止:`minihost { action:"stop", port:31177 }` + `preview { action:"stop", port:31188 }`;状态:两者都用 `action:"status"`。
|
|
151
151
|
|
|
152
|
+
**要点**:
|
|
153
|
+
|
|
154
|
+
- **`services` 必须传**(0.1.11 新增):本插件 `apply()` 依赖 `storageDomain` 等宿主服务,不注入时 `/templates` 会 500、页面显示「storageDomain 不可用」;注入 `test/api/helpers/fake-services.mjs` 后 `/templates` 返回 200 真实数据(已实测)。
|
|
155
|
+
- **autoMount**(0.1.7 起):无 spec 时 mock 宿主 `__ModuleLoader__` / `require` / `ctx`(slots / locale / effect),调插件 `apply(ctx)`,把 `slots.register(def, component)` 捕获到的组件 `createRoot().render()` 到 `#root`。**限制**:组件若依赖 `useSettings` / `useSession` 这类宿主 hooks 会渲染失败(页面会显示错误与已捕获插槽清单,不白屏);要 100% 还原仍需 spec 提供 mock。locale 取插件自己 `ctx.locale.register` 的字典,键名不一致会回退成 key。
|
|
152
156
|
- `gen` 默认产物是 `<插件根>/assets/preview.html`(会写进仓库)⇒ 不想入库就传 `out`(如 `out: "/tmp/sc-preview.html"`),并让 `serve` 的 `file` 指向同一路径。本仓库已把该产物加进 `.gitignore`。
|
|
153
|
-
- `serve` 默认端口 **31188**,显式传 `port`
|
|
154
|
-
- `backend` 可指向 minihost 的端口(`/api/*` 反代到模拟后端),也可指向**真实 DSH 后端**。
|
|
157
|
+
- `serve` 默认端口 **31188**,显式传 `port` 才用你给的;`backend` 可指向 minihost 的端口(`/api/*` 反代到模拟后端),也可指向**真实 DSH 后端**。
|
|
155
158
|
- 显式传 `spec` 但文件不存在会**直接报错**(不静默降级)—— 故意的,避免指错文件还生成一个看不懂的页面。
|
|
159
|
+
- **截图**(README 配图)走 **browser 插件的 `browser_screenshot`**;「自包含快照」形态不做(理由见 `dsh-useful-tools` 的 `docs/NOT-DONE.md`)。
|
|
160
|
+
- **隔离实例自测**(0.1.8/0.1.9):`test_env { action:"create"|"start"|"stop"|"status", plugin, pluginName }` 起独立 `DSH_HOME` + 自动端口的实例;`sync_plugin { write:true, register:true, dshHome:"<测试环境 DSH_HOME>" }` 把插件装进去(写 profile 的 `dependencies` + `dsh.profile.bundles`,并对齐 `node_modules` 副本)。
|
|
156
161
|
- 停止:`minihost { action: "stop", port: 31177 }` + `preview { action: "stop", port: 31200 }`;状态:两者都用 `action: "status"`。
|
|
157
162
|
- **截图**(README 配图)改走 **browser 插件的 `browser_screenshot`**;「自包含快照」形态不做(理由见 `dsh-useful-tools` 的 `docs/NOT-DONE.md`)。
|
|
158
163
|
|
package/lib/apply.js
CHANGED
|
@@ -318,6 +318,13 @@ export async function apply(ctx, config = {}) {
|
|
|
318
318
|
res.setHeader("Cache-Control", "no-store");
|
|
319
319
|
if (res.socket?.setNoDelay) res.socket.setNoDelay(true);
|
|
320
320
|
res.write(JSON.stringify({ ok: true, stream: true }) + "\n"); // 头行(前端跳过非会话行)
|
|
321
|
+
// 立即把头行推出去:冷缓存时后面要逐份解压上百份日志(实测 41~226 秒),
|
|
322
|
+
// 若头行压在缓冲里,浏览器这侧会一直「没有响应」,看起来就像接口挂了。
|
|
323
|
+
// flushHeaders/flush 老版本不一定有,取不到就跳过(写操作本身已尽量及时)。
|
|
324
|
+
try {
|
|
325
|
+
if (typeof res.flushHeaders === "function") res.flushHeaders();
|
|
326
|
+
if (typeof res.flush === "function") res.flush();
|
|
327
|
+
} catch { /* 不支持 flush 的宿主不影响主流程 */ }
|
|
321
328
|
const onItem = (item) => { try { res.write(JSON.stringify(item) + "\n"); } catch { /* 连接已断 */ } };
|
|
322
329
|
await buildSessionListCached(ctx, { onItem, serial: true });
|
|
323
330
|
try { res.end(); } catch { /* 连接已断 */ }
|
|
@@ -17,10 +17,24 @@
|
|
|
17
17
|
locale: NS,
|
|
18
18
|
inject: () => ({
|
|
19
19
|
onOpenSession: (sessionId) => {
|
|
20
|
+
// 打开会话的真实入口随宿主版本不同,按优先级尝试(**不要只认一个**):
|
|
21
|
+
// · workspaceNavigation.openSession(id) —— dsh-client-ui-conversation 的用法
|
|
22
|
+
// · ctx.uiWorkspace.openSession(id) —— dsh-client-ui-chat 的用法
|
|
23
|
+
// · sessions.open(id) —— 早期路径,保留兜底
|
|
24
|
+
// 以前只写了第三个,而宿主客户端并没有 `sessions` 这个服务 ⇒ 可选链把失败吞成
|
|
25
|
+
// 「点了没反应」,且不报错、难查。所以这里:多个入口都试;全失败要留下痕迹。
|
|
20
26
|
try {
|
|
21
|
-
ctx.get("
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
const nav = ctx.get?.("workspaceNavigation");
|
|
28
|
+
if (typeof nav?.openSession === "function") return nav.openSession(sessionId);
|
|
29
|
+
if (typeof ctx.uiWorkspace?.openSession === "function") return ctx.uiWorkspace.openSession(sessionId);
|
|
30
|
+
const legacy = ctx.get?.("sessions");
|
|
31
|
+
if (typeof legacy?.open === "function") return legacy.open(sessionId);
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.warn("[session-conductor] 打不开会话:宿主客户端未暴露 workspaceNavigation/uiWorkspace/sessions 任一入口", sessionId);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// 打开失败不阻塞面板,但不再静默
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.warn("[session-conductor] 打开会话失败:", error);
|
|
24
38
|
}
|
|
25
39
|
},
|
|
26
40
|
}),
|
|
@@ -38,6 +52,6 @@
|
|
|
38
52
|
* 本卡只保留:
|
|
39
53
|
* · 分组列表(GET /api/session-conductor/group/list,只读展示)
|
|
40
54
|
* · 每行「在分组下新建会话」(POST /api/session-conductor/group/new-session {workspaceId})
|
|
41
|
-
* ·
|
|
55
|
+
* · 「新建会话(上一次使用的工作区)」(new-session 不带 workspaceId = 最近活跃会话 cwd 归属分组)
|
|
42
56
|
*/
|
|
43
57
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const [groups, setGroups] = react.useState(null); // workspaces[](group/list);null = host 无分组 API
|
|
6
6
|
const [collapsed, setCollapsed] = react.useState(() => new Set()); // 折叠的分组(含 "__ungrouped__")
|
|
7
7
|
const [error, setError] = react.useState(null);
|
|
8
|
+
const [retryIn, setRetryIn] = react.useState(null); // 自动重试倒计时(秒);null = 没有待重试的定时器
|
|
8
9
|
const [busy, setBusy] = react.useState(null);
|
|
9
10
|
// —— 搜索 + 批量/按条件删除 ——
|
|
10
11
|
const [query, setQuery] = react.useState(""); // 列表过滤关键词
|
|
@@ -31,9 +32,26 @@
|
|
|
31
32
|
next.splice(i, 0, item);
|
|
32
33
|
return next;
|
|
33
34
|
};
|
|
35
|
+
// 列表拉取失败自动重试:DSH 刚重启时插件路由/宿主服务未必已就绪,首次请求会直接
|
|
36
|
+
// 网络失败(Failed to fetch),冷缓存首次构建也很慢 —— 靠用户手动点「重试」既费事又
|
|
37
|
+
// 容易误以为插件坏了。这里按退避自动重试,界面照常给出失败原因 + 下次重试倒计时,
|
|
38
|
+
// 成功一次即清零。数组长度 = 最多自动重试几次。
|
|
39
|
+
const LIST_RETRY_DELAYS = [3000, 8000, 20000, 45000];
|
|
40
|
+
const listRetryRef = react.useRef(0);
|
|
41
|
+
const listTimerRef = react.useRef(null);
|
|
42
|
+
const refreshRef = react.useRef(null);
|
|
43
|
+
// 卸载时清掉待重试的定时器,避免组件已销毁还在拉数据(也会让「已关闭面板」继续打后端)
|
|
44
|
+
react.useEffect(() => () => {
|
|
45
|
+
if (listTimerRef.current) clearTimeout(listTimerRef.current);
|
|
46
|
+
listTimerRef.current = null;
|
|
47
|
+
}, []);
|
|
34
48
|
const refresh = react.useCallback(async () => {
|
|
49
|
+
if (listTimerRef.current) { clearTimeout(listTimerRef.current); listTimerRef.current = null; }
|
|
50
|
+
setRetryIn(null);
|
|
35
51
|
try {
|
|
36
|
-
|
|
52
|
+
// 流式列表**不设超时**(第三个参数 0):冷缓存要逐份解压上百份日志,本机实测
|
|
53
|
+
// 41~226 秒;套 30 秒上限会把「慢但正常」的请求掐成「读取会话列表失败」。
|
|
54
|
+
const listPromise = window.__scFetch("/api/session-conductor/list", { headers: { Accept: "application/x-ndjson" } }, 0);
|
|
37
55
|
const groupPromise = window.__scFetch("/api/session-conductor/group/list")
|
|
38
56
|
.then((r) => (r.ok ? r.json() : { ok: false })).catch(() => ({ ok: false }));
|
|
39
57
|
const listRes = await listPromise;
|
|
@@ -73,10 +91,18 @@
|
|
|
73
91
|
const groupRes = await groupPromise;
|
|
74
92
|
setGroups(groupRes?.ok === true ? groupRes.workspaces ?? [] : null);
|
|
75
93
|
setError(null);
|
|
94
|
+
listRetryRef.current = 0; // 成功一次即清零,下次失败从最短退避重来
|
|
76
95
|
} catch (cause) {
|
|
77
96
|
setError(cause instanceof Error ? cause.message : String(cause));
|
|
97
|
+
const delay = LIST_RETRY_DELAYS[listRetryRef.current];
|
|
98
|
+
if (delay !== void 0) {
|
|
99
|
+
listRetryRef.current += 1;
|
|
100
|
+
setRetryIn(Math.round(delay / 1000));
|
|
101
|
+
listTimerRef.current = setTimeout(() => { refreshRef.current?.(); }, delay);
|
|
102
|
+
}
|
|
78
103
|
}
|
|
79
104
|
}, []);
|
|
105
|
+
refreshRef.current = refresh;
|
|
80
106
|
|
|
81
107
|
// 启动即自动加载(插件挂载就预取一次,不依赖面板打开);打开面板时再刷新一次保证最新
|
|
82
108
|
react.useEffect(() => {
|
|
@@ -768,7 +794,33 @@
|
|
|
768
794
|
}),
|
|
769
795
|
(0, react_jsx_runtime.jsx)("div", {
|
|
770
796
|
style: S.rowSid,
|
|
771
|
-
title: "
|
|
797
|
+
title: "点击复制会话 id",
|
|
798
|
+
// 单击复制:优先 Clipboard API,退回 execCommand(旧版宿主/非安全上下文);
|
|
799
|
+
// 成功后把这段文字临时换成「✅ 已复制」再复原 —— 不依赖宿主 toast(旧版未必有)。
|
|
800
|
+
onClick: (event) => {
|
|
801
|
+
event.stopPropagation();
|
|
802
|
+
const el = event.currentTarget;
|
|
803
|
+
const flash = () => {
|
|
804
|
+
try {
|
|
805
|
+
const prev = el.textContent;
|
|
806
|
+
el.textContent = "✅ 已复制";
|
|
807
|
+
setTimeout(() => { el.textContent = prev; }, 1200);
|
|
808
|
+
} catch { /* 提示失败不影响复制 */ }
|
|
809
|
+
};
|
|
810
|
+
try {
|
|
811
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
812
|
+
navigator.clipboard.writeText(session.id).then(flash, () => {});
|
|
813
|
+
} else {
|
|
814
|
+
const ta = document.createElement("textarea");
|
|
815
|
+
ta.value = session.id;
|
|
816
|
+
document.body.appendChild(ta);
|
|
817
|
+
ta.select();
|
|
818
|
+
document.execCommand("copy");
|
|
819
|
+
ta.remove();
|
|
820
|
+
flash();
|
|
821
|
+
}
|
|
822
|
+
} catch { /* 复制失败不阻塞面板 */ }
|
|
823
|
+
},
|
|
772
824
|
children: session.id,
|
|
773
825
|
}),
|
|
774
826
|
],
|
|
@@ -1189,6 +1241,7 @@
|
|
|
1189
1241
|
style: S.error,
|
|
1190
1242
|
children: [
|
|
1191
1243
|
t("list.error", { message: error }),
|
|
1244
|
+
retryIn !== null && t("list.autoRetry", { n: retryIn }),
|
|
1192
1245
|
" ",
|
|
1193
1246
|
(0, react_jsx_runtime.jsx)("button", {
|
|
1194
1247
|
style: { ...S.actionBtn(false), marginLeft: 6 },
|
|
@@ -15,10 +15,16 @@
|
|
|
15
15
|
|
|
16
16
|
// 全局 fetch 超时兜底:DSH API 挂起(重负载/路由未注册)时界面不再无限等待。
|
|
17
17
|
// 与 fetch 同签名(url, opts, timeoutMs),默认 30s;分片统一走 __scFetch。
|
|
18
|
+
// timeoutMs 传 0(或负数)= **不设超时**:只适合「本来就慢、但会持续吐数据」的流式接口
|
|
19
|
+
// (会话列表冷缓存要逐份解压上百份日志,实测 41~226 秒;套 30 秒上限必然被中途掐断,
|
|
20
|
+
// 表现为面板刚重启就报「读取会话列表失败」,等缓存建好后重试又正常)。
|
|
21
|
+
// 这类接口的正确判据不是「多久没回完」,而是「有没有在吐数据」,由调用方自己盯流。
|
|
18
22
|
const FETCH_TIMEOUT_MS = 30000;
|
|
19
23
|
window.__scFetch = function (url, opts, timeoutMs) {
|
|
24
|
+
const budget = timeoutMs === void 0 ? FETCH_TIMEOUT_MS : timeoutMs;
|
|
25
|
+
if (!(budget > 0)) return fetch(url, opts || {});
|
|
20
26
|
const controller = new AbortController();
|
|
21
|
-
const timer = setTimeout(() => controller.abort(),
|
|
27
|
+
const timer = setTimeout(() => controller.abort(), budget);
|
|
22
28
|
const next = opts ? Object.assign({}, opts, { signal: controller.signal }) : { signal: controller.signal };
|
|
23
29
|
return fetch(url, next).finally(() => clearTimeout(timer));
|
|
24
30
|
};
|
package/lib/client.js
CHANGED
|
@@ -92,10 +92,16 @@ window.__ModuleLoader__.load({
|
|
|
92
92
|
|
|
93
93
|
// 全局 fetch 超时兜底:DSH API 挂起(重负载/路由未注册)时界面不再无限等待。
|
|
94
94
|
// 与 fetch 同签名(url, opts, timeoutMs),默认 30s;分片统一走 __scFetch。
|
|
95
|
+
// timeoutMs 传 0(或负数)= **不设超时**:只适合「本来就慢、但会持续吐数据」的流式接口
|
|
96
|
+
// (会话列表冷缓存要逐份解压上百份日志,实测 41~226 秒;套 30 秒上限必然被中途掐断,
|
|
97
|
+
// 表现为面板刚重启就报「读取会话列表失败」,等缓存建好后重试又正常)。
|
|
98
|
+
// 这类接口的正确判据不是「多久没回完」,而是「有没有在吐数据」,由调用方自己盯流。
|
|
95
99
|
const FETCH_TIMEOUT_MS = 30000;
|
|
96
100
|
window.__scFetch = function (url, opts, timeoutMs) {
|
|
101
|
+
const budget = timeoutMs === void 0 ? FETCH_TIMEOUT_MS : timeoutMs;
|
|
102
|
+
if (!(budget > 0)) return fetch(url, opts || {});
|
|
97
103
|
const controller = new AbortController();
|
|
98
|
-
const timer = setTimeout(() => controller.abort(),
|
|
104
|
+
const timer = setTimeout(() => controller.abort(), budget);
|
|
99
105
|
const next = opts ? Object.assign({}, opts, { signal: controller.signal }) : { signal: controller.signal };
|
|
100
106
|
return fetch(url, next).finally(() => clearTimeout(timer));
|
|
101
107
|
};
|
|
@@ -414,6 +420,7 @@ window.__ModuleLoader__.load({
|
|
|
414
420
|
const [groups, setGroups] = react.useState(null); // workspaces[](group/list);null = host 无分组 API
|
|
415
421
|
const [collapsed, setCollapsed] = react.useState(() => new Set()); // 折叠的分组(含 "__ungrouped__")
|
|
416
422
|
const [error, setError] = react.useState(null);
|
|
423
|
+
const [retryIn, setRetryIn] = react.useState(null); // 自动重试倒计时(秒);null = 没有待重试的定时器
|
|
417
424
|
const [busy, setBusy] = react.useState(null);
|
|
418
425
|
// —— 搜索 + 批量/按条件删除 ——
|
|
419
426
|
const [query, setQuery] = react.useState(""); // 列表过滤关键词
|
|
@@ -440,9 +447,26 @@ window.__ModuleLoader__.load({
|
|
|
440
447
|
next.splice(i, 0, item);
|
|
441
448
|
return next;
|
|
442
449
|
};
|
|
450
|
+
// 列表拉取失败自动重试:DSH 刚重启时插件路由/宿主服务未必已就绪,首次请求会直接
|
|
451
|
+
// 网络失败(Failed to fetch),冷缓存首次构建也很慢 —— 靠用户手动点「重试」既费事又
|
|
452
|
+
// 容易误以为插件坏了。这里按退避自动重试,界面照常给出失败原因 + 下次重试倒计时,
|
|
453
|
+
// 成功一次即清零。数组长度 = 最多自动重试几次。
|
|
454
|
+
const LIST_RETRY_DELAYS = [3000, 8000, 20000, 45000];
|
|
455
|
+
const listRetryRef = react.useRef(0);
|
|
456
|
+
const listTimerRef = react.useRef(null);
|
|
457
|
+
const refreshRef = react.useRef(null);
|
|
458
|
+
// 卸载时清掉待重试的定时器,避免组件已销毁还在拉数据(也会让「已关闭面板」继续打后端)
|
|
459
|
+
react.useEffect(() => () => {
|
|
460
|
+
if (listTimerRef.current) clearTimeout(listTimerRef.current);
|
|
461
|
+
listTimerRef.current = null;
|
|
462
|
+
}, []);
|
|
443
463
|
const refresh = react.useCallback(async () => {
|
|
464
|
+
if (listTimerRef.current) { clearTimeout(listTimerRef.current); listTimerRef.current = null; }
|
|
465
|
+
setRetryIn(null);
|
|
444
466
|
try {
|
|
445
|
-
|
|
467
|
+
// 流式列表**不设超时**(第三个参数 0):冷缓存要逐份解压上百份日志,本机实测
|
|
468
|
+
// 41~226 秒;套 30 秒上限会把「慢但正常」的请求掐成「读取会话列表失败」。
|
|
469
|
+
const listPromise = window.__scFetch("/api/session-conductor/list", { headers: { Accept: "application/x-ndjson" } }, 0);
|
|
446
470
|
const groupPromise = window.__scFetch("/api/session-conductor/group/list")
|
|
447
471
|
.then((r) => (r.ok ? r.json() : { ok: false })).catch(() => ({ ok: false }));
|
|
448
472
|
const listRes = await listPromise;
|
|
@@ -482,10 +506,18 @@ window.__ModuleLoader__.load({
|
|
|
482
506
|
const groupRes = await groupPromise;
|
|
483
507
|
setGroups(groupRes?.ok === true ? groupRes.workspaces ?? [] : null);
|
|
484
508
|
setError(null);
|
|
509
|
+
listRetryRef.current = 0; // 成功一次即清零,下次失败从最短退避重来
|
|
485
510
|
} catch (cause) {
|
|
486
511
|
setError(cause instanceof Error ? cause.message : String(cause));
|
|
512
|
+
const delay = LIST_RETRY_DELAYS[listRetryRef.current];
|
|
513
|
+
if (delay !== void 0) {
|
|
514
|
+
listRetryRef.current += 1;
|
|
515
|
+
setRetryIn(Math.round(delay / 1000));
|
|
516
|
+
listTimerRef.current = setTimeout(() => { refreshRef.current?.(); }, delay);
|
|
517
|
+
}
|
|
487
518
|
}
|
|
488
519
|
}, []);
|
|
520
|
+
refreshRef.current = refresh;
|
|
489
521
|
|
|
490
522
|
// 启动即自动加载(插件挂载就预取一次,不依赖面板打开);打开面板时再刷新一次保证最新
|
|
491
523
|
react.useEffect(() => {
|
|
@@ -1177,7 +1209,33 @@ window.__ModuleLoader__.load({
|
|
|
1177
1209
|
}),
|
|
1178
1210
|
(0, react_jsx_runtime.jsx)("div", {
|
|
1179
1211
|
style: S.rowSid,
|
|
1180
|
-
title: "
|
|
1212
|
+
title: "点击复制会话 id",
|
|
1213
|
+
// 单击复制:优先 Clipboard API,退回 execCommand(旧版宿主/非安全上下文);
|
|
1214
|
+
// 成功后把这段文字临时换成「✅ 已复制」再复原 —— 不依赖宿主 toast(旧版未必有)。
|
|
1215
|
+
onClick: (event) => {
|
|
1216
|
+
event.stopPropagation();
|
|
1217
|
+
const el = event.currentTarget;
|
|
1218
|
+
const flash = () => {
|
|
1219
|
+
try {
|
|
1220
|
+
const prev = el.textContent;
|
|
1221
|
+
el.textContent = "✅ 已复制";
|
|
1222
|
+
setTimeout(() => { el.textContent = prev; }, 1200);
|
|
1223
|
+
} catch { /* 提示失败不影响复制 */ }
|
|
1224
|
+
};
|
|
1225
|
+
try {
|
|
1226
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
1227
|
+
navigator.clipboard.writeText(session.id).then(flash, () => {});
|
|
1228
|
+
} else {
|
|
1229
|
+
const ta = document.createElement("textarea");
|
|
1230
|
+
ta.value = session.id;
|
|
1231
|
+
document.body.appendChild(ta);
|
|
1232
|
+
ta.select();
|
|
1233
|
+
document.execCommand("copy");
|
|
1234
|
+
ta.remove();
|
|
1235
|
+
flash();
|
|
1236
|
+
}
|
|
1237
|
+
} catch { /* 复制失败不阻塞面板 */ }
|
|
1238
|
+
},
|
|
1181
1239
|
children: session.id,
|
|
1182
1240
|
}),
|
|
1183
1241
|
],
|
|
@@ -1598,6 +1656,7 @@ window.__ModuleLoader__.load({
|
|
|
1598
1656
|
style: S.error,
|
|
1599
1657
|
children: [
|
|
1600
1658
|
t("list.error", { message: error }),
|
|
1659
|
+
retryIn !== null && t("list.autoRetry", { n: retryIn }),
|
|
1601
1660
|
" ",
|
|
1602
1661
|
(0, react_jsx_runtime.jsx)("button", {
|
|
1603
1662
|
style: { ...S.actionBtn(false), marginLeft: 6 },
|
|
@@ -2106,10 +2165,24 @@ window.__ModuleLoader__.load({
|
|
|
2106
2165
|
locale: NS,
|
|
2107
2166
|
inject: () => ({
|
|
2108
2167
|
onOpenSession: (sessionId) => {
|
|
2168
|
+
// 打开会话的真实入口随宿主版本不同,按优先级尝试(**不要只认一个**):
|
|
2169
|
+
// · workspaceNavigation.openSession(id) —— dsh-client-ui-conversation 的用法
|
|
2170
|
+
// · ctx.uiWorkspace.openSession(id) —— dsh-client-ui-chat 的用法
|
|
2171
|
+
// · sessions.open(id) —— 早期路径,保留兜底
|
|
2172
|
+
// 以前只写了第三个,而宿主客户端并没有 `sessions` 这个服务 ⇒ 可选链把失败吞成
|
|
2173
|
+
// 「点了没反应」,且不报错、难查。所以这里:多个入口都试;全失败要留下痕迹。
|
|
2109
2174
|
try {
|
|
2110
|
-
ctx.get("
|
|
2111
|
-
|
|
2112
|
-
|
|
2175
|
+
const nav = ctx.get?.("workspaceNavigation");
|
|
2176
|
+
if (typeof nav?.openSession === "function") return nav.openSession(sessionId);
|
|
2177
|
+
if (typeof ctx.uiWorkspace?.openSession === "function") return ctx.uiWorkspace.openSession(sessionId);
|
|
2178
|
+
const legacy = ctx.get?.("sessions");
|
|
2179
|
+
if (typeof legacy?.open === "function") return legacy.open(sessionId);
|
|
2180
|
+
// eslint-disable-next-line no-console
|
|
2181
|
+
console.warn("[session-conductor] 打不开会话:宿主客户端未暴露 workspaceNavigation/uiWorkspace/sessions 任一入口", sessionId);
|
|
2182
|
+
} catch (error) {
|
|
2183
|
+
// 打开失败不阻塞面板,但不再静默
|
|
2184
|
+
// eslint-disable-next-line no-console
|
|
2185
|
+
console.warn("[session-conductor] 打开会话失败:", error);
|
|
2113
2186
|
}
|
|
2114
2187
|
},
|
|
2115
2188
|
}),
|
|
@@ -2127,7 +2200,7 @@ window.__ModuleLoader__.load({
|
|
|
2127
2200
|
* 本卡只保留:
|
|
2128
2201
|
* · 分组列表(GET /api/session-conductor/group/list,只读展示)
|
|
2129
2202
|
* · 每行「在分组下新建会话」(POST /api/session-conductor/group/new-session {workspaceId})
|
|
2130
|
-
* ·
|
|
2203
|
+
* · 「新建会话(上一次使用的工作区)」(new-session 不带 workspaceId = 最近活跃会话 cwd 归属分组)
|
|
2131
2204
|
*/
|
|
2132
2205
|
|
|
2133
2206
|
return module.exports;
|
|
@@ -180,7 +180,15 @@ export function formatRecallBlock(turns, opts = {}) {
|
|
|
180
180
|
kept.unshift(block);
|
|
181
181
|
used += block.length;
|
|
182
182
|
}
|
|
183
|
-
|
|
183
|
+
// 有轮次因总长上限被丢掉时**如实说明**:静默丢轮次是「要 30 轮只回 5 轮」那个 bug 的根因之一,
|
|
184
|
+
// 调用方需要知道自己拿到的是残缺结果、以及怎么取到更早的内容。
|
|
185
|
+
const dropped = list.length - kept.length;
|
|
186
|
+
const tail = dropped > 0
|
|
187
|
+
? `\n\n(⚠️ 受总长上限 ${maxTotal} 字符限制,最早的 ${dropped} 轮未包含;需要更早的内容请调小 rounds 分次取)`
|
|
188
|
+
: "";
|
|
189
|
+
// 输出末尾附上实际字符数:调用方据此判断这次取回的代价(也便于对照 maxTotalChars 的截断阈值)。
|
|
190
|
+
const body = head + kept.join("") + tail;
|
|
191
|
+
return `${body}\n\n(本次输出正文 ${body.length} 字符)`;
|
|
184
192
|
}
|
|
185
193
|
|
|
186
194
|
/**
|
|
@@ -291,7 +299,9 @@ export function registerMemoryRecallTool(ctx, defineTool, tools, deps = {}) {
|
|
|
291
299
|
});
|
|
292
300
|
if (!turns || turns.length === 0) return "没取到可用的记忆(日志缺失,或该会话还没有用户消息)。";
|
|
293
301
|
const text = format(turns, {
|
|
294
|
-
|
|
302
|
+
// 手动路径的总额**按请求规模算**(rounds × 单条上限),不套自动注入那条 4000 字符的紧上限:
|
|
303
|
+
// 调用方明确要 N 轮,就该给 N 轮;套紧上限会把最早的轮次静默丢掉(实测 rounds:30 只剩 5 轮)。
|
|
304
|
+
maxTotalChars: rounds * (config?.retrieveMemoryMaxCharsPerItem ?? 800),
|
|
295
305
|
manual: true,
|
|
296
306
|
// 跨会话时标注来源,并写明只作背景——别把别处的文字当成当前这轮的要求
|
|
297
307
|
...(fromOther ? { sourceNote: `来自另一个会话 ${targetId}` } : {}),
|
package/lib/i18n/en.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"list.activeEmpty": "No active sessions (live/running)",
|
|
19
19
|
"list.loading": "Loading…",
|
|
20
20
|
"list.error": "Failed to load sessions: {message}",
|
|
21
|
+
"list.autoRetry": " (retrying automatically in {n}s)",
|
|
21
22
|
"group.ungrouped": "Ungrouped",
|
|
22
23
|
"badge.archived": "Archived",
|
|
23
24
|
"badge.running": "Running",
|
package/lib/i18n/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-conductor",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.7",
|
|
4
4
|
"description": "DSH session management enhancement: archive/restore sessions, batch & conditional delete, AI auto-rename, interrupted-session auto-resume, release idle sessions, workspace grouping, undo last message, repair damaged session logs, value analysis, full-text search, per-member model switching (list_models / set_member_model), and plan/closing template injection (system-prompt + context).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|