@zhin.js/adapter 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,42 @@ export default defineAdapter({
16
16
  本包只依赖 Kernel 与 Feature Kit,不包含具体平台 SDK。生产 manifest 指向
17
17
  `lib/provider.js`;开发时可通过 conditional export 读取源码。
18
18
 
19
+ 单文件插件可用 `setup({ addAdapter })` 注册 `defineAdapter(...)`;Endpoint 仍由同一个
20
+ AdapterIndex 和 generation handoff 管理。
21
+
22
+ ## Endpoint 生命周期基座(createEndpointLifecycle)
23
+
24
+ WS/SSE 类端点的 start/stop/重连/心跳统一走 `createEndpointLifecycle`
25
+ (`src/endpoint-lifecycle.ts`),不要手写 `#started`/`#scheduleReconnect` 状态机——
26
+ napcat/milky/onebot11/onebot12/satori 已迁移(各自曾独立犯过同一个 start 失败竞态)。
27
+ 基座内置:start 失败复位不武装重连、仅曾 open 才按退避重连(指数+jitter 可配)、
28
+ stop 主动断开不重连、心跳 PONG 看门狗、定时器集中清理、陈旧 socket 事件防叠套。
29
+ 迁移指引见该文件 JSDoc。
30
+
31
+ ## Adapter ↔ Endpoint:固定 1 对多
32
+
33
+ 一个 adapter 插件实例固定对应一到多个 endpoint:
34
+
35
+ - `plugins.<adapter>` 配置该 adapter 所有 endpoint 的**通用配置**(如凭据共享字段、
36
+ `master`、`intents`)。
37
+ - `plugins.<adapter>.endpoints[index]` 配置单个 endpoint 的**特殊配置**,逐项覆盖通用
38
+ 配置,`name` 必填。
39
+ - 不写 `endpoints` 时退化为单 endpoint(历史行为),实例 config 原样传给 `create()`。
40
+
41
+ 展开由 `expandEndpointConfigs`(`src/adapter-index.ts`)完成:endpoint record id 为
42
+ `<slotId>~<name>`,合并顺序 `{...通用, ...项}`(项优先),`endpoints` 键不下传给适配器。
43
+ record name 即 entry.name——Console 展示、`resolve`/`instance` 查找、inbox 落库都按它命中
44
+ 唯一 endpoint(适配器实例的 live name 如 icqq uin 优先于它展示)。entry.name 不得含
45
+ `~`/`\0`(会破坏 id 结构),重名/缺名的 entry 会被丢弃并 warn。
46
+ 多账号示例见 `plugins/adapters/icqq` / `plugins/adapters/qq` 的 README 与 schema。
47
+
48
+ ## 命令前缀(commandPrefix)
49
+
50
+ 适配器实例 config 支持 `commandPrefix`(默认 `''`):`''` 表示任意文本都按命令匹配;
51
+ `'/'` 则要求消息以 `/` 开头才进命令分发。`endpoints[i].commandPrefix` 可逐项覆盖。
52
+ 解析在 `@zhin.js/core` 的 `MessageDispatcher`(`defaultCommandPrefixResolver`);
53
+ `ImRuntime({ commandPrefix })` 可设全局静态前缀覆盖该行为。
54
+
19
55
  验证:`pnpm --filter @zhin.js/adapter test && pnpm --filter @zhin.js/adapter build`。
20
56
 
21
57
  架构说明见 [Plugin Monorepo 与 Feature Provider](../../../docs/architecture/target-implementation/plugin-monorepo-and-features.md)。
@@ -1,5 +1,6 @@
1
1
  import { type CapabilityId, type CapabilitySlot, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
- import type { AdapterCapability, AdapterDefinition, EndpointInstance, EndpointSendRequest } from './definition.js';
2
+ import type { AdapterCapability, AdapterDefinition, AdapterSegmentPolicy, EndpointInstance, EndpointSendRequest } from './definition.js';
3
+ import { type EndpointManagementCapability } from './endpoint-management.js';
3
4
  export interface AdapterDescriptor {
4
5
  readonly id: CapabilityId;
5
6
  readonly owner: PluginId;
@@ -12,6 +13,7 @@ export interface AdapterEndpointSummary extends AdapterDescriptor {
12
13
  readonly connected: boolean;
13
14
  readonly status: 'online' | 'offline';
14
15
  readonly phase: AdapterEndpointPhase;
16
+ readonly managementCapabilities: readonly EndpointManagementCapability[];
15
17
  }
16
18
  export type AdapterEndpointPhase = 'pending' | 'starting' | 'online' | 'failed' | 'unconfigured';
17
19
  export declare class AdapterIndex {
@@ -35,6 +37,11 @@ export declare class AdapterIndex {
35
37
  */
36
38
  instance(adapter: string, endpointId: string): EndpointInstance | undefined;
37
39
  owner(id: CapabilityId): PluginId;
40
+ /**
41
+ * Endpoint 的消息段能力声明(出站协商降级依据);
42
+ * 未声明或未知 id 返回 undefined(调用方按历史行为处理)。
43
+ */
44
+ segmentPolicy(id: CapabilityId): AdapterSegmentPolicy | undefined;
38
45
  start(): Promise<void>;
39
46
  open(): void;
40
47
  close(): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { DisposeStack, } from '@zhin.js/plugin-runtime';
2
2
  import { createCapabilityContext } from '@zhin.js/feature-kit';
3
3
  import { formatCompact, getLogger } from '@zhin.js/logger';
4
+ import { listEndpointManagementCapabilities, } from './endpoint-management.js';
4
5
  const logger = getLogger('Adapter');
5
6
  export class AdapterIndex {
6
7
  $projection = 'zhin.adapter-index/1';
@@ -23,24 +24,29 @@ export class AdapterIndex {
23
24
  const unconfigured = [];
24
25
  try {
25
26
  for (const slot of [...slots].sort((left, right) => left.id.localeCompare(right.id))) {
26
- const endpoint = await createEndpointSoft(slot, snapshot);
27
- if (endpoint.unconfigured)
28
- unconfigured.push(slot.localName);
29
- records.push({
30
- id: slot.id,
31
- owner: slot.owner,
32
- name: slot.localName,
33
- source: slot.source,
34
- capabilities: slot.definition.capabilities,
35
- endpoint: endpoint.instance,
36
- unconfigured: endpoint.unconfigured,
37
- started: false,
38
- open: false,
39
- failed: false,
40
- startAttempted: false,
41
- // Unconfigured stubs skip start/open so kitchen-sink Roots stay quiet.
42
- stopped: endpoint.unconfigured,
43
- });
27
+ for (const expansion of expandEndpointConfigs(slot, snapshot)) {
28
+ const endpoint = await createEndpointSoft(slot, snapshot, expansion);
29
+ if (endpoint.unconfigured)
30
+ unconfigured.push(expansion.name);
31
+ records.push({
32
+ id: expansion.id,
33
+ owner: slot.owner,
34
+ // 展开模式下 record name 即 endpoint 名(entry.name),
35
+ // 保证 Console 展示与 resolve/instance 按 entry name 命中唯一 record
36
+ name: expansion.name,
37
+ source: slot.source,
38
+ capabilities: slot.definition.capabilities,
39
+ endpoint: endpoint.instance,
40
+ ...(slot.definition.segments ? { segments: slot.definition.segments } : {}),
41
+ unconfigured: endpoint.unconfigured,
42
+ started: false,
43
+ open: false,
44
+ failed: false,
45
+ startAttempted: false,
46
+ // Unconfigured stubs skip start/open so kitchen-sink Roots stay quiet.
47
+ stopped: endpoint.unconfigured,
48
+ });
49
+ }
44
50
  }
45
51
  if (unconfigured.length > 0) {
46
52
  logger.info(formatCompact({
@@ -57,7 +63,7 @@ export class AdapterIndex {
57
63
  }
58
64
  }
59
65
  list() {
60
- return this.#order.map(({ endpoint: _endpoint, unconfigured: _unconfigured, started: _started, open: _open, stopped: _stopped, failed: _failed, startAttempted: _startAttempted, ...descriptor }) => Object.freeze(descriptor));
66
+ return this.#order.map(({ endpoint: _endpoint, unconfigured: _unconfigured, started: _started, open: _open, stopped: _stopped, failed: _failed, startAttempted: _startAttempted, segments: _segments, ...descriptor }) => Object.freeze(descriptor));
61
67
  }
62
68
  /** Endpoint rows for Console `endpoint.list` / `endpoint.info`. */
63
69
  describe() {
@@ -71,6 +77,7 @@ export class AdapterIndex {
71
77
  connected: record.open && !record.stopped,
72
78
  status: record.open && !record.stopped ? 'online' : 'offline',
73
79
  phase: endpointPhase(record),
80
+ managementCapabilities: listEndpointManagementCapabilities(record.endpoint),
74
81
  })));
75
82
  }
76
83
  /**
@@ -102,6 +109,13 @@ export class AdapterIndex {
102
109
  throw new Error(`Unknown Adapter Endpoint: ${id}`);
103
110
  return record.owner;
104
111
  }
112
+ /**
113
+ * Endpoint 的消息段能力声明(出站协商降级依据);
114
+ * 未声明或未知 id 返回 undefined(调用方按历史行为处理)。
115
+ */
116
+ segmentPolicy(id) {
117
+ return this.#records.get(id)?.segments;
118
+ }
105
119
  async start() {
106
120
  // Soft-start in parallel with a short wait so kitchen-sink Roots do not
107
121
  // stall generation. Configured platforms that need longer (QQ auth, Slack
@@ -252,8 +266,13 @@ export function isAdapterIndex(value) {
252
266
  && value.$projection === 'zhin.adapter-index/1';
253
267
  }
254
268
  function matchesEndpoint(record, adapter, endpointId) {
269
+ // 消息上的 $adapter 是 CapabilityId 的 localName 段(多 endpoint 展开后形如
270
+ // `icqq~8596238`)。CapabilityId 段分隔符是 \0(owner\0feature\0localName),
271
+ // 不能用 `/` 去 endsWith,否则永远匹配不上(endpoint not found)。
272
+ const localName = record.id.split('\0').pop() ?? record.id;
255
273
  const adapterOk = record.name === adapter
256
274
  || record.id === adapter
275
+ || localName === adapter
257
276
  || record.id.endsWith(`/${adapter}`)
258
277
  || record.owner === adapter
259
278
  || record.owner.endsWith(`/${adapter}`);
@@ -295,12 +314,72 @@ function isUnconfiguredError(error) {
295
314
  return (error instanceof TypeError
296
315
  && /requires|not configured|missing|未配置|缺少/i.test(error.message));
297
316
  }
298
- async function createEndpointSoft(slot, snapshot) {
317
+ /**
318
+ * 实例配置的 endpoint 展开:插件实例 config 含非空 `endpoints: [{name, ...覆盖}]` 时
319
+ * 按数组一一创建 endpoint(基础配置为实例 config 去掉 `endpoints` 键,逐项合并),
320
+ * 否则按实例 config 创建单个 endpoint(历史行为)。
321
+ */
322
+ function expandEndpointConfigs(slot, snapshot) {
323
+ const config = snapshot.config.get(slot.owner);
324
+ const raw = config?.endpoints;
325
+ const entries = Array.isArray(raw)
326
+ ? raw.filter((entry) => !!entry && typeof entry === 'object'
327
+ && typeof entry.name === 'string'
328
+ && entry.name.length > 0)
329
+ : [];
330
+ if (entries.length === 0) {
331
+ if (Array.isArray(raw) && raw.length > 0) {
332
+ logger.warn(formatCompact({
333
+ op: 'adapter_endpoints_entries_dropped',
334
+ id: slot.id,
335
+ reason: 'every endpoints entry is missing a non-empty string name',
336
+ }));
337
+ }
338
+ return Object.freeze([{ id: slot.id, name: slot.localName }]);
339
+ }
340
+ // `~` 是 record id 的分隔符、\0 是 CapabilityId 的分隔符,混入会破坏解析
341
+ const valid = entries.filter((entry) => {
342
+ if (/[~\0]/u.test(entry.name)) {
343
+ logger.warn(formatCompact({
344
+ op: 'adapter_endpoint_name_invalid',
345
+ id: slot.id,
346
+ name: entry.name,
347
+ }));
348
+ return false;
349
+ }
350
+ return true;
351
+ });
352
+ // 重名会让 #records 覆盖与 #order/resolve 三者不一致;保留首个并告警
353
+ const seen = new Set();
354
+ const deduped = valid.filter((entry) => {
355
+ if (seen.has(entry.name)) {
356
+ logger.warn(formatCompact({
357
+ op: 'adapter_endpoint_name_duplicate',
358
+ id: slot.id,
359
+ name: entry.name,
360
+ }));
361
+ return false;
362
+ }
363
+ seen.add(entry.name);
364
+ return true;
365
+ });
366
+ if (deduped.length === 0) {
367
+ return Object.freeze([{ id: slot.id, name: slot.localName }]);
368
+ }
369
+ const { endpoints: _drop, ...base } = (config ?? {});
370
+ return Object.freeze(deduped.map((entry) => Object.freeze({
371
+ id: `${slot.id}~${entry.name}`,
372
+ name: entry.name,
373
+ config: Object.freeze({ ...base, ...entry, name: entry.name }),
374
+ })));
375
+ }
376
+ async function createEndpointSoft(slot, snapshot, expansion) {
299
377
  let endpoint;
300
378
  try {
301
379
  endpoint = await slot.definition.create(Object.freeze({
302
380
  ...createCapabilityContext(snapshot, slot.owner),
303
- id: slot.id,
381
+ ...(expansion?.config ? { config: expansion.config } : {}),
382
+ id: expansion?.id ?? slot.id,
304
383
  name: slot.localName,
305
384
  }));
306
385
  }
@@ -313,8 +392,8 @@ async function createEndpointSoft(slot, snapshot) {
313
392
  const log = isUnconfiguredError(error) ? logger.debug.bind(logger) : logger.warn.bind(logger);
314
393
  log(formatCompact({
315
394
  op: 'adapter_create_soft_fail',
316
- id: slot.id,
317
- name: slot.localName,
395
+ id: expansion?.id ?? slot.id,
396
+ name: expansion?.name ?? slot.localName,
318
397
  error: message,
319
398
  }));
320
399
  return {
@@ -325,7 +404,7 @@ async function createEndpointSoft(slot, snapshot) {
325
404
  // Programming errors (create() did not return an Endpoint) must surface:
326
405
  // they propagate to AdapterIndex.create's catch, which disposes the records
327
406
  // created so far instead of hiding the bug behind an unconfigured stub.
328
- assertEndpoint(endpoint, slot.id);
407
+ assertEndpoint(endpoint, expansion?.id ?? slot.id);
329
408
  return { instance: endpoint, unconfigured: false };
330
409
  }
331
410
  function createUnconfiguredEndpoint(reason) {
@@ -1,7 +1,12 @@
1
1
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
2
2
  import type { CapabilityContext } from '@zhin.js/feature-kit';
3
+ import type { EndpointManagement } from './endpoint-management.js';
3
4
  declare const adapterBrand: "zhin.adapter/1";
4
5
  export type AdapterCapability = 'inbound' | 'outbound';
6
+ /** 端点可消费的出站媒体来源形式。 */
7
+ export type AdapterOutboundMedia = 'url' | 'path' | 'base64' | 'upload';
8
+ /** 交互段(卡片/按钮等富交互)的端点消费方式。 */
9
+ export type AdapterInteractiveMode = 'native' | 'text';
5
10
  export interface EndpointSendRequest {
6
11
  readonly target: string;
7
12
  readonly payload: unknown;
@@ -12,6 +17,8 @@ export interface EndpointSendRequest {
12
17
  };
13
18
  }
14
19
  export interface EndpointInstance<TResult = unknown> {
20
+ /** Optional platform-neutral Console/Host management surface. */
21
+ readonly management?: EndpointManagement;
15
22
  /** Allocates transport resources but must not admit inbound events yet. */
16
23
  start?(): void | Promise<void>;
17
24
  /** Opens admission after the candidate generation has committed. */
@@ -26,11 +33,47 @@ export interface AdapterContext<TConfig = unknown> extends CapabilityContext<TCo
26
33
  readonly id: CapabilityId;
27
34
  readonly name: string;
28
35
  }
36
+ /**
37
+ * html 段出站策略:
38
+ * - `direct`:端点直接消费 html 段(Console UI 类端点),核心不做任何转换;
39
+ * - `image`:经 html-renderer 渲染成 image 段,无渲染器时降级 text(缺省);
40
+ * - `text`:直接降级为 text 段。
41
+ */
42
+ export type HtmlOutboundMode = 'direct' | 'image' | 'text';
43
+ /**
44
+ * 端点消息段能力声明(出站协商降级的依据)。
45
+ * 缺省(未声明 `segments`)保持历史行为:仅 html 段按 image/text 处理,
46
+ * 其余段原样透传给端点。
47
+ */
48
+ export interface AdapterSegmentPolicy {
49
+ /**
50
+ * 端点原生可消费的 wire 段类型(如 `['text', 'image', 'at']`)。
51
+ * 声明后,未列出的段由核心按 `formatSegmentPreview` 降级为 text 段;
52
+ * 不声明则不过滤(全部透传)。
53
+ */
54
+ readonly supported?: readonly string[];
55
+ /** html 段处理策略,缺省 `image`。 */
56
+ readonly html?: HtmlOutboundMode;
57
+ /** 端点可消费的媒体来源形式;缺省表示不做媒体来源协商。 */
58
+ readonly outboundMedia?: readonly AdapterOutboundMedia[];
59
+ /**
60
+ * 交互段(卡片/按钮等富交互)消费方式:`native` 原生渲染 / `text` 降级纯文本。
61
+ * 目前仅为声明(供出站协商与门禁消费),`text` 的降级执行随 Wave 2 落地。
62
+ */
63
+ readonly interactive?: AdapterInteractiveMode;
64
+ }
29
65
  export interface AdapterDefinition<TConfig = unknown, TResult = unknown> {
30
66
  readonly $feature: typeof adapterBrand;
31
67
  readonly capabilities: readonly AdapterCapability[];
68
+ /** 可选:端点消息段能力声明(出站协商降级挂载点)。 */
69
+ readonly segments?: AdapterSegmentPolicy;
32
70
  create(context: AdapterContext<TConfig>): EndpointInstance<TResult> | Promise<EndpointInstance<TResult>>;
33
71
  }
72
+ declare module '@zhin.js/plugin-runtime' {
73
+ interface PluginSetupContext<TConfig> {
74
+ addAdapter<TResult = unknown>(localName: string, definition: AdapterDefinition<TConfig, TResult>): void;
75
+ }
76
+ }
34
77
  export declare function defineAdapter<TConfig = unknown, TResult = unknown>(definition: Omit<AdapterDefinition<TConfig, TResult>, '$feature'>): Readonly<AdapterDefinition<TConfig, TResult>>;
35
78
  export declare function parseAdapterDefinition(value: unknown): AdapterDefinition;
36
79
  export {};
package/lib/definition.js CHANGED
@@ -1,4 +1,8 @@
1
1
  const adapterBrand = 'zhin.adapter/1';
2
+ const HTML_OUTBOUND_MODES = ['direct', 'image', 'text'];
3
+ const OUTBOUND_MEDIA_FORMS = [
4
+ 'url', 'path', 'base64', 'upload',
5
+ ];
2
6
  export function defineAdapter(definition) {
3
7
  if (typeof definition.create !== 'function') {
4
8
  throw new TypeError('Adapter create must be a function');
@@ -8,10 +12,46 @@ export function defineAdapter(definition) {
8
12
  || capabilities.some((value) => value !== 'inbound' && value !== 'outbound')) {
9
13
  throw new TypeError('Adapter capabilities must contain inbound and/or outbound');
10
14
  }
15
+ const segments = normalizeSegmentPolicy(definition.segments);
11
16
  return Object.freeze({
12
17
  ...definition,
13
18
  $feature: adapterBrand,
14
19
  capabilities: Object.freeze(capabilities),
20
+ ...(segments ? { segments } : {}),
21
+ });
22
+ }
23
+ function normalizeSegmentPolicy(policy) {
24
+ if (policy === undefined)
25
+ return undefined;
26
+ if (!policy || typeof policy !== 'object') {
27
+ throw new TypeError('Adapter segments policy must be an object');
28
+ }
29
+ if (policy.supported !== undefined
30
+ && (!Array.isArray(policy.supported)
31
+ || policy.supported.some((type) => typeof type !== 'string' || !type))) {
32
+ throw new TypeError('Adapter segments.supported must be an array of segment type names');
33
+ }
34
+ if (policy.html !== undefined && !HTML_OUTBOUND_MODES.includes(policy.html)) {
35
+ throw new TypeError('Adapter segments.html must be direct, image or text');
36
+ }
37
+ if (policy.outboundMedia !== undefined
38
+ && (!Array.isArray(policy.outboundMedia)
39
+ || policy.outboundMedia.length === 0
40
+ || policy.outboundMedia.some((form) => !OUTBOUND_MEDIA_FORMS.includes(form)))) {
41
+ throw new TypeError("Adapter segments.outboundMedia must be a non-empty array of 'url' | 'path' | 'base64' | 'upload'");
42
+ }
43
+ if (policy.interactive !== undefined
44
+ && policy.interactive !== 'native'
45
+ && policy.interactive !== 'text') {
46
+ throw new TypeError("Adapter segments.interactive must be 'native' or 'text'");
47
+ }
48
+ return Object.freeze({
49
+ ...(policy.supported ? { supported: Object.freeze([...new Set(policy.supported)]) } : {}),
50
+ ...(policy.html ? { html: policy.html } : {}),
51
+ ...(policy.outboundMedia
52
+ ? { outboundMedia: Object.freeze([...new Set(policy.outboundMedia)]) }
53
+ : {}),
54
+ ...(policy.interactive ? { interactive: policy.interactive } : {}),
15
55
  });
16
56
  }
17
57
  export function parseAdapterDefinition(value) {
@@ -24,6 +64,8 @@ export function parseAdapterDefinition(value) {
24
64
  || definition.capabilities.length === 0
25
65
  || definition.capabilities.some((capability) => capability !== 'inbound' && capability !== 'outbound'))
26
66
  throw invalidAdapter();
67
+ // defineAdapter 已校验过形状;外部手工构造的 definition 也在此兜底
68
+ normalizeSegmentPolicy(definition.segments);
27
69
  return definition;
28
70
  }
29
71
  function invalidAdapter() {
@@ -0,0 +1,127 @@
1
+ import { type Token } from '@zhin.js/plugin-runtime';
2
+ /**
3
+ * endpoint 管理命令的操作者校验:实例配置声明了 master(顶层或任一端点项)时
4
+ * 仅 master 可执行管理命令;未配置则放行。
5
+ */
6
+ export declare function isEndpointOperator(config: unknown, input: unknown): boolean;
7
+ /** add/remove 的拒绝文案(list 只读,不校验)。 */
8
+ export declare function endpointCommandForbidden(adapterDisplayName: string): string;
9
+ export type EndpointCommandReply = (text: string) => Promise<unknown>;
10
+ /**
11
+ * 从命令 input(Runtime Message)提取 $reply;非消息来源(如 Host API 调用)降级为 no-op。
12
+ */
13
+ export declare function extractEndpointCommandReply(input: unknown): EndpointCommandReply;
14
+ export interface EndpointRunningInfo {
15
+ readonly name: string;
16
+ /** 连接模式(ws / wss / polling / socket-mode …),仅用于 list 展示。 */
17
+ readonly mode?: string;
18
+ }
19
+ export interface EndpointRuntimeState {
20
+ /** 当前 generation 已成功创建的 endpoint(name → 描述) */
21
+ readonly endpoints: Map<string, EndpointRunningInfo>;
22
+ }
23
+ export declare function createEndpointRuntimeState(): EndpointRuntimeState;
24
+ /** 每个适配器在模块顶层调用一次,创建自己的 runtime state token。 */
25
+ export declare function defineEndpointRuntimeStateToken(adapterKey: string): Token<EndpointRuntimeState>;
26
+ /** 项目根:ZHIN_PROJECT_ROOT 优先,缺省 process.cwd()(替代 legacy runtimeCwd) */
27
+ export declare function resolveProjectRoot(): string;
28
+ /** 派生 endpoint 凭据的 env 键:`${ADAPTER}_${NAME}_${FIELD}`(如 `TELEGRAM_MY_BOT_TOKEN`) */
29
+ export declare function buildEndpointEnvKey(adapterKey: string, endpointName: string, fieldKey: string): string;
30
+ /** 写入或更新 `.env` 中的键值,并同步到当前进程 `process.env` */
31
+ export declare function persistEndpointEnvValues(values: Readonly<Record<string, string>>, projectRoot?: string): void;
32
+ export interface ConfiguredEndpointEntry {
33
+ name: string;
34
+ [key: string]: unknown;
35
+ }
36
+ /** 定位项目配置文件:ZHIN_CONFIG 指定优先,否则发现 zhin.config.yml/.yaml,都没有则默认新建 zhin.config.yml */
37
+ export declare function findEndpointConfigFile(adapterKey: string, projectRoot?: string): string;
38
+ /** 读取 plugins.<adapterKey>.endpoints(plain JS);plugins/<adapterKey> 缺失或形态不符时返回 [] */
39
+ export declare function listConfiguredEndpoints(adapterKey: string, projectRoot?: string): ConfiguredEndpointEntry[];
40
+ /** 追加 endpoint 到 plugins.<adapterKey>.endpoints;name 已存在时报错 */
41
+ export declare function addEndpointToConfig(adapterKey: string, entry: ConfiguredEndpointEntry, projectRoot?: string): string;
42
+ /** 按 name 移除 plugins.<adapterKey>.endpoints 项;不存在返回 false */
43
+ export declare function removeEndpointFromConfig(adapterKey: string, name: string, projectRoot?: string): {
44
+ removed: boolean;
45
+ filePath: string;
46
+ };
47
+ /** add 命令可录入的字段描述(与 schema.json 的 endpoints.items.properties 对齐)。 */
48
+ export interface EndpointFieldSpec {
49
+ /** 配置字段 key(如 token / access_token / url / baseUrl) */
50
+ readonly key: string;
51
+ /** add 时必填(schema 中 required 的凭据/连接字段) */
52
+ readonly required?: boolean;
53
+ /** 凭据类字段:值写入 .env,yaml 保存 ${REF} 引用;否则内联写入 yaml */
54
+ readonly env?: boolean;
55
+ /** 字段说明(用于用法提示) */
56
+ readonly description?: string;
57
+ }
58
+ export type EndpointCommandUse = <T>(token: Token<T>) => T;
59
+ /** bindFlow 钩子上下文:接管 add 命令的自定义绑定流程(如 QQ 扫码)。 */
60
+ export interface EndpointBindFlowContext {
61
+ /** 命令参数 name(未指定时为 undefined,流程可自行决定终名) */
62
+ readonly name?: string;
63
+ /** 向当前会话推送后续状态(二维码刷新 / 成功 / 失败) */
64
+ readonly reply: EndpointCommandReply;
65
+ readonly config: unknown;
66
+ readonly input: unknown;
67
+ readonly use: EndpointCommandUse;
68
+ }
69
+ export interface EndpointCommandsSpec {
70
+ /** 实例 key(zhin.config.yml 的 plugins.<key>,如 telegram / napcat) */
71
+ readonly adapterKey: string;
72
+ /** 展示名(QQ / Telegram / NapCat …),用于权限与列表文案 */
73
+ readonly adapterDisplayName: string;
74
+ /** add 命令可录入字段(bindFlow 接管 add 时仅用于展示) */
75
+ readonly fields?: readonly EndpointFieldSpec[];
76
+ /** 运行中 endpoints 数据源(可选):通常读 adapter create 注册的 runtime state */
77
+ readonly running?: (use: EndpointCommandUse) => Iterable<EndpointRunningInfo>;
78
+ /** list 中配置项的附加描述(如 entry => `appid: ${entry.appid}`) */
79
+ readonly describeEntry?: (entry: ConfiguredEndpointEntry) => string;
80
+ /** list 末尾的附加行(如 QQ 的进行中扫码提示);返回 undefined 不加行 */
81
+ readonly listFooter?: (use: EndpointCommandUse) => string | undefined;
82
+ /** 自定义 add 流程(扫码绑定等);提供时 add 命令忽略 kv 参数,交给钩子 */
83
+ readonly bindFlow?: (context: EndpointBindFlowContext) => Promise<string> | string;
84
+ /** add 命令描述覆盖 */
85
+ readonly addDescription?: string;
86
+ }
87
+ /**
88
+ * 命令定义的最小结构(与 @zhin.js/command 的 CommandDefinition 结构兼容)。
89
+ * provider 层不允许 import @zhin.js/command,故 defineCommand 由调用方注入,
90
+ * 这里只描述结构;适配器侧传入 defineCommand 后 TCommand 即 Readonly<CommandDefinition>。
91
+ *
92
+ * `params` 值域须与 CommandParameterValue 对齐(含 null / 结构化对象),
93
+ * 否则注入的 defineCommand 会因 TS 逆变检查失败(TS2345)。
94
+ */
95
+ export interface EndpointCommandContext {
96
+ readonly config: unknown;
97
+ /**
98
+ * 与 `@zhin.js/command` 的 `CommandContext.input` 对齐:IM 命中时有值,
99
+ * Host / 无消息路径可为 `undefined`。须保持可选,否则注入 `defineCommand` 会因
100
+ * execute 参数逆变检查失败(TS2345)。
101
+ */
102
+ readonly input?: unknown;
103
+ readonly args: readonly string[];
104
+ readonly params: Readonly<Record<string, string | number | boolean | Readonly<Record<string, unknown>> | null>>;
105
+ readonly use: EndpointCommandUse;
106
+ }
107
+ export interface EndpointCommandDefinition {
108
+ readonly description?: string;
109
+ execute(context: EndpointCommandContext): unknown;
110
+ }
111
+ export interface EndpointCommands<TCommand = EndpointCommandDefinition> {
112
+ readonly list: TCommand;
113
+ readonly add: TCommand;
114
+ readonly remove: TCommand;
115
+ }
116
+ /** list 文案:运行中 + 配置中两段,footer 可选。 */
117
+ export declare function formatEndpointList(spec: Pick<EndpointCommandsSpec, 'adapterKey' | 'adapterDisplayName' | 'describeEntry'>, source: {
118
+ readonly running: Iterable<EndpointRunningInfo>;
119
+ readonly configured: readonly ConfiguredEndpointEntry[];
120
+ readonly footer?: string;
121
+ }): string;
122
+ /** add(kv 模式)的完整业务逻辑:解析 kv → 凭据写 .env → 追加 yaml;返回回复文本。 */
123
+ export declare function addEndpointFromKeyValues(spec: EndpointCommandsSpec, name: string, args: readonly string[], projectRoot?: string): string;
124
+ /** remove 的完整业务逻辑:从 yaml 移除;返回回复文本。 */
125
+ export declare function removeEndpointByName(spec: Pick<EndpointCommandsSpec, 'adapterKey'>, name: string, projectRoot?: string): string;
126
+ /** 生成 `<adapter> endpoint` 的 list / add / remove 三个命令定义(见文件头接入步骤)。 */
127
+ export declare function createEndpointCommands<TCommand>(spec: EndpointCommandsSpec, defineCommand: (definition: EndpointCommandDefinition) => TCommand): EndpointCommands<TCommand>;