@zhin.js/core 1.5.2 → 1.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/adapter.d.ts +7 -1
- package/lib/adapter.js +28 -24
- package/lib/agent-prompt.d.ts +2 -3
- package/lib/agent-prompt.js +5 -0
- package/lib/built/ai-outbound/parse.d.ts +1 -1
- package/lib/built/ai-outbound/parse.js +2 -6
- package/lib/built/ai-outbound/plain-mention-rewrite.js +4 -4
- package/lib/built/ai-outbound/resolve.js +8 -8
- package/lib/built/ai-outbound/types.d.ts +3 -3
- package/lib/built/ai-trigger.d.ts +5 -0
- package/lib/built/ai-trigger.js +40 -13
- package/lib/built/authorization.js +4 -4
- package/lib/built/command.d.ts +2 -2
- package/lib/built/command.js +2 -2
- package/lib/built/common-adapter-tools.d.ts +11 -11
- package/lib/built/common-adapter-tools.js +13 -13
- package/lib/built/config.d.ts +0 -4
- package/lib/built/config.js +0 -4
- package/lib/built/dispatcher.d.ts +2 -0
- package/lib/built/dispatcher.js +1 -1
- package/lib/built/endpoint-commands.js +2 -2
- package/lib/built/endpoint-lifecycle-service.js +13 -13
- package/lib/built/endpoint-lifecycle.d.ts +3 -3
- package/lib/built/endpoint-lifecycle.js +2 -2
- package/lib/built/endpoint-manager.d.ts +1 -1
- package/lib/built/interactive-segments/index.d.ts +1 -1
- package/lib/built/interactive-segments/index.js +1 -1
- package/lib/built/interactive-segments/keyboard-segment.d.ts +0 -1
- package/lib/built/interactive-segments/keyboard-segment.js +0 -1
- package/lib/built/interactive-segments/onebot-keyboard.d.ts +0 -3
- package/lib/built/interactive-segments/onebot-keyboard.js +0 -2
- package/lib/built/interactive-segments/resolve.d.ts +0 -3
- package/lib/built/interactive-segments/resolve.js +0 -3
- package/lib/built/interactive-segments/types.d.ts +8 -20
- package/lib/built/interactive-segments/types.js +0 -4
- package/lib/built/login-assist.d.ts +2 -2
- package/lib/built/login-assist.js +2 -2
- package/lib/built/message-enrich.d.ts +1 -1
- package/lib/built/message-enrich.js +2 -7
- package/lib/built/permission.d.ts +0 -4
- package/lib/built/permission.js +0 -4
- package/lib/built/prepend-quote-context.js +5 -5
- package/lib/built/rich-segments/capabilities.js +0 -1
- package/lib/built/rich-segments/html-segment.js +1 -4
- package/lib/built/rich-segments/markdown-segment.js +1 -4
- package/lib/built/rich-segments/types.d.ts +0 -5
- package/lib/built/schema-endpoint-manager.js +6 -6
- package/lib/built/tool.d.ts +5 -3
- package/lib/built/tool.js +14 -23
- package/lib/command.d.ts +4 -4
- package/lib/command.js +8 -11
- package/lib/endpoint-capabilities.d.ts +2 -3
- package/lib/endpoint-capabilities.js +4 -4
- package/lib/endpoint.d.ts +2 -0
- package/lib/endpoint.js +1 -0
- package/lib/errors.d.ts +2 -2
- package/lib/errors.js +4 -4
- package/lib/host-plugin-registry.d.ts +6 -5
- package/lib/host-plugin-registry.js +9 -4
- package/lib/im-scene.d.ts +1 -1
- package/lib/im-scene.js +5 -5
- package/lib/im-session-id.d.ts +2 -2
- package/lib/im-session-id.js +4 -4
- package/lib/index.d.ts +1 -2
- package/lib/index.js +0 -2
- package/lib/jsx-runtime.d.ts +1 -0
- package/lib/jsx.d.ts +19 -21
- package/lib/message.d.ts +1 -1
- package/lib/message.js +1 -1
- package/lib/notice.d.ts +2 -2
- package/lib/plugin-context.d.ts +11 -13
- package/lib/plugin-context.js +21 -18
- package/lib/plugin-runtime/im/contracts.d.ts +91 -12
- package/lib/plugin-runtime/im/contracts.js +79 -1
- package/lib/plugin-runtime/im/im-runtime.d.ts +33 -23
- package/lib/plugin-runtime/im/im-runtime.js +104 -63
- package/lib/plugin-runtime/im/index.d.ts +2 -0
- package/lib/plugin-runtime/im/index.js +2 -0
- package/lib/plugin-runtime/im/message-bus.d.ts +38 -0
- package/lib/plugin-runtime/im/message-bus.js +40 -0
- package/lib/plugin-runtime/im/message-dispatcher.js +13 -73
- package/lib/plugin-runtime/im/outbound-renderer.d.ts +3 -2
- package/lib/plugin-runtime/im/outbound-renderer.js +44 -8
- package/lib/plugin-runtime/im/service-tokens.d.ts +18 -0
- package/lib/plugin-runtime/im/service-tokens.js +13 -0
- package/lib/plugin.d.ts +4 -7
- package/lib/plugin.js +7 -30
- package/lib/request.d.ts +2 -2
- package/lib/side-event/types.d.ts +0 -4
- package/lib/types.d.ts +6 -11
- package/lib/utils.js +3 -1
- package/package.json +13 -12
- package/lib/built/qrcode-segment.d.ts +0 -18
- package/lib/built/qrcode-segment.js +0 -78
- package/lib/types-generator.d.ts +0 -5
- package/lib/types-generator.js +0 -71
|
@@ -40,6 +40,9 @@ export class Message {
|
|
|
40
40
|
metadata;
|
|
41
41
|
segments;
|
|
42
42
|
message;
|
|
43
|
+
endpointId;
|
|
44
|
+
mentioned;
|
|
45
|
+
replyTo;
|
|
43
46
|
constructor(conversation, content, generation, reply, sender, metadata = Object.freeze({}),
|
|
44
47
|
/**
|
|
45
48
|
* 结构化段视图(与 `content` 纯文本视图同源,见 IncomingMessage.segments)。
|
|
@@ -47,7 +50,7 @@ export class Message {
|
|
|
47
50
|
*/
|
|
48
51
|
segments,
|
|
49
52
|
/** 结构化入站消息身份(平台原生 message id 经 MessageRef 传递)。 */
|
|
50
|
-
message) {
|
|
53
|
+
message, endpointId, mentioned, replyTo) {
|
|
51
54
|
this.conversation = conversation;
|
|
52
55
|
this.content = content;
|
|
53
56
|
this.generation = generation;
|
|
@@ -55,8 +58,42 @@ export class Message {
|
|
|
55
58
|
this.metadata = metadata;
|
|
56
59
|
this.segments = segments;
|
|
57
60
|
this.message = message;
|
|
61
|
+
this.endpointId = endpointId;
|
|
62
|
+
this.mentioned = mentioned;
|
|
63
|
+
this.replyTo = replyTo;
|
|
58
64
|
this.$reply = (content) => reply(content);
|
|
59
65
|
this.$replyFrom = (requester, content) => reply(content, requester);
|
|
66
|
+
this.$sendTo = (target, content) => reply(content, undefined, target);
|
|
67
|
+
this.$replyToPrivate = (content, from) => {
|
|
68
|
+
if (!sender)
|
|
69
|
+
throw new Error('Cannot $replyToPrivate: message has no sender');
|
|
70
|
+
let parent;
|
|
71
|
+
if (from === true) {
|
|
72
|
+
if (conversation.kind !== 'group' && conversation.kind !== 'channel') {
|
|
73
|
+
throw new Error('Cannot $replyToPrivate with from=true: current conversation must be a group or channel');
|
|
74
|
+
}
|
|
75
|
+
parent = { kind: conversation.kind, id: conversation.id };
|
|
76
|
+
}
|
|
77
|
+
else if (from != null && typeof from === 'object') {
|
|
78
|
+
parent = from;
|
|
79
|
+
}
|
|
80
|
+
return reply(content, undefined, {
|
|
81
|
+
kind: 'private',
|
|
82
|
+
id: sender.id,
|
|
83
|
+
...(parent ? { parent } : {}),
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
this.$replyToGroup = (groupId, content) => {
|
|
87
|
+
return reply(content, undefined, { kind: 'group', id: groupId });
|
|
88
|
+
};
|
|
89
|
+
this.$replyToChannel = (channelId, guildId, content, threadId) => {
|
|
90
|
+
return reply(content, undefined, {
|
|
91
|
+
kind: 'channel',
|
|
92
|
+
id: channelId,
|
|
93
|
+
parent: { kind: 'channel', id: guildId },
|
|
94
|
+
...(threadId ? { threadId } : {}),
|
|
95
|
+
});
|
|
96
|
+
};
|
|
60
97
|
Object.freeze(this);
|
|
61
98
|
}
|
|
62
99
|
/** 平台原生消息 id(`message` 未提供时为 undefined)。 */
|
|
@@ -65,6 +102,47 @@ export class Message {
|
|
|
65
102
|
}
|
|
66
103
|
$reply;
|
|
67
104
|
$replyFrom;
|
|
105
|
+
/**
|
|
106
|
+
* 向同 Endpoint 的另一个通道发送消息(通用)。
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* await message.$sendTo({ kind: 'channel', id: 'ch-1', parent: { kind: 'channel', id: 'guild-1' } }, '频道通知');
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
$sendTo;
|
|
113
|
+
/**
|
|
114
|
+
* 私信当前消息的发送者(同 Endpoint)。
|
|
115
|
+
*
|
|
116
|
+
* @param content 消息内容
|
|
117
|
+
* @param from 会话上下文:
|
|
118
|
+
* - `true` — 用当前群/频道作为 parent(要求当前不在私聊中)
|
|
119
|
+
* - `{ kind, id }` — 显式指定 parent(群或子频道)
|
|
120
|
+
* - 省略 — 直接私信
|
|
121
|
+
*
|
|
122
|
+
* ```ts
|
|
123
|
+
* await message.$replyToPrivate('直接私信');
|
|
124
|
+
* await message.$replyToPrivate('群临时私信', true);
|
|
125
|
+
* await message.$replyToPrivate('频道私信', { kind: 'channel', id: '子频道ID' });
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
$replyToPrivate;
|
|
129
|
+
/**
|
|
130
|
+
* 向指定群发送消息(同 Endpoint)。
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* await message.$replyToGroup('67890', '群通知');
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
$replyToGroup;
|
|
137
|
+
/**
|
|
138
|
+
* 向指定频道/子频道发送消息(同 Endpoint)。
|
|
139
|
+
*
|
|
140
|
+
* ```ts
|
|
141
|
+
* await message.$replyToChannel('channel-1', 'guild-1', '频道通知');
|
|
142
|
+
* await message.$replyToChannel('channel-1', 'guild-1', '话题回复', 'thread-1');
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
$replyToChannel;
|
|
68
146
|
}
|
|
69
147
|
export function createOutboundEnvelope(request, initialPayload) {
|
|
70
148
|
let payload = initialPayload;
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import { Scope, type PluginId, type
|
|
1
|
+
import { Scope, type PluginId, type SnapshotLease, type SnapshotStore } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { MessageBus } from './message-bus.js';
|
|
2
3
|
import { type EndpointManagement, type EndpointManagementCapability } from '@zhin.js/adapter';
|
|
3
4
|
import { type ConversationRef, type DeliveryReceipt, type MessageRef } from '@zhin.js/im-contract';
|
|
4
|
-
import { Message, type ConversationAddress, type IncomingMessage, type MessageDispatchResult, type MessageGateway, type SendRequest } from './contracts.js';
|
|
5
|
+
import { Message, type ConversationAddress, type IncomingMessage, type MessageDispatchResult, type MessageGateway, type MessageSenderRef, type SendRequest } from './contracts.js';
|
|
5
6
|
import { OutboundRenderer } from './outbound-renderer.js';
|
|
6
7
|
import { type RuntimeInteractiveHandler } from './interactive.js';
|
|
7
8
|
export declare const messageGatewayToken: import("@zhin.js/plugin-runtime").Token<MessageGateway>;
|
|
9
|
+
/** Generation-owned terminal route after interactive/command routing misses. */
|
|
10
|
+
export interface IngressRoute {
|
|
11
|
+
route(message: Message, lease: SnapshotLease, requester: PluginId): Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
export declare const ingressRouteToken: import("@zhin.js/plugin-runtime").Token<IngressRoute>;
|
|
8
14
|
/** Console 实时消息事件(SSE 推送源;content 仅为截断预览,不含完整原始段)。 */
|
|
9
15
|
export interface RuntimeMessageEvent {
|
|
10
16
|
readonly direction: 'inbound' | 'outbound';
|
|
11
17
|
readonly conversation: ConversationRef;
|
|
12
|
-
/** inbound
|
|
13
|
-
readonly sender?:
|
|
18
|
+
/** inbound:发送者。 */
|
|
19
|
+
readonly sender?: MessageSenderRef;
|
|
14
20
|
/** outbound:发起方插件。 */
|
|
15
21
|
readonly requester?: PluginId;
|
|
16
22
|
/** 预览文本,截断至 200 字。 */
|
|
@@ -26,17 +32,15 @@ export interface ImRuntimeOptions {
|
|
|
26
32
|
*/
|
|
27
33
|
readonly commandPrefix?: string;
|
|
28
34
|
readonly renderer?: OutboundRenderer;
|
|
35
|
+
/** Process-root ingress claim (pending interaction, authentication challenge, etc.). */
|
|
36
|
+
readonly inboundClaim?: (message: Message) => boolean | Promise<boolean>;
|
|
29
37
|
}
|
|
30
38
|
export declare class ImRuntime implements MessageGateway {
|
|
31
39
|
#private;
|
|
32
40
|
constructor(options?: ImRuntimeOptions);
|
|
33
41
|
attach(snapshots: SnapshotStore): void;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* Return true when the message was handled (reply already sent).
|
|
37
|
-
* `requester` is the Adapter Endpoint owner (for CapabilityIngress inheritance).
|
|
38
|
-
*/
|
|
39
|
-
setUnmatchedHandler(handler: (message: Message, snapshot: RuntimeSnapshot, requester: PluginId) => Promise<boolean>): void;
|
|
42
|
+
readonly permissionHost: import("@zhin.js/permission").PermissionHost;
|
|
43
|
+
readonly messageBus: MessageBus;
|
|
40
44
|
install(resources: Scope): void;
|
|
41
45
|
/**
|
|
42
46
|
* 注册 interactive action 回跳 handler(prefix 最长匹配;返回注销函数)。
|
|
@@ -60,7 +64,18 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
60
64
|
readonly phase: 'pending' | 'starting' | 'online' | 'failed' | 'unconfigured';
|
|
61
65
|
readonly managementCapabilities: readonly EndpointManagementCapability[];
|
|
62
66
|
}[];
|
|
63
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Console `GET /api/stats` 同源计数:非 root 插件节点 + AdapterIndex endpoints。
|
|
69
|
+
* 供命令 / 状态卡等在 Plugin Runtime 下读取(legacy `root.adapters` / `root.children` 已不存在)。
|
|
70
|
+
*/
|
|
71
|
+
inventory(): Readonly<{
|
|
72
|
+
plugins: number;
|
|
73
|
+
endpoints: {
|
|
74
|
+
readonly total: number;
|
|
75
|
+
readonly online: number;
|
|
76
|
+
};
|
|
77
|
+
}>;
|
|
78
|
+
getEndpoint(adapter: string, endpointKey: string): {
|
|
64
79
|
readonly name: string;
|
|
65
80
|
readonly adapter: string;
|
|
66
81
|
readonly connected: boolean;
|
|
@@ -70,7 +85,7 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
70
85
|
} | null;
|
|
71
86
|
sendEndpointMessage(input: {
|
|
72
87
|
readonly adapter: string;
|
|
73
|
-
readonly
|
|
88
|
+
readonly endpointKey: string;
|
|
74
89
|
readonly conversation: ConversationAddress;
|
|
75
90
|
readonly content: unknown;
|
|
76
91
|
}): Promise<{
|
|
@@ -79,7 +94,7 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
79
94
|
/** Activity-feedback: add a message reaction when the live Endpoint supports it. */
|
|
80
95
|
addEndpointReaction(input: {
|
|
81
96
|
readonly adapter: string;
|
|
82
|
-
readonly
|
|
97
|
+
readonly endpointKey: string;
|
|
83
98
|
readonly message?: MessageRef;
|
|
84
99
|
readonly messageId?: string;
|
|
85
100
|
readonly emoji: string;
|
|
@@ -88,7 +103,7 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
88
103
|
}): Promise<string | null>;
|
|
89
104
|
removeEndpointReaction(input: {
|
|
90
105
|
readonly adapter: string;
|
|
91
|
-
readonly
|
|
106
|
+
readonly endpointKey: string;
|
|
92
107
|
readonly message?: MessageRef;
|
|
93
108
|
readonly messageId?: string;
|
|
94
109
|
readonly reactionId: string;
|
|
@@ -96,32 +111,27 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
96
111
|
/** Activity-feedback autoRemove: recall a previously sent status message. */
|
|
97
112
|
recallEndpointMessage(input: {
|
|
98
113
|
readonly adapter: string;
|
|
99
|
-
readonly
|
|
114
|
+
readonly endpointKey: string;
|
|
100
115
|
readonly message?: MessageRef;
|
|
101
116
|
readonly messageId?: string;
|
|
102
117
|
}): Promise<void>;
|
|
103
118
|
editEndpointMessage(input: {
|
|
104
119
|
readonly adapter: string;
|
|
105
|
-
readonly
|
|
120
|
+
readonly endpointKey: string;
|
|
106
121
|
readonly message?: MessageRef;
|
|
107
122
|
readonly messageId?: string;
|
|
108
123
|
readonly content: unknown;
|
|
109
124
|
}): Promise<string | null>;
|
|
110
125
|
setEndpointTyping(input: {
|
|
111
126
|
readonly adapter: string;
|
|
112
|
-
readonly
|
|
127
|
+
readonly endpointKey: string;
|
|
113
128
|
readonly conversation: ConversationRef;
|
|
114
129
|
readonly active?: boolean;
|
|
115
130
|
}): Promise<void>;
|
|
116
|
-
/**
|
|
117
|
-
* Console endpoint 社交/群管 RPC:解析 live Endpoint 实例(无则 null)。
|
|
118
|
-
* @deprecated Host callers should use `getEndpointManagement()`.
|
|
119
|
-
*/
|
|
120
|
-
getLiveEndpoint(adapter: string, endpointId: string): unknown | null;
|
|
121
131
|
/**
|
|
122
132
|
* Narrow Host seam for Console social/group management. An empty object means
|
|
123
133
|
* the Endpoint exists but implements no management operations; null means it
|
|
124
134
|
* cannot be resolved.
|
|
125
135
|
*/
|
|
126
|
-
getEndpointManagement(adapter: string,
|
|
136
|
+
getEndpointManagement(adapter: string, endpointKey: string): EndpointManagement | null;
|
|
127
137
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createToken, htmlRendererToken, } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { createPermissionHost, permissionHostToken } from '@zhin.js/permission';
|
|
3
|
+
import { MessageBus, messageBusToken } from './message-bus.js';
|
|
2
4
|
import { adapterFeatureId, isAdapterIndex, resolveEndpointControl, resolveEndpointManagement, } from '@zhin.js/adapter';
|
|
3
5
|
import { formatLegacyMessageRef, isDeliveryReceipt, } from '@zhin.js/im-contract';
|
|
4
6
|
import { isMiddlewareIndex, middlewareFeatureId } from '@zhin.js/middleware';
|
|
@@ -12,6 +14,7 @@ import { keyboardFallbackStore } from '../../built/interactive-segments/fallback
|
|
|
12
14
|
import { findRuntimeInteractiveHandler, resolveRuntimeInteractivePayload, runtimeInteractiveConversationKey, } from './interactive.js';
|
|
13
15
|
const logger = getLogger('im');
|
|
14
16
|
export const messageGatewayToken = createToken('zhin.im.message-gateway');
|
|
17
|
+
export const ingressRouteToken = createToken('zhin.im.ingress-route');
|
|
15
18
|
export const messagePreviewLimit = 200;
|
|
16
19
|
export class ImRuntime {
|
|
17
20
|
#dispatcher;
|
|
@@ -19,12 +22,13 @@ export class ImRuntime {
|
|
|
19
22
|
#messageListeners = new Set();
|
|
20
23
|
#interactiveHandlers = [];
|
|
21
24
|
#snapshots;
|
|
22
|
-
#
|
|
25
|
+
#inboundClaim;
|
|
23
26
|
constructor(options = {}) {
|
|
24
27
|
this.#dispatcher = new MessageDispatcher(options.commandPrefix === undefined
|
|
25
28
|
? defaultCommandPrefixResolver
|
|
26
29
|
: () => options.commandPrefix ?? '');
|
|
27
30
|
this.#renderer = options.renderer ?? new OutboundRenderer();
|
|
31
|
+
this.#inboundClaim = options.inboundClaim;
|
|
28
32
|
}
|
|
29
33
|
attach(snapshots) {
|
|
30
34
|
if (this.#snapshots && this.#snapshots !== snapshots) {
|
|
@@ -32,16 +36,12 @@ export class ImRuntime {
|
|
|
32
36
|
}
|
|
33
37
|
this.#snapshots = snapshots;
|
|
34
38
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* Return true when the message was handled (reply already sent).
|
|
38
|
-
* `requester` is the Adapter Endpoint owner (for CapabilityIngress inheritance).
|
|
39
|
-
*/
|
|
40
|
-
setUnmatchedHandler(handler) {
|
|
41
|
-
this.#unmatchedHandler = handler;
|
|
42
|
-
}
|
|
39
|
+
permissionHost = createPermissionHost();
|
|
40
|
+
messageBus = new MessageBus();
|
|
43
41
|
install(resources) {
|
|
44
42
|
resources.provide(messageGatewayToken, this);
|
|
43
|
+
resources.provide(permissionHostToken, this.permissionHost);
|
|
44
|
+
resources.provide(messageBusToken, this.messageBus);
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* 注册 interactive action 回跳 handler(prefix 最长匹配;返回注销函数)。
|
|
@@ -82,48 +82,58 @@ export class ImRuntime {
|
|
|
82
82
|
const adapter = conversation.endpoint.id;
|
|
83
83
|
const requester = requireAdapters(lease.value).owner(adapter);
|
|
84
84
|
logger.debug(formatCompact({
|
|
85
|
-
op: '
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
sender: input.sender,
|
|
89
|
-
id: input.message?.id,
|
|
85
|
+
op: 'receive',
|
|
86
|
+
conv: formatConversationLog(conversation),
|
|
87
|
+
sender: `${input.sender?.name || 'undefined'}(${input.sender?.id || 'undefined'})`,
|
|
90
88
|
preview: truncatePreview(input.content),
|
|
91
|
-
segments: input.segments?.length,
|
|
92
|
-
generation: lease.value.generation,
|
|
93
89
|
}));
|
|
94
|
-
const message = new Message(conversation, input.content, lease.value.generation, (content, replyRequester = requester) => {
|
|
90
|
+
const message = new Message(conversation, input.content, lease.value.generation, (content, replyRequester = requester, targetConversation) => {
|
|
95
91
|
if (!active)
|
|
96
92
|
throw new Error('Message reply scope has ended');
|
|
93
|
+
const effectiveConversation = targetConversation
|
|
94
|
+
? { endpoint: conversation.endpoint, ...targetConversation }
|
|
95
|
+
: conversation;
|
|
97
96
|
return this.#sendWithSnapshot({
|
|
98
|
-
conversation,
|
|
97
|
+
conversation: effectiveConversation,
|
|
99
98
|
requester: replyRequester,
|
|
100
99
|
content,
|
|
100
|
+
incoming: {
|
|
101
|
+
sender: input.sender,
|
|
102
|
+
content: input.content,
|
|
103
|
+
segments: input.segments,
|
|
104
|
+
messageId: input.message?.id,
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
endpointId: input.endpointId,
|
|
107
|
+
mentioned: input.mentioned,
|
|
108
|
+
},
|
|
101
109
|
}, lease.value);
|
|
102
|
-
}, input.sender, Object.freeze({ ...input.metadata }), input.segments ? Object.freeze([...input.segments]) : undefined, input.message);
|
|
110
|
+
}, input.sender, Object.freeze({ ...input.metadata }), input.segments ? Object.freeze([...input.segments]) : undefined, input.message, input.endpointId, input.mentioned, input.replyTo);
|
|
103
111
|
let result = Object.freeze({ matched: false });
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
const claimed = await this.#inboundClaim?.(message) === true;
|
|
113
|
+
if (claimed) {
|
|
114
|
+
result = Object.freeze({ matched: true, command: 'interaction', owner: requester });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
await runMiddleware(lease.value, message, async () => {
|
|
118
|
+
result = await this.#dispatchInteractive(message, requester)
|
|
119
|
+
?? await this.#dispatcher.dispatch(message, lease.value);
|
|
120
|
+
const ingressRoute = resolveIngressRoute(lease.value);
|
|
121
|
+
if (!result.matched && ingressRoute) {
|
|
122
|
+
logger.debug(formatCompact({ op: 'unmatched', conv: formatConversationLog(conversation) }));
|
|
123
|
+
const handled = await ingressRoute.route(message, lease, requester);
|
|
124
|
+
if (handled) {
|
|
125
|
+
result = Object.freeze({ matched: true, command: 'ai', owner: requester });
|
|
126
|
+
}
|
|
116
127
|
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}));
|
|
128
|
+
}, 'inbound');
|
|
129
|
+
}
|
|
130
|
+
if (result.matched) {
|
|
131
|
+
logger.debug(formatCompact({
|
|
132
|
+
op: 'dispatched',
|
|
133
|
+
conv: formatConversationLog(conversation),
|
|
134
|
+
command: result.command,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
127
137
|
this.#emitMessage({
|
|
128
138
|
direction: 'inbound',
|
|
129
139
|
conversation,
|
|
@@ -172,12 +182,42 @@ export class ImRuntime {
|
|
|
172
182
|
return Object.freeze([]);
|
|
173
183
|
}
|
|
174
184
|
}
|
|
175
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Console `GET /api/stats` 同源计数:非 root 插件节点 + AdapterIndex endpoints。
|
|
187
|
+
* 供命令 / 状态卡等在 Plugin Runtime 下读取(legacy `root.adapters` / `root.children` 已不存在)。
|
|
188
|
+
*/
|
|
189
|
+
inventory() {
|
|
190
|
+
try {
|
|
191
|
+
const lease = this.#acquire();
|
|
192
|
+
try {
|
|
193
|
+
const plugins = [...lease.value.tree.values()]
|
|
194
|
+
.filter((node) => node.parent !== undefined).length;
|
|
195
|
+
const endpoints = requireAdapters(lease.value).describe();
|
|
196
|
+
return Object.freeze({
|
|
197
|
+
plugins,
|
|
198
|
+
endpoints: Object.freeze({
|
|
199
|
+
total: endpoints.length,
|
|
200
|
+
online: endpoints.filter((endpoint) => endpoint.status === 'online').length,
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
lease.release();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return Object.freeze({
|
|
210
|
+
plugins: 0,
|
|
211
|
+
endpoints: Object.freeze({ total: 0, online: 0 }),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
getEndpoint(adapter, endpointKey) {
|
|
176
216
|
try {
|
|
177
217
|
const lease = this.#acquire();
|
|
178
218
|
try {
|
|
179
219
|
const index = requireAdapters(lease.value);
|
|
180
|
-
const id = index.resolve(adapter,
|
|
220
|
+
const id = index.resolve(adapter, endpointKey);
|
|
181
221
|
if (!id)
|
|
182
222
|
return null;
|
|
183
223
|
const row = index.describe().find((item) => item.id === id);
|
|
@@ -206,7 +246,7 @@ export class ImRuntime {
|
|
|
206
246
|
const lease = this.#acquire();
|
|
207
247
|
try {
|
|
208
248
|
const index = requireAdapters(lease.value);
|
|
209
|
-
const resolved = index.resolve(input.adapter, input.
|
|
249
|
+
const resolved = index.resolve(input.adapter, input.endpointKey);
|
|
210
250
|
if (!resolved)
|
|
211
251
|
throw new Error('endpoint not found');
|
|
212
252
|
const requester = index.owner(resolved);
|
|
@@ -228,34 +268,34 @@ export class ImRuntime {
|
|
|
228
268
|
}
|
|
229
269
|
/** Activity-feedback: add a message reaction when the live Endpoint supports it. */
|
|
230
270
|
async addEndpointReaction(input) {
|
|
231
|
-
const control = this.#liveEndpointControl(input.adapter, input.
|
|
271
|
+
const control = this.#liveEndpointControl(input.adapter, input.endpointKey);
|
|
232
272
|
return control?.addReaction?.(legacyMessageTarget(input.message, input.messageId), input.emoji, {
|
|
233
273
|
sceneType: input.sceneType,
|
|
234
274
|
channelId: input.channelId,
|
|
235
275
|
}) ?? null;
|
|
236
276
|
}
|
|
237
277
|
async removeEndpointReaction(input) {
|
|
238
|
-
await this.#liveEndpointControl(input.adapter, input.
|
|
278
|
+
await this.#liveEndpointControl(input.adapter, input.endpointKey)
|
|
239
279
|
?.removeReaction?.(legacyMessageTarget(input.message, input.messageId), input.reactionId);
|
|
240
280
|
}
|
|
241
281
|
/** Activity-feedback autoRemove: recall a previously sent status message. */
|
|
242
282
|
async recallEndpointMessage(input) {
|
|
243
|
-
await this.#liveEndpointControl(input.adapter, input.
|
|
283
|
+
await this.#liveEndpointControl(input.adapter, input.endpointKey)
|
|
244
284
|
?.recall?.(legacyMessageTarget(input.message, input.messageId));
|
|
245
285
|
}
|
|
246
286
|
async editEndpointMessage(input) {
|
|
247
|
-
return this.#liveEndpointControl(input.adapter, input.
|
|
287
|
+
return this.#liveEndpointControl(input.adapter, input.endpointKey)
|
|
248
288
|
?.edit?.(legacyMessageTarget(input.message, input.messageId), input.content) ?? null;
|
|
249
289
|
}
|
|
250
290
|
async setEndpointTyping(input) {
|
|
251
|
-
await this.#liveEndpointControl(input.adapter, input.
|
|
291
|
+
await this.#liveEndpointControl(input.adapter, input.endpointKey)
|
|
252
292
|
?.typing?.(input.conversation, input.active);
|
|
253
293
|
}
|
|
254
|
-
#liveEndpoint(adapter,
|
|
294
|
+
#liveEndpoint(adapter, endpointKey) {
|
|
255
295
|
try {
|
|
256
296
|
const lease = this.#acquire();
|
|
257
297
|
try {
|
|
258
|
-
const endpoint = requireAdapters(lease.value).instance(adapter,
|
|
298
|
+
const endpoint = requireAdapters(lease.value).instance(adapter, endpointKey);
|
|
259
299
|
return endpoint ?? null;
|
|
260
300
|
}
|
|
261
301
|
finally {
|
|
@@ -266,24 +306,17 @@ export class ImRuntime {
|
|
|
266
306
|
return null;
|
|
267
307
|
}
|
|
268
308
|
}
|
|
269
|
-
#liveEndpointControl(adapter,
|
|
270
|
-
const endpoint = this.#liveEndpoint(adapter,
|
|
309
|
+
#liveEndpointControl(adapter, endpointKey) {
|
|
310
|
+
const endpoint = this.#liveEndpoint(adapter, endpointKey);
|
|
271
311
|
return resolveEndpointControl(endpoint) ?? null;
|
|
272
312
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Console endpoint 社交/群管 RPC:解析 live Endpoint 实例(无则 null)。
|
|
275
|
-
* @deprecated Host callers should use `getEndpointManagement()`.
|
|
276
|
-
*/
|
|
277
|
-
getLiveEndpoint(adapter, endpointId) {
|
|
278
|
-
return this.#liveEndpoint(adapter, endpointId);
|
|
279
|
-
}
|
|
280
313
|
/**
|
|
281
314
|
* Narrow Host seam for Console social/group management. An empty object means
|
|
282
315
|
* the Endpoint exists but implements no management operations; null means it
|
|
283
316
|
* cannot be resolved.
|
|
284
317
|
*/
|
|
285
|
-
getEndpointManagement(adapter,
|
|
286
|
-
const endpoint = this.#liveEndpoint(adapter,
|
|
318
|
+
getEndpointManagement(adapter, endpointKey) {
|
|
319
|
+
const endpoint = this.#liveEndpoint(adapter, endpointKey);
|
|
287
320
|
if (!endpoint)
|
|
288
321
|
return null;
|
|
289
322
|
return resolveEndpointManagement(endpoint) ?? Object.freeze({});
|
|
@@ -292,7 +325,7 @@ export class ImRuntime {
|
|
|
292
325
|
const adapter = request.conversation.endpoint.id;
|
|
293
326
|
let initialPayload;
|
|
294
327
|
try {
|
|
295
|
-
const rendered = await this.#renderer.render(request.content, request.requester, snapshot);
|
|
328
|
+
const rendered = await this.#renderer.render(request.content, request.requester, snapshot, request.conversation, request.incoming);
|
|
296
329
|
initialPayload = await prepareOutboundPayload(rendered, request.conversation, snapshot);
|
|
297
330
|
}
|
|
298
331
|
catch {
|
|
@@ -371,6 +404,14 @@ export class ImRuntime {
|
|
|
371
404
|
: undefined;
|
|
372
405
|
}
|
|
373
406
|
}
|
|
407
|
+
function resolveIngressRoute(snapshot) {
|
|
408
|
+
const candidate = snapshot.resources.get(snapshot.root)?.get(ingressRouteToken.id);
|
|
409
|
+
return candidate
|
|
410
|
+
&& typeof candidate === 'object'
|
|
411
|
+
&& typeof candidate.route === 'function'
|
|
412
|
+
? candidate
|
|
413
|
+
: undefined;
|
|
414
|
+
}
|
|
374
415
|
function requireAdapters(snapshot) {
|
|
375
416
|
const projection = snapshot.projections.get(adapterFeatureId);
|
|
376
417
|
if (!isAdapterIndex(projection)) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './contracts.js';
|
|
2
2
|
export * from './im-runtime.js';
|
|
3
3
|
export * from './interactive.js';
|
|
4
|
+
export * from './message-bus.js';
|
|
4
5
|
export * from './message-dispatcher.js';
|
|
5
6
|
export * from './outbound-renderer.js';
|
|
6
7
|
export * from './outbound-segments.js';
|
|
8
|
+
export * from './service-tokens.js';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './contracts.js';
|
|
2
2
|
export * from './im-runtime.js';
|
|
3
3
|
export * from './interactive.js';
|
|
4
|
+
export * from './message-bus.js';
|
|
4
5
|
export * from './message-dispatcher.js';
|
|
5
6
|
export * from './outbound-renderer.js';
|
|
6
7
|
export * from './outbound-segments.js';
|
|
8
|
+
export * from './service-tokens.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageBus — 独立于 Plugin 的 IM 生命周期事件总线。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖 Plugin.Lifecycle 中关键 IM 事件,供 Scope+Token 路径消费。
|
|
5
|
+
* 遗留 Plugin.emit 通过短暂双写桥接(迁移窗口)。
|
|
6
|
+
*/
|
|
7
|
+
import type { DeliveryReceipt } from '@zhin.js/im-contract';
|
|
8
|
+
import type { Message, OutboundEnvelope, SendContent } from './contracts.js';
|
|
9
|
+
export interface EndpointEvent {
|
|
10
|
+
readonly adapter: string;
|
|
11
|
+
readonly endpointKey: string;
|
|
12
|
+
readonly error?: Error;
|
|
13
|
+
}
|
|
14
|
+
export interface MessageBusEventMap {
|
|
15
|
+
'message.receive': [message: Message];
|
|
16
|
+
'before.sendMessage': [envelope: OutboundEnvelope];
|
|
17
|
+
'message.send': [payload: {
|
|
18
|
+
adapter: string;
|
|
19
|
+
endpointKey: string;
|
|
20
|
+
content: SendContent;
|
|
21
|
+
receipt: DeliveryReceipt;
|
|
22
|
+
}];
|
|
23
|
+
'endpoint.connect': [event: EndpointEvent];
|
|
24
|
+
'endpoint.disconnect': [event: EndpointEvent];
|
|
25
|
+
'endpoint.error': [event: EndpointEvent];
|
|
26
|
+
}
|
|
27
|
+
export type MessageBusEventName = keyof MessageBusEventMap;
|
|
28
|
+
export declare class MessageBus {
|
|
29
|
+
private readonly emitter;
|
|
30
|
+
constructor();
|
|
31
|
+
on<E extends MessageBusEventName>(event: E, listener: (...args: MessageBusEventMap[E]) => void): () => void;
|
|
32
|
+
once<E extends MessageBusEventName>(event: E, listener: (...args: MessageBusEventMap[E]) => void): () => void;
|
|
33
|
+
emit<E extends MessageBusEventName>(event: E, ...args: MessageBusEventMap[E]): void;
|
|
34
|
+
off<E extends MessageBusEventName>(event: E, listener: (...args: MessageBusEventMap[E]) => void): void;
|
|
35
|
+
listenerCount(event: MessageBusEventName): number;
|
|
36
|
+
removeAllListeners(event?: MessageBusEventName): void;
|
|
37
|
+
}
|
|
38
|
+
export declare const messageBusToken: import("@zhin.js/plugin-runtime").Token<MessageBus>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageBus — 独立于 Plugin 的 IM 生命周期事件总线。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖 Plugin.Lifecycle 中关键 IM 事件,供 Scope+Token 路径消费。
|
|
5
|
+
* 遗留 Plugin.emit 通过短暂双写桥接(迁移窗口)。
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from 'node:events';
|
|
8
|
+
import { createToken } from '@zhin.js/plugin-runtime';
|
|
9
|
+
export class MessageBus {
|
|
10
|
+
emitter = new EventEmitter();
|
|
11
|
+
constructor() {
|
|
12
|
+
this.emitter.setMaxListeners(50);
|
|
13
|
+
}
|
|
14
|
+
on(event, listener) {
|
|
15
|
+
this.emitter.on(event, listener);
|
|
16
|
+
return () => this.emitter.off(event, listener);
|
|
17
|
+
}
|
|
18
|
+
once(event, listener) {
|
|
19
|
+
this.emitter.once(event, listener);
|
|
20
|
+
return () => this.emitter.off(event, listener);
|
|
21
|
+
}
|
|
22
|
+
emit(event, ...args) {
|
|
23
|
+
this.emitter.emit(event, ...args);
|
|
24
|
+
}
|
|
25
|
+
off(event, listener) {
|
|
26
|
+
this.emitter.off(event, listener);
|
|
27
|
+
}
|
|
28
|
+
listenerCount(event) {
|
|
29
|
+
return this.emitter.listenerCount(event);
|
|
30
|
+
}
|
|
31
|
+
removeAllListeners(event) {
|
|
32
|
+
if (event) {
|
|
33
|
+
this.emitter.removeAllListeners(event);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.emitter.removeAllListeners();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export const messageBusToken = createToken('zhin.im.message-bus');
|