@openclaw/qqbot 2026.5.2 → 2026.5.3-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +646 -0
- package/dist/approval-cg0SVahb.js +94 -0
- package/dist/channel-N6Y_Rcjp.js +551 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.setup-ByCRAe5H.js +25 -0
- package/dist/config-D6545NkC.js +94 -0
- package/dist/config-schema-DFcjQw73.js +309 -0
- package/dist/exec-approvals-COUsM6wZ.js +138 -0
- package/dist/gateway-CmSUJKSt.js +6726 -0
- package/dist/handler-runtime-Bqm6N0WG.js +120 -0
- package/dist/index.js +29 -0
- package/dist/narrowing-BoieBTIU.js +25 -0
- package/dist/outbound-BJfhwrPg.js +1357 -0
- package/dist/request-context-DXtpwWui.js +921 -0
- package/dist/resolve-D_06fV6-.js +284 -0
- package/dist/runtime-BJAS3eXW.js +18 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-api.js +56 -0
- package/dist/sender-p-B14eLG.js +1987 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +2 -0
- package/dist/string-normalize-Ci6NM5DE.js +98 -0
- package/dist/target-parser-Y0prnrXD.js +245 -0
- package/package.json +15 -6
- package/api.ts +0 -56
- package/channel-plugin-api.ts +0 -1
- package/index.ts +0 -29
- package/runtime-api.ts +0 -9
- package/setup-entry.ts +0 -9
- package/setup-plugin-api.ts +0 -3
- package/src/bridge/approval/capability.ts +0 -237
- package/src/bridge/approval/handler-runtime.ts +0 -204
- package/src/bridge/bootstrap.ts +0 -135
- package/src/bridge/channel-entry.ts +0 -18
- package/src/bridge/commands/framework-context-adapter.ts +0 -60
- package/src/bridge/commands/framework-registration.ts +0 -47
- package/src/bridge/commands/from-parser.test.ts +0 -86
- package/src/bridge/commands/from-parser.ts +0 -60
- package/src/bridge/commands/result-dispatcher.ts +0 -76
- package/src/bridge/config-shared.ts +0 -132
- package/src/bridge/config.ts +0 -111
- package/src/bridge/gateway.ts +0 -174
- package/src/bridge/logger.ts +0 -31
- package/src/bridge/narrowing.ts +0 -31
- package/src/bridge/plugin-version.test.ts +0 -146
- package/src/bridge/plugin-version.ts +0 -102
- package/src/bridge/runtime.ts +0 -25
- package/src/bridge/sdk-adapter.ts +0 -131
- package/src/bridge/setup/finalize.ts +0 -144
- package/src/bridge/setup/surface.ts +0 -34
- package/src/bridge/tools/channel.ts +0 -58
- package/src/bridge/tools/index.ts +0 -15
- package/src/bridge/tools/remind.test.ts +0 -124
- package/src/bridge/tools/remind.ts +0 -91
- package/src/channel.setup.ts +0 -33
- package/src/channel.ts +0 -288
- package/src/command-auth.test.ts +0 -62
- package/src/config-schema.ts +0 -84
- package/src/config.test.ts +0 -364
- package/src/engine/access/access-control.test.ts +0 -198
- package/src/engine/access/access-control.ts +0 -226
- package/src/engine/access/index.ts +0 -16
- package/src/engine/access/resolve-policy.test.ts +0 -59
- package/src/engine/access/resolve-policy.ts +0 -57
- package/src/engine/access/sender-match.test.ts +0 -60
- package/src/engine/access/sender-match.ts +0 -55
- package/src/engine/access/types.ts +0 -53
- package/src/engine/adapter/audio.port.ts +0 -27
- package/src/engine/adapter/commands.port.ts +0 -22
- package/src/engine/adapter/history.port.ts +0 -52
- package/src/engine/adapter/index.ts +0 -139
- package/src/engine/adapter/mention-gate.port.ts +0 -50
- package/src/engine/adapter/types.ts +0 -38
- package/src/engine/api/api-client.ts +0 -212
- package/src/engine/api/media-chunked.test.ts +0 -336
- package/src/engine/api/media-chunked.ts +0 -622
- package/src/engine/api/media.ts +0 -218
- package/src/engine/api/messages.ts +0 -293
- package/src/engine/api/retry.ts +0 -217
- package/src/engine/api/routes.ts +0 -95
- package/src/engine/api/token.ts +0 -271
- package/src/engine/approval/index.test.ts +0 -22
- package/src/engine/approval/index.ts +0 -224
- package/src/engine/commands/builtin/log-helpers.ts +0 -319
- package/src/engine/commands/builtin/register-all.ts +0 -17
- package/src/engine/commands/builtin/register-approve.ts +0 -201
- package/src/engine/commands/builtin/register-basic.ts +0 -95
- package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
- package/src/engine/commands/builtin/register-logs.ts +0 -20
- package/src/engine/commands/builtin/register-streaming.ts +0 -137
- package/src/engine/commands/builtin/state.ts +0 -31
- package/src/engine/commands/slash-command-auth.ts +0 -48
- package/src/engine/commands/slash-command-handler.ts +0 -146
- package/src/engine/commands/slash-commands-impl.test.ts +0 -8
- package/src/engine/commands/slash-commands-impl.ts +0 -61
- package/src/engine/commands/slash-commands.ts +0 -199
- package/src/engine/config/credential-backup.test.ts +0 -88
- package/src/engine/config/credential-backup.ts +0 -107
- package/src/engine/config/credentials.ts +0 -76
- package/src/engine/config/group.test.ts +0 -234
- package/src/engine/config/group.ts +0 -299
- package/src/engine/config/resolve.test.ts +0 -152
- package/src/engine/config/resolve.ts +0 -283
- package/src/engine/config/setup-logic.ts +0 -84
- package/src/engine/engine-import-boundary.test.ts +0 -73
- package/src/engine/gateway/codec.ts +0 -47
- package/src/engine/gateway/constants.ts +0 -117
- package/src/engine/gateway/event-dispatcher.ts +0 -177
- package/src/engine/gateway/gateway-connection.ts +0 -371
- package/src/engine/gateway/gateway.ts +0 -291
- package/src/engine/gateway/inbound-attachments.test.ts +0 -126
- package/src/engine/gateway/inbound-attachments.ts +0 -360
- package/src/engine/gateway/inbound-context.ts +0 -195
- package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
- package/src/engine/gateway/inbound-pipeline.ts +0 -235
- package/src/engine/gateway/interaction-handler.ts +0 -220
- package/src/engine/gateway/message-queue.test.ts +0 -282
- package/src/engine/gateway/message-queue.ts +0 -499
- package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
- package/src/engine/gateway/outbound-dispatch.ts +0 -575
- package/src/engine/gateway/reconnect.ts +0 -199
- package/src/engine/gateway/stages/access-stage.ts +0 -132
- package/src/engine/gateway/stages/assembly-stage.ts +0 -156
- package/src/engine/gateway/stages/content-stage.test.ts +0 -77
- package/src/engine/gateway/stages/content-stage.ts +0 -77
- package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
- package/src/engine/gateway/stages/envelope-stage.ts +0 -144
- package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
- package/src/engine/gateway/stages/index.ts +0 -18
- package/src/engine/gateway/stages/quote-stage.ts +0 -113
- package/src/engine/gateway/stages/refidx-stage.ts +0 -62
- package/src/engine/gateway/stages/stub-contexts.ts +0 -116
- package/src/engine/gateway/types.ts +0 -264
- package/src/engine/gateway/typing-keepalive.ts +0 -79
- package/src/engine/group/activation.test.ts +0 -114
- package/src/engine/group/activation.ts +0 -147
- package/src/engine/group/history.test.ts +0 -314
- package/src/engine/group/history.ts +0 -321
- package/src/engine/group/mention.test.ts +0 -141
- package/src/engine/group/mention.ts +0 -197
- package/src/engine/group/message-gating.test.ts +0 -188
- package/src/engine/group/message-gating.ts +0 -216
- package/src/engine/messaging/decode-media-path.ts +0 -82
- package/src/engine/messaging/media-source.ts +0 -215
- package/src/engine/messaging/media-type-detect.ts +0 -37
- package/src/engine/messaging/outbound-audio-port.ts +0 -38
- package/src/engine/messaging/outbound-deliver.ts +0 -810
- package/src/engine/messaging/outbound-media-send.ts +0 -702
- package/src/engine/messaging/outbound-reply.ts +0 -27
- package/src/engine/messaging/outbound-result-helpers.ts +0 -54
- package/src/engine/messaging/outbound-types.ts +0 -45
- package/src/engine/messaging/outbound.ts +0 -485
- package/src/engine/messaging/reply-dispatcher.ts +0 -597
- package/src/engine/messaging/reply-limiter.ts +0 -164
- package/src/engine/messaging/sender.ts +0 -729
- package/src/engine/messaging/streaming-c2c.ts +0 -1192
- package/src/engine/messaging/streaming-media-send.ts +0 -544
- package/src/engine/messaging/target-parser.ts +0 -104
- package/src/engine/ref/format-message-ref.ts +0 -142
- package/src/engine/ref/format-ref-entry.test.ts +0 -60
- package/src/engine/ref/format-ref-entry.ts +0 -27
- package/src/engine/ref/store.ts +0 -208
- package/src/engine/ref/types.ts +0 -27
- package/src/engine/session/known-users.ts +0 -137
- package/src/engine/session/session-store.ts +0 -204
- package/src/engine/tools/channel-api.ts +0 -244
- package/src/engine/tools/remind-logic.test.ts +0 -280
- package/src/engine/tools/remind-logic.ts +0 -377
- package/src/engine/types.ts +0 -313
- package/src/engine/utils/attachment-tags.test.ts +0 -186
- package/src/engine/utils/attachment-tags.ts +0 -174
- package/src/engine/utils/audio.test.ts +0 -250
- package/src/engine/utils/audio.ts +0 -585
- package/src/engine/utils/data-paths.ts +0 -38
- package/src/engine/utils/diagnostics.ts +0 -109
- package/src/engine/utils/file-utils.test.ts +0 -72
- package/src/engine/utils/file-utils.ts +0 -225
- package/src/engine/utils/format.test.ts +0 -68
- package/src/engine/utils/format.ts +0 -70
- package/src/engine/utils/image-size.test.ts +0 -158
- package/src/engine/utils/image-size.ts +0 -249
- package/src/engine/utils/log.test.ts +0 -28
- package/src/engine/utils/log.ts +0 -61
- package/src/engine/utils/media-tags.test.ts +0 -32
- package/src/engine/utils/media-tags.ts +0 -177
- package/src/engine/utils/payload.test.ts +0 -68
- package/src/engine/utils/payload.ts +0 -145
- package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
- package/src/engine/utils/platform.test.ts +0 -148
- package/src/engine/utils/platform.ts +0 -343
- package/src/engine/utils/request-context.ts +0 -60
- package/src/engine/utils/string-normalize.ts +0 -91
- package/src/engine/utils/stt.test.ts +0 -104
- package/src/engine/utils/stt.ts +0 -100
- package/src/engine/utils/text-parsing.test.ts +0 -29
- package/src/engine/utils/text-parsing.ts +0 -155
- package/src/engine/utils/upload-cache.ts +0 -96
- package/src/engine/utils/voice-text.ts +0 -15
- package/src/exec-approvals.ts +0 -218
- package/src/manifest-schema.test.ts +0 -56
- package/src/qqbot-test-support.ts +0 -29
- package/src/types.ts +0 -210
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//#region extensions/qqbot/src/engine/approval/index.ts
|
|
2
|
+
function buildExecApprovalText(request) {
|
|
3
|
+
const expiresIn = Math.max(0, Math.round((request.expiresAtMs - Date.now()) / 1e3));
|
|
4
|
+
const lines = ["🔐 命令执行审批", ""];
|
|
5
|
+
const cmd = request.request.commandPreview ?? request.request.command ?? "";
|
|
6
|
+
if (cmd) lines.push(`\`\`\`\n${cmd.slice(0, 300)}\n\`\`\``);
|
|
7
|
+
if (request.request.cwd) lines.push(`\u{1f4c1} \u76ee\u5f55: ${request.request.cwd}`);
|
|
8
|
+
if (request.request.agentId) lines.push(`\u{1f916} Agent: ${request.request.agentId}`);
|
|
9
|
+
lines.push("", `\u23f1\ufe0f \u8d85\u65f6: ${expiresIn} \u79d2`);
|
|
10
|
+
return lines.join("\n");
|
|
11
|
+
}
|
|
12
|
+
function buildPluginApprovalText(request) {
|
|
13
|
+
const timeoutSec = Math.round((request.request.timeoutMs ?? 12e4) / 1e3);
|
|
14
|
+
const lines = [`${request.request.severity === "critical" ? "🔴" : request.request.severity === "info" ? "🔵" : "🟡"} \u5ba1\u6279\u8bf7\u6c42`, ""];
|
|
15
|
+
lines.push(`\u{1f4cb} ${request.request.title}`);
|
|
16
|
+
if (request.request.description) lines.push(`\u{1f4dd} ${request.request.description}`);
|
|
17
|
+
if (request.request.toolName) lines.push(`\u{1f527} \u5de5\u5177: ${request.request.toolName}`);
|
|
18
|
+
if (request.request.pluginId) lines.push(`\u{1f50c} \u63d2\u4ef6: ${request.request.pluginId}`);
|
|
19
|
+
if (request.request.agentId) lines.push(`\u{1f916} Agent: ${request.request.agentId}`);
|
|
20
|
+
lines.push("", `\u23f1\ufe0f \u8d85\u65f6: ${timeoutSec} \u79d2`);
|
|
21
|
+
return lines.join("\n");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build the three-button inline keyboard for approval messages.
|
|
25
|
+
*
|
|
26
|
+
* type=1 (Callback): click triggers INTERACTION_CREATE, button_data = data field.
|
|
27
|
+
* group_id "approval": clicking one button grays out the others (mutual exclusion).
|
|
28
|
+
* click_limit=1: each user can only click once.
|
|
29
|
+
* permission.type=2: all users can interact.
|
|
30
|
+
*/
|
|
31
|
+
function buildApprovalKeyboard(approvalId, allowedDecisions = [
|
|
32
|
+
"allow-once",
|
|
33
|
+
"allow-always",
|
|
34
|
+
"deny"
|
|
35
|
+
]) {
|
|
36
|
+
const makeBtn = (id, label, visitedLabel, data, style) => ({
|
|
37
|
+
id,
|
|
38
|
+
render_data: {
|
|
39
|
+
label,
|
|
40
|
+
visited_label: visitedLabel,
|
|
41
|
+
style
|
|
42
|
+
},
|
|
43
|
+
action: {
|
|
44
|
+
type: 1,
|
|
45
|
+
data,
|
|
46
|
+
permission: { type: 2 },
|
|
47
|
+
click_limit: 1
|
|
48
|
+
},
|
|
49
|
+
group_id: "approval"
|
|
50
|
+
});
|
|
51
|
+
const buttons = [];
|
|
52
|
+
if (allowedDecisions.includes("allow-once")) buttons.push(makeBtn("allow", "✅ 允许一次", "已允许", `approve:${approvalId}:allow-once`, 1));
|
|
53
|
+
if (allowedDecisions.includes("allow-always")) buttons.push(makeBtn("always", "⭐ 始终允许", "已始终允许", `approve:${approvalId}:allow-always`, 1));
|
|
54
|
+
if (allowedDecisions.includes("deny")) buttons.push(makeBtn("deny", "❌ 拒绝", "已拒绝", `approve:${approvalId}:deny`, 0));
|
|
55
|
+
return { content: { rows: [{ buttons }] } };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract the delivery target from a sessionKey or turnSourceTo string.
|
|
59
|
+
*
|
|
60
|
+
* Expected formats:
|
|
61
|
+
* agent:main:qqbot:direct:OPENID -> { type: "c2c", id: "OPENID" }
|
|
62
|
+
* agent:main:qqbot:c2c:OPENID -> { type: "c2c", id: "OPENID" }
|
|
63
|
+
* agent:main:qqbot:group:GROUPID -> { type: "group", id: "GROUPID" }
|
|
64
|
+
*
|
|
65
|
+
* Returns null if neither field matches the expected pattern.
|
|
66
|
+
*/
|
|
67
|
+
function resolveApprovalTarget(sessionKey, turnSourceTo) {
|
|
68
|
+
const sk = sessionKey ?? turnSourceTo;
|
|
69
|
+
if (!sk) return null;
|
|
70
|
+
const m = sk.match(/qqbot:(c2c|direct|group):([A-F0-9]+)/i);
|
|
71
|
+
if (!m) return null;
|
|
72
|
+
return {
|
|
73
|
+
type: m[1].toLowerCase() === "group" ? "group" : "c2c",
|
|
74
|
+
id: m[2]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parse the button_data string from an INTERACTION_CREATE event.
|
|
79
|
+
*
|
|
80
|
+
* Expected format: `approve:<approvalId>:<decision>`
|
|
81
|
+
* where approvalId may be prefixed with "exec:" or "plugin:".
|
|
82
|
+
*
|
|
83
|
+
* Returns null if the data does not match the approval button format.
|
|
84
|
+
*/
|
|
85
|
+
function parseApprovalButtonData(buttonData) {
|
|
86
|
+
const m = buttonData.match(/^approve:((?:(?:exec|plugin):)?[0-9a-f-]+):(allow-once|allow-always|deny)$/i);
|
|
87
|
+
if (!m) return null;
|
|
88
|
+
return {
|
|
89
|
+
approvalId: m[1],
|
|
90
|
+
decision: m[2]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { resolveApprovalTarget as a, parseApprovalButtonData as i, buildExecApprovalText as n, buildPluginApprovalText as r, buildApprovalKeyboard as t };
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { t as asOptionalObjectRecord } from "./string-normalize-Ci6NM5DE.js";
|
|
2
|
+
import { l as ensurePlatformAdapter, u as getBridgeLogger } from "./resolve-D_06fV6-.js";
|
|
3
|
+
import { a as resolveApprovalTarget } from "./approval-cg0SVahb.js";
|
|
4
|
+
import { a as resolveQQBotAccount, n as applyQQBotAccountConfig, t as DEFAULT_ACCOUNT_ID$1 } from "./config-D6545NkC.js";
|
|
5
|
+
import { a as resolveQQBotExecApprovalConfig, i as matchesQQBotApprovalAccount, n as isQQBotExecApprovalAuthorizedSender, o as shouldHandleQQBotExecApprovalRequest, r as isQQBotExecApprovalClientEnabled, t as isQQBotExecApprovalApprover } from "./exec-approvals-COUsM6wZ.js";
|
|
6
|
+
import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-DFcjQw73.js";
|
|
7
|
+
import { n as writeOpenClawConfigThroughRuntime, t as toGatewayAccount } from "./narrowing-BoieBTIU.js";
|
|
8
|
+
import { t as getQQBotRuntime } from "./runtime-BJAS3eXW.js";
|
|
9
|
+
import { n as normalizeTarget, s as getQQBotDataPath, t as looksLikeQQBotTarget } from "./target-parser-Y0prnrXD.js";
|
|
10
|
+
import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime";
|
|
11
|
+
import { createChannelApprovalCapability } from "openclaw/plugin-sdk/approval-delivery-runtime";
|
|
12
|
+
import { createLazyChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
|
|
13
|
+
import { resolveApprovalRequestSessionConversation } from "openclaw/plugin-sdk/approval-native-runtime";
|
|
14
|
+
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
//#region extensions/qqbot/src/bridge/approval/capability.ts
|
|
18
|
+
/**
|
|
19
|
+
* QQ Bot Approval Capability — entry point.
|
|
20
|
+
*
|
|
21
|
+
* QQBot uses a simpler approval model than Telegram/Slack: any user who
|
|
22
|
+
* can see the inline-keyboard buttons can approve. No explicit approver
|
|
23
|
+
* list is required — the bot simply sends the approval message to the
|
|
24
|
+
* originating conversation and whoever clicks the button resolves it.
|
|
25
|
+
*
|
|
26
|
+
* When `execApprovals` IS configured, it gates which requests are
|
|
27
|
+
* handled natively and who is authorized. When it is NOT configured,
|
|
28
|
+
* QQBot falls back to "always handle, anyone can approve".
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* When `execApprovals` is configured, delegate to the profile-based
|
|
32
|
+
* check. Otherwise fall back to target-resolvability plus the shared
|
|
33
|
+
* per-account ownership rule in `matchesQQBotApprovalAccount` so that
|
|
34
|
+
* each QQBot account handler only delivers approvals that originated
|
|
35
|
+
* from its own account (openids are account-scoped — cross-account
|
|
36
|
+
* delivery fails with 500 on the QQ Bot API).
|
|
37
|
+
*/
|
|
38
|
+
function shouldHandleRequest(params) {
|
|
39
|
+
if (hasExecApprovalConfig(params)) return shouldHandleQQBotExecApprovalRequest(params);
|
|
40
|
+
if (!canResolveTarget(params.request)) return false;
|
|
41
|
+
return matchesQQBotApprovalAccount({
|
|
42
|
+
cfg: params.cfg,
|
|
43
|
+
accountId: params.accountId,
|
|
44
|
+
request: params.request
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function hasExecApprovalConfig(params) {
|
|
48
|
+
return resolveQQBotExecApprovalConfig(params) !== void 0;
|
|
49
|
+
}
|
|
50
|
+
function isNativeDeliveryEnabled(params) {
|
|
51
|
+
if (hasExecApprovalConfig(params)) return isQQBotExecApprovalClientEnabled(params);
|
|
52
|
+
const account = resolveQQBotAccount(params.cfg, params.accountId);
|
|
53
|
+
return account.enabled && account.secretSource !== "none";
|
|
54
|
+
}
|
|
55
|
+
function canResolveTarget(request) {
|
|
56
|
+
if (resolveApprovalTarget(request.request.sessionKey ?? null, request.request.turnSourceTo ?? null)) return true;
|
|
57
|
+
return resolveApprovalRequestSessionConversation({
|
|
58
|
+
request,
|
|
59
|
+
channel: "qqbot",
|
|
60
|
+
bundledFallback: true
|
|
61
|
+
})?.id != null;
|
|
62
|
+
}
|
|
63
|
+
function createQQBotApprovalCapability() {
|
|
64
|
+
return createChannelApprovalCapability({
|
|
65
|
+
authorizeActorAction: ({ cfg, accountId, senderId, approvalKind }) => {
|
|
66
|
+
if (hasExecApprovalConfig({
|
|
67
|
+
cfg,
|
|
68
|
+
accountId
|
|
69
|
+
})) return (approvalKind === "plugin" ? isQQBotExecApprovalApprover({
|
|
70
|
+
cfg,
|
|
71
|
+
accountId,
|
|
72
|
+
senderId
|
|
73
|
+
}) : isQQBotExecApprovalAuthorizedSender({
|
|
74
|
+
cfg,
|
|
75
|
+
accountId,
|
|
76
|
+
senderId
|
|
77
|
+
})) ? { authorized: true } : {
|
|
78
|
+
authorized: false,
|
|
79
|
+
reason: "You are not authorized to approve this request."
|
|
80
|
+
};
|
|
81
|
+
return { authorized: true };
|
|
82
|
+
},
|
|
83
|
+
getActionAvailabilityState: ({ cfg, accountId }) => {
|
|
84
|
+
return isNativeDeliveryEnabled({
|
|
85
|
+
cfg,
|
|
86
|
+
accountId
|
|
87
|
+
}) ? { kind: "enabled" } : { kind: "disabled" };
|
|
88
|
+
},
|
|
89
|
+
getExecInitiatingSurfaceState: ({ cfg, accountId }) => {
|
|
90
|
+
return isNativeDeliveryEnabled({
|
|
91
|
+
cfg,
|
|
92
|
+
accountId
|
|
93
|
+
}) ? { kind: "enabled" } : { kind: "disabled" };
|
|
94
|
+
},
|
|
95
|
+
describeExecApprovalSetup: ({ accountId }) => {
|
|
96
|
+
return `QQBot native exec approvals are enabled by default. To restrict who can approve, configure \`${accountId && accountId !== "default" ? `channels.qqbot.accounts.${accountId}` : "channels.qqbot"}.execApprovals.approvers\` with QQ user OpenIDs.`;
|
|
97
|
+
},
|
|
98
|
+
delivery: {
|
|
99
|
+
hasConfiguredDmRoute: () => true,
|
|
100
|
+
shouldSuppressForwardingFallback: (input) => {
|
|
101
|
+
const channel = normalizeOptionalString(input.target?.channel);
|
|
102
|
+
if (channel !== "qqbot") return false;
|
|
103
|
+
const accountId = normalizeOptionalString(input.target?.accountId) ?? normalizeOptionalString(input.request?.request?.turnSourceAccountId);
|
|
104
|
+
const result = isNativeDeliveryEnabled({
|
|
105
|
+
cfg: input.cfg,
|
|
106
|
+
accountId
|
|
107
|
+
});
|
|
108
|
+
getBridgeLogger().debug?.(`[qqbot:approval] shouldSuppressForwardingFallback channel=${channel} accountId=${accountId} → ${result}`);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
native: {
|
|
113
|
+
describeDeliveryCapabilities: ({ cfg, accountId }) => ({
|
|
114
|
+
enabled: isNativeDeliveryEnabled({
|
|
115
|
+
cfg,
|
|
116
|
+
accountId
|
|
117
|
+
}),
|
|
118
|
+
preferredSurface: "origin",
|
|
119
|
+
supportsOriginSurface: true,
|
|
120
|
+
supportsApproverDmSurface: false,
|
|
121
|
+
notifyOriginWhenDmOnly: false
|
|
122
|
+
}),
|
|
123
|
+
resolveOriginTarget: ({ request }) => {
|
|
124
|
+
const target = resolveApprovalTarget(request.request.sessionKey ?? null, request.request.turnSourceTo ?? null);
|
|
125
|
+
if (target) return { to: `${target.type}:${target.id}` };
|
|
126
|
+
const sessionConversation = resolveApprovalRequestSessionConversation({
|
|
127
|
+
request,
|
|
128
|
+
channel: "qqbot",
|
|
129
|
+
bundledFallback: true
|
|
130
|
+
});
|
|
131
|
+
if (sessionConversation?.id) return { to: `${sessionConversation.kind === "group" ? "group" : "c2c"}:${sessionConversation.id}` };
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
nativeRuntime: createLazyChannelApprovalNativeRuntimeAdapter({
|
|
136
|
+
eventKinds: ["exec", "plugin"],
|
|
137
|
+
isConfigured: ({ cfg, accountId }) => {
|
|
138
|
+
const result = isNativeDeliveryEnabled({
|
|
139
|
+
cfg,
|
|
140
|
+
accountId
|
|
141
|
+
});
|
|
142
|
+
getBridgeLogger().debug?.(`[qqbot:approval] nativeRuntime.isConfigured accountId=${accountId} → ${result}`);
|
|
143
|
+
return result;
|
|
144
|
+
},
|
|
145
|
+
shouldHandle: ({ cfg, accountId, request }) => {
|
|
146
|
+
const result = shouldHandleRequest({
|
|
147
|
+
cfg,
|
|
148
|
+
accountId,
|
|
149
|
+
request
|
|
150
|
+
});
|
|
151
|
+
getBridgeLogger().debug?.(`[qqbot:approval] nativeRuntime.shouldHandle accountId=${accountId} → ${result}`);
|
|
152
|
+
return result;
|
|
153
|
+
},
|
|
154
|
+
load: async () => {
|
|
155
|
+
ensurePlatformAdapter();
|
|
156
|
+
return (await import("./handler-runtime-Bqm6N0WG.js")).qqbotApprovalNativeRuntime;
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const qqbotApprovalCapability = createQQBotApprovalCapability();
|
|
162
|
+
let _cachedCapability;
|
|
163
|
+
function getQQBotApprovalCapability() {
|
|
164
|
+
_cachedCapability ??= qqbotApprovalCapability;
|
|
165
|
+
return _cachedCapability;
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region extensions/qqbot/src/engine/utils/data-paths.ts
|
|
169
|
+
/**
|
|
170
|
+
* Centralised filename helpers for persisted QQBot state.
|
|
171
|
+
*
|
|
172
|
+
* Every persistence module routes file paths through these helpers so the
|
|
173
|
+
* naming convention stays in sync and legacy migrations are handled
|
|
174
|
+
* consistently.
|
|
175
|
+
*
|
|
176
|
+
* Key design decisions:
|
|
177
|
+
* - Credential backup is keyed only by `accountId` because recovery runs
|
|
178
|
+
* exactly when the appId is missing from config.
|
|
179
|
+
*/
|
|
180
|
+
/**
|
|
181
|
+
* Normalise an identifier so it is safe to embed in a filename.
|
|
182
|
+
* Keeps alphanumerics, dot, underscore, dash; everything else becomes `_`.
|
|
183
|
+
*/
|
|
184
|
+
function safeName(id) {
|
|
185
|
+
return id.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Per-accountId credential backup file. Not keyed by appId because the
|
|
189
|
+
* whole point of this file is to recover credentials when appId is
|
|
190
|
+
* missing from the live config.
|
|
191
|
+
*/
|
|
192
|
+
function getCredentialBackupFile(accountId) {
|
|
193
|
+
return path.join(getQQBotDataPath("data"), `credential-backup-${safeName(accountId)}.json`);
|
|
194
|
+
}
|
|
195
|
+
/** Legacy single-file credential backup (pre-multi-account-isolation). */
|
|
196
|
+
function getLegacyCredentialBackupFile() {
|
|
197
|
+
return path.join(getQQBotDataPath("data"), "credential-backup.json");
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region extensions/qqbot/src/engine/config/credential-backup.ts
|
|
201
|
+
/**
|
|
202
|
+
* Credential backup & recovery.
|
|
203
|
+
* 凭证暂存与恢复。
|
|
204
|
+
*
|
|
205
|
+
* Solves the "hot-upgrade interrupted, appId/secret vanished from
|
|
206
|
+
* openclaw.json" failure mode.
|
|
207
|
+
*
|
|
208
|
+
* Mechanics:
|
|
209
|
+
* - After each successful gateway start we snapshot the currently
|
|
210
|
+
* resolved `appId` / `clientSecret` to a per-account backup file.
|
|
211
|
+
* - During plugin startup, if the live config has an empty appId or
|
|
212
|
+
* secret, the gateway consults the backup and restores the values
|
|
213
|
+
* via the config mutation API.
|
|
214
|
+
* - Backups live under `~/.openclaw/qqbot/data/` so they survive
|
|
215
|
+
* plugin directory replacement.
|
|
216
|
+
*
|
|
217
|
+
* Safety notes:
|
|
218
|
+
* - Only restore when credentials are **actually empty** — never
|
|
219
|
+
* overwrite a user's intentional config change.
|
|
220
|
+
* - Atomic write (temp file + rename) to avoid torn files.
|
|
221
|
+
* - Per-account file: `credential-backup-<accountId>.json`. We do
|
|
222
|
+
* **not** also key by appId because recovery happens precisely
|
|
223
|
+
* when appId is unknown.
|
|
224
|
+
* - Legacy single `credential-backup.json` is migrated automatically
|
|
225
|
+
* when the stored accountId matches the caller.
|
|
226
|
+
*/
|
|
227
|
+
/** Persist a credential snapshot (called once gateway reaches READY). */
|
|
228
|
+
function saveCredentialBackup(accountId, appId, clientSecret) {
|
|
229
|
+
if (!appId || !clientSecret) return;
|
|
230
|
+
try {
|
|
231
|
+
const backupPath = getCredentialBackupFile(accountId);
|
|
232
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
233
|
+
const data = {
|
|
234
|
+
accountId,
|
|
235
|
+
appId,
|
|
236
|
+
clientSecret,
|
|
237
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
238
|
+
};
|
|
239
|
+
const tmpPath = `${backupPath}.tmp`;
|
|
240
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
241
|
+
fs.renameSync(tmpPath, backupPath);
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Load a credential snapshot for `accountId`.
|
|
246
|
+
*
|
|
247
|
+
* Consults the new per-account file first; falls back to the legacy
|
|
248
|
+
* global backup file and migrates it when the embedded `accountId`
|
|
249
|
+
* matches the request. Returns `null` when no usable backup exists.
|
|
250
|
+
*/
|
|
251
|
+
function loadCredentialBackup(accountId) {
|
|
252
|
+
try {
|
|
253
|
+
if (accountId) {
|
|
254
|
+
const newPath = getCredentialBackupFile(accountId);
|
|
255
|
+
if (fs.existsSync(newPath)) {
|
|
256
|
+
const data = JSON.parse(fs.readFileSync(newPath, "utf8"));
|
|
257
|
+
if (data?.appId && data.clientSecret) return data;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const legacy = getLegacyCredentialBackupFile();
|
|
261
|
+
if (fs.existsSync(legacy)) {
|
|
262
|
+
const data = JSON.parse(fs.readFileSync(legacy, "utf8"));
|
|
263
|
+
if (!data?.appId || !data?.clientSecret) return null;
|
|
264
|
+
if (accountId && data.accountId !== accountId) return null;
|
|
265
|
+
if (data.accountId) try {
|
|
266
|
+
const backupPath = getCredentialBackupFile(data.accountId);
|
|
267
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
268
|
+
const tmpPath = `${backupPath}.tmp`;
|
|
269
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
270
|
+
fs.renameSync(tmpPath, backupPath);
|
|
271
|
+
fs.unlinkSync(legacy);
|
|
272
|
+
} catch {}
|
|
273
|
+
return data;
|
|
274
|
+
}
|
|
275
|
+
} catch {}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region extensions/qqbot/src/engine/config/credentials.ts
|
|
280
|
+
/**
|
|
281
|
+
* QQBot credential management (pure logic layer).
|
|
282
|
+
* QQBot 凭证管理(纯逻辑层)。
|
|
283
|
+
*
|
|
284
|
+
* Credential clearing and field-level cleanup for logout and setup
|
|
285
|
+
* flows. All functions operate on plain objects (Record<string, unknown>)
|
|
286
|
+
* and stay framework-agnostic.
|
|
287
|
+
*/
|
|
288
|
+
/**
|
|
289
|
+
* Remove clientSecret / clientSecretFile from a QQBot account config.
|
|
290
|
+
*
|
|
291
|
+
* Returns a shallow-cloned config with credentials removed, plus flags
|
|
292
|
+
* indicating whether anything actually changed.
|
|
293
|
+
*/
|
|
294
|
+
function clearAccountCredentials(cfg, accountId) {
|
|
295
|
+
const nextCfg = { ...cfg };
|
|
296
|
+
const channels = asOptionalObjectRecord(cfg.channels);
|
|
297
|
+
const nextQQBot = channels?.qqbot ? { ...asOptionalObjectRecord(channels.qqbot) } : void 0;
|
|
298
|
+
let cleared = false;
|
|
299
|
+
let changed = false;
|
|
300
|
+
if (nextQQBot) {
|
|
301
|
+
const qqbot = nextQQBot;
|
|
302
|
+
if (accountId === "default") {
|
|
303
|
+
if (qqbot.clientSecret) {
|
|
304
|
+
delete qqbot.clientSecret;
|
|
305
|
+
cleared = true;
|
|
306
|
+
changed = true;
|
|
307
|
+
}
|
|
308
|
+
if (qqbot.clientSecretFile) {
|
|
309
|
+
delete qqbot.clientSecretFile;
|
|
310
|
+
cleared = true;
|
|
311
|
+
changed = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const accounts = qqbot.accounts;
|
|
315
|
+
if (accounts && accountId in accounts) {
|
|
316
|
+
const entry = accounts[accountId];
|
|
317
|
+
if (entry && "clientSecret" in entry) {
|
|
318
|
+
delete entry.clientSecret;
|
|
319
|
+
cleared = true;
|
|
320
|
+
changed = true;
|
|
321
|
+
}
|
|
322
|
+
if (entry && "clientSecretFile" in entry) {
|
|
323
|
+
delete entry.clientSecretFile;
|
|
324
|
+
cleared = true;
|
|
325
|
+
changed = true;
|
|
326
|
+
}
|
|
327
|
+
if (entry && Object.keys(entry).length === 0) {
|
|
328
|
+
delete accounts[accountId];
|
|
329
|
+
changed = true;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (changed && nextQQBot) nextCfg.channels = {
|
|
334
|
+
...channels,
|
|
335
|
+
qqbot: nextQQBot
|
|
336
|
+
};
|
|
337
|
+
return {
|
|
338
|
+
nextCfg,
|
|
339
|
+
cleared,
|
|
340
|
+
changed
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region extensions/qqbot/src/channel.ts
|
|
345
|
+
let _gatewayModulePromise;
|
|
346
|
+
function loadGatewayModule() {
|
|
347
|
+
_gatewayModulePromise ??= import("./gateway-CmSUJKSt.js");
|
|
348
|
+
return _gatewayModulePromise;
|
|
349
|
+
}
|
|
350
|
+
const EXEC_APPROVAL_COMMAND_RE = /\/approve(?:@[^\s]+)?\s+[A-Za-z0-9][A-Za-z0-9._:-]*\s+(?:allow-once|allow-always|always|deny)\b/i;
|
|
351
|
+
function persistAccountCredentialSnapshot(account) {
|
|
352
|
+
if (account.appId && account.clientSecret) saveCredentialBackup(account.accountId, account.appId, account.clientSecret);
|
|
353
|
+
}
|
|
354
|
+
function shouldSuppressLocalQQBotApprovalPrompt(params) {
|
|
355
|
+
if (params.hint?.kind !== "approval-pending" || params.hint.approvalKind !== "exec") return false;
|
|
356
|
+
const account = resolveQQBotAccount(params.cfg, params.accountId);
|
|
357
|
+
if (!account.enabled || account.secretSource === "none") return false;
|
|
358
|
+
if (getExecApprovalReplyMetadata(params.payload)) return true;
|
|
359
|
+
const text = typeof params.payload.text === "string" ? params.payload.text : "";
|
|
360
|
+
return EXEC_APPROVAL_COMMAND_RE.test(text);
|
|
361
|
+
}
|
|
362
|
+
const qqbotPlugin = {
|
|
363
|
+
id: "qqbot",
|
|
364
|
+
setupWizard: qqbotSetupWizard,
|
|
365
|
+
meta: { ...qqbotMeta },
|
|
366
|
+
capabilities: {
|
|
367
|
+
chatTypes: ["direct", "group"],
|
|
368
|
+
media: true,
|
|
369
|
+
reactions: false,
|
|
370
|
+
threads: false,
|
|
371
|
+
blockStreaming: true
|
|
372
|
+
},
|
|
373
|
+
reload: { configPrefixes: ["channels.qqbot"] },
|
|
374
|
+
configSchema: qqbotChannelConfigSchema,
|
|
375
|
+
config: {
|
|
376
|
+
...qqbotConfigAdapter,
|
|
377
|
+
/**
|
|
378
|
+
* Treat an account as configured when either the live config has
|
|
379
|
+
* credentials OR a recoverable credential backup exists. This mirrors
|
|
380
|
+
* the standalone plugin and lets the gateway survive a hot upgrade
|
|
381
|
+
* that wiped openclaw.json mid-flight.
|
|
382
|
+
*/
|
|
383
|
+
isConfigured: (account) => {
|
|
384
|
+
if (qqbotConfigAdapter.isConfigured(account)) return true;
|
|
385
|
+
if (!account) return false;
|
|
386
|
+
const backup = loadCredentialBackup(account.accountId);
|
|
387
|
+
return Boolean(backup?.appId && backup?.clientSecret);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
setup: { ...qqbotSetupAdapterShared },
|
|
391
|
+
approvalCapability: getQQBotApprovalCapability(),
|
|
392
|
+
messaging: {
|
|
393
|
+
targetPrefixes: ["qqbot"],
|
|
394
|
+
/** Normalize common QQ Bot target formats into the canonical qqbot:... form. */
|
|
395
|
+
normalizeTarget,
|
|
396
|
+
targetResolver: {
|
|
397
|
+
/** Return true when the id looks like a QQ Bot target. */
|
|
398
|
+
looksLikeId: looksLikeQQBotTarget,
|
|
399
|
+
hint: "QQ Bot target format: qqbot:c2c:openid (direct) or qqbot:group:groupid (group)"
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
outbound: {
|
|
403
|
+
deliveryMode: "direct",
|
|
404
|
+
chunker: (text, limit) => getQQBotRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
405
|
+
chunkerMode: "markdown",
|
|
406
|
+
textChunkLimit: 5e3,
|
|
407
|
+
shouldSuppressLocalPayloadPrompt: ({ cfg, accountId, payload, hint }) => shouldSuppressLocalQQBotApprovalPrompt({
|
|
408
|
+
cfg,
|
|
409
|
+
accountId,
|
|
410
|
+
payload,
|
|
411
|
+
hint
|
|
412
|
+
}),
|
|
413
|
+
sendText: async ({ to, text, accountId, replyToId, cfg }) => {
|
|
414
|
+
await loadGatewayModule();
|
|
415
|
+
const account = resolveQQBotAccount(cfg, accountId);
|
|
416
|
+
const { sendText } = await import("./outbound-BJfhwrPg.js").then((n) => n.t);
|
|
417
|
+
const result = await sendText({
|
|
418
|
+
to,
|
|
419
|
+
text,
|
|
420
|
+
accountId,
|
|
421
|
+
replyToId,
|
|
422
|
+
account: toGatewayAccount(account)
|
|
423
|
+
});
|
|
424
|
+
return {
|
|
425
|
+
channel: "qqbot",
|
|
426
|
+
messageId: result.messageId ?? "",
|
|
427
|
+
meta: result.error ? { error: result.error } : void 0
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
|
|
431
|
+
await loadGatewayModule();
|
|
432
|
+
const account = resolveQQBotAccount(cfg, accountId);
|
|
433
|
+
const { sendMedia } = await import("./outbound-BJfhwrPg.js").then((n) => n.t);
|
|
434
|
+
const result = await sendMedia({
|
|
435
|
+
to,
|
|
436
|
+
text: text ?? "",
|
|
437
|
+
mediaUrl: mediaUrl ?? "",
|
|
438
|
+
accountId,
|
|
439
|
+
replyToId,
|
|
440
|
+
account: toGatewayAccount(account)
|
|
441
|
+
});
|
|
442
|
+
return {
|
|
443
|
+
channel: "qqbot",
|
|
444
|
+
messageId: result.messageId ?? "",
|
|
445
|
+
meta: result.error ? { error: result.error } : void 0
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
gateway: {
|
|
450
|
+
startAccount: async (ctx) => {
|
|
451
|
+
let { account, cfg } = ctx;
|
|
452
|
+
const { abortSignal, log } = ctx;
|
|
453
|
+
if (!account.appId || !account.clientSecret) {
|
|
454
|
+
const backup = loadCredentialBackup(account.accountId);
|
|
455
|
+
if (backup?.appId && backup?.clientSecret) try {
|
|
456
|
+
const nextCfg = applyQQBotAccountConfig(cfg, account.accountId, {
|
|
457
|
+
appId: backup.appId,
|
|
458
|
+
clientSecret: backup.clientSecret
|
|
459
|
+
});
|
|
460
|
+
await writeOpenClawConfigThroughRuntime(getQQBotRuntime(), nextCfg);
|
|
461
|
+
cfg = nextCfg;
|
|
462
|
+
account = resolveQQBotAccount(nextCfg, account.accountId);
|
|
463
|
+
log?.info(`[qqbot:${account.accountId}] Restored credentials from backup (appId=${account.appId})`);
|
|
464
|
+
} catch (err) {
|
|
465
|
+
log?.error(`[qqbot:${account.accountId}] Failed to restore credentials from backup: ${err instanceof Error ? err.message : String(err)}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const { startGateway } = await loadGatewayModule();
|
|
469
|
+
log?.info(`[qqbot:${account.accountId}] Starting gateway — appId=${account.appId}, enabled=${account.enabled}, name=${account.name ?? "unnamed"}`);
|
|
470
|
+
await startGateway({
|
|
471
|
+
account,
|
|
472
|
+
abortSignal,
|
|
473
|
+
cfg,
|
|
474
|
+
log,
|
|
475
|
+
channelRuntime: ctx.channelRuntime,
|
|
476
|
+
onReady: () => {
|
|
477
|
+
log?.info(`[qqbot:${account.accountId}] Gateway ready`);
|
|
478
|
+
ctx.setStatus({
|
|
479
|
+
...ctx.getStatus(),
|
|
480
|
+
running: true,
|
|
481
|
+
connected: true,
|
|
482
|
+
lastConnectedAt: Date.now()
|
|
483
|
+
});
|
|
484
|
+
persistAccountCredentialSnapshot(account);
|
|
485
|
+
},
|
|
486
|
+
onResumed: () => {
|
|
487
|
+
log?.info(`[qqbot:${account.accountId}] Gateway resumed`);
|
|
488
|
+
ctx.setStatus({
|
|
489
|
+
...ctx.getStatus(),
|
|
490
|
+
running: true,
|
|
491
|
+
connected: true,
|
|
492
|
+
lastConnectedAt: Date.now()
|
|
493
|
+
});
|
|
494
|
+
persistAccountCredentialSnapshot(account);
|
|
495
|
+
},
|
|
496
|
+
onError: (error) => {
|
|
497
|
+
log?.error(`[qqbot:${account.accountId}] Gateway error: ${error.message}`);
|
|
498
|
+
ctx.setStatus({
|
|
499
|
+
...ctx.getStatus(),
|
|
500
|
+
lastError: error.message
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
},
|
|
505
|
+
logoutAccount: async ({ accountId, cfg }) => {
|
|
506
|
+
const { nextCfg, cleared, changed } = clearAccountCredentials(cfg, accountId);
|
|
507
|
+
if (changed) await writeOpenClawConfigThroughRuntime(getQQBotRuntime(), nextCfg);
|
|
508
|
+
const loggedOut = resolveQQBotAccount(changed ? nextCfg : cfg, accountId).secretSource === "none";
|
|
509
|
+
return {
|
|
510
|
+
ok: true,
|
|
511
|
+
cleared,
|
|
512
|
+
envToken: Boolean(process.env.QQBOT_CLIENT_SECRET),
|
|
513
|
+
loggedOut
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
status: {
|
|
518
|
+
defaultRuntime: {
|
|
519
|
+
accountId: DEFAULT_ACCOUNT_ID$1,
|
|
520
|
+
running: false,
|
|
521
|
+
connected: false,
|
|
522
|
+
lastConnectedAt: null,
|
|
523
|
+
lastError: null,
|
|
524
|
+
lastInboundAt: null,
|
|
525
|
+
lastOutboundAt: null
|
|
526
|
+
},
|
|
527
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
528
|
+
configured: snapshot.configured ?? false,
|
|
529
|
+
tokenSource: snapshot.tokenSource ?? "none",
|
|
530
|
+
running: snapshot.running ?? false,
|
|
531
|
+
connected: snapshot.connected ?? false,
|
|
532
|
+
lastConnectedAt: snapshot.lastConnectedAt ?? null,
|
|
533
|
+
lastError: snapshot.lastError ?? null
|
|
534
|
+
}),
|
|
535
|
+
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
536
|
+
accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID$1,
|
|
537
|
+
name: account?.name,
|
|
538
|
+
enabled: account?.enabled ?? false,
|
|
539
|
+
configured: Boolean(account?.appId && account?.clientSecret),
|
|
540
|
+
tokenSource: account?.secretSource,
|
|
541
|
+
running: runtime?.running ?? false,
|
|
542
|
+
connected: runtime?.connected ?? false,
|
|
543
|
+
lastConnectedAt: runtime?.lastConnectedAt ?? null,
|
|
544
|
+
lastError: runtime?.lastError ?? null,
|
|
545
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
546
|
+
lastOutboundAt: runtime?.lastOutboundAt ?? null
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
//#endregion
|
|
551
|
+
export { qqbotPlugin as t };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import "./resolve-D_06fV6-.js";
|
|
2
|
+
import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-DFcjQw73.js";
|
|
3
|
+
//#region extensions/qqbot/src/channel.setup.ts
|
|
4
|
+
/**
|
|
5
|
+
* Setup-only QQBot plugin — lightweight subset used during `openclaw onboard`
|
|
6
|
+
* and `openclaw configure` without pulling the full runtime dependencies.
|
|
7
|
+
*/
|
|
8
|
+
const qqbotSetupPlugin = {
|
|
9
|
+
id: "qqbot",
|
|
10
|
+
setupWizard: qqbotSetupWizard,
|
|
11
|
+
meta: { ...qqbotMeta },
|
|
12
|
+
capabilities: {
|
|
13
|
+
chatTypes: ["direct", "group"],
|
|
14
|
+
media: true,
|
|
15
|
+
reactions: false,
|
|
16
|
+
threads: false,
|
|
17
|
+
blockStreaming: true
|
|
18
|
+
},
|
|
19
|
+
reload: { configPrefixes: ["channels.qqbot"] },
|
|
20
|
+
configSchema: qqbotChannelConfigSchema,
|
|
21
|
+
config: { ...qqbotConfigAdapter },
|
|
22
|
+
setup: { ...qqbotSetupAdapterShared }
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { qqbotSetupPlugin as t };
|