@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
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Convention entry: discover `adapters/weixin-ilink/index.ts` → defineAdapter.
3
+ */
4
+ import { defineAdapter } from 'zhin.js/adapter';
5
+ import { WeixinIlinkEndpoint } from '../../src/endpoint.js';
6
+ import {
7
+ resolveWeixinIlinkConfig,
8
+ type WeixinIlinkEndpointConfig,
9
+ } from '../../src/protocol.js';
10
+ import { weixinIlinkRuntimeStateToken } from '../../src/weixin-ilink-runtime-state.js';
11
+
12
+ export { WeixinIlinkEndpoint } from '../../src/endpoint.js';
13
+ export type {
14
+ WeixinIlinkEndpointOptions,
15
+ WeixinIlinkGetUpdates,
16
+ WeixinIlinkNotifyStart,
17
+ WeixinIlinkNotifyStop,
18
+ WeixinIlinkSendText,
19
+ } from '../../src/endpoint.js';
20
+
21
+ export default defineAdapter<WeixinIlinkEndpointConfig>({
22
+ capabilities: ['inbound', 'outbound'],
23
+ operations: ['typing'],
24
+ // 所有媒体(url 下载 / base64 落盘 / 本地 path)统一物化后走 CDN 上传(sendWeixinMediaFile),
25
+ // 无直发面;微信无卡片交互面,交互段降级纯文本。
26
+ segments: {
27
+ outboundMedia: ['upload'],
28
+ interactive: 'text',
29
+ },
30
+ create(context) {
31
+ const config = resolveWeixinIlinkConfig(context.config);
32
+ // 注册到插件运行时状态(weixin-ilink endpoint list 的"运行中"数据源)
33
+ context.use(weixinIlinkRuntimeStateToken).endpoints.set(config.id, {
34
+ id: config.id,
35
+ // iLink 仅长轮询一种接入方式,无 config.connection 字段
36
+ mode: 'long-poll',
37
+ });
38
+ return new WeixinIlinkEndpoint({
39
+ id: context.id,
40
+ config,
41
+ });
42
+ },
43
+ });
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { weixinIlinkEndpointCommands } from '../../definition.js';
3
+ export default weixinIlinkEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ import { weixinIlinkEndpointCommands } from '../../definition.js';
2
+
3
+ export default weixinIlinkEndpointCommands.add;
@@ -0,0 +1,17 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ /**
3
+ * `weixin-ilink endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
4
+ * commands/weixin-ilink/endpoint/ 下的 list / add / remove 直接默认导出这三项。
5
+ */
6
+ import { createEndpointCommands } from 'zhin.js/adapter';
7
+ import { defineCommand } from 'zhin.js/command';
8
+ import { weixinIlinkRuntimeStateToken } from "../../../lib/weixin-ilink-runtime-state.js";
9
+ export const weixinIlinkEndpointCommands = createEndpointCommands({
10
+ adapterKey: 'weixin-ilink',
11
+ adapterDisplayName: 'Weixin iLink',
12
+ fields: [
13
+ { key: 'botToken', required: true, env: true, description: 'iLink bot token' },
14
+ ],
15
+ running: (use) => use(weixinIlinkRuntimeStateToken).endpoints.values(),
16
+ describeEntry: (entry) => `botToken: ${String(entry.botToken)}`,
17
+ }, defineCommand);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `weixin-ilink endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
+ * commands/weixin-ilink/endpoint/ 下的 list / add / remove 直接默认导出这三项。
4
+ */
5
+ import { createEndpointCommands } from 'zhin.js/adapter';
6
+ import { defineCommand } from 'zhin.js/command';
7
+ import { weixinIlinkRuntimeStateToken } from '../../../src/weixin-ilink-runtime-state.js';
8
+
9
+ export const weixinIlinkEndpointCommands = createEndpointCommands({
10
+ adapterKey: 'weixin-ilink',
11
+ adapterDisplayName: 'Weixin iLink',
12
+ fields: [
13
+ { key: 'botToken', required: true, env: true, description: 'iLink bot token' },
14
+ ],
15
+ running: (use) => use(weixinIlinkRuntimeStateToken).endpoints.values(),
16
+ describeEntry: (entry) => `botToken: ${String(entry.botToken)}`,
17
+ }, defineCommand);
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { weixinIlinkEndpointCommands } from '../definition.js';
3
+ export default weixinIlinkEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ import { weixinIlinkEndpointCommands } from '../definition.js';
2
+
3
+ export default weixinIlinkEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { weixinIlinkEndpointCommands } from '../../definition.js';
3
+ export default weixinIlinkEndpointCommands.remove;
@@ -0,0 +1,3 @@
1
+ import { weixinIlinkEndpointCommands } from '../../definition.js';
2
+
3
+ export default weixinIlinkEndpointCommands.remove;
package/lib/aes-ecb.d.ts CHANGED
@@ -4,4 +4,3 @@ export declare function encryptAesEcb(plaintext: Buffer, key: Buffer): Buffer;
4
4
  export declare function decryptAesEcb(ciphertext: Buffer, key: Buffer): Buffer;
5
5
  /** Compute AES-128-ECB ciphertext size (PKCS7 padding to 16-byte boundary). */
6
6
  export declare function aesEcbPaddedSize(plaintextSize: number): number;
7
- //# sourceMappingURL=aes-ecb.d.ts.map
package/lib/aes-ecb.js CHANGED
@@ -16,4 +16,3 @@ export function decryptAesEcb(ciphertext, key) {
16
16
  export function aesEcbPaddedSize(plaintextSize) {
17
17
  return Math.ceil((plaintextSize + 1) / 16) * 16;
18
18
  }
19
- //# sourceMappingURL=aes-ecb.js.map
@@ -15,4 +15,3 @@ export declare function uploadBufferToCdn(params: {
15
15
  }): Promise<{
16
16
  downloadParam: string;
17
17
  }>;
18
- //# sourceMappingURL=cdn-upload.d.ts.map
package/lib/cdn-upload.js CHANGED
@@ -70,4 +70,3 @@ export async function uploadBufferToCdn(params) {
70
70
  }
71
71
  return { downloadParam };
72
72
  }
73
- //# sourceMappingURL=cdn-upload.js.map
package/lib/cdn-url.d.ts CHANGED
@@ -11,4 +11,3 @@ export declare function buildCdnUploadUrl(params: {
11
11
  uploadParam: string;
12
12
  filekey: string;
13
13
  }): string;
14
- //# sourceMappingURL=cdn-url.d.ts.map
package/lib/cdn-url.js CHANGED
@@ -11,4 +11,3 @@ export function buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl) {
11
11
  export function buildCdnUploadUrl(params) {
12
12
  return `${params.cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(params.uploadParam)}&filekey=${encodeURIComponent(params.filekey)}`;
13
13
  }
14
- //# sourceMappingURL=cdn-url.js.map
@@ -0,0 +1,35 @@
1
+ import type { WeixinContextTokenStore } from './context-store.js';
2
+ import type { WeixinIlinkCredentials } from './credentials.js';
3
+ import type { WeixinConfigManager } from './ilink-config-cache.js';
4
+ import type { IlinkClientMetadata } from './ilink-meta.js';
5
+ import type { ResolvedWeixinIlinkConfig } from './protocol.js';
6
+ import { sendMessageWeixin } from './weixin-send.js';
7
+ /** Direct iLink API client backed by the Endpoint's live authenticated session. */
8
+ export declare class WeixinIlinkClient {
9
+ #private;
10
+ readonly config: ResolvedWeixinIlinkConfig;
11
+ constructor(config: ResolvedWeixinIlinkConfig, metadata: IlinkClientMetadata, resolveCredentials: () => WeixinIlinkCredentials | null, resolveConfigManager: () => WeixinConfigManager | undefined, sendTextImpl: typeof sendMessageWeixin, contextTokens: WeixinContextTokenStore);
12
+ get credentials(): WeixinIlinkCredentials;
13
+ get authenticated(): boolean;
14
+ get apiBaseUrl(): string;
15
+ get cdnBaseUrl(): string;
16
+ contextToken(userId: string): string | undefined;
17
+ reachableUserIds(): string[];
18
+ sendText(to: string, text: string): Promise<{
19
+ messageId: string;
20
+ }>;
21
+ sendMedia(to: string, filePath: string, text?: string): Promise<{
22
+ messageId: string;
23
+ }>;
24
+ sendTyping(userId: string, status: number): Promise<boolean>;
25
+ }
26
+ export type WeixinIlinkClientEventMap = Record<string, unknown>;
27
+ declare module '@zhin.js/feature-kit' {
28
+ interface AdapterClientRegistry {
29
+ readonly 'weixin-ilink': {
30
+ readonly client: WeixinIlinkClient;
31
+ readonly events: WeixinIlinkClientEventMap;
32
+ };
33
+ }
34
+ }
35
+ export declare const weixinIlinkClient: import("@zhin.js/adapter").EndpointClientToken<WeixinIlinkClient, WeixinIlinkClientEventMap>;
package/lib/client.js ADDED
@@ -0,0 +1,89 @@
1
+ import { sendTyping } from './ilink-api.js';
2
+ import { sendWeixinMediaFile } from './weixin-send-media.js';
3
+ import { defineEndpointClient } from 'zhin.js/adapter';
4
+ /** Direct iLink API client backed by the Endpoint's live authenticated session. */
5
+ export class WeixinIlinkClient {
6
+ config;
7
+ #metadata;
8
+ #resolveCredentials;
9
+ #resolveConfigManager;
10
+ #sendTextImpl;
11
+ #contextTokens;
12
+ constructor(config, metadata, resolveCredentials, resolveConfigManager, sendTextImpl, contextTokens) {
13
+ this.config = config;
14
+ this.#metadata = metadata;
15
+ this.#resolveCredentials = resolveCredentials;
16
+ this.#resolveConfigManager = resolveConfigManager;
17
+ this.#sendTextImpl = sendTextImpl;
18
+ this.#contextTokens = contextTokens;
19
+ }
20
+ get credentials() {
21
+ const credentials = this.#resolveCredentials();
22
+ if (!credentials?.botToken)
23
+ throw new Error('weixin-ilink client not authenticated');
24
+ return credentials;
25
+ }
26
+ get authenticated() {
27
+ return Boolean(this.#resolveCredentials()?.botToken);
28
+ }
29
+ get apiBaseUrl() {
30
+ return this.#resolveCredentials()?.baseUrl ?? this.config.baseUrl;
31
+ }
32
+ get cdnBaseUrl() {
33
+ return this.config.cdnBaseUrl;
34
+ }
35
+ contextToken(userId) {
36
+ return this.#contextTokens.get(userId);
37
+ }
38
+ reachableUserIds() {
39
+ return this.#contextTokens.userIds();
40
+ }
41
+ async sendText(to, text) {
42
+ const contextToken = this.contextToken(to);
43
+ if (!contextToken)
44
+ throw new Error(`missing context_token for peer ${to}`);
45
+ return this.#sendTextImpl({
46
+ to,
47
+ text,
48
+ opts: {
49
+ baseUrl: this.apiBaseUrl,
50
+ metadata: this.#metadata,
51
+ token: this.credentials.botToken,
52
+ contextToken,
53
+ },
54
+ });
55
+ }
56
+ async sendMedia(to, filePath, text = '') {
57
+ const contextToken = this.contextToken(to);
58
+ if (!contextToken)
59
+ throw new Error(`missing context_token for peer ${to}`);
60
+ return sendWeixinMediaFile({
61
+ filePath,
62
+ to,
63
+ text,
64
+ opts: {
65
+ baseUrl: this.apiBaseUrl,
66
+ metadata: this.#metadata,
67
+ token: this.credentials.botToken,
68
+ contextToken,
69
+ },
70
+ cdnBaseUrl: this.cdnBaseUrl,
71
+ });
72
+ }
73
+ async sendTyping(userId, status) {
74
+ const manager = this.#resolveConfigManager();
75
+ if (!this.authenticated || !manager)
76
+ return false;
77
+ const cfg = await manager.getForUser(userId, this.contextToken(userId));
78
+ if (!cfg.typingTicket)
79
+ return false;
80
+ await sendTyping({
81
+ baseUrl: this.apiBaseUrl,
82
+ metadata: this.#metadata,
83
+ token: this.credentials.botToken,
84
+ body: { ilink_user_id: userId, typing_ticket: cfg.typingTicket, status },
85
+ });
86
+ return true;
87
+ }
88
+ }
89
+ export const weixinIlinkClient = defineEndpointClient('weixin-ilink');
@@ -1,2 +1,15 @@
1
- export { setContextToken, getContextToken, restoreContextTokens, clearContextTokensForAccount, bodyFromItemList, isMediaItem, } from "./weixin-inbound.js";
2
- //# sourceMappingURL=context-store.d.ts.map
1
+ import type { WeixinIlinkStateStore } from './credentials.js';
2
+ /** Endpoint-owned context-token repository with account-scoped persistence. */
3
+ export declare class WeixinContextTokenStore {
4
+ #private;
5
+ readonly state: WeixinIlinkStateStore;
6
+ constructor(state: WeixinIlinkStateStore);
7
+ get accountId(): string;
8
+ get(userId: string): string | undefined;
9
+ set(userId: string, token: string): void;
10
+ userIds(): string[];
11
+ restore(): void;
12
+ flush(): void;
13
+ clear(): void;
14
+ }
15
+ export { bodyFromItemList, isMediaItem } from './weixin-inbound.js';
@@ -1,2 +1,93 @@
1
- export { setContextToken, getContextToken, restoreContextTokens, clearContextTokensForAccount, bodyFromItemList, isMediaItem, } from "./weixin-inbound.js";
2
- //# sourceMappingURL=context-store.js.map
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { logger } from './ilink-logger.js';
4
+ const PERSIST_DEBOUNCE_MS = 500;
5
+ /** Endpoint-owned context-token repository with account-scoped persistence. */
6
+ export class WeixinContextTokenStore {
7
+ state;
8
+ #tokens = new Map();
9
+ #persistTimer;
10
+ constructor(state) {
11
+ this.state = state;
12
+ }
13
+ get accountId() {
14
+ return this.state.endpointId;
15
+ }
16
+ get(userId) {
17
+ const value = this.#tokens.get(userId);
18
+ logger.debug(`contextTokenStore.get: account=${this.accountId} user=${userId} found=${value !== undefined} storeSize=${this.#tokens.size}`);
19
+ return value;
20
+ }
21
+ set(userId, token) {
22
+ logger.debug(`contextTokenStore.set: account=${this.accountId} user=${userId}`);
23
+ this.#tokens.set(userId, token);
24
+ this.#schedulePersist();
25
+ }
26
+ userIds() {
27
+ return [...this.#tokens.keys()];
28
+ }
29
+ restore() {
30
+ const filePath = this.#filePath();
31
+ try {
32
+ if (!fs.existsSync(filePath))
33
+ return;
34
+ const tokens = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
35
+ let count = 0;
36
+ for (const [userId, token] of Object.entries(tokens)) {
37
+ if (typeof token !== 'string' || !token)
38
+ continue;
39
+ this.#tokens.set(userId, token);
40
+ count += 1;
41
+ }
42
+ logger.info(`contextTokenStore.restore: restored ${count} tokens for account=${this.accountId}`);
43
+ }
44
+ catch (error) {
45
+ logger.warn(`contextTokenStore.restore: failed to read ${filePath}: ${String(error)}`);
46
+ }
47
+ }
48
+ flush() {
49
+ if (this.#persistTimer) {
50
+ clearTimeout(this.#persistTimer);
51
+ this.#persistTimer = undefined;
52
+ }
53
+ this.#persist();
54
+ }
55
+ clear() {
56
+ if (this.#persistTimer) {
57
+ clearTimeout(this.#persistTimer);
58
+ this.#persistTimer = undefined;
59
+ }
60
+ this.#tokens.clear();
61
+ const filePath = this.#filePath();
62
+ try {
63
+ if (fs.existsSync(filePath))
64
+ fs.unlinkSync(filePath);
65
+ }
66
+ catch (error) {
67
+ logger.warn(`contextTokenStore.clear: failed to remove ${filePath}: ${String(error)}`);
68
+ }
69
+ }
70
+ #filePath() {
71
+ return this.state.contextTokensPath();
72
+ }
73
+ #schedulePersist() {
74
+ if (this.#persistTimer)
75
+ clearTimeout(this.#persistTimer);
76
+ this.#persistTimer = setTimeout(() => {
77
+ this.#persistTimer = undefined;
78
+ this.#persist();
79
+ }, PERSIST_DEBOUNCE_MS);
80
+ this.#persistTimer.unref?.();
81
+ }
82
+ #persist() {
83
+ const filePath = this.#filePath();
84
+ try {
85
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
86
+ fs.writeFileSync(filePath, JSON.stringify(Object.fromEntries(this.#tokens)), 'utf-8');
87
+ }
88
+ catch (error) {
89
+ logger.warn(`contextTokenStore.persist: failed to write ${filePath}: ${String(error)}`);
90
+ }
91
+ }
92
+ }
93
+ export { bodyFromItemList, isMediaItem } from './weixin-inbound.js';
@@ -5,11 +5,18 @@ export interface WeixinIlinkCredentials {
5
5
  baseUrl?: string;
6
6
  updatedAt?: string;
7
7
  }
8
- export declare function resolveStateDir(): string;
9
- export declare function credentialsPath(endpointName: string): string;
10
- export declare function loadCredentials(endpointName: string): WeixinIlinkCredentials | null;
11
- export declare function saveCredentials(endpointName: string, creds: WeixinIlinkCredentials): void;
12
- export declare function syncBufPath(endpointName: string): string;
13
- export declare function loadSyncBuf(endpointName: string): string;
14
- export declare function saveSyncBuf(endpointName: string, buf: string): void;
15
- //# sourceMappingURL=credentials.d.ts.map
8
+ /** Endpoint-owned persistence for credentials, cursors, context tokens, and media. */
9
+ export declare class WeixinIlinkStateStore {
10
+ readonly endpointId: string;
11
+ readonly rootDir: string;
12
+ constructor(endpointId: string, rootDir: string);
13
+ endpointDirectory(): string;
14
+ credentialsPath(): string;
15
+ loadCredentials(): WeixinIlinkCredentials | null;
16
+ saveCredentials(credentials: WeixinIlinkCredentials): void;
17
+ syncBufPath(): string;
18
+ loadSyncBuf(): string;
19
+ saveSyncBuf(value: string): void;
20
+ contextTokensPath(): string;
21
+ mediaDirectory(subdir: string): string;
22
+ }
@@ -1,49 +1,65 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- const DEFAULT_DATA_DIR = "./data/weixin-ilink";
4
- export function resolveStateDir() {
5
- return process.env.ZHIN_DATA_DIR
6
- ? path.join(process.env.ZHIN_DATA_DIR, "weixin-ilink")
7
- : DEFAULT_DATA_DIR;
8
- }
9
- export function credentialsPath(endpointName) {
10
- return path.join(resolveStateDir(), `${endpointName}.json`);
11
- }
12
- export function loadCredentials(endpointName) {
13
- const file = credentialsPath(endpointName);
14
- try {
15
- if (!fs.existsSync(file))
16
- return null;
17
- const raw = fs.readFileSync(file, "utf-8");
18
- const data = JSON.parse(raw);
19
- if (!data.botToken?.trim())
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /** Endpoint-owned persistence for credentials, cursors, context tokens, and media. */
4
+ export class WeixinIlinkStateStore {
5
+ endpointId;
6
+ rootDir;
7
+ constructor(endpointId, rootDir) {
8
+ if (!/^[A-Za-z0-9_.-]+$/u.test(endpointId)) {
9
+ throw new TypeError('Weixin iLink state endpoint id is invalid');
10
+ }
11
+ if (!rootDir.trim())
12
+ throw new TypeError('Weixin iLink state root is required');
13
+ this.endpointId = endpointId;
14
+ this.rootDir = rootDir;
15
+ }
16
+ endpointDirectory() {
17
+ return path.join(this.rootDir, this.endpointId);
18
+ }
19
+ credentialsPath() {
20
+ return path.join(this.endpointDirectory(), 'credentials.json');
21
+ }
22
+ loadCredentials() {
23
+ const file = this.credentialsPath();
24
+ try {
25
+ if (!fs.existsSync(file))
26
+ return null;
27
+ const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
28
+ return data.botToken?.trim() ? data : null;
29
+ }
30
+ catch {
20
31
  return null;
21
- return data;
32
+ }
22
33
  }
23
- catch {
24
- return null;
34
+ saveCredentials(credentials) {
35
+ const file = this.credentialsPath();
36
+ fs.mkdirSync(path.dirname(file), { recursive: true });
37
+ fs.writeFileSync(file, JSON.stringify({ ...credentials, updatedAt: new Date().toISOString() }, null, 2), 'utf-8');
25
38
  }
26
- }
27
- export function saveCredentials(endpointName, creds) {
28
- const file = credentialsPath(endpointName);
29
- fs.mkdirSync(path.dirname(file), { recursive: true });
30
- fs.writeFileSync(file, JSON.stringify({ ...creds, updatedAt: new Date().toISOString() }, null, 2), "utf-8");
31
- }
32
- export function syncBufPath(endpointName) {
33
- return path.join(resolveStateDir(), `${endpointName}.sync-buf`);
34
- }
35
- export function loadSyncBuf(endpointName) {
36
- const file = syncBufPath(endpointName);
37
- try {
38
- return fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "";
39
+ syncBufPath() {
40
+ return path.join(this.endpointDirectory(), 'sync-buf');
39
41
  }
40
- catch {
41
- return "";
42
+ loadSyncBuf() {
43
+ const file = this.syncBufPath();
44
+ try {
45
+ return fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
46
+ }
47
+ catch {
48
+ return '';
49
+ }
50
+ }
51
+ saveSyncBuf(value) {
52
+ const file = this.syncBufPath();
53
+ fs.mkdirSync(path.dirname(file), { recursive: true });
54
+ fs.writeFileSync(file, value, 'utf-8');
55
+ }
56
+ contextTokensPath() {
57
+ return path.join(this.endpointDirectory(), 'context-tokens.json');
58
+ }
59
+ mediaDirectory(subdir) {
60
+ if (subdir !== 'inbound' && subdir !== 'outbound') {
61
+ throw new TypeError(`Unsupported Weixin iLink media directory: ${subdir}`);
62
+ }
63
+ return path.join(this.endpointDirectory(), 'media', subdir);
42
64
  }
43
65
  }
44
- export function saveSyncBuf(endpointName, buf) {
45
- const file = syncBufPath(endpointName);
46
- fs.mkdirSync(path.dirname(file), { recursive: true });
47
- fs.writeFileSync(file, buf, "utf-8");
48
- }
49
- //# sourceMappingURL=credentials.js.map
package/lib/endpoint.d.ts CHANGED
@@ -1,37 +1,42 @@
1
- import { Endpoint, Message, type SendOptions } from "zhin.js";
2
- import type { WeixinMessage } from "./ilink-types.js";
3
- import type { WeixinInboundMediaOpts } from "./weixin-inbound.js";
4
- import type { WeixinIlinkEndpointConfig } from "./types.js";
5
- import type { WeixinIlinkAdapter } from "./adapter.js";
6
- import { WeixinIlinkTypingIndicatorManager } from "./typing-indicator.js";
7
- export type WeixinMessageWithMedia = WeixinMessage & {
8
- _media?: WeixinInboundMediaOpts;
9
- };
10
- export declare class WeixinIlinkEndpoint implements Endpoint<WeixinIlinkEndpointConfig, WeixinMessage> {
11
- adapter: WeixinIlinkAdapter;
12
- $config: WeixinIlinkEndpointConfig;
13
- $connected: boolean;
14
- $typingIndicator?: WeixinIlinkTypingIndicatorManager;
15
- private creds;
16
- private pollAbort?;
17
- private pollPromise?;
18
- private configManager?;
19
- get $id(): string;
1
+ import { Endpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
2
+ import type { CapabilityId } from 'zhin.js';
3
+ import { getUpdates, notifyStart, notifyStop } from './ilink-api.js';
4
+ import { IlinkClientMetadata } from './ilink-meta.js';
5
+ import { type WeixinIlinkCredentials, WeixinIlinkStateStore } from './credentials.js';
6
+ import { WeixinContextTokenStore } from './context-store.js';
7
+ import { IlinkSessionGuard } from './ilink-session-guard.js';
8
+ import { sendMessageWeixin } from './weixin-send.js';
9
+ import { type ResolvedWeixinIlinkConfig, type WeixinMessageWithMedia } from './protocol.js';
10
+ import { WeixinIlinkClient } from './client.js';
11
+ export type WeixinIlinkSendText = typeof sendMessageWeixin;
12
+ export type WeixinIlinkNotifyStart = typeof notifyStart;
13
+ export type WeixinIlinkNotifyStop = typeof notifyStop;
14
+ export type WeixinIlinkGetUpdates = typeof getUpdates;
15
+ export interface WeixinIlinkEndpointOptions {
16
+ readonly id: CapabilityId;
17
+ readonly config: ResolvedWeixinIlinkConfig;
18
+ readonly resolveCredentials?: (config: ResolvedWeixinIlinkConfig, metadata: IlinkClientMetadata, state: WeixinIlinkStateStore, signal?: AbortSignal) => Promise<WeixinIlinkCredentials>;
19
+ /** Test / internal: override network side effects. */
20
+ readonly notifyStart?: WeixinIlinkNotifyStart;
21
+ readonly notifyStop?: WeixinIlinkNotifyStop;
22
+ readonly getUpdates?: WeixinIlinkGetUpdates;
23
+ readonly sendText?: WeixinIlinkSendText;
24
+ readonly state?: WeixinIlinkStateStore;
25
+ readonly contextTokens?: WeixinContextTokenStore;
26
+ readonly sessionGuard?: IlinkSessionGuard;
27
+ }
28
+ export declare class WeixinIlinkEndpoint extends Endpoint<WeixinIlinkClient> {
29
+ #private;
30
+ readonly client: WeixinIlinkClient;
31
+ readonly management: EndpointManagement;
32
+ readonly control: EndpointControl;
33
+ constructor(options: WeixinIlinkEndpointOptions);
20
34
  get hasCredentials(): boolean;
21
- get pluginLogger(): import("zhin.js").Logger;
22
- constructor(adapter: WeixinIlinkAdapter, $config: WeixinIlinkEndpointConfig);
23
- apiBaseUrl(): string;
24
- cdnBaseUrl(): string;
25
- $connect(): Promise<void>;
26
- $disconnect(): Promise<void>;
27
- sendTypingToUser(userId: string, status: number): Promise<void>;
28
- $recallMessage(_id: string): Promise<void>;
29
- $formatMessage(msg: WeixinMessageWithMedia): Message<WeixinMessage>;
30
- private buildInboundContent;
31
- $sendMessage(options: SendOptions): Promise<string>;
32
- private pollLoop;
33
- private handleInboundMessage;
34
- private downloadInboundMedia;
35
- private saveInboundMedia;
35
+ start(): Promise<void>;
36
+ open(): void;
37
+ close(): void;
38
+ stop(): Promise<void>;
39
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
40
+ /** Test / internal: admit a parsed message when the endpoint is open. */
41
+ admit(msg: WeixinMessageWithMedia): void;
36
42
  }
37
- //# sourceMappingURL=endpoint.d.ts.map