@zaofan/dsh-qqbot 1.10.1 → 1.10.3

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.
@@ -1,180 +1,180 @@
1
- /**
2
- * sessions_list.mjs — 列出 DSH 的工作区与各会话(跨工作区 / 跨会话,纯只读)
3
- *
4
- * 定位:这是「把子代理那套搬到跨会话级」的第 1 块积木 —— 先能看见,再谈操作。
5
- * expose: 'all' ⇒ 所有通道(QQ / Web / 终端…)的会话都能用;env.ctx 是宿主的 cordis ctx。
6
- *
7
- * 数据来源(都是宿主现成服务,不自己扫盘):
8
- * · ctx.workspaceRegistry (@deepseek-ai/dsh-workspace) —— 有序、持久的工作区项目列表
9
- * · ctx.sessionQuery (@deepseek-ai/dsh-session-query) —— 列会话(最新在前)、过滤、trace
10
- * ⚠️ 取服务用 cordis 运行时 `ctx.get(name)`,不需要 inject(见 qqbot 的实测经验);
11
- * 并且优先用 `ctx.root`(插件根作用域)—— 会话作用域的 ctx 会随会话切换而失效。
12
- *
13
- * 失败哲学:任何一个服务缺失都只降级、不报错中断 —— 把"哪些服务可用"如实报出来,
14
- * 方便在别的 DSH 版本上继续排查。
15
- */
16
- const WORKSPACE_SERVICE = 'workspaceRegistry';
17
- const SESSION_SERVICE = 'sessionQuery';
18
- /** 探测用的候选服务名(有就报出来,便于后续工具挑正确的那个) */
19
- const PROBE = [WORKSPACE_SERVICE, SESSION_SERVICE, 'sessions', 'sessionPersistence', 'tools', 'systemPrompt', 'agents', 'goals'];
20
-
21
- /** 宽松取字段:宿主返回结构可能随版本变,这里按候选名依次试 */
22
- function pick(obj, names) {
23
- if (!obj || typeof obj !== 'object') return undefined;
24
- for (const n of names) {
25
- const v = obj[n];
26
- if (v !== undefined && v !== null) return v;
27
- }
28
- const h = obj.header;
29
- if (h && typeof h === 'object') {
30
- for (const n of names) {
31
- const v = h[n];
32
- if (v !== undefined && v !== null) return v;
33
- }
34
- }
35
- return undefined;
36
- }
37
-
38
- /** 时间戳 → 本地 MM-dd HH:mm(吃 number / string / Date) */
39
- function fmtTime(v) {
40
- if (v === undefined || v === null) return '';
41
- try {
42
- const d = v instanceof Date ? v : new Date(typeof v === 'number' ? v : String(v));
43
- if (Number.isNaN(d.getTime())) return '';
44
- // ⚠️ 用 UTC 字段显示:实测宿主把**本地时间**当 UTC 存
45
- // (createdAt=1791762071432 → UTC 23:41 才与主人的实际时间对得上;按本地读会多 8 小时)。
46
- const p = (n) => String(n).padStart(2, '0');
47
- return `${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
48
- } catch { return ''; }
49
- }
50
-
51
- function safeGet(root, name) {
52
- try { return root.get(name); } catch { return undefined; }
53
- }
54
-
55
- export default {
56
- name: 'sessions_list',
57
- description:
58
- '列出 DSH 的工作区及其会话(跨工作区 / 跨会话):默认按最新活跃在前,可用 limit 控制条数,可只看某个工作区。纯只读。',
59
- expose: 'all',
60
- inputSchema: {
61
- workspace: { type: 'string', description: '只看标题/路径里含这段文字的工作区(可选;不填=全部)' },
62
- limit: { type: 'integer', description: '每个工作区最多列几个会话(默认 10,上限 50)' },
63
- debug: { type: 'boolean', description: 'true=附上服务探测结果与一条原始会话记录(排查用,默认 false)' },
64
- full: { type: 'boolean', description: 'true=显示完整 session id 与血缘(parentSession/origin/depth),编排用(默认 false=精简)' },
65
- },
66
-
67
- async run(args = {}, env = {}) {
68
- const ctx = env?.ctx ?? null;
69
- const root = ctx && typeof ctx === 'object' && ctx.root ? ctx.root : ctx;
70
- if (!root || typeof root.get !== 'function') {
71
- return { ok: false, msg: '拿不到宿主 ctx(env.ctx 缺失或没有 .get)—— 本工具需要 expose:\'all\' 才能在跨通道环境里拿到宿主句柄' };
72
- }
73
-
74
- const limit = Math.max(1, Math.min(50, Number(args?.limit) > 0 ? Math.floor(Number(args.limit)) : 10));
75
- const filter = String(args?.workspace ?? '').trim().toLowerCase();
76
- const lines = [];
77
- const notes = [];
78
-
79
- // ── 服务探测(如实报出可用性,缺了也能看出原因)──
80
- const probe = {};
81
- for (const n of PROBE) {
82
- const s = safeGet(root, n);
83
- probe[n] = s === undefined || s === null ? null : typeof s;
84
- }
85
- const reg = safeGet(root, WORKSPACE_SERVICE);
86
- const sq = safeGet(root, SESSION_SERVICE);
87
-
88
- // ── ① 工作区列表 ──
89
- let projects = [];
90
- if (reg && typeof reg.list === 'function') {
91
- try { projects = reg.list() || []; } catch (e) { notes.push('workspaceRegistry.list() 抛错: ' + (e?.message ?? e)); }
92
- } else if (!reg) {
93
- notes.push(`宿主没挂 ${WORKSPACE_SERVICE} 服务(工作区分组不可用)`);
94
- }
95
- if (!Array.isArray(projects)) projects = [];
96
-
97
- // ── ② 会话列表(sessionQuery 优先)──
98
- let sessions = [];
99
- if (sq && typeof sq.listSessions === 'function') {
100
- try { sessions = (await sq.listSessions()) || []; } catch (e) { notes.push('sessionQuery.listSessions() 抛错: ' + (e?.message ?? e)); }
101
- } else if (!sq) {
102
- notes.push(`宿主没挂 ${SESSION_SERVICE} 服务(会话查询不可用)`);
103
- }
104
- if (!Array.isArray(sessions)) sessions = [];
105
-
106
- // 会话按"最新活跃"排序:优先 lastActivity / updatedAt / 创建时间,取不到的排最后
107
- const timeOf = (s) => {
108
- const t = pick(s, ['lastActivity', 'lastActivityAt', 'updatedAt', 'lastMessageAt', 'createdAt', 'createdAtMs', 'timestamp', 'startedAt']);
109
- const d = t instanceof Date ? t : new Date(typeof t === 'number' ? t : String(t ?? ''));
110
- return Number.isNaN(d.getTime()) ? 0 : d.getTime();
111
- };
112
- sessions.sort((a, b) => timeOf(b) - timeOf(a));
113
-
114
- // 归属:一个会话只归给它 cwd 最长的那个工作区(否则父目录 aiwork 会把子目录鲸鱼娘的会话全列走)
115
- const owners = new Map();
116
- for (const s of sessions) {
117
- const cwd = String(pick(s, ['cwd', 'workdir', 'directory', 'workingDirectory']) ?? '');
118
- if (!cwd) continue;
119
- let best = null;
120
- for (const p of projects) {
121
- const pd = String(pick(p, ['path', 'directory', 'dir', 'cwd']) ?? '');
122
- if (!pd) continue;
123
- if (cwd === pd) { const hit = pd.length + 10000; if (!best || hit > best.len) best = { len: hit, p }; continue; }
124
- if (cwd.startsWith(pd + '\\') || cwd.startsWith(pd + '/')) { const hit = pd.length; if (!best || hit > best.len) best = { len: hit, p }; }
125
- }
126
- if (best) owners.set(s, best.p);
127
- }
128
-
129
- const full = !!args?.full;
130
- const line = (s) => {
131
- const id = String(pick(s, ['id', 'sessionId', 'sid']) ?? '?');
132
- const title = String(pick(s, ['title', 'name', 'summary']) ?? '').slice(0, 40);
133
- const live = pick(s, ['live', 'isLive']) ? 'live' : 'persisted';
134
- const t = fmtTime(pick(s, ['lastActivity', 'lastActivityAt', 'updatedAt', 'createdAt', 'createdAtMs', 'startedAt']));
135
- const short = id.length > 14 && !full ? id.slice(0, 8) + '…' + id.slice(-4) : id;
136
- // 血缘(子代理实测建议:跨会话编排必须看到 parent/origin/depth,否则只能另开 debug 手工比对)
137
- const origin = pick(s, ['origin']) ?? '';
138
- const depth = pick(s, ['delegationDepth']);
139
- const parent = String(pick(s, ['parentSession']) ?? '');
140
- const lin = origin
141
- ? ` origin=${origin}${depth !== undefined ? ' depth=' + depth : ''}${parent ? ' parent=' + (full ? parent : parent.slice(0, 8) + '…') : ''}`
142
- : '';
143
- return ` · ${t || ' '} [${live}] ${short}${title ? ' ' + title : ''}${lin}`;
144
- };
145
-
146
- // 有 workspaceRegistry 就按"目录 → 它的会话"分组;否则退化成一张平表
147
- if (projects.length) {
148
- for (const p of projects) {
149
- const ptitle = String(pick(p, ['title', 'name']) ?? '(未命名)');
150
- const pdir = String(pick(p, ['path', 'directory', 'dir', 'cwd']) ?? '');
151
- if (filter && !(ptitle.toLowerCase().includes(filter) || pdir.toLowerCase().includes(filter))) continue;
152
- const mine = sessions.filter((s) => owners.get(s) === p);
153
- lines.push(`${ptitle}${pdir ? ' (' + pdir + ')' : ''} —— ${mine.length} 个会话`);
154
- if (!mine.length) lines.push(' · (这个工作区下没有可列的会话)');
155
- for (const s of mine.slice(0, limit)) lines.push(line(s));
156
- if (mine.length > limit) lines.push(` … 还有 ${mine.length - limit} 个(用 limit 调整)`);
157
- }
158
- // 不属于任何工作区的会话
159
- const ungrouped = sessions.filter((s) => !owners.get(s));
160
- if (ungrouped.length && !filter) {
161
- lines.push(`(不属于任何工作区的会话) —— ${ungrouped.length} 个`);
162
- for (const s of ungrouped.slice(0, limit)) lines.push(line(s));
163
- }
164
- } else {
165
- lines.push(`(工作区分组不可用,退化成会话平表)共 ${sessions.length} 个`);
166
- for (const s of sessions.slice(0, limit)) lines.push(line(s));
167
- }
168
-
169
- // 注(2026-10-12 主人定):**不再**在首行显示"我 / 父代"。
170
- // 会话来源改用另一条更直接的约定:**模拟用户消息的首行直接给出源会话 id**
171
- // (见 session_send 与 lib/session_watch_lib.mjs)。
172
- const head = `【DSH 工作区与会话】 工作区 ${projects.length} 个 / 会话 ${sessions.length} 个${filter ? `(筛选: ${args.workspace})` : ''}`;
173
- const tail = notes.length ? '\n' + notes.map((n) => '⚠ ' + n).join('\n') : '';
174
- const dbg = args?.debug
175
- ? '\n— 服务探测: ' + Object.entries(probe).map(([k, v]) => `${k}=${v ?? '✗'}`).join(', ') +
176
- '\n— 首条会话原始结构: ' + JSON.stringify(sessions[0] ?? null).slice(0, 600)
177
- : '';
178
- return { ok: true, msg: [head, ...lines].join('\n') + tail + dbg };
179
- },
180
- };
1
+ /**
2
+ * sessions_list.mjs — 列出 DSH 的工作区与各会话(跨工作区 / 跨会话,纯只读)
3
+ *
4
+ * 定位:这是「把子代理那套搬到跨会话级」的第 1 块积木 —— 先能看见,再谈操作。
5
+ * expose: 'all' ⇒ 所有通道(QQ / Web / 终端…)的会话都能用;env.ctx 是宿主的 cordis ctx。
6
+ *
7
+ * 数据来源(都是宿主现成服务,不自己扫盘):
8
+ * · ctx.workspaceRegistry (@deepseek-ai/dsh-workspace) —— 有序、持久的工作区项目列表
9
+ * · ctx.sessionQuery (@deepseek-ai/dsh-session-query) —— 列会话(最新在前)、过滤、trace
10
+ * ⚠️ 取服务用 cordis 运行时 `ctx.get(name)`,不需要 inject(见 qqbot 的实测经验);
11
+ * 并且优先用 `ctx.root`(插件根作用域)—— 会话作用域的 ctx 会随会话切换而失效。
12
+ *
13
+ * 失败哲学:任何一个服务缺失都只降级、不报错中断 —— 把"哪些服务可用"如实报出来,
14
+ * 方便在别的 DSH 版本上继续排查。
15
+ */
16
+ const WORKSPACE_SERVICE = 'workspaceRegistry';
17
+ const SESSION_SERVICE = 'sessionQuery';
18
+ /** 探测用的候选服务名(有就报出来,便于后续工具挑正确的那个) */
19
+ const PROBE = [WORKSPACE_SERVICE, SESSION_SERVICE, 'sessions', 'sessionPersistence', 'tools', 'systemPrompt', 'agents', 'goals'];
20
+
21
+ /** 宽松取字段:宿主返回结构可能随版本变,这里按候选名依次试 */
22
+ function pick(obj, names) {
23
+ if (!obj || typeof obj !== 'object') return undefined;
24
+ for (const n of names) {
25
+ const v = obj[n];
26
+ if (v !== undefined && v !== null) return v;
27
+ }
28
+ const h = obj.header;
29
+ if (h && typeof h === 'object') {
30
+ for (const n of names) {
31
+ const v = h[n];
32
+ if (v !== undefined && v !== null) return v;
33
+ }
34
+ }
35
+ return undefined;
36
+ }
37
+
38
+ /** 时间戳 → 本地 MM-dd HH:mm(吃 number / string / Date) */
39
+ function fmtTime(v) {
40
+ if (v === undefined || v === null) return '';
41
+ try {
42
+ const d = v instanceof Date ? v : new Date(typeof v === 'number' ? v : String(v));
43
+ if (Number.isNaN(d.getTime())) return '';
44
+ // ⚠️ 用 UTC 字段显示:实测宿主把**本地时间**当 UTC 存
45
+ // (createdAt=1791762071432 → UTC 23:41 才与主人的实际时间对得上;按本地读会多 8 小时)。
46
+ const p = (n) => String(n).padStart(2, '0');
47
+ return `${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
48
+ } catch { return ''; }
49
+ }
50
+
51
+ function safeGet(root, name) {
52
+ try { return root.get(name); } catch { return undefined; }
53
+ }
54
+
55
+ export default {
56
+ name: 'sessions_list',
57
+ description:
58
+ '列出 DSH 的工作区及其会话(跨工作区 / 跨会话):默认按最新活跃在前,可用 limit 控制条数,可只看某个工作区。纯只读。',
59
+ expose: 'all',
60
+ inputSchema: {
61
+ workspace: { type: 'string', description: '只看标题/路径里含这段文字的工作区(可选;不填=全部)' },
62
+ limit: { type: 'integer', description: '每个工作区最多列几个会话(默认 10,上限 50)' },
63
+ debug: { type: 'boolean', description: 'true=附上服务探测结果与一条原始会话记录(排查用,默认 false)' },
64
+ full: { type: 'boolean', description: 'true=显示完整 session id 与血缘(parentSession/origin/depth),编排用(默认 false=精简)' },
65
+ },
66
+
67
+ async run(args = {}, env = {}) {
68
+ const ctx = env?.ctx ?? null;
69
+ const root = ctx && typeof ctx === 'object' && ctx.root ? ctx.root : ctx;
70
+ if (!root || typeof root.get !== 'function') {
71
+ return { ok: false, msg: '拿不到宿主 ctx(env.ctx 缺失或没有 .get)—— 本工具需要 expose:\'all\' 才能在跨通道环境里拿到宿主句柄' };
72
+ }
73
+
74
+ const limit = Math.max(1, Math.min(50, Number(args?.limit) > 0 ? Math.floor(Number(args.limit)) : 10));
75
+ const filter = String(args?.workspace ?? '').trim().toLowerCase();
76
+ const lines = [];
77
+ const notes = [];
78
+
79
+ // ── 服务探测(如实报出可用性,缺了也能看出原因)──
80
+ const probe = {};
81
+ for (const n of PROBE) {
82
+ const s = safeGet(root, n);
83
+ probe[n] = s === undefined || s === null ? null : typeof s;
84
+ }
85
+ const reg = safeGet(root, WORKSPACE_SERVICE);
86
+ const sq = safeGet(root, SESSION_SERVICE);
87
+
88
+ // ── ① 工作区列表 ──
89
+ let projects = [];
90
+ if (reg && typeof reg.list === 'function') {
91
+ try { projects = reg.list() || []; } catch (e) { notes.push('workspaceRegistry.list() 抛错: ' + (e?.message ?? e)); }
92
+ } else if (!reg) {
93
+ notes.push(`宿主没挂 ${WORKSPACE_SERVICE} 服务(工作区分组不可用)`);
94
+ }
95
+ if (!Array.isArray(projects)) projects = [];
96
+
97
+ // ── ② 会话列表(sessionQuery 优先)──
98
+ let sessions = [];
99
+ if (sq && typeof sq.listSessions === 'function') {
100
+ try { sessions = (await sq.listSessions()) || []; } catch (e) { notes.push('sessionQuery.listSessions() 抛错: ' + (e?.message ?? e)); }
101
+ } else if (!sq) {
102
+ notes.push(`宿主没挂 ${SESSION_SERVICE} 服务(会话查询不可用)`);
103
+ }
104
+ if (!Array.isArray(sessions)) sessions = [];
105
+
106
+ // 会话按"最新活跃"排序:优先 lastActivity / updatedAt / 创建时间,取不到的排最后
107
+ const timeOf = (s) => {
108
+ const t = pick(s, ['lastActivity', 'lastActivityAt', 'updatedAt', 'lastMessageAt', 'createdAt', 'createdAtMs', 'timestamp', 'startedAt']);
109
+ const d = t instanceof Date ? t : new Date(typeof t === 'number' ? t : String(t ?? ''));
110
+ return Number.isNaN(d.getTime()) ? 0 : d.getTime();
111
+ };
112
+ sessions.sort((a, b) => timeOf(b) - timeOf(a));
113
+
114
+ // 归属:一个会话只归给它 cwd 最长的那个工作区(否则父目录 aiwork 会把子目录鲸鱼娘的会话全列走)
115
+ const owners = new Map();
116
+ for (const s of sessions) {
117
+ const cwd = String(pick(s, ['cwd', 'workdir', 'directory', 'workingDirectory']) ?? '');
118
+ if (!cwd) continue;
119
+ let best = null;
120
+ for (const p of projects) {
121
+ const pd = String(pick(p, ['path', 'directory', 'dir', 'cwd']) ?? '');
122
+ if (!pd) continue;
123
+ if (cwd === pd) { const hit = pd.length + 10000; if (!best || hit > best.len) best = { len: hit, p }; continue; }
124
+ if (cwd.startsWith(pd + '\\') || cwd.startsWith(pd + '/')) { const hit = pd.length; if (!best || hit > best.len) best = { len: hit, p }; }
125
+ }
126
+ if (best) owners.set(s, best.p);
127
+ }
128
+
129
+ const full = !!args?.full;
130
+ const line = (s) => {
131
+ const id = String(pick(s, ['id', 'sessionId', 'sid']) ?? '?');
132
+ const title = String(pick(s, ['title', 'name', 'summary']) ?? '').slice(0, 40);
133
+ const live = pick(s, ['live', 'isLive']) ? 'live' : 'persisted';
134
+ const t = fmtTime(pick(s, ['lastActivity', 'lastActivityAt', 'updatedAt', 'createdAt', 'createdAtMs', 'startedAt']));
135
+ const short = id.length > 14 && !full ? id.slice(0, 8) + '…' + id.slice(-4) : id;
136
+ // 血缘(子代理实测建议:跨会话编排必须看到 parent/origin/depth,否则只能另开 debug 手工比对)
137
+ const origin = pick(s, ['origin']) ?? '';
138
+ const depth = pick(s, ['delegationDepth']);
139
+ const parent = String(pick(s, ['parentSession']) ?? '');
140
+ const lin = origin
141
+ ? ` origin=${origin}${depth !== undefined ? ' depth=' + depth : ''}${parent ? ' parent=' + (full ? parent : parent.slice(0, 8) + '…') : ''}`
142
+ : '';
143
+ return ` · ${t || ' '} [${live}] ${short}${title ? ' ' + title : ''}${lin}`;
144
+ };
145
+
146
+ // 有 workspaceRegistry 就按"目录 → 它的会话"分组;否则退化成一张平表
147
+ if (projects.length) {
148
+ for (const p of projects) {
149
+ const ptitle = String(pick(p, ['title', 'name']) ?? '(未命名)');
150
+ const pdir = String(pick(p, ['path', 'directory', 'dir', 'cwd']) ?? '');
151
+ if (filter && !(ptitle.toLowerCase().includes(filter) || pdir.toLowerCase().includes(filter))) continue;
152
+ const mine = sessions.filter((s) => owners.get(s) === p);
153
+ lines.push(`${ptitle}${pdir ? ' (' + pdir + ')' : ''} —— ${mine.length} 个会话`);
154
+ if (!mine.length) lines.push(' · (这个工作区下没有可列的会话)');
155
+ for (const s of mine.slice(0, limit)) lines.push(line(s));
156
+ if (mine.length > limit) lines.push(` … 还有 ${mine.length - limit} 个(用 limit 调整)`);
157
+ }
158
+ // 不属于任何工作区的会话
159
+ const ungrouped = sessions.filter((s) => !owners.get(s));
160
+ if (ungrouped.length && !filter) {
161
+ lines.push(`(不属于任何工作区的会话) —— ${ungrouped.length} 个`);
162
+ for (const s of ungrouped.slice(0, limit)) lines.push(line(s));
163
+ }
164
+ } else {
165
+ lines.push(`(工作区分组不可用,退化成会话平表)共 ${sessions.length} 个`);
166
+ for (const s of sessions.slice(0, limit)) lines.push(line(s));
167
+ }
168
+
169
+ // 注(2026-10-12 主人定):**不再**在首行显示"我 / 父代"。
170
+ // 会话来源改用另一条更直接的约定:**模拟用户消息的首行直接给出源会话 id**
171
+ // (见 session_send 与 lib/session_watch_lib.mjs)。
172
+ const head = `【DSH 工作区与会话】 工作区 ${projects.length} 个 / 会话 ${sessions.length} 个${filter ? `(筛选: ${args.workspace})` : ''}`;
173
+ const tail = notes.length ? '\n' + notes.map((n) => '⚠ ' + n).join('\n') : '';
174
+ const dbg = args?.debug
175
+ ? '\n— 服务探测: ' + Object.entries(probe).map(([k, v]) => `${k}=${v ?? '✗'}`).join(', ') +
176
+ '\n— 首条会话原始结构: ' + JSON.stringify(sessions[0] ?? null).slice(0, 600)
177
+ : '';
178
+ return { ok: true, msg: [head, ...lines].join('\n') + tail + dbg };
179
+ },
180
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaofan/dsh-qqbot",
3
- "version": "1.10.1",
3
+ "version": "1.10.3",
4
4
  "description": "QQ Bot IM channel plugin for deepseek-harness (dsh)",
5
5
  "type": "module",
6
6
  "main": "./entry.js",