@zhin.js/adapter 1.1.10 → 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 +14 -6
- package/lib/adapter-index.d.ts +11 -2
- package/lib/adapter-index.js +50 -8
- package/lib/definition.d.ts +22 -10
- package/lib/definition.js +27 -4
- package/lib/endpoint-content.d.ts +12 -0
- package/lib/endpoint-content.js +10 -0
- package/lib/endpoint-control.d.ts +5 -1
- package/lib/endpoint-control.js +38 -1
- package/lib/endpoint-management.d.ts +15 -1
- package/lib/endpoint-management.js +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/provider.d.ts +1 -1
- package/package.json +5 -5
- package/src/adapter-index.ts +78 -22
- package/src/definition.ts +58 -15
- package/src/endpoint-content.ts +28 -0
- package/src/endpoint-control.ts +45 -1
- package/src/endpoint-management.ts +16 -0
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ generation lifecycle。候选 Endpoint 可完成连接 readiness,但入站由
|
|
|
11
11
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
12
12
|
|
|
13
13
|
export default defineAdapter({
|
|
14
|
-
capabilities: ['inbound'
|
|
14
|
+
capabilities: ['inbound'],
|
|
15
15
|
create: (context) => ({ name: context.name }),
|
|
16
16
|
});
|
|
17
17
|
```
|
|
@@ -26,13 +26,20 @@ 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`.
|
|
34
39
|
`EndpointSendRequest` is `{ conversation, payload }`; platform adapters derive
|
|
35
|
-
their native target from `conversation` at the endpoint boundary
|
|
40
|
+
their native target from `conversation` at the endpoint boundary and return one
|
|
41
|
+
non-empty platform message id. IM Runtime alone wraps that id as a structured
|
|
42
|
+
`MessageRef` / `DeliveryReceipt`; arbitrary endpoint result shapes are rejected.
|
|
36
43
|
|
|
37
44
|
## Endpoint Control Port
|
|
38
45
|
|
|
@@ -43,7 +50,8 @@ boundary.
|
|
|
43
50
|
|
|
44
51
|
New adapters should provide `control` directly and declare matching
|
|
45
52
|
`operations`. Protocol-specific methods and compound string identifiers are not
|
|
46
|
-
inspected
|
|
53
|
+
inspected by the runtime. `createRecallEndpointControl()` bridges the common
|
|
54
|
+
platform `recall(messageId)` shape without leaking that shape into Core.
|
|
47
55
|
|
|
48
56
|
## Endpoint 生命周期基座(createEndpointLifecycle)
|
|
49
57
|
|
|
@@ -66,7 +74,7 @@ stop 主动断开不重连、心跳 PONG 看门狗、定时器集中清理、陈
|
|
|
66
74
|
|
|
67
75
|
展开由 `expandEndpointConfigs`(`src/adapter-index.ts`)完成:endpoint record id 为
|
|
68
76
|
`<slotId>~<name>`,合并顺序 `{...通用, ...项}`(项优先),`endpoints` 键不下传给适配器。
|
|
69
|
-
record name 即 entry.name——Console
|
|
77
|
+
record name 即 entry.name——Console 展示、endpoint identity 解析、inbox 落库都按它命中
|
|
70
78
|
唯一 endpoint(适配器实例的 live name 如 icqq uin 优先于它展示)。entry.name 不得含
|
|
71
79
|
`~`/`\0`(会破坏 id 结构),重名/缺名的 entry 会被丢弃并 warn。
|
|
72
80
|
多账号示例见 `plugins/adapters/icqq` / `plugins/adapters/qq` 的 README 与 schema。
|
package/lib/adapter-index.d.ts
CHANGED
|
@@ -1,12 +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';
|
|
5
|
+
import { type EndpointContentResolveContext } from './endpoint-content.js';
|
|
6
|
+
import type { ConversationReference, ConversationResolution, EndpointCapabilities } from '@zhin.js/im-contract';
|
|
4
7
|
export interface AdapterDescriptor {
|
|
5
8
|
readonly id: CapabilityId;
|
|
6
9
|
readonly owner: PluginId;
|
|
7
10
|
readonly name: string;
|
|
8
11
|
readonly source: string;
|
|
9
12
|
readonly capabilities: readonly AdapterCapability[];
|
|
13
|
+
readonly operations: readonly AdapterOperation[];
|
|
10
14
|
}
|
|
11
15
|
/** Console / Host-facing endpoint row (connected = admission open). */
|
|
12
16
|
export interface AdapterEndpointSummary extends AdapterDescriptor {
|
|
@@ -35,6 +39,10 @@ export declare class AdapterIndex {
|
|
|
35
39
|
*/
|
|
36
40
|
instance(adapter: string, endpointKey: string): EndpointInstance | undefined;
|
|
37
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;
|
|
38
46
|
/**
|
|
39
47
|
* Endpoint 的消息段能力声明(出站协商降级依据);
|
|
40
48
|
* 未声明或未知 id 返回 undefined(调用方按历史行为处理)。
|
|
@@ -46,6 +54,7 @@ export declare class AdapterIndex {
|
|
|
46
54
|
open(): void;
|
|
47
55
|
close(): Promise<void>;
|
|
48
56
|
stop(): Promise<void>;
|
|
49
|
-
send(id: CapabilityId, request: EndpointSendRequest): Promise<
|
|
57
|
+
send(id: CapabilityId, request: EndpointSendRequest): Promise<string>;
|
|
58
|
+
resolveContent(id: CapabilityId, reference: ConversationReference, context: EndpointContentResolveContext): Promise<ConversationResolution>;
|
|
50
59
|
}
|
|
51
60
|
export declare function isAdapterIndex(value: unknown): value is AdapterIndex;
|
package/lib/adapter-index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { DisposeStack, GenerationCompensationError, createGenerationAdmissionGate, generationAdmissionSource, } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import { createCapabilityContext } from '@zhin.js/feature-kit';
|
|
3
|
+
import { endpointCapabilitiesOf, resolveAdapterOperations, } from './definition.js';
|
|
3
4
|
import { listEndpointManagementCapabilities, } from './endpoint-management.js';
|
|
4
|
-
import { assertDeclaredEndpointOperations } from './endpoint-control.js';
|
|
5
|
+
import { assertDeclaredEndpointOperations, endpointControlOf, } from './endpoint-control.js';
|
|
6
|
+
import { endpointContentOf } from './endpoint-content.js';
|
|
5
7
|
export class AdapterIndex {
|
|
6
8
|
$projection = 'zhin.adapter-index/1';
|
|
7
9
|
#records = new Map();
|
|
@@ -20,7 +22,7 @@ export class AdapterIndex {
|
|
|
20
22
|
for (const slot of [...slots].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
21
23
|
signal.throwIfAborted();
|
|
22
24
|
for (const expansion of expandEndpointConfigs(slot, snapshot)) {
|
|
23
|
-
const
|
|
25
|
+
const created = await createEndpoint(slot, snapshot, admission, signal, expansion);
|
|
24
26
|
signal.throwIfAborted();
|
|
25
27
|
records.push({
|
|
26
28
|
id: expansion.id,
|
|
@@ -30,7 +32,8 @@ export class AdapterIndex {
|
|
|
30
32
|
name: expansion.endpointId,
|
|
31
33
|
source: slot.source,
|
|
32
34
|
capabilities: slot.definition.capabilities,
|
|
33
|
-
|
|
35
|
+
operations: created.operations,
|
|
36
|
+
endpoint: created.endpoint,
|
|
34
37
|
...(slot.definition.segments ? { segments: slot.definition.segments } : {}),
|
|
35
38
|
started: false,
|
|
36
39
|
open: false,
|
|
@@ -58,6 +61,7 @@ export class AdapterIndex {
|
|
|
58
61
|
name: endpointLiveName(record.endpoint) ?? record.name,
|
|
59
62
|
source: record.source,
|
|
60
63
|
capabilities: record.capabilities,
|
|
64
|
+
operations: record.operations,
|
|
61
65
|
connected: record.open && !record.stopped,
|
|
62
66
|
status: record.open && !record.stopped ? 'online' : 'offline',
|
|
63
67
|
phase: endpointPhase(record),
|
|
@@ -93,6 +97,22 @@ export class AdapterIndex {
|
|
|
93
97
|
throw new Error(`Unknown Adapter Endpoint: ${id}`);
|
|
94
98
|
return record.owner;
|
|
95
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
|
+
}
|
|
96
116
|
/**
|
|
97
117
|
* Endpoint 的消息段能力声明(出站协商降级依据);
|
|
98
118
|
* 未声明或未知 id 返回 undefined(调用方按历史行为处理)。
|
|
@@ -194,7 +214,24 @@ export class AdapterIndex {
|
|
|
194
214
|
if (!record.started || record.stopped) {
|
|
195
215
|
throw new Error(`Adapter Endpoint is not active: ${id}`);
|
|
196
216
|
}
|
|
197
|
-
|
|
217
|
+
const messageId = await record.endpoint.send(request);
|
|
218
|
+
if (typeof messageId !== 'string' || !messageId.trim()) {
|
|
219
|
+
throw new TypeError(`Adapter Endpoint send() must return a non-empty platform message id: ${id}`);
|
|
220
|
+
}
|
|
221
|
+
return messageId;
|
|
222
|
+
}
|
|
223
|
+
async resolveContent(id, reference, context) {
|
|
224
|
+
const record = this.#records.get(id);
|
|
225
|
+
if (!record)
|
|
226
|
+
return Object.freeze({ status: 'not_found', code: 'endpoint_not_found' });
|
|
227
|
+
if (!record.started || record.stopped) {
|
|
228
|
+
return Object.freeze({ status: 'failed', code: 'endpoint_not_active' });
|
|
229
|
+
}
|
|
230
|
+
const content = endpointContentOf(record.endpoint);
|
|
231
|
+
if (!content)
|
|
232
|
+
return Object.freeze({ status: 'unsupported', code: 'content_resolution_unsupported' });
|
|
233
|
+
context.signal.throwIfAborted();
|
|
234
|
+
return content.resolve(reference, context);
|
|
198
235
|
}
|
|
199
236
|
}
|
|
200
237
|
export function isAdapterIndex(value) {
|
|
@@ -282,15 +319,20 @@ function expandEndpointConfigs(slot, snapshot) {
|
|
|
282
319
|
})));
|
|
283
320
|
}
|
|
284
321
|
async function createEndpoint(slot, snapshot, admission, signal, expansion) {
|
|
285
|
-
const
|
|
322
|
+
const context = Object.freeze({
|
|
286
323
|
...createCapabilityContext(snapshot, slot.owner, admission, signal),
|
|
287
324
|
...(expansion?.config ? { config: expansion.config } : {}),
|
|
288
325
|
id: expansion?.id ?? slot.id,
|
|
289
326
|
name: slot.localName,
|
|
290
|
-
})
|
|
327
|
+
});
|
|
328
|
+
const operations = resolveAdapterOperations(slot.definition, context);
|
|
329
|
+
const endpoint = await slot.definition.create(context);
|
|
291
330
|
assertEndpoint(endpoint, expansion?.id ?? slot.id);
|
|
292
|
-
|
|
293
|
-
|
|
331
|
+
if (slot.definition.capabilities.includes('outbound') && typeof endpoint.send !== 'function') {
|
|
332
|
+
throw new TypeError(`Adapter Endpoint ${String(expansion?.id ?? slot.id)} declares outbound but send() is missing`);
|
|
333
|
+
}
|
|
334
|
+
assertDeclaredEndpointOperations(endpoint, operations, String(expansion?.id ?? slot.id));
|
|
335
|
+
return Object.freeze({ endpoint, operations });
|
|
294
336
|
}
|
|
295
337
|
async function stopRecords(records, primaryError) {
|
|
296
338
|
const stack = new DisposeStack();
|
package/lib/definition.d.ts
CHANGED
|
@@ -3,24 +3,31 @@ import type { CapabilityContext } from '@zhin.js/feature-kit';
|
|
|
3
3
|
import type { ConversationRef, EndpointCapabilities, EndpointOperation } from '@zhin.js/im-contract';
|
|
4
4
|
import type { EndpointManagement } from './endpoint-management.js';
|
|
5
5
|
import type { EndpointControl } from './endpoint-control.js';
|
|
6
|
+
import type { EndpointContentPort } from './endpoint-content.js';
|
|
6
7
|
declare const adapterBrand: "zhin.adapter/1";
|
|
7
8
|
export type AdapterCapability = 'inbound' | 'outbound';
|
|
8
9
|
/** Operations beyond sending, declared by an Adapter definition. */
|
|
9
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[]);
|
|
10
13
|
/** 端点可消费的出站媒体来源形式。 */
|
|
11
14
|
export type AdapterOutboundMedia = 'url' | 'path' | 'base64' | 'upload';
|
|
12
15
|
/** 交互段(卡片/按钮等富交互)的端点消费方式。 */
|
|
13
16
|
export type AdapterInteractiveMode = 'native' | 'text';
|
|
17
|
+
/** Markdown semantic segment consumption mode. */
|
|
18
|
+
export type AdapterMarkdownMode = 'native' | 'text';
|
|
14
19
|
export interface EndpointSendRequest {
|
|
15
20
|
/** 结构化会话寻址;端点在平台边界自行派生原生 target。 */
|
|
16
21
|
readonly conversation: ConversationRef;
|
|
17
22
|
readonly payload: unknown;
|
|
18
23
|
}
|
|
19
|
-
export interface EndpointInstance
|
|
24
|
+
export interface EndpointInstance {
|
|
20
25
|
/** Optional platform-neutral Console/Host management surface. */
|
|
21
26
|
readonly management?: EndpointManagement;
|
|
22
27
|
/** Optional platform-neutral control surface for existing messages. */
|
|
23
28
|
readonly control?: EndpointControl;
|
|
29
|
+
/** Optional canonical resolver for message, merged-forward and media references. */
|
|
30
|
+
readonly content?: EndpointContentPort;
|
|
24
31
|
/** Required readiness; must observe abort and settle before rollback returns. */
|
|
25
32
|
start?(signal: AbortSignal): void | Promise<void>;
|
|
26
33
|
/** Opens Endpoint-local flow behind the candidate generation admission gate. */
|
|
@@ -29,7 +36,8 @@ export interface EndpointInstance<TResult = unknown> {
|
|
|
29
36
|
close?(): void | Promise<void>;
|
|
30
37
|
/** Releases transport resources. Calls must be idempotent. */
|
|
31
38
|
stop?(): void | Promise<void>;
|
|
32
|
-
|
|
39
|
+
/** Platform message id. Core wraps it in the canonical MessageRef/DeliveryReceipt. */
|
|
40
|
+
send?(request: EndpointSendRequest): string | Promise<string>;
|
|
33
41
|
}
|
|
34
42
|
export interface AdapterContext<TConfig = unknown> extends CapabilityContext<TConfig> {
|
|
35
43
|
readonly id: CapabilityId;
|
|
@@ -60,11 +68,13 @@ export interface AdapterSegmentPolicy {
|
|
|
60
68
|
readonly outboundMedia?: readonly AdapterOutboundMedia[];
|
|
61
69
|
/**
|
|
62
70
|
* 交互段(卡片/按钮等富交互)消费方式:`native` 原生渲染 / `text` 降级纯文本。
|
|
63
|
-
*
|
|
71
|
+
* Core 在最终出站阶段执行统一降级。
|
|
64
72
|
*/
|
|
65
73
|
readonly interactive?: AdapterInteractiveMode;
|
|
74
|
+
/** `native` preserves Markdown for the endpoint codec; `text` strips formatting in Core. */
|
|
75
|
+
readonly markdown?: AdapterMarkdownMode;
|
|
66
76
|
}
|
|
67
|
-
export interface AdapterDefinition<TConfig = unknown
|
|
77
|
+
export interface AdapterDefinition<TConfig = unknown> {
|
|
68
78
|
readonly $feature: typeof adapterBrand;
|
|
69
79
|
readonly capabilities: readonly AdapterCapability[];
|
|
70
80
|
/**
|
|
@@ -72,18 +82,20 @@ export interface AdapterDefinition<TConfig = unknown, TResult = unknown> {
|
|
|
72
82
|
* `capabilities: ['outbound']`; a method existing on an endpoint is not a
|
|
73
83
|
* capability declaration.
|
|
74
84
|
*/
|
|
75
|
-
readonly operations?:
|
|
85
|
+
readonly operations?: AdapterOperationDeclaration<TConfig>;
|
|
76
86
|
/** 可选:端点消息段能力声明(出站协商降级挂载点)。 */
|
|
77
87
|
readonly segments?: AdapterSegmentPolicy;
|
|
78
|
-
create(context: AdapterContext<TConfig>): EndpointInstance
|
|
88
|
+
create(context: AdapterContext<TConfig>): EndpointInstance | Promise<EndpointInstance>;
|
|
79
89
|
}
|
|
80
90
|
declare module '@zhin.js/plugin-runtime' {
|
|
81
|
-
interface PluginSetupContext<TConfig> {
|
|
82
|
-
addAdapter
|
|
91
|
+
interface PluginSetupContext<TConfig = unknown> {
|
|
92
|
+
addAdapter(localName: string, definition: AdapterDefinition<TConfig>): void;
|
|
83
93
|
}
|
|
84
94
|
}
|
|
85
|
-
export declare function defineAdapter<TConfig = unknown
|
|
95
|
+
export declare function defineAdapter<TConfig = unknown>(definition: Omit<AdapterDefinition<TConfig>, '$feature'>): Readonly<AdapterDefinition<TConfig>>;
|
|
86
96
|
/** Converts the definition's compact authoring form into the public contract. */
|
|
87
|
-
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[];
|
|
88
100
|
export declare function parseAdapterDefinition(value: unknown): AdapterDefinition;
|
|
89
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;
|
|
@@ -66,6 +81,11 @@ function normalizeSegmentPolicy(policy) {
|
|
|
66
81
|
&& policy.interactive !== 'text') {
|
|
67
82
|
throw new TypeError("Adapter segments.interactive must be 'native' or 'text'");
|
|
68
83
|
}
|
|
84
|
+
if (policy.markdown !== undefined
|
|
85
|
+
&& policy.markdown !== 'native'
|
|
86
|
+
&& policy.markdown !== 'text') {
|
|
87
|
+
throw new TypeError("Adapter segments.markdown must be 'native' or 'text'");
|
|
88
|
+
}
|
|
69
89
|
return Object.freeze({
|
|
70
90
|
...(policy.supported ? { supported: Object.freeze([...new Set(policy.supported)]) } : {}),
|
|
71
91
|
...(policy.html ? { html: policy.html } : {}),
|
|
@@ -73,6 +93,7 @@ function normalizeSegmentPolicy(policy) {
|
|
|
73
93
|
? { outboundMedia: Object.freeze([...new Set(policy.outboundMedia)]) }
|
|
74
94
|
: {}),
|
|
75
95
|
...(policy.interactive ? { interactive: policy.interactive } : {}),
|
|
96
|
+
...(policy.markdown ? { markdown: policy.markdown } : {}),
|
|
76
97
|
});
|
|
77
98
|
}
|
|
78
99
|
export function parseAdapterDefinition(value) {
|
|
@@ -87,7 +108,9 @@ export function parseAdapterDefinition(value) {
|
|
|
87
108
|
throw invalidAdapter();
|
|
88
109
|
// defineAdapter 已校验过形状;外部手工构造的 definition 也在此兜底
|
|
89
110
|
normalizeSegmentPolicy(definition.segments);
|
|
90
|
-
|
|
111
|
+
if (typeof definition.operations !== 'function') {
|
|
112
|
+
normalizeOperations(definition.operations);
|
|
113
|
+
}
|
|
91
114
|
return definition;
|
|
92
115
|
}
|
|
93
116
|
function invalidAdapter() {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ConversationReference, ConversationResolution } from '@zhin.js/im-contract';
|
|
2
|
+
export interface EndpointContentResolveContext {
|
|
3
|
+
readonly signal: AbortSignal;
|
|
4
|
+
readonly maxDepth: number;
|
|
5
|
+
readonly maxEntries: number;
|
|
6
|
+
readonly maxChars: number;
|
|
7
|
+
}
|
|
8
|
+
/** Platform semantic port for resolving content that was not observed locally. */
|
|
9
|
+
export interface EndpointContentPort {
|
|
10
|
+
resolve(reference: ConversationReference, context: EndpointContentResolveContext): Promise<ConversationResolution>;
|
|
11
|
+
}
|
|
12
|
+
export declare function endpointContentOf(endpoint: unknown): EndpointContentPort | undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function endpointContentOf(endpoint) {
|
|
2
|
+
if (!endpoint || typeof endpoint !== 'object')
|
|
3
|
+
return undefined;
|
|
4
|
+
const content = endpoint.content;
|
|
5
|
+
if (!content || typeof content !== 'object')
|
|
6
|
+
return undefined;
|
|
7
|
+
return typeof content.resolve === 'function'
|
|
8
|
+
? content
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
@@ -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
|
-
/** Checks only an Endpoint's explicit `control` port
|
|
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')
|
|
@@ -5,7 +11,7 @@ export function endpointControlOf(endpoint) {
|
|
|
5
11
|
const explicit = endpoint.control;
|
|
6
12
|
return explicit && typeof explicit === 'object' ? explicit : undefined;
|
|
7
13
|
}
|
|
8
|
-
/** Checks only an Endpoint's explicit `control` port
|
|
14
|
+
/** Checks only an Endpoint's explicit `control` port; protocol methods are never probed. */
|
|
9
15
|
export function hasExplicitEndpointOperation(endpoint, operation) {
|
|
10
16
|
if (!endpoint || typeof endpoint !== 'object')
|
|
11
17
|
return false;
|
|
@@ -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
|
+
}
|
|
@@ -19,6 +19,18 @@ export interface EndpointChannel {
|
|
|
19
19
|
readonly name?: string;
|
|
20
20
|
readonly parent?: EndpointChannelParent;
|
|
21
21
|
}
|
|
22
|
+
/** Pending friend/group request for Console / Host listing (live, not inbox DB). */
|
|
23
|
+
export interface EndpointPendingRequest {
|
|
24
|
+
readonly platform_request_id: string;
|
|
25
|
+
readonly type: string;
|
|
26
|
+
readonly scene_type?: string | null;
|
|
27
|
+
readonly scene_id: string;
|
|
28
|
+
readonly sub_type?: string | null;
|
|
29
|
+
readonly actor_id: string;
|
|
30
|
+
readonly actor_name?: string | null;
|
|
31
|
+
readonly comment?: string | null;
|
|
32
|
+
readonly created_at: number;
|
|
33
|
+
}
|
|
22
34
|
/**
|
|
23
35
|
* Optional, platform-neutral management surface exposed by an Endpoint.
|
|
24
36
|
*
|
|
@@ -31,6 +43,8 @@ export interface EndpointManagement {
|
|
|
31
43
|
listGroups?(): Promise<readonly EndpointGroup[]>;
|
|
32
44
|
listChannels?(): Promise<readonly EndpointChannel[]>;
|
|
33
45
|
listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
|
|
46
|
+
/** Live pending friend/group requests (preferred over unified_inbox_request). */
|
|
47
|
+
listRequests?(): Promise<readonly EndpointPendingRequest[]>;
|
|
34
48
|
approveRequest?(requestId: string, remark?: string): Promise<void>;
|
|
35
49
|
rejectRequest?(requestId: string, reason?: string): Promise<void>;
|
|
36
50
|
kickGroupMember?(groupId: string, userId: string): Promise<void>;
|
|
@@ -46,7 +60,7 @@ export interface EndpointWithManagement {
|
|
|
46
60
|
* Values intentionally mirror EndpointManagement method names so adapters only
|
|
47
61
|
* need to implement the semantic port; no second capability declaration exists.
|
|
48
62
|
*/
|
|
49
|
-
export declare const endpointManagementCapabilityIds: readonly ["listFriends", "listGroups", "listChannels", "listGroupMembers", "approveRequest", "rejectRequest", "kickGroupMember", "muteGroupMember", "setGroupAdmin", "deleteFriend"];
|
|
63
|
+
export declare const endpointManagementCapabilityIds: readonly ["listFriends", "listGroups", "listChannels", "listGroupMembers", "listRequests", "approveRequest", "rejectRequest", "kickGroupMember", "muteGroupMember", "setGroupAdmin", "deleteFriend"];
|
|
50
64
|
export type EndpointManagementCapability = (typeof endpointManagementCapabilityIds)[number];
|
|
51
65
|
export declare function resolveEndpointManagement(endpoint: unknown): EndpointManagement | undefined;
|
|
52
66
|
/** Derive advertised capabilities from the live semantic port implementation. */
|
package/lib/index.d.ts
CHANGED
|
@@ -7,5 +7,6 @@ export * from './endpoint-commands.js';
|
|
|
7
7
|
export * from './endpoint-lifecycle.js';
|
|
8
8
|
export * from './endpoint-management.js';
|
|
9
9
|
export * from './endpoint-control.js';
|
|
10
|
+
export * from './endpoint-content.js';
|
|
10
11
|
export * from './provider.js';
|
|
11
12
|
export { default } from './provider.js';
|
package/lib/index.js
CHANGED
|
@@ -7,5 +7,6 @@ export * from './endpoint-commands.js';
|
|
|
7
7
|
export * from './endpoint-lifecycle.js';
|
|
8
8
|
export * from './endpoint-management.js';
|
|
9
9
|
export * from './endpoint-control.js';
|
|
10
|
+
export * from './endpoint-content.js';
|
|
10
11
|
export * from './provider.js';
|
|
11
12
|
export { default } from './provider.js';
|
package/lib/provider.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AdapterIndex } from './adapter-index.js';
|
|
2
2
|
export declare const adapterFeatureId: import("@zhin.js/plugin-runtime").FeatureId;
|
|
3
|
-
declare const adapterFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<import("./definition.js").AdapterDefinition<unknown
|
|
3
|
+
declare const adapterFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<import("./definition.js").AdapterDefinition<unknown>, AdapterIndex>>;
|
|
4
4
|
export { adapterFeature };
|
|
5
5
|
export default adapterFeature;
|
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,15 +18,15 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"yaml": "^2.9.0",
|
|
21
|
-
"@zhin.js/feature-kit": "1.0.
|
|
22
|
-
"@zhin.js/im-contract": "1.0.
|
|
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
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
24
|
+
"@zhin.js/plugin-runtime": "1.1.7"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
|
-
"@zhin.js/command": "1.0.
|
|
29
|
+
"@zhin.js/command": "1.0.15"
|
|
30
30
|
},
|
|
31
31
|
"zhin": {
|
|
32
32
|
"protocol": 1,
|
package/src/adapter-index.ts
CHANGED
|
@@ -10,18 +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';
|
|
32
|
+
import { endpointContentOf, type EndpointContentResolveContext } from './endpoint-content.js';
|
|
33
|
+
import type {
|
|
34
|
+
ConversationReference,
|
|
35
|
+
ConversationResolution,
|
|
36
|
+
EndpointCapabilities,
|
|
37
|
+
} from '@zhin.js/im-contract';
|
|
25
38
|
|
|
26
39
|
export interface AdapterDescriptor {
|
|
27
40
|
readonly id: CapabilityId;
|
|
@@ -29,6 +42,7 @@ export interface AdapterDescriptor {
|
|
|
29
42
|
readonly name: string;
|
|
30
43
|
readonly source: string;
|
|
31
44
|
readonly capabilities: readonly AdapterCapability[];
|
|
45
|
+
readonly operations: readonly AdapterOperation[];
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
/** Console / Host-facing endpoint row (connected = admission open). */
|
|
@@ -78,7 +92,7 @@ export class AdapterIndex {
|
|
|
78
92
|
for (const slot of [...slots].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
79
93
|
signal.throwIfAborted();
|
|
80
94
|
for (const expansion of expandEndpointConfigs(slot, snapshot)) {
|
|
81
|
-
const
|
|
95
|
+
const created = await createEndpoint(slot, snapshot, admission, signal, expansion);
|
|
82
96
|
signal.throwIfAborted();
|
|
83
97
|
records.push({
|
|
84
98
|
id: expansion.id,
|
|
@@ -88,7 +102,8 @@ export class AdapterIndex {
|
|
|
88
102
|
name: expansion.endpointId,
|
|
89
103
|
source: slot.source,
|
|
90
104
|
capabilities: slot.definition.capabilities,
|
|
91
|
-
|
|
105
|
+
operations: created.operations,
|
|
106
|
+
endpoint: created.endpoint,
|
|
92
107
|
...(slot.definition.segments ? { segments: slot.definition.segments } : {}),
|
|
93
108
|
started: false,
|
|
94
109
|
open: false,
|
|
@@ -120,6 +135,7 @@ export class AdapterIndex {
|
|
|
120
135
|
name: endpointLiveName(record.endpoint) ?? record.name,
|
|
121
136
|
source: record.source,
|
|
122
137
|
capabilities: record.capabilities,
|
|
138
|
+
operations: record.operations,
|
|
123
139
|
connected: record.open && !record.stopped,
|
|
124
140
|
status: record.open && !record.stopped ? 'online' as const : 'offline' as const,
|
|
125
141
|
phase: endpointPhase(record),
|
|
@@ -156,6 +172,21 @@ export class AdapterIndex {
|
|
|
156
172
|
return record.owner;
|
|
157
173
|
}
|
|
158
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
|
+
|
|
159
190
|
/**
|
|
160
191
|
* Endpoint 的消息段能力声明(出站协商降级依据);
|
|
161
192
|
* 未声明或未知 id 返回 undefined(调用方按历史行为处理)。
|
|
@@ -251,7 +282,7 @@ export class AdapterIndex {
|
|
|
251
282
|
await stack.dispose();
|
|
252
283
|
}
|
|
253
284
|
|
|
254
|
-
async send(id: CapabilityId, request: EndpointSendRequest): Promise<
|
|
285
|
+
async send(id: CapabilityId, request: EndpointSendRequest): Promise<string> {
|
|
255
286
|
const record = this.#records.get(id);
|
|
256
287
|
if (!record) throw new Error(`Unknown Adapter Endpoint: ${id}`);
|
|
257
288
|
if (!record.capabilities.includes('outbound') || !record.endpoint.send) {
|
|
@@ -260,7 +291,27 @@ export class AdapterIndex {
|
|
|
260
291
|
if (!record.started || record.stopped) {
|
|
261
292
|
throw new Error(`Adapter Endpoint is not active: ${id}`);
|
|
262
293
|
}
|
|
263
|
-
|
|
294
|
+
const messageId = await record.endpoint.send(request);
|
|
295
|
+
if (typeof messageId !== 'string' || !messageId.trim()) {
|
|
296
|
+
throw new TypeError(`Adapter Endpoint send() must return a non-empty platform message id: ${id}`);
|
|
297
|
+
}
|
|
298
|
+
return messageId;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async resolveContent(
|
|
302
|
+
id: CapabilityId,
|
|
303
|
+
reference: ConversationReference,
|
|
304
|
+
context: EndpointContentResolveContext,
|
|
305
|
+
): Promise<ConversationResolution> {
|
|
306
|
+
const record = this.#records.get(id);
|
|
307
|
+
if (!record) return Object.freeze({ status: 'not_found', code: 'endpoint_not_found' });
|
|
308
|
+
if (!record.started || record.stopped) {
|
|
309
|
+
return Object.freeze({ status: 'failed', code: 'endpoint_not_active' });
|
|
310
|
+
}
|
|
311
|
+
const content = endpointContentOf(record.endpoint);
|
|
312
|
+
if (!content) return Object.freeze({ status: 'unsupported', code: 'content_resolution_unsupported' });
|
|
313
|
+
context.signal.throwIfAborted();
|
|
314
|
+
return content.resolve(reference, context);
|
|
264
315
|
}
|
|
265
316
|
}
|
|
266
317
|
|
|
@@ -374,22 +425,27 @@ async function createEndpoint(
|
|
|
374
425
|
admission: GenerationAdmissionGate,
|
|
375
426
|
signal: AbortSignal,
|
|
376
427
|
expansion?: EndpointExpansion,
|
|
377
|
-
): Promise<EndpointInstance
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
);
|
|
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);
|
|
386
437
|
assertEndpoint(endpoint, expansion?.id ?? slot.id);
|
|
438
|
+
if (slot.definition.capabilities.includes('outbound') && typeof endpoint.send !== 'function') {
|
|
439
|
+
throw new TypeError(
|
|
440
|
+
`Adapter Endpoint ${String(expansion?.id ?? slot.id)} declares outbound but send() is missing`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
387
443
|
assertDeclaredEndpointOperations(
|
|
388
444
|
endpoint,
|
|
389
|
-
|
|
445
|
+
operations,
|
|
390
446
|
String(expansion?.id ?? slot.id),
|
|
391
447
|
);
|
|
392
|
-
return endpoint;
|
|
448
|
+
return Object.freeze({ endpoint, operations });
|
|
393
449
|
}
|
|
394
450
|
|
|
395
451
|
async function stopRecords(
|
package/src/definition.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from '@zhin.js/im-contract';
|
|
8
8
|
import type { EndpointManagement } from './endpoint-management.js';
|
|
9
9
|
import type { EndpointControl } from './endpoint-control.js';
|
|
10
|
+
import type { EndpointContentPort } from './endpoint-content.js';
|
|
10
11
|
|
|
11
12
|
const adapterBrand = 'zhin.adapter/1' as const;
|
|
12
13
|
|
|
@@ -15,11 +16,18 @@ export type AdapterCapability = 'inbound' | 'outbound';
|
|
|
15
16
|
/** Operations beyond sending, declared by an Adapter definition. */
|
|
16
17
|
export type AdapterOperation = Exclude<EndpointOperation, 'send'>;
|
|
17
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
|
+
|
|
18
24
|
/** 端点可消费的出站媒体来源形式。 */
|
|
19
25
|
export type AdapterOutboundMedia = 'url' | 'path' | 'base64' | 'upload';
|
|
20
26
|
|
|
21
27
|
/** 交互段(卡片/按钮等富交互)的端点消费方式。 */
|
|
22
28
|
export type AdapterInteractiveMode = 'native' | 'text';
|
|
29
|
+
/** Markdown semantic segment consumption mode. */
|
|
30
|
+
export type AdapterMarkdownMode = 'native' | 'text';
|
|
23
31
|
|
|
24
32
|
export interface EndpointSendRequest {
|
|
25
33
|
/** 结构化会话寻址;端点在平台边界自行派生原生 target。 */
|
|
@@ -27,11 +35,13 @@ export interface EndpointSendRequest {
|
|
|
27
35
|
readonly payload: unknown;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
export interface EndpointInstance
|
|
38
|
+
export interface EndpointInstance {
|
|
31
39
|
/** Optional platform-neutral Console/Host management surface. */
|
|
32
40
|
readonly management?: EndpointManagement;
|
|
33
41
|
/** Optional platform-neutral control surface for existing messages. */
|
|
34
42
|
readonly control?: EndpointControl;
|
|
43
|
+
/** Optional canonical resolver for message, merged-forward and media references. */
|
|
44
|
+
readonly content?: EndpointContentPort;
|
|
35
45
|
/** Required readiness; must observe abort and settle before rollback returns. */
|
|
36
46
|
start?(signal: AbortSignal): void | Promise<void>;
|
|
37
47
|
/** Opens Endpoint-local flow behind the candidate generation admission gate. */
|
|
@@ -40,7 +50,8 @@ export interface EndpointInstance<TResult = unknown> {
|
|
|
40
50
|
close?(): void | Promise<void>;
|
|
41
51
|
/** Releases transport resources. Calls must be idempotent. */
|
|
42
52
|
stop?(): void | Promise<void>;
|
|
43
|
-
|
|
53
|
+
/** Platform message id. Core wraps it in the canonical MessageRef/DeliveryReceipt. */
|
|
54
|
+
send?(request: EndpointSendRequest): string | Promise<string>;
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
export interface AdapterContext<TConfig = unknown> extends CapabilityContext<TConfig> {
|
|
@@ -74,9 +85,11 @@ export interface AdapterSegmentPolicy {
|
|
|
74
85
|
readonly outboundMedia?: readonly AdapterOutboundMedia[];
|
|
75
86
|
/**
|
|
76
87
|
* 交互段(卡片/按钮等富交互)消费方式:`native` 原生渲染 / `text` 降级纯文本。
|
|
77
|
-
*
|
|
88
|
+
* Core 在最终出站阶段执行统一降级。
|
|
78
89
|
*/
|
|
79
90
|
readonly interactive?: AdapterInteractiveMode;
|
|
91
|
+
/** `native` preserves Markdown for the endpoint codec; `text` strips formatting in Core. */
|
|
92
|
+
readonly markdown?: AdapterMarkdownMode;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
const HTML_OUTBOUND_MODES: readonly HtmlOutboundMode[] = ['direct', 'image', 'text'];
|
|
@@ -87,7 +100,7 @@ const OUTBOUND_MEDIA_FORMS: readonly AdapterOutboundMedia[] = [
|
|
|
87
100
|
|
|
88
101
|
const ADAPTER_OPERATIONS: readonly AdapterOperation[] = ['recall', 'edit', 'reaction', 'typing'];
|
|
89
102
|
|
|
90
|
-
export interface AdapterDefinition<TConfig = unknown
|
|
103
|
+
export interface AdapterDefinition<TConfig = unknown> {
|
|
91
104
|
readonly $feature: typeof adapterBrand;
|
|
92
105
|
readonly capabilities: readonly AdapterCapability[];
|
|
93
106
|
/**
|
|
@@ -95,26 +108,26 @@ export interface AdapterDefinition<TConfig = unknown, TResult = unknown> {
|
|
|
95
108
|
* `capabilities: ['outbound']`; a method existing on an endpoint is not a
|
|
96
109
|
* capability declaration.
|
|
97
110
|
*/
|
|
98
|
-
readonly operations?:
|
|
111
|
+
readonly operations?: AdapterOperationDeclaration<TConfig>;
|
|
99
112
|
/** 可选:端点消息段能力声明(出站协商降级挂载点)。 */
|
|
100
113
|
readonly segments?: AdapterSegmentPolicy;
|
|
101
114
|
create(
|
|
102
115
|
context: AdapterContext<TConfig>,
|
|
103
|
-
): EndpointInstance
|
|
116
|
+
): EndpointInstance | Promise<EndpointInstance>;
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
declare module '@zhin.js/plugin-runtime' {
|
|
107
|
-
interface PluginSetupContext<TConfig> {
|
|
108
|
-
addAdapter
|
|
120
|
+
interface PluginSetupContext<TConfig = unknown> {
|
|
121
|
+
addAdapter(
|
|
109
122
|
localName: string,
|
|
110
|
-
definition: AdapterDefinition<TConfig
|
|
123
|
+
definition: AdapterDefinition<TConfig>,
|
|
111
124
|
): void;
|
|
112
125
|
}
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
export function defineAdapter<TConfig = unknown
|
|
116
|
-
definition: Omit<AdapterDefinition<TConfig
|
|
117
|
-
): Readonly<AdapterDefinition<TConfig
|
|
128
|
+
export function defineAdapter<TConfig = unknown>(
|
|
129
|
+
definition: Omit<AdapterDefinition<TConfig>, '$feature'>,
|
|
130
|
+
): Readonly<AdapterDefinition<TConfig>> {
|
|
118
131
|
if (typeof definition.create !== 'function') {
|
|
119
132
|
throw new TypeError('Adapter create must be a function');
|
|
120
133
|
}
|
|
@@ -126,7 +139,9 @@ export function defineAdapter<TConfig = unknown, TResult = unknown>(
|
|
|
126
139
|
throw new TypeError('Adapter capabilities must contain inbound and/or outbound');
|
|
127
140
|
}
|
|
128
141
|
const segments = normalizeSegmentPolicy(definition.segments);
|
|
129
|
-
const operations =
|
|
142
|
+
const operations = typeof definition.operations === 'function'
|
|
143
|
+
? definition.operations
|
|
144
|
+
: normalizeOperations(definition.operations);
|
|
130
145
|
return Object.freeze({
|
|
131
146
|
...definition,
|
|
132
147
|
$feature: adapterBrand,
|
|
@@ -139,8 +154,14 @@ export function defineAdapter<TConfig = unknown, TResult = unknown>(
|
|
|
139
154
|
/** Converts the definition's compact authoring form into the public contract. */
|
|
140
155
|
export function endpointCapabilitiesOf(
|
|
141
156
|
definition: Pick<AdapterDefinition, 'capabilities' | 'operations'>,
|
|
157
|
+
resolvedOperations?: readonly AdapterOperation[],
|
|
142
158
|
): EndpointCapabilities {
|
|
143
|
-
|
|
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>>>(
|
|
144
165
|
(result, operation) => ({ ...result, [operation]: true }),
|
|
145
166
|
{},
|
|
146
167
|
);
|
|
@@ -151,6 +172,18 @@ export function endpointCapabilitiesOf(
|
|
|
151
172
|
});
|
|
152
173
|
}
|
|
153
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
|
+
|
|
154
187
|
function normalizeOperations(
|
|
155
188
|
operations: readonly AdapterOperation[] | undefined,
|
|
156
189
|
): readonly AdapterOperation[] | undefined {
|
|
@@ -200,6 +233,13 @@ function normalizeSegmentPolicy(
|
|
|
200
233
|
) {
|
|
201
234
|
throw new TypeError("Adapter segments.interactive must be 'native' or 'text'");
|
|
202
235
|
}
|
|
236
|
+
if (
|
|
237
|
+
policy.markdown !== undefined
|
|
238
|
+
&& policy.markdown !== 'native'
|
|
239
|
+
&& policy.markdown !== 'text'
|
|
240
|
+
) {
|
|
241
|
+
throw new TypeError("Adapter segments.markdown must be 'native' or 'text'");
|
|
242
|
+
}
|
|
203
243
|
return Object.freeze({
|
|
204
244
|
...(policy.supported ? { supported: Object.freeze([...new Set(policy.supported)]) } : {}),
|
|
205
245
|
...(policy.html ? { html: policy.html } : {}),
|
|
@@ -207,6 +247,7 @@ function normalizeSegmentPolicy(
|
|
|
207
247
|
? { outboundMedia: Object.freeze([...new Set(policy.outboundMedia)]) }
|
|
208
248
|
: {}),
|
|
209
249
|
...(policy.interactive ? { interactive: policy.interactive } : {}),
|
|
250
|
+
...(policy.markdown ? { markdown: policy.markdown } : {}),
|
|
210
251
|
});
|
|
211
252
|
}
|
|
212
253
|
|
|
@@ -224,7 +265,9 @@ export function parseAdapterDefinition(value: unknown): AdapterDefinition {
|
|
|
224
265
|
) throw invalidAdapter();
|
|
225
266
|
// defineAdapter 已校验过形状;外部手工构造的 definition 也在此兜底
|
|
226
267
|
normalizeSegmentPolicy(definition.segments);
|
|
227
|
-
|
|
268
|
+
if (typeof definition.operations !== 'function') {
|
|
269
|
+
normalizeOperations(definition.operations);
|
|
270
|
+
}
|
|
228
271
|
return definition as AdapterDefinition;
|
|
229
272
|
}
|
|
230
273
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConversationReference,
|
|
3
|
+
ConversationResolution,
|
|
4
|
+
} from '@zhin.js/im-contract';
|
|
5
|
+
|
|
6
|
+
export interface EndpointContentResolveContext {
|
|
7
|
+
readonly signal: AbortSignal;
|
|
8
|
+
readonly maxDepth: number;
|
|
9
|
+
readonly maxEntries: number;
|
|
10
|
+
readonly maxChars: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Platform semantic port for resolving content that was not observed locally. */
|
|
14
|
+
export interface EndpointContentPort {
|
|
15
|
+
resolve(
|
|
16
|
+
reference: ConversationReference,
|
|
17
|
+
context: EndpointContentResolveContext,
|
|
18
|
+
): Promise<ConversationResolution>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function endpointContentOf(endpoint: unknown): EndpointContentPort | undefined {
|
|
22
|
+
if (!endpoint || typeof endpoint !== 'object') return undefined;
|
|
23
|
+
const content = (endpoint as { readonly content?: unknown }).content;
|
|
24
|
+
if (!content || typeof content !== 'object') return undefined;
|
|
25
|
+
return typeof (content as { readonly resolve?: unknown }).resolve === 'function'
|
|
26
|
+
? content as EndpointContentPort
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
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;
|
|
@@ -30,7 +39,7 @@ export function endpointControlOf(endpoint: unknown): EndpointControl | undefine
|
|
|
30
39
|
return explicit && typeof explicit === 'object' ? explicit : undefined;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
/** Checks only an Endpoint's explicit `control` port
|
|
42
|
+
/** Checks only an Endpoint's explicit `control` port; protocol methods are never probed. */
|
|
34
43
|
export function hasExplicitEndpointOperation(
|
|
35
44
|
endpoint: unknown,
|
|
36
45
|
operation: 'recall' | 'edit' | 'reaction' | 'typing',
|
|
@@ -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
|
+
}
|
|
@@ -23,6 +23,19 @@ export interface EndpointChannel {
|
|
|
23
23
|
readonly parent?: EndpointChannelParent;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Pending friend/group request for Console / Host listing (live, not inbox DB). */
|
|
27
|
+
export interface EndpointPendingRequest {
|
|
28
|
+
readonly platform_request_id: string;
|
|
29
|
+
readonly type: string;
|
|
30
|
+
readonly scene_type?: string | null;
|
|
31
|
+
readonly scene_id: string;
|
|
32
|
+
readonly sub_type?: string | null;
|
|
33
|
+
readonly actor_id: string;
|
|
34
|
+
readonly actor_name?: string | null;
|
|
35
|
+
readonly comment?: string | null;
|
|
36
|
+
readonly created_at: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
/**
|
|
27
40
|
* Optional, platform-neutral management surface exposed by an Endpoint.
|
|
28
41
|
*
|
|
@@ -35,6 +48,8 @@ export interface EndpointManagement {
|
|
|
35
48
|
listGroups?(): Promise<readonly EndpointGroup[]>;
|
|
36
49
|
listChannels?(): Promise<readonly EndpointChannel[]>;
|
|
37
50
|
listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
|
|
51
|
+
/** Live pending friend/group requests (preferred over unified_inbox_request). */
|
|
52
|
+
listRequests?(): Promise<readonly EndpointPendingRequest[]>;
|
|
38
53
|
approveRequest?(requestId: string, remark?: string): Promise<void>;
|
|
39
54
|
rejectRequest?(requestId: string, reason?: string): Promise<void>;
|
|
40
55
|
kickGroupMember?(groupId: string, userId: string): Promise<void>;
|
|
@@ -57,6 +72,7 @@ export const endpointManagementCapabilityIds = [
|
|
|
57
72
|
'listGroups',
|
|
58
73
|
'listChannels',
|
|
59
74
|
'listGroupMembers',
|
|
75
|
+
'listRequests',
|
|
60
76
|
'approveRequest',
|
|
61
77
|
'rejectRequest',
|
|
62
78
|
'kickGroupMember',
|
package/src/index.ts
CHANGED
|
@@ -7,5 +7,6 @@ export * from './endpoint-commands.js';
|
|
|
7
7
|
export * from './endpoint-lifecycle.js';
|
|
8
8
|
export * from './endpoint-management.js';
|
|
9
9
|
export * from './endpoint-control.js';
|
|
10
|
+
export * from './endpoint-content.js';
|
|
10
11
|
export * from './provider.js';
|
|
11
12
|
export { default } from './provider.js';
|