@workclaw/openclaw-workclaw 1.0.13 → 1.0.16

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.
@@ -1,422 +1,422 @@
1
- import type { PluginRuntime } from 'openclaw/plugin-sdk'
2
-
3
- /** Channel/plugin name constant */
4
- export const OPENCLAW_WORKCLAW_CHANNEL = 'openclaw-workclaw'
5
-
6
- export interface InboundMessage {
7
- text: string
8
- to: string
9
- from: string
10
- chatType: 'dm' | 'group'
11
- messageId: string
12
- rawPayload: string
13
- timestamp: number
14
- }
15
-
16
- export interface InboundContextPayload {
17
- [key: string]: unknown
18
- Body: string
19
- BodyForAgent: string
20
- RawBody: string
21
- From: string
22
- To: string
23
- ChatType: 'dm' | 'group'
24
- Provider: string
25
- Surface: string
26
- Timestamp: number
27
- AccountId: string
28
- AgentId?: string
29
- MessageSid?: string
30
- OriginatingChannel: string
31
- OriginatingTo: string
32
- }
33
-
34
- export type WorkClawMessageType
35
- = | 'agent_message' // 正常的业务消息
36
- | 'ping' // 需要回复 pong 的消息
37
- | 'disconnect' // 断开连接的消息
38
- | 'agent_created' // 创建账户事件
39
- | 'agent_updated' // 更新账户事件
40
- | 'agent_deleted' // 删除账户事件
41
- | 'tools_list' // 获取工具列表事件
42
- | 'skills_list' // 获取 skills 列表事件
43
- | 'skills_event' // skills 相关事件(create/update/delete/list/get/invoke)
44
- | 'init_agent' // 初始化智能体事件
45
- | 'ignored' // 忽略的消息(未知类型)
46
-
47
- export interface ParseWorkClawResult {
48
- type: WorkClawMessageType
49
- message?: {
50
- text: string
51
- userId: string
52
- openConversationId: string
53
- messageId: string
54
- agentId?: string | number
55
- }
56
- pongData?: any // 用于回复 pong 的数据
57
- eventData?: any // 事件数据(用于 AGENT_CREATED/UPDATED/DELETED)
58
- skillsEvent?: {
59
- // skills 事件数据
60
- topic: string // skills/create, skills/update, skills/delete, skills/list, skills/get, skills/invoke
61
- requestId: string
62
- callbackUrl: string
63
- authToken?: string
64
- data: any
65
- }
66
- }
67
-
68
- export function parseInboundMessage(data: string, allowRawJsonPayload?: boolean): Partial<InboundMessage> {
69
- let input: any = {}
70
- try {
71
- input = data ? JSON.parse(data) : {}
72
- }
73
- catch {
74
- // Not JSON, treat as plain text
75
- }
76
-
77
- const text
78
- = typeof input.text === 'string'
79
- ? input.text
80
- : typeof input.message === 'string'
81
- ? input.message
82
- : typeof input.body === 'string'
83
- ? input.body
84
- : data
85
-
86
- const to = String(input.to ?? input.chatId ?? input.target ?? '')
87
- const from = String(input.from ?? input.senderId ?? input.userId ?? '')
88
- const chatType = String(input.chatType || '').toLowerCase() === 'group' ? 'group' : 'dm'
89
- const messageId = String(input.messageId ?? input.id ?? '')
90
- const rawPayload = allowRawJsonPayload ? JSON.stringify(input ?? {}) : data
91
-
92
- return {
93
- text,
94
- to,
95
- from,
96
- chatType,
97
- messageId,
98
- rawPayload,
99
- }
100
- }
101
-
102
- export function parseWorkClawMessage(data: string, log?: {
103
- info?: (msg: string) => void
104
- warn?: (msg: string) => void
105
- error?: (msg: string) => void
106
- }): ParseWorkClawResult | null {
107
- // {"specVersion":"1.0","type":"EVENT","metadata":{"time":"1773749163831","event":"INIT_AGENT","contentType":"application/json"},"data":"\"12557533957824965\""}
108
- let input: any = {}
109
- try {
110
- input = data ? JSON.parse(data) : {}
111
- }
112
- catch {
113
- log?.info?.(`Failed to parse message data=${data}`)
114
- return null
115
- }
116
-
117
- // Handle plain text ping
118
- if (typeof input === 'string' && input.toLowerCase() === 'ping') {
119
- return {
120
- type: 'ping',
121
- pongData: input,
122
- }
123
- }
124
-
125
- const envelope = input && typeof input === 'object' ? input : { data: input }
126
- const topic = envelope?.metadata?.topic
127
- const type = String(envelope?.type || '').trim().toUpperCase()
128
- const topicLower = String(topic || '').trim().toLowerCase()
129
-
130
- if (type === 'INIT_AGENT') {
131
- let parsedData = envelope?.data
132
- if (typeof parsedData === 'string') {
133
- try {
134
- parsedData = JSON.parse(parsedData)
135
- }
136
- catch {
137
- // 解析失败,保持原样
138
- }
139
- }
140
-
141
- return {
142
- type: 'init_agent',
143
- eventData: parsedData,
144
- }
145
- }
146
-
147
- // Handle SYSTEM messages
148
- if (type === 'SYSTEM') {
149
- if (topicLower === 'ping') {
150
- return {
151
- type: 'ping',
152
- pongData: envelope?.data,
153
- }
154
- }
155
- if (topicLower === 'disconnect') {
156
- return {
157
- type: 'disconnect',
158
- }
159
- }
160
- // Unknown system message type, ignore
161
- return {
162
- type: 'ignored',
163
- }
164
- }
165
-
166
- // Handle EVENT messages (AGENT_CREATED, AGENT_UPDATED, AGENT_DELETED, TOOLS_LIST, SKILLS_LIST, INIT_AGENT)
167
- if (type === 'EVENT') {
168
- const eventType = String(envelope?.metadata?.event || '').trim().toUpperCase()
169
-
170
- // 解析 data 字段(可能是 JSON 字符串)
171
- let parsedData = envelope?.data
172
- if (typeof parsedData === 'string') {
173
- try {
174
- parsedData = JSON.parse(parsedData)
175
- }
176
- catch {
177
- // 解析失败,保持原样
178
- }
179
- }
180
-
181
- if (eventType === 'AGENT_CREATED') {
182
- return {
183
- type: 'agent_created',
184
- eventData: parsedData,
185
- }
186
- }
187
- if (eventType === 'AGENT_UPDATED') {
188
- return {
189
- type: 'agent_updated',
190
- eventData: parsedData,
191
- }
192
- }
193
- if (eventType === 'AGENT_DELETED') {
194
- return {
195
- type: 'agent_deleted',
196
- eventData: parsedData,
197
- }
198
- }
199
- if (eventType === 'TOOLS_LIST') {
200
- return {
201
- type: 'tools_list',
202
- eventData: parsedData,
203
- }
204
- }
205
- if (eventType === 'SKILLS_LIST') {
206
- return {
207
- type: 'skills_list',
208
- eventData: parsedData,
209
- }
210
- }
211
- if (eventType === 'INIT_AGENT') {
212
- return {
213
- type: 'init_agent',
214
- eventData: parsedData,
215
- }
216
- }
217
-
218
- if (eventType === 'SKILL_DO_REMIND') {
219
- const eventTopic = String(parsedData?.dotype || '').toLowerCase()
220
- return {
221
- type: 'skills_event',
222
- skillsEvent: {
223
- topic: `skills/${eventTopic}`,
224
- requestId: '',
225
- callbackUrl: '',
226
- authToken: '',
227
- data: parsedData,
228
- },
229
- }
230
- }
231
-
232
- // Unknown event type, ignore
233
- return {
234
- type: 'ignored',
235
- }
236
- }
237
-
238
- // Only process CALLBACK type with AGENT_MESSAGE topic
239
- if (type !== 'CALLBACK' || String(topic || '').trim().toUpperCase() !== 'AGENT_MESSAGE') {
240
- return {
241
- type: 'ignored',
242
- }
243
- }
244
-
245
- // 解析 data 字段(可能是 JSON 字符串)
246
- let payload = envelope?.data || {}
247
- if (typeof payload === 'string') {
248
- try {
249
- payload = JSON.parse(payload)
250
- }
251
- catch {
252
- // 解析失败,保持原样
253
- }
254
- }
255
-
256
- const text
257
- = typeof payload?.message?.content === 'string'
258
- ? payload.message.content
259
- : typeof payload?.message === 'string'
260
- ? payload.message
261
- : ''
262
-
263
- return {
264
- type: 'agent_message',
265
- message: {
266
- text,
267
- userId: String(payload?.userId ?? ''),
268
- openConversationId: String(payload?.conversationId ?? ''),
269
- messageId: String(payload?.msgId ?? ''),
270
- agentId: payload?.agentId,
271
- },
272
- }
273
- }
274
-
275
- /**
276
- * Build inbound context for WorkClaw channel.
277
- *
278
- * When pluginRuntime is provided, performs full session handling:
279
- * - resolveStorePath, readSessionUpdatedAt, formatInboundEnvelope
280
- * - finalizeInboundContext, recordInboundSession
281
- *
282
- * @param message - The inbound message
283
- * @param accountId - Account ID
284
- * @param config - Account config (OpenclawWorkclawConfig)
285
- * @param agentId - Target agent ID (optional)
286
- * @param pluginRuntime - OpenClaw plugin runtime (optional, enables full session handling)
287
- * @param recordSession - Whether to record session (default true)
288
- * @param log 日志对象(可选)
289
- * @param {Function} [log.info] - 信息日志函数
290
- * @param {Function} [log.error] - 错误日志函数
291
- * @param {Function} [log.warn] - 警告日志函数
292
- */
293
- export async function buildInboundContext(
294
- message: InboundMessage,
295
- accountId: string,
296
- config: any,
297
- agentId?: string,
298
- pluginRuntime?: PluginRuntime,
299
- recordSession: boolean = true,
300
- log?: { info?: (msg: string) => void, warn?: (msg: string) => void, error?: (msg: string) => void },
301
- ): Promise<InboundContextPayload> {
302
- // 尝试从 rawPayload 中提取已有的 sessionKey(新建话题时 openclaw 会生成新的 session)
303
- let existingSessionKey: string | undefined
304
- try {
305
- const rawPayload = JSON.parse(message.rawPayload)
306
- existingSessionKey = rawPayload.sessionKey
307
- }
308
- catch {
309
- // ignore
310
- }
311
-
312
- const peerId = message.from
313
- const computedSessionKey = existingSessionKey
314
- || (agentId && agentId !== 'main'
315
- ? `agent:${agentId}:openclaw-workclaw:${accountId}:direct:${peerId}`
316
- : undefined)
317
-
318
- const sessionKey = computedSessionKey || (agentId && agentId !== 'main'
319
- ? `agent:${agentId}:openclaw-workclaw:${accountId}:direct:${peerId}`
320
- : undefined)
321
-
322
- // Basic context (used as base for both paths)
323
- const result: InboundContextPayload = {
324
- Body: message.text,
325
- BodyForAgent: message.rawPayload || message.text,
326
- RawBody: JSON.stringify(message),
327
- From: message.from,
328
- To: message.to,
329
- ChatType: message.chatType,
330
- Provider: OPENCLAW_WORKCLAW_CHANNEL,
331
- Surface: OPENCLAW_WORKCLAW_CHANNEL,
332
- Timestamp: message.timestamp,
333
- AccountId: accountId,
334
- AgentId: agentId,
335
- MessageSid: message.messageId,
336
- OriginatingChannel: OPENCLAW_WORKCLAW_CHANNEL,
337
- OriginatingTo: message.to,
338
- }
339
-
340
- if (sessionKey) {
341
- result.SessionKey = sessionKey
342
- }
343
-
344
- // If no pluginRuntime provided, return basic context without session recording
345
- if (!pluginRuntime) {
346
- return result
347
- }
348
-
349
- // Full session handling path
350
- const storePath = pluginRuntime.channel.session.resolveStorePath(config.session?.store, { agentId })
351
- log?.info?.(`[Session] storePath=${storePath}, agentId=${agentId}, sessionKey=${sessionKey}`)
352
-
353
- // 获取 envelope 格式选项
354
- const envelopeOptions = pluginRuntime.channel.reply.resolveEnvelopeFormatOptions(config)
355
-
356
- // 获取上次的 session 更新时间
357
- const previousTimestamp = pluginRuntime.channel.session.readSessionUpdatedAt({
358
- storePath,
359
- sessionKey: sessionKey!,
360
- })
361
- log?.info?.(`[Session] previousTimestamp=${previousTimestamp}`)
362
-
363
- // 格式化 envelope(Body 用于日志/历史,BodyForAgent 用于 AI)
364
- const fromLabel = message.to
365
- const body = pluginRuntime.channel.reply.formatInboundEnvelope({
366
- channel: OPENCLAW_WORKCLAW_CHANNEL,
367
- from: fromLabel,
368
- timestamp: message.timestamp,
369
- body: message.text,
370
- chatType: message.chatType,
371
- sender: { name: message.to, id: message.to },
372
- previousTimestamp,
373
- envelope: envelopeOptions,
374
- })
375
-
376
- // 构建完整的 ctx
377
- const finalized = pluginRuntime.channel.reply.finalizeInboundContext({
378
- Body: body,
379
- BodyForAgent: message.text,
380
- RawBody: JSON.stringify(message),
381
- CommandBody: message.text,
382
- From: message.from,
383
- To: message.to,
384
- SessionKey: sessionKey,
385
- AccountId: accountId,
386
- ChatType: message.chatType,
387
- ConversationLabel: fromLabel,
388
- Provider: OPENCLAW_WORKCLAW_CHANNEL,
389
- Surface: 'openclaw-workclaw_dm',
390
- MessageSid: message.messageId,
391
- Timestamp: message.timestamp,
392
- OriginatingChannel: OPENCLAW_WORKCLAW_CHANNEL,
393
- OriginatingTo: message.to,
394
- })
395
-
396
- // 记录 session(需要在 dispatchReplyWithBufferedBlockDispatcher 之前调用)
397
- // 这样 OpenClaw 才能在工具执行时正确关联 session
398
- if (sessionKey && recordSession) {
399
- try {
400
- await pluginRuntime.channel.session.recordInboundSession({
401
- storePath,
402
- sessionKey: finalized.SessionKey || sessionKey,
403
- ctx: finalized,
404
- updateLastRoute: {
405
- sessionKey,
406
- channel: OPENCLAW_WORKCLAW_CHANNEL,
407
- to: message.to,
408
- accountId,
409
- },
410
- onRecordError: (err: unknown) => {
411
- log?.error?.(`[Session] recordInboundSession failed: ${String(err)}`)
412
- },
413
- })
414
- log?.info?.(`[Session] Recorded session for key=${sessionKey}`)
415
- }
416
- catch (err) {
417
- log?.warn?.(`[Session] recordInboundSession error: ${String(err)}`)
418
- }
419
- }
420
-
421
- return finalized
422
- }
1
+ import type { PluginRuntime } from 'openclaw/plugin-sdk'
2
+
3
+ /** Channel/plugin name constant */
4
+ export const OPENCLAW_WORKCLAW_CHANNEL = 'openclaw-workclaw'
5
+
6
+ export interface InboundMessage {
7
+ text: string
8
+ to: string
9
+ from: string
10
+ chatType: 'dm' | 'group'
11
+ messageId: string
12
+ rawPayload: string
13
+ timestamp: number
14
+ }
15
+
16
+ export interface InboundContextPayload {
17
+ [key: string]: unknown
18
+ Body: string
19
+ BodyForAgent: string
20
+ RawBody: string
21
+ From: string
22
+ To: string
23
+ ChatType: 'dm' | 'group'
24
+ Provider: string
25
+ Surface: string
26
+ Timestamp: number
27
+ AccountId: string
28
+ AgentId?: string
29
+ MessageSid?: string
30
+ OriginatingChannel: string
31
+ OriginatingTo: string
32
+ }
33
+
34
+ export type WorkClawMessageType
35
+ = | 'agent_message' // 正常的业务消息
36
+ | 'ping' // 需要回复 pong 的消息
37
+ | 'disconnect' // 断开连接的消息
38
+ | 'agent_created' // 创建账户事件
39
+ | 'agent_updated' // 更新账户事件
40
+ | 'agent_deleted' // 删除账户事件
41
+ | 'tools_list' // 获取工具列表事件
42
+ | 'skills_list' // 获取 skills 列表事件
43
+ | 'skills_event' // skills 相关事件(create/update/delete/list/get/invoke)
44
+ | 'init_agent' // 初始化智能体事件
45
+ | 'ignored' // 忽略的消息(未知类型)
46
+
47
+ export interface ParseWorkClawResult {
48
+ type: WorkClawMessageType
49
+ message?: {
50
+ text: string
51
+ userId: string
52
+ openConversationId: string
53
+ messageId: string
54
+ agentId?: string | number
55
+ }
56
+ pongData?: any // 用于回复 pong 的数据
57
+ eventData?: any // 事件数据(用于 AGENT_CREATED/UPDATED/DELETED)
58
+ skillsEvent?: {
59
+ // skills 事件数据
60
+ topic: string // skills/create, skills/update, skills/delete, skills/list, skills/get, skills/invoke
61
+ requestId: string
62
+ callbackUrl: string
63
+ authToken?: string
64
+ data: any
65
+ }
66
+ }
67
+
68
+ export function parseInboundMessage(data: string, allowRawJsonPayload?: boolean): Partial<InboundMessage> {
69
+ let input: any = {}
70
+ try {
71
+ input = data ? JSON.parse(data) : {}
72
+ }
73
+ catch {
74
+ // Not JSON, treat as plain text
75
+ }
76
+
77
+ const text
78
+ = typeof input.text === 'string'
79
+ ? input.text
80
+ : typeof input.message === 'string'
81
+ ? input.message
82
+ : typeof input.body === 'string'
83
+ ? input.body
84
+ : data
85
+
86
+ const to = String(input.to ?? input.chatId ?? input.target ?? '')
87
+ const from = String(input.from ?? input.senderId ?? input.userId ?? '')
88
+ const chatType = String(input.chatType || '').toLowerCase() === 'group' ? 'group' : 'dm'
89
+ const messageId = String(input.messageId ?? input.id ?? '')
90
+ const rawPayload = allowRawJsonPayload ? JSON.stringify(input ?? {}) : data
91
+
92
+ return {
93
+ text,
94
+ to,
95
+ from,
96
+ chatType,
97
+ messageId,
98
+ rawPayload,
99
+ }
100
+ }
101
+
102
+ export function parseWorkClawMessage(data: string, log?: {
103
+ info?: (msg: string) => void
104
+ warn?: (msg: string) => void
105
+ error?: (msg: string) => void
106
+ }): ParseWorkClawResult | null {
107
+ // {"specVersion":"1.0","type":"EVENT","metadata":{"time":"1773749163831","event":"INIT_AGENT","contentType":"application/json"},"data":"\"12557533957824965\""}
108
+ let input: any = {}
109
+ try {
110
+ input = data ? JSON.parse(data) : {}
111
+ }
112
+ catch {
113
+ log?.info?.(`Failed to parse message data=${data}`)
114
+ return null
115
+ }
116
+
117
+ // Handle plain text ping
118
+ if (typeof input === 'string' && input.toLowerCase() === 'ping') {
119
+ return {
120
+ type: 'ping',
121
+ pongData: input,
122
+ }
123
+ }
124
+
125
+ const envelope = input && typeof input === 'object' ? input : { data: input }
126
+ const topic = envelope?.metadata?.topic
127
+ const type = String(envelope?.type || '').trim().toUpperCase()
128
+ const topicLower = String(topic || '').trim().toLowerCase()
129
+
130
+ if (type === 'INIT_AGENT') {
131
+ let parsedData = envelope?.data
132
+ if (typeof parsedData === 'string') {
133
+ try {
134
+ parsedData = JSON.parse(parsedData)
135
+ }
136
+ catch {
137
+ // 解析失败,保持原样
138
+ }
139
+ }
140
+
141
+ return {
142
+ type: 'init_agent',
143
+ eventData: parsedData,
144
+ }
145
+ }
146
+
147
+ // Handle SYSTEM messages
148
+ if (type === 'SYSTEM') {
149
+ if (topicLower === 'ping') {
150
+ return {
151
+ type: 'ping',
152
+ pongData: envelope?.data,
153
+ }
154
+ }
155
+ if (topicLower === 'disconnect') {
156
+ return {
157
+ type: 'disconnect',
158
+ }
159
+ }
160
+ // Unknown system message type, ignore
161
+ return {
162
+ type: 'ignored',
163
+ }
164
+ }
165
+
166
+ // Handle EVENT messages (AGENT_CREATED, AGENT_UPDATED, AGENT_DELETED, TOOLS_LIST, SKILLS_LIST, INIT_AGENT)
167
+ if (type === 'EVENT') {
168
+ const eventType = String(envelope?.metadata?.event || '').trim().toUpperCase()
169
+
170
+ // 解析 data 字段(可能是 JSON 字符串)
171
+ let parsedData = envelope?.data
172
+ if (typeof parsedData === 'string') {
173
+ try {
174
+ parsedData = JSON.parse(parsedData)
175
+ }
176
+ catch {
177
+ // 解析失败,保持原样
178
+ }
179
+ }
180
+
181
+ if (eventType === 'AGENT_CREATED') {
182
+ return {
183
+ type: 'agent_created',
184
+ eventData: parsedData,
185
+ }
186
+ }
187
+ if (eventType === 'AGENT_UPDATED') {
188
+ return {
189
+ type: 'agent_updated',
190
+ eventData: parsedData,
191
+ }
192
+ }
193
+ if (eventType === 'AGENT_DELETED') {
194
+ return {
195
+ type: 'agent_deleted',
196
+ eventData: parsedData,
197
+ }
198
+ }
199
+ if (eventType === 'TOOLS_LIST') {
200
+ return {
201
+ type: 'tools_list',
202
+ eventData: parsedData,
203
+ }
204
+ }
205
+ if (eventType === 'SKILLS_LIST') {
206
+ return {
207
+ type: 'skills_list',
208
+ eventData: parsedData,
209
+ }
210
+ }
211
+ if (eventType === 'INIT_AGENT') {
212
+ return {
213
+ type: 'init_agent',
214
+ eventData: parsedData,
215
+ }
216
+ }
217
+
218
+ if (eventType === 'SKILL_DO_REMIND') {
219
+ const eventTopic = String(parsedData?.dotype || '').toLowerCase()
220
+ return {
221
+ type: 'skills_event',
222
+ eventData: {
223
+ topic: `skills/${eventTopic}`,
224
+ requestId: '',
225
+ callbackUrl: '',
226
+ authToken: '',
227
+ data: parsedData,
228
+ },
229
+ }
230
+ }
231
+
232
+ // Unknown event type, ignore
233
+ return {
234
+ type: 'ignored',
235
+ }
236
+ }
237
+
238
+ // Only process CALLBACK type with AGENT_MESSAGE topic
239
+ if (type !== 'CALLBACK' || String(topic || '').trim().toUpperCase() !== 'AGENT_MESSAGE') {
240
+ return {
241
+ type: 'ignored',
242
+ }
243
+ }
244
+
245
+ // 解析 data 字段(可能是 JSON 字符串)
246
+ let payload = envelope?.data || {}
247
+ if (typeof payload === 'string') {
248
+ try {
249
+ payload = JSON.parse(payload)
250
+ }
251
+ catch {
252
+ // 解析失败,保持原样
253
+ }
254
+ }
255
+
256
+ const text
257
+ = typeof payload?.message?.content === 'string'
258
+ ? payload.message.content
259
+ : typeof payload?.message === 'string'
260
+ ? payload.message
261
+ : ''
262
+
263
+ return {
264
+ type: 'agent_message',
265
+ message: {
266
+ text,
267
+ userId: String(payload?.userId ?? ''),
268
+ openConversationId: String(payload?.conversationId ?? ''),
269
+ messageId: String(payload?.msgId ?? ''),
270
+ agentId: payload?.agentId,
271
+ },
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Build inbound context for WorkClaw channel.
277
+ *
278
+ * When pluginRuntime is provided, performs full session handling:
279
+ * - resolveStorePath, readSessionUpdatedAt, formatInboundEnvelope
280
+ * - finalizeInboundContext, recordInboundSession
281
+ *
282
+ * @param message - The inbound message
283
+ * @param accountId - Account ID
284
+ * @param config - Account config (OpenclawWorkclawConfig)
285
+ * @param agentId - Target agent ID (optional)
286
+ * @param pluginRuntime - OpenClaw plugin runtime (optional, enables full session handling)
287
+ * @param recordSession - Whether to record session (default true)
288
+ * @param log 日志对象(可选)
289
+ * @param {Function} [log.info] - 信息日志函数
290
+ * @param {Function} [log.error] - 错误日志函数
291
+ * @param {Function} [log.warn] - 警告日志函数
292
+ */
293
+ export async function buildInboundContext(
294
+ message: InboundMessage,
295
+ accountId: string,
296
+ config: any,
297
+ agentId?: string,
298
+ pluginRuntime?: PluginRuntime,
299
+ recordSession: boolean = true,
300
+ log?: { info?: (msg: string) => void, warn?: (msg: string) => void, error?: (msg: string) => void },
301
+ ): Promise<InboundContextPayload> {
302
+ // 尝试从 rawPayload 中提取已有的 sessionKey(新建话题时 openclaw 会生成新的 session)
303
+ let existingSessionKey: string | undefined
304
+ try {
305
+ const rawPayload = JSON.parse(message.rawPayload)
306
+ existingSessionKey = rawPayload.sessionKey
307
+ }
308
+ catch {
309
+ // ignore
310
+ }
311
+
312
+ const peerId = message.from
313
+ const computedSessionKey = existingSessionKey
314
+ || (agentId && agentId !== 'main'
315
+ ? `agent:${agentId}:openclaw-workclaw:${accountId}:direct:${peerId}`
316
+ : undefined)
317
+
318
+ const sessionKey = computedSessionKey || (agentId && agentId !== 'main'
319
+ ? `agent:${agentId}:openclaw-workclaw:${accountId}:direct:${peerId}`
320
+ : undefined)
321
+
322
+ // Basic context (used as base for both paths)
323
+ const result: InboundContextPayload = {
324
+ Body: message.text,
325
+ BodyForAgent: message.rawPayload || message.text,
326
+ RawBody: JSON.stringify(message),
327
+ From: message.from,
328
+ To: message.to,
329
+ ChatType: message.chatType,
330
+ Provider: OPENCLAW_WORKCLAW_CHANNEL,
331
+ Surface: OPENCLAW_WORKCLAW_CHANNEL,
332
+ Timestamp: message.timestamp,
333
+ AccountId: accountId,
334
+ AgentId: agentId,
335
+ MessageSid: message.messageId,
336
+ OriginatingChannel: OPENCLAW_WORKCLAW_CHANNEL,
337
+ OriginatingTo: message.to,
338
+ }
339
+
340
+ if (sessionKey) {
341
+ result.SessionKey = sessionKey
342
+ }
343
+
344
+ // If no pluginRuntime provided, return basic context without session recording
345
+ if (!pluginRuntime) {
346
+ return result
347
+ }
348
+
349
+ // Full session handling path
350
+ const storePath = pluginRuntime.channel.session.resolveStorePath(config.session?.store, { agentId })
351
+ log?.info?.(`[Session] storePath=${storePath}, agentId=${agentId}, sessionKey=${sessionKey}`)
352
+
353
+ // 获取 envelope 格式选项
354
+ const envelopeOptions = pluginRuntime.channel.reply.resolveEnvelopeFormatOptions(config)
355
+
356
+ // 获取上次的 session 更新时间
357
+ const previousTimestamp = pluginRuntime.channel.session.readSessionUpdatedAt({
358
+ storePath,
359
+ sessionKey: sessionKey!,
360
+ })
361
+ log?.info?.(`[Session] previousTimestamp=${previousTimestamp}`)
362
+
363
+ // 格式化 envelope(Body 用于日志/历史,BodyForAgent 用于 AI)
364
+ const fromLabel = message.to
365
+ const body = pluginRuntime.channel.reply.formatInboundEnvelope({
366
+ channel: OPENCLAW_WORKCLAW_CHANNEL,
367
+ from: fromLabel,
368
+ timestamp: message.timestamp,
369
+ body: message.text,
370
+ chatType: message.chatType,
371
+ sender: { name: message.to, id: message.to },
372
+ previousTimestamp,
373
+ envelope: envelopeOptions,
374
+ })
375
+
376
+ // 构建完整的 ctx
377
+ const finalized = pluginRuntime.channel.reply.finalizeInboundContext({
378
+ Body: body,
379
+ BodyForAgent: message.text,
380
+ RawBody: JSON.stringify(message),
381
+ CommandBody: message.text,
382
+ From: message.from,
383
+ To: message.to,
384
+ SessionKey: sessionKey,
385
+ AccountId: accountId,
386
+ ChatType: message.chatType,
387
+ ConversationLabel: fromLabel,
388
+ Provider: OPENCLAW_WORKCLAW_CHANNEL,
389
+ Surface: 'openclaw-workclaw_dm',
390
+ MessageSid: message.messageId,
391
+ Timestamp: message.timestamp,
392
+ OriginatingChannel: OPENCLAW_WORKCLAW_CHANNEL,
393
+ OriginatingTo: message.to,
394
+ })
395
+
396
+ // 记录 session(需要在 dispatchReplyWithBufferedBlockDispatcher 之前调用)
397
+ // 这样 OpenClaw 才能在工具执行时正确关联 session
398
+ if (sessionKey && recordSession) {
399
+ try {
400
+ await pluginRuntime.channel.session.recordInboundSession({
401
+ storePath,
402
+ sessionKey: finalized.SessionKey || sessionKey,
403
+ ctx: finalized,
404
+ updateLastRoute: {
405
+ sessionKey,
406
+ channel: OPENCLAW_WORKCLAW_CHANNEL,
407
+ to: message.to,
408
+ accountId,
409
+ },
410
+ onRecordError: (err: unknown) => {
411
+ log?.error?.(`[Session] recordInboundSession failed: ${String(err)}`)
412
+ },
413
+ })
414
+ log?.info?.(`[Session] Recorded session for key=${sessionKey}`)
415
+ }
416
+ catch (err) {
417
+ log?.warn?.(`[Session] recordInboundSession error: ${String(err)}`)
418
+ }
419
+ }
420
+
421
+ return finalized
422
+ }