ylib-openclaw-weixin 2.1.7 → 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 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
@@ -2,7 +2,10 @@ import type { OpenClawPluginApi } from "ylib-openclaw/plugin-sdk";
2
2
  import { buildChannelConfigSchema } from "ylib-openclaw/plugin-sdk";
3
3
 
4
4
  import { weixinPlugin } from "./src/channel.js";
5
+ export { weixinPlugin } from "./src/channel.js";
5
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";
6
9
  import { assertHostCompatibility } from "./src/compat.js";
7
10
  import { WeixinConfigSchema } from "./src/config/config-schema.js";
8
11
  import {
@@ -10,7 +13,10 @@ import {
10
13
  resolveWeixinAgentIdByBindings,
11
14
  resolveWeixinEndpointConfig,
12
15
  } from "./src/messaging/endpoint-dispatch.js";
13
- import { sendMessageWeixin, StreamingMarkdownFilter } from "./src/messaging/send.js";
16
+ import {
17
+ sendMessageWeixin,
18
+ StreamingMarkdownFilter,
19
+ } from "./src/messaging/send.js";
14
20
  import { setWeixinRuntime } from "./src/runtime.js";
15
21
 
16
22
  export default {
@@ -41,18 +47,20 @@ export default {
41
47
  };
42
48
  const normalizeMockScope = (
43
49
  params: any,
44
- ): { scopeType: "my_workspace" | "project"; projectId: string } => {
45
- const rawScopeType = String(params?.scope_type || params?.type || "")
50
+ ): { scopeType: "workspace" | "my_workspace" | "project"; workspaceId: string; projectId: string } => {
51
+ const rawScopeType = String(
52
+ params?.scope_kind ||
53
+ params?.scopeKind ||
54
+ params?.scope_type ||
55
+ params?.scopeType ||
56
+ "",
57
+ )
46
58
  .trim()
47
59
  .toLowerCase();
48
- const rawProjectId = String(params?.project_id || "").trim();
49
- if (
50
- (rawScopeType === "project" || (!rawScopeType && rawProjectId)) &&
51
- rawProjectId
52
- ) {
53
- return { scopeType: "project", projectId: rawProjectId };
54
- }
55
- return { scopeType: "my_workspace", projectId: "" };
60
+ const workspaceId = String(
61
+ params?.workspace_id || params?.workspaceId || params?.project_id || params?.projectId || "",
62
+ ).trim();
63
+ return { scopeType: "workspace", workspaceId, projectId: workspaceId };
56
64
  };
57
65
  const resolveMockTargetId = (params: any): string => {
58
66
  return String(
@@ -63,6 +71,7 @@ export default {
63
71
  "",
64
72
  ).trim();
65
73
  };
74
+ const TRIGGER_TYPING_KEEPALIVE_INTERVAL_MS = 5000;
66
75
 
67
76
  api.registerGatewayMethod(
68
77
  "weixin.loginWithQrStart",
@@ -79,7 +88,9 @@ export default {
79
88
  force: params?.force,
80
89
  timeoutMs: params?.timeoutMs,
81
90
  verbose: params?.verbose,
82
- });
91
+ sessionKey: params?.sessionKey,
92
+ resumeOnly: params?.resumeOnly,
93
+ } as any);
83
94
  return respond(true, result);
84
95
  } catch (err) {
85
96
  return respond(false, {
@@ -142,14 +153,38 @@ export default {
142
153
  async ({ respond, cfg, params, accountId }: any) => {
143
154
  const probeAccount = (weixinPlugin.status as any)?.probeAccount;
144
155
  const resolveAccount = (weixinPlugin.config as any)?.resolveAccount;
145
- if (typeof probeAccount !== "function" || typeof resolveAccount !== "function") {
156
+ if (
157
+ typeof probeAccount !== "function" ||
158
+ typeof resolveAccount !== "function"
159
+ ) {
146
160
  return respond(false, {
147
161
  error: "weixin status probeAccount not available",
148
162
  });
149
163
  }
150
164
  try {
151
- const resolved = resolveAccount(cfg, params?.accountId ?? accountId);
152
- const result = await probeAccount({ account: resolved });
165
+ const forceProbeRaw = String(
166
+ params?.force_probe != null
167
+ ? params?.force_probe
168
+ : params?.forceProbe != null
169
+ ? params?.forceProbe
170
+ : "",
171
+ )
172
+ .trim()
173
+ .toLowerCase();
174
+ const forceProbe =
175
+ forceProbeRaw === "1" ||
176
+ forceProbeRaw === "true" ||
177
+ forceProbeRaw === "yes" ||
178
+ forceProbeRaw === "on";
179
+ const resolved = resolveAccount(
180
+ cfg,
181
+ params?.accountId ?? accountId,
182
+ );
183
+ const result = await probeAccount({
184
+ account: resolved,
185
+ forceProbe,
186
+ force_probe: forceProbe,
187
+ });
153
188
  return respond(Boolean(result?.ok), result);
154
189
  } catch (err) {
155
190
  return respond(false, {
@@ -298,6 +333,7 @@ export default {
298
333
  opts: {
299
334
  baseUrl: account.baseUrl,
300
335
  token: account.token,
336
+ accountId: account.accountId,
301
337
  },
302
338
  });
303
339
  } catch (err) {
@@ -320,39 +356,107 @@ export default {
320
356
 
321
357
  const senderId =
322
358
  String(params?.senderId || targetId).trim() || targetId;
323
- void dispatchWeixinViaEndpoint({
324
- cfg,
325
- accountId: account.accountId,
326
- ctx: {
327
- Body: content,
328
- From: senderId,
329
- To: targetId,
330
- AccountId: account.accountId,
331
- OriginatingChannel: "openclaw-weixin",
332
- OriginatingTo: targetId,
333
- MessageSid: `mock_${Date.now()}_${Math.random().toString(36).slice(2)}`,
334
- Timestamp: Date.now(),
335
- Provider: "openclaw-weixin",
336
- ChatType: "direct",
337
- },
338
- endpointConfig,
339
- routeAgentId: resolvedAgentId,
340
- baseUrl: account.baseUrl,
341
- token: account.token,
342
- cdnBaseUrl: account.cdnBaseUrl,
343
- }).catch((err: unknown) => {
344
- const errText = err instanceof Error ? err.message : String(err);
345
- if (api.logger?.error) {
346
- api.logger.error(
347
- `[Weixin][MockTrigger] async dispatch failed: ${errText}`,
348
- );
349
- } else {
350
- // eslint-disable-next-line no-console -- fallback when runtime logger is unavailable
351
- console.error(
352
- `[Weixin][MockTrigger] async dispatch failed: ${errText}`,
353
- );
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
+ }
354
458
  }
355
- });
459
+ })();
356
460
 
357
461
  return respond(true, {
358
462
  ok: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ylib-openclaw-weixin",
3
- "version": "2.1.7",
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,19 +21,23 @@
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
- "ylib-openclaw": "2026.3.9-beata.1"
36
+ "ylib-openclaw": "2026.3.9-beta.5"
31
37
  },
32
38
  "devDependencies": {
33
39
  "@vitest/coverage-v8": "^3.1.0",
34
- "ylib-openclaw": "2026.3.9-beata.1",
40
+ "ylib-openclaw": "2026.3.9-beta.5",
35
41
  "silk-wasm": "^3.7.1",
36
42
  "typescript": "^5.8.0",
37
43
  "vitest": "^3.1.0"
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}`);