evolcore 0.0.3 → 0.0.5
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/CHANGELOG.md +83 -0
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +1 -31
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +157 -5
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/channels/aun.js +220 -112
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +52 -137
- package/dist/channels/feishu.js +56 -4
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +383 -7
- package/dist/cli/aun-commands.js +11 -11
- package/dist/cli/daemon-commands.js +46 -15
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +49 -27
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +18 -0
- package/dist/config/access-policy.js +110 -0
- package/dist/config/builtin-role-templates.js +27 -14
- package/dist/config/builtin-roles.js +25 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +198 -22
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +173 -5
- package/dist/config/contact-book.js +32 -0
- package/dist/config/contact-operation-service.js +9 -3
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +3 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -2
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +6 -0
- package/dist/core/auth/operation-authorizer.js +8 -0
- package/dist/core/auth/operation-catalog.js +51 -3
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +10 -13
- package/dist/core/command/connect-menu.js +229 -26
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +269 -85
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +16 -0
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +158 -31
- package/dist/core/daemon-file-cache.js +28 -0
- package/dist/core/event-catalog.js +29 -0
- package/dist/core/evolagent-registry.js +4 -39
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +18 -14
- package/dist/core/message/inbound-admission.js +134 -0
- package/dist/core/message/message-bridge.js +616 -74
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +185 -10
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +381 -79
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +69 -24
- package/dist/core/protected-paths.js +36 -11
- package/dist/core/session/session-fs-store.js +19 -4
- package/dist/core/session/session-manager.js +232 -4
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +224 -89
- package/dist/ipc.js +81 -4
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/trigger/parser.js +55 -15
- package/dist/trigger/patch.js +4 -1
- package/dist/trigger/scheduler.js +441 -39
- package/dist/utils/cross-platform.js +8 -2
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/process-introspect.js +19 -3
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +8 -3
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +4 -4
- package/kits/docs/evolcore/msg.md +5 -5
- package/kits/docs/evolcore/trigger.md +25 -8
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +18 -7
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +364 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +46 -0
- package/kits/templates/roles/member.json +4 -0
- package/kits/templates/roles/visitor.json +4 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +4 -2
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/dist/channels/contact-bind-code.js +0 -134
- package/dist/channels/wecom-card.js +0 -101
- package/dist/channels/wecom-onboarding.js +0 -82
- package/dist/channels/wecom-state.js +0 -191
package/dist/channels/wechat.js
CHANGED
|
@@ -7,11 +7,31 @@ import { sanitizeFileName, saveToUploads, safeFetch, bufferToInboundImage } from
|
|
|
7
7
|
import { markdownToPlainText } from '../utils/markdown-to-plain-text.js';
|
|
8
8
|
import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
|
|
9
9
|
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
10
|
+
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
10
11
|
import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
|
|
11
|
-
|
|
12
|
+
import { ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
|
|
13
|
+
const wechatContactBinds = new ContactBindCodeRegistry({
|
|
14
|
+
channelType: 'wechat',
|
|
15
|
+
displayName: '微信',
|
|
16
|
+
initCommand: 'ec init wechat',
|
|
17
|
+
});
|
|
18
|
+
export function registerPendingWechatContactBind(req) {
|
|
19
|
+
return wechatContactBinds.register(req);
|
|
20
|
+
}
|
|
21
|
+
export function handlePendingWechatContactBindMessage(ctx) {
|
|
22
|
+
return handleExplicitContactBindCommand(wechatContactBinds, ctx);
|
|
23
|
+
}
|
|
24
|
+
export function getPendingWechatContactBind(selfAid, channelName, primaryId) {
|
|
25
|
+
return wechatContactBinds.get(selfAid, channelName, primaryId);
|
|
26
|
+
}
|
|
27
|
+
export function clearPendingWechatContactBinds() {
|
|
28
|
+
wechatContactBinds.clear();
|
|
29
|
+
}
|
|
30
|
+
const CHANNEL_VERSION = '2.4.6';
|
|
12
31
|
const ILINK_APP_ID = 'bot';
|
|
13
32
|
// iLink-App-ClientVersion: major<<16 | minor<<8 | patch (uint32)
|
|
14
|
-
const ILINK_APP_CLIENT_VERSION = String((
|
|
33
|
+
const ILINK_APP_CLIENT_VERSION = String((2 << 16) | (4 << 8) | 6); // 2.4.6 = 132102
|
|
34
|
+
const BOT_AGENT = 'EvolCore';
|
|
15
35
|
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
16
36
|
const DEFAULT_API_TIMEOUT_MS = 15_000;
|
|
17
37
|
const DEFAULT_CONFIG_TIMEOUT_MS = 10_000;
|
|
@@ -20,8 +40,65 @@ const BACKOFF_DELAY_MS = 30_000;
|
|
|
20
40
|
const RETRY_DELAY_MS = 2_000;
|
|
21
41
|
const TYPING_TICKET_TTL_MS = 5 * 60 * 1000; // 5 min cache
|
|
22
42
|
const SESSION_EXPIRED_ERRCODE = -14;
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
export class WechatApiError extends Error {
|
|
44
|
+
endpoint;
|
|
45
|
+
ret;
|
|
46
|
+
errcode;
|
|
47
|
+
errmsg;
|
|
48
|
+
code;
|
|
49
|
+
constructor(endpoint, ret, errcode, errmsg) {
|
|
50
|
+
const code = errcode !== undefined && errcode !== 0 ? errcode : ret;
|
|
51
|
+
super(`${endpoint} failed: ret=${ret ?? 'unset'} errcode=${errcode ?? 'unset'} errmsg=${errmsg ?? '(none)'}`);
|
|
52
|
+
this.endpoint = endpoint;
|
|
53
|
+
this.ret = ret;
|
|
54
|
+
this.errcode = errcode;
|
|
55
|
+
this.errmsg = errmsg;
|
|
56
|
+
this.name = 'WechatApiError';
|
|
57
|
+
this.code = code;
|
|
58
|
+
}
|
|
59
|
+
hasCode(code) {
|
|
60
|
+
return this.ret === code || this.errcode === code;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export class WechatContextTokenRefreshRequiredError extends Error {
|
|
64
|
+
recipient;
|
|
65
|
+
constructor(recipient, options) {
|
|
66
|
+
super(`sendmessage ret=-2 for ${recipient}; waiting for an updated context_token`);
|
|
67
|
+
this.recipient = recipient;
|
|
68
|
+
this.name = 'WechatContextTokenRefreshRequiredError';
|
|
69
|
+
if (options?.cause !== undefined) {
|
|
70
|
+
this.cause = options.cause;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Classify fetch/undici failures using the categories exposed by Tencent 2.4.6. */
|
|
75
|
+
export function classifyFetchError(err) {
|
|
76
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
77
|
+
return { type: 'timeout', description: 'request timeout' };
|
|
78
|
+
}
|
|
79
|
+
const root = err;
|
|
80
|
+
const cause = root?.cause;
|
|
81
|
+
const causeCode = typeof cause === 'object' && cause ? cause.code ?? '' : '';
|
|
82
|
+
const rootCode = root?.code ?? '';
|
|
83
|
+
const matchedCode = causeCode || rootCode || (typeof cause === 'string' ? cause : '');
|
|
84
|
+
const searchable = `${String(cause ?? err ?? '')} ${causeCode} ${rootCode}`;
|
|
85
|
+
if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(searchable)) {
|
|
86
|
+
return { type: 'dns', description: 'DNS resolution failed, check DNS configuration', ...(matchedCode ? { code: matchedCode } : {}) };
|
|
87
|
+
}
|
|
88
|
+
if (/ECONNREFUSED/i.test(searchable)) {
|
|
89
|
+
return { type: 'tcp', description: 'TCP connection refused', ...(matchedCode ? { code: matchedCode } : {}) };
|
|
90
|
+
}
|
|
91
|
+
if (/UND_ERR_CONNECT_TIMEOUT|ETIMEDOUT|ENETUNREACH|EHOSTUNREACH/i.test(searchable)) {
|
|
92
|
+
return { type: 'tcp', description: 'TCP connection timeout or unreachable', ...(matchedCode ? { code: matchedCode } : {}) };
|
|
93
|
+
}
|
|
94
|
+
if (/UND_ERR_SOCKET|SSL|TLS|CERT|UNABLE_TO_VERIFY|DEPTH_ZERO/i.test(searchable)) {
|
|
95
|
+
return { type: 'tls', description: 'TLS handshake error', ...(matchedCode ? { code: matchedCode } : {}) };
|
|
96
|
+
}
|
|
97
|
+
return { type: 'unknown', description: 'network request failed', ...(matchedCode ? { code: matchedCode } : {}) };
|
|
98
|
+
}
|
|
99
|
+
function buildBaseInfo() {
|
|
100
|
+
return { channel_version: CHANNEL_VERSION, bot_agent: BOT_AGENT };
|
|
101
|
+
}
|
|
25
102
|
const MSG_TYPE_USER = 1;
|
|
26
103
|
const MSG_TYPE_BOT = 2;
|
|
27
104
|
const MSG_ITEM_TEXT = 1;
|
|
@@ -109,14 +186,18 @@ export class WechatChannel {
|
|
|
109
186
|
recallHandler;
|
|
110
187
|
abortController;
|
|
111
188
|
connected = false;
|
|
189
|
+
connectionState = 'disconnected';
|
|
190
|
+
lastError;
|
|
191
|
+
reauthRequiredAt;
|
|
192
|
+
inflightHandlers = new Set();
|
|
112
193
|
// 内部状态(不外泄到核心层)
|
|
113
194
|
contextTokenCache = new Map();
|
|
195
|
+
rejectedContextTokens = new Map();
|
|
196
|
+
pendingContextMessages = new Map();
|
|
114
197
|
typingTicketCache = new Map();
|
|
115
198
|
getUpdatesBuf = '';
|
|
116
199
|
syncBufPath;
|
|
117
200
|
contextTokensPath;
|
|
118
|
-
// Session expired 状态
|
|
119
|
-
sessionPausedUntil = 0;
|
|
120
201
|
onSessionExpired;
|
|
121
202
|
eventBus;
|
|
122
203
|
// Project path resolver(用于保存文件到 uploads 目录)
|
|
@@ -150,14 +231,17 @@ export class WechatChannel {
|
|
|
150
231
|
setEventBus(bus) {
|
|
151
232
|
this.eventBus = bus;
|
|
152
233
|
}
|
|
153
|
-
/**
|
|
234
|
+
/** 当前 token 是否已失效,保留旧方法名以兼容现有调用方。 */
|
|
154
235
|
isSessionPaused() {
|
|
155
|
-
return
|
|
236
|
+
return this.connectionState === 'reauth_required';
|
|
156
237
|
}
|
|
157
238
|
async connect() {
|
|
158
239
|
if (!this.config.token) {
|
|
159
240
|
throw new Error('WeChat token not configured');
|
|
160
241
|
}
|
|
242
|
+
if (this.connectionState === 'reauth_required') {
|
|
243
|
+
throw new Error('WeChat reauthentication required; run: ec init wechat');
|
|
244
|
+
}
|
|
161
245
|
// 恢复游标
|
|
162
246
|
try {
|
|
163
247
|
if (fs.existsSync(this.syncBufPath)) {
|
|
@@ -173,6 +257,8 @@ export class WechatChannel {
|
|
|
173
257
|
await this.notifyLifecycle('notifystart');
|
|
174
258
|
}
|
|
175
259
|
catch (err) {
|
|
260
|
+
if (this.isSessionPaused())
|
|
261
|
+
throw err;
|
|
176
262
|
logger.warn('[WeChat] notifyStart failed during startup (ignored):', err);
|
|
177
263
|
}
|
|
178
264
|
this.abortController = new AbortController();
|
|
@@ -182,8 +268,14 @@ export class WechatChannel {
|
|
|
182
268
|
return;
|
|
183
269
|
logger.error('[WeChat] Poll loop fatal error:', err);
|
|
184
270
|
this.connected = false;
|
|
271
|
+
if (this.connectionState !== 'reauth_required') {
|
|
272
|
+
this.connectionState = 'disconnected';
|
|
273
|
+
this.lastError = err instanceof Error ? err.message : String(err);
|
|
274
|
+
}
|
|
185
275
|
});
|
|
186
276
|
this.connected = true;
|
|
277
|
+
this.connectionState = 'connected';
|
|
278
|
+
this.lastError = undefined;
|
|
187
279
|
logger.info('[WeChat] Channel connected');
|
|
188
280
|
}
|
|
189
281
|
async disconnect() {
|
|
@@ -192,21 +284,35 @@ export class WechatChannel {
|
|
|
192
284
|
this.abortController.abort();
|
|
193
285
|
this.abortController = undefined;
|
|
194
286
|
}
|
|
287
|
+
const reauthRequired = this.connectionState === 'reauth_required';
|
|
288
|
+
if (!reauthRequired)
|
|
289
|
+
this.connectionState = 'disconnected';
|
|
195
290
|
// 通知 ilink 后端:bot 下线
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
291
|
+
if (!reauthRequired) {
|
|
292
|
+
try {
|
|
293
|
+
await this.notifyLifecycle('notifystop');
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
logger.warn('[WeChat] notifyStop failed during shutdown (ignored):', err);
|
|
297
|
+
}
|
|
201
298
|
}
|
|
202
299
|
logger.info('[WeChat] Channel disconnected');
|
|
203
300
|
}
|
|
204
301
|
/** Get current connection status */
|
|
205
302
|
getStatus() {
|
|
206
|
-
return {
|
|
303
|
+
return {
|
|
304
|
+
connected: this.connected,
|
|
305
|
+
state: this.connectionState,
|
|
306
|
+
pendingContextMessages: this.pendingContextMessages.size,
|
|
307
|
+
...(this.lastError ? { error: this.lastError } : {}),
|
|
308
|
+
...(this.reauthRequiredAt ? { reauthRequiredAt: this.reauthRequiredAt } : {}),
|
|
309
|
+
};
|
|
207
310
|
}
|
|
208
311
|
/** Reconnect: disconnect then connect again */
|
|
209
312
|
async reconnect() {
|
|
313
|
+
if (this.connectionState === 'reauth_required') {
|
|
314
|
+
return '重连失败: 微信 token 已失效,请运行 ec init wechat 重新认证';
|
|
315
|
+
}
|
|
210
316
|
await this.disconnect();
|
|
211
317
|
try {
|
|
212
318
|
await this.connect();
|
|
@@ -221,12 +327,7 @@ export class WechatChannel {
|
|
|
221
327
|
logger.warn('[WeChat] Attempted to send empty message, skipping');
|
|
222
328
|
return;
|
|
223
329
|
}
|
|
224
|
-
|
|
225
|
-
if (this.isSessionPaused()) {
|
|
226
|
-
const remainingMin = Math.ceil((this.sessionPausedUntil - Date.now()) / 60_000);
|
|
227
|
-
logger.warn(`[WeChat] Session paused, ${remainingMin}min remaining, dropping outbound to ${to}`);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
330
|
+
this.assertAuthenticated();
|
|
230
331
|
const contextToken = this.contextTokenCache.get(to);
|
|
231
332
|
if (!contextToken) {
|
|
232
333
|
logger.error(`[WeChat] No context_token for ${to}, cannot send message`);
|
|
@@ -235,21 +336,11 @@ export class WechatChannel {
|
|
|
235
336
|
// Markdown → 纯文本
|
|
236
337
|
const plainText = markdownToPlainText(text);
|
|
237
338
|
const clientId = `evolcore-wechat:${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
238
|
-
const body = {
|
|
239
|
-
msg: {
|
|
240
|
-
from_user_id: '',
|
|
241
|
-
to_user_id: to,
|
|
242
|
-
client_id: clientId,
|
|
243
|
-
message_type: MSG_TYPE_BOT,
|
|
244
|
-
message_state: MSG_STATE_FINISH,
|
|
245
|
-
item_list: [{ type: MSG_ITEM_TEXT, text_item: { text: plainText } }],
|
|
246
|
-
context_token: contextToken,
|
|
247
|
-
},
|
|
248
|
-
base_info: { channel_version: CHANNEL_VERSION },
|
|
249
|
-
};
|
|
250
339
|
try {
|
|
251
|
-
await this.
|
|
252
|
-
|
|
340
|
+
const delivery = await this.sendMessageItemWithContextRetry(to, { type: MSG_ITEM_TEXT, text_item: { text: plainText } }, contextToken, clientId);
|
|
341
|
+
if (delivery === 'sent') {
|
|
342
|
+
logger.debug(`[WeChat] Sent message to ${to}, clientId=${clientId}`);
|
|
343
|
+
}
|
|
253
344
|
}
|
|
254
345
|
catch (err) {
|
|
255
346
|
logger.error(`[WeChat] Failed to send message to ${to}:`, err);
|
|
@@ -262,11 +353,7 @@ export class WechatChannel {
|
|
|
262
353
|
}
|
|
263
354
|
/** 发送文件(图片/视频/文件)给用户,通过 CDN 上传 */
|
|
264
355
|
async sendFile(to, filePath) {
|
|
265
|
-
|
|
266
|
-
if (this.isSessionPaused()) {
|
|
267
|
-
logger.warn(`[WeChat] Session paused, dropping file send to ${to}`);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
356
|
+
this.assertAuthenticated();
|
|
270
357
|
const contextToken = this.contextTokenCache.get(to);
|
|
271
358
|
if (!contextToken) {
|
|
272
359
|
logger.error(`[WeChat] No context_token for ${to}, cannot send file`);
|
|
@@ -319,15 +406,15 @@ export class WechatChannel {
|
|
|
319
406
|
let consecutiveFailures = 0;
|
|
320
407
|
let nextTimeoutMs = DEFAULT_LONG_POLL_TIMEOUT_MS;
|
|
321
408
|
logger.info('[WeChat] Starting message polling...');
|
|
322
|
-
while (!signal.aborted) {
|
|
409
|
+
while (!signal.aborted && this.connectionState !== 'reauth_required') {
|
|
323
410
|
try {
|
|
324
411
|
const body = JSON.stringify({
|
|
325
412
|
get_updates_buf: this.getUpdatesBuf,
|
|
326
|
-
base_info:
|
|
413
|
+
base_info: buildBaseInfo(),
|
|
327
414
|
});
|
|
328
|
-
let
|
|
415
|
+
let resp;
|
|
329
416
|
try {
|
|
330
|
-
|
|
417
|
+
resp = await this.apiRequest('ilink/bot/getupdates', body, nextTimeoutMs, signal);
|
|
331
418
|
}
|
|
332
419
|
catch (err) {
|
|
333
420
|
if (signal.aborted)
|
|
@@ -338,93 +425,10 @@ export class WechatChannel {
|
|
|
338
425
|
}
|
|
339
426
|
throw err;
|
|
340
427
|
}
|
|
341
|
-
const resp = JSON.parse(rawText);
|
|
342
428
|
// 更新服务端建议的轮询超时
|
|
343
429
|
if (resp.longpolling_timeout_ms != null && resp.longpolling_timeout_ms > 0) {
|
|
344
430
|
nextTimeoutMs = resp.longpolling_timeout_ms;
|
|
345
431
|
}
|
|
346
|
-
// API 错误处理
|
|
347
|
-
const isError = (resp.ret !== undefined && resp.ret !== 0) ||
|
|
348
|
-
(resp.errcode !== undefined && resp.errcode !== 0);
|
|
349
|
-
if (isError) {
|
|
350
|
-
// Session expired 专用处理
|
|
351
|
-
const isSessionExpired = resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
|
|
352
|
-
if (isSessionExpired) {
|
|
353
|
-
consecutiveFailures = 0;
|
|
354
|
-
logger.error(`[WeChat] Session expired (errcode=${resp.errcode}), retrying in ${SESSION_RETRY_DELAY_MS / 1000}s...`);
|
|
355
|
-
// 短暂停后重试一次
|
|
356
|
-
await this.sleep(SESSION_RETRY_DELAY_MS, signal);
|
|
357
|
-
if (signal.aborted)
|
|
358
|
-
return;
|
|
359
|
-
// 重试 getupdates
|
|
360
|
-
try {
|
|
361
|
-
const retryBody = JSON.stringify({
|
|
362
|
-
get_updates_buf: this.getUpdatesBuf,
|
|
363
|
-
base_info: { channel_version: CHANNEL_VERSION },
|
|
364
|
-
});
|
|
365
|
-
const retryRaw = await this.apiFetch('ilink/bot/getupdates', retryBody, nextTimeoutMs, signal);
|
|
366
|
-
const retryResp = JSON.parse(retryRaw);
|
|
367
|
-
const retryExpired = retryResp.errcode === SESSION_EXPIRED_ERRCODE || retryResp.ret === SESSION_EXPIRED_ERRCODE;
|
|
368
|
-
if (!retryExpired) {
|
|
369
|
-
// 恢复成功,静默继续
|
|
370
|
-
logger.info('[WeChat] Session recovered after retry');
|
|
371
|
-
// 把 retryResp 当正常响应处理(更新游标和消息)
|
|
372
|
-
if (retryResp.get_updates_buf) {
|
|
373
|
-
this.getUpdatesBuf = retryResp.get_updates_buf;
|
|
374
|
-
try {
|
|
375
|
-
fs.writeFileSync(this.syncBufPath, this.getUpdatesBuf, 'utf-8');
|
|
376
|
-
}
|
|
377
|
-
catch { }
|
|
378
|
-
}
|
|
379
|
-
for (const msg of retryResp.msgs ?? []) {
|
|
380
|
-
await this.handleInboundMessage(msg);
|
|
381
|
-
}
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
catch (retryErr) {
|
|
386
|
-
if (signal.aborted)
|
|
387
|
-
return;
|
|
388
|
-
logger.error('[WeChat] Retry after session expired also failed:', retryErr);
|
|
389
|
-
}
|
|
390
|
-
// 重试仍失败,进入长暂停
|
|
391
|
-
const pauseMin = SESSION_PAUSE_DURATION_MS / 60_000;
|
|
392
|
-
this.sessionPausedUntil = Date.now() + SESSION_PAUSE_DURATION_MS;
|
|
393
|
-
logger.error(`[WeChat] Session still expired, pausing for ${pauseMin}min`);
|
|
394
|
-
// 通知用户(通过事件总线或回调)
|
|
395
|
-
const authMsg = `⚠️ 微信 token 已过期,通道暂停 ${pauseMin} 分钟后自动重试。\n如需立即恢复,请运行: ec init wechat`;
|
|
396
|
-
if (this.eventBus) {
|
|
397
|
-
this.eventBus.publish({
|
|
398
|
-
type: 'channel:error',
|
|
399
|
-
channel: 'wechat',
|
|
400
|
-
status: 'auth_error',
|
|
401
|
-
message: authMsg,
|
|
402
|
-
timestamp: Date.now(),
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
else if (this.onSessionExpired) {
|
|
406
|
-
this.onSessionExpired(authMsg);
|
|
407
|
-
}
|
|
408
|
-
await this.sleep(SESSION_PAUSE_DURATION_MS, signal);
|
|
409
|
-
if (signal.aborted)
|
|
410
|
-
return;
|
|
411
|
-
// 长暂停结束,清除暂停状态,循环自动重试
|
|
412
|
-
this.sessionPausedUntil = 0;
|
|
413
|
-
logger.info('[WeChat] Session pause ended, resuming polling');
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
consecutiveFailures++;
|
|
417
|
-
logger.error(`[WeChat] getUpdates failed: ret=${resp.ret} errcode=${resp.errcode} errmsg=${resp.errmsg ?? ''}`);
|
|
418
|
-
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
419
|
-
logger.error(`[WeChat] ${MAX_CONSECUTIVE_FAILURES} consecutive failures, backing off ${BACKOFF_DELAY_MS / 1000}s`);
|
|
420
|
-
consecutiveFailures = 0;
|
|
421
|
-
await this.sleep(BACKOFF_DELAY_MS, signal);
|
|
422
|
-
}
|
|
423
|
-
else {
|
|
424
|
-
await this.sleep(RETRY_DELAY_MS, signal);
|
|
425
|
-
}
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
432
|
consecutiveFailures = 0;
|
|
429
433
|
// 保存游标
|
|
430
434
|
if (resp.get_updates_buf) {
|
|
@@ -444,8 +448,18 @@ export class WechatChannel {
|
|
|
444
448
|
catch (err) {
|
|
445
449
|
if (signal.aborted)
|
|
446
450
|
return;
|
|
451
|
+
if (this.isSessionPaused())
|
|
452
|
+
return;
|
|
447
453
|
consecutiveFailures++;
|
|
448
|
-
|
|
454
|
+
if (err instanceof WechatApiError) {
|
|
455
|
+
logger.error(`[WeChat] getUpdates API error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${err.message}`);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
const classified = classifyFetchError(err);
|
|
459
|
+
logger.error(`[WeChat] Poll network error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ` +
|
|
460
|
+
`type=${classified.type} description=${classified.description}` +
|
|
461
|
+
`${classified.code ? ` code=${classified.code}` : ''}`, err);
|
|
462
|
+
}
|
|
449
463
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
450
464
|
consecutiveFailures = 0;
|
|
451
465
|
await this.sleep(BACKOFF_DELAY_MS, signal);
|
|
@@ -463,8 +477,13 @@ export class WechatChannel {
|
|
|
463
477
|
const fromUserId = msg.from_user_id ?? '';
|
|
464
478
|
// 缓存 context_token
|
|
465
479
|
if (msg.context_token) {
|
|
480
|
+
const previousContextToken = this.contextTokenCache.get(fromUserId);
|
|
466
481
|
this.contextTokenCache.set(fromUserId, msg.context_token);
|
|
467
482
|
this.persistContextTokens();
|
|
483
|
+
if (msg.context_token !== previousContextToken) {
|
|
484
|
+
this.rejectedContextTokens.delete(fromUserId);
|
|
485
|
+
this.retryPendingContextMessage(fromUserId, msg.context_token);
|
|
486
|
+
}
|
|
468
487
|
}
|
|
469
488
|
// 提取文本(原有逻辑)
|
|
470
489
|
const text = extractTextFromMessage(msg);
|
|
@@ -483,12 +502,13 @@ export class WechatChannel {
|
|
|
483
502
|
await sendWelcomeIfNeeded(this.welcomeManager, fromUserId, fromUserId, (id, text) => this.sendMessage(id, text), 'WeChat');
|
|
484
503
|
// 回调主流程
|
|
485
504
|
if (this.messageHandler) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
catch (err) {
|
|
505
|
+
const task = Promise.resolve()
|
|
506
|
+
.then(() => this.messageHandler(fromUserId, finalContent || '', fromUserId, media.images.length ? media.images : undefined, 'private'))
|
|
507
|
+
.catch(err => {
|
|
490
508
|
logger.error('[WeChat] Message handler error:', err);
|
|
491
|
-
}
|
|
509
|
+
});
|
|
510
|
+
this.inflightHandlers.add(task);
|
|
511
|
+
void task.finally(() => this.inflightHandlers.delete(task));
|
|
492
512
|
}
|
|
493
513
|
}
|
|
494
514
|
// ── Acknowledge (sendTyping) ──────────────────────────────────────────
|
|
@@ -501,9 +521,9 @@ export class WechatChannel {
|
|
|
501
521
|
ilink_user_id: fromUserId,
|
|
502
522
|
typing_ticket: ticket,
|
|
503
523
|
status: 1, // typing
|
|
504
|
-
base_info:
|
|
524
|
+
base_info: buildBaseInfo(),
|
|
505
525
|
});
|
|
506
|
-
await this.
|
|
526
|
+
await this.apiRequest('ilink/bot/sendtyping', body, DEFAULT_CONFIG_TIMEOUT_MS);
|
|
507
527
|
logger.debug(`[WeChat] Sent typing indicator to ${fromUserId}`);
|
|
508
528
|
}
|
|
509
529
|
catch {
|
|
@@ -519,11 +539,10 @@ export class WechatChannel {
|
|
|
519
539
|
const body = JSON.stringify({
|
|
520
540
|
ilink_user_id: userId,
|
|
521
541
|
context_token: contextToken,
|
|
522
|
-
base_info:
|
|
542
|
+
base_info: buildBaseInfo(),
|
|
523
543
|
});
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
if (resp.ret === 0 && resp.typing_ticket) {
|
|
544
|
+
const resp = await this.apiRequest('ilink/bot/getconfig', body, DEFAULT_CONFIG_TIMEOUT_MS);
|
|
545
|
+
if (resp.typing_ticket) {
|
|
527
546
|
this.typingTicketCache.set(userId, { ticket: resp.typing_ticket, fetchedAt: Date.now() });
|
|
528
547
|
return resp.typing_ticket;
|
|
529
548
|
}
|
|
@@ -579,10 +598,9 @@ export class WechatChannel {
|
|
|
579
598
|
async getUploadUrl(params) {
|
|
580
599
|
const body = JSON.stringify({
|
|
581
600
|
...params,
|
|
582
|
-
base_info:
|
|
601
|
+
base_info: buildBaseInfo(),
|
|
583
602
|
});
|
|
584
|
-
const
|
|
585
|
-
const resp = JSON.parse(raw);
|
|
603
|
+
const resp = await this.apiRequest('ilink/bot/getuploadurl', body, DEFAULT_API_TIMEOUT_MS);
|
|
586
604
|
if (!resp.upload_param)
|
|
587
605
|
throw new Error('getuploadurl: no upload_param');
|
|
588
606
|
return resp;
|
|
@@ -633,7 +651,13 @@ export class WechatChannel {
|
|
|
633
651
|
}
|
|
634
652
|
async sendMediaMessage(to, item, contextToken) {
|
|
635
653
|
const clientId = `evolcore-wechat:${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
636
|
-
const
|
|
654
|
+
const delivery = await this.sendMessageItemWithContextRetry(to, item, contextToken, clientId);
|
|
655
|
+
if (delivery === 'sent') {
|
|
656
|
+
logger.info(`[WeChat] Sent media to ${to}, type=${item.type}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
buildSendMessageBody(to, item, contextToken, clientId) {
|
|
660
|
+
return JSON.stringify({
|
|
637
661
|
msg: {
|
|
638
662
|
from_user_id: '',
|
|
639
663
|
to_user_id: to,
|
|
@@ -643,27 +667,110 @@ export class WechatChannel {
|
|
|
643
667
|
item_list: [item],
|
|
644
668
|
context_token: contextToken,
|
|
645
669
|
},
|
|
646
|
-
base_info:
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
|
|
670
|
+
base_info: buildBaseInfo(),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
async sendMessageItemWithContextRetry(to, item, initialContextToken, clientId) {
|
|
674
|
+
if (this.rejectedContextTokens.get(to) === initialContextToken) {
|
|
675
|
+
this.deferUntilContextTokenRefresh(to, item, clientId);
|
|
676
|
+
return 'deferred';
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
await this.apiRequest('ilink/bot/sendmessage', this.buildSendMessageBody(to, item, initialContextToken, clientId), DEFAULT_API_TIMEOUT_MS);
|
|
680
|
+
return 'sent';
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
if (!(err instanceof WechatApiError) ||
|
|
684
|
+
!err.hasCode(-2) ||
|
|
685
|
+
err.hasCode(SESSION_EXPIRED_ERRCODE)) {
|
|
686
|
+
throw err;
|
|
687
|
+
}
|
|
688
|
+
const updatedContextToken = this.contextTokenCache.get(to);
|
|
689
|
+
if (!updatedContextToken || updatedContextToken === initialContextToken) {
|
|
690
|
+
this.rejectedContextTokens.set(to, initialContextToken);
|
|
691
|
+
this.deferUntilContextTokenRefresh(to, item, clientId, err);
|
|
692
|
+
return 'deferred';
|
|
693
|
+
}
|
|
694
|
+
logger.warn(`[WeChat] sendmessage ret=-2 for ${to}; retrying once with updated context_token`);
|
|
695
|
+
try {
|
|
696
|
+
await this.apiRequest('ilink/bot/sendmessage', this.buildSendMessageBody(to, item, updatedContextToken, clientId), DEFAULT_API_TIMEOUT_MS);
|
|
697
|
+
return 'sent';
|
|
698
|
+
}
|
|
699
|
+
catch (retryError) {
|
|
700
|
+
if (retryError instanceof WechatApiError && retryError.hasCode(-2)) {
|
|
701
|
+
this.rejectedContextTokens.set(to, updatedContextToken);
|
|
702
|
+
}
|
|
703
|
+
throw retryError;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
deferUntilContextTokenRefresh(to, item, clientId, cause) {
|
|
708
|
+
const replaced = this.pendingContextMessages.has(to);
|
|
709
|
+
this.pendingContextMessages.set(to, { item, clientId });
|
|
710
|
+
const error = new WechatContextTokenRefreshRequiredError(to, { cause });
|
|
711
|
+
logger.warn(`[WeChat] ${error.message}; deferred latest outbound` +
|
|
712
|
+
`${replaced ? ' (replaced older deferred outbound)' : ''}`);
|
|
713
|
+
}
|
|
714
|
+
retryPendingContextMessage(to, updatedContextToken) {
|
|
715
|
+
const pending = this.pendingContextMessages.get(to);
|
|
716
|
+
if (!pending)
|
|
717
|
+
return;
|
|
718
|
+
this.pendingContextMessages.delete(to);
|
|
719
|
+
void this.apiRequest('ilink/bot/sendmessage', this.buildSendMessageBody(to, pending.item, updatedContextToken, pending.clientId), DEFAULT_API_TIMEOUT_MS).then(() => {
|
|
720
|
+
logger.info(`[WeChat] Delivered deferred outbound to ${to} after context_token refresh`);
|
|
721
|
+
}).catch(err => {
|
|
722
|
+
if (err instanceof WechatApiError && err.hasCode(-2)) {
|
|
723
|
+
this.rejectedContextTokens.set(to, updatedContextToken);
|
|
724
|
+
}
|
|
725
|
+
logger.error(`[WeChat] Deferred outbound retry failed for ${to}:`, err);
|
|
726
|
+
});
|
|
650
727
|
}
|
|
651
728
|
// ── ilink API Helpers ─────────────────────────────────────────────────
|
|
652
729
|
/** Notify ilink backend of bot lifecycle events (start/stop). */
|
|
653
730
|
async notifyLifecycle(action) {
|
|
654
|
-
const body = JSON.stringify({ base_info:
|
|
655
|
-
await this.
|
|
731
|
+
const body = JSON.stringify({ base_info: buildBaseInfo() });
|
|
732
|
+
await this.apiRequest(`ilink/bot/msg/${action}`, body, DEFAULT_CONFIG_TIMEOUT_MS);
|
|
656
733
|
logger.info(`[WeChat] ${action} succeeded`);
|
|
657
734
|
}
|
|
735
|
+
async apiRequest(endpoint, body, timeoutMs, externalSignal) {
|
|
736
|
+
const rawText = await this.apiFetch(endpoint, body, timeoutMs, externalSignal);
|
|
737
|
+
let parsed;
|
|
738
|
+
try {
|
|
739
|
+
parsed = JSON.parse(rawText);
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
throw new Error(`${endpoint} returned invalid JSON`);
|
|
743
|
+
}
|
|
744
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
745
|
+
throw new Error(`${endpoint} returned an invalid response`);
|
|
746
|
+
}
|
|
747
|
+
const resp = parsed;
|
|
748
|
+
const hasError = (resp.ret !== undefined && resp.ret !== 0) ||
|
|
749
|
+
(resp.errcode !== undefined && resp.errcode !== 0);
|
|
750
|
+
if (hasError) {
|
|
751
|
+
const apiError = new WechatApiError(endpoint, resp.ret, resp.errcode, resp.errmsg);
|
|
752
|
+
if (apiError.hasCode(SESSION_EXPIRED_ERRCODE)) {
|
|
753
|
+
this.enterReauthRequired(apiError);
|
|
754
|
+
}
|
|
755
|
+
throw apiError;
|
|
756
|
+
}
|
|
757
|
+
return resp;
|
|
758
|
+
}
|
|
658
759
|
async apiFetch(endpoint, body, timeoutMs, externalSignal) {
|
|
760
|
+
this.assertAuthenticated();
|
|
659
761
|
const base = this.config.baseUrl.endsWith('/') ? this.config.baseUrl : `${this.config.baseUrl}/`;
|
|
660
762
|
const url = new URL(endpoint, base).toString();
|
|
661
|
-
const headers = this.buildHeaders(
|
|
763
|
+
const headers = this.buildHeaders();
|
|
662
764
|
const controller = new AbortController();
|
|
663
765
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
664
766
|
// 外部 signal(来自 disconnect)也要能中断
|
|
665
767
|
const onExternalAbort = () => controller.abort();
|
|
666
|
-
externalSignal?.
|
|
768
|
+
if (externalSignal?.aborted) {
|
|
769
|
+
controller.abort();
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
externalSignal?.addEventListener('abort', onExternalAbort, { once: true });
|
|
773
|
+
}
|
|
667
774
|
try {
|
|
668
775
|
const res = await fetch(url, {
|
|
669
776
|
method: 'POST',
|
|
@@ -671,27 +778,28 @@ export class WechatChannel {
|
|
|
671
778
|
body,
|
|
672
779
|
signal: controller.signal,
|
|
673
780
|
});
|
|
674
|
-
clearTimeout(timer);
|
|
675
781
|
const text = await res.text();
|
|
676
782
|
if (!res.ok)
|
|
677
783
|
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
678
784
|
return text;
|
|
679
785
|
}
|
|
680
786
|
catch (err) {
|
|
681
|
-
|
|
787
|
+
const classified = classifyFetchError(err);
|
|
788
|
+
logger.error(`[WeChat] API request failed: endpoint=${endpoint} type=${classified.type} ` +
|
|
789
|
+
`description=${classified.description}${classified.code ? ` code=${classified.code}` : ''}`);
|
|
682
790
|
throw err;
|
|
683
791
|
}
|
|
684
792
|
finally {
|
|
793
|
+
clearTimeout(timer);
|
|
685
794
|
externalSignal?.removeEventListener('abort', onExternalAbort);
|
|
686
795
|
}
|
|
687
796
|
}
|
|
688
|
-
buildHeaders(
|
|
797
|
+
buildHeaders() {
|
|
689
798
|
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
690
799
|
const wechatUin = Buffer.from(String(uint32), 'utf-8').toString('base64');
|
|
691
800
|
const headers = {
|
|
692
801
|
'Content-Type': 'application/json',
|
|
693
802
|
'AuthorizationType': 'ilink_bot_token',
|
|
694
|
-
'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
|
|
695
803
|
'X-WECHAT-UIN': wechatUin,
|
|
696
804
|
'iLink-App-Id': ILINK_APP_ID,
|
|
697
805
|
'iLink-App-ClientVersion': ILINK_APP_CLIENT_VERSION,
|
|
@@ -701,6 +809,42 @@ export class WechatChannel {
|
|
|
701
809
|
}
|
|
702
810
|
return headers;
|
|
703
811
|
}
|
|
812
|
+
assertAuthenticated() {
|
|
813
|
+
if (this.connectionState === 'reauth_required') {
|
|
814
|
+
throw new Error('WeChat reauthentication required; run: ec init wechat');
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
enterReauthRequired(error) {
|
|
818
|
+
if (this.connectionState === 'reauth_required')
|
|
819
|
+
return;
|
|
820
|
+
this.connected = false;
|
|
821
|
+
this.connectionState = 'reauth_required';
|
|
822
|
+
this.lastError = error.message;
|
|
823
|
+
this.reauthRequiredAt = Date.now();
|
|
824
|
+
this.pendingContextMessages.clear();
|
|
825
|
+
this.rejectedContextTokens.clear();
|
|
826
|
+
this.abortController?.abort();
|
|
827
|
+
const authMsg = '微信 token 已失效,通道已停止使用旧 token。请运行: ec init wechat';
|
|
828
|
+
logger.error(`[WeChat] Reauthentication required: ${error.message}`);
|
|
829
|
+
try {
|
|
830
|
+
if (this.eventBus) {
|
|
831
|
+
this.eventBus.publish({
|
|
832
|
+
type: 'channel:error',
|
|
833
|
+
channel: 'wechat',
|
|
834
|
+
...(this.channelName ? { channelName: this.channelName } : {}),
|
|
835
|
+
status: 'auth_error',
|
|
836
|
+
message: authMsg,
|
|
837
|
+
timestamp: Date.now(),
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
else if (this.onSessionExpired) {
|
|
841
|
+
this.onSessionExpired(authMsg);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch (notifyError) {
|
|
845
|
+
logger.error('[WeChat] Failed to publish reauthentication notice:', notifyError);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
704
848
|
// ── Persistence ────────────────────────────────────────────────────
|
|
705
849
|
/** 持久化 context_token 到文件,供 restart-monitor 等外部进程读取 */
|
|
706
850
|
persistContextTokens() {
|
|
@@ -718,6 +862,10 @@ export class WechatChannel {
|
|
|
718
862
|
// ── Utilities ─────────────────────────────────────────────────────────
|
|
719
863
|
sleep(ms, signal) {
|
|
720
864
|
return new Promise((resolve, reject) => {
|
|
865
|
+
if (signal.aborted) {
|
|
866
|
+
reject(new Error('aborted'));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
721
869
|
const t = setTimeout(resolve, ms);
|
|
722
870
|
signal.addEventListener('abort', () => {
|
|
723
871
|
clearTimeout(t);
|
|
@@ -795,7 +943,7 @@ export class WechatChannelPlugin {
|
|
|
795
943
|
return {
|
|
796
944
|
channelType: 'wechat', adapter, channel,
|
|
797
945
|
policy,
|
|
798
|
-
options: { fileMarkerPattern:
|
|
946
|
+
options: { fileMarkerPattern: createSendFileMarkerPattern(), flushDelay: inst.flushDelay ?? DEFAULT_FLUSH_DELAY_SECONDS },
|
|
799
947
|
connect: () => channel.connect(),
|
|
800
948
|
disconnect: () => channel.disconnect(),
|
|
801
949
|
onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
|