chore: 工作树散落活账整理提交(多工作线)

- .gitignore:忽略 AgentScope 2.0.2 源码克隆(.claude/skills/agentscope-skill/agentscope/,本机开发直读参考、~17MB、含自带 .git、非本仓产物、可重克隆)
- .claude/skills/agentscope-skill/SKILL.md:agentscope skill 更新
- contracts/prompts/04-config/cheap-system.md:便宜档 system prompt 加「核心操作非无脑」第9条好玩自检(否决项:⑨不命中则①-⑧全中也只是有元素的无趣游戏)+ 决策层与九门自动验收解耦(基础分保盲驱动器过门 / 技巧分给真人爽感)
- game-runtime/evidence/integration/evidence.json:集成 evidence 更新
- game-studio/public/mock-manifests/:U5+U2 真 UI 走查 runbook + walk 脚本 + mock manifest
- wg1/gen-worker/results/gamedef-quickcheck.json:bake-off 结果(同既有 27 tracked 同类)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-01 19:00:42 -07:00
parent a164f4c222
commit 8ea972340e
8 changed files with 531 additions and 13 deletions

View File

@ -138,16 +138,18 @@ This section provides guidance on how to effectively answer questions about Agen
**CRITICAL**: Before doing anything else, clone or update the AgentScope repository. The repository contains essential examples and references. **CRITICAL**: Before doing anything else, clone or update the AgentScope repository. The repository contains essential examples and references.
```bash ```bash
# Clone into this skill directory so that you can refer to it across different sessions # Clone the tag matching the INSTALLED version (not main — avoid source-vs-runtime drift).
cd /path/to/this/skill/directory # Installed version lives under cheap-worker/.venv/.../site-packages/ (currently agentscope 2.0.2).
git clone -b main https://github.com/agentscope-ai/agentscope.git # Already cloned for v2.0.2; the dir is gitignored and reused across sessions.
git clone --depth 1 -b v2.0.2 https://github.com/agentscope-ai/agentscope.git /path/to/this/skill/directory/agentscope
# Or update if already cloned # When the installed version bumps (check .venv dist-info or agentscope/src/agentscope/_version.py),
cd /path/to/this/skill/directory/agentscope # re-clone the new tag rather than `git pull` (the clone is a detached tag checkout):
git pull # rm -rf /path/to/this/skill/directory/agentscope
# git clone --depth 1 -b vX.Y.Z https://github.com/agentscope-ai/agentscope.git /path/to/this/skill/directory/agentscope
``` ```
**Why this matters**: The repository contains working examples, complete API documentation in source code, and implementation patterns that are more reliable than guessing. **Why this matters**: the repo carries working examples and complete API source, more reliable than guessing — and pinning to the installed tag keeps what you read identical to what runs (this project verifies behavior against the installed 2.0.2; `main` may be ahead and mislead).
### Step 2: Understand the Repository Structure ### Step 2: Understand the Repository Structure
The cloned repository is organized as follows. Note this may be outdated as the project evolves, you should always check the actual structure after cloning. The cloned repository is organized as follows. Note this may be outdated as the project evolves, you should always check the actual structure after cloning.
@ -312,4 +314,4 @@ Located in `references/` directory of this skill.
- **`v2-deploy.md`**: Deploying agents as a service — `agent-service` (multi-tenant, multi-session HTTP service) and `agent-team` (leader/worker team tools). - **`v2-deploy.md`**: Deploying agents as a service — `agent-service` (multi-tenant, multi-session HTTP service) and `agent-team` (leader/worker team tools).
- **`studio-rest-api.md`**: Condensed REST API reference for the hosted Studio / Agent Service (29 endpoints across agent/chat/credential/model/schedule/sessions/workspace) plus the key data models. Use when integrating with the hosted service over HTTP. - **`studio-rest-api.md`**: Condensed REST API reference for the hosted Studio / Agent Service (29 endpoints across agent/chat/credential/model/schedule/sessions/workspace) plus the key data models. Use when integrating with the hosted service over HTTP.
> Note (project context): in this repo AgentScope is a **long-term exploratory track**, not the MVP mainline (the agentic mainline is SAA / Spring AI Alibaba in Java). For implementation details beyond these docs, the full source clones under `/root/oss/agentscope*` are the highest authority. > Note (project context · 2026-06-25 reframe): in this repo AgentScope is the **unified generation framework across all three tiers** (Tier0/1/2), reached via the AgentScope Service `/chat`; SAA / Spring AI Alibaba is de-prioritized to a long-term pluggability-validation target, **no longer the mainline**. Project-specific AgentScope facts (per-POST hot-config, the `/agent`+`/session`+`/workspace` config surfaces, the service-mode `_extract_function_tools` mcp-discard gap, cheap-worker/tier2 wiring) live in [`.agents/knowledge/agentscope-2.0-facts.md`](../../../.agents/knowledge/agentscope-2.0-facts.md). Installed source of truth = the `agentscope` package under `cheap-worker/.venv` (v2.0.2); a full repo clone additionally provides `examples/`.

6
.gitignore vendored
View File

@ -94,3 +94,9 @@ game-runtime/games/amgen-smoke*/
# M2 auto-vs-golden 同款双驱动批跑产物(avg- 前缀,可重生成、不入库) # M2 auto-vs-golden 同款双驱动批跑产物(avg- 前缀,可重生成、不入库)
game-runtime/games/amgen-avg-*/ game-runtime/games/amgen-avg-*/
game-runtime/games/_wg1-gen/avg-*/ game-runtime/games/_wg1-gen/avg-*/
# ── AgentScope 源码库(本机开发直读参考,克隆自 github tag v2.0.2 = 已装运行版,2026-07-01)──
# clone 到 skill 目录供本机直接读 agentscope 2.0.2 源码(src/ + 已装 .venv 没有的 examples/docs/tests);
# 非本仓产物、~17MB、含自带 .git、可随时重克隆,不入库。遵「专一不笼统」:只忽略这一个克隆子目录,
# 不碰 skill 其余已跟踪文件(SKILL.md / references/ / scripts/ 共 9 文件)。版本要与 cheap-worker/.venv 装的对齐。
.claude/skills/agentscope-skill/agentscope/

View File

@ -22,16 +22,17 @@ version: 1.0.0
【先设计后写码:决定这游戏好不好玩(关键,别跳过)】 【先设计后写码:决定这游戏好不好玩(关键,别跳过)】
能跑 ≠ 好玩。动手写码前,先据 brief 在心里(或 game-logic.js 顶部一小段注释里)定一份**轻量玩法设计**,再实现: 能跑 ≠ 好玩。动手写码前,先据 brief 在心里(或 game-logic.js 顶部一小段注释里)定一份**轻量玩法设计**,再实现:
- **核心循环**(一句话:玩家做什么 → 得什么即时反馈 → 怎么变强); - **核心循环(好玩第一因 · 必含一层玩家决策或技巧)**:写清「玩家每次操作做**什么决策** → 得什么即时反馈 → 怎么变强」。**铁律:核心操作绝不能是「点了自动结算」的无脑点击**,必须让真人有发挥——三选一起步:**匹配**(顾客点指定菜、上对的才给分/给多分)、**时机**(在对的时刻操作 = 更多奖励)、**取舍/连击**(连续做对叠 combo、资源有限要权衡)。自检:把玩家换成「闭眼乱点」——若分数照样拿满,这循环就是无趣的,重设计。
- **决策层与自动验收解耦(工程现实,别绕开)**:九门自动验收会盲点 `occupied:true` 目标、判 score 涨,所以做成**两层奖励**——**基础分**(任意有效操作都加一点,保盲驱动器能跑通过门)+ **技巧分**(叠在玩家的匹配/时机/连击上,真人靠它玩出爽感)。盲驱动器拿基础分过门、真人靠技巧拿高分,两不耽误。**别为「让驱动器满分」把玩法做成无脑点;也别「不做对就零分」让驱动器过不了门。**
- **资源环**(经营/放置类必含):「进货 → 库存 → 售卖收钱 → 缺货补货」的软币循环,制造「赚→进→卖→再赚」的张力; - **资源环**(经营/放置类必含):「进货 → 库存 → 售卖收钱 → 缺货补货」的软币循环,制造「赚→进→卖→再赚」的张力;
- **3–4 级解锁阶梯**:攒够阈值解锁新商品/区域/能力,任意时刻都露出「下一个锁」; - **3–4 级解锁阶梯**:攒够阈值解锁新商品/区域/能力,任意时刻都露出「下一个锁」;
- **数值成长**:产出/成本随级上升、略带滚雪球感,别平淡线性; - **数值成长**:产出/成本随级上升、略带滚雪球感,别平淡线性;
- **音效清单**:收钱「叮」/ 升级欢呼 / 解锁号角(经 plugins.audioMusic;宁可程序化也别没有反馈音)。 - **音效清单**:收钱「叮」/ 升级欢呼 / 解锁号角(经 plugins.audioMusic;宁可程序化也别没有反馈音)。
定完过一遍 sim-business §10 的 **8 条好玩自检**(①即时反馈 ②可见成长 ③下一个解锁 ④30 秒内首次升级/解锁 ⑤数值滚雪球 ⑥情感锚〔萌角色/拥有物〕 ⑦放置回归惊喜 ⑧音反馈)——命中越多越好玩,命中 ≤3 ≈ 能玩但无趣。 定完过一遍 **9 条好玩自检**(①即时反馈 ②可见成长 ③下一个解锁 ④30 秒内首次升级/解锁 ⑤数值滚雪球 ⑥情感锚〔萌角色/拥有物〕 ⑦放置回归惊喜 ⑧音反馈 ⑨**核心操作非无脑**〔每次主操作有真实的决策/技巧含量,不是点了自动结算〕)——**⑨ 是否决项:⑨ 不命中,①–⑧ 全中也只是「有元素的无趣游戏」**(实测一款 6/8 命中却不好玩,正死在 ⑨);⑨ 命中的前提下,其余命中越多越好玩。
**8 条自检对所有品类通用**(动作/消除/跑酷也照它要即时反馈 + 可见成长 + 音反馈);**经营/养成/放置/点客类**再按 sim-business 取资源环/解锁阶梯/客流节奏范式。 **8 条自检对所有品类通用**(动作/消除/跑酷也照它要即时反馈 + 可见成长 + 音反馈);**经营/养成/放置/点客类**再按 sim-business 取资源环/解锁阶梯/客流节奏范式。
【MVP-first 铁律(钉死·关乎你能不能收敛,别一稿堆满)】 【MVP-first 铁律(钉死·关乎你能不能收敛,别一稿堆满)】
设计太满 → 你实现负担过重 → read/write 反复跳、跑不收敛(实测:满配设计循环截停、精简设计 9 步收敛)。首版**只做可玩核心**:核心循环 + 1 个主机制 + 1 个资源环(进货)+ 3–4 级解锁 + 基础数值/音效,**商品 ≤3 种起步**。**离线收益 / 看广告位 / 雇员 / 多档 BGM / hitstop / 6+ 商品 一律标「后续·MVP 不做」**、别塞进首版。**先出能玩的核心,再谈丰富。** 设计太满 → 你实现负担过重 → read/write 反复跳、跑不收敛(实测:满配设计循环截停、精简设计 9 步收敛)。首版**只做可玩核心**:核心循环 + 1 个主机制 + 1 个资源环(进货)+ 3–4 级解锁 + 基础数值/音效,**商品 ≤3 种起步**。**离线收益 / 看广告位 / 雇员 / 多档 BGM / hitstop / 6+ 商品 一律标「后续·MVP 不做」**、别塞进首版。**先出能玩的核心,再谈丰富。** **但钉死:「可玩核心」= 一个有决策深度的核心机制做透,不是一个浅机制 + 一堆 meta 元素(解锁/升级/飘字)。要砍的是商品数 / 附加系统 / 离线雇员;绝不砍核心循环那一层玩家决策(见上「核心循环」铁律)——那是 MVP 的心脏,砍了就只剩无趣骨架。**
【步骤】 【步骤】
A 读手册(1+5)+ 读你的起点 game-logic.js(4)→ B 据 brief 定**轻量玩法设计**(核心循环 / 资源环 / 解锁阶梯 / 数值 / 音效,过 §10 八条自检 + 守 MVP-first)→ C 读要用的插件 api.d.ts → D write_file 改写 **game-logic.js**(按需拆 core/render)实现玩法 → E 调 check,有错改到 PASS → F 调 build,有错改到 PASS → G check+build 都绿后调 finish。 A 读手册(1+5)+ 读你的起点 game-logic.js(4)→ B 据 brief 定**轻量玩法设计**(核心循环 / 资源环 / 解锁阶梯 / 数值 / 音效,过 §10 八条自检 + 守 MVP-first)→ C 读要用的插件 api.d.ts → D write_file 改写 **game-logic.js**(按需拆 core/render)实现玩法 → E 调 check,有错改到 PASS → F 调 build,有错改到 PASS → G check+build 都绿后调 finish。

View File

@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"ts": "2026-06-13T05:14:05.317Z", "ts": "2026-06-21T08:51:55.806Z",
"base": "http://localhost:4321", "base": "http://localhost:4321",
"cdpHttp": "http://localhost:9222", "cdpHttp": "http://localhost:9222",
"seed": 305441741, "seed": 305441741,
@ -20,7 +20,7 @@
"vfx": false, "vfx": false,
"firstInput": false "firstInput": false
}, },
"fatal": "AggregateError", "fatal": "[browser-evidence] 等 host 引导超时(window.__hostBooted 未就绪)",
"failures": [], "failures": [],
"pass": false "pass": false
} }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,54 @@
#!/usr/bin/env bash
# U5+U2 真 UI 走查 runbook(mini-desktop 本地执行)。
# chrome 生命周期:起 headless chrome(:9222) → 探活 → 跑 python 走查 → 杀 chrome 净场。
# 自身输出重定向到 /tmp/u5u2_out.txt(CDP §2 坑5:避免继承 ssh fd 被断连杀;本次一次性同步跑,输出兜底落文件)。
set -u
exec >/tmp/u5u2_out.txt 2>&1
echo "=== RUN_U5U2 START $(date -Is) ==="
SHOT=/tmp/u5u2
mkdir -p "$SHOT"
CHROME=/usr/bin/google-chrome
PROF=/tmp/u5u2_chrome_prof
rm -rf "$PROF"; mkdir -p "$PROF"
# 先清掉可能残留的 9222 chrome
pkill -f "remote-debugging-port=9222" 2>/dev/null || true
sleep 1
# 起 headless chrome(CDP §2:no-sandbox + no-zygote + remote-allow-origins=*)
"$CHROME" \
--headless=new --no-sandbox --no-zygote \
--remote-debugging-port=9222 --remote-allow-origins=* \
--user-data-dir="$PROF" \
--window-size=420,900 \
--autoplay-policy=no-user-gesture-required \
about:blank >/tmp/u5u2_chrome.log 2>&1 &
CHROME_PID=$!
echo "chrome pid=$CHROME_PID"
# 探活:起后必 curl /json/version 验活再用(最多 ~10s)
UP=0
for i in $(seq 1 20); do
if curl -s --max-time 2 http://localhost:9222/json/version >/dev/null 2>&1; then
UP=1; echo "chrome :9222 UP after ${i} tries"; break
fi
sleep 0.5
done
if [ "$UP" != "1" ]; then
echo "FATAL: chrome :9222 not up"; cat /tmp/u5u2_chrome.log; kill "$CHROME_PID" 2>/dev/null; exit 1
fi
curl -s --max-time 3 http://localhost:9222/json/version | head -c 200; echo
# 跑走查
echo "=== PYTHON WALK ==="
python3 /tmp/u5u2_walk.py
RC=$?
echo "=== PYTHON RC=$RC ==="
# 净场
kill "$CHROME_PID" 2>/dev/null || true
pkill -f "remote-debugging-port=9222" 2>/dev/null || true
echo "=== screenshots ==="
ls -la "$SHOT"
echo "=== RUN_U5U2 DONE $(date -Is) rc=$RC ==="

View File

@ -0,0 +1,392 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
U5(8 页走查) + U2(审核→发布) 真 UI 走查 —— 单文件 CDP 客户端(websocket-client 直驱)。
在 mini-desktop 本地驱动 headless chrome(:9222) 走 localhost 前端(staging-ops §1 认可的 e2e 门):
U5 studio(:4173) 6 页:feed流 / 试玩(play) / 创作(create,draft) / 创作者主页(creator) / 素材中心(material) / 玩法中心(template)
U5 admin (:4174) 2 页:经营看板(operation-dashboard) / 审核队列(review)
U2 admin 审核→发布:审核队列点「通过」approve 真实待审游戏(已 seed 9324 saa-e2e-breakout) → 验转发布态。
逐页断言(每页落 PAGE_VERDICT):
- 关键元素渲染(DOM 实查 selector)
- 控制台零未捕获异常(Runtime.exceptionThrown / console.error 累计)
- 零 5xx(Network.responseReceived 累计状态码 >=500)
- 真数据(非空壳:列表/卡片/指标计数 > 0)
鉴权:studio 受保护页用 localStorage 注入 test1 token(key=wanxiang_token,见 store/user.ts)过 requiresAuth 守卫(只读走查口径);
tenant-id=1 由前端请求拦截器无条件注入,无需手动设。admin 走真实登录(芋道源码/admin/admin123)。
不改后端/DB(seed 已在脚本外经真实 publish 路径完成),仅 UI 走查取证。
"""
import json, base64, time, sys, urllib.request
import websocket # websocket-client
CDP_HTTP = "http://localhost:9222"
STUDIO = "http://localhost:4173"
ADMIN = "http://localhost:4174"
SHOT_DIR = "/tmp/u5u2"
STUDIO_TOKEN = "test1"
# U2 seed: 经真实 publish 路径已置 9324(saa-e2e-breakout) 为 status=1 审核中
U2_TARGET_NAME = "saa-e2e-breakout"
U2_TARGET_ID = "9324"
import os
os.makedirs(SHOT_DIR, exist_ok=True)
def new_target(url="about:blank"):
"""Chrome 111+ 必须用 PUT /json/new 建 target。"""
req = urllib.request.Request(f"{CDP_HTTP}/json/new?{url}", method="PUT")
d = json.load(urllib.request.urlopen(req, timeout=10))
return d["webSocketDebuggerUrl"], d["id"]
class CDP:
def __init__(self, ws_url):
self.ws = websocket.create_connection(ws_url, max_size=None, timeout=45,
suppress_origin=True)
self._id = 0
self.console = [] # 累积 console error / exception
self.http5xx = [] # 累积 5xx 响应
self.http4xx = [] # 累积 4xx 响应(参考,不计入 pass)
def _collect(self, msg):
m = msg.get("method")
if m == "Log.entryAdded":
e = msg["params"]["entry"]
if e.get("level") == "error":
self.console.append({"src": "log", "level": "error", "text": (e.get("text") or "")[:240]})
elif m == "Runtime.consoleAPICalled":
t = msg["params"].get("type")
if t == "error":
args = msg["params"].get("args", [])
txt = " ".join(str(a.get("value", a.get("description", ""))) for a in args)[:240]
self.console.append({"src": "console", "level": "error", "text": txt})
elif m == "Runtime.exceptionThrown":
d = msg["params"].get("exceptionDetails", {})
self.console.append({"src": "exception", "level": "error",
"text": (d.get("text") or json.dumps(d.get("exception", {}), ensure_ascii=False))[:240]})
elif m == "Network.responseReceived":
r = msg["params"].get("response", {})
st = r.get("status", 0)
url = (r.get("url") or "")[:160]
if st >= 500:
self.http5xx.append({"status": st, "url": url})
elif 400 <= st < 500:
self.http4xx.append({"status": st, "url": url})
def send(self, method, params=None, timeout=45):
self._id += 1
mid = self._id
self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
end = time.time() + timeout
while time.time() < end:
self.ws.settimeout(max(0.1, end - time.time()))
try:
msg = json.loads(self.ws.recv())
except Exception:
continue
self._collect(msg)
if msg.get("id") == mid:
if "error" in msg:
raise RuntimeError(f"{method}: {msg['error']}")
return msg.get("result", {})
raise TimeoutError(method)
def drain(self, secs=1.0):
end = time.time() + secs
while time.time() < end:
self.ws.settimeout(max(0.05, end - time.time()))
try:
msg = json.loads(self.ws.recv())
except Exception:
continue
self._collect(msg)
def evaljs(self, expr, timeout=45):
r = self.send("Runtime.evaluate",
{"expression": expr, "returnByValue": True, "awaitPromise": True}, timeout)
if "exceptionDetails" in r:
return {"__js_error__": json.dumps(r.get("exceptionDetails"), ensure_ascii=False)[:300]}
return r.get("result", {}).get("value")
def nav(self, url, settle=5):
self.send("Page.navigate", {"url": url})
time.sleep(settle)
self.drain(0.8)
def shot(self, name):
r = self.send("Page.captureScreenshot", {"format": "png"})
path = f"{SHOT_DIR}/{name}"
with open(path, "wb") as f:
f.write(base64.b64decode(r["data"]))
return path
def reset_net_log(self):
"""每页走查前清网络/console 累计,使断言只针对当前页。"""
self.console = []
self.http5xx = []
self.http4xx = []
def close(self):
try:
self.ws.close()
except Exception:
pass
# ---------- JS 片段 ----------
# 注入 studio 登录态(test1):必须在 :4173 同 origin 已开 target 内 setItem(localStorage 按 origin 隔离)
def js_inject_studio_token():
return ("(()=>{try{"
f"localStorage.setItem('wanxiang_token','{STUDIO_TOKEN}');"
f"localStorage.setItem('wanxiang_user_id','1');"
"localStorage.setItem('wanxiang_creator_flag','1');"
"localStorage.setItem('wanxiang_nickname','走查创作者');"
"return 'token-set:'+localStorage.getItem('wanxiang_token');"
"}catch(e){return 'err:'+e.message}})()")
# 通用页面信息 dump
JS_DUMP = r"""(()=>{
const btns=[...document.querySelectorAll('button')].map(e=>(e.innerText||'').replace(/\s+/g,'')).filter(Boolean).slice(0,30);
return {href:location.href, title:document.title, buttons:btns,
bodyText:(document.body.innerText||'').replace(/\s+/g,' ').slice(0,400)};
})()"""
# 计数若干 selector 命中数(真数据/渲染断言用);返回 {sel:count}
def js_count(selectors):
arr = json.dumps(selectors)
return (r"""(()=>{const sels=%s;const o={};
for(const s of sels){try{o[s]=document.querySelectorAll(s).length}catch(e){o[s]=-1}}
o.__href=location.href; o.__bodyLen=(document.body.innerText||'').replace(/\s+/g,'').length;
return o;})()""" % arr)
# admin el-table 行 dump(审核队列用)
JS_QUEUE = r"""(()=>{
const rows=[...document.querySelectorAll('.el-table__row')].map(r=>{
const cells=[...r.querySelectorAll('td')].map(td=>(td.innerText||'').trim().replace(/\s+/g,' ').slice(0,40));
const btns=[...r.querySelectorAll('button')].map(b=>(b.innerText||'').replace(/\s+/g,''));
return {cells, btns};
});
const empty=document.querySelector('.el-table__empty-text');
return {href:location.href, rowCount:rows.length, rows:rows.slice(0,8),
emptyText:empty?(empty.innerText||'').trim():'',
bodyText:(document.body.innerText||'').replace(/\s+/g,' ').slice(0,260)};
})()"""
JS_DUMP_LOGIN = r"""(()=>{
const inp=[...document.querySelectorAll('input')].map(e=>({type:e.type,ph:e.placeholder||''}));
const btn=[...document.querySelectorAll('button')].map(e=>(e.innerText||'').replace(/\s+/g,'')).filter(Boolean);
return {href:location.href, inputs:inp, buttons:btn};
})()"""
JS_CLICK_LOGIN = r"""(()=>{
const b=[...document.querySelectorAll('button')].find(e=>(e.innerText||'').replace(/\s+/g,'')==='登录');
if(b){b.click();return 'clicked-login';}
return 'no-login-btn';
})()"""
def js_click_pass(name):
return r"""(()=>{
const rows=[...document.querySelectorAll('.el-table__row')];
for(const r of rows){
if((r.innerText||'').includes('%s')){
const p=[...r.querySelectorAll('button')].find(b=>(b.innerText||'').replace(/\s+/g,'')==='通过');
if(p){p.click();return 'clicked-pass';}
return 'row-matched-no-pass-btn|btns:'+[...r.querySelectorAll('button')].map(b=>(b.innerText||'').replace(/\s+/g,'')).join(',');
}
}
return 'target-not-in-rows|count:'+rows.length;
})()""" % name
JS_CONFIRM = r"""(()=>{
const btns=[...document.querySelectorAll('.el-message-box button,.el-overlay button,.el-dialog button')];
const ok=btns.find(b=>{const t=(b.innerText||'').replace(/\s+/g,'');return t==='确定'||t==='确认';});
if(ok){ok.click();return 'confirmed';}
return 'no-confirm|btns:'+btns.map(b=>(b.innerText||'').replace(/\s+/g,'')).join(',');
})()"""
def out(tag, obj):
print(f"@@{tag}@@ " + json.dumps(obj, ensure_ascii=False), flush=True)
def page_verdict(c, page, counts, render_ok, data_ok, extra=None):
"""汇总单页断言:渲染 / console 零异常 / 零 5xx / 真数据。"""
v = {
"page": page,
"url": counts.get("__href", ""),
"render_ok": render_ok,
"console_errors": c.console[:8],
"console_error_count": len(c.console),
"http5xx": c.http5xx[:6],
"http5xx_count": len(c.http5xx),
"data_ok": data_ok,
"counts": {k: v2 for k, v2 in counts.items() if not k.startswith("__")},
"pass": bool(render_ok and data_ok and len(c.console) == 0 and len(c.http5xx) == 0),
}
if extra:
v["extra"] = extra
out("PAGE_VERDICT", v)
return v
def main():
ws_url, tid = new_target("about:blank")
c = CDP(ws_url)
c.send("Page.enable")
c.send("Runtime.enable")
c.send("Log.enable")
c.send("Network.enable")
verdicts = []
# ======================================================================
# STUDIO 段:先建 :4173 origin 注入 token,再逐页走
# ======================================================================
c.nav(f"{STUDIO}/feed", settle=6) # 先到 feed(无需登录)建立 origin
print("INJECT_TOKEN:", c.evaljs(js_inject_studio_token()), flush=True)
# ---- U5-1 feed 流 ----
c.reset_net_log()
c.nav(f"{STUDIO}/feed", settle=7)
c.shot("u5_1_feed.png")
counts = c.evaljs(js_count([".feed", ".feed__zone", ".feed__page", ".game-card,[class*=card]"]))
out("FEED_DUMP", c.evaljs(JS_DUMP))
render = (counts.get(".feed", 0) > 0)
data = (counts.get(".feed__page", 0) > 0) # 至少一张卡片页
verdicts.append(page_verdict(c, "studio/feed", counts, render, data))
# ---- U5-2 试玩 play(已发布游戏 9306/93112) ----
c.reset_net_log()
c.nav(f"{STUDIO}/play/9306/93112", settle=9) # 试玩页含 iframe 宿主,留足加载时间
c.shot("u5_2_play.png")
counts = c.evaljs(js_count([".play,[class*=play]", "iframe", ".loading-bar,[class*=loading]", "[class*=error]"]))
out("PLAY_DUMP", c.evaljs(JS_DUMP))
# 渲染=有 iframe 宿主挂载;真数据=非错误态(无 error 容器主导) + iframe 在
render = (counts.get("iframe", 0) > 0)
data = (counts.get("iframe", 0) > 0)
verdicts.append(page_verdict(c, "studio/play", counts, render, data,
extra={"note": "试玩=GamePlayer iframe 宿主挂载;游戏内真玩另见 game-e2e harness"}))
# ---- U5-3 创作 create(draft,requiresAuth) ----
c.reset_net_log()
c.nav(f"{STUDIO}/create", settle=7)
c.shot("u5_3_create.png")
counts = c.evaljs(js_count([".create-page", ".create-head", "textarea", "button"]))
out("CREATE_DUMP", c.evaljs(JS_DUMP))
href = counts.get("__href", "")
render = (counts.get(".create-page", 0) > 0) and ("/login" not in href)
data = (counts.get("textarea", 0) > 0) # 创作输入框在=页面真渲染(非被守卫弹回登录)
verdicts.append(page_verdict(c, "studio/create", counts, render, data,
extra={"guard_bounced_to_login": "/login" in href}))
# ---- U5-4 创作者主页 creator/1(PUBLIC,9 作品) ----
c.reset_net_log()
c.nav(f"{STUDIO}/creator/1", settle=7)
c.shot("u5_4_creator.png")
counts = c.evaljs(js_count([".creator", ".creator__stats", ".creator__stat",
".game-card,[class*=card],[class*=work]", "[class*=empty]"]))
out("CREATOR_DUMP", c.evaljs(JS_DUMP))
render = (counts.get(".creator", 0) > 0)
data = (counts.get(".creator__stat", 0) > 0) # 统计卡(发布数/播放/点赞)渲染=真数据
verdicts.append(page_verdict(c, "studio/creator", counts, render, data))
# ---- U5-5 素材中心 material(requiresAuth) ----
c.reset_net_log()
c.nav(f"{STUDIO}/studio/material", settle=7)
c.shot("u5_5_material.png")
counts = c.evaljs(js_count([".material", ".material__head", ".material__tabs",
".material__upload", "[class*=empty]", "[class*=asset],[class*=grid] img,[class*=item]"]))
out("MATERIAL_DUMP", c.evaljs(JS_DUMP))
href = counts.get("__href", "")
render = (counts.get(".material", 0) > 0) and ("/login" not in href)
# 素材库可能为空(空态=合法非空壳);真渲染=头部+上传入口在
data = (counts.get(".material__head", 0) > 0 and counts.get(".material__upload", 0) > 0)
verdicts.append(page_verdict(c, "studio/material", counts, render, data,
extra={"guard_bounced_to_login": "/login" in href,
"note": "素材库空态合法;断言取头部+上传入口渲染"}))
# ---- U5-6 玩法中心 template(requiresAuth,5 品类) ----
c.reset_net_log()
c.nav(f"{STUDIO}/studio/template", settle=7)
c.shot("u5_6_template.png")
counts = c.evaljs(js_count([".tpl", ".tpl__head", ".tpl__list",
".tpl__card,[class*=card],[class*=item]", "[class*=empty]"]))
out("TEMPLATE_DUMP", c.evaljs(JS_DUMP))
href = counts.get("__href", "")
render = (counts.get(".tpl", 0) > 0) and ("/login" not in href)
data = (counts.get(".tpl__list", 0) > 0) # 模板列表容器在(5 品类)
verdicts.append(page_verdict(c, "studio/template", counts, render, data,
extra={"guard_bounced_to_login": "/login" in href}))
# ======================================================================
# ADMIN 段:真实登录 → 经营看板 + 审核队列(U5) → U2 审核通过
# ======================================================================
c.nav(f"{ADMIN}/", settle=8)
c.shot("u5_admin_login.png")
out("ADMIN_LOGIN_PAGE", c.evaljs(JS_DUMP_LOGIN))
print("ADMIN_CLICK_LOGIN:", c.evaljs(JS_CLICK_LOGIN), flush=True)
time.sleep(7)
c.drain(1.0)
print("ADMIN_AFTER_LOGIN_HREF:", c.evaljs("location.href"), flush=True)
# ---- U5-7 经营看板 operation-dashboard(跨模块 DAU/GMV/生成量/发布量) ----
c.reset_net_log()
c.nav(f"{ADMIN}/wanxiang/operation-dashboard", settle=8)
c.shot("u5_7_operation_dashboard.png")
counts = c.evaljs(js_count(["[class*=card]", "[class*=statistic],[class*=metric]",
".el-card", "canvas,[class*=chart],[class*=echarts]", "[class*=empty]"]))
out("OPDASH_DUMP", c.evaljs(JS_DUMP))
href = counts.get("__href", "")
render = ("/login" not in href) and (counts.get("__bodyLen", 0) > 20)
data = (counts.get(".el-card", 0) > 0 or counts.get("[class*=card]", 0) > 0
or counts.get("[class*=statistic],[class*=metric]", 0) > 0)
verdicts.append(page_verdict(c, "admin/operation-dashboard", counts, render, data,
extra={"guard_bounced_to_login": "/login" in href}))
# ---- U5-8 审核队列 review(含 U2 seed 待审游戏) ----
c.reset_net_log()
c.nav(f"{ADMIN}/wanxiang/review", settle=8)
c.shot("u5_8_review.png")
q = c.evaljs(JS_QUEUE)
counts = c.evaljs(js_count([".el-table", ".el-table__row", ".el-table__empty-text"]))
out("REVIEW_QUEUE", q)
href = counts.get("__href", "")
render = (counts.get(".el-table", 0) > 0) and ("/login" not in href)
data = (counts.get(".el-table__row", 0) > 0) # 队列有待审行(seed 9324)
rv = page_verdict(c, "admin/review", counts, render, data,
extra={"queue_rows": q.get("rows", []), "row_count": q.get("rowCount", 0),
"guard_bounced_to_login": "/login" in href})
verdicts.append(rv)
# ======================================================================
# U2 审核→发布:在审核队列点 9324(saa-e2e-breakout)「通过」
# ======================================================================
u2 = {"target_id": U2_TARGET_ID, "target_name": U2_TARGET_NAME}
c.reset_net_log()
c.shot("u2_before.png")
before_q = c.evaljs(JS_QUEUE)
u2["before_queue_rows"] = before_q.get("rowCount", 0)
print("U2_CLICK_PASS:", c.evaljs(js_click_pass(U2_TARGET_NAME)), flush=True)
time.sleep(2)
c.shot("u2_dialog.png")
print("U2_CONFIRM:", c.evaljs(JS_CONFIRM), flush=True)
time.sleep(5) # 等审核 API(同事务 publish) + 队列刷新
c.drain(1.5)
c.shot("u2_after.png")
after_q = c.evaljs(JS_QUEUE)
u2["after_queue_rows"] = after_q.get("rowCount", 0)
u2["queue_decremented"] = (after_q.get("rowCount", 0) < before_q.get("rowCount", 99))
u2["http5xx_during_review"] = c.http5xx[:6]
u2["console_errors_during_review"] = c.console[:6]
out("U2_REVIEW_RESULT", u2)
c.close()
out("WALK_SUMMARY", {"verdicts": verdicts, "u2": u2})
print("=== U5U2_WALK_DONE ===", flush=True)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,62 @@
[
{
"game_id": "gd-flappy",
"stage": "play",
"pass": false,
"guards": {
"A_boot": true,
"C_frame": true,
"I_control": true,
"D_render": true,
"E_live": true,
"B_uncaught": true,
"F_wiring": true,
"G_input": false,
"H_progress": false
},
"err": "真玩未过:\n- 守卫 G_input 未过:{\"pass\": false, \"inputHash\": \"238a2a05\", \"controlHash\": \"238a2a05\", \"atFrame\": 110, \"ctrlFrame\": 115}\n- 守卫 H_progress 未过:{\"pass\": false, \"checks\": [{\"path\": \"score\", \"op\": \">=\", \"before\": 0, \"after\": 0, \"value\": 1, \"pass\": false, \"why\": \"穿过第一对管道得分≥1\"}], \"latch\": {\"pass\": true, ",
"driver": "flap-to-gap",
"model": "deepseek-v4-pro",
"attempts": 8
},
{
"game_id": "gd-doodle",
"stage": "play",
"pass": false,
"guards": {
"A_boot": true,
"C_frame": true,
"I_control": false,
"D_render": true,
"E_live": true,
"B_uncaught": true,
"F_wiring": true,
"G_input": true,
"H_progress": false
},
"err": "真玩未过:\n- 守卫 I_control 未过:{\"pass\": false, \"paddlePath\": \"bird.x\", \"results\": [{\"tapX\": 50, \"before\": 195, \"after\": 161.66666666666657, \"moved\": -33.33333333333343, \"distToTarget\": 111.66666666666657, \"pass\": false}, {\"tapX\": 3\n- 守卫 H_progress 未过:{\"pass\": false, \"checks\": [{\"path\": \"score\", \"op\": \">=\",",
"driver": "seek-x",
"model": "deepseek-v4-pro",
"attempts": 8
},
{
"game_id": "gd-runner",
"stage": "play",
"pass": false,
"guards": {
"A_boot": true,
"C_frame": true,
"I_control": true,
"D_render": true,
"E_live": false,
"B_uncaught": true,
"F_wiring": false,
"G_input": false,
"H_progress": false
},
"err": "真玩未过:\n- 守卫 E_live 未过:{\"pass\": false, \"distinctStates\": 1, \"samples\": 62}\n- 守卫 F_wiring 未过:{\"pass\": false, \"callCount\": 0, \"sample\": [], \"expected\": [\"particles.\", \"audio.synth\"]}\n- 守卫 G_input 未过:{\"pass\": false, \"inputHash\": \"4c63f37f\", \"controlHash\": \"4c63f37f\", \"atFrame\": 1455, \"ctrlFrame\": -1}\n- 守",
"driver": "flap-to-gap",
"model": "deepseek-v4-pro",
"attempts": 8
}
]