@zhin.js/core 1.5.6 → 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 +1 -2
- package/lib/adapter.js +20 -7
- package/lib/built/adapter-process.d.ts +5 -0
- package/lib/built/adapter-process.js +5 -0
- package/lib/built/ai-outbound/parse.d.ts +1 -1
- package/lib/built/ai-outbound/parse.js +1 -1
- package/lib/built/ai-outbound/prompt.d.ts +1 -1
- package/lib/built/ai-outbound/prompt.js +1 -1
- package/lib/built/ai-outbound/structured-detect.js +1 -2
- package/lib/built/ai-outbound/types.d.ts +1 -3
- package/lib/built/ai-trigger.d.ts +0 -5
- package/lib/built/ai-trigger.js +0 -1
- package/lib/endpoint.d.ts +3 -3
- package/lib/endpoint.js +1 -1
- package/lib/plugin-runtime/im/im-runtime.d.ts +19 -19
- package/lib/plugin-runtime/im/im-runtime.js +202 -51
- package/lib/plugin-runtime/im/message-dispatcher.d.ts +2 -1
- package/lib/plugin-runtime/im/message-dispatcher.js +2 -2
- package/package.json +8 -8
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`
|
|
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 {
|
|
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`
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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>;
|
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
/**
|
|
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 触发检查结果
|
package/lib/built/ai-trigger.js
CHANGED
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 {
|
|
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 {
|
|
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 SnapshotLease, type
|
|
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';
|
|
@@ -34,14 +34,22 @@ export interface ImRuntimeOptions {
|
|
|
34
34
|
readonly renderer?: OutboundRenderer;
|
|
35
35
|
/** Process-root ingress claim (pending interaction, authentication challenge, etc.). */
|
|
36
36
|
readonly inboundClaim?: (message: Message) => boolean | Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* 入站 sender 增强:在构造 Message 前,将框架级角色(master / trusted)
|
|
39
|
+
* 合并到 sender.roles,使整个下游链路(命令分发、agent ingress 等)都能读到完整角色。
|
|
40
|
+
*
|
|
41
|
+
* 返回增强后的 sender(可原样返回)。缺省时 sender 保留适配器给出的平台角色。
|
|
42
|
+
*/
|
|
43
|
+
readonly enrichSender?: (sender: MessageSenderRef | undefined, conversation: IncomingMessage['conversation'], snapshot: RuntimeSnapshot) => MessageSenderRef | undefined;
|
|
37
44
|
}
|
|
38
45
|
export declare class ImRuntime implements MessageGateway {
|
|
39
46
|
#private;
|
|
40
47
|
constructor(options?: ImRuntimeOptions);
|
|
41
|
-
attach(snapshots:
|
|
48
|
+
attach(snapshots: SnapshotReader): void;
|
|
42
49
|
readonly permissionHost: import("@zhin.js/permission").PermissionHost;
|
|
43
50
|
readonly messageBus: MessageBus;
|
|
44
51
|
install(resources: Scope): void;
|
|
52
|
+
[generationAdmissionBinder](gate: GenerationAdmissionGate): MessageGateway;
|
|
45
53
|
/**
|
|
46
54
|
* 注册 interactive action 回跳 handler(prefix 最长匹配;返回注销函数)。
|
|
47
55
|
* 在 Command dispatch 之前路由:action 段 / 数字回跳 / 指令预填 payload。
|
|
@@ -61,7 +69,7 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
61
69
|
readonly owner: string;
|
|
62
70
|
readonly connected: boolean;
|
|
63
71
|
readonly status: 'online' | 'offline';
|
|
64
|
-
readonly phase:
|
|
72
|
+
readonly phase: AdapterEndpointPhase;
|
|
65
73
|
readonly managementCapabilities: readonly EndpointManagementCapability[];
|
|
66
74
|
}[];
|
|
67
75
|
/**
|
|
@@ -80,7 +88,7 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
80
88
|
readonly adapter: string;
|
|
81
89
|
readonly connected: boolean;
|
|
82
90
|
readonly status: 'online' | 'offline';
|
|
83
|
-
readonly phase:
|
|
91
|
+
readonly phase: AdapterEndpointPhase;
|
|
84
92
|
readonly managementCapabilities: readonly EndpointManagementCapability[];
|
|
85
93
|
} | null;
|
|
86
94
|
sendEndpointMessage(input: {
|
|
@@ -95,8 +103,7 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
95
103
|
addEndpointReaction(input: {
|
|
96
104
|
readonly adapter: string;
|
|
97
105
|
readonly endpointKey: string;
|
|
98
|
-
readonly message
|
|
99
|
-
readonly messageId?: string;
|
|
106
|
+
readonly message: MessageRef;
|
|
100
107
|
readonly emoji: string;
|
|
101
108
|
readonly sceneType?: string;
|
|
102
109
|
readonly channelId?: string;
|
|
@@ -104,22 +111,19 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
104
111
|
removeEndpointReaction(input: {
|
|
105
112
|
readonly adapter: string;
|
|
106
113
|
readonly endpointKey: string;
|
|
107
|
-
readonly message
|
|
108
|
-
readonly messageId?: string;
|
|
114
|
+
readonly message: MessageRef;
|
|
109
115
|
readonly reactionId: string;
|
|
110
116
|
}): Promise<void>;
|
|
111
117
|
/** Activity-feedback autoRemove: recall a previously sent status message. */
|
|
112
118
|
recallEndpointMessage(input: {
|
|
113
119
|
readonly adapter: string;
|
|
114
120
|
readonly endpointKey: string;
|
|
115
|
-
readonly message
|
|
116
|
-
readonly messageId?: string;
|
|
121
|
+
readonly message: MessageRef;
|
|
117
122
|
}): Promise<void>;
|
|
118
123
|
editEndpointMessage(input: {
|
|
119
124
|
readonly adapter: string;
|
|
120
125
|
readonly endpointKey: string;
|
|
121
|
-
readonly message
|
|
122
|
-
readonly messageId?: string;
|
|
126
|
+
readonly message: MessageRef;
|
|
123
127
|
readonly content: unknown;
|
|
124
128
|
}): Promise<string | null>;
|
|
125
129
|
setEndpointTyping(input: {
|
|
@@ -128,10 +132,6 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
128
132
|
readonly conversation: ConversationRef;
|
|
129
133
|
readonly active?: boolean;
|
|
130
134
|
}): Promise<void>;
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
* the Endpoint exists but implements no management operations; null means it
|
|
134
|
-
* cannot be resolved.
|
|
135
|
-
*/
|
|
136
|
-
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>;
|
|
137
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,
|
|
5
|
-
import {
|
|
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,14 +21,17 @@ export class ImRuntime {
|
|
|
21
21
|
#renderer;
|
|
22
22
|
#messageListeners = new Set();
|
|
23
23
|
#interactiveHandlers = [];
|
|
24
|
+
#promptClaims = new Map();
|
|
24
25
|
#snapshots;
|
|
25
26
|
#inboundClaim;
|
|
27
|
+
#enrichSender;
|
|
26
28
|
constructor(options = {}) {
|
|
27
29
|
this.#dispatcher = new MessageDispatcher(options.commandPrefix === undefined
|
|
28
30
|
? defaultCommandPrefixResolver
|
|
29
31
|
: () => options.commandPrefix ?? '');
|
|
30
32
|
this.#renderer = options.renderer ?? new OutboundRenderer();
|
|
31
33
|
this.#inboundClaim = options.inboundClaim;
|
|
34
|
+
this.#enrichSender = options.enrichSender;
|
|
32
35
|
}
|
|
33
36
|
attach(snapshots) {
|
|
34
37
|
if (this.#snapshots && this.#snapshots !== snapshots) {
|
|
@@ -43,12 +46,25 @@ export class ImRuntime {
|
|
|
43
46
|
resources.provide(permissionHostToken, this.permissionHost);
|
|
44
47
|
resources.provide(messageBusToken, this.messageBus);
|
|
45
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
|
+
}
|
|
46
59
|
/**
|
|
47
60
|
* 注册 interactive action 回跳 handler(prefix 最长匹配;返回注销函数)。
|
|
48
61
|
* 在 Command dispatch 之前路由:action 段 / 数字回跳 / 指令预填 payload。
|
|
49
62
|
*/
|
|
50
63
|
registerInteractiveHandler(prefix, handler) {
|
|
51
|
-
|
|
64
|
+
return this.#registerInteractiveHandler(prefix, handler);
|
|
65
|
+
}
|
|
66
|
+
#registerInteractiveHandler(prefix, handler, admission) {
|
|
67
|
+
const entry = Object.freeze({ prefix, handler, admission });
|
|
52
68
|
this.#interactiveHandlers.push(entry);
|
|
53
69
|
return () => {
|
|
54
70
|
const index = this.#interactiveHandlers.indexOf(entry);
|
|
@@ -56,6 +72,137 @@ export class ImRuntime {
|
|
|
56
72
|
this.#interactiveHandlers.splice(index, 1);
|
|
57
73
|
};
|
|
58
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
|
+
}
|
|
59
206
|
/**
|
|
60
207
|
* 订阅消息事件(入站 dispatch 完成后 / 出站发送成功后回调)。
|
|
61
208
|
* 返回注销函数。listener 抛错不会阻断消息链路。
|
|
@@ -75,6 +222,9 @@ export class ImRuntime {
|
|
|
75
222
|
}
|
|
76
223
|
}
|
|
77
224
|
async receive(input) {
|
|
225
|
+
return this.#receive(input);
|
|
226
|
+
}
|
|
227
|
+
async #receive(input, admission) {
|
|
78
228
|
const lease = this.#acquire();
|
|
79
229
|
let active = true;
|
|
80
230
|
try {
|
|
@@ -87,6 +237,9 @@ export class ImRuntime {
|
|
|
87
237
|
sender: `${input.sender?.name || 'undefined'}(${input.sender?.id || 'undefined'})`,
|
|
88
238
|
preview: truncatePreview(input.content),
|
|
89
239
|
}));
|
|
240
|
+
const enrichedSender = this.#enrichSender
|
|
241
|
+
? this.#enrichSender(input.sender, conversation, lease.value)
|
|
242
|
+
: input.sender;
|
|
90
243
|
const message = new Message(conversation, input.content, lease.value.generation, (content, replyRequester = requester, targetConversation) => {
|
|
91
244
|
if (!active)
|
|
92
245
|
throw new Error('Message reply scope has ended');
|
|
@@ -98,7 +251,7 @@ export class ImRuntime {
|
|
|
98
251
|
requester: replyRequester,
|
|
99
252
|
content,
|
|
100
253
|
incoming: {
|
|
101
|
-
sender:
|
|
254
|
+
sender: enrichedSender,
|
|
102
255
|
content: input.content,
|
|
103
256
|
segments: input.segments,
|
|
104
257
|
messageId: input.message?.id,
|
|
@@ -107,16 +260,20 @@ export class ImRuntime {
|
|
|
107
260
|
mentioned: input.mentioned,
|
|
108
261
|
},
|
|
109
262
|
}, lease.value);
|
|
110
|
-
},
|
|
263
|
+
}, enrichedSender, Object.freeze({ ...input.metadata }), input.segments ? Object.freeze([...input.segments]) : undefined, input.message, input.endpointId, input.mentioned, input.replyTo);
|
|
111
264
|
let result = Object.freeze({ matched: false });
|
|
112
265
|
const claimed = await this.#inboundClaim?.(message) === true;
|
|
113
266
|
if (claimed) {
|
|
114
267
|
result = Object.freeze({ matched: true, command: 'interaction', owner: requester });
|
|
115
268
|
}
|
|
269
|
+
else if (this.#resolvePromptClaim(message)) {
|
|
270
|
+
result = Object.freeze({ matched: true, command: 'prompt', owner: requester });
|
|
271
|
+
}
|
|
116
272
|
else {
|
|
273
|
+
const promptFactory = (source) => this.#createPromptForSource(source);
|
|
117
274
|
await runMiddleware(lease.value, message, async () => {
|
|
118
|
-
result = await this.#dispatchInteractive(message, requester)
|
|
119
|
-
?? await this.#dispatcher.dispatch(message, lease.value);
|
|
275
|
+
result = await this.#dispatchInteractive(message, requester, admission)
|
|
276
|
+
?? await this.#dispatcher.dispatch(message, lease.value, promptFactory);
|
|
120
277
|
const ingressRoute = resolveIngressRoute(lease.value);
|
|
121
278
|
if (!result.matched && ingressRoute) {
|
|
122
279
|
logger.debug(formatCompact({ op: 'unmatched', conv: formatConversationLog(conversation) }));
|
|
@@ -268,58 +425,59 @@ export class ImRuntime {
|
|
|
268
425
|
}
|
|
269
426
|
/** Activity-feedback: add a message reaction when the live Endpoint supports it. */
|
|
270
427
|
async addEndpointReaction(input) {
|
|
271
|
-
|
|
272
|
-
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, {
|
|
273
429
|
sceneType: input.sceneType,
|
|
274
430
|
channelId: input.channelId,
|
|
275
|
-
}) ?? null;
|
|
431
|
+
}) ?? null, null);
|
|
276
432
|
}
|
|
277
433
|
async removeEndpointReaction(input) {
|
|
278
|
-
await this.#
|
|
279
|
-
?.removeReaction?.(legacyMessageTarget(input.message, input.messageId), input.reactionId);
|
|
434
|
+
await this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.removeReaction?.(input.message, input.reactionId), undefined);
|
|
280
435
|
}
|
|
281
436
|
/** Activity-feedback autoRemove: recall a previously sent status message. */
|
|
282
437
|
async recallEndpointMessage(input) {
|
|
283
|
-
await this.#
|
|
284
|
-
?.recall?.(legacyMessageTarget(input.message, input.messageId));
|
|
438
|
+
await this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.recall?.(input.message), undefined);
|
|
285
439
|
}
|
|
286
440
|
async editEndpointMessage(input) {
|
|
287
|
-
return this.#
|
|
288
|
-
?.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);
|
|
289
442
|
}
|
|
290
443
|
async setEndpointTyping(input) {
|
|
291
|
-
await this.#
|
|
292
|
-
?.typing?.(input.conversation, input.active);
|
|
444
|
+
await this.#withEndpointControl(input.adapter, input.endpointKey, (control) => control.typing?.(input.conversation, input.active), undefined);
|
|
293
445
|
}
|
|
294
|
-
#
|
|
446
|
+
async #withEndpointControl(adapter, endpointKey, run, fallback) {
|
|
447
|
+
let lease;
|
|
295
448
|
try {
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
const endpoint = requireAdapters(lease.value).instance(adapter, endpointKey);
|
|
299
|
-
return endpoint ?? null;
|
|
300
|
-
}
|
|
301
|
-
finally {
|
|
302
|
-
lease.release();
|
|
303
|
-
}
|
|
449
|
+
lease = this.#acquire();
|
|
304
450
|
}
|
|
305
451
|
catch {
|
|
306
|
-
return
|
|
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();
|
|
307
461
|
}
|
|
308
462
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
* cannot be resolved.
|
|
317
|
-
*/
|
|
318
|
-
getEndpointManagement(adapter, endpointKey) {
|
|
319
|
-
const endpoint = this.#liveEndpoint(adapter, endpointKey);
|
|
320
|
-
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 {
|
|
321
470
|
return null;
|
|
322
|
-
|
|
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
|
+
}
|
|
323
481
|
}
|
|
324
482
|
async #sendWithSnapshot(request, snapshot) {
|
|
325
483
|
const adapter = request.conversation.endpoint.id;
|
|
@@ -389,13 +547,13 @@ export class ImRuntime {
|
|
|
389
547
|
* interactive 回跳分发(Command dispatch 之前):action 段 / 中央 fallback
|
|
390
548
|
* 数字回跳 / 指令预填 payload → prefix 最长匹配 handler。
|
|
391
549
|
*/
|
|
392
|
-
async #dispatchInteractive(message, requester) {
|
|
550
|
+
async #dispatchInteractive(message, requester, admission) {
|
|
393
551
|
if (this.#interactiveHandlers.length === 0)
|
|
394
552
|
return undefined;
|
|
395
553
|
const payload = resolveRuntimeInteractivePayload(message);
|
|
396
554
|
if (!payload)
|
|
397
555
|
return undefined;
|
|
398
|
-
const handler = findRuntimeInteractiveHandler(this.#interactiveHandlers, payload);
|
|
556
|
+
const handler = findRuntimeInteractiveHandler(this.#interactiveHandlers.filter((entry) => !entry.admission || entry.admission === admission), payload);
|
|
399
557
|
if (!handler)
|
|
400
558
|
return undefined;
|
|
401
559
|
const handled = await handler(message);
|
|
@@ -434,13 +592,6 @@ function isDirectHtmlConsumer(snapshot, adapter) {
|
|
|
434
592
|
const owner = snapshot.capabilities.get(adapter)?.owner;
|
|
435
593
|
return adapterTypeName(snapshot.tree.get(owner)?.packageName) === 'sandbox';
|
|
436
594
|
}
|
|
437
|
-
function legacyMessageTarget(message, messageId) {
|
|
438
|
-
if (message)
|
|
439
|
-
return formatLegacyMessageRef(message);
|
|
440
|
-
if (messageId)
|
|
441
|
-
return messageId;
|
|
442
|
-
throw new TypeError('message or messageId is required');
|
|
443
|
-
}
|
|
444
595
|
async function prepareOutboundPayload(rendered, conversation, snapshot, finalizeInteractive = false) {
|
|
445
596
|
const adapter = conversation.endpoint.id;
|
|
446
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.
|
|
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.
|
|
74
|
-
"@zhin.js/command": "1.0.
|
|
75
|
-
"@zhin.js/component": "1.0.
|
|
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/
|
|
81
|
-
"@zhin.js/
|
|
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.
|
|
98
|
+
"@zhin.js/ai": "1.5.3"
|
|
99
99
|
},
|
|
100
100
|
"repository": {
|
|
101
101
|
"type": "git",
|