@zaofan/dsh-qqbot 1.0.0 → 1.0.1
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 +12 -6
- package/client/qqbot-settings.js +3407 -3214
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +37 -17
- package/dist/channel-tools.js.map +1 -1
- package/dist/commands/outmode.d.ts +7 -3
- package/dist/commands/outmode.d.ts.map +1 -1
- package/dist/commands/outmode.js +19 -9
- package/dist/commands/outmode.js.map +1 -1
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/features/botplay.d.ts.map +1 -1
- package/dist/features/botplay.js +13 -2
- package/dist/features/botplay.js.map +1 -1
- package/dist/features/group-hub.d.ts +40 -0
- package/dist/features/group-hub.d.ts.map +1 -0
- package/dist/features/group-hub.js +174 -0
- package/dist/features/group-hub.js.map +1 -0
- package/dist/features/group-join-request.d.ts.map +1 -1
- package/dist/features/group-join-request.js +35 -33
- package/dist/features/group-join-request.js.map +1 -1
- package/dist/features/scheduler.d.ts.map +1 -1
- package/dist/features/scheduler.js +2 -1
- package/dist/features/scheduler.js.map +1 -1
- package/dist/gateway/bootstrap.d.ts.map +1 -1
- package/dist/gateway/bootstrap.js +31 -10
- package/dist/gateway/bootstrap.js.map +1 -1
- package/dist/gateway/data-root.d.ts +15 -0
- package/dist/gateway/data-root.d.ts.map +1 -0
- package/dist/gateway/data-root.js +59 -0
- package/dist/gateway/data-root.js.map +1 -0
- package/dist/gateway/debounce.d.ts.map +1 -1
- package/dist/gateway/debounce.js +3 -2
- package/dist/gateway/debounce.js.map +1 -1
- package/dist/gateway/middleware-setup.d.ts.map +1 -1
- package/dist/gateway/middleware-setup.js +35 -15
- package/dist/gateway/middleware-setup.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/attachment.d.ts.map +1 -1
- package/dist/middleware/attachment.js +2 -1
- package/dist/middleware/attachment.js.map +1 -1
- package/dist/middleware/media-history.d.ts +2 -0
- package/dist/middleware/media-history.d.ts.map +1 -1
- package/dist/middleware/media-history.js +6 -1
- package/dist/middleware/media-history.js.map +1 -1
- package/dist/middleware/sticker-capture.d.ts.map +1 -1
- package/dist/middleware/sticker-capture.js +2 -2
- package/dist/middleware/sticker-capture.js.map +1 -1
- package/dist/session/session-manager.d.ts +4 -2
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +17 -5
- package/dist/session/session-manager.js.map +1 -1
- package/dist/transport/attachment.d.ts.map +1 -1
- package/dist/transport/attachment.js +2 -1
- package/dist/transport/attachment.js.map +1 -1
- package/dist/transport/inbound.d.ts.map +1 -1
- package/dist/transport/inbound.js +9 -1
- package/dist/transport/inbound.js.map +1 -1
- package/package.json +1 -1
- package/settings-host.js +177 -52
package/settings-host.js
CHANGED
|
@@ -256,7 +256,8 @@ export function apply(ctx) {
|
|
|
256
256
|
function parsePatch() {
|
|
257
257
|
const hasFile = existsSync(PATCH_FILE);
|
|
258
258
|
const raw = hasFile ? readFileSync(PATCH_FILE, 'utf8') : '';
|
|
259
|
-
|
|
259
|
+
// 行尾容错: 兼容 CRLF(2026-09-08 曾因 patch 被写成 CRLF 导致整段解析失败)
|
|
260
|
+
const lines = raw.split(/\r?\n/);
|
|
260
261
|
const bots = [];
|
|
261
262
|
let i = 0;
|
|
262
263
|
while (i < lines.length) {
|
|
@@ -345,12 +346,15 @@ export function apply(ctx) {
|
|
|
345
346
|
out.push(`${pad4}requireMention: false`);
|
|
346
347
|
out.push(`${pad4}historyLimit: 20`);
|
|
347
348
|
if (inst.cwd) out.push(`${pad4}cwd: ${yq(inst.cwd)}`);
|
|
349
|
+
if (inst.dataRoot) out.push(`${pad4}dataRoot: ${yq(inst.dataRoot)}`); // 数据根(可选; 保存时保留, 见 saveInstances)
|
|
348
350
|
return out.join('\n');
|
|
349
351
|
}
|
|
350
352
|
|
|
351
353
|
/** 保存实例清单(全量同步; 行级重建, 其它插件行/注释原样保留)
|
|
352
354
|
* 借鉴 dsh-qqbot-panel(2026-09-06): appSecret 空值/掩码(********) = 保留原值,
|
|
353
|
-
* 只有提供全新非掩码值才覆盖 —— 前端只回显掩码, 不会因漏传/未改而误清 secret。
|
|
355
|
+
* 只有提供全新非掩码值才覆盖 —— 前端只回显掩码, 不会因漏传/未改而误清 secret。
|
|
356
|
+
* ⚠️ 2026-09-09: dataRoot 同理 —— 前端不编辑它, 保存时必须保留原值,
|
|
357
|
+
* 否则 accounts/save 会把 patch.yml 里的 dataRoot 覆盖掉(图库路径回退 cwd 的根因)。 */
|
|
354
358
|
function saveInstances(instances) {
|
|
355
359
|
const { raw, hasFile, bots } = parsePatch();
|
|
356
360
|
if (!hasFile) return { ok: false, error: '找不到 cordis.patch.yml(仅 web profile 支持)' };
|
|
@@ -363,12 +367,14 @@ export function apply(ctx) {
|
|
|
363
367
|
for (const inst of instances) {
|
|
364
368
|
if (inst.remove) continue;
|
|
365
369
|
const t = bots.find((b) => b.id === inst.id);
|
|
370
|
+
const orig = bots.find((b) => b.id === inst.id);
|
|
366
371
|
// 掩码/空 → 保留原 secret(仅当原值存在; 全新账号本就无原值则维持空)
|
|
367
372
|
if (!inst.appSecret || inst.appSecret === SECRET_MASK) {
|
|
368
|
-
const orig = bots.find((b) => b.id === inst.id);
|
|
369
373
|
if (orig && orig.cfg?.appSecret) inst.appSecret = orig.cfg.appSecret;
|
|
370
374
|
else inst.appSecret = '';
|
|
371
375
|
}
|
|
376
|
+
// 前端不编辑 dataRoot → 保留原值(2026-09-09: 曾因保存覆盖丢失导致图库路径回退 cwd)
|
|
377
|
+
if (inst.dataRoot === undefined && orig && orig.cfg?.dataRoot) inst.dataRoot = orig.cfg.dataRoot;
|
|
372
378
|
const blockText = renderBotBlock(inst);
|
|
373
379
|
if (t) edits.push({ start: t.start, end: t.end, text: blockText });
|
|
374
380
|
else edits.push({ append: blockText });
|
|
@@ -397,6 +403,7 @@ export function apply(ctx) {
|
|
|
397
403
|
hasFile,
|
|
398
404
|
instances: bots.map((b) => {
|
|
399
405
|
const cwd = b.cfg?.cwd || '';
|
|
406
|
+
const droot = b.cfg?.dataRoot || cwd;
|
|
400
407
|
return {
|
|
401
408
|
id: b.id,
|
|
402
409
|
ns: b.id, // settings 命名空间 = 实例 id(主 im-qqbot; 非主实例 render 已写 settingsNs=id)
|
|
@@ -408,8 +415,8 @@ export function apply(ctx) {
|
|
|
408
415
|
disabled: !!b.disabled,
|
|
409
416
|
online: typeof reg.isBotOnline === 'function' ? reg.isBotOnline(b.id) : false, // 在线状态(bot ws ready 事件驱动)
|
|
410
417
|
// 账号数据目录(各号各库各定时): 图库={cwd}/表情包, 定时={cwd}/.qqbot
|
|
411
|
-
dataDir:
|
|
412
|
-
schedDir:
|
|
418
|
+
dataDir: droot ? join(droot, '表情包') : '',
|
|
419
|
+
schedDir: droot ? join(droot, '.qqbot') : '',
|
|
413
420
|
};
|
|
414
421
|
}),
|
|
415
422
|
});
|
|
@@ -814,7 +821,8 @@ export function apply(ctx) {
|
|
|
814
821
|
if (!b) return null;
|
|
815
822
|
const appId = b.cfg?.appId || '';
|
|
816
823
|
const appSecret = b.cfg?.appSecret || '';
|
|
817
|
-
|
|
824
|
+
// 数据根: dataRoot(新) > cwd(旧); nsBot 的 cwd 字段按数据根返回(全为数据目录用途)
|
|
825
|
+
const cwd = (b.cfg?.dataRoot || b.cfg?.cwd || '');
|
|
818
826
|
if (!appId || !appSecret) return null;
|
|
819
827
|
return { id, appId, appSecret, cwd, ns: id };
|
|
820
828
|
}
|
|
@@ -874,14 +882,41 @@ export function apply(ctx) {
|
|
|
874
882
|
const sessionId = (u.searchParams.get('sessionId') || '').trim();
|
|
875
883
|
if (!sessionId) return writeJson(res, 400, { error: 'sessionId 必填' });
|
|
876
884
|
const { bots } = parsePatch();
|
|
885
|
+
const botByNs = new Map();
|
|
886
|
+
for (const bot of bots) {
|
|
887
|
+
if (bot.disabled || !bot.cfg?.appId || !bot.cfg?.cwd) continue;
|
|
888
|
+
botByNs.set(bot.id, bot);
|
|
889
|
+
}
|
|
877
890
|
const hits = [];
|
|
891
|
+
const seen = new Set();
|
|
892
|
+
// 按实例台账补昵称(dataRoot/表情包/known-chats.jsonl)
|
|
893
|
+
const nameOf = (ns, scope, peerId) => {
|
|
894
|
+
const bot = botByNs.get(ns);
|
|
895
|
+
if (!bot) return '';
|
|
896
|
+
try {
|
|
897
|
+
const cwd = bot.cfg.dataRoot || bot.cfg.cwd;
|
|
898
|
+
const ld = chatLedgerNames(join(cwd, '表情包')).get(scope + ':' + peerId);
|
|
899
|
+
return (ld && ld.name) || '';
|
|
900
|
+
} catch { return '' }
|
|
901
|
+
};
|
|
902
|
+
const pushHit = (ns, scope, peerId, name, via) => {
|
|
903
|
+
const key = ns + ':' + scope + ':' + peerId;
|
|
904
|
+
if (seen.has(key)) return;
|
|
905
|
+
seen.add(key);
|
|
906
|
+
hits.push({ ns, scope, peerId, name, via });
|
|
907
|
+
};
|
|
908
|
+
// ① 权威: 真实会话记录(唯一可靠源 —— fork 后 sessionId 为 randomUUID, derive 推导会 miss)
|
|
909
|
+
const sessReg = await import('./dist/features/session-registry.js');
|
|
910
|
+
for (const r of sessReg.findSessionBySessionIdWeb(sessionId)) {
|
|
911
|
+
pushHit(r.ns, r.scope, r.peerId, nameOf(r.ns, r.scope, r.peerId), 'session');
|
|
912
|
+
}
|
|
913
|
+
// ② 兜底: 确定性推导(会话记录未建立/被回收时, 从注册表+台账推导; 与权威命中自动去重)
|
|
878
914
|
for (const bot of bots) {
|
|
879
915
|
if (bot.disabled || !bot.cfg?.appId || !bot.cfg?.cwd) continue;
|
|
880
916
|
const appId = bot.cfg.appId;
|
|
881
|
-
const cwd = bot.cfg.cwd;
|
|
917
|
+
const cwd = bot.cfg.dataRoot || bot.cfg.cwd;
|
|
882
918
|
const ns = bot.id;
|
|
883
919
|
const ledger = chatLedgerNames(join(cwd, '表情包'));
|
|
884
|
-
// 群: 注册表 + 台账
|
|
885
920
|
const groups = readGroupsJson(cwd);
|
|
886
921
|
const gids = new Set([...Object.keys(groups), ...[...ledger.keys()].filter((k) => k.startsWith('group:')).map((k) => k.slice(6))]);
|
|
887
922
|
for (const gid of gids) {
|
|
@@ -889,14 +924,13 @@ export function apply(ctx) {
|
|
|
889
924
|
if (deriveSessionIdOf(appId, 'group', gid) !== sessionId) continue;
|
|
890
925
|
const regMeta = groups[gid];
|
|
891
926
|
const ld = ledger.get('group:' + gid);
|
|
892
|
-
|
|
927
|
+
pushHit(ns, 'group', gid, (regMeta && regMeta.name) || (ld && ld.name) || '', 'derive');
|
|
893
928
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const openid = key.slice(4);
|
|
929
|
+
for (const [key2, ld] of ledger) {
|
|
930
|
+
if (!key2.startsWith('c2c:')) continue;
|
|
931
|
+
const openid = key2.slice(4);
|
|
898
932
|
if (!openid || deriveSessionIdOf(appId, 'c2c', openid) !== sessionId) continue;
|
|
899
|
-
|
|
933
|
+
pushHit(ns, 'c2c', openid, ld.name || '', 'derive');
|
|
900
934
|
}
|
|
901
935
|
}
|
|
902
936
|
writeJson(res, 200, { ok: true, sessionId, hits });
|
|
@@ -1718,16 +1752,19 @@ export function apply(ctx) {
|
|
|
1718
1752
|
}
|
|
1719
1753
|
});
|
|
1720
1754
|
|
|
1721
|
-
const WHY_MAP = { busy: '
|
|
1755
|
+
const WHY_MAP = { busy: '目标会话回合活跃,已等待至回合结束仍超时(跳过注入)', 'no-session': '未找到该群/私聊的活跃会话', 'no-msg': '构造消息失败', failed: '写入失败' };
|
|
1722
1756
|
|
|
1723
|
-
//
|
|
1757
|
+
// 安全闸(兜底用): 目标会话 LLM 回合活跃(turn/start 已开、turn/end 未闭合——覆盖思考中/流式输出/工具执行)
|
|
1724
1758
|
// 时禁止外部往该会话插入任何消息(含模拟用户消息), 防止把正在生成的回合流搞乱(曾因此写坏会话)。
|
|
1759
|
+
// 与 QQ 插件入站逻辑(debounce.ts 的 turnActive 排队)一致: 活跃时【等待回合结束】再插入, 而非跳过。
|
|
1760
|
+
// ⚠️ 只作 waitTurnIdle 的兜底轮询: 它回看最近 3000 条事件找 turn/start..turn/end(曾用 400 太小,
|
|
1761
|
+
// 长回合流式/tool 事件一多就漏判"空闲"导致插进 tool_calls↔tool_result 中间, INVALID_REQUEST)。
|
|
1725
1762
|
// 查不到则放行(append 自身有 reenter 兜底)。
|
|
1726
1763
|
function sessionTurnActive(sess) {
|
|
1727
1764
|
try {
|
|
1728
1765
|
const seq = typeof sess?.seq === 'number' ? sess.seq : -1;
|
|
1729
1766
|
if (seq <= 0 || typeof sess.snapshotEvents !== 'function') return false;
|
|
1730
|
-
const tail = sess.snapshotEvents(Math.max(0, seq -
|
|
1767
|
+
const tail = sess.snapshotEvents(Math.max(0, seq - 3000), seq);
|
|
1731
1768
|
let lastStart = -1;
|
|
1732
1769
|
let lastEnd = -1;
|
|
1733
1770
|
for (const ev of tail) {
|
|
@@ -1738,11 +1775,98 @@ const WHY_MAP = { busy: '目标会话回合活跃(思考/流式中),已跳过注
|
|
|
1738
1775
|
} catch { return false; }
|
|
1739
1776
|
}
|
|
1740
1777
|
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1778
|
+
/**
|
|
1779
|
+
* 等目标会话 LLM 回合结束再返回。
|
|
1780
|
+
* 主路径: 宿主 agent.whenIdle()(权威: 空闲立即返回、回合活跃时等 turn/end —— 与入群通知
|
|
1781
|
+
* safeAppendUserMessage 同款)。⚠️ 不能先用 sessionTurnActive 当第一判断: 它只回看 400 条事件,
|
|
1782
|
+
* 长回合(流式/tool 事件多)时窗口内查不到 turn/start 会误判"空闲", 正是历史事故的漏网点。
|
|
1783
|
+
* 超时(60s, 防 turn/end 丢失卡死)或宿主无 whenIdle → 回落 800ms 轮询, 仍超时返回 false。
|
|
1784
|
+
*/
|
|
1785
|
+
const TURN_WAIT_MS = 60_000;
|
|
1786
|
+
async function waitTurnIdle(agent, sess) {
|
|
1787
|
+
// ① 权威: whenIdle —— 回合空闲立即 resolve; 活跃时等 turn/end 后再 resolve
|
|
1788
|
+
if (agent && typeof agent.whenIdle === 'function') {
|
|
1789
|
+
let idle = false;
|
|
1790
|
+
try {
|
|
1791
|
+
await Promise.race([
|
|
1792
|
+
agent.whenIdle().then(() => { idle = true; }),
|
|
1793
|
+
new Promise((r) => setTimeout(r, TURN_WAIT_MS)),
|
|
1794
|
+
]);
|
|
1795
|
+
} catch { /* whenIdle 异常按未空闲处理 */ }
|
|
1796
|
+
if (idle) return true;
|
|
1797
|
+
}
|
|
1798
|
+
// ② 兜底轮询: 与 QQ 入站 debounce 排队同款(800ms 重查, 等 turn/end), 超 60s 放弃
|
|
1799
|
+
if (sess && typeof sess.snapshotEvents === 'function') {
|
|
1800
|
+
const t0 = Date.now();
|
|
1801
|
+
for (;;) {
|
|
1802
|
+
if (!sessionTurnActive(sess)) return true;
|
|
1803
|
+
if (Date.now() - t0 >= TURN_WAIT_MS) return false;
|
|
1804
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
return true; // 无检测能力 → 放行(append 自身有 reenter 兜底)
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// ── 线B(用户代发/后台通知 → 插入上下文)异步队列(2026-09-09) ──
|
|
1811
|
+
// 与 QQ 入站 debounce 排队同款语义: **先收下消息立即返回, 不阻塞任何 HTTP 请求/事件**,
|
|
1812
|
+
// 回合活跃时攒在 per-peer 队列里, 回合结束后(whenIdle/turnActive=false)再逐条 append。
|
|
1813
|
+
// 避免把 dock 发送请求挂在 waitTurnIdle 上 60s+(曾导致"第二条消息发不出去")。
|
|
1814
|
+
//
|
|
1815
|
+
// 性能护栏:
|
|
1816
|
+
// - 队列按 peer 分 key, 单 peer 上限 RELAY_QUEUE_MAX(超限丢最老), 防止内存无限膨胀;
|
|
1817
|
+
// - 队列空即删 key(不残留); drain 有 in-flight 去重, 同 key 不并发重复消费;
|
|
1818
|
+
// - 等待靠 whenIdle 事件驱动(回合结束才 resolve, 不空转); 兜底 800ms 轮询只在无 whenIdle 时用;
|
|
1819
|
+
// - waitTurnIdle 自身 60s 超时防 turn/end 丢失卡死, 超时该条放弃(不硬塞坏记录)。
|
|
1820
|
+
const RELAY_QUEUE_MAX = 200;
|
|
1821
|
+
const relayQueues = new Map(); // key = ns|scope|peerId → [{ ns, scope, peerId, kind:'relay'|'notice', text }]
|
|
1822
|
+
const relayDraining = new Set(); // 正在 drain 的 key(防同 key 并发重复消费)
|
|
1823
|
+
|
|
1824
|
+
function relayQueueKey(ns, scope, peerId) { return ns + '|' + scope + '|' + peerId; }
|
|
1825
|
+
|
|
1826
|
+
/** 入队一条待写上下文的消息(立即返回; 后台 drain 回合结束后落盘) */
|
|
1827
|
+
function enqueueRelayWrite(ns, scope, peerId, kind, text) {
|
|
1828
|
+
const key = relayQueueKey(ns, scope, peerId);
|
|
1829
|
+
let q = relayQueues.get(key);
|
|
1830
|
+
if (!q) { q = []; relayQueues.set(key, q); }
|
|
1831
|
+
if (q.length >= RELAY_QUEUE_MAX) q.shift(); // 防内存膨胀: 超限丢最老
|
|
1832
|
+
q.push({ ns, scope, peerId, kind, text });
|
|
1833
|
+
void drainRelayQueue(key);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/** 后台串行消费某 peer 的待写队列。
|
|
1837
|
+
* - 单条 busy(回合卡死/超时): 保留队列并 break, 等下次入队再触发 drain 重试 —— 防整个队列
|
|
1838
|
+
* 逐条各等 60s 超时空挂; 回合只是暂时活跃(正常结束)时下条入队即重试成功。
|
|
1839
|
+
* - 单条 fail(no-session / no-msg / append 异常): 丢弃该条, 继续下一条。 */
|
|
1840
|
+
async function drainRelayQueue(key) {
|
|
1841
|
+
if (relayDraining.has(key)) return;
|
|
1842
|
+
relayDraining.add(key);
|
|
1843
|
+
try {
|
|
1844
|
+
for (;;) {
|
|
1845
|
+
const q = relayQueues.get(key);
|
|
1846
|
+
const item = q && q[0];
|
|
1847
|
+
if (!item) { relayQueues.delete(key); break; } // 队列空 → 删 key, 不残留
|
|
1848
|
+
let st = 'fail';
|
|
1849
|
+
try {
|
|
1850
|
+
st = await appendOneRelayItem(item);
|
|
1851
|
+
} catch { st = 'fail'; }
|
|
1852
|
+
if (st === 'busy') break; // 回合长时间卡死: 保留剩余队列, 下条入队再重试(不空挂)
|
|
1853
|
+
q.shift();
|
|
1854
|
+
if (st === 'fail') {
|
|
1855
|
+
// 写失败(no-session / no-msg / append 异常): 不阻塞队列, 丢弃并继续
|
|
1856
|
+
try {
|
|
1857
|
+
const { bots } = parsePatch();
|
|
1858
|
+
const bot = bots.find((b) => b.id === item.ns);
|
|
1859
|
+
if (bot && bot.cwd) audit(bot.cwd, { ev: 'relay.drain-skip', ns: item.ns, scope: item.scope, peerId: item.peerId, kind: item.kind, reason: 'drain-failed' });
|
|
1860
|
+
} catch { /* audit 失败不阻断 */ }
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
} finally {
|
|
1864
|
+
relayDraining.delete(key);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/** 等回合结束 + 把单条消息 append 进会话(不唤醒、不开回合)。返回 'ok' | 'busy' | 'fail' */
|
|
1869
|
+
async function appendOneRelayItem({ ns, scope, peerId, kind, text }) {
|
|
1746
1870
|
const reg = await import('./dist/features/session-registry.js');
|
|
1747
1871
|
let rec = typeof reg.findRecordByPeerWeb === 'function' ? reg.findRecordByPeerWeb(ns, scope, peerId) : undefined;
|
|
1748
1872
|
// 活跃表 miss(会话被 idle 回收/未建立)→ 与入群申请通知/定时任务同款: getOrCreate 恢复/重建会话(不开回合),
|
|
@@ -1750,45 +1874,46 @@ const WHY_MAP = { busy: '目标会话回合活跃(思考/流式中),已跳过注
|
|
|
1750
1874
|
if (!rec && typeof reg.getOrCreateByPeerWeb === 'function') {
|
|
1751
1875
|
try { rec = await reg.getOrCreateByPeerWeb(ns, scope, peerId, 'master'); } catch { /* 恢复失败按 no-session 走 */ }
|
|
1752
1876
|
}
|
|
1753
|
-
if (!rec || !rec.agent) return '
|
|
1877
|
+
if (!rec || !rec.agent) return 'fail';
|
|
1754
1878
|
const agent = rec.agent;
|
|
1755
1879
|
const sess = agent && (agent.session || (agent.ctx && agent.ctx.session));
|
|
1756
1880
|
const appendFn = sess && typeof sess.append === 'function' ? sess.append.bind(sess) : undefined;
|
|
1757
|
-
if (!appendFn) return '
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
try {
|
|
1774
|
-
const reg = await import('./dist/features/session-registry.js');
|
|
1775
|
-
let rec = typeof reg.findRecordByPeerWeb === 'function' ? reg.findRecordByPeerWeb(ns, scope, peerId) : undefined;
|
|
1776
|
-
if (!rec && typeof reg.getOrCreateByPeerWeb === 'function') {
|
|
1777
|
-
try { rec = await reg.getOrCreateByPeerWeb(ns, scope, peerId, 'master'); } catch { /* 忽略 */ }
|
|
1778
|
-
}
|
|
1779
|
-
if (!rec || !rec.agent) return false;
|
|
1780
|
-
const agent = rec.agent;
|
|
1781
|
-
const sess = agent && (agent.session || (agent.ctx && agent.ctx.session));
|
|
1782
|
-
const appendFn = sess && typeof sess.append === 'function' ? sess.append.bind(sess) : undefined;
|
|
1783
|
-
if (!appendFn || sessionTurnActive(sess)) return false;
|
|
1881
|
+
if (!appendFn) return 'fail';
|
|
1882
|
+
// 🔒 等 LLM 回合结束(与 QQ 入站排队同款; 超时=busy 由 drain 保留重试)
|
|
1883
|
+
if (!(await waitTurnIdle(agent, sess))) return 'busy';
|
|
1884
|
+
if (kind === 'relay') {
|
|
1885
|
+
// 用户代发: 文本以「用户代你发送: 」开头(只进 web 流, QQ 收到干净原文); 主人下次真人消息
|
|
1886
|
+
// 开回合时该 user/message 作为历史被 deriveMessages 组装进上下文 → bot 自然看到"主人代我发了这句"。
|
|
1887
|
+
const llm = await import('@deepseek-ai/dsh-llm');
|
|
1888
|
+
const relayText = `用户代你发送: ${text}`;
|
|
1889
|
+
const msg = llm.createUserMessage
|
|
1890
|
+
? llm.createUserMessage({ content: [{ type: 'text', text: relayText }], source: { kind: 'user' } })
|
|
1891
|
+
: undefined;
|
|
1892
|
+
if (!msg) return 'fail';
|
|
1893
|
+
// 与 agent-loop turn()/入群申请通知同款: user/message 的 data 就是消息体本身(不包 message 层)。
|
|
1894
|
+
appendFn('user/message', msg, { surfaceOp: 'append' });
|
|
1895
|
+
} else {
|
|
1896
|
+
// 后台任务结果通知(与通道工具 bgSend notifySession 同款形状: source kind user, 文本 [系统] 后台任务…)
|
|
1784
1897
|
appendFn('user/message', {
|
|
1785
1898
|
id: 'bg-' + Date.now().toString(36) + '-' + Math.floor(Math.random() * 1e9).toString(36),
|
|
1786
1899
|
role: 'user',
|
|
1787
1900
|
content: [{ type: 'text', text: String(text || '') }],
|
|
1788
1901
|
source: { kind: 'user' },
|
|
1789
1902
|
}, { surfaceOp: 'append' });
|
|
1790
|
-
|
|
1791
|
-
|
|
1903
|
+
}
|
|
1904
|
+
return 'ok';
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// 线B 入口: 用户代发→插入上下文(入队即返回, 不阻塞请求)
|
|
1908
|
+
async function appendUserRelayToPeer(ns, scope, peerId, text) {
|
|
1909
|
+
enqueueRelayWrite(ns, scope, peerId, 'relay', text);
|
|
1910
|
+
return null; // 已入队; 实际写入由后台 drain 在回合结束后完成
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// 后台任务结果通知写回会话(入队即返回, 不阻塞请求)
|
|
1914
|
+
async function appendNoticeToPeer(ns, scope, peerId, text) {
|
|
1915
|
+
enqueueRelayWrite(ns, scope, peerId, 'notice', text);
|
|
1916
|
+
return true;
|
|
1792
1917
|
}
|
|
1793
1918
|
|
|
1794
1919
|
// 入群申请红点汇总(悬浮球 dock 用): 遍历所有已配置实例, 返回各实例待审申请数(计数不上报明细)
|