@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.
- package/CHANGELOG.md +512 -0
- package/README.md +24 -42
- package/adapters/weixin-ilink/index.js +32 -0
- package/adapters/weixin-ilink/index.ts +43 -0
- package/commands/weixin-ilink/endpoint/add/[id]/index.js +3 -0
- package/commands/weixin-ilink/endpoint/add/[id]/index.ts +3 -0
- package/commands/weixin-ilink/endpoint/definition.js +17 -0
- package/commands/weixin-ilink/endpoint/definition.ts +17 -0
- package/commands/weixin-ilink/endpoint/list/index.js +3 -0
- package/commands/weixin-ilink/endpoint/list/index.ts +3 -0
- package/commands/weixin-ilink/endpoint/remove/[id]/index.js +3 -0
- package/commands/weixin-ilink/endpoint/remove/[id]/index.ts +3 -0
- package/lib/aes-ecb.d.ts +0 -1
- package/lib/aes-ecb.js +0 -1
- package/lib/cdn-upload.d.ts +0 -1
- package/lib/cdn-upload.js +0 -1
- package/lib/cdn-url.d.ts +0 -1
- package/lib/cdn-url.js +0 -1
- package/lib/client.d.ts +35 -0
- package/lib/client.js +89 -0
- package/lib/context-store.d.ts +15 -2
- package/lib/context-store.js +93 -2
- package/lib/credentials.d.ts +15 -8
- package/lib/credentials.js +59 -43
- package/lib/endpoint.d.ts +40 -35
- package/lib/endpoint.js +311 -262
- package/lib/ilink-api.d.ts +7 -6
- package/lib/ilink-api.js +26 -16
- package/lib/ilink-config-cache.d.ts +3 -8
- package/lib/ilink-config-cache.js +13 -15
- package/lib/ilink-logger.d.ts +0 -2
- package/lib/ilink-logger.js +11 -26
- package/lib/ilink-meta.d.ts +13 -8
- package/lib/ilink-meta.js +24 -21
- package/lib/ilink-random.d.ts +0 -1
- package/lib/ilink-random.js +0 -1
- package/lib/ilink-redact.d.ts +0 -1
- package/lib/ilink-redact.js +0 -1
- package/lib/ilink-session-guard.d.ts +13 -15
- package/lib/ilink-session-guard.js +29 -43
- package/lib/ilink-types.d.ts +0 -1
- package/lib/ilink-types.js +0 -1
- package/lib/index.d.ts +8 -20
- package/lib/index.js +7 -38
- package/lib/login.d.ts +5 -6
- package/lib/login.js +85 -77
- package/lib/media-download.d.ts +1 -2
- package/lib/media-download.js +1 -2
- package/lib/mime.d.ts +0 -1
- package/lib/mime.js +0 -1
- package/lib/outbound-media.d.ts +4 -5
- package/lib/outbound-media.js +85 -86
- package/lib/pic-decrypt.d.ts +0 -1
- package/lib/pic-decrypt.js +0 -1
- package/lib/protocol.d.ts +60 -0
- package/lib/protocol.js +127 -0
- package/lib/silk-transcode.d.ts +0 -1
- package/lib/silk-transcode.js +0 -1
- package/lib/types.d.ts +2 -26
- package/lib/types.js +0 -1
- package/lib/upload.d.ts +1 -2
- package/lib/upload.js +1 -2
- package/lib/weixin-ilink-runtime-state.d.ts +1 -0
- package/lib/weixin-ilink-runtime-state.js +6 -0
- package/lib/weixin-inbound.d.ts +4 -43
- package/lib/weixin-inbound.js +22 -162
- package/lib/weixin-send-media.d.ts +0 -1
- package/lib/weixin-send-media.js +5 -2
- package/lib/weixin-send.d.ts +1 -2
- package/lib/weixin-send.js +4 -3
- package/package.json +48 -39
- package/plugin.js +14 -0
- package/schema.json +116 -0
- package/src/client.ts +117 -0
- package/src/context-store.ts +101 -8
- package/src/credentials.ts +68 -45
- package/src/endpoint.ts +384 -315
- package/src/ilink-api.ts +31 -21
- package/src/ilink-config-cache.ts +17 -13
- package/src/ilink-logger.ts +11 -26
- package/src/ilink-meta.ts +31 -21
- package/src/ilink-session-guard.ts +29 -46
- package/src/index.ts +41 -56
- package/src/login.ts +112 -85
- package/src/media-download.ts +1 -2
- package/src/outbound-media.ts +97 -97
- package/src/protocol.ts +180 -0
- package/src/silk-wasm.d.ts +6 -2
- package/src/types.ts +5 -25
- package/src/upload.test.ts +11 -8
- package/src/upload.ts +2 -2
- package/src/weixin-ilink-runtime-state.ts +7 -0
- package/src/weixin-inbound.ts +25 -187
- package/src/weixin-send-media.ts +5 -1
- package/src/weixin-send.ts +6 -4
- package/client/LoginAssistPanel.tsx +0 -200
- package/client/WeixinIlinkManagement.tsx +0 -139
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -13
- package/client/utils/api.ts +0 -30
- package/dist/index.js +0 -29
- package/lib/adapter.d.ts +0 -12
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -15
- package/lib/adapter.js.map +0 -1
- package/lib/aes-ecb.d.ts.map +0 -1
- package/lib/aes-ecb.js.map +0 -1
- package/lib/cdn-upload.d.ts.map +0 -1
- package/lib/cdn-upload.js.map +0 -1
- package/lib/cdn-upload.test.d.ts +0 -2
- package/lib/cdn-upload.test.d.ts.map +0 -1
- package/lib/cdn-upload.test.js +0 -150
- package/lib/cdn-upload.test.js.map +0 -1
- package/lib/cdn-url.d.ts.map +0 -1
- package/lib/cdn-url.js.map +0 -1
- package/lib/context-store.d.ts.map +0 -1
- package/lib/context-store.js.map +0 -1
- package/lib/credentials.d.ts.map +0 -1
- package/lib/credentials.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/ilink-api.d.ts.map +0 -1
- package/lib/ilink-api.js.map +0 -1
- package/lib/ilink-config-cache.d.ts.map +0 -1
- package/lib/ilink-config-cache.js.map +0 -1
- package/lib/ilink-logger.d.ts.map +0 -1
- package/lib/ilink-logger.js.map +0 -1
- package/lib/ilink-meta.d.ts.map +0 -1
- package/lib/ilink-meta.js.map +0 -1
- package/lib/ilink-random.d.ts.map +0 -1
- package/lib/ilink-random.js.map +0 -1
- package/lib/ilink-redact.d.ts.map +0 -1
- package/lib/ilink-redact.js.map +0 -1
- package/lib/ilink-session-guard.d.ts.map +0 -1
- package/lib/ilink-session-guard.js.map +0 -1
- package/lib/ilink-types.d.ts.map +0 -1
- package/lib/ilink-types.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/login-assist-routes.d.ts +0 -7
- package/lib/login-assist-routes.d.ts.map +0 -1
- package/lib/login-assist-routes.js +0 -29
- package/lib/login-assist-routes.js.map +0 -1
- package/lib/login.d.ts.map +0 -1
- package/lib/login.js.map +0 -1
- package/lib/media-download.d.ts.map +0 -1
- package/lib/media-download.js.map +0 -1
- package/lib/mime.d.ts.map +0 -1
- package/lib/mime.js.map +0 -1
- package/lib/mime.test.d.ts +0 -2
- package/lib/mime.test.d.ts.map +0 -1
- package/lib/mime.test.js +0 -62
- package/lib/mime.test.js.map +0 -1
- package/lib/outbound-media.d.ts.map +0 -1
- package/lib/outbound-media.js.map +0 -1
- package/lib/pic-decrypt.d.ts.map +0 -1
- package/lib/pic-decrypt.js.map +0 -1
- package/lib/routes.d.ts +0 -8
- package/lib/routes.d.ts.map +0 -1
- package/lib/routes.js +0 -29
- package/lib/routes.js.map +0 -1
- package/lib/silk-transcode.d.ts.map +0 -1
- package/lib/silk-transcode.js.map +0 -1
- package/lib/silk-transcode.test.d.ts +0 -2
- package/lib/silk-transcode.test.d.ts.map +0 -1
- package/lib/silk-transcode.test.js +0 -53
- package/lib/silk-transcode.test.js.map +0 -1
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js.map +0 -1
- package/lib/typing-indicator.d.ts +0 -32
- package/lib/typing-indicator.d.ts.map +0 -1
- package/lib/typing-indicator.js +0 -95
- package/lib/typing-indicator.js.map +0 -1
- package/lib/upload.d.ts.map +0 -1
- package/lib/upload.js.map +0 -1
- package/lib/upload.test.d.ts +0 -2
- package/lib/upload.test.d.ts.map +0 -1
- package/lib/upload.test.js +0 -250
- package/lib/upload.test.js.map +0 -1
- package/lib/weixin-inbound.d.ts.map +0 -1
- package/lib/weixin-inbound.js.map +0 -1
- package/lib/weixin-send-media.d.ts.map +0 -1
- package/lib/weixin-send-media.js.map +0 -1
- package/lib/weixin-send.d.ts.map +0 -1
- package/lib/weixin-send.js.map +0 -1
- package/src/adapter.ts +0 -18
- package/src/login-assist-routes.ts +0 -36
- package/src/routes.ts +0 -39
- 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.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Zhin.js
|
|
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
|
-
|
|
51
|
-
"@
|
|
52
|
-
"@
|
|
53
|
-
"@
|
|
54
|
-
"
|
|
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/
|
|
68
|
-
"@zhin.js/
|
|
69
|
-
"@zhin.js/
|
|
70
|
-
"
|
|
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/
|
|
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
|
-
"
|
|
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": "
|
|
92
|
-
"
|
|
93
|
-
"
|
|
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');
|
package/src/context-store.ts
CHANGED
|
@@ -1,8 +1,101 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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';
|
package/src/credentials.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
29
|
+
endpointDirectory(): string {
|
|
30
|
+
return path.join(this.rootDir, this.endpointId);
|
|
31
|
+
}
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
}
|