@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,237 @@
|
|
|
1
|
+
import { resolveApprovalApprovers } from "autobot/plugin-sdk/approval-auth-runtime";
|
|
2
|
+
import {
|
|
3
|
+
createChannelExecApprovalProfile,
|
|
4
|
+
isChannelExecApprovalClientEnabledFromConfig,
|
|
5
|
+
matchesApprovalRequestFilters,
|
|
6
|
+
} from "autobot/plugin-sdk/approval-client-runtime";
|
|
7
|
+
import { resolveApprovalRequestChannelAccountId } from "autobot/plugin-sdk/approval-native-runtime";
|
|
8
|
+
import type {
|
|
9
|
+
ExecApprovalRequest,
|
|
10
|
+
PluginApprovalRequest,
|
|
11
|
+
} from "autobot/plugin-sdk/approval-runtime";
|
|
12
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
13
|
+
import { normalizeAccountId } from "autobot/plugin-sdk/routing";
|
|
14
|
+
import {
|
|
15
|
+
normalizeLowercaseStringOrEmpty,
|
|
16
|
+
normalizeOptionalString,
|
|
17
|
+
} from "autobot/plugin-sdk/string-coerce-runtime";
|
|
18
|
+
import { listQQBotAccountIds, resolveQQBotAccount } from "./bridge/config.js";
|
|
19
|
+
import type { QQBotExecApprovalConfig } from "./types.js";
|
|
20
|
+
|
|
21
|
+
function normalizeApproverId(value: string | number): string | undefined {
|
|
22
|
+
const trimmed = normalizeOptionalString(String(value));
|
|
23
|
+
return trimmed || undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveQQBotExecApprovalConfig(params: {
|
|
27
|
+
cfg: AutoBotConfig;
|
|
28
|
+
accountId?: string | null;
|
|
29
|
+
}): QQBotExecApprovalConfig | undefined {
|
|
30
|
+
const account = resolveQQBotAccount(params.cfg, params.accountId);
|
|
31
|
+
const config = account.config.execApprovals;
|
|
32
|
+
if (!config) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
...config,
|
|
37
|
+
enabled: account.enabled && account.secretSource !== "none" ? config.enabled : false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getQQBotExecApprovalApprovers(params: {
|
|
42
|
+
cfg: AutoBotConfig;
|
|
43
|
+
accountId?: string | null;
|
|
44
|
+
}): string[] {
|
|
45
|
+
const accountConfig = resolveQQBotAccount(params.cfg, params.accountId).config;
|
|
46
|
+
return resolveApprovalApprovers({
|
|
47
|
+
explicit: resolveQQBotExecApprovalConfig(params)?.approvers,
|
|
48
|
+
allowFrom: accountConfig.allowFrom,
|
|
49
|
+
normalizeApprover: normalizeApproverId,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function countQQBotExecApprovalEligibleAccounts(params: {
|
|
54
|
+
cfg: AutoBotConfig;
|
|
55
|
+
request: ExecApprovalRequest | PluginApprovalRequest;
|
|
56
|
+
}): number {
|
|
57
|
+
return listQQBotAccountIds(params.cfg).filter((accountId) => {
|
|
58
|
+
const account = resolveQQBotAccount(params.cfg, accountId);
|
|
59
|
+
if (!account.enabled || account.secretSource === "none") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const config = resolveQQBotExecApprovalConfig({
|
|
63
|
+
cfg: params.cfg,
|
|
64
|
+
accountId,
|
|
65
|
+
});
|
|
66
|
+
return (
|
|
67
|
+
isChannelExecApprovalClientEnabledFromConfig({
|
|
68
|
+
enabled: config?.enabled,
|
|
69
|
+
approverCount: getQQBotExecApprovalApprovers({ cfg: params.cfg, accountId }).length,
|
|
70
|
+
}) &&
|
|
71
|
+
matchesApprovalRequestFilters({
|
|
72
|
+
request: params.request.request,
|
|
73
|
+
agentFilter: config?.agentFilter,
|
|
74
|
+
sessionFilter: config?.sessionFilter,
|
|
75
|
+
fallbackAgentIdFromSessionKey: true,
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
}).length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function matchesQQBotRequestAccount(params: {
|
|
82
|
+
cfg: AutoBotConfig;
|
|
83
|
+
accountId?: string | null;
|
|
84
|
+
request: ExecApprovalRequest | PluginApprovalRequest;
|
|
85
|
+
}): boolean {
|
|
86
|
+
const turnSourceChannel = normalizeLowercaseStringOrEmpty(
|
|
87
|
+
params.request.request.turnSourceChannel,
|
|
88
|
+
);
|
|
89
|
+
const boundAccountId = resolveApprovalRequestChannelAccountId({
|
|
90
|
+
cfg: params.cfg,
|
|
91
|
+
request: params.request,
|
|
92
|
+
channel: "qqbot",
|
|
93
|
+
});
|
|
94
|
+
if (turnSourceChannel && turnSourceChannel !== "qqbot" && !boundAccountId) {
|
|
95
|
+
return (
|
|
96
|
+
countQQBotExecApprovalEligibleAccounts({
|
|
97
|
+
cfg: params.cfg,
|
|
98
|
+
request: params.request,
|
|
99
|
+
}) <= 1
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return (
|
|
103
|
+
!boundAccountId ||
|
|
104
|
+
!params.accountId ||
|
|
105
|
+
normalizeAccountId(boundAccountId) === normalizeAccountId(params.accountId)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Count QQBot accounts that could actually deliver a native approval
|
|
111
|
+
* message — i.e. accounts that are enabled and have resolvable secrets.
|
|
112
|
+
* Disabled or unconfigured accounts never spawn a handler, so they
|
|
113
|
+
* must not contribute to the single-account shortcut in the fallback
|
|
114
|
+
* ownership check below.
|
|
115
|
+
*/
|
|
116
|
+
function countQQBotFallbackEligibleAccounts(cfg: AutoBotConfig): number {
|
|
117
|
+
return listQQBotAccountIds(cfg).filter((accountId) => {
|
|
118
|
+
const account = resolveQQBotAccount(cfg, accountId);
|
|
119
|
+
return account.enabled && account.secretSource !== "none";
|
|
120
|
+
}).length;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fallback account-ownership check — applied when `execApprovals` is NOT
|
|
125
|
+
* configured for any QQBot account. In this mode every enabled account
|
|
126
|
+
* handler would otherwise race to deliver the same approval to its own
|
|
127
|
+
* openid namespace, so we must enforce per-account isolation.
|
|
128
|
+
*
|
|
129
|
+
* Rules:
|
|
130
|
+
* - If the request carries a bound account (via `turnSourceAccountId`
|
|
131
|
+
* or session binding), only the handler whose `accountId` matches it
|
|
132
|
+
* delivers the approval. This is strict: a handler with an unknown
|
|
133
|
+
* `accountId` (null/undefined) must not claim a bound request.
|
|
134
|
+
* - If no account is bound, only deliver when there is a single
|
|
135
|
+
* *eligible* QQBot account (enabled + secret resolved). Disabled or
|
|
136
|
+
* unconfigured accounts never deliver anyway, so they shouldn't
|
|
137
|
+
* block the remaining single account from handling the approval.
|
|
138
|
+
* Multiple eligible accounts cannot safely race because openids are
|
|
139
|
+
* account-scoped — cross-account delivery hits the QQ Bot API with
|
|
140
|
+
* a mismatched token and fails.
|
|
141
|
+
*/
|
|
142
|
+
function matchesQQBotFallbackRequestAccount(params: {
|
|
143
|
+
cfg: AutoBotConfig;
|
|
144
|
+
accountId?: string | null;
|
|
145
|
+
request: ExecApprovalRequest | PluginApprovalRequest;
|
|
146
|
+
}): boolean {
|
|
147
|
+
const boundAccountId = resolveApprovalRequestChannelAccountId({
|
|
148
|
+
cfg: params.cfg,
|
|
149
|
+
request: params.request,
|
|
150
|
+
channel: "qqbot",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (boundAccountId) {
|
|
154
|
+
if (!params.accountId) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return normalizeAccountId(boundAccountId) === normalizeAccountId(params.accountId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return countQQBotFallbackEligibleAccounts(params.cfg) <= 1;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Minimal structural shape required to evaluate per-account ownership.
|
|
165
|
+
*
|
|
166
|
+
* The SDK types (`ExecApprovalRequest` / `PluginApprovalRequest`) and the
|
|
167
|
+
* channel-local approval request types (see `engine/approval/index.ts`)
|
|
168
|
+
* share the same logical fields but differ on bookkeeping metadata
|
|
169
|
+
* (e.g. `createdAtMs`), so we accept any object exposing the relevant
|
|
170
|
+
* routing fields. Consumers can pass either flavor safely.
|
|
171
|
+
*/
|
|
172
|
+
type QQBotApprovalAccountOwnershipRequest = {
|
|
173
|
+
request: {
|
|
174
|
+
sessionKey?: string | null;
|
|
175
|
+
turnSourceChannel?: string | null;
|
|
176
|
+
turnSourceTo?: string | null;
|
|
177
|
+
turnSourceAccountId?: string | null;
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Unified per-account ownership check used by both the profile and
|
|
183
|
+
* fallback approval paths. Dispatches to the profile rules when the
|
|
184
|
+
* current account has `execApprovals` configured, otherwise uses the
|
|
185
|
+
* fallback rules.
|
|
186
|
+
*
|
|
187
|
+
* This is the single source of truth for "does this QQBot handler own
|
|
188
|
+
* this approval request?" and is consumed by both the capability
|
|
189
|
+
* gate (shouldHandle) and the lazy native runtime adapter.
|
|
190
|
+
*/
|
|
191
|
+
export function matchesQQBotApprovalAccount(params: {
|
|
192
|
+
cfg: AutoBotConfig;
|
|
193
|
+
accountId?: string | null;
|
|
194
|
+
request: QQBotApprovalAccountOwnershipRequest;
|
|
195
|
+
}): boolean {
|
|
196
|
+
const normalized = {
|
|
197
|
+
cfg: params.cfg,
|
|
198
|
+
accountId: params.accountId,
|
|
199
|
+
request: params.request as unknown as ExecApprovalRequest | PluginApprovalRequest,
|
|
200
|
+
};
|
|
201
|
+
if (resolveQQBotExecApprovalConfig(normalized) !== undefined) {
|
|
202
|
+
return matchesQQBotRequestAccount(normalized);
|
|
203
|
+
}
|
|
204
|
+
return matchesQQBotFallbackRequestAccount(normalized);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const qqbotExecApprovalProfile = createChannelExecApprovalProfile({
|
|
208
|
+
resolveConfig: resolveQQBotExecApprovalConfig,
|
|
209
|
+
resolveApprovers: getQQBotExecApprovalApprovers,
|
|
210
|
+
matchesRequestAccount: matchesQQBotRequestAccount,
|
|
211
|
+
fallbackAgentIdFromSessionKey: true,
|
|
212
|
+
requireClientEnabledForLocalPromptSuppression: false,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
export const isQQBotExecApprovalClientEnabled = qqbotExecApprovalProfile.isClientEnabled;
|
|
216
|
+
export const isQQBotExecApprovalApprover = qqbotExecApprovalProfile.isApprover;
|
|
217
|
+
export const isQQBotExecApprovalAuthorizedSender = qqbotExecApprovalProfile.isAuthorizedSender;
|
|
218
|
+
export const shouldHandleQQBotExecApprovalRequest = qqbotExecApprovalProfile.shouldHandleRequest;
|
|
219
|
+
|
|
220
|
+
export function authorizeQQBotApprovalAction(params: {
|
|
221
|
+
cfg: AutoBotConfig;
|
|
222
|
+
accountId?: string | null;
|
|
223
|
+
senderId?: string | null;
|
|
224
|
+
approvalKind: "exec" | "plugin";
|
|
225
|
+
}): { authorized: boolean; reason?: string } {
|
|
226
|
+
if (resolveQQBotExecApprovalConfig(params) === undefined) {
|
|
227
|
+
return { authorized: true };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const authorized =
|
|
231
|
+
params.approvalKind === "plugin"
|
|
232
|
+
? isQQBotExecApprovalApprover(params)
|
|
233
|
+
: isQQBotExecApprovalAuthorizedSender(params);
|
|
234
|
+
return authorized
|
|
235
|
+
? { authorized: true }
|
|
236
|
+
: { authorized: false, reason: "You are not authorized to approve this request." };
|
|
237
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
2
|
+
|
|
3
|
+
export function makeQqbotSecretRefConfig(): AutoBotConfig {
|
|
4
|
+
return {
|
|
5
|
+
channels: {
|
|
6
|
+
qqbot: {
|
|
7
|
+
appId: "123456",
|
|
8
|
+
clientSecret: {
|
|
9
|
+
source: "env",
|
|
10
|
+
provider: "default",
|
|
11
|
+
id: "QQBOT_CLIENT_SECRET",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
} as AutoBotConfig;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function makeQqbotDefaultAccountConfig(): AutoBotConfig {
|
|
19
|
+
return {
|
|
20
|
+
channels: {
|
|
21
|
+
qqbot: {
|
|
22
|
+
defaultAccount: "bot2",
|
|
23
|
+
accounts: {
|
|
24
|
+
bot2: { appId: "123456" },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
} as AutoBotConfig;
|
|
29
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectConditionalChannelFieldAssignments,
|
|
3
|
+
getChannelSurface,
|
|
4
|
+
hasConfiguredSecretInputValue,
|
|
5
|
+
type ResolverContext,
|
|
6
|
+
type SecretDefaults,
|
|
7
|
+
type SecretTargetRegistryEntry,
|
|
8
|
+
} from "autobot/plugin-sdk/channel-secret-basic-runtime";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
11
|
+
|
|
12
|
+
export const secretTargetRegistryEntries = [
|
|
13
|
+
{
|
|
14
|
+
id: "channels.qqbot.accounts.*.clientSecret",
|
|
15
|
+
targetType: "channels.qqbot.accounts.*.clientSecret",
|
|
16
|
+
configFile: "autobot.json",
|
|
17
|
+
pathPattern: "channels.qqbot.accounts.*.clientSecret",
|
|
18
|
+
secretShape: "secret_input",
|
|
19
|
+
expectedResolvedValue: "string",
|
|
20
|
+
includeInPlan: true,
|
|
21
|
+
includeInConfigure: true,
|
|
22
|
+
includeInAudit: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "channels.qqbot.clientSecret",
|
|
26
|
+
targetType: "channels.qqbot.clientSecret",
|
|
27
|
+
configFile: "autobot.json",
|
|
28
|
+
pathPattern: "channels.qqbot.clientSecret",
|
|
29
|
+
secretShape: "secret_input",
|
|
30
|
+
expectedResolvedValue: "string",
|
|
31
|
+
includeInPlan: true,
|
|
32
|
+
includeInConfigure: true,
|
|
33
|
+
includeInAudit: true,
|
|
34
|
+
},
|
|
35
|
+
] satisfies SecretTargetRegistryEntry[];
|
|
36
|
+
|
|
37
|
+
function hasTopLevelAppId(qqbot: Record<string, unknown>): boolean {
|
|
38
|
+
if (typeof qqbot.appId === "string") {
|
|
39
|
+
return qqbot.appId.trim().length > 0;
|
|
40
|
+
}
|
|
41
|
+
return typeof qqbot.appId === "number";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function collectRuntimeConfigAssignments(params: {
|
|
45
|
+
config: { channels?: Record<string, unknown> };
|
|
46
|
+
defaults?: SecretDefaults;
|
|
47
|
+
context: ResolverContext;
|
|
48
|
+
}): void {
|
|
49
|
+
const resolved = getChannelSurface(params.config, "qqbot");
|
|
50
|
+
if (!resolved) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { channel: qqbot, surface } = resolved;
|
|
55
|
+
const hasExplicitDefaultAccount = surface.accounts.some(
|
|
56
|
+
({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
collectConditionalChannelFieldAssignments({
|
|
60
|
+
channelKey: "qqbot",
|
|
61
|
+
field: "clientSecret",
|
|
62
|
+
channel: qqbot,
|
|
63
|
+
surface,
|
|
64
|
+
defaults: params.defaults,
|
|
65
|
+
context: params.context,
|
|
66
|
+
topLevelActiveWithoutAccounts: true,
|
|
67
|
+
topLevelInheritedAccountActive: ({ accountId, account, enabled }) => {
|
|
68
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
69
|
+
return enabled && !hasConfiguredSecretInputValue(account.clientSecret, params.defaults);
|
|
70
|
+
}
|
|
71
|
+
return !hasExplicitDefaultAccount && hasTopLevelAppId(qqbot);
|
|
72
|
+
},
|
|
73
|
+
accountActive: ({ enabled }) => enabled,
|
|
74
|
+
topInactiveReason: "no enabled QQ Bot default surface uses this top-level clientSecret.",
|
|
75
|
+
accountInactiveReason: "QQ Bot account is disabled.",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const channelSecrets = {
|
|
80
|
+
secretTargetRegistryEntries,
|
|
81
|
+
collectRuntimeConfigAssignments,
|
|
82
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { SecretInput } from "autobot/plugin-sdk/secret-input";
|
|
2
|
+
import type { QQBotDmPolicy, QQBotGroupPolicy } from "./engine/access/index.js";
|
|
3
|
+
|
|
4
|
+
export type { QQBotDmPolicy, QQBotGroupPolicy };
|
|
5
|
+
|
|
6
|
+
/** QQ Bot base config. */
|
|
7
|
+
export interface QQBotConfig {
|
|
8
|
+
appId: string;
|
|
9
|
+
clientSecret?: SecretInput;
|
|
10
|
+
clientSecretFile?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Resolved QQ Bot account config used at runtime. */
|
|
14
|
+
export interface ResolvedQQBotAccount {
|
|
15
|
+
accountId: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
appId: string;
|
|
19
|
+
clientSecret: string;
|
|
20
|
+
secretSource: "config" | "file" | "env" | "none";
|
|
21
|
+
/** Additional system prompt text. */
|
|
22
|
+
systemPrompt?: string;
|
|
23
|
+
/** Whether markdown output is enabled. Defaults to true. */
|
|
24
|
+
markdownSupport: boolean;
|
|
25
|
+
config: QQBotAccountConfig;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** QQBot-native exec approval delivery + approver authorization. */
|
|
29
|
+
export interface QQBotExecApprovalConfig {
|
|
30
|
+
enabled?: boolean | "auto";
|
|
31
|
+
approvers?: string[];
|
|
32
|
+
agentFilter?: string[];
|
|
33
|
+
sessionFilter?: string[];
|
|
34
|
+
target?: "dm" | "channel" | "both";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** QQ Bot account config from user settings. */
|
|
38
|
+
export interface QQBotAccountConfig {
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
name?: string;
|
|
41
|
+
appId?: string;
|
|
42
|
+
clientSecret?: SecretInput;
|
|
43
|
+
clientSecretFile?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Sender allowlist for direct-message access control and command
|
|
46
|
+
* authorization. Entries accept raw openids, `qqbot:OPENID` prefixed
|
|
47
|
+
* form, and the `"*"` wildcard. Matching is case-insensitive.
|
|
48
|
+
*
|
|
49
|
+
* Semantics depend on {@link dmPolicy}:
|
|
50
|
+
* - `dmPolicy="open"` (default when allowFrom is empty or contains `"*"`)
|
|
51
|
+
* — everyone can DM the bot; the list only influences command gating.
|
|
52
|
+
* - `dmPolicy="allowlist"` (default when a non-wildcard list is configured)
|
|
53
|
+
* — only listed openids may DM the bot; other DMs are dropped.
|
|
54
|
+
* - `dmPolicy="disabled"` — all DMs are dropped regardless of this list.
|
|
55
|
+
*
|
|
56
|
+
* For group access, see {@link groupAllowFrom} / {@link groupPolicy}.
|
|
57
|
+
*/
|
|
58
|
+
allowFrom?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Group-scoped sender allowlist. If omitted, group access falls back to
|
|
61
|
+
* {@link allowFrom}. Set explicitly when the group whitelist needs to
|
|
62
|
+
* differ from the DM whitelist.
|
|
63
|
+
*/
|
|
64
|
+
groupAllowFrom?: string[];
|
|
65
|
+
/**
|
|
66
|
+
* DM access policy. Defaults:
|
|
67
|
+
* - omitted + allowFrom empty/wildcard → `"open"`
|
|
68
|
+
* - omitted + allowFrom non-wildcard → `"allowlist"`
|
|
69
|
+
*/
|
|
70
|
+
dmPolicy?: QQBotDmPolicy;
|
|
71
|
+
/**
|
|
72
|
+
* Group access policy. Defaults mirror {@link dmPolicy}: if either
|
|
73
|
+
* `groupAllowFrom` or `allowFrom` has a non-wildcard entry the policy
|
|
74
|
+
* is `"allowlist"`, otherwise `"open"`.
|
|
75
|
+
*/
|
|
76
|
+
groupPolicy?: QQBotGroupPolicy;
|
|
77
|
+
/** Optional system prompt prepended to user messages. */
|
|
78
|
+
systemPrompt?: string;
|
|
79
|
+
/** Whether markdown output is enabled. Defaults to true. */
|
|
80
|
+
markdownSupport?: boolean;
|
|
81
|
+
/** QQBot-native exec approval delivery + approver authorization. */
|
|
82
|
+
execApprovals?: QQBotExecApprovalConfig;
|
|
83
|
+
/**
|
|
84
|
+
* @deprecated Use audioFormatPolicy.uploadDirectFormats instead.
|
|
85
|
+
* Legacy list of formats that can upload directly without SILK conversion.
|
|
86
|
+
*/
|
|
87
|
+
voiceDirectUploadFormats?: string[];
|
|
88
|
+
/**
|
|
89
|
+
* Audio format policy covering inbound STT and outbound upload behavior.
|
|
90
|
+
*/
|
|
91
|
+
audioFormatPolicy?: AudioFormatPolicy;
|
|
92
|
+
/**
|
|
93
|
+
* Whether public URLs should be uploaded to QQ directly. Defaults to true.
|
|
94
|
+
*/
|
|
95
|
+
urlDirectUpload?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Upgrade guide URL returned by `/bot-upgrade`.
|
|
98
|
+
*/
|
|
99
|
+
upgradeUrl?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Upgrade command mode.
|
|
102
|
+
* - "doc": show an upgrade guide link
|
|
103
|
+
* - "hot-reload": run an in-place npm update flow
|
|
104
|
+
*/
|
|
105
|
+
upgradeMode?: "doc" | "hot-reload";
|
|
106
|
+
/**
|
|
107
|
+
* Block streaming + optional QQ C2C official stream API.
|
|
108
|
+
* - `true`: same as `mode: "partial"` and `c2cStreamApi: true` (recommended).
|
|
109
|
+
* - `false` / omitted: no official C2C stream for this account (see object form for fine control).
|
|
110
|
+
* - Object (legacy / advanced): `mode` "partial" | "off", `c2cStreamApi` for C2C `/stream_messages`.
|
|
111
|
+
*/
|
|
112
|
+
streaming?:
|
|
113
|
+
| boolean
|
|
114
|
+
| {
|
|
115
|
+
mode?: "off" | "partial";
|
|
116
|
+
/** @deprecated Prefer `streaming: true`. */
|
|
117
|
+
c2cStreamApi?: boolean;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Audio format policy controlling which formats can skip transcoding. */
|
|
122
|
+
export interface AudioFormatPolicy {
|
|
123
|
+
/**
|
|
124
|
+
* Formats supported directly by the STT provider.
|
|
125
|
+
*/
|
|
126
|
+
sttDirectFormats?: string[];
|
|
127
|
+
/**
|
|
128
|
+
* Formats QQ accepts directly for outbound uploads.
|
|
129
|
+
*/
|
|
130
|
+
uploadDirectFormats?: string[];
|
|
131
|
+
/**
|
|
132
|
+
* Whether outbound audio transcoding is enabled. Defaults to true.
|
|
133
|
+
*/
|
|
134
|
+
transcodeEnabled?: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Rich-media attachment metadata. */
|
|
138
|
+
export interface MessageAttachment {
|
|
139
|
+
content_type: string;
|
|
140
|
+
filename?: string;
|
|
141
|
+
height?: number;
|
|
142
|
+
width?: number;
|
|
143
|
+
size?: number;
|
|
144
|
+
url: string;
|
|
145
|
+
voice_wav_url?: string;
|
|
146
|
+
asr_refer_text?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** C2C message event payload. */
|
|
150
|
+
export interface C2CMessageEvent {
|
|
151
|
+
author: {
|
|
152
|
+
id: string;
|
|
153
|
+
union_openid: string;
|
|
154
|
+
user_openid: string;
|
|
155
|
+
};
|
|
156
|
+
content: string;
|
|
157
|
+
id: string;
|
|
158
|
+
timestamp: string;
|
|
159
|
+
message_scene?: {
|
|
160
|
+
source: string;
|
|
161
|
+
/** ext can contain ref_msg_idx and msg_idx values. */
|
|
162
|
+
ext?: string[];
|
|
163
|
+
};
|
|
164
|
+
attachments?: MessageAttachment[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Guild @-message event payload. */
|
|
168
|
+
export interface GuildMessageEvent {
|
|
169
|
+
id: string;
|
|
170
|
+
channel_id: string;
|
|
171
|
+
guild_id: string;
|
|
172
|
+
content: string;
|
|
173
|
+
timestamp: string;
|
|
174
|
+
author: {
|
|
175
|
+
id: string;
|
|
176
|
+
username?: string;
|
|
177
|
+
bot?: boolean;
|
|
178
|
+
};
|
|
179
|
+
member?: {
|
|
180
|
+
nick?: string;
|
|
181
|
+
joined_at?: string;
|
|
182
|
+
};
|
|
183
|
+
attachments?: MessageAttachment[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Group @-message event payload. */
|
|
187
|
+
export interface GroupMessageEvent {
|
|
188
|
+
author: {
|
|
189
|
+
id: string;
|
|
190
|
+
member_openid: string;
|
|
191
|
+
};
|
|
192
|
+
content: string;
|
|
193
|
+
id: string;
|
|
194
|
+
timestamp: string;
|
|
195
|
+
group_id: string;
|
|
196
|
+
group_openid: string;
|
|
197
|
+
message_scene?: {
|
|
198
|
+
source: string;
|
|
199
|
+
ext?: string[];
|
|
200
|
+
};
|
|
201
|
+
attachments?: MessageAttachment[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** WebSocket event payload. */
|
|
205
|
+
export interface WSPayload {
|
|
206
|
+
op: number;
|
|
207
|
+
d?: unknown;
|
|
208
|
+
s?: number;
|
|
209
|
+
t?: string;
|
|
210
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.package-boundary.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "."
|
|
5
|
+
},
|
|
6
|
+
"include": ["./*.ts", "./src/**/*.ts"],
|
|
7
|
+
"exclude": [
|
|
8
|
+
"./**/*.test.ts",
|
|
9
|
+
"./dist/**",
|
|
10
|
+
"./node_modules/**",
|
|
11
|
+
"./src/test-support/**",
|
|
12
|
+
"./src/**/*test-helpers.ts",
|
|
13
|
+
"./src/**/*test-harness.ts",
|
|
14
|
+
"./src/**/*test-support.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|