@zhin.js/adapter-weixin-ilink 3.0.2 → 3.0.3

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 (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +16 -39
  3. package/adapters/weixin-ilink.ts +30 -0
  4. package/lib/aes-ecb.d.ts +0 -1
  5. package/lib/aes-ecb.js +0 -1
  6. package/lib/cdn-upload.d.ts +0 -1
  7. package/lib/cdn-upload.js +0 -1
  8. package/lib/cdn-url.d.ts +0 -1
  9. package/lib/cdn-url.js +0 -1
  10. package/lib/context-store.d.ts +0 -1
  11. package/lib/context-store.js +0 -1
  12. package/lib/credentials.d.ts +0 -1
  13. package/lib/credentials.js +0 -1
  14. package/lib/endpoint.d.ts +35 -30
  15. package/lib/endpoint.js +217 -235
  16. package/lib/ilink-api.d.ts +0 -1
  17. package/lib/ilink-api.js +0 -1
  18. package/lib/ilink-config-cache.d.ts +0 -1
  19. package/lib/ilink-config-cache.js +0 -1
  20. package/lib/ilink-logger.d.ts +0 -1
  21. package/lib/ilink-logger.js +0 -1
  22. package/lib/ilink-meta.d.ts +0 -1
  23. package/lib/ilink-meta.js +0 -1
  24. package/lib/ilink-random.d.ts +0 -1
  25. package/lib/ilink-random.js +0 -1
  26. package/lib/ilink-redact.d.ts +0 -1
  27. package/lib/ilink-redact.js +0 -1
  28. package/lib/ilink-session-guard.d.ts +0 -1
  29. package/lib/ilink-session-guard.js +0 -1
  30. package/lib/ilink-types.d.ts +0 -1
  31. package/lib/ilink-types.js +0 -1
  32. package/lib/index.d.ts +4 -19
  33. package/lib/index.js +2 -37
  34. package/lib/login.d.ts +4 -6
  35. package/lib/login.js +43 -65
  36. package/lib/media-download.d.ts +0 -1
  37. package/lib/media-download.js +0 -1
  38. package/lib/mime.d.ts +0 -1
  39. package/lib/mime.js +0 -1
  40. package/lib/outbound-media.d.ts +4 -4
  41. package/lib/outbound-media.js +44 -42
  42. package/lib/pic-decrypt.d.ts +0 -1
  43. package/lib/pic-decrypt.js +0 -1
  44. package/lib/protocol.d.ts +52 -0
  45. package/lib/protocol.js +109 -0
  46. package/lib/silk-transcode.d.ts +0 -1
  47. package/lib/silk-transcode.js +0 -1
  48. package/lib/types.d.ts +2 -16
  49. package/lib/types.js +0 -1
  50. package/lib/upload.d.ts +0 -1
  51. package/lib/upload.js +0 -1
  52. package/lib/weixin-inbound.d.ts +0 -1
  53. package/lib/weixin-inbound.js +0 -1
  54. package/lib/weixin-send-media.d.ts +0 -1
  55. package/lib/weixin-send-media.js +0 -1
  56. package/lib/weixin-send.d.ts +0 -1
  57. package/lib/weixin-send.js +0 -1
  58. package/package.json +37 -46
  59. package/plugin.ts +8 -0
  60. package/schema.json +30 -0
  61. package/src/endpoint.ts +261 -279
  62. package/src/index.ts +24 -53
  63. package/src/login.ts +56 -72
  64. package/src/outbound-media.ts +56 -50
  65. package/src/protocol.ts +172 -0
  66. package/src/types.ts +6 -15
  67. package/client/LoginAssistPanel.tsx +0 -200
  68. package/client/WeixinIlinkManagement.tsx +0 -139
  69. package/client/index.tsx +0 -11
  70. package/client/tsconfig.json +0 -13
  71. package/client/utils/api.ts +0 -30
  72. package/dist/index.js +0 -29
  73. package/lib/adapter.d.ts +0 -14
  74. package/lib/adapter.d.ts.map +0 -1
  75. package/lib/adapter.js +0 -21
  76. package/lib/adapter.js.map +0 -1
  77. package/lib/aes-ecb.d.ts.map +0 -1
  78. package/lib/aes-ecb.js.map +0 -1
  79. package/lib/cdn-upload.d.ts.map +0 -1
  80. package/lib/cdn-upload.js.map +0 -1
  81. package/lib/cdn-upload.test.d.ts +0 -2
  82. package/lib/cdn-upload.test.d.ts.map +0 -1
  83. package/lib/cdn-upload.test.js +0 -150
  84. package/lib/cdn-upload.test.js.map +0 -1
  85. package/lib/cdn-url.d.ts.map +0 -1
  86. package/lib/cdn-url.js.map +0 -1
  87. package/lib/context-store.d.ts.map +0 -1
  88. package/lib/context-store.js.map +0 -1
  89. package/lib/credentials.d.ts.map +0 -1
  90. package/lib/credentials.js.map +0 -1
  91. package/lib/endpoint.d.ts.map +0 -1
  92. package/lib/endpoint.js.map +0 -1
  93. package/lib/ilink-api.d.ts.map +0 -1
  94. package/lib/ilink-api.js.map +0 -1
  95. package/lib/ilink-config-cache.d.ts.map +0 -1
  96. package/lib/ilink-config-cache.js.map +0 -1
  97. package/lib/ilink-logger.d.ts.map +0 -1
  98. package/lib/ilink-logger.js.map +0 -1
  99. package/lib/ilink-meta.d.ts.map +0 -1
  100. package/lib/ilink-meta.js.map +0 -1
  101. package/lib/ilink-random.d.ts.map +0 -1
  102. package/lib/ilink-random.js.map +0 -1
  103. package/lib/ilink-redact.d.ts.map +0 -1
  104. package/lib/ilink-redact.js.map +0 -1
  105. package/lib/ilink-session-guard.d.ts.map +0 -1
  106. package/lib/ilink-session-guard.js.map +0 -1
  107. package/lib/ilink-types.d.ts.map +0 -1
  108. package/lib/ilink-types.js.map +0 -1
  109. package/lib/index.d.ts.map +0 -1
  110. package/lib/index.js.map +0 -1
  111. package/lib/login-assist-routes.d.ts +0 -7
  112. package/lib/login-assist-routes.d.ts.map +0 -1
  113. package/lib/login-assist-routes.js +0 -29
  114. package/lib/login-assist-routes.js.map +0 -1
  115. package/lib/login.d.ts.map +0 -1
  116. package/lib/login.js.map +0 -1
  117. package/lib/media-download.d.ts.map +0 -1
  118. package/lib/media-download.js.map +0 -1
  119. package/lib/mime.d.ts.map +0 -1
  120. package/lib/mime.js.map +0 -1
  121. package/lib/mime.test.d.ts +0 -2
  122. package/lib/mime.test.d.ts.map +0 -1
  123. package/lib/mime.test.js +0 -62
  124. package/lib/mime.test.js.map +0 -1
  125. package/lib/outbound-media.d.ts.map +0 -1
  126. package/lib/outbound-media.js.map +0 -1
  127. package/lib/pic-decrypt.d.ts.map +0 -1
  128. package/lib/pic-decrypt.js.map +0 -1
  129. package/lib/routes.d.ts +0 -8
  130. package/lib/routes.d.ts.map +0 -1
  131. package/lib/routes.js +0 -29
  132. package/lib/routes.js.map +0 -1
  133. package/lib/segment-mapper.d.ts +0 -2
  134. package/lib/segment-mapper.d.ts.map +0 -1
  135. package/lib/segment-mapper.js +0 -2
  136. package/lib/segment-mapper.js.map +0 -1
  137. package/lib/silk-transcode.d.ts.map +0 -1
  138. package/lib/silk-transcode.js.map +0 -1
  139. package/lib/silk-transcode.test.d.ts +0 -2
  140. package/lib/silk-transcode.test.d.ts.map +0 -1
  141. package/lib/silk-transcode.test.js +0 -53
  142. package/lib/silk-transcode.test.js.map +0 -1
  143. package/lib/types.d.ts.map +0 -1
  144. package/lib/types.js.map +0 -1
  145. package/lib/upload.d.ts.map +0 -1
  146. package/lib/upload.js.map +0 -1
  147. package/lib/upload.test.d.ts +0 -2
  148. package/lib/upload.test.d.ts.map +0 -1
  149. package/lib/upload.test.js +0 -250
  150. package/lib/upload.test.js.map +0 -1
  151. package/lib/weixin-inbound.d.ts.map +0 -1
  152. package/lib/weixin-inbound.js.map +0 -1
  153. package/lib/weixin-send-media.d.ts.map +0 -1
  154. package/lib/weixin-send-media.js.map +0 -1
  155. package/lib/weixin-send.d.ts.map +0 -1
  156. package/lib/weixin-send.js.map +0 -1
  157. package/src/adapter.ts +0 -24
  158. package/src/login-assist-routes.ts +0 -36
  159. package/src/routes.ts +0 -39
  160. package/src/segment-mapper.ts +0 -1
@@ -51,4 +51,3 @@ export function redactUrl(rawUrl) {
51
51
  return truncate(rawUrl, 80);
52
52
  }
53
53
  }
54
- //# sourceMappingURL=ilink-redact.js.map
@@ -13,4 +13,3 @@ export declare function assertSessionActive(accountId: string): void;
13
13
  * @internal
14
14
  */
15
15
  export declare function _resetForTest(): void;
16
- //# sourceMappingURL=ilink-session-guard.d.ts.map
@@ -46,4 +46,3 @@ export function assertSessionActive(accountId) {
46
46
  export function _resetForTest() {
47
47
  pauseUntilMap.clear();
48
48
  }
49
- //# sourceMappingURL=ilink-session-guard.js.map
@@ -230,4 +230,3 @@ export interface NotifyStartResp {
230
230
  ret?: number;
231
231
  errmsg?: string;
232
232
  }
233
- //# sourceMappingURL=ilink-types.d.ts.map
@@ -32,4 +32,3 @@ export const TypingStatus = {
32
32
  TYPING: 1,
33
33
  CANCEL: 2,
34
34
  };
35
- //# sourceMappingURL=ilink-types.js.map
package/lib/index.d.ts CHANGED
@@ -1,19 +1,4 @@
1
- import type { Router } from "@zhin.js/host-router";
2
- import { PageManager } from "@zhin.js/host-api";
3
- import { WeixinIlinkAdapter } from "./adapter.js";
4
- declare module "zhin.js" {
5
- namespace Plugin {
6
- interface Contexts {
7
- web: PageManager;
8
- router: Router;
9
- }
10
- }
11
- interface Adapters {
12
- "weixin-ilink": WeixinIlinkAdapter;
13
- }
14
- }
15
- export * from "./types.js";
16
- export { WeixinIlinkEndpoint } from "./endpoint.js";
17
- export { WeixinIlinkAdapter } from "./adapter.js";
18
- export { registerLoginAssistRoutes } from "./login-assist-routes.js";
19
- //# sourceMappingURL=index.d.ts.map
1
+ export { formatInboundContent, formatOutboundSegments, inboundMessageId, resolveWeixinIlinkConfig, segmentLocalPath, sleep, type ResolvedWeixinIlinkConfig, type WeixinIlinkAdapterConfig, type WeixinIlinkEndpointConfig, type WeixinInboundMediaPaths, type WeixinMessageWithMedia, type WeixinWireSegment, } from './protocol.js';
2
+ export type { WeixinMessage, MessageItem } from './ilink-types.js';
3
+ export type { WeixinIlinkCredentials } from './credentials.js';
4
+ export { WeixinIlinkEndpoint, type WeixinIlinkEndpointOptions, type WeixinIlinkGetUpdates, type WeixinIlinkNotifyStart, type WeixinIlinkNotifyStop, type WeixinIlinkSendText, } from './endpoint.js';
package/lib/index.js CHANGED
@@ -1,37 +1,2 @@
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 { registerLoginAssistRoutes } from "./login-assist-routes.js";
12
- const plugin = usePlugin();
13
- const { provide, useContext, root } = plugin;
14
- provide({
15
- name: "weixin-ilink",
16
- description: "WeChat personal account via Tencent iLink Endpoint API (ClawBot)",
17
- mounted: async (p) => {
18
- const adapter = new WeixinIlinkAdapter(p);
19
- await adapter.start();
20
- return adapter;
21
- },
22
- dispose: async (adapter) => {
23
- await adapter.stop();
24
- },
25
- });
26
- useContext("web", (pageManager) => {
27
- pageManager.addEntry({
28
- id: "weixin-ilink",
29
- development: path.resolve(import.meta.dirname, "../client/index.tsx"),
30
- production: path.resolve(import.meta.dirname, "../dist/index.js"),
31
- meta: { name: "微信 iLink" },
32
- });
33
- });
34
- useContext("router", "weixin-ilink", async (router, adapter) => {
35
- registerRoutes(router, adapter, root);
36
- });
37
- //# sourceMappingURL=index.js.map
1
+ export { formatInboundContent, formatOutboundSegments, inboundMessageId, resolveWeixinIlinkConfig, segmentLocalPath, sleep, } from './protocol.js';
2
+ export { WeixinIlinkEndpoint, } from './endpoint.js';
package/lib/login.d.ts CHANGED
@@ -1,6 +1,4 @@
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 WeixinIlinkCredentials } from './credentials.js';
2
+ import type { ResolvedWeixinIlinkConfig } from './protocol.js';
3
+ export declare function resolveCredentials(config: ResolvedWeixinIlinkConfig): Promise<WeixinIlinkCredentials>;
4
+ export declare function loginWithQr(config: ResolvedWeixinIlinkConfig): Promise<WeixinIlinkCredentials>;
package/lib/login.js CHANGED
@@ -1,8 +1,12 @@
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 (env / config / sidecar file / 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 { buildBaseInfo, DEFAULT_API_BASE_URL } from './ilink-meta.js';
7
+ import { logger } from './ilink-logger.js';
8
+ import { loadCredentials, saveCredentials, } from './credentials.js';
9
+ const DEFAULT_ILINK_BOT_TYPE = '3';
6
10
  const QR_LONG_POLL_TIMEOUT_MS = 35_000;
7
11
  const LOGIN_TIMEOUT_MS = 480_000;
8
12
  function getLocalBotTokenList() {
@@ -13,7 +17,7 @@ async function fetchQRCode(apiBaseUrl, botType) {
13
17
  baseUrl: apiBaseUrl,
14
18
  endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
15
19
  body: JSON.stringify({ local_token_list: getLocalBotTokenList(), base_info: buildBaseInfo() }),
16
- label: "fetchQRCode",
20
+ label: 'fetchQRCode',
17
21
  });
18
22
  return JSON.parse(rawText);
19
23
  }
@@ -24,19 +28,19 @@ async function pollQRStatus(apiBaseUrl, qrcode) {
24
28
  baseUrl: apiBaseUrl,
25
29
  endpoint,
26
30
  timeoutMs: QR_LONG_POLL_TIMEOUT_MS,
27
- label: "pollQRStatus",
31
+ label: 'pollQRStatus',
28
32
  });
29
33
  return JSON.parse(rawText);
30
34
  }
31
35
  catch (err) {
32
- if (err instanceof Error && err.name === "AbortError") {
33
- return { status: "wait" };
36
+ if (err instanceof Error && err.name === 'AbortError') {
37
+ return { status: 'wait' };
34
38
  }
35
39
  logger.warn(`pollQRStatus error: ${String(err)}`);
36
- return { status: "wait" };
40
+ return { status: 'wait' };
37
41
  }
38
42
  }
39
- export async function resolveCredentials(plugin, config) {
43
+ export async function resolveCredentials(config) {
40
44
  const envToken = process.env.WEIXIN_ILINK_TOKEN?.trim() || config.botToken?.trim();
41
45
  if (envToken) {
42
46
  const stored = loadCredentials(config.name);
@@ -54,68 +58,42 @@ export async function resolveCredentials(plugin, config) {
54
58
  baseUrl: config.baseUrl ?? stored.baseUrl ?? DEFAULT_API_BASE_URL,
55
59
  };
56
60
  }
57
- return loginWithQr(plugin, config);
61
+ return loginWithQr(config);
58
62
  }
59
- export async function loginWithQr(plugin, config) {
63
+ export async function loginWithQr(config) {
60
64
  const apiBaseUrl = config.baseUrl ?? DEFAULT_API_BASE_URL;
61
65
  const qr = await fetchQRCode(apiBaseUrl, DEFAULT_ILINK_BOT_TYPE);
62
66
  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);
67
+ logger.info(`weixin-ilink QR login for ${config.name}: scan with WeChat ClawBot entry — ${qrcodeUrl.slice(0, 120)}${qrcodeUrl.length > 120 ? '…' : ''}`);
76
68
  const deadline = Date.now() + LOGIN_TIMEOUT_MS;
77
69
  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;
70
+ while (Date.now() < deadline) {
71
+ const status = await pollQRStatus(currentBaseUrl, qr.qrcode);
72
+ if (status.status === 'scaned_but_redirect' && status.redirect_host) {
73
+ currentBaseUrl = status.redirect_host.startsWith('http')
74
+ ? status.redirect_host
75
+ : `https://${status.redirect_host}`;
76
+ }
77
+ if (status.status === 'confirmed' || status.status === 'binded_redirect') {
78
+ if (!status.bot_token && status.status !== 'binded_redirect') {
79
+ throw new Error('扫码成功但未返回 bot_token');
103
80
  }
104
- if (status.status === "expired") {
105
- if (assistTask)
106
- loginAssist.cancel(assistTask.id, "expired");
107
- throw new Error("二维码已过期,请重启 bot 重新登录");
81
+ const creds = {
82
+ botToken: status.bot_token ?? loadCredentials(config.name)?.botToken ?? '',
83
+ ilinkUserId: status.ilink_user_id,
84
+ ilinkBotId: status.ilink_endpoint_id,
85
+ baseUrl: status.baseurl ?? currentBaseUrl,
86
+ };
87
+ if (!creds.botToken) {
88
+ throw new Error('binded_redirect 但本地无有效 bot_token');
108
89
  }
109
- await new Promise((r) => setTimeout(r, 500));
90
+ saveCredentials(config.name, creds);
91
+ return creds;
110
92
  }
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;
93
+ if (status.status === 'expired') {
94
+ throw new Error('二维码已过期,请重启 bot 重新登录');
95
+ }
96
+ await new Promise((r) => setTimeout(r, 500));
119
97
  }
98
+ throw new Error('微信扫码登录超时');
120
99
  }
121
- //# sourceMappingURL=login.js.map
@@ -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
@@ -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,5 @@
1
- import type { MessageSegment, SendContent } from "zhin.js";
2
- export declare function segmentMediaRef(seg: MessageSegment): string | undefined;
1
+ import type { WeixinWireSegment } from './protocol.js';
2
+ export type OutboundWireContent = string | WeixinWireSegment | ReadonlyArray<string | WeixinWireSegment>;
3
+ export declare function segmentMediaRef(seg: WeixinWireSegment): string | undefined;
3
4
  /** 将 base64 / 远程图落盘,供 sendWeixinMediaFile 上传 */
4
- export declare function materializeOutboundMedia(content: SendContent, outboundDir: string): Promise<SendContent>;
5
- //# sourceMappingURL=outbound-media.d.ts.map
5
+ export declare function materializeOutboundMedia(content: OutboundWireContent, outboundDir: string): Promise<OutboundWireContent>;
@@ -1,74 +1,77 @@
1
1
  /**
2
2
  * 微信 iLink 出站媒体:base64 / 远程 URL 须落盘后再走 CDN 上传。
3
3
  */
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"]);
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
10
  function isRemoteUrl(value) {
11
11
  return /^https?:\/\//i.test(value);
12
12
  }
13
13
  function isInlineMediaRef(value) {
14
- return value.startsWith("base64://") || /^data:[^/]+\/[^;]+;base64,/i.test(value);
14
+ return value.startsWith('base64://') || /^data:[^/]+\/[^;]+;base64,/i.test(value);
15
15
  }
16
16
  function normalizeLocalPath(value) {
17
- return value.replace(/^file:\/\//, "");
17
+ return value.replace(/^file:\/\//, '');
18
18
  }
19
19
  export function segmentMediaRef(seg) {
20
20
  const data = seg.data;
21
- const candidates = [data.file, data.path, data.url].filter((v) => typeof v === "string" && Boolean(v));
21
+ if (!data)
22
+ return undefined;
23
+ const candidates = [data.file, data.path, data.url].filter((v) => typeof v === 'string' && Boolean(v));
22
24
  return candidates[0];
23
25
  }
24
26
  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");
27
+ const url = typeof data.url === 'string' ? data.url : undefined;
28
+ const file = typeof data.file === 'string' ? data.file : undefined;
29
+ const base64 = typeof data.base64 === 'string' ? data.base64 : undefined;
30
+ if (url?.startsWith('base64://')) {
31
+ return Buffer.from(url.slice(9), 'base64');
30
32
  }
31
33
  if (url && /^data:[^/]+\/[^;]+;base64,/i.test(url)) {
32
- return Buffer.from(url.replace(/^data:[^/]+\/[^;]+;base64,/, ""), "base64");
34
+ return Buffer.from(url.replace(/^data:[^/]+\/[^;]+;base64,/, ''), 'base64');
33
35
  }
34
- if (file?.startsWith("base64://")) {
35
- return Buffer.from(file.slice(9), "base64");
36
+ if (file?.startsWith('base64://')) {
37
+ return Buffer.from(file.slice(9), 'base64');
36
38
  }
37
39
  if (base64) {
38
- const raw = base64.startsWith("base64://") ? base64.slice(9) : base64;
39
- return Buffer.from(raw, "base64");
40
+ const raw = base64.startsWith('base64://') ? base64.slice(9) : base64;
41
+ return Buffer.from(raw, 'base64');
40
42
  }
41
43
  return undefined;
42
44
  }
43
45
  function resolveExt(seg, mime) {
44
- const data = seg.data;
45
- const name = typeof data.name === "string" ? data.name : undefined;
46
+ const data = (seg.data ?? {});
47
+ const name = typeof data.name === 'string' ? data.name : undefined;
46
48
  if (name) {
47
49
  const ext = path.extname(name);
48
50
  if (ext)
49
51
  return ext;
50
52
  }
51
- const mimeHint = mime ?? (typeof data.mime === "string" ? data.mime : typeof data.mimeType === "string" ? data.mimeType : undefined);
53
+ const mimeHint = mime
54
+ ?? (typeof data.mime === 'string' ? data.mime : typeof data.mimeType === 'string' ? data.mimeType : undefined);
52
55
  if (mimeHint) {
53
- return getExtensionFromMime(mimeHint) ?? ".bin";
56
+ return getExtensionFromMime(mimeHint) ?? '.bin';
54
57
  }
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";
58
+ if (seg.type === 'image')
59
+ return '.png';
60
+ if (seg.type === 'video')
61
+ return '.mp4';
62
+ if (seg.type === 'record' || seg.type === 'audio')
63
+ return '.mp3';
64
+ return '.bin';
62
65
  }
63
66
  function withLocalFile(seg, filePath) {
64
- const data = { ...seg.data, file: filePath };
67
+ const data = { ...(seg.data ?? {}), file: filePath };
65
68
  delete data.base64;
66
69
  return { type: seg.type, data };
67
70
  }
68
71
  function writeOutboundBuffer(buffer, outboundDir, seg) {
69
72
  fs.mkdirSync(outboundDir, { recursive: true });
70
73
  const ext = resolveExt(seg);
71
- const filePath = path.join(outboundDir, `out-${seg.type}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}${ext}`);
74
+ const filePath = path.join(outboundDir, `out-${seg.type}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}${ext}`);
72
75
  fs.writeFileSync(filePath, buffer);
73
76
  return filePath;
74
77
  }
@@ -86,37 +89,36 @@ async function materializeSegment(seg, outboundDir) {
86
89
  return withLocalFile(seg, local);
87
90
  }
88
91
  }
89
- const buffer = extractBase64Buffer(seg.data);
92
+ const buffer = extractBase64Buffer((seg.data ?? {}));
90
93
  if (buffer) {
91
94
  return withLocalFile(seg, writeOutboundBuffer(buffer, outboundDir, seg));
92
95
  }
93
96
  return seg;
94
97
  }
95
98
  function asSegments(content) {
96
- if (typeof content === "string") {
97
- return [{ type: "text", data: { text: content } }];
99
+ if (typeof content === 'string') {
100
+ return [{ type: 'text', data: { text: content } }];
98
101
  }
99
102
  if (!Array.isArray(content)) {
100
- return typeof content === "object" && content !== null && "type" in content
103
+ return typeof content === 'object' && content !== null && 'type' in content
101
104
  ? [content]
102
105
  : [];
103
106
  }
104
107
  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] : [];
108
+ if (typeof item === 'string')
109
+ return [{ type: 'text', data: { text: item } }];
110
+ return typeof item === 'object' && item !== null && 'type' in item ? [item] : [];
108
111
  });
109
112
  }
110
113
  /** 将 base64 / 远程图落盘,供 sendWeixinMediaFile 上传 */
111
114
  export async function materializeOutboundMedia(content, outboundDir) {
112
115
  const segments = asSegments(content);
113
116
  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;
117
+ if (typeof content === 'string') {
118
+ return out[0]?.type === 'text' ? String(out[0].data?.text ?? content) : content;
116
119
  }
117
120
  if (!Array.isArray(content)) {
118
121
  return out[0] ?? content;
119
122
  }
120
123
  return out;
121
124
  }
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,52 @@
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 { WeixinMessage } from './ilink-types.js';
6
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
7
+ export interface WeixinIlinkAdapterConfig {
8
+ readonly name?: string;
9
+ readonly botAgent?: string;
10
+ readonly baseUrl?: string;
11
+ readonly cdnBaseUrl?: string;
12
+ readonly longPollTimeoutMs?: number;
13
+ readonly botToken?: string;
14
+ /** Transitional: legacy root `endpoints[]` with `context: weixin-ilink`. */
15
+ readonly endpoints?: ReadonlyArray<Partial<ResolvedWeixinIlinkConfig> & {
16
+ readonly context?: string;
17
+ }>;
18
+ }
19
+ export interface ResolvedWeixinIlinkConfig {
20
+ readonly context: 'weixin-ilink';
21
+ readonly name: string;
22
+ readonly botAgent?: string;
23
+ readonly baseUrl: string;
24
+ readonly cdnBaseUrl: string;
25
+ readonly longPollTimeoutMs: number;
26
+ readonly botToken?: string;
27
+ }
28
+ export type WeixinIlinkEndpointConfig = ResolvedWeixinIlinkConfig;
29
+ export interface WeixinWireSegment {
30
+ readonly type: string;
31
+ readonly data?: Record<string, unknown>;
32
+ }
33
+ export interface WeixinInboundMediaPaths {
34
+ readonly decryptedPicPath?: string;
35
+ readonly decryptedVideoPath?: string;
36
+ readonly decryptedFilePath?: string;
37
+ readonly decryptedVoicePath?: string;
38
+ }
39
+ export type WeixinMessageWithMedia = WeixinMessage & {
40
+ readonly _media?: WeixinInboundMediaPaths;
41
+ };
42
+ export declare function resolveWeixinIlinkConfig(config?: WeixinIlinkAdapterConfig): ResolvedWeixinIlinkConfig;
43
+ /** Build inbound text for MessageGateway.receive (gateway owns reply routing). */
44
+ export declare function formatInboundContent(msg: WeixinMessageWithMedia): string;
45
+ export declare function inboundMessageId(msg: WeixinMessage): string;
46
+ /**
47
+ * Wire-encode an already-rendered outbound payload into Weixin wire segments.
48
+ * Segment canonicalization is intentionally not done here.
49
+ */
50
+ export declare function formatOutboundSegments(payload: unknown): WeixinWireSegment[];
51
+ export declare function segmentLocalPath(seg: WeixinWireSegment): string | undefined;
52
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,109 @@
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 { pickCredential } from '@zhin.js/adapter';
6
+ import { bodyFromItemList, isMediaItem } from './weixin-inbound.js';
7
+ import { DEFAULT_API_BASE_URL, DEFAULT_CDN_BASE_URL } from './ilink-meta.js';
8
+ const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
9
+ export function resolveWeixinIlinkConfig(config = {}) {
10
+ const entry = config.endpoints?.find((item) => item.context === 'weixin-ilink');
11
+ const name = (typeof config.name === 'string' && config.name)
12
+ || (typeof entry?.name === 'string' && entry.name)
13
+ || process.env.WEIXIN_ILINK_BOT_NAME
14
+ || 'weixin-ilink-bot';
15
+ 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());
16
+ // No throw here: missing botToken is resolved later by resolveCredentials()
17
+ // (sidecar credentials file / QR login) during endpoint.start().
18
+ return {
19
+ context: 'weixin-ilink',
20
+ name,
21
+ botAgent: config.botAgent ?? entry?.botAgent,
22
+ baseUrl: config.baseUrl ?? entry?.baseUrl ?? DEFAULT_API_BASE_URL,
23
+ cdnBaseUrl: config.cdnBaseUrl ?? entry?.cdnBaseUrl ?? DEFAULT_CDN_BASE_URL,
24
+ longPollTimeoutMs: config.longPollTimeoutMs
25
+ ?? entry?.longPollTimeoutMs
26
+ ?? DEFAULT_LONG_POLL_TIMEOUT_MS,
27
+ botToken,
28
+ };
29
+ }
30
+ /** Build inbound text for MessageGateway.receive (gateway owns reply routing). */
31
+ export function formatInboundContent(msg) {
32
+ const parts = [];
33
+ const text = bodyFromItemList(msg.item_list);
34
+ if (text)
35
+ parts.push(text);
36
+ const media = msg._media;
37
+ if (media?.decryptedPicPath) {
38
+ parts.push(`[image: ${media.decryptedPicPath}]`);
39
+ }
40
+ else if (media?.decryptedVideoPath) {
41
+ parts.push(`[video: ${media.decryptedVideoPath}]`);
42
+ }
43
+ else if (media?.decryptedFilePath) {
44
+ parts.push(`[file: ${media.decryptedFilePath}]`);
45
+ }
46
+ else if (media?.decryptedVoicePath) {
47
+ parts.push(`[record: ${media.decryptedVoicePath}]`);
48
+ }
49
+ else if (!text && msg.item_list?.some((item) => isMediaItem(item))) {
50
+ parts.push('[媒体消息]');
51
+ }
52
+ return parts.join('\n').trim() || '';
53
+ }
54
+ export function inboundMessageId(msg) {
55
+ return String(msg.message_id ?? msg.client_id ?? msg.seq ?? Date.now());
56
+ }
57
+ /**
58
+ * Wire-encode an already-rendered outbound payload into Weixin wire segments.
59
+ * Segment canonicalization is intentionally not done here.
60
+ */
61
+ export function formatOutboundSegments(payload) {
62
+ if (typeof payload === 'string') {
63
+ return [{ type: 'text', data: { text: payload } }];
64
+ }
65
+ const items = Array.isArray(payload)
66
+ ? payload
67
+ : payload && typeof payload === 'object' && 'type' in payload
68
+ ? [payload]
69
+ : [];
70
+ if (items.length === 0) {
71
+ const text = payload == null
72
+ ? ''
73
+ : typeof payload === 'object'
74
+ ? JSON.stringify(payload)
75
+ : String(payload);
76
+ return [{ type: 'text', data: { text } }];
77
+ }
78
+ const segs = [];
79
+ for (const item of items) {
80
+ if (typeof item === 'string') {
81
+ segs.push({ type: 'text', data: { text: item } });
82
+ continue;
83
+ }
84
+ segs.push({ type: item.type, data: item.data ?? {} });
85
+ }
86
+ return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
87
+ }
88
+ export function segmentLocalPath(seg) {
89
+ const data = seg.data ?? {};
90
+ const candidates = [data.file, data.path, data.url].filter((value) => typeof value === 'string' && Boolean(value));
91
+ const ref = candidates[0];
92
+ if (!ref || ref.startsWith('base64://') || /^data:/.test(ref) || /^https?:\/\//i.test(ref)) {
93
+ return undefined;
94
+ }
95
+ return ref.replace(/^file:\/\//, '');
96
+ }
97
+ export function sleep(ms, signal) {
98
+ return new Promise((resolve) => {
99
+ if (signal?.aborted) {
100
+ resolve();
101
+ return;
102
+ }
103
+ const timer = setTimeout(resolve, ms);
104
+ signal?.addEventListener('abort', () => {
105
+ clearTimeout(timer);
106
+ resolve();
107
+ }, { once: true });
108
+ });
109
+ }
@@ -6,4 +6,3 @@
6
6
  * Callers should fall back to passing the raw SILK file when null is returned.
7
7
  */
8
8
  export declare function silkToWav(silkBuf: Buffer): Promise<Buffer | null>;
9
- //# sourceMappingURL=silk-transcode.d.ts.map
@@ -61,4 +61,3 @@ export async function silkToWav(silkBuf) {
61
61
  return null;
62
62
  }
63
63
  }
64
- //# sourceMappingURL=silk-transcode.js.map