Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .cursor/skills/filter-workflow-by-tags/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ POST $BASE/api/v1/lab/workflow/<workflow_uuid>/run
**注意:**

- 该接口会使用 workflow 模板中保存的默认参数直接执行
- 如果 workflow 需要动态参数(如 CSV 路径、样品 UUID),应使用 `POST /lab/notebook` 创建 notebook 并传入 `node_params`
- 返回的 `run_uuid` 可直接传入下方「查询任务状态」接口查询实时进度

### 查询任务状态
Expand Down Expand Up @@ -206,7 +205,6 @@ GET $BASE/api/v1/lab/mcp/task/<task_uuid>
**注意:**

- 此接口的 `task_uuid` **是** `POST /lab/workflow/<uuid>/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` 做摘要

Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/ci-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ jobs:
# Fix Unicode encoding issue on Windows runner (cp1252 -> utf-8)
PYTHONIOENCODING: utf-8
PYTHONUTF8: 1
# 强制 matplotlib 使用无头后端,避免在无显示的 Windows runner 上加载 GUI 后端 DLL 导致原生崩溃
MPLBACKEND: Agg
# 原生崩溃(0xC0000005 访问违例)时打印 Python 调用栈,便于定位是哪个设备模块 import 崩溃
PYTHONFAULTHANDLER: "1"

defaults:
run:
Expand Down Expand Up @@ -50,10 +54,28 @@ jobs:
uv pip install .

- name: Run check mode (AST registry validation)
# check_mode 会真实 import 所有设备类(连带 matplotlib/opencv 等原生库)。
# Windows runner 上偶发原生崩溃(退出码 -1073741819 = 0xC0000005 访问违例),
# 这是环境/DLL 层面的 flaky 崩溃,与注册表内容无关,因此对该步做有限次重试。
# 注意:段错误退出码为负数,必须用 "%ERRORLEVEL% EQU 0" 显式判断,
# 不能用 "if errorlevel 1"(它对负数退出码会误判为成功)。
run: |
call conda activate check-env
echo Running check mode...
set ATTEMPTS=4
set COUNT=0
:retry
set /a COUNT+=1
echo === Check attempt %COUNT% of %ATTEMPTS% ===
python -m unilabos --check_mode --skip_env_check
if %ERRORLEVEL% EQU 0 (
echo Check passed on attempt %COUNT%
exit /b 0
)
echo Attempt %COUNT% failed with exit code %ERRORLEVEL%
if %COUNT% LSS %ATTEMPTS% goto retry
echo All %ATTEMPTS% attempts failed
exit /b 1

- name: Check for uncommitted changes
shell: bash
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/multi-platform-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,19 @@ jobs:
echo "CONDA_NO_PLUGINS=true" >> "$GITHUB_ENV"
echo "PYTHONUTF8=1" >> "$GITHUB_ENV"
echo "PYTHONIOENCODING=utf-8" >> "$GITHUB_ENV"
# anaconda-client 1.13+ 把 `anaconda` 入口迁移到插件化的 anaconda-cli-base,
# 新版顶层移除了 `--version`,且会改变 `anaconda upload` 的命名空间。
# 强制使用独立(standalone)CLI 以恢复完全向后兼容的行为(--version 与 upload 均可用)。
echo "ANACONDA_CLIENT_FORCE_STANDALONE=1" >> "$GITHUB_ENV"

- name: Show environment info
if: steps.should_build.outputs.should_build == 'true'
run: |
conda info
conda list -n build-env | grep -E "(rattler-build|anaconda-client)"
conda run -n build-env rattler-build --version
conda run -n build-env anaconda --version
# 用 binstar_client 模块入口查看版本,无论 anaconda CLI 处于 standalone 还是 plugin 模式都可用
conda run -n build-env python -m binstar_client.scripts.cli --version
echo "Platform: ${{ matrix.platform }}"
echo "OS: ${{ matrix.os }}"

Expand Down
16 changes: 8 additions & 8 deletions docs/developer_guide/examples/workstation_architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@

### 1.1 工作站核心架构

```{mermaid}
```mermaid
graph TB
subgraph "工作站模板组成"
WB[WorkstationBase<br/>工作流状态管理]
Expand Down Expand Up @@ -77,7 +77,7 @@ graph TB

### 1.2 外部系统对接关系

```{mermaid}
```mermaid
graph LR
subgraph "Uni-Lab-OS工作站"
WS[WorkstationBase + ROS2WorkstationNode]
Expand Down Expand Up @@ -118,7 +118,7 @@ graph LR

### 1.3 具体实现示例

```{mermaid}
```mermaid
graph TB
subgraph "工作站基类"
BASE[WorkstationBase<br/>抽象基类]
Expand Down Expand Up @@ -164,7 +164,7 @@ graph TB

## 2. 类关系图

```{mermaid}
```mermaid
classDiagram
class WorkstationBase {
<<abstract>>
Expand Down Expand Up @@ -307,7 +307,7 @@ classDiagram

## 3. 工作站启动时序图

```{mermaid}
```mermaid
sequenceDiagram
participant APP as Application
participant WS as WorkstationBase
Expand Down Expand Up @@ -360,7 +360,7 @@ sequenceDiagram

## 4. 工作流执行时序图(Protocol 模式)

```{mermaid}
```mermaid
sequenceDiagram
participant CLIENT as 客户端
participant ROS as ROS2WorkstationNode
Expand Down Expand Up @@ -412,7 +412,7 @@ sequenceDiagram

## 5. HTTP 报送处理时序图

```{mermaid}
```mermaid
sequenceDiagram
participant EXT as 外部工作站/LIMS
participant HTTP as HTTPService
Expand Down Expand Up @@ -455,7 +455,7 @@ sequenceDiagram

## 6. 错误处理时序图

```{mermaid}
```mermaid
sequenceDiagram
participant DEV as 子设备/外部系统
participant ROS as ROS2WorkstationNode
Expand Down
28 changes: 28 additions & 0 deletions unilabos/app/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""CLI 子命令模块

提供 HTTP 客户端相关的子命令:
- auth: 认证管理(login, logout, whoami)
- auth_resolver: 凭据多源解析(CLI / session / local_config.py)
- config: 配置管理(config show)
- lab: 实验室管理
- material: 物料管理
- workflow: 工作流管理
"""

from .auth import cmd_login, cmd_logout, cmd_whoami
from .auth_resolver import resolve_effective_auth
from .config import cmd_config_show
from .lab import cmd_lab_list
from .material import cmd_material_list
from .workflow import cmd_workflow_upload

__all__ = [
"cmd_login",
"cmd_logout",
"cmd_whoami",
"resolve_effective_auth",
"cmd_config_show",
"cmd_lab_list",
"cmd_material_list",
"cmd_workflow_upload",
]
40 changes: 40 additions & 0 deletions unilabos/app/cli/_client_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""已认证 HTTP 客户端工厂

从 args + SessionManager 构造一个已注入 ak/sk 鉴权的 client.HTTPClient。
凭据按 resolve_effective_auth 的优先级解析。

需要在 SessionManager 上下文管理器内调用。
"""

import sys
from typing import Any, Tuple

from unilabos.client import HTTPClient, HTTPClientConfig, SessionManager, print_error

from .auth_resolver import resolve_effective_auth


def make_authenticated_client(
args: Any, session_manager: SessionManager
) -> Tuple[HTTPClient, str]:
"""构造已认证的 HTTPClient

Returns:
(client, base_url)。如果凭据缺失,打印错误并 sys.exit(1)。
"""
effective = resolve_effective_auth(args, session_manager)
if not effective["ak"] or not effective["sk"]:
print_error(
"未找到 ak/sk。请通过以下方式之一配置:\n"
" 1. unilab login --ak <ak> --sk <sk>\n"
" 2. 命令行传入 --ak <ak> --sk <sk>\n"
" 3. 在 local_config.py 中设置 BasicConfig.ak/sk"
)
sys.exit(1)

import base64
secret = base64.b64encode(f"{effective['ak']}:{effective['sk']}".encode("utf-8")).decode("utf-8")

config = HTTPClientConfig(base_url=effective["base_url"])
client = HTTPClient(config, get_auth_secret=lambda: secret)
return client, effective["base_url"]
95 changes: 95 additions & 0 deletions unilabos/app/cli/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""认证命令模块

基于 ak/sk 的认证:
- login: 保存 ak/sk 到会话文件
- logout: 清除本地 ak/sk
- whoami: 显示当前有效的 ak/sk 来源(CLI / session.json / local_config.py)

由于后端没有 whoami 端点,验证发生在实际 API 调用时(如 lab list)返回 401。
"""

import sys

from unilabos.client import (
SessionManager,
print_error,
print_output,
print_success,
)


class AuthError(Exception):
"""认证错误"""
pass


def cmd_login(args, session_manager: SessionManager):
"""login 命令处理 — 保存 ak/sk 到会话文件

如果同时传了 --addr,会一并持久化 base_url。
"""
try:
with session_manager:
state = session_manager.get_state()

addr = getattr(args, "addr_resolved", None)
if addr:
state.base_url = addr

state.auth.ak = args.ak
state.auth.sk = args.sk

print_success(f"已保存 ak/sk 到 {session_manager.session_file}")
print_output({
"base_url": state.base_url,
"ak_prefix": args.ak[:8] + "..." if len(args.ak) > 8 else args.ak,
})
except Exception as e:
print_error(f"登录失败: {e}")
sys.exit(1)


def cmd_logout(args, session_manager: SessionManager):
"""logout 命令处理 — 清除本地 ak/sk"""
try:
with session_manager:
state = session_manager.get_state()
state.auth.ak = ""
state.auth.sk = ""
state.auth.user_name = ""
print_success("已登出")
except Exception as e:
print_error(f"登出失败: {e}")
sys.exit(1)


def cmd_whoami(args, session_manager: SessionManager):
"""whoami 命令处理 — 显示当前有效的 ak/sk 来源

优先级:CLI 参数 > session.json > local_config.py 中的 BasicConfig.ak/sk
"""
from unilabos.app.cli.auth_resolver import resolve_effective_auth

try:
with session_manager:
effective = resolve_effective_auth(args, session_manager)

if not effective["ak"] or not effective["sk"]:
print_error(
"未找到 ak/sk。请通过以下方式之一配置:\n"
" 1. unilab login --ak <ak> --sk <sk>\n"
" 2. 命令行传入 --ak <ak> --sk <sk>\n"
" 3. 在 local_config.py 中设置 BasicConfig.ak/sk"
)
sys.exit(1)

print_output({
"ak_prefix": effective["ak"][:8] + "..." if len(effective["ak"]) > 8 else effective["ak"],
"ak_source": effective["ak_source"],
"sk_source": effective["sk_source"],
"base_url": effective["base_url"],
"base_url_source": effective["base_url_source"],
})
except Exception as e:
print_error(f"操作失败: {e}")
sys.exit(1)
Loading