@sunjuntao/dsh-qqbot 0.1.7 → 0.1.13
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/lib/client.js +428 -222
- package/lib/index.js +335 -30
- package/package.json +4 -6
package/lib/index.js
CHANGED
|
@@ -196,7 +196,6 @@ var init_store_file = __esm({
|
|
|
196
196
|
BOT_CONFIG_FIELDS = [
|
|
197
197
|
"workspacePath",
|
|
198
198
|
"agentPreset",
|
|
199
|
-
"agentPresetChat",
|
|
200
199
|
"permissionPreset",
|
|
201
200
|
"model",
|
|
202
201
|
"secretEnv",
|
|
@@ -249,7 +248,6 @@ var init_store_file = __esm({
|
|
|
249
248
|
DEFAULT_BEHAVIOR_CONFIG = {
|
|
250
249
|
workspacePath: "",
|
|
251
250
|
agentPreset: "",
|
|
252
|
-
agentPresetChat: "",
|
|
253
251
|
permissionPreset: "",
|
|
254
252
|
model: "",
|
|
255
253
|
secretEnv: "",
|
|
@@ -3292,8 +3290,7 @@ var GROUP_OVERRIDE_FIELDS = [
|
|
|
3292
3290
|
"maxRepliesPerMessage",
|
|
3293
3291
|
"markdownReply",
|
|
3294
3292
|
"memoryEnabled",
|
|
3295
|
-
"bannedWords"
|
|
3296
|
-
"agentPresetChat"
|
|
3293
|
+
"bannedWords"
|
|
3297
3294
|
];
|
|
3298
3295
|
function str(value) {
|
|
3299
3296
|
return typeof value === "string" ? value.trim() : "";
|
|
@@ -3369,9 +3366,6 @@ function resolveGroupOverrides(raw) {
|
|
|
3369
3366
|
ov.bannedWords = v.filter((w) => typeof w === "string" && w.trim().length > 0).map((w) => w.trim());
|
|
3370
3367
|
}
|
|
3371
3368
|
break;
|
|
3372
|
-
case "agentPresetChat":
|
|
3373
|
-
if (typeof v === "string") ov.agentPresetChat = v.trim();
|
|
3374
|
-
break;
|
|
3375
3369
|
}
|
|
3376
3370
|
}
|
|
3377
3371
|
if (Object.keys(ov).length > 0) out[id] = ov;
|
|
@@ -3399,7 +3393,6 @@ function resolveConfig({ entry = {}, stored = {}, credentials = {} } = {}) {
|
|
|
3399
3393
|
adminToken: str(pick2("adminToken")),
|
|
3400
3394
|
workspacePath: str(pick2("workspacePath")) || process.cwd(),
|
|
3401
3395
|
agentPreset: str(pick2("agentPreset")),
|
|
3402
|
-
agentPresetChat: str(pick2("agentPresetChat")),
|
|
3403
3396
|
permissionPreset: str(pick2("permissionPreset")),
|
|
3404
3397
|
model: parseModelSelection(str(pick2("model"))),
|
|
3405
3398
|
allowC2c: boolOr(pick2("allowC2c"), true),
|
|
@@ -6169,7 +6162,6 @@ function createQqRule({
|
|
|
6169
6162
|
payload,
|
|
6170
6163
|
msgId: payload.id ?? "",
|
|
6171
6164
|
deliveryId: delivery.deliveryId,
|
|
6172
|
-
allowTools: isAt,
|
|
6173
6165
|
quoteContext: quoteContextOf(bot, groupChatKey2, payload, sender, content),
|
|
6174
6166
|
quoteMention: isAt,
|
|
6175
6167
|
attachmentContext: attachCtx2,
|
|
@@ -6228,7 +6220,6 @@ function createQqRule({
|
|
|
6228
6220
|
payload,
|
|
6229
6221
|
msgId,
|
|
6230
6222
|
deliveryId: delivery.deliveryId,
|
|
6231
|
-
allowTools: true,
|
|
6232
6223
|
quoteContext: quoteContextOf(bot, chatKey, payload, sender, content),
|
|
6233
6224
|
quoteMention: true,
|
|
6234
6225
|
attachmentContext: attachCtx,
|
|
@@ -6242,7 +6233,6 @@ function createQqRule({
|
|
|
6242
6233
|
function newMessageId2() {
|
|
6243
6234
|
return randomUUID2();
|
|
6244
6235
|
}
|
|
6245
|
-
var CHAT_ONLY_HINT = "(群聊全量模式·非 @ 触发)不要调用任何工具、不要读写文件、不要执行命令。";
|
|
6246
6236
|
async function enterConversation(args) {
|
|
6247
6237
|
const {
|
|
6248
6238
|
bot,
|
|
@@ -6252,7 +6242,6 @@ async function enterConversation(args) {
|
|
|
6252
6242
|
target,
|
|
6253
6243
|
msgId,
|
|
6254
6244
|
deliveryId,
|
|
6255
|
-
allowTools,
|
|
6256
6245
|
quoteContext,
|
|
6257
6246
|
attachmentContext,
|
|
6258
6247
|
memoryBlock,
|
|
@@ -6292,9 +6281,7 @@ async function enterConversation(args) {
|
|
|
6292
6281
|
bot.state.mergeRecords.set(mergeFp, { record, at: Date.now() });
|
|
6293
6282
|
}
|
|
6294
6283
|
const sender = senderIdOf(payload);
|
|
6295
|
-
const rawBody =
|
|
6296
|
-
|
|
6297
|
-
${promptBuilder()}`;
|
|
6284
|
+
const rawBody = promptBuilder();
|
|
6298
6285
|
if (!rawBody.trim() && !attachmentContext) return null;
|
|
6299
6286
|
if (target.scope === "c2c" && config.typingIndicator && msgId) {
|
|
6300
6287
|
bot.typing.start(chatKey, target, msgId);
|
|
@@ -6338,7 +6325,7 @@ ${prompt}`;
|
|
|
6338
6325
|
workspacePath: config.workspacePath,
|
|
6339
6326
|
title: `QQ: ${(payload.content ?? "").trim().slice(0, 40)}`,
|
|
6340
6327
|
prompt,
|
|
6341
|
-
agentPreset:
|
|
6328
|
+
agentPreset: presets.agentPreset,
|
|
6342
6329
|
permissionPreset: presets.permissionPreset,
|
|
6343
6330
|
...config.model ? { model: config.model } : {}
|
|
6344
6331
|
};
|
|
@@ -6722,7 +6709,7 @@ async function readRawBody(req) {
|
|
|
6722
6709
|
}
|
|
6723
6710
|
return Buffer.concat(chunks, size);
|
|
6724
6711
|
}
|
|
6725
|
-
function registerRpcChannel({ ctx, fence, dispatch }) {
|
|
6712
|
+
function registerRpcChannel({ ctx, fence, dispatch, onWsUpgrade }) {
|
|
6726
6713
|
ctx.inject(["webServer"], (webCtxUnknown) => {
|
|
6727
6714
|
const webCtx = webCtxUnknown;
|
|
6728
6715
|
const route = {
|
|
@@ -6741,7 +6728,8 @@ function registerRpcChannel({ ctx, fence, dispatch }) {
|
|
|
6741
6728
|
}
|
|
6742
6729
|
}
|
|
6743
6730
|
const endpoint = endpointFromPath(RPC_CHANNEL, new URL(req.url ?? "/", "http://localhost").pathname);
|
|
6744
|
-
|
|
6731
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
6732
|
+
if (method !== "POST" || endpoint === void 0) {
|
|
6745
6733
|
res.writeHead(404);
|
|
6746
6734
|
res.end("not found");
|
|
6747
6735
|
return;
|
|
@@ -6780,8 +6768,46 @@ function registerRpcChannel({ ctx, fence, dispatch }) {
|
|
|
6780
6768
|
}
|
|
6781
6769
|
};
|
|
6782
6770
|
webCtx.effect(() => webCtx.webServer.register(route), `dsh-qqbot: ${RPC_CHANNEL} rpc channel`);
|
|
6771
|
+
if (!onWsUpgrade) return;
|
|
6772
|
+
const upgradeRoute = {
|
|
6773
|
+
path: `${RPC_CHANNEL}/events`,
|
|
6774
|
+
handler: (req, socket, head) => {
|
|
6775
|
+
if (fence) {
|
|
6776
|
+
try {
|
|
6777
|
+
const rejection = fence.requestRejection({ headers: req.headers });
|
|
6778
|
+
if (rejection !== void 0) {
|
|
6779
|
+
rejectUpgrade(socket, rejection);
|
|
6780
|
+
return;
|
|
6781
|
+
}
|
|
6782
|
+
} catch {
|
|
6783
|
+
}
|
|
6784
|
+
}
|
|
6785
|
+
onWsUpgrade(req, socket, head);
|
|
6786
|
+
}
|
|
6787
|
+
};
|
|
6788
|
+
webCtx.effect(() => {
|
|
6789
|
+
try {
|
|
6790
|
+
return webCtx.webServer.registerUpgrade(upgradeRoute);
|
|
6791
|
+
} catch (error) {
|
|
6792
|
+
console?.warn?.(`[dsh-qqbot] WebSocket 端点注册失败,面板显隐仅依赖设置页动作: ${String(error)}`);
|
|
6793
|
+
return () => {
|
|
6794
|
+
};
|
|
6795
|
+
}
|
|
6796
|
+
}, `dsh-qqbot: ${RPC_CHANNEL} events websocket`);
|
|
6783
6797
|
});
|
|
6784
6798
|
}
|
|
6799
|
+
function rejectUpgrade(socket, status) {
|
|
6800
|
+
const text = status === 401 ? "Unauthorized" : "Forbidden";
|
|
6801
|
+
try {
|
|
6802
|
+
socket.write(`HTTP/1.1 ${status} ${text}\r
|
|
6803
|
+
Connection: close\r
|
|
6804
|
+
Content-Length: 0\r
|
|
6805
|
+
\r
|
|
6806
|
+
`);
|
|
6807
|
+
} catch {
|
|
6808
|
+
}
|
|
6809
|
+
socket.destroy();
|
|
6810
|
+
}
|
|
6785
6811
|
async function writeJson2(res, response) {
|
|
6786
6812
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
6787
6813
|
const headers = {};
|
|
@@ -6792,6 +6818,236 @@ async function writeJson2(res, response) {
|
|
|
6792
6818
|
res.end(buffer);
|
|
6793
6819
|
}
|
|
6794
6820
|
|
|
6821
|
+
// src/host/admin/ws-hub.ts
|
|
6822
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
6823
|
+
var OPCODE_CONTINUATION = 0;
|
|
6824
|
+
var OPCODE_TEXT = 1;
|
|
6825
|
+
var OPCODE_CLOSE = 8;
|
|
6826
|
+
var OPCODE_PING = 9;
|
|
6827
|
+
var OPCODE_PONG = 10;
|
|
6828
|
+
var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
6829
|
+
var HEARTBEAT_INTERVAL_MS = 25e3;
|
|
6830
|
+
var MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
6831
|
+
var WS_EVENT_BOTS_CHANGED = "bots-changed";
|
|
6832
|
+
var WS_EVENT_SCHED_SEND = "sched-send";
|
|
6833
|
+
function acceptKey(key) {
|
|
6834
|
+
return createHash2("sha1").update(`${key}${WS_GUID}`).digest("base64");
|
|
6835
|
+
}
|
|
6836
|
+
function encodeFrame(opcode, payload) {
|
|
6837
|
+
const length = payload.length;
|
|
6838
|
+
let header;
|
|
6839
|
+
if (length < 126) {
|
|
6840
|
+
header = Buffer.alloc(2);
|
|
6841
|
+
header[1] = length;
|
|
6842
|
+
} else if (length <= 65535) {
|
|
6843
|
+
header = Buffer.alloc(4);
|
|
6844
|
+
header[1] = 126;
|
|
6845
|
+
header.writeUInt16BE(length, 2);
|
|
6846
|
+
} else {
|
|
6847
|
+
header = Buffer.alloc(10);
|
|
6848
|
+
header[1] = 127;
|
|
6849
|
+
header.writeBigUInt64BE(BigInt(length), 2);
|
|
6850
|
+
}
|
|
6851
|
+
header[0] = 128 | opcode;
|
|
6852
|
+
return Buffer.concat([header, payload]);
|
|
6853
|
+
}
|
|
6854
|
+
function parseFrame(buf) {
|
|
6855
|
+
if (buf.length < 2) return { kind: "incomplete", rest: buf };
|
|
6856
|
+
const first = buf[0];
|
|
6857
|
+
const second = buf[1];
|
|
6858
|
+
const fin = (first & 128) !== 0;
|
|
6859
|
+
const opcode = first & 15;
|
|
6860
|
+
const masked = (second & 128) !== 0;
|
|
6861
|
+
let length = second & 127;
|
|
6862
|
+
let offset = 2;
|
|
6863
|
+
if (length === 126) {
|
|
6864
|
+
if (buf.length < offset + 2) return { kind: "incomplete", rest: buf };
|
|
6865
|
+
length = buf.readUInt16BE(offset);
|
|
6866
|
+
offset += 2;
|
|
6867
|
+
} else if (length === 127) {
|
|
6868
|
+
if (buf.length < offset + 8) return { kind: "incomplete", rest: buf };
|
|
6869
|
+
const declared = buf.readBigUInt64BE(offset);
|
|
6870
|
+
offset += 8;
|
|
6871
|
+
if (declared > BigInt(MAX_PAYLOAD_BYTES)) return { kind: "fatal" };
|
|
6872
|
+
length = Number(declared);
|
|
6873
|
+
}
|
|
6874
|
+
if (length > MAX_PAYLOAD_BYTES) return { kind: "fatal" };
|
|
6875
|
+
let mask;
|
|
6876
|
+
if (masked) {
|
|
6877
|
+
if (buf.length < offset + 4) return { kind: "incomplete", rest: buf };
|
|
6878
|
+
mask = buf.subarray(offset, offset + 4);
|
|
6879
|
+
offset += 4;
|
|
6880
|
+
}
|
|
6881
|
+
if (buf.length < offset + length) return { kind: "incomplete", rest: buf };
|
|
6882
|
+
const payload = Buffer.from(buf.subarray(offset, offset + length));
|
|
6883
|
+
if (mask) {
|
|
6884
|
+
for (let i = 0; i < payload.length; i += 1) payload[i] = payload[i] ^ mask[i & 3];
|
|
6885
|
+
}
|
|
6886
|
+
return { kind: "frame", frame: { fin, opcode, payload }, rest: buf.subarray(offset + length) };
|
|
6887
|
+
}
|
|
6888
|
+
function isReservedOpcode(opcode) {
|
|
6889
|
+
return opcode >= 3 && opcode <= 7 || opcode >= 11;
|
|
6890
|
+
}
|
|
6891
|
+
function createWsHub({ logger }) {
|
|
6892
|
+
const clients = /* @__PURE__ */ new Set();
|
|
6893
|
+
const drop = (client) => {
|
|
6894
|
+
clients.delete(client);
|
|
6895
|
+
};
|
|
6896
|
+
const send = (client, opcode, payload) => {
|
|
6897
|
+
if (client.socket.destroyed) {
|
|
6898
|
+
drop(client);
|
|
6899
|
+
return;
|
|
6900
|
+
}
|
|
6901
|
+
try {
|
|
6902
|
+
client.socket.write(encodeFrame(opcode, payload), (error) => {
|
|
6903
|
+
if (error) drop(client);
|
|
6904
|
+
});
|
|
6905
|
+
} catch (error) {
|
|
6906
|
+
drop(client);
|
|
6907
|
+
logger.warn("[dsh-qqbot] WebSocket 写入失败,已摘除该客户端:", error);
|
|
6908
|
+
}
|
|
6909
|
+
};
|
|
6910
|
+
const close = (client, code = 1001) => {
|
|
6911
|
+
if (!client.socket.destroyed) {
|
|
6912
|
+
const body = Buffer.alloc(2);
|
|
6913
|
+
body.writeUInt16BE(code, 0);
|
|
6914
|
+
send(client, OPCODE_CLOSE, body);
|
|
6915
|
+
}
|
|
6916
|
+
drop(client);
|
|
6917
|
+
try {
|
|
6918
|
+
client.socket.end();
|
|
6919
|
+
} catch {
|
|
6920
|
+
}
|
|
6921
|
+
};
|
|
6922
|
+
const attach = (client, onText, head) => {
|
|
6923
|
+
let buffer = Buffer.alloc(0);
|
|
6924
|
+
const fragments = [];
|
|
6925
|
+
let fragmentOpcode = OPCODE_CONTINUATION;
|
|
6926
|
+
const feed = (chunk) => {
|
|
6927
|
+
buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]);
|
|
6928
|
+
if (buffer.length > MAX_PAYLOAD_BYTES + 16) {
|
|
6929
|
+
client.socket.destroy();
|
|
6930
|
+
return;
|
|
6931
|
+
}
|
|
6932
|
+
for (; ; ) {
|
|
6933
|
+
const result = parseFrame(buffer);
|
|
6934
|
+
if (result.kind === "incomplete") {
|
|
6935
|
+
buffer = result.rest;
|
|
6936
|
+
return;
|
|
6937
|
+
}
|
|
6938
|
+
if (result.kind === "fatal") {
|
|
6939
|
+
client.socket.destroy();
|
|
6940
|
+
return;
|
|
6941
|
+
}
|
|
6942
|
+
buffer = result.rest;
|
|
6943
|
+
const { fin, opcode, payload } = result.frame;
|
|
6944
|
+
if (opcode === OPCODE_CONTINUATION) {
|
|
6945
|
+
fragments.push(payload);
|
|
6946
|
+
if (!fin) continue;
|
|
6947
|
+
const full = Buffer.concat(fragments);
|
|
6948
|
+
fragments.length = 0;
|
|
6949
|
+
if (fragmentOpcode === OPCODE_TEXT) onText(full.toString("utf8"));
|
|
6950
|
+
fragmentOpcode = OPCODE_CONTINUATION;
|
|
6951
|
+
continue;
|
|
6952
|
+
}
|
|
6953
|
+
if (isReservedOpcode(opcode)) {
|
|
6954
|
+
client.socket.destroy();
|
|
6955
|
+
return;
|
|
6956
|
+
}
|
|
6957
|
+
if (opcode === OPCODE_TEXT) {
|
|
6958
|
+
if (!fin) {
|
|
6959
|
+
fragmentOpcode = OPCODE_TEXT;
|
|
6960
|
+
fragments.length = 0;
|
|
6961
|
+
fragments.push(payload);
|
|
6962
|
+
continue;
|
|
6963
|
+
}
|
|
6964
|
+
onText(payload.toString("utf8"));
|
|
6965
|
+
continue;
|
|
6966
|
+
}
|
|
6967
|
+
if (opcode === OPCODE_PING) {
|
|
6968
|
+
send(client, OPCODE_PONG, payload);
|
|
6969
|
+
continue;
|
|
6970
|
+
}
|
|
6971
|
+
if (opcode === OPCODE_PONG) {
|
|
6972
|
+
client.alive = true;
|
|
6973
|
+
continue;
|
|
6974
|
+
}
|
|
6975
|
+
if (opcode === OPCODE_CLOSE) {
|
|
6976
|
+
close(client, 1e3);
|
|
6977
|
+
return;
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
};
|
|
6981
|
+
client.socket.on("data", (chunk) => {
|
|
6982
|
+
feed(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8"));
|
|
6983
|
+
});
|
|
6984
|
+
client.socket.on("error", () => drop(client));
|
|
6985
|
+
client.socket.on("close", () => drop(client));
|
|
6986
|
+
if (head.length > 0) feed(head);
|
|
6987
|
+
};
|
|
6988
|
+
const timer = setInterval(() => {
|
|
6989
|
+
for (const client of [...clients]) {
|
|
6990
|
+
if (!client.alive) {
|
|
6991
|
+
drop(client);
|
|
6992
|
+
try {
|
|
6993
|
+
client.socket.destroy();
|
|
6994
|
+
} catch {
|
|
6995
|
+
}
|
|
6996
|
+
continue;
|
|
6997
|
+
}
|
|
6998
|
+
client.alive = false;
|
|
6999
|
+
send(client, OPCODE_PING, Buffer.alloc(0));
|
|
7000
|
+
}
|
|
7001
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
7002
|
+
timer.unref?.();
|
|
7003
|
+
return {
|
|
7004
|
+
handleUpgrade(req, socket, head) {
|
|
7005
|
+
const upgrade = String(req.headers.upgrade ?? "").toLowerCase();
|
|
7006
|
+
const key = req.headers["sec-websocket-key"];
|
|
7007
|
+
const versions = String(req.headers["sec-websocket-version"] ?? "").split(",").map((v) => v.trim());
|
|
7008
|
+
if (upgrade !== "websocket" || typeof key !== "string" || key === "" || !versions.includes("13")) {
|
|
7009
|
+
try {
|
|
7010
|
+
socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
7011
|
+
} catch {
|
|
7012
|
+
}
|
|
7013
|
+
socket.destroy();
|
|
7014
|
+
return;
|
|
7015
|
+
}
|
|
7016
|
+
try {
|
|
7017
|
+
socket.write(
|
|
7018
|
+
`HTTP/1.1 101 Switching Protocols\r
|
|
7019
|
+
Upgrade: websocket\r
|
|
7020
|
+
Connection: Upgrade\r
|
|
7021
|
+
Sec-WebSocket-Accept: ${acceptKey(key)}\r
|
|
7022
|
+
\r
|
|
7023
|
+
`
|
|
7024
|
+
);
|
|
7025
|
+
} catch (error) {
|
|
7026
|
+
logger.warn("[dsh-qqbot] WebSocket 握手响应失败:", error);
|
|
7027
|
+
socket.destroy();
|
|
7028
|
+
return;
|
|
7029
|
+
}
|
|
7030
|
+
const client = { socket, alive: true };
|
|
7031
|
+
clients.add(client);
|
|
7032
|
+
attach(client, () => {
|
|
7033
|
+
}, head ?? Buffer.alloc(0));
|
|
7034
|
+
},
|
|
7035
|
+
broadcast(event, data) {
|
|
7036
|
+
if (clients.size === 0) return;
|
|
7037
|
+
const payload = Buffer.from(JSON.stringify({ event, data: data ?? null, at: Date.now() }), "utf8");
|
|
7038
|
+
for (const client of [...clients]) send(client, OPCODE_TEXT, payload);
|
|
7039
|
+
},
|
|
7040
|
+
clientCount() {
|
|
7041
|
+
return clients.size;
|
|
7042
|
+
},
|
|
7043
|
+
dispose() {
|
|
7044
|
+
clearInterval(timer);
|
|
7045
|
+
for (const client of [...clients]) close(client);
|
|
7046
|
+
clients.clear();
|
|
7047
|
+
}
|
|
7048
|
+
};
|
|
7049
|
+
}
|
|
7050
|
+
|
|
6795
7051
|
// src/host/messaging/self-id.ts
|
|
6796
7052
|
init_store_file();
|
|
6797
7053
|
import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile7 } from "node:fs/promises";
|
|
@@ -6997,6 +7253,7 @@ var QqWsSource = class {
|
|
|
6997
7253
|
#onEvent;
|
|
6998
7254
|
#onRawEvent;
|
|
6999
7255
|
#onInteraction;
|
|
7256
|
+
#onStatusChange;
|
|
7000
7257
|
#bot = null;
|
|
7001
7258
|
#abort = null;
|
|
7002
7259
|
#starting = null;
|
|
@@ -7006,10 +7263,25 @@ var QqWsSource = class {
|
|
|
7006
7263
|
this.#onEvent = options.onEvent;
|
|
7007
7264
|
this.#onRawEvent = options.onRawEvent;
|
|
7008
7265
|
this.#onInteraction = options.onInteraction;
|
|
7266
|
+
this.#onStatusChange = options.onStatusChange;
|
|
7009
7267
|
}
|
|
7010
7268
|
get status() {
|
|
7011
7269
|
return { ...this.#status };
|
|
7012
7270
|
}
|
|
7271
|
+
/**
|
|
7272
|
+
* 统一的状态赋值入口:实际发生 state / lastError 变化时触发 onStatusChange。
|
|
7273
|
+
* events 计数递增走裸赋值(#accept),不经过这里。
|
|
7274
|
+
*/
|
|
7275
|
+
#setStatus(status) {
|
|
7276
|
+
const prev = this.#status;
|
|
7277
|
+
this.#status = status;
|
|
7278
|
+
if (prev.state !== status.state || prev.lastError !== status.lastError) {
|
|
7279
|
+
try {
|
|
7280
|
+
this.#onStatusChange?.(status.appId, { ...status });
|
|
7281
|
+
} catch {
|
|
7282
|
+
}
|
|
7283
|
+
}
|
|
7284
|
+
}
|
|
7013
7285
|
/** 底层 SDK 实例(已连接后可用),供媒体上传 / 撤回 / 原生 API 调用。 */
|
|
7014
7286
|
get sdk() {
|
|
7015
7287
|
return this.#bot;
|
|
@@ -7018,13 +7290,13 @@ var QqWsSource = class {
|
|
|
7018
7290
|
async start(config) {
|
|
7019
7291
|
await this.stop();
|
|
7020
7292
|
if (!config.appId || !config.appSecret) {
|
|
7021
|
-
this.#
|
|
7293
|
+
this.#setStatus({ ...this.#status, state: "idle", appId: config.appId, lastError: null });
|
|
7022
7294
|
this.#logger.warn("[dsh-qqbot] WebSocket 未启动:凭据不完整(请扫码登录或填写 AppID/AppSecret)");
|
|
7023
7295
|
return;
|
|
7024
7296
|
}
|
|
7025
7297
|
const run = this.#start(config).catch((error) => {
|
|
7026
7298
|
const message = error instanceof Error ? error.message : String(error);
|
|
7027
|
-
this.#
|
|
7299
|
+
this.#setStatus({ ...this.#status, state: "failed", lastError: message });
|
|
7028
7300
|
this.#logger.error("[dsh-qqbot] WebSocket 连接失败:", error);
|
|
7029
7301
|
});
|
|
7030
7302
|
this.#starting = run;
|
|
@@ -7044,7 +7316,7 @@ var QqWsSource = class {
|
|
|
7044
7316
|
bot?.stop();
|
|
7045
7317
|
} catch {
|
|
7046
7318
|
}
|
|
7047
|
-
this.#
|
|
7319
|
+
this.#setStatus({ ...this.#status, state: "idle" });
|
|
7048
7320
|
}
|
|
7049
7321
|
async #start(config) {
|
|
7050
7322
|
const options = {
|
|
@@ -7064,18 +7336,18 @@ var QqWsSource = class {
|
|
|
7064
7336
|
const abort = new AbortController();
|
|
7065
7337
|
this.#bot = bot;
|
|
7066
7338
|
this.#abort = abort;
|
|
7067
|
-
this.#
|
|
7339
|
+
this.#setStatus({ state: "connecting", appId: config.appId, lastConnectedAt: null, lastError: null, events: 0 });
|
|
7068
7340
|
bot.on("ready", () => {
|
|
7069
|
-
this.#
|
|
7341
|
+
this.#setStatus({ ...this.#status, state: "connected", lastConnectedAt: Date.now(), lastError: null });
|
|
7070
7342
|
this.#logger.info(`[dsh-qqbot] QQ WebSocket 已连接(AppID ${config.appId}),开始接收消息`);
|
|
7071
7343
|
});
|
|
7072
7344
|
bot.on("resumed", () => {
|
|
7073
|
-
this.#
|
|
7345
|
+
this.#setStatus({ ...this.#status, state: "connected", lastError: null });
|
|
7074
7346
|
this.#logger.info("[dsh-qqbot] QQ WebSocket 会话已恢复(RESUME)");
|
|
7075
7347
|
});
|
|
7076
7348
|
bot.on("error", (error) => {
|
|
7077
7349
|
const message = error instanceof Error ? error.message : String(error);
|
|
7078
|
-
this.#
|
|
7350
|
+
this.#setStatus({ ...this.#status, lastError: message });
|
|
7079
7351
|
if (this.#status.state === "connected") {
|
|
7080
7352
|
this.#logger.warn("[dsh-qqbot] QQ WebSocket 连接错误(SDK 将自动重连):", error);
|
|
7081
7353
|
}
|
|
@@ -7122,7 +7394,7 @@ var QqWsSource = class {
|
|
|
7122
7394
|
const runTask = Promise.resolve().then(() => bot.start(abort.signal));
|
|
7123
7395
|
runTask.catch((error) => {
|
|
7124
7396
|
if (abort.signal.aborted) return;
|
|
7125
|
-
this.#
|
|
7397
|
+
this.#setStatus({ ...this.#status, state: "failed", lastError: error instanceof Error ? error.message : String(error) });
|
|
7126
7398
|
this.#logger.error("[dsh-qqbot] QQ WebSocket 连接停止:", error);
|
|
7127
7399
|
});
|
|
7128
7400
|
let timer;
|
|
@@ -7218,6 +7490,8 @@ var BotRuntimeManager = class {
|
|
|
7218
7490
|
#statsTimer = null;
|
|
7219
7491
|
#syncing = null;
|
|
7220
7492
|
#primaryAppId = "";
|
|
7493
|
+
/** onBotsChanged 去抖定时器:窗口内的多次状态变化合并成一次通知。 */
|
|
7494
|
+
#notifyTimer = null;
|
|
7221
7495
|
constructor(options) {
|
|
7222
7496
|
this.#options = options;
|
|
7223
7497
|
this.#stats = new StatsStore(options.logger);
|
|
@@ -7277,6 +7551,19 @@ var BotRuntimeManager = class {
|
|
|
7277
7551
|
map.delete(key);
|
|
7278
7552
|
}
|
|
7279
7553
|
}
|
|
7554
|
+
// ── 变更通知(WebSocket 推送等订阅方) ─────────────────────────────────────
|
|
7555
|
+
/** 去抖通知:300ms 窗口内的多次变化(sync 收尾 + 各 ws 状态迁移)合并为一次。 */
|
|
7556
|
+
#notifyChanged() {
|
|
7557
|
+
if (this.#notifyTimer) return;
|
|
7558
|
+
this.#notifyTimer = setTimeout(() => {
|
|
7559
|
+
this.#notifyTimer = null;
|
|
7560
|
+
try {
|
|
7561
|
+
this.#options.onBotsChanged?.();
|
|
7562
|
+
} catch {
|
|
7563
|
+
}
|
|
7564
|
+
}, 300);
|
|
7565
|
+
this.#notifyTimer.unref?.();
|
|
7566
|
+
}
|
|
7280
7567
|
// ── 同步:把 bots.json 应用到运行时 ────────────────────────────────────────
|
|
7281
7568
|
/**
|
|
7282
7569
|
* 按存储层重建/更新/停止各机器人连接。
|
|
@@ -7340,6 +7627,7 @@ var BotRuntimeManager = class {
|
|
|
7340
7627
|
this.#bots.delete(appId);
|
|
7341
7628
|
this.#options.logger.info(`[dsh-qqbot] 机器人 ${appId} 已移除,连接已断开`);
|
|
7342
7629
|
}
|
|
7630
|
+
this.#notifyChanged();
|
|
7343
7631
|
}
|
|
7344
7632
|
#buildConfig(stored, secret) {
|
|
7345
7633
|
return resolveConfig({
|
|
@@ -7402,6 +7690,9 @@ var BotRuntimeManager = class {
|
|
|
7402
7690
|
},
|
|
7403
7691
|
onInteraction: (event) => {
|
|
7404
7692
|
this.#options.onInteraction?.(bot, event);
|
|
7693
|
+
},
|
|
7694
|
+
onStatusChange: () => {
|
|
7695
|
+
this.#notifyChanged();
|
|
7405
7696
|
}
|
|
7406
7697
|
});
|
|
7407
7698
|
return bot;
|
|
@@ -7466,7 +7757,7 @@ init_store_file();
|
|
|
7466
7757
|
|
|
7467
7758
|
// src/host/schedule/schedule.ts
|
|
7468
7759
|
init_store_file();
|
|
7469
|
-
import { createHash as
|
|
7760
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
|
|
7470
7761
|
|
|
7471
7762
|
// src/shared/cron.ts
|
|
7472
7763
|
var MONTH_NAMES = {
|
|
@@ -8404,7 +8695,7 @@ var ScheduleStore = class {
|
|
|
8404
8695
|
}
|
|
8405
8696
|
};
|
|
8406
8697
|
function contentDigest(text) {
|
|
8407
|
-
return
|
|
8698
|
+
return createHash3("sha1").update((text ?? "").replace(/\s+/g, " ").trim()).digest("hex").slice(0, 16);
|
|
8408
8699
|
}
|
|
8409
8700
|
function gateSkipReason(entry, text) {
|
|
8410
8701
|
const gate = entry.gate ?? "always";
|
|
@@ -8454,6 +8745,11 @@ function nextRunFor(entry, now) {
|
|
|
8454
8745
|
if (entry.type === "at") return entry.at ? new Date(entry.at) : null;
|
|
8455
8746
|
return null;
|
|
8456
8747
|
}
|
|
8748
|
+
function sendEventSummary(entry) {
|
|
8749
|
+
const raw = entry.mode === "tool" ? entry.command ?? entry.content ?? "" : entry.content ?? "";
|
|
8750
|
+
const text = String(raw).trim();
|
|
8751
|
+
return text ? text.slice(0, 60) : entry.id;
|
|
8752
|
+
}
|
|
8457
8753
|
var Scheduler = class {
|
|
8458
8754
|
#ctx;
|
|
8459
8755
|
#timer = null;
|
|
@@ -8566,11 +8862,13 @@ var Scheduler = class {
|
|
|
8566
8862
|
this.#ctx.logger.info(
|
|
8567
8863
|
`[dsh-qqbot] 定时任务跳过发送(机器人 ${bot.appId},mode=tool,gate=${entry.gate ?? "always"}):${reason}`
|
|
8568
8864
|
);
|
|
8865
|
+
this.#ctx.onSendEvent?.({ summary: sendEventSummary(entry), appId: bot.appId, ok: true, skipped: true, reason });
|
|
8569
8866
|
} else {
|
|
8570
8867
|
bot.state.counters.proactive += 1;
|
|
8571
8868
|
entry.lastSentAt = toShanghaiISO(now);
|
|
8572
8869
|
entry.lastError = void 0;
|
|
8573
8870
|
entry.lastSkipReason = void 0;
|
|
8871
|
+
this.#ctx.onSendEvent?.({ summary: sendEventSummary(entry), appId: bot.appId, ok: true, skipped: false });
|
|
8574
8872
|
}
|
|
8575
8873
|
if (entry.type === "at") {
|
|
8576
8874
|
await this.#ctx.store.removeById(entry.id);
|
|
@@ -8586,6 +8884,7 @@ var Scheduler = class {
|
|
|
8586
8884
|
scheduleNext(entry);
|
|
8587
8885
|
if (entry.nextRunAt) entry.lastError = message;
|
|
8588
8886
|
this.#ctx.logger.error("[dsh-qqbot] 定时任务执行失败:", error);
|
|
8887
|
+
this.#ctx.onSendEvent?.({ summary: sendEventSummary(entry), appId: bot.appId, ok: false, skipped: false, reason: message });
|
|
8589
8888
|
}
|
|
8590
8889
|
void bot.archiver.append({
|
|
8591
8890
|
kind: "proactive",
|
|
@@ -10270,6 +10569,7 @@ async function apply(ctx, entryConfig) {
|
|
|
10270
10569
|
sanitizeOutgoingText(collected, { enabled: bot.config.sanitizeReplies })
|
|
10271
10570
|
);
|
|
10272
10571
|
};
|
|
10572
|
+
const wsHub = createWsHub({ logger });
|
|
10273
10573
|
const scheduler = new Scheduler({
|
|
10274
10574
|
store: schedules,
|
|
10275
10575
|
resolveBot: (appId) => (appId ? bots.get(appId) : void 0) ?? bots.primary(),
|
|
@@ -10279,7 +10579,9 @@ async function apply(ctx, entryConfig) {
|
|
|
10279
10579
|
logger,
|
|
10280
10580
|
// 宿主会话总线:测试执行时监听 session/event 等待真实投递结果
|
|
10281
10581
|
//(cordis ctx 运行时有 on/off,静态类型缺 off,此处断言)。
|
|
10282
|
-
bus: ctx
|
|
10582
|
+
bus: ctx,
|
|
10583
|
+
// 到点触发结果 → 客户端后台任务面板(文件工作台底部任务按钮)。
|
|
10584
|
+
onSendEvent: (info) => wsHub.broadcast(WS_EVENT_SCHED_SEND, info)
|
|
10283
10585
|
});
|
|
10284
10586
|
scheduler.start();
|
|
10285
10587
|
scriptGen.flushPending();
|
|
@@ -10305,6 +10607,7 @@ async function apply(ctx, entryConfig) {
|
|
|
10305
10607
|
adminToken,
|
|
10306
10608
|
resolveSecret,
|
|
10307
10609
|
resolvePresets,
|
|
10610
|
+
onBotsChanged: () => wsHub.broadcast(WS_EVENT_BOTS_CHANGED),
|
|
10308
10611
|
onEvent: (bot, eventType, payload, deliveryId) => {
|
|
10309
10612
|
if (!runtime) {
|
|
10310
10613
|
logger.warn("[dsh-qqbot] webhook 运行时不可用,事件已丢弃");
|
|
@@ -10471,7 +10774,8 @@ async function apply(ctx, entryConfig) {
|
|
|
10471
10774
|
registerRpcChannel({
|
|
10472
10775
|
ctx,
|
|
10473
10776
|
fence: connection,
|
|
10474
|
-
dispatch: (endpoint, payload) => admin.handle(endpoint, payload)
|
|
10777
|
+
dispatch: (endpoint, payload) => admin.handle(endpoint, payload),
|
|
10778
|
+
onWsUpgrade: (req, socket, head) => wsHub.handleUpgrade(req, socket, head)
|
|
10475
10779
|
});
|
|
10476
10780
|
} catch (error) {
|
|
10477
10781
|
logger.warn("[dsh-qqbot] RPC 通道注册失败,设置界面将无法连接(HTTP 管理端点仍可用):", error);
|
|
@@ -10498,6 +10802,7 @@ async function apply(ctx, entryConfig) {
|
|
|
10498
10802
|
disposeTools();
|
|
10499
10803
|
scheduler.stop();
|
|
10500
10804
|
qr.dispose();
|
|
10805
|
+
wsHub.dispose();
|
|
10501
10806
|
await bots.stopAll();
|
|
10502
10807
|
if (disposeRule) {
|
|
10503
10808
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sunjuntao/dsh-qqbot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "把 QQ 官方机器人(Webhook 回调)接入本机 DeepSeek Harness:QQ 群/单聊消息(含群全量消息上下文)→ DSH 会话 → 回复发回 QQ,支持被动回复与群主动消息。Connect the official QQ bot (webhook callback) to a local DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"@deepseek-ai/dsh-client-runtime",
|
|
47
47
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
48
48
|
"@deepseek-ai/dsh-client-ui-slots",
|
|
49
|
-
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
50
49
|
"@deepseek-ai/dsh-client-locale"
|
|
51
50
|
],
|
|
52
51
|
"platform": "web"
|
|
@@ -60,7 +59,6 @@
|
|
|
60
59
|
},
|
|
61
60
|
"scripts": {
|
|
62
61
|
"link-dsh-deps": "node scripts/deploy/link-dsh-deps.mjs",
|
|
63
|
-
"postinstall": "node scripts/deploy/link-dsh-deps.mjs",
|
|
64
62
|
"build": "node scripts/build/build.mjs",
|
|
65
63
|
"build:cli": "node scripts/build/build-cli.mjs",
|
|
66
64
|
"build:types": "tsc --emitDeclarationOnly --declaration --rootDir src --outDir types --skipLibCheck",
|
|
@@ -82,9 +80,9 @@
|
|
|
82
80
|
},
|
|
83
81
|
"peerDependencies": {
|
|
84
82
|
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
85
|
-
"@deepseek-ai/dsh-credentials": "
|
|
86
|
-
"@deepseek-ai/dsh-host-webserver": "
|
|
87
|
-
"@deepseek-ai/dsh-webhook": "0.1.
|
|
83
|
+
"@deepseek-ai/dsh-credentials": ">=0.1.6-alpha.1 <0.2.0",
|
|
84
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.6-alpha.1 <0.2.0",
|
|
85
|
+
"@deepseek-ai/dsh-webhook": ">=0.1.6-alpha.1 <0.2.0",
|
|
88
86
|
"@deepseek-ai/schemastery": "^3.18.0"
|
|
89
87
|
},
|
|
90
88
|
"peerDependenciesMeta": {
|