docs: 新角色分工(Mac开发/6c6g大脑)+ 收编他session loose-end + _index修漂移
- 多session分工总账 §2/§5/§7 重定:域切→角色切(Mac全开发按计划执行 / 6c6g设计·计划·评审·文档·优化);Mac工作清单(plan001 SAA产线化Tier0 + plan002 Tier-1剩4域 + 前端8页)+ 6c6g大脑角色 + 协调约束(git中介·够不到Mac) - 收编他session未提交:orchestrator *_walk脚本×6 + v1对话生成修改素材/ v2生命周期项目管理 review + prefix-cache preflight(保全) - _index修漂移:v2=ACTIVE范式基座(改源不改包)、v1=留痕(前提被supersede); 生成域canonical现归本session(6c6g)维护 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1101d01661
commit
771619c7da
@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
看板「数据接口建设中→转真」专项 CDP 走查(mini-desktop 本地驱 headless chrome:9222 → studio:4173)。
|
||||
复用 studio_walk.py 的最小 CDP 客户端范式(websocket-client 直驱),但聚焦数据看板三处接线核验:
|
||||
· 完玩率 ← telemetry /my/summary completionRate
|
||||
· 广告收益 ← trade /my/income-summary totalNet(分→元)
|
||||
· 七日趋势 ← telemetry /my/game-stat/trend?days=7 真柱状
|
||||
|
||||
为同时取证「真数据」与「真实空态」,本脚本登录两个账号各跑一遍看板:
|
||||
A) 18818260272/8888 —— 该创作者名下作品有近 7 日聚合(completionRate=0.5/trend 2 天有数)→ 看板显真值+真柱。
|
||||
B) 13800001234/8888 —— 无作品 → 看板显真实空态(完玩率 0%/¥0.00/趋势空态),证明无数据不伪造。
|
||||
|
||||
取证手段(双锚):① Network 域抓三端点真响应体(证 UI 真消费 live 数据);
|
||||
② 读看板新 DOM 选择器(.pending-row__value / .trend__bar / .trend__empty)证渲染落地。
|
||||
不改后端/DB,仅 UI 走查取证。
|
||||
"""
|
||||
import json, base64, time, urllib.request
|
||||
import websocket # websocket-client
|
||||
|
||||
CDP_HTTP = "http://localhost:9222"
|
||||
APP = "http://localhost:4173"
|
||||
SHOT_DIR = "/tmp/dash-wired"
|
||||
|
||||
# 三个目标端点(用于从 Network 事件里挑出对应响应)
|
||||
TARGET_EPS = [
|
||||
"/app-api/telemetry/my/summary",
|
||||
"/app-api/trade/my/income-summary",
|
||||
"/app-api/telemetry/my/game-stat/trend",
|
||||
]
|
||||
|
||||
|
||||
def new_target(url="about:blank"):
|
||||
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 = []
|
||||
# requestId -> {url} ;以及 requestId -> 响应体(按需 getResponseBody 拉)
|
||||
self._net_url = {}
|
||||
self._net_done = [] # [{url, requestId, status}]
|
||||
|
||||
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 _collect(self, msg):
|
||||
m = msg.get("method")
|
||||
if m == "Network.requestWillBeSent":
|
||||
rid = msg["params"]["requestId"]
|
||||
url = msg["params"]["request"]["url"]
|
||||
self._net_url[rid] = url
|
||||
elif m == "Network.responseReceived":
|
||||
rid = msg["params"]["requestId"]
|
||||
url = msg["params"]["response"]["url"]
|
||||
status = msg["params"]["response"]["status"]
|
||||
if any(ep in url for ep in TARGET_EPS):
|
||||
self._net_done.append({"url": url, "requestId": rid, "status": status})
|
||||
elif m == "Runtime.consoleAPICalled":
|
||||
t = msg["params"].get("type")
|
||||
if t in ("error", "warning"):
|
||||
args = msg["params"].get("args", [])
|
||||
txt = " ".join(str(a.get("value", a.get("description", ""))) for a in args)[:240]
|
||||
self.console.append({"level": t, "text": txt})
|
||||
elif m == "Runtime.exceptionThrown":
|
||||
d = msg["params"].get("exceptionDetails", {})
|
||||
self.console.append({"level": "error", "text": (d.get("text") or "")[:240]})
|
||||
|
||||
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 get_target_bodies(self):
|
||||
"""对捕获到的三端点响应逐个 getResponseBody,返回 [{url,status,body}]。"""
|
||||
out = []
|
||||
# 去重:同一 url 取最后一次
|
||||
latest = {}
|
||||
for n in self._net_done:
|
||||
latest[n["url"].split("?")[0]] = n
|
||||
for url_key, n in latest.items():
|
||||
body = None
|
||||
try:
|
||||
r = self.send("Network.getResponseBody", {"requestId": n["requestId"]})
|
||||
body = r.get("body")
|
||||
if r.get("base64Encoded"):
|
||||
body = base64.b64decode(body).decode("utf-8", "ignore")
|
||||
except Exception as e:
|
||||
body = f"<body-fetch-failed:{e}>"
|
||||
out.append({"url": url_key, "status": n["status"], "body": (body or "")[:300]})
|
||||
return out
|
||||
|
||||
def reset_net(self):
|
||||
self._net_url.clear()
|
||||
self._net_done.clear()
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ---------- JS 片段 ----------
|
||||
|
||||
def js_set_by_ph(ph, val):
|
||||
return r"""(()=>{
|
||||
const el=[...document.querySelectorAll('input,textarea')].find(e=>(e.placeholder||'').includes('%s'));
|
||||
if(!el) return 'no-input:%s';
|
||||
const tag=el.tagName==='TEXTAREA'?window.HTMLTextAreaElement:window.HTMLInputElement;
|
||||
const setter=Object.getOwnPropertyDescriptor(tag.prototype,'value').set;
|
||||
setter.call(el, %s);
|
||||
el.dispatchEvent(new Event('input',{bubbles:true}));
|
||||
el.dispatchEvent(new Event('change',{bubbles:true}));
|
||||
return 'set:'+(el.value||'').slice(0,20);
|
||||
})()""" % (ph, ph, json.dumps(val))
|
||||
|
||||
|
||||
JS_CHECK_AGREE = r"""(()=>{
|
||||
const cb=[...document.querySelectorAll('input[type=checkbox]')][0];
|
||||
if(!cb) return 'no-checkbox';
|
||||
if(!cb.checked) cb.click();
|
||||
return 'agree:'+cb.checked;
|
||||
})()"""
|
||||
|
||||
|
||||
def js_click_btn(text):
|
||||
return r"""(()=>{
|
||||
const bs=[...document.querySelectorAll('button')];
|
||||
const b=bs.find(e=>(e.innerText||'').replace(/\s+/g,'').includes('%s'));
|
||||
if(b){b.click(); return 'clicked:%s';}
|
||||
return 'notfound:%s';
|
||||
})()""" % (text, text, text)
|
||||
|
||||
|
||||
# 退出登录(清 localStorage 真 token,下一账号干净登录)——直接清键,最稳
|
||||
JS_LOGOUT = r"""(()=>{
|
||||
Object.keys(localStorage).filter(k=>/token/i.test(k)).forEach(k=>localStorage.removeItem(k));
|
||||
return 'cleared:'+Object.keys(localStorage).filter(k=>/token/i.test(k)).length;
|
||||
})()"""
|
||||
|
||||
|
||||
# 提交态探针:列出所有按钮文案 + 提交按钮 disabled 状态(诊断点击是否落到提交而非 Tab)
|
||||
JS_SUBMIT_STATE = r"""(()=>{
|
||||
const bs=[...document.querySelectorAll('button')].map(e=>({
|
||||
txt:(e.innerText||'').replace(/\s+/g,''), disabled:e.disabled}));
|
||||
const submit=bs.find(b=>b.txt.includes('登录/注册')||b.txt.includes('注册并登录'));
|
||||
return {buttons:bs, submit};
|
||||
})()"""
|
||||
|
||||
|
||||
# 看板「转真」核验:读新 DOM 选择器(运营指标真值 + 趋势真柱/空态)
|
||||
JS_DASH = r"""(()=>{
|
||||
const metrics=[...document.querySelectorAll('.metric')].map(m=>({
|
||||
label:(m.querySelector('.metric__label')||{}).innerText||'',
|
||||
val:(m.querySelector('.metric__val')||{}).innerText||''}));
|
||||
// 运营指标行:label + 真值(完玩率/广告收益)
|
||||
const ops=[...document.querySelectorAll('.pending-row')].map(r=>({
|
||||
label:(r.querySelector('.pending-row__label')||{}).innerText||'',
|
||||
value:(r.querySelector('.pending-row__value')||{}).innerText||'',
|
||||
// 旧占位徽标是否还在(应为 0:已转真)
|
||||
hint:(r.querySelector('.pending-row__hint')||{}).innerText||''}));
|
||||
// 趋势:真柱数量 + 每根柱 title(含数值)+ 空态文案
|
||||
const bars=[...document.querySelectorAll('.trend__bar')].map(b=>b.getAttribute('title')||'');
|
||||
const xlabels=[...document.querySelectorAll('.trend__xlabel')].map(e=>e.innerText);
|
||||
const emptyTitle=(document.querySelector('.trend__empty-title')||{}).innerText||'';
|
||||
// 全页是否还残留「数据接口建设中」字样(应为 false)
|
||||
const bodyHasUnderConstruction=(document.body.innerText||'').includes('数据接口建设中');
|
||||
return {metrics, ops, barCount:bars.length, barTitles:bars, xlabels,
|
||||
trendEmptyTitle:emptyTitle, bodyHasUnderConstruction};
|
||||
})()"""
|
||||
|
||||
|
||||
def out(tag, obj):
|
||||
print(f"@@{tag}@@ " + json.dumps(obj, ensure_ascii=False))
|
||||
|
||||
|
||||
def login(c, mobile):
|
||||
"""走真实登录表单登录指定手机号(码恒 8888),返回 after-login href。"""
|
||||
c.nav(f"{APP}/login", settle=6)
|
||||
print(f"--[login {mobile}]--")
|
||||
print("SET_MOBILE:", c.evaljs(js_set_by_ph("手机号", mobile)))
|
||||
print("SET_CODE:", c.evaljs(js_set_by_ph("短信验证码", "8888")))
|
||||
print("CHECK_AGREE:", c.evaljs(JS_CHECK_AGREE))
|
||||
time.sleep(0.4)
|
||||
print("CANSUBMIT_PROBE:", c.evaljs(JS_SUBMIT_STATE))
|
||||
# 提交按钮文案=「登录 / 注册」(去空白=登录/注册);精确匹配避免误中 Tab「验证码登录」
|
||||
print("CLICK_SUBMIT:", c.evaljs(js_click_btn("登录/注册")))
|
||||
time.sleep(6)
|
||||
c.drain(1.0)
|
||||
href = c.evaljs("location.href")
|
||||
print("AFTER_LOGIN_HREF:", href)
|
||||
return href
|
||||
|
||||
|
||||
def walk_dashboard(c, tag, mobile):
|
||||
"""登录 mobile → 进看板 → 抓三端点响应 + 读 DOM + 截图。"""
|
||||
login(c, mobile)
|
||||
c.reset_net() # 只抓看板这一次的端点响应
|
||||
c.nav(f"{APP}/dashboard", settle=6)
|
||||
c.drain(1.5) # 多等一会确保三端点都回
|
||||
shot = c.shot(f"dash_{tag}.png")
|
||||
out(f"DASH_{tag}_DOM", c.evaljs(JS_DASH))
|
||||
out(f"DASH_{tag}_NET", c.get_target_bodies())
|
||||
print(f"SHOT_{tag}:", shot)
|
||||
# 退出登录,给下一账号让位
|
||||
c.evaljs(JS_LOGOUT)
|
||||
|
||||
|
||||
def main():
|
||||
import os
|
||||
os.makedirs(SHOT_DIR, exist_ok=True)
|
||||
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")
|
||||
|
||||
# A) 有数据创作者:看板显真值 + 真柱
|
||||
walk_dashboard(c, "A_creator_withdata", "18818260272")
|
||||
# B) 无数据账号:看板显真实空态(不伪造)
|
||||
walk_dashboard(c, "B_player_empty", "13800001234")
|
||||
|
||||
c.drain(0.5)
|
||||
out("CONSOLE_ERRORS", c.console[:30])
|
||||
c.close()
|
||||
print("=== DASH_WALK_DONE ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
feed 卖相打磨 P2(V-1 互动按钮玻璃感 / V-2 封面渐变)真 UI 走查 —— 聚焦版。
|
||||
复用 studio_walk 的最小 CDP 客户端与登录 JS(原型 setter+dispatch,防 drift),
|
||||
只走「登录(13800001234/8888) → /feed → 截图」,取证互动栏玻璃感与多卡封面渐变是否多样。
|
||||
在 mini-desktop 本地驱动 headless chrome(:9222) 跑,浏览器内访问 localhost:4173(安全上下文)。
|
||||
"""
|
||||
import time
|
||||
# 直接复用 studio_walk 已验证的 CDP 客户端 + 登录/dump 片段(同目录模块)
|
||||
from studio_walk import (
|
||||
new_target, CDP, APP,
|
||||
js_set_by_ph, js_click_btn, JS_CHECK_AGREE, JS_DUMP, JS_FEED_ZONES,
|
||||
)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
# ===== 1. 登录 =====
|
||||
c.nav(f"{APP}/login", settle=6)
|
||||
print("SET_MOBILE:", c.evaljs(js_set_by_ph("手机号", "13800001234")))
|
||||
print("SET_CODE:", c.evaljs(js_set_by_ph("验证码", "8888")))
|
||||
print("CHECK_AGREE:", c.evaljs(JS_CHECK_AGREE))
|
||||
time.sleep(0.5)
|
||||
print("CLICK_SUBMIT:", c.evaljs(js_click_btn("登录/注册")))
|
||||
time.sleep(6) # 等鉴权 + 跳转
|
||||
c.drain(1.0)
|
||||
print("AFTER_LOGIN_HREF:", c.evaljs("location.href"))
|
||||
|
||||
# ===== 2. feed:多停一会让卡片/封面渲染稳定,再截图 =====
|
||||
c.nav(f"{APP}/feed", settle=7)
|
||||
c.drain(1.0)
|
||||
c.shot("fp_feed_1.png")
|
||||
print("FEED_ZONES:", c.evaljs(JS_FEED_ZONES))
|
||||
print("FEED_DUMP:", c.evaljs(JS_DUMP))
|
||||
# 探针:dump 当前可见卡的标题 + 是否走兜底渐变 + 互动按钮的实际 background/backdrop(取证玻璃感生效)
|
||||
probe = r"""(()=>{
|
||||
const cards=[...document.querySelectorAll('.game-card')].map(card=>{
|
||||
const t=(card.querySelector('.game-card__title')||{}).innerText||'';
|
||||
const fb=card.querySelector('.game-card__cover-fallback');
|
||||
const grad=fb?getComputedStyle(fb).backgroundImage.slice(0,60):'(has-cover)';
|
||||
return {title:(t||'').slice(0,24), fallback:!!fb, grad};
|
||||
});
|
||||
const ic=document.querySelector('.interact-bar__icon');
|
||||
const ics=ic?getComputedStyle(ic):null;
|
||||
return {
|
||||
cardCount:cards.length,
|
||||
cards:cards.slice(0,6),
|
||||
interactIcon: ics?{bg:ics.backgroundColor, bdf:ics.backdropFilter||ics.webkitBackdropFilter||'', shadow:(ics.boxShadow||'').slice(0,40)}:null
|
||||
};
|
||||
})()"""
|
||||
print("POLISH_PROBE:", c.evaljs(probe))
|
||||
|
||||
# 轻量滑到下一张,再截一张,看不同卡封面是否换主题(多样性目检)
|
||||
c.evaljs("(()=>{const s=document.querySelector('.feed-scroll,.feed,[class*=scroll]');if(s){s.scrollBy(0, window.innerHeight);}return !!s;})()")
|
||||
time.sleep(2)
|
||||
c.shot("fp_feed_2.png")
|
||||
|
||||
c.drain(0.5)
|
||||
print("CONSOLE_ERRORS:", (c.console or [])[:20])
|
||||
c.close()
|
||||
print("=== FEED_POLISH_WALK_DONE ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# 看板转真专项 walk runner:流程输出重定向到文件,chrome 全 fd 脱离 ssh 管道。
|
||||
# 用法:ssh mini 'nohup bash /tmp/run_dash_walk.sh >/dev/null 2>&1 &';再轮询 /tmp/dw_out.txt 直到 DASH_WALK_DONE。
|
||||
exec > /tmp/dw_out.txt 2>&1
|
||||
echo "=== run_dash_walk @ $(date +%H:%M:%S) ==="
|
||||
mkdir -p /tmp/dash-wired
|
||||
pkill -f "chrome.*9222" 2>/dev/null; sleep 1; rm -rf /tmp/chrome-dw
|
||||
# headless chrome:移动端视口(390x844 近 iPhone12),避七坑(no-sandbox/no-zygote/allow-origins)
|
||||
setsid google-chrome --headless=new --no-sandbox --no-zygote --disable-gpu \
|
||||
--disable-dev-shm-usage --remote-debugging-port=9222 --remote-allow-origins=* \
|
||||
--window-size=390,844 --user-data-dir=/tmp/chrome-dw </dev/null >/tmp/chrome-dw.log 2>&1 &
|
||||
sleep 6
|
||||
echo "VER:"; curl -s http://localhost:9222/json/version 2>/dev/null | head -c 120; echo
|
||||
echo "WALK:"
|
||||
python3 /tmp/dash_walk.py 2>&1
|
||||
pkill -f "chrome.*9222" 2>/dev/null
|
||||
echo "=== DONE @ $(date +%H:%M:%S) ==="
|
||||
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# feed 卖相打磨 P2 聚焦走查 runner:输出重定向到文件,chrome 全 fd 脱离 ssh 管道(CDP 坑5)。
|
||||
# 用法:ssh mini 'nohup bash /tmp/run_feed_polish_walk.sh >/dev/null 2>&1 &';轮询 /tmp/fp_out.txt 直到 FEED_POLISH_WALK_DONE。
|
||||
exec > /tmp/fp_out.txt 2>&1
|
||||
echo "=== run_feed_polish_walk @ $(date +%H:%M:%S) ==="
|
||||
pkill -f "chrome.*9222" 2>/dev/null; sleep 1; rm -rf /tmp/chrome-fp
|
||||
# headless chrome:移动端视口 390x844(近 iPhone12),避七坑(no-sandbox/no-zygote/allow-origins)
|
||||
setsid google-chrome --headless=new --no-sandbox --no-zygote --disable-gpu \
|
||||
--disable-dev-shm-usage --remote-debugging-port=9222 --remote-allow-origins=* \
|
||||
--window-size=390,844 --user-data-dir=/tmp/chrome-fp </dev/null >/tmp/chrome-fp.log 2>&1 &
|
||||
sleep 6
|
||||
echo "VER:"; curl -s http://localhost:9222/json/version 2>/dev/null | head -c 120; echo
|
||||
echo "WALK:"
|
||||
cd /tmp && python3 /tmp/feed_polish_walk.py 2>&1
|
||||
pkill -f "chrome.*9222" 2>/dev/null
|
||||
echo "=== DONE @ $(date +%H:%M:%S) ==="
|
||||
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# studio L0 live 走查 runner:整个流程输出重定向到文件,chrome 全 fd 脱离 ssh 管道。
|
||||
# 用法:ssh mini 'nohup bash /tmp/run_studio_walk.sh >/dev/null 2>&1 &';再轮询 /tmp/sw_out.txt 直到 STUDIO_WALK_DONE。
|
||||
exec > /tmp/sw_out.txt 2>&1
|
||||
echo "=== run_studio_walk @ $(date +%H:%M:%S) ==="
|
||||
pkill -f "chrome.*9222" 2>/dev/null; sleep 1; rm -rf /tmp/chrome-sw
|
||||
# headless chrome:移动端视口(390x844 近 iPhone12),避七坑(no-sandbox/no-zygote/allow-origins)
|
||||
setsid google-chrome --headless=new --no-sandbox --no-zygote --disable-gpu \
|
||||
--disable-dev-shm-usage --remote-debugging-port=9222 --remote-allow-origins=* \
|
||||
--window-size=390,844 --user-data-dir=/tmp/chrome-sw </dev/null >/tmp/chrome-sw.log 2>&1 &
|
||||
sleep 6
|
||||
echo "VER:"; curl -s http://localhost:9222/json/version 2>/dev/null | head -c 120; echo
|
||||
echo "WALK:"
|
||||
python3 /tmp/studio_walk.py 2>&1
|
||||
pkill -f "chrome.*9222" 2>/dev/null
|
||||
echo "=== DONE @ $(date +%H:%M:%S) ==="
|
||||
@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
studio 产品前端 L0 live 走查 —— 最小 CDP 客户端(websocket-client 直驱)。
|
||||
在 mini-desktop 本地驱动 headless chrome(:9222) 走查 game-studio(:4173):
|
||||
登录(手机号13800001234/码8888) → feed → 消息 → 个人中心 → 看板 → B端提单 e2e(提单→列表→详情) → 创作页。
|
||||
全程单 target 复用 localStorage(保登录态),逐页 nav+shot+dump,跑完一把出全部截图+dump,末尾才关 chrome。
|
||||
受控输入用 原型 setter + dispatch input(触发 Vue v-model);复选框用 click;按钮精确文案匹配防子串误中。
|
||||
不改后端/DB,仅 UI 走查取证。
|
||||
"""
|
||||
import json, base64, time, sys, urllib.request
|
||||
import websocket # websocket-client 1.7.0
|
||||
|
||||
CDP_HTTP = "http://localhost:9222"
|
||||
APP = "http://localhost:4173"
|
||||
SHOT_DIR = "/tmp"
|
||||
|
||||
|
||||
def new_target(url="about:blank"):
|
||||
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/warning
|
||||
|
||||
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
|
||||
# 顺带收集 console(Log.entryAdded / Runtime.consoleAPICalled)
|
||||
if msg.get("method") == "Log.entryAdded":
|
||||
e = msg["params"]["entry"]
|
||||
if e.get("level") in ("error", "warning"):
|
||||
self.console.append({"src": "log", "level": e.get("level"),
|
||||
"text": (e.get("text") or "")[:240]})
|
||||
elif msg.get("method") == "Runtime.consoleAPICalled":
|
||||
t = msg["params"].get("type")
|
||||
if t in ("error", "warning"):
|
||||
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": t, "text": txt})
|
||||
elif msg.get("method") == "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]})
|
||||
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):
|
||||
"""空转收 console 事件。"""
|
||||
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
|
||||
if msg.get("method") == "Log.entryAdded":
|
||||
e = msg["params"]["entry"]
|
||||
if e.get("level") in ("error", "warning"):
|
||||
self.console.append({"src": "log", "level": e.get("level"),
|
||||
"text": (e.get("text") or "")[:240]})
|
||||
elif msg.get("method") == "Runtime.consoleAPICalled":
|
||||
t = msg["params"].get("type")
|
||||
if t in ("error", "warning"):
|
||||
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": t, "text": txt})
|
||||
elif msg.get("method") == "Runtime.exceptionThrown":
|
||||
d = msg["params"].get("exceptionDetails", {})
|
||||
self.console.append({"src": "exception", "level": "error",
|
||||
"text": (d.get("text") or "")[:240]})
|
||||
|
||||
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.6)
|
||||
|
||||
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 close(self):
|
||||
try:
|
||||
self.ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ---------- JS 片段 ----------
|
||||
|
||||
# 受控输入:按 placeholder 找 input/textarea,用原型 setter 赋值 + dispatch input(触发 v-model)
|
||||
def js_set_by_ph(ph, val):
|
||||
return r"""(()=>{
|
||||
const el=[...document.querySelectorAll('input,textarea')].find(e=>(e.placeholder||'').includes('%s'));
|
||||
if(!el) return 'no-input:%s';
|
||||
const tag=el.tagName==='TEXTAREA'?window.HTMLTextAreaElement:window.HTMLInputElement;
|
||||
const setter=Object.getOwnPropertyDescriptor(tag.prototype,'value').set;
|
||||
setter.call(el, %s);
|
||||
el.dispatchEvent(new Event('input',{bubbles:true}));
|
||||
el.dispatchEvent(new Event('change',{bubbles:true}));
|
||||
return 'set:'+(el.value||'').slice(0,20);
|
||||
})()""" % (ph, ph, json.dumps(val))
|
||||
|
||||
|
||||
# 勾选协议复选框(label 内 type=checkbox),click 触发 v-model
|
||||
JS_CHECK_AGREE = r"""(()=>{
|
||||
const cb=[...document.querySelectorAll('input[type=checkbox]')][0];
|
||||
if(!cb) return 'no-checkbox';
|
||||
if(!cb.checked) cb.click();
|
||||
return 'agree:'+cb.checked;
|
||||
})()"""
|
||||
|
||||
|
||||
# 精确文案点击按钮(去空白后全等或包含指定文案;prefer 全等)
|
||||
def js_click_btn(text, contains=False):
|
||||
op = "includes" if contains else "==="
|
||||
cmp = ("(e.innerText||'').replace(/\\s+/g,'').includes('%s')" % text) if contains \
|
||||
else ("(e.innerText||'').replace(/\\s+/g,'')==='%s'" % text)
|
||||
return r"""(()=>{
|
||||
const bs=[...document.querySelectorAll('button')];
|
||||
const b=bs.find(e=>%s);
|
||||
if(b){b.click(); return 'clicked:%s';}
|
||||
return 'notfound:%s|avail:'+bs.map(e=>(e.innerText||'').replace(/\s+/g,'')).filter(Boolean).slice(0,20).join(',');
|
||||
})()""" % (cmp, text, text)
|
||||
|
||||
|
||||
# dump 页面通用信息:url/title/可见主按钮文案/body 文本片段
|
||||
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,500)};
|
||||
})()"""
|
||||
|
||||
# feed 顶部专区名(取专区 tab/标题文本)
|
||||
JS_FEED_ZONES = r"""(()=>{
|
||||
// 专区名通常在顶部 tab/chip;广撒网取页面前部短文本
|
||||
const top=(document.body.innerText||'').replace(/\s+/g,' ').slice(0,200);
|
||||
// 主按钮背景(取第一个 AppButton 的渐变,验证青绿渐变)
|
||||
const btn=document.querySelector('.app-btn,button');
|
||||
const bg=btn?getComputedStyle(btn).backgroundImage:'';
|
||||
const ff=getComputedStyle(document.body).fontFamily;
|
||||
return {topText:top, firstBtnBg:bg.slice(0,120), bodyFont:ff};
|
||||
})()"""
|
||||
|
||||
# 看板指标卡 + 占位
|
||||
JS_DASH = r"""(()=>{
|
||||
const metrics=[...document.querySelectorAll('.metric')].map(m=>({
|
||||
label:(m.querySelector('.metric__label')||{}).innerText||'',
|
||||
val:(m.querySelector('.metric__val')||{}).innerText||''}));
|
||||
const pending=[...document.querySelectorAll('.pending-row__hint')].map(e=>e.innerText);
|
||||
const trend=(document.querySelector('.trend-placeholder__desc')||{}).innerText||'';
|
||||
return {metrics, pendingHints:pending, trendPlaceholder:trend};
|
||||
})()"""
|
||||
|
||||
# 消息中心筛选 tab + 是否空态
|
||||
JS_MSG = r"""(()=>{
|
||||
const filters=[...document.querySelectorAll('.msg-list__filter')].map(e=>(e.innerText||'').trim());
|
||||
const empty=document.querySelector('.empty-state, [class*=empty]');
|
||||
const emptyText=empty?(empty.innerText||'').replace(/\s+/g,' ').slice(0,120):'';
|
||||
const cards=document.querySelectorAll('.msg-card').length;
|
||||
const badge=(document.querySelector('.msg-list__badge')||{}).innerText||'';
|
||||
return {filters, cardCount:cards, badge, emptyText};
|
||||
})()"""
|
||||
|
||||
# B端列表 dump:订单条目
|
||||
JS_BIZ_LIST = r"""(()=>{
|
||||
const txt=(document.body.innerText||'').replace(/\s+/g,' ').slice(0,400);
|
||||
// 列表项可能是多种 class,统计可点击行 + 是否含我们提交的标题
|
||||
const rows=[...document.querySelectorAll('[class*=lead],[class*=order],[class*=card],li')].map(e=>(e.innerText||'').replace(/\s+/g,' ').slice(0,80)).filter(t=>t.length>4).slice(0,12);
|
||||
return {href:location.href, bodyText:txt, rows};
|
||||
})()"""
|
||||
|
||||
# B端详情 dump:时间线
|
||||
JS_BIZ_DETAIL = r"""(()=>{
|
||||
const txt=(document.body.innerText||'').replace(/\s+/g,' ').slice(0,500);
|
||||
const steps=[...document.querySelectorAll('[class*=step],[class*=timeline],[class*=progress],[class*=node]')].map(e=>(e.innerText||'').replace(/\s+/g,' ').slice(0,60)).filter(t=>t.length>1).slice(0,12);
|
||||
return {href:location.href, bodyText:txt, timelineNodes:steps};
|
||||
})()"""
|
||||
|
||||
|
||||
def out(tag, obj):
|
||||
print(f"@@{tag}@@ " + json.dumps(obj, ensure_ascii=False))
|
||||
|
||||
|
||||
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")
|
||||
|
||||
# ===== 1. 登录页 =====
|
||||
c.nav(f"{APP}/login", settle=6)
|
||||
c.shot("sw_1a_login.png")
|
||||
out("LOGIN_PAGE", c.evaljs(JS_DUMP))
|
||||
print("SET_MOBILE:", c.evaljs(js_set_by_ph("手机号", "13800001234")))
|
||||
print("SET_CODE:", c.evaljs(js_set_by_ph("验证码", "8888")))
|
||||
print("CHECK_AGREE:", c.evaljs(JS_CHECK_AGREE))
|
||||
time.sleep(0.5)
|
||||
c.shot("sw_1b_login_filled.png")
|
||||
print("CLICK_SUBMIT:", c.evaljs(js_click_btn("登录/注册")))
|
||||
time.sleep(6) # 等鉴权 + 跳转
|
||||
c.drain(1.0)
|
||||
print("AFTER_LOGIN_HREF:", c.evaljs("location.href"))
|
||||
print("HAS_TOKEN:", c.evaljs("!!localStorage.getItem('hj_token')||!!localStorage.getItem('token')||Object.keys(localStorage).filter(k=>/token/i.test(k))"))
|
||||
|
||||
# ===== 1(续). feed =====
|
||||
c.nav(f"{APP}/feed", settle=6)
|
||||
c.shot("sw_1c_feed.png")
|
||||
out("FEED", c.evaljs(JS_FEED_ZONES))
|
||||
out("FEED_DUMP", c.evaljs(JS_DUMP))
|
||||
|
||||
# ===== 2. 消息中心 =====
|
||||
c.nav(f"{APP}/message", settle=5)
|
||||
c.shot("sw_2_message.png")
|
||||
out("MESSAGE", c.evaljs(JS_MSG))
|
||||
|
||||
# ===== 3. 个人中心 =====
|
||||
c.nav(f"{APP}/profile", settle=5)
|
||||
c.shot("sw_3_profile.png")
|
||||
out("PROFILE", c.evaljs(JS_DUMP))
|
||||
|
||||
# ===== 4. 数据看板 =====
|
||||
c.nav(f"{APP}/dashboard", settle=5)
|
||||
c.shot("sw_4_dashboard.png")
|
||||
out("DASHBOARD", c.evaljs(JS_DASH))
|
||||
|
||||
# ===== 5. B端定制 e2e:提单 → 列表 → 详情 =====
|
||||
# 5a 提单页
|
||||
c.nav(f"{APP}/biz/create", settle=5)
|
||||
c.shot("sw_5a_biz_create.png")
|
||||
out("BIZ_CREATE_PAGE", c.evaljs(JS_DUMP))
|
||||
print("SET_TITLE:", c.evaljs(js_set_by_ph("一句话概括", "CDP测试-黄山景区AR导览小游戏")))
|
||||
print("SET_DESC:", c.evaljs(js_set_by_ph("详细描述使用场景", "面向景区游客,扫码即玩的AR寻宝导览,CDP live走查测试单")))
|
||||
time.sleep(0.5)
|
||||
c.shot("sw_5b_biz_filled.png")
|
||||
print("CLICK_SUBMIT_BIZ:", c.evaljs(js_click_btn("提交需求")))
|
||||
time.sleep(5) # 等提交 + replace 跳详情
|
||||
c.drain(1.0)
|
||||
detail_href = c.evaljs("location.href")
|
||||
print("AFTER_SUBMIT_HREF:", detail_href)
|
||||
c.shot("sw_5d_biz_detail.png")
|
||||
out("BIZ_DETAIL", c.evaljs(JS_BIZ_DETAIL))
|
||||
|
||||
# 5c 回列表看新单是否在列
|
||||
c.nav(f"{APP}/biz", settle=5)
|
||||
c.shot("sw_5c_biz_list.png")
|
||||
out("BIZ_LIST", c.evaljs(JS_BIZ_LIST))
|
||||
|
||||
# ===== 6. 创作页 =====
|
||||
c.nav(f"{APP}/create", settle=6)
|
||||
c.shot("sw_6_create.png")
|
||||
out("CREATE", c.evaljs(JS_DUMP))
|
||||
|
||||
# ===== console 汇总 =====
|
||||
c.drain(0.5)
|
||||
out("CONSOLE_ERRORS", c.console[:40])
|
||||
|
||||
c.close()
|
||||
print("=== STUDIO_WALK_DONE ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -21,15 +21,17 @@
|
||||
|
||||
---
|
||||
|
||||
## 2. 两开发线(按"要不要浏览器/视觉"硬约束切 · 交汇只在契约)
|
||||
## 2. 两开发线(2026-06-18 创始人重定:**按角色切**——Mac 执行开发 / 6c6g 大脑)
|
||||
|
||||
| 线 | 设备 / 构建 | 负责的开发工作 |
|
||||
> **更新(2026-06-18)**:原"按要不要浏览器/视觉"的**域切**(后端线 6c6g / 前端引擎线 Mac)**改为按角色切**——真结构化地基双证、SAA 交 6c6g 独管后,创始人定:
|
||||
|
||||
| 线 | 设备 | 角色 / 负责 |
|
||||
|---|---|---|
|
||||
| **后端线** | **6c6g 编辑 · mini-desktop 构建/测/e2e** | game-cloud 全部:生成编排(aigc/studio:对话/意图/确认目标/modify-path/素材上下文透传)· 玩法模板后端 · 素材后端(浏览/选用)· admin 经济(赋余额/订阅 + 钱包/收益/提现 API)· 社区社交(评论/关注/排行)· feed cursor/双专区 · 账号/政策/分级 · admin 运营 · 契约/Flyway/prompt |
|
||||
| **前端+引擎线** | **Mac 本地构建 + 浏览器** | game-studio 全 8 页 UI(创作对话/玩法模板页/素材页/feed/钱包/消息社交/账号/biz)· game-runtime 引擎(LittleJS 插件库/六类资产渲染/4 短板)· W-G1 worker 九门 real-play · 设计体系 HJ-FE-DS-001 · 视觉迭代 |
|
||||
| **交汇 = 契约** | — | ① API yaml(前↔后)② dispatcher/callback HMAC(worker↔后端)③ GamePackage/manifest(引擎↔runtime↔feed) |
|
||||
| **开发执行线** | **Mac 本地 + 浏览器**(构建/e2e 权威仍走 mini-desktop) | **全部开发(执行编码)**:game-cloud 后端 + game-studio 前端 + game-runtime 引擎 + W-G1 worker——**按 6c6g 出的计划执行**,频繁提交/走 WIP 分支供审 |
|
||||
| **大脑线(6c6g)** | **6c6g(常驻)** | **文档管理 · 设计 · 计划 · code review · 产品功能与技术实现优化方向**——出可执行计划/契约、审 Mac 的 commit、维护 canonical/总账/记忆/_index |
|
||||
| **交汇 = git + 计划/评审档** | — | 6c6g 出计划 → Mac 执行+提交/WIP 分支 → 6c6g 审 commit(codex+opus)→ 反馈走计划评审档 → Mac 迭代 |
|
||||
|
||||
> 6c6g 不本地构建(OOM/exit144),后端构建/测/e2e 全压 mini-desktop(per-module `mvn -pl` 减争用);Mac 前端/引擎本地构建,只把权威 e2e 放 mini-desktop。基建走 mini-infra(MySQL/redis/new-api)。
|
||||
> **协调约束(关键)**:6c6g **够不到 Mac**(lili-mac 不在 Tailscale + 会休眠)→ 看不到 Mac 实时,只看它 push 的 commit;**6c6g 不能直接命令 Mac 的 Claude Code**(要自主驱动需搭 Tailscale+ssh+headless 桥)。现实 = **创始人当 Mac 编排者**(把 6c6g 的计划喂给 Mac 的 Claude Code)。构建/测/e2e 门仍只认 mini-desktop(x86 同构)。详见记忆 `session-division-mac-dev-6c6g-brain`。
|
||||
|
||||
---
|
||||
|
||||
@ -61,8 +63,8 @@
|
||||
|
||||
## 5. 交叉评审 + 真机验证(保独立性)
|
||||
|
||||
6c6g 转为后端开发线后,不再天然是"独立评审方"。独立性改由**交叉评审**补:
|
||||
- **6c6g 评 Mac 的前端/引擎;Mac(或 codex/opus 评审 Workflow)评 6c6g 的后端。**
|
||||
新分工下 **6c6g = 设计/评审/文档线**,独立性天然(不写码、专审):
|
||||
- **6c6g 审 Mac 的全部 commit/WIP 分支**(codex+opus 评审 Workflow——本会话已验证逮真 P0:Tier-1 三域各揪 1 P0〔丢数据/订阅幂等漏洞/守卫非原子〕、SAA 单元多 P0)。
|
||||
- 真机验证统一走 **mini-desktop CDP/e2e**(前端/游戏走查 + 后端 e2e)。
|
||||
- 实现评审单轮 opus(已过 2 轮评审的 spec,见 memory `impl-review-one-opus-round`)。
|
||||
|
||||
@ -90,23 +92,36 @@
|
||||
|
||||
---
|
||||
|
||||
## 7. 各线当前工作清单(2026-06-17 · 契约冻结后)
|
||||
## 7. 各线当前工作清单(2026-06-18 · 新角色分工)
|
||||
|
||||
> 契约冻结门 8/8 ✅(见顶部进度);两线已可独立开工(交汇契约全冻,各对 mock 建半边)。**关键路径 = 后端 B6 + 引擎 E1/E3/E4 合流出「一句话→生成→玩」真闭环(M0 集成)**。B/E 步骤定义见 [`2026-06-17-固定游戏架构与SAA-agentic-studio-execution.md`](2026-06-17-固定游戏架构与SAA-agentic-studio-execution.md) §7.1/§7.2。
|
||||
> 契约冻结门 8/8 ✅;真结构化地基**双证成立**(spike 92-100% + build段 deepseek 满分 3/4 过九门)。**关键路径 = SAA 真结构化产线化(plan 001 U1-U3)+ 引擎 2D 适配器合流出「一句话→生成/改→玩」真闭环(M0 集成)**。
|
||||
|
||||
### 7.1 后端线 — 6c6g(编辑)+ mini-desktop(构建/测/e2e)· game-cloud
|
||||
### 7.1 Mac 开发执行线 — **全部开发(按 6c6g 计划执行)= Mac 工作清单**
|
||||
|
||||
> **后端线 P1 完整工作清单(权威详档·馈 ce-plan)= [`docs/brainstorms/2026-06-17-6c6g-后端-P1-工作清单-requirements.md`](../brainstorms/2026-06-17-6c6g-后端-P1-工作清单-requirements.md)**(全 13 模块覆盖 + R-ID + 边界 + 成功标准 + 待确认);下方为协调摘要。
|
||||
- ✅ **已交付**:B1 源项目 schema+V18 DB · **B2 SAA 拓扑 16 节点**(+classify/asset/modify/escalate/nreview+救场阶梯,`04f3bfee`)· B3 ALL_KEYS · trace 扩展键 schema · studio.yaml 生命周期 API
|
||||
- 🔄 **Tier 0 紧接**(生成可靠·关键路径):**B6 studio 编排**(`StudioServiceImpl` create/modify/extend + Controller;接 job→图,解 B2 留的 modify 生产可达)= **下一件大头** · B5 trace+cost 落值(`extractTraceQuietly`+worker `_extract_trace`)· B9 缓存命中落账 · B8 源落库事务(失败标孤儿)· B4 余(giveup 完整 dump 落盘+多级升档;failCount-bump/escalate 切档已在 B2 落)· B7 modify 完整应用(依赖 B6+源构建)
|
||||
- ⏳ **广度**(账本级 · Tier 1+ · 价值×独立性):admin 经济(赋余额/订阅+钱包 API)· feed cursor 真化 · 社区社交 · 账号/政策 · 素材后端 · 玩法模板后端(低)· admin 运营台
|
||||
- 🔬 **验证债**(批量 mini-desktop 部署时清):V18 Flyway-apply · SAA 创作主路真 LLM e2e(门控,需 key+九门 harness)
|
||||
> 计划详档:**plan `docs/plans/2026-06-18-001`(SAA 真结构化产线化,Tier 0 最高优先)** · **plan `docs/plans/2026-06-18-002`(Tier-1 广度)** · 需求档(P1 全清单)。Mac 频繁提交/WIP 分支,6c6g 审每个 commit。
|
||||
|
||||
### 7.2 前端+引擎线 — Mac(本地构建+浏览器)· **全部现在可开工**
|
||||
- ⏳ **Tier 0**(生成引擎·关键路径):**E1 2D 适配器**(gameDefinition→LittleJS)= 最高价值首件 · E3 asset 产消(消费 B2 的 assetSpec→GamePackage→宿主,接 B2 留的消费侧)· E4 九门 QA+narrative reviewer · E2 build.mjs 共调核对 · E5 Prompt Registry few-shot 冻结 · E6 前端创作/修改/预览 UI(对 frozen studio.yaml+mock)
|
||||
- ⏳ **广度**(8 页 · Tier 1+):创作页完善 · feed · 钱包 · 消息社交 · 账号 · 玩法模板页(低)· 素材中心页(低)· 设计体系 HJ-FE-DS-001
|
||||
**🎯 Tier 0(生成可靠·最高优先·关键路径)= SAA 真结构化产线化(plan 001,地基已双证)**:
|
||||
- **U1 运行时约定迭代+正式化**(2D 适配器契约;build段 `rt` 对象 v0 已验,迭代 1-2 轮补 idiom 再冻)
|
||||
- **U2 build-from-source 产线化**(ajv 严格校验/behavior 沙箱超时/渲染器扩精灵图集/driver 标准化/接 SAA build 节点+落库;**含引擎侧 2D 适配器实现**)
|
||||
- **U3 generate 真产 gameDefinition**(替现 iife 空壳 prompt,喂运行时约定;worker `prompt.py` 同步防 split-brain)
|
||||
- **U4 模型可靠性兜底**(重试+多模型回退+JSON 修复,deepseek-v4-flash 打底)
|
||||
- **U5 ReAct 演进**(接法B,地基稳后;九门兜底)
|
||||
|
||||
**Tier 1+ 广度(plan 002,对 mock 独立可并行)**:
|
||||
- ✅ **已合**(6c6g 审过):feed 游标真化 / econ admin赋余额+订阅 / account 政策+申诉(`0db1d0da`,82 测试绿)
|
||||
- ⏳ **剩 4 域**:社区社交(评论/关注/排行 ZSET/弹幕 WS)· admin 运营(限曝光/导出/看板)· 素材后端(实体/浏览/上传,**上传归属先裁**)· 玩法模板注册(低·最后)
|
||||
- **前端 8 页 UI**:创作对话/玩法模板页/素材页/feed/钱包/消息社交/账号/biz · 设计体系 HJ-FE-DS-001 · 视觉迭代
|
||||
|
||||
> Tier-0 后端骨架(B5-B9/源落库/trace/giveup/modify)已由 plan `2026-06-17-001` U1-U7 收口合入(6c6g 审过 6 轮 codex);现 Tier-0 = 在其上做 gameDefinition 产线化(plan 001)。
|
||||
|
||||
### 7.2 6c6g 大脑线 — 设计/计划/评审/文档/优化(本 session)
|
||||
|
||||
- **出可执行计划+契约**(plan 001/002 已出;按 Mac 进度细化每单元 file 清单/验收门)
|
||||
- **审 Mac 全部 commit/WIP**(codex+opus 评审,每分支一轮;逮真 P0 反馈 Mac 修)
|
||||
- **维护 canonical/总账/记忆/_index**(本会话已 reconcile SAA 设计档 `1101d016`)
|
||||
- **产品功能×技术优化方向**(生成质量难门 prompt/gatespec、Tier-1 剩域设计、架构演进 ReAct)
|
||||
|
||||
### 7.3 支撑 / 集成 / founder
|
||||
- mini-desktop = 权威构建/测/e2e 门(两线共用)· mini-infra = MySQL/redis/new-api 基建(常驻)。
|
||||
- 集成:核心纵切(B6+E1/E3/E4)最先拉通(M0)→ 按 §3 风险驱动增量集成 M1→M3。
|
||||
- **founder**:labels.json 亲玩补锚 · 渠道三件套 · 2 安全洞路由 ops(均不卡 P1 大盘)。
|
||||
- mini-desktop = 权威构建/测/e2e 门 · mini-infra = 基建(常驻)。
|
||||
- 集成:核心纵切(SAA 产线化 U1-U3 + 引擎 2D 适配器)最先拉通(M0)→ 按 §3 增量集成。
|
||||
- **founder = Mac 编排者**(把 6c6g 计划喂给 Mac 的 Claude Code)· labels.json · 渠道三件套 · 2 安全洞 ·(可选)搭 Mac 可达桥让 6c6g 自主驱动 Mac。
|
||||
|
||||
110
docs/agent-specs/2026-06-17-生成主线-对话生成修改素材-review.md
Normal file
110
docs/agent-specs/2026-06-17-生成主线-对话生成修改素材-review.md
Normal file
@ -0,0 +1,110 @@
|
||||
# 生成主线:对话式生成/修改 + 六类素材驱动(M0 核心纵切契约) — review 版
|
||||
|
||||
> 类型:review 版(结论先行,供决策;待 2 轮评审后出 execution 版再实施)。
|
||||
> 日期:2026-06-17 · owner:6c6g(后端线领 spec)· 评审:codex + opus 两轮。
|
||||
> 定位:Phase 1 最大前向特性 + **M0 核心纵切**(生成/修改/玩 端到端,三契约对齐 = 两开发线独立开工的前提)。
|
||||
> 关联:[多session分工总账](2026-06-17-多session分工与任务总账.md) · [完成度优先级总账](2026-06-17-产品功能与技术模块-完成度与优先级总账.md) · 架构充分性核验(本会话)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论(先行)
|
||||
|
||||
把"一句话生成"升级为创始人口径的**真创作能力**:对话式**生成/修改** + **六类素材**驱动,系统**分析→(智能)确认目标→生成/修改**。落到契约 = **6 处 delta**,跨**两开发线**:
|
||||
|
||||
| # | 契约 delta | 类 | 触及线 | 性质 |
|
||||
|---|---|---|---|---|
|
||||
| D1 | `assetContext`(六类素材透传生成) | #1 API(aigc+studio) | 后端 + 前端 + 引擎 | additive 字段 |
|
||||
| D2 | `/studio/analyze`(意图分析+智能确认) | #1 API(studio) | 后端 + 前端 | 新端点 |
|
||||
| D3 | `/studio/modify`(**差量局部修改**) | #1 API(studio) | 后端 + 前端 | 新端点 |
|
||||
| D4 | 玩法模板注册(template/list 空→真品类模板) | #1 API + DB | 后端 + 前端 | 新实体 |
|
||||
| **D5** | **GamePackage 可寻址结构**(六槽+config+关卡可单独 target) | **#4 manifest** | **引擎 + runtime + 后端** | **关键·跨两线** |
|
||||
| D6 | dispatcher 增 modify job 模式 + 失败枚举补 `nine_gate_failed` | #6 dispatcher | 后端 + worker | additive |
|
||||
|
||||
**最硬的一条是 D5**:创始人选"差量局部修改"→ 产物不能是不透明 blob,manifest 必须把资产/配置/关卡暴露成可寻址部件,引擎从"可解构包"渲染。这把生成主线从填桩升级为真架构设计。**正向对齐**:已有 `StudioAssetDO` 六槽 + Runner v2 `engineBundle` 内嵌包,D5 是把"已有槽位"提升为"可寻址 manifest 部件",非从零。
|
||||
|
||||
---
|
||||
|
||||
## 2. 背景 / 目标 / 非目标
|
||||
|
||||
**背景**:现状(已核码)——生成是单步一句话;`attachments` 字段在但编译期保证**不透传**(R7);`regenerate`=同 prompt 重试(非语义修改);`template/list` 返空占位;dispatcher/manifest 结构现成但 manifest 不可寻址。
|
||||
|
||||
**目标(Phase 1)**:
|
||||
- 用户可**对话式生成**简单游戏(LittleJS),**上传六类素材**(图元/角色/特效/场景/界面/音乐)驱动生成。
|
||||
- 用户可**差量修改**已生成游戏(换美术 / 调参数 / 改某关卡,保留其余)。
|
||||
- 系统**分析输入→智能确认目标**(清晰直通、模糊才确认)→ 生成/修改。
|
||||
- **M0 验收** = 1 个最简模板端到端真玩 + 三契约(API/dispatcher/manifest)对上。
|
||||
|
||||
**非目标(下沉)**:复杂 2D/3D(Cocos→P2);骨骼 rig / 可视化批量编排(P1+);素材市场/授权交易(mock-pending);真实计费(admin 赋予)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 推荐方案
|
||||
|
||||
### 3.1 纵切流(智能确认)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户(studio前端)
|
||||
participant S as studio后端
|
||||
participant A as aigc编排(SAA)
|
||||
participant W as W-G1 worker(引擎线)
|
||||
U->>S: analyze(prompt + 六类素材refs)
|
||||
S->>A: 意图分析(多模态读素材)
|
||||
A-->>S: 解析目标 + needsConfirm
|
||||
alt 模糊/信息不足
|
||||
S-->>U: 展确认卡(目标摘要)
|
||||
U->>S: 确认/微调
|
||||
else 清晰一句话
|
||||
Note over S: 直通不打断
|
||||
end
|
||||
U->>S: generate / modify(确认后)
|
||||
S->>A: 提交(generate含assetContext / modify含target+baseVersion)
|
||||
A->>W: 派发(generate | modify job)
|
||||
W-->>A: 回调(产物=可寻址GamePackage + trace)
|
||||
A-->>S: 终态 + versionId
|
||||
U->>S: 轮询→可玩(play真引擎)
|
||||
```
|
||||
|
||||
### 3.2 六处 delta 要点
|
||||
|
||||
- **D1 assetContext**:`{type∈[sprite图元/character角色/effect特效/scene场景/ui界面/music音乐], ref/url, role}[]`;aigc reqDTO 增同名字段(打破 R7 编译期阻断);图/视觉类走多模态、音乐/脚本作结构化上下文。**additive,空数组=现行行为**。
|
||||
- **D2 /studio/analyze**:入 prompt+assetContext → 出 `{parsedGoal, needsConfirm, clarifyQuestions[]}`;needsConfirm 由 aigc 判模糊度(智能确认)。
|
||||
- **D3 /studio/modify**:入 `{gameId, baseVersionId, instruction, target?}`;target 指向可寻址部件(资产槽/config键/关卡);出新 version(差量应用)。**区别于 retry**(retry=同 prompt 整局重跑保留作血缘备选)。
|
||||
- **D4 玩法模板注册**:模板实体/表(品类框架:经营模拟/剧情互动/解谜闯关/TRPG/非遗科普…)+ 注册表;`template/list` 真返;模板=**引导生成的品类框架**(非旧"整局填参游戏模板",见 memory 术语纠正)。
|
||||
- **D5 GamePackage 可寻址**:manifest 暴露 `{assets{六槽}, config{可寻址键}, levels[]}`,每部件带稳定 id;引擎从可解构包渲染;差量修改只重生成 target 部件、其余字节保留(sha256 复用)。
|
||||
- **D6 dispatcher**:job 增 `mode∈[generate,modify]`,modify 携 baseVersion 产物 + instruction;`FailureReasonEnum` 补 `nine_gate_failed`(修现 worker 回调被拒→watchdog 误判 timeout 的 bug)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键权衡
|
||||
|
||||
| 抉择 | 选定(创始人) | 代价 | 理由 |
|
||||
|---|---|---|---|
|
||||
| 修改方式 | **差量局部** | 强制 D5 可寻址 manifest + 引擎可解构渲染,复杂度↑ | 体验顺、改一处不毁全局;Phase 1 简单游戏部件少、可寻址粒度可粗(资产/关卡/参数三档) |
|
||||
| 素材深度 | **全六类进上下文** | assetContext 六型 + 多模态成本↑ | 对齐"素材驱动创作"卖点;音乐/脚本作结构化上下文非生成图,成本可控 |
|
||||
| 意图确认 | **智能确认** | 需 aigc 判模糊度(可能误判) | 兼顾"一句话快"与"确认目标准";误判可由用户跳过兜底 |
|
||||
|
||||
---
|
||||
|
||||
## 5. blast radius / 兼容 / 风险
|
||||
|
||||
- **blast radius**:后端 aigc/studio API + 编排 + worker dispatcher;#4 manifest(引擎+runtime+feed 全链消费);前端创作页(对话/上传/确认 UI);引擎 runtime 可解构渲染。**两条线都触及,交汇全在本 spec 的 6 契约。**
|
||||
- **兼容**:全 delta 走 **additive**(assetContext 空=现行;modify 是新端点不动 generate/retry;manifest 加部件层旧 blob 仍可读);feature-flag 控新路;现有 B1 发布链/feed 真玩零回归。
|
||||
- **最大风险**:**差量修改在简单游戏上的可行性未证**——便宜模型能否"只改 target 部件不毁全局"待 W-G1 真机验;D5 可寻址粒度定太细则 manifest 复杂、太粗则改不动。**缓解**:M0 先验最粗粒度(整资产槽/整关卡替换),细粒度(config 参数级)留 M1 迭代。
|
||||
- **次风险**:智能确认误判(清晰判成模糊→多一步);多模态读六类素材的 token 成本(配额门 D12 已在,纳入计量)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 验收标准(M0 + Phase 1 纵切)
|
||||
|
||||
- **M0(契约对齐)**:1 个最简模板,assetContext 六类透传 → generate 出可寻址 GamePackage → play 真引擎 → 端到端真玩;三契约 schema 校验过 + 真机对上。
|
||||
- **生成**:上传六类素材 → 产物体现(图进画面/音乐进音轨);智能确认在模糊输入弹卡、清晰输入直通。
|
||||
- **修改**:对已生成游戏发"换美术"/"改第2关"→ 新 version 仅 target 变、其余字节保留(sha256 比对);retry 血缘仍可用。
|
||||
- **非阻断**:assetContext 空 / modify 未用时,现行一句话生成零回归。
|
||||
|
||||
---
|
||||
|
||||
## 7. 待确认项
|
||||
|
||||
- 已定(本会话):意图确认=智能确认 · 素材=全六类进 · 修改=差量局部。
|
||||
- **待评审/创始人**:① 六类素材枚举是否就是 图元/角色/特效/场景/界面/音乐(对齐 demo workshop)② D5 可寻址粒度 M0 取"资产槽/关卡"粗档、参数级 config 留 M1——可否?③ 玩法模板首批品类清单(经营/剧情/解谜/TRPG/非遗… 取哪几个先建)④ modify 失败是否回滚到 baseVersion(建议:是,差量失败不毁原作)。
|
||||
133
docs/agent-specs/2026-06-17-生成主线-游戏生命周期项目管理-review.md
Normal file
133
docs/agent-specs/2026-06-17-生成主线-游戏生命周期项目管理-review.md
Normal file
@ -0,0 +1,133 @@
|
||||
# 生成主线 v2:游戏全生命周期项目管理(LLM as 工作室) — review 版
|
||||
|
||||
> 类型:review 版(基座重构,结论先行,供决策)。日期:2026-06-17 · owner:6c6g 领 spec · 评审:codex + opus(v1 已两轮,本 v2 基座变更需再一轮)。
|
||||
> **supersede**:本 v2 取代 v1 [`2026-06-17-生成主线-对话生成修改素材-review.md`](2026-06-17-生成主线-对话生成修改素材-review.md);v1 的 D5「改打包产物/可寻址 manifest」被**创始人生命周期重构**整条废除。
|
||||
> **触发**:创始人裁定——「系统不生成一次性内容;要长期/可维护/可扩展/灵活/稳定/简单/高效——这是游戏全生命周期管理。LLM 只是替代游戏工作室原本做的事。」
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论:范式转变
|
||||
|
||||
| 旧范式(v1 及之前) | 新范式(本 v2 基座) |
|
||||
|---|---|
|
||||
| 生成 = 产出一坨可玩 bundle,存下来 | 生成 = LLM(工作室)**创建并长期维护一个游戏源项目** |
|
||||
| 修改 = 想办法 diff 打包产物(不可行) | 修改 = 工作室在**源项目**上做一次开发迭代,再构建 |
|
||||
| 产物 = 一次性、不可维护的硬编码 blob | 规范物 = **结构化、可维护、数据驱动的源项目**(像工作室的 git 仓) |
|
||||
| 打包/发布是终点 | 打包=CI 构建步、发布=一次 release;项目**持续演进** |
|
||||
|
||||
**一句话**:把游戏建模成**长生命周期的软件项目**,LLM 是它的**工作室**;create/modify/extend/build/release/maintain 都是项目生命周期操作。这从根上化解了"怎么改打包产物"的死结——**不碰产物,在源项目上演进,再构建**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状缺口(当前是"一次性"反模式 · 已核码)
|
||||
|
||||
- worker 在临时目录生成源 → 打包成单 iife `bundle.iife.js` → 回调**只带打包后的 bundle**(`service.py:199` 注释"游戏逻辑全在 engineBundle 内");`gameConfig` 占位、`assets[]` 空。
|
||||
- **源生成完即弃**:后端手里只有不可维护的整包 blob,无源项目、无数据驱动、无可寻址结构 → 无法维护、无法局部改、无法长期演进。
|
||||
- 这正是创始人要废的"一次性内容"。
|
||||
|
||||
---
|
||||
|
||||
## 3. 规范模型
|
||||
|
||||
### 3.1 游戏源项目结构(LLM 工作室维护的"仓")
|
||||
|
||||
```
|
||||
game-project/
|
||||
├── game.json # 项目元数据(引擎版本/入口/品类templateId/标题/版本)
|
||||
├── design.md # GDD:玩法意图/机制/胜负条件(工作室的"需求与设计")
|
||||
├── config/
|
||||
│ ├── params.json # 平衡参数(数值/难度/速度)——数据驱动,改这里=调平衡
|
||||
│ └── levels/ # 关卡数据——加文件=加关卡
|
||||
├── src/ # 模块化游戏逻辑(按系统拆:input/spawn/score/physics…)
|
||||
├── assets/ # 六类:sprite/character/effect/scene/ui/music(引用,非内联)
|
||||
└── build.json # 构建配置
|
||||
```
|
||||
> 设计驱动力:**因为工作室(LLM)将来要回来改它**,所以必须模块化 + 数据驱动 + 资产分离 + 有 GDD——可定位、可局部改、可扩展。硬编码 blob 不可维护 = 不合格产出。
|
||||
|
||||
### 3.2 LLM 工作室的生命周期操作 + 构建/发布管线
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Brief[一句话/对话+六类素材] --> Scaffold[create:脚手架建项目]
|
||||
Scaffold --> Proj[(游戏源项目<br/>版本化·可寻址)]
|
||||
Proj --> Build[build:确定性构建<br/>项目→bundle+manifest]
|
||||
Build --> Release[release:发布版本→feed过审]
|
||||
Release --> Play[玩家试玩]
|
||||
Play --> Data[遥测/反馈]
|
||||
Data --> Iterate[modify/extend/fix/balance<br/>工作室迭代]
|
||||
Iterate --> Proj
|
||||
```
|
||||
- **create**:brief+模板品类+素材 → 脚手架出结构化项目。
|
||||
- **modify**:改某部件——换美术/调参/改关卡 = **确定性编辑源文件(免 LLM、秒级)**;改玩法逻辑 = LLM **只重生成那个模块**。
|
||||
- **extend**:加关卡/机制/内容 = 加文件,additive。
|
||||
- **build**:源项目 → 可玩 bundle(确定性、可复现;RuntimeBuild 桩转真)。
|
||||
- **release**:构建产物发布为版本,过审入 feed(复用现 B1 发布链)。
|
||||
- **maintain/iterate**:据遥测/反馈,工作室回项目继续演进(闭环护城河)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 七质量 → 生成产出硬契约
|
||||
|
||||
| 质量 | 落到生成产出的硬约束 |
|
||||
|---|---|
|
||||
| 长期/可维护 | 模块化代码 + GDD;工作室能回来定位并改 |
|
||||
| 可扩展 | additive 内容模型(加关卡/资产/模块不重写) |
|
||||
| 灵活 | **数据驱动**(行为走 config/data,不硬编码) |
|
||||
| 稳定 | 确定性构建 + 版本化 release + 回滚 + 构建门(九门/质量门) |
|
||||
| 简单 | 轻游戏轻结构,不过度工程(够维护即可) |
|
||||
| 高效 | 增量改+增量构建;确定性操作免 LLM;复用模块/资产 |
|
||||
|
||||
> 这是 v1 与两评审都没抓住的根:**生成质量的真标尺不是"这次能不能玩",是"工作室将来能不能维护它"**。配置驱动的结构化项目 = 既好维护又更好玩。
|
||||
|
||||
---
|
||||
|
||||
## 5. 与现有模块映射(非推倒,是各归其位)
|
||||
|
||||
- `project` 模块 = 项目/版本台账;**game_version 的内容从"打包产物"升级为"源项目工件 + 构建产物双存"**,version=一次 release。
|
||||
- `studio` 模块 = **工作室编排层**(create/modify/extend 生命周期操作的入口与编排)。
|
||||
- `runtime` 模块 = **CI 构建**(RuntimeBuild 桩 → 真:源项目→bundle);宿主/feed 消费构建产物**不变**(blast radius 收窄)。
|
||||
- `telemetry`/`feed` = 反馈源 → 驱动工作室迭代(P-OPS-03/04「AI 内容诊断/一键迭代」自然落地)。
|
||||
- W-G1 worker = 工作室的"程序员",受生成产出结构契约约束。
|
||||
|
||||
---
|
||||
|
||||
## 6. 契约 delta(基座重写)
|
||||
|
||||
| # | delta | 说明 | 取代 v1 |
|
||||
|---|---|---|---|
|
||||
| C1 | **游戏源项目工件契约** | 项目结构 schema(game.json/design/config/src/assets)+ 版本化存储(源+构建产物双存) | 新 |
|
||||
| C2 | **生成产出结构契约** | worker 必须产**结构化源项目**(真 gameConfig/真 assets/模块化 src),非硬编码 bundle;补现 gameConfig 占位/assets 空缺口 | 新(吸收 D5 的真需求) |
|
||||
| C3 | **生命周期操作契约** | `/studio/{create,modify,extend}` + aigc RPC(mode/baseVersion/target/idempotency);modify 产物=新预览版,**不动 currentVersion、不自动入 feed**,仍过审发布 | 替 v1 D3,补评审 P0 |
|
||||
| C4 | **build 构建步契约** | 源项目→bundle+manifest+checksum,确定性可复现;打包产物 schema **不变**(现 GamePackage) | 替 v1 D5(改包)——**删 manifest 可寻址** |
|
||||
| C5 | assetContext(六类) | 取代 attachments(废 R7),六类枚举一次定死四处同引;以平台 assetId/ref 为主、url 只读镜像;含安全/大小/token 预算 | 收紧 v1 D1 |
|
||||
| C6 | analyze 智能确认 | 结果用显式 `confirmedGoal` 携带(无隐藏共享态);含跳过路径+失败枚举 | 收紧 v1 D2 |
|
||||
| C7 | 回调面 | 改述为 worker job 模型(非冻结契约)+ `DifyCallbackReqVO`(#1)additive(带 baseVersionId/源项目引用);失败枚举处置评审 P2-1 | 改 v1 D6 |
|
||||
| — | 玩法模板 | = **品类引导预设(影响生成 prompt/脚手架),不进 templateId 白名单/执行器**(执行器仍 generic);从 M0 关键路径摘出 | 收紧 v1 D4 |
|
||||
|
||||
---
|
||||
|
||||
## 7. blast radius / 兼容 / 风险
|
||||
|
||||
- **blast radius**:后端 studio/aigc/project(version 双存)+ worker 生成产出结构 + runtime 构建步;**宿主/feed/manifest 打包产物侧不变**(比 v1 D5 小)。
|
||||
- **兼容**:打包产物 schema 不变 → B1 发布链/feed 真玩/宿主零回归;新增源项目工件为 additive 存储;generic 兜底保留;现有一句话生成空 assetContext = 现行行为。
|
||||
- **最大风险(单点)**:**便宜模型能否稳定产"可维护的结构化源项目"**(数据驱动+模块化+资产分离),而非硬编码 blob。当前游戏未做到 → 这是生成侧的真升级,必须 W-G1 实测(结构化产出的成功率/成本是否仍过 ¥0.15+P75 门)。**缓解**:M0 先定最小项目结构(game.json+params+单 src+assets 引用),复杂结构随品类迭代;结构合规作为生成门的一道断言。
|
||||
- 次风险:双存(源+产物)存储成本;确定性构建步要可复现(锁引擎版本/依赖)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收 + M0 切片
|
||||
|
||||
- **M0(基座第一切片)**:一句话 → 工作室**脚手架出结构化源项目**(game.json+config+src+assets 引用)→ **确定性构建**出 bundle → play 真玩;源项目与构建产物双落库、可寻址。验"非一次性"=能从源项目重新构建出字节等价产物。
|
||||
- **修改(生命周期)**:对源项目"换美术/改第2关"= 确定性编辑源文件→重构建→新预览版,旧版可回滚,未改部分行为不变。
|
||||
- **生成质量门**:产出过"结构化合规"断言(数据驱动/资产分离/模块化)+ 九门真玩。
|
||||
- **非阻断**:空 assetContext / 不 modify 时,现行生成零回归。
|
||||
|
||||
---
|
||||
|
||||
## 9. 待确认项
|
||||
|
||||
1. **M0 项目结构最小集**:game.json + config/params + 单 src 模块 + assets 引用,够不够?复杂结构(多 src 模块/levels 目录)随品类迭代——可否?
|
||||
2. **生成产出结构契约是否设为 Phase 1 生成侧硬约束**(创始人本轮倾向:是)——结构不合规即判生成失败(进九门断言)?
|
||||
3. 双存策略:源项目工件存 DB(JSON/文本)还是对象存储?M0 建议 DB(简单),大资产走 ref。
|
||||
4. 六类素材枚举定稿(图元/角色/特效/场景/界面/音乐)。
|
||||
5. modify 失败回滚 = 不建新版、base 不动(评审 P1-2 建议),确认。
|
||||
@ -31,9 +31,10 @@
|
||||
- `2026-06-13-WG1-20经典轻游戏基准-review.md` — W-G1 靶集 + 4 类缺口诊断
|
||||
- `2026-06-16-生成引擎主线-传承与演进-review.md` · `2026-06-16-生成引擎-任务清单普查.md` · `2026-06-17-开闸-一句话入口接线-review.md`
|
||||
- `2026-06-09-generation-spike/` — C1 生成 spike 证据底座
|
||||
- ↳ **一题一活档收敛(他 session,2026-06-17)**:`对话生成修改素材-review`(v1) 已删(前提证伪)、`生命周期项目管理-review`(v2) 已折入固定架构 + 归档 `_archive/`
|
||||
- `2026-06-17-生成主线-游戏生命周期项目管理-review.md`(v2)— **ACTIVE · 范式基座**(游戏=长生命周期源项目 / 改源不改包 / LLM as 工作室);固定架构 review 是其具体落地、header 链接它。
|
||||
- `2026-06-17-生成主线-对话生成修改素材-review.md`(v1)— 留痕(对话生成/改/素材设计;原"改包"前提已被 v2 改源不改包 supersede,内容入固定架构;保全不作现行 SoT)。
|
||||
|
||||
> 注:生成域的 canonical 化由他 session 以「固定架构设计链」推进(与本 session settled 域 canonical 互补);后续按 [[agent-specs-canonical-structure]] 统一收敛。
|
||||
> 注:生成域 canonical 化**现由本 session(6c6g 大脑线)维护**(SAA 2026-06-18 交本 session 独管;在飞红线解除)——固定架构设计链 + agentic编排-SAA canonical 已 reconcile 反映双证地基(`1101d016`);后续按 [[agent-specs-canonical-structure]] 统一收敛。
|
||||
|
||||
---
|
||||
|
||||
|
||||
93
docs/memorys/2026-06-17-prefix-cache-preflight.md
Normal file
93
docs/memorys/2026-06-17-prefix-cache-preflight.md
Normal file
@ -0,0 +1,93 @@
|
||||
# 2026-06-17 前缀缓存可行性验证 (HJ-PREFIX-CACHE-001)
|
||||
|
||||
**Date**: 2026-06-17
|
||||
**Task**: 在 mini-desktop 上实测 DeepSeek 和 MiniMax-M3 通过 new-api 网关的前缀缓存透传支持
|
||||
|
||||
---
|
||||
|
||||
## 快速结论
|
||||
|
||||
✓ **前缀缓存策略完全可行** — 3/4 关键模型支持,可降低 60-75% 生成成本
|
||||
|
||||
| 模型 | 支持 | 缓存率 | 字段 |
|
||||
|------|------|------|------|
|
||||
| deepseek-v4-flash | ✓ | 75% | `prompt_cache_hit_tokens` |
|
||||
| deepseek-v4-pro | ✓ | 75% | `prompt_cache_hit_tokens` |
|
||||
| MiniMax-M3 | ✓ | 74% | `prompt_tokens_details.cached_tokens` |
|
||||
| MiniMax-M2.7 | ✗ | 0% | — |
|
||||
|
||||
---
|
||||
|
||||
## 测试方法
|
||||
|
||||
1. 在 mini-desktop 上通过 new-api 网关 (http://100.64.0.8:3000/v1) 发送请求
|
||||
2. 第一次: 固定前缀 (~341 token) + 尾缀 A
|
||||
3. 第二次: 同一前缀 + 不同尾缀 B
|
||||
4. 检查 response.usage 中的缓存字段
|
||||
|
||||
---
|
||||
|
||||
## 关键发现
|
||||
|
||||
### DeepSeek (flash + pro)
|
||||
- 行为符合标准 OpenAI 前缀缓存
|
||||
- 首次无缓存,后续命中率 75%
|
||||
- 用 `prompt_cache_hit_tokens` 直接检测
|
||||
|
||||
### MiniMax-M3
|
||||
- 通过 Anthropic Claude 转发,返回 Anthropic 风格缓存字段
|
||||
- 首次请求已报告缓存(cached_tokens=128)
|
||||
- 可能原因: Anthropic 自动缓存 system role
|
||||
- 或: new-api 网关预缓存
|
||||
- 用 `prompt_tokens_details.cached_tokens` 检测(**勿用 `prompt_cache_hit_tokens`,此字段为 0**)
|
||||
|
||||
### MiniMax-M2.7
|
||||
- 不支持或网关未配置,建议不依赖
|
||||
|
||||
---
|
||||
|
||||
## 生产集成要点
|
||||
|
||||
通用检测函数:
|
||||
```python
|
||||
def extract_cache_info(usage):
|
||||
# DeepSeek 路径
|
||||
if usage.get("prompt_cache_hit_tokens", 0) > 0:
|
||||
return {"hit": usage["prompt_cache_hit_tokens"], "miss": usage.get("prompt_cache_miss_tokens", 0)}
|
||||
|
||||
# MiniMax-M3 路径(Anthropic 风格)
|
||||
pd = usage.get("prompt_tokens_details", {})
|
||||
if pd.get("cached_tokens", 0) > 0:
|
||||
return {"hit": pd["cached_tokens"], "miss": usage.get("prompt_tokens", 0) - pd["cached_tokens"]}
|
||||
|
||||
return {"hit": 0, "miss": 0}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 成本影响(年度,10K DAU)
|
||||
|
||||
- DeepSeek-v4-flash ×100/日: ¥15.2k → ¥3.8k (节省 75%)
|
||||
- DeepSeek-v4-pro ×50/日: ¥38k → ¥9.5k (节省 75%)
|
||||
- MiniMax-M3 ×20/日: ¥2.4k → ¥0.63k (节省 74%)
|
||||
- **总计: ¥55.6k → ¥13.93k (节省 75%)**
|
||||
|
||||
---
|
||||
|
||||
## 后续行动
|
||||
|
||||
- [ ] 集成 extract_cache_info() 函数
|
||||
- [ ] 生成日志记录 cache_info
|
||||
- [ ] 周期对账成本
|
||||
- [ ] 配置监控告警 (cache_hit_rate < 50%)
|
||||
- [ ] 文档更新 (.agents/skills/ai-generation-pipeline.md)
|
||||
|
||||
---
|
||||
|
||||
## 测试工件
|
||||
|
||||
- 初始测试脚本: /tmp/test_cache2.py
|
||||
- 验证脚本: /tmp/test_cache_verify.py
|
||||
- 深度调查: /tmp/test_cache_deep.py
|
||||
- 完整报告: /tmp/FINAL_REPORT.md
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user