@sunnoy/wecom 2.0.0 → 2.0.1
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/package.json +1 -1
- package/wecom/workspace-template.js +21 -3
- package/wecom/ws-monitor.js +76 -19
package/package.json
CHANGED
|
@@ -157,8 +157,10 @@ export function upsertAgentIdOnlyEntry(cfg, agentId) {
|
|
|
157
157
|
const nextList = [...currentList];
|
|
158
158
|
|
|
159
159
|
// Keep "main" as the explicit default when creating agents.list for the first time.
|
|
160
|
+
// Include heartbeat: {} so main inherits agents.defaults.heartbeat even when
|
|
161
|
+
// dynamic agents also have explicit heartbeat entries (hasExplicitHeartbeatAgents).
|
|
160
162
|
if (nextList.length === 0) {
|
|
161
|
-
nextList.push({ id: "main" });
|
|
163
|
+
nextList.push({ id: "main", heartbeat: {} });
|
|
162
164
|
existingIds.add("main");
|
|
163
165
|
changed = true;
|
|
164
166
|
}
|
|
@@ -185,7 +187,7 @@ export async function ensureDynamicAgentListed(agentId, templateDir) {
|
|
|
185
187
|
|
|
186
188
|
const runtime = getRuntime();
|
|
187
189
|
const configRuntime = runtime?.config;
|
|
188
|
-
if (!configRuntime?.
|
|
190
|
+
if (!configRuntime?.writeConfigFile) {
|
|
189
191
|
return;
|
|
190
192
|
}
|
|
191
193
|
|
|
@@ -196,10 +198,26 @@ export async function ensureDynamicAgentListed(agentId, templateDir) {
|
|
|
196
198
|
return;
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
// Upsert into memory
|
|
201
|
+
// Upsert into in-memory config so the running gateway sees it immediately.
|
|
200
202
|
const changed = upsertAgentIdOnlyEntry(openclawConfig, normalizedId);
|
|
201
203
|
if (changed) {
|
|
202
204
|
logger.info("WeCom: dynamic agent added to in-memory agents.list", { agentId: normalizedId });
|
|
205
|
+
|
|
206
|
+
// Persist to disk so `openclaw agents list` (separate process) can see
|
|
207
|
+
// the dynamic agent and it survives gateway restarts.
|
|
208
|
+
// Write the mutated in-memory config directly (same pattern as logoutAccount).
|
|
209
|
+
// NOTE: loadConfig() returns runtimeConfigSnapshot in gateway mode — the same
|
|
210
|
+
// object we already mutated above — so a read-modify-write pattern silently
|
|
211
|
+
// skips the write (diskChanged=false). Writing directly avoids this.
|
|
212
|
+
try {
|
|
213
|
+
await configRuntime.writeConfigFile(openclawConfig);
|
|
214
|
+
logger.info("WeCom: dynamic agent persisted to config file", { agentId: normalizedId });
|
|
215
|
+
} catch (writeErr) {
|
|
216
|
+
logger.warn("WeCom: failed to persist dynamic agent to config file", {
|
|
217
|
+
agentId: normalizedId,
|
|
218
|
+
error: writeErr?.message || String(writeErr),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
203
221
|
}
|
|
204
222
|
|
|
205
223
|
// Always attempt seeding so recreated/cleaned dynamic agents can recover
|
package/wecom/ws-monitor.js
CHANGED
|
@@ -64,6 +64,10 @@ const LEGACY_STATE_DIRNAMES = [".clawdbot", ".moldbot", ".moltbot"];
|
|
|
64
64
|
const MAX_REPLY_MSG_ITEMS = 10;
|
|
65
65
|
const MAX_REPLY_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
66
66
|
const REASONING_STREAM_THROTTLE_MS = 800;
|
|
67
|
+
const VISIBLE_STREAM_THROTTLE_MS = 800;
|
|
68
|
+
// Reserve headroom below the SDK's per-reqId queue limit (100) so the final
|
|
69
|
+
// reply always has room.
|
|
70
|
+
const MAX_INTERMEDIATE_STREAM_MESSAGES = 85;
|
|
67
71
|
// Match MEDIA:/FILE: directives at line start, optionally preceded by markdown list markers.
|
|
68
72
|
const REPLY_MEDIA_DIRECTIVE_PATTERN = /^\s*(?:[-*•]\s+|\d+\.\s+)?(?:MEDIA|FILE)\s*:/im;
|
|
69
73
|
const WECOM_REPLY_MEDIA_GUIDANCE_HEADER = "[WeCom reply media rule]";
|
|
@@ -79,6 +83,11 @@ function withTimeout(promise, timeoutMs, message) {
|
|
|
79
83
|
timer = setTimeout(() => reject(new Error(message ?? `Timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
80
84
|
});
|
|
81
85
|
|
|
86
|
+
// Suppress unhandled rejection from the original promise if the timeout wins
|
|
87
|
+
// the race. Without this, a later rejection from the underlying SDK call
|
|
88
|
+
// becomes an unhandled promise rejection.
|
|
89
|
+
promise.catch(() => {});
|
|
90
|
+
|
|
82
91
|
return Promise.race([promise, timeout]).finally(() => {
|
|
83
92
|
if (timer) {
|
|
84
93
|
clearTimeout(timer);
|
|
@@ -1026,12 +1035,21 @@ async function processWsMessage({ frame, account, config, runtime, wsClient }) {
|
|
|
1026
1035
|
const state = { accumulatedText: "", reasoningText: "", streamId, replyMediaUrls: [] };
|
|
1027
1036
|
setMessageState(messageId, state);
|
|
1028
1037
|
|
|
1029
|
-
// Throttle reasoning stream updates to avoid exceeding
|
|
1038
|
+
// Throttle reasoning and visible text stream updates to avoid exceeding
|
|
1039
|
+
// the SDK's per-reqId reply queue limit (100).
|
|
1040
|
+
let streamMessagesSent = 0;
|
|
1030
1041
|
let lastReasoningSendAt = 0;
|
|
1031
1042
|
let pendingReasoningTimer = null;
|
|
1043
|
+
let lastVisibleSendAt = 0;
|
|
1044
|
+
let pendingVisibleTimer = null;
|
|
1045
|
+
|
|
1046
|
+
const canSendIntermediate = () => streamMessagesSent < MAX_INTERMEDIATE_STREAM_MESSAGES;
|
|
1047
|
+
|
|
1032
1048
|
const sendReasoningUpdate = async () => {
|
|
1049
|
+
if (!canSendIntermediate()) return;
|
|
1033
1050
|
lastReasoningSendAt = Date.now();
|
|
1034
1051
|
try {
|
|
1052
|
+
streamMessagesSent++;
|
|
1035
1053
|
await sendWsReply({
|
|
1036
1054
|
wsClient,
|
|
1037
1055
|
frame,
|
|
@@ -1049,12 +1067,42 @@ async function processWsMessage({ frame, account, config, runtime, wsClient }) {
|
|
|
1049
1067
|
}
|
|
1050
1068
|
};
|
|
1051
1069
|
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1070
|
+
const sendVisibleUpdate = async () => {
|
|
1071
|
+
if (!canSendIntermediate()) return;
|
|
1072
|
+
lastVisibleSendAt = Date.now();
|
|
1073
|
+
try {
|
|
1074
|
+
streamMessagesSent++;
|
|
1075
|
+
await sendWsReply({
|
|
1076
|
+
wsClient,
|
|
1077
|
+
frame,
|
|
1078
|
+
streamId: state.streamId,
|
|
1079
|
+
text: buildWsStreamContent({
|
|
1080
|
+
reasoningText: state.reasoningText,
|
|
1081
|
+
visibleText: state.accumulatedText,
|
|
1082
|
+
finish: false,
|
|
1083
|
+
}),
|
|
1084
|
+
finish: false,
|
|
1085
|
+
accountId: account.accountId,
|
|
1086
|
+
});
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
logger.warn(`[WS] Visible stream send failed (non-fatal): ${error.message}`);
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
const cancelPendingTimers = () => {
|
|
1054
1093
|
if (pendingReasoningTimer) {
|
|
1055
1094
|
clearTimeout(pendingReasoningTimer);
|
|
1056
1095
|
pendingReasoningTimer = null;
|
|
1057
1096
|
}
|
|
1097
|
+
if (pendingVisibleTimer) {
|
|
1098
|
+
clearTimeout(pendingVisibleTimer);
|
|
1099
|
+
pendingVisibleTimer = null;
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
const cleanupState = () => {
|
|
1104
|
+
deleteMessageState(messageId);
|
|
1105
|
+
cancelPendingTimers();
|
|
1058
1106
|
};
|
|
1059
1107
|
|
|
1060
1108
|
if (account.sendThinkingMessage !== false) {
|
|
@@ -1168,18 +1216,19 @@ async function processWsMessage({ frame, account, config, runtime, wsClient }) {
|
|
|
1168
1216
|
|
|
1169
1217
|
state.accumulatedText += chunk;
|
|
1170
1218
|
if (info.kind !== "final") {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1219
|
+
// Throttle visible text stream updates to avoid exceeding
|
|
1220
|
+
// the SDK queue limit together with reasoning updates.
|
|
1221
|
+
const elapsed = Date.now() - lastVisibleSendAt;
|
|
1222
|
+
if (elapsed < VISIBLE_STREAM_THROTTLE_MS) {
|
|
1223
|
+
if (!pendingVisibleTimer) {
|
|
1224
|
+
pendingVisibleTimer = setTimeout(async () => {
|
|
1225
|
+
pendingVisibleTimer = null;
|
|
1226
|
+
await sendVisibleUpdate();
|
|
1227
|
+
}, VISIBLE_STREAM_THROTTLE_MS - elapsed);
|
|
1228
|
+
}
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
await sendVisibleUpdate();
|
|
1183
1232
|
}
|
|
1184
1233
|
},
|
|
1185
1234
|
onError: (error, info) => {
|
|
@@ -1190,6 +1239,10 @@ async function processWsMessage({ frame, account, config, runtime, wsClient }) {
|
|
|
1190
1239
|
},
|
|
1191
1240
|
);
|
|
1192
1241
|
|
|
1242
|
+
// Cancel pending throttled timers before the final reply to prevent
|
|
1243
|
+
// non-final updates from being sent after finish=true.
|
|
1244
|
+
cancelPendingTimers();
|
|
1245
|
+
|
|
1193
1246
|
const preparedReplyMedia = await prepareReplyMediaOutputs({
|
|
1194
1247
|
payload: { mediaUrls: state.replyMediaUrls },
|
|
1195
1248
|
runtime,
|
|
@@ -1213,22 +1266,26 @@ async function processWsMessage({ frame, account, config, runtime, wsClient }) {
|
|
|
1213
1266
|
logger.warn("[WS] Agent API is not configured; passive non-image media delivery was skipped");
|
|
1214
1267
|
}
|
|
1215
1268
|
|
|
1216
|
-
|
|
1269
|
+
// If dispatch returned no content at all (e.g. upstream empty_stream),
|
|
1270
|
+
// send a fallback so the user isn't left waiting in silence.
|
|
1271
|
+
const effectiveFinalText = finalWsText || (msgItem.length === 0 ? "模型暂时无法响应,请稍后重试。" : "");
|
|
1272
|
+
if (effectiveFinalText || msgItem.length > 0) {
|
|
1217
1273
|
logger.info("[WS] Sending passive final reply", {
|
|
1218
1274
|
accountId: account.accountId,
|
|
1219
1275
|
agentId: route.agentId,
|
|
1220
1276
|
streamId: state.streamId,
|
|
1221
|
-
textLength:
|
|
1277
|
+
textLength: effectiveFinalText.length,
|
|
1222
1278
|
imageItemCount: msgItem.length,
|
|
1223
1279
|
deferredAgentMediaCount: preparedReplyMedia.agentMedia.length,
|
|
1224
1280
|
mirroredAgentImageCount: preparedReplyMedia.mirroredAgentMedia.length,
|
|
1281
|
+
emptyStreamFallback: !finalWsText,
|
|
1225
1282
|
});
|
|
1226
1283
|
try {
|
|
1227
1284
|
await sendWsReply({
|
|
1228
1285
|
wsClient,
|
|
1229
1286
|
frame,
|
|
1230
1287
|
streamId: state.streamId,
|
|
1231
|
-
text:
|
|
1288
|
+
text: effectiveFinalText,
|
|
1232
1289
|
finish: true,
|
|
1233
1290
|
msgItem,
|
|
1234
1291
|
accountId: account.accountId,
|
|
@@ -1241,7 +1298,7 @@ async function processWsMessage({ frame, account, config, runtime, wsClient }) {
|
|
|
1241
1298
|
senderId,
|
|
1242
1299
|
});
|
|
1243
1300
|
enqueuePendingReply(account.accountId, {
|
|
1244
|
-
text:
|
|
1301
|
+
text: effectiveFinalText,
|
|
1245
1302
|
senderId,
|
|
1246
1303
|
chatId,
|
|
1247
1304
|
isGroupChat,
|