@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.
- package/docs/USER-GUIDE.md +45 -0
- package/examples/ext-commands//344/274/232/350/257/235/345/210/227/350/241/250.mjs +603 -0
- package/examples/ext-tools/lib/session_watch_lib.mjs +1055 -767
- package/examples/ext-tools/session_watch.mjs +163 -139
- package/examples/ext-tools/sessions_list.mjs +180 -180
- package/package.json +1 -1
|
@@ -1,139 +1,163 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session_watch.mjs — 查看/补挂「回合结束回灌」钩子(含三档 + QQ 转发)
|
|
3
|
-
*
|
|
4
|
-
* 配合 session_create 用:创建时会自动挂钩子(session_create → lib/session_watch_lib.register)。
|
|
5
|
-
* 本工具负责:
|
|
6
|
-
* · list(默认):列出正在盯的子会话(子 id / 父 id / 标签 / **档位** / QQ 转发是否就绪 / 已结算几次)
|
|
7
|
-
* · attach:给一个"已存在的子会话"补挂钩子(创建时漏挂、或插件重启后钩子丢了的情况)
|
|
8
|
-
* · mode:改已挂盯梢的档位(一/二/三档随时可改)
|
|
9
|
-
* · detach:取消盯某个子会话
|
|
10
|
-
*
|
|
11
|
-
* ★ 2026-10-12 主人定:三档(mode)
|
|
12
|
-
* 1 = 一档(默认):末段正文回灌父会话 + **同时**转发到父会话所在的 QQ
|
|
13
|
-
* 2 = 二档:本回合**所有正文 + 报错**都转发 QQ,**不进**父会话上下文
|
|
14
|
-
* 3 = 三档:所有正文 + 报错**进**父会话上下文(**不唤醒**)+ 同时也转发 QQ
|
|
15
|
-
*
|
|
16
|
-
* ⚠️ 主人要求这类工具不给全部会话用:已落地 `expose: 'qq'`(2026-10-12),只在 QQ 会话注册。
|
|
17
|
-
*/
|
|
18
|
-
import { statSync } from 'node:fs';
|
|
19
|
-
import { fileURLToPath } from 'node:url';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 带版本号地加载共用库 —— **必须这么加载**。
|
|
23
|
-
*
|
|
24
|
-
* 原因同 session_create.mjs:插件只给**工具文件本身**的 import 加了 `?v=<mtime>`,
|
|
25
|
-
* 工具内部裸路径 import 会被 ESM 模块缓存永久命中 ⇒ 改 `tools/lib/` 热刷不生效。
|
|
26
|
-
* (这里原来是顶层静态 import,等于永远锁死在插件启动那一刻的旧版本。)
|
|
27
|
-
*/
|
|
28
|
-
async function loadWatchLib() {
|
|
29
|
-
const url = new URL('./lib/session_watch_lib.mjs', import.meta.url);
|
|
30
|
-
let v = '0';
|
|
31
|
-
try { v = String(statSync(fileURLToPath(url)).mtimeMs); } catch { /* 拿不到 mtime 就退回裸 import */ }
|
|
32
|
-
return import(v === '0' ? url.href : `${url.href}?v=${v}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** 档位 → 人话 */
|
|
36
|
-
const modeText = (m) => (Number(m) === 2 ? '二档·只转QQ不进父' : (Number(m) === 3 ? '三档·进父不唤醒+转QQ' : '一档·末段回灌+转QQ'));
|
|
37
|
-
/** 把用户给的档位写法归一(收 1/2/3 与中文「一档/二档/三档」) */
|
|
38
|
-
function normMode(v) {
|
|
39
|
-
const s = String(v ?? '').trim();
|
|
40
|
-
if (/^(2|二)/.test(s)) return 2;
|
|
41
|
-
if (/^(3|三)/.test(s)) return 3;
|
|
42
|
-
if (/^(1|一)/.test(s)) return 1;
|
|
43
|
-
return 1;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export default {
|
|
47
|
-
name: 'session_watch',
|
|
48
|
-
// ★ 2026-10-12:**父会话专用**("我作为父"去挂/摘回灌钩子)⇒ 不给子会话看。
|
|
49
|
-
expose: 'qq',
|
|
50
|
-
description:
|
|
51
|
-
'查看/补挂「回合结束回灌」钩子:默认列出正在盯的子会话、档位与状态;' +
|
|
52
|
-
'attach=给指定子会话补挂(子会话 id + 父会话 id + 可选 mode);mode=改已挂盯梢的档位(1/2/3);detach=取消盯梢。' +
|
|
53
|
-
'档位:1=只把末段正文回灌父会话并转发到父所在的QQ;2=只把该回合所有正文+报错转发QQ、不进父上下文;3=进父上下文但不唤醒、同时也转发QQ。',
|
|
54
|
-
inputSchema: {
|
|
55
|
-
action: { type: 'string', description: 'list(默认)=看状态 / attach=给已存在的子会话补挂 / mode=改档位 / detach=取消盯梢' },
|
|
56
|
-
child_session_id: { type: 'string', description: 'attach / mode / detach 时要指定的子会话 id' },
|
|
57
|
-
parent_agent_id: { type: 'string', description: 'attach 时填**父会话 id**(历史上叫"父 agent id",指的是同一个东西;不填=用"我自己这个会话")' },
|
|
58
|
-
label: { type: 'string', description: 'attach 时的标签,便于记认(可选)' },
|
|
59
|
-
name: { type: 'string', description: '会话名(可选;只影响转发到 QQ 时行首归属里显示什么,缺省用 label)' },
|
|
60
|
-
preset: { type: 'string', description: '预设名(可选;同上,归属行显示用)' },
|
|
61
|
-
workspace: { type: 'string', description: '工作区名(可选;同上,归属行显示用,建议只写目录名)' },
|
|
62
|
-
mode: { type: 'string', description: '档位:1/一档(默认)=末段回灌父+转QQ;2/二档=只转QQ不进父上下文;3/三档=进父上下文但不唤醒+转QQ' },
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
async run(args = {}, env = {}) {
|
|
66
|
-
const ctx = env?.ctx ?? null;
|
|
67
|
-
const root = ctx && typeof ctx === 'object' && ctx.root ? ctx.root : ctx;
|
|
68
|
-
if (!root?.get) return { ok: false, msg: '拿不到宿主 ctx(需要 expose:\'all\')' };
|
|
69
|
-
|
|
70
|
-
const lib = await loadWatchLib();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const r = await lib.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* session_watch.mjs — 查看/补挂「回合结束回灌」钩子(含三档 + QQ 转发)
|
|
3
|
+
*
|
|
4
|
+
* 配合 session_create 用:创建时会自动挂钩子(session_create → lib/session_watch_lib.register)。
|
|
5
|
+
* 本工具负责:
|
|
6
|
+
* · list(默认):列出正在盯的子会话(子 id / 父 id / 标签 / **档位** / QQ 转发是否就绪 / 已结算几次)
|
|
7
|
+
* · attach:给一个"已存在的子会话"补挂钩子(创建时漏挂、或插件重启后钩子丢了的情况)
|
|
8
|
+
* · mode:改已挂盯梢的档位(一/二/三档随时可改)
|
|
9
|
+
* · detach:取消盯某个子会话
|
|
10
|
+
*
|
|
11
|
+
* ★ 2026-10-12 主人定:三档(mode)
|
|
12
|
+
* 1 = 一档(默认):末段正文回灌父会话 + **同时**转发到父会话所在的 QQ
|
|
13
|
+
* 2 = 二档:本回合**所有正文 + 报错**都转发 QQ,**不进**父会话上下文
|
|
14
|
+
* 3 = 三档:所有正文 + 报错**进**父会话上下文(**不唤醒**)+ 同时也转发 QQ
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ 主人要求这类工具不给全部会话用:已落地 `expose: 'qq'`(2026-10-12),只在 QQ 会话注册。
|
|
17
|
+
*/
|
|
18
|
+
import { statSync } from 'node:fs';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 带版本号地加载共用库 —— **必须这么加载**。
|
|
23
|
+
*
|
|
24
|
+
* 原因同 session_create.mjs:插件只给**工具文件本身**的 import 加了 `?v=<mtime>`,
|
|
25
|
+
* 工具内部裸路径 import 会被 ESM 模块缓存永久命中 ⇒ 改 `tools/lib/` 热刷不生效。
|
|
26
|
+
* (这里原来是顶层静态 import,等于永远锁死在插件启动那一刻的旧版本。)
|
|
27
|
+
*/
|
|
28
|
+
async function loadWatchLib() {
|
|
29
|
+
const url = new URL('./lib/session_watch_lib.mjs', import.meta.url);
|
|
30
|
+
let v = '0';
|
|
31
|
+
try { v = String(statSync(fileURLToPath(url)).mtimeMs); } catch { /* 拿不到 mtime 就退回裸 import */ }
|
|
32
|
+
return import(v === '0' ? url.href : `${url.href}?v=${v}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 档位 → 人话 */
|
|
36
|
+
const modeText = (m) => (Number(m) === 2 ? '二档·只转QQ不进父' : (Number(m) === 3 ? '三档·进父不唤醒+转QQ' : '一档·末段回灌+转QQ'));
|
|
37
|
+
/** 把用户给的档位写法归一(收 1/2/3 与中文「一档/二档/三档」) */
|
|
38
|
+
function normMode(v) {
|
|
39
|
+
const s = String(v ?? '').trim();
|
|
40
|
+
if (/^(2|二)/.test(s)) return 2;
|
|
41
|
+
if (/^(3|三)/.test(s)) return 3;
|
|
42
|
+
if (/^(1|一)/.test(s)) return 1;
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default {
|
|
47
|
+
name: 'session_watch',
|
|
48
|
+
// ★ 2026-10-12:**父会话专用**("我作为父"去挂/摘回灌钩子)⇒ 不给子会话看。
|
|
49
|
+
expose: 'qq',
|
|
50
|
+
description:
|
|
51
|
+
'查看/补挂「回合结束回灌」钩子:默认列出正在盯的子会话、档位与状态;' +
|
|
52
|
+
'attach=给指定子会话补挂(子会话 id + 父会话 id + 可选 mode);mode=改已挂盯梢的档位(1/2/3);detach=取消盯梢。' +
|
|
53
|
+
'档位:1=只把末段正文回灌父会话并转发到父所在的QQ;2=只把该回合所有正文+报错转发QQ、不进父上下文;3=进父上下文但不唤醒、同时也转发QQ。',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
action: { type: 'string', description: 'list(默认)=看状态 / attach=给已存在的子会话补挂 / mode=改档位 / detach=取消盯梢' },
|
|
56
|
+
child_session_id: { type: 'string', description: 'attach / mode / detach 时要指定的子会话 id' },
|
|
57
|
+
parent_agent_id: { type: 'string', description: 'attach 时填**父会话 id**(历史上叫"父 agent id",指的是同一个东西;不填=用"我自己这个会话")' },
|
|
58
|
+
label: { type: 'string', description: 'attach 时的标签,便于记认(可选)' },
|
|
59
|
+
name: { type: 'string', description: '会话名(可选;只影响转发到 QQ 时行首归属里显示什么,缺省用 label)' },
|
|
60
|
+
preset: { type: 'string', description: '预设名(可选;同上,归属行显示用)' },
|
|
61
|
+
workspace: { type: 'string', description: '工作区名(可选;同上,归属行显示用,建议只写目录名)' },
|
|
62
|
+
mode: { type: 'string', description: '档位:1/一档(默认)=末段回灌父+转QQ;2/二档=只转QQ不进父上下文;3/三档=进父上下文但不唤醒+转QQ' },
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async run(args = {}, env = {}) {
|
|
66
|
+
const ctx = env?.ctx ?? null;
|
|
67
|
+
const root = ctx && typeof ctx === 'object' && ctx.root ? ctx.root : ctx;
|
|
68
|
+
if (!root?.get) return { ok: false, msg: '拿不到宿主 ctx(需要 expose:\'all\')' };
|
|
69
|
+
|
|
70
|
+
const lib = await loadWatchLib();
|
|
71
|
+
|
|
72
|
+
// ★ 2026-10-13:把本次 env 的 sender/replyTarget 交给回灌库。
|
|
73
|
+
// 工具 env **一定有**这两样({cwd, manager, sender, replyTarget, exec, ctx, logger}),
|
|
74
|
+
// 而插件根 ctx(handler 的 root)上实测拿不到 ⇒ 记住它,回灌库才能在"父会话发言"时
|
|
75
|
+
// 把重启后丢失的 QQ 转发能力补回来。
|
|
76
|
+
const fwdOk = lib.rememberFwd?.(env?.sender, env?.replyTarget) ?? false;
|
|
77
|
+
|
|
78
|
+
const action = String(args?.action ?? 'list').toLowerCase();
|
|
79
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
80
|
+
const fmt = (t) => {
|
|
81
|
+
if (!t) return '—';
|
|
82
|
+
const d = new Date(t);
|
|
83
|
+
return Number.isNaN(d.getTime()) ? '—' : `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ★ 2026-10-13 加:**不重启也能让最新 lib 接管**。
|
|
87
|
+
// 背景:`tools/lib/*.mjs` 是按 mtime 版本化加载的,改完只有"监听器真正用的那份 handler"
|
|
88
|
+
// 才是新代码;而 handler 指针靠 `installHook()` 更新。`rehydrate()` 同时做两件事:
|
|
89
|
+
// ① 读回落盘名单;② installHook(把 globalThis 上的 handler 换成最新实例)。
|
|
90
|
+
// ⇒ 改完 lib 跑一次本动作即可生效,**不必重启宿主**。
|
|
91
|
+
if (action === 'rehydrate' || action === '重载' || action === '刷新') {
|
|
92
|
+
const root = env?.kernel ?? env?.ctx ?? null;
|
|
93
|
+
if (!root || typeof root.on !== 'function') {
|
|
94
|
+
return { ok: false, msg: 'rehydrate 需要宿主根 ctx(env.kernel),当前拿不到' };
|
|
95
|
+
}
|
|
96
|
+
const r = await lib.rehydrate(root);
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
msg: `已重载:读回 ${r?.restored ?? 0} 条盯梢,回灌监听器${r?.hooked ? '已装' : '未装'}` +
|
|
100
|
+
`(新代码已接管;QQ 转发能力会在父会话下一次发言时自动补回)`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (action === 'attach' || action === '补挂') {
|
|
105
|
+
const child = String(args?.child_session_id ?? '').trim();
|
|
106
|
+
const parent = String(args?.parent_agent_id ?? env?.exec?.agent?.id ?? '').trim();
|
|
107
|
+
if (!child) return { ok: false, msg: 'attach 需要 child_session_id' };
|
|
108
|
+
if (!parent) return { ok: false, msg: 'attach 需要 parent_agent_id(不填会用"我自己")' };
|
|
109
|
+
const r = await lib.register(root, {
|
|
110
|
+
parentAgentId: parent,
|
|
111
|
+
childSessionId: child,
|
|
112
|
+
label: String(args?.label ?? ''),
|
|
113
|
+
mode: normMode(args?.mode),
|
|
114
|
+
// 归属行三件套(可选;不填就只显示短 id)
|
|
115
|
+
name: String(args?.name ?? args?.label ?? ''),
|
|
116
|
+
preset: String(args?.preset ?? ''),
|
|
117
|
+
workspace: String(args?.workspace ?? ''),
|
|
118
|
+
// ★ QQ 转发用的就是"我自己这个会话"的 sender + replyTarget
|
|
119
|
+
// (replyTarget 自带最近 msgId ⇒ 被动优先;插件的 sendMarkdown 自带 markdown 降级链)
|
|
120
|
+
sender: env?.sender,
|
|
121
|
+
replyTarget: env?.replyTarget,
|
|
122
|
+
peerName: String(env?.replyTarget?.name ?? ''),
|
|
123
|
+
});
|
|
124
|
+
return { ok: !String(r).startsWith('未挂'), msg: 'attach → ' + r };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (action === 'mode' || action === '档位') {
|
|
128
|
+
const child = String(args?.child_session_id ?? '').trim();
|
|
129
|
+
if (!child) return { ok: false, msg: 'mode 需要 child_session_id(先用 list 看)' };
|
|
130
|
+
// 已挂的直接改;没挂但库里能按短 id 找到也认
|
|
131
|
+
const hit = lib.listWatching().find((x) => x.childSessionId === child || x.shortId === child.slice(0, 8));
|
|
132
|
+
const target = hit ? hit.childSessionId : child;
|
|
133
|
+
const r = await lib.setMode(target, normMode(args?.mode));
|
|
134
|
+
return { ok: !String(r).includes('没找到'), msg: `mode → ${r}` };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (action === 'detach' || action === '取消') {
|
|
138
|
+
const child = String(args?.child_session_id ?? '').trim();
|
|
139
|
+
if (!child) return { ok: false, msg: 'detach 需要 child_session_id' };
|
|
140
|
+
const hit = lib.listWatching().find((x) => x.childSessionId === child || x.shortId === child.slice(0, 8));
|
|
141
|
+
const r = await lib.unregister(hit ? hit.childSessionId : child);
|
|
142
|
+
return { ok: true, msg: r + `(${String(child).slice(0, 14)}…;钩子本身是全局一个,这里只是不再为它回灌/转发)` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// list
|
|
146
|
+
const items = lib.listWatching();
|
|
147
|
+
if (!items.length) {
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
msg: '【回合结束回灌】当前没有盯着任何子会话。\n用 session_create 建会话时会自动挂(可带 mode);也可以用 attach 给已存在的子会话补挂。',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const lines = items.map(
|
|
154
|
+
(x) =>
|
|
155
|
+
` · 子 ${x.shortId ?? x.childSessionId.slice(0, 8)}… → 父 ${x.parentShortId ?? x.parentAgentId.slice(0, 8)}…` +
|
|
156
|
+
`${x.label ? ' [' + x.label + ']' : ''}\n` +
|
|
157
|
+
` 档位=${modeText(x.mode)} QQ转发=${x.qqReady ? '就绪' : '未就绪(没拿到 sender/replyTarget)'}` +
|
|
158
|
+
`${x.peerName ? ' → ' + x.peerName : ''}\n` +
|
|
159
|
+
` 已结算=${x.turnsSeen} 次 上次回灌=${fmt(x.lastFiredAt)} 最近被引用=${fmt(x.quotedAt)}`,
|
|
160
|
+
);
|
|
161
|
+
return { ok: true, msg: `【回合结束回灌】正在盯 ${items.length} 个子会话:\n` + lines.join('\n') };
|
|
162
|
+
},
|
|
163
|
+
};
|