@zhin.js/core 1.5.8 → 1.5.9

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.js CHANGED
@@ -123,10 +123,17 @@ export class Adapter extends EventEmitter {
123
123
  return false;
124
124
  }
125
125
  this.#pendingMessages++;
126
- await this.inboundPipeline.receive(message, () => {
127
- EventEmitter.prototype.emit.call(this, 'message.receive', message);
128
- });
129
- return true;
126
+ try {
127
+ await this.inboundPipeline.receive(message, () => {
128
+ EventEmitter.prototype.emit.call(this, 'message.receive', message);
129
+ });
130
+ return true;
131
+ }
132
+ finally {
133
+ // Inbound pipeline errors must not leak the concurrency budget; the
134
+ // non-await path uses InboundMessagePipeline.decrementPending in finally.
135
+ this.#pendingMessages = Math.max(0, this.#pendingMessages - 1);
136
+ }
130
137
  }
131
138
  /**
132
139
  * 出站富媒体能力(Publisher 按此过滤/降级;各 adapter 可覆盖)。
@@ -0,0 +1,10 @@
1
+ /** Authoring API for Handler Feature — implementation in `@zhin.js/feature-kit`. */
2
+ export { defineHandler, parseHandlerDefinition, HandlerIndex, isHandlerIndex, handlerFeatureId, handlerFeature, type HandlerEventMap, type HandlerDefinition, type HandlerDescriptor, } from '@zhin.js/feature-kit';
3
+ import type { Plugin } from '../plugin.js';
4
+ type KnownKeys<T> = {
5
+ [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K];
6
+ };
7
+ declare module '@zhin.js/feature-kit' {
8
+ interface HandlerEventMap extends KnownKeys<Plugin.Lifecycle> {
9
+ }
10
+ }
@@ -0,0 +1,2 @@
1
+ /** Authoring API for Handler Feature — implementation in `@zhin.js/feature-kit`. */
2
+ export { defineHandler, parseHandlerDefinition, HandlerIndex, isHandlerIndex, handlerFeatureId, handlerFeature, } from '@zhin.js/feature-kit';
@@ -3,6 +3,7 @@ import { MessageBus } from './message-bus.js';
3
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
+ import type { CommandPrompt } from '@zhin.js/command';
6
7
  import { OutboundRenderer } from './outbound-renderer.js';
7
8
  import { type RuntimeInteractiveHandler } from './interactive.js';
8
9
  export declare const messageGatewayToken: import("@zhin.js/plugin-runtime").Token<MessageGateway>;
@@ -55,6 +56,13 @@ export declare class ImRuntime implements MessageGateway {
55
56
  * 在 Command dispatch 之前路由:action 段 / 数字回跳 / 指令预填 payload。
56
57
  */
57
58
  registerInteractiveHandler(prefix: string, handler: RuntimeInteractiveHandler): () => void;
59
+ /**
60
+ * Bound Prompt for this conversation.
61
+ * `bind.subjectId` waits for that user (e.g. master) instead of the message sender.
62
+ */
63
+ createPrompt(message: Message, bind?: {
64
+ readonly subjectId: string;
65
+ }): CommandPrompt | undefined;
58
66
  /**
59
67
  * 订阅消息事件(入站 dispatch 完成后 / 出站发送成功后回调)。
60
68
  * 返回注销函数。listener 抛错不会阻断消息链路。
@@ -4,6 +4,7 @@ import { MessageBus, messageBusToken } from './message-bus.js';
4
4
  import { adapterFeatureId, isAdapterIndex, endpointControlOf, resolveEndpointManagement, } from '@zhin.js/adapter';
5
5
  import { isDeliveryReceipt, } from '@zhin.js/im-contract';
6
6
  import { isMiddlewareIndex, middlewareFeatureId } from '@zhin.js/middleware';
7
+ import { isHandlerIndex, handlerFeatureId } from '../../feature/handler.js';
7
8
  import { formatCompact, getLogger, truncatePreview } from '@zhin.js/logger';
8
9
  import { Message, createOutboundEnvelope, } from './contracts.js';
9
10
  import { defaultCommandPrefixResolver, MessageDispatcher } from './message-dispatcher.js';
@@ -75,45 +76,64 @@ export class ImRuntime {
75
76
  // ==========================================================================
76
77
  // Prompt claim — 命令对话式交互
77
78
  // ==========================================================================
78
- #promptConversationKey(message) {
79
+ #promptConversationKey(message, subjectId = message.sender?.id ?? '') {
79
80
  const conv = message.conversation;
80
- return `${conv.endpoint.adapter}:${conv.endpoint.id}:${conv.kind}:${conv.id}:${message.sender?.id ?? ''}`;
81
+ return `${conv.endpoint.adapter}:${conv.endpoint.id}:${conv.kind}:${conv.id}:${subjectId}`;
81
82
  }
82
83
  #resolvePromptClaim(message) {
83
84
  const key = this.#promptConversationKey(message);
84
85
  const claim = this.#promptClaims.get(key);
85
86
  if (!claim)
86
87
  return false;
87
- this.#promptClaims.delete(key);
88
- clearTimeout(claim.timer);
89
88
  claim.resolve(message.content);
90
89
  return true;
91
90
  }
92
- #claimNextMessage(message, timeout, timeoutText) {
91
+ #claimNextMessage(message, timeout, timeoutText, signal, subjectId) {
93
92
  return new Promise((resolve, reject) => {
94
- const key = this.#promptConversationKey(message);
93
+ if (signal?.aborted) {
94
+ reject(abortError(signal, timeoutText));
95
+ return;
96
+ }
97
+ const key = this.#promptConversationKey(message, subjectId);
95
98
  const existing = this.#promptClaims.get(key);
96
99
  if (existing) {
97
100
  clearTimeout(existing.timer);
98
101
  existing.reject(new Error('Prompt superseded'));
99
102
  }
100
103
  const timer = setTimeout(() => {
101
- this.#promptClaims.delete(key);
102
- reject(new Error(timeoutText));
104
+ settle(undefined, new Error(timeoutText));
103
105
  }, timeout);
104
- this.#promptClaims.set(key, { resolve, reject, timer });
106
+ const onAbort = () => settle(undefined, abortError(signal, timeoutText));
107
+ signal?.addEventListener('abort', onAbort, { once: true });
108
+ const settle = (value, error) => {
109
+ if (this.#promptClaims.get(key) !== claim)
110
+ return;
111
+ this.#promptClaims.delete(key);
112
+ clearTimeout(timer);
113
+ signal?.removeEventListener('abort', onAbort);
114
+ if (value !== undefined)
115
+ resolve(value);
116
+ else
117
+ reject(error ?? new Error(timeoutText));
118
+ };
119
+ const claim = {
120
+ resolve: (raw) => settle(raw),
121
+ reject: (error) => settle(undefined, error),
122
+ timer,
123
+ };
124
+ this.#promptClaims.set(key, claim);
105
125
  });
106
126
  }
107
- #buildCommandPrompt(message) {
127
+ #buildCommandPrompt(message, subjectId) {
108
128
  const DEFAULT_TIMEOUT = 3 * 60 * 1000;
109
129
  const DEFAULT_TIMEOUT_TEXT = '输入超时';
110
- const claim = (timeout, timeoutText) => this.#claimNextMessage(message, timeout, timeoutText);
130
+ const claim = (timeout, timeoutText, signal) => this.#claimNextMessage(message, timeout, timeoutText, signal, subjectId);
111
131
  const reply = (content) => message.$reply(content);
112
132
  return {
113
133
  async text(tips, options) {
114
134
  await reply(tips);
115
135
  try {
116
- return await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT);
136
+ return await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT, options?.signal);
117
137
  }
118
138
  catch (e) {
119
139
  if (options?.default !== undefined)
@@ -125,7 +145,7 @@ export class ImRuntime {
125
145
  async number(tips, options) {
126
146
  await reply(tips);
127
147
  try {
128
- const raw = await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT);
148
+ const raw = await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT, options?.signal);
129
149
  return +raw;
130
150
  }
131
151
  catch (e) {
@@ -139,7 +159,7 @@ export class ImRuntime {
139
159
  const condition = options?.condition ?? 'yes';
140
160
  await reply(`${tips}\n输入"${condition}"以确认`);
141
161
  try {
142
- const raw = await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT);
162
+ const raw = await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT, options?.signal);
143
163
  return raw === condition;
144
164
  }
145
165
  catch (e) {
@@ -153,7 +173,7 @@ export class ImRuntime {
153
173
  const separator = options?.separator ?? ',';
154
174
  await reply(`${tips}\n值之间使用"${separator}"分隔`);
155
175
  try {
156
- const raw = await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT);
176
+ const raw = await claim(options?.timeout ?? DEFAULT_TIMEOUT, options?.timeoutText ?? DEFAULT_TIMEOUT_TEXT, options?.signal);
157
177
  const type = options?.type ?? 'text';
158
178
  return raw.split(separator).map((v) => {
159
179
  if (type === 'number')
@@ -177,7 +197,7 @@ export class ImRuntime {
177
197
  items.push(`多选请用"${separator}"分隔`);
178
198
  await reply(`${tips}\n${items.join('\n')}`);
179
199
  try {
180
- const raw = await claim(options.timeout ?? DEFAULT_TIMEOUT, options.timeoutText ?? DEFAULT_TIMEOUT_TEXT);
200
+ const raw = await claim(options.timeout ?? DEFAULT_TIMEOUT, options.timeoutText ?? DEFAULT_TIMEOUT_TEXT, options.signal);
181
201
  if (!options.multiple) {
182
202
  return options.options.find((_, i) => i + 1 === +raw)?.value;
183
203
  }
@@ -195,6 +215,18 @@ export class ImRuntime {
195
215
  },
196
216
  };
197
217
  }
218
+ /**
219
+ * Bound Prompt for this conversation.
220
+ * `bind.subjectId` waits for that user (e.g. master) instead of the message sender.
221
+ */
222
+ createPrompt(message, bind) {
223
+ const subjectId = bind?.subjectId?.trim();
224
+ if (bind && !subjectId)
225
+ return undefined;
226
+ if (typeof message.$reply !== 'function' || !message.conversation)
227
+ return undefined;
228
+ return this.#buildCommandPrompt(message, subjectId);
229
+ }
198
230
  #createPromptForSource(source) {
199
231
  if (!source || typeof source !== 'object')
200
232
  return undefined;
@@ -261,6 +293,7 @@ export class ImRuntime {
261
293
  },
262
294
  }, lease.value);
263
295
  }, enrichedSender, Object.freeze({ ...input.metadata }), input.segments ? Object.freeze([...input.segments]) : undefined, input.message, input.endpointId, input.mentioned, input.replyTo);
296
+ await runHandlers(lease.value, 'message.receive', message);
264
297
  let result = Object.freeze({ matched: false });
265
298
  const claimed = await this.#inboundClaim?.(message) === true;
266
299
  if (claimed) {
@@ -534,8 +567,21 @@ export class ImRuntime {
534
567
  catch {
535
568
  return receipt ?? failedReceipt('outbound_middleware_failed');
536
569
  }
537
- if (!terminalEntered)
570
+ if (!terminalEntered) {
571
+ logger.debug(formatCompact({
572
+ op: 'replychain_runtime_send',
573
+ status: 'suppressed',
574
+ reason: 'middleware_stopped_before_terminal',
575
+ conv: formatConversationLog(request.conversation),
576
+ }));
538
577
  return suppressedReceipt();
578
+ }
579
+ logger.debug(formatCompact({
580
+ op: 'replychain_runtime_send',
581
+ status: receipt?.status ?? 'missing',
582
+ code: receipt?.failure?.code,
583
+ conv: formatConversationLog(request.conversation),
584
+ }));
539
585
  return receipt ?? failedReceipt('outbound_delivery_incomplete');
540
586
  }
541
587
  #acquire() {
@@ -675,6 +721,15 @@ async function runMiddleware(snapshot, input, terminal, target) {
675
721
  else
676
722
  await terminal();
677
723
  }
724
+ function handlers(snapshot) {
725
+ const projection = snapshot.projections.get(handlerFeatureId);
726
+ return isHandlerIndex(projection) ? projection : undefined;
727
+ }
728
+ async function runHandlers(snapshot, event, ...args) {
729
+ const index = handlers(snapshot);
730
+ if (index)
731
+ await index.dispatch(event, ...args);
732
+ }
678
733
  function normalizeConsoleContent(content) {
679
734
  if (typeof content === 'string')
680
735
  return content;
@@ -727,3 +782,8 @@ function flattenContent(content) {
727
782
  }
728
783
  return String(content);
729
784
  }
785
+ function abortError(signal, fallback) {
786
+ if (signal?.reason instanceof Error)
787
+ return signal.reason;
788
+ return new Error(fallback);
789
+ }
@@ -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, promptFactory);
46
+ const result = await commands.dispatch(matchInput, message, promptFactory, prefix);
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.8",
3
+ "version": "1.5.9",
4
4
  "description": "Zhin机器人核心框架",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -60,6 +60,11 @@
60
60
  "types": "./lib/feature/middleware.d.ts",
61
61
  "development": "./src/feature/middleware.ts",
62
62
  "import": "./lib/feature/middleware.js"
63
+ },
64
+ "./feature/handler": {
65
+ "types": "./lib/feature/handler.d.ts",
66
+ "development": "./src/feature/handler.ts",
67
+ "import": "./lib/feature/handler.js"
63
68
  }
64
69
  },
65
70
  "files": [
@@ -70,14 +75,15 @@
70
75
  "segment-matcher": "^1.0.5",
71
76
  "smol-toml": "^1.7.1",
72
77
  "yaml": "^2.9.0",
73
- "@zhin.js/adapter": "1.1.8",
74
- "@zhin.js/command": "1.0.12",
75
- "@zhin.js/component": "1.0.9",
78
+ "@zhin.js/adapter": "1.1.9",
79
+ "@zhin.js/command": "1.0.13",
80
+ "@zhin.js/component": "1.0.10",
76
81
  "@zhin.js/database": "1.0.79",
82
+ "@zhin.js/feature-kit": "1.0.10",
77
83
  "@zhin.js/im-contract": "1.0.3",
78
84
  "@zhin.js/kernel": "1.0.7",
79
- "@zhin.js/middleware": "1.0.9",
80
85
  "@zhin.js/logger": "1.0.76",
86
+ "@zhin.js/middleware": "1.0.10",
81
87
  "@zhin.js/permission": "1.0.2",
82
88
  "@zhin.js/plugin-runtime": "1.1.6",
83
89
  "@zhin.js/schema": "1.0.73"
@@ -95,7 +101,7 @@
95
101
  "@types/qrcode": "^1.5.6",
96
102
  "ajv": "8.18.0",
97
103
  "typescript": "^6.0.3",
98
- "@zhin.js/ai": "1.5.3"
104
+ "@zhin.js/ai": "1.5.4"
99
105
  },
100
106
  "repository": {
101
107
  "type": "git",
@@ -137,6 +143,10 @@
137
143
  {
138
144
  "package": "@zhin.js/middleware",
139
145
  "api": "^1.0.0"
146
+ },
147
+ {
148
+ "package": "@zhin.js/feature-kit",
149
+ "api": "^1.0.0"
140
150
  }
141
151
  ],
142
152
  "plugins": []