@zhin.js/core 1.3.5 → 1.4.0
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/lib/adapter.js +8 -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 +1 -0
- package/lib/built/segment-contract/index.js +1 -0
- package/lib/built/segment-contract/json-schema.d.ts +28 -0
- package/lib/built/segment-contract/json-schema.js +128 -0
- package/lib/endpoint.d.ts +4 -1
- package/lib/endpoint.js +1 -0
- package/lib/plugin-runtime/im/im-runtime.d.ts +41 -2
- package/lib/plugin-runtime/im/im-runtime.js +105 -5
- package/lib/plugin-runtime/im/message-dispatcher.d.ts +12 -2
- package/lib/plugin-runtime/im/message-dispatcher.js +32 -8
- package/lib/plugin.d.ts +1 -0
- 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 +8 -7
package/lib/adapter.js
CHANGED
|
@@ -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 的隐式管线) */
|
|
@@ -291,6 +295,9 @@ export class Adapter extends EventEmitter {
|
|
|
291
295
|
}
|
|
292
296
|
// 无论是否有错误,始终完成清理
|
|
293
297
|
this.endpoints.clear();
|
|
298
|
+
// Drop in-flight concurrency counter so a subsequent start() does not
|
|
299
|
+
// inherit a stale backpressure budget from the previous generation.
|
|
300
|
+
this.#pendingMessages = 0;
|
|
294
301
|
// 从 adapters 数组中移除(可能因重复 start 出现多条同名,需全部删掉)
|
|
295
302
|
const rootAdapters = this.plugin.root.adapters;
|
|
296
303
|
for (let i = rootAdapters.length - 1; i >= 0; i--) {
|
|
@@ -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,6 +1,7 @@
|
|
|
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
6
|
export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields } from './media.js';
|
|
6
7
|
export { createImageSegment } from './image.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
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
5
|
export { isMediaRef, mediaRefFromLegacyData, mediaRefToLegacyFields } from './media.js';
|
|
5
6
|
export { createImageSegment } from './image.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 {};
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
const platformJsonSchema = {
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: true,
|
|
15
|
+
description: '平台专有字段(一般无需输出)',
|
|
16
|
+
};
|
|
17
|
+
/** MediaRef(canonical 媒体引用):types.ts 的 MediaRef 接口 */
|
|
18
|
+
export const mediaRefJsonSchema = {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
kind: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
enum: ['url', 'path', 'base64'],
|
|
24
|
+
description: 'url=http(s) 链接;path=本地文件路径;base64=内联 base64 数据',
|
|
25
|
+
},
|
|
26
|
+
value: { type: 'string', description: '媒体内容:URL / 文件路径 / 纯 base64' },
|
|
27
|
+
mime_type: { type: 'string', description: '如 image/png、audio/mpeg' },
|
|
28
|
+
},
|
|
29
|
+
required: ['kind', 'value'],
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
};
|
|
32
|
+
function strictBranch(type, data) {
|
|
33
|
+
return {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
type: { const: type },
|
|
37
|
+
data,
|
|
38
|
+
platform: platformJsonSchema,
|
|
39
|
+
},
|
|
40
|
+
required: ['type', 'data'],
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function dataObject(properties, required) {
|
|
45
|
+
return { type: 'object', properties, required: [...required], additionalProperties: false };
|
|
46
|
+
}
|
|
47
|
+
/** 宽松分支:未纳入严格契约的段类型,仅约束顶层形状(与 isCanonicalSegment 宽松路径一致) */
|
|
48
|
+
const looseBranch = {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
type: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
enum: ['video', 'audio', 'voice', 'record', 'file', 'link', 'markdown', 'html', 'keyboard', 'action'],
|
|
54
|
+
},
|
|
55
|
+
data: { type: 'object' },
|
|
56
|
+
platform: platformJsonSchema,
|
|
57
|
+
},
|
|
58
|
+
required: ['type', 'data'],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
};
|
|
61
|
+
/** 严格段类型集合(SSOT:assert.ts STRICT_CANONICAL_TYPES) */
|
|
62
|
+
export const STRICT_OUTBOUND_SEGMENT_TYPES = [
|
|
63
|
+
'text', 'mention', 'image', 'reply', 'forward', 'face', 'dice', 'rps',
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* 单条 outbound 消息段的 JSON Schema。
|
|
67
|
+
* 严格段镜像 validate.ts 的 data 约束;宽松段走 generic 分支。
|
|
68
|
+
*/
|
|
69
|
+
export const outboundSegmentJsonSchema = {
|
|
70
|
+
description: 'zhin 消息段 {type, data, platform?}',
|
|
71
|
+
anyOf: [
|
|
72
|
+
strictBranch('text', dataObject({
|
|
73
|
+
text: { type: 'string', description: '文本内容' },
|
|
74
|
+
}, ['text'])),
|
|
75
|
+
strictBranch('mention', dataObject({
|
|
76
|
+
target: { type: 'string', description: '被 @ 用户的平台 id' },
|
|
77
|
+
name: { type: 'string' },
|
|
78
|
+
}, ['target'])),
|
|
79
|
+
strictBranch('image', dataObject({
|
|
80
|
+
media: mediaRefJsonSchema,
|
|
81
|
+
alt: { type: 'string' },
|
|
82
|
+
}, ['media'])),
|
|
83
|
+
strictBranch('reply', dataObject({
|
|
84
|
+
message_id: { type: 'string', description: '被引用消息的平台消息 id' },
|
|
85
|
+
}, ['message_id'])),
|
|
86
|
+
strictBranch('forward', dataObject({
|
|
87
|
+
forward_id: { type: 'string' },
|
|
88
|
+
title: { type: 'string' },
|
|
89
|
+
messages: { type: 'array', items: { type: 'array' } },
|
|
90
|
+
}, ['forward_id'])),
|
|
91
|
+
strictBranch('face', dataObject({
|
|
92
|
+
id: { anyOf: [{ type: 'string' }, { type: 'number' }], description: '平台表情 id' },
|
|
93
|
+
name: { type: 'string' },
|
|
94
|
+
}, ['id'])),
|
|
95
|
+
strictBranch('dice', dataObject({
|
|
96
|
+
result: { type: 'number' },
|
|
97
|
+
}, [])),
|
|
98
|
+
strictBranch('rps', dataObject({
|
|
99
|
+
result: { type: 'number' },
|
|
100
|
+
}, [])),
|
|
101
|
+
looseBranch,
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* AI 结构化出站根对象(ADR 0025 JSON DSL 的 schema 形态)。
|
|
106
|
+
* 与 parseAiOutboundJson / ZhinAiOutboundPayload 对齐:text、mentions、segments
|
|
107
|
+
* 均为可选(provider strict 模式兼容性考虑),"至少一项"由 prompt 与下游校验兜底。
|
|
108
|
+
*/
|
|
109
|
+
export const aiOutboundJsonSchema = {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
text: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
description: '纯文本回复正文;与 segments 至少输出一项。使用 mentions 时必填',
|
|
115
|
+
},
|
|
116
|
+
mentions: {
|
|
117
|
+
type: 'array',
|
|
118
|
+
items: { type: 'string' },
|
|
119
|
+
description: '要 @ 的会话成员引用(昵称或 id,由宿主解析为平台账号);需配合 text 使用',
|
|
120
|
+
},
|
|
121
|
+
segments: {
|
|
122
|
+
type: 'array',
|
|
123
|
+
items: outboundSegmentJsonSchema,
|
|
124
|
+
description: 'zhin 消息段数组,如 [{type:"image",data:{media:{kind:"url",value:"https://…"}}},{type:"text",data:{text:"…"}}]',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
additionalProperties: false,
|
|
128
|
+
};
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { Adapters, Adapter } from './adapter.js';
|
|
2
2
|
import type { EndpointCapabilitiesConfig, FullEndpoint } from './endpoint-capabilities.js';
|
|
3
|
+
import type { EndpointWithManagement } from '@zhin.js/adapter';
|
|
4
|
+
export type { EndpointChannel, EndpointChannelParent, EndpointFriend, EndpointGroup, EndpointManagement, EndpointWithManagement, EndpointManagementCapability, } from '@zhin.js/adapter';
|
|
5
|
+
export { endpointManagementCapabilityIds, listEndpointManagementCapabilities, resolveEndpointManagement, } from '@zhin.js/adapter';
|
|
3
6
|
export type { EndpointCapability, EndpointCapabilitiesConfig, InboundEndpoint, OutboundEndpoint, FullEndpoint, CapableEndpoint, } from './endpoint-capabilities.js';
|
|
4
7
|
export { DEFAULT_ENDPOINT_CAPABILITIES, OutboundNotSupportedError, InboundNotSupportedError, resolveEndpointCapabilities, registerEndpointCapabilities, getEndpointCapabilities, getAdapterCapabilities, hasInbound, hasOutbound, assertInbound, assertOutbound, } from './endpoint-capabilities.js';
|
|
5
8
|
/**
|
|
6
9
|
* Endpoint 接口:全双工平台机器人(入站 + 出站)。
|
|
7
10
|
* 纯入站 / 纯出站请实现 InboundEndpoint / OutboundEndpoint。
|
|
8
11
|
*/
|
|
9
|
-
export type Endpoint<Config extends object = object, Event extends object = object> = FullEndpoint<Config, Event
|
|
12
|
+
export type Endpoint<Config extends object = object, Event extends object = object> = FullEndpoint<Config, Event> & EndpointWithManagement;
|
|
10
13
|
export declare namespace Endpoint {
|
|
11
14
|
type Config<K extends keyof Adapters = keyof Adapters> = Adapter.EndpointConfig<Adapter.InferEndpoint<Adapters[K]>> & EndpointCapabilitiesConfig & {
|
|
12
15
|
context: K;
|
package/lib/endpoint.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
+
export { endpointManagementCapabilityIds, listEndpointManagementCapabilities, resolveEndpointManagement, } from '@zhin.js/adapter';
|
|
1
2
|
export { DEFAULT_ENDPOINT_CAPABILITIES, OutboundNotSupportedError, InboundNotSupportedError, resolveEndpointCapabilities, registerEndpointCapabilities, getEndpointCapabilities, getAdapterCapabilities, hasInbound, hasOutbound, assertInbound, assertOutbound, } from './endpoint-capabilities.js';
|
|
@@ -1,8 +1,30 @@
|
|
|
1
|
-
import { Scope, type PluginId, type RuntimeSnapshot, type SnapshotStore } from '@zhin.js/plugin-runtime';
|
|
1
|
+
import { Scope, type CapabilityId, type PluginId, type RuntimeSnapshot, type SnapshotStore } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { type EndpointManagement, type EndpointManagementCapability } from '@zhin.js/adapter';
|
|
2
3
|
import { Message, type ChannelParent, type IncomingMessage, type MessageDispatchResult, type MessageGateway, type SendRequest } from './contracts.js';
|
|
3
4
|
import { OutboundRenderer } from './outbound-renderer.js';
|
|
4
5
|
export declare const messageGatewayToken: import("@zhin.js/plugin-runtime").Token<MessageGateway>;
|
|
6
|
+
/** Console 实时消息事件(SSE 推送源;content 仅为截断预览,不含完整原始段)。 */
|
|
7
|
+
export interface RuntimeMessageEvent {
|
|
8
|
+
readonly direction: 'inbound' | 'outbound';
|
|
9
|
+
readonly adapter: CapabilityId;
|
|
10
|
+
readonly target: string;
|
|
11
|
+
/** inbound:发送者 id。 */
|
|
12
|
+
readonly sender?: string;
|
|
13
|
+
/** outbound:发起方插件。 */
|
|
14
|
+
readonly requester?: PluginId;
|
|
15
|
+
/** inbound:从 target 前缀解析的场景(`group:xx` → `group`)。 */
|
|
16
|
+
readonly channelType?: string;
|
|
17
|
+
/** 预览文本,截断至 200 字。 */
|
|
18
|
+
readonly contentPreview: string;
|
|
19
|
+
readonly messageId?: string;
|
|
20
|
+
readonly timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
export declare const messagePreviewLimit = 200;
|
|
5
23
|
export interface ImRuntimeOptions {
|
|
24
|
+
/**
|
|
25
|
+
* 全局静态命令前缀(如 `'/'`)。缺省时按适配器实例 config 的
|
|
26
|
+
* `commandPrefix` 解析(`endpoints[i]` 可逐项覆盖),默认 `''` 无前缀。
|
|
27
|
+
*/
|
|
6
28
|
readonly commandPrefix?: string;
|
|
7
29
|
readonly renderer?: OutboundRenderer;
|
|
8
30
|
}
|
|
@@ -17,15 +39,22 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
17
39
|
*/
|
|
18
40
|
setUnmatchedHandler(handler: (message: Message, snapshot: RuntimeSnapshot, requester: PluginId) => Promise<boolean>): void;
|
|
19
41
|
install(resources: Scope): void;
|
|
42
|
+
/**
|
|
43
|
+
* 订阅消息事件(入站 dispatch 完成后 / 出站发送成功后回调)。
|
|
44
|
+
* 返回注销函数。listener 抛错不会阻断消息链路。
|
|
45
|
+
*/
|
|
46
|
+
onMessage(listener: (event: RuntimeMessageEvent) => void): () => void;
|
|
20
47
|
receive(input: IncomingMessage): Promise<MessageDispatchResult>;
|
|
21
48
|
send(request: SendRequest): Promise<unknown>;
|
|
22
49
|
/** Console `endpoint.list` — empty until Adapter Feature projection is ready. */
|
|
23
50
|
listEndpoints(): readonly {
|
|
24
51
|
readonly name: string;
|
|
25
52
|
readonly adapter: string;
|
|
53
|
+
readonly owner: string;
|
|
26
54
|
readonly connected: boolean;
|
|
27
55
|
readonly status: 'online' | 'offline';
|
|
28
56
|
readonly phase: 'pending' | 'starting' | 'online' | 'failed' | 'unconfigured';
|
|
57
|
+
readonly managementCapabilities: readonly EndpointManagementCapability[];
|
|
29
58
|
}[];
|
|
30
59
|
getEndpoint(adapter: string, endpointId: string): {
|
|
31
60
|
readonly name: string;
|
|
@@ -33,6 +62,7 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
33
62
|
readonly connected: boolean;
|
|
34
63
|
readonly status: 'online' | 'offline';
|
|
35
64
|
readonly phase: 'pending' | 'starting' | 'online' | 'failed' | 'unconfigured';
|
|
65
|
+
readonly managementCapabilities: readonly EndpointManagementCapability[];
|
|
36
66
|
} | null;
|
|
37
67
|
sendEndpointMessage(input: {
|
|
38
68
|
readonly adapter: string;
|
|
@@ -65,6 +95,15 @@ export declare class ImRuntime implements MessageGateway {
|
|
|
65
95
|
readonly endpointId: string;
|
|
66
96
|
readonly messageId: string;
|
|
67
97
|
}): Promise<void>;
|
|
68
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Console endpoint 社交/群管 RPC:解析 live Endpoint 实例(无则 null)。
|
|
100
|
+
* @deprecated Host callers should use `getEndpointManagement()`.
|
|
101
|
+
*/
|
|
69
102
|
getLiveEndpoint(adapter: string, endpointId: string): unknown | null;
|
|
103
|
+
/**
|
|
104
|
+
* Narrow Host seam for Console social/group management. An empty object means
|
|
105
|
+
* the Endpoint exists but implements no management operations; null means it
|
|
106
|
+
* cannot be resolved.
|
|
107
|
+
*/
|
|
108
|
+
getEndpointManagement(adapter: string, endpointId: string): EndpointManagement | null;
|
|
70
109
|
}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { createToken, htmlRendererToken, } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import { adapterFeatureId, isAdapterIndex } from '@zhin.js/adapter';
|
|
2
|
+
import { adapterFeatureId, isAdapterIndex, resolveEndpointManagement, } from '@zhin.js/adapter';
|
|
3
3
|
import { isMiddlewareIndex, middlewareFeatureId } from '@zhin.js/middleware';
|
|
4
4
|
import { Message, createOutboundEnvelope, } from './contracts.js';
|
|
5
|
-
import { MessageDispatcher } from './message-dispatcher.js';
|
|
5
|
+
import { defaultCommandPrefixResolver, MessageDispatcher } from './message-dispatcher.js';
|
|
6
6
|
import { OutboundRenderer } from './outbound-renderer.js';
|
|
7
7
|
import { normalizeOutboundPayload } from './outbound-segments.js';
|
|
8
8
|
export const messageGatewayToken = createToken('zhin.im.message-gateway');
|
|
9
|
+
export const messagePreviewLimit = 200;
|
|
9
10
|
export class ImRuntime {
|
|
10
11
|
#dispatcher;
|
|
11
12
|
#renderer;
|
|
13
|
+
#messageListeners = new Set();
|
|
12
14
|
#snapshots;
|
|
13
15
|
#unmatchedHandler;
|
|
14
16
|
constructor(options = {}) {
|
|
15
|
-
this.#dispatcher = new MessageDispatcher(options.commandPrefix
|
|
17
|
+
this.#dispatcher = new MessageDispatcher(options.commandPrefix === undefined
|
|
18
|
+
? defaultCommandPrefixResolver
|
|
19
|
+
: () => options.commandPrefix ?? '');
|
|
16
20
|
this.#renderer = options.renderer ?? new OutboundRenderer();
|
|
17
21
|
}
|
|
18
22
|
attach(snapshots) {
|
|
@@ -32,6 +36,24 @@ export class ImRuntime {
|
|
|
32
36
|
install(resources) {
|
|
33
37
|
resources.provide(messageGatewayToken, this);
|
|
34
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* 订阅消息事件(入站 dispatch 完成后 / 出站发送成功后回调)。
|
|
41
|
+
* 返回注销函数。listener 抛错不会阻断消息链路。
|
|
42
|
+
*/
|
|
43
|
+
onMessage(listener) {
|
|
44
|
+
this.#messageListeners.add(listener);
|
|
45
|
+
return () => { this.#messageListeners.delete(listener); };
|
|
46
|
+
}
|
|
47
|
+
#emitMessage(event) {
|
|
48
|
+
for (const listener of this.#messageListeners) {
|
|
49
|
+
try {
|
|
50
|
+
listener(event);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// listener 异常不得影响消息收发
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
35
57
|
async receive(input) {
|
|
36
58
|
const lease = this.#acquire();
|
|
37
59
|
let active = true;
|
|
@@ -57,6 +79,18 @@ export class ImRuntime {
|
|
|
57
79
|
}
|
|
58
80
|
}
|
|
59
81
|
}, 'inbound');
|
|
82
|
+
this.#emitMessage({
|
|
83
|
+
direction: 'inbound',
|
|
84
|
+
adapter: input.adapter,
|
|
85
|
+
target: input.target,
|
|
86
|
+
...(input.sender !== undefined ? { sender: input.sender } : {}),
|
|
87
|
+
...(channelTypeOf(input.target)
|
|
88
|
+
? { channelType: channelTypeOf(input.target) }
|
|
89
|
+
: {}),
|
|
90
|
+
contentPreview: previewText(input.content),
|
|
91
|
+
...(input.id !== undefined ? { messageId: input.id } : {}),
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
});
|
|
60
94
|
return result;
|
|
61
95
|
}
|
|
62
96
|
finally {
|
|
@@ -82,9 +116,11 @@ export class ImRuntime {
|
|
|
82
116
|
name: row.name,
|
|
83
117
|
// adapter 列显示平台类型(owner 包名去 scope/adapter- 前缀),不是 slot localName
|
|
84
118
|
adapter: adapterTypeName(lease.value.tree.get(row.owner)?.packageName) ?? row.name,
|
|
119
|
+
owner: row.owner,
|
|
85
120
|
connected: row.connected,
|
|
86
121
|
status: row.status,
|
|
87
122
|
phase: row.phase,
|
|
123
|
+
managementCapabilities: row.managementCapabilities,
|
|
88
124
|
}));
|
|
89
125
|
}
|
|
90
126
|
finally {
|
|
@@ -106,12 +142,15 @@ export class ImRuntime {
|
|
|
106
142
|
const row = index.describe().find((item) => item.id === id);
|
|
107
143
|
if (!row)
|
|
108
144
|
return null;
|
|
145
|
+
// adapter 与 listEndpoints 对齐:平台类型(owner 包名去 scope/adapter- 前缀),
|
|
146
|
+
// 不是 live name(如 ICQQ uin)。此前误写 row.name 导致 endpoint.info 与 list 不一致。
|
|
109
147
|
return Object.freeze({
|
|
110
148
|
name: row.name,
|
|
111
|
-
adapter: row.name,
|
|
149
|
+
adapter: adapterTypeName(lease.value.tree.get(row.owner)?.packageName) ?? row.name,
|
|
112
150
|
connected: row.connected,
|
|
113
151
|
status: row.status,
|
|
114
152
|
phase: row.phase,
|
|
153
|
+
managementCapabilities: row.managementCapabilities,
|
|
115
154
|
});
|
|
116
155
|
}
|
|
117
156
|
finally {
|
|
@@ -203,10 +242,24 @@ export class ImRuntime {
|
|
|
203
242
|
return null;
|
|
204
243
|
}
|
|
205
244
|
}
|
|
206
|
-
/**
|
|
245
|
+
/**
|
|
246
|
+
* Console endpoint 社交/群管 RPC:解析 live Endpoint 实例(无则 null)。
|
|
247
|
+
* @deprecated Host callers should use `getEndpointManagement()`.
|
|
248
|
+
*/
|
|
207
249
|
getLiveEndpoint(adapter, endpointId) {
|
|
208
250
|
return this.#liveEndpoint(adapter, endpointId);
|
|
209
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Narrow Host seam for Console social/group management. An empty object means
|
|
254
|
+
* the Endpoint exists but implements no management operations; null means it
|
|
255
|
+
* cannot be resolved.
|
|
256
|
+
*/
|
|
257
|
+
getEndpointManagement(adapter, endpointId) {
|
|
258
|
+
const endpoint = this.#liveEndpoint(adapter, endpointId);
|
|
259
|
+
if (!endpoint)
|
|
260
|
+
return null;
|
|
261
|
+
return resolveEndpointManagement(endpoint) ?? Object.freeze({});
|
|
262
|
+
}
|
|
210
263
|
async #sendWithSnapshot(request, snapshot) {
|
|
211
264
|
const rendered = await this.#renderer.render(request.content, request.requester, snapshot);
|
|
212
265
|
// 单段对象 / html 段在此归一为适配器可消费的 wire 段数组;
|
|
@@ -229,6 +282,14 @@ export class ImRuntime {
|
|
|
229
282
|
...(request.parent ? { parent: request.parent } : {}),
|
|
230
283
|
});
|
|
231
284
|
}, 'outbound');
|
|
285
|
+
this.#emitMessage({
|
|
286
|
+
direction: 'outbound',
|
|
287
|
+
adapter: request.adapter,
|
|
288
|
+
target: request.target,
|
|
289
|
+
requester: request.requester,
|
|
290
|
+
contentPreview: previewText(envelope.payload),
|
|
291
|
+
timestamp: Date.now(),
|
|
292
|
+
});
|
|
232
293
|
return result;
|
|
233
294
|
}
|
|
234
295
|
#acquire() {
|
|
@@ -297,3 +358,42 @@ function normalizeConsoleContent(content) {
|
|
|
297
358
|
return content;
|
|
298
359
|
return String(content);
|
|
299
360
|
}
|
|
361
|
+
/** target 前缀场景:`group:123` → `group`;无前缀返回 undefined。 */
|
|
362
|
+
function channelTypeOf(target) {
|
|
363
|
+
const match = /^([a-z0-9-]+):/iu.exec(target);
|
|
364
|
+
return match?.[1];
|
|
365
|
+
}
|
|
366
|
+
/** 消息内容 → 预览文本(截断 200 字);wire 段取 `data.text`,其余段记 `[type]`。 */
|
|
367
|
+
function previewText(content) {
|
|
368
|
+
const text = flattenContent(content);
|
|
369
|
+
return text.length > messagePreviewLimit
|
|
370
|
+
? `${text.slice(0, messagePreviewLimit)}…`
|
|
371
|
+
: text;
|
|
372
|
+
}
|
|
373
|
+
function flattenContent(content) {
|
|
374
|
+
if (typeof content === 'string')
|
|
375
|
+
return content;
|
|
376
|
+
if (content == null)
|
|
377
|
+
return '';
|
|
378
|
+
if (Array.isArray(content)) {
|
|
379
|
+
return content.map((item) => flattenContent(item)).join('');
|
|
380
|
+
}
|
|
381
|
+
if (typeof content === 'object') {
|
|
382
|
+
const record = content;
|
|
383
|
+
const data = record.data;
|
|
384
|
+
if (typeof record.type === 'string') {
|
|
385
|
+
if (data && typeof data.text === 'string')
|
|
386
|
+
return data.text;
|
|
387
|
+
return `[${record.type}]`;
|
|
388
|
+
}
|
|
389
|
+
if (typeof record.text === 'string')
|
|
390
|
+
return record.text;
|
|
391
|
+
try {
|
|
392
|
+
return JSON.stringify(content) ?? '';
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return String(content);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return String(content);
|
|
399
|
+
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import type { Message, MessageDispatchResult } from './contracts.js';
|
|
3
|
+
/**
|
|
4
|
+
* 命令前缀解析器:返回该消息要求的命令前缀。
|
|
5
|
+
* `''` 表示无前缀(任意文本都尝试按命令匹配)。
|
|
6
|
+
*/
|
|
7
|
+
export type CommandPrefixResolver = (message: Message, snapshot: RuntimeSnapshot) => string;
|
|
8
|
+
/**
|
|
9
|
+
* 默认解析:读消息所属适配器实例 config 的 `commandPrefix`(默认 `''`);
|
|
10
|
+
* 实例声明 `endpoints` 数组时,按消息 endpoint 名找 entry,`entry.commandPrefix` 覆盖顶层。
|
|
11
|
+
*/
|
|
12
|
+
export declare const defaultCommandPrefixResolver: CommandPrefixResolver;
|
|
3
13
|
export declare class MessageDispatcher {
|
|
4
|
-
private readonly
|
|
5
|
-
constructor(
|
|
14
|
+
private readonly resolvePrefix;
|
|
15
|
+
constructor(resolvePrefix?: CommandPrefixResolver);
|
|
6
16
|
dispatch(message: Message, snapshot: RuntimeSnapshot): Promise<MessageDispatchResult>;
|
|
7
17
|
}
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { commandFeatureId, isCommandIndex } from '@zhin.js/command';
|
|
2
|
+
function ownerOfMessage(message) {
|
|
3
|
+
return String(message.adapter).split('\0')[0];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* 默认解析:读消息所属适配器实例 config 的 `commandPrefix`(默认 `''`);
|
|
7
|
+
* 实例声明 `endpoints` 数组时,按消息 endpoint 名找 entry,`entry.commandPrefix` 覆盖顶层。
|
|
8
|
+
*/
|
|
9
|
+
export const defaultCommandPrefixResolver = (message, snapshot) => {
|
|
10
|
+
const config = snapshot.config.get(ownerOfMessage(message));
|
|
11
|
+
if (!config)
|
|
12
|
+
return '';
|
|
13
|
+
const endpointName = typeof message.metadata?.endpoint === 'string'
|
|
14
|
+
? message.metadata.endpoint
|
|
15
|
+
: undefined;
|
|
16
|
+
if (endpointName && Array.isArray(config.endpoints)) {
|
|
17
|
+
const entry = config.endpoints.find((item) => !!item && typeof item === 'object'
|
|
18
|
+
&& item.name === endpointName);
|
|
19
|
+
if (typeof entry?.commandPrefix === 'string')
|
|
20
|
+
return entry.commandPrefix;
|
|
21
|
+
}
|
|
22
|
+
return typeof config.commandPrefix === 'string' ? config.commandPrefix : '';
|
|
23
|
+
};
|
|
2
24
|
export class MessageDispatcher {
|
|
3
|
-
|
|
4
|
-
constructor(
|
|
5
|
-
this.
|
|
6
|
-
if (!prefix)
|
|
7
|
-
throw new TypeError('Command prefix cannot be empty');
|
|
25
|
+
resolvePrefix;
|
|
26
|
+
constructor(resolvePrefix = defaultCommandPrefixResolver) {
|
|
27
|
+
this.resolvePrefix = resolvePrefix;
|
|
8
28
|
}
|
|
9
29
|
async dispatch(message, snapshot) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
30
|
+
const prefix = this.resolvePrefix(message, snapshot);
|
|
31
|
+
let input = message.content.trim();
|
|
32
|
+
if (prefix) {
|
|
33
|
+
if (!input.startsWith(prefix))
|
|
34
|
+
return Object.freeze({ matched: false });
|
|
35
|
+
input = input.slice(prefix.length).trim();
|
|
36
|
+
}
|
|
13
37
|
if (!input)
|
|
14
38
|
return Object.freeze({ matched: false });
|
|
15
39
|
const commands = snapshot.projections.get(commandFeatureId);
|
package/lib/plugin.d.ts
CHANGED
|
@@ -180,6 +180,7 @@ export declare namespace Plugin {
|
|
|
180
180
|
'message.send': [MessageSendPayload];
|
|
181
181
|
"message.receive": [import('./message.js').Message];
|
|
182
182
|
"endpoint.login.pending": [import('./built/login-assist.js').PendingLoginTask];
|
|
183
|
+
"endpoint.login.expired": [import('./built/login-assist.js').PendingLoginTask];
|
|
183
184
|
'endpoint.connect': [import('./built/endpoint-lifecycle.js').EndpointLifecyclePayload];
|
|
184
185
|
'endpoint.disconnect': [import('./built/endpoint-lifecycle.js').EndpointLifecyclePayload];
|
|
185
186
|
'endpoint.error': [import('./built/endpoint-lifecycle.js').EndpointLifecyclePayload];
|
package/lib/tool-zod.d.ts
CHANGED
|
@@ -10,9 +10,24 @@
|
|
|
10
10
|
* const tool = createToolFromZod('my_tool', '描述', z.object({ id: z.string() }), async (args) => { ... });
|
|
11
11
|
* plugin.addTool(tool);
|
|
12
12
|
*/
|
|
13
|
-
import type { Tool } from './types.js';
|
|
13
|
+
import type { Tool, ToolParametersSchema } from './types.js';
|
|
14
14
|
import type { Message } from './message.js';
|
|
15
15
|
type MaybePromise<T> = T | Promise<T>;
|
|
16
|
+
export type ToolSchemaParseResult<T> = {
|
|
17
|
+
ok: true;
|
|
18
|
+
data: T;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
error: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Normalize a JSON Schema or Zod object schema into the canonical Core tool
|
|
25
|
+
* parameter schema. Zod remains an optional peer: this adapter uses only its
|
|
26
|
+
* public instance methods and retains a structural fallback for Zod 3.
|
|
27
|
+
*/
|
|
28
|
+
export declare function toolInputSchemaToParameters(schema: unknown): ToolParametersSchema;
|
|
29
|
+
/** Validate input with a Zod-like schema, or pass it through for JSON Schema. */
|
|
30
|
+
export declare function parseToolInputSchema<T>(schema: unknown, input: unknown): ToolSchemaParseResult<T>;
|
|
16
31
|
export interface CreateToolFromZodOptions {
|
|
17
32
|
tags?: string[];
|
|
18
33
|
keywords?: string[];
|
package/lib/tool-zod.js
CHANGED
|
@@ -10,62 +10,151 @@
|
|
|
10
10
|
* const tool = createToolFromZod('my_tool', '描述', z.object({ id: z.string() }), async (args) => { ... });
|
|
11
11
|
* plugin.addTool(tool);
|
|
12
12
|
*/
|
|
13
|
-
function
|
|
14
|
-
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function getShape(schema) {
|
|
17
|
+
const shape = schema?.shape;
|
|
18
|
+
if (typeof shape === 'function') {
|
|
19
|
+
const value = shape();
|
|
20
|
+
return isRecord(value) ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
return isRecord(shape) ? shape : undefined;
|
|
23
|
+
}
|
|
24
|
+
function acceptsUndefined(schema) {
|
|
25
|
+
const candidate = schema;
|
|
26
|
+
if (typeof candidate?.safeParse === 'function') {
|
|
27
|
+
try {
|
|
28
|
+
return candidate.safeParse(undefined).success;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Fall through to structural compatibility for non-Zod lookalikes.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const def = candidate?._def;
|
|
35
|
+
const kind = def?.typeName ?? def?.type;
|
|
36
|
+
return kind === 'ZodOptional'
|
|
37
|
+
|| kind === 'ZodDefault'
|
|
38
|
+
|| kind === 'optional'
|
|
39
|
+
|| kind === 'default';
|
|
40
|
+
}
|
|
41
|
+
function requiredFromShape(schema, fallback) {
|
|
42
|
+
const shape = getShape(schema);
|
|
43
|
+
if (!shape)
|
|
44
|
+
return fallback?.length ? [...fallback] : undefined;
|
|
45
|
+
const required = Object.entries(shape)
|
|
46
|
+
.filter(([, field]) => !acceptsUndefined(field))
|
|
47
|
+
.map(([key]) => key);
|
|
48
|
+
return required.length ? required : undefined;
|
|
49
|
+
}
|
|
50
|
+
function descriptionOf(schema, def) {
|
|
51
|
+
const description = schema?.description ?? def.description;
|
|
52
|
+
return typeof description === 'string' ? description : undefined;
|
|
53
|
+
}
|
|
54
|
+
function zodFieldToJsonSchema(schema) {
|
|
55
|
+
const candidate = schema;
|
|
56
|
+
const def = candidate?._def;
|
|
57
|
+
if (!isRecord(def))
|
|
15
58
|
return { type: 'string' };
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const inner = def.innerType ?? def.type;
|
|
20
|
-
return zodFieldToJsonSchema(inner);
|
|
59
|
+
const kind = def.typeName ?? def.type;
|
|
60
|
+
if (kind === 'ZodOptional' || kind === 'ZodDefault' || kind === 'optional' || kind === 'default') {
|
|
61
|
+
return zodFieldToJsonSchema(def.innerType ?? def.type);
|
|
21
62
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
63
|
+
const description = descriptionOf(schema, def);
|
|
64
|
+
const withDescription = (type) => description
|
|
65
|
+
? { type, description }
|
|
66
|
+
: { type };
|
|
67
|
+
if (kind === 'ZodString' || kind === 'string')
|
|
68
|
+
return withDescription('string');
|
|
69
|
+
if (kind === 'ZodNumber' || kind === 'number')
|
|
70
|
+
return withDescription('number');
|
|
71
|
+
if (kind === 'ZodBoolean' || kind === 'boolean')
|
|
72
|
+
return withDescription('boolean');
|
|
73
|
+
if (kind === 'ZodEnum' || kind === 'enum') {
|
|
74
|
+
const values = Array.isArray(def.values)
|
|
75
|
+
? def.values
|
|
76
|
+
: isRecord(def.entries)
|
|
77
|
+
? Object.values(def.entries)
|
|
78
|
+
: [];
|
|
79
|
+
return { ...withDescription('string'), enum: values };
|
|
27
80
|
}
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
81
|
+
if (kind === 'ZodArray' || kind === 'array') {
|
|
82
|
+
return {
|
|
83
|
+
...withDescription('array'),
|
|
84
|
+
items: zodFieldToJsonSchema(def.element ?? def.type),
|
|
85
|
+
};
|
|
33
86
|
}
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
if (def.description)
|
|
37
|
-
out.description = def.description;
|
|
38
|
-
return out;
|
|
87
|
+
if (kind === 'ZodObject' || kind === 'object') {
|
|
88
|
+
return toolInputSchemaToParameters(schema);
|
|
39
89
|
}
|
|
40
|
-
|
|
41
|
-
|
|
90
|
+
return withDescription('string');
|
|
91
|
+
}
|
|
92
|
+
function isJsonObjectSchema(schema) {
|
|
93
|
+
if (!isRecord(schema) || schema.type !== 'object')
|
|
94
|
+
return false;
|
|
95
|
+
return typeof schema.safeParse !== 'function';
|
|
96
|
+
}
|
|
97
|
+
function fromNativeJsonSchema(schema) {
|
|
98
|
+
const candidate = schema;
|
|
99
|
+
if (typeof candidate?.toJSONSchema !== 'function')
|
|
100
|
+
return undefined;
|
|
101
|
+
try {
|
|
102
|
+
const converted = candidate.toJSONSchema();
|
|
103
|
+
if (!isRecord(converted) || converted.type !== 'object')
|
|
104
|
+
return undefined;
|
|
105
|
+
return {
|
|
106
|
+
...converted,
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: isRecord(converted.properties)
|
|
109
|
+
? converted.properties
|
|
110
|
+
: {},
|
|
111
|
+
required: requiredFromShape(schema, Array.isArray(converted.required)
|
|
112
|
+
? converted.required.filter((key) => typeof key === 'string')
|
|
113
|
+
: undefined),
|
|
114
|
+
};
|
|
42
115
|
}
|
|
43
|
-
|
|
44
|
-
return
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
45
118
|
}
|
|
46
|
-
return { type: 'string' };
|
|
47
119
|
}
|
|
48
|
-
|
|
49
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Normalize a JSON Schema or Zod object schema into the canonical Core tool
|
|
122
|
+
* parameter schema. Zod remains an optional peer: this adapter uses only its
|
|
123
|
+
* public instance methods and retains a structural fallback for Zod 3.
|
|
124
|
+
*/
|
|
125
|
+
export function toolInputSchemaToParameters(schema) {
|
|
126
|
+
if (isJsonObjectSchema(schema))
|
|
127
|
+
return schema;
|
|
128
|
+
const native = fromNativeJsonSchema(schema);
|
|
129
|
+
if (native)
|
|
130
|
+
return native;
|
|
131
|
+
const shape = getShape(schema);
|
|
132
|
+
const properties = {};
|
|
133
|
+
if (shape) {
|
|
134
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
135
|
+
properties[key] = zodFieldToJsonSchema(value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
50
139
|
type: 'object',
|
|
51
|
-
properties:
|
|
52
|
-
required:
|
|
140
|
+
properties: properties,
|
|
141
|
+
required: requiredFromShape(schema),
|
|
53
142
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const zodValue = value;
|
|
61
|
-
properties[key] = zodFieldToJsonSchema(zodValue);
|
|
62
|
-
const typeName = zodValue?._def?.typeName;
|
|
63
|
-
if (typeName !== 'ZodOptional' && typeName !== 'ZodDefault') {
|
|
64
|
-
required.push(key);
|
|
65
|
-
}
|
|
143
|
+
}
|
|
144
|
+
/** Validate input with a Zod-like schema, or pass it through for JSON Schema. */
|
|
145
|
+
export function parseToolInputSchema(schema, input) {
|
|
146
|
+
const candidate = schema;
|
|
147
|
+
if (typeof candidate?.safeParse !== 'function') {
|
|
148
|
+
return { ok: true, data: input };
|
|
66
149
|
}
|
|
67
|
-
|
|
68
|
-
|
|
150
|
+
const parsed = candidate.safeParse(input);
|
|
151
|
+
if (parsed.success)
|
|
152
|
+
return { ok: true, data: parsed.data };
|
|
153
|
+
const issues = parsed.error?.issues ?? parsed.error?.errors ?? [];
|
|
154
|
+
const error = issues
|
|
155
|
+
.map((issue) => `${(issue.path ?? []).join('.') || 'root'}: ${issue.message ?? 'invalid'}`)
|
|
156
|
+
.join('; ');
|
|
157
|
+
return { ok: false, error: error || 'Invalid arguments' };
|
|
69
158
|
}
|
|
70
159
|
/**
|
|
71
160
|
* 从 Zod 模式创建 Tool,便于类型安全与校验。
|
|
@@ -75,17 +164,15 @@ export function createToolFromZod(name, description, schema, execute, options) {
|
|
|
75
164
|
if (!schema?.safeParse) {
|
|
76
165
|
throw new Error('createToolFromZod: schema must be a Zod object schema (e.g. z.object({ ... })). Install zod: pnpm add zod');
|
|
77
166
|
}
|
|
78
|
-
const parameters =
|
|
167
|
+
const parameters = toolInputSchemaToParameters(schema);
|
|
79
168
|
return {
|
|
80
169
|
name,
|
|
81
170
|
description,
|
|
82
171
|
parameters,
|
|
83
172
|
execute: async (args, message) => {
|
|
84
|
-
const parsed = schema
|
|
85
|
-
if (!parsed.
|
|
86
|
-
|
|
87
|
-
return `Error: ${msg}`;
|
|
88
|
-
}
|
|
173
|
+
const parsed = parseToolInputSchema(schema, args);
|
|
174
|
+
if (!parsed.ok)
|
|
175
|
+
return `Error: ${parsed.error}`;
|
|
89
176
|
return execute(parsed.data, message);
|
|
90
177
|
},
|
|
91
178
|
tags: options?.tags,
|
package/lib/utils.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ import type { ButtonData, KeyboardFallback, KeyboardSegmentData } from "./built/
|
|
|
17
17
|
import type { MediaRef } from "./built/segment-contract/types.js";
|
|
18
18
|
/**
|
|
19
19
|
* 组合中间件,洋葱模型
|
|
20
|
+
*
|
|
21
|
+
* 空中间件列表时必须仍调用 `next`——入站管线把 MessageDispatcher
|
|
22
|
+
* 作为 terminal next 传入;吞掉 next 会导致命令/AI 永远不跑。
|
|
20
23
|
*/
|
|
21
24
|
export declare function compose<P extends RegisteredAdapter = RegisteredAdapter>(middlewares: MessageMiddleware<P>[]): (message: Message<AdapterMessage<P>>, next?: () => Promise<void>) => Promise<void>;
|
|
22
25
|
export declare function segment<T extends object>(type: string, data: T): {
|
package/lib/utils.js
CHANGED
|
@@ -15,12 +15,15 @@ import { KeyboardSegment } from "./built/interactive-segments/keyboard-segment.j
|
|
|
15
15
|
import { ButtonSpec, normalizeKeyboardRows } from "./built/interactive-segments/button-spec.js";
|
|
16
16
|
/**
|
|
17
17
|
* 组合中间件,洋葱模型
|
|
18
|
+
*
|
|
19
|
+
* 空中间件列表时必须仍调用 `next`——入站管线把 MessageDispatcher
|
|
20
|
+
* 作为 terminal next 传入;吞掉 next 会导致命令/AI 永远不跑。
|
|
18
21
|
*/
|
|
19
22
|
export function compose(middlewares) {
|
|
20
|
-
if (middlewares.length === 0) {
|
|
21
|
-
return () => Promise.resolve();
|
|
22
|
-
}
|
|
23
23
|
return function (message, next = () => Promise.resolve()) {
|
|
24
|
+
if (middlewares.length === 0) {
|
|
25
|
+
return next();
|
|
26
|
+
}
|
|
24
27
|
let index = -1;
|
|
25
28
|
const dispatch = async (i = 0) => {
|
|
26
29
|
if (i <= index) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Zhin机器人核心框架",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -50,14 +50,14 @@
|
|
|
50
50
|
"segment-matcher": "^1.0.5",
|
|
51
51
|
"smol-toml": "^1.7.0",
|
|
52
52
|
"yaml": "^2.9.0",
|
|
53
|
-
"@zhin.js/adapter": "1.0
|
|
54
|
-
"@zhin.js/command": "1.0.
|
|
55
|
-
"@zhin.js/component": "1.0.
|
|
53
|
+
"@zhin.js/adapter": "1.1.0",
|
|
54
|
+
"@zhin.js/command": "1.0.2",
|
|
55
|
+
"@zhin.js/component": "1.0.2",
|
|
56
|
+
"@zhin.js/database": "1.0.77",
|
|
56
57
|
"@zhin.js/kernel": "1.0.4",
|
|
57
58
|
"@zhin.js/logger": "1.0.75",
|
|
58
|
-
"@zhin.js/middleware": "1.0.
|
|
59
|
-
"@zhin.js/plugin-runtime": "1.0
|
|
60
|
-
"@zhin.js/database": "1.0.77",
|
|
59
|
+
"@zhin.js/middleware": "1.0.2",
|
|
60
|
+
"@zhin.js/plugin-runtime": "1.1.0",
|
|
61
61
|
"@zhin.js/schema": "1.0.71"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/node": "^26.1.0",
|
|
73
73
|
"@types/qrcode": "^1.5.5",
|
|
74
|
+
"ajv": "8.18.0",
|
|
74
75
|
"typescript": "^6.0.3",
|
|
75
76
|
"@zhin.js/ai": "1.4.5"
|
|
76
77
|
},
|