@sunnoy/wecom 2.2.1 → 2.3.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 +2 -1
- package/index.js +11 -2
- package/openclaw.plugin.json +137 -1
- package/package.json +2 -3
- package/wecom/accounts.js +2 -0
- package/wecom/callback-inbound.js +9 -5
- package/wecom/channel-plugin.js +65 -1
- package/wecom/constants.js +14 -7
- package/wecom/image-studio-tool.js +764 -0
- package/wecom/parent-resolver.js +26 -0
- package/wecom/plugin-config.js +484 -0
- package/wecom/welcome-messages-file.js +155 -0
- package/wecom/workspace-template.js +40 -4
- package/wecom/ws-monitor.js +73 -8
|
@@ -220,7 +220,7 @@ export function seedAgentWorkspace(agentId, config, overrideTemplateDir) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
export function upsertAgentIdOnlyEntry(cfg, agentId) {
|
|
223
|
+
export function upsertAgentIdOnlyEntry(cfg, agentId, baseAgentId) {
|
|
224
224
|
const normalizedId = String(agentId || "")
|
|
225
225
|
.trim()
|
|
226
226
|
.toLowerCase();
|
|
@@ -252,8 +252,44 @@ export function upsertAgentIdOnlyEntry(cfg, agentId) {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
if (!existingIds.has(normalizedId)) {
|
|
255
|
-
|
|
255
|
+
const entry = { id: normalizedId, heartbeat: {} };
|
|
256
|
+
|
|
257
|
+
// Inherit inheritable properties from the base agent so the dynamic
|
|
258
|
+
// agent retains model, subagents (spawn permissions), and tool config.
|
|
259
|
+
if (baseAgentId) {
|
|
260
|
+
const baseEntry = currentList.find(
|
|
261
|
+
(e) => e && typeof e.id === "string" && e.id === baseAgentId,
|
|
262
|
+
);
|
|
263
|
+
if (baseEntry) {
|
|
264
|
+
for (const key of ["model", "subagents", "tools"]) {
|
|
265
|
+
if (baseEntry[key] != null) {
|
|
266
|
+
entry[key] = JSON.parse(JSON.stringify(baseEntry[key]));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
nextList.push(entry);
|
|
256
273
|
changed = true;
|
|
274
|
+
} else if (baseAgentId) {
|
|
275
|
+
// Backfill missing inheritable properties on existing entries that were
|
|
276
|
+
// persisted before the inheritance logic was added.
|
|
277
|
+
const existingEntry = nextList.find(
|
|
278
|
+
(e) => e && typeof e.id === "string" && e.id.trim().toLowerCase() === normalizedId,
|
|
279
|
+
);
|
|
280
|
+
if (existingEntry) {
|
|
281
|
+
const baseEntry = currentList.find(
|
|
282
|
+
(e) => e && typeof e.id === "string" && e.id === baseAgentId,
|
|
283
|
+
);
|
|
284
|
+
if (baseEntry) {
|
|
285
|
+
for (const key of ["model", "subagents", "tools"]) {
|
|
286
|
+
if (existingEntry[key] == null && baseEntry[key] != null) {
|
|
287
|
+
existingEntry[key] = JSON.parse(JSON.stringify(baseEntry[key]));
|
|
288
|
+
changed = true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
257
293
|
}
|
|
258
294
|
|
|
259
295
|
if (changed) {
|
|
@@ -263,7 +299,7 @@ export function upsertAgentIdOnlyEntry(cfg, agentId) {
|
|
|
263
299
|
return changed;
|
|
264
300
|
}
|
|
265
301
|
|
|
266
|
-
export async function ensureDynamicAgentListed(agentId, templateDir) {
|
|
302
|
+
export async function ensureDynamicAgentListed(agentId, templateDir, baseAgentId) {
|
|
267
303
|
const normalizedId = String(agentId || "")
|
|
268
304
|
.trim()
|
|
269
305
|
.toLowerCase();
|
|
@@ -285,7 +321,7 @@ export async function ensureDynamicAgentListed(agentId, templateDir) {
|
|
|
285
321
|
}
|
|
286
322
|
|
|
287
323
|
// Upsert into in-memory config so the running gateway sees it immediately.
|
|
288
|
-
const changed = upsertAgentIdOnlyEntry(openclawConfig, normalizedId);
|
|
324
|
+
const changed = upsertAgentIdOnlyEntry(openclawConfig, normalizedId, baseAgentId);
|
|
289
325
|
if (changed) {
|
|
290
326
|
logger.info("WeCom: dynamic agent added to in-memory agents.list", { agentId: normalizedId });
|
|
291
327
|
|
package/wecom/ws-monitor.js
CHANGED
|
@@ -63,6 +63,8 @@ import {
|
|
|
63
63
|
startMessageStateCleanup,
|
|
64
64
|
} from "./ws-state.js";
|
|
65
65
|
import { ensureDynamicAgentListed } from "./workspace-template.js";
|
|
66
|
+
import { listAccountIds, resolveAccount } from "./accounts.js";
|
|
67
|
+
import { loadWelcomeMessagesFromFile } from "./welcome-messages-file.js";
|
|
66
68
|
|
|
67
69
|
const DEFAULT_AGENT_ID = "main";
|
|
68
70
|
const DEFAULT_STATE_DIRNAME = ".openclaw";
|
|
@@ -138,6 +140,11 @@ function buildWaitingModelContent(seconds) {
|
|
|
138
140
|
return `<think>${lines.join("\n")}`;
|
|
139
141
|
}
|
|
140
142
|
|
|
143
|
+
function buildWaitingModelReasoningText(seconds) {
|
|
144
|
+
const normalizedSeconds = Math.max(1, Number.parseInt(String(seconds ?? 1), 10) || 1);
|
|
145
|
+
return `等待模型响应 ${normalizedSeconds}s`;
|
|
146
|
+
}
|
|
147
|
+
|
|
141
148
|
function buildWsStreamContent({ reasoningText = "", visibleText = "", finish = false }) {
|
|
142
149
|
const normalizedReasoning = String(reasoningText ?? "").trim();
|
|
143
150
|
const normalizedVisible = String(visibleText ?? "").trim();
|
|
@@ -360,10 +367,23 @@ function resolveAgentWorkspaceDir(config, agentId) {
|
|
|
360
367
|
}
|
|
361
368
|
|
|
362
369
|
function resolveConfiguredReplyMediaLocalRoots(config) {
|
|
363
|
-
const
|
|
370
|
+
const topLevel = Array.isArray(config?.channels?.[CHANNEL_ID]?.mediaLocalRoots)
|
|
364
371
|
? config.channels[CHANNEL_ID].mediaLocalRoots
|
|
365
372
|
: [];
|
|
366
|
-
|
|
373
|
+
|
|
374
|
+
// Also collect mediaLocalRoots from each account entry (multi-account mode).
|
|
375
|
+
// In single-account mode the account config IS the top-level config, so
|
|
376
|
+
// listAccountIds returns ["default"] and the roots are already in topLevel.
|
|
377
|
+
const accountRoots = [];
|
|
378
|
+
for (const accountId of listAccountIds(config)) {
|
|
379
|
+
const accountConfig = resolveAccount(config, accountId)?.config;
|
|
380
|
+
if (Array.isArray(accountConfig?.mediaLocalRoots)) {
|
|
381
|
+
accountRoots.push(...accountConfig.mediaLocalRoots);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const merged = [...new Set([...topLevel, ...accountRoots])];
|
|
386
|
+
return merged.map((entry) => resolveUserPath(entry)).filter(Boolean);
|
|
367
387
|
}
|
|
368
388
|
|
|
369
389
|
function resolveReplyMediaLocalRoots(config, agentId) {
|
|
@@ -398,6 +418,7 @@ function buildReplyMediaGuidance(config, agentId) {
|
|
|
398
418
|
const workspaceDir = resolveAgentWorkspaceDir(config, agentId || resolveDefaultAgentId(config));
|
|
399
419
|
const browserMediaDir = path.join(resolveStateDir(), "media", "browser");
|
|
400
420
|
const configuredRoots = resolveConfiguredReplyMediaLocalRoots(config);
|
|
421
|
+
const qwenImageToolsConfig = config?.plugins?.entries?.wecom?.config?.qwenImageTools;
|
|
401
422
|
const guidance = [
|
|
402
423
|
WECOM_REPLY_MEDIA_GUIDANCE_HEADER,
|
|
403
424
|
`Local reply files are allowed only under the current workspace: ${workspaceDir}`,
|
|
@@ -418,6 +439,19 @@ function buildReplyMediaGuidance(config, agentId) {
|
|
|
418
439
|
guidance.push(`Additional configured host roots are also allowed: ${configuredRoots.join(", ")}`);
|
|
419
440
|
}
|
|
420
441
|
|
|
442
|
+
if (qwenImageToolsConfig?.enabled === true) {
|
|
443
|
+
guidance.push(
|
|
444
|
+
"[WeCom image_studio rule]",
|
|
445
|
+
"When the user asks to generate an image, use image_studio with action=\"generate\".",
|
|
446
|
+
"When the user asks to edit an existing image, use image_studio with action=\"edit\" and pass images.",
|
|
447
|
+
"Use aspect=\"landscape\" for architecture diagrams, flowcharts, and banners unless the user asks otherwise.",
|
|
448
|
+
"Prefer model_preference=\"qwen\" for text-heavy diagrams or label-rich images, and model_preference=\"wan\" for photorealistic scenes.",
|
|
449
|
+
"For workspace-local images, always use /workspace/... paths when calling image_studio.",
|
|
450
|
+
"Prefer n=1 unless the user explicitly asks for multiple images.",
|
|
451
|
+
"If image_studio returns MEDIA: URLs, treat the image task as completed successfully.",
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
421
455
|
guidance.push("Never reference any other host path.");
|
|
422
456
|
return guidance.join("\n");
|
|
423
457
|
}
|
|
@@ -563,6 +597,11 @@ async function sendMediaBatch({ wsClient, frame, state, account, runtime, config
|
|
|
563
597
|
|
|
564
598
|
if (result.ok) {
|
|
565
599
|
state.hasMedia = true;
|
|
600
|
+
if (result.finalType === "image") {
|
|
601
|
+
state.hasImageMedia = true;
|
|
602
|
+
} else {
|
|
603
|
+
state.hasFileMedia = true;
|
|
604
|
+
}
|
|
566
605
|
if (result.downgraded) {
|
|
567
606
|
logger.info(`[WS] Media downgraded: ${result.downgradeNote}`);
|
|
568
607
|
}
|
|
@@ -601,7 +640,19 @@ async function finishThinkingStream({ wsClient, frame, state, accountId }) {
|
|
|
601
640
|
finish: true,
|
|
602
641
|
});
|
|
603
642
|
} else if (state.hasMedia) {
|
|
604
|
-
|
|
643
|
+
const mediaVisibleText = state.hasImageMedia && !state.hasFileMedia
|
|
644
|
+
? "图片已生成,请查收。"
|
|
645
|
+
: "文件已发送,请查收。";
|
|
646
|
+
const fallbackReasoningText = state.waitingModelSeconds > 0
|
|
647
|
+
? buildWaitingModelReasoningText(state.waitingModelSeconds)
|
|
648
|
+
: state.hasImageMedia && !state.hasFileMedia
|
|
649
|
+
? "正在生成图片"
|
|
650
|
+
: "正在整理并发送文件";
|
|
651
|
+
finishText = buildWsStreamContent({
|
|
652
|
+
reasoningText: fallbackReasoningText,
|
|
653
|
+
visibleText: mediaVisibleText,
|
|
654
|
+
finish: true,
|
|
655
|
+
});
|
|
605
656
|
} else if (state.hasMediaFailed && state.mediaErrorSummary) {
|
|
606
657
|
finishText = state.mediaErrorSummary;
|
|
607
658
|
} else {
|
|
@@ -624,6 +675,12 @@ function resolveWelcomeMessage(account) {
|
|
|
624
675
|
return configured;
|
|
625
676
|
}
|
|
626
677
|
|
|
678
|
+
const fromFile = loadWelcomeMessagesFromFile(account?.config);
|
|
679
|
+
if (fromFile?.length) {
|
|
680
|
+
const pick = Math.floor(Math.random() * fromFile.length);
|
|
681
|
+
return fromFile[pick];
|
|
682
|
+
}
|
|
683
|
+
|
|
627
684
|
const index = Math.floor(Math.random() * DEFAULT_WELCOME_MESSAGES.length);
|
|
628
685
|
return DEFAULT_WELCOME_MESSAGES[index] || DEFAULT_WELCOME_MESSAGE;
|
|
629
686
|
}
|
|
@@ -1160,9 +1217,12 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1160
1217
|
replyMediaUrls: [],
|
|
1161
1218
|
pendingMediaUrls: [],
|
|
1162
1219
|
hasMedia: false,
|
|
1220
|
+
hasImageMedia: false,
|
|
1221
|
+
hasFileMedia: false,
|
|
1163
1222
|
hasMediaFailed: false,
|
|
1164
1223
|
mediaErrorSummary: "",
|
|
1165
1224
|
deliverCalled: false,
|
|
1225
|
+
waitingModelSeconds: 0,
|
|
1166
1226
|
};
|
|
1167
1227
|
setMessageState(messageId, state);
|
|
1168
1228
|
|
|
@@ -1194,6 +1254,7 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1194
1254
|
|
|
1195
1255
|
const sendWaitingModelUpdate = async (seconds) => {
|
|
1196
1256
|
const waitingText = buildWaitingModelContent(seconds);
|
|
1257
|
+
state.waitingModelSeconds = seconds;
|
|
1197
1258
|
lastStreamSentAt = Date.now();
|
|
1198
1259
|
lastNonEmptyStreamText = waitingText;
|
|
1199
1260
|
try {
|
|
@@ -1523,6 +1584,7 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1523
1584
|
if (account.sendThinkingMessage !== false) {
|
|
1524
1585
|
waitingModelActive = true;
|
|
1525
1586
|
waitingModelSeconds = 1;
|
|
1587
|
+
state.waitingModelSeconds = waitingModelSeconds;
|
|
1526
1588
|
await sendThinkingReply({
|
|
1527
1589
|
wsClient,
|
|
1528
1590
|
frame,
|
|
@@ -1547,10 +1609,6 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1547
1609
|
? generateAgentId(peerKind, peerId, account.accountId)
|
|
1548
1610
|
: null;
|
|
1549
1611
|
|
|
1550
|
-
if (dynamicAgentId) {
|
|
1551
|
-
await ensureDynamicAgentListed(dynamicAgentId, account.config.workspaceTemplate);
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
1612
|
const route = core.routing.resolveAgentRoute({
|
|
1555
1613
|
cfg: config,
|
|
1556
1614
|
channel: CHANNEL_ID,
|
|
@@ -1565,8 +1623,15 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1565
1623
|
);
|
|
1566
1624
|
|
|
1567
1625
|
if (dynamicAgentId && !hasExplicitBinding) {
|
|
1626
|
+
const routeAgentId = route.agentId;
|
|
1627
|
+
// Use the account's configured agentId as the base for property inheritance
|
|
1628
|
+
// (model, subagents, tools). route.agentId may resolve to "main" when
|
|
1629
|
+
// there is no explicit binding, but the account's agentId points to the
|
|
1630
|
+
// actual parent agent whose properties the dynamic agent should inherit.
|
|
1631
|
+
const baseAgentId = account.config.agentId || routeAgentId;
|
|
1632
|
+
await ensureDynamicAgentListed(dynamicAgentId, account.config.workspaceTemplate, baseAgentId);
|
|
1633
|
+
route.sessionKey = route.sessionKey.replace(`agent:${routeAgentId}:`, `agent:${dynamicAgentId}:`);
|
|
1568
1634
|
route.agentId = dynamicAgentId;
|
|
1569
|
-
route.sessionKey = `agent:${dynamicAgentId}:${peerKind}:${peerId}`;
|
|
1570
1635
|
}
|
|
1571
1636
|
|
|
1572
1637
|
const { ctxPayload, storePath } = buildInboundContext({
|