@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
@@ -1,7 +1,9 @@
1
+ import { type IlinkClientMetadata } from "./ilink-meta.js";
1
2
  import type { GetUploadUrlReq, GetUploadUrlResp, GetUpdatesReq, GetUpdatesResp, NotifyStopResp, NotifyStartResp, SendMessageReq, SendTypingReq, GetConfigResp } from "./ilink-types.js";
2
- export { buildBaseInfo, sanitizeBotAgent } from "./ilink-meta.js";
3
+ export { IlinkClientMetadata, sanitizeBotAgent } from "./ilink-meta.js";
3
4
  export type WeixinApiOptions = {
4
5
  baseUrl: string;
6
+ metadata: IlinkClientMetadata;
5
7
  token?: string;
6
8
  timeoutMs?: number;
7
9
  /** Long-poll timeout for getUpdates (server may hold the request up to this). */
@@ -15,9 +17,11 @@ export type WeixinApiOptions = {
15
17
  */
16
18
  export declare function apiGetFetch(params: {
17
19
  baseUrl: string;
20
+ metadata: IlinkClientMetadata;
18
21
  endpoint: string;
19
22
  timeoutMs?: number;
20
23
  label: string;
24
+ abortSignal?: AbortSignal;
21
25
  }): Promise<string>;
22
26
  /**
23
27
  * Common fetch wrapper: POST JSON to a Weixin API endpoint.
@@ -28,6 +32,7 @@ export declare function apiGetFetch(params: {
28
32
  */
29
33
  export declare function apiPostFetch(params: {
30
34
  baseUrl: string;
35
+ metadata: IlinkClientMetadata;
31
36
  endpoint: string;
32
37
  body: string;
33
38
  token?: string;
@@ -41,10 +46,7 @@ export declare function apiPostFetch(params: {
41
46
  * On client-side timeout (no server response within timeoutMs), returns an empty response
42
47
  * with ret=0 so the caller can simply retry. This is normal for long-poll.
43
48
  */
44
- export declare function getUpdates(params: GetUpdatesReq & {
45
- baseUrl: string;
46
- token?: string;
47
- timeoutMs?: number;
49
+ export declare function getUpdates(params: GetUpdatesReq & WeixinApiOptions & {
48
50
  /**
49
51
  * Optional external abort signal (e.g. from the gateway when stopping the
50
52
  * channel). When this aborts, the in-flight long-poll is terminated
@@ -78,4 +80,3 @@ export declare function notifyStop(params: WeixinApiOptions): Promise<NotifyStop
78
80
  * Notify Weixin that this channel client is starting (gateway startup / channel start).
79
81
  */
80
82
  export declare function notifyStart(params: WeixinApiOptions): Promise<NotifyStartResp>;
81
- //# sourceMappingURL=ilink-api.d.ts.map
package/lib/ilink-api.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import crypto from "node:crypto";
2
2
  import { logger } from "./ilink-logger.js";
3
- import { buildBaseInfo, ILINK_APP_CLIENT_VERSION, ILINK_APP_ID, } from "./ilink-meta.js";
3
+ import { ILINK_APP_ID, } from "./ilink-meta.js";
4
4
  import { redactBody, redactUrl } from "./ilink-redact.js";
5
- export { buildBaseInfo, sanitizeBotAgent } from "./ilink-meta.js";
5
+ export { IlinkClientMetadata, sanitizeBotAgent } from "./ilink-meta.js";
6
6
  /** Default timeout for long-poll getUpdates requests. */
7
7
  const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
8
8
  /** Default timeout for regular API requests (sendMessage, getUploadUrl). */
@@ -18,10 +18,10 @@ function randomWechatUin() {
18
18
  return Buffer.from(String(uint32), "utf-8").toString("base64");
19
19
  }
20
20
  /** Build headers shared by both GET and POST requests. */
21
- function buildCommonHeaders() {
21
+ function buildCommonHeaders(metadata) {
22
22
  const headers = {
23
23
  "iLink-App-Id": ILINK_APP_ID,
24
- "iLink-App-ClientVersion": String(ILINK_APP_CLIENT_VERSION),
24
+ "iLink-App-ClientVersion": String(metadata.appClientVersion),
25
25
  };
26
26
  return headers;
27
27
  }
@@ -30,7 +30,7 @@ function buildHeaders(opts) {
30
30
  "Content-Type": "application/json",
31
31
  AuthorizationType: "ilink_bot_token",
32
32
  "X-WECHAT-UIN": randomWechatUin(),
33
- ...buildCommonHeaders(),
33
+ ...buildCommonHeaders(opts.metadata),
34
34
  };
35
35
  if (opts.token?.trim()) {
36
36
  headers.Authorization = `Bearer ${opts.token.trim()}`;
@@ -47,18 +47,19 @@ function buildHeaders(opts) {
47
47
  export async function apiGetFetch(params) {
48
48
  const base = ensureTrailingSlash(params.baseUrl);
49
49
  const url = new URL(params.endpoint, base);
50
- const hdrs = buildCommonHeaders();
50
+ const hdrs = buildCommonHeaders(params.metadata);
51
51
  logger.debug(`GET ${redactUrl(url.toString())}`);
52
52
  const timeoutMs = params.timeoutMs;
53
53
  const controller = timeoutMs != null && timeoutMs > 0 ? new AbortController() : undefined;
54
54
  const t = controller != null && timeoutMs != null
55
55
  ? setTimeout(() => controller.abort(), timeoutMs)
56
56
  : undefined;
57
+ const { signal, cleanup } = combineAbortSignals(controller, params.abortSignal);
57
58
  try {
58
59
  const res = await fetch(url.toString(), {
59
60
  method: "GET",
60
61
  headers: hdrs,
61
- ...(controller ? { signal: controller.signal } : {}),
62
+ ...(signal ? { signal } : {}),
62
63
  });
63
64
  if (t !== undefined)
64
65
  clearTimeout(t);
@@ -74,6 +75,9 @@ export async function apiGetFetch(params) {
74
75
  clearTimeout(t);
75
76
  throw err;
76
77
  }
78
+ finally {
79
+ cleanup();
80
+ }
77
81
  }
78
82
  function combineAbortSignals(internal, external) {
79
83
  if (!internal && !external)
@@ -103,7 +107,7 @@ function combineAbortSignals(internal, external) {
103
107
  export async function apiPostFetch(params) {
104
108
  const base = ensureTrailingSlash(params.baseUrl);
105
109
  const url = new URL(params.endpoint, base);
106
- const hdrs = buildHeaders({ token: params.token });
110
+ const hdrs = buildHeaders({ metadata: params.metadata, token: params.token });
107
111
  logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
108
112
  const controller = params.timeoutMs !== undefined ? new AbortController() : undefined;
109
113
  const t = controller != null && params.timeoutMs !== undefined
@@ -144,10 +148,11 @@ export async function getUpdates(params) {
144
148
  try {
145
149
  const rawText = await apiPostFetch({
146
150
  baseUrl: params.baseUrl,
151
+ metadata: params.metadata,
147
152
  endpoint: "ilink/bot/getupdates",
148
153
  body: JSON.stringify({
149
154
  get_updates_buf: params.get_updates_buf ?? "",
150
- base_info: buildBaseInfo(),
155
+ base_info: params.metadata.buildBaseInfo(),
151
156
  }),
152
157
  token: params.token,
153
158
  timeoutMs: timeout,
@@ -179,6 +184,7 @@ export async function getUpdates(params) {
179
184
  export async function getUploadUrl(params) {
180
185
  const rawText = await apiPostFetch({
181
186
  baseUrl: params.baseUrl,
187
+ metadata: params.metadata,
182
188
  endpoint: "ilink/bot/getuploadurl",
183
189
  body: JSON.stringify({
184
190
  filekey: params.filekey,
@@ -192,7 +198,7 @@ export async function getUploadUrl(params) {
192
198
  thumb_filesize: params.thumb_filesize,
193
199
  no_need_thumb: params.no_need_thumb,
194
200
  aeskey: params.aeskey,
195
- base_info: buildBaseInfo(),
201
+ base_info: params.metadata.buildBaseInfo(),
196
202
  }),
197
203
  token: params.token,
198
204
  timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
@@ -205,8 +211,9 @@ export async function getUploadUrl(params) {
205
211
  export async function sendMessage(params) {
206
212
  await apiPostFetch({
207
213
  baseUrl: params.baseUrl,
214
+ metadata: params.metadata,
208
215
  endpoint: "ilink/bot/sendmessage",
209
- body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
216
+ body: JSON.stringify({ ...params.body, base_info: params.metadata.buildBaseInfo() }),
210
217
  token: params.token,
211
218
  timeoutMs: params.timeoutMs ?? DEFAULT_API_TIMEOUT_MS,
212
219
  label: "sendMessage",
@@ -216,11 +223,12 @@ export async function sendMessage(params) {
216
223
  export async function getConfig(params) {
217
224
  const rawText = await apiPostFetch({
218
225
  baseUrl: params.baseUrl,
226
+ metadata: params.metadata,
219
227
  endpoint: "ilink/bot/getconfig",
220
228
  body: JSON.stringify({
221
229
  ilink_user_id: params.ilinkUserId,
222
230
  context_token: params.contextToken,
223
- base_info: buildBaseInfo(),
231
+ base_info: params.metadata.buildBaseInfo(),
224
232
  }),
225
233
  token: params.token,
226
234
  timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
@@ -233,8 +241,9 @@ export async function getConfig(params) {
233
241
  export async function sendTyping(params) {
234
242
  await apiPostFetch({
235
243
  baseUrl: params.baseUrl,
244
+ metadata: params.metadata,
236
245
  endpoint: "ilink/bot/sendtyping",
237
- body: JSON.stringify({ ...params.body, base_info: buildBaseInfo() }),
246
+ body: JSON.stringify({ ...params.body, base_info: params.metadata.buildBaseInfo() }),
238
247
  token: params.token,
239
248
  timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
240
249
  label: "sendTyping",
@@ -248,8 +257,9 @@ export async function sendTyping(params) {
248
257
  export async function notifyStop(params) {
249
258
  const rawText = await apiPostFetch({
250
259
  baseUrl: params.baseUrl,
260
+ metadata: params.metadata,
251
261
  endpoint: "ilink/bot/msg/notifystop",
252
- body: JSON.stringify({ base_info: buildBaseInfo() }),
262
+ body: JSON.stringify({ base_info: params.metadata.buildBaseInfo() }),
253
263
  token: params.token,
254
264
  timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
255
265
  label: "notifyStop",
@@ -262,12 +272,12 @@ export async function notifyStop(params) {
262
272
  export async function notifyStart(params) {
263
273
  const rawText = await apiPostFetch({
264
274
  baseUrl: params.baseUrl,
275
+ metadata: params.metadata,
265
276
  endpoint: "ilink/bot/msg/notifystart",
266
- body: JSON.stringify({ base_info: buildBaseInfo() }),
277
+ body: JSON.stringify({ base_info: params.metadata.buildBaseInfo() }),
267
278
  token: params.token,
268
279
  timeoutMs: params.timeoutMs ?? DEFAULT_CONFIG_TIMEOUT_MS,
269
280
  label: "notifyStart",
270
281
  });
271
282
  return JSON.parse(rawText);
272
283
  }
273
- //# sourceMappingURL=ilink-api.js.map
@@ -1,3 +1,4 @@
1
+ import { type WeixinApiOptions } from "./ilink-api.js";
1
2
  /** Subset of getConfig fields that we actually need; add new fields here as needed. */
2
3
  export interface CachedConfig {
3
4
  typingTicket: string;
@@ -7,13 +8,7 @@ export interface CachedConfig {
7
8
  * exponential-backoff retry (up to 1h) on failure.
8
9
  */
9
10
  export declare class WeixinConfigManager {
10
- private apiOpts;
11
- private log;
12
- private cache;
13
- constructor(apiOpts: {
14
- baseUrl: string;
15
- token?: string;
16
- }, log: (msg: string) => void);
11
+ #private;
12
+ constructor(apiOptions: WeixinApiOptions, log: (msg: string) => void);
17
13
  getForUser(userId: string, contextToken?: string): Promise<CachedConfig>;
18
14
  }
19
- //# sourceMappingURL=ilink-config-cache.d.ts.map
@@ -7,39 +7,38 @@ const CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
7
7
  * exponential-backoff retry (up to 1h) on failure.
8
8
  */
9
9
  export class WeixinConfigManager {
10
- apiOpts;
11
- log;
12
- cache = new Map();
13
- constructor(apiOpts, log) {
14
- this.apiOpts = apiOpts;
15
- this.log = log;
10
+ #cache = new Map();
11
+ #apiOptions;
12
+ #log;
13
+ constructor(apiOptions, log) {
14
+ this.#apiOptions = apiOptions;
15
+ this.#log = log;
16
16
  }
17
17
  async getForUser(userId, contextToken) {
18
18
  const now = Date.now();
19
- const entry = this.cache.get(userId);
19
+ const entry = this.#cache.get(userId);
20
20
  const shouldFetch = !entry || now >= entry.nextFetchAt;
21
21
  if (shouldFetch) {
22
22
  let fetchOk = false;
23
23
  try {
24
24
  const resp = await getConfig({
25
- baseUrl: this.apiOpts.baseUrl,
26
- token: this.apiOpts.token,
25
+ ...this.#apiOptions,
27
26
  ilinkUserId: userId,
28
27
  contextToken,
29
28
  });
30
29
  if (resp.ret === 0) {
31
- this.cache.set(userId, {
30
+ this.#cache.set(userId, {
32
31
  config: { typingTicket: resp.typing_ticket ?? "" },
33
32
  everSucceeded: true,
34
33
  nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
35
34
  retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS,
36
35
  });
37
- this.log(`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`);
36
+ this.#log(`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`);
38
37
  fetchOk = true;
39
38
  }
40
39
  }
41
40
  catch (err) {
42
- this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
41
+ this.#log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
43
42
  }
44
43
  if (!fetchOk) {
45
44
  const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
@@ -49,7 +48,7 @@ export class WeixinConfigManager {
49
48
  entry.retryDelayMs = nextDelay;
50
49
  }
51
50
  else {
52
- this.cache.set(userId, {
51
+ this.#cache.set(userId, {
53
52
  config: { typingTicket: "" },
54
53
  everSucceeded: false,
55
54
  nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
@@ -58,7 +57,6 @@ export class WeixinConfigManager {
58
57
  }
59
58
  }
60
59
  }
61
- return this.cache.get(userId)?.config ?? { typingTicket: "" };
60
+ return this.#cache.get(userId)?.config ?? { typingTicket: "" };
62
61
  }
63
62
  }
64
- //# sourceMappingURL=ilink-config-cache.js.map
@@ -5,6 +5,4 @@ export type IlinkLogger = {
5
5
  error: (msg: string) => void;
6
6
  withAccount: (accountId: string) => IlinkLogger;
7
7
  };
8
- export declare function setIlinkLogger(next: IlinkLogger): void;
9
8
  export declare const logger: IlinkLogger;
10
- //# sourceMappingURL=ilink-logger.d.ts.map
@@ -1,27 +1,12 @@
1
- let sink = {
2
- debug: () => { },
3
- info: () => { },
4
- warn: (msg) => console.warn(`[weixin-ilink] ${msg}`),
5
- error: (msg) => console.error(`[weixin-ilink] ${msg}`),
6
- withAccount(accountId) {
7
- const prefix = `[weixin-ilink:${accountId}]`;
8
- return {
9
- debug: (msg) => sink.debug(`${prefix} ${msg}`),
10
- info: (msg) => sink.info(`${prefix} ${msg}`),
11
- warn: (msg) => sink.warn(`${prefix} ${msg}`),
12
- error: (msg) => sink.error(`${prefix} ${msg}`),
13
- withAccount: () => this,
14
- };
15
- },
16
- };
17
- export function setIlinkLogger(next) {
18
- sink = next;
1
+ function createIlinkLogger(prefix = '[weixin-ilink]') {
2
+ return Object.freeze({
3
+ debug: () => { },
4
+ info: () => { },
5
+ warn: (msg) => console.warn(`${prefix} ${msg}`),
6
+ error: (msg) => console.error(`${prefix} ${msg}`),
7
+ withAccount(accountId) {
8
+ return createIlinkLogger(`[weixin-ilink:${accountId}]`);
9
+ },
10
+ });
19
11
  }
20
- export const logger = {
21
- debug: (msg) => sink.debug(msg),
22
- info: (msg) => sink.info(msg),
23
- warn: (msg) => sink.warn(msg),
24
- error: (msg) => sink.error(msg),
25
- withAccount: (accountId) => sink.withAccount(accountId),
26
- };
27
- //# sourceMappingURL=ilink-logger.js.map
12
+ export const logger = createIlinkLogger();
@@ -2,12 +2,17 @@ import type { BaseInfo } from "./ilink-types.js";
2
2
  export declare const ILINK_APP_ID = "bot";
3
3
  export declare const DEFAULT_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
4
4
  export declare const DEFAULT_API_BASE_URL = "https://ilinkai.weixin.qq.com";
5
- export declare function configureIlinkMeta(opts: {
6
- botAgent?: string;
7
- version?: string;
8
- }): void;
9
5
  export declare function buildClientVersion(version: string): number;
10
- export declare const ILINK_APP_CLIENT_VERSION: number;
11
- export declare function sanitizeBotAgent(raw: string | undefined): string;
12
- export declare function buildBaseInfo(): BaseInfo;
13
- //# sourceMappingURL=ilink-meta.d.ts.map
6
+ export declare function sanitizeBotAgent(raw: string | undefined, fallback?: string): string;
7
+ export interface IlinkClientMetadataOptions {
8
+ readonly botAgent?: string;
9
+ readonly version?: string;
10
+ }
11
+ /** Immutable request identity owned by one endpoint. */
12
+ export declare class IlinkClientMetadata {
13
+ readonly channelVersion: string;
14
+ readonly botAgent: string;
15
+ readonly appClientVersion: number;
16
+ constructor(options?: IlinkClientMetadataOptions);
17
+ buildBaseInfo(): BaseInfo;
18
+ }
package/lib/ilink-meta.js CHANGED
@@ -6,8 +6,6 @@ export const DEFAULT_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
6
6
  export const DEFAULT_API_BASE_URL = "https://ilinkai.weixin.qq.com";
7
7
  const DEFAULT_BOT_AGENT = "Zhin.js";
8
8
  const BOT_AGENT_MAX_LEN = 256;
9
- let channelVersion = "0.0.0";
10
- let configuredBotAgent;
11
9
  function readPackageVersion() {
12
10
  try {
13
11
  const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
@@ -18,13 +16,7 @@ function readPackageVersion() {
18
16
  return "0.0.0";
19
17
  }
20
18
  }
21
- channelVersion = readPackageVersion();
22
- export function configureIlinkMeta(opts) {
23
- if (opts.botAgent)
24
- configuredBotAgent = opts.botAgent;
25
- if (opts.version)
26
- channelVersion = opts.version;
27
- }
19
+ const PACKAGE_VERSION = readPackageVersion();
28
20
  export function buildClientVersion(version) {
29
21
  const parts = version.split(".").map((p) => parseInt(p, 10));
30
22
  const major = parts[0] ?? 0;
@@ -32,26 +24,37 @@ export function buildClientVersion(version) {
32
24
  const patch = parts[2] ?? 0;
33
25
  return ((major & 0xff) << 16) | ((minor & 0xff) << 8) | (patch & 0xff);
34
26
  }
35
- export const ILINK_APP_CLIENT_VERSION = buildClientVersion(channelVersion);
36
- export function sanitizeBotAgent(raw) {
27
+ export function sanitizeBotAgent(raw, fallback = DEFAULT_BOT_AGENT) {
37
28
  if (!raw || typeof raw !== "string")
38
- return configuredBotAgent ?? DEFAULT_BOT_AGENT;
29
+ return fallback;
39
30
  const trimmed = raw.trim();
40
31
  if (!trimmed)
41
- return configuredBotAgent ?? DEFAULT_BOT_AGENT;
32
+ return fallback;
42
33
  const productRe = /^[A-Za-z0-9_.\-]{1,32}\/[A-Za-z0-9_.+\-]{1,32}$/;
43
34
  const tokens = trimmed.split(/\s+/).filter((tok) => productRe.test(tok));
44
35
  if (tokens.length === 0)
45
- return configuredBotAgent ?? DEFAULT_BOT_AGENT;
36
+ return fallback;
46
37
  const joined = tokens.join(" ");
47
38
  if (Buffer.byteLength(joined, "utf-8") <= BOT_AGENT_MAX_LEN)
48
39
  return joined;
49
- return (configuredBotAgent ?? DEFAULT_BOT_AGENT);
40
+ return fallback;
50
41
  }
51
- export function buildBaseInfo() {
52
- return {
53
- channel_version: channelVersion,
54
- bot_agent: sanitizeBotAgent(configuredBotAgent),
55
- };
42
+ /** Immutable request identity owned by one endpoint. */
43
+ export class IlinkClientMetadata {
44
+ channelVersion;
45
+ botAgent;
46
+ appClientVersion;
47
+ constructor(options = {}) {
48
+ this.channelVersion = options.version ?? PACKAGE_VERSION;
49
+ const defaultBotAgent = `Zhin.js/${this.channelVersion}`;
50
+ this.botAgent = sanitizeBotAgent(options.botAgent, defaultBotAgent);
51
+ this.appClientVersion = buildClientVersion(this.channelVersion);
52
+ Object.freeze(this);
53
+ }
54
+ buildBaseInfo() {
55
+ return {
56
+ channel_version: this.channelVersion,
57
+ bot_agent: this.botAgent,
58
+ };
59
+ }
56
60
  }
57
- //# sourceMappingURL=ilink-meta.js.map
@@ -8,4 +8,3 @@ export declare function generateId(prefix: string): string;
8
8
  * Format: `{prefix}-{timestamp}-{8-char hex}{ext}`
9
9
  */
10
10
  export declare function tempFileName(prefix: string, ext: string): string;
11
- //# sourceMappingURL=ilink-random.d.ts.map
@@ -13,4 +13,3 @@ export function generateId(prefix) {
13
13
  export function tempFileName(prefix, ext) {
14
14
  return `${prefix}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}${ext}`;
15
15
  }
16
- //# sourceMappingURL=ilink-random.js.map
@@ -18,4 +18,3 @@ export declare function redactBody(body: string | undefined, maxLen?: number): s
18
18
  * keeping only origin + pathname.
19
19
  */
20
20
  export declare function redactUrl(rawUrl: string): string;
21
- //# sourceMappingURL=ilink-redact.d.ts.map
@@ -51,4 +51,3 @@ export function redactUrl(rawUrl) {
51
51
  return truncate(rawUrl, 80);
52
52
  }
53
53
  }
54
- //# sourceMappingURL=ilink-redact.js.map
@@ -1,16 +1,14 @@
1
- /** Error code returned by the server when the bot session has expired. */
2
1
  export declare const SESSION_EXPIRED_ERRCODE = -14;
3
- /** Pause all inbound/outbound API calls for `accountId` for one hour. */
4
- export declare function pauseSession(accountId: string): void;
5
- /** Returns `true` when the bot is still within its one-hour cooldown window. */
6
- export declare function isSessionPaused(accountId: string): boolean;
7
- /** Milliseconds remaining until the pause expires (0 when not paused). */
8
- export declare function getRemainingPauseMs(accountId: string): number;
9
- /** Throw if the session is currently paused. Call before any API request. */
10
- export declare function assertSessionActive(accountId: string): void;
11
- /**
12
- * Reset internal state — only for tests.
13
- * @internal
14
- */
15
- export declare function _resetForTest(): void;
16
- //# sourceMappingURL=ilink-session-guard.d.ts.map
2
+ export declare const SESSION_PAUSE_DURATION_MS: number;
3
+ /** Endpoint-owned cooldown state for an authenticated iLink session. */
4
+ export declare class IlinkSessionGuard {
5
+ #private;
6
+ readonly accountId: string;
7
+ private readonly now;
8
+ private readonly pauseDurationMs;
9
+ constructor(accountId: string, now?: () => number, pauseDurationMs?: number);
10
+ pause(): void;
11
+ get paused(): boolean;
12
+ get remainingMs(): number;
13
+ assertActive(): void;
14
+ }
@@ -1,49 +1,35 @@
1
- import { logger } from "./ilink-logger.js";
2
- const SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
3
- /** Error code returned by the server when the bot session has expired. */
1
+ import { logger } from './ilink-logger.js';
4
2
  export const SESSION_EXPIRED_ERRCODE = -14;
5
- const pauseUntilMap = new Map();
6
- /** Pause all inbound/outbound API calls for `accountId` for one hour. */
7
- export function pauseSession(accountId) {
8
- const until = Date.now() + SESSION_PAUSE_DURATION_MS;
9
- pauseUntilMap.set(accountId, until);
10
- logger.info(`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`);
11
- }
12
- /** Returns `true` when the bot is still within its one-hour cooldown window. */
13
- export function isSessionPaused(accountId) {
14
- const until = pauseUntilMap.get(accountId);
15
- if (until === undefined)
16
- return false;
17
- if (Date.now() >= until) {
18
- pauseUntilMap.delete(accountId);
19
- return false;
3
+ export const SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
4
+ /** Endpoint-owned cooldown state for an authenticated iLink session. */
5
+ export class IlinkSessionGuard {
6
+ accountId;
7
+ now;
8
+ pauseDurationMs;
9
+ #pauseUntil = 0;
10
+ constructor(accountId, now = Date.now, pauseDurationMs = SESSION_PAUSE_DURATION_MS) {
11
+ this.accountId = accountId;
12
+ this.now = now;
13
+ this.pauseDurationMs = pauseDurationMs;
20
14
  }
21
- return true;
22
- }
23
- /** Milliseconds remaining until the pause expires (0 when not paused). */
24
- export function getRemainingPauseMs(accountId) {
25
- const until = pauseUntilMap.get(accountId);
26
- if (until === undefined)
27
- return 0;
28
- const remaining = until - Date.now();
29
- if (remaining <= 0) {
30
- pauseUntilMap.delete(accountId);
15
+ pause() {
16
+ this.#pauseUntil = this.now() + this.pauseDurationMs;
17
+ logger.info(`session-guard: paused accountId=${this.accountId} until=${new Date(this.#pauseUntil).toISOString()} (${this.pauseDurationMs / 1000}s)`);
18
+ }
19
+ get paused() {
20
+ return this.remainingMs > 0;
21
+ }
22
+ get remainingMs() {
23
+ const remaining = this.#pauseUntil - this.now();
24
+ if (remaining > 0)
25
+ return remaining;
26
+ this.#pauseUntil = 0;
31
27
  return 0;
32
28
  }
33
- return remaining;
34
- }
35
- /** Throw if the session is currently paused. Call before any API request. */
36
- export function assertSessionActive(accountId) {
37
- if (isSessionPaused(accountId)) {
38
- const remainingMin = Math.ceil(getRemainingPauseMs(accountId) / 60_000);
39
- throw new Error(`session paused for accountId=${accountId}, ${remainingMin} min remaining (errcode ${SESSION_EXPIRED_ERRCODE})`);
29
+ assertActive() {
30
+ const remaining = this.remainingMs;
31
+ if (remaining === 0)
32
+ return;
33
+ throw new Error(`session paused for accountId=${this.accountId}, ${Math.ceil(remaining / 60_000)} min remaining (errcode ${SESSION_EXPIRED_ERRCODE})`);
40
34
  }
41
35
  }
42
- /**
43
- * Reset internal state — only for tests.
44
- * @internal
45
- */
46
- export function _resetForTest() {
47
- pauseUntilMap.clear();
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,20 +1,8 @@
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 { WeixinIlinkTypingIndicatorManager, enableTypingIndicator, type WeixinIlinkTypingIndicatorConfig, } from "./typing-indicator.js";
19
- export { registerLoginAssistRoutes } from "./login-assist-routes.js";
20
- //# sourceMappingURL=index.d.ts.map
1
+ export { formatInboundContent, formatOutboundSegments, inboundMessageId, resolveWeixinIlinkConfig, segmentLocalPath, sleep, type ResolvedWeixinIlinkConfig, type WeixinIlinkEndpointConfig, type WeixinInboundMediaPaths, type WeixinMessageWithMedia, type WeixinWireSegment, } from './protocol.js';
2
+ export type { WeixinMessage, MessageItem } from './ilink-types.js';
3
+ export { WeixinIlinkStateStore, type WeixinIlinkCredentials, } from './credentials.js';
4
+ export { IlinkClientMetadata, sanitizeBotAgent, type IlinkClientMetadataOptions, } from './ilink-meta.js';
5
+ export { WeixinContextTokenStore } from './context-store.js';
6
+ export { IlinkSessionGuard, SESSION_EXPIRED_ERRCODE, SESSION_PAUSE_DURATION_MS, } from './ilink-session-guard.js';
7
+ export { WeixinIlinkClient, weixinIlinkClient, type WeixinIlinkClientEventMap, } from './client.js';
8
+ export { WeixinIlinkEndpoint, type WeixinIlinkEndpointOptions, type WeixinIlinkGetUpdates, type WeixinIlinkNotifyStart, type WeixinIlinkNotifyStop, type WeixinIlinkSendText, } from './endpoint.js';