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.
```bash
# Clone into this skill directory so that you can refer to it across different sessions
cd /path/to/this/skill/directory
git clone -b main https://github.com/agentscope-ai/agentscope.git
# Clone the tag matching the INSTALLED version (not main — avoid source-vs-runtime drift).
# Installed version lives under cheap-worker/.venv/.../site-packages/ (currently agentscope 2.0.2).
# 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
cd /path/to/this/skill/directory/agentscope
git pull
# When the installed version bumps (check .venv dist-info or agentscope/src/agentscope/_version.py),
# re-clone the new tag rather than `git pull` (the clone is a detached tag checkout):
# 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
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).
- **`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- 前缀,可重生成、不入库)
game-runtime/games/amgen-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 顶部一小段注释里)定一份**轻量玩法设计**,再实现:
- **核心循环**(一句话:玩家做什么 → 得什么即时反馈 → 怎么变强);
- **核心循环(好玩第一因 · 必含一层玩家决策或技巧)**:写清「玩家每次操作做**什么决策** → 得什么即时反馈 → 怎么变强」。**铁律:核心操作绝不能是「点了自动结算」的无脑点击**,必须让真人有发挥——三选一起步:**匹配**(顾客点指定菜、上对的才给分/给多分)、**时机**(在对的时刻操作 = 更多奖励)、**取舍/连击**(连续做对叠 combo、资源有限要权衡。自检把玩家换成「闭眼乱点」——若分数照样拿满这循环就是无趣的重设计。
- **决策层与自动验收解耦(工程现实,别绕开)**:九门自动验收会盲点 `occupied:true` 目标、判 score 涨,所以做成**两层奖励**——**基础分**(任意有效操作都加一点,保盲驱动器能跑通过门)+ **技巧分**(叠在玩家的匹配/时机/连击上,真人靠它玩出爽感)。盲驱动器拿基础分过门、真人靠技巧拿高分,两不耽误。**别为「让驱动器满分」把玩法做成无脑点;也别「不做对就零分」让驱动器过不了门。**
- **资源环**(经营/放置类必含):「进货 → 库存 → 售卖收钱 → 缺货补货」的软币循环,制造「赚→进→卖→再赚」的张力;
- **34 级解锁阶梯**:攒够阈值解锁新商品/区域/能力,任意时刻都露出「下一个锁」;
- **数值成长**:产出/成本随级上升、略带滚雪球感,别平淡线性;
- **音效清单**:收钱「叮」/ 升级欢呼 / 解锁号角(经 plugins.audioMusic宁可程序化也别没有反馈音
定完过一遍 sim-business §10 的 **8 条好玩自检**(①即时反馈 ②可见成长 ③下一个解锁 ④30 秒内首次升级/解锁 ⑤数值滚雪球 ⑥情感锚〔萌角色/拥有物〕 ⑦放置回归惊喜 ⑧音反馈)——命中越多越好玩,命中 ≤3 ≈ 能玩但无趣
定完过一遍 **9 条好玩自检**(①即时反馈 ②可见成长 ③下一个解锁 ④30 秒内首次升级/解锁 ⑤数值滚雪球 ⑥情感锚〔萌角色/拥有物〕 ⑦放置回归惊喜 ⑧音反馈 ⑨**核心操作非无脑**〔每次主操作有真实的决策/技巧含量,不是点了自动结算〕)——**⑨ 是否决项:⑨ 不命中,①–⑧ 全中也只是「有元素的无趣游戏」**(实测一款 6/8 命中却不好玩,正死在 ⑨);⑨ 命中的前提下,其余命中越多越好玩
**8 条自检对所有品类通用**(动作/消除/跑酷也照它要即时反馈 + 可见成长 + 音反馈);**经营/养成/放置/点客类**再按 sim-business 取资源环/解锁阶梯/客流节奏范式。
【MVP-first 铁律(钉死·关乎你能不能收敛,别一稿堆满)】
设计太满 → 你实现负担过重 → read/write 反复跳、跑不收敛(实测:满配设计循环截停、精简设计 9 步收敛)。首版**只做可玩核心**:核心循环 + 1 个主机制 + 1 个资源环(进货)+ 34 级解锁 + 基础数值/音效,**商品 ≤3 种起步**。**离线收益 / 看广告位 / 雇员 / 多档 BGM / hitstop / 6+ 商品 一律标「后续·MVP 不做」**、别塞进首版。**先出能玩的核心,再谈丰富。**
设计太满 → 你实现负担过重 → read/write 反复跳、跑不收敛(实测:满配设计循环截停、精简设计 9 步收敛)。首版**只做可玩核心**:核心循环 + 1 个主机制 + 1 个资源环(进货)+ 34 级解锁 + 基础数值/音效,**商品 ≤3 种起步**。**离线收益 / 看广告位 / 雇员 / 多档 BGM / hitstop / 6+ 商品 一律标「后续·MVP 不做」**、别塞进首版。**先出能玩的核心,再谈丰富。** **但钉死:「可玩核心」= 一个有决策深度的核心机制做透,不是一个浅机制 + 一堆 meta 元素(解锁/升级/飘字)。要砍的是商品数 / 附加系统 / 离线雇员;绝不砍核心循环那一层玩家决策(见上「核心循环」铁律)——那是 MVP 的心脏,砍了就只剩无趣骨架。**
【步骤】
A 读手册1+5+ 读你的起点 game-logic.js4→ 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": {
"ts": "2026-06-13T05:14:05.317Z",
"ts": "2026-06-21T08:51:55.806Z",
"base": "http://localhost:4321",
"cdpHttp": "http://localhost:9222",
"seed": 305441741,
@ -20,7 +20,7 @@
"vfx": false,
"firstInput": false
},
"fatal": "AggregateError",
"fatal": "[browser-evidence] 等 host 引导超时window.__hostBooted 未就绪)",
"failures": [],
"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 走查 runbookmini-desktop 本地执行)。
# chrome 生命周期:起 headless chrome(:9222) → 探活 → 跑 python 走查 → 杀 chrome 净场。
# 自身输出重定向到 /tmp/u5u2_out.txtCDP §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 chromeCDP §2no-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(draftrequiresAuth) ----
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(PUBLIC9 作品) ----
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(requiresAuth5 品类) ----
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
}
]