@optima-chat/dev-skills 0.16.4 → 0.16.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/cn-deploy/SKILL.md +2 -2
- package/.claude/skills/yzsgo-e2e/SKILL.md +16 -4
- package/.claude/skills/yzsgo-e2e/bootstrap.py +64 -0
- package/.claude/skills/yzsgo-e2e/preflight.py +52 -22
- package/.claude/skills/yzsgo-e2e/run_e2e.py +17 -1
- package/.codex/skills/cn-deploy/SKILL.md +2 -2
- package/.codex/skills/yzsgo-e2e/SKILL.md +16 -4
- package/.codex/skills/yzsgo-e2e/bootstrap.py +64 -0
- package/.codex/skills/yzsgo-e2e/preflight.py +52 -22
- package/.codex/skills/yzsgo-e2e/run_e2e.py +17 -1
- package/bin/helpers/cn-deploy.ts +2 -0
- package/dist/bin/helpers/cn-deploy.js +2 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cn-deploy
|
|
3
|
-
description: 当用户请求把服务发布/部署/构建到 cn-stage、跑云效流水线、cn-stage 发版、部署某个分支到 cn-stage、触发 cn 构建、yunxiao 部署、"把 xx 发到 cn"时,使用此技能。覆盖 cn-stage 全部
|
|
3
|
+
description: 当用户请求把服务发布/部署/构建到 cn-stage、跑云效流水线、cn-stage 发版、部署某个分支到 cn-stage、触发 cn 构建、yunxiao 部署、"把 xx 发到 cn"时,使用此技能。覆盖 cn-stage 全部 22 个服务(构建→DB迁移→SAE 发布一条龙;含 build-only 的 agent-runtime)。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# cn-deploy - 云效 cn-stage 流水线发布
|
|
@@ -21,7 +21,7 @@ optima-cn-deploy --list # 列出全部可发服务
|
|
|
21
21
|
|
|
22
22
|
## 使用要点
|
|
23
23
|
|
|
24
|
-
1. **先 `--list` 确认服务名**(
|
|
24
|
+
1. **先 `--list` 确认服务名**(22 个服务,含 commerce-backend / optima-scout / build-only 的 agent-runtime)。
|
|
25
25
|
2. 全链约 3-8 分钟(agentic-chat 最重)。触发后会输出云效 run 链接,可发给用户自行围观。
|
|
26
26
|
3. **凭证零配置**:构建凭证由云效变量组 `cn-stage-build-credentials`(41970) 供给,不需要注入任何 secret。
|
|
27
27
|
4. 分支构建会把该分支代码部署到 cn-stage(替换现运行版本)——用户明确要求部署分支时才用。
|
|
@@ -16,9 +16,20 @@ allowed-tools: ["Bash", "Read", "Write", "Agent", "Workflow"]
|
|
|
16
16
|
- 用户提到「遇到它问 X 就答 Y / 测某个反问」→ `--answer "X=Y"`。
|
|
17
17
|
- 用户说「重点看有没有 Z」→ `--expect "Z"`。
|
|
18
18
|
|
|
19
|
+
## 环境自举(第一步,自包含——别外链别的仓库)
|
|
20
|
+
脚本都在本 skill 目录(记为 `$S`,就是本 SKILL.md 所在目录)。托管 venv = `~/.cache/yzsgo-e2e/venv`(记 `$VENV`),不碰系统 python。
|
|
21
|
+
|
|
22
|
+
1. **体检**:`python3 $S/preflight.py <env>`。缺项分两类:
|
|
23
|
+
- 🅰️ **auto(能自动装)**:`venv+playwright`、`sshpass`。
|
|
24
|
+
- ✋ **manual(凭据/登录,装不了)**:`chrome-9222`(登了测试账号的调试 Chrome)、`buildbox-pw`、`test-user-id`。
|
|
25
|
+
2. **逐个补缺**(🔴 全绿才往下;不绿就停下、别硬跑半残):
|
|
26
|
+
- 每个 🅰️ 缺项:**先问用户「要我装 `<name>` 吗?」,用户同意了**再跑 `python3 $S/bootstrap.py setup`(装 venv+playwright)或 `python3 $S/bootstrap.py install-sshpass`。别不问就装。
|
|
27
|
+
- 每个 ✋ 缺项:引导用户——`chrome-9222`:`python3 $S/bootstrap.py launch-chrome` 起窗口、让用户**手动登测试账号**(登一次长期免登);`buildbox-pw`:让用户把口令放 `~/.buildbox_pw`(内部拉 wire 用,向团队要);`test-user-id`:让用户登录 Optima(run_e2e 自动从 `~/.optima/token.json` 读 userId)。
|
|
28
|
+
- 补完 `python3 $S/preflight.py <env>` 复检,直到全 ✅ 才进四段。
|
|
29
|
+
|
|
19
30
|
## 四段流程
|
|
20
|
-
1. **preflight
|
|
21
|
-
2. **驱动 + 拉 wire +
|
|
31
|
+
1. **preflight + 自举**:见上「环境自举」,**全绿**才继续。
|
|
32
|
+
2. **驱动 + 拉 wire + 备料**:`$VENV/bin/python $S/run_e2e.py --env <env> --message ... [--answer k=v] [--expect ...] --out <dir>` → 出 `prepped.md` + `meta.json`。🔑 用 **venv 的 python**(playwright 在那儿);`--user` 不给则自动读 `~/.optima/token.json`。
|
|
22
33
|
3. **判定**:`gh issue list --repo Optima-Chat/optima-gateway --state open --json number,title` 拉已知 issue 文本,`Workflow({scriptPath:"<skill>/judge_workflow.js", args:{base:"<dir>", sids:["prepped"], knownIssues:"<文本>"}})`。
|
|
23
34
|
4. **报告 + 提 issue**:
|
|
24
35
|
判定跑完后,先把 judge_workflow 输出 + meta.json 组装成 render_report 的入参(两者字段形状不同,别直接传):
|
|
@@ -45,7 +56,8 @@ allowed-tools: ["Bash", "Read", "Write", "Agent", "Workflow"]
|
|
|
45
56
|
|
|
46
57
|
## 真机 smoke(改完先跑这个验管道通)
|
|
47
58
|
```bash
|
|
48
|
-
python3
|
|
59
|
+
python3 $S/preflight.py cn-prod # 先体检,缺项按「环境自举」补齐到全绿
|
|
60
|
+
$VENV/bin/python $S/run_e2e.py --env cn-prod --message "你好" --out /tmp/yzsgo-smoke
|
|
49
61
|
# 期望:/tmp/yzsgo-smoke/prepped.md 含「前端所见」节 + wire transcript;meta.json 的 located 命中本次对话。
|
|
50
|
-
#
|
|
62
|
+
# 只验四段接线通,不追判定质量。(--user 自动读 token.json)
|
|
51
63
|
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""yzsgo-e2e 自举:把「能装的」依赖装好。**征得用户同意由 SKILL.md/Claude 负责问**,
|
|
3
|
+
本脚本只在被调用时执行对应动作。托管 venv 放 ~/.cache/yzsgo-e2e/venv——不碰系统 python
|
|
4
|
+
(macOS/发行版的系统 python 常是外部管理,pip install 会被拒),最稳、可复现、不污染。"""
|
|
5
|
+
import os, subprocess, sys
|
|
6
|
+
|
|
7
|
+
VENV = os.path.expanduser("~/.cache/yzsgo-e2e/venv")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def venv_python(base: str = VENV) -> str:
|
|
11
|
+
return os.path.join(base, "bin", "python")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def plan_actions(missing_names: list, base: str = VENV) -> list:
|
|
15
|
+
"""纯函数:把缺项名映射成要执行的命令序列。供单测 + 让 Claude/用户预知会跑什么。
|
|
16
|
+
返回 [(动作名, [[argv...], ...]), ...]。只覆盖 auto 类缺项(manual 类装不了,不在此)。"""
|
|
17
|
+
plan = []
|
|
18
|
+
for n in missing_names:
|
|
19
|
+
if n == "venv+playwright":
|
|
20
|
+
plan.append(("setup", [
|
|
21
|
+
[sys.executable, "-m", "venv", base],
|
|
22
|
+
[venv_python(base), "-m", "pip", "install", "-q", "playwright"],
|
|
23
|
+
[venv_python(base), "-m", "playwright", "install", "chromium"],
|
|
24
|
+
]))
|
|
25
|
+
elif n == "sshpass":
|
|
26
|
+
plan.append(("install-sshpass", [["brew", "install", "hudochenkov/sshpass/sshpass"]]))
|
|
27
|
+
elif n == "chrome-9222":
|
|
28
|
+
plan.append(("launch-chrome", [[
|
|
29
|
+
"open", "-na", "Google Chrome", "--args",
|
|
30
|
+
"--remote-debugging-port=9222", "--user-data-dir=/tmp/yzsgo-chrome",
|
|
31
|
+
"https://www.yzsgo.com"]]))
|
|
32
|
+
return plan
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _run(cmds: list) -> bool:
|
|
36
|
+
for c in cmds:
|
|
37
|
+
print("+", " ".join(c))
|
|
38
|
+
if subprocess.run(c).returncode != 0:
|
|
39
|
+
print(f"[bootstrap] 命令失败:{' '.join(c)}", file=sys.stderr)
|
|
40
|
+
return False
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _do(name: str) -> bool:
|
|
45
|
+
plan = plan_actions([name])
|
|
46
|
+
if not plan:
|
|
47
|
+
print(f"[bootstrap] 无法自动处理 {name}(可能是 manual 类,需人工准备)", file=sys.stderr)
|
|
48
|
+
return False
|
|
49
|
+
return _run([c for _, cmds in plan for c in cmds])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# 动作名 → 触发它的缺项名
|
|
53
|
+
_CMD_TO_MISSING = {"setup": "venv+playwright", "install-sshpass": "sshpass", "launch-chrome": "chrome-9222"}
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
cmd = sys.argv[1] if len(sys.argv) > 1 else "setup"
|
|
57
|
+
miss = _CMD_TO_MISSING.get(cmd)
|
|
58
|
+
if not miss:
|
|
59
|
+
print(f"未知命令 {cmd};可用:{' / '.join(_CMD_TO_MISSING)}", file=sys.stderr)
|
|
60
|
+
sys.exit(2)
|
|
61
|
+
ok = _do(miss)
|
|
62
|
+
if cmd == "launch-chrome" and ok:
|
|
63
|
+
print("已起调试端口 Chrome;请在弹出的窗口里手动登录测试账号(登一次即长期免登)。")
|
|
64
|
+
sys.exit(0 if ok else 1)
|
|
@@ -1,44 +1,74 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""yzsgo-e2e
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
"""yzsgo-e2e 环境体检(医生)。诊断每项依赖,分两类:
|
|
3
|
+
- auto:可由 bootstrap.py 在**征得用户同意后**自动装(托管 venv+playwright、sshpass);
|
|
4
|
+
- manual:凭据/登录类,装不了、只能引导(buildbox 口令、登了测试账号的调试 Chrome、userId)。
|
|
5
|
+
本脚本只诊断、不装东西。装由 bootstrap.py 做(同意由 SKILL.md/Claude 负责问)。"""
|
|
6
|
+
import json, os, shutil, socket, subprocess, sys
|
|
7
|
+
|
|
8
|
+
VENV = os.path.expanduser("~/.cache/yzsgo-e2e/venv")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def venv_python(base: str = VENV) -> str:
|
|
12
|
+
return os.path.join(base, "bin", "python")
|
|
13
|
+
|
|
5
14
|
|
|
6
15
|
def _port_open(port: int) -> bool:
|
|
7
16
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
8
17
|
s.settimeout(1.0)
|
|
9
18
|
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
10
19
|
|
|
11
|
-
def probe(env: str = "cn-prod") -> list[dict]:
|
|
12
|
-
return [
|
|
13
|
-
{"name": "chrome-9222", "ok": _port_open(9222),
|
|
14
|
-
"hint": '起调试端口 Chrome:open -na "Google Chrome" --args --remote-debugging-port=9222 '
|
|
15
|
-
'--user-data-dir=/tmp/yzsgo-chrome https://www.yzsgo.com(手动登测试账号)'},
|
|
16
|
-
{"name": "buildbox-pw", "ok": os.path.exists(os.path.expanduser("~/.buildbox_pw")),
|
|
17
|
-
"hint": "拉 wire 需 buildbox 口令文件 ~/.buildbox_pw(见 setting-up-yzsgo-test-env)"},
|
|
18
|
-
{"name": "sshpass", "ok": shutil.which("sshpass") is not None,
|
|
19
|
-
"hint": "brew install hudochenkov/sshpass/sshpass"},
|
|
20
|
-
{"name": "playwright", "ok": _has_playwright(),
|
|
21
|
-
"hint": "pip install playwright && playwright install chromium"},
|
|
22
|
-
]
|
|
23
20
|
|
|
24
|
-
def
|
|
21
|
+
def _venv_has_playwright() -> bool:
|
|
22
|
+
py = venv_python()
|
|
23
|
+
if not os.path.exists(py):
|
|
24
|
+
return False
|
|
25
|
+
return subprocess.run([py, "-c", "import playwright"], capture_output=True).returncode == 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _token_user() -> str | None:
|
|
25
29
|
try:
|
|
26
|
-
|
|
27
|
-
return
|
|
30
|
+
d = json.load(open(os.path.expanduser("~/.optima/token.json")))
|
|
31
|
+
return (d.get("user") or {}).get("userId")
|
|
28
32
|
except Exception:
|
|
29
|
-
return
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def probe(env: str = "cn-prod") -> list:
|
|
37
|
+
return [
|
|
38
|
+
{"name": "venv+playwright", "ok": _venv_has_playwright(), "category": "auto",
|
|
39
|
+
"fix": "python3 bootstrap.py setup # 建托管 venv(~/.cache/yzsgo-e2e/venv) + 装 playwright/chromium"},
|
|
40
|
+
{"name": "sshpass", "ok": shutil.which("sshpass") is not None, "category": "auto",
|
|
41
|
+
"fix": "python3 bootstrap.py install-sshpass # 或 brew install hudochenkov/sshpass/sshpass"},
|
|
42
|
+
{"name": "chrome-9222", "ok": _port_open(9222), "category": "manual",
|
|
43
|
+
"fix": "python3 bootstrap.py launch-chrome,然后在弹出的 Chrome 里手动登录测试账号"},
|
|
44
|
+
{"name": "buildbox-pw", "ok": os.path.exists(os.path.expanduser("~/.buildbox_pw")), "category": "manual",
|
|
45
|
+
"fix": "把 buildbox 口令写进 ~/.buildbox_pw(内部拉 wire 用;向团队要)"},
|
|
46
|
+
{"name": "test-user-id", "ok": _token_user() is not None, "category": "manual",
|
|
47
|
+
"fix": "登录 Optima 生成 ~/.optima/token.json(run_e2e 会自动读 user.userId)"},
|
|
48
|
+
]
|
|
49
|
+
|
|
30
50
|
|
|
31
51
|
def summarize_preflight(checks: list) -> dict:
|
|
32
52
|
ok = all(c["ok"] for c in checks)
|
|
33
|
-
missing = [c["name"]
|
|
53
|
+
missing = [{"name": c["name"], "category": c["category"], "fix": c["fix"]}
|
|
54
|
+
for c in checks if not c["ok"]]
|
|
34
55
|
rows = []
|
|
35
56
|
for c in checks:
|
|
36
|
-
mark = "✅" if c["ok"] else "❌"
|
|
37
|
-
rows.append(f"{mark} {c['name']}" + ("" if c["ok"] else f"\n → {c['
|
|
57
|
+
mark = "✅" if c["ok"] else ("🅰️ ❌" if c["category"] == "auto" else "✋ ❌")
|
|
58
|
+
rows.append(f"{mark} {c['name']}" + ("" if c["ok"] else f"\n → {c['fix']}"))
|
|
38
59
|
return {"ok": ok, "missing": missing, "report": "\n".join(rows)}
|
|
39
60
|
|
|
61
|
+
|
|
40
62
|
if __name__ == "__main__":
|
|
41
63
|
env = sys.argv[1] if len(sys.argv) > 1 else "cn-prod"
|
|
42
64
|
r = summarize_preflight(probe(env))
|
|
43
65
|
print(r["report"])
|
|
66
|
+
if not r["ok"]:
|
|
67
|
+
auto = [m["name"] for m in r["missing"] if m["category"] == "auto"]
|
|
68
|
+
man = [m["name"] for m in r["missing"] if m["category"] == "manual"]
|
|
69
|
+
print()
|
|
70
|
+
if auto:
|
|
71
|
+
print(f"🅰️ 可自动装(Claude 会先征得你同意再跑 bootstrap):{', '.join(auto)}")
|
|
72
|
+
if man:
|
|
73
|
+
print(f"✋ 需你人工准备(凭据/登录,装不了):{', '.join(man)}")
|
|
44
74
|
sys.exit(0 if r["ok"] else 1)
|
|
@@ -23,6 +23,14 @@ def render_report(result: dict) -> str:
|
|
|
23
23
|
def _utc_now() -> str:
|
|
24
24
|
return datetime.now(timezone.utc).isoformat()
|
|
25
25
|
|
|
26
|
+
def _read_user_from_token(path: str = None) -> str:
|
|
27
|
+
path = path or os.path.expanduser("~/.optima/token.json")
|
|
28
|
+
try:
|
|
29
|
+
d = json.load(open(path))
|
|
30
|
+
return (d.get("user") or {}).get("userId")
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
|
|
26
34
|
def _parse_answers(pairs):
|
|
27
35
|
out = []
|
|
28
36
|
for p in pairs or []:
|
|
@@ -50,7 +58,8 @@ def main():
|
|
|
50
58
|
ap.add_argument("--message", action="append", required=True, help="逐轮发送的消息(可多次)")
|
|
51
59
|
ap.add_argument("--answer", action="append", help="反问预设答案 关键词=答案(可多次)")
|
|
52
60
|
ap.add_argument("--expect", default="", help="关注点,喂给判定层")
|
|
53
|
-
ap.add_argument("--user",
|
|
61
|
+
ap.add_argument("--user", default=None,
|
|
62
|
+
help="测试账号 userId(拉 wire 用);不给则从 ~/.optima/token.json 自动读")
|
|
54
63
|
ap.add_argument("--out", default="e2e-out")
|
|
55
64
|
ap.add_argument("--timeout", type=int, default=180,
|
|
56
65
|
help="每轮等回复超时秒;长任务(简报/多工具)调大到 500+")
|
|
@@ -58,6 +67,13 @@ def main():
|
|
|
58
67
|
ap.add_argument("--issue-repo", default="Optima-Chat/optima-gateway")
|
|
59
68
|
args = ap.parse_args()
|
|
60
69
|
|
|
70
|
+
if not args.user:
|
|
71
|
+
args.user = _read_user_from_token()
|
|
72
|
+
if not args.user:
|
|
73
|
+
print("[error] 没给 --user 且 ~/.optima/token.json 读不到 userId;先登录 Optima 或显式传 --user。",
|
|
74
|
+
file=sys.stderr)
|
|
75
|
+
sys.exit(2)
|
|
76
|
+
|
|
61
77
|
if args.env == "cn-stage":
|
|
62
78
|
print("[warn] cn-stage 的 wire 取法未验证(spec §8 待核实);仅 cn-prod 全链路已打通。", file=sys.stderr)
|
|
63
79
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cn-deploy
|
|
3
|
-
description: 当用户请求把服务发布/部署/构建到 cn-stage、跑云效流水线、cn-stage 发版、部署某个分支到 cn-stage、触发 cn 构建、yunxiao 部署、"把 xx 发到 cn"时,使用此技能。覆盖 cn-stage 全部
|
|
3
|
+
description: 当用户请求把服务发布/部署/构建到 cn-stage、跑云效流水线、cn-stage 发版、部署某个分支到 cn-stage、触发 cn 构建、yunxiao 部署、"把 xx 发到 cn"时,使用此技能。覆盖 cn-stage 全部 22 个服务(构建→DB迁移→SAE 发布一条龙;含 build-only 的 agent-runtime)。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# cn-deploy - 云效 cn-stage 流水线发布
|
|
@@ -21,7 +21,7 @@ optima-cn-deploy --list # 列出全部可发服务
|
|
|
21
21
|
|
|
22
22
|
## 使用要点
|
|
23
23
|
|
|
24
|
-
1. **先 `--list` 确认服务名**(
|
|
24
|
+
1. **先 `--list` 确认服务名**(22 个服务,含 commerce-backend / optima-scout / build-only 的 agent-runtime)。
|
|
25
25
|
2. 全链约 3-8 分钟(agentic-chat 最重)。触发后会输出云效 run 链接,可发给用户自行围观。
|
|
26
26
|
3. **凭证零配置**:构建凭证由云效变量组 `cn-stage-build-credentials`(41970) 供给,不需要注入任何 secret。
|
|
27
27
|
4. 分支构建会把该分支代码部署到 cn-stage(替换现运行版本)——用户明确要求部署分支时才用。
|
|
@@ -16,9 +16,20 @@ allowed-tools: ["Bash", "Read", "Write", "Agent", "Workflow"]
|
|
|
16
16
|
- 用户提到「遇到它问 X 就答 Y / 测某个反问」→ `--answer "X=Y"`。
|
|
17
17
|
- 用户说「重点看有没有 Z」→ `--expect "Z"`。
|
|
18
18
|
|
|
19
|
+
## 环境自举(第一步,自包含——别外链别的仓库)
|
|
20
|
+
脚本都在本 skill 目录(记为 `$S`,就是本 SKILL.md 所在目录)。托管 venv = `~/.cache/yzsgo-e2e/venv`(记 `$VENV`),不碰系统 python。
|
|
21
|
+
|
|
22
|
+
1. **体检**:`python3 $S/preflight.py <env>`。缺项分两类:
|
|
23
|
+
- 🅰️ **auto(能自动装)**:`venv+playwright`、`sshpass`。
|
|
24
|
+
- ✋ **manual(凭据/登录,装不了)**:`chrome-9222`(登了测试账号的调试 Chrome)、`buildbox-pw`、`test-user-id`。
|
|
25
|
+
2. **逐个补缺**(🔴 全绿才往下;不绿就停下、别硬跑半残):
|
|
26
|
+
- 每个 🅰️ 缺项:**先问用户「要我装 `<name>` 吗?」,用户同意了**再跑 `python3 $S/bootstrap.py setup`(装 venv+playwright)或 `python3 $S/bootstrap.py install-sshpass`。别不问就装。
|
|
27
|
+
- 每个 ✋ 缺项:引导用户——`chrome-9222`:`python3 $S/bootstrap.py launch-chrome` 起窗口、让用户**手动登测试账号**(登一次长期免登);`buildbox-pw`:让用户把口令放 `~/.buildbox_pw`(内部拉 wire 用,向团队要);`test-user-id`:让用户登录 Optima(run_e2e 自动从 `~/.optima/token.json` 读 userId)。
|
|
28
|
+
- 补完 `python3 $S/preflight.py <env>` 复检,直到全 ✅ 才进四段。
|
|
29
|
+
|
|
19
30
|
## 四段流程
|
|
20
|
-
1. **preflight
|
|
21
|
-
2. **驱动 + 拉 wire +
|
|
31
|
+
1. **preflight + 自举**:见上「环境自举」,**全绿**才继续。
|
|
32
|
+
2. **驱动 + 拉 wire + 备料**:`$VENV/bin/python $S/run_e2e.py --env <env> --message ... [--answer k=v] [--expect ...] --out <dir>` → 出 `prepped.md` + `meta.json`。🔑 用 **venv 的 python**(playwright 在那儿);`--user` 不给则自动读 `~/.optima/token.json`。
|
|
22
33
|
3. **判定**:`gh issue list --repo Optima-Chat/optima-gateway --state open --json number,title` 拉已知 issue 文本,`Workflow({scriptPath:"<skill>/judge_workflow.js", args:{base:"<dir>", sids:["prepped"], knownIssues:"<文本>"}})`。
|
|
23
34
|
4. **报告 + 提 issue**:
|
|
24
35
|
判定跑完后,先把 judge_workflow 输出 + meta.json 组装成 render_report 的入参(两者字段形状不同,别直接传):
|
|
@@ -45,7 +56,8 @@ allowed-tools: ["Bash", "Read", "Write", "Agent", "Workflow"]
|
|
|
45
56
|
|
|
46
57
|
## 真机 smoke(改完先跑这个验管道通)
|
|
47
58
|
```bash
|
|
48
|
-
python3
|
|
59
|
+
python3 $S/preflight.py cn-prod # 先体检,缺项按「环境自举」补齐到全绿
|
|
60
|
+
$VENV/bin/python $S/run_e2e.py --env cn-prod --message "你好" --out /tmp/yzsgo-smoke
|
|
49
61
|
# 期望:/tmp/yzsgo-smoke/prepped.md 含「前端所见」节 + wire transcript;meta.json 的 located 命中本次对话。
|
|
50
|
-
#
|
|
62
|
+
# 只验四段接线通,不追判定质量。(--user 自动读 token.json)
|
|
51
63
|
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""yzsgo-e2e 自举:把「能装的」依赖装好。**征得用户同意由 SKILL.md/Claude 负责问**,
|
|
3
|
+
本脚本只在被调用时执行对应动作。托管 venv 放 ~/.cache/yzsgo-e2e/venv——不碰系统 python
|
|
4
|
+
(macOS/发行版的系统 python 常是外部管理,pip install 会被拒),最稳、可复现、不污染。"""
|
|
5
|
+
import os, subprocess, sys
|
|
6
|
+
|
|
7
|
+
VENV = os.path.expanduser("~/.cache/yzsgo-e2e/venv")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def venv_python(base: str = VENV) -> str:
|
|
11
|
+
return os.path.join(base, "bin", "python")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def plan_actions(missing_names: list, base: str = VENV) -> list:
|
|
15
|
+
"""纯函数:把缺项名映射成要执行的命令序列。供单测 + 让 Claude/用户预知会跑什么。
|
|
16
|
+
返回 [(动作名, [[argv...], ...]), ...]。只覆盖 auto 类缺项(manual 类装不了,不在此)。"""
|
|
17
|
+
plan = []
|
|
18
|
+
for n in missing_names:
|
|
19
|
+
if n == "venv+playwright":
|
|
20
|
+
plan.append(("setup", [
|
|
21
|
+
[sys.executable, "-m", "venv", base],
|
|
22
|
+
[venv_python(base), "-m", "pip", "install", "-q", "playwright"],
|
|
23
|
+
[venv_python(base), "-m", "playwright", "install", "chromium"],
|
|
24
|
+
]))
|
|
25
|
+
elif n == "sshpass":
|
|
26
|
+
plan.append(("install-sshpass", [["brew", "install", "hudochenkov/sshpass/sshpass"]]))
|
|
27
|
+
elif n == "chrome-9222":
|
|
28
|
+
plan.append(("launch-chrome", [[
|
|
29
|
+
"open", "-na", "Google Chrome", "--args",
|
|
30
|
+
"--remote-debugging-port=9222", "--user-data-dir=/tmp/yzsgo-chrome",
|
|
31
|
+
"https://www.yzsgo.com"]]))
|
|
32
|
+
return plan
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _run(cmds: list) -> bool:
|
|
36
|
+
for c in cmds:
|
|
37
|
+
print("+", " ".join(c))
|
|
38
|
+
if subprocess.run(c).returncode != 0:
|
|
39
|
+
print(f"[bootstrap] 命令失败:{' '.join(c)}", file=sys.stderr)
|
|
40
|
+
return False
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _do(name: str) -> bool:
|
|
45
|
+
plan = plan_actions([name])
|
|
46
|
+
if not plan:
|
|
47
|
+
print(f"[bootstrap] 无法自动处理 {name}(可能是 manual 类,需人工准备)", file=sys.stderr)
|
|
48
|
+
return False
|
|
49
|
+
return _run([c for _, cmds in plan for c in cmds])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# 动作名 → 触发它的缺项名
|
|
53
|
+
_CMD_TO_MISSING = {"setup": "venv+playwright", "install-sshpass": "sshpass", "launch-chrome": "chrome-9222"}
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
cmd = sys.argv[1] if len(sys.argv) > 1 else "setup"
|
|
57
|
+
miss = _CMD_TO_MISSING.get(cmd)
|
|
58
|
+
if not miss:
|
|
59
|
+
print(f"未知命令 {cmd};可用:{' / '.join(_CMD_TO_MISSING)}", file=sys.stderr)
|
|
60
|
+
sys.exit(2)
|
|
61
|
+
ok = _do(miss)
|
|
62
|
+
if cmd == "launch-chrome" and ok:
|
|
63
|
+
print("已起调试端口 Chrome;请在弹出的窗口里手动登录测试账号(登一次即长期免登)。")
|
|
64
|
+
sys.exit(0 if ok else 1)
|
|
@@ -1,44 +1,74 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""yzsgo-e2e
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
"""yzsgo-e2e 环境体检(医生)。诊断每项依赖,分两类:
|
|
3
|
+
- auto:可由 bootstrap.py 在**征得用户同意后**自动装(托管 venv+playwright、sshpass);
|
|
4
|
+
- manual:凭据/登录类,装不了、只能引导(buildbox 口令、登了测试账号的调试 Chrome、userId)。
|
|
5
|
+
本脚本只诊断、不装东西。装由 bootstrap.py 做(同意由 SKILL.md/Claude 负责问)。"""
|
|
6
|
+
import json, os, shutil, socket, subprocess, sys
|
|
7
|
+
|
|
8
|
+
VENV = os.path.expanduser("~/.cache/yzsgo-e2e/venv")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def venv_python(base: str = VENV) -> str:
|
|
12
|
+
return os.path.join(base, "bin", "python")
|
|
13
|
+
|
|
5
14
|
|
|
6
15
|
def _port_open(port: int) -> bool:
|
|
7
16
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
8
17
|
s.settimeout(1.0)
|
|
9
18
|
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
10
19
|
|
|
11
|
-
def probe(env: str = "cn-prod") -> list[dict]:
|
|
12
|
-
return [
|
|
13
|
-
{"name": "chrome-9222", "ok": _port_open(9222),
|
|
14
|
-
"hint": '起调试端口 Chrome:open -na "Google Chrome" --args --remote-debugging-port=9222 '
|
|
15
|
-
'--user-data-dir=/tmp/yzsgo-chrome https://www.yzsgo.com(手动登测试账号)'},
|
|
16
|
-
{"name": "buildbox-pw", "ok": os.path.exists(os.path.expanduser("~/.buildbox_pw")),
|
|
17
|
-
"hint": "拉 wire 需 buildbox 口令文件 ~/.buildbox_pw(见 setting-up-yzsgo-test-env)"},
|
|
18
|
-
{"name": "sshpass", "ok": shutil.which("sshpass") is not None,
|
|
19
|
-
"hint": "brew install hudochenkov/sshpass/sshpass"},
|
|
20
|
-
{"name": "playwright", "ok": _has_playwright(),
|
|
21
|
-
"hint": "pip install playwright && playwright install chromium"},
|
|
22
|
-
]
|
|
23
20
|
|
|
24
|
-
def
|
|
21
|
+
def _venv_has_playwright() -> bool:
|
|
22
|
+
py = venv_python()
|
|
23
|
+
if not os.path.exists(py):
|
|
24
|
+
return False
|
|
25
|
+
return subprocess.run([py, "-c", "import playwright"], capture_output=True).returncode == 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _token_user() -> str | None:
|
|
25
29
|
try:
|
|
26
|
-
|
|
27
|
-
return
|
|
30
|
+
d = json.load(open(os.path.expanduser("~/.optima/token.json")))
|
|
31
|
+
return (d.get("user") or {}).get("userId")
|
|
28
32
|
except Exception:
|
|
29
|
-
return
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def probe(env: str = "cn-prod") -> list:
|
|
37
|
+
return [
|
|
38
|
+
{"name": "venv+playwright", "ok": _venv_has_playwright(), "category": "auto",
|
|
39
|
+
"fix": "python3 bootstrap.py setup # 建托管 venv(~/.cache/yzsgo-e2e/venv) + 装 playwright/chromium"},
|
|
40
|
+
{"name": "sshpass", "ok": shutil.which("sshpass") is not None, "category": "auto",
|
|
41
|
+
"fix": "python3 bootstrap.py install-sshpass # 或 brew install hudochenkov/sshpass/sshpass"},
|
|
42
|
+
{"name": "chrome-9222", "ok": _port_open(9222), "category": "manual",
|
|
43
|
+
"fix": "python3 bootstrap.py launch-chrome,然后在弹出的 Chrome 里手动登录测试账号"},
|
|
44
|
+
{"name": "buildbox-pw", "ok": os.path.exists(os.path.expanduser("~/.buildbox_pw")), "category": "manual",
|
|
45
|
+
"fix": "把 buildbox 口令写进 ~/.buildbox_pw(内部拉 wire 用;向团队要)"},
|
|
46
|
+
{"name": "test-user-id", "ok": _token_user() is not None, "category": "manual",
|
|
47
|
+
"fix": "登录 Optima 生成 ~/.optima/token.json(run_e2e 会自动读 user.userId)"},
|
|
48
|
+
]
|
|
49
|
+
|
|
30
50
|
|
|
31
51
|
def summarize_preflight(checks: list) -> dict:
|
|
32
52
|
ok = all(c["ok"] for c in checks)
|
|
33
|
-
missing = [c["name"]
|
|
53
|
+
missing = [{"name": c["name"], "category": c["category"], "fix": c["fix"]}
|
|
54
|
+
for c in checks if not c["ok"]]
|
|
34
55
|
rows = []
|
|
35
56
|
for c in checks:
|
|
36
|
-
mark = "✅" if c["ok"] else "❌"
|
|
37
|
-
rows.append(f"{mark} {c['name']}" + ("" if c["ok"] else f"\n → {c['
|
|
57
|
+
mark = "✅" if c["ok"] else ("🅰️ ❌" if c["category"] == "auto" else "✋ ❌")
|
|
58
|
+
rows.append(f"{mark} {c['name']}" + ("" if c["ok"] else f"\n → {c['fix']}"))
|
|
38
59
|
return {"ok": ok, "missing": missing, "report": "\n".join(rows)}
|
|
39
60
|
|
|
61
|
+
|
|
40
62
|
if __name__ == "__main__":
|
|
41
63
|
env = sys.argv[1] if len(sys.argv) > 1 else "cn-prod"
|
|
42
64
|
r = summarize_preflight(probe(env))
|
|
43
65
|
print(r["report"])
|
|
66
|
+
if not r["ok"]:
|
|
67
|
+
auto = [m["name"] for m in r["missing"] if m["category"] == "auto"]
|
|
68
|
+
man = [m["name"] for m in r["missing"] if m["category"] == "manual"]
|
|
69
|
+
print()
|
|
70
|
+
if auto:
|
|
71
|
+
print(f"🅰️ 可自动装(Claude 会先征得你同意再跑 bootstrap):{', '.join(auto)}")
|
|
72
|
+
if man:
|
|
73
|
+
print(f"✋ 需你人工准备(凭据/登录,装不了):{', '.join(man)}")
|
|
44
74
|
sys.exit(0 if r["ok"] else 1)
|
|
@@ -23,6 +23,14 @@ def render_report(result: dict) -> str:
|
|
|
23
23
|
def _utc_now() -> str:
|
|
24
24
|
return datetime.now(timezone.utc).isoformat()
|
|
25
25
|
|
|
26
|
+
def _read_user_from_token(path: str = None) -> str:
|
|
27
|
+
path = path or os.path.expanduser("~/.optima/token.json")
|
|
28
|
+
try:
|
|
29
|
+
d = json.load(open(path))
|
|
30
|
+
return (d.get("user") or {}).get("userId")
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
|
|
26
34
|
def _parse_answers(pairs):
|
|
27
35
|
out = []
|
|
28
36
|
for p in pairs or []:
|
|
@@ -50,7 +58,8 @@ def main():
|
|
|
50
58
|
ap.add_argument("--message", action="append", required=True, help="逐轮发送的消息(可多次)")
|
|
51
59
|
ap.add_argument("--answer", action="append", help="反问预设答案 关键词=答案(可多次)")
|
|
52
60
|
ap.add_argument("--expect", default="", help="关注点,喂给判定层")
|
|
53
|
-
ap.add_argument("--user",
|
|
61
|
+
ap.add_argument("--user", default=None,
|
|
62
|
+
help="测试账号 userId(拉 wire 用);不给则从 ~/.optima/token.json 自动读")
|
|
54
63
|
ap.add_argument("--out", default="e2e-out")
|
|
55
64
|
ap.add_argument("--timeout", type=int, default=180,
|
|
56
65
|
help="每轮等回复超时秒;长任务(简报/多工具)调大到 500+")
|
|
@@ -58,6 +67,13 @@ def main():
|
|
|
58
67
|
ap.add_argument("--issue-repo", default="Optima-Chat/optima-gateway")
|
|
59
68
|
args = ap.parse_args()
|
|
60
69
|
|
|
70
|
+
if not args.user:
|
|
71
|
+
args.user = _read_user_from_token()
|
|
72
|
+
if not args.user:
|
|
73
|
+
print("[error] 没给 --user 且 ~/.optima/token.json 读不到 userId;先登录 Optima 或显式传 --user。",
|
|
74
|
+
file=sys.stderr)
|
|
75
|
+
sys.exit(2)
|
|
76
|
+
|
|
61
77
|
if args.env == "cn-stage":
|
|
62
78
|
print("[warn] cn-stage 的 wire 取法未验证(spec §8 待核实);仅 cn-prod 全链路已打通。", file=sys.stderr)
|
|
63
79
|
|
package/bin/helpers/cn-deploy.ts
CHANGED
|
@@ -64,6 +64,8 @@ const SERVICES: Record<string, Svc> = {
|
|
|
64
64
|
'optima-sentinel': { repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9', prodSaeAppId: 'e120e70c-b2c1-49ef-a66c-9016fd2d807d' },
|
|
65
65
|
'optima-sentinel-worker': { repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5', prodSaeAppId: 'e0f9b92c-00af-4ade-9b07-15bba48c14cc' },
|
|
66
66
|
'optima-skills': { repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921', prodSaeAppId: '55a63ee7-fb75-46e4-99f3-20854a699237' },
|
|
67
|
+
// prodSaeAppId 待 cn-prod 首发后补(流水线 tiktok-backend-cn-prod 亦未生成,prod 请求会在找流水线一步失败,符合预期)
|
|
68
|
+
'tiktok-backend': { repo: 'tiktok-api-integration', saeAppId: 'd34e8e25-1eab-473b-a01e-8df1375f7330' },
|
|
67
69
|
'user-auth': { repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2', prodSaeAppId: 'd6fbf9de-fed6-4165-978f-7b3a0a456acc' },
|
|
68
70
|
'user-auth-admin': { repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb', prodSaeAppId: '54093f99-37bf-4354-abd7-f6e76abdbcb6' },
|
|
69
71
|
'yzsgo-api': { repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6', prodSaeAppId: '5a820992-e11b-404a-9e58-8f607d8e0c5f' },
|
|
@@ -61,6 +61,8 @@ const SERVICES = {
|
|
|
61
61
|
'optima-sentinel': { repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9', prodSaeAppId: 'e120e70c-b2c1-49ef-a66c-9016fd2d807d' },
|
|
62
62
|
'optima-sentinel-worker': { repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5', prodSaeAppId: 'e0f9b92c-00af-4ade-9b07-15bba48c14cc' },
|
|
63
63
|
'optima-skills': { repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921', prodSaeAppId: '55a63ee7-fb75-46e4-99f3-20854a699237' },
|
|
64
|
+
// prodSaeAppId 待 cn-prod 首发后补(流水线 tiktok-backend-cn-prod 亦未生成,prod 请求会在找流水线一步失败,符合预期)
|
|
65
|
+
'tiktok-backend': { repo: 'tiktok-api-integration', saeAppId: 'd34e8e25-1eab-473b-a01e-8df1375f7330' },
|
|
64
66
|
'user-auth': { repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2', prodSaeAppId: 'd6fbf9de-fed6-4165-978f-7b3a0a456acc' },
|
|
65
67
|
'user-auth-admin': { repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb', prodSaeAppId: '54093f99-37bf-4354-abd7-f6e76abdbcb6' },
|
|
66
68
|
'yzsgo-api': { repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6', prodSaeAppId: '5a820992-e11b-404a-9e58-8f607d8e0c5f' },
|