@zhin.js/adapter 1.1.9 → 1.1.11

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
@@ -8,10 +8,10 @@ generation lifecycle。候选 Endpoint 可完成连接 readiness,但入站由
8
8
  销毁整组候选 Endpoint 并拒绝本次 generation,不存在 inert stub 或后台 late-open。
9
9
 
10
10
  ```ts
11
- import { defineAdapter } from '@zhin.js/adapter';
11
+ import { defineAdapter } from 'zhin.js/adapter';
12
12
 
13
13
  export default defineAdapter({
14
- capabilities: ['inbound', 'outbound'],
14
+ capabilities: ['inbound'],
15
15
  create: (context) => ({ name: context.name }),
16
16
  });
17
17
  ```
@@ -32,7 +32,9 @@ instead of probing optional endpoint methods. The zero-dependency types live in
32
32
 
33
33
  Framework-facing outbound code carries a structured `ConversationRef`.
34
34
  `EndpointSendRequest` is `{ conversation, payload }`; platform adapters derive
35
- their native target from `conversation` at the endpoint boundary.
35
+ their native target from `conversation` at the endpoint boundary and return one
36
+ non-empty platform message id. IM Runtime alone wraps that id as a structured
37
+ `MessageRef` / `DeliveryReceipt`; arbitrary endpoint result shapes are rejected.
36
38
 
37
39
  ## Endpoint Control Port
38
40
 
@@ -66,7 +68,7 @@ stop 主动断开不重连、心跳 PONG 看门狗、定时器集中清理、陈
66
68
 
67
69
  展开由 `expandEndpointConfigs`(`src/adapter-index.ts`)完成:endpoint record id 为
68
70
  `<slotId>~<name>`,合并顺序 `{...通用, ...项}`(项优先),`endpoints` 键不下传给适配器。
69
- record name 即 entry.name——Console 展示、`resolve`/`instance` 查找、inbox 落库都按它命中
71
+ record name 即 entry.name——Console 展示、endpoint identity 解析、inbox 落库都按它命中
70
72
  唯一 endpoint(适配器实例的 live name 如 icqq uin 优先于它展示)。entry.name 不得含
71
73
  `~`/`\0`(会破坏 id 结构),重名/缺名的 entry 会被丢弃并 warn。
72
74
  多账号示例见 `plugins/adapters/icqq` / `plugins/adapters/qq` 的 README 与 schema。
@@ -1,6 +1,8 @@
1
1
  import { generationAdmissionSource, type CapabilityId, type CapabilitySlot, type GenerationAdmissionGate, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
2
  import type { AdapterCapability, AdapterDefinition, AdapterSegmentPolicy, EndpointInstance, EndpointSendRequest } from './definition.js';
3
3
  import { type EndpointManagementCapability } from './endpoint-management.js';
4
+ import { type EndpointContentResolveContext } from './endpoint-content.js';
5
+ import type { ConversationReference, ConversationResolution } from '@zhin.js/im-contract';
4
6
  export interface AdapterDescriptor {
5
7
  readonly id: CapabilityId;
6
8
  readonly owner: PluginId;
@@ -46,6 +48,7 @@ export declare class AdapterIndex {
46
48
  open(): void;
47
49
  close(): Promise<void>;
48
50
  stop(): Promise<void>;
49
- send(id: CapabilityId, request: EndpointSendRequest): Promise<unknown>;
51
+ send(id: CapabilityId, request: EndpointSendRequest): Promise<string>;
52
+ resolveContent(id: CapabilityId, reference: ConversationReference, context: EndpointContentResolveContext): Promise<ConversationResolution>;
50
53
  }
51
54
  export declare function isAdapterIndex(value: unknown): value is AdapterIndex;
@@ -2,6 +2,7 @@ import { DisposeStack, GenerationCompensationError, createGenerationAdmissionGat
2
2
  import { createCapabilityContext } from '@zhin.js/feature-kit';
3
3
  import { listEndpointManagementCapabilities, } from './endpoint-management.js';
4
4
  import { assertDeclaredEndpointOperations } from './endpoint-control.js';
5
+ import { endpointContentOf } from './endpoint-content.js';
5
6
  export class AdapterIndex {
6
7
  $projection = 'zhin.adapter-index/1';
7
8
  #records = new Map();
@@ -194,7 +195,24 @@ export class AdapterIndex {
194
195
  if (!record.started || record.stopped) {
195
196
  throw new Error(`Adapter Endpoint is not active: ${id}`);
196
197
  }
197
- return record.endpoint.send(request);
198
+ const messageId = await record.endpoint.send(request);
199
+ if (typeof messageId !== 'string' || !messageId.trim()) {
200
+ throw new TypeError(`Adapter Endpoint send() must return a non-empty platform message id: ${id}`);
201
+ }
202
+ return messageId;
203
+ }
204
+ async resolveContent(id, reference, context) {
205
+ const record = this.#records.get(id);
206
+ if (!record)
207
+ return Object.freeze({ status: 'not_found', code: 'endpoint_not_found' });
208
+ if (!record.started || record.stopped) {
209
+ return Object.freeze({ status: 'failed', code: 'endpoint_not_active' });
210
+ }
211
+ const content = endpointContentOf(record.endpoint);
212
+ if (!content)
213
+ return Object.freeze({ status: 'unsupported', code: 'content_resolution_unsupported' });
214
+ context.signal.throwIfAborted();
215
+ return content.resolve(reference, context);
198
216
  }
199
217
  }
200
218
  export function isAdapterIndex(value) {
@@ -289,6 +307,9 @@ async function createEndpoint(slot, snapshot, admission, signal, expansion) {
289
307
  name: slot.localName,
290
308
  }));
291
309
  assertEndpoint(endpoint, expansion?.id ?? slot.id);
310
+ if (slot.definition.capabilities.includes('outbound') && typeof endpoint.send !== 'function') {
311
+ throw new TypeError(`Adapter Endpoint ${String(expansion?.id ?? slot.id)} declares outbound but send() is missing`);
312
+ }
292
313
  assertDeclaredEndpointOperations(endpoint, slot.definition.operations, String(expansion?.id ?? slot.id));
293
314
  return endpoint;
294
315
  }
@@ -3,6 +3,7 @@ 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. */
@@ -11,16 +12,20 @@ export type AdapterOperation = Exclude<EndpointOperation, 'send'>;
11
12
  export type AdapterOutboundMedia = 'url' | 'path' | 'base64' | 'upload';
12
13
  /** 交互段(卡片/按钮等富交互)的端点消费方式。 */
13
14
  export type AdapterInteractiveMode = 'native' | 'text';
15
+ /** Markdown semantic segment consumption mode. */
16
+ export type AdapterMarkdownMode = 'native' | 'text';
14
17
  export interface EndpointSendRequest {
15
18
  /** 结构化会话寻址;端点在平台边界自行派生原生 target。 */
16
19
  readonly conversation: ConversationRef;
17
20
  readonly payload: unknown;
18
21
  }
19
- export interface EndpointInstance<TResult = unknown> {
22
+ export interface EndpointInstance {
20
23
  /** Optional platform-neutral Console/Host management surface. */
21
24
  readonly management?: EndpointManagement;
22
25
  /** Optional platform-neutral control surface for existing messages. */
23
26
  readonly control?: EndpointControl;
27
+ /** Optional canonical resolver for message, merged-forward and media references. */
28
+ readonly content?: EndpointContentPort;
24
29
  /** Required readiness; must observe abort and settle before rollback returns. */
25
30
  start?(signal: AbortSignal): void | Promise<void>;
26
31
  /** Opens Endpoint-local flow behind the candidate generation admission gate. */
@@ -29,7 +34,8 @@ export interface EndpointInstance<TResult = unknown> {
29
34
  close?(): void | Promise<void>;
30
35
  /** Releases transport resources. Calls must be idempotent. */
31
36
  stop?(): void | Promise<void>;
32
- send?(request: EndpointSendRequest): TResult | Promise<TResult>;
37
+ /** Platform message id. Core wraps it in the canonical MessageRef/DeliveryReceipt. */
38
+ send?(request: EndpointSendRequest): string | Promise<string>;
33
39
  }
34
40
  export interface AdapterContext<TConfig = unknown> extends CapabilityContext<TConfig> {
35
41
  readonly id: CapabilityId;
@@ -60,11 +66,13 @@ export interface AdapterSegmentPolicy {
60
66
  readonly outboundMedia?: readonly AdapterOutboundMedia[];
61
67
  /**
62
68
  * 交互段(卡片/按钮等富交互)消费方式:`native` 原生渲染 / `text` 降级纯文本。
63
- * 目前仅为声明(供出站协商与门禁消费),`text` 的降级执行随 Wave 2 落地。
69
+ * Core 在最终出站阶段执行统一降级。
64
70
  */
65
71
  readonly interactive?: AdapterInteractiveMode;
72
+ /** `native` preserves Markdown for the endpoint codec; `text` strips formatting in Core. */
73
+ readonly markdown?: AdapterMarkdownMode;
66
74
  }
67
- export interface AdapterDefinition<TConfig = unknown, TResult = unknown> {
75
+ export interface AdapterDefinition<TConfig = unknown> {
68
76
  readonly $feature: typeof adapterBrand;
69
77
  readonly capabilities: readonly AdapterCapability[];
70
78
  /**
@@ -75,14 +83,14 @@ export interface AdapterDefinition<TConfig = unknown, TResult = unknown> {
75
83
  readonly operations?: readonly AdapterOperation[];
76
84
  /** 可选:端点消息段能力声明(出站协商降级挂载点)。 */
77
85
  readonly segments?: AdapterSegmentPolicy;
78
- create(context: AdapterContext<TConfig>): EndpointInstance<TResult> | Promise<EndpointInstance<TResult>>;
86
+ create(context: AdapterContext<TConfig>): EndpointInstance | Promise<EndpointInstance>;
79
87
  }
80
88
  declare module '@zhin.js/plugin-runtime' {
81
- interface PluginSetupContext<TConfig> {
82
- addAdapter<TResult = unknown>(localName: string, definition: AdapterDefinition<TConfig, TResult>): void;
89
+ interface PluginSetupContext<TConfig = unknown> {
90
+ addAdapter(localName: string, definition: AdapterDefinition<TConfig>): void;
83
91
  }
84
92
  }
85
- export declare function defineAdapter<TConfig = unknown, TResult = unknown>(definition: Omit<AdapterDefinition<TConfig, TResult>, '$feature'>): Readonly<AdapterDefinition<TConfig, TResult>>;
93
+ export declare function defineAdapter<TConfig = unknown>(definition: Omit<AdapterDefinition<TConfig>, '$feature'>): Readonly<AdapterDefinition<TConfig>>;
86
94
  /** Converts the definition's compact authoring form into the public contract. */
87
95
  export declare function endpointCapabilitiesOf(definition: Pick<AdapterDefinition, 'capabilities' | 'operations'>): EndpointCapabilities;
88
96
  export declare function parseAdapterDefinition(value: unknown): AdapterDefinition;
package/lib/definition.js CHANGED
@@ -66,6 +66,11 @@ function normalizeSegmentPolicy(policy) {
66
66
  && policy.interactive !== 'text') {
67
67
  throw new TypeError("Adapter segments.interactive must be 'native' or 'text'");
68
68
  }
69
+ if (policy.markdown !== undefined
70
+ && policy.markdown !== 'native'
71
+ && policy.markdown !== 'text') {
72
+ throw new TypeError("Adapter segments.markdown must be 'native' or 'text'");
73
+ }
69
74
  return Object.freeze({
70
75
  ...(policy.supported ? { supported: Object.freeze([...new Set(policy.supported)]) } : {}),
71
76
  ...(policy.html ? { html: policy.html } : {}),
@@ -73,6 +78,7 @@ function normalizeSegmentPolicy(policy) {
73
78
  ? { outboundMedia: Object.freeze([...new Set(policy.outboundMedia)]) }
74
79
  : {}),
75
80
  ...(policy.interactive ? { interactive: policy.interactive } : {}),
81
+ ...(policy.markdown ? { markdown: policy.markdown } : {}),
76
82
  });
77
83
  }
78
84
  export function parseAdapterDefinition(value) {
@@ -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
+ }
@@ -21,7 +21,7 @@ export interface EndpointWithControl {
21
21
  }
22
22
  /** Reads the canonical control port without probing protocol-specific methods. */
23
23
  export declare function endpointControlOf(endpoint: unknown): EndpointControl | undefined;
24
- /** Checks only an Endpoint's explicit `control` port, never the legacy bridge. */
24
+ /** Checks only an Endpoint's explicit `control` port; protocol methods are never probed. */
25
25
  export declare function hasExplicitEndpointOperation(endpoint: unknown, operation: 'recall' | 'edit' | 'reaction' | 'typing'): boolean;
26
26
  /** Rejects a declaration that cannot be fulfilled by the explicit control port. */
27
27
  export declare function assertDeclaredEndpointOperations(endpoint: unknown, operations: readonly ('recall' | 'edit' | 'reaction' | 'typing')[] | undefined, id: string): void;
@@ -5,7 +5,7 @@ export function endpointControlOf(endpoint) {
5
5
  const explicit = endpoint.control;
6
6
  return explicit && typeof explicit === 'object' ? explicit : undefined;
7
7
  }
8
- /** Checks only an Endpoint's explicit `control` port, never the legacy bridge. */
8
+ /** Checks only an Endpoint's explicit `control` port; protocol methods are never probed. */
9
9
  export function hasExplicitEndpointOperation(endpoint, operation) {
10
10
  if (!endpoint || typeof endpoint !== 'object')
11
11
  return false;
@@ -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. */
@@ -8,6 +8,7 @@ export const endpointManagementCapabilityIds = [
8
8
  'listGroups',
9
9
  'listChannels',
10
10
  'listGroupMembers',
11
+ 'listRequests',
11
12
  'approveRequest',
12
13
  'rejectRequest',
13
14
  'kickGroupMember',
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, unknown>, AdapterIndex>>;
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.1.9",
3
+ "version": "1.1.11",
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.10",
22
- "@zhin.js/im-contract": "1.0.3",
23
- "@zhin.js/plugin-runtime": "1.1.6",
24
- "@zhin.js/logger": "1.0.76"
21
+ "@zhin.js/im-contract": "1.0.4",
22
+ "@zhin.js/feature-kit": "1.0.12",
23
+ "@zhin.js/logger": "1.0.76",
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.13"
29
+ "@zhin.js/command": "1.0.15"
30
30
  },
31
31
  "zhin": {
32
32
  "protocol": 1,
@@ -22,6 +22,8 @@ import {
22
22
  type EndpointManagementCapability,
23
23
  } from './endpoint-management.js';
24
24
  import { assertDeclaredEndpointOperations } from './endpoint-control.js';
25
+ import { endpointContentOf, type EndpointContentResolveContext } from './endpoint-content.js';
26
+ import type { ConversationReference, ConversationResolution } from '@zhin.js/im-contract';
25
27
 
26
28
  export interface AdapterDescriptor {
27
29
  readonly id: CapabilityId;
@@ -251,7 +253,7 @@ export class AdapterIndex {
251
253
  await stack.dispose();
252
254
  }
253
255
 
254
- async send(id: CapabilityId, request: EndpointSendRequest): Promise<unknown> {
256
+ async send(id: CapabilityId, request: EndpointSendRequest): Promise<string> {
255
257
  const record = this.#records.get(id);
256
258
  if (!record) throw new Error(`Unknown Adapter Endpoint: ${id}`);
257
259
  if (!record.capabilities.includes('outbound') || !record.endpoint.send) {
@@ -260,7 +262,27 @@ export class AdapterIndex {
260
262
  if (!record.started || record.stopped) {
261
263
  throw new Error(`Adapter Endpoint is not active: ${id}`);
262
264
  }
263
- return record.endpoint.send(request);
265
+ const messageId = await record.endpoint.send(request);
266
+ if (typeof messageId !== 'string' || !messageId.trim()) {
267
+ throw new TypeError(`Adapter Endpoint send() must return a non-empty platform message id: ${id}`);
268
+ }
269
+ return messageId;
270
+ }
271
+
272
+ async resolveContent(
273
+ id: CapabilityId,
274
+ reference: ConversationReference,
275
+ context: EndpointContentResolveContext,
276
+ ): Promise<ConversationResolution> {
277
+ const record = this.#records.get(id);
278
+ if (!record) return Object.freeze({ status: 'not_found', code: 'endpoint_not_found' });
279
+ if (!record.started || record.stopped) {
280
+ return Object.freeze({ status: 'failed', code: 'endpoint_not_active' });
281
+ }
282
+ const content = endpointContentOf(record.endpoint);
283
+ if (!content) return Object.freeze({ status: 'unsupported', code: 'content_resolution_unsupported' });
284
+ context.signal.throwIfAborted();
285
+ return content.resolve(reference, context);
264
286
  }
265
287
  }
266
288
 
@@ -384,6 +406,11 @@ async function createEndpoint(
384
406
  }),
385
407
  );
386
408
  assertEndpoint(endpoint, expansion?.id ?? slot.id);
409
+ if (slot.definition.capabilities.includes('outbound') && typeof endpoint.send !== 'function') {
410
+ throw new TypeError(
411
+ `Adapter Endpoint ${String(expansion?.id ?? slot.id)} declares outbound but send() is missing`,
412
+ );
413
+ }
387
414
  assertDeclaredEndpointOperations(
388
415
  endpoint,
389
416
  slot.definition.operations,
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
 
@@ -20,6 +21,8 @@ export type AdapterOutboundMedia = 'url' | 'path' | 'base64' | 'upload';
20
21
 
21
22
  /** 交互段(卡片/按钮等富交互)的端点消费方式。 */
22
23
  export type AdapterInteractiveMode = 'native' | 'text';
24
+ /** Markdown semantic segment consumption mode. */
25
+ export type AdapterMarkdownMode = 'native' | 'text';
23
26
 
24
27
  export interface EndpointSendRequest {
25
28
  /** 结构化会话寻址;端点在平台边界自行派生原生 target。 */
@@ -27,11 +30,13 @@ export interface EndpointSendRequest {
27
30
  readonly payload: unknown;
28
31
  }
29
32
 
30
- export interface EndpointInstance<TResult = unknown> {
33
+ export interface EndpointInstance {
31
34
  /** Optional platform-neutral Console/Host management surface. */
32
35
  readonly management?: EndpointManagement;
33
36
  /** Optional platform-neutral control surface for existing messages. */
34
37
  readonly control?: EndpointControl;
38
+ /** Optional canonical resolver for message, merged-forward and media references. */
39
+ readonly content?: EndpointContentPort;
35
40
  /** Required readiness; must observe abort and settle before rollback returns. */
36
41
  start?(signal: AbortSignal): void | Promise<void>;
37
42
  /** Opens Endpoint-local flow behind the candidate generation admission gate. */
@@ -40,7 +45,8 @@ export interface EndpointInstance<TResult = unknown> {
40
45
  close?(): void | Promise<void>;
41
46
  /** Releases transport resources. Calls must be idempotent. */
42
47
  stop?(): void | Promise<void>;
43
- send?(request: EndpointSendRequest): TResult | Promise<TResult>;
48
+ /** Platform message id. Core wraps it in the canonical MessageRef/DeliveryReceipt. */
49
+ send?(request: EndpointSendRequest): string | Promise<string>;
44
50
  }
45
51
 
46
52
  export interface AdapterContext<TConfig = unknown> extends CapabilityContext<TConfig> {
@@ -74,9 +80,11 @@ export interface AdapterSegmentPolicy {
74
80
  readonly outboundMedia?: readonly AdapterOutboundMedia[];
75
81
  /**
76
82
  * 交互段(卡片/按钮等富交互)消费方式:`native` 原生渲染 / `text` 降级纯文本。
77
- * 目前仅为声明(供出站协商与门禁消费),`text` 的降级执行随 Wave 2 落地。
83
+ * Core 在最终出站阶段执行统一降级。
78
84
  */
79
85
  readonly interactive?: AdapterInteractiveMode;
86
+ /** `native` preserves Markdown for the endpoint codec; `text` strips formatting in Core. */
87
+ readonly markdown?: AdapterMarkdownMode;
80
88
  }
81
89
 
82
90
  const HTML_OUTBOUND_MODES: readonly HtmlOutboundMode[] = ['direct', 'image', 'text'];
@@ -87,7 +95,7 @@ const OUTBOUND_MEDIA_FORMS: readonly AdapterOutboundMedia[] = [
87
95
 
88
96
  const ADAPTER_OPERATIONS: readonly AdapterOperation[] = ['recall', 'edit', 'reaction', 'typing'];
89
97
 
90
- export interface AdapterDefinition<TConfig = unknown, TResult = unknown> {
98
+ export interface AdapterDefinition<TConfig = unknown> {
91
99
  readonly $feature: typeof adapterBrand;
92
100
  readonly capabilities: readonly AdapterCapability[];
93
101
  /**
@@ -100,21 +108,21 @@ export interface AdapterDefinition<TConfig = unknown, TResult = unknown> {
100
108
  readonly segments?: AdapterSegmentPolicy;
101
109
  create(
102
110
  context: AdapterContext<TConfig>,
103
- ): EndpointInstance<TResult> | Promise<EndpointInstance<TResult>>;
111
+ ): EndpointInstance | Promise<EndpointInstance>;
104
112
  }
105
113
 
106
114
  declare module '@zhin.js/plugin-runtime' {
107
- interface PluginSetupContext<TConfig> {
108
- addAdapter<TResult = unknown>(
115
+ interface PluginSetupContext<TConfig = unknown> {
116
+ addAdapter(
109
117
  localName: string,
110
- definition: AdapterDefinition<TConfig, TResult>,
118
+ definition: AdapterDefinition<TConfig>,
111
119
  ): void;
112
120
  }
113
121
  }
114
122
 
115
- export function defineAdapter<TConfig = unknown, TResult = unknown>(
116
- definition: Omit<AdapterDefinition<TConfig, TResult>, '$feature'>,
117
- ): Readonly<AdapterDefinition<TConfig, TResult>> {
123
+ export function defineAdapter<TConfig = unknown>(
124
+ definition: Omit<AdapterDefinition<TConfig>, '$feature'>,
125
+ ): Readonly<AdapterDefinition<TConfig>> {
118
126
  if (typeof definition.create !== 'function') {
119
127
  throw new TypeError('Adapter create must be a function');
120
128
  }
@@ -200,6 +208,13 @@ function normalizeSegmentPolicy(
200
208
  ) {
201
209
  throw new TypeError("Adapter segments.interactive must be 'native' or 'text'");
202
210
  }
211
+ if (
212
+ policy.markdown !== undefined
213
+ && policy.markdown !== 'native'
214
+ && policy.markdown !== 'text'
215
+ ) {
216
+ throw new TypeError("Adapter segments.markdown must be 'native' or 'text'");
217
+ }
203
218
  return Object.freeze({
204
219
  ...(policy.supported ? { supported: Object.freeze([...new Set(policy.supported)]) } : {}),
205
220
  ...(policy.html ? { html: policy.html } : {}),
@@ -207,6 +222,7 @@ function normalizeSegmentPolicy(
207
222
  ? { outboundMedia: Object.freeze([...new Set(policy.outboundMedia)]) }
208
223
  : {}),
209
224
  ...(policy.interactive ? { interactive: policy.interactive } : {}),
225
+ ...(policy.markdown ? { markdown: policy.markdown } : {}),
210
226
  });
211
227
  }
212
228
 
@@ -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
+ }
@@ -30,7 +30,7 @@ export function endpointControlOf(endpoint: unknown): EndpointControl | undefine
30
30
  return explicit && typeof explicit === 'object' ? explicit : undefined;
31
31
  }
32
32
 
33
- /** Checks only an Endpoint's explicit `control` port, never the legacy bridge. */
33
+ /** Checks only an Endpoint's explicit `control` port; protocol methods are never probed. */
34
34
  export function hasExplicitEndpointOperation(
35
35
  endpoint: unknown,
36
36
  operation: 'recall' | 'edit' | 'reaction' | 'typing',
@@ -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';