@zhin.js/adapter-weixin-ilink 1.0.1 → 1.1.2

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.
Files changed (189) hide show
  1. package/CHANGELOG.md +512 -0
  2. package/README.md +24 -42
  3. package/adapters/weixin-ilink/index.js +32 -0
  4. package/adapters/weixin-ilink/index.ts +43 -0
  5. package/commands/weixin-ilink/endpoint/add/[id]/index.js +3 -0
  6. package/commands/weixin-ilink/endpoint/add/[id]/index.ts +3 -0
  7. package/commands/weixin-ilink/endpoint/definition.js +17 -0
  8. package/commands/weixin-ilink/endpoint/definition.ts +17 -0
  9. package/commands/weixin-ilink/endpoint/list/index.js +3 -0
  10. package/commands/weixin-ilink/endpoint/list/index.ts +3 -0
  11. package/commands/weixin-ilink/endpoint/remove/[id]/index.js +3 -0
  12. package/commands/weixin-ilink/endpoint/remove/[id]/index.ts +3 -0
  13. package/lib/aes-ecb.d.ts +0 -1
  14. package/lib/aes-ecb.js +0 -1
  15. package/lib/cdn-upload.d.ts +0 -1
  16. package/lib/cdn-upload.js +0 -1
  17. package/lib/cdn-url.d.ts +0 -1
  18. package/lib/cdn-url.js +0 -1
  19. package/lib/client.d.ts +35 -0
  20. package/lib/client.js +89 -0
  21. package/lib/context-store.d.ts +15 -2
  22. package/lib/context-store.js +93 -2
  23. package/lib/credentials.d.ts +15 -8
  24. package/lib/credentials.js +59 -43
  25. package/lib/endpoint.d.ts +40 -35
  26. package/lib/endpoint.js +311 -262
  27. package/lib/ilink-api.d.ts +7 -6
  28. package/lib/ilink-api.js +26 -16
  29. package/lib/ilink-config-cache.d.ts +3 -8
  30. package/lib/ilink-config-cache.js +13 -15
  31. package/lib/ilink-logger.d.ts +0 -2
  32. package/lib/ilink-logger.js +11 -26
  33. package/lib/ilink-meta.d.ts +13 -8
  34. package/lib/ilink-meta.js +24 -21
  35. package/lib/ilink-random.d.ts +0 -1
  36. package/lib/ilink-random.js +0 -1
  37. package/lib/ilink-redact.d.ts +0 -1
  38. package/lib/ilink-redact.js +0 -1
  39. package/lib/ilink-session-guard.d.ts +13 -15
  40. package/lib/ilink-session-guard.js +29 -43
  41. package/lib/ilink-types.d.ts +0 -1
  42. package/lib/ilink-types.js +0 -1
  43. package/lib/index.d.ts +8 -20
  44. package/lib/index.js +7 -38
  45. package/lib/login.d.ts +5 -6
  46. package/lib/login.js +85 -77
  47. package/lib/media-download.d.ts +1 -2
  48. package/lib/media-download.js +1 -2
  49. package/lib/mime.d.ts +0 -1
  50. package/lib/mime.js +0 -1
  51. package/lib/outbound-media.d.ts +4 -5
  52. package/lib/outbound-media.js +85 -86
  53. package/lib/pic-decrypt.d.ts +0 -1
  54. package/lib/pic-decrypt.js +0 -1
  55. package/lib/protocol.d.ts +60 -0
  56. package/lib/protocol.js +127 -0
  57. package/lib/silk-transcode.d.ts +0 -1
  58. package/lib/silk-transcode.js +0 -1
  59. package/lib/types.d.ts +2 -26
  60. package/lib/types.js +0 -1
  61. package/lib/upload.d.ts +1 -2
  62. package/lib/upload.js +1 -2
  63. package/lib/weixin-ilink-runtime-state.d.ts +1 -0
  64. package/lib/weixin-ilink-runtime-state.js +6 -0
  65. package/lib/weixin-inbound.d.ts +4 -43
  66. package/lib/weixin-inbound.js +22 -162
  67. package/lib/weixin-send-media.d.ts +0 -1
  68. package/lib/weixin-send-media.js +5 -2
  69. package/lib/weixin-send.d.ts +1 -2
  70. package/lib/weixin-send.js +4 -3
  71. package/package.json +48 -39
  72. package/plugin.js +14 -0
  73. package/schema.json +116 -0
  74. package/src/client.ts +117 -0
  75. package/src/context-store.ts +101 -8
  76. package/src/credentials.ts +68 -45
  77. package/src/endpoint.ts +384 -315
  78. package/src/ilink-api.ts +31 -21
  79. package/src/ilink-config-cache.ts +17 -13
  80. package/src/ilink-logger.ts +11 -26
  81. package/src/ilink-meta.ts +31 -21
  82. package/src/ilink-session-guard.ts +29 -46
  83. package/src/index.ts +41 -56
  84. package/src/login.ts +112 -85
  85. package/src/media-download.ts +1 -2
  86. package/src/outbound-media.ts +97 -97
  87. package/src/protocol.ts +180 -0
  88. package/src/silk-wasm.d.ts +6 -2
  89. package/src/types.ts +5 -25
  90. package/src/upload.test.ts +11 -8
  91. package/src/upload.ts +2 -2
  92. package/src/weixin-ilink-runtime-state.ts +7 -0
  93. package/src/weixin-inbound.ts +25 -187
  94. package/src/weixin-send-media.ts +5 -1
  95. package/src/weixin-send.ts +6 -4
  96. package/client/LoginAssistPanel.tsx +0 -200
  97. package/client/WeixinIlinkManagement.tsx +0 -139
  98. package/client/index.tsx +0 -11
  99. package/client/tsconfig.json +0 -13
  100. package/client/utils/api.ts +0 -30
  101. package/dist/index.js +0 -29
  102. package/lib/adapter.d.ts +0 -12
  103. package/lib/adapter.d.ts.map +0 -1
  104. package/lib/adapter.js +0 -15
  105. package/lib/adapter.js.map +0 -1
  106. package/lib/aes-ecb.d.ts.map +0 -1
  107. package/lib/aes-ecb.js.map +0 -1
  108. package/lib/cdn-upload.d.ts.map +0 -1
  109. package/lib/cdn-upload.js.map +0 -1
  110. package/lib/cdn-upload.test.d.ts +0 -2
  111. package/lib/cdn-upload.test.d.ts.map +0 -1
  112. package/lib/cdn-upload.test.js +0 -150
  113. package/lib/cdn-upload.test.js.map +0 -1
  114. package/lib/cdn-url.d.ts.map +0 -1
  115. package/lib/cdn-url.js.map +0 -1
  116. package/lib/context-store.d.ts.map +0 -1
  117. package/lib/context-store.js.map +0 -1
  118. package/lib/credentials.d.ts.map +0 -1
  119. package/lib/credentials.js.map +0 -1
  120. package/lib/endpoint.d.ts.map +0 -1
  121. package/lib/endpoint.js.map +0 -1
  122. package/lib/ilink-api.d.ts.map +0 -1
  123. package/lib/ilink-api.js.map +0 -1
  124. package/lib/ilink-config-cache.d.ts.map +0 -1
  125. package/lib/ilink-config-cache.js.map +0 -1
  126. package/lib/ilink-logger.d.ts.map +0 -1
  127. package/lib/ilink-logger.js.map +0 -1
  128. package/lib/ilink-meta.d.ts.map +0 -1
  129. package/lib/ilink-meta.js.map +0 -1
  130. package/lib/ilink-random.d.ts.map +0 -1
  131. package/lib/ilink-random.js.map +0 -1
  132. package/lib/ilink-redact.d.ts.map +0 -1
  133. package/lib/ilink-redact.js.map +0 -1
  134. package/lib/ilink-session-guard.d.ts.map +0 -1
  135. package/lib/ilink-session-guard.js.map +0 -1
  136. package/lib/ilink-types.d.ts.map +0 -1
  137. package/lib/ilink-types.js.map +0 -1
  138. package/lib/index.d.ts.map +0 -1
  139. package/lib/index.js.map +0 -1
  140. package/lib/login-assist-routes.d.ts +0 -7
  141. package/lib/login-assist-routes.d.ts.map +0 -1
  142. package/lib/login-assist-routes.js +0 -29
  143. package/lib/login-assist-routes.js.map +0 -1
  144. package/lib/login.d.ts.map +0 -1
  145. package/lib/login.js.map +0 -1
  146. package/lib/media-download.d.ts.map +0 -1
  147. package/lib/media-download.js.map +0 -1
  148. package/lib/mime.d.ts.map +0 -1
  149. package/lib/mime.js.map +0 -1
  150. package/lib/mime.test.d.ts +0 -2
  151. package/lib/mime.test.d.ts.map +0 -1
  152. package/lib/mime.test.js +0 -62
  153. package/lib/mime.test.js.map +0 -1
  154. package/lib/outbound-media.d.ts.map +0 -1
  155. package/lib/outbound-media.js.map +0 -1
  156. package/lib/pic-decrypt.d.ts.map +0 -1
  157. package/lib/pic-decrypt.js.map +0 -1
  158. package/lib/routes.d.ts +0 -8
  159. package/lib/routes.d.ts.map +0 -1
  160. package/lib/routes.js +0 -29
  161. package/lib/routes.js.map +0 -1
  162. package/lib/silk-transcode.d.ts.map +0 -1
  163. package/lib/silk-transcode.js.map +0 -1
  164. package/lib/silk-transcode.test.d.ts +0 -2
  165. package/lib/silk-transcode.test.d.ts.map +0 -1
  166. package/lib/silk-transcode.test.js +0 -53
  167. package/lib/silk-transcode.test.js.map +0 -1
  168. package/lib/types.d.ts.map +0 -1
  169. package/lib/types.js.map +0 -1
  170. package/lib/typing-indicator.d.ts +0 -32
  171. package/lib/typing-indicator.d.ts.map +0 -1
  172. package/lib/typing-indicator.js +0 -95
  173. package/lib/typing-indicator.js.map +0 -1
  174. package/lib/upload.d.ts.map +0 -1
  175. package/lib/upload.js.map +0 -1
  176. package/lib/upload.test.d.ts +0 -2
  177. package/lib/upload.test.d.ts.map +0 -1
  178. package/lib/upload.test.js +0 -250
  179. package/lib/upload.test.js.map +0 -1
  180. package/lib/weixin-inbound.d.ts.map +0 -1
  181. package/lib/weixin-inbound.js.map +0 -1
  182. package/lib/weixin-send-media.d.ts.map +0 -1
  183. package/lib/weixin-send-media.js.map +0 -1
  184. package/lib/weixin-send.d.ts.map +0 -1
  185. package/lib/weixin-send.js.map +0 -1
  186. package/src/adapter.ts +0 -18
  187. package/src/login-assist-routes.ts +0 -36
  188. package/src/routes.ts +0 -39
  189. package/src/typing-indicator.ts +0 -139
package/lib/index.js CHANGED
@@ -1,38 +1,7 @@
1
- /**
2
- * 微信 iLink(ClawBot)适配器入口
3
- */
4
- import path from "node:path";
5
- import { usePlugin } from "zhin.js";
6
- import { WeixinIlinkAdapter } from "./adapter.js";
7
- import { registerRoutes } from "./routes.js";
8
- export * from "./types.js";
9
- export { WeixinIlinkEndpoint } from "./endpoint.js";
10
- export { WeixinIlinkAdapter } from "./adapter.js";
11
- export { WeixinIlinkTypingIndicatorManager, enableTypingIndicator, } from "./typing-indicator.js";
12
- export { registerLoginAssistRoutes } from "./login-assist-routes.js";
13
- const plugin = usePlugin();
14
- const { provide, useContext, root } = plugin;
15
- provide({
16
- name: "weixin-ilink",
17
- description: "WeChat personal account via Tencent iLink Endpoint API (ClawBot)",
18
- mounted: async (p) => {
19
- const adapter = new WeixinIlinkAdapter(p);
20
- await adapter.start();
21
- return adapter;
22
- },
23
- dispose: async (adapter) => {
24
- await adapter.stop();
25
- },
26
- });
27
- useContext("web", (pageManager) => {
28
- pageManager.addEntry({
29
- id: "weixin-ilink",
30
- development: path.resolve(import.meta.dirname, "../client/index.tsx"),
31
- production: path.resolve(import.meta.dirname, "../dist/index.js"),
32
- meta: { name: "微信 iLink" },
33
- });
34
- });
35
- useContext("router", "weixin-ilink", async (router, adapter) => {
36
- registerRoutes(router, adapter, root);
37
- });
38
- //# sourceMappingURL=index.js.map
1
+ export { formatInboundContent, formatOutboundSegments, inboundMessageId, resolveWeixinIlinkConfig, segmentLocalPath, sleep, } from './protocol.js';
2
+ export { WeixinIlinkStateStore, } from './credentials.js';
3
+ export { IlinkClientMetadata, sanitizeBotAgent, } from './ilink-meta.js';
4
+ export { WeixinContextTokenStore } from './context-store.js';
5
+ export { IlinkSessionGuard, SESSION_EXPIRED_ERRCODE, SESSION_PAUSE_DURATION_MS, } from './ilink-session-guard.js';
6
+ export { WeixinIlinkClient, weixinIlinkClient, } from './client.js';
7
+ export { WeixinIlinkEndpoint, } from './endpoint.js';
package/lib/login.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import type { Plugin } from "zhin.js";
2
- import { type WeixinIlinkCredentials } from "./credentials.js";
3
- import type { WeixinIlinkEndpointConfig } from "./types.js";
4
- export declare function resolveCredentials(plugin: Plugin, config: WeixinIlinkEndpointConfig): Promise<WeixinIlinkCredentials>;
5
- export declare function loginWithQr(plugin: Plugin, config: WeixinIlinkEndpointConfig): Promise<WeixinIlinkCredentials>;
6
- //# sourceMappingURL=login.d.ts.map
1
+ import type { IlinkClientMetadata } from './ilink-meta.js';
2
+ import { type WeixinIlinkCredentials, type WeixinIlinkStateStore } from './credentials.js';
3
+ import type { ResolvedWeixinIlinkConfig } from './protocol.js';
4
+ export declare function resolveCredentials(config: ResolvedWeixinIlinkConfig, metadata: IlinkClientMetadata, state: WeixinIlinkStateStore, signal?: AbortSignal): Promise<WeixinIlinkCredentials>;
5
+ export declare function loginWithQr(config: ResolvedWeixinIlinkConfig, metadata: IlinkClientMetadata, state: WeixinIlinkStateStore, signal?: AbortSignal): Promise<WeixinIlinkCredentials>;
package/lib/login.js CHANGED
@@ -1,121 +1,129 @@
1
- import { apiGetFetch, apiPostFetch } from "./ilink-api.js";
2
- import { buildBaseInfo, DEFAULT_API_BASE_URL } from "./ilink-meta.js";
3
- import { logger } from "./ilink-logger.js";
4
- import { loadCredentials, saveCredentials, } from "./credentials.js";
5
- const DEFAULT_ILINK_BOT_TYPE = "3";
1
+ /**
2
+ * Resolve iLink bot credentials (config / endpoint state / QR login).
3
+ * No legacy Plugin / loginAssist dependency — QR URL is logged for manual scan.
4
+ */
5
+ import { apiGetFetch, apiPostFetch } from './ilink-api.js';
6
+ import { logger } from './ilink-logger.js';
7
+ const DEFAULT_ILINK_BOT_TYPE = '3';
6
8
  const QR_LONG_POLL_TIMEOUT_MS = 35_000;
7
9
  const LOGIN_TIMEOUT_MS = 480_000;
8
10
  function getLocalBotTokenList() {
9
11
  return [];
10
12
  }
11
- async function fetchQRCode(apiBaseUrl, botType) {
13
+ async function fetchQRCode(apiBaseUrl, botType, metadata) {
12
14
  const rawText = await apiPostFetch({
13
15
  baseUrl: apiBaseUrl,
16
+ metadata,
14
17
  endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
15
- body: JSON.stringify({ local_token_list: getLocalBotTokenList(), base_info: buildBaseInfo() }),
16
- label: "fetchQRCode",
18
+ body: JSON.stringify({
19
+ local_token_list: getLocalBotTokenList(),
20
+ base_info: metadata.buildBaseInfo(),
21
+ }),
22
+ label: 'fetchQRCode',
17
23
  });
18
24
  return JSON.parse(rawText);
19
25
  }
20
- async function pollQRStatus(apiBaseUrl, qrcode) {
26
+ async function pollQRStatus(apiBaseUrl, qrcode, metadata, signal) {
21
27
  try {
22
28
  const endpoint = `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
23
29
  const rawText = await apiGetFetch({
24
30
  baseUrl: apiBaseUrl,
31
+ metadata,
25
32
  endpoint,
26
33
  timeoutMs: QR_LONG_POLL_TIMEOUT_MS,
27
- label: "pollQRStatus",
34
+ label: 'pollQRStatus',
35
+ abortSignal: signal,
28
36
  });
29
37
  return JSON.parse(rawText);
30
38
  }
31
39
  catch (err) {
32
- if (err instanceof Error && err.name === "AbortError") {
33
- return { status: "wait" };
40
+ if (err instanceof Error && err.name === 'AbortError') {
41
+ // 外部 stop 的 abort 不能吞掉当普通长轮询超时
42
+ if (signal?.aborted)
43
+ throw err;
44
+ return { status: 'wait' };
34
45
  }
35
46
  logger.warn(`pollQRStatus error: ${String(err)}`);
36
- return { status: "wait" };
47
+ return { status: 'wait' };
37
48
  }
38
49
  }
39
- export async function resolveCredentials(plugin, config) {
40
- const envToken = process.env.WEIXIN_ILINK_TOKEN?.trim() || config.botToken?.trim();
41
- if (envToken) {
42
- const stored = loadCredentials(config.name);
50
+ function abortError() {
51
+ return Object.assign(new Error('扫码登录已取消'), { name: 'AbortError' });
52
+ }
53
+ function sleepWithAbort(ms, signal) {
54
+ return new Promise((resolve, reject) => {
55
+ if (signal?.aborted) {
56
+ reject(abortError());
57
+ return;
58
+ }
59
+ const timer = setTimeout(() => {
60
+ signal?.removeEventListener('abort', onAbort);
61
+ resolve();
62
+ }, ms);
63
+ const onAbort = () => {
64
+ clearTimeout(timer);
65
+ reject(abortError());
66
+ };
67
+ signal?.addEventListener('abort', onAbort, { once: true });
68
+ });
69
+ }
70
+ export async function resolveCredentials(config, metadata, state, signal) {
71
+ const configuredToken = config.botToken?.trim();
72
+ if (configuredToken) {
73
+ const stored = state.loadCredentials();
43
74
  return {
44
- botToken: envToken,
75
+ botToken: configuredToken,
45
76
  ilinkUserId: stored?.ilinkUserId,
46
77
  ilinkBotId: stored?.ilinkBotId,
47
- baseUrl: config.baseUrl ?? stored?.baseUrl ?? DEFAULT_API_BASE_URL,
78
+ baseUrl: config.baseUrl,
48
79
  };
49
80
  }
50
- const stored = loadCredentials(config.name);
81
+ const stored = state.loadCredentials();
51
82
  if (stored?.botToken) {
52
83
  return {
53
84
  ...stored,
54
- baseUrl: config.baseUrl ?? stored.baseUrl ?? DEFAULT_API_BASE_URL,
85
+ baseUrl: config.baseUrl,
55
86
  };
56
87
  }
57
- return loginWithQr(plugin, config);
88
+ return loginWithQr(config, metadata, state, signal);
58
89
  }
59
- export async function loginWithQr(plugin, config) {
60
- const apiBaseUrl = config.baseUrl ?? DEFAULT_API_BASE_URL;
61
- const qr = await fetchQRCode(apiBaseUrl, DEFAULT_ILINK_BOT_TYPE);
90
+ export async function loginWithQr(config, metadata, state, signal) {
91
+ const apiBaseUrl = config.baseUrl;
92
+ const qr = await fetchQRCode(apiBaseUrl, DEFAULT_ILINK_BOT_TYPE, metadata);
62
93
  const qrcodeUrl = qr.qrcode_img_content || qr.qrcode;
63
- const loginAssist = plugin.inject("loginAssist");
64
- if (!loginAssist) {
65
- throw new Error("loginAssist 服务不可用,请确保已加载 zhin 核心服务");
66
- }
67
- void loginAssist.waitForInput("weixin-ilink", config.name, "qrcode", {
68
- message: "请用微信扫描 ClawBot 二维码完成登录",
69
- image: qrcodeUrl.startsWith("data:") ? qrcodeUrl : undefined,
70
- url: qrcodeUrl.startsWith("http") ? qrcodeUrl : undefined,
71
- qrcode: qr.qrcode,
72
- });
73
- const assistTask = loginAssist
74
- .listPending()
75
- .find((t) => t.adapter === "weixin-ilink" && t.endpointId === config.name);
94
+ logger.info(`weixin-ilink QR login for ${config.id}: scan with WeChat ClawBot entry — ${qrcodeUrl.slice(0, 120)}${qrcodeUrl.length > 120 ? '…' : ''}`);
76
95
  const deadline = Date.now() + LOGIN_TIMEOUT_MS;
77
96
  let currentBaseUrl = apiBaseUrl;
78
- try {
79
- while (Date.now() < deadline) {
80
- const status = await pollQRStatus(currentBaseUrl, qr.qrcode);
81
- if (status.status === "scaned_but_redirect" && status.redirect_host) {
82
- currentBaseUrl = status.redirect_host.startsWith("http")
83
- ? status.redirect_host
84
- : `https://${status.redirect_host}`;
85
- }
86
- if (status.status === "confirmed" || status.status === "binded_redirect") {
87
- if (!status.bot_token && status.status !== "binded_redirect") {
88
- throw new Error("扫码成功但未返回 bot_token");
89
- }
90
- const creds = {
91
- botToken: status.bot_token ?? loadCredentials(config.name)?.botToken ?? "",
92
- ilinkUserId: status.ilink_user_id,
93
- ilinkBotId: status.ilink_endpoint_id,
94
- baseUrl: status.baseurl ?? currentBaseUrl,
95
- };
96
- if (!creds.botToken) {
97
- throw new Error("binded_redirect 但本地无有效 bot_token");
98
- }
99
- saveCredentials(config.name, creds);
100
- if (assistTask)
101
- loginAssist.submit(assistTask.id, "confirmed");
102
- return creds;
97
+ while (Date.now() < deadline) {
98
+ // stop() 的 AbortSignal:不打满 8 分钟,立即退出登录循环
99
+ if (signal?.aborted)
100
+ throw abortError();
101
+ const status = await pollQRStatus(currentBaseUrl, qr.qrcode, metadata, signal);
102
+ if (status.status === 'scaned_but_redirect' && status.redirect_host) {
103
+ currentBaseUrl = status.redirect_host.startsWith('http')
104
+ ? status.redirect_host
105
+ : `https://${status.redirect_host}`;
106
+ }
107
+ if (status.status === 'confirmed' || status.status === 'binded_redirect') {
108
+ if (!status.bot_token && status.status !== 'binded_redirect') {
109
+ throw new Error('扫码成功但未返回 bot_token');
103
110
  }
104
- if (status.status === "expired") {
105
- if (assistTask)
106
- loginAssist.cancel(assistTask.id, "expired");
107
- throw new Error("二维码已过期,请重启 bot 重新登录");
111
+ const creds = {
112
+ botToken: status.bot_token ?? state.loadCredentials()?.botToken ?? '',
113
+ ilinkUserId: status.ilink_user_id,
114
+ ilinkBotId: status.ilink_endpoint_id,
115
+ baseUrl: status.baseurl ?? currentBaseUrl,
116
+ };
117
+ if (!creds.botToken) {
118
+ throw new Error('binded_redirect 但本地无有效 bot_token');
108
119
  }
109
- await new Promise((r) => setTimeout(r, 500));
120
+ state.saveCredentials(creds);
121
+ return creds;
110
122
  }
111
- if (assistTask)
112
- loginAssist.cancel(assistTask.id, "timeout");
113
- throw new Error("微信扫码登录超时");
114
- }
115
- catch (err) {
116
- if (assistTask)
117
- loginAssist.cancel(assistTask.id, String(err));
118
- throw err;
123
+ if (status.status === 'expired') {
124
+ throw new Error('二维码已过期,请重启 bot 重新登录');
125
+ }
126
+ await sleepWithAbort(500, signal);
119
127
  }
128
+ throw new Error('微信扫码登录超时');
120
129
  }
121
- //# sourceMappingURL=login.js.map
@@ -1,5 +1,5 @@
1
1
  import type { WeixinInboundMediaOpts } from "./weixin-inbound.js";
2
- import type { WeixinMessage } from "./ilink-types.js";
2
+ import { type WeixinMessage } from './ilink-types.js';
3
3
  /** Persist a buffer via the framework's unified media store. */
4
4
  type SaveMediaFn = (buffer: Buffer, contentType?: string, subdir?: string, maxBytes?: number, originalFilename?: string) => Promise<{
5
5
  path: string;
@@ -16,4 +16,3 @@ export declare function downloadMediaFromItem(item: WeixinMessage["item_list"] e
16
16
  label: string;
17
17
  }): Promise<WeixinInboundMediaOpts>;
18
18
  export {};
19
- //# sourceMappingURL=media-download.d.ts.map
@@ -2,7 +2,7 @@ import { logger } from "./ilink-logger.js";
2
2
  import { getMimeFromFilename } from "./mime.js";
3
3
  import { downloadAndDecryptBuffer, downloadPlainCdnBuffer, } from "./pic-decrypt.js";
4
4
  import { silkToWav } from "./silk-transcode.js";
5
- import { MessageItemType } from "./ilink-types.js";
5
+ import { MessageItemType } from './ilink-types.js';
6
6
  const WEIXIN_MEDIA_MAX_BYTES = 100 * 1024 * 1024;
7
7
  /**
8
8
  * Download and decrypt media from a single MessageItem.
@@ -92,4 +92,3 @@ export async function downloadMediaFromItem(item, deps) {
92
92
  }
93
93
  return result;
94
94
  }
95
- //# sourceMappingURL=media-download.js.map
package/lib/mime.d.ts CHANGED
@@ -6,4 +6,3 @@ export declare function getExtensionFromMime(mimeType: string): string;
6
6
  export declare function sniffMimeFromBuffer(buf: Buffer): string | undefined;
7
7
  /** Get file extension from Content-Type header or URL path. Returns ".bin" for unknown. */
8
8
  export declare function getExtensionFromContentTypeOrUrl(contentType: string | null, url: string): string;
9
- //# sourceMappingURL=mime.d.ts.map
package/lib/mime.js CHANGED
@@ -96,4 +96,3 @@ export function getExtensionFromContentTypeOrUrl(contentType, url) {
96
96
  const knownExts = new Set(Object.keys(EXTENSION_TO_MIME));
97
97
  return knownExts.has(ext) ? ext : ".bin";
98
98
  }
99
- //# sourceMappingURL=mime.js.map
@@ -1,5 +1,4 @@
1
- import type { MessageSegment, SendContent } from "zhin.js";
2
- export declare function segmentMediaRef(seg: MessageSegment): string | undefined;
3
- /** 将 base64 / 远程图落盘,供 sendWeixinMediaFile 上传 */
4
- export declare function materializeOutboundMedia(content: SendContent, outboundDir: string): Promise<SendContent>;
5
- //# sourceMappingURL=outbound-media.d.ts.map
1
+ import type { WeixinWireSegment } from './protocol.js';
2
+ export type OutboundWireContent = string | WeixinWireSegment | ReadonlyArray<string | WeixinWireSegment>;
3
+ /** 将 canonical MediaRef 媒体段物化为本地文件(kind=path),供 sendWeixinMediaFile 上传 */
4
+ export declare function materializeOutboundMedia(content: OutboundWireContent, outboundDir: string): Promise<OutboundWireContent>;
@@ -1,122 +1,121 @@
1
1
  /**
2
- * 微信 iLink 出站媒体:base64 / 远程 URL 须落盘后再走 CDN 上传。
2
+ * 微信 iLink 出站媒体物化:canonical MediaRef(`data.media`,MediaRef-only)→ 本地文件,
3
+ * 再由 sendWeixinMediaFile 走 CDN 上传。
4
+ * - kind=url → 下载落盘后上传;
5
+ * - kind=base64 → 解码落盘后上传;
6
+ * - kind=path → 本地文件直接上传;
7
+ * - kind=file → 微信无不透明平台引用投递面,warn 丢弃。
8
+ * 无 media 或物化失败的媒体段 warn 丢弃(weixin_ilink_outbound_media_dropped)。
3
9
  */
4
- import crypto from "node:crypto";
5
- import fs from "node:fs";
6
- import path from "node:path";
7
- import { downloadRemoteImageToTemp } from "./upload.js";
8
- import { getExtensionFromMime } from "./mime.js";
9
- const MEDIA_TYPES = new Set(["image", "video", "file", "record", "audio"]);
10
- function isRemoteUrl(value) {
11
- return /^https?:\/\//i.test(value);
10
+ import crypto from 'node:crypto';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { isMediaRef } from '@zhin.js/im-contract';
14
+ import { formatCompact, getLogger } from '@zhin.js/logger';
15
+ import { downloadRemoteImageToTemp } from './upload.js';
16
+ import { getExtensionFromMime } from './mime.js';
17
+ const logger = getLogger('weixin-ilink');
18
+ const MEDIA_TYPES = new Set(['image', 'video', 'file', 'record', 'audio']);
19
+ function dropMediaSegment(type, reason) {
20
+ logger.warn(formatCompact({
21
+ op: 'weixin_ilink_outbound_media_dropped',
22
+ type,
23
+ reason,
24
+ }));
25
+ return null;
12
26
  }
13
- function isInlineMediaRef(value) {
14
- return value.startsWith("base64://") || /^data:[^/]+\/[^;]+;base64,/i.test(value);
15
- }
16
- function normalizeLocalPath(value) {
17
- return value.replace(/^file:\/\//, "");
18
- }
19
- export function segmentMediaRef(seg) {
20
- const data = seg.data;
21
- const candidates = [data.file, data.path, data.url].filter((v) => typeof v === "string" && Boolean(v));
22
- return candidates[0];
23
- }
24
- function extractBase64Buffer(data) {
25
- const url = typeof data.url === "string" ? data.url : undefined;
26
- const file = typeof data.file === "string" ? data.file : undefined;
27
- const base64 = typeof data.base64 === "string" ? data.base64 : undefined;
28
- if (url?.startsWith("base64://")) {
29
- return Buffer.from(url.slice(9), "base64");
30
- }
31
- if (url && /^data:[^/]+\/[^;]+;base64,/i.test(url)) {
32
- return Buffer.from(url.replace(/^data:[^/]+\/[^;]+;base64,/, ""), "base64");
33
- }
34
- if (file?.startsWith("base64://")) {
35
- return Buffer.from(file.slice(9), "base64");
36
- }
37
- if (base64) {
38
- const raw = base64.startsWith("base64://") ? base64.slice(9) : base64;
39
- return Buffer.from(raw, "base64");
40
- }
41
- return undefined;
42
- }
43
- function resolveExt(seg, mime) {
44
- const data = seg.data;
45
- const name = typeof data.name === "string" ? data.name : undefined;
46
- if (name) {
47
- const ext = path.extname(name);
27
+ function resolveExt(seg, media) {
28
+ if (media.file_name) {
29
+ const ext = path.extname(media.file_name);
48
30
  if (ext)
49
31
  return ext;
50
32
  }
51
- const mimeHint = mime ?? (typeof data.mime === "string" ? data.mime : typeof data.mimeType === "string" ? data.mimeType : undefined);
52
- if (mimeHint) {
53
- return getExtensionFromMime(mimeHint) ?? ".bin";
33
+ if (media.mime_type) {
34
+ return getExtensionFromMime(media.mime_type) ?? '.bin';
54
35
  }
55
- if (seg.type === "image")
56
- return ".png";
57
- if (seg.type === "video")
58
- return ".mp4";
59
- if (seg.type === "record" || seg.type === "audio")
60
- return ".mp3";
61
- return ".bin";
36
+ if (seg.type === 'image')
37
+ return '.png';
38
+ if (seg.type === 'video')
39
+ return '.mp4';
40
+ if (seg.type === 'record' || seg.type === 'audio')
41
+ return '.mp3';
42
+ return '.bin';
62
43
  }
63
- function withLocalFile(seg, filePath) {
64
- const data = { ...seg.data, file: filePath };
65
- delete data.base64;
66
- return { type: seg.type, data };
44
+ function withLocalPath(seg, media, filePath) {
45
+ return {
46
+ type: seg.type,
47
+ data: { ...(seg.data ?? {}), media: { ...media, kind: 'path', value: filePath } },
48
+ };
67
49
  }
68
- function writeOutboundBuffer(buffer, outboundDir, seg) {
50
+ function writeOutboundBuffer(buffer, outboundDir, seg, media) {
69
51
  fs.mkdirSync(outboundDir, { recursive: true });
70
- const ext = resolveExt(seg);
71
- const filePath = path.join(outboundDir, `out-${seg.type}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}${ext}`);
52
+ const ext = resolveExt(seg, media);
53
+ const filePath = path.join(outboundDir, `out-${seg.type}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}${ext}`);
72
54
  fs.writeFileSync(filePath, buffer);
73
55
  return filePath;
74
56
  }
75
57
  async function materializeSegment(seg, outboundDir) {
76
58
  if (!MEDIA_TYPES.has(seg.type))
77
59
  return seg;
78
- const ref = segmentMediaRef(seg);
79
- if (ref && !isInlineMediaRef(ref)) {
80
- const local = normalizeLocalPath(ref);
81
- if (isRemoteUrl(local)) {
82
- const downloaded = await downloadRemoteImageToTemp(local, outboundDir);
83
- return withLocalFile(seg, downloaded);
60
+ const media = (seg.data ?? {}).media;
61
+ if (!isMediaRef(media)) {
62
+ return dropMediaSegment(seg.type, 'missing_media_ref');
63
+ }
64
+ switch (media.kind) {
65
+ case 'url': {
66
+ try {
67
+ const downloaded = await downloadRemoteImageToTemp(media.value, outboundDir);
68
+ return withLocalPath(seg, media, downloaded);
69
+ }
70
+ catch (err) {
71
+ logger.warn(formatCompact({
72
+ op: 'weixin_ilink_outbound_media_dropped',
73
+ type: seg.type,
74
+ reason: 'download_failed',
75
+ error: err instanceof Error ? err.message : String(err),
76
+ }));
77
+ return null;
78
+ }
84
79
  }
85
- if (fs.existsSync(local)) {
86
- return withLocalFile(seg, local);
80
+ case 'base64':
81
+ return withLocalPath(seg, media, writeOutboundBuffer(Buffer.from(media.value, 'base64'), outboundDir, seg, media));
82
+ case 'path': {
83
+ if (!fs.existsSync(media.value)) {
84
+ return dropMediaSegment(seg.type, 'missing_source');
85
+ }
86
+ return withLocalPath(seg, media, media.value);
87
87
  }
88
+ case 'file':
89
+ // 平台不透明引用(如 Telegram file_id):微信 iLink 无对应投递面
90
+ return dropMediaSegment(seg.type, 'unsupported_media_kind');
88
91
  }
89
- const buffer = extractBase64Buffer(seg.data);
90
- if (buffer) {
91
- return withLocalFile(seg, writeOutboundBuffer(buffer, outboundDir, seg));
92
- }
93
- return seg;
94
92
  }
95
93
  function asSegments(content) {
96
- if (typeof content === "string") {
97
- return [{ type: "text", data: { text: content } }];
94
+ if (typeof content === 'string') {
95
+ return [{ type: 'text', data: { text: content } }];
98
96
  }
99
97
  if (!Array.isArray(content)) {
100
- return typeof content === "object" && content !== null && "type" in content
98
+ return typeof content === 'object' && content !== null && 'type' in content
101
99
  ? [content]
102
100
  : [];
103
101
  }
104
102
  return content.flatMap((item) => {
105
- if (typeof item === "string")
106
- return [{ type: "text", data: { text: item } }];
107
- return typeof item === "object" && item !== null && "type" in item ? [item] : [];
103
+ if (typeof item === 'string')
104
+ return [{ type: 'text', data: { text: item } }];
105
+ return typeof item === 'object' && item !== null && 'type' in item ? [item] : [];
108
106
  });
109
107
  }
110
- /** 将 base64 / 远程图落盘,供 sendWeixinMediaFile 上传 */
108
+ /** 将 canonical MediaRef 媒体段物化为本地文件(kind=path),供 sendWeixinMediaFile 上传 */
111
109
  export async function materializeOutboundMedia(content, outboundDir) {
112
110
  const segments = asSegments(content);
113
- const out = await Promise.all(segments.map((seg) => materializeSegment(seg, outboundDir)));
114
- if (typeof content === "string") {
115
- return out[0]?.type === "text" ? out[0].data.text : content;
111
+ const out = (await Promise.all(segments.map((seg) => materializeSegment(seg, outboundDir))))
112
+ .filter((seg) => seg !== null);
113
+ if (typeof content === 'string') {
114
+ return out[0]?.type === 'text' ? String(out[0].data?.text ?? content) : content;
116
115
  }
117
116
  if (!Array.isArray(content)) {
118
- return out[0] ?? content;
117
+ // 单个媒体段被丢弃时降级为空文本,避免未物化段漏到 endpoint
118
+ return out[0] ?? { type: 'text', data: { text: '' } };
119
119
  }
120
120
  return out;
121
121
  }
122
- //# sourceMappingURL=outbound-media.js.map
@@ -7,4 +7,3 @@ export declare function downloadAndDecryptBuffer(encryptedQueryParam: string, ae
7
7
  * Download plain (unencrypted) bytes from the CDN. Returns the raw Buffer.
8
8
  */
9
9
  export declare function downloadPlainCdnBuffer(encryptedQueryParam: string, cdnBaseUrl: string, label: string, fullUrl?: string): Promise<Buffer>;
10
- //# sourceMappingURL=pic-decrypt.d.ts.map
@@ -86,4 +86,3 @@ export async function downloadPlainCdnBuffer(encryptedQueryParam, cdnBaseUrl, la
86
86
  logger.debug(`${label}: fetching url=${url}`);
87
87
  return fetchCdnBytes(url, label);
88
88
  }
89
- //# sourceMappingURL=pic-decrypt.js.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Weixin iLink protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ */
5
+ import { type ConversationRef, type Segment } from '@zhin.js/im-contract';
6
+ import type { WeixinMessage } from './ilink-types.js';
7
+ /** One expanded Weixin iLink endpoint configuration produced by AdapterIndex. */
8
+ export interface WeixinIlinkEndpointConfig {
9
+ readonly id: string;
10
+ readonly botAgent?: string;
11
+ readonly baseUrl?: string;
12
+ readonly cdnBaseUrl?: string;
13
+ readonly longPollTimeoutMs?: number;
14
+ readonly botToken?: string;
15
+ readonly dataDir?: string;
16
+ }
17
+ export interface ResolvedWeixinIlinkConfig {
18
+ readonly context: 'weixin-ilink';
19
+ readonly id: string;
20
+ readonly botAgent?: string;
21
+ readonly baseUrl: string;
22
+ readonly cdnBaseUrl: string;
23
+ readonly longPollTimeoutMs: number;
24
+ readonly botToken?: string;
25
+ readonly dataDir: string;
26
+ }
27
+ export interface WeixinWireSegment {
28
+ readonly type: string;
29
+ readonly data?: Record<string, unknown>;
30
+ }
31
+ export interface WeixinInboundMediaPaths {
32
+ readonly decryptedPicPath?: string;
33
+ readonly decryptedVideoPath?: string;
34
+ readonly decryptedFilePath?: string;
35
+ readonly decryptedVoicePath?: string;
36
+ }
37
+ export type WeixinMessageWithMedia = WeixinMessage & {
38
+ readonly _media?: WeixinInboundMediaPaths;
39
+ };
40
+ export declare function resolveWeixinIlinkConfig(config: WeixinIlinkEndpointConfig): ResolvedWeixinIlinkConfig;
41
+ /** Build inbound text for OutboundMessageService.receive (gateway owns reply routing). */
42
+ export declare function formatInboundContent(msg: WeixinMessageWithMedia): string;
43
+ export declare function weixinInboundSegments(msg: WeixinMessageWithMedia): readonly Segment[];
44
+ export declare function weixinReplyTo(msg: WeixinMessageWithMedia): Readonly<{
45
+ id: string;
46
+ }> | undefined;
47
+ export declare function inboundMessageId(msg: WeixinMessage): string;
48
+ /**
49
+ * 入站归一化 → ConversationRef:个人微信无群/频道概念,全部会话都是
50
+ * 与 `from_user_id` 的 private 会话(无 parent)。
51
+ */
52
+ export declare function weixinIlinkInboundConversation(endpointKey: string, userId: string): ConversationRef;
53
+ /**
54
+ * Wire-encode an already-rendered outbound payload into Weixin wire segments.
55
+ * Segment canonicalization is intentionally not done here.
56
+ */
57
+ export declare function formatOutboundSegments(payload: unknown): WeixinWireSegment[];
58
+ /** 物化后的媒体段(data.media kind=path)→ 本地文件路径;其余返回 undefined。 */
59
+ export declare function segmentLocalPath(seg: WeixinWireSegment): string | undefined;
60
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;