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:
parent
a164f4c222
commit
8ea972340e
@ -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
6
.gitignore
vendored
@ -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/
|
||||
|
||||
@ -22,16 +22,17 @@ version: 1.0.0
|
||||
|
||||
【先设计后写码:决定这游戏好不好玩(关键,别跳过)】
|
||||
能跑 ≠ 好玩。动手写码前,先据 brief 在心里(或 game-logic.js 顶部一小段注释里)定一份**轻量玩法设计**,再实现:
|
||||
- **核心循环**(一句话:玩家做什么 → 得什么即时反馈 → 怎么变强);
|
||||
- **核心循环(好玩第一因 · 必含一层玩家决策或技巧)**:写清「玩家每次操作做**什么决策** → 得什么即时反馈 → 怎么变强」。**铁律:核心操作绝不能是「点了自动结算」的无脑点击**,必须让真人有发挥——三选一起步:**匹配**(顾客点指定菜、上对的才给分/给多分)、**时机**(在对的时刻操作 = 更多奖励)、**取舍/连击**(连续做对叠 combo、资源有限要权衡)。自检:把玩家换成「闭眼乱点」——若分数照样拿满,这循环就是无趣的,重设计。
|
||||
- **决策层与自动验收解耦(工程现实,别绕开)**:九门自动验收会盲点 `occupied:true` 目标、判 score 涨,所以做成**两层奖励**——**基础分**(任意有效操作都加一点,保盲驱动器能跑通过门)+ **技巧分**(叠在玩家的匹配/时机/连击上,真人靠它玩出爽感)。盲驱动器拿基础分过门、真人靠技巧拿高分,两不耽误。**别为「让驱动器满分」把玩法做成无脑点;也别「不做对就零分」让驱动器过不了门。**
|
||||
- **资源环**(经营/放置类必含):「进货 → 库存 → 售卖收钱 → 缺货补货」的软币循环,制造「赚→进→卖→再赚」的张力;
|
||||
- **3–4 级解锁阶梯**:攒够阈值解锁新商品/区域/能力,任意时刻都露出「下一个锁」;
|
||||
- **数值成长**:产出/成本随级上升、略带滚雪球感,别平淡线性;
|
||||
- **音效清单**:收钱「叮」/ 升级欢呼 / 解锁号角(经 plugins.audioMusic;宁可程序化也别没有反馈音)。
|
||||
定完过一遍 sim-business §10 的 **8 条好玩自检**(①即时反馈 ②可见成长 ③下一个解锁 ④30 秒内首次升级/解锁 ⑤数值滚雪球 ⑥情感锚〔萌角色/拥有物〕 ⑦放置回归惊喜 ⑧音反馈)——命中越多越好玩,命中 ≤3 ≈ 能玩但无趣。
|
||||
定完过一遍 **9 条好玩自检**(①即时反馈 ②可见成长 ③下一个解锁 ④30 秒内首次升级/解锁 ⑤数值滚雪球 ⑥情感锚〔萌角色/拥有物〕 ⑦放置回归惊喜 ⑧音反馈 ⑨**核心操作非无脑**〔每次主操作有真实的决策/技巧含量,不是点了自动结算〕)——**⑨ 是否决项:⑨ 不命中,①–⑧ 全中也只是「有元素的无趣游戏」**(实测一款 6/8 命中却不好玩,正死在 ⑨);⑨ 命中的前提下,其余命中越多越好玩。
|
||||
**8 条自检对所有品类通用**(动作/消除/跑酷也照它要即时反馈 + 可见成长 + 音反馈);**经营/养成/放置/点客类**再按 sim-business 取资源环/解锁阶梯/客流节奏范式。
|
||||
|
||||
【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。
|
||||
|
||||
@ -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
|
||||
}
|
||||
1
game-studio/public/mock-manifests/9001.json
Normal file
1
game-studio/public/mock-manifests/9001.json
Normal file
File diff suppressed because one or more lines are too long
54
game-studio/public/mock-manifests/run_u5u2.sh
Normal file
54
game-studio/public/mock-manifests/run_u5u2.sh
Normal 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 ==="
|
||||
392
game-studio/public/mock-manifests/u5u2_walk.py
Normal file
392
game-studio/public/mock-manifests/u5u2_walk.py
Normal 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()
|
||||
62
wg1/gen-worker/results/gamedef-quickcheck.json
Normal file
62
wg1/gen-worker/results/gamedef-quickcheck.json
Normal 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
|
||||
}
|
||||
]
|
||||
Loading…
x
Reference in New Issue
Block a user