agentlink-sdk 1.0.7 → 1.0.8
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 +62 -1
- package/dist/index.d.mts +56 -1
- package/dist/index.d.ts +56 -1
- package/dist/index.js +169 -0
- package/dist/index.mjs +169 -0
- package/package.json +7 -2
- package/scripts/test-llm-schema.ts +246 -0
- package/vitest.config.ts +8 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AgentLink SDK
|
|
2
2
|
|
|
3
|
-
AgentLink 客户端 SDK,用于跨域数据同步,通过 URL hash 传递数据,支持 Token
|
|
3
|
+
AgentLink 客户端 SDK,用于跨域数据同步,通过 URL hash 传递数据,支持 Token 验证和白名单机制。同时提供 Base64 隔离、Schema 推断等工具函数,便于与 LLM 流程集成。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
@@ -278,6 +278,63 @@ interface WhitelistResponse {
|
|
|
278
278
|
}
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
+
## 工具函数(LLM 相关)
|
|
282
|
+
|
|
283
|
+
以下函数可从 `agentlink-sdk` 直接导入,用于在发送给 LLM 前处理数据(如替换 base64、生成精简 schema)。
|
|
284
|
+
|
|
285
|
+
### inferSchema
|
|
286
|
+
|
|
287
|
+
从原始数据推断出体积受控的 schema(先替换 base64 为占位符,再按深度/长度/项数截断),便于作为 LLM 输入。
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import { inferSchema, type InferredSchema, type InferSchemaOptions } from 'agentlink-sdk';
|
|
291
|
+
|
|
292
|
+
const schema = inferSchema(rawData, {
|
|
293
|
+
maxDepth: 5, // 最大递归深度,默认 5
|
|
294
|
+
maxStringLength: 500, // 单段字符串最大字符数,默认 500
|
|
295
|
+
maxTotalTextLength: 3000, // 所有字符串总字符数上限,默认 3000
|
|
296
|
+
maxArrayItems: 5, // 数组最多保留项数,默认 5
|
|
297
|
+
maxKeys: 20, // 对象每层最多键数,默认 20
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### extractAndReplaceBase64 / restoreBase64
|
|
302
|
+
|
|
303
|
+
将数据中的 base64 图片替换为占位符(如 `_IMG_0_`),或根据映射表恢复。
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
import { extractAndReplaceBase64, restoreBase64 } from 'agentlink-sdk';
|
|
307
|
+
|
|
308
|
+
const { processedData, replacements } = extractAndReplaceBase64(rawData);
|
|
309
|
+
// 将 processedData 发给 LLM,replacements 用于后续恢复
|
|
310
|
+
|
|
311
|
+
const restored = restoreBase64(processedData, replacements);
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### extractImages
|
|
315
|
+
|
|
316
|
+
从数据中递归提取所有 base64 图片(data URL),去重后返回数组。
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { extractImages } from 'agentlink-sdk';
|
|
320
|
+
|
|
321
|
+
const images = extractImages(rawData);
|
|
322
|
+
// 可与 inferSchema 配合:先发 schema 给 LLM,再在结果中回填 images
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## 开发与测试
|
|
326
|
+
|
|
327
|
+
本包使用 [Vitest](https://vitest.dev/) 做单元测试。
|
|
328
|
+
|
|
329
|
+
| 命令 | 说明 |
|
|
330
|
+
|------|------|
|
|
331
|
+
| `npm run test` | 以 watch 模式运行单元测试 |
|
|
332
|
+
| `npm run test:run` | 单次运行全部单元测试 |
|
|
333
|
+
| `npm run test:llm-schema` | 运行 Schema + LLM 集成测试(需配置 `AGENTLINK_SERVER_URL` 或 `LLM_API_URL` + `LLM_API_KEY`) |
|
|
334
|
+
|
|
335
|
+
- **单元测试**:覆盖 `base64`、`schema`、`compression`、`url` 等工具,无需网络,可直接执行 `npm run test:run`。
|
|
336
|
+
- **LLM Schema 测试**:脚本位于 `scripts/test-llm-schema.ts`,会调用真实 LLM 验证「inferSchema + 回填 images」的端到端流程,适合手动或 CI 可选执行。
|
|
337
|
+
|
|
281
338
|
## 特性
|
|
282
339
|
|
|
283
340
|
- ✅ **跨域数据同步**: 通过 URL hash 实现跨域数据传输
|
|
@@ -285,6 +342,8 @@ interface WhitelistResponse {
|
|
|
285
342
|
- ✅ **白名单机制**: 支持域名白名单验证
|
|
286
343
|
- ✅ **数据压缩**: 自动压缩大型数据,优化 URL 长度
|
|
287
344
|
- ✅ **缓存优化**: Token 和白名单验证结果缓存,减少服务器请求
|
|
345
|
+
- ✅ **LLM 工具**: `inferSchema`、`extractAndReplaceBase64`、`restoreBase64`、`extractImages` 便于与 LLM 流程集成
|
|
346
|
+
- ✅ **自动化测试**: Vitest 单元测试(base64 / schema / compression / url),可选 LLM Schema 集成测试
|
|
288
347
|
- ✅ **TypeScript 支持**: 完整的 TypeScript 类型定义
|
|
289
348
|
- ✅ **窗口复用**: 支持复用窗口,避免频繁打开新窗口
|
|
290
349
|
|
|
@@ -376,5 +435,7 @@ MIT
|
|
|
376
435
|
## 相关链接
|
|
377
436
|
|
|
378
437
|
- [示例文件](./example.html)
|
|
438
|
+
- [单元测试](src/utils/)(`*.test.ts`)
|
|
439
|
+
- [LLM Schema 集成测试脚本](scripts/test-llm-schema.ts)
|
|
379
440
|
- [GitHub 仓库](https://github.com/your-org/AgentLink)
|
|
380
441
|
|
package/dist/index.d.mts
CHANGED
|
@@ -120,4 +120,59 @@ declare function verifyWhitelist(serverUrl: string, origin?: string): Promise<bo
|
|
|
120
120
|
*/
|
|
121
121
|
declare function fetchWhitelistInfo(serverUrl: string, includeAll?: boolean): Promise<any>;
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
/**
|
|
124
|
+
* 自动遍历 rawData 推断出体积受控的 schema,供 LLM 使用
|
|
125
|
+
*/
|
|
126
|
+
/** 推断出的 schema:与 rawData 同构但被截断的树(字符串截断、base64 占位符、数组/对象可带截断标记) */
|
|
127
|
+
type InferredSchema = string | number | boolean | null | {
|
|
128
|
+
_truncated?: boolean;
|
|
129
|
+
_length?: number;
|
|
130
|
+
_depth?: number;
|
|
131
|
+
_keysTotal?: number;
|
|
132
|
+
[key: string]: InferredSchema | number | boolean | undefined;
|
|
133
|
+
} | InferredSchema[];
|
|
134
|
+
interface InferSchemaOptions {
|
|
135
|
+
/** 最大递归深度,默认 5 */
|
|
136
|
+
maxDepth?: number;
|
|
137
|
+
/** 单段字符串最大字符数,默认 500 */
|
|
138
|
+
maxStringLength?: number;
|
|
139
|
+
/** 所有字符串总字符数上限,默认 3000 */
|
|
140
|
+
maxTotalTextLength?: number;
|
|
141
|
+
/** 数组最多保留项数,默认 5 */
|
|
142
|
+
maxArrayItems?: number;
|
|
143
|
+
/** 对象每层最多键数,默认 20 */
|
|
144
|
+
maxKeys?: number;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 从 rawData 推断出体积受控的 schema:先替换 base64,再按深度/长度/项数递归截断
|
|
148
|
+
*/
|
|
149
|
+
declare function inferSchema(rawData: any, options?: InferSchemaOptions): InferredSchema;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Base64 数据隔离和替换工具
|
|
153
|
+
* 用于在 LLM 处理前替换 base64 图片数据,减少输入大小
|
|
154
|
+
*/
|
|
155
|
+
/**
|
|
156
|
+
* 提取并替换 base64 数据
|
|
157
|
+
* @param data 原始数据(可以是对象、数组、字符串等)
|
|
158
|
+
* @returns 处理后的数据和替换映射表
|
|
159
|
+
*/
|
|
160
|
+
declare function extractAndReplaceBase64(data: any): {
|
|
161
|
+
processedData: any;
|
|
162
|
+
replacements: Map<string, string>;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* 恢复 base64 数据
|
|
166
|
+
* @param data 包含占位符的数据
|
|
167
|
+
* @param replacements 替换映射表
|
|
168
|
+
* @returns 恢复后的数据
|
|
169
|
+
*/
|
|
170
|
+
declare function restoreBase64(data: any, replacements: Map<string, string>): any;
|
|
171
|
+
/**
|
|
172
|
+
* 从数据中提取所有 base64 图片
|
|
173
|
+
* @param data 原始数据
|
|
174
|
+
* @returns 图片数组(base64 data URL 格式)
|
|
175
|
+
*/
|
|
176
|
+
declare function extractImages(data: any): string[];
|
|
177
|
+
|
|
178
|
+
export { AgentLinkClient, type AgentLinkClientOptions, DEFAULT_SERVER_URL, type InferSchemaOptions, type InferredSchema, type SenderInfo, type URLData, type WhitelistInfo, type WhitelistResponse, base64ToUint8Array, compress, decodeDataFromUrl, decompress, encodeDataToUrl, extractAndReplaceBase64, extractImages, fetchWhitelistInfo, inferSchema, restoreBase64, uint8ArrayToBase64, verifyWhitelist };
|
package/dist/index.d.ts
CHANGED
|
@@ -120,4 +120,59 @@ declare function verifyWhitelist(serverUrl: string, origin?: string): Promise<bo
|
|
|
120
120
|
*/
|
|
121
121
|
declare function fetchWhitelistInfo(serverUrl: string, includeAll?: boolean): Promise<any>;
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
/**
|
|
124
|
+
* 自动遍历 rawData 推断出体积受控的 schema,供 LLM 使用
|
|
125
|
+
*/
|
|
126
|
+
/** 推断出的 schema:与 rawData 同构但被截断的树(字符串截断、base64 占位符、数组/对象可带截断标记) */
|
|
127
|
+
type InferredSchema = string | number | boolean | null | {
|
|
128
|
+
_truncated?: boolean;
|
|
129
|
+
_length?: number;
|
|
130
|
+
_depth?: number;
|
|
131
|
+
_keysTotal?: number;
|
|
132
|
+
[key: string]: InferredSchema | number | boolean | undefined;
|
|
133
|
+
} | InferredSchema[];
|
|
134
|
+
interface InferSchemaOptions {
|
|
135
|
+
/** 最大递归深度,默认 5 */
|
|
136
|
+
maxDepth?: number;
|
|
137
|
+
/** 单段字符串最大字符数,默认 500 */
|
|
138
|
+
maxStringLength?: number;
|
|
139
|
+
/** 所有字符串总字符数上限,默认 3000 */
|
|
140
|
+
maxTotalTextLength?: number;
|
|
141
|
+
/** 数组最多保留项数,默认 5 */
|
|
142
|
+
maxArrayItems?: number;
|
|
143
|
+
/** 对象每层最多键数,默认 20 */
|
|
144
|
+
maxKeys?: number;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 从 rawData 推断出体积受控的 schema:先替换 base64,再按深度/长度/项数递归截断
|
|
148
|
+
*/
|
|
149
|
+
declare function inferSchema(rawData: any, options?: InferSchemaOptions): InferredSchema;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Base64 数据隔离和替换工具
|
|
153
|
+
* 用于在 LLM 处理前替换 base64 图片数据,减少输入大小
|
|
154
|
+
*/
|
|
155
|
+
/**
|
|
156
|
+
* 提取并替换 base64 数据
|
|
157
|
+
* @param data 原始数据(可以是对象、数组、字符串等)
|
|
158
|
+
* @returns 处理后的数据和替换映射表
|
|
159
|
+
*/
|
|
160
|
+
declare function extractAndReplaceBase64(data: any): {
|
|
161
|
+
processedData: any;
|
|
162
|
+
replacements: Map<string, string>;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* 恢复 base64 数据
|
|
166
|
+
* @param data 包含占位符的数据
|
|
167
|
+
* @param replacements 替换映射表
|
|
168
|
+
* @returns 恢复后的数据
|
|
169
|
+
*/
|
|
170
|
+
declare function restoreBase64(data: any, replacements: Map<string, string>): any;
|
|
171
|
+
/**
|
|
172
|
+
* 从数据中提取所有 base64 图片
|
|
173
|
+
* @param data 原始数据
|
|
174
|
+
* @returns 图片数组(base64 data URL 格式)
|
|
175
|
+
*/
|
|
176
|
+
declare function extractImages(data: any): string[];
|
|
177
|
+
|
|
178
|
+
export { AgentLinkClient, type AgentLinkClientOptions, DEFAULT_SERVER_URL, type InferSchemaOptions, type InferredSchema, type SenderInfo, type URLData, type WhitelistInfo, type WhitelistResponse, base64ToUint8Array, compress, decodeDataFromUrl, decompress, encodeDataToUrl, extractAndReplaceBase64, extractImages, fetchWhitelistInfo, inferSchema, restoreBase64, uint8ArrayToBase64, verifyWhitelist };
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,11 @@ __export(index_exports, {
|
|
|
27
27
|
decodeDataFromUrl: () => decodeDataFromUrl,
|
|
28
28
|
decompress: () => decompress,
|
|
29
29
|
encodeDataToUrl: () => encodeDataToUrl,
|
|
30
|
+
extractAndReplaceBase64: () => extractAndReplaceBase64,
|
|
31
|
+
extractImages: () => extractImages,
|
|
30
32
|
fetchWhitelistInfo: () => fetchWhitelistInfo,
|
|
33
|
+
inferSchema: () => inferSchema,
|
|
34
|
+
restoreBase64: () => restoreBase64,
|
|
31
35
|
uint8ArrayToBase64: () => uint8ArrayToBase64,
|
|
32
36
|
verifyWhitelist: () => verifyWhitelist
|
|
33
37
|
});
|
|
@@ -339,4 +343,169 @@ _AgentLinkClient.whitelistCache = /* @__PURE__ */ new Map();
|
|
|
339
343
|
// 缓存过期时间:1 小时
|
|
340
344
|
_AgentLinkClient.CACHE_TTL = 60 * 60 * 1e3;
|
|
341
345
|
var AgentLinkClient = _AgentLinkClient;
|
|
346
|
+
|
|
347
|
+
// src/utils/base64.ts
|
|
348
|
+
var DATA_URL_REGEX = /data:image\/[^;]+;base64,[A-Za-z0-9+/=]+/gi;
|
|
349
|
+
function extractAndReplaceBase64(data) {
|
|
350
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
351
|
+
let imgIndex = 0;
|
|
352
|
+
function processValue(value) {
|
|
353
|
+
if (value === null || value === void 0) {
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
if (typeof value === "string") {
|
|
357
|
+
return value.replace(DATA_URL_REGEX, (match) => {
|
|
358
|
+
const placeholder = `_IMG_${imgIndex}_`;
|
|
359
|
+
replacements.set(placeholder, match);
|
|
360
|
+
imgIndex++;
|
|
361
|
+
return placeholder;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (Array.isArray(value)) {
|
|
365
|
+
return value.map((item) => processValue(item));
|
|
366
|
+
}
|
|
367
|
+
if (typeof value === "object") {
|
|
368
|
+
const processed = {};
|
|
369
|
+
for (const key in value) {
|
|
370
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
371
|
+
processed[key] = processValue(value[key]);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return processed;
|
|
375
|
+
}
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
const processedData = processValue(data);
|
|
379
|
+
return { processedData, replacements };
|
|
380
|
+
}
|
|
381
|
+
function restoreBase64(data, replacements) {
|
|
382
|
+
if (!replacements || replacements.size === 0) {
|
|
383
|
+
return data;
|
|
384
|
+
}
|
|
385
|
+
function processValue(value) {
|
|
386
|
+
if (value === null || value === void 0) {
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
if (typeof value === "string") {
|
|
390
|
+
let result = value;
|
|
391
|
+
for (const [placeholder, original] of replacements.entries()) {
|
|
392
|
+
result = result.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), original);
|
|
393
|
+
}
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
if (Array.isArray(value)) {
|
|
397
|
+
return value.map((item) => processValue(item));
|
|
398
|
+
}
|
|
399
|
+
if (typeof value === "object") {
|
|
400
|
+
const processed = {};
|
|
401
|
+
for (const key in value) {
|
|
402
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
403
|
+
processed[key] = processValue(value[key]);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return processed;
|
|
407
|
+
}
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
return processValue(data);
|
|
411
|
+
}
|
|
412
|
+
function extractImages(data) {
|
|
413
|
+
const images = [];
|
|
414
|
+
const seen = /* @__PURE__ */ new Set();
|
|
415
|
+
function processValue(value) {
|
|
416
|
+
if (value === null || value === void 0) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (typeof value === "string") {
|
|
420
|
+
const matches = value.matchAll(DATA_URL_REGEX);
|
|
421
|
+
for (const match of matches) {
|
|
422
|
+
if (!seen.has(match[0])) {
|
|
423
|
+
images.push(match[0]);
|
|
424
|
+
seen.add(match[0]);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (Array.isArray(value)) {
|
|
430
|
+
value.forEach((item) => processValue(item));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (typeof value === "object") {
|
|
434
|
+
for (const key in value) {
|
|
435
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
436
|
+
processValue(value[key]);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
processValue(data);
|
|
442
|
+
return images;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/utils/schema.ts
|
|
446
|
+
var DEFAULTS = {
|
|
447
|
+
maxDepth: 5,
|
|
448
|
+
maxStringLength: 500,
|
|
449
|
+
maxTotalTextLength: 3e3,
|
|
450
|
+
maxArrayItems: 5,
|
|
451
|
+
maxKeys: 20
|
|
452
|
+
};
|
|
453
|
+
var TRUNCATED_SUFFIX = "...(truncated)";
|
|
454
|
+
function inferSchema(rawData, options) {
|
|
455
|
+
const opts = { ...DEFAULTS, ...options };
|
|
456
|
+
const { processedData } = extractAndReplaceBase64(rawData);
|
|
457
|
+
let totalTextLength = 0;
|
|
458
|
+
function processValue(value, depth) {
|
|
459
|
+
if (value === null || value === void 0) {
|
|
460
|
+
return value;
|
|
461
|
+
}
|
|
462
|
+
if (depth > opts.maxDepth) {
|
|
463
|
+
return { _truncated: true, _depth: depth };
|
|
464
|
+
}
|
|
465
|
+
if (typeof value === "string") {
|
|
466
|
+
const maxForThis = Math.min(
|
|
467
|
+
opts.maxStringLength,
|
|
468
|
+
opts.maxTotalTextLength - totalTextLength
|
|
469
|
+
);
|
|
470
|
+
if (value.length <= maxForThis) {
|
|
471
|
+
totalTextLength += value.length;
|
|
472
|
+
return value;
|
|
473
|
+
}
|
|
474
|
+
totalTextLength += maxForThis;
|
|
475
|
+
return value.slice(0, maxForThis) + TRUNCATED_SUFFIX;
|
|
476
|
+
}
|
|
477
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
478
|
+
return value;
|
|
479
|
+
}
|
|
480
|
+
if (Array.isArray(value)) {
|
|
481
|
+
if (value.length === 0) return [];
|
|
482
|
+
const keep = Math.min(value.length, opts.maxArrayItems);
|
|
483
|
+
const items = [];
|
|
484
|
+
for (let i = 0; i < keep; i++) {
|
|
485
|
+
items.push(processValue(value[i], depth + 1));
|
|
486
|
+
}
|
|
487
|
+
if (value.length > keep) {
|
|
488
|
+
items.push({ _truncated: true, _length: value.length });
|
|
489
|
+
}
|
|
490
|
+
return items;
|
|
491
|
+
}
|
|
492
|
+
if (typeof value === "object") {
|
|
493
|
+
const keys = Object.keys(value).filter(
|
|
494
|
+
(k) => Object.prototype.hasOwnProperty.call(value, k)
|
|
495
|
+
);
|
|
496
|
+
const keepKeys = keys.slice(0, opts.maxKeys);
|
|
497
|
+
const out = {};
|
|
498
|
+
for (const key of keepKeys) {
|
|
499
|
+
out[key] = processValue(value[key], depth + 1);
|
|
500
|
+
}
|
|
501
|
+
if (keys.length > opts.maxKeys) {
|
|
502
|
+
out._truncated = true;
|
|
503
|
+
out._keysTotal = keys.length;
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
}
|
|
507
|
+
return value;
|
|
508
|
+
}
|
|
509
|
+
return processValue(processedData, 0);
|
|
510
|
+
}
|
|
342
511
|
//# sourceMappingURL=index.js.map
|
package/dist/index.mjs
CHANGED
|
@@ -304,6 +304,171 @@ _AgentLinkClient.whitelistCache = /* @__PURE__ */ new Map();
|
|
|
304
304
|
// 缓存过期时间:1 小时
|
|
305
305
|
_AgentLinkClient.CACHE_TTL = 60 * 60 * 1e3;
|
|
306
306
|
var AgentLinkClient = _AgentLinkClient;
|
|
307
|
+
|
|
308
|
+
// src/utils/base64.ts
|
|
309
|
+
var DATA_URL_REGEX = /data:image\/[^;]+;base64,[A-Za-z0-9+/=]+/gi;
|
|
310
|
+
function extractAndReplaceBase64(data) {
|
|
311
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
312
|
+
let imgIndex = 0;
|
|
313
|
+
function processValue(value) {
|
|
314
|
+
if (value === null || value === void 0) {
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
if (typeof value === "string") {
|
|
318
|
+
return value.replace(DATA_URL_REGEX, (match) => {
|
|
319
|
+
const placeholder = `_IMG_${imgIndex}_`;
|
|
320
|
+
replacements.set(placeholder, match);
|
|
321
|
+
imgIndex++;
|
|
322
|
+
return placeholder;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (Array.isArray(value)) {
|
|
326
|
+
return value.map((item) => processValue(item));
|
|
327
|
+
}
|
|
328
|
+
if (typeof value === "object") {
|
|
329
|
+
const processed = {};
|
|
330
|
+
for (const key in value) {
|
|
331
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
332
|
+
processed[key] = processValue(value[key]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return processed;
|
|
336
|
+
}
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
const processedData = processValue(data);
|
|
340
|
+
return { processedData, replacements };
|
|
341
|
+
}
|
|
342
|
+
function restoreBase64(data, replacements) {
|
|
343
|
+
if (!replacements || replacements.size === 0) {
|
|
344
|
+
return data;
|
|
345
|
+
}
|
|
346
|
+
function processValue(value) {
|
|
347
|
+
if (value === null || value === void 0) {
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
350
|
+
if (typeof value === "string") {
|
|
351
|
+
let result = value;
|
|
352
|
+
for (const [placeholder, original] of replacements.entries()) {
|
|
353
|
+
result = result.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), original);
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
if (Array.isArray(value)) {
|
|
358
|
+
return value.map((item) => processValue(item));
|
|
359
|
+
}
|
|
360
|
+
if (typeof value === "object") {
|
|
361
|
+
const processed = {};
|
|
362
|
+
for (const key in value) {
|
|
363
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
364
|
+
processed[key] = processValue(value[key]);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return processed;
|
|
368
|
+
}
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
return processValue(data);
|
|
372
|
+
}
|
|
373
|
+
function extractImages(data) {
|
|
374
|
+
const images = [];
|
|
375
|
+
const seen = /* @__PURE__ */ new Set();
|
|
376
|
+
function processValue(value) {
|
|
377
|
+
if (value === null || value === void 0) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (typeof value === "string") {
|
|
381
|
+
const matches = value.matchAll(DATA_URL_REGEX);
|
|
382
|
+
for (const match of matches) {
|
|
383
|
+
if (!seen.has(match[0])) {
|
|
384
|
+
images.push(match[0]);
|
|
385
|
+
seen.add(match[0]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (Array.isArray(value)) {
|
|
391
|
+
value.forEach((item) => processValue(item));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (typeof value === "object") {
|
|
395
|
+
for (const key in value) {
|
|
396
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
397
|
+
processValue(value[key]);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
processValue(data);
|
|
403
|
+
return images;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/utils/schema.ts
|
|
407
|
+
var DEFAULTS = {
|
|
408
|
+
maxDepth: 5,
|
|
409
|
+
maxStringLength: 500,
|
|
410
|
+
maxTotalTextLength: 3e3,
|
|
411
|
+
maxArrayItems: 5,
|
|
412
|
+
maxKeys: 20
|
|
413
|
+
};
|
|
414
|
+
var TRUNCATED_SUFFIX = "...(truncated)";
|
|
415
|
+
function inferSchema(rawData, options) {
|
|
416
|
+
const opts = { ...DEFAULTS, ...options };
|
|
417
|
+
const { processedData } = extractAndReplaceBase64(rawData);
|
|
418
|
+
let totalTextLength = 0;
|
|
419
|
+
function processValue(value, depth) {
|
|
420
|
+
if (value === null || value === void 0) {
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
if (depth > opts.maxDepth) {
|
|
424
|
+
return { _truncated: true, _depth: depth };
|
|
425
|
+
}
|
|
426
|
+
if (typeof value === "string") {
|
|
427
|
+
const maxForThis = Math.min(
|
|
428
|
+
opts.maxStringLength,
|
|
429
|
+
opts.maxTotalTextLength - totalTextLength
|
|
430
|
+
);
|
|
431
|
+
if (value.length <= maxForThis) {
|
|
432
|
+
totalTextLength += value.length;
|
|
433
|
+
return value;
|
|
434
|
+
}
|
|
435
|
+
totalTextLength += maxForThis;
|
|
436
|
+
return value.slice(0, maxForThis) + TRUNCATED_SUFFIX;
|
|
437
|
+
}
|
|
438
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
if (Array.isArray(value)) {
|
|
442
|
+
if (value.length === 0) return [];
|
|
443
|
+
const keep = Math.min(value.length, opts.maxArrayItems);
|
|
444
|
+
const items = [];
|
|
445
|
+
for (let i = 0; i < keep; i++) {
|
|
446
|
+
items.push(processValue(value[i], depth + 1));
|
|
447
|
+
}
|
|
448
|
+
if (value.length > keep) {
|
|
449
|
+
items.push({ _truncated: true, _length: value.length });
|
|
450
|
+
}
|
|
451
|
+
return items;
|
|
452
|
+
}
|
|
453
|
+
if (typeof value === "object") {
|
|
454
|
+
const keys = Object.keys(value).filter(
|
|
455
|
+
(k) => Object.prototype.hasOwnProperty.call(value, k)
|
|
456
|
+
);
|
|
457
|
+
const keepKeys = keys.slice(0, opts.maxKeys);
|
|
458
|
+
const out = {};
|
|
459
|
+
for (const key of keepKeys) {
|
|
460
|
+
out[key] = processValue(value[key], depth + 1);
|
|
461
|
+
}
|
|
462
|
+
if (keys.length > opts.maxKeys) {
|
|
463
|
+
out._truncated = true;
|
|
464
|
+
out._keysTotal = keys.length;
|
|
465
|
+
}
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
return value;
|
|
469
|
+
}
|
|
470
|
+
return processValue(processedData, 0);
|
|
471
|
+
}
|
|
307
472
|
export {
|
|
308
473
|
AgentLinkClient,
|
|
309
474
|
DEFAULT_SERVER_URL,
|
|
@@ -312,7 +477,11 @@ export {
|
|
|
312
477
|
decodeDataFromUrl,
|
|
313
478
|
decompress,
|
|
314
479
|
encodeDataToUrl,
|
|
480
|
+
extractAndReplaceBase64,
|
|
481
|
+
extractImages,
|
|
315
482
|
fetchWhitelistInfo,
|
|
483
|
+
inferSchema,
|
|
484
|
+
restoreBase64,
|
|
316
485
|
uint8ArrayToBase64,
|
|
317
486
|
verifyWhitelist
|
|
318
487
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlink-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "AgentLink client SDK for cross-domain data synchronization via URL hash",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsup",
|
|
17
|
-
"clean": "rimraf dist"
|
|
17
|
+
"clean": "rimraf dist",
|
|
18
|
+
"test": "vitest",
|
|
19
|
+
"test:run": "vitest run",
|
|
20
|
+
"test:llm-schema": "tsx scripts/test-llm-schema.ts"
|
|
18
21
|
},
|
|
19
22
|
"keywords": [
|
|
20
23
|
"agentlink",
|
|
@@ -27,7 +30,9 @@
|
|
|
27
30
|
"devDependencies": {
|
|
28
31
|
"@types/node": "^20.0.0",
|
|
29
32
|
"rimraf": "^4.4.0",
|
|
33
|
+
"vitest": "^2.0.0",
|
|
30
34
|
"tsup": "^8.0.0",
|
|
35
|
+
"tsx": "^4.21.0",
|
|
31
36
|
"typescript": "^5.0.2"
|
|
32
37
|
},
|
|
33
38
|
"peerDependencies": {}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Schema 真实测试(agentlink-sdk)
|
|
3
|
+
* 复用包内 inferSchema、extractImages,仅发送 schema 请求 LLM,再按前端方式回填拼接。
|
|
4
|
+
*
|
|
5
|
+
* LLM 配置(二选一,在脚本内通过环境变量配置):
|
|
6
|
+
* - 方式 A:AGENTLINK_SERVER_URL — 请求该地址的 /api/memory/llm/process(需先启动 apps/server)
|
|
7
|
+
* - 方式 B:LLM_API_URL + LLM_API_KEY(及可选 LLM_MODEL)— 直接请求 OpenAI 兼容 API
|
|
8
|
+
*
|
|
9
|
+
* 运行:npm run test:llm-schema 或 npx tsx scripts/test-llm-schema.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { inferSchema, type InferredSchema } from '../src/utils/schema';
|
|
13
|
+
import { extractImages } from '../src/utils/base64';
|
|
14
|
+
|
|
15
|
+
interface AIProcessedData {
|
|
16
|
+
source: string;
|
|
17
|
+
category: string;
|
|
18
|
+
content: string;
|
|
19
|
+
tags: string[];
|
|
20
|
+
images?: string[];
|
|
21
|
+
metadata?: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getApiUrl(): string {
|
|
25
|
+
const serverUrl = process.env.AGENTLINK_SERVER_URL;
|
|
26
|
+
const llmUrl = process.env.LLM_API_URL;
|
|
27
|
+
if (serverUrl) return `${serverUrl.replace(/\/$/, '')}/api/memory/llm/process`;
|
|
28
|
+
if (llmUrl) return ''; // 表示用方式 B,脚本内直接调 LLM
|
|
29
|
+
throw new Error(
|
|
30
|
+
'请配置 LLM:设置 AGENTLINK_SERVER_URL(方式 A)或 LLM_API_URL + LLM_API_KEY(方式 B)'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function postSchemaToServer(
|
|
35
|
+
apiUrl: string,
|
|
36
|
+
schema: InferredSchema,
|
|
37
|
+
options?: { systemPrompt?: string; temperature?: number }
|
|
38
|
+
): Promise<Response> {
|
|
39
|
+
const body: { schema: any; systemPrompt?: string; temperature?: number } = { schema };
|
|
40
|
+
if (options?.systemPrompt != null) body.systemPrompt = options.systemPrompt;
|
|
41
|
+
if (options?.temperature != null) body.temperature = options.temperature;
|
|
42
|
+
return fetch(apiUrl, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify(body),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function callLlmDirect(
|
|
50
|
+
schema: InferredSchema
|
|
51
|
+
): Promise<AIProcessedData> {
|
|
52
|
+
const url = process.env.LLM_API_URL;
|
|
53
|
+
const key = process.env.LLM_API_KEY;
|
|
54
|
+
const model = process.env.LLM_MODEL || 'gpt-4o-mini';
|
|
55
|
+
if (!url || !key) throw new Error('LLM_API_URL 与 LLM_API_KEY 必填');
|
|
56
|
+
const systemPrompt =
|
|
57
|
+
'你是一个专业的信息整理助手。根据 schema 将内容归类到 source、category、content、tags、metadata 中返回 JSON,不要返回 images。';
|
|
58
|
+
const userPrompt = `schema:\n${JSON.stringify(schema, null, 2)}\n\n请返回 JSON:{"source":"","category":"","content":"","tags":[],"metadata":{}}`;
|
|
59
|
+
const res = await fetch(url, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
Authorization: `Bearer ${key}`,
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
model,
|
|
67
|
+
messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }],
|
|
68
|
+
temperature: 0.7,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) throw new Error(`LLM 请求失败: ${res.status}`);
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
const content = data.choices?.[0]?.message?.content;
|
|
74
|
+
if (!content) throw new Error('LLM 未返回 content');
|
|
75
|
+
const parsed = JSON.parse(content.replace(/```json\s*|\s*```/g, '').trim());
|
|
76
|
+
return {
|
|
77
|
+
source: String(parsed.source ?? ''),
|
|
78
|
+
category: String(parsed.category ?? ''),
|
|
79
|
+
content: String(parsed.content ?? ''),
|
|
80
|
+
tags: Array.isArray(parsed.tags) ? parsed.tags.map(String) : [],
|
|
81
|
+
metadata: parsed.metadata ?? {},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readSSEResult(res: Response): Promise<AIProcessedData> {
|
|
86
|
+
return res.text().then((text) => {
|
|
87
|
+
const lines = text.split('\n').filter((l) => l.startsWith('data: '));
|
|
88
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
89
|
+
const data = lines[i].replace(/^data: /, '').trim();
|
|
90
|
+
try {
|
|
91
|
+
const json = JSON.parse(data);
|
|
92
|
+
if (json.result) return json.result as AIProcessedData;
|
|
93
|
+
if (json.error) throw new Error(json.error);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
if (e instanceof Error && e.message !== 'Unexpected end of JSON input') throw e;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw new Error('No result in SSE stream');
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mergeWithRawData(apiResult: AIProcessedData, rawData: any): AIProcessedData {
|
|
103
|
+
const images = extractImages(rawData);
|
|
104
|
+
return {
|
|
105
|
+
...apiResult,
|
|
106
|
+
images: images.length > 0 ? images : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hasImageInRawData(rawData: any): boolean {
|
|
111
|
+
return extractImages(rawData).length > 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasSubstantialContent(schema: any): boolean {
|
|
115
|
+
if (schema === null || schema === undefined) return false;
|
|
116
|
+
if (typeof schema === 'string') return schema.length > 0;
|
|
117
|
+
if (typeof schema === 'number' || typeof schema === 'boolean') return true;
|
|
118
|
+
if (Array.isArray(schema)) return schema.some(hasSubstantialContent);
|
|
119
|
+
if (typeof schema === 'object') return Object.values(schema).some(hasSubstantialContent);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isMinimalSchema(schema: any): boolean {
|
|
124
|
+
return !hasSubstantialContent(schema);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeResult(result: AIProcessedData): AIProcessedData {
|
|
128
|
+
return {
|
|
129
|
+
source: String(result.source ?? ''),
|
|
130
|
+
category: String(result.category ?? ''),
|
|
131
|
+
content: String(result.content ?? ''),
|
|
132
|
+
tags: Array.isArray(result.tags) ? result.tags.map(String) : [],
|
|
133
|
+
metadata: result.metadata ?? {},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const fixtures: { name: string; rawData: any }[] = [
|
|
138
|
+
{ name: '简单扁平对象', rawData: { title: '一篇笔记', content: '今天学习了 Next.js 和 LLM 集成,收获很大。', type: 'note' } },
|
|
139
|
+
{ name: '深层嵌套对象', rawData: { a: { b: { c: { d: { e: { f: 'deep' } } } } } } },
|
|
140
|
+
{ name: '长字符串', rawData: { text: '很长的正文。'.repeat(200), title: '长文' } },
|
|
141
|
+
{ name: '长数组', rawData: { list: Array.from({ length: 15 }, (_, i) => ({ id: i, name: `item${i}` })) } },
|
|
142
|
+
{
|
|
143
|
+
name: '含 base64 图片',
|
|
144
|
+
rawData: {
|
|
145
|
+
title: '带图笔记',
|
|
146
|
+
content: '有一张图',
|
|
147
|
+
image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{ name: '空对象', rawData: {} },
|
|
151
|
+
{ name: '空数组', rawData: { items: [] } },
|
|
152
|
+
{
|
|
153
|
+
name: '混合:嵌套+数组+长文本',
|
|
154
|
+
rawData: {
|
|
155
|
+
meta: { app: 'Test', version: 1 },
|
|
156
|
+
sections: [
|
|
157
|
+
{ heading: 'A', paragraphs: ['段落一。', '段落二。'.repeat(100)] },
|
|
158
|
+
{ heading: 'B', paragraphs: ['段落三'] },
|
|
159
|
+
],
|
|
160
|
+
url: 'https://example.com',
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{ name: '根级数组', rawData: [{ id: 1, name: 'a' }, { id: 2, name: 'b' }, { id: 3, name: 'c' }] },
|
|
164
|
+
{ name: '数字与布尔', rawData: { count: 42, active: true, label: '状态' } },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
async function run(): Promise<void> {
|
|
168
|
+
console.log('LLM Schema 真实测试(agentlink-sdk inferSchema + extractImages)\n');
|
|
169
|
+
const apiUrl = getApiUrl();
|
|
170
|
+
const useServer = Boolean(process.env.AGENTLINK_SERVER_URL);
|
|
171
|
+
if (useServer) console.log('模式 A: AGENTLINK_SERVER_URL\n');
|
|
172
|
+
else console.log('模式 B: LLM_API_URL + LLM_API_KEY\n');
|
|
173
|
+
|
|
174
|
+
let passed = 0;
|
|
175
|
+
let failed = 0;
|
|
176
|
+
|
|
177
|
+
for (const { name, rawData } of fixtures) {
|
|
178
|
+
process.stdout.write(`[${name}] ... `);
|
|
179
|
+
try {
|
|
180
|
+
const schema = inferSchema(rawData);
|
|
181
|
+
let apiResult: AIProcessedData;
|
|
182
|
+
|
|
183
|
+
if (useServer) {
|
|
184
|
+
const res = await postSchemaToServer(apiUrl, schema);
|
|
185
|
+
if (!res.ok) {
|
|
186
|
+
const err = await res.json().catch(() => ({}));
|
|
187
|
+
throw new Error((err as { error?: string }).error || res.statusText);
|
|
188
|
+
}
|
|
189
|
+
apiResult = await readSSEResult(res);
|
|
190
|
+
} else {
|
|
191
|
+
apiResult = await callLlmDirect(schema);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const minimal = isMinimalSchema(schema);
|
|
195
|
+
if (minimal) {
|
|
196
|
+
const normalized = normalizeResult(apiResult);
|
|
197
|
+
if (
|
|
198
|
+
typeof normalized.source !== 'string' ||
|
|
199
|
+
typeof normalized.category !== 'string' ||
|
|
200
|
+
typeof normalized.content !== 'string' ||
|
|
201
|
+
!Array.isArray(normalized.tags)
|
|
202
|
+
) {
|
|
203
|
+
throw new Error('API 返回缺少必需字段结构: source, category, content, tags');
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
if (
|
|
207
|
+
!apiResult.source ||
|
|
208
|
+
!apiResult.category ||
|
|
209
|
+
!apiResult.content ||
|
|
210
|
+
!Array.isArray(apiResult.tags)
|
|
211
|
+
) {
|
|
212
|
+
throw new Error('API 返回缺少必需字段: source, category, content, tags');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (apiResult.images !== undefined) {
|
|
216
|
+
throw new Error('API 不应返回 images(应由前端回填)');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const resultForMerge = minimal ? normalizeResult(apiResult) : apiResult;
|
|
220
|
+
const final = mergeWithRawData(resultForMerge, rawData);
|
|
221
|
+
if (
|
|
222
|
+
typeof final.source !== 'string' ||
|
|
223
|
+
typeof final.category !== 'string' ||
|
|
224
|
+
typeof final.content !== 'string' ||
|
|
225
|
+
!Array.isArray(final.tags)
|
|
226
|
+
) {
|
|
227
|
+
throw new Error('回填后仍缺少必需字段');
|
|
228
|
+
}
|
|
229
|
+
if (hasImageInRawData(rawData) && (!final.images || final.images.length === 0)) {
|
|
230
|
+
throw new Error('rawData 含图片但回填后 final.images 为空');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log('通过');
|
|
234
|
+
passed++;
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.log('失败:', e instanceof Error ? e.message : e);
|
|
237
|
+
failed++;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log('\n--- 汇总 ---');
|
|
242
|
+
console.log(`通过: ${passed}, 失败: ${failed}, 总计: ${fixtures.length}`);
|
|
243
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
run();
|