@jerryliang122/openclaw-qqbot 1.0.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.
Files changed (120) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +967 -0
  3. package/README.zh.md +790 -0
  4. package/dist/index.cjs +18072 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +1222 -0
  7. package/index.ts +92 -0
  8. package/openclaw.plugin.json +38 -0
  9. package/package.json +69 -0
  10. package/preload.cjs +19 -0
  11. package/scripts/link-sdk-core.cjs +268 -0
  12. package/scripts/proactive-api-server.ts +369 -0
  13. package/scripts/send-proactive.ts +293 -0
  14. package/scripts/test-sendmedia.ts +116 -0
  15. package/skills/qqbot-channel/SKILL.md +285 -0
  16. package/skills/qqbot-channel/references/api_references.md +521 -0
  17. package/skills/qqbot-remind/SKILL.md +159 -0
  18. package/skills/qqbot-upgrade/SKILL.md +56 -0
  19. package/src/adapter/contract.ts +63 -0
  20. package/src/adapter/lint.ts +144 -0
  21. package/src/adapter/media.ts +40 -0
  22. package/src/adapter/pairing.ts +95 -0
  23. package/src/adapter/resolve.ts +255 -0
  24. package/src/adapter/setup.ts +13 -0
  25. package/src/adapter/webhook.ts +248 -0
  26. package/src/adapter/workspace.ts +21 -0
  27. package/src/agent-prompt-adapter.ts +26 -0
  28. package/src/bot-instance.ts +60 -0
  29. package/src/channel.ts +230 -0
  30. package/src/commands/bot-approve.ts +143 -0
  31. package/src/commands/bot-clear-storage.ts +114 -0
  32. package/src/commands/bot-group-always.ts +62 -0
  33. package/src/commands/bot-group-info.ts +48 -0
  34. package/src/commands/bot-help.ts +40 -0
  35. package/src/commands/bot-logs.ts +248 -0
  36. package/src/commands/bot-me.ts +18 -0
  37. package/src/commands/bot-pairing.ts +50 -0
  38. package/src/commands/bot-ping.ts +33 -0
  39. package/src/commands/bot-streaming.ts +55 -0
  40. package/src/commands/bot-upgrade.ts +56 -0
  41. package/src/commands/bot-version.ts +41 -0
  42. package/src/commands/config-util.ts +96 -0
  43. package/src/commands/index.ts +51 -0
  44. package/src/config.ts +403 -0
  45. package/src/constants.ts +6 -0
  46. package/src/dispatch/body-assembler.ts +308 -0
  47. package/src/dispatch/ctx-builder.ts +127 -0
  48. package/src/dispatch/dispatch.ts +667 -0
  49. package/src/dispatch/envelope-builder.ts +112 -0
  50. package/src/dispatch/index.ts +2 -0
  51. package/src/features/approval-capability.ts +302 -0
  52. package/src/features/approval-helpers.ts +271 -0
  53. package/src/features/approval-utils.ts +21 -0
  54. package/src/features/command-panel.ts +301 -0
  55. package/src/features/credential-backup.ts +74 -0
  56. package/src/features/group-mode-store.ts +79 -0
  57. package/src/features/history-store.ts +75 -0
  58. package/src/features/msgid-cache.ts +55 -0
  59. package/src/features/outbound-echo-store.ts +46 -0
  60. package/src/features/proactive-budget.ts +57 -0
  61. package/src/features/proactive.ts +549 -0
  62. package/src/features/question-helpers.ts +771 -0
  63. package/src/features/quota-manager.ts +173 -0
  64. package/src/features/ref-index-store.ts +289 -0
  65. package/src/features/secret-input-store.ts +118 -0
  66. package/src/features/secret-store-cli.ts +324 -0
  67. package/src/features/typing-refresh.ts +51 -0
  68. package/src/features/update-checker.ts +166 -0
  69. package/src/gateway/event-handlers.ts +456 -0
  70. package/src/gateway/index.ts +3 -0
  71. package/src/gateway/lifecycle.ts +236 -0
  72. package/src/gateway/middleware-setup.ts +173 -0
  73. package/src/gateway/qqbot-gateway.ts +458 -0
  74. package/src/gateway-adapter.ts +44 -0
  75. package/src/heartbeat-adapter.ts +57 -0
  76. package/src/message-adapter.ts +40 -0
  77. package/src/messaging-adapter.ts +78 -0
  78. package/src/middleware/access-control.ts +125 -0
  79. package/src/middleware/attachment.ts +373 -0
  80. package/src/middleware/inbound-guard.ts +102 -0
  81. package/src/middleware/policy-injector.ts +71 -0
  82. package/src/middleware/secret-capture.ts +161 -0
  83. package/src/middleware/typing.ts +110 -0
  84. package/src/openclaw-plugin-sdk.d.ts +543 -0
  85. package/src/outbound/chunker.ts +80 -0
  86. package/src/outbound/debounce.ts +102 -0
  87. package/src/outbound/deliver-pipeline.ts +235 -0
  88. package/src/outbound/index.ts +3 -0
  89. package/src/outbound/local-file-router.ts +145 -0
  90. package/src/outbound/media-send.ts +408 -0
  91. package/src/outbound/outbound-service.ts +298 -0
  92. package/src/outbound/reply-limiter.ts +139 -0
  93. package/src/outbound/sanitize.ts +32 -0
  94. package/src/outbound/streaming-controller.ts +332 -0
  95. package/src/outbound/target.ts +109 -0
  96. package/src/outbound-adapter.ts +323 -0
  97. package/src/plugin-base.ts +42 -0
  98. package/src/request-context.ts +50 -0
  99. package/src/runtime.ts +42 -0
  100. package/src/setup/account-key.ts +41 -0
  101. package/src/setup/finalize.ts +110 -0
  102. package/src/setup/login.ts +197 -0
  103. package/src/setup/surface.ts +40 -0
  104. package/src/status-adapter.ts +56 -0
  105. package/src/tools/platform.ts +149 -0
  106. package/src/tools/remind.ts +308 -0
  107. package/src/tools/secret-input.ts +185 -0
  108. package/src/types-augment.d.ts +54 -0
  109. package/src/types-plugin.ts +82 -0
  110. package/src/types.ts +620 -0
  111. package/src/typing-lifecycle.ts +182 -0
  112. package/src/utils/mention.ts +52 -0
  113. package/src/utils/pkg-version.ts +23 -0
  114. package/src/utils/platform.ts +459 -0
  115. package/src/utils/plugin-logger.ts +104 -0
  116. package/src/utils/ssrf-guard.ts +132 -0
  117. package/src/utils/stt.ts +150 -0
  118. package/src/utils/voice-text.ts +61 -0
  119. package/tsconfig.json +17 -0
  120. package/tsup.config.ts +64 -0
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: qqbot-upgrade
3
+ description: 将 openclaw-qqbot 插件(@jerryliang122/openclaw-qqbot)升级到最新版本。当用户要求更新 QQ 机器人插件、升级 qqbot 扩展或同步最新版时使用。
4
+ metadata: {"openclaw":{"emoji":"⬆️","requires":{"config":["channels.qqbot"]}}}
5
+ ---
6
+
7
+ # QQBot 插件升级
8
+
9
+ ## 何时使用
10
+
11
+ 用户表达以下意图时,应在**本机终端**执行升级命令(不要只口头说明「去官网升级」):
12
+
13
+ - 更新 / 升级 `openclaw-qqbot` / QQBot 插件 / QQ 机器人插件
14
+ - 同步本仓库(jerryliang122/qqbot-openclaw)最新版
15
+
16
+ ---
17
+
18
+ ## 标准命令
19
+
20
+ npm 包(推荐):
21
+
22
+ ```bash
23
+ openclaw plugins install @jerryliang122/openclaw-qqbot
24
+ ```
25
+
26
+ 指定版本:
27
+
28
+ ```bash
29
+ openclaw plugins install @jerryliang122/openclaw-qqbot@<version>
30
+ ```
31
+
32
+ 或从 GitHub(无法访问 npm 时):
33
+
34
+ ```bash
35
+ openclaw plugins install git+https://github.com/jerryliang122/qqbot-openclaw.git
36
+ ```
37
+
38
+ 升级完成后重启网关使其生效:
39
+
40
+ ```bash
41
+ openclaw gateway restart
42
+ ```
43
+
44
+ ---
45
+
46
+ ## 前置条件
47
+
48
+ - OpenClaw >= 2026.9.2(低版本框架无法加载本插件)
49
+ - 需 **Node.js / npm** 环境可访问 npm registry(或 GitHub)
50
+ - 从 2.x 旧版本升级:先阅读仓库 CHANGELOG 的 Breaking Changes 与配置迁移表
51
+
52
+ ---
53
+
54
+ ## 执行后
55
+
56
+ 根据命令退出码与终端输出向用户简要汇报:成功则提示新版本号并确认网关已重启;失败则摘录关键错误并提示检查网络、npm 权限与 OpenClaw 版本。
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Runtime Contract Check — 启动时校验必需 API 可用性。
3
+ *
4
+ * 在 plugin.register() 中立刻调用,console 直接输出(register 阶段
5
+ * runtime.logging 的 getChildLogger 存在但输出管道未接通)。
6
+ */
7
+
8
+ import type { PluginRuntime } from 'openclaw/plugin-sdk';
9
+ import { createPluginLogger } from '../utils/plugin-logger.js';
10
+
11
+ const log = createPluginLogger({ prefix: '[contract]', forceConsole: false });
12
+
13
+ interface ApiProbe {
14
+ name: string;
15
+ probe: (rt: PluginRuntime) => boolean;
16
+ }
17
+
18
+ const REQUIRED: ApiProbe[] = [];
19
+
20
+ const OPTIONAL: ApiProbe[] = [
21
+ {
22
+ name: 'channel.reply.dispatchReplyWithBufferedBlockDispatcher',
23
+ probe: (rt) => typeof (rt as any).channel?.reply?.dispatchReplyWithBufferedBlockDispatcher === 'function',
24
+ },
25
+ { name: 'channel.inbound.run (degraded)', probe: (rt) => typeof (rt as any).channel?.inbound?.run === 'function' },
26
+ { name: 'channel.inbound.buildContext', probe: (rt) => typeof (rt as any).channel?.inbound?.buildContext === 'function' },
27
+ { name: 'channel.reply.formatAgentEnvelope', probe: (rt) => typeof (rt as any).channel?.reply?.formatAgentEnvelope === 'function' },
28
+ { name: 'channel.text.chunkMarkdownText', probe: (rt) => typeof (rt as any).channel?.text?.chunkMarkdownText === 'function' },
29
+ { name: 'channel.routing.resolveAgentRoute', probe: (rt) => typeof (rt as any).channel?.routing?.resolveAgentRoute === 'function' },
30
+ { name: 'channel.session.resolveStorePath', probe: (rt) => typeof (rt as any).channel?.session?.resolveStorePath === 'function' },
31
+ { name: 'channel.session.recordInboundSession', probe: (rt) => typeof (rt as any).channel?.session?.recordInboundSession === 'function' },
32
+ { name: 'config.current', probe: (rt) => typeof (rt as any).config?.current === 'function' },
33
+ ];
34
+
35
+ export interface ContractResult {
36
+ ok: boolean;
37
+ version: string;
38
+ missing: string[];
39
+ degraded: string[];
40
+ }
41
+
42
+ export function verifyRuntimeContract(rt: PluginRuntime): ContractResult {
43
+ const version = (rt as any).version ?? 'unknown';
44
+ const missing: string[] = [];
45
+ const degraded: string[] = [];
46
+
47
+ for (const r of REQUIRED) {
48
+ if (!r.probe(rt)) missing.push(r.name);
49
+ }
50
+ for (const r of OPTIONAL) {
51
+ if (!r.probe(rt)) degraded.push(r.name);
52
+ }
53
+
54
+ log.debug(`openclaw=${version} required=${REQUIRED.length - missing.length}/${REQUIRED.length} degraded=${degraded.length}/${OPTIONAL.length}`);
55
+ if (missing.length) {
56
+ log.error(`BROKEN — missing: ${missing.join(', ')}`);
57
+ }
58
+ if (degraded.length) {
59
+ log.debug(`degraded: ${degraded.join(', ')}`);
60
+ }
61
+
62
+ return { ok: missing.length === 0, version, missing, degraded };
63
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Lint: 检测 adapter/ 外部直接访问 runtime.channel.* 的代码
3
+ *
4
+ * 运行方式:
5
+ * npx tsx src/adapter/lint-runtime-access.ts
6
+ *
7
+ * 退出码:
8
+ * 0 = 无违规
9
+ * 1 = 存在未加白的直接 runtime 访问
10
+ *
11
+ * 加白方式(行内注释):
12
+ * // @adapter-bypass: <理由>
13
+ *
14
+ * 示例:
15
+ * const tts = (runtime as any)?.tts; // @adapter-bypass: TTS 非 channel API,无需经过适配层
16
+ */
17
+
18
+ import * as fs from 'node:fs';
19
+ import * as path from 'node:path';
20
+
21
+ // ── 配置 ──
22
+
23
+ const SRC_ROOT = path.resolve(import.meta.dirname, '..');
24
+ const ADAPTER_DIR = path.resolve(SRC_ROOT, 'adapter');
25
+
26
+ /** 匹配直接使用 runtime.channel / runtime.config / (runtime as any) 的模式 */
27
+ const VIOLATION_PATTERNS: RegExp[] = [
28
+ /runtime\.channel\b/,
29
+ /runtime\.config\b/,
30
+ /\(runtime\s+as\s+any\)/,
31
+ /channel\??\.(inbound|turn|reply|session|routing|text|media|runtimeContexts)\b/,
32
+ /config\s+as\s*\{.*writeConfigFile/,
33
+ ];
34
+
35
+ /** 加白注释标记 */
36
+ const BYPASS_MARKER = '@adapter-bypass';
37
+
38
+ /** 排除的目录/文件 */
39
+ const EXCLUDE_DIRS = ['adapter', 'node_modules', 'dist', 'tests'];
40
+ const EXCLUDE_FILES = ['types.ts', 'types-augment.d.ts', 'openclaw-plugin-sdk.d.ts'];
41
+
42
+ // ── 扫描逻辑 ──
43
+
44
+ interface Violation {
45
+ file: string;
46
+ line: number;
47
+ content: string;
48
+ pattern: string;
49
+ }
50
+
51
+ function shouldExclude(filePath: string): boolean {
52
+ const relative = path.relative(SRC_ROOT, filePath);
53
+ // 排除 adapter 自身
54
+ if (filePath.startsWith(ADAPTER_DIR)) return true;
55
+ // 排除配置的目录
56
+ for (const dir of EXCLUDE_DIRS) {
57
+ if (relative.startsWith(dir + '/') || relative === dir) return true;
58
+ }
59
+ // 排除特定文件
60
+ const fileName = path.basename(filePath);
61
+ if (EXCLUDE_FILES.includes(fileName)) return true;
62
+ return false;
63
+ }
64
+
65
+ function scanFile(filePath: string): Violation[] {
66
+ const violations: Violation[] = [];
67
+ const content = fs.readFileSync(filePath, 'utf8');
68
+ const lines = content.split('\n');
69
+
70
+ for (let i = 0; i < lines.length; i++) {
71
+ const line = lines[i];
72
+ const lineNum = i + 1;
73
+
74
+ // 跳过注释行(单行注释、JSDoc 行、多行注释体)
75
+ const trimmed = line.trimStart();
76
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/**') || trimmed.startsWith('*/')) continue;
77
+
78
+ // 检查加白标记
79
+ if (line.includes(BYPASS_MARKER)) continue;
80
+
81
+ // 检查违规模式
82
+ for (const pattern of VIOLATION_PATTERNS) {
83
+ if (pattern.test(line)) {
84
+ violations.push({
85
+ file: path.relative(SRC_ROOT, filePath),
86
+ line: lineNum,
87
+ content: line.trim(),
88
+ pattern: pattern.source,
89
+ });
90
+ break; // 同一行只报一次
91
+ }
92
+ }
93
+ }
94
+
95
+ return violations;
96
+ }
97
+
98
+ function walkDir(dir: string): string[] {
99
+ const results: string[] = [];
100
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
101
+ for (const entry of entries) {
102
+ const fullPath = path.join(dir, entry.name);
103
+ if (entry.isDirectory()) {
104
+ if (!EXCLUDE_DIRS.includes(entry.name)) {
105
+ results.push(...walkDir(fullPath));
106
+ }
107
+ } else if (entry.isFile() && entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
108
+ results.push(fullPath);
109
+ }
110
+ }
111
+ return results;
112
+ }
113
+
114
+ // ── 主流程 ──
115
+
116
+ function main(): void {
117
+ const files = walkDir(SRC_ROOT).filter((f) => !shouldExclude(f));
118
+ const allViolations: Violation[] = [];
119
+
120
+ for (const file of files) {
121
+ const violations = scanFile(file);
122
+ allViolations.push(...violations);
123
+ }
124
+
125
+ if (allViolations.length === 0) {
126
+ console.log('✅ No direct runtime access found outside adapter/');
127
+ process.exit(0);
128
+ }
129
+
130
+ console.error(`❌ Found ${allViolations.length} direct runtime access violation(s):\n`);
131
+ for (const v of allViolations) {
132
+ console.error(` ${v.file}:${v.line}`);
133
+ console.error(` ${v.content}`);
134
+ console.error(` → matched: ${v.pattern}`);
135
+ console.error(` → fix: use adapter or add "// @adapter-bypass: <reason>"\n`);
136
+ }
137
+ console.error(
138
+ `\nTo bypass a specific line, add an inline comment:\n` +
139
+ ` // @adapter-bypass: <reason why this cannot go through the adapter>\n`,
140
+ );
141
+ process.exit(1);
142
+ }
143
+
144
+ main();
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 远程媒体下载(plugin-sdk/media-runtime)
3
+ *
4
+ * 直接走 openclaw 的 saveRemoteMedia(含 SSRF 防护/重试/大小限制)。
5
+ */
6
+ import { createRequire } from 'node:module';
7
+ import * as path from 'node:path';
8
+
9
+ let _req: NodeRequire | undefined;
10
+
11
+ /**
12
+ * 惰性 require 工厂。
13
+ * CJS(tsup 产物)下用 __filename 锚定,与插件 node_modules 的解析路径一致;
14
+ * ESM(tsx 直跑测试)下 __filename 不存在,锚定 process.cwd()。
15
+ */
16
+ function getReq(): NodeRequire {
17
+ _req ??= createRequire(
18
+ typeof __filename !== 'undefined' ? __filename : path.join(process.cwd(), 'noop.js'),
19
+ );
20
+ return _req;
21
+ }
22
+
23
+ type SaveRemoteMedia = (opts: {
24
+ url: string;
25
+ subdir?: string;
26
+ originalFilename?: string;
27
+ maxBytes?: number;
28
+ timeoutMs?: number;
29
+ }) => Promise<{ path: string }>;
30
+
31
+ export function downloadRemoteMedia(opts: {
32
+ url: string;
33
+ subdir?: string;
34
+ originalFilename?: string;
35
+ maxBytes?: number;
36
+ timeoutMs?: number;
37
+ }): Promise<{ path: string }> {
38
+ const mod = getReq()('openclaw/plugin-sdk/media-runtime') as { saveRemoteMedia: SaveRemoteMedia };
39
+ return mod.saveRemoteMedia(opts);
40
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Pairing Runtime — openclaw/plugin-sdk/conversation-runtime
3
+ *
4
+ * readAllowFromStore / issueChallenge / buildReply 直接来自稳定 subpath。
5
+ *
6
+ * approveCode 例外:`approveChannelPairingCode` 在 2026.9.2 未从任何
7
+ * plugin-sdk subpath 导出(只存在于内部 pairing-store chunk 与
8
+ * `openclaw pairing approve` CLI),走与网关同源的 CLI 执行
9
+ * (见 secret-store-cli 的 resolveOpenClawCli 同源约束)。
10
+ */
11
+ import { spawn } from 'node:child_process';
12
+ import { createRequire } from 'node:module';
13
+ import * as path from 'node:path';
14
+ import { resolveOpenClawCli } from '../features/secret-store-cli.js';
15
+
16
+ export interface PairingApi {
17
+ readAllowFromStore: (params: { channel: string; accountId: string }) => Promise<string[]>;
18
+ issueChallenge: (params: { channel: string; id: string; accountId: string }) => Promise<{ code: string }>;
19
+ buildReply: (params: { code: string; channel: string }) => string;
20
+ /** 经 `openclaw pairing approve` 执行;返回是否成功 */
21
+ approveCode: (params: { channel: string; code: string; accountId?: string }) => Promise<{ approved: boolean }>;
22
+ }
23
+
24
+ let _req: NodeRequire | undefined;
25
+
26
+ function getReq(): NodeRequire {
27
+ _req ??= createRequire(
28
+ typeof __filename !== 'undefined' ? __filename : path.join(process.cwd(), 'noop.js'),
29
+ );
30
+ return _req;
31
+ }
32
+
33
+ let _mod: {
34
+ readChannelAllowFromStore: (channel: string, env?: unknown, accountId?: string) => Promise<string[]>;
35
+ upsertChannelPairingRequest: (params: { channel: string; id: string; accountId?: string }) => Promise<{ code: string }>;
36
+ buildPairingReply: (params: { channel: string; idLine?: string; code: string }) => string;
37
+ } | undefined;
38
+
39
+ function loadMod(): NonNullable<typeof _mod> {
40
+ _mod ??= getReq()('openclaw/plugin-sdk/conversation-runtime');
41
+ return _mod!;
42
+ }
43
+
44
+ /** 获取 Pairing API(conversation-runtime 为稳定导出,加载失败即抛错) */
45
+ export function getPairingApi(): PairingApi {
46
+ const mod = loadMod();
47
+ return {
48
+ readAllowFromStore: (params) =>
49
+ mod.readChannelAllowFromStore(params.channel, undefined, params.accountId),
50
+ issueChallenge: (params) =>
51
+ mod.upsertChannelPairingRequest({
52
+ channel: params.channel,
53
+ id: params.id,
54
+ accountId: params.accountId,
55
+ }).then((r) => ({ code: r.code })),
56
+ buildReply: (params) =>
57
+ mod.buildPairingReply({
58
+ channel: params.channel,
59
+ idLine: '', // qqbot 无额外 ID 信息,留空即可
60
+ code: params.code,
61
+ }),
62
+ approveCode: (params) => approveViaCli(params),
63
+ };
64
+ }
65
+
66
+ const APPROVE_TIMEOUT_MS = 30_000;
67
+
68
+ async function approveViaCli(params: {
69
+ channel: string;
70
+ code: string;
71
+ accountId?: string;
72
+ }): Promise<{ approved: boolean }> {
73
+ const cli = resolveOpenClawCli();
74
+ const args = [
75
+ ...cli.args,
76
+ 'pairing',
77
+ 'approve',
78
+ '--channel',
79
+ params.channel,
80
+ ...(params.accountId ? ['--account', params.accountId] : []),
81
+ params.code,
82
+ ];
83
+ return new Promise((resolve) => {
84
+ const child = spawn(cli.cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
85
+ const timer = setTimeout(() => child.kill('SIGKILL'), APPROVE_TIMEOUT_MS);
86
+ child.on('error', () => {
87
+ clearTimeout(timer);
88
+ resolve({ approved: false });
89
+ });
90
+ child.on('close', (codeNum) => {
91
+ clearTimeout(timer);
92
+ resolve({ approved: codeNum === 0 });
93
+ });
94
+ });
95
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Runtime Adapters — 一次 resolve,全程复用。
3
+ *
4
+ * Capability Probe 模式:按候选 API 路径探测,选出第一个可用函数。
5
+ * 构建基线为 openclaw 2026.9.2(peer >=2026.9.2),不再保留对更旧版本的回退。
6
+ *
7
+ * 调用方通过 `resolveRuntimeAdapters(runtime)` 获取适配层对象,
8
+ * 后续所有 dispatch / channel 代码只使用 adapters 上的方法。
9
+ */
10
+
11
+ import type { PluginRuntime } from 'openclaw/plugin-sdk';
12
+ import type { PluginLogger } from '../utils/plugin-logger.js';
13
+
14
+ // ── 类型 ──
15
+
16
+ export interface RuntimeAdapters {
17
+ /** 入站事件处理 */
18
+ inboundRun: ((params: any) => Promise<any>) | null;
19
+ /** 回复分发(带 block buffer) */
20
+ dispatchReply: ((params: any) => Promise<any>) | null;
21
+ /** Agent 路由解析 */
22
+ resolveAgentRoute: ((params: any) => any) | null;
23
+ /** 构建入站上下文 */
24
+ buildInboundContext: ((params: any) => any) | null;
25
+ /** Session store path 解析 */
26
+ resolveStorePath: ((storeConfig: any, opts: any) => string) | null;
27
+ /** Session 记录 */
28
+ recordInboundSession: ((params: any) => Promise<void>) | null;
29
+ /** 格式化 envelope */
30
+ formatEnvelope: ((params: any) => string) | null;
31
+ /** 解析 envelope format options */
32
+ resolveEnvelopeFormatOptions: ((cfg: any) => any) | null;
33
+ /** Markdown 文本分块 */
34
+ chunkMarkdownText: ((text: string, limit: number) => string[]) | null;
35
+ /** 远程媒体保存(图片/语音下载) */
36
+ saveRemoteMedia: ((opts: { url: string; subdir?: string; originalFilename?: string }) => Promise<{ path: string } | null>) | null;
37
+ /** 获取当前配置快照 */
38
+ getConfig: (() => any) | null;
39
+ /**
40
+ * 持久化配置变更。
41
+ *
42
+ * 接受一个 mutator 回调,接收当前 config 对象(可变),
43
+ * 可直接修改或返回新对象。适配器负责确保写入和热重载。
44
+ */
45
+ persistConfig: ((mutator: (cfg: any) => any) => Promise<void>) | null;
46
+ /** openclaw 版本 */
47
+ version: string;
48
+ }
49
+
50
+ // ── Probe 辅助 ──
51
+
52
+ type ProbePath = string[];
53
+
54
+ /**
55
+ * 从 runtime 对象沿路径探测函数。返回 bound function 或 null。
56
+ */
57
+ function probeFunction(rt: any, paths: ProbePath[]): ((...args: any[]) => any) | null {
58
+ for (const path of paths) {
59
+ let target = rt;
60
+ let parent = rt;
61
+ for (let i = 0; i < path.length; i++) {
62
+ parent = target;
63
+ target = target?.[path[i]];
64
+ if (target === undefined || target === null) break;
65
+ }
66
+ if (typeof target === 'function') {
67
+ // bind 到 parent(倒数第二层),确保 this 上下文正确
68
+ return target.bind(parent);
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * 从 runtime 对象沿路径探测值(非函数也可)。返回值或 null。
76
+ */
77
+ function probeValue(rt: any, paths: ProbePath[]): any {
78
+ for (const path of paths) {
79
+ let target = rt;
80
+ for (const key of path) {
81
+ target = target?.[key];
82
+ if (target === undefined || target === null) break;
83
+ }
84
+ if (target !== undefined && target !== null) return target;
85
+ }
86
+ return null;
87
+ }
88
+
89
+ // ── 主入口 ──
90
+
91
+ /**
92
+ * 一次性 resolve 所有 runtime adapter。
93
+ *
94
+ * 通常在 gateway ready 后(首条消息到来前)调用一次,结果缓存复用。
95
+ * 返回 null 的字段表示该能力不可用,调用方按需降级或走 fallback。
96
+ */
97
+ export function resolveRuntimeAdapters(
98
+ rt: PluginRuntime,
99
+ log?: PluginLogger,
100
+ ): RuntimeAdapters {
101
+ const version = (rt as any).version ?? 'unknown';
102
+
103
+ const inboundRun = probeFunction(rt, [
104
+ ['channel', 'inbound', 'run'],
105
+ ]);
106
+
107
+ const dispatchReply = probeFunction(rt, [
108
+ ['channel', 'reply', 'dispatchReplyWithBufferedBlockDispatcher'],
109
+ ]);
110
+
111
+ const resolveAgentRoute = probeFunction(rt, [
112
+ ['channel', 'routing', 'resolveAgentRoute'],
113
+ ]);
114
+
115
+ const rawBuildContext = probeFunction(rt, [
116
+ ['channel', 'inbound', 'buildContext'],
117
+ ]);
118
+
119
+ const buildInboundContext: RuntimeAdapters['buildInboundContext'] = rawBuildContext
120
+ ? (params) => rawBuildContext(params)
121
+ : null;
122
+
123
+ const resolveStorePath = probeFunction(rt, [
124
+ ['channel', 'session', 'resolveStorePath'],
125
+ ]);
126
+
127
+ const recordInboundSession = probeFunction(rt, [
128
+ ['channel', 'session', 'recordInboundSession'],
129
+ ]);
130
+
131
+ const formatEnvelope = probeFunction(rt, [
132
+ ['channel', 'reply', 'formatAgentEnvelope'],
133
+ ]);
134
+
135
+ const resolveEnvelopeFormatOptions = probeFunction(rt, [
136
+ ['channel', 'reply', 'resolveEnvelopeFormatOptions'],
137
+ ]);
138
+
139
+ const chunkMarkdownText = probeFunction(rt, [
140
+ ['channel', 'text', 'chunkMarkdownText'],
141
+ ]);
142
+
143
+ const saveRemoteMedia = probeFunction(rt, [
144
+ ['channel', 'media', 'saveRemoteMedia'],
145
+ ]);
146
+
147
+ const getConfig = probeFunction(rt, [
148
+ ['config', 'current'],
149
+ ]);
150
+
151
+ const rawMutateConfig = probeFunction(rt, [['config', 'mutateConfigFile']]);
152
+
153
+ const persistConfig: RuntimeAdapters['persistConfig'] = rawMutateConfig
154
+ ? async (mutator: (cfg: any) => any) => {
155
+ await rawMutateConfig({
156
+ afterWrite: 'hot-reload',
157
+ mutate: mutator,
158
+ });
159
+ }
160
+ : null;
161
+
162
+ // 日志汇总
163
+ const resolved = [
164
+ inboundRun && 'inboundRun',
165
+ dispatchReply && 'dispatchReply',
166
+ resolveAgentRoute && 'resolveAgentRoute',
167
+ buildInboundContext && 'buildInboundContext',
168
+ resolveStorePath && 'resolveStorePath',
169
+ recordInboundSession && 'recordInboundSession',
170
+ formatEnvelope && 'formatEnvelope',
171
+ chunkMarkdownText && 'chunkMarkdownText',
172
+ saveRemoteMedia && 'saveRemoteMedia',
173
+ getConfig && 'getConfig',
174
+ persistConfig && 'persistConfig',
175
+ ].filter(Boolean);
176
+
177
+ log?.info(
178
+ `[qqbot:adapter] openclaw=${version} resolved ${resolved.length} adapters: ${resolved.join(', ')}`,
179
+ );
180
+
181
+ return {
182
+ inboundRun,
183
+ dispatchReply,
184
+ resolveAgentRoute,
185
+ buildInboundContext,
186
+ resolveStorePath,
187
+ recordInboundSession,
188
+ formatEnvelope,
189
+ resolveEnvelopeFormatOptions,
190
+ chunkMarkdownText,
191
+ saveRemoteMedia,
192
+ getConfig,
193
+ persistConfig,
194
+ version,
195
+ };
196
+ }
197
+
198
+ // ── 全局缓存(所有消费者共享同一份 adapters) ──
199
+
200
+ let _cachedAdapters: RuntimeAdapters | null = null;
201
+ let _cachedRuntimeRef: WeakRef<PluginRuntime> | null = null;
202
+
203
+ /**
204
+ * 获取 RuntimeAdapters 的全局缓存版本。
205
+ *
206
+ * - 自动基于 runtime 实例的 WeakRef 判断是否需要重新 resolve
207
+ * - 如果 runtime 被替换(框架热更新、二次 register),自动刷新
208
+ * - 所有消费者(dispatch / channel / middleware)共用一份,避免重复 probe
209
+ *
210
+ * @param rt 当前 PluginRuntime 实例
211
+ * @param log 可选日志(仅在首次 resolve 或 runtime 变更时输出)
212
+ */
213
+ export function getAdapters(
214
+ rt: PluginRuntime,
215
+ log?: PluginLogger,
216
+ ): RuntimeAdapters {
217
+ const cached = _cachedRuntimeRef?.deref();
218
+ if (cached === rt && _cachedAdapters) {
219
+ return _cachedAdapters;
220
+ }
221
+ // runtime 引用变化(首次 / 热更新)→ 重新 resolve
222
+ _cachedAdapters = resolveRuntimeAdapters(rt, log);
223
+ _cachedRuntimeRef = new WeakRef(rt);
224
+ return _cachedAdapters;
225
+ }
226
+
227
+ /**
228
+ * 持久化配置(auth.login 场景)。
229
+ *
230
+ * 走 config.mutateConfigFile(原子操作,支持 afterWrite 策略)。
231
+ */
232
+ export async function persistAuthConfig(
233
+ runtime: Record<string, unknown>,
234
+ cfg: Record<string, unknown>,
235
+ afterWrite = 'restart',
236
+ ): Promise<void> {
237
+ const config: Record<string, unknown> | undefined = runtime.config as any;
238
+
239
+ if (typeof config?.mutateConfigFile !== 'function') {
240
+ throw new Error('persistAuthConfig: runtime.config.mutateConfigFile is unavailable (requires openclaw >= 2026.9.2)');
241
+ }
242
+ await (config.mutateConfigFile as Function)({
243
+ mutate: () => cfg,
244
+ afterWrite,
245
+ });
246
+ }
247
+
248
+ /**
249
+ * 强制清除 adapter 缓存(仅测试用)
250
+ */
251
+ export function _resetAdaptersCache(): void {
252
+ _cachedAdapters = null;
253
+ _cachedRuntimeRef = null;
254
+ }
255
+
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Setup 工具 re-export(plugin-sdk/setup + plugin-sdk/setup-tools)
3
+ *
4
+ * 构建基线 openclaw 2026.9.2,两个 subpath 均为正式导出,直接转发。
5
+ */
6
+ export type { ChannelSetupWizard } from 'openclaw/plugin-sdk/setup';
7
+
8
+ export {
9
+ createStandardChannelSetupStatus,
10
+ setSetupChannelEnabled,
11
+ } from 'openclaw/plugin-sdk/setup';
12
+
13
+ export { formatDocsLink } from 'openclaw/plugin-sdk/setup-tools';