@sunnoy/wecom 2.2.0 → 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 +13 -2
- package/openclaw.plugin.json +137 -1
- package/package.json +2 -3
- package/skills/wecom-contact-lookup/SKILL.md +167 -0
- package/skills/wecom-doc-manager/SKILL.md +106 -0
- package/skills/wecom-doc-manager/references/api-create-doc.md +56 -0
- package/skills/wecom-doc-manager/references/api-edit-doc-content.md +68 -0
- package/skills/wecom-doc-manager/references/api-export-document.md +88 -0
- package/skills/wecom-edit-todo/SKILL.md +254 -0
- package/skills/wecom-get-todo-detail/SKILL.md +148 -0
- package/skills/wecom-get-todo-list/SKILL.md +132 -0
- package/skills/wecom-meeting-create/SKILL.md +163 -0
- package/skills/wecom-meeting-create/references/example-full.md +30 -0
- package/skills/wecom-meeting-create/references/example-reminder.md +46 -0
- package/skills/wecom-meeting-create/references/example-security.md +22 -0
- package/skills/wecom-meeting-manage/SKILL.md +141 -0
- package/skills/wecom-meeting-query/SKILL.md +335 -0
- package/skills/wecom-preflight/SKILL.md +103 -0
- package/skills/wecom-schedule/SKILL.md +164 -0
- package/skills/wecom-schedule/references/api-check-availability.md +56 -0
- package/skills/wecom-schedule/references/api-create-schedule.md +38 -0
- package/skills/wecom-schedule/references/api-get-schedule-detail.md +81 -0
- package/skills/wecom-schedule/references/api-update-schedule.md +30 -0
- package/skills/wecom-schedule/references/ref-reminders.md +24 -0
- package/skills/wecom-smartsheet-data/SKILL.md +76 -0
- package/skills/wecom-smartsheet-data/references/api-get-records.md +61 -0
- package/skills/wecom-smartsheet-data/references/cell-value-formats.md +120 -0
- package/skills/wecom-smartsheet-schema/SKILL.md +96 -0
- package/skills/wecom-smartsheet-schema/references/field-types.md +43 -0
- package/wecom/accounts.js +2 -0
- package/wecom/callback-inbound.js +9 -5
- package/wecom/channel-plugin.js +65 -1
- package/wecom/constants.js +18 -7
- package/wecom/image-studio-tool.js +764 -0
- package/wecom/mcp-tool.js +660 -0
- package/wecom/parent-resolver.js +26 -0
- package/wecom/plugin-config.js +484 -0
- package/wecom/target.js +3 -2
- package/wecom/welcome-messages-file.js +155 -0
- package/wecom/workspace-template.js +40 -4
- package/wecom/ws-monitor.js +186 -12
- package/skills/wecom-doc/SKILL.md +0 -363
- package/skills/wecom-doc/references/doc-api.md +0 -224
|
@@ -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
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
MEDIA_IMAGE_PLACEHOLDER,
|
|
31
31
|
MESSAGE_PROCESS_TIMEOUT_MS,
|
|
32
32
|
REPLY_SEND_TIMEOUT_MS,
|
|
33
|
+
STREAM_MAX_LIFETIME_MS,
|
|
33
34
|
THINKING_MESSAGE,
|
|
34
35
|
WS_HEARTBEAT_INTERVAL_MS,
|
|
35
36
|
WS_MAX_RECONNECT_ATTEMPTS,
|
|
@@ -62,6 +63,8 @@ import {
|
|
|
62
63
|
startMessageStateCleanup,
|
|
63
64
|
} from "./ws-state.js";
|
|
64
65
|
import { ensureDynamicAgentListed } from "./workspace-template.js";
|
|
66
|
+
import { listAccountIds, resolveAccount } from "./accounts.js";
|
|
67
|
+
import { loadWelcomeMessagesFromFile } from "./welcome-messages-file.js";
|
|
65
68
|
|
|
66
69
|
const DEFAULT_AGENT_ID = "main";
|
|
67
70
|
const DEFAULT_STATE_DIRNAME = ".openclaw";
|
|
@@ -72,8 +75,9 @@ const VISIBLE_STREAM_THROTTLE_MS = 800;
|
|
|
72
75
|
// Reserve headroom below the SDK's per-reqId queue limit (100) so the final
|
|
73
76
|
// reply always has room.
|
|
74
77
|
const MAX_INTERMEDIATE_STREAM_MESSAGES = 85;
|
|
75
|
-
// WeCom stream messages
|
|
76
|
-
//
|
|
78
|
+
// WeCom stream messages have a hard 6-minute absolute lifetime from creation.
|
|
79
|
+
// Keepalive updates every 4 minutes maintain visible progress but do NOT extend
|
|
80
|
+
// the lifetime. Stream rotation (see rotateStream) is the actual fix.
|
|
77
81
|
const STREAM_KEEPALIVE_INTERVAL_MS = 4 * 60 * 1000;
|
|
78
82
|
// Match MEDIA:/FILE: directives at line start, optionally preceded by markdown list markers.
|
|
79
83
|
const REPLY_MEDIA_DIRECTIVE_PATTERN = /^\s*(?:[-*•]\s+|\d+\.\s+)?(?:MEDIA|FILE)\s*:/im;
|
|
@@ -136,6 +140,11 @@ function buildWaitingModelContent(seconds) {
|
|
|
136
140
|
return `<think>${lines.join("\n")}`;
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
function buildWaitingModelReasoningText(seconds) {
|
|
144
|
+
const normalizedSeconds = Math.max(1, Number.parseInt(String(seconds ?? 1), 10) || 1);
|
|
145
|
+
return `等待模型响应 ${normalizedSeconds}s`;
|
|
146
|
+
}
|
|
147
|
+
|
|
139
148
|
function buildWsStreamContent({ reasoningText = "", visibleText = "", finish = false }) {
|
|
140
149
|
const normalizedReasoning = String(reasoningText ?? "").trim();
|
|
141
150
|
const normalizedVisible = String(visibleText ?? "").trim();
|
|
@@ -357,10 +366,31 @@ function resolveAgentWorkspaceDir(config, agentId) {
|
|
|
357
366
|
return path.join(stateDir, `workspace-${normalizedAgentId}`);
|
|
358
367
|
}
|
|
359
368
|
|
|
369
|
+
function resolveConfiguredReplyMediaLocalRoots(config) {
|
|
370
|
+
const topLevel = Array.isArray(config?.channels?.[CHANNEL_ID]?.mediaLocalRoots)
|
|
371
|
+
? config.channels[CHANNEL_ID].mediaLocalRoots
|
|
372
|
+
: [];
|
|
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);
|
|
387
|
+
}
|
|
388
|
+
|
|
360
389
|
function resolveReplyMediaLocalRoots(config, agentId) {
|
|
361
390
|
const workspaceDir = resolveAgentWorkspaceDir(config, agentId || resolveDefaultAgentId(config));
|
|
362
391
|
const browserMediaDir = path.join(resolveStateDir(), "media", "browser");
|
|
363
|
-
|
|
392
|
+
const configuredRoots = resolveConfiguredReplyMediaLocalRoots(config);
|
|
393
|
+
return [...new Set([workspaceDir, browserMediaDir, ...configuredRoots].map((entry) => path.resolve(entry)))];
|
|
364
394
|
}
|
|
365
395
|
|
|
366
396
|
function mergeReplyMediaUrls(...lists) {
|
|
@@ -387,12 +417,13 @@ function mergeReplyMediaUrls(...lists) {
|
|
|
387
417
|
function buildReplyMediaGuidance(config, agentId) {
|
|
388
418
|
const workspaceDir = resolveAgentWorkspaceDir(config, agentId || resolveDefaultAgentId(config));
|
|
389
419
|
const browserMediaDir = path.join(resolveStateDir(), "media", "browser");
|
|
390
|
-
|
|
420
|
+
const configuredRoots = resolveConfiguredReplyMediaLocalRoots(config);
|
|
421
|
+
const qwenImageToolsConfig = config?.plugins?.entries?.wecom?.config?.qwenImageTools;
|
|
422
|
+
const guidance = [
|
|
391
423
|
WECOM_REPLY_MEDIA_GUIDANCE_HEADER,
|
|
392
424
|
`Local reply files are allowed only under the current workspace: ${workspaceDir}`,
|
|
393
425
|
"Inside the agent sandbox, that same workspace is visible as /workspace.",
|
|
394
426
|
`Browser-generated files are also allowed only under: ${browserMediaDir}`,
|
|
395
|
-
"Never reference any other host path.",
|
|
396
427
|
"Do NOT call message.send or message.sendAttachment to deliver files back to the current WeCom chat/user; use MEDIA: or FILE: directives instead.",
|
|
397
428
|
"For images: put each image path on its own line as MEDIA:/abs/path.",
|
|
398
429
|
"If a local file is in the current sandbox workspace, use its /workspace/... path directly.",
|
|
@@ -402,7 +433,27 @@ function buildReplyMediaGuidance(config, agentId) {
|
|
|
402
433
|
"CRITICAL: If a tool already returned a path prefixed with FILE: (e.g. FILE:/abs/path.pdf), keep the FILE: prefix exactly as-is. Do NOT change it to MEDIA:.",
|
|
403
434
|
"Each directive MUST be on its own line with no other text on that line.",
|
|
404
435
|
"The plugin will automatically send the media to the user.",
|
|
405
|
-
]
|
|
436
|
+
];
|
|
437
|
+
|
|
438
|
+
if (configuredRoots.length > 0) {
|
|
439
|
+
guidance.push(`Additional configured host roots are also allowed: ${configuredRoots.join(", ")}`);
|
|
440
|
+
}
|
|
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
|
+
|
|
455
|
+
guidance.push("Never reference any other host path.");
|
|
456
|
+
return guidance.join("\n");
|
|
406
457
|
}
|
|
407
458
|
|
|
408
459
|
function normalizeReplyMediaUrlForLoad(mediaUrl, config, agentId) {
|
|
@@ -546,6 +597,11 @@ async function sendMediaBatch({ wsClient, frame, state, account, runtime, config
|
|
|
546
597
|
|
|
547
598
|
if (result.ok) {
|
|
548
599
|
state.hasMedia = true;
|
|
600
|
+
if (result.finalType === "image") {
|
|
601
|
+
state.hasImageMedia = true;
|
|
602
|
+
} else {
|
|
603
|
+
state.hasFileMedia = true;
|
|
604
|
+
}
|
|
549
605
|
if (result.downgraded) {
|
|
550
606
|
logger.info(`[WS] Media downgraded: ${result.downgradeNote}`);
|
|
551
607
|
}
|
|
@@ -575,8 +631,28 @@ async function finishThinkingStream({ wsClient, frame, state, accountId }) {
|
|
|
575
631
|
visibleText: finalVisibleText,
|
|
576
632
|
finish: true,
|
|
577
633
|
});
|
|
634
|
+
} else if (state.reasoningText) {
|
|
635
|
+
// If the model only emitted reasoning tokens, close the thinking stream
|
|
636
|
+
// instead of replacing it with a generic completion stub.
|
|
637
|
+
finishText = buildWsStreamContent({
|
|
638
|
+
reasoningText: state.reasoningText,
|
|
639
|
+
visibleText: "",
|
|
640
|
+
finish: true,
|
|
641
|
+
});
|
|
578
642
|
} else if (state.hasMedia) {
|
|
579
|
-
|
|
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
|
+
});
|
|
580
656
|
} else if (state.hasMediaFailed && state.mediaErrorSummary) {
|
|
581
657
|
finishText = state.mediaErrorSummary;
|
|
582
658
|
} else {
|
|
@@ -599,6 +675,12 @@ function resolveWelcomeMessage(account) {
|
|
|
599
675
|
return configured;
|
|
600
676
|
}
|
|
601
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
|
+
|
|
602
684
|
const index = Math.floor(Math.random() * DEFAULT_WELCOME_MESSAGES.length);
|
|
603
685
|
return DEFAULT_WELCOME_MESSAGES[index] || DEFAULT_WELCOME_MESSAGE;
|
|
604
686
|
}
|
|
@@ -1072,6 +1154,14 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1072
1154
|
return;
|
|
1073
1155
|
}
|
|
1074
1156
|
text = extractGroupMessageContent(originalText, account.config);
|
|
1157
|
+
if (!text.trim() && imageUrls.length === 0 && fileUrls.length === 0) {
|
|
1158
|
+
logger.debug("[WS] Group message mention stripped to empty content; skipping reply", {
|
|
1159
|
+
accountId: account.accountId,
|
|
1160
|
+
chatId,
|
|
1161
|
+
senderId,
|
|
1162
|
+
});
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1075
1165
|
}
|
|
1076
1166
|
|
|
1077
1167
|
const senderIsAdmin = isWecomAdmin(senderId, account.config);
|
|
@@ -1123,12 +1213,16 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1123
1213
|
accumulatedText: "",
|
|
1124
1214
|
reasoningText: "",
|
|
1125
1215
|
streamId,
|
|
1216
|
+
streamCreatedAt: Date.now(),
|
|
1126
1217
|
replyMediaUrls: [],
|
|
1127
1218
|
pendingMediaUrls: [],
|
|
1128
1219
|
hasMedia: false,
|
|
1220
|
+
hasImageMedia: false,
|
|
1221
|
+
hasFileMedia: false,
|
|
1129
1222
|
hasMediaFailed: false,
|
|
1130
1223
|
mediaErrorSummary: "",
|
|
1131
1224
|
deliverCalled: false,
|
|
1225
|
+
waitingModelSeconds: 0,
|
|
1132
1226
|
};
|
|
1133
1227
|
setMessageState(messageId, state);
|
|
1134
1228
|
|
|
@@ -1143,6 +1237,7 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1143
1237
|
let lastNonEmptyStreamText = "";
|
|
1144
1238
|
let lastForwardedVisibleText = "";
|
|
1145
1239
|
let keepaliveTimer = null;
|
|
1240
|
+
let rotationTimer = null;
|
|
1146
1241
|
let waitingModelTimer = null;
|
|
1147
1242
|
let waitingModelSeconds = 0;
|
|
1148
1243
|
let waitingModelActive = false;
|
|
@@ -1159,6 +1254,7 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1159
1254
|
|
|
1160
1255
|
const sendWaitingModelUpdate = async (seconds) => {
|
|
1161
1256
|
const waitingText = buildWaitingModelContent(seconds);
|
|
1257
|
+
state.waitingModelSeconds = seconds;
|
|
1162
1258
|
lastStreamSentAt = Date.now();
|
|
1163
1259
|
lastNonEmptyStreamText = waitingText;
|
|
1164
1260
|
try {
|
|
@@ -1391,6 +1487,75 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1391
1487
|
}, STREAM_KEEPALIVE_INTERVAL_MS);
|
|
1392
1488
|
};
|
|
1393
1489
|
|
|
1490
|
+
// --- Stream rotation: finish the current stream before the 6-minute hard
|
|
1491
|
+
// limit and seamlessly continue on a new streamId. ----
|
|
1492
|
+
|
|
1493
|
+
const rotateStream = async () => {
|
|
1494
|
+
// Flush any pending throttled updates to the current stream first.
|
|
1495
|
+
await flushPendingStreamUpdates();
|
|
1496
|
+
|
|
1497
|
+
const oldStreamId = state.streamId;
|
|
1498
|
+
const hasVisibleText = Boolean(stripThinkTags(state.accumulatedText));
|
|
1499
|
+
|
|
1500
|
+
// Build finish content. When still in the thinking phase (no visible
|
|
1501
|
+
// text yet), append a small marker so the finished message is not empty.
|
|
1502
|
+
const finishText = buildWsStreamContent({
|
|
1503
|
+
reasoningText: state.reasoningText,
|
|
1504
|
+
visibleText: hasVisibleText ? state.accumulatedText : "⏳ 处理中…",
|
|
1505
|
+
finish: true,
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
try {
|
|
1509
|
+
streamMessagesSent++;
|
|
1510
|
+
await sendWsReply({
|
|
1511
|
+
wsClient,
|
|
1512
|
+
frame,
|
|
1513
|
+
streamId: oldStreamId,
|
|
1514
|
+
text: finishText,
|
|
1515
|
+
finish: true,
|
|
1516
|
+
accountId: account.accountId,
|
|
1517
|
+
});
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
logger.warn(`[WS] Stream rotation: failed to finish old stream: ${err.message}`);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// Switch to new stream.
|
|
1523
|
+
const newStreamId = generateReqId("stream");
|
|
1524
|
+
state.streamId = newStreamId;
|
|
1525
|
+
state.streamCreatedAt = Date.now();
|
|
1526
|
+
state.accumulatedText = "";
|
|
1527
|
+
state.reasoningText = "";
|
|
1528
|
+
|
|
1529
|
+
// Reset per-stream counters.
|
|
1530
|
+
streamMessagesSent = 0;
|
|
1531
|
+
lastStreamSentAt = Date.now();
|
|
1532
|
+
lastReasoningSendAt = 0;
|
|
1533
|
+
lastVisibleSendAt = 0;
|
|
1534
|
+
lastNonEmptyStreamText = "";
|
|
1535
|
+
lastForwardedVisibleText = "";
|
|
1536
|
+
|
|
1537
|
+
if (reqIdStore) reqIdStore.set(chatId, newStreamId);
|
|
1538
|
+
|
|
1539
|
+
logPerf("stream_rotated", { oldStreamId, newStreamId });
|
|
1540
|
+
|
|
1541
|
+
// Re-arm timers for the new stream.
|
|
1542
|
+
scheduleRotation();
|
|
1543
|
+
scheduleKeepalive();
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
const scheduleRotation = () => {
|
|
1547
|
+
if (rotationTimer) clearTimeout(rotationTimer);
|
|
1548
|
+
const remaining = STREAM_MAX_LIFETIME_MS - (Date.now() - state.streamCreatedAt);
|
|
1549
|
+
if (remaining <= 0) {
|
|
1550
|
+
void rotateStream();
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
rotationTimer = setTimeout(() => {
|
|
1554
|
+
rotationTimer = null;
|
|
1555
|
+
void rotateStream();
|
|
1556
|
+
}, remaining);
|
|
1557
|
+
};
|
|
1558
|
+
|
|
1394
1559
|
const cancelPendingTimers = () => {
|
|
1395
1560
|
if (pendingReasoningTimer) {
|
|
1396
1561
|
clearTimeout(pendingReasoningTimer);
|
|
@@ -1404,6 +1569,10 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1404
1569
|
clearTimeout(keepaliveTimer);
|
|
1405
1570
|
keepaliveTimer = null;
|
|
1406
1571
|
}
|
|
1572
|
+
if (rotationTimer) {
|
|
1573
|
+
clearTimeout(rotationTimer);
|
|
1574
|
+
rotationTimer = null;
|
|
1575
|
+
}
|
|
1407
1576
|
stopWaitingModelUpdates();
|
|
1408
1577
|
};
|
|
1409
1578
|
|
|
@@ -1415,6 +1584,7 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1415
1584
|
if (account.sendThinkingMessage !== false) {
|
|
1416
1585
|
waitingModelActive = true;
|
|
1417
1586
|
waitingModelSeconds = 1;
|
|
1587
|
+
state.waitingModelSeconds = waitingModelSeconds;
|
|
1418
1588
|
await sendThinkingReply({
|
|
1419
1589
|
wsClient,
|
|
1420
1590
|
frame,
|
|
@@ -1428,6 +1598,7 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1428
1598
|
}
|
|
1429
1599
|
lastStreamSentAt = Date.now();
|
|
1430
1600
|
scheduleKeepalive();
|
|
1601
|
+
scheduleRotation();
|
|
1431
1602
|
|
|
1432
1603
|
const peerKind = isGroupChat ? "group" : "dm";
|
|
1433
1604
|
const peerId = isGroupChat ? chatId : senderId;
|
|
@@ -1438,10 +1609,6 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1438
1609
|
? generateAgentId(peerKind, peerId, account.accountId)
|
|
1439
1610
|
: null;
|
|
1440
1611
|
|
|
1441
|
-
if (dynamicAgentId) {
|
|
1442
|
-
await ensureDynamicAgentListed(dynamicAgentId, account.config.workspaceTemplate);
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
1612
|
const route = core.routing.resolveAgentRoute({
|
|
1446
1613
|
cfg: config,
|
|
1447
1614
|
channel: CHANNEL_ID,
|
|
@@ -1456,8 +1623,15 @@ async function processWsMessage({ frame, account, config, runtime, wsClient, req
|
|
|
1456
1623
|
);
|
|
1457
1624
|
|
|
1458
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}:`);
|
|
1459
1634
|
route.agentId = dynamicAgentId;
|
|
1460
|
-
route.sessionKey = `agent:${dynamicAgentId}:${peerKind}:${peerId}`;
|
|
1461
1635
|
}
|
|
1462
1636
|
|
|
1463
1637
|
const { ctxPayload, storePath } = buildInboundContext({
|