@zhin.js/core 1.5.1 → 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.
Files changed (99) hide show
  1. package/README.md +6 -3
  2. package/lib/adapter.d.ts +7 -1
  3. package/lib/adapter.js +28 -24
  4. package/lib/agent-prompt.d.ts +2 -3
  5. package/lib/agent-prompt.js +5 -0
  6. package/lib/built/ai-outbound/parse.d.ts +1 -1
  7. package/lib/built/ai-outbound/parse.js +2 -6
  8. package/lib/built/ai-outbound/plain-mention-rewrite.js +4 -4
  9. package/lib/built/ai-outbound/resolve.js +8 -8
  10. package/lib/built/ai-outbound/types.d.ts +3 -3
  11. package/lib/built/ai-trigger.d.ts +5 -0
  12. package/lib/built/ai-trigger.js +40 -13
  13. package/lib/built/authorization.js +4 -4
  14. package/lib/built/command.d.ts +2 -2
  15. package/lib/built/command.js +2 -2
  16. package/lib/built/common-adapter-tools.d.ts +11 -11
  17. package/lib/built/common-adapter-tools.js +13 -13
  18. package/lib/built/config.d.ts +0 -4
  19. package/lib/built/config.js +0 -4
  20. package/lib/built/dispatcher.d.ts +2 -0
  21. package/lib/built/dispatcher.js +1 -1
  22. package/lib/built/endpoint-commands.js +2 -2
  23. package/lib/built/endpoint-lifecycle-service.js +13 -13
  24. package/lib/built/endpoint-lifecycle.d.ts +3 -3
  25. package/lib/built/endpoint-lifecycle.js +2 -2
  26. package/lib/built/endpoint-manager.d.ts +1 -1
  27. package/lib/built/interactive-segments/index.d.ts +1 -1
  28. package/lib/built/interactive-segments/index.js +1 -1
  29. package/lib/built/interactive-segments/keyboard-segment.d.ts +0 -1
  30. package/lib/built/interactive-segments/keyboard-segment.js +0 -1
  31. package/lib/built/interactive-segments/onebot-keyboard.d.ts +0 -3
  32. package/lib/built/interactive-segments/onebot-keyboard.js +0 -2
  33. package/lib/built/interactive-segments/resolve.d.ts +0 -3
  34. package/lib/built/interactive-segments/resolve.js +0 -3
  35. package/lib/built/interactive-segments/types.d.ts +8 -20
  36. package/lib/built/interactive-segments/types.js +0 -4
  37. package/lib/built/login-assist.d.ts +2 -2
  38. package/lib/built/login-assist.js +2 -2
  39. package/lib/built/message-enrich.d.ts +1 -1
  40. package/lib/built/message-enrich.js +2 -7
  41. package/lib/built/permission.d.ts +0 -4
  42. package/lib/built/permission.js +0 -4
  43. package/lib/built/prepend-quote-context.js +5 -5
  44. package/lib/built/rich-segments/capabilities.js +0 -1
  45. package/lib/built/rich-segments/html-segment.js +1 -4
  46. package/lib/built/rich-segments/markdown-segment.js +1 -4
  47. package/lib/built/rich-segments/types.d.ts +0 -5
  48. package/lib/built/schema-endpoint-manager.js +6 -6
  49. package/lib/built/tool.d.ts +5 -3
  50. package/lib/built/tool.js +14 -23
  51. package/lib/command.d.ts +4 -4
  52. package/lib/command.js +8 -11
  53. package/lib/endpoint-capabilities.d.ts +2 -3
  54. package/lib/endpoint-capabilities.js +4 -4
  55. package/lib/endpoint.d.ts +2 -0
  56. package/lib/endpoint.js +1 -0
  57. package/lib/errors.d.ts +2 -2
  58. package/lib/errors.js +4 -4
  59. package/lib/host-plugin-registry.d.ts +6 -5
  60. package/lib/host-plugin-registry.js +9 -4
  61. package/lib/im-scene.d.ts +1 -1
  62. package/lib/im-scene.js +5 -5
  63. package/lib/im-session-id.d.ts +2 -2
  64. package/lib/im-session-id.js +4 -4
  65. package/lib/index.d.ts +1 -2
  66. package/lib/index.js +0 -2
  67. package/lib/jsx-runtime.d.ts +1 -0
  68. package/lib/jsx.d.ts +19 -21
  69. package/lib/message.d.ts +1 -1
  70. package/lib/message.js +1 -1
  71. package/lib/notice.d.ts +2 -2
  72. package/lib/plugin-context.d.ts +11 -13
  73. package/lib/plugin-context.js +21 -18
  74. package/lib/plugin-runtime/im/contracts.d.ts +104 -60
  75. package/lib/plugin-runtime/im/contracts.js +87 -17
  76. package/lib/plugin-runtime/im/im-runtime.d.ts +38 -35
  77. package/lib/plugin-runtime/im/im-runtime.js +140 -131
  78. package/lib/plugin-runtime/im/index.d.ts +2 -0
  79. package/lib/plugin-runtime/im/index.js +2 -0
  80. package/lib/plugin-runtime/im/interactive.d.ts +3 -2
  81. package/lib/plugin-runtime/im/interactive.js +9 -4
  82. package/lib/plugin-runtime/im/message-bus.d.ts +38 -0
  83. package/lib/plugin-runtime/im/message-bus.js +40 -0
  84. package/lib/plugin-runtime/im/message-dispatcher.js +14 -74
  85. package/lib/plugin-runtime/im/outbound-renderer.d.ts +3 -2
  86. package/lib/plugin-runtime/im/outbound-renderer.js +44 -8
  87. package/lib/plugin-runtime/im/service-tokens.d.ts +18 -0
  88. package/lib/plugin-runtime/im/service-tokens.js +13 -0
  89. package/lib/plugin.d.ts +4 -7
  90. package/lib/plugin.js +7 -30
  91. package/lib/request.d.ts +2 -2
  92. package/lib/side-event/types.d.ts +0 -4
  93. package/lib/types.d.ts +6 -11
  94. package/lib/utils.js +3 -1
  95. package/package.json +13 -12
  96. package/lib/built/qrcode-segment.d.ts +0 -18
  97. package/lib/built/qrcode-segment.js +0 -78
  98. package/lib/types-generator.d.ts +0 -5
  99. package/lib/types-generator.js +0 -71
package/README.md CHANGED
@@ -36,18 +36,21 @@ export default definePlugin({
36
36
  ```
37
37
 
38
38
  ```typescript
39
- // commands/hello/[name:string].ts
39
+ // commands/hello/[name].ts
40
40
  import { defineCommand } from '@zhin.js/command'
41
41
 
42
42
  export default defineCommand({
43
43
  description: '打招呼',
44
+ params: {
45
+ name: { type: 'string' },
46
+ },
44
47
  execute({ params }) {
45
48
  return `Hello, ${params.name}!`
46
49
  },
47
50
  })
48
51
  ```
49
52
 
50
- > **已弃用**:`usePlugin()` / `MessageCommand` / `Plugin.addCommand` 仅经典路径(`zhin.js/node`)残留,新代码勿用。见 [public-api-surface](../../docs/contributing/public-api-surface.md)。
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)。
51
54
 
52
55
  本包仍提供 IM 运行时契约(Message / Adapter / Endpoint);创作面在 Feature 包与 `@zhin.js/plugin-runtime`。
53
56
 
@@ -206,7 +209,7 @@ Core **不包含** ZhinAgent 实现。IM 侧的 AI 对话、工具收集、执
206
209
 
207
210
  ```typescript
208
211
  // 插件系统
209
- export { Plugin, usePlugin, getPlugin, definePlugin } from './plugin.js'
212
+ export { Plugin, usePlugin, getPlugin, definePlugin } from './plugin.js' // usePlugin/getPlugin 为 throwing stub
210
213
 
211
214
  // Feature 体系(Cron / Scheduler 来自 @zhin.js/kernel)
212
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 实现了 $editMessage,调用平台编辑 API
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, resolveInteractiveSegments, } from "./built/interactive-segments/index.js";
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 = async (endpoint_id, id) => {
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({ drop: 'concurrency', limit: this.maxConcurrentMessages }));
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: resolveInteractiveSegments(options.content, this.getInteractivePolicy()),
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 实现了 $editMessage,调用平台编辑 API
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 editable = endpoint;
237
- if (editable.$editMessage) {
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 editable.$editMessage({ ...options, content: rendered.content });
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: '$editMessage not implemented',
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 = async (endpoint_id, id) => {
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) {
@@ -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
- /** 当前 turn Message 通讯上下文 */
16
- commMessage: Message<any>;
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?: {
@@ -1 +1,6 @@
1
+ /**
2
+ * Agent system-prompt extension contract (per IM platform / adapter).
3
+ *
4
+ * Implementations live in adapter plugins; resolution runs in @zhin.js/agent.
5
+ */
1
6
  export {};
@@ -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 legacy kind/mode)。 */
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 legacy kind/mode)。 */
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 endpointId = mentionResolver(token);
17
- if (!endpointId)
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 endpointId = mentionResolver(token);
27
- return endpointId ? '' : full;
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(endpointIds, text, ctx) {
2
+ function buildMentionSegments(endpointKeys, text, ctx) {
3
3
  const segments = [];
4
- for (const endpointId of endpointIds) {
5
- const target = ctx.atIdResolver?.(endpointId) ?? endpointId;
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 endpointIds = [];
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 (!endpointIds.includes(resolved))
29
- endpointIds.push(resolved);
28
+ if (!endpointKeys.includes(resolved))
29
+ endpointKeys.push(resolved);
30
30
  }
31
- return { ok: true, endpointIds };
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.endpointIds, text, ctx));
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
- (endpointId: string): string;
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)解析为 endpointId */
43
+ /** 将 peer ref(role/endpoint/primary)解析为 endpointKey */
44
44
  mentionResolver?: AiOutboundMentionResolver;
45
- /** 将 endpointId 解析为 platform @ id */
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
@@ -39,23 +39,23 @@ function normalizeAtIds(endpointAtIds) {
39
39
  function segmentAtUserId(seg) {
40
40
  return readMentionSegmentTarget(seg);
41
41
  }
42
- function isAtSegmentForEndpoint(seg, endpointIds) {
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 !== '' && endpointIds.includes(uid);
46
+ return uid !== '' && endpointKeys.includes(uid);
47
47
  }
48
- function textMentionsEndpoint(text, endpointIds) {
49
- for (const id of endpointIds) {
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, endpointIds) {
56
+ function stripTextAtEndpoint(text, endpointKeys) {
57
57
  let result = text;
58
- for (const id of endpointIds) {
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 endpointIds = normalizeAtIds(endpointAtIds?.length ? endpointAtIds : collectEndpointAtIds(message));
80
- if (message.$content.some((seg) => isAtSegmentForEndpoint(seg, endpointIds))) {
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, endpointIds)) {
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 endpointIds = normalizeAtIds(endpointAtIds?.length ? endpointAtIds : collectEndpointAtIds(message));
136
+ const endpointKeys = normalizeAtIds(endpointAtIds?.length ? endpointAtIds : collectEndpointAtIds(message));
121
137
  return message.$content
122
- .filter((seg) => !isAtSegmentForEndpoint(seg, endpointIds))
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, endpointIds).trim();
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: content.length ? segment.toString(content).trim() : '',
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, endpointId) {
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.name) === endpointId);
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.name) === endpointId)
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 ?? { name: endpointId }),
24
+ ...(entry ?? { id: endpointKey }),
25
25
  };
26
26
  if (adapterConfig.master != null && merged.master == null) {
27
27
  merged.master = adapterConfig.master;
@@ -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>>, plugin: Plugin): Promise<any>;
57
+ handle(message: Message<AdapterMessage<RegisteredAdapter>>, host: PermissionHost | null): Promise<any>;
58
58
  /**
59
59
  * 生命周期: 销毁时清理所有命令
60
60
  */
@@ -54,10 +54,10 @@ export class CommandFeature extends Feature {
54
54
  /**
55
55
  * 处理消息,依次尝试匹配命令(较长前缀优先,避免 `teach` 抢 `teach-list`)
56
56
  */
57
- async handle(message, plugin) {
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, plugin);
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?(endpointId: string, sceneId: string, userId: string): Promise<any>;
22
- muteMember?(endpointId: string, sceneId: string, userId: string, duration?: number): Promise<any>;
23
- setMemberNickname?(endpointId: string, sceneId: string, userId: string, nickname: string): Promise<any>;
24
- setModerator?(endpointId: string, sceneId: string, userId: string, enable?: boolean): Promise<any>;
25
- listMembers?(endpointId: string, sceneId: string): Promise<any>;
26
- banMember?(endpointId: string, sceneId: string, userId: string, reason?: string): Promise<any>;
27
- unbanMember?(endpointId: string, sceneId: string, userId: string): Promise<any>;
28
- renameScene?(endpointId: string, sceneId: string, name: string): Promise<any>;
29
- setSceneMuted?(endpointId: string, sceneId: string, enable?: boolean): Promise<any>;
30
- getSceneInfo?(endpointId: string, sceneId: string): Promise<any>;
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, endpointId: string, sceneId: string, rest: Record<string, any>): any[];
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: 'endpointId' },
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, endpointId, sceneId, rest) {
189
+ export function buildSceneMethodArgs(method, endpointKey, sceneId, rest) {
190
190
  switch (method) {
191
- case 'removeMember': return [endpointId, sceneId, rest.user_id];
192
- case 'muteMember': return [endpointId, sceneId, rest.user_id, rest.duration ?? 600];
193
- case 'setMemberNickname': return [endpointId, sceneId, rest.user_id, rest.nickname];
194
- case 'setModerator': return [endpointId, sceneId, rest.user_id, rest.enable ?? true];
195
- case 'listMembers': return [endpointId, sceneId];
196
- case 'banMember': return [endpointId, sceneId, rest.user_id, rest.reason];
197
- case 'unbanMember': return [endpointId, sceneId, rest.user_id];
198
- case 'renameScene': return [endpointId, sceneId, rest.name];
199
- case 'setSceneMuted': return [endpointId, sceneId, rest.enable ?? true];
200
- case 'getSceneInfo': return [endpointId, sceneId];
201
- default: return [endpointId, sceneId, ...Object.values(rest)];
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
  }
@@ -96,7 +96,3 @@ export declare class ConfigFeature extends Feature<ConfigRecord> {
96
96
  addConfig(key: string, defaultValue: any): () => void;
97
97
  };
98
98
  }
99
- /**
100
- * @deprecated Use ConfigFeature instead
101
- */
102
- export declare const ConfigService: typeof ConfigFeature;
@@ -370,7 +370,3 @@ export class ConfigFeature extends Feature {
370
370
  };
371
371
  }
372
372
  }
373
- /**
374
- * @deprecated Use ConfigFeature instead
375
- */
376
- export const ConfigService = ConfigFeature;
@@ -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>;
@@ -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, rootPlugin);
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 [endpointId, endpoint] of adapter.endpoints.entries()) {
15
+ for (const [endpointKey, endpoint] of adapter.endpoints.entries()) {
16
16
  rows.push({
17
17
  adapter: String(adapterName),
18
- name: endpointId,
18
+ name: endpointKey,
19
19
  online: !!endpoint.$connected,
20
20
  });
21
21
  }