@workclaw/openclaw-workclaw 1.0.20 → 1.0.23
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 +0 -1
- package/api.ts +3 -0
- package/index.ts +325 -0
- package/package.json +3 -11
- package/setup-entry.ts +13 -0
- package/src/accounts.ts +360 -0
- package/src/api/accounts-api.ts +156 -0
- package/src/api/prompts-api.ts +122 -0
- package/src/api/session-api.ts +246 -0
- package/src/api/skills-api.ts +74 -0
- package/src/api/workspace.ts +45 -0
- package/src/channel.ts +226 -0
- package/src/config-schema.ts +60 -0
- package/src/connection/workclaw-client.ts +618 -0
- package/src/gateway/agent-handlers.ts +551 -0
- package/src/gateway/config-writer.ts +378 -0
- package/src/gateway/cron-tasks-handler.ts +230 -0
- package/src/gateway/message-context.ts +645 -0
- package/src/gateway/message-dispatcher.ts +688 -0
- package/src/gateway/reconnect.ts +260 -0
- package/src/gateway/skills-handler.ts +805 -0
- package/src/gateway/skills-list-handler.ts +332 -0
- package/src/gateway/tools-list-handler.ts +161 -0
- package/src/gateway/workclaw-gateway.ts +298 -0
- package/src/media/upload.ts +168 -0
- package/src/outbound/index.ts +191 -0
- package/src/outbound/workclaw-sender.ts +161 -0
- package/src/runtime.ts +520 -0
- package/src/secret-contract-api.ts +4 -0
- package/src/send.ts +1 -0
- package/src/setup-api.ts +3 -0
- package/src/setup-core.ts +25 -0
- package/src/setup-surface.ts +498 -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 +177 -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 +195 -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/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 +50 -0
- package/src/utils/content.ts +40 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Dispatcher - handles all WorkClaw WebSocket message types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ResolvedWorkclawAccount, WorkClawAccountConfig, WorkClawBaseConfig } from '../types.js'
|
|
6
|
+
import type { InboundMessage, ParseWorkClawResult } from './message-context.js'
|
|
7
|
+
|
|
8
|
+
import { maybeResolveTextAlias, resolveNativeCommandSessionTargets } from 'openclaw/plugin-sdk/command-auth'
|
|
9
|
+
import { request } from 'undici'
|
|
10
|
+
import { resolveAccountByUserIdAndAgentId, resolveWorkclawAccountWithCache } from '../accounts.js'
|
|
11
|
+
import { getWorkclawAccessToken, getWorkclawModelConfigByAppKey } from '../connection/workclaw-client.js'
|
|
12
|
+
import { sendMessageWorkclaw } from '../outbound/index.js'
|
|
13
|
+
import { getWorkclawRuntime, getWorkclawWsConnection, setToolContext } from '../runtime.js'
|
|
14
|
+
|
|
15
|
+
import { handleAgentCreated, handleAgentDeleted, handleAgentUpdated } from './agent-handlers.js'
|
|
16
|
+
import { initWorkclawAgent, saveOpenConversationId } from './config-writer.js'
|
|
17
|
+
import { buildInboundContext, parseWorkClawMessage } from './message-context.js'
|
|
18
|
+
|
|
19
|
+
import { handleSkillsEvent } from './skills-handler.js'
|
|
20
|
+
import { handleSkillsListEvent as doHandleSkillsListEvent } from './skills-list-handler.js'
|
|
21
|
+
import { handleToolsListEvent as doHandleToolsListEvent } from './tools-list-handler.js'
|
|
22
|
+
|
|
23
|
+
export interface MessageDispatcherContext {
|
|
24
|
+
accountId: string
|
|
25
|
+
account: ResolvedWorkclawAccount
|
|
26
|
+
cfg: any
|
|
27
|
+
baseConfig: WorkClawBaseConfig
|
|
28
|
+
accountConfig: WorkClawAccountConfig
|
|
29
|
+
log?: {
|
|
30
|
+
info?: (msg: string) => void
|
|
31
|
+
warn?: (msg: string) => void
|
|
32
|
+
error?: (msg: string) => void
|
|
33
|
+
debug?: (msg: string) => void
|
|
34
|
+
}
|
|
35
|
+
scheduleReconnect: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Log an error safely */
|
|
39
|
+
function logError(log: MessageDispatcherContext['log'], context: string, err: unknown): void {
|
|
40
|
+
const errAny = err as { stack?: string }
|
|
41
|
+
const stack = typeof errAny?.stack === 'string' ? `\nStack: ${errAny.stack}` : ''
|
|
42
|
+
log?.error?.(`Dispatcher ${context}: ${String(err)}${stack}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Individual message handlers
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
async function handlePing(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
|
|
50
|
+
const { accountId, log } = ctx
|
|
51
|
+
const ws = getWorkclawWsConnection(accountId)
|
|
52
|
+
if (!ws)
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const pongResponse = {
|
|
57
|
+
code: 200,
|
|
58
|
+
message: 'pong',
|
|
59
|
+
metadata: { contentType: 'application/json' },
|
|
60
|
+
data: JSON.stringify(result.pongData),
|
|
61
|
+
}
|
|
62
|
+
ws.send(JSON.stringify(pongResponse))
|
|
63
|
+
log?.debug?.(`pong`)
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
logError(log, 'Failed to send pong', err)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function handleDisconnect(ctx: MessageDispatcherContext): Promise<void> {
|
|
71
|
+
const { accountId, log, scheduleReconnect } = ctx
|
|
72
|
+
const ws = getWorkclawWsConnection(accountId)
|
|
73
|
+
if (ws) {
|
|
74
|
+
try {
|
|
75
|
+
ws.close()
|
|
76
|
+
log?.info?.(`Closed WebSocket due to disconnect command`)
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
logError(log, 'Failed to close WebSocket', err)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
log?.info?.(`WebSocket already cleared, triggering reconnect...`)
|
|
84
|
+
scheduleReconnect()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function handleAgentEvent(
|
|
89
|
+
ctx: MessageDispatcherContext,
|
|
90
|
+
result: ParseWorkClawResult,
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
const { cfg, log, baseConfig, accountId } = ctx
|
|
93
|
+
const eventData = result.eventData
|
|
94
|
+
if (!eventData) {
|
|
95
|
+
log?.warn?.(`Received ${result.type} event but no data`)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const agentId = String(eventData.id || '')
|
|
100
|
+
const userId = String(eventData.futureId || '')
|
|
101
|
+
|
|
102
|
+
if (!agentId) {
|
|
103
|
+
log?.warn?.(`Received ${result.type} event but missing agent id`)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
log?.info?.(`Processing ${result.type} event for agent ${agentId}, userId: ${userId}`)
|
|
108
|
+
|
|
109
|
+
const accountData = {
|
|
110
|
+
agentId,
|
|
111
|
+
userId,
|
|
112
|
+
nickName: eventData.nickName,
|
|
113
|
+
phone: eventData.phone,
|
|
114
|
+
status: eventData.status,
|
|
115
|
+
introduction: eventData.introduction,
|
|
116
|
+
name: eventData.name,
|
|
117
|
+
tip: eventData.tip,
|
|
118
|
+
characterSettings: eventData.characterSettings,
|
|
119
|
+
personFeatures: eventData.personFeatures,
|
|
120
|
+
workFeatures: eventData.workFeatures,
|
|
121
|
+
learningFeatures: eventData.learningFeatures,
|
|
122
|
+
socializeFeatures: eventData.socializeFeatures,
|
|
123
|
+
job: eventData.job,
|
|
124
|
+
city: eventData.city,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
switch (result.type) {
|
|
128
|
+
case 'agent_created':
|
|
129
|
+
await handleAgentCreated(accountData, cfg, log)
|
|
130
|
+
handleAgentCreatedMessage(baseConfig, agentId, accountId, log)
|
|
131
|
+
break
|
|
132
|
+
case 'agent_updated':
|
|
133
|
+
await handleAgentUpdated(accountData, cfg, log)
|
|
134
|
+
break
|
|
135
|
+
case 'agent_deleted':
|
|
136
|
+
await handleAgentDeleted(accountData, cfg, log)
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function handleSkillsProcessingEvent(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
|
|
142
|
+
const { accountId, baseConfig, log } = ctx
|
|
143
|
+
if (!result.eventData)
|
|
144
|
+
return
|
|
145
|
+
try {
|
|
146
|
+
const connConfig = {
|
|
147
|
+
baseUrl: baseConfig.baseUrl || '',
|
|
148
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
149
|
+
appKey: baseConfig.appKey,
|
|
150
|
+
appSecret: baseConfig.appSecret,
|
|
151
|
+
localIp: baseConfig.localIp,
|
|
152
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
153
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
154
|
+
}
|
|
155
|
+
const tokenCacheKey = baseConfig.appKey || accountId
|
|
156
|
+
const token = await getWorkclawAccessToken(tokenCacheKey, connConfig)
|
|
157
|
+
await handleSkillsEvent(result.eventData, token, connConfig.baseUrl, connConfig.appKey || accountId, log)
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
logError(log, 'handleSkillsEvent failed', err)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function handleToolsListEvent(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
|
|
165
|
+
const { accountId, baseConfig, log } = ctx
|
|
166
|
+
if (!result.eventData)
|
|
167
|
+
return
|
|
168
|
+
try {
|
|
169
|
+
const connConfig = {
|
|
170
|
+
baseUrl: baseConfig.baseUrl || '',
|
|
171
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
172
|
+
appKey: baseConfig.appKey,
|
|
173
|
+
appSecret: baseConfig.appSecret,
|
|
174
|
+
localIp: baseConfig.localIp,
|
|
175
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
176
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
177
|
+
}
|
|
178
|
+
const tokenCacheKey = baseConfig.appKey || accountId
|
|
179
|
+
const token = await getWorkclawAccessToken(tokenCacheKey, connConfig)
|
|
180
|
+
await doHandleToolsListEvent(result.eventData, connConfig.baseUrl, token, log)
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
logError(log, 'handleToolsListEvent failed', err)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function handleSkillsListEvent(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
|
|
188
|
+
const { accountId, baseConfig, log } = ctx
|
|
189
|
+
if (!result.eventData)
|
|
190
|
+
return
|
|
191
|
+
try {
|
|
192
|
+
const connConfig = {
|
|
193
|
+
baseUrl: baseConfig.baseUrl || '',
|
|
194
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
195
|
+
appKey: baseConfig.appKey,
|
|
196
|
+
appSecret: baseConfig.appSecret,
|
|
197
|
+
localIp: baseConfig.localIp,
|
|
198
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
199
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
200
|
+
}
|
|
201
|
+
const tokenCacheKey = baseConfig.appKey || accountId
|
|
202
|
+
const token = await getWorkclawAccessToken(tokenCacheKey, connConfig)
|
|
203
|
+
await doHandleSkillsListEvent(result.eventData, connConfig.baseUrl, token, log)
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
logError(log, 'handleSkillsListEvent failed', err)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function handleInitAgentEvent(
|
|
211
|
+
ctx: MessageDispatcherContext,
|
|
212
|
+
result: ParseWorkClawResult,
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
const { accountId, cfg, baseConfig, log } = ctx
|
|
215
|
+
const initStartedAt = Date.now()
|
|
216
|
+
const eventData = result.eventData
|
|
217
|
+
|
|
218
|
+
if (!eventData) {
|
|
219
|
+
log?.warn?.(`init_agent received but no data`)
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
log?.info?.(
|
|
224
|
+
`init_agent accountId=${accountId} eventDataType=${typeof eventData} preview=${JSON.stringify(eventData).slice(0, 200)}`,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
const connConfig = {
|
|
228
|
+
baseUrl: baseConfig.baseUrl || '',
|
|
229
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
230
|
+
appKey: baseConfig.appKey,
|
|
231
|
+
appSecret: baseConfig.appSecret,
|
|
232
|
+
localIp: baseConfig.localIp,
|
|
233
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
234
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Fetch apiKey and init agent config in one write
|
|
238
|
+
try {
|
|
239
|
+
const tokenCacheKey = baseConfig.appKey || accountId
|
|
240
|
+
const { baseUrl: modelBaseUrl, apiKey } = await getWorkclawModelConfigByAppKey(tokenCacheKey, connConfig)
|
|
241
|
+
const maskedKey = apiKey ? `${String(apiKey).slice(0, 6)}...(${String(apiKey).length})` : 'missing'
|
|
242
|
+
|
|
243
|
+
log?.info?.(`init_agent model fetched baseUrl=${modelBaseUrl} apiKey=${maskedKey}`)
|
|
244
|
+
|
|
245
|
+
const agentId = String(eventData?.id || eventData?.agentId || '')
|
|
246
|
+
const userId = String(eventData?.futureId || eventData?.userId || '')
|
|
247
|
+
|
|
248
|
+
await initWorkclawAgent({ apiKey, agentId, userId, accountId: 'default' }, cfg, log)
|
|
249
|
+
log?.info?.(`init_agent config saved elapsedMs=${Date.now() - initStartedAt}`)
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
logError(log, 'init_agent failed', err)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
log?.info?.(`init_agent handled elapsedMs=${Date.now() - initStartedAt}`)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function handleCronTaskEvent(ctx: MessageDispatcherContext, result: ParseWorkClawResult): Promise<void> {
|
|
259
|
+
const { accountId, baseConfig, log } = ctx
|
|
260
|
+
if (!result.cronTaskEvent)
|
|
261
|
+
return
|
|
262
|
+
try {
|
|
263
|
+
const { handleCronTaskEvent } = await import('./cron-tasks-handler.js')
|
|
264
|
+
const connConfig = {
|
|
265
|
+
baseUrl: baseConfig.baseUrl || '',
|
|
266
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
267
|
+
appKey: baseConfig.appKey,
|
|
268
|
+
appSecret: baseConfig.appSecret,
|
|
269
|
+
localIp: baseConfig.localIp,
|
|
270
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
271
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
272
|
+
}
|
|
273
|
+
const tokenCacheKey = baseConfig.appKey || accountId
|
|
274
|
+
const token = await getWorkclawAccessToken(tokenCacheKey, connConfig)
|
|
275
|
+
await handleCronTaskEvent(result.cronTaskEvent, token, connConfig.baseUrl, connConfig.appKey || accountId, log)
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
logError(log, 'handleCronTaskEvent failed', err)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function handleAgentMessage(
|
|
283
|
+
ctx: MessageDispatcherContext,
|
|
284
|
+
data: string,
|
|
285
|
+
result: ParseWorkClawResult,
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
const { accountId, account, cfg, accountConfig, log } = ctx
|
|
288
|
+
const { message } = result
|
|
289
|
+
if (!message)
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
// 这里的msgAgentId是云平台的agentId
|
|
293
|
+
const { text, userId, openConversationId, messageId: parsedMessageId, agentId: msgAgentId } = message
|
|
294
|
+
|
|
295
|
+
// Resolve account from message
|
|
296
|
+
let messageAccountId = accountId
|
|
297
|
+
let messageAccount = account
|
|
298
|
+
|
|
299
|
+
if (userId && msgAgentId) {
|
|
300
|
+
const resolvedAccountId = resolveAccountByUserIdAndAgentId(cfg, String(userId), String(msgAgentId))
|
|
301
|
+
if (resolvedAccountId) {
|
|
302
|
+
const resolved = resolveWorkclawAccountWithCache({ cfg, accountId: resolvedAccountId })
|
|
303
|
+
messageAccountId = resolvedAccountId
|
|
304
|
+
messageAccount = resolved
|
|
305
|
+
log?.info?.(`Resolved account ${resolvedAccountId} from message (userId=${userId}, agentId=${msgAgentId})`)
|
|
306
|
+
}
|
|
307
|
+
else if (String(msgAgentId) !== String(accountConfig.agentId)) {
|
|
308
|
+
log?.warn?.(`Account not found for userId=${userId}, agentId=${msgAgentId}, ignoring`)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const resolvedAccountConfig = messageAccount.config as unknown as WorkClawAccountConfig
|
|
314
|
+
const msgAgentIdResolved = msgAgentId ?? accountConfig.agentId
|
|
315
|
+
|
|
316
|
+
// Save openConversationId if new
|
|
317
|
+
if (openConversationId && openConversationId !== resolvedAccountConfig.openConversationId) {
|
|
318
|
+
try {
|
|
319
|
+
await saveOpenConversationId(messageAccountId, userId, openConversationId, cfg, log)
|
|
320
|
+
log?.info?.(`Saved openConversationId ${openConversationId} to account ${messageAccountId}`)
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
logError(log, 'saveOpenConversationId failed', err)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const resolvedOpenConversationId = openConversationId || resolvedAccountConfig.openConversationId || ''
|
|
328
|
+
const to = userId
|
|
329
|
+
const bodyText = text || data
|
|
330
|
+
|
|
331
|
+
if (!bodyText) {
|
|
332
|
+
log?.warn?.(`Empty body, ignoring`)
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Route via bindings
|
|
337
|
+
const bindings = cfg?.bindings ?? []
|
|
338
|
+
let targetAgentId = 'default'
|
|
339
|
+
const matchedBinding = bindings.find((binding: any) => {
|
|
340
|
+
const match = binding?.match
|
|
341
|
+
if (!match || match.channel !== 'openclaw-workclaw')
|
|
342
|
+
return false
|
|
343
|
+
return match.accountId === messageAccountId || match.accountId === '*'
|
|
344
|
+
})
|
|
345
|
+
if (matchedBinding) {
|
|
346
|
+
targetAgentId = matchedBinding.agentId
|
|
347
|
+
log?.info?.(`Routing to agent ${targetAgentId} via binding (account: ${messageAccountId})`)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!targetAgentId) {
|
|
351
|
+
log?.warn?.(`Missing target, ignoring`)
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 构建用于解析 sessionKey 的消息对象
|
|
356
|
+
// to = agentId (智能体/机器人), from = userId (用户)
|
|
357
|
+
const attachments = message.attachments
|
|
358
|
+
const inboundMessage: InboundMessage = {
|
|
359
|
+
text: bodyText,
|
|
360
|
+
to,
|
|
361
|
+
from: userId,
|
|
362
|
+
chatType: 'dm',
|
|
363
|
+
messageId: parsedMessageId,
|
|
364
|
+
rawPayload: data,
|
|
365
|
+
timestamp: Date.now(),
|
|
366
|
+
attachments,
|
|
367
|
+
}
|
|
368
|
+
log?.info?.(`handleAgentMessage: text=${bodyText?.substring(0, 50)}, attachments=${attachments?.length || 0}, rawPayload=${data.substring(0, 300)}`)
|
|
369
|
+
const pluginRuntime = getWorkclawRuntime()
|
|
370
|
+
|
|
371
|
+
// 检测是否是框架命令(如 /new)
|
|
372
|
+
const textForCommandCheck = bodyText?.replace(/^\[引用[^\]]*\]\s*/, '').trim() || ''
|
|
373
|
+
const resolvedCommand = maybeResolveTextAlias(textForCommandCheck, messageAccount.config as any)
|
|
374
|
+
const isFrameworkCommand = resolvedCommand !== null
|
|
375
|
+
if (isFrameworkCommand) {
|
|
376
|
+
log?.info?.(`[DELIVER] Framework command detected: ${bodyText}, will set CommandSource=native`)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 使用扩展后的 buildInboundContext,它会处理完整的 session 流程
|
|
380
|
+
const finalized = await buildInboundContext(
|
|
381
|
+
inboundMessage,
|
|
382
|
+
messageAccountId,
|
|
383
|
+
messageAccount.config,
|
|
384
|
+
targetAgentId,
|
|
385
|
+
pluginRuntime,
|
|
386
|
+
true,
|
|
387
|
+
log,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
// 如果是框架命令,设置 CommandSource 等字段,让 OpenClaw 框架识别为原生命令
|
|
391
|
+
if (isFrameworkCommand) {
|
|
392
|
+
const targetSessionKey = finalized.SessionKey as string
|
|
393
|
+
const { sessionKey: commandSessionKey, commandTargetSessionKey } = resolveNativeCommandSessionTargets({
|
|
394
|
+
agentId: targetAgentId,
|
|
395
|
+
sessionPrefix: 'openclaw-workclaw:new',
|
|
396
|
+
userId: String(userId ?? ''),
|
|
397
|
+
targetSessionKey,
|
|
398
|
+
});
|
|
399
|
+
(finalized as any).SessionKey = commandSessionKey;
|
|
400
|
+
(finalized as any).CommandTargetSessionKey = commandTargetSessionKey;
|
|
401
|
+
(finalized as any).CommandSource = 'native';
|
|
402
|
+
(finalized as any).CommandAuthorized = true
|
|
403
|
+
log?.info?.(`[DELIVER] Set CommandSource=native, commandSessionKey=${commandSessionKey}, targetSessionKey=${targetSessionKey}`)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 维护当前工具名称状态
|
|
407
|
+
let currentToolName: string | null = null
|
|
408
|
+
// 追踪是否有文本被 deliver 过
|
|
409
|
+
let textDelivered = false
|
|
410
|
+
|
|
411
|
+
// 发送工具消息的辅助函数
|
|
412
|
+
const sendToolMessage = async (text: string, isStart: boolean = false) => {
|
|
413
|
+
try {
|
|
414
|
+
await sendMessageWorkclaw({
|
|
415
|
+
cfg,
|
|
416
|
+
to,
|
|
417
|
+
text,
|
|
418
|
+
accountId: messageAccountId,
|
|
419
|
+
openConversationId: resolvedOpenConversationId,
|
|
420
|
+
agentId: msgAgentIdResolved,
|
|
421
|
+
replyToMessageId: parsedMessageId,
|
|
422
|
+
last: false,
|
|
423
|
+
source: 'deliver',
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
logError(log, isStart ? 'Tool start message send failed' : 'Tool result message send failed', err)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
await pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
432
|
+
ctx: finalized,
|
|
433
|
+
cfg,
|
|
434
|
+
replyOptions: {
|
|
435
|
+
onAgentRunStart(runId: string) {
|
|
436
|
+
// 保存 runId 和 ctx 的关联,供 hook 使用
|
|
437
|
+
setToolContext(runId, {
|
|
438
|
+
target: targetAgentId,
|
|
439
|
+
replyToMessageId: parsedMessageId,
|
|
440
|
+
openConversationId: resolvedOpenConversationId,
|
|
441
|
+
accountId: messageAccountId,
|
|
442
|
+
agentId: String(msgAgentIdResolved),
|
|
443
|
+
sessionKey: (finalized.SessionKey as string) || '',
|
|
444
|
+
})
|
|
445
|
+
log?.info?.(`[AGENT] onAgentRunStart: runId=${runId} target=${targetAgentId}`)
|
|
446
|
+
},
|
|
447
|
+
/* onToolStart(payload) {
|
|
448
|
+
log?.info?.(`[AGENT] onToolStart: ${JSON.stringify(payload)}`);
|
|
449
|
+
if (payload.phase === "start") {
|
|
450
|
+
currentToolName = payload.name ?? null;
|
|
451
|
+
const hint = getToolStartHint(currentToolName);
|
|
452
|
+
log?.info?.(`[TOOL] Starting tool: ${currentToolName}, sending hint: ${hint}`);
|
|
453
|
+
sendToolMessage(hint, true).catch((err) => {
|
|
454
|
+
logError(log, "Tool start hint send failed", err);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
} */
|
|
458
|
+
},
|
|
459
|
+
dispatcherOptions: {
|
|
460
|
+
deliver: async (payload: any, info?: any) => {
|
|
461
|
+
const resolvedReplyToId = payload?.replyToId || parsedMessageId
|
|
462
|
+
const textOut = String(payload.text ?? payload.body ?? '')
|
|
463
|
+
const target = to || String(payload.to ?? '')
|
|
464
|
+
const kind = info?.kind
|
|
465
|
+
|
|
466
|
+
if (!target) {
|
|
467
|
+
log?.warn?.(`[DELIVER] skip: no target`)
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (kind === 'tool') {
|
|
472
|
+
// 工具结果已在工具 hook 中处理,此处跳过避免重复
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!textOut.trim()) {
|
|
477
|
+
log?.warn?.(`[DELIVER] skip: empty text (kind=${kind})`)
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 处理工具类型的 deliver
|
|
482
|
+
/* if (info?.kind === "tool") {
|
|
483
|
+
// log?.info?.(`[DELIVER] tool reply: ${JSON.stringify(payload)}`);
|
|
484
|
+
const toolText = payload.text ?? "";
|
|
485
|
+
if (toolText.trim()) {
|
|
486
|
+
// 使用 formattedToolOutput 格式发送
|
|
487
|
+
try {
|
|
488
|
+
await sendMessageWorkclaw({
|
|
489
|
+
cfg,
|
|
490
|
+
to: targetAgentId,
|
|
491
|
+
text: JSON.stringify({
|
|
492
|
+
name: currentToolName || "Unknown",
|
|
493
|
+
content: toolText,
|
|
494
|
+
state: "result",
|
|
495
|
+
}),
|
|
496
|
+
msgType: "26",
|
|
497
|
+
accountId: messageAccountId,
|
|
498
|
+
openConversationId: resolvedOpenConversationId,
|
|
499
|
+
agentId: msgAgentIdResolved,
|
|
500
|
+
replyToMessageId: parsedMessageId,
|
|
501
|
+
});
|
|
502
|
+
} catch (err) {
|
|
503
|
+
logError(log, "Tool result send failed", err);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
} */
|
|
508
|
+
|
|
509
|
+
// last: true 统一由最后的 fallback 兜底发送,deliver 阶段全部传 false 避免重复触发
|
|
510
|
+
try {
|
|
511
|
+
const result = await sendMessageWorkclaw({
|
|
512
|
+
cfg,
|
|
513
|
+
to: target,
|
|
514
|
+
text: textOut,
|
|
515
|
+
accountId: messageAccountId,
|
|
516
|
+
openConversationId: resolvedOpenConversationId,
|
|
517
|
+
agentId: msgAgentIdResolved,
|
|
518
|
+
replyToMessageId: resolvedReplyToId,
|
|
519
|
+
last: false,
|
|
520
|
+
source: 'deliver',
|
|
521
|
+
})
|
|
522
|
+
textDelivered = true
|
|
523
|
+
log?.info?.(`[DELIVER] sent "${textOut.substring(0, 80)}" (last=false) kind=${kind} msgId=${result.messageId}`)
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
logError(log, 'Reply send failed', err)
|
|
527
|
+
throw err
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
currentToolName = null
|
|
534
|
+
|
|
535
|
+
// 原始消息是命令但 AI 没有回复文本时,发送"命令已执行"确认
|
|
536
|
+
if (!textDelivered && bodyText?.startsWith('/')) {
|
|
537
|
+
log?.info?.(`[DELIVER] command ${bodyText} executed with no text reply, sending confirmation`)
|
|
538
|
+
try {
|
|
539
|
+
await sendMessageWorkclaw({
|
|
540
|
+
cfg,
|
|
541
|
+
to: to || String(userId || ''),
|
|
542
|
+
text: `${bodyText} 已执行`,
|
|
543
|
+
accountId: messageAccountId,
|
|
544
|
+
openConversationId: resolvedOpenConversationId,
|
|
545
|
+
agentId: msgAgentIdResolved,
|
|
546
|
+
replyToMessageId: parsedMessageId,
|
|
547
|
+
last: false,
|
|
548
|
+
source: 'deliver',
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
logError(log, 'Command confirmation send failed', err)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// last: true 结束信号:每次回复最后都发,确保对话结束
|
|
557
|
+
log?.info?.(`[DELIVER] sending last=true end signal`)
|
|
558
|
+
try {
|
|
559
|
+
await sendMessageWorkclaw({
|
|
560
|
+
cfg,
|
|
561
|
+
to: to || String(userId || ''),
|
|
562
|
+
text: '',
|
|
563
|
+
accountId: messageAccountId,
|
|
564
|
+
openConversationId: resolvedOpenConversationId,
|
|
565
|
+
agentId: msgAgentIdResolved,
|
|
566
|
+
replyToMessageId: parsedMessageId,
|
|
567
|
+
last: true,
|
|
568
|
+
source: 'deliver',
|
|
569
|
+
})
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
const errStr = String(err)
|
|
573
|
+
if (errStr.includes("content can't be empty")) {
|
|
574
|
+
log?.info?.(`[DELIVER] last=true skipped (empty content not supported)`)
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
logError(log, 'last=true signal send failed', err)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
// Main dispatcher
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
|
|
586
|
+
export async function dispatchWorkclawMessage(
|
|
587
|
+
data: string,
|
|
588
|
+
ctx: MessageDispatcherContext,
|
|
589
|
+
): Promise<void> {
|
|
590
|
+
const { log } = ctx
|
|
591
|
+
|
|
592
|
+
const rawHasAuthToken
|
|
593
|
+
= data.includes('authToken')
|
|
594
|
+
|| data.toLowerCase().includes('authorization')
|
|
595
|
+
|| data.toLowerCase().includes('bearer ')
|
|
596
|
+
const rawPreview = data.length > 300 ? `${data.slice(0, 300)}...` : data
|
|
597
|
+
/* log?.info?.(
|
|
598
|
+
`Raw message bytes=${data.length} hasAuthToken=${rawHasAuthToken} preview=${rawPreview}`,
|
|
599
|
+
); */
|
|
600
|
+
|
|
601
|
+
const result = parseWorkClawMessage(data, log)
|
|
602
|
+
if (!result) {
|
|
603
|
+
log?.warn?.(`Failed to parse message`)
|
|
604
|
+
return
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
log?.info?.(`Message type=${result.type}`)
|
|
608
|
+
|
|
609
|
+
switch (result.type) {
|
|
610
|
+
case 'ping':
|
|
611
|
+
await handlePing(ctx, result)
|
|
612
|
+
return
|
|
613
|
+
|
|
614
|
+
case 'disconnect':
|
|
615
|
+
await handleDisconnect(ctx)
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
case 'agent_created':
|
|
619
|
+
case 'agent_updated':
|
|
620
|
+
case 'agent_deleted':
|
|
621
|
+
await handleAgentEvent(ctx, result)
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
case 'skills_event':
|
|
625
|
+
await handleSkillsProcessingEvent(ctx, result)
|
|
626
|
+
return
|
|
627
|
+
|
|
628
|
+
case 'tools_list':
|
|
629
|
+
await handleToolsListEvent(ctx, result)
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
case 'skills_list':
|
|
633
|
+
await handleSkillsListEvent(ctx, result)
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
case 'init_agent':
|
|
637
|
+
await handleInitAgentEvent(ctx, result)
|
|
638
|
+
return
|
|
639
|
+
|
|
640
|
+
case 'cron_task_event':
|
|
641
|
+
await handleCronTaskEvent(ctx, result)
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
case 'ignored':
|
|
645
|
+
log?.info?.(`Ignoring message type=${result.type}`)
|
|
646
|
+
return
|
|
647
|
+
|
|
648
|
+
case 'agent_message':
|
|
649
|
+
await handleAgentMessage(ctx, data, result)
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
default:
|
|
653
|
+
log?.info?.(`Unknown message type: ${result.type}, ignoring`)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// 创建成功回调
|
|
658
|
+
async function handleAgentCreatedMessage(baseConfig: WorkClawBaseConfig, agentId: string, accountId: string, log?: any) {
|
|
659
|
+
// 通知
|
|
660
|
+
try {
|
|
661
|
+
const base = baseConfig.baseUrl.replace(/\/$/, '') // 去掉末尾多余的 /
|
|
662
|
+
const callbackUrl = `${base}/open-apis/instance/agent-created/status`
|
|
663
|
+
const tokenCacheKey = baseConfig.appKey || accountId
|
|
664
|
+
const callbackData = {
|
|
665
|
+
agentId,
|
|
666
|
+
success: true,
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
log?.info?.(`[AgentCreated] Calling callback API: ${callbackUrl}`)
|
|
670
|
+
const token = await getWorkclawAccessToken(tokenCacheKey, baseConfig)
|
|
671
|
+
|
|
672
|
+
const response = await request(callbackUrl, {
|
|
673
|
+
method: 'POST',
|
|
674
|
+
headers: {
|
|
675
|
+
'Content-Type': 'application/json',
|
|
676
|
+
'Authorization': `Bearer ${token}`,
|
|
677
|
+
},
|
|
678
|
+
body: JSON.stringify(callbackData),
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
const responseBody = await response.body.text()
|
|
682
|
+
log?.info?.(`[AgentCreated] Callback API response: status=${response.statusCode}, body=${responseBody}`)
|
|
683
|
+
}
|
|
684
|
+
catch (callbackErr) {
|
|
685
|
+
// 接口调用失败不影响主流程,只记录错误
|
|
686
|
+
log?.error?.(`[AgentCreated] Callback API failed: ${String(callbackErr)}`)
|
|
687
|
+
}
|
|
688
|
+
}
|