@zhin.js/adapter-weixin-ilink 1.1.0 → 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 +74 -0
- package/README.md +11 -6
- package/adapters/{weixin-ilink.js → weixin-ilink/index.js} +6 -6
- package/adapters/{weixin-ilink.ts → weixin-ilink/index.ts} +9 -9
- package/commands/{endpoint/add/[id].js → weixin-ilink/endpoint/add/[id]/index.js} +1 -1
- package/commands/weixin-ilink/endpoint/add/[id]/index.ts +3 -0
- package/{lib/weixin-ilink-endpoint-commands.js → commands/weixin-ilink/endpoint/definition.js} +4 -3
- package/{src/weixin-ilink-endpoint-commands.ts → commands/weixin-ilink/endpoint/definition.ts} +3 -3
- package/commands/{endpoint/list.js → weixin-ilink/endpoint/list/index.js} +1 -1
- package/commands/weixin-ilink/endpoint/list/index.ts +3 -0
- package/commands/{endpoint/remove/[id].js → weixin-ilink/endpoint/remove/[id]/index.js} +1 -1
- package/commands/weixin-ilink/endpoint/remove/[id]/index.ts +3 -0
- package/lib/client.d.ts +5 -4
- package/lib/client.js +33 -16
- package/lib/context-store.d.ts +15 -1
- package/lib/context-store.js +93 -1
- package/lib/credentials.d.ts +15 -7
- package/lib/credentials.js +59 -42
- package/lib/endpoint.d.ts +9 -4
- package/lib/endpoint.js +41 -27
- package/lib/ilink-api.d.ts +6 -5
- package/lib/ilink-api.js +21 -14
- package/lib/ilink-config-cache.d.ts +3 -7
- package/lib/ilink-config-cache.js +13 -14
- package/lib/ilink-logger.d.ts +0 -1
- package/lib/ilink-logger.js +11 -25
- package/lib/ilink-meta.d.ts +13 -7
- package/lib/ilink-meta.js +24 -20
- package/lib/ilink-session-guard.d.ts +13 -14
- package/lib/ilink-session-guard.js +29 -42
- package/lib/index.d.ts +5 -2
- package/lib/index.js +4 -0
- package/lib/login.d.ts +4 -3
- package/lib/login.js +24 -21
- package/lib/outbound-media.js +1 -1
- package/lib/protocol.d.ts +7 -10
- package/lib/protocol.js +15 -15
- package/lib/types.d.ts +1 -1
- package/lib/weixin-ilink-runtime-state.js +1 -1
- package/lib/weixin-inbound.d.ts +3 -49
- package/lib/weixin-inbound.js +20 -183
- package/lib/weixin-send-media.js +5 -1
- package/lib/weixin-send.js +2 -0
- package/package.json +11 -11
- package/plugin.js +1 -1
- package/schema.json +32 -4
- package/src/client.ts +46 -14
- package/src/context-store.ts +101 -10
- package/src/credentials.ts +68 -45
- package/src/endpoint.ts +59 -38
- package/src/ilink-api.ts +26 -20
- 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 +15 -2
- package/src/login.ts +32 -19
- package/src/outbound-media.ts +1 -1
- package/src/protocol.ts +20 -29
- package/src/types.ts +0 -1
- package/src/upload.test.ts +11 -8
- package/src/weixin-ilink-runtime-state.ts +1 -1
- package/src/weixin-inbound.ts +23 -215
- package/src/weixin-send-media.ts +5 -1
- package/src/weixin-send.ts +2 -0
- package/commands/endpoint/add/[id].ts +0 -3
- package/commands/endpoint/list.ts +0 -3
- package/commands/endpoint/remove/[id].ts +0 -3
- package/lib/weixin-ilink-endpoint-commands.d.ts +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# @zhin.js/adapter-weixin-ilink
|
|
2
2
|
|
|
3
|
+
## 1.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a9e4a40: Derive Command user routes only from explicit `commands/**/*/index.ts(x)` entry paths instead of prepending the plugin owner. Child plugins can now publish top-level static or dynamic commands, while duplicate routes across owners fail during generation construction. Adapter endpoint commands move to explicit paths such as `commands/qq/endpoint/list/index.ts` and are invoked as `qq endpoint list`.
|
|
8
|
+
- 8740059: Standardize TypeScript capabilities on named module directories such as `commands/foo/index.ts`, `middlewares/audit/index.ts`, `handlers/message-receive/index.ts`, `pages/workroom/index.tsx`, and `mcps/filesystem/index.ts`. Only the fixed `index` entry is discovered; sibling files remain private helpers.
|
|
9
|
+
|
|
10
|
+
Command route segments come from directories, while `[name]`, `[[name]]`, `[...name]`, and `[[...name]]` directories declare dynamic parameters. Plugin owners do not enter the route unless their config explicitly sets `commandNamespace`; Endpoint `commandPrefix` remains platform-owned and defaults to an empty string.
|
|
11
|
+
|
|
12
|
+
Migrate the built-in adapters, plugins, examples, generators, migration tooling, hot reload classification, Agent authoring surfaces, documentation, and release artifacts to the explicit entry convention.
|
|
13
|
+
|
|
14
|
+
Make Tool ownership and progressive disclosure explicit across all four supported locations: plugin-public `tools/`, Agent-private `agents/<name>/tools/`, Skill-private `skills/<name>/tools/`, and Agent-Skill-private `agents/<name>/skills/<name>/tools/`. Move adapter and group-suite operations that require domain instructions into their owning Skills so `load_skill` is the only path that unlocks their schemas.
|
|
15
|
+
|
|
16
|
+
Remove the package-root `agent/` convention. Public capabilities now use named package-root directories, schedules use `schedules/<name>/index.ts` or `plugin.ts` injection, MCP connections use `mcps/<name>/index.ts`, Prompt Sections use `prompt-sections/<name>/index.ts`, Agent definitions use `agents/<name>/`, and permission vocabulary is published as `PERMITS.md`.
|
|
17
|
+
|
|
18
|
+
- f68f5dd: Move context tokens, persistence timers, and expired-session cooldowns into endpoint-owned classes. Multiple iLink endpoints no longer share mutable module state, and inbound token updates now produce one persistence side effect.
|
|
19
|
+
- 9c81bf1: Require one canonical expanded Weixin iLink endpoint configuration and make each Endpoint own one account-scoped state directory for credentials, long-poll cursors, context tokens, and media. Remove nested endpoint and environment compatibility and add an explicit `dataDir` boundary.
|
|
20
|
+
- 6582a00: Replace process-global iLink request metadata and logger mutation with immutable endpoint-owned request identity. Every login, polling, typing, upload, and send request now carries the owning endpoint's version and bot agent explicitly, preventing one endpoint from changing another endpoint's wire identity.
|
|
21
|
+
- 4380cf9: Colocate adapter command definitions and Agent Skill handlers with their owning capabilities, and provide a shared validated platform permission constructor.
|
|
22
|
+
- 5c3858e: Make `@zhin.js/im-contract` the explicit zero-dependency owner of transport identities, canonical segments, media guards, conversation facts, stores, and delivery contracts. Platform adapters now read media contracts from that foundation instead of reaching through Core.
|
|
23
|
+
- Updated dependencies [c861789]
|
|
24
|
+
- Updated dependencies [1414ccb]
|
|
25
|
+
- Updated dependencies [743d470]
|
|
26
|
+
- Updated dependencies [62dee52]
|
|
27
|
+
- Updated dependencies [cd54131]
|
|
28
|
+
- Updated dependencies [a9e4a40]
|
|
29
|
+
- Updated dependencies [5855db7]
|
|
30
|
+
- Updated dependencies [ec921d2]
|
|
31
|
+
- Updated dependencies [8740059]
|
|
32
|
+
- Updated dependencies [9110ab8]
|
|
33
|
+
- Updated dependencies [eb3227a]
|
|
34
|
+
- Updated dependencies [b853dba]
|
|
35
|
+
- Updated dependencies [e561309]
|
|
36
|
+
- Updated dependencies [d4c6175]
|
|
37
|
+
- Updated dependencies [7a0e1ca]
|
|
38
|
+
- Updated dependencies [103b5d3]
|
|
39
|
+
- Updated dependencies [5a7a7f7]
|
|
40
|
+
- Updated dependencies [b076eae]
|
|
41
|
+
- Updated dependencies [73a24b7]
|
|
42
|
+
- Updated dependencies [1cb1163]
|
|
43
|
+
- Updated dependencies [be3061e]
|
|
44
|
+
- Updated dependencies [75f8332]
|
|
45
|
+
- Updated dependencies [81935e2]
|
|
46
|
+
- Updated dependencies [f9ed01b]
|
|
47
|
+
- Updated dependencies [ac0ab50]
|
|
48
|
+
- Updated dependencies [5140ce1]
|
|
49
|
+
- Updated dependencies [522d75f]
|
|
50
|
+
- Updated dependencies [a7611b3]
|
|
51
|
+
- Updated dependencies [8823044]
|
|
52
|
+
- Updated dependencies [379439b]
|
|
53
|
+
- Updated dependencies [11c9352]
|
|
54
|
+
- Updated dependencies [135ac91]
|
|
55
|
+
- Updated dependencies [140cf0f]
|
|
56
|
+
- Updated dependencies [251e4d2]
|
|
57
|
+
- Updated dependencies [203ad34]
|
|
58
|
+
- Updated dependencies [e6c5113]
|
|
59
|
+
- Updated dependencies [e0f6478]
|
|
60
|
+
- Updated dependencies [2fd8017]
|
|
61
|
+
- Updated dependencies [5c3858e]
|
|
62
|
+
- Updated dependencies [df9f76b]
|
|
63
|
+
- @zhin.js/core@1.1.37
|
|
64
|
+
- @zhin.js/adapter@1.1.14
|
|
65
|
+
- @zhin.js/feature-kit@1.1.1
|
|
66
|
+
- @zhin.js/logger@1.1.1
|
|
67
|
+
- @zhin.js/im-contract@1.1.1
|
|
68
|
+
|
|
69
|
+
## 1.1.1
|
|
70
|
+
|
|
71
|
+
### Patch Changes
|
|
72
|
+
|
|
73
|
+
- Updated dependencies [769e67f]
|
|
74
|
+
- @zhin.js/adapter@1.1.13
|
|
75
|
+
- @zhin.js/core@1.1.36
|
|
76
|
+
|
|
3
77
|
## 1.1.0
|
|
4
78
|
|
|
5
79
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -28,25 +28,30 @@ plugins:
|
|
|
28
28
|
# botAgent: "Zhin.js/1.0.0"
|
|
29
29
|
# longPollTimeoutMs: 35000
|
|
30
30
|
# baseUrl: https://ilinkai.weixin.qq.com
|
|
31
|
+
# dataDir: data/weixin-ilink
|
|
31
32
|
endpoints:
|
|
32
|
-
-
|
|
33
|
-
# botToken: "..." #
|
|
33
|
+
- id: my-wechat
|
|
34
|
+
# botToken: "..." # 可选;不填则读取状态文件或扫码
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
`
|
|
37
|
+
`AdapterIndex` 会把实例默认值与 endpoint 覆盖值合并后再调用 adapter。协议层只接受这份
|
|
38
|
+
展开后的 endpoint 配置,不读取环境变量,也不再次解析嵌套 `endpoints`。
|
|
39
|
+
|
|
40
|
+
Endpoint 通过一个 `WeixinIlinkStateStore` 统一拥有凭据、长轮询 cursor、context token 和媒体文件。
|
|
41
|
+
`botToken` 也可保存在 `dataDir` 下的状态文件(不进 git):
|
|
37
42
|
|
|
38
43
|
```
|
|
39
|
-
data/weixin-ilink/<
|
|
44
|
+
data/weixin-ilink/<id>/credentials.json
|
|
40
45
|
```
|
|
41
46
|
|
|
42
47
|
## 登录流程
|
|
43
48
|
|
|
44
49
|
1. 首次启动无凭证时,调用 `get_bot_qrcode` 并在日志中打印二维码内容
|
|
45
50
|
2. 用户用微信扫码确认
|
|
46
|
-
3. 后台轮询 `get_qrcode_status` 直至 `confirmed`,写入 `
|
|
51
|
+
3. 后台轮询 `get_qrcode_status` 直至 `confirmed`,写入 `dataDir/<id>/credentials.json`
|
|
47
52
|
4. 调用 `notifyStart` 后进入长轮询
|
|
48
53
|
|
|
49
|
-
> Console 扫码面板(旧 `loginAssist` + host-router)已从生产路径移除;优先用 `botToken` /
|
|
54
|
+
> Console 扫码面板(旧 `loginAssist` + host-router)已从生产路径移除;优先用 `botToken` / Endpoint 状态凭证。
|
|
50
55
|
|
|
51
56
|
## 出站说明
|
|
52
57
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
/**
|
|
3
|
-
* Convention entry: discover `adapters/weixin-ilink.ts` → defineAdapter.
|
|
3
|
+
* Convention entry: discover `adapters/weixin-ilink/index.ts` → defineAdapter.
|
|
4
4
|
*/
|
|
5
5
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
6
|
-
import { WeixinIlinkEndpoint } from "
|
|
7
|
-
import { resolveWeixinIlinkConfig, } from "
|
|
8
|
-
import { weixinIlinkRuntimeStateToken } from "
|
|
9
|
-
export { WeixinIlinkEndpoint } from "
|
|
6
|
+
import { WeixinIlinkEndpoint } from "../../lib/endpoint.js";
|
|
7
|
+
import { resolveWeixinIlinkConfig, } from "../../lib/protocol.js";
|
|
8
|
+
import { weixinIlinkRuntimeStateToken } from "../../lib/weixin-ilink-runtime-state.js";
|
|
9
|
+
export { WeixinIlinkEndpoint } from "../../lib/endpoint.js";
|
|
10
10
|
export default defineAdapter({
|
|
11
11
|
capabilities: ['inbound', 'outbound'],
|
|
12
12
|
operations: ['typing'],
|
|
@@ -18,7 +18,7 @@ export default defineAdapter({
|
|
|
18
18
|
},
|
|
19
19
|
create(context) {
|
|
20
20
|
const config = resolveWeixinIlinkConfig(context.config);
|
|
21
|
-
// 注册到插件运行时状态(weixin-ilink
|
|
21
|
+
// 注册到插件运行时状态(weixin-ilink endpoint list 的"运行中"数据源)
|
|
22
22
|
context.use(weixinIlinkRuntimeStateToken).endpoints.set(config.id, {
|
|
23
23
|
id: config.id,
|
|
24
24
|
// iLink 仅长轮询一种接入方式,无 config.connection 字段
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Convention entry: discover `adapters/weixin-ilink.ts` → defineAdapter.
|
|
2
|
+
* Convention entry: discover `adapters/weixin-ilink/index.ts` → defineAdapter.
|
|
3
3
|
*/
|
|
4
4
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
5
|
-
import { WeixinIlinkEndpoint } from '
|
|
5
|
+
import { WeixinIlinkEndpoint } from '../../src/endpoint.js';
|
|
6
6
|
import {
|
|
7
7
|
resolveWeixinIlinkConfig,
|
|
8
|
-
type
|
|
9
|
-
} from '
|
|
10
|
-
import { weixinIlinkRuntimeStateToken } from '
|
|
8
|
+
type WeixinIlinkEndpointConfig,
|
|
9
|
+
} from '../../src/protocol.js';
|
|
10
|
+
import { weixinIlinkRuntimeStateToken } from '../../src/weixin-ilink-runtime-state.js';
|
|
11
11
|
|
|
12
|
-
export { WeixinIlinkEndpoint } from '
|
|
12
|
+
export { WeixinIlinkEndpoint } from '../../src/endpoint.js';
|
|
13
13
|
export type {
|
|
14
14
|
WeixinIlinkEndpointOptions,
|
|
15
15
|
WeixinIlinkGetUpdates,
|
|
16
16
|
WeixinIlinkNotifyStart,
|
|
17
17
|
WeixinIlinkNotifyStop,
|
|
18
18
|
WeixinIlinkSendText,
|
|
19
|
-
} from '
|
|
19
|
+
} from '../../src/endpoint.js';
|
|
20
20
|
|
|
21
|
-
export default defineAdapter<
|
|
21
|
+
export default defineAdapter<WeixinIlinkEndpointConfig>({
|
|
22
22
|
capabilities: ['inbound', 'outbound'],
|
|
23
23
|
operations: ['typing'],
|
|
24
24
|
// 所有媒体(url 下载 / base64 落盘 / 本地 path)统一物化后走 CDN 上传(sendWeixinMediaFile),
|
|
@@ -29,7 +29,7 @@ export default defineAdapter<WeixinIlinkAdapterConfig>({
|
|
|
29
29
|
},
|
|
30
30
|
create(context) {
|
|
31
31
|
const config = resolveWeixinIlinkConfig(context.config);
|
|
32
|
-
// 注册到插件运行时状态(weixin-ilink
|
|
32
|
+
// 注册到插件运行时状态(weixin-ilink endpoint list 的"运行中"数据源)
|
|
33
33
|
context.use(weixinIlinkRuntimeStateToken).endpoints.set(config.id, {
|
|
34
34
|
id: config.id,
|
|
35
35
|
// iLink 仅长轮询一种接入方式,无 config.connection 字段
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
-
import { weixinIlinkEndpointCommands } from
|
|
2
|
+
import { weixinIlinkEndpointCommands } from '../../definition.js';
|
|
3
3
|
export default weixinIlinkEndpointCommands.add;
|
package/{lib/weixin-ilink-endpoint-commands.js → commands/weixin-ilink/endpoint/definition.js}
RENAMED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
1
2
|
/**
|
|
2
|
-
* `weixin-ilink
|
|
3
|
-
* commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
3
|
+
* `weixin-ilink endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
4
|
+
* commands/weixin-ilink/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
5
|
*/
|
|
5
6
|
import { createEndpointCommands } from 'zhin.js/adapter';
|
|
6
7
|
import { defineCommand } from 'zhin.js/command';
|
|
7
|
-
import { weixinIlinkRuntimeStateToken } from
|
|
8
|
+
import { weixinIlinkRuntimeStateToken } from "../../../lib/weixin-ilink-runtime-state.js";
|
|
8
9
|
export const weixinIlinkEndpointCommands = createEndpointCommands({
|
|
9
10
|
adapterKey: 'weixin-ilink',
|
|
10
11
|
adapterDisplayName: 'Weixin iLink',
|
package/{src/weixin-ilink-endpoint-commands.ts → commands/weixin-ilink/endpoint/definition.ts}
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `weixin-ilink
|
|
3
|
-
* commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
2
|
+
* `weixin-ilink endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
3
|
+
* commands/weixin-ilink/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
4
|
*/
|
|
5
5
|
import { createEndpointCommands } from 'zhin.js/adapter';
|
|
6
6
|
import { defineCommand } from 'zhin.js/command';
|
|
7
|
-
import { weixinIlinkRuntimeStateToken } from '
|
|
7
|
+
import { weixinIlinkRuntimeStateToken } from '../../../src/weixin-ilink-runtime-state.js';
|
|
8
8
|
|
|
9
9
|
export const weixinIlinkEndpointCommands = createEndpointCommands({
|
|
10
10
|
adapterKey: 'weixin-ilink',
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
-
import { weixinIlinkEndpointCommands } from
|
|
2
|
+
import { weixinIlinkEndpointCommands } from '../../definition.js';
|
|
3
3
|
export default weixinIlinkEndpointCommands.remove;
|
package/lib/client.d.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
import type { WeixinContextTokenStore } from './context-store.js';
|
|
1
2
|
import type { WeixinIlinkCredentials } from './credentials.js';
|
|
2
3
|
import type { WeixinConfigManager } from './ilink-config-cache.js';
|
|
4
|
+
import type { IlinkClientMetadata } from './ilink-meta.js';
|
|
3
5
|
import type { ResolvedWeixinIlinkConfig } from './protocol.js';
|
|
4
6
|
import { sendMessageWeixin } from './weixin-send.js';
|
|
5
7
|
/** Direct iLink API client backed by the Endpoint's live authenticated session. */
|
|
6
8
|
export declare class WeixinIlinkClient {
|
|
9
|
+
#private;
|
|
7
10
|
readonly config: ResolvedWeixinIlinkConfig;
|
|
8
|
-
|
|
9
|
-
private readonly resolveConfigManager;
|
|
10
|
-
private readonly sendTextImpl;
|
|
11
|
-
constructor(config: ResolvedWeixinIlinkConfig, resolveCredentials: () => WeixinIlinkCredentials | null, resolveConfigManager: () => WeixinConfigManager | undefined, sendTextImpl: typeof sendMessageWeixin);
|
|
11
|
+
constructor(config: ResolvedWeixinIlinkConfig, metadata: IlinkClientMetadata, resolveCredentials: () => WeixinIlinkCredentials | null, resolveConfigManager: () => WeixinConfigManager | undefined, sendTextImpl: typeof sendMessageWeixin, contextTokens: WeixinContextTokenStore);
|
|
12
12
|
get credentials(): WeixinIlinkCredentials;
|
|
13
13
|
get authenticated(): boolean;
|
|
14
14
|
get apiBaseUrl(): string;
|
|
15
15
|
get cdnBaseUrl(): string;
|
|
16
16
|
contextToken(userId: string): string | undefined;
|
|
17
|
+
reachableUserIds(): string[];
|
|
17
18
|
sendText(to: string, text: string): Promise<{
|
|
18
19
|
messageId: string;
|
|
19
20
|
}>;
|
package/lib/client.js
CHANGED
|
@@ -1,45 +1,56 @@
|
|
|
1
|
-
import { getContextToken } from './context-store.js';
|
|
2
1
|
import { sendTyping } from './ilink-api.js';
|
|
3
2
|
import { sendWeixinMediaFile } from './weixin-send-media.js';
|
|
4
3
|
import { defineEndpointClient } from 'zhin.js/adapter';
|
|
5
4
|
/** Direct iLink API client backed by the Endpoint's live authenticated session. */
|
|
6
5
|
export class WeixinIlinkClient {
|
|
7
6
|
config;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
#metadata;
|
|
8
|
+
#resolveCredentials;
|
|
9
|
+
#resolveConfigManager;
|
|
10
|
+
#sendTextImpl;
|
|
11
|
+
#contextTokens;
|
|
12
|
+
constructor(config, metadata, resolveCredentials, resolveConfigManager, sendTextImpl, contextTokens) {
|
|
12
13
|
this.config = config;
|
|
13
|
-
this
|
|
14
|
-
this
|
|
15
|
-
this
|
|
14
|
+
this.#metadata = metadata;
|
|
15
|
+
this.#resolveCredentials = resolveCredentials;
|
|
16
|
+
this.#resolveConfigManager = resolveConfigManager;
|
|
17
|
+
this.#sendTextImpl = sendTextImpl;
|
|
18
|
+
this.#contextTokens = contextTokens;
|
|
16
19
|
}
|
|
17
20
|
get credentials() {
|
|
18
|
-
const credentials = this
|
|
21
|
+
const credentials = this.#resolveCredentials();
|
|
19
22
|
if (!credentials?.botToken)
|
|
20
23
|
throw new Error('weixin-ilink client not authenticated');
|
|
21
24
|
return credentials;
|
|
22
25
|
}
|
|
23
26
|
get authenticated() {
|
|
24
|
-
return Boolean(this
|
|
27
|
+
return Boolean(this.#resolveCredentials()?.botToken);
|
|
25
28
|
}
|
|
26
29
|
get apiBaseUrl() {
|
|
27
|
-
return this
|
|
30
|
+
return this.#resolveCredentials()?.baseUrl ?? this.config.baseUrl;
|
|
28
31
|
}
|
|
29
32
|
get cdnBaseUrl() {
|
|
30
33
|
return this.config.cdnBaseUrl;
|
|
31
34
|
}
|
|
32
35
|
contextToken(userId) {
|
|
33
|
-
return
|
|
36
|
+
return this.#contextTokens.get(userId);
|
|
37
|
+
}
|
|
38
|
+
reachableUserIds() {
|
|
39
|
+
return this.#contextTokens.userIds();
|
|
34
40
|
}
|
|
35
41
|
async sendText(to, text) {
|
|
36
42
|
const contextToken = this.contextToken(to);
|
|
37
43
|
if (!contextToken)
|
|
38
44
|
throw new Error(`missing context_token for peer ${to}`);
|
|
39
|
-
return this
|
|
45
|
+
return this.#sendTextImpl({
|
|
40
46
|
to,
|
|
41
47
|
text,
|
|
42
|
-
opts: {
|
|
48
|
+
opts: {
|
|
49
|
+
baseUrl: this.apiBaseUrl,
|
|
50
|
+
metadata: this.#metadata,
|
|
51
|
+
token: this.credentials.botToken,
|
|
52
|
+
contextToken,
|
|
53
|
+
},
|
|
43
54
|
});
|
|
44
55
|
}
|
|
45
56
|
async sendMedia(to, filePath, text = '') {
|
|
@@ -50,12 +61,17 @@ export class WeixinIlinkClient {
|
|
|
50
61
|
filePath,
|
|
51
62
|
to,
|
|
52
63
|
text,
|
|
53
|
-
opts: {
|
|
64
|
+
opts: {
|
|
65
|
+
baseUrl: this.apiBaseUrl,
|
|
66
|
+
metadata: this.#metadata,
|
|
67
|
+
token: this.credentials.botToken,
|
|
68
|
+
contextToken,
|
|
69
|
+
},
|
|
54
70
|
cdnBaseUrl: this.cdnBaseUrl,
|
|
55
71
|
});
|
|
56
72
|
}
|
|
57
73
|
async sendTyping(userId, status) {
|
|
58
|
-
const manager = this
|
|
74
|
+
const manager = this.#resolveConfigManager();
|
|
59
75
|
if (!this.authenticated || !manager)
|
|
60
76
|
return false;
|
|
61
77
|
const cfg = await manager.getForUser(userId, this.contextToken(userId));
|
|
@@ -63,6 +79,7 @@ export class WeixinIlinkClient {
|
|
|
63
79
|
return false;
|
|
64
80
|
await sendTyping({
|
|
65
81
|
baseUrl: this.apiBaseUrl,
|
|
82
|
+
metadata: this.#metadata,
|
|
66
83
|
token: this.credentials.botToken,
|
|
67
84
|
body: { ilink_user_id: userId, typing_ticket: cfg.typingTicket, status },
|
|
68
85
|
});
|
package/lib/context-store.d.ts
CHANGED
|
@@ -1 +1,15 @@
|
|
|
1
|
-
|
|
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';
|
package/lib/context-store.js
CHANGED
|
@@ -1 +1,93 @@
|
|
|
1
|
-
|
|
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';
|
package/lib/credentials.d.ts
CHANGED
|
@@ -5,10 +5,18 @@ export interface WeixinIlinkCredentials {
|
|
|
5
5
|
baseUrl?: string;
|
|
6
6
|
updatedAt?: string;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
export declare
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
}
|
package/lib/credentials.js
CHANGED
|
@@ -1,48 +1,65 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
32
|
+
}
|
|
22
33
|
}
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
const file = credentialsPath(endpointId);
|
|
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(endpointId) {
|
|
33
|
-
return path.join(resolveStateDir(), `${endpointId}.sync-buf`);
|
|
34
|
-
}
|
|
35
|
-
export function loadSyncBuf(endpointId) {
|
|
36
|
-
const file = syncBufPath(endpointId);
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
}
|
|
44
|
-
export function saveSyncBuf(endpointId, buf) {
|
|
45
|
-
const file = syncBufPath(endpointId);
|
|
46
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
47
|
-
fs.writeFileSync(file, buf, "utf-8");
|
|
48
65
|
}
|