@zhin.js/core 1.3.5 → 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 +17 -2
- 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/login-assist.d.ts +19 -2
- package/lib/built/login-assist.js +43 -4
- package/lib/built/segment-contract/index.d.ts +3 -1
- package/lib/built/segment-contract/index.js +3 -1
- package/lib/built/segment-contract/json-schema.d.ts +28 -0
- package/lib/built/segment-contract/json-schema.js +128 -0
- 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/endpoint.d.ts +4 -1
- package/lib/endpoint.js +1 -0
- 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 +47 -2
- package/lib/plugin-runtime/im/im-runtime.js +176 -11
- 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.d.ts +12 -2
- package/lib/plugin-runtime/im/message-dispatcher.js +153 -12
- 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 +6 -3
- package/lib/plugin.js +38 -24
- package/lib/tool-zod.d.ts +16 -1
- package/lib/tool-zod.js +138 -51
- package/lib/utils.d.ts +3 -0
- package/lib/utils.js +6 -3
- package/package.json +57 -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";
|
|
@@ -77,7 +77,11 @@ export class Adapter extends EventEmitter {
|
|
|
77
77
|
logger: this.logger,
|
|
78
78
|
getMaxConcurrentMessages: () => this.maxConcurrentMessages,
|
|
79
79
|
getPendingMessages: () => this.#pendingMessages,
|
|
80
|
-
decrementPending: () => {
|
|
80
|
+
decrementPending: () => {
|
|
81
|
+
// stop() may zero the counter while in-flight receives still finish;
|
|
82
|
+
// never let the budget go negative.
|
|
83
|
+
this.#pendingMessages = Math.max(0, this.#pendingMessages - 1);
|
|
84
|
+
},
|
|
81
85
|
});
|
|
82
86
|
}
|
|
83
87
|
/** 入站消息管线(替代 emit override 的隐式管线) */
|
|
@@ -166,6 +170,14 @@ export class Adapter extends EventEmitter {
|
|
|
166
170
|
},
|
|
167
171
|
})),
|
|
168
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
|
+
}
|
|
169
181
|
options = {
|
|
170
182
|
...options,
|
|
171
183
|
content: resolveInteractiveSegments(options.content, this.getInteractivePolicy()),
|
|
@@ -291,6 +303,9 @@ export class Adapter extends EventEmitter {
|
|
|
291
303
|
}
|
|
292
304
|
// 无论是否有错误,始终完成清理
|
|
293
305
|
this.endpoints.clear();
|
|
306
|
+
// Drop in-flight concurrency counter so a subsequent start() does not
|
|
307
|
+
// inherit a stale backpressure budget from the previous generation.
|
|
308
|
+
this.#pendingMessages = 0;
|
|
294
309
|
// 从 adapters 数组中移除(可能因重复 start 出现多条同名,需全部删掉)
|
|
295
310
|
const rootAdapters = this.plugin.root.adapters;
|
|
296
311
|
for (let i = rootAdapters.length - 1; i >= 0; i--) {
|
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) {
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 事件:
|
|
9
9
|
* endpoint.login.pending — 有新待办时触发,payload: PendingLoginTask
|
|
10
|
+
* endpoint.login.expired — 超时自动取消时触发,payload: PendingLoginTask
|
|
10
11
|
*/
|
|
11
12
|
import type { Plugin } from '../plugin.js';
|
|
12
13
|
export type LoginAssistType = 'qrcode' | 'sms' | 'slider' | 'device' | 'auth' | 'other';
|
|
14
|
+
/** 默认 5 分钟;扫码/滑块长期挂起会泄漏 Promise 与 Map 条目 */
|
|
15
|
+
export declare const DEFAULT_LOGIN_ASSIST_TIMEOUT_MS: number;
|
|
13
16
|
export interface PendingLoginTaskPayload {
|
|
14
17
|
/** 说明文案(如「请扫码登录」) */
|
|
15
18
|
message?: string;
|
|
@@ -27,16 +30,30 @@ export interface PendingLoginTask {
|
|
|
27
30
|
type: LoginAssistType;
|
|
28
31
|
payload: PendingLoginTaskPayload;
|
|
29
32
|
createdAt: number;
|
|
33
|
+
/** 到期时间戳(ms);无超时则为 undefined */
|
|
34
|
+
expiresAt?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface WaitForInputOptions {
|
|
37
|
+
/**
|
|
38
|
+
* 超时毫秒。默认 {@link DEFAULT_LOGIN_ASSIST_TIMEOUT_MS}。
|
|
39
|
+
* 传 `0` / `Infinity` / 负数表示不超时(仅测试或显式长驻场景)。
|
|
40
|
+
*/
|
|
41
|
+
timeoutMs?: number;
|
|
30
42
|
}
|
|
31
43
|
export declare class LoginAssist {
|
|
32
44
|
private readonly plugin;
|
|
33
45
|
private readonly pending;
|
|
34
46
|
private idSeq;
|
|
35
|
-
|
|
47
|
+
/** 构造时默认超时;可被 waitForInput options 覆盖 */
|
|
48
|
+
private readonly defaultTimeoutMs;
|
|
49
|
+
constructor(plugin: Plugin, options?: {
|
|
50
|
+
defaultTimeoutMs?: number;
|
|
51
|
+
});
|
|
36
52
|
/**
|
|
37
53
|
* 生产者:等待用户输入后 resolve。会发出 endpoint.login.pending 事件,未消费前可被 listPending 拉取(刷新后可继续消费)。
|
|
54
|
+
* 超时后 reject 并 emit `endpoint.login.expired`。
|
|
38
55
|
*/
|
|
39
|
-
waitForInput(adapter: string, endpointId: string, type: LoginAssistType, payload?: PendingLoginTaskPayload): Promise<string | Record<string, unknown>>;
|
|
56
|
+
waitForInput(adapter: string, endpointId: string, type: LoginAssistType, payload?: PendingLoginTaskPayload, options?: WaitForInputOptions): Promise<string | Record<string, unknown>>;
|
|
40
57
|
/**
|
|
41
58
|
* 消费者:提交结果,对应 waitForInput 的 Promise 会 resolve。
|
|
42
59
|
*/
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 事件:
|
|
9
9
|
* endpoint.login.pending — 有新待办时触发,payload: PendingLoginTask
|
|
10
|
+
* endpoint.login.expired — 超时自动取消时触发,payload: PendingLoginTask
|
|
10
11
|
*/
|
|
12
|
+
/** 默认 5 分钟;扫码/滑块长期挂起会泄漏 Promise 与 Map 条目 */
|
|
13
|
+
export const DEFAULT_LOGIN_ASSIST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
11
14
|
// ============================================================================
|
|
12
15
|
// LoginAssist 服务
|
|
13
16
|
// ============================================================================
|
|
@@ -15,24 +18,48 @@ export class LoginAssist {
|
|
|
15
18
|
plugin;
|
|
16
19
|
pending = new Map();
|
|
17
20
|
idSeq = 0;
|
|
18
|
-
|
|
21
|
+
/** 构造时默认超时;可被 waitForInput options 覆盖 */
|
|
22
|
+
defaultTimeoutMs;
|
|
23
|
+
constructor(plugin, options) {
|
|
19
24
|
this.plugin = plugin;
|
|
25
|
+
this.defaultTimeoutMs = options?.defaultTimeoutMs ?? DEFAULT_LOGIN_ASSIST_TIMEOUT_MS;
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
22
28
|
* 生产者:等待用户输入后 resolve。会发出 endpoint.login.pending 事件,未消费前可被 listPending 拉取(刷新后可继续消费)。
|
|
29
|
+
* 超时后 reject 并 emit `endpoint.login.expired`。
|
|
23
30
|
*/
|
|
24
|
-
waitForInput(adapter, endpointId, type, payload = {}) {
|
|
31
|
+
waitForInput(adapter, endpointId, type, payload = {}, options) {
|
|
25
32
|
const id = `login-${Date.now()}-${++this.idSeq}`;
|
|
33
|
+
const timeoutMs = resolveTimeoutMs(options?.timeoutMs, this.defaultTimeoutMs);
|
|
34
|
+
const createdAt = Date.now();
|
|
26
35
|
const task = {
|
|
27
36
|
id,
|
|
28
37
|
adapter,
|
|
29
38
|
endpointId,
|
|
30
39
|
type,
|
|
31
40
|
payload: { message: payload.message ?? '', ...payload },
|
|
32
|
-
createdAt
|
|
41
|
+
createdAt,
|
|
42
|
+
...(timeoutMs != null ? { expiresAt: createdAt + timeoutMs } : {}),
|
|
33
43
|
};
|
|
34
44
|
const promise = new Promise((resolve, reject) => {
|
|
35
|
-
|
|
45
|
+
const entry = { task, resolve, reject };
|
|
46
|
+
if (timeoutMs != null) {
|
|
47
|
+
entry.timer = setTimeout(() => {
|
|
48
|
+
if (!this.pending.has(id))
|
|
49
|
+
return;
|
|
50
|
+
this.pending.delete(id);
|
|
51
|
+
try {
|
|
52
|
+
this.plugin.emit('endpoint.login.expired', task);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* observer failure must not leave the producer hanging forever */
|
|
56
|
+
}
|
|
57
|
+
reject(new Error(`LoginAssist task timed out after ${timeoutMs}ms`));
|
|
58
|
+
}, timeoutMs);
|
|
59
|
+
// Node: 不阻止进程退出
|
|
60
|
+
entry.timer.unref?.();
|
|
61
|
+
}
|
|
62
|
+
this.pending.set(id, entry);
|
|
36
63
|
this.plugin.emit('endpoint.login.pending', task);
|
|
37
64
|
});
|
|
38
65
|
return promise;
|
|
@@ -45,6 +72,8 @@ export class LoginAssist {
|
|
|
45
72
|
if (!entry)
|
|
46
73
|
return false;
|
|
47
74
|
this.pending.delete(id);
|
|
75
|
+
if (entry.timer)
|
|
76
|
+
clearTimeout(entry.timer);
|
|
48
77
|
entry.resolve(value);
|
|
49
78
|
return true;
|
|
50
79
|
}
|
|
@@ -56,6 +85,8 @@ export class LoginAssist {
|
|
|
56
85
|
if (!entry)
|
|
57
86
|
return false;
|
|
58
87
|
this.pending.delete(id);
|
|
88
|
+
if (entry.timer)
|
|
89
|
+
clearTimeout(entry.timer);
|
|
59
90
|
entry.reject(new Error(reason));
|
|
60
91
|
return true;
|
|
61
92
|
}
|
|
@@ -67,8 +98,16 @@ export class LoginAssist {
|
|
|
67
98
|
}
|
|
68
99
|
dispose() {
|
|
69
100
|
for (const [, entry] of this.pending) {
|
|
101
|
+
if (entry.timer)
|
|
102
|
+
clearTimeout(entry.timer);
|
|
70
103
|
entry.reject(new Error('LoginAssist disposed'));
|
|
71
104
|
}
|
|
72
105
|
this.pending.clear();
|
|
73
106
|
}
|
|
74
107
|
}
|
|
108
|
+
function resolveTimeoutMs(explicit, fallback) {
|
|
109
|
+
const raw = explicit === undefined ? fallback : explicit;
|
|
110
|
+
if (!Number.isFinite(raw) || raw <= 0)
|
|
111
|
+
return undefined;
|
|
112
|
+
return raw;
|
|
113
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export type { Segment, SegmentBase, MediaRef, TextSegment, MentionSegment, ImageSegment, ReplySegment, ForwardSegment, FaceSegment, DiceSegment, RpsSegment, } from './types.js';
|
|
2
2
|
export { mediaRefSchema, textSegmentSchema, mentionSegmentSchema, imageSegmentSchema, replySegmentSchema, forwardSegmentSchema, faceSegmentSchema, diceSegmentSchema, rpsSegmentSchema, canonicalSegmentSchema, segmentArraySchema, } from './validate.js';
|
|
3
3
|
export { assertCanonicalSegments, isCanonicalSegment } from './assert.js';
|
|
4
|
+
export { mediaRefJsonSchema, outboundSegmentJsonSchema, aiOutboundJsonSchema, STRICT_OUTBOUND_SEGMENT_TYPES, } from './json-schema.js';
|
|
4
5
|
export { segmentsForImDelivery } from './delivery.js';
|
|
5
|
-
export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields } from './media.js';
|
|
6
|
+
export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields, collectSegmentMedia, type SegmentMediaRef, } from './media.js';
|
|
6
7
|
export { createImageSegment } from './image.js';
|
|
7
8
|
export { formatSegmentPreview } from './preview.js';
|
|
9
|
+
export { segmentsToPlainText } from './text.js';
|
|
8
10
|
export { readMentionTarget, readMentionName, readMentionSegmentTarget } from './mention.js';
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { mediaRefSchema, textSegmentSchema, mentionSegmentSchema, imageSegmentSchema, replySegmentSchema, forwardSegmentSchema, faceSegmentSchema, diceSegmentSchema, rpsSegmentSchema, canonicalSegmentSchema, segmentArraySchema, } from './validate.js';
|
|
2
2
|
export { assertCanonicalSegments, isCanonicalSegment } from './assert.js';
|
|
3
|
+
export { mediaRefJsonSchema, outboundSegmentJsonSchema, aiOutboundJsonSchema, STRICT_OUTBOUND_SEGMENT_TYPES, } from './json-schema.js';
|
|
3
4
|
export { segmentsForImDelivery } from './delivery.js';
|
|
4
|
-
export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields } from './media.js';
|
|
5
|
+
export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields, collectSegmentMedia, } from './media.js';
|
|
5
6
|
export { createImageSegment } from './image.js';
|
|
6
7
|
export { formatSegmentPreview } from './preview.js';
|
|
8
|
+
export { segmentsToPlainText } from './text.js';
|
|
7
9
|
export { readMentionTarget, readMentionName, readMentionSegmentTarget } from './mention.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical outbound segment 的 JSON Schema SSOT —— 供 AI structured output
|
|
3
|
+
* (AI SDK `Output.object({ schema })`)约束模型直接输出 zhin 消息段数组。
|
|
4
|
+
*
|
|
5
|
+
* 与 validate.ts(@zhin.js/schema 运行时校验)表达同一组约束的 JSON Schema 形态;
|
|
6
|
+
* 严格段类型集合必须与 assert.ts 的 STRICT_CANONICAL_TYPES 保持一致
|
|
7
|
+
* (tests/segment-contract/json-schema.test.ts 有交叉防漂移测试)。
|
|
8
|
+
*
|
|
9
|
+
* 注意:解析侧 parseOutboundSegment 对严格段要求 canonical 形态
|
|
10
|
+
* (如 image 必须携带 data.media: MediaRef),本 schema 与之对齐。
|
|
11
|
+
*/
|
|
12
|
+
type JsonSchemaObject = Record<string, unknown>;
|
|
13
|
+
/** MediaRef(canonical 媒体引用):types.ts 的 MediaRef 接口 */
|
|
14
|
+
export declare const mediaRefJsonSchema: JsonSchemaObject;
|
|
15
|
+
/** 严格段类型集合(SSOT:assert.ts STRICT_CANONICAL_TYPES) */
|
|
16
|
+
export declare const STRICT_OUTBOUND_SEGMENT_TYPES: readonly ["text", "mention", "image", "reply", "forward", "face", "dice", "rps"];
|
|
17
|
+
/**
|
|
18
|
+
* 单条 outbound 消息段的 JSON Schema。
|
|
19
|
+
* 严格段镜像 validate.ts 的 data 约束;宽松段走 generic 分支。
|
|
20
|
+
*/
|
|
21
|
+
export declare const outboundSegmentJsonSchema: JsonSchemaObject;
|
|
22
|
+
/**
|
|
23
|
+
* AI 结构化出站根对象(ADR 0025 JSON DSL 的 schema 形态)。
|
|
24
|
+
* 与 parseAiOutboundJson / ZhinAiOutboundPayload 对齐:text、mentions、segments
|
|
25
|
+
* 均为可选(provider strict 模式兼容性考虑),"至少一项"由 prompt 与下游校验兜底。
|
|
26
|
+
*/
|
|
27
|
+
export declare const aiOutboundJsonSchema: JsonSchemaObject;
|
|
28
|
+
export {};
|