@zhin.js/core 1.3.4 → 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/README.md +12 -0
- package/lib/adapter.js +8 -1
- package/lib/built/authorization.js +2 -2
- package/lib/built/login-assist.d.ts +19 -2
- package/lib/built/login-assist.js +43 -4
- package/lib/built/platform-permit.js +35 -10
- 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/contracts.d.ts +71 -0
- package/lib/plugin-runtime/im/contracts.js +55 -0
- package/lib/plugin-runtime/im/im-runtime.d.ts +109 -0
- package/lib/plugin-runtime/im/im-runtime.js +399 -0
- package/lib/plugin-runtime/im/index.d.ts +5 -0
- package/lib/plugin-runtime/im/index.js +5 -0
- package/lib/plugin-runtime/im/message-dispatcher.d.ts +17 -0
- package/lib/plugin-runtime/im/message-dispatcher.js +50 -0
- package/lib/plugin-runtime/im/outbound-renderer.d.ts +6 -0
- package/lib/plugin-runtime/im/outbound-renderer.js +31 -0
- package/lib/plugin-runtime/im/outbound-segments.d.ts +22 -0
- package/lib/plugin-runtime/im/outbound-segments.js +59 -0
- 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 +16 -5
package/README.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @zhin.js/core
|
|
2
2
|
|
|
3
|
+
## Plugin Runtime 子路径
|
|
4
|
+
|
|
5
|
+
新的 owner-aware IM 能力已经并入 Core,作为 Plugin Runtime 的正式领域接口:
|
|
6
|
+
|
|
7
|
+
- `@zhin.js/core/runtime`
|
|
8
|
+
|
|
9
|
+
`@zhin.js/adapter`、`@zhin.js/command`、`@zhin.js/component` 与
|
|
10
|
+
`@zhin.js/middleware` 提供纯 definition、约定发现 provider 和 generation projection;
|
|
11
|
+
Core Runtime 只消费它们发布的 snapshot。
|
|
12
|
+
旧根入口的 `addCommand`、`addComponent`、`addMiddleware` 暂时作为作者兼容接口保留,后续
|
|
13
|
+
只向 RuntimeSnapshot 投影,不再维护第二套运行时权威。
|
|
14
|
+
|
|
3
15
|
Zhin.js **IM/多通道运行时**包:Plugin、Adapter、**Endpoint**、MessageDispatcher 与统一出站链。**AI 编排(ZhinAgent、工具安全、MCP)在 [`@zhin.js/agent`](../agent/README.md)**;本包仅 selective re-export `@zhin.js/ai` 的 Provider / Agent 原语供插件直接使用。
|
|
4
16
|
|
|
5
17
|
领域词汇见 [CONTEXT.md](./CONTEXT.md);入站/出站流程见 [消息如何流转](../../docs/essentials/message-flow.md)。
|
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--) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mergeAITriggerConfig, resolveSenderRoles, } from './ai-trigger.js';
|
|
2
|
-
import { formatCompact,
|
|
3
|
-
const logger =
|
|
2
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
3
|
+
const logger = getLogger('Authorization');
|
|
4
4
|
function findEndpointEntryFromConfig(config, adapter, endpointId) {
|
|
5
5
|
const endpoints = config.endpoints;
|
|
6
6
|
if (!Array.isArray(endpoints))
|
|
@@ -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,18 +1,25 @@
|
|
|
1
1
|
import { parsePlatformPermitName, isPlatformPermit } from './permit-parse.js';
|
|
2
2
|
const checkers = new Map();
|
|
3
|
-
const
|
|
3
|
+
const defaultSceneRegistrations = new Map();
|
|
4
4
|
export function registerPlatformPermitChecker(adapter, checker) {
|
|
5
5
|
const key = String(adapter);
|
|
6
|
-
checkers.
|
|
6
|
+
const registrations = checkers.get(key) ?? [];
|
|
7
|
+
registrations.push(checker);
|
|
8
|
+
checkers.set(key, registrations);
|
|
7
9
|
return () => {
|
|
8
|
-
|
|
10
|
+
const current = checkers.get(key);
|
|
11
|
+
if (!current)
|
|
12
|
+
return;
|
|
13
|
+
const index = current.lastIndexOf(checker);
|
|
14
|
+
if (index >= 0)
|
|
15
|
+
current.splice(index, 1);
|
|
16
|
+
if (current.length === 0)
|
|
9
17
|
checkers.delete(key);
|
|
10
|
-
}
|
|
11
18
|
};
|
|
12
19
|
}
|
|
13
20
|
export function clearPlatformPermitCheckers() {
|
|
14
21
|
checkers.clear();
|
|
15
|
-
|
|
22
|
+
defaultSceneRegistrations.clear();
|
|
16
23
|
}
|
|
17
24
|
export function checkPlatformPermit(name, message) {
|
|
18
25
|
const parsed = parsePlatformPermitName(name);
|
|
@@ -20,7 +27,8 @@ export function checkPlatformPermit(name, message) {
|
|
|
20
27
|
return false;
|
|
21
28
|
if (String(message.$adapter) !== parsed.adapter)
|
|
22
29
|
return false;
|
|
23
|
-
const
|
|
30
|
+
const registrations = checkers.get(parsed.adapter);
|
|
31
|
+
const checker = registrations?.[registrations.length - 1];
|
|
24
32
|
if (!checker)
|
|
25
33
|
return false;
|
|
26
34
|
return checker(parsed.perm, message);
|
|
@@ -58,9 +66,26 @@ export function createSceneRolePlatformChecker() {
|
|
|
58
66
|
/** 为适配器注册默认场景治理 platform checker(幂等) */
|
|
59
67
|
export function registerDefaultScenePlatformPermitChecker(adapter) {
|
|
60
68
|
const key = String(adapter);
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
const existing = defaultSceneRegistrations.get(key);
|
|
70
|
+
if (existing) {
|
|
71
|
+
existing.references += 1;
|
|
72
|
+
return () => releaseDefaultSceneChecker(key, existing);
|
|
63
73
|
}
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
const checker = createSceneRolePlatformChecker();
|
|
75
|
+
const registration = {
|
|
76
|
+
checker,
|
|
77
|
+
dispose: registerPlatformPermitChecker(key, checker),
|
|
78
|
+
references: 1,
|
|
79
|
+
};
|
|
80
|
+
defaultSceneRegistrations.set(key, registration);
|
|
81
|
+
return () => releaseDefaultSceneChecker(key, registration);
|
|
82
|
+
}
|
|
83
|
+
function releaseDefaultSceneChecker(key, registration) {
|
|
84
|
+
if (defaultSceneRegistrations.get(key) !== registration)
|
|
85
|
+
return;
|
|
86
|
+
registration.references -= 1;
|
|
87
|
+
if (registration.references > 0)
|
|
88
|
+
return;
|
|
89
|
+
defaultSceneRegistrations.delete(key);
|
|
90
|
+
registration.dispose();
|
|
66
91
|
}
|
|
@@ -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';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CapabilityId, PluginId } from '@zhin.js/plugin-runtime';
|
|
2
|
+
declare const componentCallBrand: "zhin.component-call/1";
|
|
3
|
+
declare const rawContentBrand: "zhin.raw-content/1";
|
|
4
|
+
export interface ComponentCall<TProps = unknown> {
|
|
5
|
+
readonly $content: typeof componentCallBrand;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly props: TProps;
|
|
8
|
+
}
|
|
9
|
+
export interface RawContent<TPayload = unknown> {
|
|
10
|
+
readonly $content: typeof rawContentBrand;
|
|
11
|
+
readonly payload: TPayload;
|
|
12
|
+
}
|
|
13
|
+
export type SendContent = string | ComponentCall | RawContent | readonly SendContent[];
|
|
14
|
+
export declare function component<TProps>(name: string, props: TProps): ComponentCall<TProps>;
|
|
15
|
+
export declare function raw<TPayload>(payload: TPayload): RawContent<TPayload>;
|
|
16
|
+
export declare function isComponentCall(value: SendContent): value is ComponentCall;
|
|
17
|
+
export declare function isRawContent(value: SendContent): value is RawContent;
|
|
18
|
+
export interface IncomingMessage {
|
|
19
|
+
readonly adapter: CapabilityId;
|
|
20
|
+
readonly target: string;
|
|
21
|
+
readonly content: string;
|
|
22
|
+
readonly id?: string;
|
|
23
|
+
readonly sender?: string;
|
|
24
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
25
|
+
}
|
|
26
|
+
export interface SendRequest {
|
|
27
|
+
readonly adapter: CapabilityId;
|
|
28
|
+
readonly target: string;
|
|
29
|
+
readonly requester: PluginId;
|
|
30
|
+
readonly content: SendContent;
|
|
31
|
+
readonly parent?: ChannelParent;
|
|
32
|
+
}
|
|
33
|
+
/** Console 通道的来源场景(群临时会话 parent.group / QQ 子频道 parent.guild)。 */
|
|
34
|
+
export interface ChannelParent {
|
|
35
|
+
readonly type?: string;
|
|
36
|
+
readonly id?: string;
|
|
37
|
+
readonly name?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface OutboundEnvelope {
|
|
40
|
+
readonly adapter: CapabilityId;
|
|
41
|
+
readonly target: string;
|
|
42
|
+
readonly requester: PluginId;
|
|
43
|
+
readonly generation: number;
|
|
44
|
+
readonly payload: unknown;
|
|
45
|
+
readonly parent?: ChannelParent;
|
|
46
|
+
replace(payload: unknown): void;
|
|
47
|
+
}
|
|
48
|
+
export interface MessageGateway {
|
|
49
|
+
receive(input: IncomingMessage): Promise<MessageDispatchResult>;
|
|
50
|
+
send(request: SendRequest): Promise<unknown>;
|
|
51
|
+
}
|
|
52
|
+
export interface MessageDispatchResult {
|
|
53
|
+
readonly matched: boolean;
|
|
54
|
+
readonly command?: string;
|
|
55
|
+
readonly owner?: PluginId;
|
|
56
|
+
readonly value?: unknown;
|
|
57
|
+
}
|
|
58
|
+
export declare class Message {
|
|
59
|
+
readonly adapter: CapabilityId;
|
|
60
|
+
readonly target: string;
|
|
61
|
+
readonly content: string;
|
|
62
|
+
readonly generation: number;
|
|
63
|
+
readonly id?: string | undefined;
|
|
64
|
+
readonly sender?: string | undefined;
|
|
65
|
+
readonly metadata: Readonly<Record<string, unknown>>;
|
|
66
|
+
constructor(adapter: CapabilityId, target: string, content: string, generation: number, reply: (content: SendContent, requester?: PluginId) => Promise<unknown>, id?: string | undefined, sender?: string | undefined, metadata?: Readonly<Record<string, unknown>>);
|
|
67
|
+
readonly $reply: (content: SendContent) => Promise<unknown>;
|
|
68
|
+
readonly $replyFrom: (requester: PluginId, content: SendContent) => Promise<unknown>;
|
|
69
|
+
}
|
|
70
|
+
export declare function createOutboundEnvelope(request: Omit<OutboundEnvelope, 'payload' | 'replace'>, initialPayload: unknown): OutboundEnvelope;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const componentCallBrand = 'zhin.component-call/1';
|
|
2
|
+
const rawContentBrand = 'zhin.raw-content/1';
|
|
3
|
+
export function component(name, props) {
|
|
4
|
+
if (!name.trim())
|
|
5
|
+
throw new TypeError('Component name cannot be empty');
|
|
6
|
+
return Object.freeze({ $content: componentCallBrand, name, props });
|
|
7
|
+
}
|
|
8
|
+
export function raw(payload) {
|
|
9
|
+
return Object.freeze({ $content: rawContentBrand, payload });
|
|
10
|
+
}
|
|
11
|
+
export function isComponentCall(value) {
|
|
12
|
+
return !Array.isArray(value)
|
|
13
|
+
&& typeof value === 'object'
|
|
14
|
+
&& value !== null
|
|
15
|
+
&& '$content' in value
|
|
16
|
+
&& value.$content === componentCallBrand;
|
|
17
|
+
}
|
|
18
|
+
export function isRawContent(value) {
|
|
19
|
+
return !Array.isArray(value)
|
|
20
|
+
&& typeof value === 'object'
|
|
21
|
+
&& value !== null
|
|
22
|
+
&& '$content' in value
|
|
23
|
+
&& value.$content === rawContentBrand;
|
|
24
|
+
}
|
|
25
|
+
export class Message {
|
|
26
|
+
adapter;
|
|
27
|
+
target;
|
|
28
|
+
content;
|
|
29
|
+
generation;
|
|
30
|
+
id;
|
|
31
|
+
sender;
|
|
32
|
+
metadata;
|
|
33
|
+
constructor(adapter, target, content, generation, reply, id, sender, metadata = Object.freeze({})) {
|
|
34
|
+
this.adapter = adapter;
|
|
35
|
+
this.target = target;
|
|
36
|
+
this.content = content;
|
|
37
|
+
this.generation = generation;
|
|
38
|
+
this.id = id;
|
|
39
|
+
this.sender = sender;
|
|
40
|
+
this.metadata = metadata;
|
|
41
|
+
this.$reply = (content) => reply(content);
|
|
42
|
+
this.$replyFrom = (requester, content) => reply(content, requester);
|
|
43
|
+
Object.freeze(this);
|
|
44
|
+
}
|
|
45
|
+
$reply;
|
|
46
|
+
$replyFrom;
|
|
47
|
+
}
|
|
48
|
+
export function createOutboundEnvelope(request, initialPayload) {
|
|
49
|
+
let payload = initialPayload;
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
...request,
|
|
52
|
+
get payload() { return payload; },
|
|
53
|
+
replace(next) { payload = next; },
|
|
54
|
+
});
|
|
55
|
+
}
|