@xtalpi/agentic-lab-skills 0.0.7 → 0.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtalpi/agentic-lab-skills",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Agentic Lab Skills",
5
5
  "license": "MIT",
6
6
  "author": "TheoXiong",
@@ -83,7 +83,7 @@ metadata:
83
83
  - **必须**向用户交付 **《流程文档合规问题与优化建议》**(可用 Markdown 小节组织),且至少包含:
84
84
  1. **问题清单**:每条对应标准中的位置(例如「§4 清单第 n 项」「§3.2 不支持的 API」「§3.3 `items` 结构不完整」)。
85
85
  2. **现状说明**:流程文档中**缺失**、**矛盾**、**模糊**或**不可解析**之处(可引用现有标题、表格列名、YAML 片段;对不支持的依赖/API/字段须**点名**)。
86
- 3. **可执行修改建议**:具体应**补写/改写**哪一类内容(例如消除模糊条件、改为 SDK 已支持方法、补全 `process.execute` 的 `items`/`params`/`baseURL` 说明、统一字段拼写)。
86
+ 3. **可执行修改建议**:具体应**补写/改写**哪一类内容(例如消除模糊条件、改为 SDK 已支持方法、补全 SDK 调用方法的参数说明、统一字段拼写)。
87
87
  4. **严重程度**:**阻断**(不满足则无法稳定生成,含不支持的 API、不可判定逻辑、Process 参数无法落地)与**建议**(不阻断但易导致脚本/Schema 歧义)。
88
88
  5. **(若适用)不支持或不可生成项**:集中列出标准 **§3.2 / §3.3** 拦截项,避免与一般格式问题混排。
89
89
  - 仅当用户**随后**提供已按建议修订的流程文档时,才允许重新从本预检开始执行。
@@ -119,7 +119,7 @@ metadata:
119
119
  - 数据来源与操作按钮描述
120
120
  - **后置处理** → 生成 `Processer.complete` 函数逻辑。须提取:
121
121
  - 规则摘要表(条件要点 + 业务动作),落入 `complete` 函数体
122
- - 提交参数结构(如 `process.execute` 的 `items` 定义与 `params` 键级说明)
122
+ - 提交参数结构(如 SDK 调用方法的参数定义与键级说明)
123
123
  - 出口池路由条件与字段映射
124
124
  - **门控脚本配置项**:提取全局配置表中的各项,落入脚本为**模块级常量**:
125
125
  - `PageUrl`(含 `{bookid}` 占位符)→ 脚本常量 `PAGE_URL`
@@ -161,7 +161,7 @@ metadata:
161
161
 
162
162
  - **`### 数据池(Pool)`**:说明 Pool 存记录、每池有独立 Schema、随阶段变化。
163
163
  - **`### 门控(Valve)`**:说明每个 Valve 对应脚本、`Processer` 的 `start` / `complete` 职责分工。
164
- 另起简短列表说明执行引擎通过 `this.context` 暴露的能力,**仅写** [references/agentic-lab-sdk.md](references/agentic-lab-sdk.md) 中已出现项(如 `ticket.list` / `update` / `append`、`pool.getNext`、`process.list`、`process.execute`、`station.list`、`compound.getStock`、`compound.getChemicalRecord`),**不要**抄写模板里已过期的成员名。
164
+ 另起简短列表说明执行引擎通过 `this.context` 暴露的能力,**仅写** [references/agentic-lab-sdk.md](references/agentic-lab-sdk.md)「可用方法一览」中已列出的方法名(按流程实际使用选取),**不要**抄写模板里已过期的成员名。
165
165
 
166
166
  #### 3.4 `## 流程图`(本节内须含「连接关系」列表,与模板一致)
167
167
 
@@ -238,11 +238,11 @@ metadata:
238
238
 
239
239
  若流程说明对某门控给出了下列块,须在对应 `valves/<基名>.md` 中**原样结构化呈现**(标题可用 `##` / `###`,便于脚本作者对照):
240
240
 
241
- - **门控 YAML**(`valve_id`、`name`、`order`、`input`、`output`);若文档使用 `Stash:` 等非标准键表示目标池,**保留原文**,并在 valve 文内加一句说明:实现时与 `pool.getNext` 返回的 `pool.name` 按**池显示名称**匹配。
241
+ - **门控 YAML**(`valve_id`、`name`、`order`、`input`、`output`);若文档使用 `Stash:` 等非标准键表示目标池,**保留原文**,并在 valve 文内加一句说明:实现时按 [agentic-lab-sdk.md](references/agentic-lab-sdk.md) 获取出口池,以返回的 `pool.name` 与**池显示名称**匹配。
242
242
  - **前置处理**(对应 `start`):提取配置项(如 `bookid`)、规则摘要表,说明 `start` 函数需执行的逻辑。
243
243
  - **人工处理**:界面形态与数据绑定描述(仅供参考,不由门控脚本实现)。
244
- - **后置处理**(对应 `complete`):规则摘要表、提交参数结构(如 `process.execute` `items` 定义),说明 `complete` 函数需执行的逻辑。
245
- - **化合物数据查询方式**(或等价标题):表或段落中「查 compound / chemical_record」等表述,与 [references/agentic-lab-sdk.md](references/agentic-lab-sdk.md) `compound.getStock`、`compound.getChemicalRecord` 的对应关系写清。
244
+ - **后置处理**(对应 `complete`):规则摘要表、提交参数结构(如 SDK 调用方法的参数定义),说明 `complete` 函数需执行的逻辑。
245
+ - **化合物数据查询方式**(或等价标题):表或段落中「查化合物库存」等表述,与 [references/agentic-lab-sdk.md](references/agentic-lab-sdk.md) 中对应 SDK 方法的映射关系写清。
246
246
  - **字段映射表**:「门控加工数据 ↔ 库字段 / API 字段」;脚本写入 `ticket.detail` 的键须与 **pools Schema「字段」列(snake_case)** 一致。
247
247
  - **数据处理规则**表:序号(如 1.1、1.2)须在脚本注释中可逐条追溯。
248
248
 
@@ -272,23 +272,20 @@ metadata:
272
272
 
273
273
  | 来源 | 落点(脚本侧) |
274
274
  |------|----------------|
275
- | 门控 YAML `input` | `start` `ticket.list({ pool_id__in, limit, offset })` `pool_id__in`(入口池 ID 数组;`limit`/`offset` 遵守 §2);**返回** `TicketListResponse`,业务数据取 **`response.list`**;含 `secondary` 时合并多个入口池 ID 后再传入 `pool_id__in` |
276
- | 门控 YAML `output` | `complete` `await this.context.pool.getNext(valve_id)` 后按 `pool.name` 与条件分支筛选目标池 |
277
- | **化合物数据查询方式** | `compound.getStock`、`compound.getChemicalRecord`:入参为 **`filter`(NocoBase Repository 风格)** + 可选 `limit`/`offset`;算子见 agentic-lab-sdk 篇首与 [Filter Operators](https://docs.nocobase.com/api/database/operators) |
275
+ | 门控 YAML `input` | `start` 中按 [agentic-lab-sdk.md](references/agentic-lab-sdk.md) 查询入口池 tickets(`limit`/`offset` 遵守 §2);含 `secondary` 时合并多个入口池 ID |
276
+ | 门控 YAML `output` | `complete` 中按 [agentic-lab-sdk.md](references/agentic-lab-sdk.md) 获取出口池,以 `pool.name` 与条件分支筛选目标池 |
277
+ | **化合物数据查询方式** | [agentic-lab-sdk.md](references/agentic-lab-sdk.md) 调用化合物库存查询方法 |
278
278
  | **字段映射表** | 将 API 返回字段写入 `ticket.detail` 的 snake_case 键 |
279
279
  | **数据处理规则**(序号表) | `start` 内计算与回填,或 `complete` 内最终路由前校验;关键分支写 `// 规则 1.x` 注释 |
280
280
  | **门控脚本配置项**表 | 脚本模块级常量:`PAGE_URL`(取 `PageUrl` 值)、`DEFAULT_QUERY_LIMIT`(取入口池查询上限,缺省 `999999`)、`STATION_BASE_URL`(若有);**各门控前置处理**配置中的 `bookid` → `BOOK_ID`(每个门控独立值) |
281
281
 
282
282
  ### 2. 分页与「查全量」(`limit` / `offset`)
283
283
 
284
- 凡 [references/agentic-lab-sdk.md](references/agentic-lab-sdk.md) 中带 **`limit`** / **`offset`** 的查询(如 **`ticket.list`**、**`process.list`**、**`compound.getStock`**、**`compound.getChemicalRecord`**),生成门控脚本时遵守:
284
+ 凡 [references/agentic-lab-sdk.md](references/agentic-lab-sdk.md)「可用方法一览」中标注使用 `limit` / `offset` 分页的查询方法,生成门控脚本时遵守:
285
285
 
286
- 1. **流程说明未写分页**:未出现「每页/只查前/offset/limit/条数上限」等明确要求时,**默认**显式传入 **`limit: 999999`**、**`offset: 0`**(可用模块级常量如 `DEFAULT_QUERY_LIMIT`),避免依赖引擎默认小页长导致**截断漏数据**。**例外**:若流程文档**门控脚本配置项**表中写明了入口池查询上限(如 `门控脚本对入口池单次查询上限: 9000`),以该值作为 `DEFAULT_QUERY_LIMIT`,不使用默认 `999999`。
287
286
  2. **流程说明写了分页**:按文档给出的 `limit`、`offset`(或等价参数名)原样写入调用。
288
287
  3. **流程说明写了「查全部/不限制条数」及具体数值**(例如明确要求 `limit: 500000`):**以流程文档为准**,不得擅自改为 `999999`。
289
288
 
290
- `pool.getNext`、`station.list` 等无分页参数的调用不受影响。
291
-
292
289
  ### 3. `start` 与 `complete` 分工(与门控三阶段对齐)
293
290
 
294
291
  门控执行顺序为 **前置处理(脚本)→ 人工处理(页面)→ 后置处理(脚本)**。`start` 对应**前置处理**,`complete` 对应**后置处理**。
@@ -299,16 +296,53 @@ metadata:
299
296
  - **`start` 返回** `StartExecutionResult`:**必须**含 `orbit_link`(`string`)、`ticket_ids`(`number[]`)。
300
297
  - **`complete` 入参** `CompleteExecutionParams`:**必须**含 `valve_id`(`number`)、`tickets`(`Record<string, any>[]`)。
301
298
  - **`complete` 返回** `CompleteExecutionResult`:通过 `new_tickets`(`Record<string, any>[]`,可选)返回待写入出口池的工单,由引擎自动创建。
302
- - **一级参数与返回值键名一律 snake_case**(如 `valve_id`、`pool_ids`、`ticket_ids`、`orbit_link`、`new_tickets`),**禁止** camelCase(如 ~~`ticketIds`~~、~~`poolIds`~~)。嵌套数据(如 `ticket.detail` 内容、`process.execute` `params`/`items` 等 JSON 结构)保持流程文档或 API 原有格式,不受此约束。
299
+ - **一级参数与返回值键名一律 snake_case**(如 `valve_id`、`pool_ids`、`ticket_ids`、`orbit_link`、`new_tickets`),**禁止** camelCase(如 ~~`ticketIds`~~、~~`poolIds`~~)。嵌套数据(如 `ticket.detail` 内容、SDK 方法的 `params`/`items` 等 JSON 结构)保持流程文档或 API 原有格式,不受此约束。
303
300
 
304
- - **`start(params)`**(对应**前置处理**):解构 **`valve_id`、`pool_ids`**(`Processer` 入参 snake_case)→ `ticket.list({ pool_id__in: pool_ids, … })`(§2 默认大 `limit`;**返回值**为 `TicketListResponse`,取 **`response.list`** 得 `Ticket[]`;筛选键名以 [agentic-lab-sdk.md](references/agentic-lab-sdk.md) `ListTicketParams` 为准)→ **仅当**流程说明前置处理中写明化合物查询 / 流程列表 / 工站等需求时,才分别调用 `compound.getChemicalRecord`、`compound.getStock`、`process.list`、`station.list`(缺则**不调**)→ **仅按**数据处理规则与映射更新 `ticket.detail` 中**文档涉及的键** → `ticket.update`(若确有写回)→ 拼装 `orbit_link`(见下方步骤)→ 返回 `{ orbit_link, ticket_ids }` 。
301
+ - **`start(params)`**(对应**前置处理**):解构 **`valve_id`、`pool_ids`**(`Processer` 入参 snake_case)→ [agentic-lab-sdk.md](references/agentic-lab-sdk.md) 查询入口池数据(§2 默认大 `limit`)→ **仅当**流程说明前置处理中写明化合物查询 / 流程列表 / 工站等需求时,才按需调用对应 SDK 方法(缺则**不调**;各方法见 [agentic-lab-sdk.md](references/agentic-lab-sdk.md))→ **仅按**数据处理规则与映射更新 `ticket.detail` 中**文档涉及的键** → SDK 更新 tickets(若确有写回)→ 拼装 `orbit_link`(见下方步骤)→ 返回 `{ orbit_link, ticket_ids }` 。
305
302
 
306
303
  **`orbit_link` 生成步骤**:
307
304
  1. 从**门控脚本配置项**表提取 `PageUrl`(含 `{bookid}` 占位符),写为脚本模块级常量 `PAGE_URL`
308
305
  2. 从该门控**前置处理**配置项中提取 `bookid` 值,写为脚本模块级常量 `BOOK_ID`
309
306
  3. `start` 函数末尾拼装:`const orbit_link = PAGE_URL.replace('{bookid}', BOOK_ID)`
310
307
 
311
- - **`complete(params)`**(对应**后置处理**):解构 **`valve_id`** 与 **`tickets`**(snake_case 入参)→ **优先使用 `params.tickets` 作为业务数据源**(引擎已传入完整 ticket 数据,无需再通过 `ticket.list` 重新查询;仅当流程说明后置处理中**明确要求**获取额外数据或最新状态时才按需调用 `ticket.list`)→ 按流程说明后置处理中的规则执行业务逻辑(准备提交参数、调用外部接口如 `process.execute`、判定成败等)→ **`await`** `this.context.pool.getNext(valve_id)` → 将入口池 tickets 更新为 `finished`(`ticket.update`)→ 按出口池条件映射 `new_tickets`(含 `flow_id`、`pool_id`、`order_id`、`detail`、`status: 'created'`、`parent_ticket_id`)→ 返回 `{ new_tickets }`(由引擎自动创建,**不**在脚本内调用 `ticket.append`)。
308
+ - **`complete(params)`**(对应**后置处理**):解构 **`valve_id`** 与 **`tickets`**(snake_case 入参)→ **优先使用 `params.tickets` 作为业务数据源**(引擎已传入完整 ticket 数据,无需重新查询;仅当流程说明后置处理中**明确要求**获取额外数据或最新状态时才按需查询)→ 按流程说明后置处理中的规则执行业务逻辑(准备提交参数、按需调用 SDK 方法、判定成败等)→ [agentic-lab-sdk.md](references/agentic-lab-sdk.md) 获取出口池 → 将入口池 tickets 更新为 `finished` 按出口池条件映射 `new_tickets`(含 `flow_id`、`pool_id`、`order_id`、`detail`、`status: 'created'`、`parent_ticket_id`)→ 返回 `{ new_tickets }`(由引擎自动创建,**不**在脚本内直接追加 tickets)。
309
+
310
+ ### 4. 运行环境约束(沙箱可用全局对象)
311
+
312
+ 门控脚本在**沙箱**中执行,**仅**以下全局对象/函数可用:
313
+
314
+ | 类别 | 可用 |
315
+ |------|------|
316
+ | 基础类型 | `Array`, `Object`, `Map`, `Set`, `JSON`, `Math`, `Date`, `Number`, `String`, `Boolean`, `RegExp` |
317
+ | 错误类型 | `Error`, `TypeError`, `ReferenceError` |
318
+ | 数值函数 | `parseInt`, `parseFloat`, `isNaN`, `isFinite` |
319
+ | 编码函数 | `encodeURIComponent`, `decodeURIComponent`, `encodeURI`, `decodeURI` |
320
+ | 异步 | `Promise`, `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval` |
321
+ | 常量 | `undefined`, `NaN`, `Infinity` |
322
+ | IO | `console`(`log`/`info`/`warn`/`error`/`debug`)、`fetch`(仅限 SDK 无法覆盖的外部调用) |
323
+
324
+ **不可用**(使用即抛 `ReferenceError`):
325
+
326
+ - `URLSearchParams`、`URL`、`Headers`、`Request`、`Response`、`FormData`、`Blob`、`Buffer`
327
+ - `TextEncoder`、`TextDecoder`
328
+ - `process`、`require`、`module`、`exports`、`__dirname`、`__filename`
329
+ - `window`、`document`、`globalThis`(浏览器/Node.js 专属)
330
+
331
+ **替代方案**:
332
+
333
+ 1. **拼接 query string**——用模板字符串 + `encodeURIComponent`(不要用 `URLSearchParams`):
334
+ ```js
335
+ // ✅ 正确
336
+ const qs = `view_id=${encodeURIComponent(viewId)}&page_size=500`;
337
+ const url = `${BASE}?${qs}`;
338
+
339
+ // ❌ 错误——URLSearchParams 不存在
340
+ const qs = new URLSearchParams({ view_id: viewId, page_size: '500' });
341
+ ```
342
+
343
+ 2. **外部 HTTP 调用**——优先使用 `this.context` SDK 方法(见 [references/agentic-lab-sdk.md](references/agentic-lab-sdk.md))。仅当流程文档要求调用 SDK 未覆盖的第三方 API(如飞书/Lark)时,才使用 `fetch`;此时 URL 手动拼接,**不依赖** `URLSearchParams`。
344
+
345
+ 3. **JSON 序列化**——`JSON.stringify` / `JSON.parse` 可用(`JSON` 在沙箱白名单中)。
312
346
 
313
347
  ### 5. 类型安全的字段访问(防止运行时 TypeError)
314
348
 
@@ -335,9 +369,9 @@ metadata:
335
369
  ### 6. 参考代码(按优先序打开)
336
370
 
337
371
  1. [references/agentic-lab-processer.md](references/agentic-lab-processer.md)(**`Processer` 类型定义与代码风格**参考;`start`/`complete` 输入输出类型、snake_case 命名)
338
- 2. [embedded-template/scripts/示例数据与校验门控.js](embedded-template/scripts/示例数据与校验门控.js)(**结构与分页约定**参考;compound / `pool.getNext` / `ticket` 等**仅当本门控文档需要时**才纳入生成,勿默认照抄示例中的全部调用;**列表类查询默认 `limit: 999999`**)
372
+ 2. [embedded-template/scripts/示例数据与校验门控.js](embedded-template/scripts/示例数据与校验门控.js)(**结构与分页约定**参考;SDK 方法**仅当本门控文档需要时**才纳入生成,勿默认照抄示例中的全部调用;**列表类查询默认 `limit: 999999`**)
339
373
  3. [references/agentic-lab-sdk.md](references/agentic-lab-sdk.md)
340
- 4. 多门控流程:按拓扑为每个门控各写一对 `valves/<基名>.md` 与 `scripts/<基名>.js`,API 与分页约定同上;勿使用 `agentic-lab-sdk.md` 未列出的 `context` 成员(如旧代码中的 `processe` 等,应使用 **`process.list`**)。
374
+ 4. 多门控流程:按拓扑为每个门控各写一对 `valves/<基名>.md` 与 `scripts/<基名>.js`,API 与分页约定同上;勿使用 `agentic-lab-sdk.md` 未列出的 `context` 成员。
341
375
 
342
376
  ### 7. 产物版本追踪
343
377
 
@@ -400,6 +434,8 @@ console.info(`[Processer] v${__ARTIFACT_VERSION__} (skill: ${__ARTIFACT_SKILL__}
400
434
  - 脚本以 `Processer` 类结束,禁止 `return`/`module.exports`/`export`(见「生成流程 §6」)
401
435
  - 按需生成,禁止冗余:代码和 Schema 仅覆盖流程文档明确写出的内容(见「编写指引 §0」)
402
436
  - **类型安全**:禁止直接对 `ticket.detail` 值调用 `.trim()` / `.split()` 等字符串方法;必须先 `String()` 转换或 `typeof` 判断(见「编写指引 §5」)
437
+ - **运行环境**:门控脚本在沙箱执行,`URLSearchParams`/`URL`/`Buffer` 等 Web/Node API **不可用**;拼接 query string 须用模板字符串 + `encodeURIComponent`(见「编写指引 §4」)
438
+ - **外部 HTTP**:优先用 `this.context` SDK 方法;仅 SDK 未覆盖的第三方 API 才用 `fetch`,URL 手动拼接(见「编写指引 §4」)
403
439
  - **产物版本**:SKILL.md frontmatter 须含 `metadata.version`;每个脚本须含 `__ARTIFACT_VERSION__` / `__ARTIFACT_SKILL__` 常量及 constructor `console.info`(见「编写指引 §7」)
404
440
  - **脚本自测**:凡写入或修改 `scripts/*.js`,必须用 `testing/test-processer.mjs` 执行自测并自动修复,直到正常轮全 `pass`(见「脚本自测」)
405
441
 
@@ -414,8 +450,6 @@ console.info(`[Processer] v${__ARTIFACT_VERSION__} (skill: ${__ARTIFACT_SKILL__}
414
450
  **流程来源**:若该包为 **初次生成** 产物,源流程文档应已通过 **「流程文档合规预检」**;复查可对照 [references/业务流程文档标准.md](references/业务流程文档标准.md) §4。**迭代修改**路径无此强制要求。
415
451
  **结构清单**:根目录 `SKILL.md`(合法 `name`/`description`、固定二级标题:`概述`、`核心概念`、`流程图`、`节点清单`、`门控执行规范`、`使用方式`)、`**连接关系:**`、`### Pool 节点` / `### Valve 节点`、`Processer` 类与门控执行规范代码块、存在 `pools/` / `valves/` / `scripts/`、`valves` 与 `scripts` 同名成对、任取一个 `pools/*.md` 的 Schema 表头含 **字段** / **字段标题** / **属性** 且数据行「字段」列为 snake_case。
416
452
 
417
- **人工抽查**:生成脚本里 `ticket.list`、`process.list`、`compound.getStock`、`compound.getChemicalRecord` 在无流程分页说明时是否传入 **`limit: 999999`**(或流程说明指定的查全量数值),见 [references/agentic-lab-sdk.md](references/agentic-lab-sdk.md) 篇首约定。
418
-
419
453
  **版本追踪**:`SKILL.md` frontmatter 含 `metadata.version` / `metadata.generated_by` / `metadata.generated_at`;每个 `scripts/*.js` 含 `__ARTIFACT_VERSION__` 常量(值与 `metadata.version` 一致)和 `__ARTIFACT_SKILL__` 常量;`Processer` constructor 含 `console.info` 版本打印。
420
454
 
421
455
  **对照物**:[embedded-template/](embedded-template/)(已知良好缩小范例)、[references/skill-package-layout.md](references/skill-package-layout.md)、[references/agentic-lab-sdk.md](references/agentic-lab-sdk.md)。
@@ -55,20 +55,13 @@ class Processer {
55
55
  ),
56
56
  ];
57
57
 
58
- let chemicalRecords = [];
59
58
  let stockRecords = [];
60
59
  if (compoundIdList.length > 0) {
61
- const filter = { cmpd_id: { $in: compoundIdList } };
62
- chemicalRecords = await this.context.compound.getChemicalRecord({
60
+ const filter = { 'cas.$in': compoundIdList };
61
+ const stockResponse = await this.context.compound.getStockFromXfcSh({
63
62
  filter,
64
- limit: DEFAULT_QUERY_LIMIT,
65
- offset: 0,
66
- });
67
- stockRecords = await this.context.compound.getStock({
68
- filter,
69
- limit: DEFAULT_QUERY_LIMIT,
70
- offset: 0,
71
63
  });
64
+ stockRecords = stockResponse.data || [];
72
65
  }
73
66
 
74
67
  const stations = await this.context.station.list();
@@ -87,14 +80,8 @@ class Processer {
87
80
  // --- 规则 1.1~1.4:按 valves/示例数据与校验门控.md「数据处理规则」写回 detail(以下为示意)---
88
81
  for (const ticket of tickets) {
89
82
  const d = ticket.detail || {};
90
- const chem = chemicalRecords.find((r) => r.cmpd_id === d.cmpd_id);
91
- const stock = stockRecords.filter((r) => r.cmpd_id === d.cmpd_id);
83
+ const stock = stockRecords.filter((r) => str(r.cas) === str(d.cmpd_id));
92
84
 
93
- if (chem) {
94
- d.chemist_property = str(chem.chemist_property);
95
- d.dispensable = chem.dispensable;
96
- d.dispensing_remark = str(chem.dispensing_remark);
97
- }
98
85
  if (stock.length) {
99
86
  d.available_amount_mg = stock.reduce(
100
87
  (s, r) => s + num(r.latest_amount_mg, 0),
@@ -12,7 +12,7 @@
12
12
 
13
13
  ## 门控 YAML(与流程说明对齐)
14
14
 
15
- 流程说明中可能使用 `Stash:` 等键表示目标池;生成脚本时以 **`pool.name`**(或文档中的池显示名称)与 `pool.getNext(valve_id)` 返回值匹配(`valve_id` 来自 `Processer` 入参)。
15
+ 流程说明中可能使用 `Stash:` 等键表示目标池;生成脚本时按 [agentic-lab-sdk.md](../../references/agentic-lab-sdk.md) 获取出口池,以返回的 **`pool.name`**(或文档中的池显示名称)匹配目标池(`valve_id` 来自 `Processer` 入参)。
16
16
 
17
17
  ```yaml
18
18
  valve_id: valve_example_1
@@ -29,8 +29,7 @@ output:
29
29
 
30
30
  | 流程说明中的表述 | 推荐 SDK 调用 |
31
31
  |----------------|---------------|
32
- | compound 表 / 可用量 / 库存汇总 | `context.compound.getStock({ filter, limit, offset })`;`filter` 为 NocoBase 条件;**流程未写分页时**建议 `limit: 999999`、`offset: 0`(见主 `SKILL.md` 门控脚本指引)。另见 [agentic-lab-sdk](../../references/agentic-lab-sdk.md)、[Filter Operators](https://docs.nocobase.com/api/database/operators) |
33
- | 查 chemical_record / 知识库属性 | `context.compound.getChemicalRecord({ filter, limit, offset })`;同上,默认大 `limit` 防截断 |
32
+ | 查化合物库存 / 可用量 / 库存汇总 | [agentic-lab-sdk.md](../../references/agentic-lab-sdk.md) 中的化合物库存查询方法 |
34
33
 
35
34
  ## 字段映射(示例:知识库 → ticket.detail)
36
35
 
@@ -53,7 +52,7 @@ output:
53
52
  | 序号 | 规则摘要 | 业务动作(示意) |
54
53
  |------|----------|------------------|
55
54
  | 1.1 | 汇总需求量 | `detail.requested_amount = sum(target_amount_n)` |
56
- | 1.2 | 缺料判定 | 结合 `getStock` 汇总可用量与 `requested_amount` 比较 |
55
+ | 1.2 | 缺料判定 | 结合化合物库存查询(SDK)汇总可用量与 `requested_amount` 比较 |
57
56
  | 1.3 | 容差默认 | 目标量 >0 且容差空 → 默认 0.5 mg |
58
57
  | 1.4 | 缺料标记 | `source_barcode` 空 → `是否缺料` = 缺料 |
59
58
 
@@ -69,9 +68,9 @@ output:
69
68
 
70
69
  | 规则摘要 | 条件要点 | 业务动作或结果 |
71
70
  | -------- | -------- | -------------- |
72
- | 查询入口池 | 门控启动 | `ticket.list({ pool_id__in: pool_ids, limit: 999999, offset: 0 })`,取返回体 `list` |
73
- | 查化合物数据 | 有 compound 查询需求 | `compound.getChemicalRecord` / `compound.getStock`(同上默认大 `limit`) |
74
- | 写回 detail | 查询完成 | 按映射表与规则表写回 `ticket.detail` → `ticket.update` |
71
+ | 查询入口池 | 门控启动 | SDK 查询入口池 tickets(分页遵守主 SKILL.md 编写指引 §2) |
72
+ | 查化合物数据 | 有 compound 查询需求 | SDK 查询化合物库存数据 |
73
+ | 写回 detail | 查询完成 | 按映射表与规则表写回 `ticket.detail` → SDK 更新 tickets |
75
74
  | 拼装 orbit_link | 返回前 | `PageUrl.replace('{bookid}', bookid)` |
76
75
 
77
76
  **start 返回**:`{ orbit_link, ticket_ids }`
@@ -88,11 +87,11 @@ output:
88
87
 
89
88
  | 规则摘要 | 条件要点 | 业务动作或结果 |
90
89
  | -------- | -------- | -------------- |
91
- | 获取出口池 | `pool.getNext(valve_id)` | 按 `pool.name` 匹配出口池 |
92
- | 更新入口池状态 | 出口池确定 | 入口池 tickets 更新为 `status: 'finished'` → `ticket.update` |
90
+ | 获取出口池 | SDK 获取出口池 | 按 `pool.name` 匹配出口池 |
91
+ | 更新入口池状态 | 出口池确定 | 入口池 tickets 更新为 `status: 'finished'` → SDK 更新 tickets |
93
92
  | 映射出口池记录 | 状态更新完成 | 按出口池 Schema 构建 `new_tickets`(含 `flow_id`、`pool_id`、`order_id`、`detail`、`status: 'created'`、`parent_ticket_id`) |
94
93
 
95
- **complete 返回**:`{ new_tickets }`(由引擎自动创建,脚本不调用 `ticket.append`)
94
+ **complete 返回**:`{ new_tickets }`(由引擎自动创建,脚本不直接追加 tickets)
96
95
 
97
96
  ## 输入/输出
98
97
 
@@ -1,4 +1,6 @@
1
1
  > Agentic Lab 门控脚本
2
+ >
3
+ > `this.context` 上可用的 SDK 方法详见 [agentic-lab-sdk.md](agentic-lab-sdk.md)。本文件仅定义 `Processer` 类的 `start`/`complete` 输入输出类型与代码风格。
2
4
 
3
5
  ### 类型定义
4
6
 
@@ -50,53 +52,27 @@ class Processer {
50
52
 
51
53
  // params: { valve_id, pool_ids }
52
54
  async start(params) {
53
- const tickets = await this.context.ticket.list({ pool_id__in: pool_ids });
54
- const stations = await this.context.station.list();
55
-
56
- const process_ids = []; // 从tickets detail 按需提取出process ids
57
- const processes = await this.context.process.list({ process_ids });
58
-
59
- // 基于stations、processes等数据源,筛选待处理tickets
60
- const finalTickets = tickets.filter(ticket => ticket);
61
-
62
- // 将processes更新到finalTickets detail中
63
- finalTickets.forEach(ticket => { ticket.detail.processes = processes })
64
- // 更新数据库中的tickets,以便Orbit视图获取最新数据
65
- await this.context.ticket.update(finalTickets)
66
-
55
+ // agentic-lab-sdk.md 查询入口池数据
56
+ // 按需调用 SDK 方法查询化合物/工站/流程等外部数据
57
+ // 将查询结果写回 ticket.detail
58
+ // agentic-lab-sdk.md 更新 tickets
67
59
  return {
68
- orbit_link: 'https://orbit-sh.xtalpi.xyz/?view=book&book=3947',
69
- ticket_ids: finalTickets.map(ticket => ticket.id),
60
+ orbit_link: 'https://...',
61
+ ticket_ids: [/* 待处理 ticket ID */],
70
62
  }
71
63
  }
72
64
 
73
65
  // params: { valve_id, tickets }
74
66
  async complete(params) {
75
- // params.tickets 已包含本次待处理的人工录入数据,通常无需重新查询;如需补充数据库数据请按实际需求调用查询方法
76
-
77
- // 获取当前门控的(一个或多个)出口池
78
- const nextPools = this.context.pool.getNext(valveId);
79
- const successPool = nextPools.find(pool => pool.name === 'xxx成功');
80
- const failedPool = nextPools.find(pool => pool.name === 'xxx失败');
81
-
82
- // 基于实时tickets,按实际业务场景(流程说明书中的相关定义),筛选出“可流入出口池”的tickets
83
- const finalTickets = params.tickets.filter(ticket => ticket).map(t => ({ ...t, status: 'finished' }));
84
-
85
- // 将入口池的tickets更新为“已完成”状态,避免重复处理
86
- await this.context.ticket.update(finalTickets);
87
-
88
- // 按实际的业务场景(流程说明书中的相关定义), 筛选出“可流入成功出口池”的tickets,并按出口池的schema 映射为新的tickets
89
- const newSuccessTickets = finalTickets.filter(ticket => ticket).map(t => ({ ...t, status: 'created', pool_id: successPool.id }));
90
- // 按实际的业务场景(流程说明书中的相关定义), 筛选出“可流入失败出口池”的tickets,并按出口池的schema 映射为新的tickets
91
- const newFailedTickets = finalTickets.filter(ticket => ticket).map(t => ({ ...t, status: 'created', pool_id: failedPool.id }));
92
-
93
- // 脚本不负责创建新ticket,交由引擎自动创建(需返回new_tickets)
67
+ // 按 agentic-lab-sdk.md 获取出口池
68
+ // 按业务规则筛选可流入出口池的 tickets
69
+ // 将入口池 tickets 更新为 finished 状态
70
+ // 按出口池 Schema 映射 new_tickets(脚本不直接追加 tickets,交由引擎自动创建)
94
71
  return {
95
- new_tickets: [
96
- ...newSuccessTickets,
97
- ...newFailedTickets
98
- ]
72
+ new_tickets: [/* 按出口池映射的新 tickets */]
99
73
  }
100
74
  }
101
75
  }
102
76
  ```
77
+
78
+ 完整可运行代码参考见 [`embedded-template/scripts/示例数据与校验门控.js`](../embedded-template/scripts/示例数据与校验门控.js)。
@@ -6,7 +6,27 @@
6
6
 
7
7
  ## 门控脚本生成约定(lab-flow-designer)
8
8
 
9
- 对 **`ticket.list`**、**`process.list`**、**`compound.getStock`**、**`compound.getChemicalRecord`** 等支持 **`limit` / `offset`** 的查询:若流程说明**未**明确要求分页或条数,生成门控脚本时应显式传入 **`limit: 999999`**、**`offset: 0`**,避免引擎默认小页长截断。若流程说明写了分页或「查全部」的**具体数值**,以流程说明为准。
9
+ 对 **`ticket.list`**、**`process.list`** 等支持 **`limit` / `offset`** 的查询:若流程说明**未**明确要求分页或条数,生成门控脚本时应显式传入 **`limit: 999999`**、**`offset: 0`**,避免引擎默认小页长截断。若流程说明写了分页或「查全部」的**具体数值**,以流程说明为准。
10
+
11
+ > **`compound.getStockFromXfcSh`** 使用 `pageSize` / `page` 分页(非 `limit` / `offset`),不传分页参数时使用 API 默认值。
12
+
13
+ ---
14
+
15
+ ## 可用方法一览
16
+
17
+ | 方法 | 命名空间 | 用途 | 分页方式 |
18
+ |------|----------|------|----------|
19
+ | `pool.getNext(valveId)` | `context.pool` | 获取指定门控的出口池数组 | 无 |
20
+ | `ticket.list(params)` | `context.ticket` | 按条件筛选并分页返回 ticket 列表 | `limit` / `offset` |
21
+ | `ticket.update(tickets)` | `context.ticket` | 批量更新 ticket | 无 |
22
+ | `ticket.append(tickets)` | `context.ticket` | 批量追加 ticket | 无 |
23
+ | `process.list(params)` | `context.process` | 查询 process 列表 | `limit` / `offset` |
24
+ | `process.execute(items, baseURL)` | `context.process` | 批量提交 Process 执行任务 | 无 |
25
+ | `station.list()` | `context.station` | 列出所有工站 | 无 |
26
+ | `compound.getStockFromXfcSh(params)` | `context.compound` | 获取 XFC 上海化合物库存 | `pageSize` / `page` |
27
+ | `agent.chat(params)` | `context.agent` | 调用外部 Agent(流式/非流式),返回文本或解析后的 JSON | 无 |
28
+
29
+ > 生成「核心概念」中的 `this.context` 能力列表时,从本表选取流程实际使用的方法即可。各方法的完整签名、参数类型与返回类型见下文各节。
10
30
 
11
31
  ---
12
32
 
@@ -176,265 +196,166 @@ async function station.list(): Promise<Station[]>
176
196
 
177
197
  ---
178
198
 
179
- ## compound.getStock
199
+ ## compound.getStockFromXfcSh
180
200
 
181
- - **别名**:`context.compound.getStock`
182
- - **描述**:按 NocoBase Repository 风格的 **`filter`** 查询化合物库 **库存/瓶位**(collection `compound`)。`filter` 支持 `$eq`、`$in`、`$and`、`$or` 等算子,字段名与库一致,便于流程自定义任意组合条件。算子说明见 NocoBase 文档:[Filter Operators](https://docs.nocobase.com/api/database/operators)。
201
+ - **别名**:`context.compound.getStockFromXfcSh`
202
+ - **描述**:获取 XFC 上海系统化合物库存。使用 `pageSize` / `page` 分页(非 `limit` / `offset`),不传分页参数时使用 API 默认值(默认 `pageSize: 400`、`page: 1`)。
183
203
 
184
204
  ```typescript
185
- /**
186
- * NocoBase `Repository.find` / `findOne` / `count` 等方法的 `filter` 参数形状(`$eq`、`$in`、`$and`、`$or` 等)。
187
- * 官方算子列表:<https://docs.nocobase.com/api/database/operators>
188
- */
189
- type NocoBaseFilter = Record<string, unknown>;
190
-
191
- /** 查询参数:以 `filter` 表达条件(空对象是否合法以实现为准,建议至少包含一条有效条件)。 */
192
- interface GetCompoundStockParams {
193
- /**
194
- * 作用于库存 collection 的筛选;键为库字段名,例如:
195
- * - `{ cmpd_id: { $in: ['CA1078', 'CA1079'] } }`(批量化合物编号)
196
- * - `{ cas: { $in: ['28022-43-7'] } }`(按 CAS)
197
- * - `{ $or: [{ cmpd_id: 'CA1' }, { barcode: { $startsWith: 'BC' } }] }`
198
- */
199
- filter: NocoBaseFilter;
200
- /** 最大返回条数;默认以实现为准 */
201
- limit?: number;
202
- /** 分页偏移;默认以实现为准 */
203
- offset?: number;
204
- }
205
-
206
- /**
207
- * 嵌套:项目(`project` 关联对象,节选)
208
- * 注释体例:库字段 `name`;别名:`uiSchema.title`;备注:`description`
209
- */
210
- interface CompoundStockProjectRef {
211
- /** 库字段 `id`;别名:id;备注:关联主键 */
212
- id: number;
213
- /** 库字段 `createdAt`;别名:Created at;备注:无 */
214
- createdAt?: string;
215
- /** 库字段 `updatedAt`;别名:Last updated at;备注:无 */
216
- updatedAt?: string;
217
- /** 库字段 `project_code`;别名:项目编码;备注:以运行时为准 */
218
- project_code?: string;
219
- /** 库字段 `project_name`;别名:项目名称;备注:以运行时为准 */
220
- project_name?: string;
221
- /** 库字段 `project_link`;别名:项目链接;备注:以运行时为准 */
222
- project_link?: string;
223
- /** 库字段 `is_sync`;别名:是否同步;备注:以运行时为准 */
224
- is_sync?: boolean;
225
- /** 库字段 `createdById`;别名:Created by;备注:外键 users */
226
- createdById?: number | null;
227
- /** 库字段 `updatedById`;别名:Last updated by;备注:外键 users */
228
- updatedById?: number | null;
205
+ interface StockFromXfcShParams {
206
+ filter?: Record<string, any>;
207
+ fields?: string[] | string;
208
+ pageSize?: number;
209
+ page?: number;
210
+ [propName: string]: any;
229
211
  }
230
212
 
231
- /**
232
- * 嵌套:账户(`account` 关联对象,节选)
233
- */
234
- interface CompoundStockAccountRef {
235
- /** 库字段 `id`;别名:id;备注:关联主键 */
236
- id: number;
237
- createdAt?: string;
238
- updatedAt?: string;
239
- /** 库字段 `account`;别名:Account;备注:账户编码等 */
240
- account?: string;
241
- /** 库字段 `name`;别名:name;备注:账户名称 */
242
- name?: string;
243
- createdById?: number | null;
244
- updatedById?: number | null;
245
- }
213
+ async function compound.getStockFromXfcSh(
214
+ params?: StockFromXfcShParams
215
+ ): Promise<CompoundResponse>
216
+ ```
246
217
 
247
- /**
248
- * 嵌套:品类(`category` 关联对象,节选)
249
- */
250
- interface CompoundStockCategoryRef {
251
- /** 库字段 `id`;别名:id;备注:关联主键 */
252
- id: number;
253
- createdAt?: string;
254
- updatedAt?: string;
255
- /** 库字段 `status`;别名:状态;备注:以运行时为准 */
256
- status?: string;
257
- /** 库字段 `category_name`;别名:Category Name;备注:品类名称 */
258
- category_name?: string;
259
- createdById?: number | null;
260
- updatedById?: number | null;
261
- }
218
+ ### 参数说明
219
+
220
+ | 参数名 | 含义 | 备注 |
221
+ |--------|------|------|
222
+ | `filter` | 筛选条件 | 查询化合物常用 `cas`、`cmpd_id`;**不传时默认筛选 `'status': '可用'`**。使用 **dot-notation** 格式(如 `'cas.$in': [...]`),**非**嵌套对象格式 |
223
+ | `fields` | 查询返回的字段 | **默认返回字段(不传时)**:`id, cas, status, source, barcode, cmpd_id, owner`。**全部可用字段**:`barcode, cas, cmpd_id, po_num, source, owner, hazard_class_adr, odor, initial_weight, initial_amount, initial_checkin_time, latest_weight, latest_amount_mg, latest_amount_ml, latest_checkin_time, latest_checkout_time, latest_location, location_detail, checkin_user, status, checkout_user, chemical_name`。**外键**:`f_category, f_account, f_latest_request_user, f_project` |
224
+ | `pageSize` | 分页大小 | 默认 `400` |
225
+ | `page` | 页码 | 默认 `1` |
226
+
227
+ ### 调用示例
228
+
229
+ ```javascript
230
+ const xfcStockSH = await this.context.compound.getStockFromXfcSh({
231
+ filter: {
232
+ 'cas.$in': ['1065657-51-3']
233
+ }
234
+ });
235
+ // xfcStockSH.data → 数据数组
236
+ // xfcStockSH.meta → 分页信息
237
+ ```
262
238
 
263
- /**
264
- * 嵌套:最近申领人(`latest_request_user` 关联对象,节选)
265
- * 对应 compound 上 `latest_request_user` 关联,`foreignKey` 为 `f_latest_request_user`
266
- */
267
- interface CompoundStockUserRef {
268
- id: number;
269
- createdAt?: string;
270
- updatedAt?: string;
271
- nickname?: string;
272
- username?: string;
273
- email?: string | null;
274
- phone?: string | null;
275
- passwordChangeTz?: string | null;
276
- appLang?: string | null;
277
- systemSettings?: Record<string, unknown>;
278
- createdById?: number | null;
279
- updatedById?: number | null;
280
- }
239
+ ### 返回类型
281
240
 
282
- /**
283
- * 单条 compound 库存/瓶记录(与 collection `compound` 及典型 API 返回对齐)
284
- * 字段注释:`name` = 接口/库字段 key;**别名** = `uiSchema.title`(含 i18n 模板时已展开为可读文案);**备注** = `description`(无则写「无」)
285
- * 时间字段一般为 ISO8601 字符串
286
- */
287
- interface CompoundStockRecord {
288
- /** 库字段 `id`;别名:ID;备注:无 */
241
+ ```typescript
242
+ interface CompoundItem {
289
243
  id: number;
290
- /** 库字段 `createdAt`;别名:Created at;备注:无 */
291
- createdAt: string;
292
- /** 库字段 `updatedAt`;别名:Last updated at;备注:无 */
293
- updatedAt: string;
294
- /** 库字段 `initial_checkin_time`;别名:Initial Check-in Time;备注:第一次进入系统的时间 */
295
- initial_checkin_time?: string;
296
- /** 库字段 `latest_checkin_time`;别名:Latest Check-in Time;备注:最近一次Check-in时间 */
297
- latest_checkin_time?: string;
298
- /** 库字段 `latest_checkout_time`;别名:Latest Check-out Time;备注:最近一次Check-out时间 */
299
- latest_checkout_time?: string | null;
300
- /** 库字段 `cas`;别名:CAS;备注:无 */
301
244
  cas: string;
302
- /** 库字段 `po_num`;别名:PO Num;备注:无 */
303
- po_num?: string;
304
- /** 库字段 `f_account`;别名:f_account;备注:外键 → account(化合物关联的客户) */
305
- f_account?: number;
306
- /** 库字段 `f_project`;别名:f_project;备注:外键 → project(化合物关联的项目) */
307
- f_project?: number;
308
- /** 库字段 `source`;别名:Source;备注:BB来源的供应商 */
309
- source?: string;
310
- /** 库字段 `barcode`;别名:Barcode;备注:全局唯一 */
245
+ status: string;
246
+ source: string;
311
247
  barcode: string;
312
- /** 库字段 `cmpd_id`;别名:CMPD-ID;备注:对应于自主实验室申请的Compound ID,示例:CA1078 */
313
248
  cmpd_id: string;
314
- /** 库字段 `owner`;别名:Owner;备注:化合物的归属人,邮箱 */
315
- owner?: string;
316
- /** 库字段 `f_category`;别名:f_category;备注:外键 → category(化合物种类) */
317
- f_category?: number;
318
- /** 库字段 `hazard_class_adr`;别名:HazardClassAdr;备注:危险品等级 */
319
- hazard_class_adr?: string | null;
320
- /** 库字段 `odor`;别名:Odor;备注:气味 */
321
- odor?: string;
322
- /** 库字段 `initial_amount`;别名:Initial Amount;备注:第一次进入系统的原料重量(新瓶子),单位mg */
323
- initial_amount?: number;
324
- /** 库字段 `initial_weight`;别名:Initial Weight;备注:第一次进入系统的瓶子重量(新瓶子),单位g */
325
- initial_weight?: number;
326
- /** 库字段 `latest_weight`;别名:Latest Weight;备注:当前瓶重,每一次重新回到系统后更新,单位g */
327
- latest_weight?: number;
328
- /** 库字段 `latest_amount_mg`;别名:Latest Amount(mg);备注:原料的当前实际重量,每次Check-in时重新计算 */
329
- latest_amount_mg?: number;
330
- /** 库字段 `latest_amount_ml`;别名:Latest Amount(mL);备注:当前实际重量,单位mL,手填 */
331
- latest_amount_ml?: number | null;
332
- /** 库字段 `latest_location`;别名:Latest Location;备注:当前位置 */
333
- latest_location?: string;
334
- /** 库字段 `location_detail`;别名:Location Detail;备注:详细位置信息 */
335
- location_detail?: Record<string, unknown>;
336
- /** 库字段 `f_latest_request_user`;别名:f_latest_request_user;备注:外键 → users(申请出库使用者) */
337
- f_latest_request_user?: number;
338
- /** 库字段 `status`;别名:Status;备注:无(可选值如:可用、已预约、已出库、已核销) */
339
- status?: string;
340
- /** 库字段 `checkin_user`;别名:Check-in User;备注:初始入库用户的邮箱, 由orbit提交 */
341
- checkin_user?: string;
342
- /** 库字段 `checkout_user`;别名:Check-out User;备注:出库用户的邮箱, 由orbit提交 */
343
- checkout_user?: string;
344
- /** 库字段 `chemical_name`;别名:Chemical Name;备注:无 */
345
- chemical_name?: string | null;
346
- /** 库字段 `createdById`;别名:Created by(外键);备注:无 */
347
- createdById?: number | null;
348
- /** 库字段 `updatedById`;别名:Last updated by(外键);备注:无 */
349
- updatedById?: number | null;
350
- /** 库字段 `project`(关联);别名:Project;备注:化合物关联的项目 */
351
- project?: CompoundStockProjectRef;
352
- /** 库字段 `account`(关联);别名:Account;备注:化合物关联的客户 */
353
- account?: CompoundStockAccountRef;
354
- /** 库字段 `category`(关联);别名:Category;备注:化合物种类 */
355
- category?: CompoundStockCategoryRef;
356
- /** 库字段 `latest_request_user`(关联);别名:Latest Request User;备注:申请出库使用者 */
357
- latest_request_user?: CompoundStockUserRef;
249
+ owner: string;
250
+ }
251
+
252
+ interface CompoundMeta {
253
+ count: number;
254
+ page: number;
255
+ pageSize: number;
256
+ totalPage: number;
358
257
  }
359
258
 
360
- async function compound.getStock(
361
- params: GetCompoundStockParams
362
- ): Promise<CompoundStockRecord[]>
259
+ interface CompoundResponse {
260
+ data: CompoundItem[];
261
+ meta: CompoundMeta;
262
+ }
263
+ ```
264
+
265
+ ### 返回数据示例
266
+
267
+ ```json
268
+ {
269
+ "data": [
270
+ {
271
+ "id": 151,
272
+ "cas": "1065657-51-3",
273
+ "status": "可用",
274
+ "source": "上海锐谱医药科技有限公司",
275
+ "barcode": "XFC25103100049",
276
+ "cmpd_id": "BA0062",
277
+ "owner": "张琳"
278
+ },
279
+ {
280
+ "id": 6613,
281
+ "cas": "1065657-51-3",
282
+ "status": "可用",
283
+ "source": "上海锐谱医药科技有限公司",
284
+ "barcode": "XFC25121900056",
285
+ "cmpd_id": "BA0062",
286
+ "owner": "张琳"
287
+ }
288
+ ],
289
+ "meta": { "count": 2, "page": 1, "pageSize": 400, "totalPage": 1 }
290
+ }
363
291
  ```
364
292
 
365
293
  ---
366
294
 
367
- ## compound.getChemicalRecord
295
+ ## agent.chat
368
296
 
369
- - **别名**:`context.compound.getChemicalRecord`
370
- - **描述**:按 NocoBase **`filter`** 查询 **化合物知识库** `chemical_record`(可分装性、化性状、备注等),与库存 `compound.getStock` 区分;算子与 `GetCompoundStockParams.filter` 相同,见 [Filter Operators](https://docs.nocobase.com/api/database/operators)。
297
+ - **别名**:`context.agent.chat`
298
+ - **描述**:调用外部 Agent 服务。SDK 负责发起请求、消费流式(SSE / NDJSON)或普通 JSON 响应,并将各 chunk 的 `content` 拼接为完整文本。`url`、`query` **必填**。`parseJson: true` 时从返回文本中提取并解析 JSON(支持 Markdown 代码块与多段 JSON 合并);解析成功返回 object / array,失败返回 `null`。未开启 `parseJson` 时返回原始字符串。请求失败抛出以 `agent.chat:` 为前缀的错误。
371
299
 
372
300
  ```typescript
373
- type NocoBaseFilter = Record<string, unknown>;
374
-
375
- /** 查询参数:以 `filter` 描述对 `chemical_record` 的筛选。 */
376
- interface GetChemicalRecordParams {
301
+ interface ChatAgentParams {
302
+ /** Agent 服务完整 URL(必填) */
303
+ url: string; // Agent 服务完整 URL(必填
304
+ /** 用户查询文本(必填;非空字符串) */
305
+ query: string;
377
306
  /**
378
- * 示例:`{ cmpd_id: { $in: ['CA1078'] } }`;
379
- * `{ compound_id: { $in: ['28022-43-7'] } }`(业务将 CAS 存于 `compound_id` 时);
380
- * `{ dispensable: { $isTruly: true } }`(布尔字段算子见 NocoBase 文档)。
307
+ * true:从返回文本提取并解析 JSON,成功返回 object/array,失败返回 null;
308
+ * 未传或 false:返回拼接后的原始 string
381
309
  */
382
- filter: NocoBaseFilter;
383
- limit?: number;
384
- offset?: number;
310
+ parseJson?: boolean;
311
+ [propName: string]: any;
385
312
  }
386
313
 
387
314
  /**
388
- * 单条 `chemical_record` 化合物知识库记录(与 collection 元数据及典型 API 返回对齐)
389
- * 字段注释:库字段 `name`;**别名** = `uiSchema.title`;**备注** = `description`(无则写「无」)
315
+ * parseJson 未开启:Promise<string>
316
+ * parseJson 开启且解析成功:Promise<object | any[]>
317
+ * parseJson 开启且解析失败:Promise<null>
390
318
  */
391
- interface ChemicalRecord {
392
- /** 库字段 `id`;别名:ID;备注:无 */
393
- id: number;
394
- /** 库字段 `createdAt`;别名:Created at;备注:无 */
395
- createdAt: string;
396
- /** 库字段 `updatedAt`;别名:Last updated at;备注:无 */
397
- updatedAt: string;
398
- /** 库字段 `compound_id`;别名:Compound ID (CAS#);备注:无 */
399
- compound_id: string;
400
- /** 库字段 `cmpd_id`;别名:CMPD-ID;备注:无 */
401
- cmpd_id: string;
402
- /** 库字段 `business_type`;别名:Business Type;备注:无(可选值如:Fragment、VAST、D2B) */
403
- business_type?: string;
404
- /** 库字段 `chemist_property`;别名:Chemist Property;备注:性状(Chemist) */
405
- chemist_property?: string[];
406
- /** 库字段 `dispensable`;别名:Dispensable;备注:可分装判断(Chemist) */
407
- dispensable: boolean;
408
- /** 库字段 `dispensing_remark`;别名:Dispensing Remark;备注:不可分装备注(Chemist) */
409
- dispensing_remark?: string[];
410
- /** 库字段 `large_particle`;别名:Large Particle;备注:固体颗粒过大(Large Particle) */
411
- large_particle?: boolean | null;
412
- /**
413
- * 库字段 `poor_flowability`;别名:Poor Flowability;备注:流动性差(Poor Flowability)
414
- * 库类型为 string;若历史数据为 boolean,以实现为准
415
- */
416
- poor_flowability?: string | null;
417
- /** 库字段 `hygroscopicity`;别名:Hygroscopicity;备注:强吸水性(Hygroscopicity) */
418
- hygroscopicity?: boolean | null;
419
- /** 库字段 `agglomeration_tendency`;别名:Agglomeration Tendency;备注:易结块(Agglomeration Tendency) */
420
- agglomeration_tendency?: boolean | null;
421
- /** 库字段 `sticky`;别名:Sticky;备注:易粘黏(Sticky) */
422
- sticky?: boolean | null;
423
- /** 库字段 `strong_electrostatic_adsorption`;别名:Strong Electrostatic Adsorption;备注:强静电吸附(Strong Electrostatic Adsorption) */
424
- strong_electrostatic_adsorption?: boolean | null;
425
- /** 库字段 `remark`;别名:Remark;备注:无(schema 默认文案见数据源配置) */
426
- remark?: string;
427
- /** 库字段 `duplicate_check`;别名:Duplicate Check;备注:重复判断 */
428
- duplicate_check?: string;
429
- /** 库字段 `lark_record_id`;别名:Lark Record ID;备注:无 */
430
- lark_record_id?: string;
431
- /** 库字段 `createdById`;别名:Created by(外键);备注:无 */
432
- createdById?: number | null;
433
- /** 库字段 `updatedById`;别名:Last updated by(外键);备注:无 */
434
- updatedById?: number | null;
435
- }
319
+ async function agent.chat(params: ChatAgentParams): Promise<string | object | any[] | null>
320
+ ```
436
321
 
437
- async function compound.getChemicalRecord(
438
- params: GetChemicalRecordParams
439
- ): Promise<ChemicalRecord[]>
322
+ ### 参数说明
323
+
324
+ | 参数名 | 含义 | 备注 |
325
+ |--------|------|------|
326
+ | `url` | Agent 服务完整 URL | **必填**;空字符串会抛错 |
327
+ | `query` | 用户查询文本 | **必填**;空字符串会抛错 |
328
+ | `parseJson` | 是否解析返回 JSON | 默认不解析,返回拼接后的 string。为 `true` 时尝试从正文或 markdown 的 json 代码块中提取 JSON;多段合法 JSON 合并为数组;失败返回 `null`(不抛错) |
329
+
330
+ ### 调用示例
331
+
332
+ ```javascript
333
+ // 返回原始文本
334
+ const text = await this.context.agent.chat({
335
+ url: agentUrl,
336
+ query: '请根据以下实验数据给出结论:...',
337
+ });
338
+
339
+ // 期望 Agent 返回 JSON,解析失败时得到 null
340
+ const result = await this.context.agent.chat({
341
+ url: agentUrl,
342
+ query: prompt,
343
+ parseJson: true
344
+ });
345
+ if (result == null) {
346
+ throw new Error('agent.chat: failed to parse JSON from response');
347
+ }
440
348
  ```
349
+
350
+ ### 返回说明
351
+
352
+ | 场景 | 返回值 |
353
+ |------|--------|
354
+ | `parseJson` 未开启 / 为 `false` | `string`:流式各 chunk `content` 的拼接结果 |
355
+ | `parseJson: true` 且解析成功 | `object` 或 `any[]` |
356
+ | `parseJson: true` 且解析失败 | `null` |
357
+ | 请求 / 流读取失败 | 抛出 `Error`,消息以 `agent.chat:` 为前缀 |
358
+
359
+ ### 请求体要点(SDK 封装,门控不必手拼)
360
+
361
+ SDK 向 `url` 发送的 JSON 主体主要包括:`query`、`messages`、`context_link`、`message_id`、`response_mode`、`agent_params_global`、`user_info`、`skills`。Header 使用 `Authorization: Bearer <api_key>`。门控脚本侧只需组装 `ChatAgentParams` 并调用 **`this.context.agent.chat(params)`**。
@@ -100,7 +100,7 @@ class Processer {
100
100
  // returns: { orbit_link, ticket_ids }
101
101
  async start(params) {
102
102
  const { valve_id, pool_ids } = params;
103
- // ticket.list → 按需查询化合物/工站/流程 → 写回 detail → ticket.update
103
+ // 查询入口池数据 → 按需查询化合物/工站/流程 → 写回 detail → 更新 tickets
104
104
  // orbit_link = PageUrl.replace('{bookid}', bookid)
105
105
  return { orbit_link, ticket_ids };
106
106
  }
@@ -110,8 +110,8 @@ class Processer {
110
110
  // returns: { new_tickets }
111
111
  async complete(params) {
112
112
  const { valve_id, tickets } = params;
113
- // pool.getNext → 入口池 tickets 更新为 finished → 按出口池映射 new_tickets
114
- // 由引擎自动创建,不在脚本内调用 ticket.append
113
+ // 获取出口池 → 入口池 tickets 更新为 finished → 按出口池映射 new_tickets
114
+ // 由引擎自动创建,脚本不直接追加 tickets
115
115
  return { new_tickets };
116
116
  }
117
117
  }
@@ -173,8 +173,8 @@ class Processer {
173
173
 
174
174
  ## 执行流程
175
175
 
176
- 1. **start 阶段**:`ticket.list` `pool_id__in`(由 `Processer` 入参 `pool_ids` 映射)拉数,**取返回体 `list` 字段** 按需拉化合物库 → 写回 `detail` → `ticket.update`。
177
- 2. **complete 阶段**:`pool.getNext` 取出口池`ticket.update` / `ticket.append`。
176
+ 1. **start 阶段**:按 [agentic-lab-sdk.md](agentic-lab-sdk.md) 查询入口池数据 按需调用其他 SDK 方法查询外部数据 → 写回 `detail` → 按 SDK 更新 tickets。
177
+ 2. **complete 阶段**:按 [agentic-lab-sdk.md](agentic-lab-sdk.md) 获取出口池更新入口池 ticket 状态 → 映射出口池 `new_tickets`。
178
178
 
179
179
  ## 输入/输出
180
180
 
@@ -105,7 +105,7 @@
105
105
  5. **数据处理规则表**(强烈推荐):列至少包含 **`序号` | `规则摘要` | `条件要点` | `业务动作或结果`**;序号建议 **章内连续小数编号**(如 `1.1`、`1.2`,与门控章节号前缀一致),便于脚本注释逐条对应。
106
106
  6. **操作员步骤**(若为人工门控):编号列表,步骤可执行、可验收。
107
107
 
108
- **Process 批量提交(可选)**:若门控需触发 **Rhea / 设备侧批量提交任务**,须在流程文档中写明调用时机、**`items` 每条**中的 **`process`(流程模板名,即 process_name 语义)**、**`params` / `table_objects` 等如何从业务数据拼装**,以及 **`baseURL`(服务根地址)的语义来源**(配置、环境变量、订单字段等,由集成解析为字符串 URL)。**path、HTTP 方法** 由 SDK 固定,流程文档**不要求**写 path / method;生成脚本时映射为 **`this.context.process.execute(items, baseURL)`**(见 [`references/agentic-lab-sdk.md`](agentic-lab-sdk.md);Wire 细节见 [`references/rhea-api/execute_process_batch.md`](rhea-api/execute_process_batch.md) 供预检对照 `items[]` 形状)。
108
+ **Process 批量提交(可选)**:若门控需触发 **Rhea / 设备侧批量提交任务**,须在流程文档中写明调用时机、**`items` 每条**中的 **`process`(流程模板名,即 process_name 语义)**、**`params` / `table_objects` 等如何从业务数据拼装**,以及 **`baseURL`(服务根地址)的语义来源**(配置、环境变量、订单字段等,由集成解析为字符串 URL)。**path、HTTP 方法** 由 SDK 固定,流程文档**不要求**写 path / method;生成脚本时映射为 SDK 中对应方法(见 [`references/agentic-lab-sdk.md`](agentic-lab-sdk.md);Wire 细节见 [`references/rhea-api/execute_process_batch.md`](rhea-api/execute_process_batch.md) 供预检对照 `items[]` 形状)。
109
109
 
110
110
  **检查点**:每个 `output` 条件互斥或可判定优先级在文中有说明;缺料/失败等异常分支有定义。
111
111
  **检查点**:`input.secondary` 存在时,§4 文字补充中已描述合流。
@@ -154,12 +154,12 @@
154
154
 
155
155
  ### 3.3 Process / 任务提交 / 其它 SDK 调用的参数与格式
156
156
 
157
- 凡流程文档出现 **Process 执行、批量任务提交、`process.execute`、或任何明确绑定 SDK 的写操作/查询参数**,除满足 **§2.5 Process 批量提交** 小节外,还须满足(否则预检 **不通过**):
157
+ 凡流程文档出现 **Process 执行、批量任务提交、或任何明确绑定 SDK 的写操作/查询参数**,除满足 **§2.5 Process 批量提交** 小节外,还须满足(否则预检 **不通过**):
158
158
 
159
- 1. **方法名**:与 **agentic-lab-sdk** 中条目一致(如 `process.execute`、`process.list`、`ticket.list` 等),且用途与文档叙述一致。
160
- 2. **请求体与类型**:对 **`process.execute`**,须能整理出 **`items[]`**(与 **[`rhea-api/execute_process_batch.md`](rhea-api/execute_process_batch.md)** 请求体中 `items` 一项同形):每条至少含 **`process`**、**`params`**(键、类型、默认值或来源字段);若流程需绑定业务表记录,则须说明 **`table_objects`**(表名、主键与 ticket 明细的对应);并须说明 **`baseURL`** 如何得到;**禁止**仅写「组装 JSON」而无键级说明。
161
- 3. **查询类接口**:若使用 `ticket.list`、`compound.getStock` 等,分页策略须与流程说明及 skill「门控脚本编写指引」一致或显式写出 `limit`/`offset`;**`filter`** 须说明字段名与算子意图,避免无法落地的自然语言。
162
- 4. **`process.execute` HTTP**:流程文档约束 **`items`**、**`baseURL` 语义**及任务参数;**不要求**写 path / HTTP 方法(由 SDK 封装)。
159
+ 1. **方法名**:与 [agentic-lab-sdk.md](agentic-lab-sdk.md) 中条目一致,且用途与文档叙述一致。
160
+ 2. **请求体与类型**:对 **Process 批量提交**,须能整理出 **`items[]`**(与 **[`rhea-api/execute_process_batch.md`](rhea-api/execute_process_batch.md)** 请求体中 `items` 一项同形):每条至少含 **`process`**、**`params`**(键、类型、默认值或来源字段);若流程需绑定业务表记录,则须说明 **`table_objects`**(表名、主键与 ticket 明细的对应);并须说明 **`baseURL`** 如何得到;**禁止**仅写「组装 JSON」而无键级说明。
161
+ 3. **查询类接口**:分页策略须与 [agentic-lab-sdk.md](agentic-lab-sdk.md) 中该方法的分页方式一致或显式写出分页参数;**`filter`** 须说明字段名与算子意图,避免无法落地的自然语言。
162
+ 4. **Process 批量提交与 HTTP**:流程文档约束 **`items`**、**`baseURL` 语义**及任务参数;**不要求**写 path / HTTP 方法(由 SDK 封装)。
163
163
 
164
164
  **预检产出**:凡属 **§3.2 / §3.3** 的不通过项,须在《流程文档合规问题与优化建议》中单独列出 **「不支持或不可生成项」**,避免进入代码生成阶段才发现无法对接。
165
165
 
@@ -194,8 +194,8 @@
194
194
  - §3 → `pools/*.md` 文件名与 Schema;
195
195
  - §4 + §5 → 根 `SKILL.md` 流程图与**连接关系**列表;
196
196
  - §5 → `valves/*.md`、`scripts/*.js` 及规则注释序号;
197
- - §2、字段映射、查询方式 → 脚本内 `ticket.detail` `compound.*` 等调用;
198
- - 流程中若需 **Process 批量提交** → 脚本内 **`this.context.process.execute(items, baseURL)`**(参数以流程文档为准,见 `references/agentic-lab-sdk.md` 与 `references/rhea-api/execute_process_batch.md`)。
197
+ - §2、字段映射、查询方式 → 脚本内 `ticket.detail` 与按 [agentic-lab-sdk.md](agentic-lab-sdk.md) 调用对应方法;
198
+ - 流程中若需 **Process 批量提交** → 脚本内按 SDK 对应方法执行(参数以流程文档为准,见 `references/agentic-lab-sdk.md` 与 `references/rhea-api/execute_process_batch.md`)。
199
199
 
200
200
  主 `SKILL.md` 中 **门控脚本编写指引** 约定:生成 `scripts/*.js` 与 `pools/*.md` Schema 时,**仅实现或收录流程文档在本门控逻辑或池字段描述中明确写出的**查询、回写、路由与字段行,**禁止**为对齐 embedded 示例而附加未文档化的代码或字段。
201
201
 
@@ -135,9 +135,9 @@ output:
135
135
 
136
136
  **化合物数据查询方式**
137
137
 
138
- 查询化合物库的可用量:使用输入池 Compound ID 作为索引,查询 compound 表的 CAS 字段,查找 status = 可用的记录,返回 latest amount 字段的值并汇总加和。
138
+ 查询化合物库存的可用量:使用输入池 Compound ID 作为索引,通过 XFC 上海系统接口查询化合物库存(`compound.getStockFromXfcSh`),查找 status = 可用的记录,返回可用量字段的值并汇总加和。
139
139
 
140
- 查询化合物知识库属性:使用输入池 Compound ID 查询 chemical_record 表,返回舀粉相关属性。
140
+ 查询化合物属性:使用输入池 Compound ID 通过 XFC 上海系统接口查询化合物相关属性。
141
141
 
142
142
  **字段映射表**
143
143
 
@@ -212,7 +212,7 @@ function generateMockTickets(pools, valveConfig, adversarial) {
212
212
  const KNOWN_SDK_NAMESPACES = new Set(['ticket', 'pool', 'compound', 'process', 'station']);
213
213
 
214
214
  const PAGINATED_METHODS = new Set([
215
- 'ticket.list', 'process.list', 'compound.getStock', 'compound.getChemicalRecord',
215
+ 'ticket.list', 'process.list', 'compound.getStockFromXfcSh',
216
216
  ]);
217
217
 
218
218
  function createMockContext(pools, valveConfig, adversarial) {
@@ -250,31 +250,14 @@ function createMockContext(pools, valveConfig, adversarial) {
250
250
  },
251
251
  },
252
252
  compound: {
253
- getStock: async (params) => {
254
- callLog.push({ method: 'compound.getStock', params });
255
- if (params?.limit === undefined) {
256
- warnings.push('compound.getStock called without explicit limit');
257
- }
258
- return [1, 2].map(i => ({
259
- id: i, createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
260
- cas: `12345-67-${i}`, barcode: `BC-MOCK-${i}`, cmpd_id: `CA${1000 + i}`,
261
- status: 'available', latest_amount_mg: 50 * i, latest_weight: 10 * i,
262
- chemical_name: `MockChemical${i}`,
263
- }));
264
- },
265
- getChemicalRecord: async (params) => {
266
- callLog.push({ method: 'compound.getChemicalRecord', params });
267
- if (params?.limit === undefined) {
268
- warnings.push('compound.getChemicalRecord called without explicit limit');
269
- }
270
- return [1, 2].map(i => ({
271
- id: i, createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
272
- compound_id: `12345-67-${i}`, cmpd_id: `CA${1000 + i}`,
273
- dispensable: i % 2 === 0, chemist_property: ['solid'],
274
- dispensing_remark: [], large_particle: false, hygroscopicity: false,
275
- agglomeration_tendency: false, sticky: false,
276
- strong_electrostatic_adsorption: false,
253
+ getStockFromXfcSh: async (params) => {
254
+ callLog.push({ method: 'compound.getStockFromXfcSh', params });
255
+ const data = [1, 2].map(i => ({
256
+ id: i, cas: `12345-67-${i}`, status: '可用',
257
+ source: 'MockSupplier', barcode: `XFC-MOCK-${i}`,
258
+ cmpd_id: `CA${1000 + i}`, owner: 'mock@example.com',
277
259
  }));
260
+ return { data, meta: { count: data.length, page: 1, pageSize: 400, totalPage: 1 } };
278
261
  },
279
262
  },
280
263
  process: {
@@ -390,6 +373,17 @@ function loadProcesserClass(scriptContent, filename) {
390
373
  Promise, Array, Object, Map, Set, JSON, Math, Date,
391
374
  Number, String, Boolean, RegExp, Error, TypeError, ReferenceError,
392
375
  parseInt, parseFloat, isNaN, isFinite, undefined, NaN, Infinity,
376
+ encodeURIComponent, decodeURIComponent, encodeURI, decodeURI,
377
+ fetch: async (url, opts) => {
378
+ const method = (opts && opts.method) || 'GET';
379
+ sandbox.console.warn(`[test-processer] fetch called: ${method} ${url}`);
380
+ return {
381
+ ok: true,
382
+ status: 200,
383
+ json: async () => ({ data: {} }),
384
+ text: async () => '{}',
385
+ };
386
+ },
393
387
  };
394
388
  const ctx = createContext(sandbox);
395
389
  return script.runInContext(ctx);
@@ -551,25 +545,14 @@ function createTracingContext(pools, valveConfig, tickets) {
551
545
  },
552
546
  },
553
547
  compound: {
554
- getStock: async (params) => {
555
- const result = [1, 2].map(i => ({
556
- id: i, createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
557
- cas: `12345-67-${i}`, barcode: `BC-MOCK-${i}`, cmpd_id: `CA${1000 + i}`,
558
- status: 'available', latest_amount_mg: 50 * i, latest_weight: 10 * i,
559
- chemical_name: `MockChemical${i}`,
548
+ getStockFromXfcSh: async (params) => {
549
+ const data = [1, 2].map(i => ({
550
+ id: i, cas: `12345-67-${i}`, status: '可用',
551
+ source: 'MockSupplier', barcode: `XFC-MOCK-${i}`,
552
+ cmpd_id: `CA${1000 + i}`, owner: 'mock@example.com',
560
553
  }));
561
- traceLog.push({ seq: seq++, method: 'compound.getStock', params: JSON.parse(JSON.stringify(params || {})), returnSummary: `${result.length} records` });
562
- return result;
563
- },
564
- getChemicalRecord: async (params) => {
565
- const result = [1, 2].map(i => ({
566
- id: i, createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
567
- compound_id: `12345-67-${i}`, cmpd_id: `CA${1000 + i}`,
568
- dispensable: i % 2 === 0, chemist_property: ['solid'],
569
- dispensing_remark: [], large_particle: false, hygroscopicity: false,
570
- agglomeration_tendency: false, sticky: false, strong_electrostatic_adsorption: false,
571
- }));
572
- traceLog.push({ seq: seq++, method: 'compound.getChemicalRecord', params: JSON.parse(JSON.stringify(params || {})), returnSummary: `${result.length} records` });
554
+ const result = { data, meta: { count: data.length, page: 1, pageSize: 400, totalPage: 1 } };
555
+ traceLog.push({ seq: seq++, method: 'compound.getStockFromXfcSh', params: JSON.parse(JSON.stringify(params || {})), returnSummary: `${data.length} records` });
573
556
  return result;
574
557
  },
575
558
  },
@@ -743,7 +726,7 @@ function formatPreviewReport(trace) {
743
726
  const allCalls = [...startTraceLog, ...completeTraceLog];
744
727
  const usedMethods = new Set(allCalls.map(c => c.method));
745
728
  const allMethods = ['ticket.list', 'ticket.update', 'ticket.append', 'pool.getNext',
746
- 'compound.getStock', 'compound.getChemicalRecord', 'process.list', 'process.execute', 'station.list'];
729
+ 'compound.getStockFromXfcSh', 'process.list', 'process.execute', 'station.list'];
747
730
  const unused = allMethods.filter(m => !usedMethods.has(m));
748
731
 
749
732
  const startStatus = startError ? `✗ 失败: ${startError.split('\n')[0]}` : `✓ 成功 (${startTraceLog.length} SDK 调用, ${inputTickets.length} tickets)`;
@@ -1003,6 +986,16 @@ async function runTests(args) {
1003
986
  );
1004
987
  }
1005
988
  }
989
+
990
+ // Check for sandbox-unavailable APIs in source code
991
+ const UNAVAILABLE_APIS = ['URLSearchParams', 'TextEncoder', 'TextDecoder', 'Buffer'];
992
+ for (const api of UNAVAILABLE_APIS) {
993
+ if (scriptContent.includes(api)) {
994
+ result.phases.compliance.warnings.push(
995
+ `Script uses '${api}' which is not available in the gate script sandbox — use template literals + encodeURIComponent instead`
996
+ );
997
+ }
998
+ }
1006
999
  }
1007
1000
 
1008
1001
  const normalPass = result.phases.normalRun.start.pass && result.phases.normalRun.complete.pass;