@sunjuntao/dsh-qqbot 0.1.7 → 0.1.10
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 +195 -31
- package/lib/index.js +320 -13
- package/package.json +4 -4
package/lib/client.js
CHANGED
|
@@ -608,6 +608,7 @@ var cn = Object.freeze({
|
|
|
608
608
|
"update.noNewVersion": "暂无新版本(当前 v{0} 已是最新)",
|
|
609
609
|
"update.found": "发现新版本 v{0},正在自动更新…",
|
|
610
610
|
"update.updatedTo": "已自动更新到 v{0}(备份于安装目录 .update-backup/),重启 DSH 后生效",
|
|
611
|
+
"update.keepOpen": "更新尚未执行完,请勿关闭本面板",
|
|
611
612
|
// ── replyLocale ──
|
|
612
613
|
"replyLocale.label": "回复语言(replyLocale)",
|
|
613
614
|
"replyLocale.labelShort": "回复语言",
|
|
@@ -754,7 +755,7 @@ var cn = Object.freeze({
|
|
|
754
755
|
"panel.archiveAllHint": "所有机器人模式仅显示聚合条数;选择单个机器人可展开预览记录",
|
|
755
756
|
"panel.tabId": "面板",
|
|
756
757
|
"ui.panelSwitchTitle": "右侧面板「QQ 定时消息」入口",
|
|
757
|
-
"ui.panelSwitchDesc": "
|
|
758
|
+
"ui.panelSwitchDesc": "默认关闭。开启后,编辑器右侧栏显示 QQ 定时消息管理入口并记住该选择:刷新页面、重启 DSH、QQ 掉线都不会让它消失;关闭即隐藏(改动即时生效)",
|
|
758
759
|
"sched.view": "查看",
|
|
759
760
|
"sched.back": "返回",
|
|
760
761
|
"sched.detailTitle": "执行逻辑详情",
|
|
@@ -1344,6 +1345,7 @@ var en = Object.freeze({
|
|
|
1344
1345
|
"update.noNewVersion": "No new version (v{0} is the latest)",
|
|
1345
1346
|
"update.found": "New version v{0} found; updating automatically…",
|
|
1346
1347
|
"update.updatedTo": "Updated to v{0} (old files backed up in .update-backup/ inside the install directory). Restart DSH to take effect.",
|
|
1348
|
+
"update.keepOpen": "Update still running — do not close this panel",
|
|
1347
1349
|
// ── replyLocale ──
|
|
1348
1350
|
"replyLocale.label": "Reply language (replyLocale)",
|
|
1349
1351
|
"replyLocale.labelShort": "Reply language",
|
|
@@ -1490,7 +1492,7 @@ var en = Object.freeze({
|
|
|
1490
1492
|
"panel.archiveAllHint": "All-bots mode shows aggregated counts only; pick a single bot to expand and preview records",
|
|
1491
1493
|
"panel.tabId": "Tab",
|
|
1492
1494
|
"ui.panelSwitchTitle": 'Right panel "QQ Scheduled Messages" entry',
|
|
1493
|
-
"ui.panelSwitchDesc": "When on, the QQ scheduled-messages manager appears in the right sidebar
|
|
1495
|
+
"ui.panelSwitchDesc": "Off by default. When on, the QQ scheduled-messages manager appears in the right sidebar and the choice is remembered: reloading, restarting DSH, or losing the QQ connection will not hide it; off hides it (takes effect immediately)",
|
|
1494
1496
|
"sched.view": "View",
|
|
1495
1497
|
"sched.back": "Back",
|
|
1496
1498
|
"sched.detailTitle": "Execution details",
|
|
@@ -1745,6 +1747,53 @@ function QqBotGuideIcon(props) {
|
|
|
1745
1747
|
return h(QqBotGlyph, { uid: props.uid ?? "guide", className: props.className, size: props.size });
|
|
1746
1748
|
}
|
|
1747
1749
|
|
|
1750
|
+
// src/client/settings-nav-icon.ts
|
|
1751
|
+
var QBOT_SETTINGS_NAV_MARKER = "data-qbot-settings-nav";
|
|
1752
|
+
var NAV_ICON_MASK = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5v2.5'/%3E%3Ccircle cx='12' cy='2.5' r='.9' fill='black' stroke='none'/%3E%3Crect x='5' y='6' width='14' height='12' rx='3.5'/%3E%3Ccircle cx='9.4' cy='11.5' r='.9' fill='black' stroke='none'/%3E%3Ccircle cx='14.6' cy='11.5' r='.9' fill='black' stroke='none'/%3E%3Cpath d='M9.5 14.8q2.5 1.8 5 0'/%3E%3C/svg%3E";
|
|
1753
|
+
var QBOT_SETTINGS_NAV_CSS = `
|
|
1754
|
+
[${QBOT_SETTINGS_NAV_MARKER}] > svg:first-child {
|
|
1755
|
+
display: none;
|
|
1756
|
+
}
|
|
1757
|
+
[${QBOT_SETTINGS_NAV_MARKER}]::before {
|
|
1758
|
+
content: '';
|
|
1759
|
+
flex: none;
|
|
1760
|
+
width: 16px;
|
|
1761
|
+
height: 16px;
|
|
1762
|
+
background: currentColor;
|
|
1763
|
+
-webkit-mask: url("${NAV_ICON_MASK}") center / contain no-repeat;
|
|
1764
|
+
mask: url("${NAV_ICON_MASK}") center / contain no-repeat;
|
|
1765
|
+
}
|
|
1766
|
+
`;
|
|
1767
|
+
function registerQbotSettingsNavIcon(label) {
|
|
1768
|
+
let disposed = false;
|
|
1769
|
+
const sync = () => {
|
|
1770
|
+
if (disposed) return;
|
|
1771
|
+
const currentLabel = label().trim();
|
|
1772
|
+
const buttons = document.querySelectorAll(
|
|
1773
|
+
'[role="dialog"] nav button'
|
|
1774
|
+
);
|
|
1775
|
+
for (const button of Array.from(buttons)) {
|
|
1776
|
+
const matches2 = currentLabel.length > 0 && button.textContent?.trim() === currentLabel;
|
|
1777
|
+
if (matches2) button.setAttribute(QBOT_SETTINGS_NAV_MARKER, "");
|
|
1778
|
+
else button.removeAttribute(QBOT_SETTINGS_NAV_MARKER);
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
sync();
|
|
1782
|
+
const observer = new MutationObserver(sync);
|
|
1783
|
+
observer.observe(document.body, {
|
|
1784
|
+
childList: true,
|
|
1785
|
+
subtree: true,
|
|
1786
|
+
characterData: true
|
|
1787
|
+
});
|
|
1788
|
+
return () => {
|
|
1789
|
+
disposed = true;
|
|
1790
|
+
observer.disconnect();
|
|
1791
|
+
document.querySelectorAll(`[${QBOT_SETTINGS_NAV_MARKER}]`).forEach((element) => {
|
|
1792
|
+
element.removeAttribute(QBOT_SETTINGS_NAV_MARKER);
|
|
1793
|
+
});
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1748
1797
|
// src/client/ui.tsx
|
|
1749
1798
|
var React2 = __toESM(require("react"), 1);
|
|
1750
1799
|
|
|
@@ -1999,6 +2048,10 @@ var CSS_TEXT = `
|
|
|
1999
2048
|
.qbot-noticeCount { margin-left: auto; flex: none; align-self: flex-start; font-variant-numeric: tabular-nums; font-weight: 600; font-size: 12px; line-height: 1.4; padding: 1px 9px; border-radius: 999px; white-space: nowrap; background: color-mix(in srgb, var(--qbot-business) 14%, var(--dsw-alias-bg-layer-1, #fff)); border: 1px solid color-mix(in srgb, var(--qbot-business) 28%, var(--dsw-alias-border-l2, #dfe1e5)); }
|
|
2000
2049
|
/* 页面级更新提示(qbot-page 是普通块布局,无 channelPage 的 flex gap,需自带下边距与面板隔开) */
|
|
2001
2050
|
.qbot-updateNotice { margin: 0 0 18px; }
|
|
2051
|
+
/* 进行中:不吃 8s 淡出动画(更新可能耗时较久),并改为竖排以突出「请勿关闭」警示行。 */
|
|
2052
|
+
.qbot-updateNotice.is-busy { flex-direction: column; align-items: stretch; gap: 6px; animation: none; }
|
|
2053
|
+
.qbot-updateMsg { min-width: 0; overflow-wrap: anywhere; }
|
|
2054
|
+
.qbot-updateKeepOpen { color: var(--dsw-alias-state-error-primary, #d54941); font-size: 13px; font-weight: 650; line-height: 1.5; }
|
|
2002
2055
|
|
|
2003
2056
|
/* ── 机器人卡片(dim-botCard)─────────────────────────────────────────── */
|
|
2004
2057
|
.qbot-botList { min-width: 0; width: 100%; max-width: 100%; display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
|
|
@@ -5193,9 +5246,9 @@ function notifyBotsChanged() {
|
|
|
5193
5246
|
var PANEL_VISIBLE_KEY = "dsh-qqbot.panel.visible";
|
|
5194
5247
|
function getPanelVisible() {
|
|
5195
5248
|
try {
|
|
5196
|
-
return localStorage.getItem(PANEL_VISIBLE_KEY)
|
|
5249
|
+
return localStorage.getItem(PANEL_VISIBLE_KEY) === "1";
|
|
5197
5250
|
} catch {
|
|
5198
|
-
return
|
|
5251
|
+
return false;
|
|
5199
5252
|
}
|
|
5200
5253
|
}
|
|
5201
5254
|
function setPanelVisible(v) {
|
|
@@ -5320,9 +5373,41 @@ function TitleView() {
|
|
|
5320
5373
|
var name = "qqbot-settings";
|
|
5321
5374
|
var inject = ["slots", "connection", "locale", "sidebarRightTabs", "sidebarRight"];
|
|
5322
5375
|
var RPC_CHANNEL = "/qqbot-settings";
|
|
5376
|
+
function eventsWsUrl() {
|
|
5377
|
+
const scheme = typeof location !== "undefined" && location.protocol === "https:" ? "wss:" : "ws:";
|
|
5378
|
+
return `${scheme}//${location.host}${RPC_CHANNEL}/events`;
|
|
5379
|
+
}
|
|
5323
5380
|
var QQBOT_PANEL_ID = "@sunjuntao/dsh-qqbot";
|
|
5324
5381
|
var QQBOT_PANEL_KIND = "qqbot-sched";
|
|
5325
5382
|
var NOTICE_TTL_SECONDS = 8;
|
|
5383
|
+
var UPDATE_IDLE = { busy: false, message: "", done: false };
|
|
5384
|
+
var updateStore = /* @__PURE__ */ (() => {
|
|
5385
|
+
let current = UPDATE_IDLE;
|
|
5386
|
+
const listeners2 = /* @__PURE__ */ new Set();
|
|
5387
|
+
return {
|
|
5388
|
+
snapshot() {
|
|
5389
|
+
return current;
|
|
5390
|
+
},
|
|
5391
|
+
publish(next) {
|
|
5392
|
+
current = next.busy ? next : UPDATE_IDLE;
|
|
5393
|
+
for (const listener of [...listeners2]) {
|
|
5394
|
+
try {
|
|
5395
|
+
listener(next);
|
|
5396
|
+
} catch {
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
},
|
|
5400
|
+
subscribe(listener) {
|
|
5401
|
+
listeners2.add(listener);
|
|
5402
|
+
return () => {
|
|
5403
|
+
listeners2.delete(listener);
|
|
5404
|
+
};
|
|
5405
|
+
}
|
|
5406
|
+
};
|
|
5407
|
+
})();
|
|
5408
|
+
function publishUpdate(next) {
|
|
5409
|
+
updateStore.publish(next);
|
|
5410
|
+
}
|
|
5326
5411
|
function QqbotSettingsTab({ rpcCall }) {
|
|
5327
5412
|
const [status, setStatus] = React10.useState(null);
|
|
5328
5413
|
const [bots, setBots] = React10.useState(null);
|
|
@@ -5339,11 +5424,9 @@ function QqbotSettingsTab({ rpcCall }) {
|
|
|
5339
5424
|
const [refreshing, setRefreshing] = React10.useState(false);
|
|
5340
5425
|
const [reconnecting, setReconnecting] = React10.useState(false);
|
|
5341
5426
|
const [panelVisible, setPanelVisibleState] = React10.useState(() => getPanelVisible());
|
|
5342
|
-
const [update,
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
done: false
|
|
5346
|
-
});
|
|
5427
|
+
const [update, setLocalUpdate] = React10.useState(() => updateStore.snapshot());
|
|
5428
|
+
const setUpdate = publishUpdate;
|
|
5429
|
+
React10.useEffect(() => updateStore.subscribe(setLocalUpdate), []);
|
|
5347
5430
|
const refresh = React10.useCallback(async (appId) => {
|
|
5348
5431
|
try {
|
|
5349
5432
|
const [s, c, b, cat] = await Promise.all([
|
|
@@ -5488,9 +5571,9 @@ function QqbotSettingsTab({ rpcCall }) {
|
|
|
5488
5571
|
};
|
|
5489
5572
|
React10.useEffect(() => {
|
|
5490
5573
|
if (!update.message || update.busy) return void 0;
|
|
5491
|
-
const timer = setTimeout(() =>
|
|
5574
|
+
const timer = setTimeout(() => publishUpdate({ ...update, message: "" }), NOTICE_TTL_SECONDS * 1e3);
|
|
5492
5575
|
return () => clearTimeout(timer);
|
|
5493
|
-
}, [update
|
|
5576
|
+
}, [update]);
|
|
5494
5577
|
const groupOverrides = form.groupOverrides && typeof form.groupOverrides === "object" && !Array.isArray(form.groupOverrides) ? form.groupOverrides : {};
|
|
5495
5578
|
const overrideSummary = (ov) => {
|
|
5496
5579
|
const parts = [];
|
|
@@ -6179,7 +6262,7 @@ function QqbotSettingsTab({ rpcCall }) {
|
|
|
6179
6262
|
"div",
|
|
6180
6263
|
{ className: "qbot-brandHeading" },
|
|
6181
6264
|
h("strong", { className: "qbot-brandName" }, t("app.title")),
|
|
6182
|
-
h("span", { className: "qbot-brandVersion" }, `v${true ? "0.1.
|
|
6265
|
+
h("span", { className: "qbot-brandVersion" }, `v${true ? "0.1.10" : "0.0.2"}`),
|
|
6183
6266
|
h("button", {
|
|
6184
6267
|
className: `qbot-btn qbot-updateBtn${update.done ? " is-done" : ""}`,
|
|
6185
6268
|
type: "button",
|
|
@@ -6193,7 +6276,19 @@ function QqbotSettingsTab({ rpcCall }) {
|
|
|
6193
6276
|
),
|
|
6194
6277
|
h("div", { className: "qbot-titleActions" }, globalBadge)
|
|
6195
6278
|
),
|
|
6196
|
-
|
|
6279
|
+
// 更新期间(busy)的提示条不自动淡出,并额外给出「请勿关闭」警示:
|
|
6280
|
+
// update.apply 会下载并覆盖安装目录,中途切走 / 关闭面板不影响宿主执行,
|
|
6281
|
+
// 但用户看不到结果、也容易误以为卡死,所以显式告知。
|
|
6282
|
+
update.message ? h(
|
|
6283
|
+
"div",
|
|
6284
|
+
{
|
|
6285
|
+
className: `qbot-infoNotice qbot-updateNotice${update.busy ? " is-busy" : ""}`,
|
|
6286
|
+
role: "status",
|
|
6287
|
+
"aria-busy": update.busy ? "true" : "false"
|
|
6288
|
+
},
|
|
6289
|
+
h("span", { className: "qbot-updateMsg" }, update.message),
|
|
6290
|
+
update.busy ? h("strong", { className: "qbot-updateKeepOpen" }, t("update.keepOpen")) : null
|
|
6291
|
+
) : null,
|
|
6197
6292
|
h(
|
|
6198
6293
|
"div",
|
|
6199
6294
|
{ className: "qbot-panel", id: "qbot-panel" },
|
|
@@ -6270,17 +6365,31 @@ function apply(ctx) {
|
|
|
6270
6365
|
};
|
|
6271
6366
|
setRpcCall(rpcCall);
|
|
6272
6367
|
ctx.effect(() => installStyles(), "qqbot-settings: styles");
|
|
6368
|
+
ctx.effect(
|
|
6369
|
+
() => {
|
|
6370
|
+
let style = document.getElementById("qbot-settings-nav-style");
|
|
6371
|
+
if (!style) {
|
|
6372
|
+
style = document.createElement("style");
|
|
6373
|
+
style.id = "qbot-settings-nav-style";
|
|
6374
|
+
style.textContent = QBOT_SETTINGS_NAV_CSS;
|
|
6375
|
+
document.head.appendChild(style);
|
|
6376
|
+
}
|
|
6377
|
+
const disposeMarker = registerQbotSettingsNavIcon(() => t("app.title"));
|
|
6378
|
+
return () => {
|
|
6379
|
+
disposeMarker();
|
|
6380
|
+
style?.remove();
|
|
6381
|
+
};
|
|
6382
|
+
},
|
|
6383
|
+
"qqbot-settings: settings navigation icon"
|
|
6384
|
+
);
|
|
6273
6385
|
ctx.slots.inject("settings.section", () => ctx.slots.register(
|
|
6274
6386
|
{
|
|
6275
6387
|
name: "settings.section",
|
|
6276
6388
|
id: "dsh-qqbot",
|
|
6277
6389
|
order: 31,
|
|
6278
|
-
label
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
h(QqBotGlyph, { className: "qbot-navGlyph", uid: "nav" }),
|
|
6282
|
-
h("span", { className: "qbot-navText" }, t("app.title"))
|
|
6283
|
-
),
|
|
6390
|
+
// 宿主新版导航行自带系统图标(navIcon),label 契约是纯文本;
|
|
6391
|
+
// 这里不再内嵌 QqBotGlyph,否则会和宿主图标重复。
|
|
6392
|
+
label: () => t("app.title"),
|
|
6284
6393
|
inject: () => ({ rpcCall })
|
|
6285
6394
|
},
|
|
6286
6395
|
QqbotSettingsTab
|
|
@@ -6311,22 +6420,77 @@ function apply(ctx) {
|
|
|
6311
6420
|
}
|
|
6312
6421
|
disposePanelTab = null;
|
|
6313
6422
|
};
|
|
6314
|
-
const syncPanelTab =
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
const bots = res.ok ? val(res)?.bots : void 0;
|
|
6318
|
-
const anyConnected = Array.isArray(bots) && bots.some((b) => b?.ws?.state === "connected");
|
|
6319
|
-
if (anyConnected && getPanelVisible()) registerPanelTab();
|
|
6320
|
-
else unregisterPanelTab();
|
|
6321
|
-
} catch {
|
|
6322
|
-
}
|
|
6423
|
+
const syncPanelTab = () => {
|
|
6424
|
+
if (getPanelVisible()) registerPanelTab();
|
|
6425
|
+
else unregisterPanelTab();
|
|
6323
6426
|
};
|
|
6324
|
-
|
|
6427
|
+
syncPanelTab();
|
|
6325
6428
|
const offBotsChanged = onBotsChanged(() => void syncPanelTab());
|
|
6326
|
-
const
|
|
6429
|
+
const onStorage = (event) => {
|
|
6430
|
+
if (event.storageArea === localStorage && (event.key === null || event.key === PANEL_VISIBLE_KEY)) {
|
|
6431
|
+
syncPanelTab();
|
|
6432
|
+
}
|
|
6433
|
+
};
|
|
6434
|
+
window.addEventListener("storage", onStorage);
|
|
6435
|
+
let wsSocket = null;
|
|
6436
|
+
let wsRetry = 0;
|
|
6437
|
+
let wsTimer = null;
|
|
6438
|
+
let wsDisposed = false;
|
|
6439
|
+
const scheduleWsReconnect = () => {
|
|
6440
|
+
if (wsDisposed || wsTimer !== null) return;
|
|
6441
|
+
const delay = Math.min(1e3 * 2 ** wsRetry, 1e4);
|
|
6442
|
+
wsRetry = Math.min(wsRetry + 1, 10);
|
|
6443
|
+
wsTimer = setTimeout(() => {
|
|
6444
|
+
wsTimer = null;
|
|
6445
|
+
connectWs();
|
|
6446
|
+
}, delay);
|
|
6447
|
+
};
|
|
6448
|
+
function connectWs() {
|
|
6449
|
+
if (wsDisposed || typeof WebSocket === "undefined") return;
|
|
6450
|
+
let socket;
|
|
6451
|
+
try {
|
|
6452
|
+
socket = new WebSocket(eventsWsUrl());
|
|
6453
|
+
} catch (error) {
|
|
6454
|
+
console?.debug?.("[dsh-qqbot] WebSocket 订阅失败,面板显隐依赖设置页动作触发:", error);
|
|
6455
|
+
scheduleWsReconnect();
|
|
6456
|
+
return;
|
|
6457
|
+
}
|
|
6458
|
+
wsSocket = socket;
|
|
6459
|
+
socket.onopen = () => {
|
|
6460
|
+
wsRetry = 0;
|
|
6461
|
+
syncPanelTab();
|
|
6462
|
+
};
|
|
6463
|
+
socket.onmessage = (event) => {
|
|
6464
|
+
if (typeof event.data !== "string") return;
|
|
6465
|
+
try {
|
|
6466
|
+
const message = JSON.parse(event.data);
|
|
6467
|
+
if (message.event === "bots-changed") notifyBotsChanged();
|
|
6468
|
+
} catch {
|
|
6469
|
+
}
|
|
6470
|
+
};
|
|
6471
|
+
socket.onerror = () => {
|
|
6472
|
+
};
|
|
6473
|
+
socket.onclose = () => {
|
|
6474
|
+
if (wsSocket === socket) wsSocket = null;
|
|
6475
|
+
scheduleWsReconnect();
|
|
6476
|
+
};
|
|
6477
|
+
}
|
|
6478
|
+
connectWs();
|
|
6327
6479
|
ctx.effect(
|
|
6328
6480
|
() => () => {
|
|
6329
|
-
|
|
6481
|
+
wsDisposed = true;
|
|
6482
|
+
if (wsTimer !== null) clearTimeout(wsTimer);
|
|
6483
|
+
wsTimer = null;
|
|
6484
|
+
try {
|
|
6485
|
+
const socket = wsSocket;
|
|
6486
|
+
wsSocket = null;
|
|
6487
|
+
if (socket) {
|
|
6488
|
+
socket.onclose = null;
|
|
6489
|
+
socket.close();
|
|
6490
|
+
}
|
|
6491
|
+
} catch {
|
|
6492
|
+
}
|
|
6493
|
+
window.removeEventListener("storage", onStorage);
|
|
6330
6494
|
offBotsChanged();
|
|
6331
6495
|
unregisterPanelTab();
|
|
6332
6496
|
},
|
package/lib/index.js
CHANGED
|
@@ -6722,7 +6722,7 @@ async function readRawBody(req) {
|
|
|
6722
6722
|
}
|
|
6723
6723
|
return Buffer.concat(chunks, size);
|
|
6724
6724
|
}
|
|
6725
|
-
function registerRpcChannel({ ctx, fence, dispatch }) {
|
|
6725
|
+
function registerRpcChannel({ ctx, fence, dispatch, onWsUpgrade }) {
|
|
6726
6726
|
ctx.inject(["webServer"], (webCtxUnknown) => {
|
|
6727
6727
|
const webCtx = webCtxUnknown;
|
|
6728
6728
|
const route = {
|
|
@@ -6741,7 +6741,8 @@ function registerRpcChannel({ ctx, fence, dispatch }) {
|
|
|
6741
6741
|
}
|
|
6742
6742
|
}
|
|
6743
6743
|
const endpoint = endpointFromPath(RPC_CHANNEL, new URL(req.url ?? "/", "http://localhost").pathname);
|
|
6744
|
-
|
|
6744
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
6745
|
+
if (method !== "POST" || endpoint === void 0) {
|
|
6745
6746
|
res.writeHead(404);
|
|
6746
6747
|
res.end("not found");
|
|
6747
6748
|
return;
|
|
@@ -6780,8 +6781,46 @@ function registerRpcChannel({ ctx, fence, dispatch }) {
|
|
|
6780
6781
|
}
|
|
6781
6782
|
};
|
|
6782
6783
|
webCtx.effect(() => webCtx.webServer.register(route), `dsh-qqbot: ${RPC_CHANNEL} rpc channel`);
|
|
6784
|
+
if (!onWsUpgrade) return;
|
|
6785
|
+
const upgradeRoute = {
|
|
6786
|
+
path: `${RPC_CHANNEL}/events`,
|
|
6787
|
+
handler: (req, socket, head) => {
|
|
6788
|
+
if (fence) {
|
|
6789
|
+
try {
|
|
6790
|
+
const rejection = fence.requestRejection({ headers: req.headers });
|
|
6791
|
+
if (rejection !== void 0) {
|
|
6792
|
+
rejectUpgrade(socket, rejection);
|
|
6793
|
+
return;
|
|
6794
|
+
}
|
|
6795
|
+
} catch {
|
|
6796
|
+
}
|
|
6797
|
+
}
|
|
6798
|
+
onWsUpgrade(req, socket, head);
|
|
6799
|
+
}
|
|
6800
|
+
};
|
|
6801
|
+
webCtx.effect(() => {
|
|
6802
|
+
try {
|
|
6803
|
+
return webCtx.webServer.registerUpgrade(upgradeRoute);
|
|
6804
|
+
} catch (error) {
|
|
6805
|
+
console?.warn?.(`[dsh-qqbot] WebSocket 端点注册失败,面板显隐仅依赖设置页动作: ${String(error)}`);
|
|
6806
|
+
return () => {
|
|
6807
|
+
};
|
|
6808
|
+
}
|
|
6809
|
+
}, `dsh-qqbot: ${RPC_CHANNEL} events websocket`);
|
|
6783
6810
|
});
|
|
6784
6811
|
}
|
|
6812
|
+
function rejectUpgrade(socket, status) {
|
|
6813
|
+
const text = status === 401 ? "Unauthorized" : "Forbidden";
|
|
6814
|
+
try {
|
|
6815
|
+
socket.write(`HTTP/1.1 ${status} ${text}\r
|
|
6816
|
+
Connection: close\r
|
|
6817
|
+
Content-Length: 0\r
|
|
6818
|
+
\r
|
|
6819
|
+
`);
|
|
6820
|
+
} catch {
|
|
6821
|
+
}
|
|
6822
|
+
socket.destroy();
|
|
6823
|
+
}
|
|
6785
6824
|
async function writeJson2(res, response) {
|
|
6786
6825
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
6787
6826
|
const headers = {};
|
|
@@ -6792,6 +6831,235 @@ async function writeJson2(res, response) {
|
|
|
6792
6831
|
res.end(buffer);
|
|
6793
6832
|
}
|
|
6794
6833
|
|
|
6834
|
+
// src/host/admin/ws-hub.ts
|
|
6835
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
6836
|
+
var OPCODE_CONTINUATION = 0;
|
|
6837
|
+
var OPCODE_TEXT = 1;
|
|
6838
|
+
var OPCODE_CLOSE = 8;
|
|
6839
|
+
var OPCODE_PING = 9;
|
|
6840
|
+
var OPCODE_PONG = 10;
|
|
6841
|
+
var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
6842
|
+
var HEARTBEAT_INTERVAL_MS = 25e3;
|
|
6843
|
+
var MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
6844
|
+
var WS_EVENT_BOTS_CHANGED = "bots-changed";
|
|
6845
|
+
function acceptKey(key) {
|
|
6846
|
+
return createHash2("sha1").update(`${key}${WS_GUID}`).digest("base64");
|
|
6847
|
+
}
|
|
6848
|
+
function encodeFrame(opcode, payload) {
|
|
6849
|
+
const length = payload.length;
|
|
6850
|
+
let header;
|
|
6851
|
+
if (length < 126) {
|
|
6852
|
+
header = Buffer.alloc(2);
|
|
6853
|
+
header[1] = length;
|
|
6854
|
+
} else if (length <= 65535) {
|
|
6855
|
+
header = Buffer.alloc(4);
|
|
6856
|
+
header[1] = 126;
|
|
6857
|
+
header.writeUInt16BE(length, 2);
|
|
6858
|
+
} else {
|
|
6859
|
+
header = Buffer.alloc(10);
|
|
6860
|
+
header[1] = 127;
|
|
6861
|
+
header.writeBigUInt64BE(BigInt(length), 2);
|
|
6862
|
+
}
|
|
6863
|
+
header[0] = 128 | opcode;
|
|
6864
|
+
return Buffer.concat([header, payload]);
|
|
6865
|
+
}
|
|
6866
|
+
function parseFrame(buf) {
|
|
6867
|
+
if (buf.length < 2) return { kind: "incomplete", rest: buf };
|
|
6868
|
+
const first = buf[0];
|
|
6869
|
+
const second = buf[1];
|
|
6870
|
+
const fin = (first & 128) !== 0;
|
|
6871
|
+
const opcode = first & 15;
|
|
6872
|
+
const masked = (second & 128) !== 0;
|
|
6873
|
+
let length = second & 127;
|
|
6874
|
+
let offset = 2;
|
|
6875
|
+
if (length === 126) {
|
|
6876
|
+
if (buf.length < offset + 2) return { kind: "incomplete", rest: buf };
|
|
6877
|
+
length = buf.readUInt16BE(offset);
|
|
6878
|
+
offset += 2;
|
|
6879
|
+
} else if (length === 127) {
|
|
6880
|
+
if (buf.length < offset + 8) return { kind: "incomplete", rest: buf };
|
|
6881
|
+
const declared = buf.readBigUInt64BE(offset);
|
|
6882
|
+
offset += 8;
|
|
6883
|
+
if (declared > BigInt(MAX_PAYLOAD_BYTES)) return { kind: "fatal" };
|
|
6884
|
+
length = Number(declared);
|
|
6885
|
+
}
|
|
6886
|
+
if (length > MAX_PAYLOAD_BYTES) return { kind: "fatal" };
|
|
6887
|
+
let mask;
|
|
6888
|
+
if (masked) {
|
|
6889
|
+
if (buf.length < offset + 4) return { kind: "incomplete", rest: buf };
|
|
6890
|
+
mask = buf.subarray(offset, offset + 4);
|
|
6891
|
+
offset += 4;
|
|
6892
|
+
}
|
|
6893
|
+
if (buf.length < offset + length) return { kind: "incomplete", rest: buf };
|
|
6894
|
+
const payload = Buffer.from(buf.subarray(offset, offset + length));
|
|
6895
|
+
if (mask) {
|
|
6896
|
+
for (let i = 0; i < payload.length; i += 1) payload[i] = payload[i] ^ mask[i & 3];
|
|
6897
|
+
}
|
|
6898
|
+
return { kind: "frame", frame: { fin, opcode, payload }, rest: buf.subarray(offset + length) };
|
|
6899
|
+
}
|
|
6900
|
+
function isReservedOpcode(opcode) {
|
|
6901
|
+
return opcode >= 3 && opcode <= 7 || opcode >= 11;
|
|
6902
|
+
}
|
|
6903
|
+
function createWsHub({ logger }) {
|
|
6904
|
+
const clients = /* @__PURE__ */ new Set();
|
|
6905
|
+
const drop = (client) => {
|
|
6906
|
+
clients.delete(client);
|
|
6907
|
+
};
|
|
6908
|
+
const send = (client, opcode, payload) => {
|
|
6909
|
+
if (client.socket.destroyed) {
|
|
6910
|
+
drop(client);
|
|
6911
|
+
return;
|
|
6912
|
+
}
|
|
6913
|
+
try {
|
|
6914
|
+
client.socket.write(encodeFrame(opcode, payload), (error) => {
|
|
6915
|
+
if (error) drop(client);
|
|
6916
|
+
});
|
|
6917
|
+
} catch (error) {
|
|
6918
|
+
drop(client);
|
|
6919
|
+
logger.warn("[dsh-qqbot] WebSocket 写入失败,已摘除该客户端:", error);
|
|
6920
|
+
}
|
|
6921
|
+
};
|
|
6922
|
+
const close = (client, code = 1001) => {
|
|
6923
|
+
if (!client.socket.destroyed) {
|
|
6924
|
+
const body = Buffer.alloc(2);
|
|
6925
|
+
body.writeUInt16BE(code, 0);
|
|
6926
|
+
send(client, OPCODE_CLOSE, body);
|
|
6927
|
+
}
|
|
6928
|
+
drop(client);
|
|
6929
|
+
try {
|
|
6930
|
+
client.socket.end();
|
|
6931
|
+
} catch {
|
|
6932
|
+
}
|
|
6933
|
+
};
|
|
6934
|
+
const attach = (client, onText, head) => {
|
|
6935
|
+
let buffer = Buffer.alloc(0);
|
|
6936
|
+
const fragments = [];
|
|
6937
|
+
let fragmentOpcode = OPCODE_CONTINUATION;
|
|
6938
|
+
const feed = (chunk) => {
|
|
6939
|
+
buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]);
|
|
6940
|
+
if (buffer.length > MAX_PAYLOAD_BYTES + 16) {
|
|
6941
|
+
client.socket.destroy();
|
|
6942
|
+
return;
|
|
6943
|
+
}
|
|
6944
|
+
for (; ; ) {
|
|
6945
|
+
const result = parseFrame(buffer);
|
|
6946
|
+
if (result.kind === "incomplete") {
|
|
6947
|
+
buffer = result.rest;
|
|
6948
|
+
return;
|
|
6949
|
+
}
|
|
6950
|
+
if (result.kind === "fatal") {
|
|
6951
|
+
client.socket.destroy();
|
|
6952
|
+
return;
|
|
6953
|
+
}
|
|
6954
|
+
buffer = result.rest;
|
|
6955
|
+
const { fin, opcode, payload } = result.frame;
|
|
6956
|
+
if (opcode === OPCODE_CONTINUATION) {
|
|
6957
|
+
fragments.push(payload);
|
|
6958
|
+
if (!fin) continue;
|
|
6959
|
+
const full = Buffer.concat(fragments);
|
|
6960
|
+
fragments.length = 0;
|
|
6961
|
+
if (fragmentOpcode === OPCODE_TEXT) onText(full.toString("utf8"));
|
|
6962
|
+
fragmentOpcode = OPCODE_CONTINUATION;
|
|
6963
|
+
continue;
|
|
6964
|
+
}
|
|
6965
|
+
if (isReservedOpcode(opcode)) {
|
|
6966
|
+
client.socket.destroy();
|
|
6967
|
+
return;
|
|
6968
|
+
}
|
|
6969
|
+
if (opcode === OPCODE_TEXT) {
|
|
6970
|
+
if (!fin) {
|
|
6971
|
+
fragmentOpcode = OPCODE_TEXT;
|
|
6972
|
+
fragments.length = 0;
|
|
6973
|
+
fragments.push(payload);
|
|
6974
|
+
continue;
|
|
6975
|
+
}
|
|
6976
|
+
onText(payload.toString("utf8"));
|
|
6977
|
+
continue;
|
|
6978
|
+
}
|
|
6979
|
+
if (opcode === OPCODE_PING) {
|
|
6980
|
+
send(client, OPCODE_PONG, payload);
|
|
6981
|
+
continue;
|
|
6982
|
+
}
|
|
6983
|
+
if (opcode === OPCODE_PONG) {
|
|
6984
|
+
client.alive = true;
|
|
6985
|
+
continue;
|
|
6986
|
+
}
|
|
6987
|
+
if (opcode === OPCODE_CLOSE) {
|
|
6988
|
+
close(client, 1e3);
|
|
6989
|
+
return;
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
};
|
|
6993
|
+
client.socket.on("data", (chunk) => {
|
|
6994
|
+
feed(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8"));
|
|
6995
|
+
});
|
|
6996
|
+
client.socket.on("error", () => drop(client));
|
|
6997
|
+
client.socket.on("close", () => drop(client));
|
|
6998
|
+
if (head.length > 0) feed(head);
|
|
6999
|
+
};
|
|
7000
|
+
const timer = setInterval(() => {
|
|
7001
|
+
for (const client of [...clients]) {
|
|
7002
|
+
if (!client.alive) {
|
|
7003
|
+
drop(client);
|
|
7004
|
+
try {
|
|
7005
|
+
client.socket.destroy();
|
|
7006
|
+
} catch {
|
|
7007
|
+
}
|
|
7008
|
+
continue;
|
|
7009
|
+
}
|
|
7010
|
+
client.alive = false;
|
|
7011
|
+
send(client, OPCODE_PING, Buffer.alloc(0));
|
|
7012
|
+
}
|
|
7013
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
7014
|
+
timer.unref?.();
|
|
7015
|
+
return {
|
|
7016
|
+
handleUpgrade(req, socket, head) {
|
|
7017
|
+
const upgrade = String(req.headers.upgrade ?? "").toLowerCase();
|
|
7018
|
+
const key = req.headers["sec-websocket-key"];
|
|
7019
|
+
const versions = String(req.headers["sec-websocket-version"] ?? "").split(",").map((v) => v.trim());
|
|
7020
|
+
if (upgrade !== "websocket" || typeof key !== "string" || key === "" || !versions.includes("13")) {
|
|
7021
|
+
try {
|
|
7022
|
+
socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
7023
|
+
} catch {
|
|
7024
|
+
}
|
|
7025
|
+
socket.destroy();
|
|
7026
|
+
return;
|
|
7027
|
+
}
|
|
7028
|
+
try {
|
|
7029
|
+
socket.write(
|
|
7030
|
+
`HTTP/1.1 101 Switching Protocols\r
|
|
7031
|
+
Upgrade: websocket\r
|
|
7032
|
+
Connection: Upgrade\r
|
|
7033
|
+
Sec-WebSocket-Accept: ${acceptKey(key)}\r
|
|
7034
|
+
\r
|
|
7035
|
+
`
|
|
7036
|
+
);
|
|
7037
|
+
} catch (error) {
|
|
7038
|
+
logger.warn("[dsh-qqbot] WebSocket 握手响应失败:", error);
|
|
7039
|
+
socket.destroy();
|
|
7040
|
+
return;
|
|
7041
|
+
}
|
|
7042
|
+
const client = { socket, alive: true };
|
|
7043
|
+
clients.add(client);
|
|
7044
|
+
attach(client, () => {
|
|
7045
|
+
}, head ?? Buffer.alloc(0));
|
|
7046
|
+
},
|
|
7047
|
+
broadcast(event, data) {
|
|
7048
|
+
if (clients.size === 0) return;
|
|
7049
|
+
const payload = Buffer.from(JSON.stringify({ event, data: data ?? null, at: Date.now() }), "utf8");
|
|
7050
|
+
for (const client of [...clients]) send(client, OPCODE_TEXT, payload);
|
|
7051
|
+
},
|
|
7052
|
+
clientCount() {
|
|
7053
|
+
return clients.size;
|
|
7054
|
+
},
|
|
7055
|
+
dispose() {
|
|
7056
|
+
clearInterval(timer);
|
|
7057
|
+
for (const client of [...clients]) close(client);
|
|
7058
|
+
clients.clear();
|
|
7059
|
+
}
|
|
7060
|
+
};
|
|
7061
|
+
}
|
|
7062
|
+
|
|
6795
7063
|
// src/host/messaging/self-id.ts
|
|
6796
7064
|
init_store_file();
|
|
6797
7065
|
import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile7 } from "node:fs/promises";
|
|
@@ -6997,6 +7265,7 @@ var QqWsSource = class {
|
|
|
6997
7265
|
#onEvent;
|
|
6998
7266
|
#onRawEvent;
|
|
6999
7267
|
#onInteraction;
|
|
7268
|
+
#onStatusChange;
|
|
7000
7269
|
#bot = null;
|
|
7001
7270
|
#abort = null;
|
|
7002
7271
|
#starting = null;
|
|
@@ -7006,10 +7275,25 @@ var QqWsSource = class {
|
|
|
7006
7275
|
this.#onEvent = options.onEvent;
|
|
7007
7276
|
this.#onRawEvent = options.onRawEvent;
|
|
7008
7277
|
this.#onInteraction = options.onInteraction;
|
|
7278
|
+
this.#onStatusChange = options.onStatusChange;
|
|
7009
7279
|
}
|
|
7010
7280
|
get status() {
|
|
7011
7281
|
return { ...this.#status };
|
|
7012
7282
|
}
|
|
7283
|
+
/**
|
|
7284
|
+
* 统一的状态赋值入口:实际发生 state / lastError 变化时触发 onStatusChange。
|
|
7285
|
+
* events 计数递增走裸赋值(#accept),不经过这里。
|
|
7286
|
+
*/
|
|
7287
|
+
#setStatus(status) {
|
|
7288
|
+
const prev = this.#status;
|
|
7289
|
+
this.#status = status;
|
|
7290
|
+
if (prev.state !== status.state || prev.lastError !== status.lastError) {
|
|
7291
|
+
try {
|
|
7292
|
+
this.#onStatusChange?.(status.appId, { ...status });
|
|
7293
|
+
} catch {
|
|
7294
|
+
}
|
|
7295
|
+
}
|
|
7296
|
+
}
|
|
7013
7297
|
/** 底层 SDK 实例(已连接后可用),供媒体上传 / 撤回 / 原生 API 调用。 */
|
|
7014
7298
|
get sdk() {
|
|
7015
7299
|
return this.#bot;
|
|
@@ -7018,13 +7302,13 @@ var QqWsSource = class {
|
|
|
7018
7302
|
async start(config) {
|
|
7019
7303
|
await this.stop();
|
|
7020
7304
|
if (!config.appId || !config.appSecret) {
|
|
7021
|
-
this.#
|
|
7305
|
+
this.#setStatus({ ...this.#status, state: "idle", appId: config.appId, lastError: null });
|
|
7022
7306
|
this.#logger.warn("[dsh-qqbot] WebSocket 未启动:凭据不完整(请扫码登录或填写 AppID/AppSecret)");
|
|
7023
7307
|
return;
|
|
7024
7308
|
}
|
|
7025
7309
|
const run = this.#start(config).catch((error) => {
|
|
7026
7310
|
const message = error instanceof Error ? error.message : String(error);
|
|
7027
|
-
this.#
|
|
7311
|
+
this.#setStatus({ ...this.#status, state: "failed", lastError: message });
|
|
7028
7312
|
this.#logger.error("[dsh-qqbot] WebSocket 连接失败:", error);
|
|
7029
7313
|
});
|
|
7030
7314
|
this.#starting = run;
|
|
@@ -7044,7 +7328,7 @@ var QqWsSource = class {
|
|
|
7044
7328
|
bot?.stop();
|
|
7045
7329
|
} catch {
|
|
7046
7330
|
}
|
|
7047
|
-
this.#
|
|
7331
|
+
this.#setStatus({ ...this.#status, state: "idle" });
|
|
7048
7332
|
}
|
|
7049
7333
|
async #start(config) {
|
|
7050
7334
|
const options = {
|
|
@@ -7064,18 +7348,18 @@ var QqWsSource = class {
|
|
|
7064
7348
|
const abort = new AbortController();
|
|
7065
7349
|
this.#bot = bot;
|
|
7066
7350
|
this.#abort = abort;
|
|
7067
|
-
this.#
|
|
7351
|
+
this.#setStatus({ state: "connecting", appId: config.appId, lastConnectedAt: null, lastError: null, events: 0 });
|
|
7068
7352
|
bot.on("ready", () => {
|
|
7069
|
-
this.#
|
|
7353
|
+
this.#setStatus({ ...this.#status, state: "connected", lastConnectedAt: Date.now(), lastError: null });
|
|
7070
7354
|
this.#logger.info(`[dsh-qqbot] QQ WebSocket 已连接(AppID ${config.appId}),开始接收消息`);
|
|
7071
7355
|
});
|
|
7072
7356
|
bot.on("resumed", () => {
|
|
7073
|
-
this.#
|
|
7357
|
+
this.#setStatus({ ...this.#status, state: "connected", lastError: null });
|
|
7074
7358
|
this.#logger.info("[dsh-qqbot] QQ WebSocket 会话已恢复(RESUME)");
|
|
7075
7359
|
});
|
|
7076
7360
|
bot.on("error", (error) => {
|
|
7077
7361
|
const message = error instanceof Error ? error.message : String(error);
|
|
7078
|
-
this.#
|
|
7362
|
+
this.#setStatus({ ...this.#status, lastError: message });
|
|
7079
7363
|
if (this.#status.state === "connected") {
|
|
7080
7364
|
this.#logger.warn("[dsh-qqbot] QQ WebSocket 连接错误(SDK 将自动重连):", error);
|
|
7081
7365
|
}
|
|
@@ -7122,7 +7406,7 @@ var QqWsSource = class {
|
|
|
7122
7406
|
const runTask = Promise.resolve().then(() => bot.start(abort.signal));
|
|
7123
7407
|
runTask.catch((error) => {
|
|
7124
7408
|
if (abort.signal.aborted) return;
|
|
7125
|
-
this.#
|
|
7409
|
+
this.#setStatus({ ...this.#status, state: "failed", lastError: error instanceof Error ? error.message : String(error) });
|
|
7126
7410
|
this.#logger.error("[dsh-qqbot] QQ WebSocket 连接停止:", error);
|
|
7127
7411
|
});
|
|
7128
7412
|
let timer;
|
|
@@ -7218,6 +7502,8 @@ var BotRuntimeManager = class {
|
|
|
7218
7502
|
#statsTimer = null;
|
|
7219
7503
|
#syncing = null;
|
|
7220
7504
|
#primaryAppId = "";
|
|
7505
|
+
/** onBotsChanged 去抖定时器:窗口内的多次状态变化合并成一次通知。 */
|
|
7506
|
+
#notifyTimer = null;
|
|
7221
7507
|
constructor(options) {
|
|
7222
7508
|
this.#options = options;
|
|
7223
7509
|
this.#stats = new StatsStore(options.logger);
|
|
@@ -7277,6 +7563,19 @@ var BotRuntimeManager = class {
|
|
|
7277
7563
|
map.delete(key);
|
|
7278
7564
|
}
|
|
7279
7565
|
}
|
|
7566
|
+
// ── 变更通知(WebSocket 推送等订阅方) ─────────────────────────────────────
|
|
7567
|
+
/** 去抖通知:300ms 窗口内的多次变化(sync 收尾 + 各 ws 状态迁移)合并为一次。 */
|
|
7568
|
+
#notifyChanged() {
|
|
7569
|
+
if (this.#notifyTimer) return;
|
|
7570
|
+
this.#notifyTimer = setTimeout(() => {
|
|
7571
|
+
this.#notifyTimer = null;
|
|
7572
|
+
try {
|
|
7573
|
+
this.#options.onBotsChanged?.();
|
|
7574
|
+
} catch {
|
|
7575
|
+
}
|
|
7576
|
+
}, 300);
|
|
7577
|
+
this.#notifyTimer.unref?.();
|
|
7578
|
+
}
|
|
7280
7579
|
// ── 同步:把 bots.json 应用到运行时 ────────────────────────────────────────
|
|
7281
7580
|
/**
|
|
7282
7581
|
* 按存储层重建/更新/停止各机器人连接。
|
|
@@ -7340,6 +7639,7 @@ var BotRuntimeManager = class {
|
|
|
7340
7639
|
this.#bots.delete(appId);
|
|
7341
7640
|
this.#options.logger.info(`[dsh-qqbot] 机器人 ${appId} 已移除,连接已断开`);
|
|
7342
7641
|
}
|
|
7642
|
+
this.#notifyChanged();
|
|
7343
7643
|
}
|
|
7344
7644
|
#buildConfig(stored, secret) {
|
|
7345
7645
|
return resolveConfig({
|
|
@@ -7402,6 +7702,9 @@ var BotRuntimeManager = class {
|
|
|
7402
7702
|
},
|
|
7403
7703
|
onInteraction: (event) => {
|
|
7404
7704
|
this.#options.onInteraction?.(bot, event);
|
|
7705
|
+
},
|
|
7706
|
+
onStatusChange: () => {
|
|
7707
|
+
this.#notifyChanged();
|
|
7405
7708
|
}
|
|
7406
7709
|
});
|
|
7407
7710
|
return bot;
|
|
@@ -7466,7 +7769,7 @@ init_store_file();
|
|
|
7466
7769
|
|
|
7467
7770
|
// src/host/schedule/schedule.ts
|
|
7468
7771
|
init_store_file();
|
|
7469
|
-
import { createHash as
|
|
7772
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
|
|
7470
7773
|
|
|
7471
7774
|
// src/shared/cron.ts
|
|
7472
7775
|
var MONTH_NAMES = {
|
|
@@ -8404,7 +8707,7 @@ var ScheduleStore = class {
|
|
|
8404
8707
|
}
|
|
8405
8708
|
};
|
|
8406
8709
|
function contentDigest(text) {
|
|
8407
|
-
return
|
|
8710
|
+
return createHash3("sha1").update((text ?? "").replace(/\s+/g, " ").trim()).digest("hex").slice(0, 16);
|
|
8408
8711
|
}
|
|
8409
8712
|
function gateSkipReason(entry, text) {
|
|
8410
8713
|
const gate = entry.gate ?? "always";
|
|
@@ -10299,12 +10602,14 @@ async function apply(ctx, entryConfig) {
|
|
|
10299
10602
|
}, 6e4);
|
|
10300
10603
|
outboxTimer.unref?.();
|
|
10301
10604
|
let runtime = null;
|
|
10605
|
+
const wsHub = createWsHub({ logger });
|
|
10302
10606
|
const bots = new BotRuntimeManager({
|
|
10303
10607
|
logger,
|
|
10304
10608
|
entryConfig,
|
|
10305
10609
|
adminToken,
|
|
10306
10610
|
resolveSecret,
|
|
10307
10611
|
resolvePresets,
|
|
10612
|
+
onBotsChanged: () => wsHub.broadcast(WS_EVENT_BOTS_CHANGED),
|
|
10308
10613
|
onEvent: (bot, eventType, payload, deliveryId) => {
|
|
10309
10614
|
if (!runtime) {
|
|
10310
10615
|
logger.warn("[dsh-qqbot] webhook 运行时不可用,事件已丢弃");
|
|
@@ -10471,7 +10776,8 @@ async function apply(ctx, entryConfig) {
|
|
|
10471
10776
|
registerRpcChannel({
|
|
10472
10777
|
ctx,
|
|
10473
10778
|
fence: connection,
|
|
10474
|
-
dispatch: (endpoint, payload) => admin.handle(endpoint, payload)
|
|
10779
|
+
dispatch: (endpoint, payload) => admin.handle(endpoint, payload),
|
|
10780
|
+
onWsUpgrade: (req, socket, head) => wsHub.handleUpgrade(req, socket, head)
|
|
10475
10781
|
});
|
|
10476
10782
|
} catch (error) {
|
|
10477
10783
|
logger.warn("[dsh-qqbot] RPC 通道注册失败,设置界面将无法连接(HTTP 管理端点仍可用):", error);
|
|
@@ -10498,6 +10804,7 @@ async function apply(ctx, entryConfig) {
|
|
|
10498
10804
|
disposeTools();
|
|
10499
10805
|
scheduler.stop();
|
|
10500
10806
|
qr.dispose();
|
|
10807
|
+
wsHub.dispose();
|
|
10501
10808
|
await bots.stopAll();
|
|
10502
10809
|
if (disposeRule) {
|
|
10503
10810
|
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.10",
|
|
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",
|
|
@@ -82,9 +82,9 @@
|
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
84
|
"@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.
|
|
85
|
+
"@deepseek-ai/dsh-credentials": ">=0.1.6-alpha.1 <0.2.0",
|
|
86
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.6-alpha.1 <0.2.0",
|
|
87
|
+
"@deepseek-ai/dsh-webhook": ">=0.1.6-alpha.1 <0.2.0",
|
|
88
88
|
"@deepseek-ai/schemastery": "^3.18.0"
|
|
89
89
|
},
|
|
90
90
|
"peerDependenciesMeta": {
|