@zhin.js/core 1.4.0 → 1.4.1
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 +29 -43
- package/lib/adapter.js +9 -1
- package/lib/built/command.d.ts +5 -2
- package/lib/built/command.js +4 -1
- package/lib/built/interactive-segments/fallback-store.d.ts +29 -0
- package/lib/built/interactive-segments/fallback-store.js +63 -0
- package/lib/built/interactive-segments/handlers.d.ts +7 -0
- package/lib/built/interactive-segments/handlers.js +20 -4
- package/lib/built/interactive-segments/index.d.ts +1 -0
- package/lib/built/interactive-segments/index.js +1 -0
- package/lib/built/interactive-segments/resolve.d.ts +10 -1
- package/lib/built/interactive-segments/resolve.js +30 -1
- package/lib/built/segment-contract/index.d.ts +2 -1
- package/lib/built/segment-contract/index.js +2 -1
- package/lib/built/segment-contract/json-schema.js +2 -2
- package/lib/built/segment-contract/media.d.ts +11 -1
- package/lib/built/segment-contract/media.js +30 -0
- package/lib/built/segment-contract/text.d.ts +7 -0
- package/lib/built/segment-contract/text.js +34 -0
- package/lib/built/segment-contract/types.d.ts +6 -2
- package/lib/built/segment-contract/validate.js +1 -0
- package/lib/command.d.ts +5 -2
- package/lib/command.js +5 -2
- package/lib/feature/adapter.d.ts +2 -0
- package/lib/feature/adapter.js +2 -0
- package/lib/feature/command.d.ts +2 -0
- package/lib/feature/command.js +2 -0
- package/lib/feature/component.d.ts +2 -0
- package/lib/feature/component.js +2 -0
- package/lib/feature/middleware.d.ts +2 -0
- package/lib/feature/middleware.js +2 -0
- package/lib/plugin-runtime/im/contracts.d.ts +37 -3
- package/lib/plugin-runtime/im/contracts.js +8 -1
- package/lib/plugin-runtime/im/im-runtime.d.ts +6 -0
- package/lib/plugin-runtime/im/im-runtime.js +71 -6
- package/lib/plugin-runtime/im/index.d.ts +1 -0
- package/lib/plugin-runtime/im/index.js +1 -0
- package/lib/plugin-runtime/im/interactive.d.ts +25 -0
- package/lib/plugin-runtime/im/interactive.js +41 -0
- package/lib/plugin-runtime/im/message-dispatcher.js +122 -5
- package/lib/plugin-runtime/im/outbound-segments.d.ts +53 -6
- package/lib/plugin-runtime/im/outbound-segments.js +173 -10
- package/lib/plugin.d.ts +5 -3
- package/lib/plugin.js +38 -24
- package/package.json +56 -10
package/README.md
CHANGED
|
@@ -18,66 +18,52 @@ Zhin.js **IM/多通道运行时**包:Plugin、Adapter、**Endpoint**、Message
|
|
|
18
18
|
|
|
19
19
|
## 核心概念
|
|
20
20
|
|
|
21
|
-
### Plugin
|
|
21
|
+
### Plugin(插件)— Plugin Runtime
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
唯一启动路径:`zhin runtime start`。`plugin.ts` 必须 default-export `definePlugin()`;能力按约定目录发现。
|
|
24
24
|
|
|
25
25
|
```typescript
|
|
26
|
-
|
|
26
|
+
// plugin.ts
|
|
27
|
+
import { definePlugin } from '@zhin.js/plugin-runtime'
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.desc('打招呼')
|
|
36
|
-
.action((_, result) => `Hello, ${result.params.name}!`)
|
|
37
|
-
)
|
|
29
|
+
export default definePlugin({
|
|
30
|
+
name: 'hello-bot',
|
|
31
|
+
metadata: { displayName: 'Hello Bot' },
|
|
32
|
+
setup(context) {
|
|
33
|
+
context.lifecycle.add(() => { /* cleanup */ })
|
|
34
|
+
},
|
|
35
|
+
})
|
|
38
36
|
```
|
|
39
37
|
|
|
40
|
-
插件名称默认从文件路径推导,也可以显式声明:
|
|
41
|
-
|
|
42
38
|
```typescript
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// 方式 2: 使用 definePlugin 声明式 API
|
|
47
|
-
import { definePlugin } from '@zhin.js/core'
|
|
39
|
+
// commands/hello/[name:string].ts
|
|
40
|
+
import { defineCommand } from '@zhin.js/command'
|
|
48
41
|
|
|
49
|
-
export default
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
export default defineCommand({
|
|
43
|
+
description: '打招呼',
|
|
44
|
+
execute({ params }) {
|
|
45
|
+
return `Hello, ${params.name}!`
|
|
53
46
|
},
|
|
54
47
|
})
|
|
55
|
-
|
|
56
|
-
// 方式 3: 手动设置
|
|
57
|
-
const plugin = usePlugin()
|
|
58
|
-
plugin.setName('my-awesome-plugin')
|
|
59
48
|
```
|
|
60
49
|
|
|
50
|
+
> **已弃用**:`usePlugin()` / `MessageCommand` / `Plugin.addCommand` 仅经典路径(`zhin.js/node`)残留,新代码勿用。见 [public-api-surface](../../docs/contributing/public-api-surface.md)。
|
|
51
|
+
|
|
52
|
+
本包仍提供 IM 运行时契约(Message / Adapter / Endpoint);创作面在 Feature 包与 `@zhin.js/plugin-runtime`。
|
|
53
|
+
|
|
61
54
|
### Feature(特性抽象)
|
|
62
55
|
|
|
63
|
-
Feature
|
|
56
|
+
约定式 Feature(Command / Middleware / Component / Adapter…)由独立包提供 definition + 约定发现;Core Runtime 消费 snapshot。
|
|
57
|
+
|
|
58
|
+
经典 `CommandFeature` / `addCommand` 等仍挂在 `Plugin.prototype`,**已 deprecated**,仅兼容 Agent init / game-kit:
|
|
64
59
|
|
|
65
60
|
```
|
|
66
|
-
Feature (抽象基类)
|
|
67
|
-
├── CommandFeature —
|
|
68
|
-
├── ToolFeature — AI
|
|
69
|
-
├──
|
|
70
|
-
├── ScheduleFeature — 调度任务 addSchedule()
|
|
71
|
-
├── DatabaseFeature — 数据模型 defineModel()
|
|
72
|
-
├── ComponentFeature — 消息组件 addComponent()
|
|
73
|
-
├── ConfigFeature — 插件配置 addConfig()
|
|
74
|
-
└── PermissionFeature — 权限管理
|
|
61
|
+
Feature (抽象基类) — legacy 注册表仍在
|
|
62
|
+
├── CommandFeature — MessageCommand(→ defineCommand)
|
|
63
|
+
├── ToolFeature — AI 工具(→ defineAgentTool)
|
|
64
|
+
├── …
|
|
75
65
|
```
|
|
76
66
|
|
|
77
|
-
每个 Feature 都会在 `Plugin.prototype` 上注入对应的扩展方法(如 `addCommand`、`addTool`),插件通过 `usePlugin()` 获取这些方法。
|
|
78
|
-
|
|
79
|
-
Feature 支持变更事件监听,依赖方可实时响应 item 的增删:
|
|
80
|
-
|
|
81
67
|
```typescript
|
|
82
68
|
const toolFeature = plugin.inject('tool')
|
|
83
69
|
const off = toolFeature.on('add', (tool, pluginName) => {
|
|
@@ -229,7 +215,7 @@ export { CommandFeature, ToolFeature, SkillFeature, ScheduleFeature, DatabaseFea
|
|
|
229
215
|
// 消息路由
|
|
230
216
|
export { createMessageDispatcher } from './built/dispatcher.js'
|
|
231
217
|
|
|
232
|
-
//
|
|
218
|
+
// 适配器与消息(MessageCommand 已 deprecated)
|
|
233
219
|
export { Adapter, Message, MessageCommand, Endpoint, segment, ... } from './'
|
|
234
220
|
|
|
235
221
|
// 富媒体出站段
|
package/lib/adapter.js
CHANGED
|
@@ -3,7 +3,7 @@ import { connectEndpointInstance, disconnectEndpointInstance } from "./built/con
|
|
|
3
3
|
import { EventEmitter } from "node:events";
|
|
4
4
|
import { getOutboundReplyStore } from "./built/dispatcher.js";
|
|
5
5
|
import { DEFAULT_OUTBOUND_RICH_SEGMENT_POLICY, resolveRichSegments, } from "./built/rich-segments/index.js";
|
|
6
|
-
import { DEFAULT_INTERACTIVE_POLICY, resolveInteractiveSegments, } from "./built/interactive-segments/index.js";
|
|
6
|
+
import { DEFAULT_INTERACTIVE_POLICY, collectKeyboardFallbackMaps, keyboardFallbackStore, resolveInteractiveSegments, } from "./built/interactive-segments/index.js";
|
|
7
7
|
import { DEFAULT_AI_OUTBOUND_CAPABILITIES, } from "./built/ai-outbound/index.js";
|
|
8
8
|
import { createRichSegmentRenderContext } from "./built/rich-segments/capabilities.js";
|
|
9
9
|
import { collectOutboundMediaKinds } from "./built/outbound-media-utils.js";
|
|
@@ -170,6 +170,14 @@ export class Adapter extends EventEmitter {
|
|
|
170
170
|
},
|
|
171
171
|
})),
|
|
172
172
|
};
|
|
173
|
+
// 'text' 策略端点:keyboard 即将降级为编号文本,先把有效 fallback
|
|
174
|
+
// 映射写入中央存储(频道键与入站回跳解析一致),供数字回跳路由。
|
|
175
|
+
if (this.getInteractivePolicy() === 'text') {
|
|
176
|
+
const channelKey = `${this.name}-${options.endpoint}-${options.type}:${options.id}`;
|
|
177
|
+
for (const map of collectKeyboardFallbackMaps(options.content)) {
|
|
178
|
+
keyboardFallbackStore.remember(channelKey, map);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
173
181
|
options = {
|
|
174
182
|
...options,
|
|
175
183
|
content: resolveInteractiveSegments(options.content, this.getInteractivePolicy()),
|
package/lib/built/command.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { RegisteredAdapter, AdapterMessage } from "../types.js";
|
|
|
15
15
|
* CommandContext 扩展方法类型
|
|
16
16
|
*/
|
|
17
17
|
export interface CommandContextExtensions {
|
|
18
|
-
/**
|
|
18
|
+
/** @deprecated 使用 `defineCommand` + `commands/`;勿在新代码调用。 */
|
|
19
19
|
addCommand<T extends RegisteredAdapter>(command: MessageCommand<T>): () => void;
|
|
20
20
|
}
|
|
21
21
|
declare module "../plugin.js" {
|
|
@@ -28,7 +28,10 @@ declare module "../plugin.js" {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* 命令服务 Feature
|
|
31
|
+
* 命令服务 Feature(经典 MessageCommand 注册表)。
|
|
32
|
+
*
|
|
33
|
+
* @deprecated 新命令走 `@zhin.js/command` 的 `defineCommand` + 约定目录发现。
|
|
34
|
+
* 本 Feature 仍服务 Agent / game-kit / legacy Plugin.addCommand。
|
|
32
35
|
*/
|
|
33
36
|
export declare class CommandFeature extends Feature<MessageCommand<RegisteredAdapter>> {
|
|
34
37
|
readonly name: "command";
|
package/lib/built/command.js
CHANGED
|
@@ -20,7 +20,10 @@ export function compareCommandPatterns(a, b) {
|
|
|
20
20
|
return tokB - tokA;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* 命令服务 Feature
|
|
23
|
+
* 命令服务 Feature(经典 MessageCommand 注册表)。
|
|
24
|
+
*
|
|
25
|
+
* @deprecated 新命令走 `@zhin.js/command` 的 `defineCommand` + 约定目录发现。
|
|
26
|
+
* 本 Feature 仍服务 Agent / game-kit / legacy Plugin.addCommand。
|
|
24
27
|
*/
|
|
25
28
|
export class CommandFeature extends Feature {
|
|
26
29
|
name = 'command';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keyboard 文本降级 fallback map 的中央存储。
|
|
3
|
+
*
|
|
4
|
+
* 出站:端点 interactive 策略为 'text' 时,框架把 keyboard 段降级为编号文本,
|
|
5
|
+
* 同时把「数字 → payload」映射按频道写入本存储(后写覆盖先写,等价
|
|
6
|
+
* game-kit lastMenus 的“最近一张键盘”语义)。
|
|
7
|
+
* 入站:框架中间件 / dispatcher 钩子用本存储把用户回复的裸数字解析回
|
|
8
|
+
* payload,再按 prefix 最长匹配路由给注册的 interactive handler。
|
|
9
|
+
*
|
|
10
|
+
* 模块级共享实例与 handlers.ts 的 handler 注册表同例:状态按频道键控、
|
|
11
|
+
* TTL 有界(对齐 game-hub 菜单上下文的 1h),无裸悬挂业务对象。
|
|
12
|
+
*/
|
|
13
|
+
export declare const KEYBOARD_FALLBACK_TTL_MS: number;
|
|
14
|
+
export declare class KeyboardFallbackStore {
|
|
15
|
+
#private;
|
|
16
|
+
private readonly defaultTtlMs;
|
|
17
|
+
constructor(defaultTtlMs?: number);
|
|
18
|
+
/** 记录频道最近一次 keyboard 降级的数字→payload 映射(空 map 忽略)。 */
|
|
19
|
+
remember(channelKey: string, map: Record<string, string>, ttlMs?: number): void;
|
|
20
|
+
/** 频道当前有效的 fallback map(过期即删并返回 undefined)。 */
|
|
21
|
+
mapFor(channelKey: string): Record<string, string> | undefined;
|
|
22
|
+
/** 裸数字 / fallback 键 → payload(无映射或过期返回 undefined)。 */
|
|
23
|
+
resolve(channelKey: string, raw: string): string | undefined;
|
|
24
|
+
clear(): void;
|
|
25
|
+
}
|
|
26
|
+
/** 中央共享实例:出站降级写入、入站回跳读取。 */
|
|
27
|
+
export declare const keyboardFallbackStore: KeyboardFallbackStore;
|
|
28
|
+
/** 测试专用:清空中央 fallback 存储。 */
|
|
29
|
+
export declare function resetKeyboardFallbackStoreForTests(): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { resolveTextFallbackPayload } from './action.js';
|
|
2
|
+
/**
|
|
3
|
+
* keyboard 文本降级 fallback map 的中央存储。
|
|
4
|
+
*
|
|
5
|
+
* 出站:端点 interactive 策略为 'text' 时,框架把 keyboard 段降级为编号文本,
|
|
6
|
+
* 同时把「数字 → payload」映射按频道写入本存储(后写覆盖先写,等价
|
|
7
|
+
* game-kit lastMenus 的“最近一张键盘”语义)。
|
|
8
|
+
* 入站:框架中间件 / dispatcher 钩子用本存储把用户回复的裸数字解析回
|
|
9
|
+
* payload,再按 prefix 最长匹配路由给注册的 interactive handler。
|
|
10
|
+
*
|
|
11
|
+
* 模块级共享实例与 handlers.ts 的 handler 注册表同例:状态按频道键控、
|
|
12
|
+
* TTL 有界(对齐 game-hub 菜单上下文的 1h),无裸悬挂业务对象。
|
|
13
|
+
*/
|
|
14
|
+
export const KEYBOARD_FALLBACK_TTL_MS = 60 * 60 * 1000;
|
|
15
|
+
export class KeyboardFallbackStore {
|
|
16
|
+
defaultTtlMs;
|
|
17
|
+
#entries = new Map();
|
|
18
|
+
constructor(defaultTtlMs = KEYBOARD_FALLBACK_TTL_MS) {
|
|
19
|
+
this.defaultTtlMs = defaultTtlMs;
|
|
20
|
+
}
|
|
21
|
+
/** 记录频道最近一次 keyboard 降级的数字→payload 映射(空 map 忽略)。 */
|
|
22
|
+
remember(channelKey, map, ttlMs = this.defaultTtlMs) {
|
|
23
|
+
if (Object.keys(map).length === 0)
|
|
24
|
+
return;
|
|
25
|
+
this.#prune();
|
|
26
|
+
this.#entries.set(channelKey, {
|
|
27
|
+
map: Object.freeze({ ...map }),
|
|
28
|
+
expiresAt: Date.now() + ttlMs,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** 频道当前有效的 fallback map(过期即删并返回 undefined)。 */
|
|
32
|
+
mapFor(channelKey) {
|
|
33
|
+
const entry = this.#entries.get(channelKey);
|
|
34
|
+
if (!entry)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (entry.expiresAt < Date.now()) {
|
|
37
|
+
this.#entries.delete(channelKey);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return entry.map;
|
|
41
|
+
}
|
|
42
|
+
/** 裸数字 / fallback 键 → payload(无映射或过期返回 undefined)。 */
|
|
43
|
+
resolve(channelKey, raw) {
|
|
44
|
+
const map = this.mapFor(channelKey);
|
|
45
|
+
return map ? resolveTextFallbackPayload(raw, map) : undefined;
|
|
46
|
+
}
|
|
47
|
+
clear() {
|
|
48
|
+
this.#entries.clear();
|
|
49
|
+
}
|
|
50
|
+
#prune() {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
for (const [key, entry] of this.#entries) {
|
|
53
|
+
if (entry.expiresAt < now)
|
|
54
|
+
this.#entries.delete(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** 中央共享实例:出站降级写入、入站回跳读取。 */
|
|
59
|
+
export const keyboardFallbackStore = new KeyboardFallbackStore();
|
|
60
|
+
/** 测试专用:清空中央 fallback 存储。 */
|
|
61
|
+
export function resetKeyboardFallbackStoreForTests() {
|
|
62
|
+
keyboardFallbackStore.clear();
|
|
63
|
+
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { Message } from '../../message.js';
|
|
2
2
|
import type { MessageMiddleware } from '../../types.js';
|
|
3
3
|
import type { InteractiveHandler, RegisteredInteractiveHandler } from './types.js';
|
|
4
|
+
/** 旧轨频道键:与出站降级(Adapter.renderSendMessage)及 game-kit channelKey 一致。 */
|
|
5
|
+
export declare function interactiveChannelKey(message: Message<any>): string;
|
|
6
|
+
/**
|
|
7
|
+
* 文本回跳解析:中央 fallback map(裸数字)→ payload;
|
|
8
|
+
* 或 QQ 指令预填等直出 `prefix:session:id` payload。
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveInboundTextPayload(message: Message<any>): string | undefined;
|
|
4
11
|
export declare function registerInteractiveHandler(prefix: string, handler: InteractiveHandler): () => void;
|
|
5
12
|
export declare function getInteractiveHandlers(): readonly RegisteredInteractiveHandler[];
|
|
6
13
|
export declare function resetInteractiveHandlersForTests(): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getActionFromMessage } from './action.js';
|
|
1
|
+
import { getActionFromMessage, resolvePayloadFromText } from './action.js';
|
|
2
|
+
import { keyboardFallbackStore } from './fallback-store.js';
|
|
2
3
|
const handlers = [];
|
|
3
4
|
let middlewareInstalled = false;
|
|
4
5
|
function findHandler(payload) {
|
|
@@ -12,6 +13,20 @@ function findHandler(payload) {
|
|
|
12
13
|
}
|
|
13
14
|
return match?.handler;
|
|
14
15
|
}
|
|
16
|
+
/** 旧轨频道键:与出站降级(Adapter.renderSendMessage)及 game-kit channelKey 一致。 */
|
|
17
|
+
export function interactiveChannelKey(message) {
|
|
18
|
+
return `${String(message.$adapter)}-${message.$endpoint}-${message.$channel.type}:${message.$channel.id}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 文本回跳解析:中央 fallback map(裸数字)→ payload;
|
|
22
|
+
* 或 QQ 指令预填等直出 `prefix:session:id` payload。
|
|
23
|
+
*/
|
|
24
|
+
export function resolveInboundTextPayload(message) {
|
|
25
|
+
const raw = message.$raw?.trim() ?? '';
|
|
26
|
+
if (!raw)
|
|
27
|
+
return undefined;
|
|
28
|
+
return resolvePayloadFromText(raw, keyboardFallbackStore.mapFor(interactiveChannelKey(message)));
|
|
29
|
+
}
|
|
15
30
|
export function registerInteractiveHandler(prefix, handler) {
|
|
16
31
|
const entry = { prefix, handler };
|
|
17
32
|
handlers.push(entry);
|
|
@@ -35,10 +50,11 @@ export function ensureInteractiveMiddleware(addMiddleware) {
|
|
|
35
50
|
return;
|
|
36
51
|
middlewareInstalled = true;
|
|
37
52
|
addMiddleware(async (message, next) => {
|
|
38
|
-
const
|
|
39
|
-
|
|
53
|
+
const payload = getActionFromMessage(message)?.payload
|
|
54
|
+
?? resolveInboundTextPayload(message);
|
|
55
|
+
if (!payload)
|
|
40
56
|
return next();
|
|
41
|
-
const handler = findHandler(
|
|
57
|
+
const handler = findHandler(payload);
|
|
42
58
|
if (!handler)
|
|
43
59
|
return next();
|
|
44
60
|
const handled = await handler(message);
|
|
@@ -3,5 +3,6 @@ export * from './button-spec.js';
|
|
|
3
3
|
export * from './action.js';
|
|
4
4
|
export * from './resolve.js';
|
|
5
5
|
export * from './handlers.js';
|
|
6
|
+
export * from './fallback-store.js';
|
|
6
7
|
export { KeyboardSegment, InteractiveSegment } from './keyboard-segment.js';
|
|
7
8
|
export * from './onebot-keyboard.js';
|
|
@@ -3,5 +3,6 @@ export * from './button-spec.js';
|
|
|
3
3
|
export * from './action.js';
|
|
4
4
|
export * from './resolve.js';
|
|
5
5
|
export * from './handlers.js';
|
|
6
|
+
export * from './fallback-store.js';
|
|
6
7
|
export { KeyboardSegment, InteractiveSegment } from './keyboard-segment.js';
|
|
7
8
|
export * from './onebot-keyboard.js';
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { SendContent } from '../../types.js';
|
|
2
|
-
import { type InteractivePolicy } from './types.js';
|
|
2
|
+
import { type InteractivePolicy, type KeyboardSegmentData } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* keyboard 的有效 fallback 映射:显式 `fallback.map` 优先;否则按按钮顺序
|
|
5
|
+
* 自动编号(与 {@link renderKeyboardAsText} 的自动编号一致,含 disabled 按钮)。
|
|
6
|
+
*/
|
|
7
|
+
export declare function effectiveKeyboardFallbackMap(data: KeyboardSegmentData): Record<string, string>;
|
|
8
|
+
/** 收集 content 中所有 keyboard 段的有效 fallback 映射(文本降级前调用)。 */
|
|
9
|
+
export declare function collectKeyboardFallbackMaps(content: SendContent | undefined): Record<string, string>[];
|
|
10
|
+
/** keyboard 段 → 编号文本('text' 策略端点的中央降级渲染)。 */
|
|
11
|
+
export declare function renderKeyboardAsText(data: KeyboardSegmentData): string;
|
|
3
12
|
export declare function hasKeyboardSegment(content: SendContent | undefined): boolean;
|
|
4
13
|
/** @deprecated 使用 {@link hasKeyboardSegment} */
|
|
5
14
|
export declare const hasInteractiveSegment: typeof hasKeyboardSegment;
|
|
@@ -21,7 +21,36 @@ function packSegments(out) {
|
|
|
21
21
|
return out[0];
|
|
22
22
|
return out;
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* keyboard 的有效 fallback 映射:显式 `fallback.map` 优先;否则按按钮顺序
|
|
26
|
+
* 自动编号(与 {@link renderKeyboardAsText} 的自动编号一致,含 disabled 按钮)。
|
|
27
|
+
*/
|
|
28
|
+
export function effectiveKeyboardFallbackMap(data) {
|
|
29
|
+
const map = data.fallback?.map ?? {};
|
|
30
|
+
if (Object.keys(map).length > 0)
|
|
31
|
+
return { ...map };
|
|
32
|
+
const out = {};
|
|
33
|
+
data.rows.flat().forEach((btn, idx) => {
|
|
34
|
+
out[String(idx + 1)] = btn.payload;
|
|
35
|
+
});
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** 收集 content 中所有 keyboard 段的有效 fallback 映射(文本降级前调用)。 */
|
|
39
|
+
export function collectKeyboardFallbackMaps(content) {
|
|
40
|
+
if (content == null)
|
|
41
|
+
return [];
|
|
42
|
+
const maps = [];
|
|
43
|
+
for (const item of asArray(content)) {
|
|
44
|
+
if (typeof item === 'string')
|
|
45
|
+
continue;
|
|
46
|
+
const data = asKeyboardData(item);
|
|
47
|
+
if (data)
|
|
48
|
+
maps.push(effectiveKeyboardFallbackMap(data));
|
|
49
|
+
}
|
|
50
|
+
return maps;
|
|
51
|
+
}
|
|
52
|
+
/** keyboard 段 → 编号文本('text' 策略端点的中央降级渲染)。 */
|
|
53
|
+
export function renderKeyboardAsText(data) {
|
|
25
54
|
const lines = [];
|
|
26
55
|
const flat = data.rows.flat();
|
|
27
56
|
if (data.fallback?.hint) {
|
|
@@ -3,7 +3,8 @@ export { mediaRefSchema, textSegmentSchema, mentionSegmentSchema, imageSegmentSc
|
|
|
3
3
|
export { assertCanonicalSegments, isCanonicalSegment } from './assert.js';
|
|
4
4
|
export { mediaRefJsonSchema, outboundSegmentJsonSchema, aiOutboundJsonSchema, STRICT_OUTBOUND_SEGMENT_TYPES, } from './json-schema.js';
|
|
5
5
|
export { segmentsForImDelivery } from './delivery.js';
|
|
6
|
-
export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields } from './media.js';
|
|
6
|
+
export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields, collectSegmentMedia, type SegmentMediaRef, } from './media.js';
|
|
7
7
|
export { createImageSegment } from './image.js';
|
|
8
8
|
export { formatSegmentPreview } from './preview.js';
|
|
9
|
+
export { segmentsToPlainText } from './text.js';
|
|
9
10
|
export { readMentionTarget, readMentionName, readMentionSegmentTarget } from './mention.js';
|
|
@@ -2,7 +2,8 @@ export { mediaRefSchema, textSegmentSchema, mentionSegmentSchema, imageSegmentSc
|
|
|
2
2
|
export { assertCanonicalSegments, isCanonicalSegment } from './assert.js';
|
|
3
3
|
export { mediaRefJsonSchema, outboundSegmentJsonSchema, aiOutboundJsonSchema, STRICT_OUTBOUND_SEGMENT_TYPES, } from './json-schema.js';
|
|
4
4
|
export { segmentsForImDelivery } from './delivery.js';
|
|
5
|
-
export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields } from './media.js';
|
|
5
|
+
export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields, collectSegmentMedia, } from './media.js';
|
|
6
6
|
export { createImageSegment } from './image.js';
|
|
7
7
|
export { formatSegmentPreview } from './preview.js';
|
|
8
|
+
export { segmentsToPlainText } from './text.js';
|
|
8
9
|
export { readMentionTarget, readMentionName, readMentionSegmentTarget } from './mention.js';
|
|
@@ -20,8 +20,8 @@ export const mediaRefJsonSchema = {
|
|
|
20
20
|
properties: {
|
|
21
21
|
kind: {
|
|
22
22
|
type: 'string',
|
|
23
|
-
enum: ['url', 'path', 'base64'],
|
|
24
|
-
description: 'url=http(s) 链接;path=本地文件路径;base64=内联 base64
|
|
23
|
+
enum: ['url', 'path', 'base64', 'file'],
|
|
24
|
+
description: 'url=http(s) 链接;path=本地文件路径;base64=内联 base64 数据;file=平台不透明文件引用(如 Telegram file_id)',
|
|
25
25
|
},
|
|
26
26
|
value: { type: 'string', description: '媒体内容:URL / 文件路径 / 纯 base64' },
|
|
27
27
|
mime_type: { type: 'string', description: '如 image/png、audio/mpeg' },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MediaRef } from './types.js';
|
|
1
|
+
import type { MediaRef, Segment } from './types.js';
|
|
2
2
|
import { isMediaRef } from './validate.js';
|
|
3
3
|
export { isMediaRef };
|
|
4
4
|
export declare function mediaRefFromLegacyData(data: Record<string, unknown>): MediaRef | undefined;
|
|
@@ -6,3 +6,13 @@ export declare function mediaRefToLegacyFields(media: MediaRef): {
|
|
|
6
6
|
url?: string;
|
|
7
7
|
file?: string;
|
|
8
8
|
};
|
|
9
|
+
export interface SegmentMediaRef {
|
|
10
|
+
readonly type: string;
|
|
11
|
+
readonly media: MediaRef;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 从入站 canonical 段收集媒体引用(image / audio / video / file)。
|
|
15
|
+
* 兼容 canonical `data.media` 与旧轨 url/file/base64 字段;
|
|
16
|
+
* 无媒体段时返回空数组(调用方无需特判 undefined)。
|
|
17
|
+
*/
|
|
18
|
+
export declare function collectSegmentMedia(segments: readonly Segment[] | undefined): SegmentMediaRef[];
|
|
@@ -5,6 +5,13 @@ export function mediaRefFromLegacyData(data) {
|
|
|
5
5
|
return data.media;
|
|
6
6
|
}
|
|
7
7
|
const mimeType = typeof data.mime_type === 'string' ? data.mime_type : undefined;
|
|
8
|
+
// 平台不透明文件引用(Telegram file_id / Milky resource_id 等)
|
|
9
|
+
const fileRef = typeof data.file_id === 'string' && data.file_id.trim()
|
|
10
|
+
? data.file_id.trim()
|
|
11
|
+
: undefined;
|
|
12
|
+
if (fileRef) {
|
|
13
|
+
return { kind: 'file', value: fileRef, ...(mimeType ? { mime_type: mimeType } : {}) };
|
|
14
|
+
}
|
|
8
15
|
const base64 = typeof data.base64 === 'string' && data.base64.trim()
|
|
9
16
|
? data.base64.trim()
|
|
10
17
|
: typeof data.data === 'string' && data.data.trim() && !String(data.data).startsWith('http')
|
|
@@ -34,6 +41,29 @@ export function mediaRefToLegacyFields(media) {
|
|
|
34
41
|
return { url: media.value, file: media.value };
|
|
35
42
|
if (media.kind === 'path')
|
|
36
43
|
return { file: media.value, url: media.value };
|
|
44
|
+
if (media.kind === 'file')
|
|
45
|
+
return { file: media.value, url: media.value };
|
|
37
46
|
const encoded = media.value.startsWith('base64://') ? media.value : `base64://${media.value}`;
|
|
38
47
|
return { file: encoded, url: encoded };
|
|
39
48
|
}
|
|
49
|
+
/** 入站段携带媒体引用的段类型(纯文本视图无法承载的信息)。 */
|
|
50
|
+
const MEDIA_SEGMENT_TYPES = new Set(['image', 'audio', 'video', 'file']);
|
|
51
|
+
/**
|
|
52
|
+
* 从入站 canonical 段收集媒体引用(image / audio / video / file)。
|
|
53
|
+
* 兼容 canonical `data.media` 与旧轨 url/file/base64 字段;
|
|
54
|
+
* 无媒体段时返回空数组(调用方无需特判 undefined)。
|
|
55
|
+
*/
|
|
56
|
+
export function collectSegmentMedia(segments) {
|
|
57
|
+
if (!segments?.length)
|
|
58
|
+
return [];
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const segment of segments) {
|
|
61
|
+
if (!segment || typeof segment.type !== 'string' || !MEDIA_SEGMENT_TYPES.has(segment.type)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const media = mediaRefFromLegacyData(segment.data ?? {});
|
|
65
|
+
if (media)
|
|
66
|
+
out.push({ type: segment.type, media });
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Segment } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 提取纯文本视图:text 段原文拼接,mention/at 段渲染为 `@name`(无名回退
|
|
4
|
+
* `@target`,`all` → `@all`);其余段类型不产生文本(媒体 / 回复等结构化
|
|
5
|
+
* 信息经 `segments` 轨道消费,不污染命令输入)。
|
|
6
|
+
*/
|
|
7
|
+
export declare function segmentsToPlainText(segments: readonly Segment[]): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Segment[] → 纯文本视图(SSOT)。
|
|
3
|
+
* 入站双轨:结构化段为唯一内部契约,纯文本视图供命令匹配 / AI 兜底消费。
|
|
4
|
+
* @see docs/architecture/segment-content-model.md
|
|
5
|
+
*/
|
|
6
|
+
import { readMentionName, readMentionTarget } from './mention.js';
|
|
7
|
+
/**
|
|
8
|
+
* 提取纯文本视图:text 段原文拼接,mention/at 段渲染为 `@name`(无名回退
|
|
9
|
+
* `@target`,`all` → `@all`);其余段类型不产生文本(媒体 / 回复等结构化
|
|
10
|
+
* 信息经 `segments` 轨道消费,不污染命令输入)。
|
|
11
|
+
*/
|
|
12
|
+
export function segmentsToPlainText(segments) {
|
|
13
|
+
let out = '';
|
|
14
|
+
for (const segment of segments) {
|
|
15
|
+
// Segment 含 SegmentBase 兜底成员,type 窄化不能联动窄化 data,统一按字典读
|
|
16
|
+
const data = segment.data;
|
|
17
|
+
if (segment.type === 'text') {
|
|
18
|
+
if (typeof data.text === 'string')
|
|
19
|
+
out += data.text;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (segment.type === 'mention' || segment.type === 'at') {
|
|
23
|
+
const name = readMentionName(data);
|
|
24
|
+
if (name) {
|
|
25
|
+
out += `@${name}`;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const target = readMentionTarget(data);
|
|
29
|
+
if (target)
|
|
30
|
+
out += `@${target}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
@@ -7,9 +7,13 @@ export interface SegmentBase {
|
|
|
7
7
|
data: Record<string, unknown>;
|
|
8
8
|
platform?: Record<string, unknown>;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* 媒体引用占位(完整 schema 随 adapter 迁移补齐)。
|
|
12
|
+
* kind=file:平台侧不透明文件引用(如 Telegram file_id、Milky resource_id),
|
|
13
|
+
* 非 URL/本地路径,消费方需经平台 API 解析。
|
|
14
|
+
*/
|
|
11
15
|
export interface MediaRef {
|
|
12
|
-
kind: 'url' | 'path' | 'base64';
|
|
16
|
+
kind: 'url' | 'path' | 'base64' | 'file';
|
|
13
17
|
value: string;
|
|
14
18
|
mime_type?: string;
|
|
15
19
|
}
|
package/lib/command.d.ts
CHANGED
|
@@ -5,8 +5,11 @@ import { Plugin } from './plugin.js';
|
|
|
5
5
|
type ConstructFirstParam<T extends new (...args: any[]) => any> = T extends new (...args: [infer U, ...any[]]) => any ? U : never;
|
|
6
6
|
type ConstructSecondParam<T extends new (...args: any[]) => any> = T extends new (...args: [any, infer V, ...any[]]) => any ? V : never;
|
|
7
7
|
/**
|
|
8
|
-
* MessageCommand
|
|
9
|
-
*
|
|
8
|
+
* MessageCommand类:经典命令系统(segment-matcher)。
|
|
9
|
+
*
|
|
10
|
+
* @deprecated 新插件请用 `defineCommand`(`zhin.js/command`)+ `commands/` 约定目录。
|
|
11
|
+
* 本类仍供 Agent init / game-kit hub / legacy `CommandFeature` 使用;计划随经典
|
|
12
|
+
* Plugin 路径一并退役。见 `docs/contributing/public-api-surface.md`。
|
|
10
13
|
*/
|
|
11
14
|
export declare class MessageCommand<T extends RegisteredAdapter = RegisteredAdapter> extends SegmentMatcher {
|
|
12
15
|
#private;
|
package/lib/command.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { SegmentMatcher } from 'segment-matcher';
|
|
2
2
|
/**
|
|
3
|
-
* MessageCommand
|
|
4
|
-
*
|
|
3
|
+
* MessageCommand类:经典命令系统(segment-matcher)。
|
|
4
|
+
*
|
|
5
|
+
* @deprecated 新插件请用 `defineCommand`(`zhin.js/command`)+ `commands/` 约定目录。
|
|
6
|
+
* 本类仍供 Agent init / game-kit hub / legacy `CommandFeature` 使用;计划随经典
|
|
7
|
+
* Plugin 路径一并退役。见 `docs/contributing/public-api-surface.md`。
|
|
5
8
|
*/
|
|
6
9
|
export class MessageCommand extends SegmentMatcher {
|
|
7
10
|
#callbacks = [];
|