@zhin.js/adapter 1.2.0 → 1.2.1
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/README.md +47 -1
- package/lib/adapter-index.d.ts +12 -5
- package/lib/adapter-index.js +46 -7
- package/lib/definition.d.ts +41 -26
- package/lib/definition.js +28 -2
- package/lib/endpoint-client.d.ts +62 -0
- package/lib/endpoint-client.js +163 -0
- package/lib/endpoint-control.d.ts +1 -1
- package/lib/endpoint-lifecycle.js +10 -16
- package/lib/endpoint.d.ts +78 -0
- package/lib/endpoint.js +100 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/provider.d.ts +1 -1
- package/package.json +5 -5
- package/src/adapter-index.ts +52 -12
- package/src/definition.ts +49 -26
- package/src/endpoint-client.ts +250 -0
- package/src/endpoint-control.ts +1 -1
- package/src/endpoint-lifecycle.ts +10 -16
- package/src/endpoint.ts +181 -0
- package/src/index.ts +6 -0
package/README.md
CHANGED
|
@@ -22,6 +22,28 @@ export default defineAdapter({
|
|
|
22
22
|
单文件插件可用 `setup({ addAdapter })` 注册 `defineAdapter(...)`;Endpoint 仍由同一个
|
|
23
23
|
AdapterIndex 和 generation lifecycle 管理。
|
|
24
24
|
|
|
25
|
+
## Adapter 与 Endpoint 职责
|
|
26
|
+
|
|
27
|
+
二者不是同一个运行时对象的两种叫法。固定职责如下:
|
|
28
|
+
|
|
29
|
+
| Module | 负责 | 禁止承担 |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| Adapter definition | 声明平台能力与段策略;解析单个 endpoint 配置;注入依赖;选择并构造一种 Endpoint implementation | 建连、收发消息、持有 socket/timer/listener、维护在线状态、保存 live Endpoint Map |
|
|
32
|
+
| Endpoint instance | 代表一个具体账号/连接;拥有 transport、协议编解码、send/control/content/management、start/open/close/stop 与资源清理 | 展开多账号配置、查找兄弟 Endpoint、发布 generation、维护全局 registry、执行 endpoint add/edit/remove 配置命令 |
|
|
33
|
+
| AdapterIndex | 展开 1:N 配置;作为当前 generation 的 Endpoint directory;校验能力;编排 admission 与生命周期;提供 Runtime 查询 | 理解平台协议、鉴权、媒体上传或 SDK 类型 |
|
|
34
|
+
| Plugin composition | 提供 schema、Resource、命令、HTTP Host 和平台专属 Agent tools | 绕过 AdapterIndex 保存另一份 live Endpoint 权威状态 |
|
|
35
|
+
|
|
36
|
+
`defineAdapter().create()` 是 Adapter 与 Endpoint 的唯一 Seam:调用前属于配置、能力和
|
|
37
|
+
依赖装配,返回后属于具体 Endpoint 的运行期。Adapter definition 应保持无连接状态;
|
|
38
|
+
Endpoint 不得把自己注册进模块级 Map。需要从命令、Agent tool 或 Host 查找当前 Endpoint
|
|
39
|
+
时,应解析当前 generation 的 AdapterIndex/Resource View,不能建立 second source of truth。
|
|
40
|
+
|
|
41
|
+
旧 `@zhin.js/core` 的 `Adapter` class 同时承担集合、消息管线、发送和 Registry,属于兼容
|
|
42
|
+
外壳,不是 Plugin Runtime 的 authoring model。新代码不得依赖、继承或伪造该 class;运行
|
|
43
|
+
期协作应依赖 `OutboundMessageService`、`OutboundHost`、`EndpointControl` 等窄 Interface。
|
|
44
|
+
`pnpm check:adapter-endpoint-boundaries` 对现存 legacy Adapter consumer 与模块级 Agent
|
|
45
|
+
Endpoint registry 使用基线 allowlist 做单调收缩门禁:允许逐项删除,但禁止新增。
|
|
46
|
+
|
|
25
47
|
## Transport Contract
|
|
26
48
|
|
|
27
49
|
Adapter definitions declare `capabilities` for inbound/outbound admission and
|
|
@@ -43,7 +65,7 @@ non-empty platform message id. IM Runtime alone wraps that id as a structured
|
|
|
43
65
|
|
|
44
66
|
## Endpoint Control Port
|
|
45
67
|
|
|
46
|
-
`
|
|
68
|
+
`Endpoint.control` owns actions addressed to an existing message:
|
|
47
69
|
`recall`, `addReaction`, and `removeReaction`. IM Core consumes only this port;
|
|
48
70
|
adapter-specific method names and compound message ids stay at the protocol
|
|
49
71
|
boundary.
|
|
@@ -53,6 +75,30 @@ New adapters should provide `control` directly and declare matching
|
|
|
53
75
|
inspected by the runtime. `createRecallEndpointControl()` bridges the common
|
|
54
76
|
platform `recall(messageId)` shape without leaking that shape into Core.
|
|
55
77
|
|
|
78
|
+
## Operation-scoped Client resolution
|
|
79
|
+
|
|
80
|
+
每个平台包公开一个由 `defineEndpointClient<Client, EventMap>()` 创建的 token,并通过
|
|
81
|
+
`AdapterClientRegistry` 注册 Client/EventMap 类型。当前 IM operation 不需要手动查找
|
|
82
|
+
Endpoint:Handler 的事件参数直接携带 `client`,Command、Middleware 和 Agent Tool 的
|
|
83
|
+
`context.$client` 是按需解析的属性 getter:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
defineCommand({
|
|
87
|
+
adapter: 'icqq',
|
|
88
|
+
execute(context) {
|
|
89
|
+
return context.$client.getGroupList();
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const client = icqqClient.get(context, id); // task / schedule / Host / 跨账号
|
|
94
|
+
const client = icqqClient.find(context, id); // 可选显式查找;不存在时返回 undefined
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
声明字面量 `adapter` 后 `$client` 会反射为确切 Client;不声明时保持 `unknown`。
|
|
98
|
+
Token 的 `get()` 会校验 Endpoint adapter,并从当前 generation 的 AdapterIndex 解析;
|
|
99
|
+
返回值不得缓存到当前 operation 之外。平台 SDK 方法直接在 Client 上调用,Endpoint 不复制
|
|
100
|
+
SDK interface。普通消息发送仍必须走统一 outbound chain。
|
|
101
|
+
|
|
56
102
|
## Endpoint 生命周期基座(createEndpointLifecycle)
|
|
57
103
|
|
|
58
104
|
WS/SSE 类端点的 start/stop/重连/心跳统一走 `createEndpointLifecycle`
|
package/lib/adapter-index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { generationAdmissionSource, type CapabilityId, type CapabilitySlot, type GenerationAdmissionGate, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import { type AdapterCapability, type AdapterDefinition, type AdapterOperation, type AdapterSegmentPolicy, type
|
|
2
|
+
import { type AdapterCapability, type AdapterDefinition, type AdapterOperation, type AdapterSegmentPolicy, type EndpointSendRequest } from './definition.js';
|
|
3
|
+
import { type Endpoint } from './endpoint.js';
|
|
3
4
|
import { type EndpointManagementCapability } from './endpoint-management.js';
|
|
4
5
|
import { type EndpointControl } from './endpoint-control.js';
|
|
5
6
|
import { type EndpointContentResolveContext } from './endpoint-content.js';
|
|
@@ -34,10 +35,16 @@ export declare class AdapterIndex {
|
|
|
34
35
|
* Matches local name, capability id, or owner path segments.
|
|
35
36
|
*/
|
|
36
37
|
resolve(adapter: string, endpointKey: string): CapabilityId | undefined;
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
/** Resolve the framework-owned Endpoint for internal Host control ports. */
|
|
39
|
+
connection(adapter: string, endpointKey: string): Endpoint | undefined;
|
|
40
|
+
/** Resolve the platform-native client owned by one active Endpoint. */
|
|
41
|
+
client<TClient>(adapter: string, endpointKey: string): TClient;
|
|
42
|
+
/** Resolve the Client directly from a generation-stable CapabilityId. */
|
|
43
|
+
clientById<TClient>(id: CapabilityId): TClient;
|
|
44
|
+
/** Literal adapter name used by authoring-context type discrimination. */
|
|
45
|
+
clientAdapter(id: CapabilityId): string;
|
|
46
|
+
/** Optional Client lookup for cross-platform middleware and routing. */
|
|
47
|
+
findClient<TClient>(adapter: string, endpointKey: string): TClient | undefined;
|
|
41
48
|
owner(id: CapabilityId): PluginId;
|
|
42
49
|
/** Exact, serializable capabilities for one concrete Endpoint. */
|
|
43
50
|
capabilities(id: CapabilityId): EndpointCapabilities;
|
package/lib/adapter-index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DisposeStack, GenerationCompensationError, createGenerationAdmissionGate, generationAdmissionSource, } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import { createCapabilityContext } from '@zhin.js/feature-kit';
|
|
3
3
|
import { endpointCapabilitiesOf, resolveAdapterOperations, } from './definition.js';
|
|
4
|
+
import { bindEndpoint, isEndpoint } from './endpoint.js';
|
|
4
5
|
import { listEndpointManagementCapabilities, } from './endpoint-management.js';
|
|
5
6
|
import { assertDeclaredEndpointOperations, endpointControlOf, } from './endpoint-control.js';
|
|
6
7
|
import { endpointContentOf } from './endpoint-content.js';
|
|
@@ -82,15 +83,49 @@ export class AdapterIndex {
|
|
|
82
83
|
const exact = matches.find((record) => record.name === endpointKey);
|
|
83
84
|
return exact?.id ?? matches[0]?.id;
|
|
84
85
|
}
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
*/
|
|
88
|
-
instance(adapter, endpointKey) {
|
|
86
|
+
/** Resolve the framework-owned Endpoint for internal Host control ports. */
|
|
87
|
+
connection(adapter, endpointKey) {
|
|
89
88
|
const id = this.resolve(adapter, endpointKey);
|
|
90
89
|
if (!id)
|
|
91
90
|
return undefined;
|
|
92
91
|
return this.#records.get(id)?.endpoint;
|
|
93
92
|
}
|
|
93
|
+
/** Resolve the platform-native client owned by one active Endpoint. */
|
|
94
|
+
client(adapter, endpointKey) {
|
|
95
|
+
const id = this.resolve(adapter, endpointKey);
|
|
96
|
+
const record = id ? this.#records.get(id) : undefined;
|
|
97
|
+
if (!record)
|
|
98
|
+
throw new Error(`Endpoint ${adapter}/${endpointKey} does not exist`);
|
|
99
|
+
if (!record.started || record.stopped) {
|
|
100
|
+
throw new Error(`Endpoint ${adapter}/${endpointKey} is not active`);
|
|
101
|
+
}
|
|
102
|
+
return record.endpoint.client;
|
|
103
|
+
}
|
|
104
|
+
/** Resolve the Client directly from a generation-stable CapabilityId. */
|
|
105
|
+
clientById(id) {
|
|
106
|
+
const record = this.#records.get(id);
|
|
107
|
+
if (!record)
|
|
108
|
+
throw new Error(`Unknown Adapter Endpoint: ${id}`);
|
|
109
|
+
if (!record.started || record.stopped) {
|
|
110
|
+
throw new Error(`Adapter Endpoint ${id} is not active`);
|
|
111
|
+
}
|
|
112
|
+
return record.endpoint.client;
|
|
113
|
+
}
|
|
114
|
+
/** Literal adapter name used by authoring-context type discrimination. */
|
|
115
|
+
clientAdapter(id) {
|
|
116
|
+
const record = this.#records.get(id);
|
|
117
|
+
if (!record)
|
|
118
|
+
throw new Error(`Unknown Adapter Endpoint: ${id}`);
|
|
119
|
+
return record.endpoint.identity.adapter;
|
|
120
|
+
}
|
|
121
|
+
/** Optional Client lookup for cross-platform middleware and routing. */
|
|
122
|
+
findClient(adapter, endpointKey) {
|
|
123
|
+
const id = this.resolve(adapter, endpointKey);
|
|
124
|
+
const record = id ? this.#records.get(id) : undefined;
|
|
125
|
+
if (!record || !record.started || record.stopped)
|
|
126
|
+
return undefined;
|
|
127
|
+
return record.endpoint.client;
|
|
128
|
+
}
|
|
94
129
|
owner(id) {
|
|
95
130
|
const record = this.#records.get(id);
|
|
96
131
|
if (!record)
|
|
@@ -149,6 +184,9 @@ export class AdapterIndex {
|
|
|
149
184
|
signal.throwIfAborted();
|
|
150
185
|
if (record.stopped)
|
|
151
186
|
throw new Error(`Adapter Endpoint stopped during start: ${record.id}`);
|
|
187
|
+
if (record.endpoint.client === record.endpoint) {
|
|
188
|
+
throw new TypeError(`Adapter Endpoint ${record.id} must expose a distinct platform client`);
|
|
189
|
+
}
|
|
152
190
|
record.started = true;
|
|
153
191
|
}
|
|
154
192
|
}
|
|
@@ -249,7 +287,7 @@ function matchesEndpoint(record, adapter, endpointKey) {
|
|
|
249
287
|
|| record.id.endsWith(`/${adapter}`)
|
|
250
288
|
|| record.owner === adapter
|
|
251
289
|
|| record.owner.endsWith(`/${adapter}`);
|
|
252
|
-
//
|
|
290
|
+
// The live Endpoint identity is the bot runtime id (e.g. ICQQ uin). Host /
|
|
253
291
|
// activity-feedback resolve with that id; slot.localName alone is not enough
|
|
254
292
|
// when multiple plugin instances share localName "icqq".
|
|
255
293
|
const liveName = endpointLiveName(record.endpoint);
|
|
@@ -271,8 +309,8 @@ function endpointPhase(record) {
|
|
|
271
309
|
return 'pending';
|
|
272
310
|
}
|
|
273
311
|
function assertEndpoint(value, id) {
|
|
274
|
-
if (!value
|
|
275
|
-
throw new TypeError(`Adapter ${id} create() must return an Endpoint
|
|
312
|
+
if (!isEndpoint(value)) {
|
|
313
|
+
throw new TypeError(`Adapter ${id} create() must return an Endpoint subclass`);
|
|
276
314
|
}
|
|
277
315
|
}
|
|
278
316
|
/**
|
|
@@ -328,6 +366,7 @@ async function createEndpoint(slot, snapshot, admission, signal, expansion) {
|
|
|
328
366
|
const operations = resolveAdapterOperations(slot.definition, context);
|
|
329
367
|
const endpoint = await slot.definition.create(context);
|
|
330
368
|
assertEndpoint(endpoint, expansion?.id ?? slot.id);
|
|
369
|
+
bindEndpoint(endpoint, context, admission);
|
|
331
370
|
if (slot.definition.capabilities.includes('outbound') && typeof endpoint.send !== 'function') {
|
|
332
371
|
throw new TypeError(`Adapter Endpoint ${String(expansion?.id ?? slot.id)} declares outbound but send() is missing`);
|
|
333
372
|
}
|
package/lib/definition.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter authoring API consumed from `zhin.js/adapter`.
|
|
3
|
+
* @module zhin.js/adapter
|
|
4
|
+
*/
|
|
1
5
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
2
6
|
import type { CapabilityContext } from '@zhin.js/feature-kit';
|
|
3
7
|
import type { ConversationRef, EndpointCapabilities, EndpointOperation } from '@zhin.js/im-contract';
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
import type { Endpoint } from './endpoint.js';
|
|
9
|
+
export { Endpoint } from './endpoint.js';
|
|
10
|
+
export type { EndpointEvent, EndpointIdentity, PlatformEvent, } from './endpoint.js';
|
|
11
|
+
export { defineEndpointClient } from './endpoint-client.js';
|
|
12
|
+
export type { EndpointClientContext, EndpointClientToken, } from './endpoint-client.js';
|
|
7
13
|
declare const adapterBrand: "zhin.adapter/1";
|
|
8
14
|
export type AdapterCapability = 'inbound' | 'outbound';
|
|
9
15
|
/** Operations beyond sending, declared by an Adapter definition. */
|
|
@@ -21,24 +27,9 @@ export interface EndpointSendRequest {
|
|
|
21
27
|
readonly conversation: ConversationRef;
|
|
22
28
|
readonly payload: unknown;
|
|
23
29
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/** Optional platform-neutral control surface for existing messages. */
|
|
28
|
-
readonly control?: EndpointControl;
|
|
29
|
-
/** Optional canonical resolver for message, merged-forward and media references. */
|
|
30
|
-
readonly content?: EndpointContentPort;
|
|
31
|
-
/** Required readiness; must observe abort and settle before rollback returns. */
|
|
32
|
-
start?(signal: AbortSignal): void | Promise<void>;
|
|
33
|
-
/** Opens Endpoint-local flow behind the candidate generation admission gate. */
|
|
34
|
-
open?(): void;
|
|
35
|
-
/** Stops new inbound events while preserving in-flight work. */
|
|
36
|
-
close?(): void | Promise<void>;
|
|
37
|
-
/** Releases transport resources. Calls must be idempotent. */
|
|
38
|
-
stop?(): void | Promise<void>;
|
|
39
|
-
/** Platform message id. Core wraps it in the canonical MessageRef/DeliveryReceipt. */
|
|
40
|
-
send?(request: EndpointSendRequest): string | Promise<string>;
|
|
41
|
-
}
|
|
30
|
+
/**
|
|
31
|
+
* Generation-owned construction context for one runtime Endpoint.
|
|
32
|
+
*/
|
|
42
33
|
export interface AdapterContext<TConfig = unknown> extends CapabilityContext<TConfig> {
|
|
43
34
|
readonly id: CapabilityId;
|
|
44
35
|
readonly name: string;
|
|
@@ -74,7 +65,8 @@ export interface AdapterSegmentPolicy {
|
|
|
74
65
|
/** `native` preserves Markdown for the endpoint codec; `text` strips formatting in Core. */
|
|
75
66
|
readonly markdown?: AdapterMarkdownMode;
|
|
76
67
|
}
|
|
77
|
-
export interface AdapterDefinition<TConfig = unknown> {
|
|
68
|
+
export interface AdapterDefinition<TConfig = unknown, TClient = unknown> {
|
|
69
|
+
/** @internal Runtime feature brand. */
|
|
78
70
|
readonly $feature: typeof adapterBrand;
|
|
79
71
|
readonly capabilities: readonly AdapterCapability[];
|
|
80
72
|
/**
|
|
@@ -85,17 +77,40 @@ export interface AdapterDefinition<TConfig = unknown> {
|
|
|
85
77
|
readonly operations?: AdapterOperationDeclaration<TConfig>;
|
|
86
78
|
/** 可选:端点消息段能力声明(出站协商降级挂载点)。 */
|
|
87
79
|
readonly segments?: AdapterSegmentPolicy;
|
|
88
|
-
create(context: AdapterContext<TConfig>):
|
|
80
|
+
create(context: AdapterContext<TConfig>): Endpoint<TClient> | Promise<Endpoint<TClient>>;
|
|
89
81
|
}
|
|
90
82
|
declare module '@zhin.js/plugin-runtime' {
|
|
91
83
|
interface PluginSetupContext<TConfig = unknown> {
|
|
92
84
|
addAdapter(localName: string, definition: AdapterDefinition<TConfig>): void;
|
|
93
85
|
}
|
|
94
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Define an Adapter module for the `adapters/` convention directory.
|
|
89
|
+
*
|
|
90
|
+
* The returned definition is immutable and declares capabilities before an
|
|
91
|
+
* Endpoint is created, so Runtime admission can fail closed.
|
|
92
|
+
*
|
|
93
|
+
* @public
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { defineAdapter } from 'zhin.js/adapter';
|
|
97
|
+
*
|
|
98
|
+
* export default defineAdapter({
|
|
99
|
+
* capabilities: ['inbound', 'outbound'],
|
|
100
|
+
* create: () => new MyPlatformEndpoint(),
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
95
104
|
export declare function defineAdapter<TConfig = unknown>(definition: Omit<AdapterDefinition<TConfig>, '$feature'>): Readonly<AdapterDefinition<TConfig>>;
|
|
96
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Converts the definition's compact authoring form into the Runtime contract.
|
|
107
|
+
* @internal Adapter projection helper.
|
|
108
|
+
*/
|
|
97
109
|
export declare function endpointCapabilitiesOf(definition: Pick<AdapterDefinition, 'capabilities' | 'operations'>, resolvedOperations?: readonly AdapterOperation[]): EndpointCapabilities;
|
|
98
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Resolve and validate the operation declaration for one concrete Endpoint.
|
|
112
|
+
* @internal Adapter projection helper.
|
|
113
|
+
*/
|
|
99
114
|
export declare function resolveAdapterOperations<TConfig>(definition: Pick<AdapterDefinition<TConfig>, 'operations'>, context: AdapterContext<TConfig>): readonly AdapterOperation[];
|
|
115
|
+
/** @internal Runtime validation for convention-discovered modules. */
|
|
100
116
|
export declare function parseAdapterDefinition(value: unknown): AdapterDefinition;
|
|
101
|
-
export {};
|
package/lib/definition.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
|
+
export { Endpoint } from './endpoint.js';
|
|
2
|
+
export { defineEndpointClient } from './endpoint-client.js';
|
|
1
3
|
const adapterBrand = 'zhin.adapter/1';
|
|
2
4
|
const HTML_OUTBOUND_MODES = ['direct', 'image', 'text'];
|
|
3
5
|
const OUTBOUND_MEDIA_FORMS = [
|
|
4
6
|
'url', 'path', 'base64', 'upload',
|
|
5
7
|
];
|
|
6
8
|
const ADAPTER_OPERATIONS = ['recall', 'edit', 'reaction', 'typing'];
|
|
9
|
+
/**
|
|
10
|
+
* Define an Adapter module for the `adapters/` convention directory.
|
|
11
|
+
*
|
|
12
|
+
* The returned definition is immutable and declares capabilities before an
|
|
13
|
+
* Endpoint is created, so Runtime admission can fail closed.
|
|
14
|
+
*
|
|
15
|
+
* @public
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { defineAdapter } from 'zhin.js/adapter';
|
|
19
|
+
*
|
|
20
|
+
* export default defineAdapter({
|
|
21
|
+
* capabilities: ['inbound', 'outbound'],
|
|
22
|
+
* create: () => new MyPlatformEndpoint(),
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
7
26
|
export function defineAdapter(definition) {
|
|
8
27
|
if (typeof definition.create !== 'function') {
|
|
9
28
|
throw new TypeError('Adapter create must be a function');
|
|
@@ -25,7 +44,10 @@ export function defineAdapter(definition) {
|
|
|
25
44
|
...(segments ? { segments } : {}),
|
|
26
45
|
});
|
|
27
46
|
}
|
|
28
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Converts the definition's compact authoring form into the Runtime contract.
|
|
49
|
+
* @internal Adapter projection helper.
|
|
50
|
+
*/
|
|
29
51
|
export function endpointCapabilitiesOf(definition, resolvedOperations) {
|
|
30
52
|
if (typeof definition.operations === 'function' && resolvedOperations === undefined) {
|
|
31
53
|
throw new TypeError('Dynamic Adapter operations must be resolved for one Endpoint');
|
|
@@ -39,7 +61,10 @@ export function endpointCapabilitiesOf(definition, resolvedOperations) {
|
|
|
39
61
|
...(operations && Object.keys(operations).length > 0 ? { operations: Object.freeze(operations) } : {}),
|
|
40
62
|
});
|
|
41
63
|
}
|
|
42
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Resolve and validate the operation declaration for one concrete Endpoint.
|
|
66
|
+
* @internal Adapter projection helper.
|
|
67
|
+
*/
|
|
43
68
|
export function resolveAdapterOperations(definition, context) {
|
|
44
69
|
const declaration = definition.operations;
|
|
45
70
|
const operations = typeof declaration === 'function'
|
|
@@ -96,6 +121,7 @@ function normalizeSegmentPolicy(policy) {
|
|
|
96
121
|
...(policy.markdown ? { markdown: policy.markdown } : {}),
|
|
97
122
|
});
|
|
98
123
|
}
|
|
124
|
+
/** @internal Runtime validation for convention-discovered modules. */
|
|
99
125
|
export function parseAdapterDefinition(value) {
|
|
100
126
|
if (!value || typeof value !== 'object')
|
|
101
127
|
throw invalidAdapter();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CapabilityContext } from '@zhin.js/feature-kit';
|
|
2
|
+
import { Endpoint } from './endpoint.js';
|
|
3
|
+
declare const endpointClientBrand: "zhin.endpoint-client/1";
|
|
4
|
+
export type { AdapterClient, AdapterClientRegistry, AdapterClientTypes, AdapterEvents, RegisteredAdapterName, } from '@zhin.js/feature-kit';
|
|
5
|
+
/** Convert an EventEmitter-style tuple map into the payload map used by handlers. */
|
|
6
|
+
export type ClientEventPayloads<TEvents extends object> = {
|
|
7
|
+
readonly [K in keyof TEvents]: TEvents[K] extends readonly [infer TPayload, ...unknown[]] ? TPayload : never;
|
|
8
|
+
};
|
|
9
|
+
interface ClientEventSource {
|
|
10
|
+
on(name: string, listener: (payload: unknown) => void): unknown;
|
|
11
|
+
off(name: string, listener: (payload: unknown) => void): unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Forward one Client's public event surface through the Endpoint boundary.
|
|
15
|
+
* The returned disposer is useful when a Client can outlive its Endpoint.
|
|
16
|
+
*/
|
|
17
|
+
export declare function forwardEndpointClientEvents(client: ClientEventSource, names: readonly string[], receive: (name: string, payload: unknown) => void): () => void;
|
|
18
|
+
export type ClientEventSubscription = (receive: (name: string, payload: unknown) => void) => () => void;
|
|
19
|
+
/**
|
|
20
|
+
* Deep Endpoint base for SDK/protocol Clients.
|
|
21
|
+
*
|
|
22
|
+
* It owns the open admission gate and the single Client-event → Endpoint-event
|
|
23
|
+
* bridge. Concrete Endpoints only own account transport and raw-event
|
|
24
|
+
* normalization; they do not repeat dispatch plumbing.
|
|
25
|
+
*/
|
|
26
|
+
export declare abstract class ClientEndpoint<TClient = unknown> extends Endpoint<TClient> {
|
|
27
|
+
#private;
|
|
28
|
+
open(): void;
|
|
29
|
+
close(): void;
|
|
30
|
+
protected get clientEventsOpen(): boolean;
|
|
31
|
+
protected bindClientEvents(subscribe: ClientEventSubscription, receive?: (name: string, payload: unknown) => void, onError?: (name: string, error: unknown) => void): void;
|
|
32
|
+
protected releaseClientEvents(): void;
|
|
33
|
+
}
|
|
34
|
+
/** Typed identity for one platform's native Client surface. */
|
|
35
|
+
export interface EndpointClientToken<TClient, TEvents extends object = Record<string, unknown>> {
|
|
36
|
+
readonly $client: typeof endpointClientBrand;
|
|
37
|
+
readonly adapter: string;
|
|
38
|
+
/** @internal Type-only covariance anchor. */
|
|
39
|
+
readonly _client?: TClient;
|
|
40
|
+
/** @internal Type-only covariance anchor for native platform events. */
|
|
41
|
+
readonly _events?: TEvents;
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the Client from an operation context. Current inbound operations
|
|
44
|
+
* infer their Endpoint; detached operations must provide `endpointKey`.
|
|
45
|
+
*/
|
|
46
|
+
get(context: EndpointClientContext, endpointKey?: string): TClient;
|
|
47
|
+
/** Resolve when this operation belongs to the platform; otherwise return undefined. */
|
|
48
|
+
find(context: EndpointClientContext, endpointKey?: string): TClient | undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Operation-scoped sources that can resolve an Endpoint Client. */
|
|
51
|
+
export interface EndpointClientContext {
|
|
52
|
+
readonly project?: CapabilityContext['project'];
|
|
53
|
+
readonly message?: unknown;
|
|
54
|
+
readonly input?: unknown;
|
|
55
|
+
readonly endpoint?: string;
|
|
56
|
+
readonly origin?: unknown;
|
|
57
|
+
readonly conversation?: unknown;
|
|
58
|
+
readonly $client?: unknown;
|
|
59
|
+
readonly clientAdapter?: string;
|
|
60
|
+
}
|
|
61
|
+
/** Declare the native Client type exported by a platform adapter. */
|
|
62
|
+
export declare function defineEndpointClient<TClient, TEvents extends object = Record<string, unknown>>(adapter: string): EndpointClientToken<TClient, TEvents>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { isAdapterIndex } from './adapter-index.js';
|
|
2
|
+
import { Endpoint } from './endpoint.js';
|
|
3
|
+
import { adapterFeatureId } from './provider.js';
|
|
4
|
+
const endpointClientBrand = 'zhin.endpoint-client/1';
|
|
5
|
+
/**
|
|
6
|
+
* Forward one Client's public event surface through the Endpoint boundary.
|
|
7
|
+
* The returned disposer is useful when a Client can outlive its Endpoint.
|
|
8
|
+
*/
|
|
9
|
+
export function forwardEndpointClientEvents(client, names, receive) {
|
|
10
|
+
const subscriptions = names.map((name) => {
|
|
11
|
+
const listener = (payload) => receive(name, payload);
|
|
12
|
+
client.on(name, listener);
|
|
13
|
+
return { name, listener };
|
|
14
|
+
});
|
|
15
|
+
return () => {
|
|
16
|
+
for (const { name, listener } of subscriptions)
|
|
17
|
+
client.off(name, listener);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Deep Endpoint base for SDK/protocol Clients.
|
|
22
|
+
*
|
|
23
|
+
* It owns the open admission gate and the single Client-event → Endpoint-event
|
|
24
|
+
* bridge. Concrete Endpoints only own account transport and raw-event
|
|
25
|
+
* normalization; they do not repeat dispatch plumbing.
|
|
26
|
+
*/
|
|
27
|
+
export class ClientEndpoint extends Endpoint {
|
|
28
|
+
#clientEventsOpen = false;
|
|
29
|
+
#clientEventsRelease;
|
|
30
|
+
open() {
|
|
31
|
+
this.#clientEventsOpen = true;
|
|
32
|
+
}
|
|
33
|
+
close() {
|
|
34
|
+
this.#clientEventsOpen = false;
|
|
35
|
+
}
|
|
36
|
+
get clientEventsOpen() {
|
|
37
|
+
return this.#clientEventsOpen;
|
|
38
|
+
}
|
|
39
|
+
bindClientEvents(subscribe, receive, onError) {
|
|
40
|
+
this.#clientEventsRelease?.();
|
|
41
|
+
this.#clientEventsRelease = subscribe((name, payload) => {
|
|
42
|
+
if (!this.#clientEventsOpen)
|
|
43
|
+
return;
|
|
44
|
+
void this.emitPlatform(name, payload).catch((error) => onError?.(name, error));
|
|
45
|
+
try {
|
|
46
|
+
receive?.(name, payload);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
onError?.(name, error);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
releaseClientEvents() {
|
|
54
|
+
this.#clientEventsRelease?.();
|
|
55
|
+
this.#clientEventsRelease = undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Declare the native Client type exported by a platform adapter. */
|
|
59
|
+
export function defineEndpointClient(adapter) {
|
|
60
|
+
const normalized = adapter.trim();
|
|
61
|
+
if (!normalized)
|
|
62
|
+
throw new TypeError('Endpoint Client adapter cannot be empty');
|
|
63
|
+
const token = {
|
|
64
|
+
$client: endpointClientBrand,
|
|
65
|
+
adapter: normalized,
|
|
66
|
+
get(context, endpointKey) {
|
|
67
|
+
return resolveEndpointClient(context, token, endpointKey);
|
|
68
|
+
},
|
|
69
|
+
find(context, endpointKey) {
|
|
70
|
+
return findEndpointClient(context, token, endpointKey);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
return Object.freeze(token);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a platform Client from the current generation.
|
|
77
|
+
*
|
|
78
|
+
* The returned object is valid only for the lifetime of `context`; callers
|
|
79
|
+
* must not retain it beyond the current command, handler, tool, task, or
|
|
80
|
+
* schedule operation.
|
|
81
|
+
*/
|
|
82
|
+
function resolveEndpointClient(context, token, endpointKey) {
|
|
83
|
+
if (token.$client !== endpointClientBrand) {
|
|
84
|
+
throw new TypeError('Invalid Endpoint Client token');
|
|
85
|
+
}
|
|
86
|
+
const current = currentClientSource(context);
|
|
87
|
+
if (current && '$client' in current) {
|
|
88
|
+
if (endpointKey && !matchesCurrentEndpoint(current, endpointKey)) {
|
|
89
|
+
throw new Error(`Endpoint Client ${endpointKey} does not match the current operation Endpoint`);
|
|
90
|
+
}
|
|
91
|
+
if (current.clientAdapter && current.clientAdapter !== token.adapter) {
|
|
92
|
+
throw new Error(`Endpoint Client ${token.adapter} cannot access ${current.clientAdapter}`);
|
|
93
|
+
}
|
|
94
|
+
return current.$client;
|
|
95
|
+
}
|
|
96
|
+
const resolvedEndpointKey = endpointKey ?? endpointKeyFromContext(context);
|
|
97
|
+
if (!resolvedEndpointKey) {
|
|
98
|
+
throw new Error('Detached Endpoint Client access requires an explicit endpoint key');
|
|
99
|
+
}
|
|
100
|
+
if (!context.project) {
|
|
101
|
+
throw new Error('Endpoint Client access requires a generation operation context');
|
|
102
|
+
}
|
|
103
|
+
const projection = context.project(adapterFeatureId);
|
|
104
|
+
if (!isAdapterIndex(projection)) {
|
|
105
|
+
throw new Error('Adapter Feature projection is not installed');
|
|
106
|
+
}
|
|
107
|
+
return projection.client(token.adapter, resolvedEndpointKey);
|
|
108
|
+
}
|
|
109
|
+
function findEndpointClient(context, token, endpointKey) {
|
|
110
|
+
const current = currentClientSource(context);
|
|
111
|
+
if (current && '$client' in current) {
|
|
112
|
+
if (current.clientAdapter && current.clientAdapter !== token.adapter)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (endpointKey && !matchesCurrentEndpoint(current, endpointKey))
|
|
115
|
+
return undefined;
|
|
116
|
+
try {
|
|
117
|
+
return current.$client;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const resolvedEndpointKey = endpointKey ?? endpointKeyFromContext(context);
|
|
124
|
+
if (!resolvedEndpointKey || !context.project)
|
|
125
|
+
return undefined;
|
|
126
|
+
const projection = context.project(adapterFeatureId);
|
|
127
|
+
if (!isAdapterIndex(projection))
|
|
128
|
+
return undefined;
|
|
129
|
+
return projection.findClient(token.adapter, resolvedEndpointKey);
|
|
130
|
+
}
|
|
131
|
+
function currentClientSource(context) {
|
|
132
|
+
for (const candidate of [context, context.message, context.input]) {
|
|
133
|
+
if (candidate && typeof candidate === 'object'
|
|
134
|
+
&& ('$client' in candidate || 'clientAdapter' in candidate)) {
|
|
135
|
+
return candidate;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
function matchesCurrentEndpoint(source, endpointKey) {
|
|
141
|
+
const candidates = [source.endpointId, conversationEndpointId(source.conversation)]
|
|
142
|
+
.filter((value) => typeof value === 'string' && value.length > 0);
|
|
143
|
+
return candidates.length === 0 || candidates.includes(endpointKey);
|
|
144
|
+
}
|
|
145
|
+
function endpointKeyFromContext(context) {
|
|
146
|
+
if (typeof context.endpoint === 'string' && context.endpoint.length > 0)
|
|
147
|
+
return context.endpoint;
|
|
148
|
+
const origin = context.origin;
|
|
149
|
+
if (origin?.kind === 'im' && typeof origin.endpoint === 'string' && origin.endpoint.length > 0) {
|
|
150
|
+
return origin.endpoint;
|
|
151
|
+
}
|
|
152
|
+
return conversationEndpointId(context.conversation)
|
|
153
|
+
?? conversationEndpointId(context.input?.conversation);
|
|
154
|
+
}
|
|
155
|
+
function conversationEndpointId(value) {
|
|
156
|
+
if (!value || typeof value !== 'object')
|
|
157
|
+
return undefined;
|
|
158
|
+
const endpoint = value.endpoint;
|
|
159
|
+
if (!endpoint || typeof endpoint !== 'object')
|
|
160
|
+
return undefined;
|
|
161
|
+
const id = endpoint.id;
|
|
162
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
163
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { ConversationRef, MessageRef } from '@zhin.js/im-contract';
|
|
|
2
2
|
/**
|
|
3
3
|
* Transport-neutral control plane for a live endpoint.
|
|
4
4
|
*
|
|
5
|
-
* Sending belongs to
|
|
5
|
+
* Sending belongs to Endpoint.send(). This port intentionally owns
|
|
6
6
|
* operations that address an existing platform message, so Core never needs
|
|
7
7
|
* to know a protocol's method names or identifier layout.
|
|
8
8
|
*/
|
|
@@ -22,25 +22,19 @@
|
|
|
22
22
|
* 构造器里 `this.#lifecycle = createEndpointLifecycle({ name: config.id, reconnect, heartbeat })`。
|
|
23
23
|
* 2. `start()` 改为:
|
|
24
24
|
* ```ts
|
|
25
|
-
* this.#
|
|
26
|
-
*
|
|
27
|
-
* await
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* ws.on('close', (code, reason) => { handle.notifyClosed(...); rejectIfNotSettled(...); });
|
|
33
|
-
* ws.on('error', (err) => rejectIfNotSettled(err));
|
|
34
|
-
* });
|
|
25
|
+
* await this.#lifecycle.start(async (handle) => {
|
|
26
|
+
* this.#handle = handle; // 供 ws 'close' 回调引用
|
|
27
|
+
* await new Promise<void>((resolve, reject) => {
|
|
28
|
+
* const ws = createWebSocket(...); this.#ws = ws;
|
|
29
|
+
* ws.on('open', () => { this.#lifecycle.startHeartbeat(() => beat(), interval); resolve(); });
|
|
30
|
+
* ws.on('close', (code, reason) => { handle.notifyClosed(...); rejectIfNotSettled(...); });
|
|
31
|
+
* ws.on('error', (err) => rejectIfNotSettled(err));
|
|
35
32
|
* });
|
|
36
|
-
* }
|
|
37
|
-
* this.#unregisterAgent?.(); this.#unregisterAgent = undefined; // 反注册对称
|
|
38
|
-
* throw err;
|
|
39
|
-
* }
|
|
33
|
+
* });
|
|
40
34
|
* ```
|
|
41
|
-
* start 失败复位由基座保证;
|
|
35
|
+
* start 失败复位由基座保证;Client 由 Endpoint 直接持有,不建立旁路 registry。
|
|
42
36
|
* 3. `stop()` 改为:先 `await this.#lifecycle.stop()`(清定时器 + 强关 ws + 竞态 settle),
|
|
43
|
-
* 再做适配器专有清理(rejectAllPending、deduper.clear
|
|
37
|
+
* 再做适配器专有清理(rejectAllPending、deduper.clear)。
|
|
44
38
|
* 4. `handle.onForceClose(() => this.#ws?.close())` 在每次拿到新 socket 后注册,
|
|
45
39
|
* 供心跳看门狗主动断开;ws 'message'/'pong' 回调里调 `notifyHeartbeatAck()` 喂狗。
|
|
46
40
|
* 5. 退避参数由配置映射:reconnect_interval → initialIntervalMs,可按需覆盖
|