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 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
- export { AgentLinkClient, type AgentLinkClientOptions, DEFAULT_SERVER_URL, type SenderInfo, type URLData, type WhitelistInfo, type WhitelistResponse, base64ToUint8Array, compress, decodeDataFromUrl, decompress, encodeDataToUrl, fetchWhitelistInfo, uint8ArrayToBase64, verifyWhitelist };
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
- export { AgentLinkClient, type AgentLinkClientOptions, DEFAULT_SERVER_URL, type SenderInfo, type URLData, type WhitelistInfo, type WhitelistResponse, base64ToUint8Array, compress, decodeDataFromUrl, decompress, encodeDataToUrl, fetchWhitelistInfo, uint8ArrayToBase64, verifyWhitelist };
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.7",
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();
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['src/**/*.test.ts', 'src/**/*.spec.ts'],
7
+ },
8
+ });