@axiom-lattice/examples-deep_research 1.0.93 → 1.0.94

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.
@@ -18,6 +18,28 @@ interface ApiEntry {
18
18
  fields: string[];
19
19
  }
20
20
 
21
+ // ============================================================
22
+ // 支持 $expand 的文档类实体
23
+ // ============================================================
24
+
25
+ const EXPAND_FIELDS: Record<string, string[]> = {
26
+ "DocumentLines": [
27
+ "LineNum", "ItemCode", "ItemDescription", "Quantity", "UnitPrice",
28
+ "WarehouseCode", "Total", "Currency", "TaxCode", "PriceAfterVAT",
29
+ "UoMEntry", "UoMCode",
30
+ ],
31
+ "DocumentAdditionalExpenses": [
32
+ "LineNum", "ExpenseCode", "LineTotal", "TaxCode", "VatGroup",
33
+ ],
34
+ };
35
+
36
+ const ENTITIES_WITH_LINES = new Set([
37
+ "Orders", "DeliveryNotes", "Invoices", "Quotations", "CreditNotes",
38
+ "Returns", "Drafts", "PurchaseOrders", "PurchaseDeliveryNotes",
39
+ "PurchaseInvoices", "PurchaseReturns", "PurchaseQuotations",
40
+ "DownPayments", "InventoryGenEntries", "InventoryGenExits",
41
+ ]);
42
+
21
43
  const API_LIST: ApiEntry[] = [
22
44
  // ========== Business Partner ==========
23
45
  {
@@ -30,9 +52,11 @@ const API_LIST: ApiEntry[] = [
30
52
  fields: [
31
53
  "CardCode", "CardName", "CardType", "GroupCode", "Currency",
32
54
  "Phone1", "Phone2", "EmailAddress", "Address", "City", "Country",
33
- "SalesPersonCode", "PriceListNum", "CreditLimit", "Balance",
55
+ "SalesPersonCode", "PriceListNum", "CreditLimit",
56
+ "CurrentAccountBalance", "OpenOrdersBalance", "OpenDeliveryNotesBalance",
34
57
  "PayTermsGrpCode", "VatGroup", "VatLiable", "FederalTaxID",
35
- "Valid", "Frozen", "CompanyPrivate", "CreateDate", "UpdateDate",
58
+ "Valid", "Frozen", "CompanyPrivate", "CreateDate", "CreateTime", "UpdateDate", "UpdateTime",
59
+ "BPAddresses", "ContactEmployees",
36
60
  ],
37
61
  },
38
62
  {
@@ -70,6 +94,7 @@ const API_LIST: ApiEntry[] = [
70
94
  "ManageSerialNumbers", "ManageBatchNumbers",
71
95
  "SalesVATGroup", "PurchaseVATGroup",
72
96
  "Valid", "Frozen", "CreateDate", "UpdateDate",
97
+ "ItemPrices", "ItemWarehouseInfoCollection",
73
98
  ],
74
99
  },
75
100
  {
@@ -120,6 +145,7 @@ const API_LIST: ApiEntry[] = [
120
145
  "Comments", "Reference1", "Reference2", "NumAtCard",
121
146
  "VatSum", "RoundDif", "DiscountPercent",
122
147
  "PaymentGroupCode", "Project",
148
+ "DocumentLines",
123
149
  ],
124
150
  },
125
151
  {
@@ -134,6 +160,7 @@ const API_LIST: ApiEntry[] = [
134
160
  "CardCode", "CardName", "DocTotal", "DocCurrency",
135
161
  "SalesPersonCode", "Confirmed", "Cancelled", "DocumentStatus",
136
162
  "Comments", "NumAtCard",
163
+ "DocumentLines",
137
164
  ],
138
165
  },
139
166
  {
@@ -148,6 +175,7 @@ const API_LIST: ApiEntry[] = [
148
175
  "CardCode", "CardName", "DocTotal", "DocCurrency",
149
176
  "SalesPersonCode", "Confirmed", "Cancelled", "DocumentStatus",
150
177
  "Comments", "NumAtCard", "VatSum",
178
+ "DocumentLines",
151
179
  ],
152
180
  },
153
181
  {
@@ -161,6 +189,7 @@ const API_LIST: ApiEntry[] = [
161
189
  "DocEntry", "DocNum", "DocType", "DocDate", "DocDueDate",
162
190
  "CardCode", "CardName", "DocTotal", "DocCurrency",
163
191
  "SalesPersonCode", "Comments", "DocumentStatus",
192
+ "DocumentLines",
164
193
  ],
165
194
  },
166
195
  {
@@ -174,6 +203,7 @@ const API_LIST: ApiEntry[] = [
174
203
  "DocEntry", "DocNum", "DocType", "DocDate",
175
204
  "CardCode", "CardName", "DocTotal", "DocCurrency",
176
205
  "Comments",
206
+ "DocumentLines",
177
207
  ],
178
208
  },
179
209
  {
@@ -187,6 +217,7 @@ const API_LIST: ApiEntry[] = [
187
217
  "DocEntry", "DocNum", "DocType", "DocDate",
188
218
  "CardCode", "CardName", "DocTotal", "DocCurrency",
189
219
  "Comments",
220
+ "DocumentLines",
190
221
  ],
191
222
  },
192
223
  {
@@ -200,6 +231,7 @@ const API_LIST: ApiEntry[] = [
200
231
  "DocEntry", "DocNum", "DocType", "DocDate",
201
232
  "CardCode", "CardName", "DocTotal", "DocCurrency",
202
233
  "DownPaymentType", "DownPaymentAmount",
234
+ "DocumentLines",
203
235
  ],
204
236
  },
205
237
  {
@@ -231,6 +263,7 @@ const API_LIST: ApiEntry[] = [
231
263
  "CardCode", "CardName", "DocTotal", "DocCurrency",
232
264
  "SalesPersonCode", "Confirmed", "Cancelled", "DocumentStatus",
233
265
  "Comments", "NumAtCard",
266
+ "DocumentLines",
234
267
  ],
235
268
  },
236
269
  {
@@ -243,6 +276,7 @@ const API_LIST: ApiEntry[] = [
243
276
  fields: [
244
277
  "DocEntry", "DocNum", "DocType", "DocDate",
245
278
  "CardCode", "CardName", "DocTotal", "Comments",
279
+ "DocumentLines",
246
280
  ],
247
281
  },
248
282
  {
@@ -256,6 +290,7 @@ const API_LIST: ApiEntry[] = [
256
290
  "DocEntry", "DocNum", "DocType", "DocDate", "DocDueDate",
257
291
  "CardCode", "CardName", "DocTotal", "DocCurrency",
258
292
  "Comments",
293
+ "DocumentLines",
259
294
  ],
260
295
  },
261
296
  {
@@ -268,6 +303,7 @@ const API_LIST: ApiEntry[] = [
268
303
  fields: [
269
304
  "DocEntry", "DocNum", "DocType", "DocDate",
270
305
  "CardCode", "CardName", "DocTotal", "Comments",
306
+ "DocumentLines",
271
307
  ],
272
308
  },
273
309
  {
@@ -280,6 +316,7 @@ const API_LIST: ApiEntry[] = [
280
316
  fields: [
281
317
  "DocEntry", "DocNum", "DocType", "DocDate",
282
318
  "CardCode", "CardName", "DocTotal", "Comments",
319
+ "DocumentLines",
283
320
  ],
284
321
  },
285
322
 
@@ -295,6 +332,7 @@ const API_LIST: ApiEntry[] = [
295
332
  "DocEntry", "DocNum", "DocType", "DocDate",
296
333
  "Comments", "JournalMemo", "Reference1", "Reference2",
297
334
  "WareHouseUpdateType",
335
+ "DocumentLines",
298
336
  ],
299
337
  },
300
338
  {
@@ -307,6 +345,7 @@ const API_LIST: ApiEntry[] = [
307
345
  fields: [
308
346
  "DocEntry", "DocNum", "DocType", "DocDate",
309
347
  "Comments", "JournalMemo", "Reference1", "Reference2",
348
+ "DocumentLines",
310
349
  ],
311
350
  },
312
351
  {
@@ -573,20 +612,51 @@ const API_LIST: ApiEntry[] = [
573
612
  // Tool 1: sap_api_search
574
613
  // ============================================================
575
614
 
615
+ function mapResult(e: ApiEntry): Record<string, unknown> {
616
+ const r: Record<string, unknown> = {
617
+ name: e.name,
618
+ kind: e.kind,
619
+ domain: e.domain,
620
+ description: e.description,
621
+ primaryKey: e.primaryKey || null,
622
+ fields: e.fields,
623
+ readySelect: e.fields.length > 0 ? `$select=${e.fields.join(",")}` : undefined,
624
+ hint:
625
+ e.kind === "EntitySet"
626
+ ? `${e.name} — ${e.description}。主键: ${e.primaryKey}。调用 sap_api_call 进行 CRUD 操作。`
627
+ : `${e.name} — ${e.description}。调用 sap_api_call 执行此方法。`,
628
+ };
629
+ if (ENTITIES_WITH_LINES.has(e.name)) {
630
+ r.expand = ["DocumentLines", "DocumentAdditionalExpenses"];
631
+ r.lineFields = ["LineNum", "ItemCode", "ItemDescription", "Quantity", "UnitPrice", "Price", "WarehouseCode", "UoMEntry", "UoMCode", "VatGroup", "Currency", "TaxCode", "PriceAfterVAT"];
632
+ }
633
+ if (e.name === "Items") {
634
+ r.expand = ["ItemPrices", "ItemWarehouseInfoCollection"];
635
+ }
636
+ if (e.name === "BusinessPartners") {
637
+ r.expand = ["BPAddresses", "ContactEmployees"];
638
+ }
639
+ return r;
640
+ }
641
+
576
642
  registerToolLattice(
577
643
  "sap_api_search",
578
644
  {
579
645
  name: "sap_api_search",
580
646
  description:
581
- "搜索 SAP B1 Service Layer API 接口。覆盖业务伙伴(BP)、物料(Item)、销售/采购订单(Document/Order)、" +
582
- "库存(Inventory/Warehouse) 四大领域。返回接口名称、主键、常用字段列表及描述。",
647
+ "搜索 SAP B1 Service Layer API 接口元数据,覆盖 BP/物料/订单/库存四大领域。" +
648
+ "返回接口名、主键、字段列表、可选 expand 导航。" +
649
+ "可用 discover 模式发现关联接口(如搜 SalesPerson 也能找到 SalesPersons API)。" +
650
+ "搜索结果中的 readySelect 可直接复制到 $select。",
583
651
  needUserApprove: false,
584
652
  schema: z.object({
585
653
  query: z
586
654
  .string()
655
+ .optional()
587
656
  .describe(
588
- "搜索关键词。可以是 API 名称(如 'BusinessPartners', 'Orders', 'Items', 'PurchaseOrders')" +
589
- "或业务描述(如 '客户', '订单', '物料', '库存', '采购', '仓库')"
657
+ "搜索关键词(可选,不传则返回全部接口)。" +
658
+ "API 名称如 'BusinessPartners', 'Orders', 'Items', 'PurchaseOrders'," +
659
+ "或中文描述如 '客户', '订单', '物料', '库存', '采购', '仓库'"
590
660
  ),
591
661
  domain: z
592
662
  .string()
@@ -594,14 +664,15 @@ registerToolLattice(
594
664
  .describe(
595
665
  "按领域过滤: 'BusinessPartner'(BP), 'Item / Product'(物料), 'Document'(订单/发票), 'Inventory / Warehouse'(库存/仓库)"
596
666
  ),
597
- maxResults: z.number().optional().default(10).describe("最大返回条数"),
667
+ maxResults: z.number().optional().default(20).describe("最大返回条数"),
598
668
  }),
599
669
  },
600
670
  async (input) => {
601
- const q = input.query.toLowerCase();
602
- const max = input.maxResults ?? 10;
671
+ const q = input.query?.toLowerCase();
672
+ const max = input.maxResults ?? 20;
673
+
674
+ const hasQuery = q && q.trim().length > 0;
603
675
 
604
- // 中文关键词映射到领域
605
676
  const domainHints: Record<string, string> = {
606
677
  "客户": "BusinessPartner",
607
678
  "供应商": "BusinessPartner",
@@ -617,6 +688,8 @@ registerToolLattice(
617
688
  "交货": "Document",
618
689
  "报价": "Document",
619
690
  "草稿": "Document",
691
+ "draft": "Document",
692
+ "暂存": "Document",
620
693
  "库存": "Inventory / Warehouse",
621
694
  "仓库": "Inventory / Warehouse",
622
695
  "库位": "Inventory / Warehouse",
@@ -633,13 +706,29 @@ registerToolLattice(
633
706
  "到岸": "Document",
634
707
  };
635
708
 
636
- const hintedDomain = domainHints[q] || undefined;
709
+ const hintedDomain = q ? (domainHints[q] || undefined) : undefined;
637
710
  const effectiveDomain = input.domain || hintedDomain;
638
711
 
639
- const scored = API_LIST.filter((e) => {
712
+ const filtered = API_LIST.filter((e) => {
640
713
  if (effectiveDomain && e.domain !== effectiveDomain) return false;
641
714
  return true;
642
- }).map((e) => {
715
+ });
716
+
717
+ if (!q) {
718
+ const top = filtered.slice(0, max);
719
+ const domainCounts: Record<string, number> = {};
720
+ for (const e of top) domainCounts[e.domain] = (domainCounts[e.domain] || 0) + 1;
721
+ return {
722
+ query: input.query || null,
723
+ domainFilter: effectiveDomain || null,
724
+ totalMatches: filtered.length,
725
+ domainsFound: domainCounts,
726
+ results: top.map(mapResult),
727
+ suggestion: undefined,
728
+ };
729
+ }
730
+
731
+ const scored = filtered.map((e) => {
643
732
  let score = 0;
644
733
  const nameLo = e.name.toLowerCase();
645
734
  const descLo = e.description.toLowerCase();
@@ -668,22 +757,11 @@ registerToolLattice(
668
757
  for (const e of top) domainCounts[e.domain] = (domainCounts[e.domain] || 0) + 1;
669
758
 
670
759
  return {
671
- query: input.query,
760
+ query: input.query!,
672
761
  domainFilter: effectiveDomain || null,
673
762
  totalMatches: scored.filter((e) => e.score > 0).length,
674
763
  domainsFound: domainCounts,
675
- results: top.map((e) => ({
676
- name: e.name,
677
- kind: e.kind,
678
- domain: e.domain,
679
- description: e.description,
680
- primaryKey: e.primaryKey || null,
681
- fields: e.fields,
682
- hint:
683
- e.kind === "EntitySet"
684
- ? `${e.name} — ${e.description}。主键: ${e.primaryKey}。调用 sap_api_call 进行 CRUD 操作。`
685
- : `${e.name} — ${e.description}。调用 sap_api_call 执行此方法。`,
686
- })),
764
+ results: top.map(mapResult),
687
765
  suggestion:
688
766
  top.length === 0
689
767
  ? `未找到匹配 "${input.query}" 的接口。可用领域: BusinessPartner(客户/供应商), Item / Product(物料), Document(订单/发票), Inventory / Warehouse(库存/仓库)。尝试用英文名称搜索。`
@@ -705,9 +783,24 @@ registerToolLattice(
705
783
  {
706
784
  name: "sap_api_call",
707
785
  description:
708
- "执行对 SAP B1 Service Layer 的 OData API 调用。直接发起 HTTP 请求并返回响应数据。" +
709
- `当前 Base URL: ${BASE_URL}。` +
710
- "GET 请求通常无需认证,POST/PATCH/DELETE 需设置环境变量 SAP_B1SESSION。",
786
+ "执行 SAP B1 Service Layer 的 OData API 查询/创建/更新/删除。" +
787
+ `Base: ${BASE_URL}。` +
788
+ "⚠️ 先确认是否已通过 sap_api_search 查过字段列表,勿凭记忆编字段名。\n" +
789
+ "⚠️ 草稿/draft/暂存 → 用 Drafts 接口,不是 Orders/Invoices 等正式单据。" +
790
+ "Drafts 通过 DocObjectCode 区分单据类型(17=订单,13=发票,16=交货单,23=采购订单)。\n" +
791
+ "$filter 操作符: eq/ne/gt/lt/ge/le/contains(f,'v')/startswith(f,'v')/endswith(f,'v'),多条件用 and/or。" +
792
+ "字符串值必须单引号包裹。$orderby=Field desc 排序。\n\n" +
793
+ "⚠️ SAP B1 实战经验:\n" +
794
+ "1. 不要用 $expand!$expand 极易触发 400/500。改用 $select 包含嵌套字段,如 $select=DocEntry,DocNum,DocumentLines,\n" +
795
+ " DocumentLines 会自动作为嵌套 JSON 返回。ItemPrices、BPAddresses 等同理。\n" +
796
+ "2. 不要用主键路径 /Orders('1173'),易 500。用 $filter=DocEntry eq 1173 代替。\n" +
797
+ "3. 查单条记录时优先 $filter,而非传 id 参数。\n" +
798
+ "400 多为特殊字符未编码(引号用 %27);500 多为字段不存在或用错了 $expand;无结果则放宽 filter。\n" +
799
+ "POST 创建: sap_api_search 返回的 lineFields 是 DocumentLines 子字段。" +
800
+ "body 必含 DocObjectCode(DocType)、CardCode、DocDate;DocumentLines 为数组,每项必含 ItemCode、Quantity。" +
801
+ "PATCH 只传变更字段;DELETE 需传 id。\n" +
802
+ "GET 自动注入 $select+$top=20,手动传入可覆盖。嵌套集合(DocumentLines等)自动裁剪只保留常用字段,防 token 爆炸。" +
803
+ "认证需 SAP_B1SESSION 环境变量。",
711
804
  needUserApprove: false,
712
805
  schema: z.object({
713
806
  entitySet: z
@@ -722,18 +815,18 @@ registerToolLattice(
722
815
  .string()
723
816
  .optional()
724
817
  .describe(
725
- "OData 查询参数(不含 `?` 前缀)。常用: " +
726
- "$top=10, $skip=20, " +
727
- "$select=CardCode,CardName, " +
728
- "$filter=contains(CardName,'清华'), " +
729
- "$orderby=DocDate desc, " +
730
- "$expand=DocumentLines"
818
+ "OData 查询参数(不含 `?` 前缀)。" +
819
+ "常用: $top=10, $select=CardCode,CardName, " +
820
+ "$filter=CardName eq 'xxx' or contains(CardName,'xxx'), " +
821
+ "$orderby=DocDate desc, $expand=DocumentLines。字符串值用单引号。" +
822
+ "sap_api_search 返回的 readySelect 可直接复制到 $select。"
731
823
  ),
732
824
  body: z.record(z.unknown()).optional().describe("POST/PATCH 时的 JSON 请求体"),
733
825
  }),
734
826
  },
735
827
  async (input) => {
736
- const url = buildUrl(input.entitySet, input.method, input.id, input.queryOptions);
828
+ const queryOptions = applyDefaultSelect(input.entitySet, input.method, input.id, input.queryOptions);
829
+ const url = buildUrl(input.entitySet, input.method, input.id, queryOptions);
737
830
  const method = input.method;
738
831
 
739
832
  const headers: Record<string, string> = {
@@ -749,7 +842,8 @@ registerToolLattice(
749
842
 
750
843
  try {
751
844
  const res = await fetch(url, fetchOptions);
752
- const text = await res.text();
845
+ const buffer = await res.arrayBuffer();
846
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(buffer);
753
847
 
754
848
  let data: unknown;
755
849
  try {
@@ -758,6 +852,8 @@ registerToolLattice(
758
852
  data = text;
759
853
  }
760
854
 
855
+ cleanODataNoise(data);
856
+
761
857
  const result: Record<string, unknown> = {
762
858
  ok: res.ok,
763
859
  status: res.status,
@@ -792,6 +888,74 @@ registerToolLattice(
792
888
  // Helpers
793
889
  // ============================================================
794
890
 
891
+ function applyDefaultSelect(entitySet: string, method: string, id?: string, queryOptions?: string): string | undefined {
892
+ if (method !== "GET") return queryOptions;
893
+
894
+ const parts: string[] = [];
895
+
896
+ if (!id && (!queryOptions || !/\$top\s*=/.test(queryOptions))) {
897
+ parts.push("$top=20");
898
+ }
899
+
900
+ if (!queryOptions || !/\$select\s*=/.test(queryOptions)) {
901
+ const entry = API_LIST.find(
902
+ (e) => e.kind === "EntitySet" && e.name === entitySet
903
+ );
904
+ if (entry && entry.fields.length > 0) {
905
+ parts.push(`$select=${entry.fields.join(",")}`);
906
+ }
907
+ }
908
+
909
+ if (queryOptions) {
910
+ parts.push(queryOptions);
911
+ }
912
+
913
+ return parts.length > 0 ? parts.join("&").replace(/'/g, "%27") : undefined;
914
+ }
915
+
916
+ function cleanODataNoise(data: unknown): void {
917
+ if (!data || typeof data !== "object") return;
918
+ if (Array.isArray(data)) {
919
+ for (const item of data) cleanODataNoise(item);
920
+ return;
921
+ }
922
+ const obj = data as Record<string, unknown>;
923
+
924
+ delete obj["odata.metadata"];
925
+ delete obj["odata.etag"];
926
+ delete obj["odata.nextLink"];
927
+
928
+ if (Array.isArray(obj.value)) {
929
+ for (const record of obj.value) {
930
+ if (record && typeof record === "object") {
931
+ trimNestedCollections(record as Record<string, unknown>);
932
+ delete (record as Record<string, unknown>)["odata.etag"];
933
+ }
934
+ }
935
+ }
936
+
937
+ for (const v of Object.values(obj)) {
938
+ if (v && typeof v === "object") cleanODataNoise(v);
939
+ }
940
+ }
941
+
942
+ function trimNestedCollections(obj: Record<string, unknown>): void {
943
+ for (const [key, val] of Object.entries(obj)) {
944
+ if (Array.isArray(val) && EXPAND_FIELDS[key]) {
945
+ const keep = new Set(EXPAND_FIELDS[key]);
946
+ for (const item of val) {
947
+ if (item && typeof item === "object") {
948
+ const record = item as Record<string, unknown>;
949
+ for (const k of Object.keys(record)) {
950
+ if (!keep.has(k)) delete record[k];
951
+ }
952
+ cleanODataNoise(item);
953
+ }
954
+ }
955
+ }
956
+ }
957
+ }
958
+
795
959
  function buildUrl(
796
960
  entitySet: string,
797
961
  method: string,