@workclaw/openclaw-workclaw 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/README.md +325 -0
- package/index.ts +298 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +43 -0
- package/skills/openclaw-workclaw-cron/SKILL.md +458 -0
- package/src/accounts.ts +287 -0
- package/src/api/accounts-api.ts +157 -0
- package/src/api/prompts-api.ts +123 -0
- package/src/api/session-api.ts +247 -0
- package/src/api/skills-api.ts +74 -0
- package/src/api/workspace.ts +43 -0
- package/src/channel.ts +227 -0
- package/src/config-schema.ts +110 -0
- package/src/connection/workclaw-client.ts +656 -0
- package/src/gateway/agent-handlers.ts +557 -0
- package/src/gateway/config-writer.ts +311 -0
- package/src/gateway/message-context.ts +422 -0
- package/src/gateway/message-dispatcher.ts +601 -0
- package/src/gateway/reconnect.ts +149 -0
- package/src/gateway/skills-handler.ts +759 -0
- package/src/gateway/skills-list-handler.ts +332 -0
- package/src/gateway/tools-list-handler.ts +162 -0
- package/src/gateway/workclaw-gateway.ts +521 -0
- package/src/media/upload.ts +168 -0
- package/src/outbound/index.ts +183 -0
- package/src/outbound/workclaw-sender.ts +157 -0
- package/src/runtime.ts +400 -0
- package/src/send.ts +1 -0
- package/src/tools/openclaw-workclaw-cron/api/index.ts +326 -0
- package/src/tools/openclaw-workclaw-cron/index.ts +39 -0
- package/src/tools/openclaw-workclaw-cron/src/add/params.ts +176 -0
- package/src/tools/openclaw-workclaw-cron/src/add/sync.ts +188 -0
- package/src/tools/openclaw-workclaw-cron/src/disable/params.ts +100 -0
- package/src/tools/openclaw-workclaw-cron/src/disable/sync.ts +127 -0
- package/src/tools/openclaw-workclaw-cron/src/enable/params.ts +100 -0
- package/src/tools/openclaw-workclaw-cron/src/enable/sync.ts +127 -0
- package/src/tools/openclaw-workclaw-cron/src/notify/sync.ts +148 -0
- package/src/tools/openclaw-workclaw-cron/src/remove/params.ts +109 -0
- package/src/tools/openclaw-workclaw-cron/src/remove/sync.ts +127 -0
- package/src/tools/openclaw-workclaw-cron/src/update/params.ts +197 -0
- package/src/tools/openclaw-workclaw-cron/src/update/sync.ts +161 -0
- package/src/tools/openclaw-workclaw-cron/types/index.ts +55 -0
- package/src/tools/openclaw-workclaw-cron/utils/index.ts +141 -0
- package/src/types.ts +60 -0
- package/src/utils/content.ts +40 -0
- package/templates/IDENTITY.md +14 -0
- package/templates/SOUL.md +0 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Dispatcher - handles all WorkClaw WebSocket message types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ResolvedOpenclawWorkclawAccount, WorkClawAccountConfig, WorkClawBaseConfig } from '../types.js'
|
|
6
|
+
import type { InboundMessage, ParseWorkClawResult } from './message-context.js'
|
|
7
|
+
|
|
8
|
+
import { resolveAccountByUserIdAndAgentId, resolveOpenclawWorkclawAccount } from '../accounts.js'
|
|
9
|
+
import { getOpenclawWorkclawAccessToken } from '../connection/workclaw-client.js'
|
|
10
|
+
|
|
11
|
+
import { sendMessageOpenclawWorkclaw } from '../outbound/index.js'
|
|
12
|
+
import { getOpenclawWorkclawRuntime, getOpenclawWorkclawWsConnection, setToolContext } from '../runtime.js'
|
|
13
|
+
import { handleAgentCreated, handleAgentDeleted, handleAgentUpdated } from './agent-handlers.js'
|
|
14
|
+
import { saveOpenConversationId, saveWorkClawAgentId, saveWorkClawApiKey, saveWorkClawUserId } from './config-writer.js'
|
|
15
|
+
import { buildInboundContext, parseWorkClawMessage } from './message-context.js'
|
|
16
|
+
|
|
17
|
+
export interface MessageDispatcherContext {
|
|
18
|
+
accountId: string
|
|
19
|
+
account: ResolvedOpenclawWorkclawAccount
|
|
20
|
+
cfg: any
|
|
21
|
+
baseConfig: WorkClawBaseConfig
|
|
22
|
+
accountConfig: WorkClawAccountConfig
|
|
23
|
+
log?: {
|
|
24
|
+
info?: (msg: string) => void
|
|
25
|
+
warn?: (msg: string) => void
|
|
26
|
+
error?: (msg: string) => void
|
|
27
|
+
debug?: (msg: string) => void
|
|
28
|
+
}
|
|
29
|
+
scheduleReconnect: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Log an error safely */
|
|
33
|
+
function logError(log: MessageDispatcherContext['log'], context: string, err: unknown): void {
|
|
34
|
+
const errAny = err as { stack?: string }
|
|
35
|
+
const stack = typeof errAny?.stack === 'string' ? `\nStack: ${errAny.stack}` : ''
|
|
36
|
+
log?.error?.(`Dispatcher ${context}: ${String(err)}${stack}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Individual message handlers
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
async function handlePing(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
|
|
44
|
+
const { accountId, log } = ctx
|
|
45
|
+
const ws = getOpenclawWorkclawWsConnection(accountId)
|
|
46
|
+
if (!ws)
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const pongResponse = {
|
|
51
|
+
code: 200,
|
|
52
|
+
message: 'pong',
|
|
53
|
+
metadata: { contentType: 'application/json' },
|
|
54
|
+
data: JSON.stringify(result.pongData),
|
|
55
|
+
}
|
|
56
|
+
ws.send(JSON.stringify(pongResponse))
|
|
57
|
+
log?.debug?.(`pong`)
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
logError(log, 'Failed to send pong', err)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function handleDisconnect(ctx: MessageDispatcherContext): Promise<void> {
|
|
65
|
+
const { accountId, log, scheduleReconnect } = ctx
|
|
66
|
+
const ws = getOpenclawWorkclawWsConnection(accountId)
|
|
67
|
+
if (ws) {
|
|
68
|
+
try {
|
|
69
|
+
ws.close()
|
|
70
|
+
log?.info?.(`Closed WebSocket due to disconnect command`)
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
logError(log, 'Failed to close WebSocket', err)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
log?.info?.(`WebSocket already cleared, triggering reconnect...`)
|
|
78
|
+
scheduleReconnect()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleAgentEvent(
|
|
83
|
+
ctx: MessageDispatcherContext,
|
|
84
|
+
result: ParseWorkClawResult,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const { cfg, log, baseConfig, accountId } = ctx
|
|
87
|
+
const eventData = result.eventData
|
|
88
|
+
if (!eventData) {
|
|
89
|
+
log?.warn?.(`Received ${result.type} event but no data`)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const agentId = String(eventData.id || '')
|
|
94
|
+
const userId = String(eventData.futureId || '')
|
|
95
|
+
|
|
96
|
+
if (!agentId) {
|
|
97
|
+
log?.warn?.(`Received ${result.type} event but missing agent id`)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
log?.info?.(`Processing ${result.type} event for agent ${agentId}, userId: ${userId}`)
|
|
102
|
+
|
|
103
|
+
const accountData = {
|
|
104
|
+
agentId,
|
|
105
|
+
userId,
|
|
106
|
+
nickName: eventData.nickName,
|
|
107
|
+
phone: eventData.phone,
|
|
108
|
+
status: eventData.status,
|
|
109
|
+
introduction: eventData.introduction,
|
|
110
|
+
name: eventData.name,
|
|
111
|
+
tip: eventData.tip,
|
|
112
|
+
characterSettings: eventData.characterSettings,
|
|
113
|
+
personFeatures: eventData.personFeatures,
|
|
114
|
+
workFeatures: eventData.workFeatures,
|
|
115
|
+
learningFeatures: eventData.learningFeatures,
|
|
116
|
+
socializeFeatures: eventData.socializeFeatures,
|
|
117
|
+
job: eventData.job,
|
|
118
|
+
city: eventData.city,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
switch (result.type) {
|
|
122
|
+
case 'agent_created':
|
|
123
|
+
await handleAgentCreated(accountData, cfg, log)
|
|
124
|
+
handleAgentCreatedMessage(baseConfig, agentId, accountId, log)
|
|
125
|
+
break
|
|
126
|
+
case 'agent_updated':
|
|
127
|
+
await handleAgentUpdated(accountData, cfg, log)
|
|
128
|
+
break
|
|
129
|
+
case 'agent_deleted':
|
|
130
|
+
await handleAgentDeleted(accountData, cfg, log)
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function handleSkillsProcessingEvent(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
|
|
136
|
+
const { accountId, baseConfig, log } = ctx
|
|
137
|
+
if (!result.skillsEvent)
|
|
138
|
+
return
|
|
139
|
+
try {
|
|
140
|
+
const { handleSkillsEvent } = await import('./skills-handler.js')
|
|
141
|
+
const connConfig = {
|
|
142
|
+
baseUrl: baseConfig.baseUrl || '',
|
|
143
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
144
|
+
appKey: baseConfig.appKey,
|
|
145
|
+
appSecret: baseConfig.appSecret,
|
|
146
|
+
localIp: baseConfig.localIp,
|
|
147
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
148
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
149
|
+
}
|
|
150
|
+
const tokenCacheKey = baseConfig.appKey || accountId
|
|
151
|
+
const token = await getOpenclawWorkclawAccessToken(tokenCacheKey, connConfig)
|
|
152
|
+
await handleSkillsEvent(result.skillsEvent, token, connConfig.baseUrl, connConfig.appKey || accountId, log)
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
logError(log, 'handleSkillsEvent failed', err)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function handleToolsListEvent(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
|
|
160
|
+
const { accountId, baseConfig, log } = ctx
|
|
161
|
+
if (!result.eventData)
|
|
162
|
+
return
|
|
163
|
+
try {
|
|
164
|
+
const { handleToolsListEvent } = await import('./tools-list-handler.js')
|
|
165
|
+
const connConfig = {
|
|
166
|
+
baseUrl: baseConfig.baseUrl || '',
|
|
167
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
168
|
+
appKey: baseConfig.appKey,
|
|
169
|
+
appSecret: baseConfig.appSecret,
|
|
170
|
+
localIp: baseConfig.localIp,
|
|
171
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
172
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
173
|
+
}
|
|
174
|
+
const tokenCacheKey = baseConfig.appKey || accountId
|
|
175
|
+
const token = await getOpenclawWorkclawAccessToken(tokenCacheKey, connConfig)
|
|
176
|
+
await handleToolsListEvent(result.eventData, connConfig.baseUrl, token, log)
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
logError(log, 'handleToolsListEvent failed', err)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function handleSkillsListEvent(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
|
|
184
|
+
const { accountId, baseConfig, log } = ctx
|
|
185
|
+
if (!result.eventData)
|
|
186
|
+
return
|
|
187
|
+
try {
|
|
188
|
+
const { handleSkillsListEvent } = await import('./skills-list-handler.js')
|
|
189
|
+
const connConfig = {
|
|
190
|
+
baseUrl: baseConfig.baseUrl || '',
|
|
191
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
192
|
+
appKey: baseConfig.appKey,
|
|
193
|
+
appSecret: baseConfig.appSecret,
|
|
194
|
+
localIp: baseConfig.localIp,
|
|
195
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
196
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
197
|
+
}
|
|
198
|
+
const tokenCacheKey = baseConfig.appKey || accountId
|
|
199
|
+
const token = await getOpenclawWorkclawAccessToken(tokenCacheKey, connConfig)
|
|
200
|
+
await handleSkillsListEvent(result.eventData, connConfig.baseUrl, token, log)
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
logError(log, 'handleSkillsListEvent failed', err)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function handleInitAgentEvent(
|
|
208
|
+
ctx: MessageDispatcherContext,
|
|
209
|
+
result: ParseWorkClawResult,
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
const { accountId, cfg, baseConfig, log } = ctx
|
|
212
|
+
const initStartedAt = Date.now()
|
|
213
|
+
const eventData = result.eventData
|
|
214
|
+
|
|
215
|
+
if (!eventData) {
|
|
216
|
+
log?.warn?.(`init_agent received but no data`)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
log?.info?.(
|
|
221
|
+
`init_agent accountId=${accountId} eventDataType=${typeof eventData} preview=${JSON.stringify(eventData).slice(0, 200)}`,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const connConfig = {
|
|
225
|
+
baseUrl: baseConfig.baseUrl || '',
|
|
226
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
227
|
+
appKey: baseConfig.appKey,
|
|
228
|
+
appSecret: baseConfig.appSecret,
|
|
229
|
+
localIp: baseConfig.localIp,
|
|
230
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
231
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Fetch and save apiKey
|
|
235
|
+
try {
|
|
236
|
+
const { getOpenclawWorkclawModelConfigByAppKey } = await import('../connection/workclaw-client.js')
|
|
237
|
+
const tokenCacheKey = baseConfig.appKey || accountId
|
|
238
|
+
const { baseUrl: modelBaseUrl, apiKey } = await getOpenclawWorkclawModelConfigByAppKey(tokenCacheKey, connConfig)
|
|
239
|
+
const maskedKey = apiKey ? `${String(apiKey).slice(0, 6)}...(${String(apiKey).length})` : 'missing'
|
|
240
|
+
|
|
241
|
+
log?.info?.(`init_agent model fetched baseUrl=${modelBaseUrl} apiKey=${maskedKey}`)
|
|
242
|
+
|
|
243
|
+
await saveWorkClawApiKey(apiKey, cfg, log)
|
|
244
|
+
log?.info?.(`init_agent model config saved elapsedMs=${Date.now() - initStartedAt}`)
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
logError(log, 'init_agent model fetch/save failed', err)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Save default agentId
|
|
251
|
+
const agentId = String(eventData?.id || eventData?.agentId || '')
|
|
252
|
+
if (agentId) {
|
|
253
|
+
try {
|
|
254
|
+
await saveWorkClawAgentId('default', agentId, cfg, log)
|
|
255
|
+
log?.info?.(`init_agent saved default agentId=${agentId}`)
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
logError(log, 'init_agent agentId save failed', err)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Save default userId
|
|
263
|
+
const userId = String(eventData?.futureId || eventData?.userId || '')
|
|
264
|
+
if (userId) {
|
|
265
|
+
try {
|
|
266
|
+
await saveWorkClawUserId('default', userId, cfg, log)
|
|
267
|
+
log?.info?.(`init_agent saved default userId=${userId}`)
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
logError(log, 'init_agent userId save failed', err)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
log?.info?.(`init_agent handled elapsedMs=${Date.now() - initStartedAt}`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function handleAgentMessage(
|
|
278
|
+
ctx: MessageDispatcherContext,
|
|
279
|
+
data: string,
|
|
280
|
+
result: ParseWorkClawResult,
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
const { accountId, account, cfg, accountConfig, log } = ctx
|
|
283
|
+
const { message } = result
|
|
284
|
+
if (!message)
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
// 这里的msgAgentId是云平台的agentId
|
|
288
|
+
const { text, userId, openConversationId, messageId: parsedMessageId, agentId: msgAgentId } = message
|
|
289
|
+
|
|
290
|
+
// Resolve account from message
|
|
291
|
+
let messageAccountId = accountId
|
|
292
|
+
let messageAccount = account
|
|
293
|
+
|
|
294
|
+
if (userId && msgAgentId) {
|
|
295
|
+
const resolvedAccountId = resolveAccountByUserIdAndAgentId(cfg, String(userId), String(msgAgentId))
|
|
296
|
+
if (resolvedAccountId) {
|
|
297
|
+
const resolved = resolveOpenclawWorkclawAccount({ cfg, accountId: resolvedAccountId })
|
|
298
|
+
messageAccountId = resolvedAccountId
|
|
299
|
+
messageAccount = resolved
|
|
300
|
+
log?.info?.(`Resolved account ${resolvedAccountId} from message (userId=${userId}, agentId=${msgAgentId})`)
|
|
301
|
+
}
|
|
302
|
+
else if (String(msgAgentId) !== String(accountConfig.agentId)) {
|
|
303
|
+
log?.warn?.(`Account not found for userId=${userId}, agentId=${msgAgentId}, ignoring`)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const resolvedAccountConfig = messageAccount.config as unknown as WorkClawAccountConfig
|
|
309
|
+
const msgAgentIdResolved = msgAgentId ?? accountConfig.agentId
|
|
310
|
+
|
|
311
|
+
// Save agentId if not already set
|
|
312
|
+
if (msgAgentId && !resolvedAccountConfig.agentId) {
|
|
313
|
+
try {
|
|
314
|
+
await saveWorkClawAgentId(messageAccountId, msgAgentId, cfg, log)
|
|
315
|
+
log?.info?.(`Saved agentId ${msgAgentId} to account ${messageAccountId}`)
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
logError(log, 'saveWorkClawAgentId failed', err)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Save openConversationId if new
|
|
323
|
+
if (openConversationId && openConversationId !== resolvedAccountConfig.openConversationId) {
|
|
324
|
+
try {
|
|
325
|
+
await saveOpenConversationId(messageAccountId, openConversationId, cfg, log)
|
|
326
|
+
log?.info?.(`Saved openConversationId ${openConversationId} to account ${messageAccountId}`)
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
logError(log, 'saveOpenConversationId failed', err)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Save userId if not already set (directly to openclawWorkclaw level)
|
|
334
|
+
const openclawWorkclawConfig = cfg?.channels?.['openclaw-workclaw'] || {}
|
|
335
|
+
if (userId && !openclawWorkclawConfig.userId) {
|
|
336
|
+
try {
|
|
337
|
+
await saveWorkClawUserId(messageAccountId, userId, cfg, log)
|
|
338
|
+
log?.info?.(`Saved userId ${userId} to openclawWorkclaw config`)
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
logError(log, 'saveWorkClawUserId failed', err)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const resolvedOpenConversationId = openConversationId || resolvedAccountConfig.openConversationId || ''
|
|
346
|
+
const to = userId
|
|
347
|
+
const bodyText = text || data
|
|
348
|
+
|
|
349
|
+
if (!bodyText) {
|
|
350
|
+
log?.warn?.(`Empty body, ignoring`)
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Route via bindings
|
|
355
|
+
const bindings = cfg?.bindings ?? []
|
|
356
|
+
let targetAgentId = 'default'
|
|
357
|
+
const matchedBinding = bindings.find((binding: any) => {
|
|
358
|
+
const match = binding?.match
|
|
359
|
+
if (!match || match.channel !== 'openclaw-workclaw')
|
|
360
|
+
return false
|
|
361
|
+
return match.accountId === messageAccountId || match.accountId === '*'
|
|
362
|
+
})
|
|
363
|
+
if (matchedBinding) {
|
|
364
|
+
targetAgentId = matchedBinding.agentId
|
|
365
|
+
log?.info?.(`Routing to agent ${targetAgentId} via binding (account: ${messageAccountId})`)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!targetAgentId) {
|
|
369
|
+
log?.warn?.(`Missing target, ignoring`)
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 构建用于解析 sessionKey 的消息对象
|
|
374
|
+
// to = agentId (智能体/机器人), from = userId (用户)
|
|
375
|
+
const inboundMessage: InboundMessage = {
|
|
376
|
+
text: bodyText,
|
|
377
|
+
to,
|
|
378
|
+
from: userId,
|
|
379
|
+
chatType: 'dm',
|
|
380
|
+
messageId: parsedMessageId,
|
|
381
|
+
rawPayload: data,
|
|
382
|
+
timestamp: Date.now(),
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const pluginRuntime = getOpenclawWorkclawRuntime()
|
|
386
|
+
|
|
387
|
+
// 使用扩展后的 buildInboundContext,它会处理完整的 session 流程
|
|
388
|
+
const finalized = await buildInboundContext(
|
|
389
|
+
inboundMessage,
|
|
390
|
+
messageAccountId,
|
|
391
|
+
messageAccount.config,
|
|
392
|
+
targetAgentId,
|
|
393
|
+
pluginRuntime,
|
|
394
|
+
true,
|
|
395
|
+
log,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
await pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
399
|
+
ctx: finalized,
|
|
400
|
+
cfg,
|
|
401
|
+
replyOptions: {
|
|
402
|
+
onAgentRunStart(runId: string) {
|
|
403
|
+
// 保存 runId 和 ctx 的关联,供 hook 使用
|
|
404
|
+
setToolContext(runId, {
|
|
405
|
+
target: targetAgentId,
|
|
406
|
+
replyToMessageId: parsedMessageId,
|
|
407
|
+
openConversationId: resolvedOpenConversationId,
|
|
408
|
+
accountId: messageAccountId,
|
|
409
|
+
agentId: String(msgAgentIdResolved),
|
|
410
|
+
sessionKey: (finalized.SessionKey as string) || '',
|
|
411
|
+
})
|
|
412
|
+
log?.info?.(`[AGENT] onAgentRunStart: runId=${runId} target=${targetAgentId}`)
|
|
413
|
+
},
|
|
414
|
+
/* onToolStart(payload) {
|
|
415
|
+
log?.info?.(`[AGENT] onToolStart: ${JSON.stringify(payload)}`);
|
|
416
|
+
if (payload.phase === "start") {
|
|
417
|
+
currentToolName = payload.name ?? null;
|
|
418
|
+
const hint = getToolStartHint(currentToolName);
|
|
419
|
+
log?.info?.(`[TOOL] Starting tool: ${currentToolName}, sending hint: ${hint}`);
|
|
420
|
+
sendToolMessage(hint, true).catch((err) => {
|
|
421
|
+
logError(log, "Tool start hint send failed", err);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
} */
|
|
425
|
+
},
|
|
426
|
+
dispatcherOptions: {
|
|
427
|
+
deliver: async (payload: any, info?: any) => {
|
|
428
|
+
// 记录每次 deliver 调用,帮助调试 info.kind 问题
|
|
429
|
+
log?.info?.(`[DELIVER] deliver called: info.kind=${info?.kind}, payload.text=${String(payload?.text ?? '').substring(0, 100)}`)
|
|
430
|
+
const resolvedReplyToId = payload?.replyToId || parsedMessageId
|
|
431
|
+
const textOut = String(payload.text ?? payload.body ?? '')
|
|
432
|
+
const target = to || String(payload.to ?? '')
|
|
433
|
+
|
|
434
|
+
if (!target) {
|
|
435
|
+
log?.warn?.(`Skipping reply: no target`)
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!textOut.trim()) {
|
|
440
|
+
log?.warn?.(`Empty reply text, skipping`)
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (info?.kind === 'tool')
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
// 处理工具类型的 deliver
|
|
448
|
+
/* if (info?.kind === "tool") {
|
|
449
|
+
// log?.info?.(`[DELIVER] tool reply: ${JSON.stringify(payload)}`);
|
|
450
|
+
const toolText = payload.text ?? "";
|
|
451
|
+
if (toolText.trim()) {
|
|
452
|
+
// 使用 formattedToolOutput 格式发送
|
|
453
|
+
try {
|
|
454
|
+
await sendMessageOpenclawWorkclaw({
|
|
455
|
+
cfg,
|
|
456
|
+
to: targetAgentId,
|
|
457
|
+
text: JSON.stringify({
|
|
458
|
+
name: currentToolName || "Unknown",
|
|
459
|
+
content: toolText,
|
|
460
|
+
state: "result",
|
|
461
|
+
}),
|
|
462
|
+
msgType: "26",
|
|
463
|
+
accountId: messageAccountId,
|
|
464
|
+
openConversationId: resolvedOpenConversationId,
|
|
465
|
+
agentId: msgAgentIdResolved,
|
|
466
|
+
replyToMessageId: parsedMessageId,
|
|
467
|
+
});
|
|
468
|
+
} catch (err) {
|
|
469
|
+
logError(log, "Tool result send failed", err);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
} */
|
|
474
|
+
|
|
475
|
+
log?.info?.(`[DELIVER] is last: ${info?.kind === 'final'}`)
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const result = await sendMessageOpenclawWorkclaw({
|
|
479
|
+
cfg,
|
|
480
|
+
to: target,
|
|
481
|
+
text: textOut,
|
|
482
|
+
accountId: messageAccountId,
|
|
483
|
+
openConversationId: resolvedOpenConversationId,
|
|
484
|
+
agentId: msgAgentIdResolved,
|
|
485
|
+
replyToMessageId: resolvedReplyToId,
|
|
486
|
+
last: info?.kind === 'final',
|
|
487
|
+
})
|
|
488
|
+
log?.info?.(`Reply sent messageId=${result.messageId}`)
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
logError(log, 'Reply send failed', err)
|
|
492
|
+
throw err
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// Main dispatcher
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
export async function dispatchOpenclawWorkclawMessage(
|
|
504
|
+
data: string,
|
|
505
|
+
ctx: MessageDispatcherContext,
|
|
506
|
+
): Promise<void> {
|
|
507
|
+
const { log } = ctx
|
|
508
|
+
|
|
509
|
+
const rawHasAuthToken
|
|
510
|
+
= data.includes('authToken')
|
|
511
|
+
|| data.toLowerCase().includes('authorization')
|
|
512
|
+
|| data.toLowerCase().includes('bearer ')
|
|
513
|
+
const rawPreview = data.length > 300 ? `${data.slice(0, 300)}...` : data
|
|
514
|
+
log?.info?.(
|
|
515
|
+
`Raw message bytes=${data.length} hasAuthToken=${rawHasAuthToken} preview=${rawPreview}`,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
const result = parseWorkClawMessage(data, log)
|
|
519
|
+
if (!result) {
|
|
520
|
+
log?.warn?.(`Failed to parse message`)
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
log?.info?.(`Message type=${result.type}`)
|
|
525
|
+
|
|
526
|
+
switch (result.type) {
|
|
527
|
+
case 'ping':
|
|
528
|
+
await handlePing(ctx, result)
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
case 'disconnect':
|
|
532
|
+
await handleDisconnect(ctx)
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
case 'agent_created':
|
|
536
|
+
case 'agent_updated':
|
|
537
|
+
case 'agent_deleted':
|
|
538
|
+
await handleAgentEvent(ctx, result)
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
case 'skills_event':
|
|
542
|
+
await handleSkillsProcessingEvent(ctx, result)
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
case 'tools_list':
|
|
546
|
+
await handleToolsListEvent(ctx, result)
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
case 'skills_list':
|
|
550
|
+
await handleSkillsListEvent(ctx, result)
|
|
551
|
+
return
|
|
552
|
+
|
|
553
|
+
case 'init_agent':
|
|
554
|
+
await handleInitAgentEvent(ctx, result)
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
case 'ignored':
|
|
558
|
+
log?.info?.(`Ignoring message type=${result.type}`)
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
case 'agent_message':
|
|
562
|
+
await handleAgentMessage(ctx, data, result)
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
default:
|
|
566
|
+
log?.info?.(`Unknown message type: ${result.type}, ignoring`)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 创建成功回调
|
|
571
|
+
async function handleAgentCreatedMessage(baseConfig: WorkClawBaseConfig, agentId: string, accountId: string, log?: any): Promise<void> {
|
|
572
|
+
// 通知
|
|
573
|
+
try {
|
|
574
|
+
const callbackUrl = `${baseConfig.baseUrl}/open-apis/instance/agent-created/status`
|
|
575
|
+
const tokenCacheKey = baseConfig.appKey || accountId
|
|
576
|
+
const callbackData = {
|
|
577
|
+
agentId,
|
|
578
|
+
success: true,
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
log?.info?.(`[AgentCreated] Calling callback API: ${callbackUrl}`)
|
|
582
|
+
const token = await getOpenclawWorkclawAccessToken(tokenCacheKey, baseConfig)
|
|
583
|
+
|
|
584
|
+
const { request } = await import('undici')
|
|
585
|
+
const response = await request(callbackUrl, {
|
|
586
|
+
method: 'POST',
|
|
587
|
+
headers: {
|
|
588
|
+
'Content-Type': 'application/json',
|
|
589
|
+
'Authorization': `Bearer ${token}`,
|
|
590
|
+
},
|
|
591
|
+
body: JSON.stringify(callbackData),
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
const responseBody = await response.body.text()
|
|
595
|
+
log?.info?.(`[AgentCreated] Callback API response: status=${response.statusCode}, body=${responseBody}`)
|
|
596
|
+
}
|
|
597
|
+
catch (callbackErr) {
|
|
598
|
+
// 接口调用失败不影响主流程,只记录错误
|
|
599
|
+
log?.error?.(`[AgentCreated] Callback API failed: ${String(callbackErr)}`)
|
|
600
|
+
}
|
|
601
|
+
}
|