@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 +9 -3
- package/lib/adapter-index.d.ts +8 -2
- package/lib/adapter-index.js +28 -7
- package/lib/definition.d.ts +6 -2
- package/lib/definition.js +21 -4
- package/lib/endpoint-control.d.ts +4 -0
- package/lib/endpoint-control.js +37 -0
- package/package.json +2 -2
- package/src/adapter-index.ts +50 -21
- package/src/definition.ts +31 -4
- package/src/endpoint-control.ts +44 -0
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`.
|
|
30
|
-
|
|
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
|
|
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
|
|
package/lib/adapter-index.d.ts
CHANGED
|
@@ -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
|
|
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(调用方按历史行为处理)。
|
package/lib/adapter-index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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();
|
package/lib/definition.d.ts
CHANGED
|
@@ -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?:
|
|
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'
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/lib/endpoint-control.js
CHANGED
|
@@ -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.
|
|
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
|
},
|
package/src/adapter-index.ts
CHANGED
|
@@ -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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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?:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
268
|
+
if (typeof definition.operations !== 'function') {
|
|
269
|
+
normalizeOperations(definition.operations);
|
|
270
|
+
}
|
|
244
271
|
return definition as AdapterDefinition;
|
|
245
272
|
}
|
|
246
273
|
|
package/src/endpoint-control.ts
CHANGED
|
@@ -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
|
+
}
|