@workclaw/openclaw-workclaw 1.0.19 → 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 +363 -364
- package/api.ts +3 -0
- package/dist/index.js +2 -1
- package/dist/src/api/workspace.js +2 -0
- package/dist/src/channel.js +1 -0
- package/dist/src/gateway/agent-handlers.js +87 -87
- package/dist/src/gateway/config-writer.js +4 -14
- package/dist/src/gateway/cron-tasks-handler.d.ts +19 -0
- package/dist/src/gateway/cron-tasks-handler.js +188 -0
- package/dist/src/gateway/message-context.d.ts +8 -4
- package/dist/src/gateway/message-context.js +148 -138
- package/dist/src/gateway/message-dispatcher.d.ts +0 -1
- package/dist/src/gateway/message-dispatcher.js +383 -323
- package/dist/src/gateway/reconnect.js +4 -0
- package/dist/src/gateway/skills-handler.js +187 -148
- package/dist/src/gateway/workclaw-gateway.d.ts +1 -1
- package/dist/src/gateway/workclaw-gateway.js +47 -33
- package/dist/src/tools/openclaw-workclaw-cron/src/update/params.js +17 -17
- package/index.ts +325 -0
- package/package.json +45 -62
- 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
|
@@ -5,7 +5,7 @@ import { parseWorkClawMessage } from "./message-context.js";
|
|
|
5
5
|
import { buildInboundContext } from "./message-context.js";
|
|
6
6
|
import { handleAgentCreated, handleAgentUpdated, handleAgentDeleted } from "./agent-handlers.js";
|
|
7
7
|
import { saveOpenConversationId, initWorkclawAgent } from "./config-writer.js";
|
|
8
|
-
import { getWorkclawWsConnection
|
|
8
|
+
import { getWorkclawWsConnection } from "../runtime.js";
|
|
9
9
|
import { getWorkclawRuntime } from "../runtime.js";
|
|
10
10
|
import { sendMessageWorkclaw } from "../outbound/index.js";
|
|
11
11
|
import { resolveAccountByUserIdAndAgentId, resolveWorkclawAccountWithCache } from "../accounts.js";
|
|
@@ -13,8 +13,8 @@ import { getWorkclawAccessToken, getWorkclawModelConfigByAppKey } from "../conne
|
|
|
13
13
|
import { setToolContext } from "../runtime.js";
|
|
14
14
|
import { handleSkillsEvent } from "./skills-handler.js";
|
|
15
15
|
import { handleToolsListEvent as doHandleToolsListEvent } from "./tools-list-handler.js";
|
|
16
|
-
import { handleSkillsListEvent as doHandleSkillsListEvent } from "./skills-list-handler.js";
|
|
17
16
|
import { request } from "undici";
|
|
17
|
+
import { resolveNativeCommandSessionTargets, maybeResolveTextAlias } from "openclaw/plugin-sdk/command-auth";
|
|
18
18
|
/** Log an error safely */
|
|
19
19
|
function logError(log, context, err) {
|
|
20
20
|
const errAny = err;
|
|
@@ -153,8 +153,9 @@ async function handleSkillsListEvent(ctx, result) {
|
|
|
153
153
|
if (!result.eventData)
|
|
154
154
|
return;
|
|
155
155
|
try {
|
|
156
|
+
const { handleSkillsListEvent } = await import('./skills-handler.js');
|
|
156
157
|
const connConfig = {
|
|
157
|
-
baseUrl: baseConfig.baseUrl ||
|
|
158
|
+
baseUrl: baseConfig.baseUrl || '',
|
|
158
159
|
websocketUrl: baseConfig.websocketUrl,
|
|
159
160
|
appKey: baseConfig.appKey,
|
|
160
161
|
appSecret: baseConfig.appSecret,
|
|
@@ -162,351 +163,410 @@ async function handleSkillsListEvent(ctx, result) {
|
|
|
162
163
|
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
163
164
|
requestTimeout: baseConfig.requestTimeout,
|
|
164
165
|
};
|
|
165
|
-
const tokenCacheKey = baseConfig.appKey || accountId;
|
|
166
|
-
const token = await getWorkclawAccessToken(tokenCacheKey, connConfig);
|
|
167
|
-
await doHandleSkillsListEvent(result.eventData, connConfig.baseUrl, token, log);
|
|
168
|
-
}
|
|
169
|
-
catch (err) {
|
|
170
|
-
logError(log, "handleSkillsListEvent failed", err);
|
|
171
166
|
}
|
|
172
|
-
|
|
173
|
-
async function handleInitAgentEvent(ctx, result) {
|
|
174
|
-
const { accountId, cfg, baseConfig, log } = ctx;
|
|
175
|
-
const initStartedAt = Date.now();
|
|
176
|
-
const eventData = result.eventData;
|
|
177
|
-
if (!eventData) {
|
|
178
|
-
log?.warn?.(`init_agent received but no data`);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
log?.info?.(`init_agent accountId=${accountId} eventDataType=${typeof eventData} preview=${JSON.stringify(eventData).slice(0, 200)}`);
|
|
182
|
-
const connConfig = {
|
|
183
|
-
baseUrl: baseConfig.baseUrl || "",
|
|
184
|
-
websocketUrl: baseConfig.websocketUrl,
|
|
185
|
-
appKey: baseConfig.appKey,
|
|
186
|
-
appSecret: baseConfig.appSecret,
|
|
187
|
-
localIp: baseConfig.localIp,
|
|
188
|
-
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
189
|
-
requestTimeout: baseConfig.requestTimeout,
|
|
190
|
-
};
|
|
191
|
-
// Fetch apiKey and init agent config in one write
|
|
192
|
-
try {
|
|
193
|
-
const tokenCacheKey = baseConfig.appKey || accountId;
|
|
194
|
-
const { baseUrl: modelBaseUrl, apiKey } = await getWorkclawModelConfigByAppKey(tokenCacheKey, connConfig);
|
|
195
|
-
const maskedKey = apiKey ? `${String(apiKey).slice(0, 6)}...(${String(apiKey).length})` : "missing";
|
|
196
|
-
log?.info?.(`init_agent model fetched baseUrl=${modelBaseUrl} apiKey=${maskedKey}`);
|
|
197
|
-
const agentId = String(eventData?.id || eventData?.agentId || "");
|
|
198
|
-
const userId = String(eventData?.futureId || eventData?.userId || "");
|
|
199
|
-
await initWorkclawAgent({ apiKey, agentId, userId, accountId: "default" }, cfg, log);
|
|
200
|
-
log?.info?.(`init_agent config saved elapsedMs=${Date.now() - initStartedAt}`);
|
|
201
|
-
}
|
|
202
|
-
catch (err) {
|
|
203
|
-
logError(log, "init_agent failed", err);
|
|
167
|
+
finally {
|
|
204
168
|
}
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return;
|
|
212
|
-
// 这里的msgAgentId是云平台的agentId
|
|
213
|
-
const { text, userId, openConversationId, messageId: parsedMessageId, agentId: msgAgentId } = message;
|
|
214
|
-
// Resolve account from message
|
|
215
|
-
let messageAccountId = accountId;
|
|
216
|
-
let messageAccount = account;
|
|
217
|
-
if (userId && msgAgentId) {
|
|
218
|
-
const resolvedAccountId = resolveAccountByUserIdAndAgentId(cfg, String(userId), String(msgAgentId));
|
|
219
|
-
if (resolvedAccountId) {
|
|
220
|
-
const resolved = resolveWorkclawAccountWithCache({ cfg, accountId: resolvedAccountId });
|
|
221
|
-
messageAccountId = resolvedAccountId;
|
|
222
|
-
messageAccount = resolved;
|
|
223
|
-
log?.info?.(`Resolved account ${resolvedAccountId} from message (userId=${userId}, agentId=${msgAgentId})`);
|
|
224
|
-
}
|
|
225
|
-
else if (String(msgAgentId) !== String(accountConfig.agentId)) {
|
|
226
|
-
log?.warn?.(`Account not found for userId=${userId}, agentId=${msgAgentId}, ignoring`);
|
|
169
|
+
async function handleInitAgentEvent(ctx, result) {
|
|
170
|
+
const { accountId, cfg, baseConfig, log } = ctx;
|
|
171
|
+
const initStartedAt = Date.now();
|
|
172
|
+
const eventData = result.eventData;
|
|
173
|
+
if (!eventData) {
|
|
174
|
+
log?.warn?.(`init_agent received but no data`);
|
|
227
175
|
return;
|
|
228
176
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
177
|
+
log?.info?.(`init_agent accountId=${accountId} eventDataType=${typeof eventData} preview=${JSON.stringify(eventData).slice(0, 200)}`);
|
|
178
|
+
const connConfig = {
|
|
179
|
+
baseUrl: baseConfig.baseUrl || "",
|
|
180
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
181
|
+
appKey: baseConfig.appKey,
|
|
182
|
+
appSecret: baseConfig.appSecret,
|
|
183
|
+
localIp: baseConfig.localIp,
|
|
184
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
185
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
186
|
+
};
|
|
187
|
+
// Fetch apiKey and init agent config in one write
|
|
234
188
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
189
|
+
const tokenCacheKey = baseConfig.appKey || accountId;
|
|
190
|
+
const { baseUrl: modelBaseUrl, apiKey } = await getWorkclawModelConfigByAppKey(tokenCacheKey, connConfig);
|
|
191
|
+
const maskedKey = apiKey ? `${String(apiKey).slice(0, 6)}...(${String(apiKey).length})` : "missing";
|
|
192
|
+
log?.info?.(`init_agent model fetched baseUrl=${modelBaseUrl} apiKey=${maskedKey}`);
|
|
193
|
+
const agentId = String(eventData?.id || eventData?.agentId || "");
|
|
194
|
+
const userId = String(eventData?.futureId || eventData?.userId || "");
|
|
195
|
+
await initWorkclawAgent({ apiKey, agentId, userId, accountId: "default" }, cfg, log);
|
|
196
|
+
log?.info?.(`init_agent config saved elapsedMs=${Date.now() - initStartedAt}`);
|
|
237
197
|
}
|
|
238
198
|
catch (err) {
|
|
239
|
-
logError(log, "
|
|
199
|
+
logError(log, "init_agent failed", err);
|
|
240
200
|
}
|
|
201
|
+
log?.info?.(`init_agent handled elapsedMs=${Date.now() - initStartedAt}`);
|
|
241
202
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
log?.warn?.(`Empty body, ignoring`);
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
// Route via bindings
|
|
250
|
-
const bindings = cfg?.bindings ?? [];
|
|
251
|
-
let targetAgentId = "default";
|
|
252
|
-
const matchedBinding = bindings.find((binding) => {
|
|
253
|
-
const match = binding?.match;
|
|
254
|
-
if (!match || match.channel !== "openclaw-workclaw")
|
|
255
|
-
return false;
|
|
256
|
-
return match.accountId === messageAccountId || match.accountId === "*";
|
|
257
|
-
});
|
|
258
|
-
if (matchedBinding) {
|
|
259
|
-
targetAgentId = matchedBinding.agentId;
|
|
260
|
-
log?.info?.(`Routing to agent ${targetAgentId} via binding (account: ${messageAccountId})`);
|
|
261
|
-
}
|
|
262
|
-
if (!targetAgentId) {
|
|
263
|
-
log?.warn?.(`Missing target, ignoring`);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
// 构建用于解析 sessionKey 的消息对象
|
|
267
|
-
// to = agentId (智能体/机器人), from = userId (用户)
|
|
268
|
-
const attachments = message.attachments;
|
|
269
|
-
const inboundMessage = {
|
|
270
|
-
text: bodyText,
|
|
271
|
-
to: to,
|
|
272
|
-
from: userId,
|
|
273
|
-
chatType: "dm",
|
|
274
|
-
messageId: parsedMessageId,
|
|
275
|
-
rawPayload: data,
|
|
276
|
-
timestamp: Date.now(),
|
|
277
|
-
attachments: attachments,
|
|
278
|
-
};
|
|
279
|
-
log?.info?.(`handleAgentMessage: text=${bodyText?.substring(0, 50)}, attachments=${attachments?.length || 0}`);
|
|
280
|
-
const pluginRuntime = getWorkclawRuntime();
|
|
281
|
-
// 使用扩展后的 buildInboundContext,它会处理完整的 session 流程
|
|
282
|
-
const finalized = await buildInboundContext(inboundMessage, messageAccountId, messageAccount.config, targetAgentId, pluginRuntime, true, log);
|
|
283
|
-
// 维护当前工具名称状态
|
|
284
|
-
let currentToolName = null;
|
|
285
|
-
// 追踪 final 是否已投递(框架在流式输出时会 drop final,需要手动补发 last: true)
|
|
286
|
-
let finalDelivered = false;
|
|
287
|
-
// 发送工具消息的辅助函数
|
|
288
|
-
const sendToolMessage = async (text, isStart = false) => {
|
|
203
|
+
async function handleCronTaskEvent(ctx, result) {
|
|
204
|
+
const { accountId, baseConfig, log } = ctx;
|
|
205
|
+
if (!result.cronTaskEvent)
|
|
206
|
+
return;
|
|
289
207
|
try {
|
|
290
|
-
await
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
208
|
+
const { handleCronTaskEvent } = await import('./cron-tasks-handler.js');
|
|
209
|
+
const connConfig = {
|
|
210
|
+
baseUrl: baseConfig.baseUrl || '',
|
|
211
|
+
websocketUrl: baseConfig.websocketUrl,
|
|
212
|
+
appKey: baseConfig.appKey,
|
|
213
|
+
appSecret: baseConfig.appSecret,
|
|
214
|
+
localIp: baseConfig.localIp,
|
|
215
|
+
allowInsecureTls: baseConfig.allowInsecureTls,
|
|
216
|
+
requestTimeout: baseConfig.requestTimeout,
|
|
217
|
+
};
|
|
218
|
+
const tokenCacheKey = baseConfig.appKey || accountId;
|
|
219
|
+
const token = await getWorkclawAccessToken(tokenCacheKey, connConfig);
|
|
220
|
+
await handleCronTaskEvent(result.cronTaskEvent, token, connConfig.baseUrl, connConfig.appKey || accountId, log);
|
|
300
221
|
}
|
|
301
222
|
catch (err) {
|
|
302
|
-
logError(log,
|
|
223
|
+
logError(log, 'handleCronTaskEvent failed', err);
|
|
303
224
|
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
225
|
+
}
|
|
226
|
+
async function handleAgentMessage(ctx, data, result) {
|
|
227
|
+
const { accountId, account, cfg, accountConfig, log } = ctx;
|
|
228
|
+
const { message } = result;
|
|
229
|
+
if (!message)
|
|
230
|
+
return;
|
|
231
|
+
// 这里的msgAgentId是云平台的agentId
|
|
232
|
+
const { text, userId, openConversationId, messageId: parsedMessageId, agentId: msgAgentId } = message;
|
|
233
|
+
// Resolve account from message
|
|
234
|
+
let messageAccountId = accountId;
|
|
235
|
+
let messageAccount = account;
|
|
236
|
+
if (userId && msgAgentId) {
|
|
237
|
+
const resolvedAccountId = resolveAccountByUserIdAndAgentId(cfg, String(userId), String(msgAgentId));
|
|
238
|
+
if (resolvedAccountId) {
|
|
239
|
+
const resolved = resolveWorkclawAccountWithCache({ cfg, accountId: resolvedAccountId });
|
|
240
|
+
messageAccountId = resolvedAccountId;
|
|
241
|
+
messageAccount = resolved;
|
|
242
|
+
log?.info?.(`Resolved account ${resolvedAccountId} from message (userId=${userId}, agentId=${msgAgentId})`);
|
|
243
|
+
}
|
|
244
|
+
else if (String(msgAgentId) !== String(accountConfig.agentId)) {
|
|
245
|
+
log?.warn?.(`Account not found for userId=${userId}, agentId=${msgAgentId}, ignoring`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const resolvedAccountConfig = messageAccount.config;
|
|
250
|
+
const msgAgentIdResolved = msgAgentId ?? accountConfig.agentId;
|
|
251
|
+
// Save openConversationId if new
|
|
252
|
+
if (openConversationId && openConversationId !== resolvedAccountConfig.openConversationId) {
|
|
253
|
+
try {
|
|
254
|
+
await saveOpenConversationId(messageAccountId, userId, openConversationId, cfg, log);
|
|
255
|
+
log?.info?.(`Saved openConversationId ${openConversationId} to account ${messageAccountId}`);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
logError(log, "saveOpenConversationId failed", err);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const resolvedOpenConversationId = openConversationId || resolvedAccountConfig.openConversationId || "";
|
|
262
|
+
const to = userId;
|
|
263
|
+
const bodyText = text || data;
|
|
264
|
+
if (!bodyText) {
|
|
265
|
+
log?.warn?.(`Empty body, ignoring`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// Route via bindings
|
|
269
|
+
const bindings = cfg?.bindings ?? [];
|
|
270
|
+
let targetAgentId = "default";
|
|
271
|
+
const matchedBinding = bindings.find((binding) => {
|
|
272
|
+
const match = binding?.match;
|
|
273
|
+
if (!match || match.channel !== "openclaw-workclaw")
|
|
274
|
+
return false;
|
|
275
|
+
return match.accountId === messageAccountId || match.accountId === "*";
|
|
276
|
+
});
|
|
277
|
+
if (matchedBinding) {
|
|
278
|
+
targetAgentId = matchedBinding.agentId;
|
|
279
|
+
log?.info?.(`Routing to agent ${targetAgentId} via binding (account: ${messageAccountId})`);
|
|
280
|
+
}
|
|
281
|
+
if (!targetAgentId) {
|
|
282
|
+
log?.warn?.(`Missing target, ignoring`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// 构建用于解析 sessionKey 的消息对象
|
|
286
|
+
// to = agentId (智能体/机器人), from = userId (用户)
|
|
287
|
+
const attachments = message.attachments;
|
|
288
|
+
const inboundMessage = {
|
|
289
|
+
text: bodyText,
|
|
290
|
+
to: to,
|
|
291
|
+
from: userId,
|
|
292
|
+
chatType: "dm",
|
|
293
|
+
messageId: parsedMessageId,
|
|
294
|
+
rawPayload: data,
|
|
295
|
+
timestamp: Date.now(),
|
|
296
|
+
attachments: attachments,
|
|
297
|
+
};
|
|
298
|
+
log?.info?.(`handleAgentMessage: text=${bodyText?.substring(0, 50)}, attachments=${attachments?.length || 0}, rawPayload=${data.substring(0, 300)}`);
|
|
299
|
+
const pluginRuntime = getWorkclawRuntime();
|
|
300
|
+
// 检测是否是框架命令(如 /new)
|
|
301
|
+
const textForCommandCheck = bodyText?.replace(/^\[引用[^\]]*\]\s*/, "").trim() || "";
|
|
302
|
+
const resolvedCommand = maybeResolveTextAlias(textForCommandCheck, messageAccount.config);
|
|
303
|
+
const isFrameworkCommand = resolvedCommand !== null;
|
|
304
|
+
if (isFrameworkCommand) {
|
|
305
|
+
log?.info?.(`[DELIVER] Framework command detected: ${bodyText}, will set CommandSource=native`);
|
|
306
|
+
}
|
|
307
|
+
// 使用扩展后的 buildInboundContext,它会处理完整的 session 流程
|
|
308
|
+
const finalized = await buildInboundContext(inboundMessage, messageAccountId, messageAccount.config, targetAgentId, pluginRuntime, true, log);
|
|
309
|
+
// 如果是框架命令,设置 CommandSource 等字段,让 OpenClaw 框架识别为原生命令
|
|
310
|
+
if (isFrameworkCommand) {
|
|
311
|
+
const targetSessionKey = finalized.SessionKey;
|
|
312
|
+
const { sessionKey: commandSessionKey, commandTargetSessionKey } = resolveNativeCommandSessionTargets({
|
|
313
|
+
agentId: targetAgentId,
|
|
314
|
+
sessionPrefix: "openclaw-workclaw:new",
|
|
315
|
+
userId: String(userId ?? ""),
|
|
316
|
+
targetSessionKey,
|
|
317
|
+
});
|
|
318
|
+
finalized.SessionKey = commandSessionKey;
|
|
319
|
+
finalized.CommandTargetSessionKey = commandTargetSessionKey;
|
|
320
|
+
finalized.CommandSource = "native";
|
|
321
|
+
finalized.CommandAuthorized = true;
|
|
322
|
+
log?.info?.(`[DELIVER] Set CommandSource=native, commandSessionKey=${commandSessionKey}, targetSessionKey=${targetSessionKey}`);
|
|
323
|
+
}
|
|
324
|
+
// 维护当前工具名称状态
|
|
325
|
+
let currentToolName = null;
|
|
326
|
+
// 追踪 final 是否已投递(框架在流式输出时会 drop final,需要手动补发 last: true)
|
|
327
|
+
let finalDelivered = false;
|
|
328
|
+
// 追踪是否有文本被 deliver 过
|
|
329
|
+
let textDelivered = false;
|
|
330
|
+
// 发送工具消息的辅助函数
|
|
331
|
+
const sendToolMessage = async (text, isStart = false) => {
|
|
332
|
+
try {
|
|
333
|
+
await sendMessageWorkclaw({
|
|
334
|
+
cfg,
|
|
335
|
+
to: to,
|
|
336
|
+
text,
|
|
315
337
|
accountId: messageAccountId,
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
openConversationId: resolvedOpenConversationId,
|
|
339
|
+
agentId: msgAgentIdResolved,
|
|
340
|
+
replyToMessageId: parsedMessageId,
|
|
341
|
+
last: false,
|
|
318
342
|
});
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
logError(log, isStart ? "Tool start message send failed" : "Tool result message send failed", err);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
await pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
349
|
+
ctx: finalized,
|
|
350
|
+
cfg,
|
|
351
|
+
replyOptions: {
|
|
352
|
+
onAgentRunStart(runId) {
|
|
353
|
+
// 保存 runId 和 ctx 的关联,供 hook 使用
|
|
354
|
+
setToolContext(runId, {
|
|
355
|
+
target: targetAgentId,
|
|
356
|
+
replyToMessageId: parsedMessageId,
|
|
357
|
+
openConversationId: resolvedOpenConversationId,
|
|
358
|
+
accountId: messageAccountId,
|
|
359
|
+
agentId: String(msgAgentIdResolved),
|
|
360
|
+
sessionKey: finalized.SessionKey || "",
|
|
329
361
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
log?.warn?.(`Skipping reply: no target`);
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
if (!textOut.trim()) {
|
|
345
|
-
log?.warn?.(`Empty reply text, skipping`);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
if (info?.kind === "tool")
|
|
349
|
-
return;
|
|
350
|
-
// 处理工具类型的 deliver
|
|
351
|
-
/*if (info?.kind === "tool") {
|
|
352
|
-
// log?.info?.(`[DELIVER] tool reply: ${JSON.stringify(payload)}`);
|
|
353
|
-
const toolText = payload.text ?? "";
|
|
354
|
-
if (toolText.trim()) {
|
|
355
|
-
// 使用 formattedToolOutput 格式发送
|
|
356
|
-
try {
|
|
357
|
-
await sendMessageWorkclaw({
|
|
358
|
-
cfg,
|
|
359
|
-
to: targetAgentId,
|
|
360
|
-
text: JSON.stringify({
|
|
361
|
-
name: currentToolName || "Unknown",
|
|
362
|
-
content: toolText,
|
|
363
|
-
state: "result",
|
|
364
|
-
}),
|
|
365
|
-
msgType: "26",
|
|
366
|
-
accountId: messageAccountId,
|
|
367
|
-
openConversationId: resolvedOpenConversationId,
|
|
368
|
-
agentId: msgAgentIdResolved,
|
|
369
|
-
replyToMessageId: parsedMessageId,
|
|
370
|
-
});
|
|
371
|
-
} catch (err) {
|
|
372
|
-
logError(log, "Tool result send failed", err);
|
|
373
|
-
}
|
|
362
|
+
log?.info?.(`[AGENT] onAgentRunStart: runId=${runId} target=${targetAgentId}`);
|
|
363
|
+
},
|
|
364
|
+
/*onToolStart(payload) {
|
|
365
|
+
log?.info?.(`[AGENT] onToolStart: ${JSON.stringify(payload)}`);
|
|
366
|
+
if (payload.phase === "start") {
|
|
367
|
+
currentToolName = payload.name ?? null;
|
|
368
|
+
const hint = getToolStartHint(currentToolName);
|
|
369
|
+
log?.info?.(`[TOOL] Starting tool: ${currentToolName}, sending hint: ${hint}`);
|
|
370
|
+
sendToolMessage(hint, true).catch((err) => {
|
|
371
|
+
logError(log, "Tool start hint send failed", err);
|
|
372
|
+
});
|
|
374
373
|
}
|
|
375
|
-
return;
|
|
376
374
|
}*/
|
|
377
|
-
// final 时清空工具状态
|
|
378
|
-
if (info?.kind === "final") {
|
|
379
|
-
currentToolName = null;
|
|
380
|
-
finalDelivered = true;
|
|
381
|
-
log?.info?.(`[DELIVER] final received, cleared currentToolName`);
|
|
382
|
-
}
|
|
383
|
-
// 明确设置 last:final 时为 true,否则为 false(显式转换,避免 undefined)
|
|
384
|
-
const isFinal = info?.kind === "final";
|
|
385
|
-
const isLast = isFinal ? true : false;
|
|
386
|
-
log?.info?.(`[DELIVER] is last: ${isLast} (info.kind=${info?.kind})`);
|
|
387
|
-
try {
|
|
388
|
-
const result = await sendMessageWorkclaw({
|
|
389
|
-
cfg, to: target, text: textOut,
|
|
390
|
-
accountId: messageAccountId, openConversationId: resolvedOpenConversationId,
|
|
391
|
-
agentId: msgAgentIdResolved, replyToMessageId: resolvedReplyToId,
|
|
392
|
-
last: isLast,
|
|
393
|
-
});
|
|
394
|
-
log?.info?.(`Reply sent messageId=${result.messageId}`);
|
|
395
|
-
}
|
|
396
|
-
catch (err) {
|
|
397
|
-
logError(log, "Reply send failed", err);
|
|
398
|
-
throw err;
|
|
399
|
-
}
|
|
400
375
|
},
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
376
|
+
dispatcherOptions: {
|
|
377
|
+
deliver: async (payload, info) => {
|
|
378
|
+
// 记录每次 deliver 调用,帮助调试 info.kind 问题
|
|
379
|
+
log?.info?.(`[DELIVER] deliver called: info.kind=${info?.kind}, payload.text=${String(payload?.text ?? "").substring(0, 100)}`);
|
|
380
|
+
const resolvedReplyToId = payload?.replyToId || parsedMessageId;
|
|
381
|
+
let textOut = String(payload.text ?? payload.body ?? "");
|
|
382
|
+
const target = to || String(payload.to ?? "");
|
|
383
|
+
if (!target) {
|
|
384
|
+
log?.warn?.(`Skipping reply: no target`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (!textOut.trim()) {
|
|
388
|
+
log?.warn?.(`Empty reply text, skipping`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (info?.kind === "tool")
|
|
392
|
+
return;
|
|
393
|
+
// 处理工具类型的 deliver
|
|
394
|
+
/*if (info?.kind === "tool") {
|
|
395
|
+
// log?.info?.(`[DELIVER] tool reply: ${JSON.stringify(payload)}`);
|
|
396
|
+
const toolText = payload.text ?? "";
|
|
397
|
+
if (toolText.trim()) {
|
|
398
|
+
// 使用 formattedToolOutput 格式发送
|
|
399
|
+
try {
|
|
400
|
+
await sendMessageWorkclaw({
|
|
401
|
+
cfg,
|
|
402
|
+
to: targetAgentId,
|
|
403
|
+
text: JSON.stringify({
|
|
404
|
+
name: currentToolName || "Unknown",
|
|
405
|
+
content: toolText,
|
|
406
|
+
state: "result",
|
|
407
|
+
}),
|
|
408
|
+
msgType: "26",
|
|
409
|
+
accountId: messageAccountId,
|
|
410
|
+
openConversationId: resolvedOpenConversationId,
|
|
411
|
+
agentId: msgAgentIdResolved,
|
|
412
|
+
replyToMessageId: parsedMessageId,
|
|
413
|
+
});
|
|
414
|
+
} catch (err) {
|
|
415
|
+
logError(log, "Tool result send failed", err);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}*/
|
|
420
|
+
// final 时清空工具状态
|
|
421
|
+
if (info?.kind === "final") {
|
|
422
|
+
currentToolName = null;
|
|
423
|
+
finalDelivered = true;
|
|
424
|
+
log?.info?.(`[DELIVER] final received, cleared currentToolName`);
|
|
425
|
+
}
|
|
426
|
+
// 明确设置 last:final 时为 true,否则为 false(显式转换,避免 undefined)
|
|
427
|
+
const isFinal = info?.kind === "final";
|
|
428
|
+
const isLast = isFinal ? true : false;
|
|
429
|
+
log?.info?.(`[DELIVER] is last: ${isLast} (info.kind=${info?.kind})`);
|
|
430
|
+
try {
|
|
431
|
+
const result = await sendMessageWorkclaw({
|
|
432
|
+
cfg, to: target, text: textOut,
|
|
433
|
+
accountId: messageAccountId, openConversationId: resolvedOpenConversationId,
|
|
434
|
+
agentId: msgAgentIdResolved, replyToMessageId: resolvedReplyToId,
|
|
435
|
+
last: isLast,
|
|
436
|
+
});
|
|
437
|
+
textDelivered = true;
|
|
438
|
+
log?.info?.(`Reply sent messageId=${result.messageId}`);
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
logError(log, "Reply send failed", err);
|
|
442
|
+
throw err;
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
// 原始消息是命令但 AI 没有回复文本时,发送"命令已执行"确认
|
|
448
|
+
if (!textDelivered && bodyText?.startsWith('/')) {
|
|
449
|
+
log?.info?.(`[DELIVER] command ${bodyText} executed with no text reply, sending confirmation`);
|
|
450
|
+
try {
|
|
451
|
+
await sendMessageWorkclaw({
|
|
452
|
+
cfg, to: to || String(userId || ""),
|
|
453
|
+
text: `${bodyText} 已执行`,
|
|
454
|
+
accountId: messageAccountId, openConversationId: resolvedOpenConversationId,
|
|
455
|
+
agentId: msgAgentIdResolved, replyToMessageId: parsedMessageId,
|
|
456
|
+
last: false,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
logError(log, "Command confirmation send failed", err);
|
|
461
|
+
}
|
|
415
462
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
463
|
+
// 框架在流式输出时会 drop final payload,检测到时应手动补发 last: true
|
|
464
|
+
if (!finalDelivered) {
|
|
465
|
+
log?.warn?.(`[DELIVER] final was dropped by framework, sending last: true`);
|
|
466
|
+
try {
|
|
467
|
+
// 发送一个空格作为占位内容,确保 last: true 能被服务端接受
|
|
468
|
+
await sendMessageWorkclaw({
|
|
469
|
+
cfg, to: to || String(userId || ""),
|
|
470
|
+
text: "",
|
|
471
|
+
accountId: messageAccountId, openConversationId: resolvedOpenConversationId,
|
|
472
|
+
agentId: msgAgentIdResolved, replyToMessageId: parsedMessageId,
|
|
473
|
+
last: true,
|
|
474
|
+
});
|
|
421
475
|
}
|
|
422
|
-
|
|
423
|
-
|
|
476
|
+
catch (err) {
|
|
477
|
+
// 服务端可能拒绝空内容的 last 信号,忽略即可
|
|
478
|
+
const errStr = String(err);
|
|
479
|
+
if (errStr.includes("content can't be empty")) {
|
|
480
|
+
log?.info?.(`[DELIVER] last signal skipped (empty content not supported)`);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
logError(log, "Final/last signal send failed", err);
|
|
484
|
+
}
|
|
424
485
|
}
|
|
425
486
|
}
|
|
426
487
|
}
|
|
427
|
-
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
data.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
log?.warn?.(`Failed to parse message`);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
log?.info?.(`Message type=${result.type}`);
|
|
446
|
-
// 更新上次消息时间(始终用 accountId,因为每个账号的入站时间需要独立追踪)
|
|
447
|
-
const { accountId } = ctx;
|
|
448
|
-
if (accountId) {
|
|
449
|
-
setLastInboundAt(accountId, Date.now());
|
|
450
|
-
}
|
|
451
|
-
switch (result.type) {
|
|
452
|
-
case "ping":
|
|
453
|
-
await handlePing(ctx, result);
|
|
454
|
-
return;
|
|
455
|
-
case "disconnect":
|
|
456
|
-
await handleDisconnect(ctx);
|
|
457
|
-
return;
|
|
458
|
-
case "agent_created":
|
|
459
|
-
case "agent_updated":
|
|
460
|
-
case "agent_deleted":
|
|
461
|
-
await handleAgentEvent(ctx, result);
|
|
462
|
-
return;
|
|
463
|
-
case "skills_event":
|
|
464
|
-
await handleSkillsProcessingEvent(ctx, result);
|
|
465
|
-
return;
|
|
466
|
-
case "tools_list":
|
|
467
|
-
await handleToolsListEvent(ctx, result);
|
|
468
|
-
return;
|
|
469
|
-
case "skills_list":
|
|
470
|
-
await handleSkillsListEvent(ctx, result);
|
|
471
|
-
return;
|
|
472
|
-
case "init_agent":
|
|
473
|
-
await handleInitAgentEvent(ctx, result);
|
|
474
|
-
return;
|
|
475
|
-
case "ignored":
|
|
476
|
-
log?.info?.(`Ignoring message type=${result.type}`);
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// Main dispatcher
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
export async function dispatchWorkclawMessage(data, ctx) {
|
|
492
|
+
const { log } = ctx;
|
|
493
|
+
const rawHasAuthToken = data.includes("authToken") ||
|
|
494
|
+
data.toLowerCase().includes("authorization") ||
|
|
495
|
+
data.toLowerCase().includes("bearer ");
|
|
496
|
+
const rawPreview = data.length > 300 ? `${data.slice(0, 300)}...` : data;
|
|
497
|
+
/*log?.info?.(
|
|
498
|
+
`Raw message bytes=${data.length} hasAuthToken=${rawHasAuthToken} preview=${rawPreview}`,
|
|
499
|
+
);*/
|
|
500
|
+
const result = parseWorkClawMessage(data, log);
|
|
501
|
+
if (!result) {
|
|
502
|
+
log?.warn?.(`Failed to parse message`);
|
|
477
503
|
return;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
504
|
+
}
|
|
505
|
+
log?.info?.(`Message type=${result.type}`);
|
|
506
|
+
switch (result.type) {
|
|
507
|
+
case 'ping':
|
|
508
|
+
await handlePing(ctx, result);
|
|
509
|
+
return;
|
|
510
|
+
case 'disconnect':
|
|
511
|
+
await handleDisconnect(ctx);
|
|
512
|
+
return;
|
|
513
|
+
case 'agent_created':
|
|
514
|
+
case 'agent_updated':
|
|
515
|
+
case 'agent_deleted':
|
|
516
|
+
await handleAgentEvent(ctx, result);
|
|
517
|
+
return;
|
|
518
|
+
case 'skills_event':
|
|
519
|
+
await handleSkillsProcessingEvent(ctx, result);
|
|
520
|
+
return;
|
|
521
|
+
case 'tools_list':
|
|
522
|
+
await handleToolsListEvent(ctx, result);
|
|
523
|
+
return;
|
|
524
|
+
case 'skills_list':
|
|
525
|
+
await handleSkillsListEvent(ctx, result);
|
|
526
|
+
return;
|
|
527
|
+
case 'init_agent':
|
|
528
|
+
await handleInitAgentEvent(ctx, result);
|
|
529
|
+
return;
|
|
530
|
+
case 'cron_task_event':
|
|
531
|
+
await handleCronTaskEvent(ctx, result);
|
|
532
|
+
return;
|
|
533
|
+
case 'ignored':
|
|
534
|
+
log?.info?.(`Ignoring message type=${result.type}`);
|
|
535
|
+
return;
|
|
536
|
+
case 'agent_message':
|
|
537
|
+
await handleAgentMessage(ctx, data, result);
|
|
538
|
+
return;
|
|
539
|
+
default:
|
|
540
|
+
log?.info?.(`Unknown message type: ${result.type}, ignoring`);
|
|
541
|
+
}
|
|
507
542
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
543
|
+
// 创建成功回调
|
|
544
|
+
async function handleAgentCreatedMessage(baseConfig, agentId, accountId, log) {
|
|
545
|
+
// 通知
|
|
546
|
+
try {
|
|
547
|
+
const base = baseConfig.baseUrl.replace(/\/$/, ""); // 去掉末尾多余的 /
|
|
548
|
+
const callbackUrl = `${base}/open-apis/instance/agent-created/status`;
|
|
549
|
+
const tokenCacheKey = baseConfig.appKey || accountId;
|
|
550
|
+
const callbackData = {
|
|
551
|
+
"agentId": agentId,
|
|
552
|
+
"success": true
|
|
553
|
+
};
|
|
554
|
+
log?.info?.(`[AgentCreated] Calling callback API: ${callbackUrl}`);
|
|
555
|
+
const token = await getWorkclawAccessToken(tokenCacheKey, baseConfig);
|
|
556
|
+
const response = await request(callbackUrl, {
|
|
557
|
+
method: "POST",
|
|
558
|
+
headers: {
|
|
559
|
+
"Content-Type": "application/json",
|
|
560
|
+
"Authorization": `Bearer ${token}`,
|
|
561
|
+
},
|
|
562
|
+
body: JSON.stringify(callbackData),
|
|
563
|
+
});
|
|
564
|
+
const responseBody = await response.body.text();
|
|
565
|
+
log?.info?.(`[AgentCreated] Callback API response: status=${response.statusCode}, body=${responseBody}`);
|
|
566
|
+
}
|
|
567
|
+
catch (callbackErr) {
|
|
568
|
+
// 接口调用失败不影响主流程,只记录错误
|
|
569
|
+
log?.error?.(`[AgentCreated] Callback API failed: ${String(callbackErr)}`);
|
|
570
|
+
}
|
|
511
571
|
}
|
|
512
572
|
}
|