@zhin.js/core 1.0.38 → 1.0.40

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/lib/ai/index.d.ts +10 -5
  3. package/lib/ai/index.d.ts.map +1 -1
  4. package/lib/ai/index.js +7 -4
  5. package/lib/ai/index.js.map +1 -1
  6. package/lib/ai/providers/anthropic.d.ts.map +1 -1
  7. package/lib/ai/providers/anthropic.js +2 -0
  8. package/lib/ai/providers/anthropic.js.map +1 -1
  9. package/lib/ai/providers/openai.d.ts.map +1 -1
  10. package/lib/ai/providers/openai.js +8 -0
  11. package/lib/ai/providers/openai.js.map +1 -1
  12. package/lib/ai/types.d.ts +1 -0
  13. package/lib/ai/types.d.ts.map +1 -1
  14. package/lib/cron.d.ts +2 -43
  15. package/lib/cron.d.ts.map +1 -1
  16. package/lib/cron.js +2 -126
  17. package/lib/cron.js.map +1 -1
  18. package/lib/errors.d.ts +3 -146
  19. package/lib/errors.d.ts.map +1 -1
  20. package/lib/errors.js +3 -279
  21. package/lib/errors.js.map +1 -1
  22. package/lib/feature.d.ts +5 -87
  23. package/lib/feature.d.ts.map +1 -1
  24. package/lib/feature.js +4 -105
  25. package/lib/feature.js.map +1 -1
  26. package/lib/index.d.ts +1 -0
  27. package/lib/index.d.ts.map +1 -1
  28. package/lib/scheduler/index.d.ts +3 -7
  29. package/lib/scheduler/index.d.ts.map +1 -1
  30. package/lib/scheduler/index.js +2 -9
  31. package/lib/scheduler/index.js.map +1 -1
  32. package/lib/types.d.ts +8 -1
  33. package/lib/types.d.ts.map +1 -1
  34. package/lib/utils.d.ts +7 -52
  35. package/lib/utils.d.ts.map +1 -1
  36. package/lib/utils.js +9 -325
  37. package/lib/utils.js.map +1 -1
  38. package/package.json +6 -4
  39. package/src/ai/index.ts +15 -9
  40. package/src/ai/providers/anthropic.ts +1 -0
  41. package/src/ai/providers/openai.ts +5 -1
  42. package/src/ai/types.ts +1 -0
  43. package/src/cron.ts +2 -140
  44. package/src/errors.ts +15 -334
  45. package/src/feature.ts +5 -154
  46. package/src/index.ts +3 -1
  47. package/src/scheduler/index.ts +8 -17
  48. package/src/types.ts +10 -2
  49. package/src/utils.ts +37 -334
  50. package/tests/cron.test.ts +4 -299
  51. package/tests/errors.test.ts +17 -307
  52. package/tests/utils.test.ts +11 -516
  53. package/tests/feature.test.ts +0 -145
@@ -1,7 +1,11 @@
1
1
  /**
2
- * Scheduler module — at / every / cron + heartbeat
2
+ * Re-export from @zhin.js/kernel for backward compatibility.
3
3
  */
4
-
4
+ export {
5
+ Scheduler,
6
+ getScheduler,
7
+ setScheduler,
8
+ } from '@zhin.js/kernel';
5
9
  export type {
6
10
  Schedule,
7
11
  JobPayload,
@@ -11,18 +15,5 @@ export type {
11
15
  JobCallback,
12
16
  AddJobOptions,
13
17
  IScheduler,
14
- } from './types.js';
15
- export { Scheduler } from './scheduler.js';
16
- export type { SchedulerOptions } from './scheduler.js';
17
-
18
- import type { Scheduler } from './scheduler.js';
19
-
20
- let schedulerInstance: Scheduler | null = null;
21
-
22
- export function getScheduler(): Scheduler | null {
23
- return schedulerInstance;
24
- }
25
-
26
- export function setScheduler(s: Scheduler | null): void {
27
- schedulerInstance = s;
28
- }
18
+ SchedulerOptions,
19
+ } from '@zhin.js/kernel';
package/src/types.ts CHANGED
@@ -221,7 +221,8 @@ export interface ToolParametersSchema<TArgs extends Record<string, any> = Record
221
221
 
222
222
  /**
223
223
  * 工具执行上下文
224
- * 包含消息来源、发送者等信息
224
+ * 包含消息来源、发送者等 IM 信息。
225
+ * 通用(IM 无关)版本请使用 @zhin.js/ai 的 ToolContext。
225
226
  */
226
227
  export interface ToolContext {
227
228
  /** 来源平台 */
@@ -462,4 +463,11 @@ export namespace Tool {
462
463
  // ============================================================================
463
464
 
464
465
  /** @deprecated 使用 Tool 替代 */
465
- export type AITool = Tool;
466
+ export type AITool = Tool;
467
+
468
+ /**
469
+ * IMToolContext — ToolContext 的显式 IM 别名。
470
+ * 当同时使用 @zhin.js/ai (通用 ToolContext) 和 @zhin.js/core (IM ToolContext) 时,
471
+ * 用此类型消除歧义。
472
+ */
473
+ export type IMToolContext = ToolContext;
package/src/utils.ts CHANGED
@@ -1,55 +1,46 @@
1
- import * as path from "path";
2
- import * as fs from "fs";
3
- import * as vm from "vm";
1
+ /**
2
+ * @zhin.js/core utilities.
3
+ *
4
+ * Generic utilities are re-exported from @zhin.js/kernel.
5
+ * IM-specific utilities (compose, segment) remain here.
6
+ */
7
+
8
+ // ── Re-export generic utils from kernel ──
9
+ export {
10
+ evaluate,
11
+ execute,
12
+ clearEvalCache,
13
+ getEvalCacheStats,
14
+ getValueWithRuntime,
15
+ compiler,
16
+ remove,
17
+ isEmpty,
18
+ Time,
19
+ supportedPluginExtensions,
20
+ resolveEntry,
21
+ sleep,
22
+ } from '@zhin.js/kernel';
23
+
24
+ // ── IM-specific utilities ──
4
25
  import {
5
26
  AdapterMessage,
6
- Dict,
7
27
  MessageElement,
8
28
  MessageMiddleware,
9
29
  RegisteredAdapter,
10
30
  SendContent,
11
- } from "./types";
31
+ } from "./types.js";
12
32
  import { Message } from "./message.js";
13
33
 
14
- export function getValueWithRuntime(template: string, ctx: Dict) {
15
- return evaluate(template, ctx);
16
- }
17
- /**
18
- * Evaluate a single expression in a sandboxed vm context.
19
- * Unlike `execute`, does NOT wrap in IIFE — the expression value is returned directly.
20
- */
21
- export const evaluate = <S extends Record<string, unknown>, T = unknown>(exp: string, context: S): T | undefined => {
22
- const script = getOrCompileScript(exp);
23
- if (!script) return undefined;
24
-
25
- try {
26
- return script.runInNewContext(buildSandbox(context), { timeout: 200 }) as T;
27
- } catch {
28
- return undefined;
29
- }
30
- };
31
34
  /**
32
35
  * 组合中间件,洋葱模型
33
- * 灵感来源于 zhinjs/next 的 Hooks.compose
34
- *
35
- * @param middlewares 中间件列表
36
- * @returns 中间件处理函数
37
- *
38
- * @example
39
- * ```typescript
40
- * const composed = compose([middleware1, middleware2]);
41
- * await composed(message);
42
- * ```
43
36
  */
44
37
  export function compose<P extends RegisteredAdapter=RegisteredAdapter>(
45
38
  middlewares: MessageMiddleware<P>[]
46
39
  ) {
47
- // 性能优化:空数组直接返回空函数
48
40
  if (middlewares.length === 0) {
49
41
  return () => Promise.resolve();
50
42
  }
51
43
 
52
- // 性能优化:单个中间件直接返回
53
44
  if (middlewares.length === 1) {
54
45
  return (message: Message<AdapterMessage<P>>, next: () => Promise<void> = () => Promise.resolve()) => {
55
46
  return middlewares[0](message, next);
@@ -62,7 +53,6 @@ export function compose<P extends RegisteredAdapter=RegisteredAdapter>(
62
53
  ) {
63
54
  let index = -1;
64
55
  const dispatch = async (i: number = 0): Promise<void> => {
65
- // 防止 next() 被多次调用
66
56
  if (i <= index) {
67
57
  return Promise.reject(new Error("next() called multiple times"));
68
58
  }
@@ -73,7 +63,6 @@ export function compose<P extends RegisteredAdapter=RegisteredAdapter>(
73
63
  try {
74
64
  return await fn(message, () => dispatch(i + 1));
75
65
  } catch (error) {
76
- // 中间件异常应该被记录但不中断整个流程
77
66
  console.error("Middleware error:", error);
78
67
  throw error;
79
68
  }
@@ -81,87 +70,7 @@ export function compose<P extends RegisteredAdapter=RegisteredAdapter>(
81
70
  return dispatch(0);
82
71
  };
83
72
  }
84
- // LRU cache for compiled vm.Script instances
85
- const MAX_EVAL_CACHE_SIZE = 1000;
86
- const scriptCache = new Map<string, vm.Script>();
87
-
88
- function getOrCompileScript(code: string): vm.Script | null {
89
- let script = scriptCache.get(code);
90
- if (script) return script;
91
- try {
92
- script = new vm.Script(code);
93
- } catch {
94
- return null;
95
- }
96
- if (scriptCache.size >= MAX_EVAL_CACHE_SIZE) {
97
- const oldest = scriptCache.keys().next().value;
98
- if (oldest !== undefined) scriptCache.delete(oldest);
99
- }
100
- scriptCache.set(code, script);
101
- return script;
102
- }
103
-
104
- function buildSandbox<S extends Record<string, unknown>>(context: S): Record<string, unknown> {
105
- return {
106
- ...context,
107
- process: {
108
- version: process.version,
109
- versions: process.versions,
110
- platform: process.platform,
111
- arch: process.arch,
112
- release: process.release,
113
- uptime: process.uptime(),
114
- memoryUsage: process.memoryUsage(),
115
- cpuUsage: process.cpuUsage(),
116
- pid: process.pid,
117
- ppid: process.ppid,
118
- },
119
- global: undefined,
120
- globalThis: undefined,
121
- Buffer: undefined,
122
- crypto: undefined,
123
- require: undefined,
124
- import: undefined,
125
- __dirname: undefined,
126
- __filename: undefined,
127
- Bun: undefined,
128
- Deno: undefined,
129
- };
130
- }
131
-
132
- /**
133
- * Execute a code block in a sandboxed vm context.
134
- * Supports `return` statements by wrapping in an IIFE.
135
- * Throws on compilation or runtime errors.
136
- */
137
- export const execute = <S extends Record<string, unknown>, T = unknown>(code: string, context: S): T => {
138
- const wrapped = `(function(){${code}})()`;
139
- const script = getOrCompileScript(wrapped);
140
- if (!script) throw new SyntaxError(`Failed to compile: ${code.slice(0, 80)}`);
141
-
142
- return script.runInNewContext(buildSandbox(context), { timeout: 200 }) as T;
143
- };
144
-
145
- export function clearEvalCache(): void {
146
- scriptCache.clear();
147
- }
148
73
 
149
- export function getEvalCacheStats(): { size: number; maxSize: number } {
150
- return {
151
- size: scriptCache.size,
152
- maxSize: MAX_EVAL_CACHE_SIZE,
153
- };
154
- }
155
- export function compiler(template: string, ctx: Dict) {
156
- const matched = [...template.matchAll(/\${([^}]*?)}/g)];
157
- for (const item of matched) {
158
- const tpl = item[1];
159
- const raw = getValueWithRuntime(tpl, ctx);
160
- const value = typeof raw === 'string' ? raw : (raw == null ? 'undefined' : JSON.stringify(raw, null, 2));
161
- template = template.replace(`\${${item[1]}}`, value);
162
- }
163
- return template;
164
- }
165
74
  export function segment<T extends object>(type: string, data: T) {
166
75
  return {
167
76
  type,
@@ -197,33 +106,27 @@ export namespace segment {
197
106
  if (!Array.isArray(content)) content = [content];
198
107
  const toString = (template: string | MessageElement) => {
199
108
  if (typeof template !== "string") return [template];
200
-
201
- // 安全检查:限制输入长度,防止 ReDoS 攻击
202
- const MAX_TEMPLATE_LENGTH = 100000; // 100KB
109
+
110
+ const MAX_TEMPLATE_LENGTH = 100000;
203
111
  if (template.length > MAX_TEMPLATE_LENGTH) {
204
112
  throw new Error(`Template too large: ${template.length} > ${MAX_TEMPLATE_LENGTH}`);
205
113
  }
206
-
114
+
207
115
  template = unescape(template);
208
116
  const result: MessageElement[] = [];
209
- // 修复 ReDoS 漏洞:使用更安全的正则表达式
210
- // 注意:需要使用捕获组来获取属性字符串,否则无法正确重建原始标签
211
- // closingReg: 自闭合标签 <type attr="val"/>
212
117
  const closingReg = /<(\w+)(\s+[^>]*?)?\/>/;
213
- // twinningReg: 成对标签 <type attr="val">child</type>
214
118
  const twinningReg = /<(\w+)(\s+[^>]*?)?>([^]*?)<\/\1>/;
215
-
119
+
216
120
  let iterations = 0;
217
- const MAX_ITERATIONS = 1000; // 防止无限循环
218
-
121
+ const MAX_ITERATIONS = 1000;
122
+
219
123
  while (template.length && iterations++ < MAX_ITERATIONS) {
220
124
  const twinMatch = template.match(twinningReg);
221
125
  const closeMatch = template.match(closingReg);
222
-
223
- // 选择位置更靠前的匹配
126
+
224
127
  let match: RegExpMatchArray | null = null;
225
128
  let isClosing = false;
226
-
129
+
227
130
  if (twinMatch && closeMatch) {
228
131
  const twinIndex = template.indexOf(twinMatch[0]);
229
132
  const closeIndex = template.indexOf(closeMatch[0]);
@@ -239,14 +142,14 @@ export namespace segment {
239
142
  } else if (twinMatch) {
240
143
  match = twinMatch;
241
144
  }
242
-
145
+
243
146
  if (!match) break;
244
-
245
- const [fullMatch, type, attrStr = "", child = ""] = isClosing
147
+
148
+ const [fullMatch, type, attrStr = "", child = ""] = isClosing
246
149
  ? [match[0], match[1], match[2] || ""]
247
150
  : [match[0], match[1], match[2] || "", match[3] || ""];
248
151
  const index = template.indexOf(fullMatch);
249
- if (index === -1) break; // 安全检查
152
+ if (index === -1) break;
250
153
  const prevText = template.slice(0, index);
251
154
  if (prevText)
252
155
  result.push({
@@ -256,8 +159,6 @@ export namespace segment {
256
159
  },
257
160
  });
258
161
  template = template.slice(index + fullMatch.length);
259
- // 修复 ReDoS 漏洞:使用更简单的正则表达式
260
- // 原: /\s([^=]+)(?=(?=="([^"]+)")|(?=='([^']+)'))/g 嵌套前瞻断言
261
162
  const attrArr = [
262
163
  ...attrStr.matchAll(/\s+([^=\s]+)=(?:"([^"]*)"|'([^']*)')/g),
263
164
  ];
@@ -324,201 +225,3 @@ export namespace segment {
324
225
  .join("");
325
226
  }
326
227
  }
327
-
328
- export function remove<T>(list: T[], fn: (item: T) => boolean): void;
329
- export function remove<T>(list: T[], item: T): void;
330
- export function remove<T>(list: T[], arg: T | ((item: T) => boolean)) {
331
- const index =
332
- typeof arg === "function" &&
333
- !list.every((item) => typeof item === "function")
334
- ? list.findIndex(arg as (item: T) => boolean)
335
- : list.indexOf(arg as T);
336
- if (index !== -1) list.splice(index, 1);
337
- }
338
- export function isEmpty<T>(item: T) {
339
- if (Array.isArray(item)) return item.length === 0;
340
- if (typeof item === "object") {
341
- if (!item) return true;
342
- return Reflect.ownKeys(item).length === 0;
343
- }
344
- return false;
345
- }
346
-
347
- export namespace Time {
348
- export const millisecond = 1;
349
- export const second = 1000;
350
- export const minute = second * 60;
351
- export const hour = minute * 60;
352
- export const day = hour * 24;
353
- export const week = day * 7;
354
-
355
- let timezoneOffset = new Date().getTimezoneOffset();
356
-
357
- export function setTimezoneOffset(offset: number) {
358
- timezoneOffset = offset;
359
- }
360
-
361
- export function getTimezoneOffset() {
362
- return timezoneOffset;
363
- }
364
-
365
- export function getDateNumber(
366
- date: number | Date = new Date(),
367
- offset?: number
368
- ) {
369
- if (typeof date === "number") date = new Date(date);
370
- if (offset === undefined) offset = timezoneOffset;
371
- return Math.floor((date.valueOf() / minute - offset) / 1440);
372
- }
373
-
374
- export function fromDateNumber(value: number, offset?: number) {
375
- const date = new Date(value * day);
376
- if (offset === undefined) offset = timezoneOffset;
377
- return new Date(+date + offset * minute);
378
- }
379
-
380
- const numeric = /\d+(?:\.\d+)?/.source;
381
- const timeRegExp = new RegExp(
382
- `^${[
383
- "w(?:eek(?:s)?)?",
384
- "d(?:ay(?:s)?)?",
385
- "h(?:our(?:s)?)?",
386
- "m(?:in(?:ute)?(?:s)?)?",
387
- "s(?:ec(?:ond)?(?:s)?)?",
388
- ]
389
- .map((unit) => `(${numeric}${unit})?`)
390
- .join("")}$`
391
- );
392
-
393
- export function parseTime(source: string) {
394
- const capture = timeRegExp.exec(source);
395
- if (!capture) return 0;
396
- return (
397
- (parseFloat(capture[1]) * week || 0) +
398
- (parseFloat(capture[2]) * day || 0) +
399
- (parseFloat(capture[3]) * hour || 0) +
400
- (parseFloat(capture[4]) * minute || 0) +
401
- (parseFloat(capture[5]) * second || 0)
402
- );
403
- }
404
-
405
- export function parseDate(date: string) {
406
- const parsed = parseTime(date);
407
- let dateInput: string | number = date;
408
- if (parsed) {
409
- dateInput = Date.now() + parsed;
410
- } else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
411
- dateInput = `${new Date().toLocaleDateString()}-${date}`;
412
- } else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
413
- dateInput = `${new Date().getFullYear()}-${date}`;
414
- }
415
- return dateInput ? new Date(dateInput) : new Date();
416
- }
417
-
418
- export function formatTimeShort(ms: number) {
419
- const abs = Math.abs(ms);
420
- if (abs >= day - hour / 2) {
421
- return Math.round(ms / day) + "d";
422
- } else if (abs >= hour - minute / 2) {
423
- return Math.round(ms / hour) + "h";
424
- } else if (abs >= minute - second / 2) {
425
- return Math.round(ms / minute) + "m";
426
- } else if (abs >= second) {
427
- return Math.round(ms / second) + "s";
428
- }
429
- return ms + "ms";
430
- }
431
-
432
- export function formatTime(ms: number) {
433
- let result: string;
434
- if (ms >= day - hour / 2) {
435
- ms += hour / 2;
436
- result = Math.floor(ms / day) + " 天";
437
- if (ms % day > hour) {
438
- result += ` ${Math.floor((ms % day) / hour)} 小时`;
439
- }
440
- } else if (ms >= hour - minute / 2) {
441
- ms += minute / 2;
442
- result = Math.floor(ms / hour) + " 小时";
443
- if (ms % hour > minute) {
444
- result += ` ${Math.floor((ms % hour) / minute)} 分钟`;
445
- }
446
- } else if (ms >= minute - second / 2) {
447
- ms += second / 2;
448
- result = Math.floor(ms / minute) + " 分钟";
449
- if (ms % minute > second) {
450
- result += ` ${Math.floor((ms % minute) / second)} 秒`;
451
- }
452
- } else {
453
- result = Math.round(ms / second) + " 秒";
454
- }
455
- return result;
456
- }
457
-
458
- const dayMap = ["日", "一", "二", "三", "四", "五", "六"];
459
-
460
- function toDigits(source: number, length = 2) {
461
- return source.toString().padStart(length, "0");
462
- }
463
-
464
- export function template(template: string, time = new Date()) {
465
- return template
466
- .replace("yyyy", time.getFullYear().toString())
467
- .replace("yy", time.getFullYear().toString().slice(2))
468
- .replace("MM", toDigits(time.getMonth() + 1))
469
- .replace("dd", toDigits(time.getDate()))
470
- .replace("hh", toDigits(time.getHours()))
471
- .replace("mm", toDigits(time.getMinutes()))
472
- .replace("ss", toDigits(time.getSeconds()))
473
- .replace("SSS", toDigits(time.getMilliseconds(), 3));
474
- }
475
-
476
- function toHourMinute(time: Date) {
477
- return `${toDigits(time.getHours())}:${toDigits(time.getMinutes())}`;
478
- }
479
-
480
- export function formatTimeInterval(time: Date, interval?: number) {
481
- if (!interval) {
482
- return template("yyyy-MM-dd hh:mm:ss", time);
483
- } else if (interval === day) {
484
- return `每天 ${toHourMinute(time)}`;
485
- } else if (interval === week) {
486
- return `每周${dayMap[time.getDay()]} ${toHourMinute(time)}`;
487
- } else {
488
- return `${template("yyyy-MM-dd hh:mm:ss", time)} 起每隔 ${formatTime(
489
- interval
490
- )}`;
491
- }
492
- }
493
- }
494
- export const supportedPluginExtensions = [
495
- ".js",
496
- ".ts",
497
- ".mjs",
498
- ".cjs",
499
- ".jsx",
500
- ".tsx",
501
- "",
502
- ];
503
-
504
- export function resolveEntry(entry: string) {
505
- if (fs.existsSync(entry)) {
506
- const stat = fs.statSync(entry);
507
- if (stat.isFile()) return entry;
508
- if (stat.isSymbolicLink()) return resolveEntry(fs.realpathSync(entry));
509
- if (stat.isDirectory()) {
510
- const packageJsonPath = path.resolve(entry, 'package.json');
511
- if (!fs.existsSync(packageJsonPath)) return resolveEntry(path.join(entry, 'index'));
512
- const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
513
- return resolveEntry(path.resolve(entry, pkgJson.main || 'index.js'));
514
- }
515
- } else {
516
- for (const ext of supportedPluginExtensions) {
517
- const fullPath = path.resolve(entry + ext);
518
- if (fs.existsSync(fullPath)) return resolveEntry(fullPath);
519
- }
520
- }
521
- }
522
- export function sleep(ms: number) {
523
- return new Promise((resolve) => setTimeout(resolve, ms));
524
- }