@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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
+
}
|
|
@@ -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}:${
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
reject(new Error(timeoutText));
|
|
104
|
+
settle(undefined, new Error(timeoutText));
|
|
103
105
|
}, timeout);
|
|
104
|
-
|
|
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.
|
|
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.
|
|
74
|
-
"@zhin.js/command": "1.0.
|
|
75
|
-
"@zhin.js/component": "1.0.
|
|
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.
|
|
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": []
|