From 916a6dfc60e280a15f37b07205fd76335bc2f28b Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:47:52 +0800 Subject: [PATCH] env installation fix fix pack install 2 fix pip install & git install failed fix pack build 1 Update SKILL.md Update Skills Update registry for all param desc --- .../skills/filter-workflow-by-tags/SKILL.md | 450 ++++++++++++++++++ .../scripts/filter_workflows.py | 191 ++++++++ .github/workflows/ci-check.yml | 2 +- .github/workflows/conda-pack-build.yml | 77 +-- .github/workflows/deploy-docs.yml | 4 +- .github/workflows/multi-platform-build.yml | 22 +- .github/workflows/unilabos-conda-build.yml | 57 ++- recipes/conda_build_config.yaml | 2 +- unilabos/app/utils.py | 165 ++++++- unilabos/registry/devices/Qone_nmr.yaml | 14 + unilabos/registry/devices/bioyond_cell.yaml | 30 +- .../devices/bioyond_dispensing_station.yaml | 10 +- .../devices/coin_cell_workstation.yaml | 58 ++- .../registry/devices/laiyu_liquid_test.yaml | 47 ++ unilabos/registry/devices/liquid_handler.yaml | 22 + .../devices/neware_battery_test_system.yaml | 21 +- .../devices/organic_miscellaneous.yaml | 9 + unilabos/registry/devices/pump_and_valve.yaml | 8 + .../devices/reaction_station_bioyond.yaml | 91 ++-- unilabos/registry/devices/robot_arm.yaml | 5 + .../registry/devices/robot_linear_motion.yaml | 5 + unilabos/registry/devices/virtual_device.yaml | 19 + unilabos/registry/devices/xrd_d7mate.yaml | 11 +- unilabos/registry/devices/zhida_gcms.yaml | 5 + unilabos/registry/registry.py | 22 +- unilabos/utils/environment_check.py | 128 +++-- unilabos/utils/import_manager.py | 2 + 27 files changed, 1282 insertions(+), 195 deletions(-) create mode 100644 .cursor/skills/filter-workflow-by-tags/SKILL.md create mode 100755 .cursor/skills/filter-workflow-by-tags/scripts/filter_workflows.py diff --git a/.cursor/skills/filter-workflow-by-tags/SKILL.md b/.cursor/skills/filter-workflow-by-tags/SKILL.md new file mode 100644 index 00000000..6cedd7c4 --- /dev/null +++ b/.cursor/skills/filter-workflow-by-tags/SKILL.md @@ -0,0 +1,450 @@ +--- +name: filter-workflow-by-tags +description: Query backend workflow list, aggregate all tags, and filter workflows by domain/scenario requirements using tags. Use when the user wants to search workflows, find workflows by tags, list available workflow tags, filter workflows by category/domain/scenario, or mentions 工作流筛选/标签查询/workflow tags/按领域查找工作流. +--- +# Uni-Lab 工作流标签筛选指南 + +通过 Uni-Lab 云端 API 查询工作流列表,汇总所有可用标签(tags),并根据领域和场景要求筛选工作流。 + +> **重要**:本指南中的 `Authorization: Lab ` 是 **Uni-Lab 平台专用的认证方式**,`Lab` 是 Uni-Lab 的 auth scheme 关键字,**不是** HTTP Basic 认证。请勿将其替换为 `Basic`。 + +## 使用模式识别 + +**用户可能一开始就给出场景目标**(如"我要做有机合成实验"、"找柱层析相关的 protocol")。此时: + +1. **识别场景关键词** → 映射到可能的 tags(如 synthesis、organic、chromatography、purification) +2. **直接执行完整流程**(获取 ak/sk/addr → 拉取所有工作流 → 汇总 tags → 按场景筛选) +3. **展示筛选结果** → 引导用户从候选 workflow 中**选择明确的实验 protocol** +4. **如果用户确认某个 workflow** → 记录 `workflow_uuid`,准备对接“与其他 Skill 的协作” + +**如果用户未给场景目标**,则按标准 checklist 询问筛选条件。 + +--- + +## 前置条件 + +使用本指南前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。 + +### 1. ak / sk → AUTH + +询问用户的启动参数,从 `--ak` `--sk` 或 config.py 中获取。 + +生成 AUTH token: + +```bash +python -c "import base64,sys; print('Authorization: Lab ' + base64.b64encode(f'{sys.argv[1]}:{sys.argv[2]}'.encode()).decode())" +``` + +### 2. --addr → BASE URL + +| `--addr` 值 | BASE | +| ------------- | ------------------------------------- | +| `test` | `https://leap-lab.test.bohrium.com` | +| `uat` | `https://leap-lab.uat.bohrium.com` | +| `local` | `http://127.0.0.1:48197` | +| 不传(默认) | `https://leap-lab.bohrium.com` | + +确认后设置: + +```bash +BASE="<根据 addr 确定的 URL>" +AUTH="Authorization: Lab <上面命令输出的 token>" +``` + +### 3. lab_uuid(实验室 UUID) + +如果用户未提供 `lab_uuid`,通过以下 API 自动获取: + +```bash +curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH" +``` + +返回 `data.uuid` 即为 `lab_uuid`。 + +**三项全部就绪后才可开始。** + +## Session State + +在整个对话过程中,agent 需要记住以下状态: + +- `lab_uuid` — 实验室 UUID +- `all_workflows` — 完整工作流列表(分页获取后缓存到内存或临时文件) +- `all_tags` — 所有工作流的标签汇总 + +--- + +## API 端点 + +### 查询工作流列表(支持分页) + +``` +GET $BASE/api/v1/lab/workflow/owner/list?page=&page_size=&lab_uuid=$lab_uuid +``` + +**参数:** + +- `page` — 页码,从 1 开始 +- `page_size` — 每页数量,建议 1000 +- `lab_uuid` — 实验室 UUID + +**返回结构:** + +```json +{ + "code": 0, + "data": { + "has_more": true, + "data": [ + { + "uuid": "9661bba2-1b9f-4687-a63d-910245df174b", + "name": "Untitled", + "description": "", + "user_id": "114211", + "published": false, + "tags": null + }, + { + "uuid": "e0436638-190b-46bc-b1a1-2711d9602f6a", + "name": "Synthesis v2", + "user_id": "114211", + "published": true, + "tags": ["synthesis", "organic"] + } + ] + } +} +``` + +**字段说明:** + +- `has_more` — 若为 `true`,需要继续请求 `page+1` +- `tags` — 可能为 `null`、空数组或字符串数组;聚合时必须容忍 `null` + +### 启动工作流(直接运行) + +``` +POST $BASE/api/v1/lab/workflow//run +``` + +**用途:** 直接启动一个 workflow 的默认执行(使用模板中预设的参数),无需创建 notebook。适用于快速测试或无参数变化的重复执行。 + +**请求体:** 空 JSON `{}` 或省略 + +**返回:** + +```json +{ + "code": 0, + "data": "" +} +``` + +- `run_uuid` — 本次执行的唯一标识(不是 notebook UUID) + +**注意:** + +- 该接口会使用 workflow 模板中保存的默认参数直接执行 +- 如果 workflow 需要动态参数(如 CSV 路径、样品 UUID),应使用 `POST /lab/notebook` 创建 notebook 并传入 `node_params` +- 返回的 `run_uuid` 可直接传入下方「查询任务状态」接口查询实时进度 + +### 查询任务状态 + +``` +GET $BASE/api/v1/lab/mcp/task/ +``` + +**用途:** 查询由 `POST /lab/workflow//run` 返回的 `run_uuid`(即 task_uuid)的实时执行状态,包括整体状态和每个节点(JOS:Job On Station)的执行详情。 + +**路径参数:** + +- `task_uuid` — 等同于启动工作流接口返回的 `run_uuid` + +**返回:** + +```json +{ + "code": 0, + "data": { + "status": "running", + "jos_status": [ + { + "uuid": "d0e24bfe-8d99-450e-b19d-f25849dfbaad", + "node_name": "PRCXI_BioER_96_wellplate_slot_1", + "action_name": "create_resource", + "status": "success", + "return_info": { + "suc": true, + "error": "", + "return_value": { ... } + } + }, + { + "uuid": "...", + "node_name": "...", + "action_name": "transfer_liquid", + "status": "pending", + "return_info": null + } + ] + } +} +``` + +**字段说明:** + +- `data.status` — 整体任务状态 + - `running` — 正在执行(至少一个节点 pending 或 running) + - `success` — 全部节点成功 + - `failed` — 有节点失败 +- `data.jos_status[]` — 节点级执行列表(按执行顺序) + - `uuid` — 节点执行实例 UUID + - `node_name` — 节点名称(资源/设备名或工位名) + - `action_name` — 动作类型(`create_resource`、`transfer_liquid`、`centrifuge`、等) + - `status` — 节点状态:`success`、`pending`、`running`、`failed` + - `return_info` — 执行返回,失败时 `suc=false` 且 `error` 有错误信息 + +**注意:** + +- 此接口的 `task_uuid` **是** `POST /lab/workflow//run` 返回的 `run_uuid`,二者为同一个 ID 的不同称呼 +- **不要**把 notebook UUID(`POST /lab/notebook` 返回)传进来——那条路径用 `GET /lab/notebook/status` 查询 +- `jos_status` 数组按节点执行顺序给出;从 pending 数量可以估算剩余进度 +- 返回体可能较大(`return_info.return_value` 中可能包含完整 resource tree),可在脚本中只提取 `status` + `node_name` + `action_name` 做摘要 + +**状态轮询示例:** + +```bash +# 每 5 秒轮询一次直至完成 +TASK="b183d97e-d2b5-4b24-b14b-820df04d87c0" +while :; do + st=$(curl -s -X GET "$BASE/api/v1/lab/mcp/task/$TASK" -H "$AUTH" \ + | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; \ + print(d['status'], '|', sum(1 for j in d['jos_status'] if j['status']=='success'), '/', len(d['jos_status']))") + echo "$(date +%H:%M:%S) $st" + [[ "$st" == success* || "$st" == failed* ]] && break + sleep 5 +done +``` + +--- + +## 完整工作流 Checklist + +``` +Task Progress: +- [ ] Step 0: 识别用户是否已给出场景目标(如"有机合成"、"柱层析") + - 若已给出 → 记录场景关键词,自动进入后续步骤 + - 若未给出 → 在 Step 6 询问用户 +- [ ] Step 1: 确认 ak/sk → 生成 AUTH token +- [ ] Step 2: 确认 --addr → 设置 BASE URL +- [ ] Step 3: GET /edge/lab/info → 获取 lab_uuid(如用户未提供) +- [ ] Step 4: 分页获取所有工作流(从 page=1 开始直到 has_more=false) +- [ ] Step 5: 汇总所有非空 tags → 生成 all_tags(去重、排序、附出现次数) +- [ ] Step 6: 根据场景关键词(Step 0 或新询问)在 all_tags 中做语义映射 → 确定候选 tags + - 若语义映射不唯一,列出候选 tags 让用户确认 +- [ ] Step 7: 按候选 tags 筛选工作流(默认 any 模式,召回优先) +- [ ] Step 8: 展示筛选结果(uuid、name、description、tags、published) +- [ ] Step 9: 引导用户从结果中选择**明确的实验 protocol** + - 若结果只有 1 条 → 直接确认该 workflow_uuid + - 若结果 2–10 条 → 让用户按编号选择 + - 若结果过多 → 提示收紧条件(加 tag、切换 all 模式、仅 published) + - 若结果为空 → 放宽条件(去掉最稀有 tag)或提示用户换关键词 +- [ ] Step 10: 记录用户选中的 workflow_uuid,并提示提交实验或查看详情 +``` + +--- + +## 推荐路径:使用脚本 + +同目录下提供 `scripts/filter_workflows.py`,一次完成分页抓取、标签聚合与筛选: + +```bash +# 1. 仅汇总标签(不筛选) +python scripts/filter_workflows.py \ + --auth "" \ + --base "$BASE" \ + --lab-uuid "$lab_uuid" \ + --summary-only + +# 2. 按标签筛选(ANY 模式:包含任一) +python scripts/filter_workflows.py \ + --auth "" \ + --base "$BASE" \ + --lab-uuid "$lab_uuid" \ + --tags synthesis organic \ + --mode any + +# 3. 按标签筛选(ALL 模式:必须同时包含) +python scripts/filter_workflows.py \ + --auth "" \ + --base "$BASE" \ + --lab-uuid "$lab_uuid" \ + --tags synthesis organic \ + --mode all \ + --output filtered.json + +# 4. 仅筛选已发布 +python scripts/filter_workflows.py \ + --auth "" \ + --base "$BASE" \ + --lab-uuid "$lab_uuid" \ + --tags synthesis \ + --published-only +``` + +**`--auth` 参数说明**:传入 `Authorization` 头中 `Lab` 之后的 base64 token(不带 `Lab ` 前缀),脚本内部会自动补上 scheme。 + +**输出结构:** + +```json +{ + "total_workflows": 150, + "tag_counts": {"synthesis": 12, "organic": 8, "analysis": 5}, + "all_tags": ["analysis", "organic", "synthesis"], + "filter": {"tags": ["synthesis", "organic"], "mode": "any"}, + "filtered_workflows": [ + {"uuid": "...", "name": "...", "description": "...", "tags": [...], "published": true} + ] +} +``` + +--- + +## 手动路径:curl + jq + +如果脚本不可用或环境缺少 Python,可用 shell 实现。 + +### 1. 分页抓取(写入 `all_workflows.json`) + +```bash +page=1 +echo "[]" > all_workflows.json + +while :; do + resp=$(curl -s -X GET \ + "$BASE/api/v1/lab/workflow/owner/list?page=$page&page_size=1000&lab_uuid=$lab_uuid" \ + -H "$AUTH") + + page_data=$(echo "$resp" | jq -c '.data.data // []') + jq -c --argjson p "$page_data" '. + $p' all_workflows.json > _tmp.json && mv _tmp.json all_workflows.json + + has_more=$(echo "$resp" | jq -r '.data.has_more') + [ "$has_more" != "true" ] && break + page=$((page + 1)) +done + +echo "Total: $(jq 'length' all_workflows.json)" +``` + +### 2. 汇总所有标签(含出现次数) + +```bash +jq '[.[].tags // [] | .[]] | group_by(.) | map({tag: .[0], count: length}) | sort_by(-.count)' \ + all_workflows.json +``` + +### 3. 按标签筛选 + +```bash +# ANY:包含任一指定标签 +jq --argjson want '["synthesis","organic"]' \ + '[.[] | select((.tags // []) | any(. as $t | $want | index($t)))]' \ + all_workflows.json + +# ALL:同时包含所有指定标签 +jq --argjson want '["synthesis","organic"]' \ + '[.[] | select(($want | all(. as $w | (.tags // []) | index($w))))]' \ + all_workflows.json +``` + +--- + +## 筛选策略 + +agent 拿到用户的「领域 + 场景」自然语言描述时,按如下顺序选择 tag: + +1. **优先用户显式指定的 tags**:若用户明确给出标签词,直接精确匹配。 +2. **从 all_tags 中做语义映射**:若用户描述是自然语言(如"有机合成、柱层析"),在 all_tags 中找语义相关项(如 `synthesis`、`organic`、`chromatography`)。必要时展示候选 tag 让用户确认。 +3. **模式选择**: + - 默认 `any`(更多召回),给出 tag 集合的并集匹配 + - 用户强调"必须同时满足"时用 `all` +4. **空结果兜底**:如果筛选为空,放宽条件(去掉最稀有 tag、切换 any 模式),并提醒用户。 + +--- + +## 引导到明确的 Protocol + +筛选完成后,**最终目标是让用户确认一个具体的 workflow_uuid**,而不是停留在"一堆候选"上。按结果数量采取不同策略: + +| 结果数量 | 策略 | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| 0 条 | 放宽筛选(去掉最稀有 tag → 切换 any 模式 → 去掉 `--published-only`)。仍为空则提示换关键词,或列出 `all_tags` 让用户重新选。 | +| 1 条 | 直接确认:"找到唯一匹配:`` (uuid ``),是否用它?"用户确认后记录 `workflow_uuid`。 | +| 2–10 条 | 编号列表展示,让用户选编号。每项给出 name、tags、description 摘要、published 状态。 | +| 10–30 条 | 先展示 tag 分布帮助用户进一步收紧:列出匹配结果中最常见的子标签,提示"加一个 tag 可将结果缩小到 N 条"。 | +| >30 条 | 强制要求用户补充条件:仅 published、指定具体 tag 组合、或按名称关键词过滤。 | + +**确认 workflow 后**: + +1. 将 `workflow_uuid` 写入 session state +2. 提示用户下一步可用的 skill: + - 提交实验 → 引导到“与其他 Skill 的协作” + - 查看 workflow 详细节点 → `GET /api/v1/lab/workflow/template/detail/` +3. 若用户想换一个,回到筛选步骤。 + +--- + +## 展示结果 + +推荐格式(表格 + 汇总统计): + +``` +共 150 个工作流,其中 32 个匹配筛选条件 [tags: synthesis OR organic] + +| UUID (短) | 名称 | Tags | 已发布 | +|-----------|--------------------------|------------------------------|--------| +| e0436638 | Synthesis v2 | synthesis, organic | ✓ | +| 5b60dbb8 | Grignard Protocol | synthesis, organometallic | ✓ | +| ... | ... | ... | ... | + +所有可用标签(按频次): + synthesis (12), organic (8), analysis (5), purification (4), ... +``` + +如果用户下一步想执行某工作流 → 引导到“与其他 Skill 的协作”。 + +--- + +## 常见问题 + +### Q: tags 为 null 的工作流要不要展示? + +默认**不展示**在筛选结果中(因为无法按 tag 匹配)。但在 `--summary-only` 或无筛选条件时,这些工作流仍会计入总数,并在输出中单独列出"未打标签"计数。 + +### Q: 如何按名称/描述做模糊匹配? + +脚本未内置,但可在 jq 中组合: + +```bash +jq '[.[] | select((.name + " " + (.description // "")) | test("organic"; "i"))]' all_workflows.json +``` + +### Q: `page_size=1000` 是否会被服务端限制? + +接口通常允许最大 1000;如果返回量少于 1000 且 `has_more=false`,说明已到末页。极端情况下若服务端返回错误,可降到 200 或 500 再试。 + +### Q: 工作流数量极大(>10k)怎么办? + +1. 先跑 `--summary-only` 了解 tag 分布 +2. 提示用户先限定 `--published-only` 或指定 tag +3. 考虑将 `all_workflows.json` 缓存到本地,下次直接复用 + +--- + +## 与其他 Skill 的协作 + +- 正常情况下,找到 workflow 之后可以直接用它提交实验(启动工作流的 api 端点 POST $BASE/api/v1/lab/workflow//run,不用别的 skill) +- **仅当需要进行多次实验时,使用 batch-submit-experiment** — 筛选到目标工作流后,`workflow_uuid` 直接用于实验提交 + +## 脚本依赖 + +`scripts/filter_workflows.py` 仅使用 Python 标准库(`urllib`、`json`、`argparse`),无需额外安装。 diff --git a/.cursor/skills/filter-workflow-by-tags/scripts/filter_workflows.py b/.cursor/skills/filter-workflow-by-tags/scripts/filter_workflows.py new file mode 100755 index 00000000..87bb0b1c --- /dev/null +++ b/.cursor/skills/filter-workflow-by-tags/scripts/filter_workflows.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""分页拉取 Uni-Lab 工作流列表,汇总 tags 并按 tag 筛选。 + +使用示例: + python filter_workflows.py \ + --auth \ + --base https://leap-lab.test.bohrium.com \ + --lab-uuid a9059772-... \ + --tags synthesis organic --mode any + +仅依赖 Python 标准库。 +""" + +from __future__ import annotations + +import argparse +import json +import sys +import urllib.error +import urllib.parse +import urllib.request +from collections import Counter + + +def fetch_all_workflows(base: str, auth_token: str, lab_uuid: str, page_size: int = 1000) -> list[dict]: + """分页拉取所有 owner 工作流,直到 has_more=false。""" + workflows: list[dict] = [] + page = 1 + while True: + query = urllib.parse.urlencode( + {"page": page, "page_size": page_size, "lab_uuid": lab_uuid} + ) + url = f"{base.rstrip('/')}/api/v1/lab/workflow/owner/list?{query}" + req = urllib.request.Request( + url, + headers={ + "Authorization": f"Lab {auth_token}", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=30) as resp: + payload = json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as e: + sys.exit(f"[ERROR] HTTP {e.code} on page {page}: {e.read().decode('utf-8', 'ignore')}") + except urllib.error.URLError as e: + sys.exit(f"[ERROR] URL error on page {page}: {e.reason}") + + if payload.get("code") != 0: + sys.exit(f"[ERROR] API returned non-zero code: {payload}") + + data = payload.get("data") or {} + page_items = data.get("data") or [] + workflows.extend(page_items) + + if not data.get("has_more"): + break + page += 1 + # 防御性兜底,避免接口异常导致无限循环 + if page > 1000: + print(f"[WARN] page count exceeded 1000, stopping early", file=sys.stderr) + break + + return workflows + + +def aggregate_tags(workflows: list[dict]) -> tuple[list[str], dict[str, int], int]: + """返回 (sorted_tags, tag_counts, untagged_count)。""" + counter: Counter[str] = Counter() + untagged = 0 + for wf in workflows: + tags = wf.get("tags") + if not tags: + untagged += 1 + continue + for t in tags: + if isinstance(t, str) and t.strip(): + counter[t.strip()] += 1 + return sorted(counter.keys()), dict(counter), untagged + + +def filter_workflows( + workflows: list[dict], + want_tags: list[str], + mode: str, + published_only: bool, +) -> list[dict]: + """按 tag 筛选。mode 取值 any / all。""" + want_set = {t.strip() for t in want_tags if t.strip()} + out: list[dict] = [] + for wf in workflows: + if published_only and not wf.get("published"): + continue + if not want_set: + out.append(wf) + continue + tags = wf.get("tags") or [] + tag_set = {t for t in tags if isinstance(t, str)} + if mode == "all": + if want_set.issubset(tag_set): + out.append(wf) + else: # any + if want_set & tag_set: + out.append(wf) + return out + + +def project_workflow(wf: dict) -> dict: + """精简输出字段。""" + return { + "uuid": wf.get("uuid"), + "name": wf.get("name"), + "description": wf.get("description", ""), + "tags": wf.get("tags") or [], + "published": bool(wf.get("published")), + "user_id": wf.get("user_id"), + } + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="Fetch & filter Uni-Lab workflows by tags.") + p.add_argument("--auth", required=True, help="Base64 token (the part after `Lab `).") + p.add_argument("--base", required=True, help="Base URL, e.g. https://leap-lab.test.bohrium.com") + p.add_argument("--lab-uuid", required=True, help="Lab UUID.") + p.add_argument("--tags", nargs="*", default=[], help="Tags to filter by (space separated).") + p.add_argument( + "--mode", + choices=["any", "all"], + default="any", + help="any: workflow contains at least one tag; all: workflow contains every tag.", + ) + p.add_argument("--published-only", action="store_true", help="Only include published workflows.") + p.add_argument("--page-size", type=int, default=1000, help="Page size, default 1000.") + p.add_argument( + "--summary-only", + action="store_true", + help="Print tag summary without applying filter (still fetches everything).", + ) + p.add_argument("--output", help="Write JSON result to this path. If omitted, print to stdout.") + return p.parse_args() + + +def main() -> None: + args = parse_args() + workflows = fetch_all_workflows( + base=args.base, + auth_token=args.auth, + lab_uuid=args.lab_uuid, + page_size=args.page_size, + ) + sorted_tags, tag_counts, untagged = aggregate_tags(workflows) + + if args.summary_only: + result = { + "total_workflows": len(workflows), + "untagged_count": untagged, + "tag_counts": tag_counts, + "all_tags": sorted_tags, + } + else: + filtered = filter_workflows( + workflows, + want_tags=args.tags, + mode=args.mode, + published_only=args.published_only, + ) + result = { + "total_workflows": len(workflows), + "untagged_count": untagged, + "tag_counts": tag_counts, + "all_tags": sorted_tags, + "filter": { + "tags": args.tags, + "mode": args.mode, + "published_only": args.published_only, + }, + "matched_count": len(filtered), + "filtered_workflows": [project_workflow(wf) for wf in filtered], + } + + payload = json.dumps(result, ensure_ascii=False, indent=2) + if args.output: + with open(args.output, "w", encoding="utf-8") as f: + f.write(payload) + print(f"Wrote {len(workflows)} workflows summary → {args.output}", file=sys.stderr) + else: + print(payload) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml index 402edc26..698344bf 100644 --- a/.github/workflows/ci-check.yml +++ b/.github/workflows/ci-check.yml @@ -38,7 +38,7 @@ jobs: - name: Install ROS dependencies, uv and unilabos-msgs run: | echo Installing ROS dependencies... - mamba install -n check-env conda-forge::uv conda-forge::opencv robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs robostack-staging::ros-humble-nav2-msgs uni-lab::ros-humble-unilabos-msgs robostack-staging::ros-humble-cv-bridge robostack-staging::ros-humble-vision-opencv robostack-staging::ros-humble-tf-transformations robostack-staging::ros-humble-moveit-msgs robostack-staging::ros-humble-tf2-ros robostack-staging::ros-humble-tf2-ros-py conda-forge::transforms3d -c robostack-staging -c conda-forge -c uni-lab -y + mamba install -n check-env --override-channels -c robostack-staging -c conda-forge -c uni-lab conda-forge::uv conda-forge::opencv robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs robostack-staging::ros-humble-nav2-msgs uni-lab::ros-humble-unilabos-msgs robostack-staging::ros-humble-cv-bridge robostack-staging::ros-humble-vision-opencv robostack-staging::ros-humble-tf-transformations robostack-staging::ros-humble-moveit-msgs robostack-staging::ros-humble-tf2-ros robostack-staging::ros-humble-tf2-ros-py conda-forge::transforms3d -y - name: Install pip dependencies and unilabos run: | diff --git a/.github/workflows/conda-pack-build.yml b/.github/workflows/conda-pack-build.yml index ed45db9d..3da148dd 100644 --- a/.github/workflows/conda-pack-build.yml +++ b/.github/workflows/conda-pack-build.yml @@ -1,6 +1,10 @@ name: Build Conda-Pack Environment on: + # 在 UniLabOS Conda Build 成功上传后自动构建非全量 conda-pack + workflow_run: + workflows: ["UniLabOS Conda Build"] + types: [completed] workflow_dispatch: inputs: branch: @@ -21,6 +25,16 @@ on: jobs: build-conda-pack: + if: | + github.event_name == 'workflow_dispatch' || + ( + github.event_name == 'workflow_run' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'workflow_run' + ) + env: + BUILD_FULL: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }} + PACKAGE_REF: ${{ github.event.inputs.branch || github.event.workflow_run.head_sha || github.ref_name }} strategy: fail-fast: false matrix: @@ -54,7 +68,9 @@ jobs: id: should_build shell: bash run: | - if [[ -z "${{ github.event.inputs.platforms }}" ]]; then + if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then + echo "should_build=true" >> $GITHUB_OUTPUT + elif [[ -z "${{ github.event.inputs.platforms }}" ]]; then echo "should_build=true" >> $GITHUB_OUTPUT elif [[ "${{ github.event.inputs.platforms }}" == *"${{ matrix.platform }}"* ]]; then echo "should_build=true" >> $GITHUB_OUTPUT @@ -65,7 +81,7 @@ jobs: - uses: actions/checkout@v6 if: steps.should_build.outputs.should_build == 'true' with: - ref: ${{ github.event.inputs.branch }} + ref: ${{ github.event.inputs.branch || github.event.workflow_run.head_sha || github.ref }} fetch-depth: 0 - name: Setup Miniforge (with mamba) @@ -75,7 +91,7 @@ jobs: miniforge-version: latest use-mamba: true python-version: '3.11.14' - channels: conda-forge,robostack-staging,uni-lab,defaults + channels: conda-forge,robostack-staging,uni-lab channel-priority: flexible activate-environment: unilab auto-update-conda: false @@ -86,13 +102,13 @@ jobs: run: | echo Installing unilabos and dependencies to unilab environment... echo Using mamba for faster and more reliable dependency resolution... - echo Build full: ${{ github.event.inputs.build_full }} - if "${{ github.event.inputs.build_full }}"=="true" ( + echo Build full: ${{ env.BUILD_FULL }} + if "${{ env.BUILD_FULL }}"=="true" ( echo Installing unilabos-full ^(complete package^)... - mamba install -n unilab uni-lab::unilabos-full conda-pack -c uni-lab -c robostack-staging -c conda-forge -y + mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos-full conda-pack zstandard -y ) else ( echo Installing unilabos ^(minimal package^)... - mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y + mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos conda-pack zstandard -y ) - name: Install conda-pack, unilabos and dependencies (Unix) @@ -101,13 +117,13 @@ jobs: run: | echo "Installing unilabos and dependencies to unilab environment..." echo "Using mamba for faster and more reliable dependency resolution..." - echo "Build full: ${{ github.event.inputs.build_full }}" - if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then + echo "Build full: ${{ env.BUILD_FULL }}" + if [[ "${{ env.BUILD_FULL }}" == "true" ]]; then echo "Installing unilabos-full (complete package)..." - mamba install -n unilab uni-lab::unilabos-full conda-pack -c uni-lab -c robostack-staging -c conda-forge -y + mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos-full conda-pack zstandard -y else echo "Installing unilabos (minimal package)..." - mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y + mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos conda-pack zstandard -y fi - name: Get latest ros-humble-unilabos-msgs version (Windows) @@ -134,27 +150,27 @@ jobs: if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64' run: | echo Checking for available ros-humble-unilabos-msgs versions... - mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo Search completed + mamba search --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs || echo Search completed echo. echo Updating ros-humble-unilabos-msgs to latest version... - mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo Already at latest version + mamba update -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs -y || echo Already at latest version - name: Check for newer ros-humble-unilabos-msgs (Unix) if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64' shell: bash run: | echo "Checking for available ros-humble-unilabos-msgs versions..." - mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo "Search completed" + mamba search --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs || echo "Search completed" echo "" echo "Updating ros-humble-unilabos-msgs to latest version..." - mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo "Already at latest version" + mamba update -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs -y || echo "Already at latest version" - name: Install latest unilabos from source (Windows) if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64' run: | echo Uninstalling existing unilabos... mamba run -n unilab pip uninstall unilabos -y || echo unilabos not installed via pip - echo Installing unilabos from source (branch: ${{ github.event.inputs.branch }})... + echo Installing unilabos from source (ref: ${{ env.PACKAGE_REF }})... mamba run -n unilab pip install . echo Verifying installation... mamba run -n unilab pip show unilabos @@ -165,7 +181,7 @@ jobs: run: | echo "Uninstalling existing unilabos..." mamba run -n unilab pip uninstall unilabos -y || echo "unilabos not installed via pip" - echo "Installing unilabos from source (branch: ${{ github.event.inputs.branch }})..." + echo "Installing unilabos from source (ref: ${{ env.PACKAGE_REF }})..." mamba run -n unilab pip install . echo "Verifying installation..." mamba run -n unilab pip show unilabos @@ -226,7 +242,9 @@ jobs: if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64' run: | echo Packing unilab environment with conda-pack... - mamba activate unilab && conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files + for /f "delims=" %%i in ('mamba run -n unilab python -c "import os; print(os.environ['CONDA_PREFIX'])"') do set "UNILAB_PREFIX=%%i" + echo Packing environment at: %UNILAB_PREFIX% + mamba run -n unilab conda-pack -p "%UNILAB_PREFIX%" -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files echo Pack file created: dir unilab-env-${{ matrix.platform }}.tar.gz @@ -235,8 +253,9 @@ jobs: shell: bash run: | echo "Packing unilab environment with conda-pack..." - mamba install conda-pack -c conda-forge -y - conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files + UNILAB_PREFIX="$(mamba run -n unilab python -c 'import os; print(os.environ["CONDA_PREFIX"])')" + echo "Packing environment at: $UNILAB_PREFIX" + mamba run -n unilab conda-pack -p "$UNILAB_PREFIX" -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files echo "Pack file created:" ls -lh unilab-env-${{ matrix.platform }}.tar.gz @@ -267,7 +286,7 @@ jobs: rem Create README using Python script echo Creating: README.txt - python scripts\create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package\README.txt + python scripts\create_readme.py ${{ matrix.platform }} ${{ env.PACKAGE_REF }} dist-package\README.txt echo. echo Distribution package contents: @@ -303,7 +322,7 @@ jobs: # Create README using Python script echo "Creating: README.txt" - python scripts/create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package/README.txt + python scripts/create_readme.py ${{ matrix.platform }} ${{ env.PACKAGE_REF }} dist-package/README.txt echo "" echo "Distribution package contents:" @@ -314,7 +333,7 @@ jobs: if: steps.should_build.outputs.should_build == 'true' uses: actions/upload-artifact@v6 with: - name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }} + name: unilab-pack-${{ matrix.platform }}-${{ env.PACKAGE_REF }} path: dist-package/ retention-days: 90 if-no-files-found: error @@ -326,9 +345,9 @@ jobs: echo Build Summary echo ========================================== echo Platform: ${{ matrix.platform }} - echo Branch: ${{ github.event.inputs.branch }} + echo Branch: ${{ env.PACKAGE_REF }} echo Python version: 3.11.14 - if "${{ github.event.inputs.build_full }}"=="true" ( + if "${{ env.BUILD_FULL }}"=="true" ( echo Package: unilabos-full ^(complete^) ) else ( echo Package: unilabos ^(minimal^) @@ -337,7 +356,7 @@ jobs: echo Distribution package contents: dir dist-package echo. - echo Artifact name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }} + echo Artifact name: unilab-pack-${{ matrix.platform }}-${{ env.PACKAGE_REF }} echo. echo After download, extract the ZIP and run: echo install_unilab.bat @@ -351,9 +370,9 @@ jobs: echo "Build Summary" echo "==========================================" echo "Platform: ${{ matrix.platform }}" - echo "Branch: ${{ github.event.inputs.branch }}" + echo "Branch: ${{ env.PACKAGE_REF }}" echo "Python version: 3.11.14" - if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then + if [[ "${{ env.BUILD_FULL }}" == "true" ]]; then echo "Package: unilabos-full (complete)" else echo "Package: unilabos (minimal)" @@ -362,7 +381,7 @@ jobs: echo "Distribution package contents:" ls -lh dist-package/ echo "" - echo "Artifact name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}" + echo "Artifact name: unilab-pack-${{ matrix.platform }}-${{ env.PACKAGE_REF }}" echo "" echo "After download:" echo " install_unilab.sh" diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index f3ac4d11..a3ca6469 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -56,7 +56,7 @@ jobs: miniforge-version: latest use-mamba: true python-version: '3.11.14' - channels: conda-forge,robostack-staging,uni-lab,defaults + channels: conda-forge,robostack-staging,uni-lab channel-priority: flexible activate-environment: unilab auto-update-conda: false @@ -66,7 +66,7 @@ jobs: run: | echo "Installing unilabos and dependencies to unilab environment..." echo "Using mamba for faster and more reliable dependency resolution..." - mamba install -n unilab uni-lab::unilabos -c uni-lab -c robostack-staging -c conda-forge -y + mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos -y - name: Install latest unilabos from source run: | diff --git a/.github/workflows/multi-platform-build.yml b/.github/workflows/multi-platform-build.yml index 4e1cf4f7..1c5dd757 100644 --- a/.github/workflows/multi-platform-build.yml +++ b/.github/workflows/multi-platform-build.yml @@ -10,6 +10,9 @@ on: # 支持 tag 推送(不依赖 CI Check) push: tags: ['v*'] + # GitHub Release 发布时自动构建并上传 + release: + types: [published] # 手动触发 workflow_dispatch: inputs: @@ -80,7 +83,7 @@ jobs: - uses: actions/checkout@v6 with: # 如果是 workflow_run 触发,使用触发 CI Check 的 commit - ref: ${{ github.event.workflow_run.head_sha || github.ref }} + ref: ${{ github.event.workflow_run.head_sha || github.event.release.tag_name || github.ref }} fetch-depth: 0 - name: Check if platform should be built @@ -96,12 +99,13 @@ jobs: echo "should_build=false" >> $GITHUB_OUTPUT fi - - name: Setup Miniconda + - name: Setup Miniforge if: steps.should_build.outputs.should_build == 'true' uses: conda-incubator/setup-miniconda@v3 with: - miniconda-version: 'latest' - channels: conda-forge,robostack-staging,defaults + miniforge-version: latest + use-mamba: true + channels: conda-forge,robostack-staging channel-priority: strict activate-environment: build-env auto-update-conda: false @@ -110,7 +114,7 @@ jobs: - name: Install rattler-build and anaconda-client if: steps.should_build.outputs.should_build == 'true' run: | - conda install -c conda-forge rattler-build anaconda-client + mamba install --override-channels -c conda-forge rattler-build anaconda-client -y - name: Show environment info if: steps.should_build.outputs.should_build == 'true' @@ -157,7 +161,13 @@ jobs: retention-days: 30 - name: Upload to Anaconda.org (unilab organization) - if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true' + if: | + steps.should_build.outputs.should_build == 'true' && + ( + github.event_name == 'release' || + startsWith(github.ref, 'refs/tags/') || + github.event.inputs.upload_to_anaconda == 'true' + ) run: | for package in $(find ./output -name "*.conda"); do echo "Uploading $package to unilab organization..." diff --git a/.github/workflows/unilabos-conda-build.yml b/.github/workflows/unilabos-conda-build.yml index d116a67e..11025543 100644 --- a/.github/workflows/unilabos-conda-build.yml +++ b/.github/workflows/unilabos-conda-build.yml @@ -1,14 +1,10 @@ name: UniLabOS Conda Build on: - # 在 CI Check 成功后自动触发 + # 在 Multi-Platform Conda Build 成功上传 msgs 后自动触发 workflow_run: - workflows: ["CI Check"] + workflows: ["Multi-Platform Conda Build"] types: [completed] - branches: [main, dev] - # 标签推送时直接触发(发布版本) - push: - tags: ['v*'] # 手动触发 workflow_dispatch: inputs: @@ -33,30 +29,30 @@ on: type: boolean jobs: - # 等待 CI Check 完成的 job (仅用于 workflow_run 触发) - wait-for-ci: + # 等待上游 msgs 构建完成的 job (仅用于 workflow_run 触发) + wait-for-upstream: runs-on: ubuntu-latest if: github.event_name == 'workflow_run' outputs: should_continue: ${{ steps.check.outputs.should_continue }} steps: - - name: Check CI status + - name: Check upstream workflow status id: check run: | - if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then + if [[ "${{ github.event.workflow_run.conclusion }}" == "success" && ( "${{ github.event.workflow_run.event }}" == "release" || "${{ github.event.workflow_run.event }}" == "push" ) ]]; then echo "should_continue=true" >> $GITHUB_OUTPUT - echo "CI Check passed, proceeding with build" + echo "Multi-Platform Conda Build passed for release/tag, proceeding with UniLabOS build" else echo "should_continue=false" >> $GITHUB_OUTPUT - echo "CI Check did not succeed (status: ${{ github.event.workflow_run.conclusion }}), skipping build" + echo "Upstream workflow is not a successful release/tag build (status: ${{ github.event.workflow_run.conclusion }}, event: ${{ github.event.workflow_run.event }}), skipping build" fi build: - needs: [wait-for-ci] - # 运行条件:workflow_run 触发且 CI 成功,或者其他触发方式 + needs: [wait-for-upstream] + # 运行条件:workflow_run 触发且上游成功,或者手动触发 if: | always() && - (needs.wait-for-ci.result == 'skipped' || needs.wait-for-ci.outputs.should_continue == 'true') + (needs.wait-for-upstream.result == 'skipped' || needs.wait-for-upstream.outputs.should_continue == 'true') strategy: fail-fast: false matrix: @@ -79,7 +75,7 @@ jobs: steps: - uses: actions/checkout@v6 with: - # 如果是 workflow_run 触发,使用触发 CI Check 的 commit + # 如果是 workflow_run 触发,使用上游 conda 包构建的 commit ref: ${{ github.event.workflow_run.head_sha || github.ref }} fetch-depth: 0 @@ -96,12 +92,13 @@ jobs: echo "should_build=false" >> $GITHUB_OUTPUT fi - - name: Setup Miniconda + - name: Setup Miniforge if: steps.should_build.outputs.should_build == 'true' uses: conda-incubator/setup-miniconda@v3 with: - miniconda-version: 'latest' - channels: conda-forge,robostack-staging,uni-lab,defaults + miniforge-version: latest + use-mamba: true + channels: conda-forge,robostack-staging,uni-lab channel-priority: strict activate-environment: build-env auto-update-conda: false @@ -110,7 +107,7 @@ jobs: - name: Install rattler-build and anaconda-client if: steps.should_build.outputs.should_build == 'true' run: | - conda install -c conda-forge rattler-build anaconda-client + mamba install --override-channels -c conda-forge rattler-build anaconda-client -y - name: Show environment info if: steps.should_build.outputs.should_build == 'true' @@ -119,11 +116,11 @@ jobs: conda list | grep -E "(rattler-build|anaconda-client)" echo "Platform: ${{ matrix.platform }}" echo "OS: ${{ matrix.os }}" - echo "Build full package: ${{ github.event.inputs.build_full || 'false' }}" + echo "Build full package: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }}" echo "Building packages:" echo " - unilabos-env (environment dependencies)" echo " - unilabos (with pip package)" - if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then + if [[ "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }}" == "true" ]]; then echo " - unilabos-full (complete package)" fi @@ -134,7 +131,12 @@ jobs: rattler-build build -r .conda/environment/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge - name: Upload unilabos-env to Anaconda.org (if enabled) - if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true' + if: | + steps.should_build.outputs.should_build == 'true' && + ( + github.event_name == 'workflow_run' || + github.event.inputs.upload_to_anaconda == 'true' + ) run: | echo "Uploading unilabos-env to uni-lab organization..." for package in $(find ./output -name "unilabos-env*.conda"); do @@ -149,7 +151,12 @@ jobs: rattler-build build -r .conda/base/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output - name: Upload unilabos to Anaconda.org (if enabled) - if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true' + if: | + steps.should_build.outputs.should_build == 'true' && + ( + github.event_name == 'workflow_run' || + github.event.inputs.upload_to_anaconda == 'true' + ) run: | echo "Uploading unilabos to uni-lab organization..." for package in $(find ./output -name "unilabos-0*.conda" -o -name "unilabos-[0-9]*.conda"); do @@ -159,6 +166,7 @@ jobs: - name: Build unilabos-full - Only when explicitly requested if: | steps.should_build.outputs.should_build == 'true' && + github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' run: | echo "Building unilabos-full package on ${{ matrix.platform }}..." @@ -167,6 +175,7 @@ jobs: - name: Upload unilabos-full to Anaconda.org (if enabled) if: | steps.should_build.outputs.should_build == 'true' && + github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' && github.event.inputs.upload_to_anaconda == 'true' run: | diff --git a/recipes/conda_build_config.yaml b/recipes/conda_build_config.yaml index 8e95491c..c8915207 100644 --- a/recipes/conda_build_config.yaml +++ b/recipes/conda_build_config.yaml @@ -1,5 +1,5 @@ channel_sources: - - robostack,robostack-staging,conda-forge,defaults + - robostack,robostack-staging,conda-forge gazebo: - '11' diff --git a/unilabos/app/utils.py b/unilabos/app/utils.py index f6114a13..a225e3ae 100644 --- a/unilabos/app/utils.py +++ b/unilabos/app/utils.py @@ -10,29 +10,170 @@ import shutil import sys +_PATCH_MARKER = "# UniLabOS DLL Patch" +_PATCH_END_MARKER = "# End UniLabOS DLL Patch" + +# 75 = EX_TEMPFAIL: 临时失败、重试即可,避免与业务退出码冲突 +_RESTART_EXIT_CODE = 75 + + +def _build_dll_patch(lib_bin: str, preload_pyd: str = "") -> str: + """生成一段加在目标文件顶部的 DLL 加载补丁源码。 + + - 始终把 ``lib_bin`` 加入 DLL 搜索路径,并把 handle 挂在模块属性上, + 防止 GC 清掉搜索路径(``os.add_dll_directory`` 的句柄被回收时 + 目录会被移除)。 + - 可选地用 ``ctypes.CDLL`` 预加载一个 .pyd,把它的依赖 DLL 提前装入 + 进程内存,作为 ``rclpy._rclpy_pybind11`` 这类首次加载点的兜底。 + """ + # 用 repr() 序列化路径:Python 解析 repr 的结果会还原成原始字符串, + # 不需要也不能再叠加 raw-string 前缀(叠了反而会让 \\ 变成两个反斜杠)。 + lines = [ + _PATCH_MARKER, + "import os as _ulab_os", + f"_ulab_p = {lib_bin!r}", + 'if hasattr(_ulab_os, "add_dll_directory") and _ulab_os.path.isdir(_ulab_p):', + " try: _UNILAB_DLL_HANDLE = _ulab_os.add_dll_directory(_ulab_p)", + " except Exception: _UNILAB_DLL_HANDLE = None", + ] + if preload_pyd: + lines.extend( + [ + "import ctypes as _ulab_ctypes", + f"try: _ulab_ctypes.CDLL({preload_pyd!r})", + "except Exception: pass", + ] + ) + lines.append(_PATCH_END_MARKER) + return "\n".join(lines) + "\n" + + +def _apply_dll_patch(file_path: str, lib_bin: str, preload_pyd: str = "") -> bool: + """把 DLL 补丁前置到 ``file_path``。文件不存在或已打过补丁则返回 False。""" + if not os.path.isfile(file_path): + return False + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + if _PATCH_MARKER in content: + return False + shutil.copy2(file_path, file_path + ".bak") + with open(file_path, "w", encoding="utf-8") as f: + f.write(_build_dll_patch(lib_bin, preload_pyd) + content) + return True + + +def _print_restart_banner(patched_files): + """打印重启提示并以 EX_TEMPFAIL 退出。 + + - 不使用 ANSI 颜色码:Windows 旧版 cmd / PowerShell 5 默认不开 VT 处理, + 会把 ``\\033[1;33m`` 当做字面字符显示,反而让用户看不到正文。 + - 同时写入 stderr 与 stdout:某些上层 launcher / supervisor 只重定向 + 其中一路,写两遍能保证用户至少看到一份。 + - 写入前防御性把流切到 UTF-8 with replace:``main.py`` 里已经做过一次, + 但本模块也可能被绕过 ``main.py`` 的代码路径直接 import;reconfigure + 失败也只是退回 errors=replace,不影响整体流程。 + """ + if sys.platform == "win32": + for _stream in (sys.stdout, sys.stderr): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined] + except (AttributeError, OSError): + pass + + bar = "#" * 78 + files_lines = [f"[UniLabOS] - {p}" for p in patched_files] + body = "\n".join( + [ + "", + bar, + bar, + "##", + "## [UniLabOS] Windows + conda 下检测到 DLL 加载失败,已自动打补丁。", + "## [UniLabOS] DLL load failure detected on Windows + conda;", + "## [UniLabOS] the following files have been auto-patched:", + "##", + *[f"## {line}" for line in files_lines], + "##", + "## [UniLabOS] 当前进程的 rclpy 状态已损坏,补丁需要在新进程才生效。", + "## [UniLabOS] The current process is unusable; the patch only takes", + "## [UniLabOS] effect on a fresh process.", + "##", + "## >>> 请重新运行刚才的命令 / Please re-run the same command. <<<", + "##", + bar, + bar, + "", + ] + ) + + for stream in (sys.stderr, sys.stdout): + try: + stream.write(body) + stream.flush() + except Exception: + try: + print(body, file=stream) + except Exception: + pass + + sys.exit(_RESTART_EXIT_CODE) + + def patch_rclpy_dll_windows(): - """在 Windows + conda 环境下为 rclpy 打 DLL 加载补丁""" + """在 Windows + conda 环境下修复 rclpy / rosidl typesupport 的 DLL 加载。 + + 背景:conda 安装的 ros 系列包,其原生扩展依赖 ``$CONDA_PREFIX/Library/bin`` + 下的 DLL;只有 conda 环境被正确激活、且 PATH 中含 ``Library/bin`` 时, + ``os.add_dll_directory`` 才能找到它们。当从快捷方式 / IDE / 子进程 / + 没激活的 shell 启动 ``unilab`` 时,会出现 ``DLL load failed``。 + + 本函数会: + 1) 修补 ``rclpy/impl/implementation_singleton.py`` —— rclpy 自身的 C 扩展入口; + 2) 修补 ``rpyutils/add_dll_directories.py`` —— 所有 ``*_s__rosidl_typesupport_c.pyd`` + (``geometry_msgs`` / ``std_msgs`` / ``sensor_msgs`` 等)的统一加载入口。 + + 打完补丁后**必须重启进程**才能生效(当前进程的 rclpy 已经发生过 + ``ImportError``,子模块仍处于损坏状态)。因此函数会主动退出,并在 + stdout/stderr 同时打印明显的重启提示,避免用户被后续报错淹没。 + """ if sys.platform != "win32" or not os.environ.get("CONDA_PREFIX"): return + try: - import rclpy + import rclpy # noqa: F401 return except ImportError as e: if not str(e).startswith("DLL load failed"): return + cp = os.environ["CONDA_PREFIX"] - impl = os.path.join(cp, "Lib", "site-packages", "rclpy", "impl", "implementation_singleton.py") - pyd = glob.glob(os.path.join(cp, "Lib", "site-packages", "rclpy", "_rclpy_pybind11*.pyd")) - if not os.path.exists(impl) or not pyd: + lib_bin = os.path.join(cp, "Library", "bin") + site_packages = os.path.join(cp, "Lib", "site-packages") + if not os.path.isdir(lib_bin): return - with open(impl, "r", encoding="utf-8") as f: - content = f.read() - lib_bin = os.path.join(cp, "Library", "bin").replace("\\", "/") - patch = f'# UniLabOS DLL Patch\nimport os,ctypes\nos.add_dll_directory("{lib_bin}") if hasattr(os,"add_dll_directory") else None\ntry: ctypes.CDLL("{pyd[0].replace(chr(92),"/")}")\nexcept: pass\n# End Patch\n' - shutil.copy2(impl, impl + ".bak") - with open(impl, "w", encoding="utf-8") as f: - f.write(patch + content) + + patched = [] + + # 1) rclpy 自身的入口 + rclpy_impl = os.path.join(site_packages, "rclpy", "impl", "implementation_singleton.py") + rclpy_pyd_matches = glob.glob(os.path.join(site_packages, "rclpy", "_rclpy_pybind11*.pyd")) + rclpy_pyd = rclpy_pyd_matches[0] if rclpy_pyd_matches else "" + if rclpy_pyd and _apply_dll_patch(rclpy_impl, lib_bin, preload_pyd=rclpy_pyd): + patched.append(rclpy_impl) + + # 2) rpyutils —— 所有 rosidl typesupport pyd 的加载点;放在 rclpy 之后 + # 例:geometry_msgs/geometry_msgs_s__rosidl_typesupport_c.pyd + rpyutils_dll = os.path.join(site_packages, "rpyutils", "add_dll_directories.py") + if _apply_dll_patch(rpyutils_dll, lib_bin): + patched.append(rpyutils_dll) + + if not patched: + # 已经打过补丁但 rclpy 仍然加载失败:原因不是缺 DLL 搜索路径, + # 不要再次打补丁污染文件,让上层看到真实的 ImportError。 + return + + _print_restart_banner(patched) patch_rclpy_dll_windows() diff --git a/unilabos/registry/devices/Qone_nmr.yaml b/unilabos/registry/devices/Qone_nmr.yaml index 5c5f1f8a..fd2761e4 100644 --- a/unilabos/registry/devices/Qone_nmr.yaml +++ b/unilabos/registry/devices/Qone_nmr.yaml @@ -51,14 +51,18 @@ Qone_nmr: properties: check_interval: default: 60 + description: 检查间隔时间(秒),默认60秒 type: string expected_count: default: 1 + description: 期望生成的.nmr文件数量,默认1个 type: string monitor_dir: + description: 要监督的目录路径,如果未指定则使用self.monitor_directory type: string stability_checks: default: 3 + description: 文件大小稳定性检查次数,默认3次 type: string required: [] type: object @@ -85,11 +89,14 @@ Qone_nmr: goal: properties: output_dir: + description: 输出目录(如果未指定,使用self.output_directory) type: string string_list: + description: 字符串列表 type: string txt_encoding: default: utf-8 + description: 文件编码 type: string required: - string_list @@ -151,6 +158,13 @@ Qone_nmr: additionalProperties: false properties: string: + description: '包含多个字符串的输入数据,支持两种格式: + + 1. 逗号分隔:如 "A 1 B 2 C 3, X 10 Y 20 Z 30" + + 2. 换行分隔:如 "A 1 B 2 C 3 + + X 10 Y 20 Z 30"' type: string title: StrSingleInput_Goal type: object diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index f57cd35c..6b2d1b17 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -491,14 +491,17 @@ bioyond_cell: goal: properties: material_names: + description: 物料名称列表;默认使用 [LiPF6, LiDFOB, DTD, LiFSI, LiPO2F2] items: type: string type: array type_id: default: 3a190ca0-b2f6-9aeb-8067-547e72c11469 + description: 物料类型ID type: string warehouse_name: default: 粉末加样头堆栈 + description: 目标仓库名(用于取位置信息) type: string required: [] type: object @@ -527,12 +530,16 @@ bioyond_cell: goal: properties: location_name_or_id: + description: 具体库位名称(如 A01)或库位 UUID,由用户指定。 type: string material_name: + description: 物料名称(会优先匹配配置模板)。 type: string type_id: + description: 物料类型 ID(若为空则尝试从配置推断)。 type: string warehouse_name: + description: 需要入库的仓库名称;若为空则仅创建不入库。 type: string required: - material_name @@ -661,15 +668,20 @@ bioyond_cell: goal: properties: board_type: + description: 板类型,如 "5ml分液瓶板"、"配液瓶(小)板" type: string bottle_type: + description: 瓶类型,如 "5ml分液瓶"、"配液瓶(小)" type: string location_code: + description: 库位编号,例如 "A01" type: string name: + description: 物料名称 type: string warehouse_name: default: 手动堆栈 + description: 仓库名称,默认为 "手动堆栈",支持 "自动堆栈-左"、"自动堆栈-右" 等 type: string required: - name @@ -1956,19 +1968,19 @@ bioyond_cell: properties: source_wh_id: default: 3a19debc-84b4-0359-e2d4-b3beea49348b - description: 来源仓库ID + description: 来源仓库 Id (默认为3号仓库) type: string source_x: default: 1 - description: 来源位置X坐标 + description: 来源位置 X 坐标 type: integer source_y: default: 1 - description: 来源位置Y坐标 + description: 来源位置 Y 坐标 type: integer source_z: default: 1 - description: 来源位置Z坐标 + description: 来源位置 Z 坐标 type: integer required: [] type: object @@ -2061,9 +2073,11 @@ bioyond_cell: goal: properties: order_code: + description: 任务编号 type: string timeout: default: 36000 + description: 超时时间(秒) type: integer required: - order_code @@ -2092,12 +2106,15 @@ bioyond_cell: goal: properties: order_code: + description: 任务编号 type: string poll_interval: default: 0.5 + description: 轮询间隔(秒),默认 0.5 秒 type: number timeout: default: 36000 + description: 超时时间(秒) type: integer required: - order_code @@ -2154,10 +2171,15 @@ bioyond_cell: config: properties: bioyond_config: + description: '从 JSON 文件加载的 bioyond 配置字典 + + 包含 api_host, api_key, HTTP_host, HTTP_port 等配置' type: object deck: + description: Deck 配置(可选,会从 JSON 中自动处理) type: string protocol_type: + description: 协议类型(可选) type: string required: [] type: object diff --git a/unilabos/registry/devices/bioyond_dispensing_station.yaml b/unilabos/registry/devices/bioyond_dispensing_station.yaml index 547b54ff..21f36e16 100644 --- a/unilabos/registry/devices/bioyond_dispensing_station.yaml +++ b/unilabos/registry/devices/bioyond_dispensing_station.yaml @@ -47,8 +47,10 @@ bioyond_dispensing_station: goal: properties: report_request: + description: WorkstationReportRequest 对象,包含任务完成信息 type: string used_materials: + description: 物料使用记录列表 type: string required: - report_request @@ -102,6 +104,7 @@ bioyond_dispensing_station: goal: properties: material_name: + description: 物料名称 type: string required: - material_name @@ -611,10 +614,10 @@ bioyond_dispensing_station: goal: properties: target_device_id: - description: 目标反应站设备ID(从设备列表中选择,所有转移组都使用同一个目标设备) + description: 目标反应站设备ID(所有转移组使用同一个设备) type: string transfer_groups: - description: 转移任务组列表,每组包含物料名称、目标堆栈和目标库位,可以添加多组 + description: '转移任务组列表,每组包含:' type: array required: - target_device_id @@ -694,10 +697,13 @@ bioyond_dispensing_station: config: properties: config: + description: 配置字典,应包含material_type_mappings等配置 type: object deck: + description: Deck对象 type: string protocol_type: + description: 协议类型(由ROS系统传递,此处忽略) type: string required: [] type: object diff --git a/unilabos/registry/devices/coin_cell_workstation.yaml b/unilabos/registry/devices/coin_cell_workstation.yaml index df5a3508..b692506c 100644 --- a/unilabos/registry/devices/coin_cell_workstation.yaml +++ b/unilabos/registry/devices/coin_cell_workstation.yaml @@ -150,15 +150,15 @@ coincellassemblyworkstation_device: properties: assembly_pressure: default: 4200 - description: 电池压制力(N) + description: 电池压制力 (N) type: integer assembly_type: default: 7 - description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫) + description: 组装类型 (7=不用铝箔垫, 8=使用铝箔垫) type: integer battery_clean_ignore: default: false - description: 是否忽略电池清洁步骤 + description: 是否忽略电池清洁 type: boolean battery_pressure_mode: default: true @@ -166,29 +166,29 @@ coincellassemblyworkstation_device: type: boolean dual_drop_first_volume: default: 25 - description: 二次滴液第一次排液体积(μL) + description: 二次滴液第一次排液体积 (μL) type: integer dual_drop_mode: default: false - description: 电解液添加模式(false=单次滴液, true=二次滴液) + description: 电解液添加模式 (False=单次滴液, True=二次滴液) type: boolean dual_drop_start_timing: default: false - description: 二次滴液开始滴液时机(false=正极片前, true=正极片后) + description: 二次滴液开始滴液时机 (False=正极片前, True=正极片后) type: boolean dual_drop_suction_timing: default: false - description: 二次滴液吸液时机(false=正常吸液, true=先吸液) + description: 二次滴液吸液时机 (False=正常吸液, True=先吸液) type: boolean elec_num: description: 电解液瓶数 type: string elec_use_num: - description: 每瓶电解液组装电池数 + description: 每瓶电解液组装的电池数 type: string elec_vol: default: 50 - description: 电解液吸液量(μL) + description: 电解液吸液量 (μL) type: integer file_path: default: /Users/sml/work @@ -196,7 +196,7 @@ coincellassemblyworkstation_device: type: string fujipian_juzhendianwei: default: 0 - description: 负极片矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + description: 负极片矩阵点位 type: integer fujipian_panshu: default: 0 @@ -204,7 +204,7 @@ coincellassemblyworkstation_device: type: integer gemo_juzhendianwei: default: 0 - description: 隔膜矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + description: 隔膜矩阵点位 type: integer gemopanshu: default: 0 @@ -216,7 +216,7 @@ coincellassemblyworkstation_device: type: boolean qiangtou_juzhendianwei: default: 0 - description: 枪头盒矩阵点位。盘位置从1开始计数,有效范围:1-32, 64-96 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + description: 枪头盒矩阵点位 type: integer required: - elec_num @@ -308,7 +308,13 @@ coincellassemblyworkstation_device: properties: material_search_enable: default: false - description: 是否启用物料搜寻功能。设备初始化后会弹出物料搜寻确认弹窗,此参数控制自动点击"是"(启用)或"否"(不启用)。默认为false(不启用物料搜寻) + description: '是否启用物料搜寻功能。 + + 设备初始化后会弹出物料搜寻确认弹窗, + + 此参数控制自动点击''是''(启用)或''否''(不启用)。 + + 默认为False(不启用物料搜寻)。' type: boolean required: [] type: object @@ -547,15 +553,15 @@ coincellassemblyworkstation_device: properties: assembly_pressure: default: 4200 - description: 电池压制力(N) + description: 电池压制力 (N) type: integer assembly_type: default: 7 - description: 组装类型(7=不用铝箔垫, 8=使用铝箔垫) + description: 组装类型 (7=不用铝箔垫, 8=使用铝箔垫) type: integer battery_clean_ignore: default: false - description: 是否忽略电池清洁步骤 + description: 是否忽略电池清洁 type: boolean battery_pressure_mode: default: true @@ -563,29 +569,29 @@ coincellassemblyworkstation_device: type: boolean dual_drop_first_volume: default: 25 - description: 二次滴液第一次排液体积(μL) + description: 二次滴液第一次排液体积 (μL) type: integer dual_drop_mode: default: false - description: 电解液添加模式(false=单次滴液, true=二次滴液) + description: 电解液添加模式 (False=单次滴液, True=二次滴液) type: boolean dual_drop_start_timing: default: false - description: 二次滴液开始滴液时机(false=正极片前, true=正极片后) + description: 二次滴液开始滴液时机 (False=正极片前, True=正极片后) type: boolean dual_drop_suction_timing: default: false - description: 二次滴液吸液时机(false=正常吸液, true=先吸液) + description: 二次滴液吸液时机 (False=正常吸液, True=先吸液) type: boolean elec_num: - description: 电解液瓶数,如果在workflow中已通过handles连接上游(create_orders的bottle_count输出),则此参数会自动从上游获取,无需手动填写;如果单独使用此函数(没有上游连接),则必须手动填写电解液瓶数 + description: 电解液瓶数 type: string elec_use_num: - description: 每瓶电解液组装电池数 + description: 每瓶电解液组装的电池数 type: string elec_vol: default: 50 - description: 电解液吸液量(μL) + description: 电解液吸液量 (μL) type: integer file_path: default: /Users/sml/work @@ -593,7 +599,7 @@ coincellassemblyworkstation_device: type: string fujipian_juzhendianwei: default: 0 - description: 负极片矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + description: 负极片矩阵点位 type: integer fujipian_panshu: default: 0 @@ -601,7 +607,7 @@ coincellassemblyworkstation_device: type: integer gemo_juzhendianwei: default: 0 - description: 隔膜矩阵点位。盘位置从1开始计数,有效范围:1-8, 13-20 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + description: 隔膜矩阵点位 type: integer gemopanshu: default: 0 @@ -613,7 +619,7 @@ coincellassemblyworkstation_device: type: boolean qiangtou_juzhendianwei: default: 0 - description: 枪头盒矩阵点位。盘位置从1开始计数,有效范围:1-32, 64-96 (写入值比实际位置少1,例如:写0取盘位1,写1取盘位2) + description: 枪头盒矩阵点位 type: integer required: - elec_num diff --git a/unilabos/registry/devices/laiyu_liquid_test.yaml b/unilabos/registry/devices/laiyu_liquid_test.yaml index 6d87f429..e3494cac 100644 --- a/unilabos/registry/devices/laiyu_liquid_test.yaml +++ b/unilabos/registry/devices/laiyu_liquid_test.yaml @@ -18,6 +18,7 @@ xyz_stepper_controller: goal: properties: degrees: + description: 角度值 type: number required: - degrees @@ -44,6 +45,7 @@ xyz_stepper_controller: goal: properties: axis: + description: 电机轴 type: object required: - axis @@ -71,6 +73,7 @@ xyz_stepper_controller: properties: enable: default: true + description: True为使能,False为失能 type: boolean required: [] type: object @@ -99,9 +102,11 @@ xyz_stepper_controller: goal: properties: axis: + description: 电机轴 type: object enable: default: true + description: True为使能,False为失能 type: boolean required: - axis @@ -152,6 +157,7 @@ xyz_stepper_controller: goal: properties: axis: + description: 电机轴 type: object required: - axis @@ -183,16 +189,21 @@ xyz_stepper_controller: properties: acceleration: default: 1000 + description: 加速度(rpm/s) type: integer axis: + description: 电机轴 type: object position: + description: 目标位置(步数) type: integer precision: default: 100 + description: 到位精度 type: integer speed: default: 5000 + description: 运行速度(rpm) type: integer required: - axis @@ -225,16 +236,21 @@ xyz_stepper_controller: properties: acceleration: default: 1000 + description: 加速度 type: integer axis: + description: 电机轴 type: object degrees: + description: 目标角度(度) type: number precision: default: 100 + description: 精度 type: integer speed: default: 5000 + description: 移动速度 type: integer required: - axis @@ -267,16 +283,21 @@ xyz_stepper_controller: properties: acceleration: default: 1000 + description: 加速度 type: integer axis: + description: 电机轴 type: object precision: default: 100 + description: 精度 type: integer revolutions: + description: 目标圈数 type: number speed: default: 5000 + description: 移动速度 type: integer required: - axis @@ -309,15 +330,20 @@ xyz_stepper_controller: properties: acceleration: default: 1000 + description: 加速度 type: integer speed: default: 5000 + description: 运行速度 type: integer x: + description: X轴目标位置 type: integer y: + description: Y轴目标位置 type: integer z: + description: Z轴目标位置 type: integer required: [] type: object @@ -350,15 +376,20 @@ xyz_stepper_controller: properties: acceleration: default: 1000 + description: 加速度 type: integer speed: default: 5000 + description: 移动速度 type: integer x_deg: + description: X轴目标角度(度) type: number y_deg: + description: Y轴目标角度(度) type: number z_deg: + description: Z轴目标角度(度) type: number required: [] type: object @@ -391,15 +422,20 @@ xyz_stepper_controller: properties: acceleration: default: 1000 + description: 加速度 type: integer speed: default: 5000 + description: 移动速度 type: integer x_rev: + description: X轴目标圈数 type: number y_rev: + description: Y轴目标圈数 type: number z_rev: + description: Z轴目标圈数 type: number required: [] type: object @@ -427,6 +463,7 @@ xyz_stepper_controller: goal: properties: revolutions: + description: 圈数 type: number required: - revolutions @@ -456,10 +493,13 @@ xyz_stepper_controller: properties: acceleration: default: 1000 + description: 加速度(rpm/s) type: integer axis: + description: 电机轴 type: object speed: + description: 运行速度(rpm),正值正转,负值反转 type: integer required: - axis @@ -487,6 +527,7 @@ xyz_stepper_controller: goal: properties: steps: + description: 步数 type: integer required: - steps @@ -513,6 +554,7 @@ xyz_stepper_controller: goal: properties: steps: + description: 步数 type: integer required: - steps @@ -564,9 +606,11 @@ xyz_stepper_controller: goal: properties: axis: + description: 电机轴 type: object timeout: default: 30.0 + description: 超时时间(秒) type: number required: - axis @@ -591,11 +635,14 @@ xyz_stepper_controller: properties: baudrate: default: 115200 + description: 波特率 type: integer port: + description: 串口端口名 type: string timeout: default: 1.0 + description: 通信超时时间 type: number required: - port diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index 4d2f7288..c01662c3 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -510,9 +510,11 @@ liquid_handler: goal: properties: msg: + description: information to be printed type: string seconds: default: 0 + description: seconds to wait type: string required: [] type: object @@ -2963,15 +2965,22 @@ liquid_handler: additionalProperties: false properties: channel: + description: int maximum: 2147483647 minimum: -2147483648 type: integer dis_to_top: + description: 'float + + Height in mm to move to relative to the well top.' maximum: 1.7976931348623157e+308 minimum: -1.7976931348623157e+308 type: number well: additionalProperties: false + description: 'Well + + The target well.' properties: category: type: string @@ -4829,11 +4838,13 @@ liquid_handler: config: properties: backend: + description: Backend to use. type: object channel_num: default: 8 type: integer deck: + description: Deck to use. type: object simulator: default: false @@ -4883,14 +4894,17 @@ liquid_handler.biomek: bind_parent_id: type: string liquid_input_slot: + description: 液体输入槽列表 items: type: integer type: array liquid_type: + description: 液体类型列表 items: type: string type: array liquid_volume: + description: 液体体积列表 items: type: integer type: array @@ -4901,6 +4915,7 @@ liquid_handler.biomek: type: object type: array slot_on_deck: + description: 甲板上的槽位 type: integer required: - resource_tracker @@ -5036,20 +5051,27 @@ liquid_handler.biomek: additionalProperties: false properties: none_keys: + description: 需要设置为None的键列表 items: type: string type: array protocol_author: + description: 协议作者 type: string protocol_date: + description: 协议日期 type: string protocol_description: + description: 协议描述 type: string protocol_name: + description: 协议名称 type: string protocol_type: + description: 协议类型 type: string protocol_version: + description: 协议版本 type: string title: LiquidHandlerProtocolCreation_Goal type: object diff --git a/unilabos/registry/devices/neware_battery_test_system.yaml b/unilabos/registry/devices/neware_battery_test_system.yaml index 4f3b972a..63411b53 100644 --- a/unilabos/registry/devices/neware_battery_test_system.yaml +++ b/unilabos/registry/devices/neware_battery_test_system.yaml @@ -87,7 +87,7 @@ neware_battery_test_system: properties: filepath: default: bts_status.json - description: 输出JSON文件路径 + description: 输出文件路径 type: string required: [] type: object @@ -146,7 +146,7 @@ neware_battery_test_system: goal: properties: plate_num: - description: 盘号 (1 或 2),如果为null则返回所有盘的状态 + description: 盘号 (1 或 2),如果为None则返回所有盘的状态 type: integer required: [] type: object @@ -237,11 +237,11 @@ neware_battery_test_system: goal: properties: csv_path: - description: 输入CSV文件的绝对路径 + description: 输入CSV文件路径 type: string output_dir: default: . - description: 输出目录(用于存储XML和备份文件),默认当前目录 + description: 输出目录,用于存储XML文件和备份,默认当前目录 type: string required: - csv_path @@ -302,14 +302,14 @@ neware_battery_test_system: goal: properties: backup_dir: - description: 备份目录路径(默认使用最近一次submit_from_csv的backup_dir) + description: 备份目录路径,默认使用最近一次 submit_from_csv 的 backup_dir type: string file_pattern: default: '*' - description: 文件通配符模式,例如 *.csv 或 Battery_*.nda + description: 文件通配符模式,默认 "*" 上传所有文件(例如 "*.csv" 仅上传 CSV 文件) type: string oss_prefix: - description: OSS对象路径前缀(默认使用self.oss_prefix) + description: OSS 对象前缀,默认使用类初始化时的配置 type: string required: [] type: object @@ -336,19 +336,25 @@ neware_battery_test_system: config: properties: devtype: + description: 设备类型标识 type: string ip: + description: TCP服务器IP地址 type: string machine_id: default: 1 + description: 机器ID type: integer oss_prefix: default: neware_backup + description: OSS对象路径前缀,默认"neware_backup" type: string oss_upload_enabled: default: false + description: 是否启用OSS上传功能,默认False type: boolean port: + description: TCP端口 type: integer size_x: default: 50 @@ -360,6 +366,7 @@ neware_battery_test_system: default: 20 type: number timeout: + description: 通信超时时间(秒) type: integer required: [] type: object diff --git a/unilabos/registry/devices/organic_miscellaneous.yaml b/unilabos/registry/devices/organic_miscellaneous.yaml index c1290bea..dc81b671 100644 --- a/unilabos/registry/devices/organic_miscellaneous.yaml +++ b/unilabos/registry/devices/organic_miscellaneous.yaml @@ -207,8 +207,12 @@ separator.homemade: goal: properties: condition: + description: The condition to be monitored, either 'delta' or 'time'. type: string value: + description: 'The threshold value for the condition. + + `delta > 0.05`, `time > 60`' type: string required: - condition @@ -305,12 +309,17 @@ separator.homemade: event: type: string settling_time: + description: The duration for which to settle after stirring, in + seconds. Defaults to 10. type: string stir_speed: + description: The speed of stirring, in RPM. Defaults to 300. maximum: 1.7976931348623157e+308 minimum: -1.7976931348623157e+308 type: number stir_time: + description: The duration for which to stir, in seconds. Defaults + to 10. maximum: 1.7976931348623157e+308 minimum: -1.7976931348623157e+308 type: number diff --git a/unilabos/registry/devices/pump_and_valve.yaml b/unilabos/registry/devices/pump_and_valve.yaml index 95a082d5..25d647f7 100644 --- a/unilabos/registry/devices/pump_and_valve.yaml +++ b/unilabos/registry/devices/pump_and_valve.yaml @@ -456,6 +456,7 @@ syringe_pump_with_valve.runze.SY03B-T06: goal: properties: volume: + description: 'absolute position of the plunger, unit: mL' type: number required: - volume @@ -481,6 +482,7 @@ syringe_pump_with_valve.runze.SY03B-T06: goal: properties: volume: + description: 'absolute position of the plunger, unit: mL' type: number required: - volume @@ -687,8 +689,10 @@ syringe_pump_with_valve.runze.SY03B-T06: goal: properties: max_velocity: + description: 'maximum velocity of the plunger, unit: ml/s' type: number position: + description: 'absolute position of the plunger, unit: ml' type: number required: - position @@ -1003,6 +1007,7 @@ syringe_pump_with_valve.runze.SY03B-T08: goal: properties: volume: + description: 'absolute position of the plunger, unit: mL' type: number required: - volume @@ -1028,6 +1033,7 @@ syringe_pump_with_valve.runze.SY03B-T08: goal: properties: volume: + description: 'absolute position of the plunger, unit: mL' type: number required: - volume @@ -1234,8 +1240,10 @@ syringe_pump_with_valve.runze.SY03B-T08: goal: properties: max_velocity: + description: 'maximum velocity of the plunger, unit: ml/s' type: number position: + description: 'absolute position of the plunger, unit: ml' type: number required: - position diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml index 1372140d..7ab22df6 100644 --- a/unilabos/registry/devices/reaction_station_bioyond.yaml +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -32,7 +32,7 @@ reaction_station.bioyond: type: integer end_point: default: 0 - description: 终点计时点 (Start=开始前, End=结束后) + description: 终点计时点 (Start=0, End=1) type: integer end_step_key: default: '' @@ -40,11 +40,11 @@ reaction_station.bioyond: type: string start_point: default: 0 - description: 起点计时点 (Start=开始前, End=结束后) + description: 起点计时点 (Start=0, End=1) type: integer start_step_key: default: '' - description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择) + description: 起点步骤Key (可选, 默认为空则自动选择) type: string required: - duration @@ -91,6 +91,7 @@ reaction_station.bioyond: goal: properties: json_str: + description: 订单参数的JSON字符串 type: string required: - json_str @@ -117,6 +118,7 @@ reaction_station.bioyond: goal: properties: workflow_ids: + description: 要删除的工作流ID数组 items: type: string type: array @@ -145,6 +147,7 @@ reaction_station.bioyond: goal: properties: json_str: + description: 'JSON格式的字符串,包含:' type: string required: - json_str @@ -197,6 +200,7 @@ reaction_station.bioyond: goal: properties: web_workflow_json: + description: JSON 格式的网页工作流列表 type: string required: - web_workflow_json @@ -228,8 +232,10 @@ reaction_station.bioyond: goal: properties: reactor_id: + description: 反应器编号 (1-5) type: integer temperature: + description: 目标温度 (°C) type: number required: - reactor_id @@ -257,6 +263,7 @@ reaction_station.bioyond: goal: properties: preintake_id: + description: 通量ID type: string required: - preintake_id @@ -338,6 +345,7 @@ reaction_station.bioyond: goal: properties: value: + description: 工作流 ID 列表 items: type: string type: array @@ -365,6 +373,7 @@ reaction_station.bioyond: goal: properties: workflow_id: + description: 工作流ID type: string required: - workflow_id @@ -424,11 +433,11 @@ reaction_station.bioyond: goal: properties: assign_material_name: - description: 物料名称(不能为空) + description: 物料名称(液体种类) type: string temperature: default: 25.0 - description: 温度设定(°C) + description: 温度(C) type: number time: default: '90' @@ -436,14 +445,14 @@ reaction_station.bioyond: type: string titration_type: default: '1' - description: 是否滴定(NO=否, YES=是) + description: 是否滴定(NO=1, YES=2) type: string torque_variation: default: 2 - description: 是否观察 (NO=否, YES=是) + description: 是否观察(NO=1, YES=2) type: integer volume: - description: 分液公式(mL) + description: 分液量(μL) type: string required: - assign_material_name @@ -525,11 +534,11 @@ reaction_station.bioyond: properties: assign_material_name: default: BAPP - description: 物料名称 + description: 物料名称(试剂瓶位) type: string temperature: default: 25.0 - description: 温度设定(°C) + description: 温度设定(C) type: number time: default: '0' @@ -537,15 +546,15 @@ reaction_station.bioyond: type: string titration_type: default: '1' - description: 是否滴定(NO=否, YES=是) + description: 是否滴定(NO=1, YES=2) type: string torque_variation: default: 1 - description: 是否观察 (NO=否, YES=是) + description: 是否观察(int类型, 1=否, 2=是) type: integer volume: default: '350' - description: 分液公式(mL) + description: 分液质量(g) type: string required: [] type: object @@ -593,26 +602,28 @@ reaction_station.bioyond: description: 物料名称 type: string solvents: - description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume' + description: '溶剂信息的字典或JSON字符串(可选),格式如下: + + {' type: string temperature: default: 25.0 - description: 温度设定(°C),默认25.00 + description: 温度设定(C) type: number time: default: '360' - description: 观察时间(分钟),默认360 + description: 观察时间(分钟) type: string titration_type: default: '1' - description: 是否滴定(NO=否, YES=是),默认NO + description: 是否滴定(NO=1, YES=2) type: string torque_variation: default: 2 - description: 是否观察 (NO=否, YES=是),默认YES + description: 是否观察(NO=1, YES=2) type: integer volume: - description: 分液量(mL)。可直接提供,或通过solvents参数自动计算 + description: 分液量(μL),直接指定体积(可选,如果提供solvents则自动计算) type: string required: - assign_material_name @@ -671,33 +682,32 @@ reaction_station.bioyond: description: 物料名称 type: string extracted_actuals: - description: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh(m二酐滴定)和actualVolume(V二酐滴定) + description: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh和actualVolume type: string feeding_order_data: - description: 'feeding_order JSON对象,用于获取m二酐值(type为main_anhydride的amount)。示例: - {"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}' + description: feeding_order JSON字符串或对象,用于获取m二酐值 type: string temperature: default: 25.0 - description: 温度设定(°C),默认25.00 + description: 温度(C) type: number time: default: '90' - description: 观察时间(分钟),默认90 + description: 观察时间(分钟) type: string titration_type: default: '2' - description: 是否滴定(NO=否, YES=是),默认YES + description: 是否滴定(NO=1, YES=2),默认2 type: string torque_variation: default: 2 - description: 是否观察 (NO=否, YES=是),默认YES + description: 是否观察(NO=1, YES=2) type: integer volume_formula: - description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成 + description: 分液公式(μL),如果提供则直接使用,否则自动计算 type: string x_value: - description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算 + description: 手工输入的x值,格式如 "1-2-3" type: string required: - assign_material_name @@ -738,7 +748,7 @@ reaction_station.bioyond: type: string temperature: default: 25.0 - description: 温度设定(°C) + description: 温度(C) type: number time: default: '0' @@ -746,14 +756,14 @@ reaction_station.bioyond: type: string titration_type: default: '1' - description: 是否滴定(NO=否, YES=是) + description: 是否滴定(NO=1, YES=2) type: string torque_variation: default: 1 - description: 是否观察 (NO=否, YES=是) + description: 是否观察(NO=1, YES=2) type: integer volume_formula: - description: 分液公式(mL) + description: 分液公式(μL) type: string required: - volume_formula @@ -786,7 +796,7 @@ reaction_station.bioyond: description: 任务名称 type: string workflow_name: - description: 工作流名称 + description: 合并后的工作流名称 type: string required: - workflow_name @@ -819,15 +829,15 @@ reaction_station.bioyond: goal: properties: assign_material_name: - description: 物料名称 + description: 物料名称(不能为空) type: string cutoff: default: '900000' - description: 粘度上限 + description: 粘度上限(需为有效数字字符串,默认 "900000") type: string temperature: default: -10.0 - description: 温度设定(°C) + description: 温度设定(C,范围:-50.00 至 100.00) type: number required: - assign_material_name @@ -909,11 +919,11 @@ reaction_station.bioyond: description: 物料名称(用于获取试剂瓶位ID) type: string material_id: - description: 粉末类型ID,Salt=盐(21分钟),Flour=面粉(27分钟),BTDA=BTDA(38分钟) + description: 粉末类型ID, Salt=1, Flour=2, BTDA=3 type: string temperature: default: 25.0 - description: 温度设定(°C) + description: 温度设定(C) type: number time: default: '0' @@ -921,7 +931,7 @@ reaction_station.bioyond: type: string torque_variation: default: 1 - description: 是否观察 (NO=否, YES=是) + description: 是否观察(NO=1, YES=2) type: integer required: - material_id @@ -945,10 +955,13 @@ reaction_station.bioyond: config: properties: config: + description: 配置字典,应包含workflow_mappings等配置 type: object deck: + description: Deck对象 type: string protocol_type: + description: 协议类型(由ROS系统传递,此处忽略) type: string required: [] type: object diff --git a/unilabos/registry/devices/robot_arm.yaml b/unilabos/registry/devices/robot_arm.yaml index d4874677..b96e5341 100644 --- a/unilabos/registry/devices/robot_arm.yaml +++ b/unilabos/registry/devices/robot_arm.yaml @@ -198,6 +198,8 @@ robotic_arm.SCARA_with_slider.moveit.virtual: additionalProperties: false properties: command: + description: A JSON-formatted string that includes option, target, + speed, lift_height, mt_height type: string title: SendCmd_Goal type: object @@ -241,6 +243,8 @@ robotic_arm.SCARA_with_slider.moveit.virtual: additionalProperties: false properties: command: + description: A JSON-formatted string that includes quaternion, speed, + position type: string title: SendCmd_Goal type: object @@ -284,6 +288,7 @@ robotic_arm.SCARA_with_slider.moveit.virtual: additionalProperties: false properties: command: + description: A JSON-formatted string that includes speed type: string title: SendCmd_Goal type: object diff --git a/unilabos/registry/devices/robot_linear_motion.yaml b/unilabos/registry/devices/robot_linear_motion.yaml index 74b01e80..14539321 100644 --- a/unilabos/registry/devices/robot_linear_motion.yaml +++ b/unilabos/registry/devices/robot_linear_motion.yaml @@ -709,6 +709,8 @@ linear_motion.toyo_xyz.sim: additionalProperties: false properties: command: + description: A JSON-formatted string that includes option, target, + speed, lift_height, mt_height type: string title: SendCmd_Goal type: object @@ -752,6 +754,8 @@ linear_motion.toyo_xyz.sim: additionalProperties: false properties: command: + description: A JSON-formatted string that includes quaternion, speed, + position type: string title: SendCmd_Goal type: object @@ -795,6 +799,7 @@ linear_motion.toyo_xyz.sim: additionalProperties: false properties: command: + description: A JSON-formatted string that includes speed type: string title: SendCmd_Goal type: object diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index b828c6d2..a34d6f55 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -2179,6 +2179,7 @@ virtual_multiway_valve: goal: properties: port_number: + description: 端口号 (1-8) type: integer required: - port_number @@ -2225,6 +2226,7 @@ virtual_multiway_valve: goal: properties: port_number: + description: 目标端口号 (1-8) type: integer required: - port_number @@ -2261,6 +2263,7 @@ virtual_multiway_valve: additionalProperties: false properties: command: + description: 目标位置 (0-8) 或位置字符串 type: string title: SendCmd_Goal type: object @@ -2304,6 +2307,7 @@ virtual_multiway_valve: additionalProperties: false properties: command: + description: 目标位置 (0-8) 或位置字符串 type: string title: SendCmd_Goal type: object @@ -4215,6 +4219,7 @@ virtual_solenoid_valve: additionalProperties: false properties: string: + description: '"ON"/"OFF" 或 "OPEN"/"CLOSED"' type: string title: StrSingleInput_Goal type: object @@ -4258,6 +4263,7 @@ virtual_solenoid_valve: additionalProperties: false properties: command: + description: '"OPEN"/"CLOSED" 或其他控制命令' type: string title: SendCmd_Goal type: object @@ -4418,16 +4424,20 @@ virtual_solid_dispenser: event: type: string mass: + description: 质量字符串 (如 "2.9 g") type: string mol: + description: 摩尔数字符串 (如 "0.12 mol") type: string purpose: + description: 添加目的 type: string rate_spec: type: string ratio: type: string reagent: + description: 试剂名称 type: string stir: type: boolean @@ -4439,6 +4449,7 @@ virtual_solid_dispenser: type: string vessel: additionalProperties: false + description: 目标容器 properties: category: type: string @@ -5568,8 +5579,10 @@ virtual_transfer_pump: goal: properties: velocity: + description: 拉取速度 (ml/s) type: number volume: + description: 要拉取的体积 (ml) type: number required: - volume @@ -5596,8 +5609,10 @@ virtual_transfer_pump: goal: properties: velocity: + description: 推出速度 (ml/s) type: number volume: + description: 要推出的体积 (ml) type: number required: - volume @@ -5693,10 +5708,12 @@ virtual_transfer_pump: additionalProperties: false properties: max_velocity: + description: 移动速度 (ml/s) maximum: 1.7976931348623157e+308 minimum: -1.7976931348623157e+308 type: number position: + description: 目标位置 (ml) maximum: 1.7976931348623157e+308 minimum: -1.7976931348623157e+308 type: number @@ -5845,8 +5862,10 @@ virtual_transfer_pump: config: properties: config: + description: 配置字典,包含max_volume, port等参数 type: object device_id: + description: 设备ID type: string required: [] type: object diff --git a/unilabos/registry/devices/xrd_d7mate.yaml b/unilabos/registry/devices/xrd_d7mate.yaml index 2b49ae55..38e31718 100644 --- a/unilabos/registry/devices/xrd_d7mate.yaml +++ b/unilabos/registry/devices/xrd_d7mate.yaml @@ -409,11 +409,11 @@ xrd_d7mate: properties: end_theta: default: 80.0 - description: 结束角度(≥5.5°,且必须大于start_theta) + description: 结束角度(≥5.5°,且必须大于 start_theta) type: number exp_time: default: 0.1 - description: 曝光时间(0.1-5.0秒) + description: 曝光时间(0.1-5.0 秒) type: number increment: default: 0.05 @@ -421,7 +421,7 @@ xrd_d7mate: type: number sample_id: default: '' - description: 样品标识符 + description: 样品名称 type: string start_theta: default: 10.0 @@ -433,7 +433,7 @@ xrd_d7mate: type: string wait_minutes: default: 3.0 - description: 允许上样后等待分钟数 + description: 在允许上样后、发送样品准备完成前的等待分钟数(默认 3 分钟) type: number required: [] title: StartWorkflow_Goal @@ -492,12 +492,15 @@ xrd_d7mate: properties: host: default: 127.0.0.1 + description: 设备IP地址 type: string port: default: 6001 + description: 通信端口,默认6001 type: string timeout: default: 10.0 + description: 超时时间,单位秒 type: string required: [] type: object diff --git a/unilabos/registry/devices/zhida_gcms.yaml b/unilabos/registry/devices/zhida_gcms.yaml index 37adbd79..b10b29ad 100644 --- a/unilabos/registry/devices/zhida_gcms.yaml +++ b/unilabos/registry/devices/zhida_gcms.yaml @@ -217,6 +217,7 @@ zhida_gcms: additionalProperties: false properties: string: + description: Base64编码的CSV数据(ROS2参数名) type: string title: StrSingleInput_Goal type: object @@ -257,6 +258,7 @@ zhida_gcms: additionalProperties: false properties: string: + description: CSV文件路径(ROS2参数名) type: string title: StrSingleInput_Goal type: object @@ -289,12 +291,15 @@ zhida_gcms: properties: host: default: 192.168.3.184 + description: 设备IP地址,本地部署时可使用'127.0.0.1' type: string port: default: 5792 + description: 通信端口,默认5792 type: string timeout: default: 10.0 + description: 超时时间,单位秒 type: string required: [] type: object diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 17306810..75677b4f 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -571,6 +571,7 @@ class Registry: schema: Dict[str, Any], doc_info: Dict[str, Any], field_to_param: Optional[Dict[str, str]] = None, + apply_defaults: bool = False, ) -> None: """Apply parsed docstring display names and descriptions to schema properties.""" if not schema or not doc_info: @@ -589,12 +590,20 @@ class Registry: if not isinstance(param_name, str): continue param_name = param_name.removesuffix("[]") - prop_schema["title"] = param_display_names.get(param_name, prop_schema.get("title") or field_name) - prop_schema["description"] = param_descs.get(param_name, prop_schema.get("description") or "") + if param_name in param_display_names: + prop_schema["title"] = param_display_names[param_name] + elif apply_defaults and not prop_schema.get("title"): + prop_schema["title"] = field_name + + if param_name in param_descs: + prop_schema["description"] = param_descs[param_name] + elif apply_defaults and "description" not in prop_schema: + prop_schema["description"] = "" def _generate_unilab_json_command_schema( self, method_args: list, docstring: Optional[str] = None, import_map: Optional[Dict[str, str]] = None, + apply_doc_defaults: bool = False, ) -> Dict[str, Any]: """根据方法参数和 docstring 生成 UniLabJsonCommand schema""" doc_info = parse_docstring(docstring) @@ -631,7 +640,7 @@ class Registry: if param_required: schema["required"].append(param_name) - self._apply_docstring_param_metadata(schema, doc_info) + self._apply_docstring_param_metadata(schema, doc_info, apply_defaults=apply_doc_defaults) return schema def _generate_status_types_schema(self, status_methods: Dict[str, Any]) -> Dict[str, Any]: @@ -1038,6 +1047,7 @@ class Registry: goal_schema_for_docs, parse_docstring(method_info.get("docstring")), goal, + apply_defaults=True, ) action_value_mappings[action_name] = action_entry @@ -1127,7 +1137,7 @@ class Registry: if prequired: schema["required"].append(pname) - self._apply_docstring_param_metadata(schema, doc_info) + self._apply_docstring_param_metadata(schema, doc_info, apply_defaults=True) return schema def _generate_status_schema_from_ast( @@ -2211,10 +2221,6 @@ class Registry: }, **schema["properties"]["goal"]["properties"], } - for field_name, field_schema in schema["properties"]["goal"]["properties"].items(): - if isinstance(field_schema, dict): - field_schema.setdefault("title", field_name) - field_schema.setdefault("description", "") # 将 placeholder_keys 信息添加到 schema 中 if "placeholder_keys" in action_config and action_config.get("schema", {}).get( "properties", {} diff --git a/unilabos/utils/environment_check.py b/unilabos/utils/environment_check.py index 18b5f158..5dcff22f 100644 --- a/unilabos/utils/environment_check.py +++ b/unilabos/utils/environment_check.py @@ -33,10 +33,83 @@ _USE_UV: Optional[bool] = None def _has_uv() -> bool: global _USE_UV if _USE_UV is None: - _USE_UV = shutil.which("uv") is not None + uv_path = shutil.which("uv") + if not uv_path: + _USE_UV = False + else: + try: + result = subprocess.run([uv_path, "--version"], capture_output=True, text=True, timeout=10) + _USE_UV = result.returncode == 0 + except Exception: + _USE_UV = False return _USE_UV +def _install_command(installer: str, package: str, upgrade: bool, is_chinese: bool) -> List[str]: + if installer == "uv": + # uv >= 0.5 默认要求虚拟环境,对 conda env 会报 "No virtual environment found"。 + # 显式 --python sys.executable 让 uv 把当前解释器(conda/venv/system 都行) + # 视为目标环境,绕开 venv 检测。 + cmd = ["uv", "pip", "install", "--python", sys.executable] + if upgrade: + cmd.append("--upgrade") + cmd.append(package) + if is_chinese: + cmd.extend(["--index-url", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"]) + return cmd + + cmd = [sys.executable, "-m", "pip", "install", "--disable-pip-version-check"] + if upgrade: + cmd.append("--upgrade") + cmd.append(package) + if is_chinese: + cmd.extend(["-i", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"]) + return cmd + + +def _installer_candidates() -> List[str]: + installers: List[str] = [] + if _has_uv(): + installers.append("uv") + installers.append("pip") + return installers + + +def _git_url_from_requirement(requirement: str) -> Optional[str]: + if not requirement.startswith("git+"): + return None + return requirement[4:].split("#", 1)[0] + + +def _repo_dir_name(git_url: str) -> str: + repo_name = git_url.rstrip("/").rsplit("/", 1)[-1] + return repo_name[:-4] if repo_name.endswith(".git") else repo_name + + +def _print_manual_git_install_hint(requirement: str) -> None: + git_url = _git_url_from_requirement(requirement) + if not git_url: + return + + repo_dir = _repo_dir_name(git_url) + install_cmd = ( + f'uv pip install --python "{sys.executable}" -e .' + if _has_uv() + else f"{sys.executable} -m pip install -e ." + ) + if _is_chinese_locale() and not _has_uv(): + install_cmd += " -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" + + print_status("Git 依赖自动安装失败,通常是网络连接被重置或代码托管站点暂时不可达。", "warning") + print_status("可以手动拉取代码后在本地安装:", "warning") + print_status(f" git clone {git_url}", "warning") + print_status(f" cd {repo_dir}", "warning") + print_status(" git pull", "warning") + print_status(f" {install_cmd}", "warning") + print_status(f"如果目录 {repo_dir} 已存在,直接进入该目录执行 git pull 后再安装。", "warning") + print_status("如果 git clone 仍失败,请切换网络/代理,或从浏览器下载源码后进入源码目录执行本地安装命令。", "warning") + + def _install_packages( packages: List[str], upgrade: bool = False, @@ -53,7 +126,7 @@ def _install_packages( return True is_chinese = _is_chinese_locale() - use_uv = _has_uv() + installers = _installer_candidates() failed: List[str] = [] for pkg in packages: @@ -63,35 +136,30 @@ def _install_packages( else: print_status(f"正在{action_word} {pkg}...", "info") - if use_uv: - cmd = ["uv", "pip", "install"] - if upgrade: - cmd.append("--upgrade") - cmd.append(pkg) - if is_chinese: - cmd.extend(["--index-url", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"]) - else: - cmd = [sys.executable, "-m", "pip", "install"] - if upgrade: - cmd.append("--upgrade") - cmd.append(pkg) - if is_chinese: - cmd.extend(["-i", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"]) + pkg_installed = False + last_error = "unknown error" - try: - result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) - if result.returncode == 0: - installer = "uv" if use_uv else "pip" - print_status(f"✓ {pkg} {action_word}成功 (via {installer})", "success") - else: - stderr_short = result.stderr.strip().split("\n")[-1] if result.stderr else "unknown error" - print_status(f"× {pkg} {action_word}失败: {stderr_short}", "error") - failed.append(pkg) - except subprocess.TimeoutExpired: - print_status(f"× {pkg} {action_word}超时 (300s)", "error") - failed.append(pkg) - except Exception as e: - print_status(f"× {pkg} {action_word}异常: {e}", "error") + for installer in installers: + cmd = _install_command(installer, pkg, upgrade, is_chinese) + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + if result.returncode == 0: + print_status(f"✓ {pkg} {action_word}成功 (via {installer})", "success") + pkg_installed = True + break + + last_error = result.stderr.strip().split("\n")[-1] if result.stderr else "unknown error" + print_status(f"× {pkg} {action_word}失败 (via {installer}): {last_error}", "warning") + except subprocess.TimeoutExpired: + last_error = "timeout after 300s" + print_status(f"× {pkg} {action_word}超时 (via {installer}, 300s)", "warning") + except Exception as e: + last_error = str(e) + print_status(f"× {pkg} {action_word}异常 (via {installer}): {e}", "warning") + + if not pkg_installed: + print_status(f"× {pkg} {action_word}失败: {last_error}", "error") + _print_manual_git_install_hint(pkg) failed.append(pkg) if failed: diff --git a/unilabos/utils/import_manager.py b/unilabos/utils/import_manager.py index 7fe2f501..8d0e8bf1 100644 --- a/unilabos/utils/import_manager.py +++ b/unilabos/utils/import_manager.py @@ -206,6 +206,7 @@ class ImportManager: "ast_analysis_success": False, "import_map": {}, "init_params": [], + "init_docstring": None, "status_methods": {}, "action_methods": {}, } @@ -251,6 +252,7 @@ class ImportManager: # 映射到统一字段名(与 registry.py complete_registry 消费端一致) result["init_params"] = body.get("init_params", []) + result["init_docstring"] = body.get("init_docstring") result["status_methods"] = body.get("status_properties", {}) result["action_methods"] = { k: {