@zhin.js/adapter 1.1.11 → 1.2.0

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
@@ -26,8 +26,13 @@ AdapterIndex 和 generation lifecycle 管理。
26
26
 
27
27
  Adapter definitions declare `capabilities` for inbound/outbound admission and
28
28
  `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
29
+ `typing`. `operations` accepts either a static list or a resolver receiving the
30
+ concrete `AdapterContext`; use the resolver when connection modes expose different
31
+ operations. `AdapterIndex` resolves, freezes, and exposes the exact set for every
32
+ expanded Endpoint. Runtime callers should query the resulting `EndpointCapabilities`
33
+ instead of probing optional endpoint methods. Declarations and the explicit
34
+ `EndpointControl` port are validated in both directions, so hidden or unimplemented
35
+ operations fail candidate generation before commit. The zero-dependency types live in
31
36
  [`@zhin.js/im-contract`](../im-contract/README.md).
32
37
 
33
38
  Framework-facing outbound code carries a structured `ConversationRef`.
@@ -45,7 +50,8 @@ boundary.
45
50
 
46
51
  New adapters should provide `control` directly and declare matching
47
52
  `operations`. Protocol-specific methods and compound string identifiers are not
48
- inspected or adapted by the runtime.
53
+ inspected by the runtime. `createRecallEndpointControl()` bridges the common
54
+ platform `recall(messageId)` shape without leaking that shape into Core.
49
55
 
50
56
  ## Endpoint 生命周期基座(createEndpointLifecycle)
51
57
 
@@ -1,14 +1,16 @@
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 EndpointInstance, type EndpointSendRequest } from './definition.js';
3
3
  import { type EndpointManagementCapability } from './endpoint-management.js';
4
+ import { type EndpointControl } from './endpoint-control.js';
4
5
  import { type EndpointContentResolveContext } from './endpoint-content.js';
5
- import type { ConversationReference, ConversationResolution } from '@zhin.js/im-contract';
6
+ import type { ConversationReference, ConversationResolution, EndpointCapabilities } from '@zhin.js/im-contract';
6
7
  export interface AdapterDescriptor {
7
8
  readonly id: CapabilityId;
8
9
  readonly owner: PluginId;
9
10
  readonly name: string;
10
11
  readonly source: string;
11
12
  readonly capabilities: readonly AdapterCapability[];
13
+ readonly operations: readonly AdapterOperation[];
12
14
  }
13
15
  /** Console / Host-facing endpoint row (connected = admission open). */
14
16
  export interface AdapterEndpointSummary extends AdapterDescriptor {
@@ -37,6 +39,10 @@ export declare class AdapterIndex {
37
39
  */
38
40
  instance(adapter: string, endpointKey: string): EndpointInstance | undefined;
39
41
  owner(id: CapabilityId): PluginId;
42
+ /** Exact, serializable capabilities for one concrete Endpoint. */
43
+ capabilities(id: CapabilityId): EndpointCapabilities;
44
+ /** Returns the control port only when the concrete Endpoint declared the operation and is active. */
45
+ control(id: CapabilityId, operation: AdapterOperation): EndpointControl | undefined;
40
46
  /**
41
47
  * Endpoint 的消息段能力声明(出站协商降级依据);
42
48
  * 未声明或未知 id 返回 undefined(调用方按历史行为处理)。
@@ -1,7 +1,8 @@
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';
3
4
  import { listEndpointManagementCapabilities, } from './endpoint-management.js';
4
- import { assertDeclaredEndpointOperations } from './endpoint-control.js';
5
+ import { assertDeclaredEndpointOperations, endpointControlOf, } from './endpoint-control.js';
5
6
  import { endpointContentOf } from './endpoint-content.js';
6
7
  export class AdapterIndex {
7
8
  $projection = 'zhin.adapter-index/1';
@@ -21,7 +22,7 @@ export class AdapterIndex {
21
22
  for (const slot of [...slots].sort((left, right) => left.id.localeCompare(right.id))) {
22
23
  signal.throwIfAborted();
23
24
  for (const expansion of expandEndpointConfigs(slot, snapshot)) {
24
- const endpoint = await createEndpoint(slot, snapshot, admission, signal, expansion);
25
+ const created = await createEndpoint(slot, snapshot, admission, signal, expansion);
25
26
  signal.throwIfAborted();
26
27
  records.push({
27
28
  id: expansion.id,
@@ -31,7 +32,8 @@ export class AdapterIndex {
31
32
  name: expansion.endpointId,
32
33
  source: slot.source,
33
34
  capabilities: slot.definition.capabilities,
34
- endpoint,
35
+ operations: created.operations,
36
+ endpoint: created.endpoint,
35
37
  ...(slot.definition.segments ? { segments: slot.definition.segments } : {}),
36
38
  started: false,
37
39
  open: false,
@@ -59,6 +61,7 @@ export class AdapterIndex {
59
61
  name: endpointLiveName(record.endpoint) ?? record.name,
60
62
  source: record.source,
61
63
  capabilities: record.capabilities,
64
+ operations: record.operations,
62
65
  connected: record.open && !record.stopped,
63
66
  status: record.open && !record.stopped ? 'online' : 'offline',
64
67
  phase: endpointPhase(record),
@@ -94,6 +97,22 @@ export class AdapterIndex {
94
97
  throw new Error(`Unknown Adapter Endpoint: ${id}`);
95
98
  return record.owner;
96
99
  }
100
+ /** Exact, serializable capabilities for one concrete Endpoint. */
101
+ capabilities(id) {
102
+ const record = this.#records.get(id);
103
+ if (!record)
104
+ throw new Error(`Unknown Adapter Endpoint: ${id}`);
105
+ return endpointCapabilitiesOf(record, record.operations);
106
+ }
107
+ /** Returns the control port only when the concrete Endpoint declared the operation and is active. */
108
+ control(id, operation) {
109
+ const record = this.#records.get(id);
110
+ if (!record || !record.started || record.stopped)
111
+ return undefined;
112
+ if (!record.operations.includes(operation))
113
+ return undefined;
114
+ return endpointControlOf(record.endpoint);
115
+ }
97
116
  /**
98
117
  * Endpoint 的消息段能力声明(出站协商降级依据);
99
118
  * 未声明或未知 id 返回 undefined(调用方按历史行为处理)。
@@ -300,18 +319,20 @@ function expandEndpointConfigs(slot, snapshot) {
300
319
  })));
301
320
  }
302
321
  async function createEndpoint(slot, snapshot, admission, signal, expansion) {
303
- const endpoint = await slot.definition.create(Object.freeze({
322
+ const context = Object.freeze({
304
323
  ...createCapabilityContext(snapshot, slot.owner, admission, signal),
305
324
  ...(expansion?.config ? { config: expansion.config } : {}),
306
325
  id: expansion?.id ?? slot.id,
307
326
  name: slot.localName,
308
- }));
327
+ });
328
+ const operations = resolveAdapterOperations(slot.definition, context);
329
+ const endpoint = await slot.definition.create(context);
309
330
  assertEndpoint(endpoint, expansion?.id ?? slot.id);
310
331
  if (slot.definition.capabilities.includes('outbound') && typeof endpoint.send !== 'function') {
311
332
  throw new TypeError(`Adapter Endpoint ${String(expansion?.id ?? slot.id)} declares outbound but send() is missing`);
312
333
  }
313
- assertDeclaredEndpointOperations(endpoint, slot.definition.operations, String(expansion?.id ?? slot.id));
314
- return endpoint;
334
+ assertDeclaredEndpointOperations(endpoint, operations, String(expansion?.id ?? slot.id));
335
+ return Object.freeze({ endpoint, operations });
315
336
  }
316
337
  async function stopRecords(records, primaryError) {
317
338
  const stack = new DisposeStack();
@@ -8,6 +8,8 @@ declare const adapterBrand: "zhin.adapter/1";
8
8
  export type AdapterCapability = 'inbound' | 'outbound';
9
9
  /** Operations beyond sending, declared by an Adapter definition. */
10
10
  export type AdapterOperation = Exclude<EndpointOperation, 'send'>;
11
+ /** Resolve operations for one concrete Endpoint configuration. */
12
+ export type AdapterOperationDeclaration<TConfig = unknown> = readonly AdapterOperation[] | ((context: AdapterContext<TConfig>) => readonly AdapterOperation[]);
11
13
  /** 端点可消费的出站媒体来源形式。 */
12
14
  export type AdapterOutboundMedia = 'url' | 'path' | 'base64' | 'upload';
13
15
  /** 交互段(卡片/按钮等富交互)的端点消费方式。 */
@@ -80,7 +82,7 @@ export interface AdapterDefinition<TConfig = unknown> {
80
82
  * `capabilities: ['outbound']`; a method existing on an endpoint is not a
81
83
  * capability declaration.
82
84
  */
83
- readonly operations?: readonly AdapterOperation[];
85
+ readonly operations?: AdapterOperationDeclaration<TConfig>;
84
86
  /** 可选:端点消息段能力声明(出站协商降级挂载点)。 */
85
87
  readonly segments?: AdapterSegmentPolicy;
86
88
  create(context: AdapterContext<TConfig>): EndpointInstance | Promise<EndpointInstance>;
@@ -92,6 +94,8 @@ declare module '@zhin.js/plugin-runtime' {
92
94
  }
93
95
  export declare function defineAdapter<TConfig = unknown>(definition: Omit<AdapterDefinition<TConfig>, '$feature'>): Readonly<AdapterDefinition<TConfig>>;
94
96
  /** Converts the definition's compact authoring form into the public contract. */
95
- export declare function endpointCapabilitiesOf(definition: Pick<AdapterDefinition, 'capabilities' | 'operations'>): EndpointCapabilities;
97
+ export declare function endpointCapabilitiesOf(definition: Pick<AdapterDefinition, 'capabilities' | 'operations'>, resolvedOperations?: readonly AdapterOperation[]): EndpointCapabilities;
98
+ /** Resolve and validate the operation declaration for one concrete Endpoint. */
99
+ export declare function resolveAdapterOperations<TConfig>(definition: Pick<AdapterDefinition<TConfig>, 'operations'>, context: AdapterContext<TConfig>): readonly AdapterOperation[];
96
100
  export declare function parseAdapterDefinition(value: unknown): AdapterDefinition;
97
101
  export {};
package/lib/definition.js CHANGED
@@ -14,7 +14,9 @@ export function defineAdapter(definition) {
14
14
  throw new TypeError('Adapter capabilities must contain inbound and/or outbound');
15
15
  }
16
16
  const segments = normalizeSegmentPolicy(definition.segments);
17
- const operations = normalizeOperations(definition.operations);
17
+ const operations = typeof definition.operations === 'function'
18
+ ? definition.operations
19
+ : normalizeOperations(definition.operations);
18
20
  return Object.freeze({
19
21
  ...definition,
20
22
  $feature: adapterBrand,
@@ -24,14 +26,27 @@ export function defineAdapter(definition) {
24
26
  });
25
27
  }
26
28
  /** 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 }), {});
29
+ export function endpointCapabilitiesOf(definition, resolvedOperations) {
30
+ if (typeof definition.operations === 'function' && resolvedOperations === undefined) {
31
+ throw new TypeError('Dynamic Adapter operations must be resolved for one Endpoint');
32
+ }
33
+ const declared = resolvedOperations
34
+ ?? (Array.isArray(definition.operations) ? definition.operations : undefined);
35
+ const operations = declared?.reduce((result, operation) => ({ ...result, [operation]: true }), {});
29
36
  return Object.freeze({
30
37
  inbound: definition.capabilities.includes('inbound'),
31
38
  outbound: definition.capabilities.includes('outbound'),
32
39
  ...(operations && Object.keys(operations).length > 0 ? { operations: Object.freeze(operations) } : {}),
33
40
  });
34
41
  }
42
+ /** Resolve and validate the operation declaration for one concrete Endpoint. */
43
+ export function resolveAdapterOperations(definition, context) {
44
+ const declaration = definition.operations;
45
+ const operations = typeof declaration === 'function'
46
+ ? declaration(context)
47
+ : declaration;
48
+ return normalizeOperations(operations) ?? Object.freeze([]);
49
+ }
35
50
  function normalizeOperations(operations) {
36
51
  if (operations === undefined)
37
52
  return undefined;
@@ -93,7 +108,9 @@ export function parseAdapterDefinition(value) {
93
108
  throw invalidAdapter();
94
109
  // defineAdapter 已校验过形状;外部手工构造的 definition 也在此兜底
95
110
  normalizeSegmentPolicy(definition.segments);
96
- normalizeOperations(definition.operations);
111
+ if (typeof definition.operations !== 'function') {
112
+ normalizeOperations(definition.operations);
113
+ }
97
114
  return definition;
98
115
  }
99
116
  function invalidAdapter() {
@@ -19,9 +19,13 @@ export interface EndpointControl {
19
19
  export interface EndpointWithControl {
20
20
  readonly control?: EndpointControl;
21
21
  }
22
+ /** Bridges the common platform `recall(messageId)` shape into canonical control. */
23
+ export declare function createRecallEndpointControl(recallById: (messageId: string) => void | Promise<void>): Readonly<EndpointControl>;
22
24
  /** Reads the canonical control port without probing protocol-specific methods. */
23
25
  export declare function endpointControlOf(endpoint: unknown): EndpointControl | undefined;
24
26
  /** Checks only an Endpoint's explicit `control` port; protocol methods are never probed. */
25
27
  export declare function hasExplicitEndpointOperation(endpoint: unknown, operation: 'recall' | 'edit' | 'reaction' | 'typing'): boolean;
28
+ /** Lists the semantic operations implemented by an Endpoint's explicit control port. */
29
+ export declare function listExplicitEndpointOperations(endpoint: unknown): readonly ('recall' | 'edit' | 'reaction' | 'typing')[];
26
30
  /** Rejects a declaration that cannot be fulfilled by the explicit control port. */
27
31
  export declare function assertDeclaredEndpointOperations(endpoint: unknown, operations: readonly ('recall' | 'edit' | 'reaction' | 'typing')[] | undefined, id: string): void;
@@ -1,3 +1,9 @@
1
+ /** Bridges the common platform `recall(messageId)` shape into canonical control. */
2
+ export function createRecallEndpointControl(recallById) {
3
+ return Object.freeze({
4
+ recall: (message) => Promise.resolve(recallById(message.id)),
5
+ });
6
+ }
1
7
  /** Reads the canonical control port without probing protocol-specific methods. */
2
8
  export function endpointControlOf(endpoint) {
3
9
  if (!endpoint || typeof endpoint !== 'object')
@@ -19,6 +25,25 @@ export function hasExplicitEndpointOperation(endpoint, operation) {
19
25
  case 'typing': return typeof control.typing === 'function';
20
26
  }
21
27
  }
28
+ /** Lists the semantic operations implemented by an Endpoint's explicit control port. */
29
+ export function listExplicitEndpointOperations(endpoint) {
30
+ if (!endpoint || typeof endpoint !== 'object')
31
+ return Object.freeze([]);
32
+ const control = endpoint.control;
33
+ if (!control || typeof control !== 'object')
34
+ return Object.freeze([]);
35
+ const operations = [];
36
+ if (typeof control.recall === 'function')
37
+ operations.push('recall');
38
+ if (typeof control.edit === 'function')
39
+ operations.push('edit');
40
+ if (typeof control.addReaction === 'function'
41
+ || typeof control.removeReaction === 'function')
42
+ operations.push('reaction');
43
+ if (typeof control.typing === 'function')
44
+ operations.push('typing');
45
+ return Object.freeze(operations);
46
+ }
22
47
  /** Rejects a declaration that cannot be fulfilled by the explicit control port. */
23
48
  export function assertDeclaredEndpointOperations(endpoint, operations, id) {
24
49
  for (const operation of operations ?? []) {
@@ -26,7 +51,19 @@ export function assertDeclaredEndpointOperations(endpoint, operations, id) {
26
51
  throw new TypeError(`Adapter Endpoint ${id} declares ${operation} but control.${controlMethodName(operation)} is missing`);
27
52
  }
28
53
  }
54
+ const declared = new Set(operations ?? []);
55
+ for (const operation of listExplicitEndpointOperations(endpoint)) {
56
+ if (!declared.has(operation)) {
57
+ throw new TypeError(`Adapter Endpoint ${id} exposes control.${explicitControlMethodName(endpoint, operation)} but does not declare ${operation}`);
58
+ }
59
+ }
29
60
  }
30
61
  function controlMethodName(operation) {
31
62
  return operation === 'reaction' ? 'addReaction' : operation;
32
63
  }
64
+ function explicitControlMethodName(endpoint, operation) {
65
+ if (operation !== 'reaction')
66
+ return operation;
67
+ const control = endpoint.control;
68
+ return typeof control?.addReaction === 'function' ? 'addReaction' : 'removeReaction';
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter",
3
- "version": "1.1.11",
3
+ "version": "1.2.0",
4
4
  "description": "Convention-based Adapter and Endpoint Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,8 +18,8 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "yaml": "^2.9.0",
21
- "@zhin.js/im-contract": "1.0.4",
22
21
  "@zhin.js/feature-kit": "1.0.12",
22
+ "@zhin.js/im-contract": "1.0.4",
23
23
  "@zhin.js/logger": "1.0.76",
24
24
  "@zhin.js/plugin-runtime": "1.1.7"
25
25
  },
@@ -10,20 +10,31 @@ import {
10
10
  type RuntimeSnapshot,
11
11
  } from '@zhin.js/plugin-runtime';
12
12
  import { createCapabilityContext } from '@zhin.js/feature-kit';
13
- import type {
14
- AdapterCapability,
15
- AdapterDefinition,
16
- AdapterSegmentPolicy,
17
- EndpointInstance,
18
- EndpointSendRequest,
13
+ import {
14
+ endpointCapabilitiesOf,
15
+ resolveAdapterOperations,
16
+ type AdapterCapability,
17
+ type AdapterDefinition,
18
+ type AdapterOperation,
19
+ type AdapterSegmentPolicy,
20
+ type EndpointInstance,
21
+ type EndpointSendRequest,
19
22
  } from './definition.js';
20
23
  import {
21
24
  listEndpointManagementCapabilities,
22
25
  type EndpointManagementCapability,
23
26
  } from './endpoint-management.js';
24
- import { assertDeclaredEndpointOperations } from './endpoint-control.js';
27
+ import {
28
+ assertDeclaredEndpointOperations,
29
+ endpointControlOf,
30
+ type EndpointControl,
31
+ } from './endpoint-control.js';
25
32
  import { endpointContentOf, type EndpointContentResolveContext } from './endpoint-content.js';
26
- import type { ConversationReference, ConversationResolution } from '@zhin.js/im-contract';
33
+ import type {
34
+ ConversationReference,
35
+ ConversationResolution,
36
+ EndpointCapabilities,
37
+ } from '@zhin.js/im-contract';
27
38
 
28
39
  export interface AdapterDescriptor {
29
40
  readonly id: CapabilityId;
@@ -31,6 +42,7 @@ export interface AdapterDescriptor {
31
42
  readonly name: string;
32
43
  readonly source: string;
33
44
  readonly capabilities: readonly AdapterCapability[];
45
+ readonly operations: readonly AdapterOperation[];
34
46
  }
35
47
 
36
48
  /** Console / Host-facing endpoint row (connected = admission open). */
@@ -80,7 +92,7 @@ export class AdapterIndex {
80
92
  for (const slot of [...slots].sort((left, right) => left.id.localeCompare(right.id))) {
81
93
  signal.throwIfAborted();
82
94
  for (const expansion of expandEndpointConfigs(slot, snapshot)) {
83
- const endpoint = await createEndpoint(slot, snapshot, admission, signal, expansion);
95
+ const created = await createEndpoint(slot, snapshot, admission, signal, expansion);
84
96
  signal.throwIfAborted();
85
97
  records.push({
86
98
  id: expansion.id,
@@ -90,7 +102,8 @@ export class AdapterIndex {
90
102
  name: expansion.endpointId,
91
103
  source: slot.source,
92
104
  capabilities: slot.definition.capabilities,
93
- endpoint,
105
+ operations: created.operations,
106
+ endpoint: created.endpoint,
94
107
  ...(slot.definition.segments ? { segments: slot.definition.segments } : {}),
95
108
  started: false,
96
109
  open: false,
@@ -122,6 +135,7 @@ export class AdapterIndex {
122
135
  name: endpointLiveName(record.endpoint) ?? record.name,
123
136
  source: record.source,
124
137
  capabilities: record.capabilities,
138
+ operations: record.operations,
125
139
  connected: record.open && !record.stopped,
126
140
  status: record.open && !record.stopped ? 'online' as const : 'offline' as const,
127
141
  phase: endpointPhase(record),
@@ -158,6 +172,21 @@ export class AdapterIndex {
158
172
  return record.owner;
159
173
  }
160
174
 
175
+ /** Exact, serializable capabilities for one concrete Endpoint. */
176
+ capabilities(id: CapabilityId): EndpointCapabilities {
177
+ const record = this.#records.get(id);
178
+ if (!record) throw new Error(`Unknown Adapter Endpoint: ${id}`);
179
+ return endpointCapabilitiesOf(record, record.operations);
180
+ }
181
+
182
+ /** Returns the control port only when the concrete Endpoint declared the operation and is active. */
183
+ control(id: CapabilityId, operation: AdapterOperation): EndpointControl | undefined {
184
+ const record = this.#records.get(id);
185
+ if (!record || !record.started || record.stopped) return undefined;
186
+ if (!record.operations.includes(operation)) return undefined;
187
+ return endpointControlOf(record.endpoint);
188
+ }
189
+
161
190
  /**
162
191
  * Endpoint 的消息段能力声明(出站协商降级依据);
163
192
  * 未声明或未知 id 返回 undefined(调用方按历史行为处理)。
@@ -396,15 +425,15 @@ async function createEndpoint(
396
425
  admission: GenerationAdmissionGate,
397
426
  signal: AbortSignal,
398
427
  expansion?: EndpointExpansion,
399
- ): Promise<EndpointInstance> {
400
- const endpoint = await slot.definition.create(
401
- Object.freeze({
402
- ...createCapabilityContext(snapshot, slot.owner, admission, signal),
403
- ...(expansion?.config ? { config: expansion.config } : {}),
404
- id: expansion?.id ?? slot.id,
405
- name: slot.localName,
406
- }),
407
- );
428
+ ): Promise<Readonly<{ endpoint: EndpointInstance; operations: readonly AdapterOperation[] }>> {
429
+ const context = Object.freeze({
430
+ ...createCapabilityContext(snapshot, slot.owner, admission, signal),
431
+ ...(expansion?.config ? { config: expansion.config } : {}),
432
+ id: expansion?.id ?? slot.id,
433
+ name: slot.localName,
434
+ });
435
+ const operations = resolveAdapterOperations(slot.definition, context);
436
+ const endpoint = await slot.definition.create(context);
408
437
  assertEndpoint(endpoint, expansion?.id ?? slot.id);
409
438
  if (slot.definition.capabilities.includes('outbound') && typeof endpoint.send !== 'function') {
410
439
  throw new TypeError(
@@ -413,10 +442,10 @@ async function createEndpoint(
413
442
  }
414
443
  assertDeclaredEndpointOperations(
415
444
  endpoint,
416
- slot.definition.operations,
445
+ operations,
417
446
  String(expansion?.id ?? slot.id),
418
447
  );
419
- return endpoint;
448
+ return Object.freeze({ endpoint, operations });
420
449
  }
421
450
 
422
451
  async function stopRecords(
package/src/definition.ts CHANGED
@@ -16,6 +16,11 @@ export type AdapterCapability = 'inbound' | 'outbound';
16
16
  /** Operations beyond sending, declared by an Adapter definition. */
17
17
  export type AdapterOperation = Exclude<EndpointOperation, 'send'>;
18
18
 
19
+ /** Resolve operations for one concrete Endpoint configuration. */
20
+ export type AdapterOperationDeclaration<TConfig = unknown> =
21
+ | readonly AdapterOperation[]
22
+ | ((context: AdapterContext<TConfig>) => readonly AdapterOperation[]);
23
+
19
24
  /** 端点可消费的出站媒体来源形式。 */
20
25
  export type AdapterOutboundMedia = 'url' | 'path' | 'base64' | 'upload';
21
26
 
@@ -103,7 +108,7 @@ export interface AdapterDefinition<TConfig = unknown> {
103
108
  * `capabilities: ['outbound']`; a method existing on an endpoint is not a
104
109
  * capability declaration.
105
110
  */
106
- readonly operations?: readonly AdapterOperation[];
111
+ readonly operations?: AdapterOperationDeclaration<TConfig>;
107
112
  /** 可选:端点消息段能力声明(出站协商降级挂载点)。 */
108
113
  readonly segments?: AdapterSegmentPolicy;
109
114
  create(
@@ -134,7 +139,9 @@ export function defineAdapter<TConfig = unknown>(
134
139
  throw new TypeError('Adapter capabilities must contain inbound and/or outbound');
135
140
  }
136
141
  const segments = normalizeSegmentPolicy(definition.segments);
137
- const operations = normalizeOperations(definition.operations);
142
+ const operations = typeof definition.operations === 'function'
143
+ ? definition.operations
144
+ : normalizeOperations(definition.operations);
138
145
  return Object.freeze({
139
146
  ...definition,
140
147
  $feature: adapterBrand,
@@ -147,8 +154,14 @@ export function defineAdapter<TConfig = unknown>(
147
154
  /** Converts the definition's compact authoring form into the public contract. */
148
155
  export function endpointCapabilitiesOf(
149
156
  definition: Pick<AdapterDefinition, 'capabilities' | 'operations'>,
157
+ resolvedOperations?: readonly AdapterOperation[],
150
158
  ): EndpointCapabilities {
151
- const operations = definition.operations?.reduce<Partial<Record<AdapterOperation, true>>>(
159
+ if (typeof definition.operations === 'function' && resolvedOperations === undefined) {
160
+ throw new TypeError('Dynamic Adapter operations must be resolved for one Endpoint');
161
+ }
162
+ const declared = resolvedOperations
163
+ ?? (Array.isArray(definition.operations) ? definition.operations : undefined);
164
+ const operations = declared?.reduce<Partial<Record<AdapterOperation, true>>>(
152
165
  (result, operation) => ({ ...result, [operation]: true }),
153
166
  {},
154
167
  );
@@ -159,6 +172,18 @@ export function endpointCapabilitiesOf(
159
172
  });
160
173
  }
161
174
 
175
+ /** Resolve and validate the operation declaration for one concrete Endpoint. */
176
+ export function resolveAdapterOperations<TConfig>(
177
+ definition: Pick<AdapterDefinition<TConfig>, 'operations'>,
178
+ context: AdapterContext<TConfig>,
179
+ ): readonly AdapterOperation[] {
180
+ const declaration = definition.operations;
181
+ const operations = typeof declaration === 'function'
182
+ ? declaration(context)
183
+ : declaration;
184
+ return normalizeOperations(operations) ?? Object.freeze([]);
185
+ }
186
+
162
187
  function normalizeOperations(
163
188
  operations: readonly AdapterOperation[] | undefined,
164
189
  ): readonly AdapterOperation[] | undefined {
@@ -240,7 +265,9 @@ export function parseAdapterDefinition(value: unknown): AdapterDefinition {
240
265
  ) throw invalidAdapter();
241
266
  // defineAdapter 已校验过形状;外部手工构造的 definition 也在此兜底
242
267
  normalizeSegmentPolicy(definition.segments);
243
- normalizeOperations(definition.operations);
268
+ if (typeof definition.operations !== 'function') {
269
+ normalizeOperations(definition.operations);
270
+ }
244
271
  return definition as AdapterDefinition;
245
272
  }
246
273
 
@@ -23,6 +23,15 @@ export interface EndpointWithControl {
23
23
  readonly control?: EndpointControl;
24
24
  }
25
25
 
26
+ /** Bridges the common platform `recall(messageId)` shape into canonical control. */
27
+ export function createRecallEndpointControl(
28
+ recallById: (messageId: string) => void | Promise<void>,
29
+ ): Readonly<EndpointControl> {
30
+ return Object.freeze<EndpointControl>({
31
+ recall: (message) => Promise.resolve(recallById(message.id)),
32
+ });
33
+ }
34
+
26
35
  /** Reads the canonical control port without probing protocol-specific methods. */
27
36
  export function endpointControlOf(endpoint: unknown): EndpointControl | undefined {
28
37
  if (!endpoint || typeof endpoint !== 'object') return undefined;
@@ -46,6 +55,24 @@ export function hasExplicitEndpointOperation(
46
55
  }
47
56
  }
48
57
 
58
+ /** Lists the semantic operations implemented by an Endpoint's explicit control port. */
59
+ export function listExplicitEndpointOperations(
60
+ endpoint: unknown,
61
+ ): readonly ('recall' | 'edit' | 'reaction' | 'typing')[] {
62
+ if (!endpoint || typeof endpoint !== 'object') return Object.freeze([]);
63
+ const control = (endpoint as EndpointWithControl).control;
64
+ if (!control || typeof control !== 'object') return Object.freeze([]);
65
+ const operations: Array<'recall' | 'edit' | 'reaction' | 'typing'> = [];
66
+ if (typeof control.recall === 'function') operations.push('recall');
67
+ if (typeof control.edit === 'function') operations.push('edit');
68
+ if (
69
+ typeof control.addReaction === 'function'
70
+ || typeof control.removeReaction === 'function'
71
+ ) operations.push('reaction');
72
+ if (typeof control.typing === 'function') operations.push('typing');
73
+ return Object.freeze(operations);
74
+ }
75
+
49
76
  /** Rejects a declaration that cannot be fulfilled by the explicit control port. */
50
77
  export function assertDeclaredEndpointOperations(
51
78
  endpoint: unknown,
@@ -59,8 +86,25 @@ export function assertDeclaredEndpointOperations(
59
86
  );
60
87
  }
61
88
  }
89
+ const declared = new Set(operations ?? []);
90
+ for (const operation of listExplicitEndpointOperations(endpoint)) {
91
+ if (!declared.has(operation)) {
92
+ throw new TypeError(
93
+ `Adapter Endpoint ${id} exposes control.${explicitControlMethodName(endpoint, operation)} but does not declare ${operation}`,
94
+ );
95
+ }
96
+ }
62
97
  }
63
98
 
64
99
  function controlMethodName(operation: 'recall' | 'edit' | 'reaction' | 'typing'): string {
65
100
  return operation === 'reaction' ? 'addReaction' : operation;
66
101
  }
102
+
103
+ function explicitControlMethodName(
104
+ endpoint: unknown,
105
+ operation: 'recall' | 'edit' | 'reaction' | 'typing',
106
+ ): string {
107
+ if (operation !== 'reaction') return operation;
108
+ const control = (endpoint as EndpointWithControl).control;
109
+ return typeof control?.addReaction === 'function' ? 'addReaction' : 'removeReaction';
110
+ }