Phase1 skeleton: replace demo main.py with src.server contract

- Overlay ModelHub XC Phase1 (health/status, STRATEGY_ID gate, dry-run)
- Dockerfile CMD -> python -m src.server; FROM stays platform TCR base
- Remove demo main.py and requirements.txt (stdlib only)
- Add tests/, scripts/, docs/, Makefile, config/examples (no secrets)
This commit is contained in:
VC_noSugar
2026-10-16 15:47:10 +08:00
parent 26351cefee
commit 0d0f3b611a
31 changed files with 1436 additions and 106 deletions

107
scripts/dry_run.py Executable file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Generate a dry-run adaptation report (no platform side effects).
Reads config/candidates.example.json (or DRY_RUN_CANDIDATES path).
Writes reports/dry-run-<Asia/Shanghai-timestamp>.md
"""
from __future__ import annotations
import json
import os
import sys
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
ROOT = Path(__file__).resolve().parents[1]
TZ = ZoneInfo("Asia/Shanghai")
def _load_candidates(path: Path) -> list[dict]:
data = json.loads(path.read_text(encoding="utf-8"))
if isinstance(data, list):
return data
if isinstance(data, dict) and "candidates" in data:
return list(data["candidates"])
raise SystemExit(f"unexpected candidates schema in {path}")
def _render(candidates: list[dict], ts: str, src: Path) -> str:
lines = [
f"# Dry-run 适配建议报告 — {ts} (Asia/Shanghai)",
"",
"> **模式:dry-run** — 本报告不产生平台侧真实提交。",
"> **需 Victor 人工提交**:请登录模盒控制台按草稿确认后手工提交。",
"> **积分:待平台核实**(发布智能体本身不得分;成功适配按账本计)。",
"",
"## 闸门状态",
"",
f"- ALLOW_LIVE_SUBMIT is 1: {os.environ.get('ALLOW_LIVE_SUBMIT', '0').strip() == '1'}",
f"- LIVE_CONFIRM set: {'yes' if os.environ.get('LIVE_CONFIRM', '').strip() else 'no'}",
f"- STRATEGY_ID present: {'yes' if os.environ.get('STRATEGY_ID', '').strip() else 'no'}",
"",
"## 输入",
"",
f"- 候选文件:`{src}`",
f"- 候选条数:{len(candidates)}",
"",
"## 建议草稿(字段示意,非真实 POST)",
"",
]
for i, c in enumerate(candidates, 1):
model = c.get("modelAddress") or c.get("model_id") or "(missing model_id)"
lines.append(f"### {i}. `{model}`")
lines.append("")
lines.append("```json")
draft = {
"modelAddress": model,
"taskType": c.get("taskType", "text-generation"),
"targetGpu": c.get("targetGpu", "(unset)"),
"framework": c.get("framework", "(unset)"),
"strategyId": "<PLATFORM_INJECTED_STRATEGY_ID>",
"configParams": "<from build-config — not fetched in Phase1 dry-run>",
"notes": c.get("notes", ""),
}
lines.append(json.dumps(draft, ensure_ascii=False, indent=2))
lines.append("```")
lines.append("")
lines.append("- 风险:Phase1 未做唯一性/硬门预检;提交前请对照 do-not-resubmit 与队列空位。")
lines.append("- **需 Victor 人工提交**(或经双重闸门的受控 live;本骨架默认拒绝 live)。")
lines.append("- **积分:待平台核实**")
lines.append("")
lines.extend(
[
"## 后续对接(Phase2)",
"",
"- **待对接 pending-submit**:尚未读取 `/workspace/modelhub-adapt/pending-submit.md`,未验证 A→B、PASS、硬门与 do-not-resubmit;本报告仅保留输入顺序。",
"- 适配 W1 优先;同一 100 队列,≥95 全体停交;空位先 A 再 B,人工核验后决定,不凭示例报告提交。",
"- 禁止 Makefile / 脚本调用 `/api/adapt/task/add` 或 `/agent_platform/strategies*` 写接口。",
"",
]
)
return "\n".join(lines)
def main() -> None:
src = Path(
os.environ.get(
"DRY_RUN_CANDIDATES",
str(ROOT / "config" / "candidates.example.json"),
)
)
if not src.is_file():
raise SystemExit(f"candidates file not found: {src}")
candidates = _load_candidates(src)
now = datetime.now(TZ)
ts = now.strftime("%Y%m%d-%H%M%S")
out_dir = ROOT / "reports"
out_dir.mkdir(parents=True, exist_ok=True)
out_path = out_dir / f"dry-run-{ts}.md"
out_path.write_text(_render(candidates, now.strftime("%Y-%m-%d %H:%M:%S"), src), encoding="utf-8")
print(f"Wrote {out_path}")
if __name__ == "__main__":
main()

100
scripts/smoke_health.py Executable file
View File

@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Smoke: GET /health must return 200 {"status":"ok"}.
Usage:
# Against already-running server:
SMOKE_BASE_URL=http://127.0.0.1:8080 python3 scripts/smoke_health.py
# Or let this script start a local server (needs STRATEGY_ID):
STRATEGY_ID=test python3 scripts/smoke_health.py
"""
from __future__ import annotations
import json
import os
import signal
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _get(url: str, timeout: float = 2.0) -> tuple[int, dict]:
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = resp.read().decode("utf-8")
return resp.status, json.loads(body)
def _wait_health(base: str, attempts: int = 30) -> None:
url = base.rstrip("/") + "/health"
last_err: Exception | None = None
for _ in range(attempts):
try:
code, data = _get(url)
if code == 200 and data.get("status") == "ok":
print(f"OK: GET {url} -> {code} {data}")
return
raise AssertionError(f"unexpected response: {code} {data}")
except (urllib.error.URLError, ConnectionError, AssertionError, TimeoutError) as exc:
last_err = exc
time.sleep(0.2)
raise SystemExit(f"smoke-health failed: {last_err}")
def main() -> None:
external = os.environ.get("SMOKE_BASE_URL", "").strip()
if external:
_wait_health(external)
# optional status check
try:
code, data = _get(external.rstrip("/") + "/status")
print(f"OK: GET /status -> {code} {data}")
except Exception as exc: # noqa: BLE001
print(f"WARN: /status check skipped: {exc}", file=sys.stderr)
return
if not os.environ.get("STRATEGY_ID", "").strip():
os.environ["STRATEGY_ID"] = "smoke-test-strategy"
port = os.environ.get("SMOKE_PORT", "18080")
env = os.environ.copy()
env["PORT"] = port
env["HOST"] = "127.0.0.1"
env["STRATEGY_ID"] = env.get("STRATEGY_ID") or "smoke-test-strategy"
proc = subprocess.Popen(
[sys.executable, "-m", "src.server"],
cwd=str(ROOT),
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
try:
_wait_health(f"http://127.0.0.1:{port}")
code, data = _get(f"http://127.0.0.1:{port}/status")
print(f"OK: GET /status -> {code} {data}")
assert "strategy_id_present" in data
assert data["strategy_id_present"] is True
# never assert on secret values — only booleans
finally:
proc.send_signal(signal.SIGTERM)
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=2)
if proc.returncode not in (0, -signal.SIGTERM, signal.SIGTERM):
# 0 or terminated by SIGTERM is fine
err = (proc.stderr.read() or b"").decode("utf-8", errors="replace")
if proc.returncode is not None and proc.returncode > 0:
print(f"WARN: server exit code {proc.returncode}: {err}", file=sys.stderr)
if __name__ == "__main__":
main()

43
scripts/tag_release.py Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Create a local git tag only. Does NOT push. Does NOT register on ModelHub.
Usage:
python3 scripts/tag_release.py v0.1.0
python3 scripts/tag_release.py v0.1.0 --message "Phase1 skeleton"
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def main() -> None:
parser = argparse.ArgumentParser(description="Local git tag only (no push, no register)")
parser.add_argument("tag", help="Tag name, e.g. v0.1.0")
parser.add_argument("--message", "-m", default="", help="Annotated tag message")
args = parser.parse_args()
if not (ROOT / ".git").exists():
print(
"WARN: not a git repository yet. Init locally if needed; "
"this script will not push or register.",
file=sys.stderr,
)
# Still attempt so the failure is clear
msg = args.message or f"Release {args.tag} (local tag only — no push / no ModelHub register)"
cmd = ["git", "tag", "-a", args.tag, "-m", msg]
print(f"Running (cwd={ROOT}): {' '.join(cmd)}")
print("NOTE: This does NOT git push and does NOT call ModelHub APIs.")
result = subprocess.run(cmd, cwd=str(ROOT))
if result.returncode != 0:
raise SystemExit(result.returncode)
print(f"OK: local tag {args.tag!r} created. Push tag yourself when ready for Kaniko.")
if __name__ == "__main__":
main()