开发技巧

用 Devin Local Skill 与 Cascade Workflow 构建可复用的发布前检查

名称变更:2026-10-21 我核对时,windsurf.com 跳转到 devin.ai/desktop,docs.windsurf.com 跳转到 docs.devin.ai/desktop,产品文档里叫 Devin Desktop。Cascade 智能体还在,但新标签页默认用的是 Devin Local 智能体;文档说 Workflows 和 Mem

BINBHO · 图文教程

名称变更:2026-10-21 我核对时,windsurf.com 跳转到 devin.ai/desktop,docs.windsurf.com 跳转到 docs.devin.ai/desktop,产品文档里叫 Devin Desktop。Cascade 智能体还在,但新标签页默认用的是 Devin Local 智能体;文档说 Workflows 和 Memories 只适用于 Cascade(旧版智能体),Devin Local 用 Skills。配置目录优先用 .devin/,旧的 .windsurf/ 仍会被读取。本文按当前文档写,界面细节以你安装的版本为准。

这篇把“发版前检查”做成可复用的流程:一个检查脚本 release_check.py(版本号三处一致、CHANGELOG 有当前版本、git 工作区干净、测试通过),一个 Skill(Devin Local 和 Cascade 都能用,模型可自动调用),一个 Workflow(Cascade 中用 /release-check 手动调用)。两者都要求 AI 只检查和起草,不打 tag、不推送。另写 lint_custom.py 检查技能和工作流文件。

先看清成品

  • 检查脚本在示例项目上真实运行:干净仓库全部通过;改版本号后正确报出 3 项失败。
  • 发现并修复:测试运行时生成的 __pycache__ 让 git 工作区变“脏”,第二次检查就失败。
  • lint_custom.py 在故意写错的目录里找出 5 个问题。4 项 pytest 通过。

合规提醒:用你自己的账号和授权的套餐,不共享账号。规则、工作流和 MCP 配置会把指令和工具交给 AI,不要写进密钥;公司代码先确认组织政策是否允许使用。AI 运行命令前要看清楚,不要开启“自动执行所有命令”。

运行说明:box 是 Linux 服务器,没有图形界面,我没有安装或运行 Devin Desktop / Windsurf 编辑器,所以没有验证编辑器实际加载这些文件的效果。真实运行过的是:本文的检查脚本、示例项目和 MCP 服务器(2026-10-21,UTC+8)。Workflow 文件的具体格式文档里没有给出示例(只说包含标题、描述和步骤),我用 frontmatter 写了 description;如果编辑器用“+ Workspace”生成的格式不同,以它为准。

做完之后你会得到什么?

成果 说明
demo/ 示例项目 tinycalc
demo/.devin/skills/release-check/ SKILL.md + release_check.py
demo/.devin/workflows/release-check.md 工作流
lint_custom.py 检查技能和工作流
broken/ 故意写错的例子
test_89b.py 4 项测试

需要准备哪些环境?

项目 说明
编辑器 Devin Desktop(原 Windsurf),从 https://windsurf.com 下载
Python 3.11 以上(用到 tomllib),pip install pytest
git 检查工作区是否干净

关键事实

事实 来源
Workflows:.devin/workflows/*.md(或旧 .windsurf/workflows/),每个最多 12000 字符;只能手动用 /名称 调用,可以在一个工作流里调用另一个 官方 Workflows
Workflows 只适用于 Cascade;Devin Local 智能体不支持,可用 “Devin: Open Cascade Migration Wizard” 迁移成 Skills 同上
Skills:.devin/skills/<名称>/SKILL.md,frontmatter 必填 name 和 description;默认只把 name 和 description 给模型,调用时才读全文和附带文件;也可以 @名称 手动调用 官方 Skills
name:1–64 字符,小写字母、数字、连字符,不能以连字符开头或结尾、不能连续两个连字符,必须与目录名相同;description 最多 1024 字符 Agent Skills 规范

文件结构

89-windsurf-B-src/
├── demo/
│   ├── pyproject.toml
│   ├── CHANGELOG.md
│   ├── .gitignore
│   ├── src/tinycalc/__init__.py
│   ├── tests/test_calc.py
│   └── .devin/
│       ├── skills/release-check/
│       │   ├── SKILL.md
│       │   └── release_check.py
│       └── workflows/release-check.md
├── broken/
├── lint_custom.py
└── test_89b.py

第 1 步:Skill

---
name: release-check
description: 发版前检查 tinycalc:版本号是否一致、CHANGELOG 是否更新、测试是否通过。用户提到“发版”“发布新版本”“打 tag 前检查”时使用。
---

## 步骤

1. 在项目根目录运行 `python .devin/skills/release-check/release_check.py .`,把输出的 JSON 原样给我看。
2. 如果 `ok` 为 false:逐项说明哪一项没通过、可能的原因,**不要自己改版本号或 CHANGELOG**,等我确认。
3. 如果 `ok` 为 true:根据 CHANGELOG 中当前版本的条目,起草一段发布说明(新增 / 修复 / 变更)。
4. 到此为止。**不要**执行 `git tag`、`git push` 或任何发布命令,这些由我手动执行。

## 说明

- `git_clean` 为 null 表示当前目录不是 git 仓库,不算失败,但要提醒我。

第 2 步:检查脚本

"""发版前的机械检查:版本号三处一致、CHANGELOG 有当前版本、工作区干净、测试通过。只检查,不打 tag、不推送。"""
import json, os, re, subprocess, sys, tomllib
from pathlib import Path


def run(root):
    root = Path(root); res = {}
    ver = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))["project"]["version"]
    init = next((root / "src").glob("*/__init__.py"))
    m = re.search(r'__version__\s*=\s*"([^"]+)"', init.read_text(encoding="utf-8"))
    res["version"] = ver
    res["version_match"] = bool(m and m.group(1) == ver)
    res["changelog"] = f"## {ver}" in (root / "CHANGELOG.md").read_text(encoding="utf-8")
    git = subprocess.run(["git", "status", "--porcelain"], cwd=root, capture_output=True, text=True)
    res["git_clean"] = None if git.returncode else (git.stdout.strip() == "")
    # 不写 .pyc:否则测试本身会让工作区变“脏”(实测踩过)
    env = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}
    t = subprocess.run([sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider"], cwd=root, capture_output=True, text=True, env=env)
    res["tests"] = t.returncode == 0
    res["tests_tail"] = (t.stdout.strip().splitlines() or [""])[-1]
    res["ok"] = all(res[k] for k in ("version_match", "changelog", "tests")) and res["git_clean"] is not False
    return res


if __name__ == "__main__":
    r = run(sys.argv[1] if len(sys.argv) > 1 else ".")
    print(json.dumps(r, ensure_ascii=False, indent=2))
    sys.exit(0 if r["ok"] else 1)

第 3 步:Workflow

---
description: 发版前检查(手动用 /release-check 调用)
---

1. 运行 `python .devin/skills/release-check/release_check.py .`,把 JSON 结果原样贴出来。
2. 有任何一项没通过:列出失败项和建议的修改,停下来等我确认,不要自己修改。
3. 全部通过:根据 CHANGELOG 当前版本条目起草发布说明。
4. 不要执行 git tag、git push 或任何发布命令。

第 4 步:.gitignore

__pycache__/
*.pyc
.pytest_cache/

第 5 步:lint 脚本

"""检查工作流和技能文件。
工作流(docs.devin.ai/desktop/cascade/workflows):.devin/workflows/*.md(旧 .windsurf/workflows/),每个文件最多 12000 字符,只能手动 /名称 调用。
技能(.../skills 与 agentskills.io 规范):.devin/skills/<名称>/SKILL.md,frontmatter 必填 name 和 description;
name 1–64 个字符,只能小写字母、数字、连字符,不能以连字符开头/结尾、不能连续两个连字符,并且要和所在目录同名;description 1–1024 字符。"""
import re, sys
from pathlib import Path

NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")


def fm(text):
    if not text.startswith("---\n") or "\n---" not in text[4:]:
        return {}
    end = text.index("\n---", 4)
    return {k.strip(): v.strip() for k, v in (l.split(":", 1) for l in text[4:end].splitlines() if ":" in l)}


def lint(root):
    root = Path(root); out = []
    for f in sorted(list(root.glob(".devin/workflows/*.md")) + list(root.glob(".windsurf/workflows/*.md"))):
        t = f.read_text(encoding="utf-8"); e = []
        if len(t) > 12000: e.append(f"{len(t)} 字符,超过 12000")
        if not NAME_RE.match(f.stem): e.append("文件名建议只用小写字母、数字和连字符")
        if not re.search(r"^\s*\d+\.", t, re.M): e.append("没有编号步骤")
        out.append((f.relative_to(root).as_posix(), "workflow", e))
    for d in sorted(p for p in list(root.glob(".devin/skills/*")) + list(root.glob(".windsurf/skills/*")) if p.is_dir()):
        e = []; s = d / "SKILL.md"
        if not s.exists():
            out.append((d.relative_to(root).as_posix(), "skill", ["缺少 SKILL.md"])); continue
        m = fm(s.read_text(encoding="utf-8")); name = m.get("name", ""); desc = m.get("description", "")
        if not name: e.append("缺少 name")
        elif not (1 <= len(name) <= 64 and NAME_RE.match(name)): e.append(f"name={name!r} 不合规(小写字母/数字/单个连字符,1–64)")
        elif name != d.name: e.append(f"name={name!r} 与目录名 {d.name!r} 不一致")
        if not desc: e.append("缺少 description(模型靠它决定何时调用)")
        elif len(desc) > 1024: e.append("description 超过 1024 字符")
        out.append((s.relative_to(root).as_posix(), "skill", e))
    return out


def main(argv=None):
    root = (argv if argv is not None else sys.argv[1:] or ["."])[0]
    res = lint(root); bad = 0
    for p, kind, e in res:
        print(f"{'✗' if e else '✓'} [{kind}] {p}")
        for x in e: print("    ", x)
        bad += len(e)
    print(f"{len(res)} 项,{bad} 个问题"); return 1 if bad else 0


if __name__ == "__main__":
    sys.exit(main())

第 6 步:测试

import importlib.util, subprocess, sys, shutil
from pathlib import Path
import lint_custom as L

spec = importlib.util.spec_from_file_location("rc", "demo/.devin/skills/release-check/release_check.py")
rc = importlib.util.module_from_spec(spec); spec.loader.exec_module(rc)

def test_lint_demo_clean():
    assert all(not e for _, _, e in L.lint("demo"))

def test_lint_broken():
    errs = {p: e for p, _, e in L.lint("broken")}
    assert len(errs) == 4 and sum(len(e) for e in errs.values()) == 5

def git(cwd, *a):
    subprocess.run(["git", "-c", "user.email=t@example.com", "-c", "user.name=t", *a], cwd=cwd, check=True, capture_output=True)

def test_release_check_clean_repo_stays_clean(tmp_path):
    d = tmp_path / "p"; shutil.copytree("demo", d)
    git(d, "init", "-q"); git(d, "add", "-A"); git(d, "commit", "-qm", "init")
    assert rc.run(d)["ok"] and rc.run(d)["git_clean"]      # 连续两次:测试不能把工作区弄脏

def test_release_check_version_bump_detected(tmp_path):
    d = tmp_path / "p"; shutil.copytree("demo", d)
    p = d / "pyproject.toml"; p.write_text(p.read_text().replace("0.3.0", "0.4.0"))
    r = rc.run(d)
    assert not r["ok"] and not r["version_match"] and not r["changelog"] and r["git_clean"] is None

第 7 步:运行

# 2026-10-21 22:01 UTC+8,示例项目(未初始化 git)
$ python .devin/skills/release-check/release_check.py .
{
  "version": "0.3.0",
  "version_match": true,
  "changelog": true,
  "git_clean": null,
  "tests": true,
  "tests_tail": "2 passed in 0.01s",
  "ok": true
}

# 初始化 git 并提交后,连续检查两次(修复后)
  "git_clean": true,
  "ok": true
  "git_clean": true,
  "ok": true

# 把 pyproject.toml 的版本改成 0.4.0
$ python .devin/skills/release-check/release_check.py .
{
  "version": "0.4.0",
  "version_match": false,
  "changelog": false,
  "git_clean": false,
  "tests": true,
  "tests_tail": "2 passed in 0.00s",
  "ok": false
}
$ echo $?
1

$ python lint_custom.py demo
✓ [workflow] .devin/workflows/release-check.md
✓ [skill] .devin/skills/release-check/SKILL.md
2 项,0 个问题
$ python lint_custom.py broken
✗ [workflow] .devin/workflows/Run Tests.md
     文件名建议只用小写字母、数字和连字符
     没有编号步骤
✗ [skill] .devin/skills/Deploy/SKILL.md
     name='Deploy' 不合规(小写字母/数字/单个连字符,1–64)
✗ [skill] .devin/skills/empty
     缺少 SKILL.md
✗ [skill] .devin/skills/review/SKILL.md
     name='code-review' 与目录名 'review' 不一致
4 项,5 个问题
$ python -m pytest -q test_89b.py
4 passed

怎么检查结果?

  • 在编辑器的 Customizations 面板里,Skills 下应出现 release-check,Workflows 下应出现 release-check。
  • 对 AI 说“准备发 0.3.0”,看它是否自动调用 skill、贴出 JSON,并在最后停下而不是打 tag。
  • 用 Cascade 时输入 /release-check 调用工作流。

调试示例

测试把 git 工作区弄“脏”了。 第一次在初始化好的 git 仓库里测试时,连续运行两次检查,第二次 git_clean 变成 false:

$ git ls-files | rg pycache
src/tinycalc/__pycache__/__init__.cpython-313.pyc
tests/__pycache__/test_calc.cpython-313-pytest-9.1.1.pyc
$ git status --porcelain
 M src/tinycalc/__pycache__/__init__.cpython-313.pyc
 M tests/__pycache__/test_calc.cpython-313-pytest-9.1.1.pyc

原因有两层:我先在示例目录里跑过测试,__pycache__ 被 git add -A 提交了;之后每次检查都会运行测试,重新生成 .pyc,工作区就变了。修复:

  1. 示例项目加 .gitignore(__pycache__/、*.pyc、.pytest_cache/);
  2. 检查脚本运行 pytest 时设置 PYTHONDONTWRITEBYTECODE=1,即使别的项目没有 .gitignore 也不会写 .pyc。

测试 test_release_check_clean_repo_stays_clean 连续调用两次,确认第二次仍然干净。这类问题如果让 AI 自己处理,它很可能“顺手”把 .pyc 提交或者删掉,所以 SKILL.md 里写明了:检查不通过时只报告,不自己修改。

常见问题

现象 原因 处理
/release-check 不出现 用的是 Devin Local 智能体 Workflows 只适用于 Cascade,改用 Skill
Skill 不被自动调用 description 太笼统 写清楚做什么、什么时候用
Skill 不显示 name 与目录名不一致或含大写 用 lint_custom.py 检查
git_clean 一直 false 构建产物被提交 加 .gitignore,清理已提交的产物

模板规则

  • 机械检查写成脚本,AI 只负责运行和解读。
  • 明确写出不能做的事(tag、push)。
  • Skill 的 description 要具体。
  • 检查本身不能改变仓库状态。

项目资源

用途 地址
Devin Desktop 文档(原 docs.windsurf.com) https://docs.windsurf.com
Workflows(官方) https://docs.devin.ai/desktop/cascade/workflows
Skills(官方) https://docs.devin.ai/desktop/cascade/skills
Agent Skills 规范 https://agentskills.io/specification
Devin Local 智能体(官方) https://docs.devin.ai/desktop/devin-local

把“怎么做”写成脚本,把“什么时候停”写进说明。