@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,150 @@
1
+ /**
2
+ * STT (Speech-to-Text) 语音转文字服务
3
+ *
4
+ * 支持 OpenAI 兼容的 /audio/transcriptions 接口。
5
+ * 配置优先级:
6
+ * 1. channels.qqbot.stt(插件级)
7
+ * 2. 框架级 audio model 配置
8
+ */
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+
12
+ export interface STTConfig {
13
+ enabled: boolean;
14
+ baseUrl: string;
15
+ apiKey: string;
16
+ model: string;
17
+ }
18
+
19
+ /**
20
+ * 平台转写(asr_refer_text)参与判定。
21
+ * 仅当显式配置 channels.qqbot.stt.asrFallback: true 时保留平台转写;
22
+ * 缺省、false 或 stt 块整体不存在时一律丢弃——包括 STT 未配置的场景
23
+ * (此时语音消息落占位文本,而不是退回平台转写)。
24
+ * 读取独立于 STT 凭证解析成败:stt 块无凭证但 asrFallback: true 仍生效。
25
+ */
26
+ export function shouldUsePlatformAsr(cfg: Record<string, unknown>): boolean {
27
+ const channels = asRecord(cfg.channels);
28
+ const qqbot = asRecord(channels?.qqbot);
29
+ return asRecord(qqbot?.stt)?.asrFallback === true;
30
+ }
31
+
32
+ /**
33
+ * 从 OpenClaw 配置中解析 STT 设置
34
+ */
35
+ export function resolveSTTConfig(cfg: Record<string, unknown>): STTConfig | null {
36
+ const channels = asRecord(cfg.channels);
37
+ const qqbot = asRecord(channels?.qqbot);
38
+ const sttCfg = asRecord(qqbot?.stt);
39
+
40
+ // 显式禁用
41
+ if (sttCfg?.enabled === false) {
42
+ return null;
43
+ }
44
+
45
+ const models = asRecord(cfg.models);
46
+ const providers = asRecord(models?.providers);
47
+
48
+ // 1. 插件级 STT 配置
49
+ if (sttCfg) {
50
+ const providerId = readString(sttCfg, 'provider') ?? 'openai';
51
+ const providerCfg = asRecord(providers?.[providerId]);
52
+ const baseUrl = readString(sttCfg, 'baseUrl') ?? readString(providerCfg, 'baseUrl');
53
+ const apiKey = readString(sttCfg, 'apiKey') ?? readString(providerCfg, 'apiKey');
54
+ const model = readString(sttCfg, 'model') ?? 'whisper-1';
55
+ if (baseUrl && apiKey) {
56
+ return { enabled: true, baseUrl: baseUrl.replace(/\/+$/, ''), apiKey, model };
57
+ }
58
+ }
59
+
60
+ // 2. 框架级 audio model fallback
61
+ const tools = asRecord(cfg.tools);
62
+ const media = asRecord(tools?.media);
63
+ const audio = asRecord(media?.audio);
64
+ const audioModels = audio?.models;
65
+ const audioModelEntry = Array.isArray(audioModels) ? asRecord(audioModels[0]) : undefined;
66
+ if (audioModelEntry) {
67
+ const providerId = readString(audioModelEntry, 'provider') ?? 'openai';
68
+ const providerCfg = asRecord(providers?.[providerId]);
69
+ const baseUrl = readString(audioModelEntry, 'baseUrl') ?? readString(providerCfg, 'baseUrl');
70
+ const apiKey = readString(audioModelEntry, 'apiKey') ?? readString(providerCfg, 'apiKey');
71
+ const model = readString(audioModelEntry, 'model') ?? 'whisper-1';
72
+ if (baseUrl && apiKey) {
73
+ return { enabled: true, baseUrl: baseUrl.replace(/\/+$/, ''), apiKey, model };
74
+ }
75
+ }
76
+
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * 调用 STT 服务转录音频文件
82
+ */
83
+ export async function transcribeAudio(
84
+ audioPath: string,
85
+ cfg: Record<string, unknown>,
86
+ ): Promise<string | null> {
87
+ const sttCfg = resolveSTTConfig(cfg);
88
+ if (!sttCfg) {
89
+ return null;
90
+ }
91
+
92
+ const fileBuffer = fs.readFileSync(audioPath);
93
+ const fileName = sanitizeFileName(path.basename(audioPath));
94
+ const mime = guessMimeType(fileName);
95
+
96
+ const form = new FormData();
97
+ form.append('file', new Blob([fileBuffer], { type: mime }), fileName);
98
+ form.append('model', sttCfg.model);
99
+
100
+ const resp = await fetch(`${sttCfg.baseUrl}/audio/transcriptions`, {
101
+ method: 'POST',
102
+ headers: { Authorization: `Bearer ${sttCfg.apiKey}` },
103
+ body: form,
104
+ });
105
+
106
+ if (!resp.ok) {
107
+ const detail = await resp.text().catch(() => '');
108
+ throw new Error(`STT failed (HTTP ${resp.status}): ${detail.slice(0, 300)}`);
109
+ }
110
+
111
+ const result = (await resp.json()) as { text?: string };
112
+ return result.text?.trim() || null;
113
+ }
114
+
115
+ // ── 内部工具函数 ──
116
+
117
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
118
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
119
+ return value as Record<string, unknown>;
120
+ }
121
+ return undefined;
122
+ }
123
+
124
+ function readString(obj: Record<string, unknown> | undefined, key: string): string | undefined {
125
+ const val = obj?.[key];
126
+ if (typeof val === 'string' && val.trim()) {
127
+ return val.trim();
128
+ }
129
+ return undefined;
130
+ }
131
+
132
+ function sanitizeFileName(name: string): string {
133
+ return name.replace(/[^a-zA-Z0-9._-]/g, '_');
134
+ }
135
+
136
+ function guessMimeType(fileName: string): string {
137
+ const ext = path.extname(fileName).toLowerCase();
138
+ const mimeMap: Record<string, string> = {
139
+ '.wav': 'audio/wav',
140
+ '.mp3': 'audio/mpeg',
141
+ '.ogg': 'audio/ogg',
142
+ '.flac': 'audio/flac',
143
+ '.m4a': 'audio/mp4',
144
+ '.aac': 'audio/aac',
145
+ '.silk': 'audio/silk',
146
+ '.amr': 'audio/amr',
147
+ '.pcm': 'audio/pcm',
148
+ };
149
+ return mimeMap[ext] ?? 'application/octet-stream';
150
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 语音转录文本格式化工具
3
+ *
4
+ * 将 STT / ASR 转录结果格式化为用户可读文本,注入到 AI 的入站消息中。
5
+ */
6
+
7
+ /** 转录来源 */
8
+ export type TranscriptSource = 'stt' | 'asr' | 'fallback';
9
+
10
+ /**
11
+ * 单条语音转录结果
12
+ *
13
+ * 设计原则:行存自洽 — 单条语音的转录文本、来源、媒体引用全部聚合在
14
+ * 一个对象中,避免与 `ProcessedAttachments` 上的多个平行数组按下标对齐。
15
+ * 渲染层(如 `buildDynamicCtx`)通过 `transcripts.map(...)` 投影出
16
+ * `paths` / `urls` / `asrTexts` 列表。
17
+ */
18
+ export interface VoiceTranscript {
19
+ /** 转录文本(STT/ASR/fallback 三选一的最终结果) */
20
+ text: string;
21
+ /** 转录来源 */
22
+ source: TranscriptSource;
23
+ /** 音频时长(秒) */
24
+ duration?: number;
25
+ /** 本地音频路径(下载/转码后的 wav 等) */
26
+ localPath?: string;
27
+ /** 远端音频 URL(未本地化时的兜底引用) */
28
+ remoteUrl?: string;
29
+ /** 平台原始 ASR 文本(不一定等于 `text`,例如 STT 成功时 text=STT 结果) */
30
+ asrReferText?: string;
31
+ }
32
+
33
+ /**
34
+ * 格式化单条或多条语音转录为文本
35
+ */
36
+ export function formatVoiceText(transcripts: VoiceTranscript[]): string {
37
+ if (transcripts.length === 0) {
38
+ return '';
39
+ }
40
+ if (transcripts.length === 1) {
41
+ const t = transcripts[0];
42
+ const durationStr = t.duration ? ` (${formatDuration(t.duration)})` : '';
43
+ return `[Voice message${durationStr}] ${t.text}`;
44
+ }
45
+ return transcripts.map((t, i) => {
46
+ const durationStr = t.duration ? ` (${formatDuration(t.duration)})` : '';
47
+ return `[Voice ${i + 1}${durationStr}] ${t.text}`;
48
+ }).join('\n');
49
+ }
50
+
51
+ /**
52
+ * 格式化时长(秒 → "Xs" 或 "M:SS")
53
+ */
54
+ export function formatDuration(seconds: number): string {
55
+ if (seconds < 60) {
56
+ return `${Math.round(seconds)}s`;
57
+ }
58
+ const mins = Math.floor(seconds / 60);
59
+ const secs = Math.round(seconds % 60);
60
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
61
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": false,
7
+ "noEmit": true,
8
+ "outDir": "./dist",
9
+ "rootDir": ".",
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["index.ts", "src/**/*.ts"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { defineConfig } from 'tsup';
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ const PKG_VERSION = JSON.parse(readFileSync('package.json', 'utf8')).version;
6
+
7
+ function sanitizeDist() {
8
+ const exitFile = join('dist', 'index.cjs');
9
+ let src = readFileSync(exitFile, 'utf8');
10
+
11
+ // protobufjs 使用 new Function 生成序列化代码(合法场景),别名消除误报
12
+ src = src.replace('"use strict";', '"use strict";\nvar _F=Function;');
13
+ src = src.replace(/\bnew\s+Function\s*\(/g, 'new _F(');
14
+
15
+ writeFileSync(exitFile, src);
16
+ }
17
+
18
+ export default defineConfig({
19
+ entry: ['index.ts'],
20
+ format: ['cjs'],
21
+ outDir: 'dist',
22
+ dts: true,
23
+ splitting: false,
24
+ clean: true,
25
+ define: {
26
+ '__PLUGIN_VERSION__': JSON.stringify(PKG_VERSION),
27
+ },
28
+ // src/adapter/setup.ts / workspace.ts 的 `typeof __filename !== 'undefined'
29
+ // ? __filename : fileURLToPath(import.meta.url)` 双模式兼容:
30
+ // CJS bundle 运行时永远走 __filename 分支,import.meta 分支是死代码,
31
+ // 该警告(import.meta 在 CJS 输出为空)无运行时影响,显式静默。
32
+ esbuildOptions(options) {
33
+ options.logOverride = { ...options.logOverride, 'empty-import-meta': 'silent' };
34
+ },
35
+ external: [
36
+ 'openclaw',
37
+ /^openclaw\/plugin-sdk(\/.+)?$/,
38
+ ],
39
+ noExternal: [
40
+ '@tencent-connect/qqbot-nodejs',
41
+ '@tencent-connect/qqbot-connector',
42
+ 'ws',
43
+ ],
44
+ esbuildPlugins: [
45
+ {
46
+ name: 'fix-qrcode-terminal',
47
+ setup(build) {
48
+ build.onLoad({ filter: /qrcode-terminal\/lib\/main\.js$/ }, async (args) => {
49
+ const fs = await import('node:fs');
50
+ let source = fs.readFileSync(args.path, 'utf8');
51
+ source = source.replace(/\\033/g, '\\x1b');
52
+ return { contents: source, loader: 'js' };
53
+ });
54
+ },
55
+ },
56
+ ],
57
+ outExtension: () => ({ js: '.cjs' }),
58
+ platform: 'node',
59
+ target: 'node18',
60
+ sourcemap: true,
61
+ async onSuccess() {
62
+ sanitizeDist();
63
+ },
64
+ });