@workclaw/openclaw-workclaw 1.0.1 → 1.0.12
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/README.md +325 -325
- package/index.ts +4 -0
- package/package.json +43 -43
- package/src/config-schema.ts +1 -1
- package/src/gateway/message-context.ts +422 -422
- package/src/gateway/skills-list-handler.ts +332 -332
- package/src/gateway/tools-list-handler.ts +162 -162
- package/src/media/upload.ts +12 -12
- package/src/outbound/index.ts +183 -183
- package/src/outbound/workclaw-sender.ts +2 -2
- package/src/tools/openclaw-workclaw-cron/api/index.ts +5 -5
- package/src/tools/openclaw-workclaw-system/index.ts +17 -0
- package/src/tools/openclaw-workclaw-system/src/get/index.ts +77 -0
- package/src/tools/openclaw-workclaw-system/src/token/index.ts +93 -0
- package/src/types.ts +2 -2
- package/src/utils/content.ts +5 -5
- package/templates/IDENTITY.md +1 -1
|
@@ -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
|
+
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
|
+
}
|