@zhin.js/core 1.5.2 → 1.5.5
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
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ export default defineCommand({
|
|
|
50
50
|
})
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
>
|
|
53
|
+
> **已移除**:`usePlugin()` / `getPlugin()` / `bootstrapNode`(`zhin.js/node`)调用即 throw;唯一入口为 `definePlugin` + `zhin runtime start`。`MessageCommand` / `Plugin.addCommand` 仍 deprecated。见 [public-api-surface](../../docs/contributing/public-api-surface.md)。
|
|
54
54
|
|
|
55
55
|
本包仍提供 IM 运行时契约(Message / Adapter / Endpoint);创作面在 Feature 包与 `@zhin.js/plugin-runtime`。
|
|
56
56
|
|
|
@@ -209,7 +209,7 @@ Core **不包含** ZhinAgent 实现。IM 侧的 AI 对话、工具收集、执
|
|
|
209
209
|
|
|
210
210
|
```typescript
|
|
211
211
|
// 插件系统
|
|
212
|
-
export { Plugin, usePlugin, getPlugin, definePlugin } from './plugin.js'
|
|
212
|
+
export { Plugin, usePlugin, getPlugin, definePlugin } from './plugin.js' // usePlugin/getPlugin 为 throwing stub
|
|
213
213
|
|
|
214
214
|
// Feature 体系(Cron / Scheduler 来自 @zhin.js/kernel)
|
|
215
215
|
export { Feature, Cron, Scheduler } from '@zhin.js/kernel'
|
package/lib/adapter.d.ts
CHANGED
|
@@ -86,9 +86,15 @@ export declare abstract class Adapter<R extends Endpoint = Endpoint, const Caps
|
|
|
86
86
|
*/
|
|
87
87
|
protected renderSendMessage(options: SendOptions): Promise<SendOptions>;
|
|
88
88
|
sendMessage(options: SendOptions): Promise<string>;
|
|
89
|
+
/**
|
|
90
|
+
* `call.recallMessage` 事件链(prompt.ts 超时撤回等)的统一出口:
|
|
91
|
+
* 经 canonical `EndpointControl` 端口撤回,classic 端点的 `$` 方法由
|
|
92
|
+
* `resolveEndpointControl` 迁移桥适配。
|
|
93
|
+
*/
|
|
94
|
+
private recallEndpointMessage;
|
|
89
95
|
/**
|
|
90
96
|
* 编辑已发送的消息。
|
|
91
|
-
* - 如果 Endpoint
|
|
97
|
+
* - 如果 Endpoint 的 control 端口实现了 edit,调用平台编辑 API
|
|
92
98
|
* - 否则 fallback 到发送新消息
|
|
93
99
|
* @returns 消息 ID(编辑时返回原 ID,fallback 时返回新消息 ID)
|
|
94
100
|
*/
|
package/lib/adapter.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { assertOutbound, DEFAULT_ENDPOINT_CAPABILITIES, hasInbound, } from "./endpoint-capabilities.js";
|
|
2
|
+
import { resolveEndpointControl } from "@zhin.js/adapter";
|
|
2
3
|
import { connectEndpointInstance, disconnectEndpointInstance } from "./built/connect-endpoint-instance.js";
|
|
3
4
|
import { EventEmitter } from "node:events";
|
|
4
5
|
import { getOutboundReplyStore } from "./built/dispatcher.js";
|
|
5
6
|
import { DEFAULT_OUTBOUND_RICH_SEGMENT_POLICY, resolveRichSegments, } from "./built/rich-segments/index.js";
|
|
6
|
-
import { DEFAULT_INTERACTIVE_POLICY, collectKeyboardFallbackMaps, keyboardFallbackStore,
|
|
7
|
+
import { DEFAULT_INTERACTIVE_POLICY, collectKeyboardFallbackMaps, keyboardFallbackStore, resolveKeyboardSegments, } from "./built/interactive-segments/index.js";
|
|
7
8
|
import { DEFAULT_AI_OUTBOUND_CAPABILITIES, } from "./built/ai-outbound/index.js";
|
|
8
9
|
import { createRichSegmentRenderContext } from "./built/rich-segments/capabilities.js";
|
|
9
10
|
import { collectOutboundMediaKinds } from "./built/outbound-media-utils.js";
|
|
@@ -63,14 +64,7 @@ export class Adapter extends EventEmitter {
|
|
|
63
64
|
this.plugin = plugin;
|
|
64
65
|
this.name = name;
|
|
65
66
|
this.config = config;
|
|
66
|
-
this.recallMessageHandler =
|
|
67
|
-
const endpoint = this.endpoints.get(endpoint_id);
|
|
68
|
-
if (!endpoint)
|
|
69
|
-
throw new Error(`Endpoint ${endpoint_id} not found`);
|
|
70
|
-
assertOutbound(endpoint);
|
|
71
|
-
this.logger.debug(formatCompact({ recall: id, endpoint: endpoint_id }));
|
|
72
|
-
await endpoint.$recallMessage(id);
|
|
73
|
-
};
|
|
67
|
+
this.recallMessageHandler = (endpoint_id, id) => this.recallEndpointMessage(endpoint_id, id);
|
|
74
68
|
this.on('call.recallMessage', this.recallMessageHandler);
|
|
75
69
|
this.inboundPipeline = new InboundMessagePipeline({
|
|
76
70
|
getPlugin: () => this.plugin,
|
|
@@ -100,7 +94,7 @@ export class Adapter extends EventEmitter {
|
|
|
100
94
|
const message = args[0];
|
|
101
95
|
// 背压控制:同步返回 false 表示丢弃
|
|
102
96
|
if (this.inboundPipeline.shouldDropDueToBackpressure()) {
|
|
103
|
-
this.logger.warn(formatCompact({
|
|
97
|
+
this.logger.warn(formatCompact({ op: 'backpressure_drop', limit: this.maxConcurrentMessages }));
|
|
104
98
|
return false;
|
|
105
99
|
}
|
|
106
100
|
// 同步计数,保证背压判断的准确性
|
|
@@ -180,7 +174,7 @@ export class Adapter extends EventEmitter {
|
|
|
180
174
|
}
|
|
181
175
|
options = {
|
|
182
176
|
...options,
|
|
183
|
-
content:
|
|
177
|
+
content: resolveKeyboardSegments(options.content, this.getInteractivePolicy()),
|
|
184
178
|
};
|
|
185
179
|
const mediaKinds = collectOutboundMediaKinds(options.content);
|
|
186
180
|
if (mediaKinds.length > 0) {
|
|
@@ -222,9 +216,26 @@ export class Adapter extends EventEmitter {
|
|
|
222
216
|
});
|
|
223
217
|
return messageId;
|
|
224
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* `call.recallMessage` 事件链(prompt.ts 超时撤回等)的统一出口:
|
|
221
|
+
* 经 canonical `EndpointControl` 端口撤回,classic 端点的 `$` 方法由
|
|
222
|
+
* `resolveEndpointControl` 迁移桥适配。
|
|
223
|
+
*/
|
|
224
|
+
async recallEndpointMessage(endpointKey, messageId) {
|
|
225
|
+
const endpoint = this.endpoints.get(endpointKey);
|
|
226
|
+
if (!endpoint)
|
|
227
|
+
throw new Error(`Endpoint ${endpointKey} not found`);
|
|
228
|
+
assertOutbound(endpoint);
|
|
229
|
+
const control = resolveEndpointControl(endpoint);
|
|
230
|
+
if (!control?.recall) {
|
|
231
|
+
throw new Error(`Endpoint ${endpointKey} does not support recall`);
|
|
232
|
+
}
|
|
233
|
+
this.logger.debug(formatCompact({ op: 'recall_message', msgId: messageId, endpoint: endpointKey }));
|
|
234
|
+
await control.recall(messageId);
|
|
235
|
+
}
|
|
225
236
|
/**
|
|
226
237
|
* 编辑已发送的消息。
|
|
227
|
-
* - 如果 Endpoint
|
|
238
|
+
* - 如果 Endpoint 的 control 端口实现了 edit,调用平台编辑 API
|
|
228
239
|
* - 否则 fallback 到发送新消息
|
|
229
240
|
* @returns 消息 ID(编辑时返回原 ID,fallback 时返回新消息 ID)
|
|
230
241
|
*/
|
|
@@ -233,8 +244,8 @@ export class Adapter extends EventEmitter {
|
|
|
233
244
|
if (!endpoint)
|
|
234
245
|
throw new Error(`Endpoint ${options.endpoint} not found`);
|
|
235
246
|
assertOutbound(endpoint);
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
247
|
+
const control = resolveEndpointControl(endpoint);
|
|
248
|
+
if (control?.edit) {
|
|
238
249
|
const rendered = await this.renderSendMessage({
|
|
239
250
|
context: options.context,
|
|
240
251
|
endpoint: options.endpoint,
|
|
@@ -242,7 +253,7 @@ export class Adapter extends EventEmitter {
|
|
|
242
253
|
type: options.type,
|
|
243
254
|
content: options.content,
|
|
244
255
|
});
|
|
245
|
-
await
|
|
256
|
+
await control.edit(options.messageId, rendered.content);
|
|
246
257
|
this.logger.debug(formatCompact({
|
|
247
258
|
edit: `${options.type}(${options.id})`,
|
|
248
259
|
endpoint: options.endpoint,
|
|
@@ -253,7 +264,7 @@ export class Adapter extends EventEmitter {
|
|
|
253
264
|
this.logger.debug(formatCompact({
|
|
254
265
|
editFallback: 'sendMessage',
|
|
255
266
|
endpoint: options.endpoint,
|
|
256
|
-
reason: '
|
|
267
|
+
reason: 'control.edit not implemented',
|
|
257
268
|
}));
|
|
258
269
|
return this.sendMessage({
|
|
259
270
|
context: options.context,
|
|
@@ -314,14 +325,7 @@ export class Adapter extends EventEmitter {
|
|
|
314
325
|
}
|
|
315
326
|
// 移除所有事件监听器
|
|
316
327
|
this.removeAllListeners();
|
|
317
|
-
this.recallMessageHandler =
|
|
318
|
-
const endpoint = this.endpoints.get(endpoint_id);
|
|
319
|
-
if (!endpoint)
|
|
320
|
-
throw new Error(`Endpoint ${endpoint_id} not found`);
|
|
321
|
-
assertOutbound(endpoint);
|
|
322
|
-
this.logger.debug(formatCompact({ recall: id, endpoint: endpoint_id }));
|
|
323
|
-
await endpoint.$recallMessage(id);
|
|
324
|
-
};
|
|
328
|
+
this.recallMessageHandler = (endpoint_id, id) => this.recallEndpointMessage(endpoint_id, id);
|
|
325
329
|
this.on('call.recallMessage', this.recallMessageHandler);
|
|
326
330
|
this.logger.debug(formatCompact({ stop: this.name }));
|
|
327
331
|
if (errors.length) {
|
package/lib/agent-prompt.d.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Implementations live in adapter plugins; resolution runs in @zhin.js/agent.
|
|
5
5
|
*/
|
|
6
|
-
import type { Message } from './message.js';
|
|
7
6
|
export type AgentPromptSlot = 'orchestrator' | 'deferred_worker';
|
|
8
7
|
/** Minimal tool shape for deferred catalog selection (no @zhin.js/ai import). */
|
|
9
8
|
export interface DeferredToolCatalogItem {
|
|
@@ -12,8 +11,8 @@ export interface DeferredToolCatalogItem {
|
|
|
12
11
|
}
|
|
13
12
|
export interface AgentPromptBuildContext {
|
|
14
13
|
slot: AgentPromptSlot;
|
|
15
|
-
/**
|
|
16
|
-
|
|
14
|
+
/** Authenticated IM platform projected by the ingress adapter. */
|
|
15
|
+
platform: string;
|
|
17
16
|
/** Truncated user message for intent hints (~500 chars). */
|
|
18
17
|
userMessagePreview?: string;
|
|
19
18
|
deferred?: {
|
package/lib/agent-prompt.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Segment } from '../segment-contract/types.js';
|
|
2
2
|
import type { ZhinAiOutboundPayload } from './types.js';
|
|
3
|
-
/** 解析单条 outbound segment(canonical
|
|
3
|
+
/** 解析单条 outbound segment(canonical type/data 形态)。 */
|
|
4
4
|
export declare function parseOutboundSegment(raw: unknown): Segment | null;
|
|
5
5
|
/** 去掉嵌入 JSON 候选末尾的 markdown 围栏(模型常见 ```json … ``` 误输出)。 */
|
|
6
6
|
export declare function trimTrailingMarkdownFence(raw: string): string;
|
|
@@ -2,15 +2,11 @@ import { isCanonicalSegment } from '../segment-contract/assert.js';
|
|
|
2
2
|
function isPlainObject(value) {
|
|
3
3
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
|
-
/** 解析单条 outbound segment(canonical
|
|
5
|
+
/** 解析单条 outbound segment(canonical type/data 形态)。 */
|
|
6
6
|
export function parseOutboundSegment(raw) {
|
|
7
7
|
if (!isPlainObject(raw))
|
|
8
8
|
return null;
|
|
9
|
-
const type = typeof raw.type === 'string' && raw.type.trim()
|
|
10
|
-
? raw.type.trim()
|
|
11
|
-
: typeof raw.kind === 'string' && raw.kind.trim()
|
|
12
|
-
? raw.kind.trim()
|
|
13
|
-
: undefined;
|
|
9
|
+
const type = typeof raw.type === 'string' && raw.type.trim() ? raw.type.trim() : undefined;
|
|
14
10
|
if (!type)
|
|
15
11
|
return null;
|
|
16
12
|
const data = isPlainObject(raw.data) ? raw.data : {};
|
|
@@ -13,8 +13,8 @@ export function rewritePlainTextMentions(plainText, mentionResolver) {
|
|
|
13
13
|
const token = match[1];
|
|
14
14
|
if (!token)
|
|
15
15
|
continue;
|
|
16
|
-
const
|
|
17
|
-
if (!
|
|
16
|
+
const endpointKey = mentionResolver(token);
|
|
17
|
+
if (!endpointKey)
|
|
18
18
|
continue;
|
|
19
19
|
const ref = token;
|
|
20
20
|
if (!mentions.includes(ref))
|
|
@@ -23,8 +23,8 @@ export function rewritePlainTextMentions(plainText, mentionResolver) {
|
|
|
23
23
|
if (mentions.length === 0)
|
|
24
24
|
return null;
|
|
25
25
|
rewritten = trimmed.replace(PLAIN_MENTION_PATTERN, (full, token) => {
|
|
26
|
-
const
|
|
27
|
-
return
|
|
26
|
+
const endpointKey = mentionResolver(token);
|
|
27
|
+
return endpointKey ? '' : full;
|
|
28
28
|
});
|
|
29
29
|
rewritten = rewritten
|
|
30
30
|
.replace(/[ \t]+\n/g, '\n')
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { segmentsForImDelivery } from '../segment-contract/delivery.js';
|
|
2
|
-
function buildMentionSegments(
|
|
2
|
+
function buildMentionSegments(endpointKeys, text, ctx) {
|
|
3
3
|
const segments = [];
|
|
4
|
-
for (const
|
|
5
|
-
const target = ctx.atIdResolver?.(
|
|
4
|
+
for (const endpointKey of endpointKeys) {
|
|
5
|
+
const target = ctx.atIdResolver?.(endpointKey) ?? endpointKey;
|
|
6
6
|
segments.push({ type: 'mention', data: { target } });
|
|
7
7
|
}
|
|
8
8
|
const body = text.trim();
|
|
@@ -19,16 +19,16 @@ function resolveMentionEndpointIds(mentions, ctx) {
|
|
|
19
19
|
if (!ctx.mentionResolver) {
|
|
20
20
|
return { ok: false, error: 'mentionResolver 未配置' };
|
|
21
21
|
}
|
|
22
|
-
const
|
|
22
|
+
const endpointKeys = [];
|
|
23
23
|
for (const ref of mentions) {
|
|
24
24
|
const resolved = ctx.mentionResolver(ref);
|
|
25
25
|
if (!resolved) {
|
|
26
26
|
return { ok: false, error: `未知 peer "${ref}"` };
|
|
27
27
|
}
|
|
28
|
-
if (!
|
|
29
|
-
|
|
28
|
+
if (!endpointKeys.includes(resolved))
|
|
29
|
+
endpointKeys.push(resolved);
|
|
30
30
|
}
|
|
31
|
-
return { ok: true,
|
|
31
|
+
return { ok: true, endpointKeys };
|
|
32
32
|
}
|
|
33
33
|
/** 将 ZhinAiOutboundPayload 转为 MessageElement[](canonical segments + extensions)。 */
|
|
34
34
|
export async function resolveAiOutboundToMessageElements(payload, ctx) {
|
|
@@ -40,7 +40,7 @@ export async function resolveAiOutboundToMessageElements(payload, ctx) {
|
|
|
40
40
|
const text = payload.text?.trim() ?? '';
|
|
41
41
|
if (!text)
|
|
42
42
|
return null;
|
|
43
|
-
segments.push(...buildMentionSegments(resolved.
|
|
43
|
+
segments.push(...buildMentionSegments(resolved.endpointKeys, text, ctx));
|
|
44
44
|
}
|
|
45
45
|
else if (payload.text?.trim()) {
|
|
46
46
|
segments.push({ type: 'text', data: { text: payload.text.trim() } });
|
|
@@ -35,14 +35,14 @@ export interface AiOutboundMentionResolver {
|
|
|
35
35
|
(ref: string): string | undefined;
|
|
36
36
|
}
|
|
37
37
|
export interface AiOutboundAtIdResolver {
|
|
38
|
-
(
|
|
38
|
+
(endpointKey: string): string;
|
|
39
39
|
}
|
|
40
40
|
/** 解析 AI 出站 JSON 时的上下文。 */
|
|
41
41
|
export interface AiOutboundParseContext {
|
|
42
42
|
message: Message;
|
|
43
|
-
/** 将 peer ref(role/endpoint/primary)解析为
|
|
43
|
+
/** 将 peer ref(role/endpoint/primary)解析为 endpointKey */
|
|
44
44
|
mentionResolver?: AiOutboundMentionResolver;
|
|
45
|
-
/** 将
|
|
45
|
+
/** 将 endpointKey 解析为 platform @ id */
|
|
46
46
|
atIdResolver?: AiOutboundAtIdResolver;
|
|
47
47
|
/** Adapter 声明的 extensions(运行时从 Adapter 静态属性注入) */
|
|
48
48
|
extensions?: readonly AiOutboundExtensionDefinition[];
|
|
@@ -76,6 +76,11 @@ export declare function isAtEndpoint<T extends object>(message: Message<T>, endp
|
|
|
76
76
|
* 使用 segment.toString 将 MessageElement 转为 XML 格式
|
|
77
77
|
*/
|
|
78
78
|
export declare function extractTextContent<T extends object>(message: Message<T>): string;
|
|
79
|
+
/**
|
|
80
|
+
* 为群/频道消息构建发送者前缀(与 sender-extra.ts buildSenderPrefix 格式对齐)
|
|
81
|
+
* 私聊消息返回 null
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildSenderPrefixForMessage<T extends object>(message: Message<T>, roles: SenderRole[]): string | null;
|
|
79
84
|
/**
|
|
80
85
|
* 解析 AI 回复中的富媒体内容
|
|
81
86
|
* 将字符串中的 XML-like 标签解析为 MessageElement
|
package/lib/built/ai-trigger.js
CHANGED
|
@@ -39,23 +39,23 @@ function normalizeAtIds(endpointAtIds) {
|
|
|
39
39
|
function segmentAtUserId(seg) {
|
|
40
40
|
return readMentionSegmentTarget(seg);
|
|
41
41
|
}
|
|
42
|
-
function isAtSegmentForEndpoint(seg,
|
|
42
|
+
function isAtSegmentForEndpoint(seg, endpointKeys) {
|
|
43
43
|
if (seg.type !== 'at' && seg.type !== 'mention')
|
|
44
44
|
return false;
|
|
45
45
|
const uid = segmentAtUserId(seg);
|
|
46
|
-
return uid !== '' &&
|
|
46
|
+
return uid !== '' && endpointKeys.includes(uid);
|
|
47
47
|
}
|
|
48
|
-
function textMentionsEndpoint(text,
|
|
49
|
-
for (const id of
|
|
48
|
+
function textMentionsEndpoint(text, endpointKeys) {
|
|
49
|
+
for (const id of endpointKeys) {
|
|
50
50
|
const re = new RegExp(`@${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|$|[\\u200b\\uFEFF])`);
|
|
51
51
|
if (re.test(text))
|
|
52
52
|
return true;
|
|
53
53
|
}
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
|
-
function stripTextAtEndpoint(text,
|
|
56
|
+
function stripTextAtEndpoint(text, endpointKeys) {
|
|
57
57
|
let result = text;
|
|
58
|
-
for (const id of
|
|
58
|
+
for (const id of endpointKeys) {
|
|
59
59
|
const re = new RegExp(`@${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|[\\u200b\\uFEFF])?`, 'g');
|
|
60
60
|
result = result.replace(re, '');
|
|
61
61
|
}
|
|
@@ -76,12 +76,12 @@ export function collectEndpointAtIds(message, extraIds) {
|
|
|
76
76
|
* 检查消息是否 @ 了 Endpoint
|
|
77
77
|
*/
|
|
78
78
|
export function isAtEndpoint(message, endpointAtIds) {
|
|
79
|
-
const
|
|
80
|
-
if (message.$content.some((seg) => isAtSegmentForEndpoint(seg,
|
|
79
|
+
const endpointKeys = normalizeAtIds(endpointAtIds?.length ? endpointAtIds : collectEndpointAtIds(message));
|
|
80
|
+
if (message.$content.some((seg) => isAtSegmentForEndpoint(seg, endpointKeys))) {
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
83
|
for (const seg of message.$content) {
|
|
84
|
-
if (seg.type === 'text' && seg.data?.text && textMentionsEndpoint(seg.data.text,
|
|
84
|
+
if (seg.type === 'text' && seg.data?.text && textMentionsEndpoint(seg.data.text, endpointKeys)) {
|
|
85
85
|
return true;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -94,6 +94,22 @@ export function isAtEndpoint(message, endpointAtIds) {
|
|
|
94
94
|
export function extractTextContent(message) {
|
|
95
95
|
return segment.toString(message.$content);
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* 为群/频道消息构建发送者前缀(与 sender-extra.ts buildSenderPrefix 格式对齐)
|
|
99
|
+
* 私聊消息返回 null
|
|
100
|
+
*/
|
|
101
|
+
export function buildSenderPrefixForMessage(message, roles) {
|
|
102
|
+
const scope = message.$channel?.type || 'private';
|
|
103
|
+
if (scope !== 'group' && scope !== 'channel')
|
|
104
|
+
return null;
|
|
105
|
+
const rawId = String(message.$sender?.id ?? 'unknown');
|
|
106
|
+
const id = rawId.trim().replace(/[\]\s]+/g, '_').slice(0, 64) || 'unknown';
|
|
107
|
+
const rawName = message.$sender?.name;
|
|
108
|
+
const name = (rawName?.trim() || 'unknown').replace(/[\]\s]+/g, '_').slice(0, 64);
|
|
109
|
+
const nonUserRoles = roles.filter((r) => r !== 'user');
|
|
110
|
+
const roleStr = nonUserRoles.join(',') || 'user';
|
|
111
|
+
return `[sender:id=${id} name=${name} roles=${roleStr}]`;
|
|
112
|
+
}
|
|
97
113
|
/**
|
|
98
114
|
* 解析 AI 回复中的富媒体内容
|
|
99
115
|
* 将字符串中的 XML-like 标签解析为 MessageElement
|
|
@@ -117,13 +133,13 @@ export function parseRichMediaContent(content) {
|
|
|
117
133
|
* 移除 @ Endpoint 的部分
|
|
118
134
|
*/
|
|
119
135
|
export function removeAtEndpoint(message, endpointAtIds) {
|
|
120
|
-
const
|
|
136
|
+
const endpointKeys = normalizeAtIds(endpointAtIds?.length ? endpointAtIds : collectEndpointAtIds(message));
|
|
121
137
|
return message.$content
|
|
122
|
-
.filter((seg) => !isAtSegmentForEndpoint(seg,
|
|
138
|
+
.filter((seg) => !isAtSegmentForEndpoint(seg, endpointKeys))
|
|
123
139
|
.map((seg) => {
|
|
124
140
|
if (seg.type !== 'text' || !seg.data?.text)
|
|
125
141
|
return seg;
|
|
126
|
-
const stripped = stripTextAtEndpoint(seg.data.text,
|
|
142
|
+
const stripped = stripTextAtEndpoint(seg.data.text, endpointKeys).trim();
|
|
127
143
|
if (!stripped)
|
|
128
144
|
return null;
|
|
129
145
|
return { ...seg, data: { ...seg.data, text: stripped } };
|
|
@@ -185,6 +201,16 @@ export function shouldTriggerAI(message, config, options) {
|
|
|
185
201
|
const text = extractTextContent(message);
|
|
186
202
|
const scope = message.$channel?.type || 'private';
|
|
187
203
|
const isSharedSession = scope === 'group' || scope === 'channel';
|
|
204
|
+
/** 对群/频道触发结果附加发送者前缀(与 DB 历史的 buildSenderPrefix 格式对齐) */
|
|
205
|
+
const withSenderPrefix = (content) => {
|
|
206
|
+
if (!isSharedSession)
|
|
207
|
+
return content;
|
|
208
|
+
const { roles } = resolveSenderRoles(message, config);
|
|
209
|
+
const prefix = buildSenderPrefixForMessage(message, roles);
|
|
210
|
+
if (!prefix)
|
|
211
|
+
return content;
|
|
212
|
+
return content ? `${prefix} ${content}` : prefix;
|
|
213
|
+
};
|
|
188
214
|
// 检查忽略前缀
|
|
189
215
|
for (const prefix of fullConfig.ignorePrefixes) {
|
|
190
216
|
if (text.startsWith(prefix)) {
|
|
@@ -202,9 +228,10 @@ export function shouldTriggerAI(message, config, options) {
|
|
|
202
228
|
// 2. 检查 @ 触发(群/频道主路径;仅 @ 无正文时也触发,由 Agent 处理空输入)
|
|
203
229
|
if (fullConfig.respondToAt && isAtEndpoint(message, endpointAtIds)) {
|
|
204
230
|
const content = removeAtEndpoint(message, endpointAtIds);
|
|
231
|
+
const body = content.length ? segment.toString(content).trim() : '';
|
|
205
232
|
return {
|
|
206
233
|
triggered: true,
|
|
207
|
-
content:
|
|
234
|
+
content: withSenderPrefix(body),
|
|
208
235
|
};
|
|
209
236
|
}
|
|
210
237
|
// 3. 检查私聊触发
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { mergeAITriggerConfig, resolveSenderRoles, } from './ai-trigger.js';
|
|
2
2
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
3
3
|
const logger = getLogger('Authorization');
|
|
4
|
-
function findEndpointEntryFromConfig(config, adapter,
|
|
4
|
+
function findEndpointEntryFromConfig(config, adapter, endpointKey) {
|
|
5
5
|
// 1) top-level endpoints[] (legacy / flat format)
|
|
6
6
|
const topLevel = config.endpoints;
|
|
7
7
|
if (Array.isArray(topLevel)) {
|
|
8
|
-
const found = topLevel.find((b) => b.context === adapter && String(b.
|
|
8
|
+
const found = topLevel.find((b) => b.context === adapter && String(b.id) === endpointKey);
|
|
9
9
|
if (found)
|
|
10
10
|
return found;
|
|
11
11
|
}
|
|
@@ -15,13 +15,13 @@ function findEndpointEntryFromConfig(config, adapter, endpointId) {
|
|
|
15
15
|
return undefined;
|
|
16
16
|
const nested = adapterConfig.endpoints;
|
|
17
17
|
const entry = Array.isArray(nested)
|
|
18
|
-
? nested.find((b) => String(b.
|
|
18
|
+
? nested.find((b) => String(b.id) === endpointKey)
|
|
19
19
|
: undefined;
|
|
20
20
|
// Merge adapter-level master/trusted onto the matched endpoint entry
|
|
21
21
|
// so that resolveSenderRoles sees them in one place
|
|
22
22
|
const merged = {
|
|
23
23
|
context: adapter,
|
|
24
|
-
...(entry ?? {
|
|
24
|
+
...(entry ?? { id: endpointKey }),
|
|
25
25
|
};
|
|
26
26
|
if (adapterConfig.master != null && merged.master == null) {
|
|
27
27
|
merged.master = adapterConfig.master;
|
package/lib/built/command.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ export declare function commandLeadingToken(pattern: string): string;
|
|
|
9
9
|
export declare function compareCommandPatterns(a: string, b: string): number;
|
|
10
10
|
import { MessageCommand } from "../command.js";
|
|
11
11
|
import { Message } from "../message.js";
|
|
12
|
-
import { Plugin } from "../plugin.js";
|
|
13
12
|
import type { RegisteredAdapter, AdapterMessage } from "../types.js";
|
|
13
|
+
import type { PermissionHost } from '@zhin.js/permission';
|
|
14
14
|
/**
|
|
15
15
|
* CommandContext 扩展方法类型
|
|
16
16
|
*/
|
|
@@ -54,7 +54,7 @@ export declare class CommandFeature extends Feature<MessageCommand<RegisteredAda
|
|
|
54
54
|
/**
|
|
55
55
|
* 处理消息,依次尝试匹配命令(较长前缀优先,避免 `teach` 抢 `teach-list`)
|
|
56
56
|
*/
|
|
57
|
-
handle(message: Message<AdapterMessage<RegisteredAdapter>>,
|
|
57
|
+
handle(message: Message<AdapterMessage<RegisteredAdapter>>, host: PermissionHost | null): Promise<any>;
|
|
58
58
|
/**
|
|
59
59
|
* 生命周期: 销毁时清理所有命令
|
|
60
60
|
*/
|
package/lib/built/command.js
CHANGED
|
@@ -54,10 +54,10 @@ export class CommandFeature extends Feature {
|
|
|
54
54
|
/**
|
|
55
55
|
* 处理消息,依次尝试匹配命令(较长前缀优先,避免 `teach` 抢 `teach-list`)
|
|
56
56
|
*/
|
|
57
|
-
async handle(message,
|
|
57
|
+
async handle(message, host) {
|
|
58
58
|
const commands = [...this.items].sort((a, b) => compareCommandPatterns(a.pattern, b.pattern));
|
|
59
59
|
for (const command of commands) {
|
|
60
|
-
const result = await command.handle(message,
|
|
60
|
+
const result = await command.handle(message, host);
|
|
61
61
|
if (result)
|
|
62
62
|
return result;
|
|
63
63
|
}
|
|
@@ -18,16 +18,16 @@ import type { Tool } from '../types.js';
|
|
|
18
18
|
* Adapter 基类通过此接口声明方法签名,子类选择性覆写。
|
|
19
19
|
*/
|
|
20
20
|
export interface ISceneManagement {
|
|
21
|
-
removeMember?(
|
|
22
|
-
muteMember?(
|
|
23
|
-
setMemberNickname?(
|
|
24
|
-
setModerator?(
|
|
25
|
-
listMembers?(
|
|
26
|
-
banMember?(
|
|
27
|
-
unbanMember?(
|
|
28
|
-
renameScene?(
|
|
29
|
-
setSceneMuted?(
|
|
30
|
-
getSceneInfo?(
|
|
21
|
+
removeMember?(endpointKey: string, sceneId: string, userId: string): Promise<any>;
|
|
22
|
+
muteMember?(endpointKey: string, sceneId: string, userId: string, duration?: number): Promise<any>;
|
|
23
|
+
setMemberNickname?(endpointKey: string, sceneId: string, userId: string, nickname: string): Promise<any>;
|
|
24
|
+
setModerator?(endpointKey: string, sceneId: string, userId: string, enable?: boolean): Promise<any>;
|
|
25
|
+
listMembers?(endpointKey: string, sceneId: string): Promise<any>;
|
|
26
|
+
banMember?(endpointKey: string, sceneId: string, userId: string, reason?: string): Promise<any>;
|
|
27
|
+
unbanMember?(endpointKey: string, sceneId: string, userId: string): Promise<any>;
|
|
28
|
+
renameScene?(endpointKey: string, sceneId: string, name: string): Promise<any>;
|
|
29
|
+
setSceneMuted?(endpointKey: string, sceneId: string, enable?: boolean): Promise<any>;
|
|
30
|
+
getSceneInfo?(endpointKey: string, sceneId: string): Promise<any>;
|
|
31
31
|
}
|
|
32
32
|
export interface SceneManagementMethodSpec {
|
|
33
33
|
method: keyof ISceneManagement;
|
|
@@ -58,4 +58,4 @@ export interface CreateSceneManagementToolsOptions {
|
|
|
58
58
|
}
|
|
59
59
|
export declare function createSceneManagementTools(adapter: ISceneManagement, prefix: string, options?: CreateSceneManagementToolsOptions): Tool[];
|
|
60
60
|
export declare function createSceneManagementToolsRaw<T>(adapter: ISceneManagement, prefix: string, factory: SceneManagementToolFactory<T>): T[];
|
|
61
|
-
export declare function buildSceneMethodArgs(method: keyof ISceneManagement,
|
|
61
|
+
export declare function buildSceneMethodArgs(method: keyof ISceneManagement, endpointKey: string, sceneId: string, rest: Record<string, any>): any[];
|
|
@@ -152,7 +152,7 @@ export function createSceneManagementTools(adapter, prefix, options = {}) {
|
|
|
152
152
|
parameters: {
|
|
153
153
|
type: 'object',
|
|
154
154
|
properties: {
|
|
155
|
-
endpoint_id: { type: 'string', description: 'Endpoint ID', contextKey: '
|
|
155
|
+
endpoint_id: { type: 'string', description: 'Endpoint ID', contextKey: 'endpointKey' },
|
|
156
156
|
scene_id: { type: 'string', description: 'IM 场景 ID', contextKey: 'sceneId' },
|
|
157
157
|
...Object.fromEntries(Object.entries(spec.extraParams)),
|
|
158
158
|
},
|
|
@@ -186,18 +186,18 @@ export function createSceneManagementToolsRaw(adapter, prefix, factory) {
|
|
|
186
186
|
// ============================================================================
|
|
187
187
|
// 参数映射(method → 有序参数列表)
|
|
188
188
|
// ============================================================================
|
|
189
|
-
export function buildSceneMethodArgs(method,
|
|
189
|
+
export function buildSceneMethodArgs(method, endpointKey, sceneId, rest) {
|
|
190
190
|
switch (method) {
|
|
191
|
-
case 'removeMember': return [
|
|
192
|
-
case 'muteMember': return [
|
|
193
|
-
case 'setMemberNickname': return [
|
|
194
|
-
case 'setModerator': return [
|
|
195
|
-
case 'listMembers': return [
|
|
196
|
-
case 'banMember': return [
|
|
197
|
-
case 'unbanMember': return [
|
|
198
|
-
case 'renameScene': return [
|
|
199
|
-
case 'setSceneMuted': return [
|
|
200
|
-
case 'getSceneInfo': return [
|
|
201
|
-
default: return [
|
|
191
|
+
case 'removeMember': return [endpointKey, sceneId, rest.user_id];
|
|
192
|
+
case 'muteMember': return [endpointKey, sceneId, rest.user_id, rest.duration ?? 600];
|
|
193
|
+
case 'setMemberNickname': return [endpointKey, sceneId, rest.user_id, rest.nickname];
|
|
194
|
+
case 'setModerator': return [endpointKey, sceneId, rest.user_id, rest.enable ?? true];
|
|
195
|
+
case 'listMembers': return [endpointKey, sceneId];
|
|
196
|
+
case 'banMember': return [endpointKey, sceneId, rest.user_id, rest.reason];
|
|
197
|
+
case 'unbanMember': return [endpointKey, sceneId, rest.user_id];
|
|
198
|
+
case 'renameScene': return [endpointKey, sceneId, rest.name];
|
|
199
|
+
case 'setSceneMuted': return [endpointKey, sceneId, rest.enable ?? true];
|
|
200
|
+
case 'getSceneInfo': return [endpointKey, sceneId];
|
|
201
|
+
default: return [endpointKey, sceneId, ...Object.values(rest)];
|
|
202
202
|
}
|
|
203
203
|
}
|
package/lib/built/config.d.ts
CHANGED
package/lib/built/config.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { Message } from '../message.js';
|
|
30
30
|
import { type Context } from '../plugin.js';
|
|
31
|
+
import { type PermissionHost } from '@zhin.js/permission';
|
|
31
32
|
import type { MessageMiddleware, RegisteredAdapter, MaybePromise, SendContent, OutboundReplySource, OutboundPolishMiddleware, OutboundReplyStore } from '../types.js';
|
|
32
33
|
export declare function getOutboundReplyStore(): OutboundReplyStore | undefined;
|
|
33
34
|
/**
|
|
@@ -88,6 +89,7 @@ export interface ReplyWithPolishOptions {
|
|
|
88
89
|
}
|
|
89
90
|
export interface CreateMessageDispatcherOptions {
|
|
90
91
|
dualRoute?: Partial<DualRouteConfig>;
|
|
92
|
+
permissionHost?: PermissionHost | null;
|
|
91
93
|
}
|
|
92
94
|
export interface MessageDispatcherService {
|
|
93
95
|
dispatch(message: Message<any>): Promise<void>;
|
package/lib/built/dispatcher.js
CHANGED
|
@@ -239,7 +239,7 @@ export function createMessageDispatcher(options) {
|
|
|
239
239
|
const commandService = rootPlugin.inject('command');
|
|
240
240
|
if (!commandService)
|
|
241
241
|
return;
|
|
242
|
-
const response = await commandService.handle(message,
|
|
242
|
+
const response = await commandService.handle(message, options?.permissionHost ?? null);
|
|
243
243
|
if (response !== undefined && response !== null) {
|
|
244
244
|
await replyWithPolishInternal(message, 'command', response);
|
|
245
245
|
}
|
|
@@ -12,10 +12,10 @@ function collectRuntimeEndpoints(root) {
|
|
|
12
12
|
const adapter = root.inject(adapterName);
|
|
13
13
|
if (!(adapter instanceof Adapter))
|
|
14
14
|
continue;
|
|
15
|
-
for (const [
|
|
15
|
+
for (const [endpointKey, endpoint] of adapter.endpoints.entries()) {
|
|
16
16
|
rows.push({
|
|
17
17
|
adapter: String(adapterName),
|
|
18
|
-
name:
|
|
18
|
+
name: endpointKey,
|
|
19
19
|
online: !!endpoint.$connected,
|
|
20
20
|
});
|
|
21
21
|
}
|