@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.
Files changed (3) hide show
  1. package/lib/client.js +428 -222
  2. package/lib/index.js +335 -30
  3. 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 = allowTools ? promptBuilder() : `${CHAT_ONLY_HINT}
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: allowTools ? presets.agentPreset : config.agentPresetChat || presets.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
- if ((req.method ?? "GET").toUpperCase() !== "POST" || endpoint === void 0) {
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.#status = { ...this.#status, state: "idle", appId: config.appId, lastError: null };
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.#status = { ...this.#status, state: "failed", lastError: message };
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.#status = { ...this.#status, state: "idle" };
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.#status = { state: "connecting", appId: config.appId, lastConnectedAt: null, lastError: null, events: 0 };
7339
+ this.#setStatus({ state: "connecting", appId: config.appId, lastConnectedAt: null, lastError: null, events: 0 });
7068
7340
  bot.on("ready", () => {
7069
- this.#status = { ...this.#status, state: "connected", lastConnectedAt: Date.now(), lastError: null };
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.#status = { ...this.#status, state: "connected", lastError: null };
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.#status = { ...this.#status, lastError: message };
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.#status = { ...this.#status, state: "failed", lastError: error instanceof Error ? error.message : String(error) };
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 createHash2, randomUUID as randomUUID3 } from "node:crypto";
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 createHash2("sha1").update((text ?? "").replace(/\s+/g, " ").trim()).digest("hex").slice(0, 16);
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.7",
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": "0.0.1-rc.1 || >=0.1.0-rc.1 <0.2.0-0",
86
- "@deepseek-ai/dsh-host-webserver": "0.0.1-rc.1 || >=0.1.0-rc.1 <0.2.0-0",
87
- "@deepseek-ai/dsh-webhook": "0.1.2-alpha.2 || >=0.1.2-rc.1 <0.2.0-0",
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": {