@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
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_ACCOUNT_ID,
|
|
4
|
-
listAccountIds,
|
|
5
|
-
resolveDefaultAccountId,
|
|
6
|
-
resolveAccountBase,
|
|
7
|
-
} from "./resolve.js";
|
|
8
|
-
|
|
9
|
-
describe("engine/config/resolve", () => {
|
|
10
|
-
it("returns empty list when no accounts configured", () => {
|
|
11
|
-
expect(listAccountIds({})).toEqual([]);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("returns default when top-level appId is set", () => {
|
|
15
|
-
const cfg = {
|
|
16
|
-
channels: {
|
|
17
|
-
qqbot: { appId: "123456" },
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
expect(listAccountIds(cfg)).toEqual([DEFAULT_ACCOUNT_ID]);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("lists named accounts", () => {
|
|
24
|
-
const cfg = {
|
|
25
|
-
channels: {
|
|
26
|
-
qqbot: {
|
|
27
|
-
accounts: {
|
|
28
|
-
bot2: { appId: "654321" },
|
|
29
|
-
bot3: { appId: "111222" },
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
const ids = listAccountIds(cfg);
|
|
35
|
-
expect(ids).toContain("bot2");
|
|
36
|
-
expect(ids).toContain("bot3");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("resolves default account id to 'default' when top-level appId exists", () => {
|
|
40
|
-
const cfg = {
|
|
41
|
-
channels: {
|
|
42
|
-
qqbot: { appId: "123456" },
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
expect(resolveDefaultAccountId(cfg)).toBe(DEFAULT_ACCOUNT_ID);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("honors configured defaultAccount", () => {
|
|
49
|
-
const cfg = {
|
|
50
|
-
channels: {
|
|
51
|
-
qqbot: {
|
|
52
|
-
defaultAccount: "bot2",
|
|
53
|
-
accounts: {
|
|
54
|
-
bot2: { appId: "654321" },
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
expect(resolveDefaultAccountId(cfg)).toBe("bot2");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("falls back to first named account when no default configured", () => {
|
|
63
|
-
const cfg = {
|
|
64
|
-
channels: {
|
|
65
|
-
qqbot: {
|
|
66
|
-
accounts: {
|
|
67
|
-
mybot: { appId: "999999" },
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
expect(resolveDefaultAccountId(cfg)).toBe("mybot");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("resolves base account info for default account", () => {
|
|
76
|
-
const cfg = {
|
|
77
|
-
channels: {
|
|
78
|
-
qqbot: {
|
|
79
|
-
appId: "123456",
|
|
80
|
-
name: "Test Bot",
|
|
81
|
-
systemPrompt: "You are helpful.",
|
|
82
|
-
markdownSupport: true,
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
const base = resolveAccountBase(cfg, DEFAULT_ACCOUNT_ID);
|
|
87
|
-
expect(base.accountId).toBe(DEFAULT_ACCOUNT_ID);
|
|
88
|
-
expect(base.appId).toBe("123456");
|
|
89
|
-
expect(base.name).toBe("Test Bot");
|
|
90
|
-
expect(base.systemPrompt).toBe("You are helpful.");
|
|
91
|
-
expect(base.markdownSupport).toBe(true);
|
|
92
|
-
expect(base.enabled).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("resolves base account info for named account", () => {
|
|
96
|
-
const cfg = {
|
|
97
|
-
channels: {
|
|
98
|
-
qqbot: {
|
|
99
|
-
accounts: {
|
|
100
|
-
bot2: {
|
|
101
|
-
appId: "654321",
|
|
102
|
-
name: "Bot Two",
|
|
103
|
-
enabled: false,
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
const base = resolveAccountBase(cfg, "bot2");
|
|
110
|
-
expect(base.accountId).toBe("bot2");
|
|
111
|
-
expect(base.appId).toBe("654321");
|
|
112
|
-
expect(base.name).toBe("Bot Two");
|
|
113
|
-
expect(base.enabled).toBe(false);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("uses configured defaultAccount when accountId is omitted", () => {
|
|
117
|
-
const cfg = {
|
|
118
|
-
channels: {
|
|
119
|
-
qqbot: {
|
|
120
|
-
defaultAccount: "bot2",
|
|
121
|
-
accounts: {
|
|
122
|
-
bot2: { appId: "654321" },
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
const base = resolveAccountBase(cfg);
|
|
128
|
-
expect(base.accountId).toBe("bot2");
|
|
129
|
-
expect(base.appId).toBe("654321");
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("preserves audioFormatPolicy on the config object", () => {
|
|
133
|
-
const cfg = {
|
|
134
|
-
channels: {
|
|
135
|
-
qqbot: {
|
|
136
|
-
appId: "123456",
|
|
137
|
-
audioFormatPolicy: {
|
|
138
|
-
sttDirectFormats: [".wav"],
|
|
139
|
-
uploadDirectFormats: [".mp3"],
|
|
140
|
-
transcodeEnabled: false,
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
const base = resolveAccountBase(cfg, DEFAULT_ACCOUNT_ID);
|
|
146
|
-
expect(base.config.audioFormatPolicy).toEqual({
|
|
147
|
-
sttDirectFormats: [".wav"],
|
|
148
|
-
uploadDirectFormats: [".mp3"],
|
|
149
|
-
transcodeEnabled: false,
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
});
|
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QQBot config resolution (pure logic layer).
|
|
3
|
-
* QQBot 配置解析(纯逻辑层)。
|
|
4
|
-
*
|
|
5
|
-
* Resolves account IDs, default account selection, and base account
|
|
6
|
-
* info from raw config objects. Secret/credential resolution is
|
|
7
|
-
* intentionally left to the outer layer (src/bridge/config.ts) so that
|
|
8
|
-
* this module stays framework-agnostic and self-contained.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { getPlatformAdapter } from "../adapter/index.js";
|
|
12
|
-
import {
|
|
13
|
-
asOptionalObjectRecord as asRecord,
|
|
14
|
-
normalizeOptionalLowercaseString,
|
|
15
|
-
normalizeStringifiedOptionalString,
|
|
16
|
-
readStringField as readString,
|
|
17
|
-
} from "../utils/string-normalize.js";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Default account ID, used for the unnamed top-level account.
|
|
21
|
-
* 默认账号 ID,用于顶层配置中未命名的账号。
|
|
22
|
-
*/
|
|
23
|
-
export const DEFAULT_ACCOUNT_ID = "default";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Internal shape of the channels.qqbot config section.
|
|
27
|
-
* channels.qqbot 配置节的内部结构。
|
|
28
|
-
*/
|
|
29
|
-
interface QQBotChannelConfig {
|
|
30
|
-
appId?: unknown;
|
|
31
|
-
clientSecret?: unknown;
|
|
32
|
-
clientSecretFile?: string;
|
|
33
|
-
accounts?: Record<string, Record<string, unknown>>;
|
|
34
|
-
defaultAccount?: unknown;
|
|
35
|
-
[key: string]: unknown;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Base account resolution result (without credentials).
|
|
40
|
-
* 账号基础解析结果(不含凭证信息)。
|
|
41
|
-
*
|
|
42
|
-
* The outer config.ts layer extends this with clientSecret / secretSource.
|
|
43
|
-
*/
|
|
44
|
-
interface ResolvedAccountBase {
|
|
45
|
-
accountId: string;
|
|
46
|
-
name?: string;
|
|
47
|
-
enabled: boolean;
|
|
48
|
-
appId: string;
|
|
49
|
-
systemPrompt?: string;
|
|
50
|
-
markdownSupport: boolean;
|
|
51
|
-
config: Record<string, unknown>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function normalizeAppId(raw: unknown): string {
|
|
55
|
-
if (typeof raw === "string") {
|
|
56
|
-
return raw.trim();
|
|
57
|
-
}
|
|
58
|
-
if (typeof raw === "number") {
|
|
59
|
-
return String(raw);
|
|
60
|
-
}
|
|
61
|
-
return "";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function normalizeAccountConfig(
|
|
65
|
-
account: Record<string, unknown> | undefined,
|
|
66
|
-
): Record<string, unknown> {
|
|
67
|
-
if (!account) {
|
|
68
|
-
return {};
|
|
69
|
-
}
|
|
70
|
-
const audioPolicy = asRecord(account.audioFormatPolicy);
|
|
71
|
-
return {
|
|
72
|
-
...account,
|
|
73
|
-
...(audioPolicy ? { audioFormatPolicy: { ...audioPolicy } } : {}),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function readQQBotSection(cfg: Record<string, unknown>): QQBotChannelConfig | undefined {
|
|
78
|
-
const channels = asRecord(cfg.channels);
|
|
79
|
-
return asRecord(channels?.qqbot) as QQBotChannelConfig | undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* List all configured QQBot account IDs.
|
|
84
|
-
* 列出所有已配置的 QQBot 账号 ID。
|
|
85
|
-
*/
|
|
86
|
-
export function listAccountIds(cfg: Record<string, unknown>): string[] {
|
|
87
|
-
const ids = new Set<string>();
|
|
88
|
-
const qqbot = readQQBotSection(cfg);
|
|
89
|
-
|
|
90
|
-
if (qqbot?.appId || process.env.QQBOT_APP_ID) {
|
|
91
|
-
ids.add(DEFAULT_ACCOUNT_ID);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (qqbot?.accounts) {
|
|
95
|
-
for (const accountId of Object.keys(qqbot.accounts)) {
|
|
96
|
-
if (qqbot.accounts[accountId]?.appId) {
|
|
97
|
-
ids.add(accountId);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return Array.from(ids);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Resolve the default QQBot account ID.
|
|
107
|
-
* 解析默认 QQBot 账号 ID(优先级:defaultAccount > 顶层 appId > 第一个命名账号)。
|
|
108
|
-
*/
|
|
109
|
-
export function resolveDefaultAccountId(cfg: Record<string, unknown>): string {
|
|
110
|
-
const qqbot = readQQBotSection(cfg);
|
|
111
|
-
const configuredDefaultAccountId = normalizeOptionalLowercaseString(qqbot?.defaultAccount);
|
|
112
|
-
if (
|
|
113
|
-
configuredDefaultAccountId &&
|
|
114
|
-
(configuredDefaultAccountId === DEFAULT_ACCOUNT_ID ||
|
|
115
|
-
Boolean(qqbot?.accounts?.[configuredDefaultAccountId]?.appId))
|
|
116
|
-
) {
|
|
117
|
-
return configuredDefaultAccountId;
|
|
118
|
-
}
|
|
119
|
-
if (qqbot?.appId || process.env.QQBOT_APP_ID) {
|
|
120
|
-
return DEFAULT_ACCOUNT_ID;
|
|
121
|
-
}
|
|
122
|
-
if (qqbot?.accounts) {
|
|
123
|
-
const ids = Object.keys(qqbot.accounts);
|
|
124
|
-
if (ids.length > 0) {
|
|
125
|
-
return ids[0];
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return DEFAULT_ACCOUNT_ID;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Resolve base account info (without credentials).
|
|
133
|
-
* 解析账号基础信息(不含凭证)。
|
|
134
|
-
*
|
|
135
|
-
* Resolves everything except Secret/credential fields. The outer
|
|
136
|
-
* config.ts layer calls this and adds Secret handling on top.
|
|
137
|
-
*/
|
|
138
|
-
export function resolveAccountBase(
|
|
139
|
-
cfg: Record<string, unknown>,
|
|
140
|
-
accountId?: string | null,
|
|
141
|
-
): ResolvedAccountBase {
|
|
142
|
-
const resolvedAccountId = accountId ?? resolveDefaultAccountId(cfg);
|
|
143
|
-
const qqbot = readQQBotSection(cfg);
|
|
144
|
-
|
|
145
|
-
let accountConfig: Record<string, unknown> = {};
|
|
146
|
-
let appId = "";
|
|
147
|
-
|
|
148
|
-
if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
|
149
|
-
accountConfig = normalizeAccountConfig(asRecord(qqbot));
|
|
150
|
-
appId = normalizeAppId(qqbot?.appId);
|
|
151
|
-
} else {
|
|
152
|
-
const account = qqbot?.accounts?.[resolvedAccountId];
|
|
153
|
-
accountConfig = normalizeAccountConfig(asRecord(account));
|
|
154
|
-
appId = normalizeAppId(asRecord(account)?.appId);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (!appId && process.env.QQBOT_APP_ID && resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
|
158
|
-
appId = normalizeAppId(process.env.QQBOT_APP_ID);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
accountId: resolvedAccountId,
|
|
163
|
-
name: readString(accountConfig, "name"),
|
|
164
|
-
enabled: accountConfig.enabled !== false,
|
|
165
|
-
appId,
|
|
166
|
-
systemPrompt: readString(accountConfig, "systemPrompt"),
|
|
167
|
-
markdownSupport: accountConfig.markdownSupport !== false,
|
|
168
|
-
config: accountConfig,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ---- Account config apply ----
|
|
173
|
-
|
|
174
|
-
interface ApplyAccountInput {
|
|
175
|
-
appId?: string;
|
|
176
|
-
clientSecret?: string;
|
|
177
|
-
clientSecretFile?: string;
|
|
178
|
-
name?: string;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** Apply account config updates into a raw config object. */
|
|
182
|
-
export function applyAccountConfig(
|
|
183
|
-
cfg: Record<string, unknown>,
|
|
184
|
-
accountId: string,
|
|
185
|
-
input: ApplyAccountInput,
|
|
186
|
-
): Record<string, unknown> {
|
|
187
|
-
const next = { ...cfg };
|
|
188
|
-
const channels = asRecord(cfg.channels) ?? {};
|
|
189
|
-
const existingQQBot = asRecord(channels.qqbot) ?? {};
|
|
190
|
-
|
|
191
|
-
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
192
|
-
const allowFrom = (existingQQBot.allowFrom as unknown[]) ?? ["*"];
|
|
193
|
-
next.channels = {
|
|
194
|
-
...channels,
|
|
195
|
-
qqbot: {
|
|
196
|
-
...existingQQBot,
|
|
197
|
-
enabled: true,
|
|
198
|
-
allowFrom,
|
|
199
|
-
...(input.appId ? { appId: input.appId } : {}),
|
|
200
|
-
...(input.clientSecret
|
|
201
|
-
? { clientSecret: input.clientSecret, clientSecretFile: undefined }
|
|
202
|
-
: input.clientSecretFile
|
|
203
|
-
? { clientSecretFile: input.clientSecretFile, clientSecret: undefined }
|
|
204
|
-
: {}),
|
|
205
|
-
...(input.name ? { name: input.name } : {}),
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
} else {
|
|
209
|
-
const accounts = (existingQQBot.accounts ?? {}) as Record<string, Record<string, unknown>>;
|
|
210
|
-
const existingAccount = accounts[accountId] ?? {};
|
|
211
|
-
const allowFrom = (existingAccount.allowFrom as unknown[]) ?? ["*"];
|
|
212
|
-
next.channels = {
|
|
213
|
-
...channels,
|
|
214
|
-
qqbot: {
|
|
215
|
-
...existingQQBot,
|
|
216
|
-
enabled: true,
|
|
217
|
-
accounts: {
|
|
218
|
-
...accounts,
|
|
219
|
-
[accountId]: {
|
|
220
|
-
...existingAccount,
|
|
221
|
-
enabled: true,
|
|
222
|
-
allowFrom,
|
|
223
|
-
...(input.appId ? { appId: input.appId } : {}),
|
|
224
|
-
...(input.clientSecret
|
|
225
|
-
? { clientSecret: input.clientSecret, clientSecretFile: undefined }
|
|
226
|
-
: input.clientSecretFile
|
|
227
|
-
? { clientSecretFile: input.clientSecretFile, clientSecret: undefined }
|
|
228
|
-
: {}),
|
|
229
|
-
...(input.name ? { name: input.name } : {}),
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return next;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ---- Account status helpers ----
|
|
240
|
-
|
|
241
|
-
/** Resolved account shape expected by isAccountConfigured / describeAccount. */
|
|
242
|
-
interface AccountSnapshot {
|
|
243
|
-
accountId: string;
|
|
244
|
-
name?: string;
|
|
245
|
-
enabled: boolean;
|
|
246
|
-
appId: string;
|
|
247
|
-
clientSecret?: string;
|
|
248
|
-
secretSource?: string;
|
|
249
|
-
config: Record<string, unknown> & {
|
|
250
|
-
clientSecret?: unknown;
|
|
251
|
-
clientSecretFile?: string;
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Check whether a QQBot account has been fully configured. */
|
|
256
|
-
export function isAccountConfigured(account: AccountSnapshot | undefined): boolean {
|
|
257
|
-
return Boolean(
|
|
258
|
-
account?.appId &&
|
|
259
|
-
(Boolean(account?.clientSecret) ||
|
|
260
|
-
getPlatformAdapter().hasConfiguredSecret(account?.config?.clientSecret) ||
|
|
261
|
-
Boolean(account?.config?.clientSecretFile?.trim())),
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/** Build a summary description of an account. */
|
|
266
|
-
export function describeAccount(account: AccountSnapshot | undefined) {
|
|
267
|
-
return {
|
|
268
|
-
accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
269
|
-
name: account?.name,
|
|
270
|
-
enabled: account?.enabled ?? false,
|
|
271
|
-
configured: isAccountConfigured(account),
|
|
272
|
-
tokenSource: account?.secretSource,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** Normalize allowFrom entries into uppercase strings without the qqbot: prefix. */
|
|
277
|
-
export function formatAllowFrom(allowFrom: Array<string | number> | undefined | null): string[] {
|
|
278
|
-
return (allowFrom ?? [])
|
|
279
|
-
.map((entry) => normalizeStringifiedOptionalString(entry))
|
|
280
|
-
.filter((entry): entry is string => Boolean(entry))
|
|
281
|
-
.map((entry) => entry.replace(/^qqbot:/i, ""))
|
|
282
|
-
.map((entry) => entry.toUpperCase());
|
|
283
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QQBot setup business logic (pure layer).
|
|
3
|
-
* QQBot setup 相关纯业务逻辑。
|
|
4
|
-
*
|
|
5
|
-
* Token parsing, input validation, and setup config application.
|
|
6
|
-
* All functions are framework-agnostic and operate on plain objects.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { applyAccountConfig } from "./resolve.js";
|
|
10
|
-
import { DEFAULT_ACCOUNT_ID } from "./resolve.js";
|
|
11
|
-
|
|
12
|
-
/** Parse an inline "appId:clientSecret" token string. */
|
|
13
|
-
function parseInlineToken(token: string): { appId: string; clientSecret: string } | null {
|
|
14
|
-
const colonIdx = token.indexOf(":");
|
|
15
|
-
if (colonIdx <= 0 || colonIdx === token.length - 1) {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const appId = token.slice(0, colonIdx).trim();
|
|
20
|
-
const clientSecret = token.slice(colonIdx + 1).trim();
|
|
21
|
-
if (!appId || !clientSecret) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return { appId, clientSecret };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface SetupInput {
|
|
29
|
-
token?: string;
|
|
30
|
-
tokenFile?: string;
|
|
31
|
-
useEnv?: boolean;
|
|
32
|
-
name?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Validate setup input for a QQBot account. Returns an error string or null. */
|
|
36
|
-
export function validateSetupInput(accountId: string, input: SetupInput): string | null {
|
|
37
|
-
if (!input.token && !input.tokenFile && !input.useEnv) {
|
|
38
|
-
return "QQBot requires --token (format: appId:clientSecret) or --use-env";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
|
42
|
-
return "QQBot --use-env only supports the default account";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (input.token && !parseInlineToken(input.token)) {
|
|
46
|
-
return "QQBot --token must be in appId:clientSecret format";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Apply setup input to account config. Returns updated config. */
|
|
53
|
-
export function applySetupAccountConfig(
|
|
54
|
-
cfg: Record<string, unknown>,
|
|
55
|
-
accountId: string,
|
|
56
|
-
input: SetupInput,
|
|
57
|
-
): Record<string, unknown> {
|
|
58
|
-
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
|
59
|
-
return cfg;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
let appId = "";
|
|
63
|
-
let clientSecret = "";
|
|
64
|
-
|
|
65
|
-
if (input.token) {
|
|
66
|
-
const parsed = parseInlineToken(input.token);
|
|
67
|
-
if (!parsed) {
|
|
68
|
-
return cfg;
|
|
69
|
-
}
|
|
70
|
-
appId = parsed.appId;
|
|
71
|
-
clientSecret = parsed.clientSecret;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!appId && !input.tokenFile && !input.useEnv) {
|
|
75
|
-
return cfg;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return applyAccountConfig(cfg, accountId, {
|
|
79
|
-
appId,
|
|
80
|
-
clientSecret,
|
|
81
|
-
clientSecretFile: input.tokenFile,
|
|
82
|
-
name: input.name,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Engine import boundary test.
|
|
3
|
-
*
|
|
4
|
-
* Ensures that engine/ sources only import from `openclaw/plugin-sdk/*`
|
|
5
|
-
* and never reach into other openclaw internals directly.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import fs from "node:fs";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import { describe, expect, it } from "vitest";
|
|
11
|
-
|
|
12
|
-
const ENGINE_DIR = path.resolve(import.meta.dirname);
|
|
13
|
-
|
|
14
|
-
/** Recursively collect all non-test .ts files under a directory. */
|
|
15
|
-
function walkSourceFiles(dir: string, files: string[] = []): string[] {
|
|
16
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
17
|
-
const fullPath = path.join(dir, entry.name);
|
|
18
|
-
if (entry.isDirectory()) {
|
|
19
|
-
if (entry.name === "node_modules" || entry.name === "dist") {
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
walkSourceFiles(fullPath, files);
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
if (
|
|
26
|
-
entry.name.endsWith(".ts") &&
|
|
27
|
-
!entry.name.endsWith(".test.ts") &&
|
|
28
|
-
!entry.name.endsWith(".spec.ts")
|
|
29
|
-
) {
|
|
30
|
-
files.push(fullPath);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return files;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Extract all `openclaw/...` import specifiers from source text.
|
|
38
|
-
* Matches: import ... from "openclaw/...", import("openclaw/...")
|
|
39
|
-
*/
|
|
40
|
-
function findOpenclawImports(source: string): string[] {
|
|
41
|
-
return [
|
|
42
|
-
...source.matchAll(/from\s+["'](openclaw\/[^"']+)["']/g),
|
|
43
|
-
...source.matchAll(/import\(\s*["'](openclaw\/[^"']+)["']\s*\)/g),
|
|
44
|
-
].map((match) => match[1]);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Check if an import specifier is an allowed openclaw/plugin-sdk subpath. */
|
|
48
|
-
const ALLOWED_PREFIX = ["openclaw", "plugin-sdk"].join("/");
|
|
49
|
-
function isAllowedImport(specifier: string): boolean {
|
|
50
|
-
return specifier.startsWith(ALLOWED_PREFIX);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe("engine import boundary", () => {
|
|
54
|
-
it("only imports from openclaw/plugin-sdk, never from other openclaw internals", () => {
|
|
55
|
-
const sourceFiles = walkSourceFiles(ENGINE_DIR);
|
|
56
|
-
const offenders: Array<{ file: string; imports: string[] }> = [];
|
|
57
|
-
|
|
58
|
-
for (const file of sourceFiles) {
|
|
59
|
-
const source = fs.readFileSync(file, "utf8");
|
|
60
|
-
const openclawImports = findOpenclawImports(source);
|
|
61
|
-
const forbidden = openclawImports.filter((specifier) => !isAllowedImport(specifier));
|
|
62
|
-
|
|
63
|
-
if (forbidden.length > 0) {
|
|
64
|
-
offenders.push({
|
|
65
|
-
file: path.relative(ENGINE_DIR, file),
|
|
66
|
-
imports: forbidden,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
expect(offenders).toEqual([]);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gateway message decoding utilities.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from `gateway.ts` — handles the various data formats that
|
|
5
|
-
* the QQ Bot WebSocket can deliver (string, Buffer, Buffer[], ArrayBuffer).
|
|
6
|
-
*
|
|
7
|
-
* Zero external dependencies beyond Node.js built-ins.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Decode raw WebSocket `data` into a UTF-8 string.
|
|
12
|
-
*
|
|
13
|
-
* The QQ Bot gateway can send data as a plain string, a single Buffer,
|
|
14
|
-
* an array of Buffer chunks, an ArrayBuffer, or a typed array view.
|
|
15
|
-
*/
|
|
16
|
-
export function decodeGatewayMessageData(data: unknown): string {
|
|
17
|
-
if (typeof data === "string") {
|
|
18
|
-
return data;
|
|
19
|
-
}
|
|
20
|
-
if (Buffer.isBuffer(data)) {
|
|
21
|
-
return data.toString("utf8");
|
|
22
|
-
}
|
|
23
|
-
if (Array.isArray(data) && data.every((chunk) => Buffer.isBuffer(chunk))) {
|
|
24
|
-
return Buffer.concat(data).toString("utf8");
|
|
25
|
-
}
|
|
26
|
-
if (data instanceof ArrayBuffer) {
|
|
27
|
-
return Buffer.from(data).toString("utf8");
|
|
28
|
-
}
|
|
29
|
-
if (ArrayBuffer.isView(data)) {
|
|
30
|
-
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
|
|
31
|
-
}
|
|
32
|
-
return "";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Read the optional `message_scene.ext` array from an event payload.
|
|
37
|
-
*
|
|
38
|
-
* Guild, C2C, and Group events may carry a `message_scene` object
|
|
39
|
-
* with an `ext` string array used for ref-index parsing.
|
|
40
|
-
*/
|
|
41
|
-
export function readOptionalMessageSceneExt(event: Record<string, unknown>): string[] | undefined {
|
|
42
|
-
if (!("message_scene" in event)) {
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
const scene = event.message_scene as { ext?: string[] } | undefined;
|
|
46
|
-
return scene?.ext;
|
|
47
|
-
}
|