@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.
@@ -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
- nextList.push({ id: normalizedId, heartbeat: {} });
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
 
@@ -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 roots = Array.isArray(config?.channels?.[CHANNEL_ID]?.mediaLocalRoots)
370
+ const topLevel = Array.isArray(config?.channels?.[CHANNEL_ID]?.mediaLocalRoots)
364
371
  ? config.channels[CHANNEL_ID].mediaLocalRoots
365
372
  : [];
366
- return roots.map((entry) => resolveUserPath(entry)).filter(Boolean);
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
- finishText = "文件已发送,请查收。";
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({