@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
|
@@ -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,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);
|
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
package/lib/cdn-upload.d.ts
CHANGED
package/lib/cdn-upload.js
CHANGED
package/lib/cdn-url.d.ts
CHANGED
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
|
package/lib/client.d.ts
ADDED
|
@@ -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');
|
package/lib/context-store.d.ts
CHANGED
|
@@ -1,2 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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,2 +1,93 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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,11 +5,18 @@ export interface WeixinIlinkCredentials {
|
|
|
5
5
|
baseUrl?: string;
|
|
6
6
|
updatedAt?: string;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
export declare
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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,49 +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(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
|
-
|
|
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
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,
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import type
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|