ylib-openclaw-weixin 2.1.7-beta.9 → 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/README.zh_CN.md +18 -0
- package/index.ts +107 -42
- package/package.json +8 -2
- package/schema.json +205 -0
- package/scripts/generate-schema.mjs +30 -0
- package/src/api/api.ts +1 -1
- package/src/cdn/upload.ts +49 -7
- package/src/config/config-schema.ts +8 -1
- package/src/media/media-download.ts +9 -1
- package/src/messaging/endpoint-dispatch.ts +1351 -131
- package/src/messaging/inbound.ts +18 -0
- package/src/messaging/markdown-filter.ts +39 -2
- package/src/messaging/process-message.ts +157 -22
- package/src/messaging/send-media.ts +29 -5
- package/src/messaging/send.ts +521 -24
package/README.md
CHANGED
|
@@ -310,3 +310,21 @@ Ensure `plugins.entries.openclaw-weixin.enabled` is `true` in `~/.openclaw/openc
|
|
|
310
310
|
openclaw config set plugins.entries.openclaw-weixin.enabled true
|
|
311
311
|
openclaw gateway restart
|
|
312
312
|
```
|
|
313
|
+
|
|
314
|
+
## Long Markdown Regression (One-Click)
|
|
315
|
+
|
|
316
|
+
Use this to run an end-to-end send with a real long markdown payload (links/images/code blocks included):
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
npm run regression:long-markdown -- \
|
|
320
|
+
--base-url https://weixin-gateway.example.com \
|
|
321
|
+
--to o9cq809SI88YrRxbFQXu39Lf3Xu8@im.wechat \
|
|
322
|
+
--context-token <context_token> \
|
|
323
|
+
--token <token> \
|
|
324
|
+
--repeat 120 \
|
|
325
|
+
--timeout-ms 45000
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Notes:
|
|
329
|
+
- This command builds first, then runs `scripts/regression-long-markdown.mjs`.
|
|
330
|
+
- It validates chunked sending behavior in real traffic.
|
package/README.zh_CN.md
CHANGED
|
@@ -309,3 +309,21 @@ openclaw plugins install @tencent-weixin/openclaw-weixin@legacy
|
|
|
309
309
|
openclaw config set plugins.entries.openclaw-weixin.enabled true
|
|
310
310
|
openclaw gateway restart
|
|
311
311
|
```
|
|
312
|
+
|
|
313
|
+
## 超长 Markdown 一键回归
|
|
314
|
+
|
|
315
|
+
用于在真实链路下发送“超长 markdown 样例”(含链接/图片/代码块):
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
npm run regression:long-markdown -- \
|
|
319
|
+
--base-url https://weixin-gateway.example.com \
|
|
320
|
+
--to o9cq809SI88YrRxbFQXu39Lf3Xu8@im.wechat \
|
|
321
|
+
--context-token <context_token> \
|
|
322
|
+
--token <token> \
|
|
323
|
+
--repeat 120 \
|
|
324
|
+
--timeout-ms 45000
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
说明:
|
|
328
|
+
- 命令会先构建,再执行 `scripts/regression-long-markdown.mjs`。
|
|
329
|
+
- 可用于验证分片发送在真实流量中的行为。
|
package/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { buildChannelConfigSchema } from "ylib-openclaw/plugin-sdk";
|
|
|
4
4
|
import { weixinPlugin } from "./src/channel.js";
|
|
5
5
|
export { weixinPlugin } from "./src/channel.js";
|
|
6
6
|
import { resolveWeixinAccount } from "./src/auth/accounts.js";
|
|
7
|
+
import { getConfig, sendTyping } from "./src/api/api.js";
|
|
8
|
+
import { TypingStatus } from "./src/api/types.js";
|
|
7
9
|
import { assertHostCompatibility } from "./src/compat.js";
|
|
8
10
|
import { WeixinConfigSchema } from "./src/config/config-schema.js";
|
|
9
11
|
import {
|
|
@@ -45,7 +47,7 @@ export default {
|
|
|
45
47
|
};
|
|
46
48
|
const normalizeMockScope = (
|
|
47
49
|
params: any,
|
|
48
|
-
): { scopeType: "my_workspace" | "project"; projectId: string } => {
|
|
50
|
+
): { scopeType: "workspace" | "my_workspace" | "project"; workspaceId: string; projectId: string } => {
|
|
49
51
|
const rawScopeType = String(
|
|
50
52
|
params?.scope_kind ||
|
|
51
53
|
params?.scopeKind ||
|
|
@@ -55,16 +57,10 @@ export default {
|
|
|
55
57
|
)
|
|
56
58
|
.trim()
|
|
57
59
|
.toLowerCase();
|
|
58
|
-
const
|
|
59
|
-
params?.project_id || params?.projectId || "",
|
|
60
|
+
const workspaceId = String(
|
|
61
|
+
params?.workspace_id || params?.workspaceId || params?.project_id || params?.projectId || "",
|
|
60
62
|
).trim();
|
|
61
|
-
|
|
62
|
-
(rawScopeType === "project" || (!rawScopeType && rawProjectId)) &&
|
|
63
|
-
rawProjectId
|
|
64
|
-
) {
|
|
65
|
-
return { scopeType: "project", projectId: rawProjectId };
|
|
66
|
-
}
|
|
67
|
-
return { scopeType: "my_workspace", projectId: "" };
|
|
63
|
+
return { scopeType: "workspace", workspaceId, projectId: workspaceId };
|
|
68
64
|
};
|
|
69
65
|
const resolveMockTargetId = (params: any): string => {
|
|
70
66
|
return String(
|
|
@@ -75,6 +71,7 @@ export default {
|
|
|
75
71
|
"",
|
|
76
72
|
).trim();
|
|
77
73
|
};
|
|
74
|
+
const TRIGGER_TYPING_KEEPALIVE_INTERVAL_MS = 5000;
|
|
78
75
|
|
|
79
76
|
api.registerGatewayMethod(
|
|
80
77
|
"weixin.loginWithQrStart",
|
|
@@ -359,39 +356,107 @@ export default {
|
|
|
359
356
|
|
|
360
357
|
const senderId =
|
|
361
358
|
String(params?.senderId || targetId).trim() || targetId;
|
|
362
|
-
void
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
359
|
+
void (async () => {
|
|
360
|
+
let triggerTypingTicket = "";
|
|
361
|
+
let typingKeepaliveTimer: ReturnType<typeof setInterval> | null =
|
|
362
|
+
null;
|
|
363
|
+
let typingKeepaliveInFlight = false;
|
|
364
|
+
const sendTriggerTyping = async (status: number): Promise<void> => {
|
|
365
|
+
if (!triggerTypingTicket) return;
|
|
366
|
+
await sendTyping({
|
|
367
|
+
baseUrl: account.baseUrl,
|
|
368
|
+
token: account.token,
|
|
369
|
+
body: {
|
|
370
|
+
ilink_user_id: targetId,
|
|
371
|
+
typing_ticket: triggerTypingTicket,
|
|
372
|
+
status,
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
try {
|
|
377
|
+
try {
|
|
378
|
+
const cfgResp = await getConfig({
|
|
379
|
+
baseUrl: account.baseUrl,
|
|
380
|
+
token: account.token,
|
|
381
|
+
ilinkUserId: targetId,
|
|
382
|
+
});
|
|
383
|
+
triggerTypingTicket = String(cfgResp?.typing_ticket || "").trim();
|
|
384
|
+
if (triggerTypingTicket) {
|
|
385
|
+
await sendTriggerTyping(TypingStatus.TYPING);
|
|
386
|
+
typingKeepaliveTimer = setInterval(() => {
|
|
387
|
+
if (typingKeepaliveInFlight) return;
|
|
388
|
+
typingKeepaliveInFlight = true;
|
|
389
|
+
void sendTriggerTyping(TypingStatus.TYPING)
|
|
390
|
+
.catch((err) => {
|
|
391
|
+
const errText =
|
|
392
|
+
err instanceof Error ? err.message : String(err);
|
|
393
|
+
api.logger?.warn?.(
|
|
394
|
+
`[Weixin][MockTrigger] typing keepalive failed: ${errText}`,
|
|
395
|
+
);
|
|
396
|
+
})
|
|
397
|
+
.finally(() => {
|
|
398
|
+
typingKeepaliveInFlight = false;
|
|
399
|
+
});
|
|
400
|
+
}, TRIGGER_TYPING_KEEPALIVE_INTERVAL_MS);
|
|
401
|
+
}
|
|
402
|
+
} catch (err) {
|
|
403
|
+
const errText = err instanceof Error ? err.message : String(err);
|
|
404
|
+
api.logger?.warn?.(
|
|
405
|
+
`[Weixin][MockTrigger] typing start skipped: ${errText}`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
await dispatchWeixinViaEndpoint({
|
|
410
|
+
cfg,
|
|
411
|
+
accountId: account.accountId,
|
|
412
|
+
ctx: {
|
|
413
|
+
Body: content,
|
|
414
|
+
From: senderId,
|
|
415
|
+
To: targetId,
|
|
416
|
+
AccountId: account.accountId,
|
|
417
|
+
OriginatingChannel: "openclaw-weixin",
|
|
418
|
+
OriginatingTo: targetId,
|
|
419
|
+
MessageSid: `mock_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
420
|
+
Timestamp: Date.now(),
|
|
421
|
+
Provider: "openclaw-weixin",
|
|
422
|
+
ChatType: "direct",
|
|
423
|
+
},
|
|
424
|
+
endpointConfig,
|
|
425
|
+
routeAgentId: resolvedAgentId,
|
|
426
|
+
baseUrl: account.baseUrl,
|
|
427
|
+
token: account.token,
|
|
428
|
+
cdnBaseUrl: account.cdnBaseUrl,
|
|
429
|
+
});
|
|
430
|
+
} catch (err: unknown) {
|
|
431
|
+
const errText = err instanceof Error ? err.message : String(err);
|
|
432
|
+
if (api.logger?.error) {
|
|
433
|
+
api.logger.error(
|
|
434
|
+
`[Weixin][MockTrigger] async dispatch failed: ${errText}`,
|
|
435
|
+
);
|
|
436
|
+
} else {
|
|
437
|
+
// eslint-disable-next-line no-console -- fallback when runtime logger is unavailable
|
|
438
|
+
console.error(
|
|
439
|
+
`[Weixin][MockTrigger] async dispatch failed: ${errText}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
} finally {
|
|
443
|
+
if (typingKeepaliveTimer) {
|
|
444
|
+
clearInterval(typingKeepaliveTimer);
|
|
445
|
+
typingKeepaliveTimer = null;
|
|
446
|
+
}
|
|
447
|
+
if (triggerTypingTicket) {
|
|
448
|
+
try {
|
|
449
|
+
await sendTriggerTyping(TypingStatus.CANCEL);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
const errText =
|
|
452
|
+
err instanceof Error ? err.message : String(err);
|
|
453
|
+
api.logger?.warn?.(
|
|
454
|
+
`[Weixin][MockTrigger] typing cancel failed: ${errText}`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
393
458
|
}
|
|
394
|
-
});
|
|
459
|
+
})();
|
|
395
460
|
|
|
396
461
|
return respond(true, {
|
|
397
462
|
ok: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ylib-openclaw-weixin",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.8",
|
|
4
4
|
"description": "OpenClaw Weixin channel",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tencent",
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"!src/**/*.test.ts",
|
|
11
11
|
"!src/**/node_modules/",
|
|
12
12
|
"index.ts",
|
|
13
|
+
"schema.json",
|
|
14
|
+
"scripts/generate-schema.mjs",
|
|
13
15
|
"openclaw.plugin.json",
|
|
14
16
|
"README.md",
|
|
15
17
|
"README.zh_CN.md",
|
|
@@ -19,12 +21,16 @@
|
|
|
19
21
|
"scripts": {
|
|
20
22
|
"test": "vitest run --coverage",
|
|
21
23
|
"typecheck": "tsc --noEmit",
|
|
22
|
-
"build": "tsc"
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"regression:long-markdown": "npm run build && node ./scripts/regression-long-markdown.mjs",
|
|
26
|
+
"schema:generate": "node ./scripts/generate-schema.mjs",
|
|
27
|
+
"prepublishOnly": "npm run schema:generate"
|
|
23
28
|
},
|
|
24
29
|
"engines": {
|
|
25
30
|
"node": ">=22"
|
|
26
31
|
},
|
|
27
32
|
"dependencies": {
|
|
33
|
+
"marked": "^15.0.12",
|
|
28
34
|
"qrcode-terminal": "0.12.0",
|
|
29
35
|
"zod": "4.3.6",
|
|
30
36
|
"ylib-openclaw": "2026.3.9-beta.5"
|
package/schema.json
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginId": "openclaw-weixin",
|
|
3
|
+
"channel": "weixin",
|
|
4
|
+
"rawSchema": {
|
|
5
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"name": {
|
|
9
|
+
"type": "string"
|
|
10
|
+
},
|
|
11
|
+
"enabled": {
|
|
12
|
+
"type": "boolean"
|
|
13
|
+
},
|
|
14
|
+
"baseUrl": {
|
|
15
|
+
"default": "https://ilinkai.weixin.qq.com",
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"cdnBaseUrl": {
|
|
19
|
+
"default": "https://novac2c.cdn.weixin.qq.com/c2c",
|
|
20
|
+
"type": "string"
|
|
21
|
+
},
|
|
22
|
+
"routeTag": {
|
|
23
|
+
"type": "number"
|
|
24
|
+
},
|
|
25
|
+
"botToken": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
},
|
|
28
|
+
"botId": {
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"modelName": {
|
|
32
|
+
"default": "main",
|
|
33
|
+
"type": "string"
|
|
34
|
+
},
|
|
35
|
+
"agentId": {
|
|
36
|
+
"default": "main",
|
|
37
|
+
"type": "string"
|
|
38
|
+
},
|
|
39
|
+
"gatewayBaseUrl": {
|
|
40
|
+
"type": "string"
|
|
41
|
+
},
|
|
42
|
+
"gatewayToken": {
|
|
43
|
+
"type": "string"
|
|
44
|
+
},
|
|
45
|
+
"uploadHost": {
|
|
46
|
+
"type": "string"
|
|
47
|
+
},
|
|
48
|
+
"showFinalAnswerOnly": {
|
|
49
|
+
"default": false,
|
|
50
|
+
"type": "boolean"
|
|
51
|
+
},
|
|
52
|
+
"scopeType": {
|
|
53
|
+
"default": "workspace",
|
|
54
|
+
"readOnly": true,
|
|
55
|
+
"type": "string"
|
|
56
|
+
},
|
|
57
|
+
"workspaceId": {
|
|
58
|
+
"default": "",
|
|
59
|
+
"readOnly": true,
|
|
60
|
+
"type": "string"
|
|
61
|
+
},
|
|
62
|
+
"projectId": {
|
|
63
|
+
"default": "",
|
|
64
|
+
"readOnly": true,
|
|
65
|
+
"type": "string"
|
|
66
|
+
},
|
|
67
|
+
"scope_kind": {
|
|
68
|
+
"readOnly": true,
|
|
69
|
+
"type": "string"
|
|
70
|
+
},
|
|
71
|
+
"scopeKind": {
|
|
72
|
+
"readOnly": true,
|
|
73
|
+
"type": "string"
|
|
74
|
+
},
|
|
75
|
+
"scope_type": {
|
|
76
|
+
"readOnly": true,
|
|
77
|
+
"type": "string"
|
|
78
|
+
},
|
|
79
|
+
"workspace_id": {
|
|
80
|
+
"readOnly": true,
|
|
81
|
+
"type": "string"
|
|
82
|
+
},
|
|
83
|
+
"project_id": {
|
|
84
|
+
"readOnly": true,
|
|
85
|
+
"type": "string"
|
|
86
|
+
},
|
|
87
|
+
"accounts": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"propertyNames": {
|
|
90
|
+
"type": "string"
|
|
91
|
+
},
|
|
92
|
+
"additionalProperties": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"name": {
|
|
96
|
+
"type": "string"
|
|
97
|
+
},
|
|
98
|
+
"enabled": {
|
|
99
|
+
"type": "boolean"
|
|
100
|
+
},
|
|
101
|
+
"baseUrl": {
|
|
102
|
+
"default": "https://ilinkai.weixin.qq.com",
|
|
103
|
+
"type": "string"
|
|
104
|
+
},
|
|
105
|
+
"cdnBaseUrl": {
|
|
106
|
+
"default": "https://novac2c.cdn.weixin.qq.com/c2c",
|
|
107
|
+
"type": "string"
|
|
108
|
+
},
|
|
109
|
+
"routeTag": {
|
|
110
|
+
"type": "number"
|
|
111
|
+
},
|
|
112
|
+
"botToken": {
|
|
113
|
+
"type": "string"
|
|
114
|
+
},
|
|
115
|
+
"botId": {
|
|
116
|
+
"type": "string"
|
|
117
|
+
},
|
|
118
|
+
"modelName": {
|
|
119
|
+
"default": "main",
|
|
120
|
+
"type": "string"
|
|
121
|
+
},
|
|
122
|
+
"agentId": {
|
|
123
|
+
"default": "main",
|
|
124
|
+
"type": "string"
|
|
125
|
+
},
|
|
126
|
+
"gatewayBaseUrl": {
|
|
127
|
+
"type": "string"
|
|
128
|
+
},
|
|
129
|
+
"gatewayToken": {
|
|
130
|
+
"type": "string"
|
|
131
|
+
},
|
|
132
|
+
"uploadHost": {
|
|
133
|
+
"type": "string"
|
|
134
|
+
},
|
|
135
|
+
"showFinalAnswerOnly": {
|
|
136
|
+
"default": false,
|
|
137
|
+
"type": "boolean"
|
|
138
|
+
},
|
|
139
|
+
"scopeType": {
|
|
140
|
+
"default": "workspace",
|
|
141
|
+
"readOnly": true,
|
|
142
|
+
"type": "string"
|
|
143
|
+
},
|
|
144
|
+
"workspaceId": {
|
|
145
|
+
"default": "",
|
|
146
|
+
"readOnly": true,
|
|
147
|
+
"type": "string"
|
|
148
|
+
},
|
|
149
|
+
"projectId": {
|
|
150
|
+
"default": "",
|
|
151
|
+
"readOnly": true,
|
|
152
|
+
"type": "string"
|
|
153
|
+
},
|
|
154
|
+
"scope_kind": {
|
|
155
|
+
"readOnly": true,
|
|
156
|
+
"type": "string"
|
|
157
|
+
},
|
|
158
|
+
"scopeKind": {
|
|
159
|
+
"readOnly": true,
|
|
160
|
+
"type": "string"
|
|
161
|
+
},
|
|
162
|
+
"scope_type": {
|
|
163
|
+
"readOnly": true,
|
|
164
|
+
"type": "string"
|
|
165
|
+
},
|
|
166
|
+
"workspace_id": {
|
|
167
|
+
"readOnly": true,
|
|
168
|
+
"type": "string"
|
|
169
|
+
},
|
|
170
|
+
"project_id": {
|
|
171
|
+
"readOnly": true,
|
|
172
|
+
"type": "string"
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"required": [
|
|
176
|
+
"baseUrl",
|
|
177
|
+
"cdnBaseUrl",
|
|
178
|
+
"modelName",
|
|
179
|
+
"agentId",
|
|
180
|
+
"showFinalAnswerOnly",
|
|
181
|
+
"scopeType",
|
|
182
|
+
"workspaceId",
|
|
183
|
+
"projectId"
|
|
184
|
+
],
|
|
185
|
+
"additionalProperties": false
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"channelConfigUpdatedAt": {
|
|
189
|
+
"type": "string"
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"required": [
|
|
193
|
+
"baseUrl",
|
|
194
|
+
"cdnBaseUrl",
|
|
195
|
+
"modelName",
|
|
196
|
+
"agentId",
|
|
197
|
+
"showFinalAnswerOnly",
|
|
198
|
+
"scopeType",
|
|
199
|
+
"workspaceId",
|
|
200
|
+
"projectId"
|
|
201
|
+
],
|
|
202
|
+
"additionalProperties": false
|
|
203
|
+
},
|
|
204
|
+
"schemaVersion": 1
|
|
205
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const entryPath = path.join(root, "dist", "index.js");
|
|
8
|
+
const outputPath = path.join(root, "schema.json");
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(entryPath)) {
|
|
11
|
+
throw new Error("dist/index.js not found, run build first");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const mod = await import(pathToFileURL(entryPath).href);
|
|
15
|
+
const plugin = mod.default;
|
|
16
|
+
const rawSchema = plugin?.configSchema?.schema;
|
|
17
|
+
if (!rawSchema || typeof rawSchema !== "object" || Array.isArray(rawSchema)) {
|
|
18
|
+
throw new Error("failed to resolve weixin plugin raw schema from dist/index.js");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const payload = {
|
|
22
|
+
pluginId: "openclaw-weixin",
|
|
23
|
+
channel: "weixin",
|
|
24
|
+
rawSchema,
|
|
25
|
+
schemaVersion: 1,
|
|
26
|
+
// generatedAt: new Date().toISOString(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
30
|
+
console.log(`[schema] generated ${outputPath}`);
|
package/src/api/api.ts
CHANGED
|
@@ -134,7 +134,6 @@ function buildHeaders(opts: {
|
|
|
134
134
|
const headers: Record<string, string> = {
|
|
135
135
|
"Content-Type": "application/json",
|
|
136
136
|
AuthorizationType: "ilink_bot_token",
|
|
137
|
-
"Content-Length": String(Buffer.byteLength(opts.body, "utf-8")),
|
|
138
137
|
"X-WECHAT-UIN": randomWechatUin(),
|
|
139
138
|
...buildCommonHeaders(),
|
|
140
139
|
};
|
|
@@ -274,6 +273,7 @@ async function apiPostFetch(params: {
|
|
|
274
273
|
}
|
|
275
274
|
return rawText;
|
|
276
275
|
} catch (err) {
|
|
276
|
+
console.error(`Error in ${params.label}:`, err);
|
|
277
277
|
clearTimeout(t);
|
|
278
278
|
if (params.abortSignal) {
|
|
279
279
|
params.abortSignal.removeEventListener("abort", onExternalAbort);
|
package/src/cdn/upload.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { getExtensionFromContentTypeOrUrl } from "../media/mime.js";
|
|
|
11
11
|
import { tempFileName } from "../util/random.js";
|
|
12
12
|
import { UploadMediaType } from "../api/types.js";
|
|
13
13
|
|
|
14
|
+
const DOWNLOAD_TEMP_WRITE_MAX_RETRIES = 8;
|
|
15
|
+
|
|
14
16
|
export type UploadedFileInfo = {
|
|
15
17
|
filekey: string;
|
|
16
18
|
/** 由 upload_param 上传后 CDN 返回的下载加密参数; fill into ImageItem.media.encrypt_query_param */
|
|
@@ -23,15 +25,21 @@ export type UploadedFileInfo = {
|
|
|
23
25
|
fileSizeCiphertext: number;
|
|
24
26
|
};
|
|
25
27
|
|
|
28
|
+
export type DownloadedRemoteTempFile = {
|
|
29
|
+
filePath: string;
|
|
30
|
+
contentDisposition?: string;
|
|
31
|
+
contentType?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
26
34
|
/**
|
|
27
35
|
* Download a remote media URL (image, video, file) to a local temp file in destDir.
|
|
28
36
|
* Returns the local file path; extension is inferred from Content-Type / URL.
|
|
29
37
|
*/
|
|
30
|
-
export async function
|
|
38
|
+
export async function downloadRemoteImageToTempWithMeta(
|
|
31
39
|
url: string,
|
|
32
40
|
destDir: string,
|
|
33
41
|
opts?: { gatewayToken?: string },
|
|
34
|
-
): Promise<
|
|
42
|
+
): Promise<DownloadedRemoteTempFile> {
|
|
35
43
|
logger.debug(`downloadRemoteImageToTemp: fetching url=${url}`);
|
|
36
44
|
const gatewayToken = opts?.gatewayToken?.trim();
|
|
37
45
|
const fetchInit: RequestInit | undefined = gatewayToken
|
|
@@ -47,11 +55,45 @@ export async function downloadRemoteImageToTemp(
|
|
|
47
55
|
logger.debug(`downloadRemoteImageToTemp: downloaded ${buf.length} bytes`);
|
|
48
56
|
await fs.mkdir(destDir, { recursive: true });
|
|
49
57
|
const ext = getExtensionFromContentTypeOrUrl(res.headers.get("content-type"), url);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
let lastErr: unknown;
|
|
59
|
+
for (let i = 0; i < DOWNLOAD_TEMP_WRITE_MAX_RETRIES; i += 1) {
|
|
60
|
+
const name = tempFileName("weixin-remote", ext);
|
|
61
|
+
const filePath = path.join(destDir, name);
|
|
62
|
+
try {
|
|
63
|
+
const fd = await fs.open(filePath, "wx");
|
|
64
|
+
try {
|
|
65
|
+
await fd.writeFile(buf);
|
|
66
|
+
} finally {
|
|
67
|
+
await fd.close();
|
|
68
|
+
}
|
|
69
|
+
logger.debug(`downloadRemoteImageToTemp: saved to ${filePath} ext=${ext}`);
|
|
70
|
+
return {
|
|
71
|
+
filePath,
|
|
72
|
+
contentDisposition: res.headers.get("content-disposition") ?? undefined,
|
|
73
|
+
contentType: res.headers.get("content-type") ?? undefined,
|
|
74
|
+
};
|
|
75
|
+
} catch (err) {
|
|
76
|
+
lastErr = err;
|
|
77
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
78
|
+
if (code !== "EEXIST") {
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
logger.warn(
|
|
82
|
+
`downloadRemoteImageToTemp: temp file name conflict, retry=${i + 1}/${DOWNLOAD_TEMP_WRITE_MAX_RETRIES} url=${url}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw new Error(
|
|
87
|
+
`downloadRemoteImageToTemp: failed to persist temp file after ${DOWNLOAD_TEMP_WRITE_MAX_RETRIES} retries url=${url} err=${String(lastErr)}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function downloadRemoteImageToTemp(
|
|
92
|
+
url: string,
|
|
93
|
+
destDir: string,
|
|
94
|
+
opts?: { gatewayToken?: string },
|
|
95
|
+
): Promise<string> {
|
|
96
|
+
return (await downloadRemoteImageToTempWithMeta(url, destDir, opts)).filePath;
|
|
55
97
|
}
|
|
56
98
|
|
|
57
99
|
/**
|
|
@@ -22,16 +22,23 @@ const weixinAccountSchema = z.object({
|
|
|
22
22
|
gatewayBaseUrl: z.string().optional(),
|
|
23
23
|
gatewayToken: z.string().optional(),
|
|
24
24
|
uploadHost: z.string().optional(),
|
|
25
|
+
/**
|
|
26
|
+
* 默认关闭以保持历史输出;开启后普通 content、sub-agent 过程文本和资源过程产物不展示,
|
|
27
|
+
* 但 error 与主流程确认兜底仍会展示,避免失败信息丢失或确认流程卡住。
|
|
28
|
+
*/
|
|
29
|
+
showFinalAnswerOnly: z.boolean().optional().default(false),
|
|
25
30
|
/** Runtime readonly scope fields injected by backend. */
|
|
26
31
|
scopeType: z
|
|
27
32
|
.string()
|
|
28
33
|
.optional()
|
|
29
|
-
.default("
|
|
34
|
+
.default("workspace")
|
|
30
35
|
.meta({ readOnly: true }),
|
|
36
|
+
workspaceId: z.string().optional().default("").meta({ readOnly: true }),
|
|
31
37
|
projectId: z.string().optional().default("").meta({ readOnly: true }),
|
|
32
38
|
scope_kind: z.string().optional().meta({ readOnly: true }),
|
|
33
39
|
scopeKind: z.string().optional().meta({ readOnly: true }),
|
|
34
40
|
scope_type: z.string().optional().meta({ readOnly: true }),
|
|
41
|
+
workspace_id: z.string().optional().meta({ readOnly: true }),
|
|
35
42
|
project_id: z.string().optional().meta({ readOnly: true }),
|
|
36
43
|
});
|
|
37
44
|
|
|
@@ -39,7 +39,14 @@ export async function downloadMediaFromItem(
|
|
|
39
39
|
|
|
40
40
|
if (item.type === MessageItemType.IMAGE) {
|
|
41
41
|
const img = item.image_item;
|
|
42
|
-
if (!img?.media?.encrypt_query_param && !img?.media?.full_url)
|
|
42
|
+
if (!img?.media?.encrypt_query_param && !img?.media?.full_url) {
|
|
43
|
+
logger.warn(
|
|
44
|
+
`${label} image skipped: missing media.encrypt_query_param/full_url hasImageItem=${Boolean(
|
|
45
|
+
img,
|
|
46
|
+
)} hasMedia=${Boolean(img?.media)} hasUrlOnly=${Boolean(img?.url)}`,
|
|
47
|
+
);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
43
50
|
const aesKeyBase64 = img.aeskey
|
|
44
51
|
? Buffer.from(img.aeskey, "hex").toString("base64")
|
|
45
52
|
: img.media.aes_key;
|
|
@@ -119,6 +126,7 @@ export async function downloadMediaFromItem(
|
|
|
119
126
|
);
|
|
120
127
|
result.decryptedFilePath = saved.path;
|
|
121
128
|
result.fileMediaType = mime;
|
|
129
|
+
result.fileName = fileItem.file_name ?? undefined;
|
|
122
130
|
logger.debug(`${label} file: saved to ${saved.path} mime=${mime}`);
|
|
123
131
|
} catch (err) {
|
|
124
132
|
logger.error(`${label} file download failed: ${String(err)}`);
|