@zhin.js/core 1.5.7 → 1.5.8

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/lib/adapter.d.ts CHANGED
@@ -88,8 +88,7 @@ export declare abstract class Adapter<R extends Endpoint = Endpoint, const Caps
88
88
  sendMessage(options: SendOptions): Promise<string>;
89
89
  /**
90
90
  * `call.recallMessage` 事件链(prompt.ts 超时撤回等)的统一出口:
91
- * 经 canonical `EndpointControl` 端口撤回,classic 端点的 `$` 方法由
92
- * `resolveEndpointControl` 迁移桥适配。
91
+ * 经 canonical `EndpointControl` 端口撤回。
93
92
  */
94
93
  private recallEndpointMessage;
95
94
  /**
package/lib/adapter.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { assertOutbound, DEFAULT_ENDPOINT_CAPABILITIES, hasInbound, } from "./endpoint-capabilities.js";
2
- import { resolveEndpointControl } from "@zhin.js/adapter";
2
+ import { endpointControlOf } from "@zhin.js/adapter";
3
3
  import { connectEndpointInstance, disconnectEndpointInstance } from "./built/connect-endpoint-instance.js";
4
4
  import { EventEmitter } from "node:events";
5
5
  import { getOutboundReplyStore } from "./built/dispatcher.js";
@@ -218,20 +218,26 @@ export class Adapter extends EventEmitter {
218
218
  }
219
219
  /**
220
220
  * `call.recallMessage` 事件链(prompt.ts 超时撤回等)的统一出口:
221
- * 经 canonical `EndpointControl` 端口撤回,classic 端点的 `$` 方法由
222
- * `resolveEndpointControl` 迁移桥适配。
221
+ * 经 canonical `EndpointControl` 端口撤回。
223
222
  */
224
223
  async recallEndpointMessage(endpointKey, messageId) {
225
224
  const endpoint = this.endpoints.get(endpointKey);
226
225
  if (!endpoint)
227
226
  throw new Error(`Endpoint ${endpointKey} not found`);
228
227
  assertOutbound(endpoint);
229
- const control = resolveEndpointControl(endpoint);
228
+ const control = endpointControlOf(endpoint);
230
229
  if (!control?.recall) {
231
230
  throw new Error(`Endpoint ${endpointKey} does not support recall`);
232
231
  }
233
232
  this.logger.debug(formatCompact({ op: 'recall_message', msgId: messageId, endpoint: endpointKey }));
234
- await control.recall(messageId);
233
+ await control.recall({
234
+ conversation: {
235
+ endpoint: { id: endpointKey, adapter: String(this.name) },
236
+ kind: 'private',
237
+ id: endpointKey,
238
+ },
239
+ id: messageId,
240
+ });
235
241
  }
236
242
  /**
237
243
  * 编辑已发送的消息。
@@ -244,7 +250,7 @@ export class Adapter extends EventEmitter {
244
250
  if (!endpoint)
245
251
  throw new Error(`Endpoint ${options.endpoint} not found`);
246
252
  assertOutbound(endpoint);
247
- const control = resolveEndpointControl(endpoint);
253
+ const control = endpointControlOf(endpoint);
248
254
  if (control?.edit) {
249
255
  const rendered = await this.renderSendMessage({
250
256
  context: options.context,
@@ -253,7 +259,14 @@ export class Adapter extends EventEmitter {
253
259
  type: options.type,
254
260
  content: options.content,
255
261
  });
256
- await control.edit(options.messageId, rendered.content);
262
+ await control.edit({
263
+ conversation: {
264
+ endpoint: { id: options.endpoint, adapter: String(this.name) },
265
+ kind: options.type === 'group' ? 'group' : 'private',
266
+ id: options.id,
267
+ },
268
+ id: options.messageId,
269
+ }, rendered.content);
257
270
  this.logger.debug(formatCompact({
258
271
  edit: `${options.type}(${options.id})`,
259
272
  endpoint: options.endpoint,
@@ -31,6 +31,11 @@ export declare class ProcessEndpoint implements Endpoint<{
31
31
  content: string;
32
32
  ts: number;
33
33
  }>;
34
+ get control(): {
35
+ recall: (message: {
36
+ id: string;
37
+ }) => Promise<void>;
38
+ };
34
39
  $recallMessage(id: string): Promise<void>;
35
40
  $sendMessage(options: SendOptions): Promise<string>;
36
41
  $connect(): Promise<void>;
@@ -62,6 +62,11 @@ export class ProcessEndpoint {
62
62
  };
63
63
  return Message.from(event, base);
64
64
  }
65
+ get control() {
66
+ return {
67
+ recall: async (message) => this.$recallMessage(message.id),
68
+ };
69
+ }
65
70
  async $recallMessage(id) {
66
71
  }
67
72
  async $sendMessage(options) {
@@ -5,7 +5,7 @@ export declare function parseOutboundSegment(raw: unknown): Segment | null;
5
5
  /** 去掉嵌入 JSON 候选末尾的 markdown 围栏(模型常见 ```json … ``` 误输出)。 */
6
6
  export declare function trimTrailingMarkdownFence(raw: string): string;
7
7
  export declare function unwrapAiOutboundJsonCandidate(raw: string): string;
8
- /** 从「正文 + 嵌入 JSON」混合文本中提取 AI outbound JSON(协作群常见误输出)。 */
8
+ /** Extract an embedded AI outbound JSON object from mixed prose. */
9
9
  export declare function extractEmbeddedAiOutboundJson(plain: string): {
10
10
  prose: string;
11
11
  jsonRaw: string;
@@ -31,7 +31,7 @@ export function unwrapAiOutboundJsonCandidate(raw) {
31
31
  }
32
32
  return trimTrailingMarkdownFence(trimmed);
33
33
  }
34
- /** 从「正文 + 嵌入 JSON」混合文本中提取 AI outbound JSON(协作群常见误输出)。 */
34
+ /** Extract an embedded AI outbound JSON object from mixed prose. */
35
35
  export function extractEmbeddedAiOutboundJson(plain) {
36
36
  const trimmed = plain.trim();
37
37
  if (!trimmed.includes('{'))
@@ -7,5 +7,5 @@ export declare function buildAiOutboundPromptHint(input: {
7
7
  rosterLines?: string[];
8
8
  forceJsonOnly?: boolean;
9
9
  }): string;
10
- /** 检测入站是否含 handoff / @ 意图(协作群 structured_only 信号)。 */
10
+ /** Detect whether inbound text explicitly requests a structured handoff. */
11
11
  export declare function detectInboundHandoffIntent(content: string): boolean;
@@ -33,7 +33,7 @@ export function buildAiOutboundPromptHint(input) {
33
33
  }
34
34
  return lines.join('\n');
35
35
  }
36
- /** 检测入站是否含 handoff / @ 意图(协作群 structured_only 信号)。 */
36
+ /** Detect whether inbound text explicitly requests a structured handoff. */
37
37
  export function detectInboundHandoffIntent(content) {
38
38
  const t = content.trim();
39
39
  if (!t)
@@ -1,7 +1,6 @@
1
1
  /** structured_only:满足任一即进入结构化出站模式。 */
2
2
  export function isStructuredOutboundRequired(input) {
3
- return Boolean(input.collaborationCell
4
- || input.toolRequiresStructured
3
+ return Boolean(input.toolRequiresStructured
5
4
  || input.inboundHandoffIntent
6
5
  || input.adapterHasExtensions);
7
6
  }
@@ -48,9 +48,7 @@ export interface AiOutboundParseContext {
48
48
  extensions?: readonly AiOutboundExtensionDefinition[];
49
49
  }
50
50
  export interface StructuredOutboundDetectInput {
51
- /** 协作 Cell 活跃且可能 @ peer */
52
- collaborationCell?: boolean;
53
- /** 工具链要求结构化出站(如 group_delegate message JSON) */
51
+ /** 工具链要求结构化出站。 */
54
52
  toolRequiresStructured?: boolean;
55
53
  /** 用户入站含 handoff / @ 意图 */
56
54
  inboundHandoffIntent?: boolean;
@@ -39,11 +39,6 @@ export interface AITriggerConfig {
39
39
  trusted?: string[];
40
40
  /** 是否在 AI 入参前拉取 $quote_id 对应消息正文(默认 true) */
41
41
  resolveQuotedMessages?: boolean;
42
- /**
43
- * 协作单元内 peer Bot 入站策略(ADR 0023)。
44
- * mention-only:仅被 @ 时触发;off:与普通人消息相同规则。
45
- */
46
- peerMode?: 'mention-only' | 'off';
47
42
  }
48
43
  /**
49
44
  * AI 触发检查结果
@@ -27,7 +27,6 @@ export const DEFAULT_AI_TRIGGER_CONFIG = {
27
27
  masters: [],
28
28
  trusted: [],
29
29
  resolveQuotedMessages: true,
30
- peerMode: 'mention-only',
31
30
  };
32
31
  // ============================================================================
33
32
  // 工具函数
package/lib/endpoint.d.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  import type { Adapters, Adapter } from './adapter.js';
2
2
  import type { EndpointCapabilitiesConfig, FullEndpoint } from './endpoint-capabilities.js';
3
- import type { EndpointWithManagement } from '@zhin.js/adapter';
3
+ import type { EndpointWithControl, EndpointWithManagement } from '@zhin.js/adapter';
4
4
  export type { EndpointChannel, EndpointChannelParent, EndpointFriend, EndpointGroup, EndpointManagement, EndpointWithManagement, EndpointManagementCapability, } from '@zhin.js/adapter';
5
5
  export { endpointManagementCapabilityIds, listEndpointManagementCapabilities, resolveEndpointManagement, } from '@zhin.js/adapter';
6
6
  export type { EndpointControl, EndpointWithControl } from '@zhin.js/adapter';
7
- export { resolveEndpointControl } from '@zhin.js/adapter';
7
+ export { endpointControlOf } from '@zhin.js/adapter';
8
8
  export type { EndpointCapability, EndpointCapabilitiesConfig, InboundEndpoint, OutboundEndpoint, FullEndpoint, CapableEndpoint, } from './endpoint-capabilities.js';
9
9
  export { DEFAULT_ENDPOINT_CAPABILITIES, OutboundNotSupportedError, InboundNotSupportedError, resolveEndpointCapabilities, registerEndpointCapabilities, getEndpointCapabilities, getAdapterCapabilities, hasInbound, hasOutbound, assertInbound, assertOutbound, } from './endpoint-capabilities.js';
10
10
  /**
11
11
  * Endpoint 接口:全双工平台机器人(入站 + 出站)。
12
12
  * 纯入站 / 纯出站请实现 InboundEndpoint / OutboundEndpoint。
13
13
  */
14
- export type Endpoint<Config extends object = object, Event extends object = object> = FullEndpoint<Config, Event> & EndpointWithManagement;
14
+ export type Endpoint<Config extends object = object, Event extends object = object> = FullEndpoint<Config, Event> & EndpointWithManagement & EndpointWithControl;
15
15
  export declare namespace Endpoint {
16
16
  type Config<K extends keyof Adapters = keyof Adapters> = Adapter.EndpointConfig<Adapter.InferEndpoint<Adapters[K]>> & EndpointCapabilitiesConfig & {
17
17
  context: K;
package/lib/endpoint.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { endpointManagementCapabilityIds, listEndpointManagementCapabilities, resolveEndpointManagement, } from '@zhin.js/adapter';
2
- export { resolveEndpointControl } from '@zhin.js/adapter';
2
+ export { endpointControlOf } from '@zhin.js/adapter';
3
3
  export { DEFAULT_ENDPOINT_CAPABILITIES, OutboundNotSupportedError, InboundNotSupportedError, resolveEndpointCapabilities, registerEndpointCapabilities, getEndpointCapabilities, getAdapterCapabilities, hasInbound, hasOutbound, assertInbound, assertOutbound, } from './endpoint-capabilities.js';
@@ -1,6 +1,6 @@
1
- import { Scope, type PluginId, type RuntimeSnapshot, type SnapshotLease, type SnapshotStore } from '@zhin.js/plugin-runtime';
1
+ import { Scope, generationAdmissionBinder, type GenerationAdmissionGate, type PluginId, type RuntimeSnapshot, type SnapshotLease, type SnapshotReader } from '@zhin.js/plugin-runtime';
2
2
  import { MessageBus } from './message-bus.js';
3
- import { type EndpointManagement, type EndpointManagementCapability } from '@zhin.js/adapter';
3
+ import { type EndpointManagement, type EndpointManagementCapability, type AdapterEndpointPhase } from '@zhin.js/adapter';
4
4
  import { type ConversationRef, type DeliveryReceipt, type MessageRef } from '@zhin.js/im-contract';
5
5
  import { Message, type ConversationAddress, type IncomingMessage, type MessageDispatchResult, type MessageGateway, type MessageSenderRef, type SendRequest } from './contracts.js';
6
6
  import { OutboundRenderer } from './outbound-renderer.js';
@@ -45,10 +45,11 @@ export interface ImRuntimeOptions {
45
45
  export declare class ImRuntime implements MessageGateway {
46
46
  #private;
47
47
  constructor(options?: ImRuntimeOptions);
48
- attach(snapshots: SnapshotStore): void;
48
+ attach(snapshots: SnapshotReader): void;
49
49
  readonly permissionHost: import("@zhin.js/permission").PermissionHost;
50
50
  readonly messageBus: MessageBus;
51
51
  install(resources: Scope): void;
52
+ [generationAdmissionBinder](gate: GenerationAdmissionGate): MessageGateway;
52
53
  /**
53
54
  * 注册 interactive action 回跳 handler(prefix 最长匹配;返回注销函数)。
54
55
  * 在 Command dispatch 之前路由:action 段 / 数字回跳 / 指令预填 payload。
@@ -68,7 +69,7 @@ export declare class ImRuntime implements MessageGateway {
68
69
  readonly owner: string;
69
70
  readonly connected: boolean;
70
71
  readonly status: 'online' | 'offline';
71
- readonly phase: 'pending' | 'starting' | 'online' | 'failed' | 'unconfigured';
72
+ readonly phase: AdapterEndpointPhase;
72
73
  readonly managementCapabilities: readonly EndpointManagementCapability[];
73
74
  }[];
74
75
  /**
@@ -87,7 +88,7 @@ export declare class ImRuntime implements MessageGateway {
87
88
  readonly adapter: string;
88
89
  readonly connected: boolean;
89
90
  readonly status: 'online' | 'offline';
90
- readonly phase: 'pending' | 'starting' | 'online' | 'failed' | 'unconfigured';
91
+ readonly phase: AdapterEndpointPhase;
91
92
  readonly managementCapabilities: readonly EndpointManagementCapability[];
92
93
  } | null;
93
94
  sendEndpointMessage(input: {
@@ -102,8 +103,7 @@ export declare class ImRuntime implements MessageGateway {
102
103
  addEndpointReaction(input: {
103
104
  readonly adapter: string;
104
105
  readonly endpointKey: string;
105
- readonly message?: MessageRef;
106
- readonly messageId?: string;
106
+ readonly message: MessageRef;
107
107
  readonly emoji: string;
108
108
  readonly sceneType?: string;
109
109
  readonly channelId?: string;
@@ -111,22 +111,19 @@ export declare class ImRuntime implements MessageGateway {
111
111
  removeEndpointReaction(input: {
112
112
  readonly adapter: string;
113
113
  readonly endpointKey: string;
114
- readonly message?: MessageRef;
115
- readonly messageId?: string;
114
+ readonly message: MessageRef;
116
115
  readonly reactionId: string;
117
116
  }): Promise<void>;
118
117
  /** Activity-feedback autoRemove: recall a previously sent status message. */
119
118
  recallEndpointMessage(input: {
120
119
  readonly adapter: string;
121
120
  readonly endpointKey: string;
122
- readonly message?: MessageRef;
123
- readonly messageId?: string;
121
+ readonly message: MessageRef;
124
122
  }): Promise<void>;
125
123
  editEndpointMessage(input: {
126
124
  readonly adapter: string;
127
125
  readonly endpointKey: string;
128
- readonly message?: MessageRef;
129
- readonly messageId?: string;
126
+ readonly message: MessageRef;
130
127
  readonly content: unknown;
131
128
  }): Promise<string | null>;
132
129
  setEndpointTyping(input: {
@@ -135,10 +132,6 @@ export declare class ImRuntime implements MessageGateway {
135
132
  readonly conversation: ConversationRef;
136
133
  readonly active?: boolean;
137
134
  }): Promise<void>;
138
- /**
139
- * Narrow Host seam for Console social/group management. An empty object means
140
- * the Endpoint exists but implements no management operations; null means it
141
- * cannot be resolved.
142
- */
143
- getEndpointManagement(adapter: string, endpointKey: string): EndpointManagement | null;
135
+ /** Run one management operation while the Endpoint generation stays leased. */
136
+ withEndpointManagement<T>(adapter: string, endpointKey: string, run: (management: EndpointManagement) => T | Promise<T>): Promise<T | null>;
144
137
  }
@@ -1,8 +1,8 @@
1
- import { createToken, htmlRendererToken, } from '@zhin.js/plugin-runtime';
1
+ import { createToken, generationAdmissionBinder, htmlRendererToken, } from '@zhin.js/plugin-runtime';
2
2
  import { createPermissionHost, permissionHostToken } from '@zhin.js/permission';
3
3
  import { MessageBus, messageBusToken } from './message-bus.js';
4
- import { adapterFeatureId, isAdapterIndex, resolveEndpointControl, resolveEndpointManagement, } from '@zhin.js/adapter';
5
- import { formatLegacyMessageRef, isDeliveryReceipt, } from '@zhin.js/im-contract';
4
+ import { adapterFeatureId, isAdapterIndex, endpointControlOf, resolveEndpointManagement, } from '@zhin.js/adapter';
5
+ import { isDeliveryReceipt, } from '@zhin.js/im-contract';
6
6
  import { isMiddlewareIndex, middlewareFeatureId } from '@zhin.js/middleware';
7
7
  import { formatCompact, getLogger, truncatePreview } from '@zhin.js/logger';
8
8
  import { Message, createOutboundEnvelope, } from './contracts.js';
@@ -21,6 +21,7 @@ export class ImRuntime {
21
21
  #renderer;
22
22
  #messageListeners = new Set();
23
23
  #interactiveHandlers = [];
24
+ #promptClaims = new Map();
24
25
  #snapshots;
25
26
  #inboundClaim;
26
27
  #enrichSender;
@@ -45,12 +46,25 @@ export class ImRuntime {
45
46
  resources.provide(permissionHostToken, this.permissionHost);
46
47
  resources.provide(messageBusToken, this.messageBus);
47
48
  }
49
+ [generationAdmissionBinder](gate) {
50
+ const gateway = {
51
+ receive: async (input) => gate.enter(() => this.#receive(input, gate))
52
+ ?? Object.freeze({ matched: false }),
53
+ send: async (request) => gate.enter(() => this.send(request))
54
+ ?? failedReceipt('generation_not_admitted'),
55
+ registerInteractiveHandler: (prefix, handler) => this.#registerInteractiveHandler(prefix, handler, gate),
56
+ };
57
+ return Object.freeze(gateway);
58
+ }
48
59
  /**
49
60
  * 注册 interactive action 回跳 handler(prefix 最长匹配;返回注销函数)。
50
61
  * 在 Command dispatch 之前路由:action 段 / 数字回跳 / 指令预填 payload。
51
62
  */
52
63
  registerInteractiveHandler(prefix, handler) {
53
- const entry = Object.freeze({ prefix, handler });
64
+ return this.#registerInteractiveHandler(prefix, handler);
65
+ }
66
+ #registerInteractiveHandler(prefix, handler, admission) {
67
+ const entry = Object.freeze({ prefix, handler, admission });
54
68
  this.#interactiveHandlers.push(entry);
55
69
  return () => {
56
70
  const index = this.#interactiveHandlers.indexOf(entry);
@@ -58,6 +72,137 @@ export class ImRuntime {
58
72
  this.#interactiveHandlers.splice(index, 1);
59
73
  };
60
74
  }
75
+ // ==========================================================================
76
+ // Prompt claim — 命令对话式交互
77
+ // ==========================================================================
78
+ #promptConversationKey(message) {
79
+ const conv = message.conversation;
80
+ return `${conv.endpoint.adapter}:${conv.endpoint.id}:${conv.kind}:${conv.id}:${message.sender?.id ?? ''}`;
81
+ }
82
+ #resolvePromptClaim(message) {
83
+ const key = this.#promptConversationKey(message);
84
+ const claim = this.#promptClaims.get(key);
85
+ if (!claim)
86
+ return false;
87
+ this.#promptClaims.delete(key);
88
+ clearTimeout(claim.timer);
89
+ claim.resolve(message.content);
90
+ return true;
91
+ }
92
+ #claimNextMessage(message, timeout, timeoutText) {
93
+ return new Promise((resolve, reject) => {
94
+ const key = this.#promptConversationKey(message);
95
+ const existing = this.#promptClaims.get(key);
96
+ if (existing) {
97
+ clearTimeout(existing.timer);
98
+ existing.reject(new Error('Prompt superseded'));
99
+ }
100
+ const timer = setTimeout(() => {
101
+ this.#promptClaims.delete(key);
102
+ reject(new Error(timeoutText));
103
+ }, timeout);
104
+ this.#promptClaims.set(key, { resolve, reject, timer });
105
+ });
106
+ }
107
+ #buildCommandPrompt(message) {
108
+ const DEFAULT_TIMEOUT = 3 * 60 * 1000;
109
+ const DEFAULT_TIMEOUT_TEXT = '输入超时';
110
+ const claim = (timeout, timeoutText) => this.#claimNextMessage(message, timeout, timeoutText);
111
+ const reply = (content) => message.$reply(content);
112
+ return {
113
+ async text(tips, options) {
114
+ await reply(tips);
115
+ try {
116
+ return await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT);
117
+ }
118
+ catch (e) {
119
+ if (options?.default !== undefined)
120
+ return options.default;
121
+ await reply(e.message);
122
+ throw e;
123
+ }
124
+ },
125
+ async number(tips, options) {
126
+ await reply(tips);
127
+ try {
128
+ const raw = await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT);
129
+ return +raw;
130
+ }
131
+ catch (e) {
132
+ if (options?.default !== undefined)
133
+ return options.default;
134
+ await reply(e.message);
135
+ throw e;
136
+ }
137
+ },
138
+ async confirm(tips, options) {
139
+ const condition = options?.condition ?? 'yes';
140
+ await reply(`${tips}\n输入"${condition}"以确认`);
141
+ try {
142
+ const raw = await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT);
143
+ return raw === condition;
144
+ }
145
+ catch (e) {
146
+ if (options?.default !== undefined)
147
+ return options.default;
148
+ await reply(e.message);
149
+ throw e;
150
+ }
151
+ },
152
+ async list(tips, options) {
153
+ const separator = options?.separator ?? ',';
154
+ await reply(`${tips}\n值之间使用"${separator}"分隔`);
155
+ try {
156
+ const raw = await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT);
157
+ const type = options?.type ?? 'text';
158
+ return raw.split(separator).map((v) => {
159
+ if (type === 'number')
160
+ return +v;
161
+ if (type === 'boolean')
162
+ return v === 'true';
163
+ return v;
164
+ });
165
+ }
166
+ catch (e) {
167
+ if (options?.default !== undefined)
168
+ return options.default;
169
+ await reply(e.message);
170
+ throw e;
171
+ }
172
+ },
173
+ async pick(tips, options) {
174
+ const items = options.options.map((o, i) => `${i + 1}.${o.label}`);
175
+ const separator = options.separator ?? ',';
176
+ if (options.multiple)
177
+ items.push(`多选请用"${separator}"分隔`);
178
+ await reply(`${tips}\n${items.join('\n')}`);
179
+ try {
180
+ const raw = await claim(options.timeout ?? DEFAULT_TIMEOUT, options.timeoutText ?? DEFAULT_TIMEOUT_TEXT);
181
+ if (!options.multiple) {
182
+ return options.options.find((_, i) => i + 1 === +raw)?.value;
183
+ }
184
+ const indices = raw.split(separator).map(Number);
185
+ return options.options
186
+ .filter((_, i) => indices.includes(i + 1))
187
+ .map((o) => o.value);
188
+ }
189
+ catch (e) {
190
+ if (options.default !== undefined)
191
+ return options.default;
192
+ await reply(e.message);
193
+ throw e;
194
+ }
195
+ },
196
+ };
197
+ }
198
+ #createPromptForSource(source) {
199
+ if (!source || typeof source !== 'object')
200
+ return undefined;
201
+ const msg = source;
202
+ if (typeof msg.$reply !== 'function' || !msg.conversation)
203
+ return undefined;
204
+ return this.#buildCommandPrompt(msg);
205
+ }
61
206
  /**
62
207
  * 订阅消息事件(入站 dispatch 完成后 / 出站发送成功后回调)。
63
208
  * 返回注销函数。listener 抛错不会阻断消息链路。
@@ -77,6 +222,9 @@ export class ImRuntime {
77
222
  }
78
223
  }
79
224
  async receive(input) {
225
+ return this.#receive(input);
226
+ }
227
+ async #receive(input, admission) {
80
228
  const lease = this.#acquire();
81
229
  let active = true;
82
230
  try {
@@ -118,10 +266,14 @@ export class ImRuntime {
118
266
  if (claimed) {
119
267
  result = Object.freeze({ matched: true, command: 'interaction', owner: requester });
120
268
  }
269
+ else if (this.#resolvePromptClaim(message)) {
270
+ result = Object.freeze({ matched: true, command: 'prompt', owner: requester });
271
+ }
121
272
  else {
273
+ const promptFactory = (source) => this.#createPromptForSource(source);
122
274
  await runMiddleware(lease.value, message, async () => {
123
- result = await this.#dispatchInteractive(message, requester)
124
- ?? await this.#dispatcher.dispatch(message, lease.value);
275
+ result = await this.#dispatchInteractive(message, requester, admission)
276
+ ?? await this.#dispatcher.dispatch(message, lease.value, promptFactory);
125
277
  const ingressRoute = resolveIngressRoute(lease.value);
126
278
  if (!result.matched && ingressRoute) {
127
279
  logger.debug(formatCompact({ op: 'unmatched', conv: formatConversationLog(conversation) }));
@@ -273,58 +425,59 @@ export class ImRuntime {
273
425
  }
274
426
  /** Activity-feedback: add a message reaction when the live Endpoint supports it. */
275
427
  async addEndpointReaction(input) {
276
- const control = this.#liveEndpointControl(input.adapter, input.endpointKey);
277
- return control?.addReaction?.(legacyMessageTarget(input.message, input.messageId), input.emoji, {
428
+ return this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.addReaction?.(input.message, input.emoji, {
278
429
  sceneType: input.sceneType,
279
430
  channelId: input.channelId,
280
- }) ?? null;
431
+ }) ?? null, null);
281
432
  }
282
433
  async removeEndpointReaction(input) {
283
- await this.#liveEndpointControl(input.adapter, input.endpointKey)
284
- ?.removeReaction?.(legacyMessageTarget(input.message, input.messageId), input.reactionId);
434
+ await this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.removeReaction?.(input.message, input.reactionId), undefined);
285
435
  }
286
436
  /** Activity-feedback autoRemove: recall a previously sent status message. */
287
437
  async recallEndpointMessage(input) {
288
- await this.#liveEndpointControl(input.adapter, input.endpointKey)
289
- ?.recall?.(legacyMessageTarget(input.message, input.messageId));
438
+ await this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.recall?.(input.message), undefined);
290
439
  }
291
440
  async editEndpointMessage(input) {
292
- return this.#liveEndpointControl(input.adapter, input.endpointKey)
293
- ?.edit?.(legacyMessageTarget(input.message, input.messageId), input.content) ?? null;
441
+ return this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.edit?.(input.message, input.content) ?? null, null);
294
442
  }
295
443
  async setEndpointTyping(input) {
296
- await this.#liveEndpointControl(input.adapter, input.endpointKey)
297
- ?.typing?.(input.conversation, input.active);
444
+ await this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.typing?.(input.conversation, input.active), undefined);
298
445
  }
299
- #liveEndpoint(adapter, endpointKey) {
446
+ async #withEndpointControl(adapter, endpointKey, run, fallback) {
447
+ let lease;
300
448
  try {
301
- const lease = this.#acquire();
302
- try {
303
- const endpoint = requireAdapters(lease.value).instance(adapter, endpointKey);
304
- return endpoint ?? null;
305
- }
306
- finally {
307
- lease.release();
308
- }
449
+ lease = this.#acquire();
309
450
  }
310
451
  catch {
311
- return null;
452
+ return fallback;
453
+ }
454
+ try {
455
+ const endpoint = requireAdapters(lease.value).instance(adapter, endpointKey);
456
+ const control = endpointControlOf(endpoint);
457
+ return control ? await run(control) : fallback;
458
+ }
459
+ finally {
460
+ lease.release();
312
461
  }
313
462
  }
314
- #liveEndpointControl(adapter, endpointKey) {
315
- const endpoint = this.#liveEndpoint(adapter, endpointKey);
316
- return resolveEndpointControl(endpoint) ?? null;
317
- }
318
- /**
319
- * Narrow Host seam for Console social/group management. An empty object means
320
- * the Endpoint exists but implements no management operations; null means it
321
- * cannot be resolved.
322
- */
323
- getEndpointManagement(adapter, endpointKey) {
324
- const endpoint = this.#liveEndpoint(adapter, endpointKey);
325
- if (!endpoint)
463
+ /** Run one management operation while the Endpoint generation stays leased. */
464
+ async withEndpointManagement(adapter, endpointKey, run) {
465
+ let lease;
466
+ try {
467
+ lease = this.#acquire();
468
+ }
469
+ catch {
326
470
  return null;
327
- return resolveEndpointManagement(endpoint) ?? Object.freeze({});
471
+ }
472
+ try {
473
+ const endpoint = requireAdapters(lease.value).instance(adapter, endpointKey);
474
+ if (!endpoint)
475
+ return null;
476
+ return await run(resolveEndpointManagement(endpoint) ?? Object.freeze({}));
477
+ }
478
+ finally {
479
+ lease.release();
480
+ }
328
481
  }
329
482
  async #sendWithSnapshot(request, snapshot) {
330
483
  const adapter = request.conversation.endpoint.id;
@@ -394,13 +547,13 @@ export class ImRuntime {
394
547
  * interactive 回跳分发(Command dispatch 之前):action 段 / 中央 fallback
395
548
  * 数字回跳 / 指令预填 payload → prefix 最长匹配 handler。
396
549
  */
397
- async #dispatchInteractive(message, requester) {
550
+ async #dispatchInteractive(message, requester, admission) {
398
551
  if (this.#interactiveHandlers.length === 0)
399
552
  return undefined;
400
553
  const payload = resolveRuntimeInteractivePayload(message);
401
554
  if (!payload)
402
555
  return undefined;
403
- const handler = findRuntimeInteractiveHandler(this.#interactiveHandlers, payload);
556
+ const handler = findRuntimeInteractiveHandler(this.#interactiveHandlers.filter((entry) => !entry.admission || entry.admission === admission), payload);
404
557
  if (!handler)
405
558
  return undefined;
406
559
  const handled = await handler(message);
@@ -439,13 +592,6 @@ function isDirectHtmlConsumer(snapshot, adapter) {
439
592
  const owner = snapshot.capabilities.get(adapter)?.owner;
440
593
  return adapterTypeName(snapshot.tree.get(owner)?.packageName) === 'sandbox';
441
594
  }
442
- function legacyMessageTarget(message, messageId) {
443
- if (message)
444
- return formatLegacyMessageRef(message);
445
- if (messageId)
446
- return messageId;
447
- throw new TypeError('message or messageId is required');
448
- }
449
595
  async function prepareOutboundPayload(rendered, conversation, snapshot, finalizeInteractive = false) {
450
596
  const adapter = conversation.endpoint.id;
451
597
  const directHtml = isDirectHtmlConsumer(snapshot, adapter);
@@ -1,3 +1,4 @@
1
+ import { type CommandPromptFactory } from '@zhin.js/command';
1
2
  import type { RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
3
  import type { Message, MessageDispatchResult } from './contracts.js';
3
4
  /**
@@ -13,5 +14,5 @@ export declare const defaultCommandPrefixResolver: CommandPrefixResolver;
13
14
  export declare class MessageDispatcher {
14
15
  private readonly resolvePrefix;
15
16
  constructor(resolvePrefix?: CommandPrefixResolver);
16
- dispatch(message: Message, snapshot: RuntimeSnapshot): Promise<MessageDispatchResult>;
17
+ dispatch(message: Message, snapshot: RuntimeSnapshot, promptFactory?: CommandPromptFactory): Promise<MessageDispatchResult>;
17
18
  }
@@ -26,7 +26,7 @@ export class MessageDispatcher {
26
26
  constructor(resolvePrefix = defaultCommandPrefixResolver) {
27
27
  this.resolvePrefix = resolvePrefix;
28
28
  }
29
- async dispatch(message, snapshot) {
29
+ async dispatch(message, snapshot, promptFactory) {
30
30
  const prefix = this.resolvePrefix(message, snapshot);
31
31
  let input = message.content.trim();
32
32
  if (prefix && !input.startsWith(prefix)) {
@@ -43,7 +43,7 @@ export class MessageDispatcher {
43
43
  ? stripCommandPrefix(message.segments, prefix)
44
44
  : undefined;
45
45
  const matchInput = structuredInput ?? input;
46
- const result = await commands.dispatch(matchInput, message);
46
+ const result = await commands.dispatch(matchInput, message, promptFactory);
47
47
  if (result.matched && result.value !== undefined) {
48
48
  if (!result.owner)
49
49
  throw new Error('Matched Command is missing its owner');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/core",
3
- "version": "1.5.7",
3
+ "version": "1.5.8",
4
4
  "description": "Zhin机器人核心框架",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -70,16 +70,16 @@
70
70
  "segment-matcher": "^1.0.5",
71
71
  "smol-toml": "^1.7.1",
72
72
  "yaml": "^2.9.0",
73
- "@zhin.js/adapter": "1.1.7",
74
- "@zhin.js/command": "1.0.11",
75
- "@zhin.js/component": "1.0.8",
73
+ "@zhin.js/adapter": "1.1.8",
74
+ "@zhin.js/command": "1.0.12",
75
+ "@zhin.js/component": "1.0.9",
76
76
  "@zhin.js/database": "1.0.79",
77
77
  "@zhin.js/im-contract": "1.0.3",
78
78
  "@zhin.js/kernel": "1.0.7",
79
+ "@zhin.js/middleware": "1.0.9",
79
80
  "@zhin.js/logger": "1.0.76",
80
- "@zhin.js/middleware": "1.0.8",
81
- "@zhin.js/permission": "1.0.1",
82
- "@zhin.js/plugin-runtime": "1.1.5",
81
+ "@zhin.js/permission": "1.0.2",
82
+ "@zhin.js/plugin-runtime": "1.1.6",
83
83
  "@zhin.js/schema": "1.0.73"
84
84
  },
85
85
  "peerDependencies": {
@@ -95,7 +95,7 @@
95
95
  "@types/qrcode": "^1.5.6",
96
96
  "ajv": "8.18.0",
97
97
  "typescript": "^6.0.3",
98
- "@zhin.js/ai": "1.5.2"
98
+ "@zhin.js/ai": "1.5.3"
99
99
  },
100
100
  "repository": {
101
101
  "type": "git",