@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,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge-layer logger — holds the framework logger injected at gateway startup.
|
|
3
|
+
*
|
|
4
|
+
* Bridge modules (approval, tools, etc.) use this instead of `console.log` or
|
|
5
|
+
* engine's `debugLog` so that all logs flow through the AutoBot log system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface BridgeLogger {
|
|
9
|
+
info: (msg: string) => void;
|
|
10
|
+
error: (msg: string) => void;
|
|
11
|
+
warn?: (msg: string) => void;
|
|
12
|
+
debug?: (msg: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let loggerInstance: BridgeLogger | null = null;
|
|
16
|
+
|
|
17
|
+
/** Register the framework logger. Called once in startGateway(). */
|
|
18
|
+
export function setBridgeLogger(logger: BridgeLogger): void {
|
|
19
|
+
loggerInstance = logger;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Get the bridge logger. Falls back to console if not yet registered. */
|
|
23
|
+
export function getBridgeLogger(): BridgeLogger {
|
|
24
|
+
return (
|
|
25
|
+
loggerInstance ?? {
|
|
26
|
+
info: (msg) => console.log(msg),
|
|
27
|
+
error: (msg) => console.error(msg),
|
|
28
|
+
debug: (msg) => console.log(msg),
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
2
|
+
import type { PluginRuntime } from "autobot/plugin-sdk/core";
|
|
3
|
+
import type { GatewayAccount } from "../engine/types.js";
|
|
4
|
+
import type { ResolvedQQBotAccount } from "../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Map resolved plugin account to the engine gateway account shape (single assertion on nested config).
|
|
8
|
+
*/
|
|
9
|
+
export function toGatewayAccount(account: ResolvedQQBotAccount): GatewayAccount {
|
|
10
|
+
return {
|
|
11
|
+
accountId: account.accountId,
|
|
12
|
+
appId: account.appId,
|
|
13
|
+
clientSecret: account.clientSecret,
|
|
14
|
+
markdownSupport: account.markdownSupport,
|
|
15
|
+
systemPrompt: account.systemPrompt,
|
|
16
|
+
config: account.config as GatewayAccount["config"],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Persist AutoBot config through the injected plugin runtime (typed entry point).
|
|
22
|
+
*/
|
|
23
|
+
export async function writeAutoBotConfigThroughRuntime(
|
|
24
|
+
runtime: PluginRuntime,
|
|
25
|
+
cfg: AutoBotConfig,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
await runtime.config.replaceConfigFile({
|
|
28
|
+
nextConfig: cfg,
|
|
29
|
+
afterWrite: { mode: "auto" },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQBot plugin version resolver.
|
|
3
|
+
*
|
|
4
|
+
* Reads the version field from this plugin's own `package.json` by
|
|
5
|
+
* walking up the directory tree starting from `import.meta.url` of the
|
|
6
|
+
* caller until a `package.json` whose `name` field matches the plugin
|
|
7
|
+
* package id is located.
|
|
8
|
+
*
|
|
9
|
+
* Why not a hardcoded relative path?
|
|
10
|
+
* - The source file can live at different depths depending on whether
|
|
11
|
+
* we run from raw sources (`src/bridge/gateway.ts`) or a future
|
|
12
|
+
* compiled output. Hardcoding `"../../package.json"` breaks as soon
|
|
13
|
+
* as the source layout changes, which is what caused the previous
|
|
14
|
+
* `vunknown` regression.
|
|
15
|
+
* - A `name` guard prevents accidentally reading the parent
|
|
16
|
+
* `autobot/package.json` (the framework root) when the plugin
|
|
17
|
+
* lives inside the monorepo.
|
|
18
|
+
*
|
|
19
|
+
* The lookup is performed only once per process at startup, so the
|
|
20
|
+
* synchronous file I/O is negligible.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
/** `name` field in this plugin's `package.json`. */
|
|
28
|
+
const QQBOT_PLUGIN_PKG_NAME = "@gakr-gakr/qqbot";
|
|
29
|
+
|
|
30
|
+
/** Sentinel used when the version cannot be resolved. */
|
|
31
|
+
export const QQBOT_PLUGIN_VERSION_UNKNOWN = "unknown";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the QQBot plugin version from `package.json`.
|
|
35
|
+
*
|
|
36
|
+
* @param startUrl — pass `import.meta.url` from the call site so the
|
|
37
|
+
* lookup begins at the caller's file regardless of where this helper
|
|
38
|
+
* itself lives. Falls back to this module's own location when omitted.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveQQBotPluginVersion(startUrl?: string): string {
|
|
41
|
+
const entryUrl = startUrl ?? import.meta.url;
|
|
42
|
+
let dir: string;
|
|
43
|
+
try {
|
|
44
|
+
dir = path.dirname(fileURLToPath(entryUrl));
|
|
45
|
+
} catch {
|
|
46
|
+
return QQBOT_PLUGIN_VERSION_UNKNOWN;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const root = path.parse(dir).root;
|
|
50
|
+
while (dir && dir !== root) {
|
|
51
|
+
const candidate = path.join(dir, "package.json");
|
|
52
|
+
if (fs.existsSync(candidate)) {
|
|
53
|
+
const version = readQQBotVersionFromManifest(candidate);
|
|
54
|
+
if (version) {
|
|
55
|
+
return version;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const parent = path.dirname(dir);
|
|
59
|
+
if (parent === dir) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
dir = parent;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return QQBOT_PLUGIN_VERSION_UNKNOWN;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Read the `version` field from a `package.json` file and return it
|
|
70
|
+
* only when the manifest describes the QQBot plugin itself.
|
|
71
|
+
*
|
|
72
|
+
* Returning `null` for mismatched or malformed manifests lets the
|
|
73
|
+
* caller keep walking up the directory tree until the correct package
|
|
74
|
+
* boundary is located.
|
|
75
|
+
*/
|
|
76
|
+
function readQQBotVersionFromManifest(manifestPath: string): string | null {
|
|
77
|
+
let raw: string;
|
|
78
|
+
try {
|
|
79
|
+
raw = fs.readFileSync(manifestPath, "utf8");
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let parsed: unknown;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(raw);
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!parsed || typeof parsed !== "object") {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const manifest = parsed as { name?: unknown; version?: unknown };
|
|
95
|
+
if (manifest.name !== QQBOT_PLUGIN_PKG_NAME) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (typeof manifest.version !== "string" || manifest.version.length === 0) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return manifest.version;
|
|
102
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PluginRuntime } from "autobot/plugin-sdk/core";
|
|
2
|
+
import { createPluginRuntimeStore } from "autobot/plugin-sdk/runtime-store";
|
|
3
|
+
import type { GatewayPluginRuntime } from "../engine/gateway/types.js";
|
|
4
|
+
import { setAutoBotVersion } from "../engine/messaging/sender.js";
|
|
5
|
+
|
|
6
|
+
// Single plugin runtime per process — concurrent multi-tenant qqbot runtimes are not supported.
|
|
7
|
+
const { setRuntime: _setRuntime, getRuntime: getQQBotRuntime } =
|
|
8
|
+
createPluginRuntimeStore<PluginRuntime>({
|
|
9
|
+
pluginId: "qqbot",
|
|
10
|
+
errorMessage: "QQBot runtime not initialized",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** Set the QQBot runtime and inject the framework version into the User-Agent. */
|
|
14
|
+
function setQQBotRuntime(runtime: PluginRuntime): void {
|
|
15
|
+
_setRuntime(runtime);
|
|
16
|
+
// Inject the framework version into the User-Agent string (same as standalone).
|
|
17
|
+
setAutoBotVersion(runtime.version);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { getQQBotRuntime, setQQBotRuntime };
|
|
21
|
+
|
|
22
|
+
/** Type-narrowed getter for engine/ modules that need GatewayPluginRuntime. */
|
|
23
|
+
export function getQQBotRuntimeForEngine(): GatewayPluginRuntime {
|
|
24
|
+
return getQQBotRuntime() as GatewayPluginRuntime;
|
|
25
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createChannelIngressResolver,
|
|
3
|
+
defineStableChannelIngressIdentity,
|
|
4
|
+
} from "autobot/plugin-sdk/channel-ingress-runtime";
|
|
5
|
+
import { resolveInboundMentionDecision } from "autobot/plugin-sdk/channel-mention-gating";
|
|
6
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
7
|
+
import {
|
|
8
|
+
createChannelHistoryWindow,
|
|
9
|
+
type HistoryEntry as SdkHistoryEntry,
|
|
10
|
+
} from "autobot/plugin-sdk/reply-history";
|
|
11
|
+
import { resolveQQBotEffectivePolicies } from "../engine/access/resolve-policy.js";
|
|
12
|
+
import { normalizeQQBotAllowFrom, normalizeQQBotSenderId } from "../engine/access/sender-match.js";
|
|
13
|
+
import type { HistoryPort, HistoryEntryLike } from "../engine/adapter/history.port.js";
|
|
14
|
+
import type { AccessPort } from "../engine/adapter/index.js";
|
|
15
|
+
import type { MentionGatePort } from "../engine/adapter/mention-gate.port.js";
|
|
16
|
+
|
|
17
|
+
const qqbotIngressIdentity = defineStableChannelIngressIdentity({
|
|
18
|
+
key: "sender-id",
|
|
19
|
+
normalize: normalizeQQBotSenderId,
|
|
20
|
+
isWildcardEntry: (entry) => normalizeQQBotSenderId(entry) === "*",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function asSdkMap<T>(map: Map<string, T[]>): Map<string, SdkHistoryEntry[]> {
|
|
24
|
+
return map as unknown as Map<string, SdkHistoryEntry[]>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createSdkHistoryAdapter(): HistoryPort {
|
|
28
|
+
return {
|
|
29
|
+
recordPendingHistoryEntry<T extends HistoryEntryLike>(params: {
|
|
30
|
+
historyMap: Map<string, T[]>;
|
|
31
|
+
historyKey: string;
|
|
32
|
+
entry?: T | null;
|
|
33
|
+
limit: number;
|
|
34
|
+
}) {
|
|
35
|
+
return createChannelHistoryWindow({ historyMap: asSdkMap(params.historyMap) }).record({
|
|
36
|
+
historyKey: params.historyKey,
|
|
37
|
+
entry: params.entry as SdkHistoryEntry | undefined,
|
|
38
|
+
limit: params.limit,
|
|
39
|
+
}) as T[];
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
buildPendingHistoryContext(params) {
|
|
43
|
+
return createChannelHistoryWindow({
|
|
44
|
+
historyMap: asSdkMap(params.historyMap),
|
|
45
|
+
}).buildPendingContext({
|
|
46
|
+
historyKey: params.historyKey,
|
|
47
|
+
limit: params.limit,
|
|
48
|
+
currentMessage: params.currentMessage,
|
|
49
|
+
formatEntry: params.formatEntry as (entry: SdkHistoryEntry) => string,
|
|
50
|
+
lineBreak: params.lineBreak,
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
clearPendingHistory(params) {
|
|
55
|
+
createChannelHistoryWindow({ historyMap: asSdkMap(params.historyMap) }).clear({
|
|
56
|
+
historyKey: params.historyKey,
|
|
57
|
+
limit: params.limit,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createSdkMentionGateAdapter(): MentionGatePort {
|
|
64
|
+
return {
|
|
65
|
+
resolveInboundMentionDecision(params) {
|
|
66
|
+
return resolveInboundMentionDecision(params);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createSdkAccessAdapter(): AccessPort {
|
|
72
|
+
return {
|
|
73
|
+
async resolveInboundAccess(input) {
|
|
74
|
+
const { dmPolicy, groupPolicy } = resolveQQBotEffectivePolicies(input);
|
|
75
|
+
const rawGroupAllowFrom =
|
|
76
|
+
input.groupAllowFrom && input.groupAllowFrom.length > 0
|
|
77
|
+
? input.groupAllowFrom
|
|
78
|
+
: (input.allowFrom ?? []);
|
|
79
|
+
const normalizedAllowFrom = normalizeQQBotAllowFrom(input.allowFrom);
|
|
80
|
+
const dmAllowFromForIngress =
|
|
81
|
+
dmPolicy === "open" && normalizedAllowFrom.length === 0 ? ["*"] : (input.allowFrom ?? []);
|
|
82
|
+
|
|
83
|
+
const commandOwnerAllowFrom = input.isGroup
|
|
84
|
+
? []
|
|
85
|
+
: input.allowFrom && input.allowFrom.length > 0
|
|
86
|
+
? input.allowFrom
|
|
87
|
+
: ["*"];
|
|
88
|
+
const resolved = await createChannelIngressResolver({
|
|
89
|
+
channelId: "qqbot",
|
|
90
|
+
accountId: input.accountId,
|
|
91
|
+
identity: qqbotIngressIdentity,
|
|
92
|
+
cfg: input.cfg as AutoBotConfig,
|
|
93
|
+
}).message({
|
|
94
|
+
subject: { stableId: input.senderId },
|
|
95
|
+
conversation: {
|
|
96
|
+
kind: input.isGroup ? "group" : "direct",
|
|
97
|
+
id: input.conversationId,
|
|
98
|
+
},
|
|
99
|
+
event: {
|
|
100
|
+
mayPair: false,
|
|
101
|
+
},
|
|
102
|
+
dmPolicy,
|
|
103
|
+
groupPolicy,
|
|
104
|
+
policy: {
|
|
105
|
+
groupAllowFromFallbackToAllowFrom: false,
|
|
106
|
+
},
|
|
107
|
+
allowFrom: dmAllowFromForIngress,
|
|
108
|
+
groupAllowFrom: rawGroupAllowFrom,
|
|
109
|
+
command: {
|
|
110
|
+
commandOwnerAllowFrom,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
return resolved;
|
|
114
|
+
},
|
|
115
|
+
async resolveSlashCommandAuthorization(input) {
|
|
116
|
+
return await resolveQQBotSlashCommandAuthorized(input);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function resolveQQBotSlashCommandAuthorized(params: {
|
|
122
|
+
cfg: unknown;
|
|
123
|
+
accountId: string;
|
|
124
|
+
isGroup: boolean;
|
|
125
|
+
senderId: string;
|
|
126
|
+
conversationId: string;
|
|
127
|
+
allowFrom?: Array<string | number> | null;
|
|
128
|
+
groupAllowFrom?: Array<string | number> | null;
|
|
129
|
+
commandsAllowFrom?: Array<string | number> | null;
|
|
130
|
+
}): Promise<boolean> {
|
|
131
|
+
const rawAllowFrom =
|
|
132
|
+
params.commandsAllowFrom ??
|
|
133
|
+
(params.isGroup && params.groupAllowFrom && params.groupAllowFrom.length > 0
|
|
134
|
+
? params.groupAllowFrom
|
|
135
|
+
: params.allowFrom);
|
|
136
|
+
const explicitAllowFrom = normalizeQQBotAllowFrom(rawAllowFrom).filter((entry) => entry !== "*");
|
|
137
|
+
if (explicitAllowFrom.length === 0) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const resolved = await createChannelIngressResolver({
|
|
141
|
+
channelId: "qqbot",
|
|
142
|
+
accountId: params.accountId,
|
|
143
|
+
identity: qqbotIngressIdentity,
|
|
144
|
+
cfg: params.cfg as AutoBotConfig,
|
|
145
|
+
}).message({
|
|
146
|
+
subject: { stableId: params.senderId },
|
|
147
|
+
conversation: {
|
|
148
|
+
kind: params.isGroup ? "group" : "direct",
|
|
149
|
+
id: params.conversationId,
|
|
150
|
+
},
|
|
151
|
+
event: {
|
|
152
|
+
kind: "slash-command",
|
|
153
|
+
authMode: "none",
|
|
154
|
+
mayPair: false,
|
|
155
|
+
},
|
|
156
|
+
dmPolicy: "allowlist",
|
|
157
|
+
groupPolicy: "open",
|
|
158
|
+
allowFrom: explicitAllowFrom,
|
|
159
|
+
command: {
|
|
160
|
+
modeWhenAccessGroupsOff: "configured",
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
return resolved.commandAccess.authorized;
|
|
164
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
|
|
2
|
+
import type { ChannelSetupWizard } from "autobot/plugin-sdk/setup";
|
|
3
|
+
import { DEFAULT_ACCOUNT_ID } from "autobot/plugin-sdk/setup";
|
|
4
|
+
import { formatDocsLink } from "autobot/plugin-sdk/setup-tools";
|
|
5
|
+
import { applyQQBotAccountConfig, resolveQQBotAccount } from "../config.js";
|
|
6
|
+
|
|
7
|
+
type SetupPrompter = Parameters<NonNullable<ChannelSetupWizard["finalize"]>>[0]["prompter"];
|
|
8
|
+
type SetupRuntime = Parameters<NonNullable<ChannelSetupWizard["finalize"]>>[0]["runtime"];
|
|
9
|
+
|
|
10
|
+
function isQQBotAccountConfigured(cfg: AutoBotConfig, accountId: string): boolean {
|
|
11
|
+
const account = resolveQQBotAccount(cfg, accountId, { allowUnresolvedSecretRef: true });
|
|
12
|
+
return Boolean(account.appId && account.clientSecret);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function linkViaQrCode(params: {
|
|
16
|
+
cfg: AutoBotConfig;
|
|
17
|
+
accountId: string;
|
|
18
|
+
prompter: SetupPrompter;
|
|
19
|
+
runtime: SetupRuntime;
|
|
20
|
+
}): Promise<AutoBotConfig> {
|
|
21
|
+
try {
|
|
22
|
+
const { qrConnect } = await import("@tencent-connect/qqbot-connector");
|
|
23
|
+
|
|
24
|
+
const accounts: { appId: string; appSecret: string }[] = await qrConnect({
|
|
25
|
+
source: "autobot",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (accounts.length === 0) {
|
|
29
|
+
await params.prompter.note("未获取到任何 QQ Bot 账号信息。", "QQ Bot");
|
|
30
|
+
return params.cfg;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let next = params.cfg;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
36
|
+
const { appId, appSecret } = accounts[i];
|
|
37
|
+
// use current account id for first account, and use app id for subsequent accounts
|
|
38
|
+
const targetAccountId = i === 0 ? params.accountId : appId;
|
|
39
|
+
|
|
40
|
+
next = applyQQBotAccountConfig(next, targetAccountId, {
|
|
41
|
+
appId,
|
|
42
|
+
clientSecret: appSecret,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (accounts.length === 1) {
|
|
47
|
+
params.runtime.log(`✔ QQ Bot 绑定成功!(AppID: ${accounts[0].appId})`);
|
|
48
|
+
} else {
|
|
49
|
+
const idList = accounts.map((a) => a.appId).join(", ");
|
|
50
|
+
params.runtime.log(`✔ ${accounts.length} 个 QQ Bot 绑定成功!(AppID: ${idList})`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return next;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
params.runtime.error(`QQ Bot 绑定失败: ${String(error)}`);
|
|
56
|
+
await params.prompter.note(
|
|
57
|
+
[
|
|
58
|
+
"绑定失败,您可以稍后手动配置。",
|
|
59
|
+
`文档: ${formatDocsLink("/channels/qqbot", "qqbot")}`,
|
|
60
|
+
].join("\n"),
|
|
61
|
+
"QQ Bot",
|
|
62
|
+
);
|
|
63
|
+
return params.cfg;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function linkViaManualInput(params: {
|
|
68
|
+
cfg: AutoBotConfig;
|
|
69
|
+
accountId: string;
|
|
70
|
+
prompter: SetupPrompter;
|
|
71
|
+
}): Promise<AutoBotConfig> {
|
|
72
|
+
const appId = await params.prompter.text({
|
|
73
|
+
message: "请输入 QQ Bot AppID",
|
|
74
|
+
validate: (value: string) => (value.trim() ? undefined : "AppID 不能为空"),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const appSecret = await params.prompter.text({
|
|
78
|
+
message: "请输入 QQ Bot AppSecret",
|
|
79
|
+
validate: (value: string) => (value.trim() ? undefined : "AppSecret 不能为空"),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const next = applyQQBotAccountConfig(params.cfg, params.accountId, {
|
|
83
|
+
appId: appId.trim(),
|
|
84
|
+
clientSecret: appSecret.trim(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await params.prompter.note("✔ QQ Bot 配置完成!", "QQ Bot");
|
|
88
|
+
return next;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function finalizeQQBotSetup(params: {
|
|
92
|
+
cfg: AutoBotConfig;
|
|
93
|
+
accountId: string;
|
|
94
|
+
forceAllowFrom: boolean;
|
|
95
|
+
prompter: SetupPrompter;
|
|
96
|
+
runtime: SetupRuntime;
|
|
97
|
+
}): Promise<{ cfg: AutoBotConfig }> {
|
|
98
|
+
const accountId = params.accountId.trim() || DEFAULT_ACCOUNT_ID;
|
|
99
|
+
let next = params.cfg;
|
|
100
|
+
|
|
101
|
+
const configured = isQQBotAccountConfigured(next, accountId);
|
|
102
|
+
|
|
103
|
+
const mode = await params.prompter.select({
|
|
104
|
+
message: configured ? "QQ 已绑定,选择操作" : "选择 QQ 绑定方式",
|
|
105
|
+
options: [
|
|
106
|
+
{
|
|
107
|
+
value: "qr",
|
|
108
|
+
label: "扫码绑定(推荐)",
|
|
109
|
+
hint: "使用 QQ 扫描二维码自动完成绑定",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
value: "manual",
|
|
113
|
+
label: "手动输入 QQ Bot AppID 和 AppSecret",
|
|
114
|
+
hint: "需到 QQ 开放平台 q.qq.com 查看",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
value: "skip",
|
|
118
|
+
label: configured ? "保持当前配置" : "稍后配置",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (mode === "qr") {
|
|
124
|
+
next = await linkViaQrCode({
|
|
125
|
+
cfg: next,
|
|
126
|
+
accountId,
|
|
127
|
+
prompter: params.prompter,
|
|
128
|
+
runtime: params.runtime,
|
|
129
|
+
});
|
|
130
|
+
} else if (mode === "manual") {
|
|
131
|
+
next = await linkViaManualInput({
|
|
132
|
+
cfg: next,
|
|
133
|
+
accountId,
|
|
134
|
+
prompter: params.prompter,
|
|
135
|
+
});
|
|
136
|
+
} else if (!configured) {
|
|
137
|
+
await params.prompter.note(
|
|
138
|
+
["您可以稍后运行以下命令重新选择 QQ Bot 进行配置:", " autobot channels add"].join("\n"),
|
|
139
|
+
"QQ Bot",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { cfg: next };
|
|
144
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createStandardChannelSetupStatus,
|
|
3
|
+
setSetupChannelEnabled,
|
|
4
|
+
} from "autobot/plugin-sdk/setup";
|
|
5
|
+
import type { ChannelSetupWizard } from "autobot/plugin-sdk/setup";
|
|
6
|
+
import { isAccountConfigured } from "../../engine/config/resolve.js";
|
|
7
|
+
import { listQQBotAccountIds, resolveQQBotAccount } from "../config.js";
|
|
8
|
+
import { finalizeQQBotSetup } from "./finalize.js";
|
|
9
|
+
|
|
10
|
+
const channel = "qqbot" as const;
|
|
11
|
+
|
|
12
|
+
export const qqbotSetupWizard: ChannelSetupWizard = {
|
|
13
|
+
channel,
|
|
14
|
+
status: createStandardChannelSetupStatus({
|
|
15
|
+
channelLabel: "QQ Bot",
|
|
16
|
+
configuredLabel: "configured",
|
|
17
|
+
unconfiguredLabel: "needs AppID + AppSercet",
|
|
18
|
+
configuredHint: "configured",
|
|
19
|
+
unconfiguredHint: "needs AppID + AppSercet",
|
|
20
|
+
configuredScore: 1,
|
|
21
|
+
unconfiguredScore: 6,
|
|
22
|
+
resolveConfigured: ({ cfg, accountId }) =>
|
|
23
|
+
(accountId ? [accountId] : listQQBotAccountIds(cfg)).some((resolvedAccountId) => {
|
|
24
|
+
const account = resolveQQBotAccount(cfg, resolvedAccountId, {
|
|
25
|
+
allowUnresolvedSecretRef: true,
|
|
26
|
+
});
|
|
27
|
+
return isAccountConfigured(account as never);
|
|
28
|
+
}),
|
|
29
|
+
}),
|
|
30
|
+
credentials: [],
|
|
31
|
+
finalize: async ({ cfg, accountId, forceAllowFrom, prompter, runtime }) =>
|
|
32
|
+
await finalizeQQBotSetup({ cfg, accountId, forceAllowFrom, prompter, runtime }),
|
|
33
|
+
disable: (cfg) => setSetupChannelEnabled(cfg, channel, false),
|
|
34
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AutoBotPluginApi } from "autobot/plugin-sdk/core";
|
|
2
|
+
import { getAccessToken } from "../../engine/messaging/sender.js";
|
|
3
|
+
import { ChannelApiSchema, executeChannelApi } from "../../engine/tools/channel-api.js";
|
|
4
|
+
import type { ChannelApiParams } from "../../engine/tools/channel-api.js";
|
|
5
|
+
import { listQQBotAccountIds, resolveQQBotAccount } from "../config.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register the QQ channel API proxy tool.
|
|
9
|
+
*
|
|
10
|
+
* The tool acts as an authenticated HTTP proxy for the QQ Open Platform
|
|
11
|
+
* channel APIs. Agents learn endpoint details from the skill docs and
|
|
12
|
+
* send requests through this proxy.
|
|
13
|
+
*/
|
|
14
|
+
export function registerChannelTool(api: AutoBotPluginApi): void {
|
|
15
|
+
const cfg = api.config;
|
|
16
|
+
if (!cfg) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const accountIds = listQQBotAccountIds(cfg);
|
|
21
|
+
if (accountIds.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const firstAccountId = accountIds[0];
|
|
26
|
+
const account = resolveQQBotAccount(cfg, firstAccountId);
|
|
27
|
+
|
|
28
|
+
if (!account.appId || !account.clientSecret) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
api.registerTool(
|
|
33
|
+
{
|
|
34
|
+
name: "qqbot_channel_api",
|
|
35
|
+
label: "QQBot Channel API",
|
|
36
|
+
description:
|
|
37
|
+
"Authenticated HTTP proxy for QQ Open Platform channel APIs. " +
|
|
38
|
+
"Common endpoints: " +
|
|
39
|
+
"list guilds GET /users/@me/guilds | " +
|
|
40
|
+
"list channels GET /guilds/{guild_id}/channels | " +
|
|
41
|
+
"get channel GET /channels/{channel_id} | " +
|
|
42
|
+
"create channel POST /guilds/{guild_id}/channels | " +
|
|
43
|
+
"list members GET /guilds/{guild_id}/members?after=0&limit=100 | " +
|
|
44
|
+
"get member GET /guilds/{guild_id}/members/{user_id} | " +
|
|
45
|
+
"list threads GET /channels/{channel_id}/threads | " +
|
|
46
|
+
"create thread PUT /channels/{channel_id}/threads | " +
|
|
47
|
+
"create announce POST /guilds/{guild_id}/announces | " +
|
|
48
|
+
"create schedule POST /channels/{channel_id}/schedules. " +
|
|
49
|
+
"See the qqbot-channel skill for full endpoint details.",
|
|
50
|
+
parameters: ChannelApiSchema,
|
|
51
|
+
async execute(_toolCallId, params) {
|
|
52
|
+
const accessToken = await getAccessToken(account.appId, account.clientSecret);
|
|
53
|
+
return executeChannelApi(params as ChannelApiParams, { accessToken });
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{ name: "qqbot_channel_api" },
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregate QQBot plugin tool registrations.
|
|
3
|
+
*
|
|
4
|
+
* New tools should be added here rather than in the channel-entry contract
|
|
5
|
+
* file so that the plugin-level `index.ts` stays a pure declaration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AutoBotPluginApi } from "autobot/plugin-sdk/core";
|
|
9
|
+
import { registerChannelTool } from "./channel.js";
|
|
10
|
+
import { registerRemindTool } from "./remind.js";
|
|
11
|
+
|
|
12
|
+
export function registerQQBotTools(api: AutoBotPluginApi): void {
|
|
13
|
+
registerChannelTool(api);
|
|
14
|
+
registerRemindTool(api);
|
|
15
|
+
}
|