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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/CHANGELOG.md +512 -0
  2. package/README.md +24 -42
  3. package/adapters/weixin-ilink/index.js +32 -0
  4. package/adapters/weixin-ilink/index.ts +43 -0
  5. package/commands/weixin-ilink/endpoint/add/[id]/index.js +3 -0
  6. package/commands/weixin-ilink/endpoint/add/[id]/index.ts +3 -0
  7. package/commands/weixin-ilink/endpoint/definition.js +17 -0
  8. package/commands/weixin-ilink/endpoint/definition.ts +17 -0
  9. package/commands/weixin-ilink/endpoint/list/index.js +3 -0
  10. package/commands/weixin-ilink/endpoint/list/index.ts +3 -0
  11. package/commands/weixin-ilink/endpoint/remove/[id]/index.js +3 -0
  12. package/commands/weixin-ilink/endpoint/remove/[id]/index.ts +3 -0
  13. package/lib/aes-ecb.d.ts +0 -1
  14. package/lib/aes-ecb.js +0 -1
  15. package/lib/cdn-upload.d.ts +0 -1
  16. package/lib/cdn-upload.js +0 -1
  17. package/lib/cdn-url.d.ts +0 -1
  18. package/lib/cdn-url.js +0 -1
  19. package/lib/client.d.ts +35 -0
  20. package/lib/client.js +89 -0
  21. package/lib/context-store.d.ts +15 -2
  22. package/lib/context-store.js +93 -2
  23. package/lib/credentials.d.ts +15 -8
  24. package/lib/credentials.js +59 -43
  25. package/lib/endpoint.d.ts +40 -35
  26. package/lib/endpoint.js +311 -262
  27. package/lib/ilink-api.d.ts +7 -6
  28. package/lib/ilink-api.js +26 -16
  29. package/lib/ilink-config-cache.d.ts +3 -8
  30. package/lib/ilink-config-cache.js +13 -15
  31. package/lib/ilink-logger.d.ts +0 -2
  32. package/lib/ilink-logger.js +11 -26
  33. package/lib/ilink-meta.d.ts +13 -8
  34. package/lib/ilink-meta.js +24 -21
  35. package/lib/ilink-random.d.ts +0 -1
  36. package/lib/ilink-random.js +0 -1
  37. package/lib/ilink-redact.d.ts +0 -1
  38. package/lib/ilink-redact.js +0 -1
  39. package/lib/ilink-session-guard.d.ts +13 -15
  40. package/lib/ilink-session-guard.js +29 -43
  41. package/lib/ilink-types.d.ts +0 -1
  42. package/lib/ilink-types.js +0 -1
  43. package/lib/index.d.ts +8 -20
  44. package/lib/index.js +7 -38
  45. package/lib/login.d.ts +5 -6
  46. package/lib/login.js +85 -77
  47. package/lib/media-download.d.ts +1 -2
  48. package/lib/media-download.js +1 -2
  49. package/lib/mime.d.ts +0 -1
  50. package/lib/mime.js +0 -1
  51. package/lib/outbound-media.d.ts +4 -5
  52. package/lib/outbound-media.js +85 -86
  53. package/lib/pic-decrypt.d.ts +0 -1
  54. package/lib/pic-decrypt.js +0 -1
  55. package/lib/protocol.d.ts +60 -0
  56. package/lib/protocol.js +127 -0
  57. package/lib/silk-transcode.d.ts +0 -1
  58. package/lib/silk-transcode.js +0 -1
  59. package/lib/types.d.ts +2 -26
  60. package/lib/types.js +0 -1
  61. package/lib/upload.d.ts +1 -2
  62. package/lib/upload.js +1 -2
  63. package/lib/weixin-ilink-runtime-state.d.ts +1 -0
  64. package/lib/weixin-ilink-runtime-state.js +6 -0
  65. package/lib/weixin-inbound.d.ts +4 -43
  66. package/lib/weixin-inbound.js +22 -162
  67. package/lib/weixin-send-media.d.ts +0 -1
  68. package/lib/weixin-send-media.js +5 -2
  69. package/lib/weixin-send.d.ts +1 -2
  70. package/lib/weixin-send.js +4 -3
  71. package/package.json +48 -39
  72. package/plugin.js +14 -0
  73. package/schema.json +116 -0
  74. package/src/client.ts +117 -0
  75. package/src/context-store.ts +101 -8
  76. package/src/credentials.ts +68 -45
  77. package/src/endpoint.ts +384 -315
  78. package/src/ilink-api.ts +31 -21
  79. package/src/ilink-config-cache.ts +17 -13
  80. package/src/ilink-logger.ts +11 -26
  81. package/src/ilink-meta.ts +31 -21
  82. package/src/ilink-session-guard.ts +29 -46
  83. package/src/index.ts +41 -56
  84. package/src/login.ts +112 -85
  85. package/src/media-download.ts +1 -2
  86. package/src/outbound-media.ts +97 -97
  87. package/src/protocol.ts +180 -0
  88. package/src/silk-wasm.d.ts +6 -2
  89. package/src/types.ts +5 -25
  90. package/src/upload.test.ts +11 -8
  91. package/src/upload.ts +2 -2
  92. package/src/weixin-ilink-runtime-state.ts +7 -0
  93. package/src/weixin-inbound.ts +25 -187
  94. package/src/weixin-send-media.ts +5 -1
  95. package/src/weixin-send.ts +6 -4
  96. package/client/LoginAssistPanel.tsx +0 -200
  97. package/client/WeixinIlinkManagement.tsx +0 -139
  98. package/client/index.tsx +0 -11
  99. package/client/tsconfig.json +0 -13
  100. package/client/utils/api.ts +0 -30
  101. package/dist/index.js +0 -29
  102. package/lib/adapter.d.ts +0 -12
  103. package/lib/adapter.d.ts.map +0 -1
  104. package/lib/adapter.js +0 -15
  105. package/lib/adapter.js.map +0 -1
  106. package/lib/aes-ecb.d.ts.map +0 -1
  107. package/lib/aes-ecb.js.map +0 -1
  108. package/lib/cdn-upload.d.ts.map +0 -1
  109. package/lib/cdn-upload.js.map +0 -1
  110. package/lib/cdn-upload.test.d.ts +0 -2
  111. package/lib/cdn-upload.test.d.ts.map +0 -1
  112. package/lib/cdn-upload.test.js +0 -150
  113. package/lib/cdn-upload.test.js.map +0 -1
  114. package/lib/cdn-url.d.ts.map +0 -1
  115. package/lib/cdn-url.js.map +0 -1
  116. package/lib/context-store.d.ts.map +0 -1
  117. package/lib/context-store.js.map +0 -1
  118. package/lib/credentials.d.ts.map +0 -1
  119. package/lib/credentials.js.map +0 -1
  120. package/lib/endpoint.d.ts.map +0 -1
  121. package/lib/endpoint.js.map +0 -1
  122. package/lib/ilink-api.d.ts.map +0 -1
  123. package/lib/ilink-api.js.map +0 -1
  124. package/lib/ilink-config-cache.d.ts.map +0 -1
  125. package/lib/ilink-config-cache.js.map +0 -1
  126. package/lib/ilink-logger.d.ts.map +0 -1
  127. package/lib/ilink-logger.js.map +0 -1
  128. package/lib/ilink-meta.d.ts.map +0 -1
  129. package/lib/ilink-meta.js.map +0 -1
  130. package/lib/ilink-random.d.ts.map +0 -1
  131. package/lib/ilink-random.js.map +0 -1
  132. package/lib/ilink-redact.d.ts.map +0 -1
  133. package/lib/ilink-redact.js.map +0 -1
  134. package/lib/ilink-session-guard.d.ts.map +0 -1
  135. package/lib/ilink-session-guard.js.map +0 -1
  136. package/lib/ilink-types.d.ts.map +0 -1
  137. package/lib/ilink-types.js.map +0 -1
  138. package/lib/index.d.ts.map +0 -1
  139. package/lib/index.js.map +0 -1
  140. package/lib/login-assist-routes.d.ts +0 -7
  141. package/lib/login-assist-routes.d.ts.map +0 -1
  142. package/lib/login-assist-routes.js +0 -29
  143. package/lib/login-assist-routes.js.map +0 -1
  144. package/lib/login.d.ts.map +0 -1
  145. package/lib/login.js.map +0 -1
  146. package/lib/media-download.d.ts.map +0 -1
  147. package/lib/media-download.js.map +0 -1
  148. package/lib/mime.d.ts.map +0 -1
  149. package/lib/mime.js.map +0 -1
  150. package/lib/mime.test.d.ts +0 -2
  151. package/lib/mime.test.d.ts.map +0 -1
  152. package/lib/mime.test.js +0 -62
  153. package/lib/mime.test.js.map +0 -1
  154. package/lib/outbound-media.d.ts.map +0 -1
  155. package/lib/outbound-media.js.map +0 -1
  156. package/lib/pic-decrypt.d.ts.map +0 -1
  157. package/lib/pic-decrypt.js.map +0 -1
  158. package/lib/routes.d.ts +0 -8
  159. package/lib/routes.d.ts.map +0 -1
  160. package/lib/routes.js +0 -29
  161. package/lib/routes.js.map +0 -1
  162. package/lib/silk-transcode.d.ts.map +0 -1
  163. package/lib/silk-transcode.js.map +0 -1
  164. package/lib/silk-transcode.test.d.ts +0 -2
  165. package/lib/silk-transcode.test.d.ts.map +0 -1
  166. package/lib/silk-transcode.test.js +0 -53
  167. package/lib/silk-transcode.test.js.map +0 -1
  168. package/lib/types.d.ts.map +0 -1
  169. package/lib/types.js.map +0 -1
  170. package/lib/typing-indicator.d.ts +0 -32
  171. package/lib/typing-indicator.d.ts.map +0 -1
  172. package/lib/typing-indicator.js +0 -95
  173. package/lib/typing-indicator.js.map +0 -1
  174. package/lib/upload.d.ts.map +0 -1
  175. package/lib/upload.js.map +0 -1
  176. package/lib/upload.test.d.ts +0 -2
  177. package/lib/upload.test.d.ts.map +0 -1
  178. package/lib/upload.test.js +0 -250
  179. package/lib/upload.test.js.map +0 -1
  180. package/lib/weixin-inbound.d.ts.map +0 -1
  181. package/lib/weixin-inbound.js.map +0 -1
  182. package/lib/weixin-send-media.d.ts.map +0 -1
  183. package/lib/weixin-send-media.js.map +0 -1
  184. package/lib/weixin-send.d.ts.map +0 -1
  185. package/lib/weixin-send.js.map +0 -1
  186. package/src/adapter.ts +0 -18
  187. package/src/login-assist-routes.ts +0 -36
  188. package/src/routes.ts +0 -39
  189. package/src/typing-indicator.ts +0 -139
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-weixin-ilink",
3
- "version": "1.0.1",
3
+ "version": "1.1.2",
4
4
  "type": "module",
5
- "description": "Zhin.js adapter for WeChat personal account via Tencent iLink Endpoint API (ClawBot)",
5
+ "description": "Zhin.js Weixin iLink (ClawBot) adapter for Plugin Runtime (long-poll)",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
8
8
  "keywords": [
@@ -27,11 +27,12 @@
27
27
  "directory": "plugins/adapters/weixin-ilink"
28
28
  },
29
29
  "files": [
30
+ "adapters",
31
+ "commands",
32
+ "plugin.js",
33
+ "schema.json",
30
34
  "src",
31
35
  "lib",
32
- "client",
33
- "dist",
34
- "skills",
35
36
  "README.md",
36
37
  "CHANGELOG.md"
37
38
  ],
@@ -46,50 +47,58 @@
46
47
  "import": "./lib/index.js"
47
48
  }
48
49
  },
49
- "dependencies": {},
50
- "devDependencies": {
51
- "@radix-ui/themes": "^3.2.1",
52
- "@types/react": "^19.2.15",
53
- "@types/react-dom": "^19.2.3",
54
- "lucide-react": "^1.17.0",
55
- "radix-ui": "^1.5.0",
56
- "typescript": "^6.0.3",
57
- "@zhin.js/agent": "0.2.1",
58
- "@zhin.js/cli": "1.0.89",
59
- "@zhin.js/client": "2.0.3",
60
- "@zhin.js/contract": "1.0.1",
61
- "@zhin.js/core": "1.2.1",
62
- "@zhin.js/host-api": "1.0.1",
63
- "@zhin.js/host-router": "1.0.1",
64
- "zhin.js": "2.0.1"
50
+ "dependencies": {
51
+ "@zhin.js/adapter": "1.1.14",
52
+ "@zhin.js/core": "1.1.37",
53
+ "@zhin.js/feature-kit": "1.1.1",
54
+ "@zhin.js/im-contract": "1.1.1",
55
+ "@zhin.js/logger": "1.1.1"
65
56
  },
66
57
  "peerDependencies": {
67
- "@zhin.js/agent": "0.2.1",
68
- "@zhin.js/contract": "1.0.1",
69
- "@zhin.js/client": "2.0.3",
70
- "@zhin.js/core": "1.2.1",
71
- "@zhin.js/host-router": "1.0.1",
72
- "@zhin.js/host-api": "1.0.1",
73
- "zhin.js": "2.0.1"
58
+ "@zhin.js/adapter": "^1.1.14",
59
+ "@zhin.js/command": "^1.1.1",
60
+ "@zhin.js/core": "^1.1.37",
61
+ "zhin.js": "^1.1.2"
74
62
  },
75
63
  "peerDependenciesMeta": {
76
- "@zhin.js/agent": {
77
- "optional": true
78
- },
79
- "@zhin.js/client": {
80
- "optional": true
81
- },
82
- "@zhin.js/host-api": {
64
+ "@zhin.js/command": {
83
65
  "optional": true
84
66
  },
85
- "@zhin.js/host-router": {
67
+ "zhin.js": {
86
68
  "optional": true
87
69
  }
88
70
  },
71
+ "devDependencies": {
72
+ "@types/node": "^26.1.2",
73
+ "typescript": "^6.0.3",
74
+ "vitest": "^4.1.10",
75
+ "zhin.js": "1.1.2"
76
+ },
89
77
  "ilink_appid": "bot",
78
+ "engines": {
79
+ "node": "^20.19.0 || >=22.12.0"
80
+ },
81
+ "zhin": {
82
+ "protocol": 1,
83
+ "type": "plugin",
84
+ "entry": "./plugin.js",
85
+ "engine": "^1.0.0",
86
+ "runtime": "trusted",
87
+ "features": [
88
+ {
89
+ "package": "@zhin.js/adapter",
90
+ "api": "^1.0.0"
91
+ },
92
+ {
93
+ "package": "@zhin.js/command",
94
+ "api": "^1.0.0"
95
+ }
96
+ ],
97
+ "plugins": []
98
+ },
90
99
  "scripts": {
91
- "build": "zhin build",
92
- "build:node": "tsc",
93
- "clean": "rimraf lib dist"
100
+ "build": "tsc",
101
+ "clean": "rimraf lib",
102
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/weixin-ilink/tests"
94
103
  }
95
104
  }
package/plugin.js ADDED
@@ -0,0 +1,14 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { createEndpointRuntimeState } from 'zhin.js/adapter';
3
+ import { definePlugin } from 'zhin.js';
4
+ import { weixinIlinkRuntimeStateToken } from "./lib/weixin-ilink-runtime-state.js";
5
+ export default definePlugin({
6
+ name: 'weixin-ilink',
7
+ metadata: {
8
+ displayName: 'Weixin iLink Adapter',
9
+ },
10
+ setup(context) {
11
+ // 运行中 endpoint 注册表(weixin-ilink endpoint list 的"运行中"数据源)
12
+ context.resources.provide(weixinIlinkRuntimeStateToken, createEndpointRuntimeState());
13
+ },
14
+ });
package/schema.json ADDED
@@ -0,0 +1,116 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "botAgent": {
7
+ "type": "string"
8
+ },
9
+ "baseUrl": {
10
+ "type": "string",
11
+ "default": "https://ilinkai.weixin.qq.com"
12
+ },
13
+ "cdnBaseUrl": {
14
+ "type": "string",
15
+ "default": "https://novac2c.cdn.weixin.qq.com/c2c"
16
+ },
17
+ "longPollTimeoutMs": {
18
+ "type": "number",
19
+ "default": 35000
20
+ },
21
+ "dataDir": {
22
+ "type": "string",
23
+ "minLength": 1,
24
+ "default": "data/weixin-ilink",
25
+ "description": "Endpoint state root for credentials, cursors, context tokens, and media"
26
+ },
27
+ "master": {
28
+ "type": [
29
+ "string",
30
+ "number"
31
+ ],
32
+ "description": "框架 master(Weixin user id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
33
+ },
34
+ "trusted": {
35
+ "type": "array",
36
+ "items": {
37
+ "type": [
38
+ "string",
39
+ "number"
40
+ ],
41
+ "description": "Trusted Weixin user id"
42
+ },
43
+ "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
44
+ },
45
+ "endpoints": {
46
+ "type": "array",
47
+ "minItems": 1,
48
+ "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
49
+ "items": {
50
+ "type": "object",
51
+ "additionalProperties": false,
52
+ "properties": {
53
+ "master": {
54
+ "type": [
55
+ "string",
56
+ "number"
57
+ ],
58
+ "description": "本 endpoint 的框架 master(Weixin user id);覆盖顶层 master"
59
+ },
60
+ "trusted": {
61
+ "type": "array",
62
+ "items": {
63
+ "type": [
64
+ "string",
65
+ "number"
66
+ ],
67
+ "description": "Trusted Weixin user id"
68
+ },
69
+ "description": "本 endpoint 的 trusted 列表"
70
+ },
71
+ "botToken": {
72
+ "type": "string",
73
+ "description": "iLink bot token; omit to load endpoint state or start QR login"
74
+ },
75
+ "id": {
76
+ "type": "string",
77
+ "pattern": "^[A-Za-z0-9_.-]+$",
78
+ "description": "Weixin iLink bot name"
79
+ },
80
+ "botAgent": {
81
+ "type": "string"
82
+ },
83
+ "baseUrl": {
84
+ "type": "string"
85
+ },
86
+ "cdnBaseUrl": {
87
+ "type": "string"
88
+ },
89
+ "longPollTimeoutMs": {
90
+ "type": "number"
91
+ },
92
+ "dataDir": {
93
+ "type": "string",
94
+ "minLength": 1,
95
+ "description": "Endpoint state root"
96
+ },
97
+ "commandPrefix": {
98
+ "type": "string",
99
+ "description": "本 endpoint 的命令前缀"
100
+ }
101
+ },
102
+ "required": [
103
+ "id"
104
+ ]
105
+ }
106
+ },
107
+ "commandPrefix": {
108
+ "type": "string",
109
+ "default": "",
110
+ "description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
111
+ }
112
+ },
113
+ "required": [
114
+ "endpoints"
115
+ ]
116
+ }
package/src/client.ts ADDED
@@ -0,0 +1,117 @@
1
+ import type { WeixinContextTokenStore } from './context-store.js';
2
+ import type { WeixinIlinkCredentials } from './credentials.js';
3
+ import { sendTyping } from './ilink-api.js';
4
+ import type { WeixinConfigManager } from './ilink-config-cache.js';
5
+ import type { IlinkClientMetadata } from './ilink-meta.js';
6
+ import type { ResolvedWeixinIlinkConfig } from './protocol.js';
7
+ import { sendMessageWeixin } from './weixin-send.js';
8
+ import { sendWeixinMediaFile } from './weixin-send-media.js';
9
+ import { defineEndpointClient } from 'zhin.js/adapter';
10
+
11
+ /** Direct iLink API client backed by the Endpoint's live authenticated session. */
12
+ export class WeixinIlinkClient {
13
+ readonly config: ResolvedWeixinIlinkConfig;
14
+ readonly #metadata: IlinkClientMetadata;
15
+ readonly #resolveCredentials: () => WeixinIlinkCredentials | null;
16
+ readonly #resolveConfigManager: () => WeixinConfigManager | undefined;
17
+ readonly #sendTextImpl: typeof sendMessageWeixin;
18
+ readonly #contextTokens: WeixinContextTokenStore;
19
+
20
+ constructor(
21
+ config: ResolvedWeixinIlinkConfig,
22
+ metadata: IlinkClientMetadata,
23
+ resolveCredentials: () => WeixinIlinkCredentials | null,
24
+ resolveConfigManager: () => WeixinConfigManager | undefined,
25
+ sendTextImpl: typeof sendMessageWeixin,
26
+ contextTokens: WeixinContextTokenStore,
27
+ ) {
28
+ this.config = config;
29
+ this.#metadata = metadata;
30
+ this.#resolveCredentials = resolveCredentials;
31
+ this.#resolveConfigManager = resolveConfigManager;
32
+ this.#sendTextImpl = sendTextImpl;
33
+ this.#contextTokens = contextTokens;
34
+ }
35
+
36
+ get credentials(): WeixinIlinkCredentials {
37
+ const credentials = this.#resolveCredentials();
38
+ if (!credentials?.botToken) throw new Error('weixin-ilink client not authenticated');
39
+ return credentials;
40
+ }
41
+
42
+ get authenticated(): boolean {
43
+ return Boolean(this.#resolveCredentials()?.botToken);
44
+ }
45
+
46
+ get apiBaseUrl(): string {
47
+ return this.#resolveCredentials()?.baseUrl ?? this.config.baseUrl;
48
+ }
49
+
50
+ get cdnBaseUrl(): string {
51
+ return this.config.cdnBaseUrl;
52
+ }
53
+
54
+ contextToken(userId: string): string | undefined {
55
+ return this.#contextTokens.get(userId);
56
+ }
57
+
58
+ reachableUserIds(): string[] {
59
+ return this.#contextTokens.userIds();
60
+ }
61
+
62
+ async sendText(to: string, text: string): Promise<{ messageId: string }> {
63
+ const contextToken = this.contextToken(to);
64
+ if (!contextToken) throw new Error(`missing context_token for peer ${to}`);
65
+ return this.#sendTextImpl({
66
+ to,
67
+ text,
68
+ opts: {
69
+ baseUrl: this.apiBaseUrl,
70
+ metadata: this.#metadata,
71
+ token: this.credentials.botToken,
72
+ contextToken,
73
+ },
74
+ });
75
+ }
76
+
77
+ async sendMedia(to: string, filePath: string, text = ''): Promise<{ messageId: string }> {
78
+ const contextToken = this.contextToken(to);
79
+ if (!contextToken) throw new Error(`missing context_token for peer ${to}`);
80
+ return sendWeixinMediaFile({
81
+ filePath,
82
+ to,
83
+ text,
84
+ opts: {
85
+ baseUrl: this.apiBaseUrl,
86
+ metadata: this.#metadata,
87
+ token: this.credentials.botToken,
88
+ contextToken,
89
+ },
90
+ cdnBaseUrl: this.cdnBaseUrl,
91
+ });
92
+ }
93
+
94
+ async sendTyping(userId: string, status: number): Promise<boolean> {
95
+ const manager = this.#resolveConfigManager();
96
+ if (!this.authenticated || !manager) return false;
97
+ const cfg = await manager.getForUser(userId, this.contextToken(userId));
98
+ if (!cfg.typingTicket) return false;
99
+ await sendTyping({
100
+ baseUrl: this.apiBaseUrl,
101
+ metadata: this.#metadata,
102
+ token: this.credentials.botToken,
103
+ body: { ilink_user_id: userId, typing_ticket: cfg.typingTicket, status },
104
+ });
105
+ return true;
106
+ }
107
+ }
108
+
109
+ export type WeixinIlinkClientEventMap = Record<string, unknown>;
110
+
111
+ declare module '@zhin.js/feature-kit' {
112
+ interface AdapterClientRegistry {
113
+ readonly 'weixin-ilink': { readonly client: WeixinIlinkClient; readonly events: WeixinIlinkClientEventMap };
114
+ }
115
+ }
116
+
117
+ export const weixinIlinkClient = defineEndpointClient<WeixinIlinkClient, WeixinIlinkClientEventMap>('weixin-ilink');
@@ -1,8 +1,101 @@
1
- export {
2
- setContextToken,
3
- getContextToken,
4
- restoreContextTokens,
5
- clearContextTokensForAccount,
6
- bodyFromItemList,
7
- isMediaItem,
8
- } from "./weixin-inbound.js";
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { WeixinIlinkStateStore } from './credentials.js';
4
+ import { logger } from './ilink-logger.js';
5
+
6
+ const PERSIST_DEBOUNCE_MS = 500;
7
+
8
+ /** Endpoint-owned context-token repository with account-scoped persistence. */
9
+ export class WeixinContextTokenStore {
10
+ readonly state: WeixinIlinkStateStore;
11
+ readonly #tokens = new Map<string, string>();
12
+ #persistTimer?: ReturnType<typeof setTimeout>;
13
+
14
+ constructor(state: WeixinIlinkStateStore) {
15
+ this.state = state;
16
+ }
17
+
18
+ get accountId(): string {
19
+ return this.state.endpointId;
20
+ }
21
+
22
+ get(userId: string): string | undefined {
23
+ const value = this.#tokens.get(userId);
24
+ logger.debug(`contextTokenStore.get: account=${this.accountId} user=${userId} found=${value !== undefined} storeSize=${this.#tokens.size}`);
25
+ return value;
26
+ }
27
+
28
+ set(userId: string, token: string): void {
29
+ logger.debug(`contextTokenStore.set: account=${this.accountId} user=${userId}`);
30
+ this.#tokens.set(userId, token);
31
+ this.#schedulePersist();
32
+ }
33
+
34
+ userIds(): string[] {
35
+ return [...this.#tokens.keys()];
36
+ }
37
+
38
+ restore(): void {
39
+ const filePath = this.#filePath();
40
+ try {
41
+ if (!fs.existsSync(filePath)) return;
42
+ const tokens = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, unknown>;
43
+ let count = 0;
44
+ for (const [userId, token] of Object.entries(tokens)) {
45
+ if (typeof token !== 'string' || !token) continue;
46
+ this.#tokens.set(userId, token);
47
+ count += 1;
48
+ }
49
+ logger.info(`contextTokenStore.restore: restored ${count} tokens for account=${this.accountId}`);
50
+ } catch (error) {
51
+ logger.warn(`contextTokenStore.restore: failed to read ${filePath}: ${String(error)}`);
52
+ }
53
+ }
54
+
55
+ flush(): void {
56
+ if (this.#persistTimer) {
57
+ clearTimeout(this.#persistTimer);
58
+ this.#persistTimer = undefined;
59
+ }
60
+ this.#persist();
61
+ }
62
+
63
+ clear(): void {
64
+ if (this.#persistTimer) {
65
+ clearTimeout(this.#persistTimer);
66
+ this.#persistTimer = undefined;
67
+ }
68
+ this.#tokens.clear();
69
+ const filePath = this.#filePath();
70
+ try {
71
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
72
+ } catch (error) {
73
+ logger.warn(`contextTokenStore.clear: failed to remove ${filePath}: ${String(error)}`);
74
+ }
75
+ }
76
+
77
+ #filePath(): string {
78
+ return this.state.contextTokensPath();
79
+ }
80
+
81
+ #schedulePersist(): void {
82
+ if (this.#persistTimer) clearTimeout(this.#persistTimer);
83
+ this.#persistTimer = setTimeout(() => {
84
+ this.#persistTimer = undefined;
85
+ this.#persist();
86
+ }, PERSIST_DEBOUNCE_MS);
87
+ this.#persistTimer.unref?.();
88
+ }
89
+
90
+ #persist(): void {
91
+ const filePath = this.#filePath();
92
+ try {
93
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
94
+ fs.writeFileSync(filePath, JSON.stringify(Object.fromEntries(this.#tokens)), 'utf-8');
95
+ } catch (error) {
96
+ logger.warn(`contextTokenStore.persist: failed to write ${filePath}: ${String(error)}`);
97
+ }
98
+ }
99
+ }
100
+
101
+ export { bodyFromItemList, isMediaItem } from './weixin-inbound.js';
@@ -1,5 +1,5 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
 
4
4
  export interface WeixinIlinkCredentials {
5
5
  botToken: string;
@@ -9,56 +9,79 @@ export interface WeixinIlinkCredentials {
9
9
  updatedAt?: string;
10
10
  }
11
11
 
12
- const DEFAULT_DATA_DIR = "./data/weixin-ilink";
12
+ /** Endpoint-owned persistence for credentials, cursors, context tokens, and media. */
13
+ export class WeixinIlinkStateStore {
14
+ readonly endpointId: string;
15
+ readonly rootDir: string;
13
16
 
14
- export function resolveStateDir(): string {
15
- return process.env.ZHIN_DATA_DIR
16
- ? path.join(process.env.ZHIN_DATA_DIR, "weixin-ilink")
17
- : DEFAULT_DATA_DIR;
18
- }
17
+ constructor(
18
+ endpointId: string,
19
+ rootDir: string,
20
+ ) {
21
+ if (!/^[A-Za-z0-9_.-]+$/u.test(endpointId)) {
22
+ throw new TypeError('Weixin iLink state endpoint id is invalid');
23
+ }
24
+ if (!rootDir.trim()) throw new TypeError('Weixin iLink state root is required');
25
+ this.endpointId = endpointId;
26
+ this.rootDir = rootDir;
27
+ }
19
28
 
20
- export function credentialsPath(endpointName: string): string {
21
- return path.join(resolveStateDir(), `${endpointName}.json`);
22
- }
29
+ endpointDirectory(): string {
30
+ return path.join(this.rootDir, this.endpointId);
31
+ }
23
32
 
24
- export function loadCredentials(endpointName: string): WeixinIlinkCredentials | null {
25
- const file = credentialsPath(endpointName);
26
- try {
27
- if (!fs.existsSync(file)) return null;
28
- const raw = fs.readFileSync(file, "utf-8");
29
- const data = JSON.parse(raw) as WeixinIlinkCredentials;
30
- if (!data.botToken?.trim()) return null;
31
- return data;
32
- } catch {
33
- return null;
33
+ credentialsPath(): string {
34
+ return path.join(this.endpointDirectory(), 'credentials.json');
34
35
  }
35
- }
36
36
 
37
- export function saveCredentials(endpointName: string, creds: WeixinIlinkCredentials): void {
38
- const file = credentialsPath(endpointName);
39
- fs.mkdirSync(path.dirname(file), { recursive: true });
40
- fs.writeFileSync(
41
- file,
42
- JSON.stringify({ ...creds, updatedAt: new Date().toISOString() }, null, 2),
43
- "utf-8",
44
- );
45
- }
37
+ loadCredentials(): WeixinIlinkCredentials | null {
38
+ const file = this.credentialsPath();
39
+ try {
40
+ if (!fs.existsSync(file)) return null;
41
+ const data = JSON.parse(fs.readFileSync(file, 'utf-8')) as WeixinIlinkCredentials;
42
+ return data.botToken?.trim() ? data : null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
46
47
 
47
- export function syncBufPath(endpointName: string): string {
48
- return path.join(resolveStateDir(), `${endpointName}.sync-buf`);
49
- }
48
+ saveCredentials(credentials: WeixinIlinkCredentials): void {
49
+ const file = this.credentialsPath();
50
+ fs.mkdirSync(path.dirname(file), { recursive: true });
51
+ fs.writeFileSync(
52
+ file,
53
+ JSON.stringify({ ...credentials, updatedAt: new Date().toISOString() }, null, 2),
54
+ 'utf-8',
55
+ );
56
+ }
50
57
 
51
- export function loadSyncBuf(endpointName: string): string {
52
- const file = syncBufPath(endpointName);
53
- try {
54
- return fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "";
55
- } catch {
56
- return "";
58
+ syncBufPath(): string {
59
+ return path.join(this.endpointDirectory(), 'sync-buf');
60
+ }
61
+
62
+ loadSyncBuf(): string {
63
+ const file = this.syncBufPath();
64
+ try {
65
+ return fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
66
+ } catch {
67
+ return '';
68
+ }
69
+ }
70
+
71
+ saveSyncBuf(value: string): void {
72
+ const file = this.syncBufPath();
73
+ fs.mkdirSync(path.dirname(file), { recursive: true });
74
+ fs.writeFileSync(file, value, 'utf-8');
75
+ }
76
+
77
+ contextTokensPath(): string {
78
+ return path.join(this.endpointDirectory(), 'context-tokens.json');
57
79
  }
58
- }
59
80
 
60
- export function saveSyncBuf(endpointName: string, buf: string): void {
61
- const file = syncBufPath(endpointName);
62
- fs.mkdirSync(path.dirname(file), { recursive: true });
63
- fs.writeFileSync(file, buf, "utf-8");
81
+ mediaDirectory(subdir: string): string {
82
+ if (subdir !== 'inbound' && subdir !== 'outbound') {
83
+ throw new TypeError(`Unsupported Weixin iLink media directory: ${subdir}`);
84
+ }
85
+ return path.join(this.endpointDirectory(), 'media', subdir);
86
+ }
64
87
  }