@zhin.js/adapter 1.1.11 → 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 +56 -4
- package/lib/adapter-index.d.ts +19 -6
- package/lib/adapter-index.js +74 -14
- package/lib/definition.d.ts +46 -27
- package/lib/definition.js +48 -5
- package/lib/endpoint-client.d.ts +62 -0
- package/lib/endpoint-client.js +163 -0
- package/lib/endpoint-control.d.ts +5 -1
- package/lib/endpoint-control.js +37 -0
- 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 +100 -31
- package/src/definition.ts +79 -29
- package/src/endpoint-client.ts +250 -0
- package/src/endpoint-control.ts +45 -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,12 +22,39 @@ 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
|
|
28
50
|
`operations` for optional actions such as `recall`, `edit`, `reaction`, and
|
|
29
|
-
`typing`.
|
|
30
|
-
|
|
51
|
+
`typing`. `operations` accepts either a static list or a resolver receiving the
|
|
52
|
+
concrete `AdapterContext`; use the resolver when connection modes expose different
|
|
53
|
+
operations. `AdapterIndex` resolves, freezes, and exposes the exact set for every
|
|
54
|
+
expanded Endpoint. Runtime callers should query the resulting `EndpointCapabilities`
|
|
55
|
+
instead of probing optional endpoint methods. Declarations and the explicit
|
|
56
|
+
`EndpointControl` port are validated in both directions, so hidden or unimplemented
|
|
57
|
+
operations fail candidate generation before commit. The zero-dependency types live in
|
|
31
58
|
[`@zhin.js/im-contract`](../im-contract/README.md).
|
|
32
59
|
|
|
33
60
|
Framework-facing outbound code carries a structured `ConversationRef`.
|
|
@@ -38,14 +65,39 @@ non-empty platform message id. IM Runtime alone wraps that id as a structured
|
|
|
38
65
|
|
|
39
66
|
## Endpoint Control Port
|
|
40
67
|
|
|
41
|
-
`
|
|
68
|
+
`Endpoint.control` owns actions addressed to an existing message:
|
|
42
69
|
`recall`, `addReaction`, and `removeReaction`. IM Core consumes only this port;
|
|
43
70
|
adapter-specific method names and compound message ids stay at the protocol
|
|
44
71
|
boundary.
|
|
45
72
|
|
|
46
73
|
New adapters should provide `control` directly and declare matching
|
|
47
74
|
`operations`. Protocol-specific methods and compound string identifiers are not
|
|
48
|
-
inspected
|
|
75
|
+
inspected by the runtime. `createRecallEndpointControl()` bridges the common
|
|
76
|
+
platform `recall(messageId)` shape without leaking that shape into Core.
|
|
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。
|
|
49
101
|
|
|
50
102
|
## Endpoint 生命周期基座(createEndpointLifecycle)
|
|
51
103
|
|
package/lib/adapter-index.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { generationAdmissionSource, type CapabilityId, type CapabilitySlot, type GenerationAdmissionGate, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import 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';
|
|
5
|
+
import { type EndpointControl } from './endpoint-control.js';
|
|
4
6
|
import { type EndpointContentResolveContext } from './endpoint-content.js';
|
|
5
|
-
import type { ConversationReference, ConversationResolution } from '@zhin.js/im-contract';
|
|
7
|
+
import type { ConversationReference, ConversationResolution, EndpointCapabilities } from '@zhin.js/im-contract';
|
|
6
8
|
export interface AdapterDescriptor {
|
|
7
9
|
readonly id: CapabilityId;
|
|
8
10
|
readonly owner: PluginId;
|
|
9
11
|
readonly name: string;
|
|
10
12
|
readonly source: string;
|
|
11
13
|
readonly capabilities: readonly AdapterCapability[];
|
|
14
|
+
readonly operations: readonly AdapterOperation[];
|
|
12
15
|
}
|
|
13
16
|
/** Console / Host-facing endpoint row (connected = admission open). */
|
|
14
17
|
export interface AdapterEndpointSummary extends AdapterDescriptor {
|
|
@@ -32,11 +35,21 @@ export declare class AdapterIndex {
|
|
|
32
35
|
* Matches local name, capability id, or owner path segments.
|
|
33
36
|
*/
|
|
34
37
|
resolve(adapter: string, endpointKey: string): CapabilityId | undefined;
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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;
|
|
39
48
|
owner(id: CapabilityId): PluginId;
|
|
49
|
+
/** Exact, serializable capabilities for one concrete Endpoint. */
|
|
50
|
+
capabilities(id: CapabilityId): EndpointCapabilities;
|
|
51
|
+
/** Returns the control port only when the concrete Endpoint declared the operation and is active. */
|
|
52
|
+
control(id: CapabilityId, operation: AdapterOperation): EndpointControl | undefined;
|
|
40
53
|
/**
|
|
41
54
|
* Endpoint 的消息段能力声明(出站协商降级依据);
|
|
42
55
|
* 未声明或未知 id 返回 undefined(调用方按历史行为处理)。
|
package/lib/adapter-index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { DisposeStack, GenerationCompensationError, createGenerationAdmissionGate, generationAdmissionSource, } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import { createCapabilityContext } from '@zhin.js/feature-kit';
|
|
3
|
+
import { endpointCapabilitiesOf, resolveAdapterOperations, } from './definition.js';
|
|
4
|
+
import { bindEndpoint, isEndpoint } from './endpoint.js';
|
|
3
5
|
import { listEndpointManagementCapabilities, } from './endpoint-management.js';
|
|
4
|
-
import { assertDeclaredEndpointOperations } from './endpoint-control.js';
|
|
6
|
+
import { assertDeclaredEndpointOperations, endpointControlOf, } from './endpoint-control.js';
|
|
5
7
|
import { endpointContentOf } from './endpoint-content.js';
|
|
6
8
|
export class AdapterIndex {
|
|
7
9
|
$projection = 'zhin.adapter-index/1';
|
|
@@ -21,7 +23,7 @@ export class AdapterIndex {
|
|
|
21
23
|
for (const slot of [...slots].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
22
24
|
signal.throwIfAborted();
|
|
23
25
|
for (const expansion of expandEndpointConfigs(slot, snapshot)) {
|
|
24
|
-
const
|
|
26
|
+
const created = await createEndpoint(slot, snapshot, admission, signal, expansion);
|
|
25
27
|
signal.throwIfAborted();
|
|
26
28
|
records.push({
|
|
27
29
|
id: expansion.id,
|
|
@@ -31,7 +33,8 @@ export class AdapterIndex {
|
|
|
31
33
|
name: expansion.endpointId,
|
|
32
34
|
source: slot.source,
|
|
33
35
|
capabilities: slot.definition.capabilities,
|
|
34
|
-
|
|
36
|
+
operations: created.operations,
|
|
37
|
+
endpoint: created.endpoint,
|
|
35
38
|
...(slot.definition.segments ? { segments: slot.definition.segments } : {}),
|
|
36
39
|
started: false,
|
|
37
40
|
open: false,
|
|
@@ -59,6 +62,7 @@ export class AdapterIndex {
|
|
|
59
62
|
name: endpointLiveName(record.endpoint) ?? record.name,
|
|
60
63
|
source: record.source,
|
|
61
64
|
capabilities: record.capabilities,
|
|
65
|
+
operations: record.operations,
|
|
62
66
|
connected: record.open && !record.stopped,
|
|
63
67
|
status: record.open && !record.stopped ? 'online' : 'offline',
|
|
64
68
|
phase: endpointPhase(record),
|
|
@@ -79,21 +83,71 @@ export class AdapterIndex {
|
|
|
79
83
|
const exact = matches.find((record) => record.name === endpointKey);
|
|
80
84
|
return exact?.id ?? matches[0]?.id;
|
|
81
85
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
*/
|
|
85
|
-
instance(adapter, endpointKey) {
|
|
86
|
+
/** Resolve the framework-owned Endpoint for internal Host control ports. */
|
|
87
|
+
connection(adapter, endpointKey) {
|
|
86
88
|
const id = this.resolve(adapter, endpointKey);
|
|
87
89
|
if (!id)
|
|
88
90
|
return undefined;
|
|
89
91
|
return this.#records.get(id)?.endpoint;
|
|
90
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
|
+
}
|
|
91
129
|
owner(id) {
|
|
92
130
|
const record = this.#records.get(id);
|
|
93
131
|
if (!record)
|
|
94
132
|
throw new Error(`Unknown Adapter Endpoint: ${id}`);
|
|
95
133
|
return record.owner;
|
|
96
134
|
}
|
|
135
|
+
/** Exact, serializable capabilities for one concrete Endpoint. */
|
|
136
|
+
capabilities(id) {
|
|
137
|
+
const record = this.#records.get(id);
|
|
138
|
+
if (!record)
|
|
139
|
+
throw new Error(`Unknown Adapter Endpoint: ${id}`);
|
|
140
|
+
return endpointCapabilitiesOf(record, record.operations);
|
|
141
|
+
}
|
|
142
|
+
/** Returns the control port only when the concrete Endpoint declared the operation and is active. */
|
|
143
|
+
control(id, operation) {
|
|
144
|
+
const record = this.#records.get(id);
|
|
145
|
+
if (!record || !record.started || record.stopped)
|
|
146
|
+
return undefined;
|
|
147
|
+
if (!record.operations.includes(operation))
|
|
148
|
+
return undefined;
|
|
149
|
+
return endpointControlOf(record.endpoint);
|
|
150
|
+
}
|
|
97
151
|
/**
|
|
98
152
|
* Endpoint 的消息段能力声明(出站协商降级依据);
|
|
99
153
|
* 未声明或未知 id 返回 undefined(调用方按历史行为处理)。
|
|
@@ -130,6 +184,9 @@ export class AdapterIndex {
|
|
|
130
184
|
signal.throwIfAborted();
|
|
131
185
|
if (record.stopped)
|
|
132
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
|
+
}
|
|
133
190
|
record.started = true;
|
|
134
191
|
}
|
|
135
192
|
}
|
|
@@ -230,7 +287,7 @@ function matchesEndpoint(record, adapter, endpointKey) {
|
|
|
230
287
|
|| record.id.endsWith(`/${adapter}`)
|
|
231
288
|
|| record.owner === adapter
|
|
232
289
|
|| record.owner.endsWith(`/${adapter}`);
|
|
233
|
-
//
|
|
290
|
+
// The live Endpoint identity is the bot runtime id (e.g. ICQQ uin). Host /
|
|
234
291
|
// activity-feedback resolve with that id; slot.localName alone is not enough
|
|
235
292
|
// when multiple plugin instances share localName "icqq".
|
|
236
293
|
const liveName = endpointLiveName(record.endpoint);
|
|
@@ -252,8 +309,8 @@ function endpointPhase(record) {
|
|
|
252
309
|
return 'pending';
|
|
253
310
|
}
|
|
254
311
|
function assertEndpoint(value, id) {
|
|
255
|
-
if (!value
|
|
256
|
-
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`);
|
|
257
314
|
}
|
|
258
315
|
}
|
|
259
316
|
/**
|
|
@@ -300,18 +357,21 @@ function expandEndpointConfigs(slot, snapshot) {
|
|
|
300
357
|
})));
|
|
301
358
|
}
|
|
302
359
|
async function createEndpoint(slot, snapshot, admission, signal, expansion) {
|
|
303
|
-
const
|
|
360
|
+
const context = Object.freeze({
|
|
304
361
|
...createCapabilityContext(snapshot, slot.owner, admission, signal),
|
|
305
362
|
...(expansion?.config ? { config: expansion.config } : {}),
|
|
306
363
|
id: expansion?.id ?? slot.id,
|
|
307
364
|
name: slot.localName,
|
|
308
|
-
})
|
|
365
|
+
});
|
|
366
|
+
const operations = resolveAdapterOperations(slot.definition, context);
|
|
367
|
+
const endpoint = await slot.definition.create(context);
|
|
309
368
|
assertEndpoint(endpoint, expansion?.id ?? slot.id);
|
|
369
|
+
bindEndpoint(endpoint, context, admission);
|
|
310
370
|
if (slot.definition.capabilities.includes('outbound') && typeof endpoint.send !== 'function') {
|
|
311
371
|
throw new TypeError(`Adapter Endpoint ${String(expansion?.id ?? slot.id)} declares outbound but send() is missing`);
|
|
312
372
|
}
|
|
313
|
-
assertDeclaredEndpointOperations(endpoint,
|
|
314
|
-
return endpoint;
|
|
373
|
+
assertDeclaredEndpointOperations(endpoint, operations, String(expansion?.id ?? slot.id));
|
|
374
|
+
return Object.freeze({ endpoint, operations });
|
|
315
375
|
}
|
|
316
376
|
async function stopRecords(records, primaryError) {
|
|
317
377
|
const stack = new DisposeStack();
|
package/lib/definition.d.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
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. */
|
|
10
16
|
export type AdapterOperation = Exclude<EndpointOperation, 'send'>;
|
|
17
|
+
/** Resolve operations for one concrete Endpoint configuration. */
|
|
18
|
+
export type AdapterOperationDeclaration<TConfig = unknown> = readonly AdapterOperation[] | ((context: AdapterContext<TConfig>) => readonly AdapterOperation[]);
|
|
11
19
|
/** 端点可消费的出站媒体来源形式。 */
|
|
12
20
|
export type AdapterOutboundMedia = 'url' | 'path' | 'base64' | 'upload';
|
|
13
21
|
/** 交互段(卡片/按钮等富交互)的端点消费方式。 */
|
|
@@ -19,24 +27,9 @@ export interface EndpointSendRequest {
|
|
|
19
27
|
readonly conversation: ConversationRef;
|
|
20
28
|
readonly payload: unknown;
|
|
21
29
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/** Optional platform-neutral control surface for existing messages. */
|
|
26
|
-
readonly control?: EndpointControl;
|
|
27
|
-
/** Optional canonical resolver for message, merged-forward and media references. */
|
|
28
|
-
readonly content?: EndpointContentPort;
|
|
29
|
-
/** Required readiness; must observe abort and settle before rollback returns. */
|
|
30
|
-
start?(signal: AbortSignal): void | Promise<void>;
|
|
31
|
-
/** Opens Endpoint-local flow behind the candidate generation admission gate. */
|
|
32
|
-
open?(): void;
|
|
33
|
-
/** Stops new inbound events while preserving in-flight work. */
|
|
34
|
-
close?(): void | Promise<void>;
|
|
35
|
-
/** Releases transport resources. Calls must be idempotent. */
|
|
36
|
-
stop?(): void | Promise<void>;
|
|
37
|
-
/** Platform message id. Core wraps it in the canonical MessageRef/DeliveryReceipt. */
|
|
38
|
-
send?(request: EndpointSendRequest): string | Promise<string>;
|
|
39
|
-
}
|
|
30
|
+
/**
|
|
31
|
+
* Generation-owned construction context for one runtime Endpoint.
|
|
32
|
+
*/
|
|
40
33
|
export interface AdapterContext<TConfig = unknown> extends CapabilityContext<TConfig> {
|
|
41
34
|
readonly id: CapabilityId;
|
|
42
35
|
readonly name: string;
|
|
@@ -72,7 +65,8 @@ export interface AdapterSegmentPolicy {
|
|
|
72
65
|
/** `native` preserves Markdown for the endpoint codec; `text` strips formatting in Core. */
|
|
73
66
|
readonly markdown?: AdapterMarkdownMode;
|
|
74
67
|
}
|
|
75
|
-
export interface AdapterDefinition<TConfig = unknown> {
|
|
68
|
+
export interface AdapterDefinition<TConfig = unknown, TClient = unknown> {
|
|
69
|
+
/** @internal Runtime feature brand. */
|
|
76
70
|
readonly $feature: typeof adapterBrand;
|
|
77
71
|
readonly capabilities: readonly AdapterCapability[];
|
|
78
72
|
/**
|
|
@@ -80,18 +74,43 @@ export interface AdapterDefinition<TConfig = unknown> {
|
|
|
80
74
|
* `capabilities: ['outbound']`; a method existing on an endpoint is not a
|
|
81
75
|
* capability declaration.
|
|
82
76
|
*/
|
|
83
|
-
readonly operations?:
|
|
77
|
+
readonly operations?: AdapterOperationDeclaration<TConfig>;
|
|
84
78
|
/** 可选:端点消息段能力声明(出站协商降级挂载点)。 */
|
|
85
79
|
readonly segments?: AdapterSegmentPolicy;
|
|
86
|
-
create(context: AdapterContext<TConfig>):
|
|
80
|
+
create(context: AdapterContext<TConfig>): Endpoint<TClient> | Promise<Endpoint<TClient>>;
|
|
87
81
|
}
|
|
88
82
|
declare module '@zhin.js/plugin-runtime' {
|
|
89
83
|
interface PluginSetupContext<TConfig = unknown> {
|
|
90
84
|
addAdapter(localName: string, definition: AdapterDefinition<TConfig>): void;
|
|
91
85
|
}
|
|
92
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
|
+
*/
|
|
93
104
|
export declare function defineAdapter<TConfig = unknown>(definition: Omit<AdapterDefinition<TConfig>, '$feature'>): Readonly<AdapterDefinition<TConfig>>;
|
|
94
|
-
/**
|
|
95
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Converts the definition's compact authoring form into the Runtime contract.
|
|
107
|
+
* @internal Adapter projection helper.
|
|
108
|
+
*/
|
|
109
|
+
export declare function endpointCapabilitiesOf(definition: Pick<AdapterDefinition, 'capabilities' | 'operations'>, resolvedOperations?: readonly AdapterOperation[]): EndpointCapabilities;
|
|
110
|
+
/**
|
|
111
|
+
* Resolve and validate the operation declaration for one concrete Endpoint.
|
|
112
|
+
* @internal Adapter projection helper.
|
|
113
|
+
*/
|
|
114
|
+
export declare function resolveAdapterOperations<TConfig>(definition: Pick<AdapterDefinition<TConfig>, 'operations'>, context: AdapterContext<TConfig>): readonly AdapterOperation[];
|
|
115
|
+
/** @internal Runtime validation for convention-discovered modules. */
|
|
96
116
|
export declare function parseAdapterDefinition(value: unknown): AdapterDefinition;
|
|
97
|
-
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');
|
|
@@ -14,7 +33,9 @@ export function defineAdapter(definition) {
|
|
|
14
33
|
throw new TypeError('Adapter capabilities must contain inbound and/or outbound');
|
|
15
34
|
}
|
|
16
35
|
const segments = normalizeSegmentPolicy(definition.segments);
|
|
17
|
-
const operations =
|
|
36
|
+
const operations = typeof definition.operations === 'function'
|
|
37
|
+
? definition.operations
|
|
38
|
+
: normalizeOperations(definition.operations);
|
|
18
39
|
return Object.freeze({
|
|
19
40
|
...definition,
|
|
20
41
|
$feature: adapterBrand,
|
|
@@ -23,15 +44,34 @@ export function defineAdapter(definition) {
|
|
|
23
44
|
...(segments ? { segments } : {}),
|
|
24
45
|
});
|
|
25
46
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Converts the definition's compact authoring form into the Runtime contract.
|
|
49
|
+
* @internal Adapter projection helper.
|
|
50
|
+
*/
|
|
51
|
+
export function endpointCapabilitiesOf(definition, resolvedOperations) {
|
|
52
|
+
if (typeof definition.operations === 'function' && resolvedOperations === undefined) {
|
|
53
|
+
throw new TypeError('Dynamic Adapter operations must be resolved for one Endpoint');
|
|
54
|
+
}
|
|
55
|
+
const declared = resolvedOperations
|
|
56
|
+
?? (Array.isArray(definition.operations) ? definition.operations : undefined);
|
|
57
|
+
const operations = declared?.reduce((result, operation) => ({ ...result, [operation]: true }), {});
|
|
29
58
|
return Object.freeze({
|
|
30
59
|
inbound: definition.capabilities.includes('inbound'),
|
|
31
60
|
outbound: definition.capabilities.includes('outbound'),
|
|
32
61
|
...(operations && Object.keys(operations).length > 0 ? { operations: Object.freeze(operations) } : {}),
|
|
33
62
|
});
|
|
34
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve and validate the operation declaration for one concrete Endpoint.
|
|
66
|
+
* @internal Adapter projection helper.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveAdapterOperations(definition, context) {
|
|
69
|
+
const declaration = definition.operations;
|
|
70
|
+
const operations = typeof declaration === 'function'
|
|
71
|
+
? declaration(context)
|
|
72
|
+
: declaration;
|
|
73
|
+
return normalizeOperations(operations) ?? Object.freeze([]);
|
|
74
|
+
}
|
|
35
75
|
function normalizeOperations(operations) {
|
|
36
76
|
if (operations === undefined)
|
|
37
77
|
return undefined;
|
|
@@ -81,6 +121,7 @@ function normalizeSegmentPolicy(policy) {
|
|
|
81
121
|
...(policy.markdown ? { markdown: policy.markdown } : {}),
|
|
82
122
|
});
|
|
83
123
|
}
|
|
124
|
+
/** @internal Runtime validation for convention-discovered modules. */
|
|
84
125
|
export function parseAdapterDefinition(value) {
|
|
85
126
|
if (!value || typeof value !== 'object')
|
|
86
127
|
throw invalidAdapter();
|
|
@@ -93,7 +134,9 @@ export function parseAdapterDefinition(value) {
|
|
|
93
134
|
throw invalidAdapter();
|
|
94
135
|
// defineAdapter 已校验过形状;外部手工构造的 definition 也在此兜底
|
|
95
136
|
normalizeSegmentPolicy(definition.segments);
|
|
96
|
-
|
|
137
|
+
if (typeof definition.operations !== 'function') {
|
|
138
|
+
normalizeOperations(definition.operations);
|
|
139
|
+
}
|
|
97
140
|
return definition;
|
|
98
141
|
}
|
|
99
142
|
function 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>;
|