
Workflow
- 405 installs
- 534 repo stars
- Updated June 29, 2026
- zhinkgit/embeddedskills
Orchestrate embedded firmware build, flash, debug, and observe across Keil, GCC, EIDE, J-Link, OpenOCD, and probe-rs from one workspace-aware workflow.
About
Workflow is a thin orchestration skill for embedded workspaces in the embeddedskills ecosystem. It does not replace Keil, GCC, EIDE, J-Link, OpenOCD, or probe-rs; it discovers which projects exist, picks backends for build, flash, debug, and observe, threads state through `.embeddedskills/state.json`, and aggregates script output. Preferred backends (auto or explicit) live under the workflow section of `.embeddedskills/config.json` alongside keil, gcc, eide, jlink, openocd, and probe-rs blocks. Operators run Python scripts such as workflow_plan and workflow_run with JSON flags for plan-only runs, full build-flash-debug chains, observe mode, and diagnose. It suits indie hardware and firmware builders who juggle multiple IDEs and probes and want one agent-guided path instead of re-documenting commands every session.
- Thin orchestration: discovers Keil / GCC / EIDE projects and selects build, flash, debug, observe backends
- CLI entrypoints: workflow_plan, workflow_run for plan, build, build-flash, build-debug, observe, diagnose
- Observe candidates: jlink:rtt, jlink:swo, openocd:semihosting, openocd:itm, probe-rs:rtt
- Unified config from `.embeddedskills/config.json` plus `.embeddedskills/state.json`—no duplicate workflow config schema
- CLI overrides beat config: e.g. `--build-backend=keil` wins over preferred_build auto
Workflow by the numbers
- 405 all-time installs (skills.sh)
- +23 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #297 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/zhinkgit/embeddedskills --skill workflowAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 405 |
|---|---|
| repo stars | ★ 534 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 29, 2026 |
| Repository | zhinkgit/embeddedskills ↗ |
What it does
Orchestrate embedded firmware build, flash, debug, and observe across Keil, GCC, EIDE, J-Link, OpenOCD, and probe-rs from one workspace-aware workflow.
Files
Workflow 编排层
本 skill 不重复实现底层逻辑,只做发现、选择、串联和聚合。
支持 Keil / GCC / EIDE 三种构建后端,以及 jlink / openocd / probe-rs 三种 flash/debug/observe 后端。
observe 阶段当前会给出 jlink:rtt、jlink:swo、openocd:semihosting、openocd:itm、probe-rs:rtt 这几类候选观测后端。
命令
python <skill-dir>/scripts/workflow_plan.py --json
python <skill-dir>/scripts/workflow_run.py plan --json
python <skill-dir>/scripts/workflow_run.py build --json
python <skill-dir>/scripts/workflow_run.py build-flash --json
python <skill-dir>/scripts/workflow_run.py build-debug --json
python <skill-dir>/scripts/workflow_run.py observe --json
python <skill-dir>/scripts/workflow_run.py diagnose --json配置说明
workflow 不再维护独立的工程配置结构,所有工程参数统一从 .embeddedskills/config.json 读取。
配置结构
.embeddedskills/config.json 中的 workflow 段仅包含首选后端配置:
{
"workflow": {
"preferred_build": "auto",
"preferred_flash": "auto",
"preferred_debug": "auto",
"preferred_observe": "auto"
}
}workflow 通过读取 .embeddedskills/config.json 中其他 skill 的配置段来获取工程参数(如 keil.project、eide.project、eide.config、jlink.device、probe-rs.chip 等)。
参数解析顺序
按以下决策树依次判断,命中即停止:
1. CLI 参数(优先级最高)
- 条件:用户在命令行传入
--build-backend、--flash-backend等参数 - 示例:
workflow_run.py build-flash --build-backend=keil --flash-backend=jlink --build-backend可选值:auto/keil/gcc/eide- 行为:直接使用该参数指定的后端,跳过后续步骤
2. 配置文件(次优先)
- 条件:CLI 未指定,且
.embeddedskills/config.json的workflow段中对应preferred_*字段不为"auto" - 示例:
"preferred_build": "keil"→ 使用 keil 作为构建后端 - 行为:读取配置值并使用,跳过自动发现
3. 自动发现(兜底)
- 条件:CLI 未指定,且配置中
preferred_*为"auto"或字段缺失 - 示例:
"preferred_flash": "auto"→ 扫描 workspace 自动推断可用 flash 后端 - 行为:枚举候选后端列表;若唯一则直接使用,若多个则返回列表请用户确认
成功执行后,实际使用的后端会自动写回 .embeddedskills/config.json 的 workflow 段。
规则
- 发现多个工程或多个候选后端时,只返回候选列表,不自动猜测
- 构建、烧录、调试、观测之间优先通过
.embeddedskills/state.json串联 observe只生成推荐命令,不在 workflow 内直接长时间占用观测通道- 失败时优先返回哪个阶段失败,以及底层脚本的结构化错误
- workflow 与其他 Skill 的协同只通过
.embeddedskills/config.json、.embeddedskills/state.json和子进程调用底层 Skill
{
"preferred_build": "auto",
"preferred_flash": "auto",
"preferred_debug": "auto",
"preferred_observe": "auto"
}
Workflow
workflow 是一个薄编排层,只负责:
- 发现当前 workspace 中的 Keil / GCC / EIDE 工程
- 选择构建、烧录、调试、观测后端
- 串联
.embeddedskills/state.json - 聚合底层脚本输出
当前 observe 阶段会返回 jlink:rtt、jlink:swo、openocd:semihosting、openocd:itm、probe-rs:rtt 这几类候选观测后端。
它不会重写底层构建器、烧录器或 GDB 解析逻辑。
命令
python workflow/scripts/workflow_plan.py --json
python workflow/scripts/workflow_run.py plan --json
python workflow/scripts/workflow_run.py build --json
python workflow/scripts/workflow_run.py build-flash --json
python workflow/scripts/workflow_run.py build-debug --json
python workflow/scripts/workflow_run.py observe --json
python workflow/scripts/workflow_run.py diagnose --json配置
workflow 不再维护独立的工程配置结构,所有工程参数统一从 .embeddedskills/config.json 读取。
工程级共享配置
在 .embeddedskills/config.json 中,workflow 段仅包含首选后端配置:
{
"workflow": {
"preferred_build": "auto",
"preferred_flash": "auto",
"preferred_debug": "auto",
"preferred_observe": "auto"
},
"keil": { "project": "...", "target": "..." },
"gcc": { "project": "...", "preset": "..." },
"eide": { "project": "...", "config": "..." },
"jlink": { "device": "...", "interface": "SWD" },
"openocd": { "board": "...", "interface": "..." },
"probe-rs": { "chip": "...", "protocol": "swd" }
}参数解析顺序
1. CLI 参数(如 --build-backend=keil)优先级最高 2. `.embeddedskills/config.json` 中的 workflow 段配置 3. 自动发现(当 preferred_* 为 "auto" 时)
成功执行后,实际使用的后端会自动写回 .embeddedskills/config.json 的 workflow 段,供下次使用。
与其他 Skill 的协同
workflow 与其他 Skill 的协同只通过以下方式:
.embeddedskills/config.json:读取各 Skill 的工程配置.embeddedskills/state.json:读取/写入运行状态- 子进程调用底层 Skill 脚本
"""workflow 规划:发现工程、候选后端和状态。"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from workflow_runtime import ( # noqa: E402
get_state_entry,
load_effective_project_config,
load_workspace_state,
make_result,
make_timing,
now_iso,
output_json,
parameter_context,
workspace_root,
)
def discover_projects(root: Path) -> dict:
keil_projects = sorted(str(path.resolve()) for path in root.rglob("*.uvprojx"))
gcc_projects = sorted(str(path.parent.resolve()) for path in root.rglob("CMakePresets.json"))
eide_projects = sorted(str(path.parents[1].resolve()) for path in root.rglob(".eide/eide.yml"))
return {
"keil_projects": keil_projects,
"gcc_projects": gcc_projects,
"eide_projects": eide_projects,
}
def main() -> None:
parser = argparse.ArgumentParser(description="workflow plan")
parser.add_argument("--workspace", default=None, help="workspace 根目录,默认当前目录")
parser.add_argument("--config", default=None, help="workflow config.json 路径(已废弃,仅保留兼容性)")
parser.add_argument("--json", action="store_true", dest="as_json")
args = parser.parse_args()
started_at = now_iso()
started_ts = __import__("time").time()
workspace = workspace_root(args.workspace)
try:
full_config, config_path = load_effective_project_config(str(workspace), args.config)
except (FileNotFoundError, ValueError, json.JSONDecodeError) as exc:
result = make_result(
status="error",
action="plan",
summary=str(exc),
context=parameter_context(provider="workflow", workspace=str(workspace)),
error={"code": "invalid_config", "message": str(exc)},
timing=make_timing(started_at, (__import__("time").time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {exc}", file=sys.stderr)
sys.exit(1)
workflow_config = full_config.get("workflow", {})
state = load_workspace_state(str(workspace))
discovery = discover_projects(workspace)
build_candidates = []
if discovery["keil_projects"]:
build_candidates.append("keil")
if discovery["gcc_projects"]:
build_candidates.append("gcc")
if discovery["eide_projects"]:
build_candidates.append("eide")
# 从配置中读取 preferred 设置
preferred = {
"build": workflow_config.get("preferred_build", "auto"),
"flash": workflow_config.get("preferred_flash", "auto"),
"debug": workflow_config.get("preferred_debug", "auto"),
"observe": workflow_config.get("preferred_observe", "auto"),
}
result = make_result(
status="ok",
action="plan",
summary="workflow 规划已生成",
details={
"workspace": str(workspace),
"build_candidates": build_candidates,
"flash_candidates": ["jlink", "openocd", "probe-rs"],
"debug_candidates": ["jlink", "openocd", "probe-rs"],
"observe_candidates": ["jlink:rtt", "openocd:semihosting", "jlink:swo", "openocd:itm", "probe-rs:rtt"],
"preferred": preferred,
},
context=parameter_context(provider="workflow", workspace=str(workspace), config_path=config_path),
metrics={
"keil_projects": len(discovery["keil_projects"]),
"gcc_projects": len(discovery["gcc_projects"]),
"eide_projects": len(discovery["eide_projects"]),
},
state={
"last_build": get_state_entry(state, "last_build"),
"last_flash": get_state_entry(state, "last_flash"),
"last_debug": get_state_entry(state, "last_debug"),
"last_observe": get_state_entry(state, "last_observe"),
},
next_actions=["若存在多个候选工程,workflow run 会返回候选列表而不会自动猜测"],
timing=make_timing(started_at, (__import__("time").time() - started_ts) * 1000),
)
result["details"].update(discovery)
if args.as_json:
output_json(result)
else:
print(result["summary"])
print(f"workspace: {workspace}")
print(f"keil: {len(discovery['keil_projects'])}, gcc: {len(discovery['gcc_projects'])}, eide: {len(discovery['eide_projects'])}")
print(f"preferred: build={preferred['build']}, flash={preferred['flash']}, debug={preferred['debug']}, observe={preferred['observe']}")
if __name__ == "__main__":
main()
"""workflow 薄编排执行层。"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import time
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from workflow_runtime import ( # noqa: E402
get_state_entry,
hidden_subprocess_kwargs,
load_effective_project_config,
load_workspace_state,
make_result,
make_timing,
now_iso,
output_json,
parameter_context,
save_project_config,
update_state_entry,
workspace_root,
)
PYTHON_EXE = sys.executable
def _with_backend(result: dict, backend: str) -> dict:
details = dict(result.get("details") or {})
details["backend"] = backend
result["details"] = details
return result
def _workflow_state_key(action: str) -> str:
return f"last_workflow_{action.replace('-', '_')}"
def _workflow_state_details(action: str, result: dict) -> dict:
details = result.get("details") or {}
if action in ("build", "observe"):
return {"backend": details.get("backend"), "summary": result.get("summary", "")}
if action == "build-flash":
build = details.get("build") or {}
flash = details.get("flash") or {}
return {
"summary": result.get("summary", ""),
"build_backend": (build.get("details") or {}).get("backend"),
"flash_backend": (flash.get("details") or {}).get("backend"),
}
if action == "build-debug":
build = details.get("build") or {}
debug = details.get("debug") or {}
return {
"summary": result.get("summary", ""),
"build_backend": (build.get("details") or {}).get("backend"),
"debug_backend": (debug.get("details") or {}).get("backend"),
}
return {"summary": result.get("summary", "")}
def discover_projects(root: Path) -> dict:
return {
"keil": sorted(str(path.resolve()) for path in root.rglob("*.uvprojx")),
"gcc": sorted(str(path.parent.resolve()) for path in root.rglob("CMakePresets.json")),
"eide": sorted(str(path.parents[1].resolve()) for path in root.rglob(".eide/eide.yml")),
}
def _single_or_error(items: list[str], label: str) -> tuple[str | None, dict | None]:
if len(items) == 1:
return items[0], None
if len(items) > 1:
return None, {"code": "multiple_candidates", "message": f"发现多个{label},请在配置或命令中显式指定", "candidates": items}
return None, {"code": "not_found", "message": f"未发现可用的{label}", "candidates": []}
def _is_openocd_ready(full_config: dict) -> bool:
openocd_cfg = full_config.get("openocd", {})
return bool(openocd_cfg.get("board") or (openocd_cfg.get("interface") and openocd_cfg.get("target")))
def _is_jlink_ready(full_config: dict) -> bool:
return bool((full_config.get("jlink") or {}).get("device"))
def _is_probe_rs_ready(full_config: dict) -> bool:
return bool((full_config.get("probe-rs") or {}).get("chip"))
def _select_backend(explicit: str | None, preferred: str | None, ready_backends: list[str], action: str) -> tuple[str | None, dict | None]:
backend = explicit or preferred or "auto"
if backend != "auto":
return backend, None
if len(ready_backends) == 1:
return ready_backends[0], None
if len(ready_backends) > 1:
return None, {
"code": "multiple_backend_candidates",
"message": f"{action} 存在多个可用后端,请通过 CLI 或 workflow.preferred_* 显式指定",
"candidates": ready_backends,
}
return None, {
"code": "no_backend_available",
"message": f"未找到可用的 {action} 后端,请补充 jlink/openocd/probe-rs 配置",
"candidates": [],
}
def run_json(cmd: list[str], workdir: Path) -> dict:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=str(workdir),
encoding="utf-8",
errors="replace",
**hidden_subprocess_kwargs(),
)
payload = (proc.stdout or proc.stderr).strip()
try:
return json.loads(payload)
except json.JSONDecodeError:
return {
"status": "error",
"action": "subprocess",
"error": {"code": "invalid_json", "message": payload[-500:] or "子进程未返回 JSON"},
}
def select_build_backend(workflow_config: dict, discovery: dict, explicit: str | None) -> tuple[str | None, dict | None]:
backend = explicit or workflow_config.get("preferred_build") or "auto"
if backend != "auto":
return backend, None
candidates = [name for name in ("keil", "gcc", "eide") if discovery[name]]
if len(candidates) == 1:
return candidates[0], None
if len(candidates) > 1:
return None, {"code": "multiple_build_backends", "message": "同时发现多个构建后端(Keil/GCC/EIDE),请显式指定 build backend", "candidates": candidates}
return None, {"code": "no_build_backend", "message": "未发现可构建工程", "candidates": []}
def build_eide_project(workspace: Path, full_config: dict, discovery: dict) -> dict:
eide_config = full_config.get("eide", {})
project = eide_config.get("project")
if not project:
project, error = _single_or_error(discovery["eide"], "EIDE 工程")
if error:
return {"status": "error", "action": "build", "error": error}
config_name = eide_config.get("config")
if not config_name:
return {"status": "error", "action": "build", "error": {"code": "missing_config", "message": "需要在 .embeddedskills/config.json 的 eide 段配置 config(构建配置名称)"}}
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "eide" / "scripts" / "eide_build.py"),
"build",
"--workspace",
str(workspace),
"--project",
project,
"--config",
config_name,
"--json",
]
log_dir = eide_config.get("log_dir")
if log_dir:
cmd.extend(["--log-dir", log_dir])
return _with_backend(run_json(cmd, workspace), "eide")
def build_project(workspace: Path, full_config: dict, discovery: dict, backend: str | None) -> dict:
workflow_config = full_config.get("workflow", {})
selected, error = select_build_backend(workflow_config, discovery, backend)
if error:
return {"status": "error", "action": "build", "error": error}
if selected == "keil":
keil_config = full_config.get("keil", {})
project = keil_config.get("project")
if not project:
project, error = _single_or_error(discovery["keil"], "Keil 工程")
if error:
return {"status": "error", "action": "build", "error": error}
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "keil" / "scripts" / "keil_build.py"),
"build",
"--workspace",
str(workspace),
"--project",
project,
"--json",
]
target = keil_config.get("target")
uv4_exe = keil_config.get("uv4_exe")
if target:
cmd.extend(["--target", target])
if uv4_exe:
cmd.extend(["--uv4", uv4_exe])
return _with_backend(run_json(cmd, workspace), selected)
if selected == "eide":
return build_eide_project(workspace, full_config, discovery)
gcc_config = full_config.get("gcc", {})
project = gcc_config.get("project")
if not project:
project, error = _single_or_error(discovery["gcc"], "GCC 工程")
if error:
return {"status": "error", "action": "build", "error": error}
preset = gcc_config.get("preset")
if not preset:
return {"status": "error", "action": "build", "error": {"code": "missing_preset", "message": "需要在 .embeddedskills/config.json 的 gcc 段配置 preset"}}
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "gcc" / "scripts" / "gcc_build.py"),
"build",
"--workspace",
str(workspace),
"--project",
project,
"--preset",
preset,
"--json",
]
cmake_exe = gcc_config.get("cmake_exe")
if cmake_exe:
cmd.extend(["--cmake", cmake_exe])
return _with_backend(run_json(cmd, workspace), selected)
def flash_project(workspace: Path, full_config: dict, state: dict, explicit: str | None) -> dict:
workflow_config = full_config.get("workflow", {})
selected, error = _select_backend(
explicit,
workflow_config.get("preferred_flash"),
[name for name, ready in (("openocd", _is_openocd_ready(full_config)), ("jlink", _is_jlink_ready(full_config)), ("probe-rs", _is_probe_rs_ready(full_config))) if ready],
"flash",
)
if error:
return {"status": "error", "action": "flash", "error": error}
last_build = get_state_entry(state, "last_build")
artifacts = last_build.get("artifacts", {})
flash_file = last_build.get("flash_file") or artifacts.get("flash_file")
if not flash_file:
return {"status": "error", "action": "flash", "error": {"code": "missing_last_build", "message": "未找到 last_build.flash_file,请先执行 workflow build"}}
if selected == "openocd":
openocd_cfg = full_config.get("openocd", {})
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "openocd" / "scripts" / "openocd_run.py"),
"flash",
"--workspace",
str(workspace),
"--file",
flash_file,
"--json",
]
if openocd_cfg.get("board"):
cmd.extend(["--board", openocd_cfg["board"]])
if openocd_cfg.get("interface"):
cmd.extend(["--interface", openocd_cfg["interface"]])
if openocd_cfg.get("target"):
cmd.extend(["--target", openocd_cfg["target"]])
return _with_backend(run_json(cmd, workspace), "openocd")
if selected == "jlink":
jlink_cfg = full_config.get("jlink", {})
if not jlink_cfg.get("device"):
return {"status": "error", "action": "flash", "error": {"code": "missing_device", "message": "使用 jlink flash 时需要在 .embeddedskills/config.json 的 jlink 段提供 device"}}
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "jlink" / "scripts" / "jlink_exec.py"),
"flash",
"--file",
flash_file,
"--device",
jlink_cfg["device"],
"--json",
]
if jlink_cfg.get("interface"):
cmd.extend(["--interface", jlink_cfg["interface"]])
if jlink_cfg.get("speed"):
cmd.extend(["--speed", str(jlink_cfg["speed"])])
return _with_backend(run_json(cmd, workspace), "jlink")
probe_rs_cfg = full_config.get("probe-rs", {})
if not probe_rs_cfg.get("chip"):
return {"status": "error", "action": "flash", "error": {"code": "missing_chip", "message": "使用 probe-rs flash 时需要在 .embeddedskills/config.json 的 probe-rs 段提供 chip"}}
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "probe-rs" / "scripts" / "probe_rs_exec.py"),
"flash",
"--workspace",
str(workspace),
"--file",
flash_file,
"--chip",
probe_rs_cfg["chip"],
"--json",
]
if probe_rs_cfg.get("protocol"):
cmd.extend(["--protocol", probe_rs_cfg["protocol"]])
if probe_rs_cfg.get("probe"):
cmd.extend(["--probe", probe_rs_cfg["probe"]])
if probe_rs_cfg.get("speed"):
cmd.extend(["--speed", str(probe_rs_cfg["speed"])])
if probe_rs_cfg.get("connect_under_reset"):
cmd.append("--connect-under-reset")
return _with_backend(run_json(cmd, workspace), "probe-rs")
def debug_project(workspace: Path, full_config: dict, state: dict, explicit: str | None) -> dict:
workflow_config = full_config.get("workflow", {})
selected, error = _select_backend(
explicit,
workflow_config.get("preferred_debug"),
[name for name, ready in (("openocd", _is_openocd_ready(full_config)), ("jlink", _is_jlink_ready(full_config)), ("probe-rs", _is_probe_rs_ready(full_config))) if ready],
"debug",
)
if error:
return {"status": "error", "action": "build-debug", "error": error}
last_build = get_state_entry(state, "last_build")
artifacts = last_build.get("artifacts", {})
debug_file = last_build.get("debug_file") or artifacts.get("debug_file")
if not debug_file:
return {"status": "error", "action": "build-debug", "error": {"code": "missing_last_build", "message": "未找到 last_build.debug_file,请先执行 workflow build"}}
if selected == "openocd":
openocd_cfg = full_config.get("openocd", {})
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "openocd" / "scripts" / "openocd_gdb.py"),
"crash-report",
"--workspace",
str(workspace),
"--elf",
debug_file,
"--json",
]
if openocd_cfg.get("board"):
cmd.extend(["--board", openocd_cfg["board"]])
if openocd_cfg.get("interface"):
cmd.extend(["--interface", openocd_cfg["interface"]])
if openocd_cfg.get("target"):
cmd.extend(["--target", openocd_cfg["target"]])
if openocd_cfg.get("gdb_exe"):
cmd.extend(["--gdb-exe", openocd_cfg["gdb_exe"]])
return _with_backend(run_json(cmd, workspace), "openocd")
if selected == "jlink":
jlink_cfg = full_config.get("jlink", {})
if not jlink_cfg.get("device"):
return {"status": "error", "action": "build-debug", "error": {"code": "missing_device", "message": "使用 jlink gdb 时需要在 .embeddedskills/config.json 的 jlink 段提供 device"}}
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "jlink" / "scripts" / "jlink_gdb.py"),
"crash-report",
"--workspace",
str(workspace),
"--elf",
debug_file,
"--device",
jlink_cfg["device"],
"--json",
]
if jlink_cfg.get("interface"):
cmd.extend(["--interface", jlink_cfg["interface"]])
if jlink_cfg.get("speed"):
cmd.extend(["--speed", str(jlink_cfg["speed"])])
return _with_backend(run_json(cmd, workspace), "jlink")
probe_rs_cfg = full_config.get("probe-rs", {})
if not probe_rs_cfg.get("chip"):
return {"status": "error", "action": "build-debug", "error": {"code": "missing_chip", "message": "使用 probe-rs gdb 时需要在 .embeddedskills/config.json 的 probe-rs 段提供 chip"}}
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "probe-rs" / "scripts" / "probe_rs_gdb.py"),
"crash-report",
"--workspace",
str(workspace),
"--elf",
debug_file,
"--chip",
probe_rs_cfg["chip"],
"--json",
]
if probe_rs_cfg.get("protocol"):
cmd.extend(["--protocol", probe_rs_cfg["protocol"]])
if probe_rs_cfg.get("probe"):
cmd.extend(["--probe", probe_rs_cfg["probe"]])
if probe_rs_cfg.get("speed"):
cmd.extend(["--speed", str(probe_rs_cfg["speed"])])
if probe_rs_cfg.get("connect_under_reset"):
cmd.append("--connect-under-reset")
return _with_backend(run_json(cmd, workspace), "probe-rs")
def observe_project(workspace: Path, full_config: dict, explicit: str | None) -> dict:
workflow_config = full_config.get("workflow", {})
selected, error = _select_backend(
explicit,
workflow_config.get("preferred_observe"),
[name for name, ready in (("openocd", _is_openocd_ready(full_config)), ("jlink", _is_jlink_ready(full_config)), ("probe-rs", _is_probe_rs_ready(full_config))) if ready],
"observe",
)
if error:
return {"status": "error", "action": "observe", "error": error}
if selected == "openocd":
openocd_cfg = full_config.get("openocd", {})
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "openocd" / "scripts" / "openocd_semihosting.py"),
"--workspace",
str(workspace),
"--json",
]
if openocd_cfg.get("board"):
cmd.extend(["--board", openocd_cfg["board"]])
if openocd_cfg.get("interface"):
cmd.extend(["--interface", openocd_cfg["interface"]])
if openocd_cfg.get("target"):
cmd.extend(["--target", openocd_cfg["target"]])
return {"status": "ok", "action": "observe", "summary": "已生成 openocd semihosting 观察命令", "details": {"command": cmd, "backend": "openocd"}}
if selected == "jlink":
jlink_cfg = full_config.get("jlink", {})
if not jlink_cfg.get("device"):
return {"status": "error", "action": "observe", "error": {"code": "missing_device", "message": "使用 jlink 观测时需要在 .embeddedskills/config.json 的 jlink 段提供 device"}}
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "jlink" / "scripts" / "jlink_rtt.py"),
"--workspace",
str(workspace),
"--device",
jlink_cfg["device"],
"--json",
]
return {"status": "ok", "action": "observe", "summary": "已生成 jlink RTT 观察命令", "details": {"command": cmd, "backend": "jlink"}}
probe_rs_cfg = full_config.get("probe-rs", {})
if not probe_rs_cfg.get("chip"):
return {"status": "error", "action": "observe", "error": {"code": "missing_chip", "message": "使用 probe-rs 观测时需要在 .embeddedskills/config.json 的 probe-rs 段提供 chip"}}
cmd = [
PYTHON_EXE,
str(ROOT_DIR / "probe-rs" / "scripts" / "probe_rs_rtt.py"),
"--workspace",
str(workspace),
"--chip",
probe_rs_cfg["chip"],
"--json",
]
if probe_rs_cfg.get("protocol"):
cmd.extend(["--protocol", probe_rs_cfg["protocol"]])
if probe_rs_cfg.get("probe"):
cmd.extend(["--probe", probe_rs_cfg["probe"]])
if probe_rs_cfg.get("speed"):
cmd.extend(["--speed", str(probe_rs_cfg["speed"])])
if probe_rs_cfg.get("connect_under_reset"):
cmd.append("--connect-under-reset")
return {"status": "ok", "action": "observe", "summary": "已生成 probe-rs RTT 观察命令", "details": {"command": cmd, "backend": "probe-rs"}}
def diagnose(workspace: Path, full_config: dict, discovery: dict, state: dict) -> dict:
workflow_config = full_config.get("workflow", {})
hints = []
if not discovery["keil"] and not discovery["gcc"] and not discovery["eide"]:
hints.append("当前 workspace 未发现 Keil/GCC/EIDE 工程")
if not get_state_entry(state, "last_build"):
hints.append("尚未生成 last_build,后续 flash/debug 无法自动串联")
if workflow_config.get("preferred_build") == "auto" and sum(1 for k in ("keil", "gcc", "eide") if discovery[k]) > 1:
hints.append("同时存在多个构建后端(Keil/GCC/EIDE),建议在 .embeddedskills/config.json 的 workflow 段固定 preferred_build")
return {
"status": "ok",
"action": "diagnose",
"summary": "workflow 诊断完成",
"details": {
"workspace": str(workspace),
"discovery": discovery,
"state": {
"last_build": get_state_entry(state, "last_build"),
"last_flash": get_state_entry(state, "last_flash"),
"last_debug": get_state_entry(state, "last_debug"),
"last_observe": get_state_entry(state, "last_observe"),
},
"hints": hints,
},
}
def main() -> None:
parser = argparse.ArgumentParser(description="workflow run")
parser.add_argument("action", choices=["plan", "build", "build-flash", "build-debug", "observe", "diagnose"])
parser.add_argument("--workspace", default=None, help="workspace 根目录,默认当前目录")
parser.add_argument("--config", default=None, help="workflow config.json 路径(已废弃,仅保留兼容性)")
parser.add_argument("--build-backend", choices=["auto", "keil", "gcc", "eide"], default=None)
parser.add_argument("--flash-backend", choices=["auto", "jlink", "openocd", "probe-rs"], default=None)
parser.add_argument("--debug-backend", choices=["auto", "jlink", "openocd", "probe-rs"], default=None)
parser.add_argument("--observe-backend", choices=["auto", "jlink", "openocd", "probe-rs"], default=None)
parser.add_argument("--json", action="store_true", dest="as_json")
args = parser.parse_args()
started_at = now_iso()
started_ts = time.time()
workspace = workspace_root(args.workspace)
try:
full_config, config_path = load_effective_project_config(str(workspace), args.config)
except (FileNotFoundError, ValueError, json.JSONDecodeError) as exc:
wrapped = make_result(
status="error",
action=args.action,
summary=str(exc),
context=parameter_context(provider="workflow", workspace=str(workspace)),
error={"code": "invalid_config", "message": str(exc)},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(wrapped)
else:
print(f"[workflow {args.action}] {wrapped['summary']}")
sys.exit(1)
workflow_config = full_config.get("workflow", {})
state = load_workspace_state(str(workspace))
discovery = discover_projects(workspace)
# 用于追踪实际使用的后端,成功后将写回配置
used_backends = {}
if args.action == "plan":
cmd = [PYTHON_EXE, str(ROOT_DIR / "workflow" / "scripts" / "workflow_plan.py"), "--workspace", str(workspace), "--json"]
if config_path:
cmd.extend(["--config", config_path])
result = run_json(cmd, workspace)
elif args.action == "build":
result = build_project(workspace, full_config, discovery, args.build_backend)
if result.get("status") == "ok" and result.get("details", {}).get("backend"):
used_backends["preferred_build"] = result["details"]["backend"]
elif args.action == "build-flash":
build_result = build_project(workspace, full_config, discovery, args.build_backend)
if build_result.get("status") == "error":
result = build_result
else:
if build_result.get("details", {}).get("backend"):
used_backends["preferred_build"] = build_result["details"]["backend"]
state = load_workspace_state(str(workspace))
flash_result = flash_project(workspace, full_config, state, args.flash_backend)
if flash_result.get("status") == "ok" and flash_result.get("details", {}).get("backend"):
used_backends["preferred_flash"] = flash_result["details"]["backend"]
result = {
"status": flash_result.get("status", "error"),
"action": "build-flash",
"summary": "build-flash 完成" if flash_result.get("status") == "ok" else flash_result.get("error", {}).get("message", "build-flash 失败"),
"details": {"build": build_result, "flash": flash_result},
}
elif args.action == "build-debug":
build_result = build_project(workspace, full_config, discovery, args.build_backend)
if build_result.get("status") == "error":
result = build_result
else:
if build_result.get("details", {}).get("backend"):
used_backends["preferred_build"] = build_result["details"]["backend"]
state = load_workspace_state(str(workspace))
debug_result = debug_project(workspace, full_config, state, args.debug_backend)
if debug_result.get("status") == "ok" and debug_result.get("details", {}).get("backend"):
used_backends["preferred_debug"] = debug_result["details"]["backend"]
result = {
"status": debug_result.get("status", "error"),
"action": "build-debug",
"summary": "build-debug 完成" if debug_result.get("status") == "ok" else debug_result.get("error", {}).get("message", "build-debug 失败"),
"details": {"build": build_result, "debug": debug_result},
}
elif args.action == "observe":
result = observe_project(workspace, full_config, args.observe_backend)
if result.get("status") == "ok" and result.get("details", {}).get("backend"):
used_backends["preferred_observe"] = result["details"]["backend"]
else:
result = diagnose(workspace, full_config, discovery, state)
# 将确认过的 preferred 值写回 .embeddedskills/config.json
if used_backends:
save_project_config(str(workspace), used_backends)
# 更新 workflow 自己的运行状态到 state.json,避免覆盖底层 skill 的 last_build/last_flash/last_debug/last_observe
if result.get("status") == "ok" and args.action in ("build", "build-flash", "build-debug", "observe"):
state_record = {
"action": args.action,
"timestamp": now_iso(),
}
state_details = _workflow_state_details(args.action, result)
if state_details:
state_record["details"] = state_details
update_state_entry(_workflow_state_key(args.action), state_record, str(workspace))
wrapped = make_result(
status=result.get("status", "error"),
action=args.action,
summary=result.get("summary") or (result.get("error") or {}).get("message") or "workflow 执行完成",
details=result.get("details", {}),
context=parameter_context(provider="workflow", workspace=str(workspace), config_path=config_path),
error=result.get("error"),
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(wrapped)
else:
print(f"[workflow {args.action}] {wrapped['summary']}")
if wrapped.get("error"):
sys.exit(1)
if __name__ == "__main__":
main()
"""workflow skill 私有运行时工具。"""
from __future__ import annotations
import json
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
STATE_DIR_NAME = ".embeddedskills"
STATE_FILE_NAME = "state.json"
PROJECT_CONFIG_FILE_NAME = "config.json"
SKILL_NAME = "workflow"
LEGACY_WORKFLOW_KEYS = {"preferred_build", "preferred_flash", "preferred_debug", "preferred_observe"}
def now_iso() -> str:
return datetime.now().astimezone().isoformat(timespec="seconds")
def default_config_path(script_file: str) -> Path:
return Path(script_file).resolve().parents[1] / "config.json"
def load_local_config(script_file: str | None = None) -> dict:
"""加载 workflow/config.json(环境级配置)- workflow 基本为空或只有扩展位
路径:当前脚本所在 skill 目录下的 config.json
"""
if script_file is None:
import inspect
frame = inspect.currentframe()
if frame and frame.f_back:
script_file = frame.f_back.f_globals.get("__file__", "")
if not script_file:
return {}
config_path = default_config_path(script_file)
return load_json_file(config_path)
def save_local_config(data: dict, script_file: str | None = None) -> Path | None:
"""保存环境级配置到 workflow/config.json"""
if script_file is None:
import inspect
frame = inspect.currentframe()
if frame and frame.f_back:
script_file = frame.f_back.f_globals.get("__file__", "")
if not script_file:
return None
config_path = default_config_path(script_file)
existing = load_json_file(config_path)
existing.update(data)
save_json_file(config_path, existing)
return config_path
def load_project_config(workspace: str | None = None) -> dict:
"""从 workspace/.embeddedskills/config.json 读取 workflow 的工程级配置
参数: workspace - 工作区路径,None 时使用 cwd
返回: config["workflow"] 部分
"""
ws = workspace_root(workspace)
config_file = ws / STATE_DIR_NAME / PROJECT_CONFIG_FILE_NAME
data = load_json_file(config_file)
return data.get(SKILL_NAME, {})
def save_project_config(workspace: str | None = None, values: dict | None = None) -> Path | None:
"""写回 workflow 工程级配置到 .embeddedskills/config.json
- 只更新 workflow 部分,不覆盖其他 skill 的配置
- 目录不存在时自动创建 .embeddedskills/
"""
if values is None:
values = {}
ws = workspace_root(workspace)
config_file = ws / STATE_DIR_NAME / PROJECT_CONFIG_FILE_NAME
data = load_json_file(config_file)
data[SKILL_NAME] = {**(data.get(SKILL_NAME, {})), **values}
save_json_file(config_file, data)
return config_file
def load_full_project_config(workspace: str | None = None) -> dict:
"""读取完整的 .embeddedskills/config.json(workflow 需要读取其他 skill 的配置)"""
ws = workspace_root(workspace)
config_file = ws / STATE_DIR_NAME / PROJECT_CONFIG_FILE_NAME
return load_json_file(config_file)
def resolve_compat_config_path(config_path: str | None, workspace: str | None = None) -> Path | None:
if is_missing(config_path):
return None
raw_path = Path(str(config_path)).expanduser()
if raw_path.is_absolute():
return raw_path.resolve()
cwd_path = raw_path.resolve()
if cwd_path.exists():
return cwd_path
return (workspace_root(workspace) / raw_path).resolve()
def load_json_file_strict(path: str | Path) -> dict:
file_path = Path(path)
data = json.loads(file_path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError(f"配置文件必须是 JSON 对象: {file_path}")
return data
def merge_project_config(base: dict | None, override: dict | None) -> dict:
merged = dict(base or {})
for key, value in (override or {}).items():
if isinstance(merged.get(key), dict) and isinstance(value, dict):
merged[key] = {**merged[key], **value}
else:
merged[key] = value
return merged
def load_effective_project_config(workspace: str | None = None, config_path: str | None = None) -> tuple[dict, str | None]:
full_config = load_full_project_config(workspace)
resolved_path = resolve_compat_config_path(config_path, workspace)
if resolved_path is None:
return full_config, None
if not resolved_path.exists():
raise FileNotFoundError(f"配置文件不存在: {resolved_path}")
compat_config = load_json_file_strict(resolved_path)
if compat_config and "workflow" not in compat_config and LEGACY_WORKFLOW_KEYS.intersection(compat_config):
compat_config = {"workflow": compat_config}
return merge_project_config(full_config, compat_config), str(resolved_path)
def output_json(data: dict, *, indent: int = 2) -> None:
sys.stdout.reconfigure(encoding="utf-8")
print(json.dumps(data, ensure_ascii=False, indent=indent), flush=True)
def is_missing(value: Any) -> bool:
return value is None or value == ""
def normalize_path(value: str | None) -> str:
if is_missing(value):
return ""
return str(Path(str(value)).expanduser().resolve())
def _serialize_state_value(value: Any, workspace: Path) -> Any:
if isinstance(value, dict):
return {key: _serialize_state_value(item, workspace) for key, item in value.items()}
if isinstance(value, list):
return [_serialize_state_value(item, workspace) for item in value]
if not isinstance(value, str) or "://" in value:
return value
path = Path(value).expanduser()
if not path.is_absolute():
return value
try:
return Path(os.path.relpath(path.resolve(), workspace)).as_posix()
except ValueError:
return value
def hidden_subprocess_kwargs() -> dict:
if sys.platform != "win32":
return {}
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = getattr(subprocess, "SW_HIDE", 0)
return {
"creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0),
"startupinfo": startupinfo,
}
def load_json_file(path: str | Path) -> dict:
file_path = Path(path)
if not file_path.exists():
return {}
try:
return json.loads(file_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return {}
def save_json_file(path: str | Path, data: dict) -> None:
file_path = Path(path)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def workspace_root(workspace: str | None = None) -> Path:
if not is_missing(workspace):
return Path(str(workspace)).expanduser().resolve()
return Path.cwd().resolve()
def load_workspace_state(workspace: str | None = None) -> dict:
return load_json_file(workspace_root(workspace) / STATE_DIR_NAME / STATE_FILE_NAME)
def save_workspace_state(state: dict, workspace: str | None = None) -> Path:
ws = workspace_root(workspace)
file_path = ws / STATE_DIR_NAME / STATE_FILE_NAME
save_json_file(file_path, _serialize_state_value(state, ws))
return file_path
def update_state_entry(category: str, record: dict, workspace: str | None = None) -> dict:
ws = workspace_root(workspace)
state = load_workspace_state(workspace)
state[category] = _serialize_state_value({**record, "timestamp": record.get("timestamp") or now_iso()}, ws)
file_path = save_workspace_state(state, workspace)
return {
"workspace": str(ws),
"file": str(file_path),
"updated_keys": [category],
category: state[category],
}
def get_state_entry(state: dict | None, key: str) -> dict:
if not isinstance(state, dict):
return {}
value = state.get(key, {})
return value if isinstance(value, dict) else {}
def compact_dict(data: dict | None) -> dict:
if not isinstance(data, dict):
return {}
return {key: value for key, value in data.items() if value not in (None, "", [], {})}
def make_result(
*,
status: str,
action: str,
summary: str,
details: dict | None = None,
context: dict | None = None,
artifacts: dict | None = None,
metrics: dict | None = None,
state: dict | None = None,
next_actions: list[str] | None = None,
timing: dict | None = None,
error: dict | None = None,
) -> dict:
result = {"status": status, "action": action, "summary": summary, "details": compact_dict(details)}
optional = {
"context": compact_dict(context),
"artifacts": compact_dict(artifacts),
"metrics": compact_dict(metrics),
"state": compact_dict(state),
"timing": compact_dict(timing),
}
for key, value in optional.items():
if value:
result[key] = value
if next_actions:
result["next_actions"] = [item for item in next_actions if item]
if error:
result["error"] = compact_dict(error)
return result
def make_timing(started_at: str, elapsed_ms: int | float) -> dict:
return {"started_at": started_at, "finished_at": now_iso(), "elapsed_ms": int(elapsed_ms)}
def parameter_context(*, provider: str, workspace: str | None = None, parameter_sources: dict | None = None, config_path: str | None = None) -> dict:
context = {"provider": provider, "workspace": str(workspace_root(workspace))}
if parameter_sources:
context["parameter_sources"] = compact_dict(parameter_sources)
if not is_missing(config_path):
context["config_path"] = normalize_path(str(config_path))
return context
Related skills
FAQ
Is Workflow safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.