@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.
- package/LICENSE +22 -0
- package/README.md +967 -0
- package/README.zh.md +790 -0
- package/dist/index.cjs +18072 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1222 -0
- package/index.ts +92 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +69 -0
- package/preload.cjs +19 -0
- package/scripts/link-sdk-core.cjs +268 -0
- package/scripts/proactive-api-server.ts +369 -0
- package/scripts/send-proactive.ts +293 -0
- package/scripts/test-sendmedia.ts +116 -0
- package/skills/qqbot-channel/SKILL.md +285 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-remind/SKILL.md +159 -0
- package/skills/qqbot-upgrade/SKILL.md +56 -0
- package/src/adapter/contract.ts +63 -0
- package/src/adapter/lint.ts +144 -0
- package/src/adapter/media.ts +40 -0
- package/src/adapter/pairing.ts +95 -0
- package/src/adapter/resolve.ts +255 -0
- package/src/adapter/setup.ts +13 -0
- package/src/adapter/webhook.ts +248 -0
- package/src/adapter/workspace.ts +21 -0
- package/src/agent-prompt-adapter.ts +26 -0
- package/src/bot-instance.ts +60 -0
- package/src/channel.ts +230 -0
- package/src/commands/bot-approve.ts +143 -0
- package/src/commands/bot-clear-storage.ts +114 -0
- package/src/commands/bot-group-always.ts +62 -0
- package/src/commands/bot-group-info.ts +48 -0
- package/src/commands/bot-help.ts +40 -0
- package/src/commands/bot-logs.ts +248 -0
- package/src/commands/bot-me.ts +18 -0
- package/src/commands/bot-pairing.ts +50 -0
- package/src/commands/bot-ping.ts +33 -0
- package/src/commands/bot-streaming.ts +55 -0
- package/src/commands/bot-upgrade.ts +56 -0
- package/src/commands/bot-version.ts +41 -0
- package/src/commands/config-util.ts +96 -0
- package/src/commands/index.ts +51 -0
- package/src/config.ts +403 -0
- package/src/constants.ts +6 -0
- package/src/dispatch/body-assembler.ts +308 -0
- package/src/dispatch/ctx-builder.ts +127 -0
- package/src/dispatch/dispatch.ts +667 -0
- package/src/dispatch/envelope-builder.ts +112 -0
- package/src/dispatch/index.ts +2 -0
- package/src/features/approval-capability.ts +302 -0
- package/src/features/approval-helpers.ts +271 -0
- package/src/features/approval-utils.ts +21 -0
- package/src/features/command-panel.ts +301 -0
- package/src/features/credential-backup.ts +74 -0
- package/src/features/group-mode-store.ts +79 -0
- package/src/features/history-store.ts +75 -0
- package/src/features/msgid-cache.ts +55 -0
- package/src/features/outbound-echo-store.ts +46 -0
- package/src/features/proactive-budget.ts +57 -0
- package/src/features/proactive.ts +549 -0
- package/src/features/question-helpers.ts +771 -0
- package/src/features/quota-manager.ts +173 -0
- package/src/features/ref-index-store.ts +289 -0
- package/src/features/secret-input-store.ts +118 -0
- package/src/features/secret-store-cli.ts +324 -0
- package/src/features/typing-refresh.ts +51 -0
- package/src/features/update-checker.ts +166 -0
- package/src/gateway/event-handlers.ts +456 -0
- package/src/gateway/index.ts +3 -0
- package/src/gateway/lifecycle.ts +236 -0
- package/src/gateway/middleware-setup.ts +173 -0
- package/src/gateway/qqbot-gateway.ts +458 -0
- package/src/gateway-adapter.ts +44 -0
- package/src/heartbeat-adapter.ts +57 -0
- package/src/message-adapter.ts +40 -0
- package/src/messaging-adapter.ts +78 -0
- package/src/middleware/access-control.ts +125 -0
- package/src/middleware/attachment.ts +373 -0
- package/src/middleware/inbound-guard.ts +102 -0
- package/src/middleware/policy-injector.ts +71 -0
- package/src/middleware/secret-capture.ts +161 -0
- package/src/middleware/typing.ts +110 -0
- package/src/openclaw-plugin-sdk.d.ts +543 -0
- package/src/outbound/chunker.ts +80 -0
- package/src/outbound/debounce.ts +102 -0
- package/src/outbound/deliver-pipeline.ts +235 -0
- package/src/outbound/index.ts +3 -0
- package/src/outbound/local-file-router.ts +145 -0
- package/src/outbound/media-send.ts +408 -0
- package/src/outbound/outbound-service.ts +298 -0
- package/src/outbound/reply-limiter.ts +139 -0
- package/src/outbound/sanitize.ts +32 -0
- package/src/outbound/streaming-controller.ts +332 -0
- package/src/outbound/target.ts +109 -0
- package/src/outbound-adapter.ts +323 -0
- package/src/plugin-base.ts +42 -0
- package/src/request-context.ts +50 -0
- package/src/runtime.ts +42 -0
- package/src/setup/account-key.ts +41 -0
- package/src/setup/finalize.ts +110 -0
- package/src/setup/login.ts +197 -0
- package/src/setup/surface.ts +40 -0
- package/src/status-adapter.ts +56 -0
- package/src/tools/platform.ts +149 -0
- package/src/tools/remind.ts +308 -0
- package/src/tools/secret-input.ts +185 -0
- package/src/types-augment.d.ts +54 -0
- package/src/types-plugin.ts +82 -0
- package/src/types.ts +620 -0
- package/src/typing-lifecycle.ts +182 -0
- package/src/utils/mention.ts +52 -0
- package/src/utils/pkg-version.ts +23 -0
- package/src/utils/platform.ts +459 -0
- package/src/utils/plugin-logger.ts +104 -0
- package/src/utils/ssrf-guard.ts +132 -0
- package/src/utils/stt.ts +150 -0
- package/src/utils/voice-text.ts +61 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +64 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Body 组装器(消息入站 body 组装)
|
|
3
|
+
*
|
|
4
|
+
* SDK 中间件链已经完成了所有预处理:
|
|
5
|
+
* - ctx.message.content ← contentSanitizer(face 解析 + mention 清洗)
|
|
6
|
+
* - ctx.state.quote ← quoteRef
|
|
7
|
+
* - ctx.state.history ← historyBuffer
|
|
8
|
+
* - ctx.state.mention ← mentionGate
|
|
9
|
+
* - ctx.state.processedAttachments ← attachmentProcessor(语音 STT、图片 URL)
|
|
10
|
+
*
|
|
11
|
+
* 本模块仅负责"按框架协议把上述上下文拼成最终字符串",是纯函数。
|
|
12
|
+
*
|
|
13
|
+
* 输出字段语义(与框架 buildCtxPayload 语义一致):
|
|
14
|
+
* - webBody → ctxPayload.Body (Web UI 展示)
|
|
15
|
+
* - agentBody → ctxPayload.BodyForAgent (AI 看到的)
|
|
16
|
+
* - rawBody → ctxPayload.RawBody / CommandBody(命令解析、审计)
|
|
17
|
+
* - systemPrompt → ctxPayload.GroupSystemPrompt
|
|
18
|
+
*/
|
|
19
|
+
import type {
|
|
20
|
+
MiddlewareContext,
|
|
21
|
+
QQBotInboundMessage,
|
|
22
|
+
ResolvedQuote,
|
|
23
|
+
HistoryEntry,
|
|
24
|
+
} from '@tencent-connect/qqbot-nodejs';
|
|
25
|
+
import type { ResolvedQQBotAccount } from '../types.js';
|
|
26
|
+
import type { ProcessedAttachments } from '../middleware/attachment.js';
|
|
27
|
+
import { getAdapters } from '../adapter/resolve.js';
|
|
28
|
+
|
|
29
|
+
// ── 协议常量 ─────────────────────────────
|
|
30
|
+
const QUOTE_BEGIN = '[Quoted message begins]';
|
|
31
|
+
const QUOTE_END = '[Quoted message ends]';
|
|
32
|
+
const REF_BEGIN = '[Reference message begins]';
|
|
33
|
+
const REF_END = '[Reference message ends]';
|
|
34
|
+
const HISTORY_BEGIN = '[Chat history begins]';
|
|
35
|
+
const HISTORY_END = '[Chat history ends]';
|
|
36
|
+
const CURRENT_MSG = '[Current message]';
|
|
37
|
+
|
|
38
|
+
export interface AssembledBody {
|
|
39
|
+
/** Web UI 展示用 body */
|
|
40
|
+
webBody: string;
|
|
41
|
+
/** AI 实际接收的 body(dynamicCtx + userMessage [+ history 前缀]) */
|
|
42
|
+
agentBody: string;
|
|
43
|
+
/** 原始 content(命令解析 / 审计用) */
|
|
44
|
+
rawBody: string;
|
|
45
|
+
/** 群系统提示(account.systemPrompt + 群级提示拼接) */
|
|
46
|
+
systemPrompt?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 把 SDK 中间件链产出的所有上下文组装为框架规约的 body。
|
|
51
|
+
*/
|
|
52
|
+
export function assembleBody(
|
|
53
|
+
ctx: MiddlewareContext,
|
|
54
|
+
msg: QQBotInboundMessage,
|
|
55
|
+
account: ResolvedQQBotAccount,
|
|
56
|
+
getRuntime?: () => any,
|
|
57
|
+
): AssembledBody {
|
|
58
|
+
const rawBody = msg.content ?? '';
|
|
59
|
+
const isGroup = msg.kind === 'group';
|
|
60
|
+
|
|
61
|
+
const mentionState = ctx.state.mention as { wasMentioned?: boolean } | undefined;
|
|
62
|
+
const wasMentioned = mentionState?.wasMentioned ?? false;
|
|
63
|
+
const processed = ctx.state.processedAttachments as ProcessedAttachments | undefined;
|
|
64
|
+
const quote = ctx.state.quote as ResolvedQuote | undefined;
|
|
65
|
+
const history = ctx.state.history as HistoryEntry[] | undefined;
|
|
66
|
+
|
|
67
|
+
// ── Layer 1: userContent(清洗后的文本 + 语音转录 + 附件描述) ──
|
|
68
|
+
const userContent = buildUserContent(ctx.message.content ?? '', processed);
|
|
69
|
+
|
|
70
|
+
// ── Layer 2: quotePart ──
|
|
71
|
+
const quotePart = buildQuotePart(quote);
|
|
72
|
+
|
|
73
|
+
// ── Layer 3: userMessage(群带 [sender] 前缀 + (@you)) ──
|
|
74
|
+
const userMessage = buildUserMessage({ msg, userContent, quotePart, isGroup, wasMentioned });
|
|
75
|
+
|
|
76
|
+
// ── Layer 4: dynamicCtx(媒体元数据块 + msg_elements 上下文) ──
|
|
77
|
+
const dynamicCtx = buildDynamicCtx(processed, msg, quote);
|
|
78
|
+
|
|
79
|
+
// ── Layer 5: agentBody(命令直通 / 群被@时前置历史) ──
|
|
80
|
+
const agentBody = buildAgentBody({
|
|
81
|
+
userContent,
|
|
82
|
+
base: dynamicCtx + userMessage,
|
|
83
|
+
isGroup,
|
|
84
|
+
wasMentioned,
|
|
85
|
+
history,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ── webBody ──
|
|
89
|
+
const bodyContent = `${quotePart}${userContent}`;
|
|
90
|
+
|
|
91
|
+
// ── webBody 外层 envelope 渲染(用 formatInboundEnvelope 包装) ──
|
|
92
|
+
const webBody = getRuntime
|
|
93
|
+
? renderWebBody(getRuntime, bodyContent, msg, isGroup)
|
|
94
|
+
: bodyContent;
|
|
95
|
+
|
|
96
|
+
const systemPrompt = account.systemPrompt?.trim() || undefined;
|
|
97
|
+
|
|
98
|
+
return { webBody, agentBody, rawBody, systemPrompt };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 局部组装函数 ─────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/** Layer 1:sanitized + 语音转录 + 附件描述 */
|
|
104
|
+
function buildUserContent(sanitizedRaw: string, processed: ProcessedAttachments | undefined): string {
|
|
105
|
+
const sanitized = sanitizedRaw.trim();
|
|
106
|
+
const voiceText = processed?.voiceText ?? '';
|
|
107
|
+
const attachmentInfo = processed?.otherInfo ? `\n${processed.otherInfo}` : '';
|
|
108
|
+
|
|
109
|
+
if (voiceText) {
|
|
110
|
+
return (sanitized ? `${sanitized}\n${voiceText}` : voiceText) + attachmentInfo;
|
|
111
|
+
}
|
|
112
|
+
return sanitized + attachmentInfo;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Layer 2:[Quoted message begins]…[Quoted message ends] */
|
|
116
|
+
function buildQuotePart(quote: ResolvedQuote | undefined): string {
|
|
117
|
+
if (!quote) return '';
|
|
118
|
+
const text = quote.text || 'Original content unavailable';
|
|
119
|
+
return `${QUOTE_BEGIN}\n${text}\n${QUOTE_END}\n${CURRENT_MSG}\n`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Layer 3:quote + [Sender] {content}{(@you)?} */
|
|
123
|
+
function buildUserMessage(input: {
|
|
124
|
+
msg: QQBotInboundMessage;
|
|
125
|
+
userContent: string;
|
|
126
|
+
quotePart: string;
|
|
127
|
+
isGroup: boolean;
|
|
128
|
+
wasMentioned: boolean;
|
|
129
|
+
}): string {
|
|
130
|
+
const { msg, userContent, quotePart, isGroup, wasMentioned } = input;
|
|
131
|
+
const atYouTag = isGroup && wasMentioned ? ' (@you)' : '';
|
|
132
|
+
|
|
133
|
+
if (isGroup) {
|
|
134
|
+
const senderLabel = formatSenderLabel(msg.senderName, msg.senderId);
|
|
135
|
+
return `${quotePart}[${senderLabel}] ${userContent}${atYouTag}`;
|
|
136
|
+
}
|
|
137
|
+
return `${quotePart}${userContent}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Layer 3(合并版):消息合并处理已移除——群消息合并完全交给框架队列
|
|
141
|
+
* (coalesce.enabled → dispatch 侧 queueModeOverride collect/followup)。
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
/** Layer 4:- Images / - Voice / - ASR 元数据块 + msg_elements 引用上下文 */
|
|
145
|
+
function buildDynamicCtx(
|
|
146
|
+
processed: ProcessedAttachments | undefined,
|
|
147
|
+
msg: QQBotInboundMessage,
|
|
148
|
+
quote: ResolvedQuote | undefined,
|
|
149
|
+
): string {
|
|
150
|
+
const lines: string[] = [];
|
|
151
|
+
|
|
152
|
+
// Images
|
|
153
|
+
if (processed?.imageUrls.length) {
|
|
154
|
+
lines.push(`- Images: ${processed.imageUrls.join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Voice:从 transcripts 行存投影出 paths + urls,去重后拼接
|
|
158
|
+
const transcripts = processed?.transcripts ?? [];
|
|
159
|
+
const voiceRefs = unique([
|
|
160
|
+
...transcripts.map((t) => t.localPath).filter(isNonEmpty),
|
|
161
|
+
...transcripts.map((t) => t.remoteUrl).filter(isNonEmpty),
|
|
162
|
+
]);
|
|
163
|
+
if (voiceRefs.length > 0) {
|
|
164
|
+
lines.push(`- Voice: ${voiceRefs.join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ASR:source==='asr' 的 text,或任意 transcript 上的 asrReferText
|
|
168
|
+
const asrTexts = unique(
|
|
169
|
+
transcripts
|
|
170
|
+
.map((t) => (t.source === 'asr' ? t.text : t.asrReferText))
|
|
171
|
+
.filter(isNonEmpty),
|
|
172
|
+
);
|
|
173
|
+
if (asrTexts.length > 0) {
|
|
174
|
+
lines.push(`- ASR: ${asrTexts.join(' | ')}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// msg_elements 上下文(引用内容本身来自 msg_elements[0] 时跳过该元素避免重复,
|
|
178
|
+
// 其余元素——如 AT+最近N 模式平台附带的消息记录——仍照常渲染)
|
|
179
|
+
const quoteConsumedFirstElement = quote?.source === 'msg_elements';
|
|
180
|
+
const elementsCtx = buildMsgElementsContext(msg, quoteConsumedFirstElement);
|
|
181
|
+
if (elementsCtx.length > 0) {
|
|
182
|
+
lines.push(REF_BEGIN, ...elementsCtx, REF_END);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return lines.length > 0 ? `${lines.join('\n')}\n\n` : '';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 检测平台预渲染文本(示例3形态:`=== 消息 1 ===` + `[消息内容]` 行) */
|
|
189
|
+
function isPlatformPrerenderedContent(content: string): boolean {
|
|
190
|
+
return /^===\s*消息\s*\d+\s*===/.test(content.trimStart()) && content.includes('[消息内容]');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 从 msg_elements 提取上下文(非引用消息也解析,如被回复的 bot 消息等) */
|
|
194
|
+
function buildMsgElementsContext(msg: QQBotInboundMessage, skipFirstElement = false): string[] {
|
|
195
|
+
const elements = msg.msgElements;
|
|
196
|
+
if (!elements || elements.length === 0) return [];
|
|
197
|
+
|
|
198
|
+
const lines: string[] = [];
|
|
199
|
+
let index = 0;
|
|
200
|
+
for (let i = 0; i < elements.length; i++) {
|
|
201
|
+
if (skipFirstElement && i === 0) continue;
|
|
202
|
+
const el = elements[i]!;
|
|
203
|
+
const content = el.content?.trim();
|
|
204
|
+
const attachments = Array.isArray(el.attachments) ? el.attachments : [];
|
|
205
|
+
if (!content && attachments.length === 0) continue;
|
|
206
|
+
index += 1;
|
|
207
|
+
|
|
208
|
+
// 平台已预渲染的多消息文本(如聊天记录转发)直接透传,避免二次包裹
|
|
209
|
+
if (content && isPlatformPrerenderedContent(content)) {
|
|
210
|
+
lines.push(`=== 消息记录 ${index} ===`, content);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const author = (el as Record<string, unknown>).author as
|
|
215
|
+
| { username?: string }
|
|
216
|
+
| undefined;
|
|
217
|
+
const sender = author?.username ?? '未知';
|
|
218
|
+
|
|
219
|
+
lines.push(
|
|
220
|
+
`=== 消息 ${index} ===`,
|
|
221
|
+
`[消息内容] ${content || '(无文本)'}`,
|
|
222
|
+
);
|
|
223
|
+
for (const att of attachments) {
|
|
224
|
+
const a = att as { url?: string; content_type?: string };
|
|
225
|
+
const kind = a.content_type ?? 'attachment';
|
|
226
|
+
const url = a.url ?? '';
|
|
227
|
+
lines.push(`[附件] ${kind}${url ? `: ${url}` : ''}`);
|
|
228
|
+
}
|
|
229
|
+
lines.push(`[发送者] ${sender}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return lines;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function unique<T>(arr: T[]): T[] {
|
|
236
|
+
return [...new Set(arr)];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isNonEmpty(x: string | undefined): x is string {
|
|
240
|
+
return typeof x === 'string' && x.length > 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Layer 5:base = dynamicCtx+userMessage;命令直通;群被@叠历史前缀 */
|
|
244
|
+
function buildAgentBody(input: {
|
|
245
|
+
userContent: string;
|
|
246
|
+
base: string;
|
|
247
|
+
isGroup: boolean;
|
|
248
|
+
wasMentioned: boolean;
|
|
249
|
+
history: HistoryEntry[] | undefined;
|
|
250
|
+
}): string {
|
|
251
|
+
const { userContent, base, isGroup, wasMentioned, history } = input;
|
|
252
|
+
|
|
253
|
+
// 斜杠命令直通:去除一切装饰及首尾空白
|
|
254
|
+
if (userContent.trim().startsWith('/')) {
|
|
255
|
+
return userContent;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 群被@ + 有历史 → 叠历史前缀
|
|
259
|
+
if (isGroup && wasMentioned && history && history.length > 0) {
|
|
260
|
+
const historyText = history
|
|
261
|
+
.map((h) => {
|
|
262
|
+
const label = formatSenderLabel(h.senderName, h.senderId);
|
|
263
|
+
return `[${label}] ${h.content}`;
|
|
264
|
+
})
|
|
265
|
+
.join('\n');
|
|
266
|
+
return [HISTORY_BEGIN, historyText, '', HISTORY_END, CURRENT_MSG, base].join('\n');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return base;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** "Nick (openid)" 标签;当 name 已含 id 时避免双重包裹 */
|
|
273
|
+
function formatSenderLabel(name: string | undefined, id: string): string {
|
|
274
|
+
if (!name) return id;
|
|
275
|
+
return name.includes(id) ? name : `${name} (${id})`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** webBody 外层 envelope 渲染 */
|
|
279
|
+
function renderWebBody(
|
|
280
|
+
getRuntime: () => any,
|
|
281
|
+
bodyContent: string,
|
|
282
|
+
msg: QQBotInboundMessage,
|
|
283
|
+
isGroup: boolean,
|
|
284
|
+
): string {
|
|
285
|
+
try {
|
|
286
|
+
const { formatEnvelope } = getAdapters(getRuntime());
|
|
287
|
+
if (!formatEnvelope) return bodyContent;
|
|
288
|
+
return formatEnvelope({
|
|
289
|
+
channel: 'qqbot',
|
|
290
|
+
from: msg.senderName ?? msg.senderId,
|
|
291
|
+
timestamp: normalizeTimestamp(msg.timestamp),
|
|
292
|
+
body: bodyContent,
|
|
293
|
+
chatType: isGroup ? 'group' : 'direct',
|
|
294
|
+
});
|
|
295
|
+
} catch {
|
|
296
|
+
return bodyContent;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** timestamp 归一化到 epoch ms */
|
|
301
|
+
function normalizeTimestamp(ts: string | number | undefined): number {
|
|
302
|
+
if (typeof ts === 'number') return ts;
|
|
303
|
+
if (typeof ts === 'string') {
|
|
304
|
+
const d = new Date(ts).getTime();
|
|
305
|
+
if (!Number.isNaN(d)) return d;
|
|
306
|
+
}
|
|
307
|
+
return Date.now();
|
|
308
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctxPayload 构建器
|
|
3
|
+
*
|
|
4
|
+
* 与内置版 outbound-dispatch.ts:buildCtxPayload 对齐,
|
|
5
|
+
* 将 SDK 消息数据转换为 OpenClaw 框架标准的入站上下文。
|
|
6
|
+
*/
|
|
7
|
+
import type { MiddlewareContext, QQBotInboundMessage } from '@tencent-connect/qqbot-nodejs';
|
|
8
|
+
import type { ResolvedQQBotAccount } from '../types.js';
|
|
9
|
+
import type { AssembledBody } from './body-assembler.js';
|
|
10
|
+
import type { OpenClawInboundMessage } from './envelope-builder.js';
|
|
11
|
+
import type { RuntimeAdapters } from '../adapter/resolve.js';
|
|
12
|
+
|
|
13
|
+
export interface CtxPayloadParams {
|
|
14
|
+
assembled: AssembledBody;
|
|
15
|
+
envelope: OpenClawInboundMessage;
|
|
16
|
+
route: { sessionKey: string; accountId: string; agentId?: string };
|
|
17
|
+
msg: QQBotInboundMessage;
|
|
18
|
+
ctx: MiddlewareContext;
|
|
19
|
+
adapters: RuntimeAdapters;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildCtxPayload(params: CtxPayloadParams): any {
|
|
23
|
+
const { assembled, envelope, route, msg, ctx, adapters } = params;
|
|
24
|
+
|
|
25
|
+
const isSlashCommand = /^\//.test(assembled.rawBody ?? '');
|
|
26
|
+
const convKind = envelope.chatScope === 'group' ? 'group' : 'direct';
|
|
27
|
+
const peerId = convKind === 'group'
|
|
28
|
+
? (envelope.groupId ?? envelope.senderId)
|
|
29
|
+
: envelope.senderId;
|
|
30
|
+
const groupId = convKind === 'group' ? envelope.groupId : undefined;
|
|
31
|
+
|
|
32
|
+
const processed = ctx.state.processedAttachments as any;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 正规媒体通道(对齐 core ChannelInboundMediaInput):
|
|
36
|
+
* 全部附件(图片/语音/文件)按原始顺序作为 media facts 传入,
|
|
37
|
+
* 字段名必须是 path(不是 localPath);url 用本地路径(QQ CDN URL 有时效),
|
|
38
|
+
* 与 telegram toInboundMedia 的 { path, url: path, contentType, kind, transcribed } 一致。
|
|
39
|
+
* media facts 经 core 归一化后生成 MediaPath/MediaPaths/MediaTypes/MediaTranscribedIndexes。
|
|
40
|
+
*/
|
|
41
|
+
const mediaFacts = (processed?.media as Array<{
|
|
42
|
+
kind: 'image' | 'audio' | 'video' | 'document';
|
|
43
|
+
localPath?: string;
|
|
44
|
+
remoteUrl?: string;
|
|
45
|
+
contentType?: string;
|
|
46
|
+
transcribed?: boolean;
|
|
47
|
+
}> | undefined)?.map((m) => ({
|
|
48
|
+
kind: m.kind,
|
|
49
|
+
...(m.localPath
|
|
50
|
+
? { path: m.localPath, url: m.localPath }
|
|
51
|
+
: m.remoteUrl
|
|
52
|
+
? { url: m.remoteUrl }
|
|
53
|
+
: {}),
|
|
54
|
+
...(m.contentType ? { contentType: m.contentType } : {}),
|
|
55
|
+
...(m.transcribed ? { transcribed: true } : {}),
|
|
56
|
+
})) ?? [];
|
|
57
|
+
|
|
58
|
+
const msgTimestamp = (msg as any).timestamp ?? (msg as any).Timestamp;
|
|
59
|
+
|
|
60
|
+
// 群成员角色(平台 member_role:admin/owner)→ sender.roles,
|
|
61
|
+
// 供框架群权限/工具策略分级使用;c2c / 未标注为空
|
|
62
|
+
const memberRole = (msg as { raw?: { author?: { member_role?: string } } }).raw?.author?.member_role;
|
|
63
|
+
const senderRoles = memberRole === 'admin' || memberRole === 'owner' ? [memberRole] : undefined;
|
|
64
|
+
|
|
65
|
+
return adapters.buildInboundContext?.({
|
|
66
|
+
channel: 'qqbot',
|
|
67
|
+
accountId: route.accountId,
|
|
68
|
+
provider: 'qqbot',
|
|
69
|
+
surface: 'qqbot',
|
|
70
|
+
messageId: envelope.messageId,
|
|
71
|
+
timestamp: msgTimestamp ? new Date(msgTimestamp).getTime() : Date.now(),
|
|
72
|
+
from: envelope.targetId,
|
|
73
|
+
sender: { id: envelope.senderId, name: envelope.senderName, ...(senderRoles ? { roles: senderRoles } : {}) },
|
|
74
|
+
conversation: {
|
|
75
|
+
kind: convKind,
|
|
76
|
+
id: peerId,
|
|
77
|
+
label: assembled.systemPrompt,
|
|
78
|
+
},
|
|
79
|
+
message: {
|
|
80
|
+
body: assembled.webBody,
|
|
81
|
+
bodyForAgent: assembled.agentBody,
|
|
82
|
+
rawBody: assembled.rawBody,
|
|
83
|
+
commandBody: assembled.rawBody,
|
|
84
|
+
},
|
|
85
|
+
route: {
|
|
86
|
+
agentId: route.agentId ?? 'default',
|
|
87
|
+
routeSessionKey: route.sessionKey,
|
|
88
|
+
accountId: route.accountId,
|
|
89
|
+
},
|
|
90
|
+
reply: {
|
|
91
|
+
to: envelope.targetId,
|
|
92
|
+
replyToId: envelope.messageId,
|
|
93
|
+
originatingTo: envelope.targetId,
|
|
94
|
+
},
|
|
95
|
+
access: {
|
|
96
|
+
commands: { authorized: isSlashCommand },
|
|
97
|
+
},
|
|
98
|
+
command: isSlashCommand
|
|
99
|
+
? { kind: 'text-slash' as const, body: assembled.rawBody!, authorized: true }
|
|
100
|
+
: undefined,
|
|
101
|
+
media: mediaFacts.length > 0 ? mediaFacts : undefined,
|
|
102
|
+
supplemental: {
|
|
103
|
+
quote: envelope.quote
|
|
104
|
+
? { id: envelope.messageId, body: envelope.quote.content, sender: envelope.quote.senderId }
|
|
105
|
+
: undefined,
|
|
106
|
+
groupSystemPrompt: envelope.systemPrompt,
|
|
107
|
+
},
|
|
108
|
+
extra: {
|
|
109
|
+
...(isSlashCommand ? { CommandSource: 'text' } : {}),
|
|
110
|
+
...(groupId ? { QQGroupOpenid: groupId } : {}),
|
|
111
|
+
...(processed?.localMediaPaths?.length
|
|
112
|
+
? {
|
|
113
|
+
MediaPaths: processed.localMediaPaths,
|
|
114
|
+
MediaPath: processed.localMediaPaths[0],
|
|
115
|
+
MediaTypes: processed.localMediaTypes,
|
|
116
|
+
MediaType: processed.localMediaTypes?.[0],
|
|
117
|
+
}
|
|
118
|
+
: {}),
|
|
119
|
+
...(processed?.remoteMediaUrls?.length
|
|
120
|
+
? {
|
|
121
|
+
MediaUrls: processed.remoteMediaUrls,
|
|
122
|
+
MediaUrl: processed.remoteMediaUrls[0],
|
|
123
|
+
}
|
|
124
|
+
: {}),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|