@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 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`. Runtime callers should query the resulting `EndpointCapabilities`
30
- instead of probing optional endpoint methods. The zero-dependency types live in
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
- `EndpointInstance.control` owns actions addressed to an existing message:
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 or adapted by the runtime.
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
 
@@ -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 { AdapterCapability, AdapterDefinition, AdapterSegmentPolicy, EndpointInstance, EndpointSendRequest } from './definition.js';
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
- * Resolve a live EndpointInstance for Host-side side channels (reactions, etc.).
37
- */
38
- instance(adapter: string, endpointKey: string): EndpointInstance | undefined;
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(调用方按历史行为处理)。
@@ -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 endpoint = await createEndpoint(slot, snapshot, admission, signal, expansion);
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
- endpoint,
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
- * Resolve a live EndpointInstance for Host-side side channels (reactions, etc.).
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
- // Live EndpointInstance.name is the bot runtime id (e.g. ICQQ uin). Host /
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 || typeof value !== 'object') {
256
- throw new TypeError(`Adapter ${id} create() must return an Endpoint instance`);
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 endpoint = await slot.definition.create(Object.freeze({
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, slot.definition.operations, String(expansion?.id ?? slot.id));
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();
@@ -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 { EndpointManagement } from './endpoint-management.js';
5
- import type { EndpointControl } from './endpoint-control.js';
6
- import type { EndpointContentPort } from './endpoint-content.js';
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
- export interface EndpointInstance {
23
- /** Optional platform-neutral Console/Host management surface. */
24
- readonly management?: EndpointManagement;
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?: readonly AdapterOperation[];
77
+ readonly operations?: AdapterOperationDeclaration<TConfig>;
84
78
  /** 可选:端点消息段能力声明(出站协商降级挂载点)。 */
85
79
  readonly segments?: AdapterSegmentPolicy;
86
- create(context: AdapterContext<TConfig>): EndpointInstance | Promise<EndpointInstance>;
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
- /** Converts the definition's compact authoring form into the public contract. */
95
- export declare function endpointCapabilitiesOf(definition: Pick<AdapterDefinition, 'capabilities' | 'operations'>): EndpointCapabilities;
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 = normalizeOperations(definition.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
- /** Converts the definition's compact authoring form into the public contract. */
27
- export function endpointCapabilitiesOf(definition) {
28
- const operations = definition.operations?.reduce((result, operation) => ({ ...result, [operation]: true }), {});
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
- normalizeOperations(definition.operations);
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>;