@zhin.js/adapter-weixin-ilink 5.0.2 → 5.0.4
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 +37 -0
- package/README.md +0 -1
- package/adapters/weixin-ilink.js +4 -3
- package/adapters/weixin-ilink.ts +4 -3
- package/lib/credentials.d.ts +6 -6
- package/lib/credentials.js +12 -12
- package/lib/endpoint.js +36 -36
- package/lib/login.js +5 -5
- package/lib/protocol.d.ts +3 -3
- package/lib/protocol.js +5 -5
- package/package.json +11 -11
- package/schema.json +41 -5
- package/src/credentials.ts +12 -12
- package/src/endpoint.ts +38 -38
- package/src/login.ts +5 -5
- package/src/protocol.ts +7 -7
- package/src/silk-wasm.d.ts +6 -2
- /package/commands/endpoint/add/{[name].js → [id].js} +0 -0
- /package/commands/endpoint/add/{[name].ts → [id].ts} +0 -0
- /package/commands/endpoint/remove/{[name].js → [id].js} +0 -0
- /package/commands/endpoint/remove/{[name].ts → [id].ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @zhin.js/adapter-weixin-ilink
|
|
2
2
|
|
|
3
|
+
## 5.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [c106ecc]
|
|
8
|
+
- Updated dependencies [ca92e03]
|
|
9
|
+
- Updated dependencies [b0f37ae]
|
|
10
|
+
- Updated dependencies [ba08a2f]
|
|
11
|
+
- Updated dependencies [daffd4c]
|
|
12
|
+
- Updated dependencies [36c7400]
|
|
13
|
+
- Updated dependencies [162fa34]
|
|
14
|
+
- Updated dependencies [e40b048]
|
|
15
|
+
- Updated dependencies [f1708c3]
|
|
16
|
+
- Updated dependencies [e53444f]
|
|
17
|
+
- Updated dependencies [92b0dd7]
|
|
18
|
+
- Updated dependencies [a7df753]
|
|
19
|
+
- @zhin.js/im-contract@1.0.3
|
|
20
|
+
- @zhin.js/adapter@1.1.7
|
|
21
|
+
- @zhin.js/plugin-runtime@1.1.5
|
|
22
|
+
- @zhin.js/command@1.0.9
|
|
23
|
+
- @zhin.js/core@1.5.4
|
|
24
|
+
- zhin.js@6.0.4
|
|
25
|
+
|
|
26
|
+
## 5.0.3
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- f8c7a54: fix: im
|
|
31
|
+
- Updated dependencies [f8c7a54]
|
|
32
|
+
- @zhin.js/logger@1.0.76
|
|
33
|
+
- @zhin.js/adapter@1.1.6
|
|
34
|
+
- @zhin.js/command@1.0.8
|
|
35
|
+
- @zhin.js/core@1.5.3
|
|
36
|
+
- @zhin.js/im-contract@1.0.2
|
|
37
|
+
- @zhin.js/plugin-runtime@1.1.4
|
|
38
|
+
- zhin.js@6.0.3
|
|
39
|
+
|
|
3
40
|
## 5.0.2
|
|
4
41
|
|
|
5
42
|
### Patch Changes
|
package/README.md
CHANGED
package/adapters/weixin-ilink.js
CHANGED
|
@@ -19,9 +19,10 @@ export default defineAdapter({
|
|
|
19
19
|
create(context) {
|
|
20
20
|
const config = resolveWeixinIlinkConfig(context.config);
|
|
21
21
|
// 注册到插件运行时状态(weixin-ilink.endpoint list 的"运行中"数据源)
|
|
22
|
-
context.use(weixinIlinkRuntimeStateToken).endpoints.set(config.
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
context.use(weixinIlinkRuntimeStateToken).endpoints.set(config.id, {
|
|
23
|
+
id: config.id,
|
|
24
|
+
// iLink 仅长轮询一种接入方式,无 config.connection 字段
|
|
25
|
+
mode: 'long-poll',
|
|
25
26
|
});
|
|
26
27
|
return new WeixinIlinkEndpoint({
|
|
27
28
|
id: context.id,
|
package/adapters/weixin-ilink.ts
CHANGED
|
@@ -30,9 +30,10 @@ export default defineAdapter<WeixinIlinkAdapterConfig>({
|
|
|
30
30
|
create(context) {
|
|
31
31
|
const config = resolveWeixinIlinkConfig(context.config);
|
|
32
32
|
// 注册到插件运行时状态(weixin-ilink.endpoint list 的"运行中"数据源)
|
|
33
|
-
context.use(weixinIlinkRuntimeStateToken).endpoints.set(config.
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
context.use(weixinIlinkRuntimeStateToken).endpoints.set(config.id, {
|
|
34
|
+
id: config.id,
|
|
35
|
+
// iLink 仅长轮询一种接入方式,无 config.connection 字段
|
|
36
|
+
mode: 'long-poll',
|
|
36
37
|
});
|
|
37
38
|
return new WeixinIlinkEndpoint({
|
|
38
39
|
id: context.id,
|
package/lib/credentials.d.ts
CHANGED
|
@@ -6,9 +6,9 @@ export interface WeixinIlinkCredentials {
|
|
|
6
6
|
updatedAt?: string;
|
|
7
7
|
}
|
|
8
8
|
export declare function resolveStateDir(): string;
|
|
9
|
-
export declare function credentialsPath(
|
|
10
|
-
export declare function loadCredentials(
|
|
11
|
-
export declare function saveCredentials(
|
|
12
|
-
export declare function syncBufPath(
|
|
13
|
-
export declare function loadSyncBuf(
|
|
14
|
-
export declare function saveSyncBuf(
|
|
9
|
+
export declare function credentialsPath(endpointId: string): string;
|
|
10
|
+
export declare function loadCredentials(endpointId: string): WeixinIlinkCredentials | null;
|
|
11
|
+
export declare function saveCredentials(endpointId: string, creds: WeixinIlinkCredentials): void;
|
|
12
|
+
export declare function syncBufPath(endpointId: string): string;
|
|
13
|
+
export declare function loadSyncBuf(endpointId: string): string;
|
|
14
|
+
export declare function saveSyncBuf(endpointId: string, buf: string): void;
|
package/lib/credentials.js
CHANGED
|
@@ -6,11 +6,11 @@ export function resolveStateDir() {
|
|
|
6
6
|
? path.join(process.env.ZHIN_DATA_DIR, "weixin-ilink")
|
|
7
7
|
: DEFAULT_DATA_DIR;
|
|
8
8
|
}
|
|
9
|
-
export function credentialsPath(
|
|
10
|
-
return path.join(resolveStateDir(), `${
|
|
9
|
+
export function credentialsPath(endpointId) {
|
|
10
|
+
return path.join(resolveStateDir(), `${endpointId}.json`);
|
|
11
11
|
}
|
|
12
|
-
export function loadCredentials(
|
|
13
|
-
const file = credentialsPath(
|
|
12
|
+
export function loadCredentials(endpointId) {
|
|
13
|
+
const file = credentialsPath(endpointId);
|
|
14
14
|
try {
|
|
15
15
|
if (!fs.existsSync(file))
|
|
16
16
|
return null;
|
|
@@ -24,16 +24,16 @@ export function loadCredentials(endpointName) {
|
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
export function saveCredentials(
|
|
28
|
-
const file = credentialsPath(
|
|
27
|
+
export function saveCredentials(endpointId, creds) {
|
|
28
|
+
const file = credentialsPath(endpointId);
|
|
29
29
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
30
30
|
fs.writeFileSync(file, JSON.stringify({ ...creds, updatedAt: new Date().toISOString() }, null, 2), "utf-8");
|
|
31
31
|
}
|
|
32
|
-
export function syncBufPath(
|
|
33
|
-
return path.join(resolveStateDir(), `${
|
|
32
|
+
export function syncBufPath(endpointId) {
|
|
33
|
+
return path.join(resolveStateDir(), `${endpointId}.sync-buf`);
|
|
34
34
|
}
|
|
35
|
-
export function loadSyncBuf(
|
|
36
|
-
const file = syncBufPath(
|
|
35
|
+
export function loadSyncBuf(endpointId) {
|
|
36
|
+
const file = syncBufPath(endpointId);
|
|
37
37
|
try {
|
|
38
38
|
return fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "";
|
|
39
39
|
}
|
|
@@ -41,8 +41,8 @@ export function loadSyncBuf(endpointName) {
|
|
|
41
41
|
return "";
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
export function saveSyncBuf(
|
|
45
|
-
const file = syncBufPath(
|
|
44
|
+
export function saveSyncBuf(endpointId, buf) {
|
|
45
|
+
const file = syncBufPath(endpointId);
|
|
46
46
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
47
47
|
fs.writeFileSync(file, buf, "utf-8");
|
|
48
48
|
}
|
package/lib/endpoint.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { formatCompact,
|
|
7
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
8
8
|
import { getUpdates, notifyStart, notifyStop, sendTyping } from './ilink-api.js';
|
|
9
9
|
import { configureIlinkMeta } from './ilink-meta.js';
|
|
10
10
|
import { loadSyncBuf, resolveStateDir, saveSyncBuf, } from './credentials.js';
|
|
@@ -19,7 +19,6 @@ import { materializeOutboundMedia } from './outbound-media.js';
|
|
|
19
19
|
import { MessageItemType } from './ilink-types.js';
|
|
20
20
|
import { getExtensionFromMime, sniffMimeFromBuffer } from './mime.js';
|
|
21
21
|
import { formatInboundContent, formatOutboundSegments, inboundMessageId, segmentLocalPath, sleep, weixinIlinkInboundConversation, } from './protocol.js';
|
|
22
|
-
const logger = getLogger('weixin-ilink');
|
|
23
22
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
24
23
|
const BACKOFF_DELAY_MS = 30_000;
|
|
25
24
|
const RETRY_DELAY_MS = 2_000;
|
|
@@ -28,6 +27,7 @@ const WEIXIN_MEDIA_MAX_BYTES = 100 * 1024 * 1024;
|
|
|
28
27
|
const INBOUND_MEDIA_TTL_MS = 24 * 60 * 60 * 1000;
|
|
29
28
|
const MEDIA_SWEEP_INTERVAL_MS = 60 * 60 * 1000;
|
|
30
29
|
export class WeixinIlinkEndpoint {
|
|
30
|
+
#logger;
|
|
31
31
|
#options;
|
|
32
32
|
#resolveCredentials;
|
|
33
33
|
#notifyStart;
|
|
@@ -44,6 +44,7 @@ export class WeixinIlinkEndpoint {
|
|
|
44
44
|
#started = false;
|
|
45
45
|
management = createWeixinIlinkEndpointManagement(this);
|
|
46
46
|
constructor(options) {
|
|
47
|
+
this.#logger = getAdapterLogger('weixin-ilink', options.config.id);
|
|
47
48
|
this.#options = options;
|
|
48
49
|
this.#resolveCredentials = options.resolveCredentials ?? resolveCredentials;
|
|
49
50
|
this.#notifyStart = options.notifyStart ?? notifyStart;
|
|
@@ -56,7 +57,7 @@ export class WeixinIlinkEndpoint {
|
|
|
56
57
|
}
|
|
57
58
|
/** Live endpoint 名(context token 按 account 名分桶,management 推导对端用)。 */
|
|
58
59
|
get configName() {
|
|
59
|
-
return this.#options.config.
|
|
60
|
+
return this.#options.config.id;
|
|
60
61
|
}
|
|
61
62
|
apiBaseUrl() {
|
|
62
63
|
return this.#creds?.baseUrl ?? this.#options.config.baseUrl;
|
|
@@ -73,23 +74,23 @@ export class WeixinIlinkEndpoint {
|
|
|
73
74
|
// QR 登录最长 8 分钟:stop() 必须能打断
|
|
74
75
|
this.#loginAbort = new AbortController();
|
|
75
76
|
this.#creds = await this.#resolveCredentials(this.#options.config, this.#loginAbort.signal);
|
|
76
|
-
restoreContextTokens(this.#options.config.
|
|
77
|
+
restoreContextTokens(this.#options.config.id);
|
|
77
78
|
await this.#notifyStart({
|
|
78
79
|
baseUrl: this.apiBaseUrl(),
|
|
79
80
|
token: this.#creds.botToken,
|
|
80
81
|
});
|
|
81
|
-
this.#configManager = new WeixinConfigManager({ baseUrl: this.apiBaseUrl(), token: this.#creds.botToken }, (msg) => logger.debug(msg));
|
|
82
|
+
this.#configManager = new WeixinConfigManager({ baseUrl: this.apiBaseUrl(), token: this.#creds.botToken }, (msg) => this.#logger.debug(msg));
|
|
82
83
|
this.#pollAbort = new AbortController();
|
|
83
84
|
this.#pollPromise = this.#pollLoop(this.#pollAbort.signal);
|
|
84
85
|
this.#startMediaSweep();
|
|
85
|
-
logger.info(formatCompact({
|
|
86
|
+
this.#logger.info(formatCompact({
|
|
86
87
|
op: 'connect',
|
|
87
|
-
endpoint: this.#options.config.
|
|
88
|
+
endpoint: this.#options.config.id,
|
|
88
89
|
}));
|
|
89
90
|
}
|
|
90
91
|
catch (error) {
|
|
91
92
|
await this.stop();
|
|
92
|
-
logger.error('Failed to connect weixin-ilink bot:', error);
|
|
93
|
+
this.#logger.error('Failed to connect weixin-ilink bot:', error);
|
|
93
94
|
throw error;
|
|
94
95
|
}
|
|
95
96
|
}
|
|
@@ -111,13 +112,13 @@ export class WeixinIlinkEndpoint {
|
|
|
111
112
|
}
|
|
112
113
|
this.#stopMediaSweep();
|
|
113
114
|
// 防抖中的 context token 落盘,避免 stop 丢尾部写入
|
|
114
|
-
flushContextTokenPersist(this.#options.config.
|
|
115
|
+
flushContextTokenPersist(this.#options.config.id);
|
|
115
116
|
if (this.#creds?.botToken) {
|
|
116
117
|
try {
|
|
117
118
|
await this.#notifyStop({ baseUrl: this.apiBaseUrl(), token: this.#creds.botToken });
|
|
118
119
|
}
|
|
119
120
|
catch (err) {
|
|
120
|
-
logger.warn(formatCompact({
|
|
121
|
+
this.#logger.warn(formatCompact({
|
|
121
122
|
op: 'notify_stop',
|
|
122
123
|
ok: false,
|
|
123
124
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -125,9 +126,8 @@ export class WeixinIlinkEndpoint {
|
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
this.#started = false;
|
|
128
|
-
logger.debug(formatCompact({
|
|
129
|
+
this.#logger.debug(formatCompact({
|
|
129
130
|
op: 'disconnect',
|
|
130
|
-
endpoint: this.#options.config.name,
|
|
131
131
|
}));
|
|
132
132
|
}
|
|
133
133
|
async send({ conversation, payload }) {
|
|
@@ -136,9 +136,9 @@ export class WeixinIlinkEndpoint {
|
|
|
136
136
|
}
|
|
137
137
|
// 个人微信仅 private 会话,对端 user_id 即 conversation.id
|
|
138
138
|
const target = conversation.id;
|
|
139
|
-
const contextToken = getContextToken(this.#options.config.
|
|
139
|
+
const contextToken = getContextToken(this.#options.config.id, target);
|
|
140
140
|
if (!contextToken) {
|
|
141
|
-
logger.warn(formatCompact({
|
|
141
|
+
this.#logger.warn(formatCompact({
|
|
142
142
|
op: 'send',
|
|
143
143
|
ok: false,
|
|
144
144
|
reason: 'missing_context_token',
|
|
@@ -192,7 +192,7 @@ export class WeixinIlinkEndpoint {
|
|
|
192
192
|
continue;
|
|
193
193
|
}
|
|
194
194
|
if (seg.type === 'image' || seg.type === 'video' || seg.type === 'file' || seg.type === 'record') {
|
|
195
|
-
logger.warn(formatCompact({
|
|
195
|
+
this.#logger.warn(formatCompact({
|
|
196
196
|
op: 'send',
|
|
197
197
|
skip: 'no_local_file',
|
|
198
198
|
type: seg.type,
|
|
@@ -215,21 +215,21 @@ export class WeixinIlinkEndpoint {
|
|
|
215
215
|
return;
|
|
216
216
|
const userId = msg.from_user_id ?? '';
|
|
217
217
|
if (msg.context_token && userId) {
|
|
218
|
-
setContextToken(this.#options.config.
|
|
218
|
+
setContextToken(this.#options.config.id, userId, msg.context_token);
|
|
219
219
|
}
|
|
220
220
|
const conversation = weixinIlinkInboundConversation(String(this.#options.id), userId);
|
|
221
221
|
void this.#options.gateway.receive({
|
|
222
222
|
conversation,
|
|
223
223
|
message: { conversation, id: inboundMessageId(msg) },
|
|
224
224
|
content: formatInboundContent(msg),
|
|
225
|
-
sender: userId,
|
|
225
|
+
sender: { id: userId },
|
|
226
|
+
endpointId: this.#options.config.id,
|
|
226
227
|
metadata: Object.freeze({
|
|
227
|
-
endpoint: this.#options.config.name,
|
|
228
228
|
messageType: msg.message_type,
|
|
229
229
|
createTimeMs: msg.create_time_ms,
|
|
230
230
|
}),
|
|
231
231
|
}).catch((err) => {
|
|
232
|
-
logger.warn(formatCompact({
|
|
232
|
+
this.#logger.warn(formatCompact({
|
|
233
233
|
op: 'weixin_ilink_gateway_receive_failed',
|
|
234
234
|
target: userId,
|
|
235
235
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -239,10 +239,10 @@ export class WeixinIlinkEndpoint {
|
|
|
239
239
|
async sendTypingToUser(userId, status) {
|
|
240
240
|
if (!this.#creds?.botToken || !this.#configManager)
|
|
241
241
|
return;
|
|
242
|
-
const contextToken = getContextToken(this.#options.config.
|
|
242
|
+
const contextToken = getContextToken(this.#options.config.id, userId);
|
|
243
243
|
const cfg = await this.#configManager.getForUser(userId, contextToken);
|
|
244
244
|
if (!cfg.typingTicket) {
|
|
245
|
-
logger.debug(formatCompact({
|
|
245
|
+
this.#logger.debug(formatCompact({
|
|
246
246
|
op: 'send_typing',
|
|
247
247
|
skip: 'no_typing_ticket',
|
|
248
248
|
userId,
|
|
@@ -262,13 +262,13 @@ export class WeixinIlinkEndpoint {
|
|
|
262
262
|
async #pollLoop(abortSignal) {
|
|
263
263
|
if (!this.#creds?.botToken)
|
|
264
264
|
return;
|
|
265
|
-
const
|
|
266
|
-
let getUpdatesBuf = loadSyncBuf(
|
|
265
|
+
const endpointId = this.#options.config.id;
|
|
266
|
+
let getUpdatesBuf = loadSyncBuf(endpointId);
|
|
267
267
|
let nextTimeoutMs = this.#options.config.longPollTimeoutMs;
|
|
268
268
|
let consecutiveFailures = 0;
|
|
269
269
|
while (!abortSignal.aborted) {
|
|
270
|
-
if (isSessionPaused(
|
|
271
|
-
await sleep(getRemainingPauseMs(
|
|
270
|
+
if (isSessionPaused(endpointId)) {
|
|
271
|
+
await sleep(getRemainingPauseMs(endpointId), abortSignal);
|
|
272
272
|
continue;
|
|
273
273
|
}
|
|
274
274
|
try {
|
|
@@ -287,9 +287,9 @@ export class WeixinIlinkEndpoint {
|
|
|
287
287
|
if (isApiError) {
|
|
288
288
|
const sessionExpired = resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
|
|
289
289
|
if (sessionExpired) {
|
|
290
|
-
pauseSession(
|
|
290
|
+
pauseSession(endpointId);
|
|
291
291
|
consecutiveFailures = 0;
|
|
292
|
-
await sleep(getRemainingPauseMs(
|
|
292
|
+
await sleep(getRemainingPauseMs(endpointId), abortSignal);
|
|
293
293
|
continue;
|
|
294
294
|
}
|
|
295
295
|
consecutiveFailures += 1;
|
|
@@ -310,14 +310,14 @@ export class WeixinIlinkEndpoint {
|
|
|
310
310
|
}
|
|
311
311
|
if (nextBuf) {
|
|
312
312
|
getUpdatesBuf = nextBuf;
|
|
313
|
-
saveSyncBuf(
|
|
313
|
+
saveSyncBuf(endpointId, getUpdatesBuf);
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
catch (err) {
|
|
317
317
|
if (abortSignal.aborted)
|
|
318
318
|
return;
|
|
319
319
|
consecutiveFailures += 1;
|
|
320
|
-
logger.error(formatCompact({
|
|
320
|
+
this.#logger.error(formatCompact({
|
|
321
321
|
op: 'poll',
|
|
322
322
|
ok: false,
|
|
323
323
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -334,13 +334,13 @@ export class WeixinIlinkEndpoint {
|
|
|
334
334
|
async #handleInboundMessage(full) {
|
|
335
335
|
const fromUserId = full.from_user_id ?? '';
|
|
336
336
|
if (full.context_token && fromUserId) {
|
|
337
|
-
setContextToken(this.#options.config.
|
|
337
|
+
setContextToken(this.#options.config.id, fromUserId, full.context_token);
|
|
338
338
|
}
|
|
339
339
|
const mediaOpts = await this.#downloadInboundMedia(full);
|
|
340
340
|
this.admit({ ...full, _media: mediaOpts });
|
|
341
|
-
logger.debug(formatCompact({
|
|
341
|
+
this.#logger.debug(formatCompact({
|
|
342
342
|
op: 'recv',
|
|
343
|
-
endpoint: this.#options.config.
|
|
343
|
+
endpoint: this.#options.config.id,
|
|
344
344
|
target: fromUserId,
|
|
345
345
|
preview: formatInboundContent({ ...full, _media: mediaOpts }).slice(0, 80),
|
|
346
346
|
}));
|
|
@@ -356,8 +356,8 @@ export class WeixinIlinkEndpoint {
|
|
|
356
356
|
return downloadMediaFromItem(mediaItem, {
|
|
357
357
|
cdnBaseUrl: this.cdnBaseUrl(),
|
|
358
358
|
saveMedia: (buffer, contentType, subdir, maxBytes, originalFilename) => this.#saveInboundMedia(buffer, contentType, subdir, maxBytes, originalFilename),
|
|
359
|
-
log: (msg) => logger.debug(msg),
|
|
360
|
-
errLog: (msg) => logger.warn(msg),
|
|
359
|
+
log: (msg) => this.#logger.debug(msg),
|
|
360
|
+
errLog: (msg) => this.#logger.warn(msg),
|
|
361
361
|
label: `inbound:${full.from_user_id ?? 'unknown'}`,
|
|
362
362
|
});
|
|
363
363
|
}
|
|
@@ -414,9 +414,9 @@ export class WeixinIlinkEndpoint {
|
|
|
414
414
|
if (now - stat.mtimeMs <= INBOUND_MEDIA_TTL_MS)
|
|
415
415
|
continue;
|
|
416
416
|
fs.unlinkSync(filePath);
|
|
417
|
-
logger.debug(formatCompact({
|
|
417
|
+
this.#logger.debug(formatCompact({
|
|
418
418
|
op: 'media_sweep',
|
|
419
|
-
endpoint: this.#options.config.
|
|
419
|
+
endpoint: this.#options.config.id,
|
|
420
420
|
file: entry.name,
|
|
421
421
|
}));
|
|
422
422
|
}
|
package/lib/login.js
CHANGED
|
@@ -67,7 +67,7 @@ function sleepWithAbort(ms, signal) {
|
|
|
67
67
|
export async function resolveCredentials(config, signal) {
|
|
68
68
|
const envToken = process.env.WEIXIN_ILINK_TOKEN?.trim() || config.botToken?.trim();
|
|
69
69
|
if (envToken) {
|
|
70
|
-
const stored = loadCredentials(config.
|
|
70
|
+
const stored = loadCredentials(config.id);
|
|
71
71
|
return {
|
|
72
72
|
botToken: envToken,
|
|
73
73
|
ilinkUserId: stored?.ilinkUserId,
|
|
@@ -75,7 +75,7 @@ export async function resolveCredentials(config, signal) {
|
|
|
75
75
|
baseUrl: config.baseUrl ?? stored?.baseUrl ?? DEFAULT_API_BASE_URL,
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
|
-
const stored = loadCredentials(config.
|
|
78
|
+
const stored = loadCredentials(config.id);
|
|
79
79
|
if (stored?.botToken) {
|
|
80
80
|
return {
|
|
81
81
|
...stored,
|
|
@@ -88,7 +88,7 @@ export async function loginWithQr(config, signal) {
|
|
|
88
88
|
const apiBaseUrl = config.baseUrl ?? DEFAULT_API_BASE_URL;
|
|
89
89
|
const qr = await fetchQRCode(apiBaseUrl, DEFAULT_ILINK_BOT_TYPE);
|
|
90
90
|
const qrcodeUrl = qr.qrcode_img_content || qr.qrcode;
|
|
91
|
-
logger.info(`weixin-ilink QR login for ${config.
|
|
91
|
+
logger.info(`weixin-ilink QR login for ${config.id}: scan with WeChat ClawBot entry — ${qrcodeUrl.slice(0, 120)}${qrcodeUrl.length > 120 ? '…' : ''}`);
|
|
92
92
|
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
93
93
|
let currentBaseUrl = apiBaseUrl;
|
|
94
94
|
while (Date.now() < deadline) {
|
|
@@ -106,7 +106,7 @@ export async function loginWithQr(config, signal) {
|
|
|
106
106
|
throw new Error('扫码成功但未返回 bot_token');
|
|
107
107
|
}
|
|
108
108
|
const creds = {
|
|
109
|
-
botToken: status.bot_token ?? loadCredentials(config.
|
|
109
|
+
botToken: status.bot_token ?? loadCredentials(config.id)?.botToken ?? '',
|
|
110
110
|
ilinkUserId: status.ilink_user_id,
|
|
111
111
|
ilinkBotId: status.ilink_endpoint_id,
|
|
112
112
|
baseUrl: status.baseurl ?? currentBaseUrl,
|
|
@@ -114,7 +114,7 @@ export async function loginWithQr(config, signal) {
|
|
|
114
114
|
if (!creds.botToken) {
|
|
115
115
|
throw new Error('binded_redirect 但本地无有效 bot_token');
|
|
116
116
|
}
|
|
117
|
-
saveCredentials(config.
|
|
117
|
+
saveCredentials(config.id, creds);
|
|
118
118
|
return creds;
|
|
119
119
|
}
|
|
120
120
|
if (status.status === 'expired') {
|
package/lib/protocol.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ConversationRef } from '@zhin.js/im-contract';
|
|
|
6
6
|
import type { WeixinMessage } from './ilink-types.js';
|
|
7
7
|
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
8
8
|
export interface WeixinIlinkAdapterConfig {
|
|
9
|
-
readonly
|
|
9
|
+
readonly id?: string;
|
|
10
10
|
readonly botAgent?: string;
|
|
11
11
|
readonly baseUrl?: string;
|
|
12
12
|
readonly cdnBaseUrl?: string;
|
|
@@ -19,7 +19,7 @@ export interface WeixinIlinkAdapterConfig {
|
|
|
19
19
|
}
|
|
20
20
|
export interface ResolvedWeixinIlinkConfig {
|
|
21
21
|
readonly context: 'weixin-ilink';
|
|
22
|
-
readonly
|
|
22
|
+
readonly id: string;
|
|
23
23
|
readonly botAgent?: string;
|
|
24
24
|
readonly baseUrl: string;
|
|
25
25
|
readonly cdnBaseUrl: string;
|
|
@@ -48,7 +48,7 @@ export declare function inboundMessageId(msg: WeixinMessage): string;
|
|
|
48
48
|
* 入站归一化 → ConversationRef:个人微信无群/频道概念,全部会话都是
|
|
49
49
|
* 与 `from_user_id` 的 private 会话(无 parent)。
|
|
50
50
|
*/
|
|
51
|
-
export declare function weixinIlinkInboundConversation(
|
|
51
|
+
export declare function weixinIlinkInboundConversation(endpointKey: string, userId: string): ConversationRef;
|
|
52
52
|
/**
|
|
53
53
|
* Wire-encode an already-rendered outbound payload into Weixin wire segments.
|
|
54
54
|
* Segment canonicalization is intentionally not done here.
|
package/lib/protocol.js
CHANGED
|
@@ -9,8 +9,8 @@ import { DEFAULT_API_BASE_URL, DEFAULT_CDN_BASE_URL } from './ilink-meta.js';
|
|
|
9
9
|
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
10
10
|
export function resolveWeixinIlinkConfig(config = {}) {
|
|
11
11
|
const entry = config.endpoints?.find((item) => item.context === 'weixin-ilink');
|
|
12
|
-
const
|
|
13
|
-
|| (typeof entry?.
|
|
12
|
+
const id = (typeof config.id === 'string' && config.id)
|
|
13
|
+
|| (typeof entry?.id === 'string' && entry.id)
|
|
14
14
|
|| process.env.WEIXIN_ILINK_BOT_NAME
|
|
15
15
|
|| 'weixin-ilink-bot';
|
|
16
16
|
const botToken = pickCredential(typeof config.botToken === 'string' ? config.botToken.trim() : config.botToken, typeof entry?.botToken === 'string' ? entry.botToken.trim() : entry?.botToken, process.env.WEIXIN_ILINK_TOKEN?.trim());
|
|
@@ -18,7 +18,7 @@ export function resolveWeixinIlinkConfig(config = {}) {
|
|
|
18
18
|
// (sidecar credentials file / QR login) during endpoint.start().
|
|
19
19
|
return {
|
|
20
20
|
context: 'weixin-ilink',
|
|
21
|
-
|
|
21
|
+
id,
|
|
22
22
|
botAgent: config.botAgent ?? entry?.botAgent,
|
|
23
23
|
baseUrl: config.baseUrl ?? entry?.baseUrl ?? DEFAULT_API_BASE_URL,
|
|
24
24
|
cdnBaseUrl: config.cdnBaseUrl ?? entry?.cdnBaseUrl ?? DEFAULT_CDN_BASE_URL,
|
|
@@ -59,9 +59,9 @@ export function inboundMessageId(msg) {
|
|
|
59
59
|
* 入站归一化 → ConversationRef:个人微信无群/频道概念,全部会话都是
|
|
60
60
|
* 与 `from_user_id` 的 private 会话(无 parent)。
|
|
61
61
|
*/
|
|
62
|
-
export function weixinIlinkInboundConversation(
|
|
62
|
+
export function weixinIlinkInboundConversation(endpointKey, userId) {
|
|
63
63
|
return {
|
|
64
|
-
endpoint: { id:
|
|
64
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
65
65
|
kind: 'private',
|
|
66
66
|
id: userId,
|
|
67
67
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-weixin-ilink",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Zhin.js Weixin iLink (ClawBot) adapter for Plugin Runtime (long-poll)",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -48,18 +48,18 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@zhin.js/adapter": "1.1.
|
|
52
|
-
"@zhin.js/command": "1.0.
|
|
53
|
-
"@zhin.js/core": "1.5.
|
|
54
|
-
"@zhin.js/im-contract": "1.0.
|
|
55
|
-
"@zhin.js/logger": "1.0.
|
|
56
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
51
|
+
"@zhin.js/adapter": "1.1.7",
|
|
52
|
+
"@zhin.js/command": "1.0.9",
|
|
53
|
+
"@zhin.js/core": "1.5.4",
|
|
54
|
+
"@zhin.js/im-contract": "1.0.3",
|
|
55
|
+
"@zhin.js/logger": "1.0.76",
|
|
56
|
+
"@zhin.js/plugin-runtime": "1.1.5"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@zhin.js/adapter": "1.1.
|
|
60
|
-
"@zhin.js/core": "1.5.
|
|
61
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
62
|
-
"zhin.js": "6.0.
|
|
59
|
+
"@zhin.js/adapter": "1.1.7",
|
|
60
|
+
"@zhin.js/core": "1.5.4",
|
|
61
|
+
"@zhin.js/plugin-runtime": "1.1.5",
|
|
62
|
+
"zhin.js": "6.0.4"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"zhin.js": {
|
package/schema.json
CHANGED
|
@@ -18,24 +18,60 @@
|
|
|
18
18
|
"type": "number",
|
|
19
19
|
"default": 35000
|
|
20
20
|
},
|
|
21
|
+
"master": {
|
|
22
|
+
"type": [
|
|
23
|
+
"string",
|
|
24
|
+
"number"
|
|
25
|
+
],
|
|
26
|
+
"description": "框架 master(Weixin user id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
|
|
27
|
+
},
|
|
28
|
+
"trusted": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": {
|
|
31
|
+
"type": [
|
|
32
|
+
"string",
|
|
33
|
+
"number"
|
|
34
|
+
],
|
|
35
|
+
"description": "Trusted Weixin user id"
|
|
36
|
+
},
|
|
37
|
+
"description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
|
|
38
|
+
},
|
|
21
39
|
"endpoints": {
|
|
22
40
|
"type": "array",
|
|
23
|
-
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(
|
|
41
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
|
|
24
42
|
"items": {
|
|
25
43
|
"type": "object",
|
|
26
44
|
"additionalProperties": true,
|
|
27
45
|
"properties": {
|
|
28
|
-
"
|
|
29
|
-
"type":
|
|
30
|
-
|
|
46
|
+
"master": {
|
|
47
|
+
"type": [
|
|
48
|
+
"string",
|
|
49
|
+
"number"
|
|
50
|
+
],
|
|
51
|
+
"description": "本 endpoint 的框架 master(Weixin user id);覆盖顶层 master"
|
|
52
|
+
},
|
|
53
|
+
"trusted": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": {
|
|
56
|
+
"type": [
|
|
57
|
+
"string",
|
|
58
|
+
"number"
|
|
59
|
+
],
|
|
60
|
+
"description": "Trusted Weixin user id"
|
|
61
|
+
},
|
|
62
|
+
"description": "本 endpoint 的 trusted 列表"
|
|
31
63
|
},
|
|
32
64
|
"botToken": {
|
|
33
65
|
"type": "string",
|
|
34
66
|
"description": "iLink bot token (prefer env WEIXIN_ILINK_TOKEN or sidecar credential file)"
|
|
67
|
+
},
|
|
68
|
+
"id": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "Weixin iLink bot name"
|
|
35
71
|
}
|
|
36
72
|
},
|
|
37
73
|
"required": [
|
|
38
|
-
"
|
|
74
|
+
"id",
|
|
39
75
|
"botToken"
|
|
40
76
|
]
|
|
41
77
|
}
|
package/src/credentials.ts
CHANGED
|
@@ -17,12 +17,12 @@ export function resolveStateDir(): string {
|
|
|
17
17
|
: DEFAULT_DATA_DIR;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function credentialsPath(
|
|
21
|
-
return path.join(resolveStateDir(), `${
|
|
20
|
+
export function credentialsPath(endpointId: string): string {
|
|
21
|
+
return path.join(resolveStateDir(), `${endpointId}.json`);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function loadCredentials(
|
|
25
|
-
const file = credentialsPath(
|
|
24
|
+
export function loadCredentials(endpointId: string): WeixinIlinkCredentials | null {
|
|
25
|
+
const file = credentialsPath(endpointId);
|
|
26
26
|
try {
|
|
27
27
|
if (!fs.existsSync(file)) return null;
|
|
28
28
|
const raw = fs.readFileSync(file, "utf-8");
|
|
@@ -34,8 +34,8 @@ export function loadCredentials(endpointName: string): WeixinIlinkCredentials |
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function saveCredentials(
|
|
38
|
-
const file = credentialsPath(
|
|
37
|
+
export function saveCredentials(endpointId: string, creds: WeixinIlinkCredentials): void {
|
|
38
|
+
const file = credentialsPath(endpointId);
|
|
39
39
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
40
40
|
fs.writeFileSync(
|
|
41
41
|
file,
|
|
@@ -44,12 +44,12 @@ export function saveCredentials(endpointName: string, creds: WeixinIlinkCredenti
|
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function syncBufPath(
|
|
48
|
-
return path.join(resolveStateDir(), `${
|
|
47
|
+
export function syncBufPath(endpointId: string): string {
|
|
48
|
+
return path.join(resolveStateDir(), `${endpointId}.sync-buf`);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export function loadSyncBuf(
|
|
52
|
-
const file = syncBufPath(
|
|
51
|
+
export function loadSyncBuf(endpointId: string): string {
|
|
52
|
+
const file = syncBufPath(endpointId);
|
|
53
53
|
try {
|
|
54
54
|
return fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "";
|
|
55
55
|
} catch {
|
|
@@ -57,8 +57,8 @@ export function loadSyncBuf(endpointName: string): string {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export function saveSyncBuf(
|
|
61
|
-
const file = syncBufPath(
|
|
60
|
+
export function saveSyncBuf(endpointId: string, buf: string): void {
|
|
61
|
+
const file = syncBufPath(endpointId);
|
|
62
62
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
63
63
|
fs.writeFileSync(file, buf, "utf-8");
|
|
64
64
|
}
|
package/src/endpoint.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
EndpointSendRequest,
|
|
12
12
|
} from '@zhin.js/adapter';
|
|
13
13
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
14
|
-
import { formatCompact,
|
|
14
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
15
15
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
16
16
|
import { getUpdates, notifyStart, notifyStop, sendTyping } from './ilink-api.js';
|
|
17
17
|
import { configureIlinkMeta } from './ilink-meta.js';
|
|
@@ -55,8 +55,6 @@ import {
|
|
|
55
55
|
type WeixinWireSegment,
|
|
56
56
|
} from './protocol.js';
|
|
57
57
|
|
|
58
|
-
const logger = getLogger('weixin-ilink');
|
|
59
|
-
|
|
60
58
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
61
59
|
const BACKOFF_DELAY_MS = 30_000;
|
|
62
60
|
const RETRY_DELAY_MS = 2_000;
|
|
@@ -86,6 +84,8 @@ export interface WeixinIlinkEndpointOptions {
|
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
87
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
88
|
+
|
|
89
89
|
readonly #options: WeixinIlinkEndpointOptions;
|
|
90
90
|
readonly #resolveCredentials: (
|
|
91
91
|
config: ResolvedWeixinIlinkConfig,
|
|
@@ -106,6 +106,7 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
106
106
|
readonly management: EndpointManagement = createWeixinIlinkEndpointManagement(this);
|
|
107
107
|
|
|
108
108
|
constructor(options: WeixinIlinkEndpointOptions) {
|
|
109
|
+
this.#logger = getAdapterLogger('weixin-ilink', options.config.id);
|
|
109
110
|
this.#options = options;
|
|
110
111
|
this.#resolveCredentials = options.resolveCredentials ?? resolveCredentials;
|
|
111
112
|
this.#notifyStart = options.notifyStart ?? notifyStart;
|
|
@@ -120,7 +121,7 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
120
121
|
|
|
121
122
|
/** Live endpoint 名(context token 按 account 名分桶,management 推导对端用)。 */
|
|
122
123
|
get configName(): string {
|
|
123
|
-
return this.#options.config.
|
|
124
|
+
return this.#options.config.id;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
apiBaseUrl(): string {
|
|
@@ -139,7 +140,7 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
139
140
|
// QR 登录最长 8 分钟:stop() 必须能打断
|
|
140
141
|
this.#loginAbort = new AbortController();
|
|
141
142
|
this.#creds = await this.#resolveCredentials(this.#options.config, this.#loginAbort.signal);
|
|
142
|
-
restoreContextTokens(this.#options.config.
|
|
143
|
+
restoreContextTokens(this.#options.config.id);
|
|
143
144
|
|
|
144
145
|
await this.#notifyStart({
|
|
145
146
|
baseUrl: this.apiBaseUrl(),
|
|
@@ -148,19 +149,19 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
148
149
|
|
|
149
150
|
this.#configManager = new WeixinConfigManager(
|
|
150
151
|
{ baseUrl: this.apiBaseUrl(), token: this.#creds.botToken },
|
|
151
|
-
(msg) => logger.debug(msg),
|
|
152
|
+
(msg) => this.#logger.debug(msg),
|
|
152
153
|
);
|
|
153
154
|
|
|
154
155
|
this.#pollAbort = new AbortController();
|
|
155
156
|
this.#pollPromise = this.#pollLoop(this.#pollAbort.signal);
|
|
156
157
|
this.#startMediaSweep();
|
|
157
|
-
logger.info(formatCompact({
|
|
158
|
+
this.#logger.info(formatCompact({
|
|
158
159
|
op: 'connect',
|
|
159
|
-
endpoint: this.#options.config.
|
|
160
|
+
endpoint: this.#options.config.id,
|
|
160
161
|
}));
|
|
161
162
|
} catch (error) {
|
|
162
163
|
await this.stop();
|
|
163
|
-
logger.error('Failed to connect weixin-ilink bot:', error);
|
|
164
|
+
this.#logger.error('Failed to connect weixin-ilink bot:', error);
|
|
164
165
|
throw error;
|
|
165
166
|
}
|
|
166
167
|
}
|
|
@@ -184,12 +185,12 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
184
185
|
}
|
|
185
186
|
this.#stopMediaSweep();
|
|
186
187
|
// 防抖中的 context token 落盘,避免 stop 丢尾部写入
|
|
187
|
-
flushContextTokenPersist(this.#options.config.
|
|
188
|
+
flushContextTokenPersist(this.#options.config.id);
|
|
188
189
|
if (this.#creds?.botToken) {
|
|
189
190
|
try {
|
|
190
191
|
await this.#notifyStop({ baseUrl: this.apiBaseUrl(), token: this.#creds.botToken });
|
|
191
192
|
} catch (err) {
|
|
192
|
-
logger.warn(formatCompact({
|
|
193
|
+
this.#logger.warn(formatCompact({
|
|
193
194
|
op: 'notify_stop',
|
|
194
195
|
ok: false,
|
|
195
196
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -197,9 +198,8 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
this.#started = false;
|
|
200
|
-
logger.debug(formatCompact({
|
|
201
|
-
|
|
202
|
-
endpoint: this.#options.config.name,
|
|
201
|
+
this.#logger.debug(formatCompact({
|
|
202
|
+
op: 'disconnect',
|
|
203
203
|
}));
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -209,9 +209,9 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
209
209
|
}
|
|
210
210
|
// 个人微信仅 private 会话,对端 user_id 即 conversation.id
|
|
211
211
|
const target = conversation.id;
|
|
212
|
-
const contextToken = getContextToken(this.#options.config.
|
|
212
|
+
const contextToken = getContextToken(this.#options.config.id, target);
|
|
213
213
|
if (!contextToken) {
|
|
214
|
-
logger.warn(formatCompact({
|
|
214
|
+
this.#logger.warn(formatCompact({
|
|
215
215
|
op: 'send',
|
|
216
216
|
ok: false,
|
|
217
217
|
reason: 'missing_context_token',
|
|
@@ -269,7 +269,7 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
if (seg.type === 'image' || seg.type === 'video' || seg.type === 'file' || seg.type === 'record') {
|
|
272
|
-
logger.warn(formatCompact({
|
|
272
|
+
this.#logger.warn(formatCompact({
|
|
273
273
|
op: 'send',
|
|
274
274
|
skip: 'no_local_file',
|
|
275
275
|
type: seg.type,
|
|
@@ -294,21 +294,21 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
294
294
|
if (!this.#open) return;
|
|
295
295
|
const userId = msg.from_user_id ?? '';
|
|
296
296
|
if (msg.context_token && userId) {
|
|
297
|
-
setContextToken(this.#options.config.
|
|
297
|
+
setContextToken(this.#options.config.id, userId, msg.context_token);
|
|
298
298
|
}
|
|
299
299
|
const conversation = weixinIlinkInboundConversation(String(this.#options.id), userId);
|
|
300
300
|
void this.#options.gateway.receive({
|
|
301
301
|
conversation,
|
|
302
302
|
message: { conversation, id: inboundMessageId(msg) },
|
|
303
303
|
content: formatInboundContent(msg),
|
|
304
|
-
sender: userId,
|
|
304
|
+
sender: { id: userId },
|
|
305
|
+
endpointId: this.#options.config.id,
|
|
305
306
|
metadata: Object.freeze({
|
|
306
|
-
endpoint: this.#options.config.name,
|
|
307
307
|
messageType: msg.message_type,
|
|
308
308
|
createTimeMs: msg.create_time_ms,
|
|
309
309
|
}),
|
|
310
310
|
}).catch((err) => {
|
|
311
|
-
logger.warn(formatCompact({
|
|
311
|
+
this.#logger.warn(formatCompact({
|
|
312
312
|
op: 'weixin_ilink_gateway_receive_failed',
|
|
313
313
|
target: userId,
|
|
314
314
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -318,10 +318,10 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
318
318
|
|
|
319
319
|
async sendTypingToUser(userId: string, status: number): Promise<void> {
|
|
320
320
|
if (!this.#creds?.botToken || !this.#configManager) return;
|
|
321
|
-
const contextToken = getContextToken(this.#options.config.
|
|
321
|
+
const contextToken = getContextToken(this.#options.config.id, userId);
|
|
322
322
|
const cfg = await this.#configManager.getForUser(userId, contextToken);
|
|
323
323
|
if (!cfg.typingTicket) {
|
|
324
|
-
logger.debug(formatCompact({
|
|
324
|
+
this.#logger.debug(formatCompact({
|
|
325
325
|
op: 'send_typing',
|
|
326
326
|
skip: 'no_typing_ticket',
|
|
327
327
|
userId,
|
|
@@ -342,14 +342,14 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
342
342
|
async #pollLoop(abortSignal: AbortSignal): Promise<void> {
|
|
343
343
|
if (!this.#creds?.botToken) return;
|
|
344
344
|
|
|
345
|
-
const
|
|
346
|
-
let getUpdatesBuf = loadSyncBuf(
|
|
345
|
+
const endpointId = this.#options.config.id;
|
|
346
|
+
let getUpdatesBuf = loadSyncBuf(endpointId);
|
|
347
347
|
let nextTimeoutMs = this.#options.config.longPollTimeoutMs;
|
|
348
348
|
let consecutiveFailures = 0;
|
|
349
349
|
|
|
350
350
|
while (!abortSignal.aborted) {
|
|
351
|
-
if (isSessionPaused(
|
|
352
|
-
await sleep(getRemainingPauseMs(
|
|
351
|
+
if (isSessionPaused(endpointId)) {
|
|
352
|
+
await sleep(getRemainingPauseMs(endpointId), abortSignal);
|
|
353
353
|
continue;
|
|
354
354
|
}
|
|
355
355
|
|
|
@@ -374,9 +374,9 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
374
374
|
const sessionExpired =
|
|
375
375
|
resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
|
|
376
376
|
if (sessionExpired) {
|
|
377
|
-
pauseSession(
|
|
377
|
+
pauseSession(endpointId);
|
|
378
378
|
consecutiveFailures = 0;
|
|
379
|
-
await sleep(getRemainingPauseMs(
|
|
379
|
+
await sleep(getRemainingPauseMs(endpointId), abortSignal);
|
|
380
380
|
continue;
|
|
381
381
|
}
|
|
382
382
|
consecutiveFailures += 1;
|
|
@@ -397,12 +397,12 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
397
397
|
}
|
|
398
398
|
if (nextBuf) {
|
|
399
399
|
getUpdatesBuf = nextBuf;
|
|
400
|
-
saveSyncBuf(
|
|
400
|
+
saveSyncBuf(endpointId, getUpdatesBuf);
|
|
401
401
|
}
|
|
402
402
|
} catch (err) {
|
|
403
403
|
if (abortSignal.aborted) return;
|
|
404
404
|
consecutiveFailures += 1;
|
|
405
|
-
logger.error(formatCompact({
|
|
405
|
+
this.#logger.error(formatCompact({
|
|
406
406
|
op: 'poll',
|
|
407
407
|
ok: false,
|
|
408
408
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -419,14 +419,14 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
419
419
|
async #handleInboundMessage(full: WeixinMessage): Promise<void> {
|
|
420
420
|
const fromUserId = full.from_user_id ?? '';
|
|
421
421
|
if (full.context_token && fromUserId) {
|
|
422
|
-
setContextToken(this.#options.config.
|
|
422
|
+
setContextToken(this.#options.config.id, fromUserId, full.context_token);
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
const mediaOpts = await this.#downloadInboundMedia(full);
|
|
426
426
|
this.admit({ ...full, _media: mediaOpts });
|
|
427
|
-
logger.debug(formatCompact({
|
|
427
|
+
this.#logger.debug(formatCompact({
|
|
428
428
|
op: 'recv',
|
|
429
|
-
endpoint: this.#options.config.
|
|
429
|
+
endpoint: this.#options.config.id,
|
|
430
430
|
target: fromUserId,
|
|
431
431
|
preview: formatInboundContent({ ...full, _media: mediaOpts }).slice(0, 80),
|
|
432
432
|
}));
|
|
@@ -456,8 +456,8 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
456
456
|
cdnBaseUrl: this.cdnBaseUrl(),
|
|
457
457
|
saveMedia: (buffer, contentType, subdir, maxBytes, originalFilename) =>
|
|
458
458
|
this.#saveInboundMedia(buffer, contentType, subdir, maxBytes, originalFilename),
|
|
459
|
-
log: (msg) => logger.debug(msg),
|
|
460
|
-
errLog: (msg) => logger.warn(msg),
|
|
459
|
+
log: (msg) => this.#logger.debug(msg),
|
|
460
|
+
errLog: (msg) => this.#logger.warn(msg),
|
|
461
461
|
label: `inbound:${full.from_user_id ?? 'unknown'}`,
|
|
462
462
|
});
|
|
463
463
|
}
|
|
@@ -521,9 +521,9 @@ export class WeixinIlinkEndpoint implements EndpointInstance {
|
|
|
521
521
|
const stat = fs.statSync(filePath);
|
|
522
522
|
if (now - stat.mtimeMs <= INBOUND_MEDIA_TTL_MS) continue;
|
|
523
523
|
fs.unlinkSync(filePath);
|
|
524
|
-
logger.debug(formatCompact({
|
|
524
|
+
this.#logger.debug(formatCompact({
|
|
525
525
|
op: 'media_sweep',
|
|
526
|
-
endpoint: this.#options.config.
|
|
526
|
+
endpoint: this.#options.config.id,
|
|
527
527
|
file: entry.name,
|
|
528
528
|
}));
|
|
529
529
|
} catch {
|
package/src/login.ts
CHANGED
|
@@ -106,7 +106,7 @@ export async function resolveCredentials(
|
|
|
106
106
|
): Promise<WeixinIlinkCredentials> {
|
|
107
107
|
const envToken = process.env.WEIXIN_ILINK_TOKEN?.trim() || config.botToken?.trim();
|
|
108
108
|
if (envToken) {
|
|
109
|
-
const stored = loadCredentials(config.
|
|
109
|
+
const stored = loadCredentials(config.id);
|
|
110
110
|
return {
|
|
111
111
|
botToken: envToken,
|
|
112
112
|
ilinkUserId: stored?.ilinkUserId,
|
|
@@ -115,7 +115,7 @@ export async function resolveCredentials(
|
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
const stored = loadCredentials(config.
|
|
118
|
+
const stored = loadCredentials(config.id);
|
|
119
119
|
if (stored?.botToken) {
|
|
120
120
|
return {
|
|
121
121
|
...stored,
|
|
@@ -135,7 +135,7 @@ export async function loginWithQr(
|
|
|
135
135
|
const qrcodeUrl = qr.qrcode_img_content || qr.qrcode;
|
|
136
136
|
|
|
137
137
|
logger.info(
|
|
138
|
-
`weixin-ilink QR login for ${config.
|
|
138
|
+
`weixin-ilink QR login for ${config.id}: scan with WeChat ClawBot entry — ${qrcodeUrl.slice(0, 120)}${qrcodeUrl.length > 120 ? '…' : ''}`,
|
|
139
139
|
);
|
|
140
140
|
|
|
141
141
|
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
@@ -157,7 +157,7 @@ export async function loginWithQr(
|
|
|
157
157
|
throw new Error('扫码成功但未返回 bot_token');
|
|
158
158
|
}
|
|
159
159
|
const creds: WeixinIlinkCredentials = {
|
|
160
|
-
botToken: status.bot_token ?? loadCredentials(config.
|
|
160
|
+
botToken: status.bot_token ?? loadCredentials(config.id)?.botToken ?? '',
|
|
161
161
|
ilinkUserId: status.ilink_user_id,
|
|
162
162
|
ilinkBotId: status.ilink_endpoint_id,
|
|
163
163
|
baseUrl: status.baseurl ?? currentBaseUrl,
|
|
@@ -165,7 +165,7 @@ export async function loginWithQr(
|
|
|
165
165
|
if (!creds.botToken) {
|
|
166
166
|
throw new Error('binded_redirect 但本地无有效 bot_token');
|
|
167
167
|
}
|
|
168
|
-
saveCredentials(config.
|
|
168
|
+
saveCredentials(config.id, creds);
|
|
169
169
|
return creds;
|
|
170
170
|
}
|
|
171
171
|
|
package/src/protocol.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { WeixinMessage } from './ilink-types.js';
|
|
|
12
12
|
|
|
13
13
|
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
14
14
|
export interface WeixinIlinkAdapterConfig {
|
|
15
|
-
readonly
|
|
15
|
+
readonly id?: string;
|
|
16
16
|
readonly botAgent?: string;
|
|
17
17
|
readonly baseUrl?: string;
|
|
18
18
|
readonly cdnBaseUrl?: string;
|
|
@@ -26,7 +26,7 @@ export interface WeixinIlinkAdapterConfig {
|
|
|
26
26
|
|
|
27
27
|
export interface ResolvedWeixinIlinkConfig {
|
|
28
28
|
readonly context: 'weixin-ilink';
|
|
29
|
-
readonly
|
|
29
|
+
readonly id: string;
|
|
30
30
|
readonly botAgent?: string;
|
|
31
31
|
readonly baseUrl: string;
|
|
32
32
|
readonly cdnBaseUrl: string;
|
|
@@ -58,8 +58,8 @@ export function resolveWeixinIlinkConfig(
|
|
|
58
58
|
config: WeixinIlinkAdapterConfig = {},
|
|
59
59
|
): ResolvedWeixinIlinkConfig {
|
|
60
60
|
const entry = config.endpoints?.find((item) => item.context === 'weixin-ilink');
|
|
61
|
-
const
|
|
62
|
-
|| (typeof entry?.
|
|
61
|
+
const id = (typeof config.id === 'string' && config.id)
|
|
62
|
+
|| (typeof entry?.id === 'string' && entry.id)
|
|
63
63
|
|| process.env.WEIXIN_ILINK_BOT_NAME
|
|
64
64
|
|| 'weixin-ilink-bot';
|
|
65
65
|
const botToken = pickCredential(
|
|
@@ -71,7 +71,7 @@ export function resolveWeixinIlinkConfig(
|
|
|
71
71
|
// (sidecar credentials file / QR login) during endpoint.start().
|
|
72
72
|
return {
|
|
73
73
|
context: 'weixin-ilink',
|
|
74
|
-
|
|
74
|
+
id,
|
|
75
75
|
botAgent: config.botAgent ?? entry?.botAgent,
|
|
76
76
|
baseUrl: config.baseUrl ?? entry?.baseUrl ?? DEFAULT_API_BASE_URL,
|
|
77
77
|
cdnBaseUrl: config.cdnBaseUrl ?? entry?.cdnBaseUrl ?? DEFAULT_CDN_BASE_URL,
|
|
@@ -112,9 +112,9 @@ export function inboundMessageId(msg: WeixinMessage): string {
|
|
|
112
112
|
* 入站归一化 → ConversationRef:个人微信无群/频道概念,全部会话都是
|
|
113
113
|
* 与 `from_user_id` 的 private 会话(无 parent)。
|
|
114
114
|
*/
|
|
115
|
-
export function weixinIlinkInboundConversation(
|
|
115
|
+
export function weixinIlinkInboundConversation(endpointKey: string, userId: string): ConversationRef {
|
|
116
116
|
return {
|
|
117
|
-
endpoint: { id:
|
|
117
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
118
118
|
kind: 'private',
|
|
119
119
|
id: userId,
|
|
120
120
|
};
|
package/src/silk-wasm.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* silk-wasm 是纯可选运行时依赖(不入 package.json):silk-transcode.ts 在
|
|
3
|
+
* try/catch 里动态 import,缺失时降级返回 null。这里只声明用到的最小表面。
|
|
4
|
+
*/
|
|
5
|
+
declare module 'silk-wasm' {
|
|
2
6
|
export function decode(
|
|
3
|
-
|
|
7
|
+
input: Uint8Array,
|
|
4
8
|
sampleRate: number,
|
|
5
9
|
): Promise<{ data: Uint8Array; duration: number }>;
|
|
6
10
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|