@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.
@@ -0,0 +1,59 @@
1
+ import { htmlToFallbackText } from '../../built/html-to-text.js';
2
+ const DEFAULT_CARD_WIDTH = 540;
3
+ const DEFAULT_CARD_FILENAME = 'card.png';
4
+ export function isOutboundSegment(value) {
5
+ return typeof value === 'object'
6
+ && value !== null
7
+ && !Array.isArray(value)
8
+ && typeof value.type === 'string';
9
+ }
10
+ /**
11
+ * Normalize a rendered outbound payload to wire segments:
12
+ * - segment arrays stay arrays (html segments converted per element);
13
+ * - a single segment object is wrapped into a one-element array;
14
+ * - anything else (plain strings, legacy `{ text }` shorthands) passes through.
15
+ */
16
+ export async function normalizeOutboundPayload(payload, renderer) {
17
+ if (Array.isArray(payload)) {
18
+ return Promise.all(payload.map((item) => normalizeOutboundSegment(item, renderer)));
19
+ }
20
+ if (isOutboundSegment(payload)) {
21
+ return [await normalizeOutboundSegment(payload, renderer)];
22
+ }
23
+ return payload;
24
+ }
25
+ async function normalizeOutboundSegment(segment, renderer) {
26
+ if (!isOutboundSegment(segment) || segment.type !== 'html')
27
+ return segment;
28
+ const data = segment.data ?? {};
29
+ const html = typeof data.html === 'string' ? data.html : '';
30
+ if (html && renderer) {
31
+ try {
32
+ const result = await renderer.render(html, {
33
+ width: typeof data.width === 'number' ? data.width : DEFAULT_CARD_WIDTH,
34
+ format: 'png',
35
+ ...(typeof data.backgroundColor === 'string'
36
+ ? { backgroundColor: data.backgroundColor }
37
+ : {}),
38
+ });
39
+ if (result.format === 'png' && result.data && typeof result.data === 'object') {
40
+ return {
41
+ type: 'image',
42
+ data: {
43
+ base64: Buffer.from(result.data).toString('base64'),
44
+ name: typeof data.fileName === 'string' ? data.fileName : DEFAULT_CARD_FILENAME,
45
+ },
46
+ };
47
+ }
48
+ }
49
+ catch {
50
+ // 渲染失败 → 文本降级
51
+ }
52
+ }
53
+ return { type: 'text', data: { text: htmlSegmentFallbackText(data, html) } };
54
+ }
55
+ function htmlSegmentFallbackText(data, html) {
56
+ if (typeof data.text === 'string' && data.text.length > 0)
57
+ return data.text;
58
+ return html ? htmlToFallbackText(html) : '';
59
+ }
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 zodFieldToJsonSchema(z) {
14
- if (!z || !z._def)
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 def = z._def;
17
- const typeName = def.typeName;
18
- if (typeName === 'ZodOptional' || typeName === 'ZodDefault') {
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
- if (typeName === 'ZodString') {
23
- const out = { type: 'string' };
24
- if (def.description)
25
- out.description = def.description;
26
- return out;
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 (typeName === 'ZodNumber') {
29
- const out = { type: 'number' };
30
- if (def.description)
31
- out.description = def.description;
32
- return out;
81
+ if (kind === 'ZodArray' || kind === 'array') {
82
+ return {
83
+ ...withDescription('array'),
84
+ items: zodFieldToJsonSchema(def.element ?? def.type),
85
+ };
33
86
  }
34
- if (typeName === 'ZodBoolean') {
35
- const out = { type: 'boolean' };
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
- if (typeName === 'ZodEnum') {
41
- return { type: 'string', enum: def.values };
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
- if (typeName === 'ZodArray') {
44
- return { type: 'array', items: zodFieldToJsonSchema(def.type ?? def.element) };
116
+ catch {
117
+ return undefined;
45
118
  }
46
- return { type: 'string' };
47
119
  }
48
- function zodToJsonSchema(schema) {
49
- const result = {
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
- if (!schema || !schema.shape)
55
- return result;
56
- const shape = schema.shape;
57
- const properties = result.properties;
58
- const required = [];
59
- for (const [key, value] of Object.entries(shape)) {
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
- result.required = required.length > 0 ? required : undefined;
68
- return result;
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 = zodToJsonSchema(schema);
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.safeParse(args);
85
- if (!parsed.success) {
86
- const msg = parsed.error.errors?.map((e) => `${e.path?.join('.') ?? 'root'}: ${e instanceof Error ? e.message : String(e)}`).join('; ') ?? 'Invalid arguments';
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.4",
3
+ "version": "1.4.0",
4
4
  "description": "Zhin机器人核心框架",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -21,6 +21,11 @@
21
21
  "development": "./src/built/queue-im-field-contract.ts",
22
22
  "import": "./lib/built/queue-im-field-contract.js"
23
23
  },
24
+ "./runtime": {
25
+ "types": "./lib/plugin-runtime/im/index.d.ts",
26
+ "development": "./src/plugin-runtime/im/index.ts",
27
+ "import": "./lib/plugin-runtime/im/index.js"
28
+ },
24
29
  "./jsx": {
25
30
  "types": "./lib/jsx.d.ts",
26
31
  "development": "./src/jsx.ts",
@@ -45,9 +50,14 @@
45
50
  "segment-matcher": "^1.0.5",
46
51
  "smol-toml": "^1.7.0",
47
52
  "yaml": "^2.9.0",
48
- "@zhin.js/kernel": "1.0.3",
49
- "@zhin.js/database": "1.0.76",
50
- "@zhin.js/logger": "1.0.74",
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",
57
+ "@zhin.js/kernel": "1.0.4",
58
+ "@zhin.js/logger": "1.0.75",
59
+ "@zhin.js/middleware": "1.0.2",
60
+ "@zhin.js/plugin-runtime": "1.1.0",
51
61
  "@zhin.js/schema": "1.0.71"
52
62
  },
53
63
  "peerDependencies": {
@@ -61,8 +71,9 @@
61
71
  "devDependencies": {
62
72
  "@types/node": "^26.1.0",
63
73
  "@types/qrcode": "^1.5.5",
74
+ "ajv": "8.18.0",
64
75
  "typescript": "^6.0.3",
65
- "@zhin.js/ai": "1.4.4"
76
+ "@zhin.js/ai": "1.4.5"
66
77
  },
67
78
  "repository": {
68
79
  "type": "git",