@gakr-gakr/qqbot 0.1.0
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/api.ts +56 -0
- package/autobot.plugin.json +167 -0
- package/channel-plugin-api.ts +1 -0
- package/index.ts +33 -0
- package/package.json +64 -0
- package/runtime-api.ts +9 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/skills/qqbot-channel/SKILL.md +262 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-media/SKILL.md +37 -0
- package/skills/qqbot-remind/SKILL.md +153 -0
- package/src/bridge/approval/capability.ts +225 -0
- package/src/bridge/approval/handler-runtime.ts +204 -0
- package/src/bridge/bootstrap.ts +135 -0
- package/src/bridge/channel-entry.ts +18 -0
- package/src/bridge/commands/framework-context-adapter.ts +60 -0
- package/src/bridge/commands/framework-registration.ts +66 -0
- package/src/bridge/commands/from-parser.ts +60 -0
- package/src/bridge/commands/result-dispatcher.ts +76 -0
- package/src/bridge/config-shared.ts +132 -0
- package/src/bridge/config.ts +176 -0
- package/src/bridge/gateway.ts +178 -0
- package/src/bridge/logger.ts +31 -0
- package/src/bridge/narrowing.ts +31 -0
- package/src/bridge/plugin-version.ts +102 -0
- package/src/bridge/runtime.ts +25 -0
- package/src/bridge/sdk-adapter.ts +164 -0
- package/src/bridge/setup/finalize.ts +144 -0
- package/src/bridge/setup/surface.ts +34 -0
- package/src/bridge/tools/channel.ts +58 -0
- package/src/bridge/tools/index.ts +15 -0
- package/src/bridge/tools/remind.ts +91 -0
- package/src/channel.setup.ts +33 -0
- package/src/channel.ts +399 -0
- package/src/config-schema.ts +84 -0
- package/src/engine/access/index.ts +2 -0
- package/src/engine/access/resolve-policy.ts +30 -0
- package/src/engine/access/sender-match.ts +55 -0
- package/src/engine/access/types.ts +2 -0
- package/src/engine/adapter/audio.port.ts +27 -0
- package/src/engine/adapter/commands.port.ts +22 -0
- package/src/engine/adapter/history.port.ts +52 -0
- package/src/engine/adapter/index.ts +76 -0
- package/src/engine/adapter/mention-gate.port.ts +50 -0
- package/src/engine/adapter/types.ts +38 -0
- package/src/engine/api/api-client.ts +212 -0
- package/src/engine/api/media-chunked.ts +644 -0
- package/src/engine/api/media.ts +218 -0
- package/src/engine/api/messages.ts +293 -0
- package/src/engine/api/retry.ts +217 -0
- package/src/engine/api/routes.ts +95 -0
- package/src/engine/api/token.ts +277 -0
- package/src/engine/approval/index.ts +224 -0
- package/src/engine/commands/builtin/log-helpers.ts +341 -0
- package/src/engine/commands/builtin/register-all.ts +17 -0
- package/src/engine/commands/builtin/register-approve.ts +201 -0
- package/src/engine/commands/builtin/register-basic.ts +95 -0
- package/src/engine/commands/builtin/register-clear-storage.ts +187 -0
- package/src/engine/commands/builtin/register-logs.ts +20 -0
- package/src/engine/commands/builtin/register-streaming.ts +138 -0
- package/src/engine/commands/builtin/state.ts +31 -0
- package/src/engine/commands/slash-command-auth.ts +88 -0
- package/src/engine/commands/slash-command-handler.ts +168 -0
- package/src/engine/commands/slash-command-test-support.ts +39 -0
- package/src/engine/commands/slash-commands-impl.ts +61 -0
- package/src/engine/commands/slash-commands.ts +202 -0
- package/src/engine/config/credential-backup.ts +108 -0
- package/src/engine/config/credentials.ts +76 -0
- package/src/engine/config/group.ts +227 -0
- package/src/engine/config/resolve.ts +283 -0
- package/src/engine/config/setup-logic.ts +84 -0
- package/src/engine/gateway/active-cfg.ts +52 -0
- package/src/engine/gateway/codec.ts +47 -0
- package/src/engine/gateway/constants.ts +117 -0
- package/src/engine/gateway/event-dispatcher.ts +177 -0
- package/src/engine/gateway/gateway-connection.ts +356 -0
- package/src/engine/gateway/gateway.ts +267 -0
- package/src/engine/gateway/inbound-attachments.ts +360 -0
- package/src/engine/gateway/inbound-context.ts +82 -0
- package/src/engine/gateway/inbound-pipeline.ts +171 -0
- package/src/engine/gateway/interaction-handler.ts +345 -0
- package/src/engine/gateway/message-queue.ts +404 -0
- package/src/engine/gateway/outbound-dispatch.ts +590 -0
- package/src/engine/gateway/reconnect.ts +199 -0
- package/src/engine/gateway/stages/access-stage.ts +99 -0
- package/src/engine/gateway/stages/assembly-stage.ts +156 -0
- package/src/engine/gateway/stages/content-stage.ts +77 -0
- package/src/engine/gateway/stages/envelope-stage.ts +144 -0
- package/src/engine/gateway/stages/group-gate-stage.ts +223 -0
- package/src/engine/gateway/stages/index.ts +18 -0
- package/src/engine/gateway/stages/quote-stage.ts +113 -0
- package/src/engine/gateway/stages/refidx-stage.ts +62 -0
- package/src/engine/gateway/stages/stub-contexts.ts +77 -0
- package/src/engine/gateway/types.ts +230 -0
- package/src/engine/gateway/typing-keepalive.ts +102 -0
- package/src/engine/gateway/ws-client.ts +16 -0
- package/src/engine/group/activation.ts +88 -0
- package/src/engine/group/history.ts +321 -0
- package/src/engine/group/mention.ts +114 -0
- package/src/engine/group/message-gating.ts +108 -0
- package/src/engine/messaging/decode-media-path.ts +82 -0
- package/src/engine/messaging/media-source.ts +210 -0
- package/src/engine/messaging/media-type-detect.ts +27 -0
- package/src/engine/messaging/outbound-audio-port.ts +38 -0
- package/src/engine/messaging/outbound-deliver.ts +810 -0
- package/src/engine/messaging/outbound-media-send.ts +658 -0
- package/src/engine/messaging/outbound-reply.ts +27 -0
- package/src/engine/messaging/outbound-result-helpers.ts +54 -0
- package/src/engine/messaging/outbound-types.ts +47 -0
- package/src/engine/messaging/outbound.ts +485 -0
- package/src/engine/messaging/reply-dispatcher.ts +597 -0
- package/src/engine/messaging/reply-limiter.ts +164 -0
- package/src/engine/messaging/sender.ts +741 -0
- package/src/engine/messaging/streaming-c2c.ts +1192 -0
- package/src/engine/messaging/streaming-media-send.ts +544 -0
- package/src/engine/messaging/target-parser.ts +104 -0
- package/src/engine/ref/format-message-ref.ts +142 -0
- package/src/engine/ref/format-ref-entry.ts +27 -0
- package/src/engine/ref/store.ts +211 -0
- package/src/engine/ref/types.ts +27 -0
- package/src/engine/session/known-users.ts +138 -0
- package/src/engine/session/session-store.ts +207 -0
- package/src/engine/tools/channel-api.ts +244 -0
- package/src/engine/tools/remind-logic.ts +377 -0
- package/src/engine/types.ts +313 -0
- package/src/engine/utils/attachment-tags.ts +174 -0
- package/src/engine/utils/audio.ts +525 -0
- package/src/engine/utils/data-paths.ts +38 -0
- package/src/engine/utils/diagnostics.ts +93 -0
- package/src/engine/utils/file-utils.ts +215 -0
- package/src/engine/utils/format.ts +70 -0
- package/src/engine/utils/image-size.ts +249 -0
- package/src/engine/utils/log.ts +77 -0
- package/src/engine/utils/media-tags.ts +177 -0
- package/src/engine/utils/payload.ts +157 -0
- package/src/engine/utils/platform.ts +265 -0
- package/src/engine/utils/request-context.ts +60 -0
- package/src/engine/utils/string-normalize.ts +91 -0
- package/src/engine/utils/stt.ts +103 -0
- package/src/engine/utils/text-parsing.ts +155 -0
- package/src/engine/utils/upload-cache.ts +96 -0
- package/src/engine/utils/voice-text.ts +15 -0
- package/src/exec-approvals.ts +237 -0
- package/src/qqbot-test-support.ts +29 -0
- package/src/secret-contract.ts +82 -0
- package/src/types.ts +210 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQBot credential management (pure logic layer).
|
|
3
|
+
* QQBot 凭证管理(纯逻辑层)。
|
|
4
|
+
*
|
|
5
|
+
* Credential clearing and field-level cleanup for logout and setup
|
|
6
|
+
* flows. All functions operate on plain objects (Record<string, unknown>)
|
|
7
|
+
* and stay framework-agnostic.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { asOptionalObjectRecord as asRecord } from "../utils/string-normalize.js";
|
|
11
|
+
import { DEFAULT_ACCOUNT_ID } from "./resolve.js";
|
|
12
|
+
|
|
13
|
+
// ---- Logout: clear all credential fields for an account ----
|
|
14
|
+
|
|
15
|
+
interface ClearCredentialsResult {
|
|
16
|
+
nextCfg: Record<string, unknown>;
|
|
17
|
+
cleared: boolean;
|
|
18
|
+
changed: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Remove clientSecret / clientSecretFile from a QQBot account config.
|
|
23
|
+
*
|
|
24
|
+
* Returns a shallow-cloned config with credentials removed, plus flags
|
|
25
|
+
* indicating whether anything actually changed.
|
|
26
|
+
*/
|
|
27
|
+
export function clearAccountCredentials(
|
|
28
|
+
cfg: Record<string, unknown>,
|
|
29
|
+
accountId: string,
|
|
30
|
+
): ClearCredentialsResult {
|
|
31
|
+
const nextCfg = { ...cfg };
|
|
32
|
+
const channels = asRecord(cfg.channels);
|
|
33
|
+
const nextQQBot = channels?.qqbot ? { ...asRecord(channels.qqbot) } : undefined;
|
|
34
|
+
let cleared = false;
|
|
35
|
+
let changed = false;
|
|
36
|
+
|
|
37
|
+
if (nextQQBot) {
|
|
38
|
+
const qqbot = nextQQBot as Record<string, unknown>;
|
|
39
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
40
|
+
if (qqbot.clientSecret) {
|
|
41
|
+
delete qqbot.clientSecret;
|
|
42
|
+
cleared = true;
|
|
43
|
+
changed = true;
|
|
44
|
+
}
|
|
45
|
+
if (qqbot.clientSecretFile) {
|
|
46
|
+
delete qqbot.clientSecretFile;
|
|
47
|
+
cleared = true;
|
|
48
|
+
changed = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const accounts = qqbot.accounts as Record<string, Record<string, unknown>> | undefined;
|
|
52
|
+
if (accounts && accountId in accounts) {
|
|
53
|
+
const entry = accounts[accountId] as Record<string, unknown> | undefined;
|
|
54
|
+
if (entry && "clientSecret" in entry) {
|
|
55
|
+
delete entry.clientSecret;
|
|
56
|
+
cleared = true;
|
|
57
|
+
changed = true;
|
|
58
|
+
}
|
|
59
|
+
if (entry && "clientSecretFile" in entry) {
|
|
60
|
+
delete entry.clientSecretFile;
|
|
61
|
+
cleared = true;
|
|
62
|
+
changed = true;
|
|
63
|
+
}
|
|
64
|
+
if (entry && Object.keys(entry).length === 0) {
|
|
65
|
+
delete accounts[accountId];
|
|
66
|
+
changed = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (changed && nextQQBot) {
|
|
72
|
+
nextCfg.channels = { ...channels, qqbot: nextQQBot };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { nextCfg, cleared, changed };
|
|
76
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { asOptionalObjectRecord as asRecord } from "../utils/string-normalize.js";
|
|
2
|
+
import { resolveAccountBase } from "./resolve.js";
|
|
3
|
+
|
|
4
|
+
type GroupToolPolicy = "full" | "restricted" | "none";
|
|
5
|
+
|
|
6
|
+
interface GroupConfig {
|
|
7
|
+
requireMention: boolean;
|
|
8
|
+
ignoreOtherMentions: boolean;
|
|
9
|
+
toolPolicy: GroupToolPolicy;
|
|
10
|
+
name: string;
|
|
11
|
+
prompt?: string;
|
|
12
|
+
historyLimit: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_GROUP_HISTORY_LIMIT = 50;
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_GROUP_PROMPT =
|
|
18
|
+
"If the sender is a bot, respond only when they explicitly @mention you to ask a question or request assistance with a specific task; keep your replies concise and clear, avoiding the urge to race other bots to answer or engage in lengthy, unproductive exchanges. In group chats, prioritize responding to messages from human users; bots should maintain a collaborative rather than competitive dynamic to ensure the conversation remains orderly and does not result in message flooding.";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_GROUP_CONFIG: Readonly<Omit<GroupConfig, "prompt">> = {
|
|
21
|
+
requireMention: true,
|
|
22
|
+
ignoreOtherMentions: false,
|
|
23
|
+
toolPolicy: "restricted",
|
|
24
|
+
name: "",
|
|
25
|
+
historyLimit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function readGroupsMap(
|
|
29
|
+
cfg: Record<string, unknown>,
|
|
30
|
+
accountId?: string | null,
|
|
31
|
+
): Record<string, Record<string, unknown>> {
|
|
32
|
+
const account = resolveAccountBase(cfg, accountId);
|
|
33
|
+
const groups = asRecord(account.config.groups);
|
|
34
|
+
if (!groups) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
const normalized: Record<string, Record<string, unknown>> = {};
|
|
38
|
+
for (const [key, value] of Object.entries(groups)) {
|
|
39
|
+
const sub = asRecord(value);
|
|
40
|
+
if (sub) {
|
|
41
|
+
normalized[key] = sub;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readBoolean(obj: Record<string, unknown>, key: string): boolean | undefined {
|
|
48
|
+
const v = obj[key];
|
|
49
|
+
return typeof v === "boolean" ? v : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readString(obj: Record<string, unknown>, key: string): string | undefined {
|
|
53
|
+
const v = obj[key];
|
|
54
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readToolPolicy(obj: Record<string, unknown>, key: string): GroupToolPolicy | undefined {
|
|
58
|
+
const v = obj[key];
|
|
59
|
+
return v === "full" || v === "restricted" || v === "none" ? v : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readHistoryLimit(obj: Record<string, unknown>, key: string): number | undefined {
|
|
63
|
+
const v = obj[key];
|
|
64
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return Math.max(0, Math.floor(v));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveGroupConfig(
|
|
71
|
+
cfg: Record<string, unknown>,
|
|
72
|
+
groupOpenid?: string | null,
|
|
73
|
+
accountId?: string | null,
|
|
74
|
+
): GroupConfig {
|
|
75
|
+
const groups = readGroupsMap(cfg, accountId);
|
|
76
|
+
const wildcard = groups["*"] ?? {};
|
|
77
|
+
const specific = groupOpenid ? (groups[groupOpenid] ?? {}) : {};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
requireMention:
|
|
81
|
+
readBoolean(specific, "requireMention") ??
|
|
82
|
+
readBoolean(wildcard, "requireMention") ??
|
|
83
|
+
DEFAULT_GROUP_CONFIG.requireMention,
|
|
84
|
+
ignoreOtherMentions:
|
|
85
|
+
readBoolean(specific, "ignoreOtherMentions") ??
|
|
86
|
+
readBoolean(wildcard, "ignoreOtherMentions") ??
|
|
87
|
+
DEFAULT_GROUP_CONFIG.ignoreOtherMentions,
|
|
88
|
+
toolPolicy:
|
|
89
|
+
readToolPolicy(specific, "toolPolicy") ??
|
|
90
|
+
readToolPolicy(wildcard, "toolPolicy") ??
|
|
91
|
+
DEFAULT_GROUP_CONFIG.toolPolicy,
|
|
92
|
+
name: readString(specific, "name") ?? readString(wildcard, "name") ?? DEFAULT_GROUP_CONFIG.name,
|
|
93
|
+
prompt: readString(specific, "prompt") ?? readString(wildcard, "prompt"),
|
|
94
|
+
historyLimit:
|
|
95
|
+
readHistoryLimit(specific, "historyLimit") ??
|
|
96
|
+
readHistoryLimit(wildcard, "historyLimit") ??
|
|
97
|
+
DEFAULT_GROUP_CONFIG.historyLimit,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveHistoryLimit(
|
|
102
|
+
cfg: Record<string, unknown>,
|
|
103
|
+
groupOpenid?: string | null,
|
|
104
|
+
accountId?: string | null,
|
|
105
|
+
): number {
|
|
106
|
+
return resolveGroupConfig(cfg, groupOpenid, accountId).historyLimit;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resolveRequireMention(
|
|
110
|
+
cfg: Record<string, unknown>,
|
|
111
|
+
groupOpenid?: string | null,
|
|
112
|
+
accountId?: string | null,
|
|
113
|
+
): boolean {
|
|
114
|
+
return resolveGroupConfig(cfg, groupOpenid, accountId).requireMention;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function resolveIgnoreOtherMentions(
|
|
118
|
+
cfg: Record<string, unknown>,
|
|
119
|
+
groupOpenid?: string | null,
|
|
120
|
+
accountId?: string | null,
|
|
121
|
+
): boolean {
|
|
122
|
+
return resolveGroupConfig(cfg, groupOpenid, accountId).ignoreOtherMentions;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Resolve tool policy for a given group. */
|
|
126
|
+
export function resolveGroupToolPolicy(
|
|
127
|
+
cfg: Record<string, unknown>,
|
|
128
|
+
groupOpenid?: string | null,
|
|
129
|
+
accountId?: string | null,
|
|
130
|
+
): GroupToolPolicy {
|
|
131
|
+
return resolveGroupConfig(cfg, groupOpenid, accountId).toolPolicy;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolve the behaviour prompt (PE) for a group. Falls back to the built-in
|
|
136
|
+
* default when neither specific nor wildcard configuration provides one.
|
|
137
|
+
*/
|
|
138
|
+
export function resolveGroupPrompt(
|
|
139
|
+
cfg: Record<string, unknown>,
|
|
140
|
+
groupOpenid?: string | null,
|
|
141
|
+
accountId?: string | null,
|
|
142
|
+
): string {
|
|
143
|
+
return resolveGroupConfig(cfg, groupOpenid, accountId).prompt ?? DEFAULT_GROUP_PROMPT;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve the display name for a group.
|
|
148
|
+
*
|
|
149
|
+
* When no name is configured, the first 8 characters of the openid are used
|
|
150
|
+
* as a short identifier so log lines stay compact.
|
|
151
|
+
*/
|
|
152
|
+
export function resolveGroupName(
|
|
153
|
+
cfg: Record<string, unknown>,
|
|
154
|
+
groupOpenid: string,
|
|
155
|
+
accountId?: string | null,
|
|
156
|
+
): string {
|
|
157
|
+
const name = resolveGroupConfig(cfg, groupOpenid, accountId).name;
|
|
158
|
+
return name || groupOpenid.slice(0, 8);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============ GroupSettings (aggregate) ============
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Per-inbound aggregate of everything the pipeline needs about a group.
|
|
165
|
+
*
|
|
166
|
+
* Built once at the top of the group-gate stage so downstream consumers
|
|
167
|
+
* don't repeatedly re-parse the same `cfg` tree. Superset of
|
|
168
|
+
* {@link GroupConfig}: also includes the effective `mentionPatterns`
|
|
169
|
+
* (which depend on `agentId`, not on the group itself) and a
|
|
170
|
+
* pre-computed display name for logging.
|
|
171
|
+
*/
|
|
172
|
+
interface GroupSettings {
|
|
173
|
+
/** Merged group config (specific > wildcard > defaults). */
|
|
174
|
+
config: GroupConfig;
|
|
175
|
+
/** Display name — `config.name` or the first 8 chars of the openid. */
|
|
176
|
+
name: string;
|
|
177
|
+
/** Raw mentionPatterns (agent > global > []). */
|
|
178
|
+
mentionPatterns: string[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function resolveGroupSettings(params: {
|
|
182
|
+
cfg: Record<string, unknown>;
|
|
183
|
+
groupOpenid: string;
|
|
184
|
+
accountId?: string | null;
|
|
185
|
+
agentId?: string | null;
|
|
186
|
+
}): GroupSettings {
|
|
187
|
+
const config = resolveGroupConfig(params.cfg, params.groupOpenid, params.accountId);
|
|
188
|
+
const name = config.name || params.groupOpenid.slice(0, 8);
|
|
189
|
+
const mentionPatterns = resolveMentionPatterns(params.cfg, params.agentId);
|
|
190
|
+
return { config, name, mentionPatterns };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface AgentEntry {
|
|
194
|
+
id?: unknown;
|
|
195
|
+
groupChat?: { mentionPatterns?: unknown };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function resolveMentionPatterns(
|
|
199
|
+
cfg: Record<string, unknown>,
|
|
200
|
+
agentId?: string | null,
|
|
201
|
+
): string[] {
|
|
202
|
+
if (agentId) {
|
|
203
|
+
const agents = asRecord(cfg.agents);
|
|
204
|
+
const list = Array.isArray(agents?.list) ? (agents?.list as AgentEntry[]) : [];
|
|
205
|
+
const entry = list.find(
|
|
206
|
+
(a) => typeof a.id === "string" && a.id.trim().toLowerCase() === agentId.trim().toLowerCase(),
|
|
207
|
+
);
|
|
208
|
+
const agentGroupChat = entry?.groupChat;
|
|
209
|
+
if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
|
|
210
|
+
const patterns = agentGroupChat.mentionPatterns;
|
|
211
|
+
return Array.isArray(patterns)
|
|
212
|
+
? patterns.filter((p): p is string => typeof p === "string")
|
|
213
|
+
: [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const messages = asRecord(cfg.messages);
|
|
218
|
+
const globalGroupChat = asRecord(messages?.groupChat);
|
|
219
|
+
if (globalGroupChat && Object.hasOwn(globalGroupChat, "mentionPatterns")) {
|
|
220
|
+
const patterns = globalGroupChat.mentionPatterns;
|
|
221
|
+
return Array.isArray(patterns)
|
|
222
|
+
? patterns.filter((p): p is string => typeof p === "string")
|
|
223
|
+
: [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
}
|