|
|
@@ -1,7 +1,11 @@
|
|
|
package handlers
|
|
|
|
|
|
import (
|
|
|
+ "encoding/json"
|
|
|
+ "fmt"
|
|
|
"net/http"
|
|
|
+ neturl "net/url"
|
|
|
+ "time"
|
|
|
|
|
|
"cmr-backend/internal/httpx"
|
|
|
"cmr-backend/internal/service"
|
|
|
@@ -24,6 +28,59 @@ func (h *DevHandler) BootstrapDemo(w http.ResponseWriter, r *http.Request) {
|
|
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
|
|
}
|
|
|
|
|
|
+func (h *DevHandler) CreateClientLog(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if !h.devService.Enabled() {
|
|
|
+ http.NotFound(w, r)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var input service.CreateClientDebugLogInput
|
|
|
+ if err := httpx.DecodeJSON(r, &input); err != nil {
|
|
|
+ httpx.WriteError(w, fmt.Errorf("decode client log: %w", err))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ entry, err := h.devService.AddClientDebugLog(r.Context(), input)
|
|
|
+ if err != nil {
|
|
|
+ httpx.WriteError(w, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": entry})
|
|
|
+}
|
|
|
+
|
|
|
+func (h *DevHandler) ListClientLogs(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if !h.devService.Enabled() {
|
|
|
+ http.NotFound(w, r)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ limit := 50
|
|
|
+ if raw := r.URL.Query().Get("limit"); raw != "" {
|
|
|
+ var parsed int
|
|
|
+ if _, err := fmt.Sscanf(raw, "%d", &parsed); err == nil {
|
|
|
+ limit = parsed
|
|
|
+ }
|
|
|
+ }
|
|
|
+ items, err := h.devService.ListClientDebugLogs(r.Context(), limit)
|
|
|
+ if err != nil {
|
|
|
+ httpx.WriteError(w, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": items})
|
|
|
+}
|
|
|
+
|
|
|
+func (h *DevHandler) ClearClientLogs(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if !h.devService.Enabled() {
|
|
|
+ http.NotFound(w, r)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if err := h.devService.ClearClientDebugLogs(r.Context()); err != nil {
|
|
|
+ httpx.WriteError(w, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"cleared": true}})
|
|
|
+}
|
|
|
+
|
|
|
func (h *DevHandler) Workbench(w http.ResponseWriter, r *http.Request) {
|
|
|
if !h.devService.Enabled() {
|
|
|
http.NotFound(w, r)
|
|
|
@@ -34,6 +91,75 @@ func (h *DevHandler) Workbench(w http.ResponseWriter, r *http.Request) {
|
|
|
_, _ = w.Write([]byte(devWorkbenchHTML))
|
|
|
}
|
|
|
|
|
|
+func (h *DevHandler) ManifestSummary(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if !h.devService.Enabled() {
|
|
|
+ http.NotFound(w, r)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ rawURL := r.URL.Query().Get("url")
|
|
|
+ if rawURL == "" {
|
|
|
+ httpx.WriteError(w, fmt.Errorf("manifest summary url is required"))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ parsed, err := neturl.Parse(rawURL)
|
|
|
+ if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
|
+ httpx.WriteError(w, fmt.Errorf("invalid manifest url"))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ client := &http.Client{Timeout: 15 * time.Second}
|
|
|
+ resp, err := client.Get(parsed.String())
|
|
|
+ if err != nil {
|
|
|
+ httpx.WriteError(w, fmt.Errorf("fetch manifest: %w", err))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer resp.Body.Close()
|
|
|
+
|
|
|
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
+ httpx.WriteError(w, fmt.Errorf("fetch manifest: http %d", resp.StatusCode))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var manifest map[string]any
|
|
|
+ if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
|
|
|
+ httpx.WriteError(w, fmt.Errorf("decode manifest: %w", err))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ summary := map[string]any{
|
|
|
+ "url": parsed.String(),
|
|
|
+ "schemaVersion": pickString(manifest["schemaVersion"]),
|
|
|
+ "playfieldKind": pickNestedString(manifest, "playfield", "kind"),
|
|
|
+ "gameMode": pickNestedString(manifest, "game", "mode"),
|
|
|
+ }
|
|
|
+ httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": summary})
|
|
|
+}
|
|
|
+
|
|
|
+func pickString(v any) string {
|
|
|
+ switch t := v.(type) {
|
|
|
+ case string:
|
|
|
+ return t
|
|
|
+ case float64:
|
|
|
+ return fmt.Sprintf("%.0f", t)
|
|
|
+ default:
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func pickNestedString(m map[string]any, parent, child string) string {
|
|
|
+ value, ok := m[parent]
|
|
|
+ if !ok {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ nested, ok := value.(map[string]any)
|
|
|
+ if !ok {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ return pickString(nested[child])
|
|
|
+}
|
|
|
+
|
|
|
const devWorkbenchHTML = `<!doctype html>
|
|
|
<html lang="zh-CN">
|
|
|
<head>
|
|
|
@@ -191,6 +317,7 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
display: grid;
|
|
|
gap: 16px;
|
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
|
+ align-items: start;
|
|
|
}
|
|
|
.stack {
|
|
|
display: grid;
|
|
|
@@ -203,6 +330,7 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
padding: 16px;
|
|
|
display: grid;
|
|
|
gap: 12px;
|
|
|
+ align-content: start;
|
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
|
|
|
}
|
|
|
.panel h2 {
|
|
|
@@ -261,6 +389,46 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
color: var(--text);
|
|
|
border: 1px solid var(--line);
|
|
|
}
|
|
|
+ .btn-stack {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+ .btn-badge {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-width: 38px;
|
|
|
+ min-height: 20px;
|
|
|
+ padding: 0 8px;
|
|
|
+ border-radius: 999px;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 800;
|
|
|
+ letter-spacing: 0.02em;
|
|
|
+ background: rgba(255,255,255,0.16);
|
|
|
+ color: #08231a;
|
|
|
+ }
|
|
|
+ .btn-badge.home {
|
|
|
+ background: rgba(79, 209, 165, 0.22);
|
|
|
+ color: #083226;
|
|
|
+ }
|
|
|
+ .btn-badge.game {
|
|
|
+ background: rgba(255, 209, 102, 0.3);
|
|
|
+ color: #3c2a00;
|
|
|
+ }
|
|
|
+ .btn-badge.publish {
|
|
|
+ background: rgba(125, 211, 252, 0.28);
|
|
|
+ color: #082a43;
|
|
|
+ }
|
|
|
+ .btn-badge.verify {
|
|
|
+ background: rgba(251, 146, 60, 0.3);
|
|
|
+ color: #482100;
|
|
|
+ }
|
|
|
+ .btn-badge.recommend {
|
|
|
+ background: rgba(248, 113, 113, 0.28);
|
|
|
+ color: #4a1111;
|
|
|
+ }
|
|
|
.actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
@@ -469,19 +637,22 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
|
|
|
<div class="category-head" id="nav-main" data-modes="frontend config">
|
|
|
<div class="category-kicker">Main Flow</div>
|
|
|
- <h2>联调主区</h2>
|
|
|
- <p>前台联调和配置发布最常用的入口都在这里。先跑通用户主链,再处理配置与发布。</p>
|
|
|
+ <h2>第一步:选玩法与准备数据</h2>
|
|
|
+ <p>先在这里选当前要测的玩法,workbench 后面的发布链、launch、result、history 都会复用这里的 event。</p>
|
|
|
</div>
|
|
|
<div class="grid">
|
|
|
<section class="panel" data-modes="frontend config admin">
|
|
|
- <h2>准备 Demo 数据</h2>
|
|
|
- <p>初始化 demo tenant / channel / event / card。</p>
|
|
|
+ <h2>第一步:选玩法</h2>
|
|
|
+ <p>先在这里选玩法入口。顺序赛、积分赛、多赛道各有一套独立 demo 数据,后面一键流程都会复用这里选中的 event。</p>
|
|
|
<div class="actions">
|
|
|
- <button id="btn-bootstrap">Bootstrap Demo</button>
|
|
|
- <button class="secondary" id="btn-use-variant-manual-demo">Use Manual Variant Demo</button>
|
|
|
+ <button id="btn-bootstrap"><span class="btn-stack"><span class="btn-badge recommend">先点</span><span>Bootstrap Demo</span></span></button>
|
|
|
+ <button class="secondary" id="btn-use-classic-demo"><span class="btn-stack"><span class="btn-badge home">顺序赛</span><span>Use Classic Demo</span></span></button>
|
|
|
+ <button class="secondary" id="btn-use-score-o-demo"><span class="btn-stack"><span class="btn-badge home">积分赛</span><span>Use Score-O Demo</span></span></button>
|
|
|
+ <button class="secondary" id="btn-use-variant-manual-demo"><span class="btn-stack"><span class="btn-badge home">多赛道</span><span>Use Manual Variant Demo</span></span></button>
|
|
|
</div>
|
|
|
<div class="kv">
|
|
|
<div>默认入口 <code id="bootstrap-entry">tenant_demo / mini-demo / evt_demo_001</code></div>
|
|
|
+ <div>积分赛入口 <code id="bootstrap-score-o-entry">tenant_demo / mini-demo / evt_demo_score_o_001</code></div>
|
|
|
<div>多赛道入口 <code id="bootstrap-variant-entry">tenant_demo / mini-demo / evt_demo_variant_manual_001</code></div>
|
|
|
</div>
|
|
|
</section>
|
|
|
@@ -720,23 +891,30 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
|
|
|
<div class="category-head" id="nav-fast" data-modes="frontend config admin">
|
|
|
<div class="category-kicker">Fast Path</div>
|
|
|
- <h2>快捷操作</h2>
|
|
|
- <p>当你只是想验证“能不能跑通”,优先使用这一组。</p>
|
|
|
+ <h2>第二步:选测试目标</h2>
|
|
|
+ <p>玩法选好以后,再决定你现在要测首页、发布链、局内流程,还是整条链一次验收。</p>
|
|
|
</div>
|
|
|
- <div class="grid" style="margin-top:16px;" data-modes="frontend config admin">
|
|
|
+ <div class="grid" style="margin-top:16px; grid-template-columns:minmax(380px,1.2fr) minmax(320px,0.8fr);" data-modes="frontend config admin">
|
|
|
<section class="panel" data-modes="frontend config admin">
|
|
|
- <h2>一键流程</h2>
|
|
|
- <p>把常用接口串成一键工作流,减少重复点击。</p>
|
|
|
+ <h2>第二步:点测试目标</h2>
|
|
|
+ <p>先选玩法入口,再按“你现在想测什么”点对应按钮。大多数情况下,你只需要点最后一个“一键标准回归”。</p>
|
|
|
<div class="actions">
|
|
|
- <button id="btn-flow-home">Bootstrap + WeChat + Entry Home</button>
|
|
|
- <button class="secondary" id="btn-flow-launch">Login + Launch + Start</button>
|
|
|
- <button class="ghost" id="btn-flow-finish">Finish Current Session</button>
|
|
|
- <button class="ghost" id="btn-flow-result">Finish + Result</button>
|
|
|
- <button class="secondary" id="btn-flow-admin-default-publish">一键默认绑定发布</button>
|
|
|
- <button class="secondary" id="btn-flow-admin-runtime-publish">一键补齐 Runtime 并发布</button>
|
|
|
- <button class="secondary" id="btn-flow-standard-regression">一键标准回归</button>
|
|
|
+ <button id="btn-flow-home"><span class="btn-stack"><span class="btn-badge home">首页</span><span>看首页是否正常</span></span></button>
|
|
|
+ <button class="secondary" id="btn-flow-launch"><span class="btn-stack"><span class="btn-badge game">局内</span><span>快速进一局</span></span></button>
|
|
|
+ <button class="ghost" id="btn-flow-finish"><span class="btn-stack"><span class="btn-badge game">局内</span><span>结束当前这一局</span></span></button>
|
|
|
+ <button class="ghost" id="btn-flow-result"><span class="btn-stack"><span class="btn-badge game">结果</span><span>结束并看结果</span></span></button>
|
|
|
+ <button class="secondary" id="btn-flow-admin-default-publish"><span class="btn-stack"><span class="btn-badge publish">发布</span><span>发布活动配置(默认绑定)</span></span></button>
|
|
|
+ <button class="secondary" id="btn-flow-admin-runtime-publish"><span class="btn-stack"><span class="btn-badge publish">发布</span><span>发布活动配置(自动补 Runtime)</span></span></button>
|
|
|
+ <button class="secondary" id="btn-flow-standard-regression"><span class="btn-stack"><span class="btn-badge verify">推荐</span><span>整条链一键验收</span></span></button>
|
|
|
+ </div>
|
|
|
+ <div class="muted-note">
|
|
|
+ 推荐顺序:
|
|
|
+ <br>1. 先点上面的玩法入口:Use Classic Demo / Use Score-O Demo / Use Manual Variant Demo
|
|
|
+ <br>2. 想直接验收,就点 整条链一键验收
|
|
|
+ <br>3. 想只测发布链,就点 发布活动配置(自动补 Runtime)
|
|
|
+ <br>4. 想只测局内流程,就点 快速进一局、结束并看结果
|
|
|
</div>
|
|
|
- <div class="muted-note">这些流程会复用当前表单里的手机号、设备、event、channel 等输入。“一键默认绑定发布” 会自动执行:Get Event -> Import Presentation -> Import Bundle -> Save Event Defaults -> Build Source -> Publish Build -> Get Release。“一键补齐 Runtime 并发布” 会在缺少默认 runtime 时自动创建 Runtime Binding,再继续发布链。“一键标准回归” 会继续执行:play -> launch -> start -> finish -> result -> history。</div>
|
|
|
+ <div class="muted-note">这些流程会复用当前表单里的手机号、设备、event、channel 等输入。发布活动配置(默认绑定)会自动执行:Get Event -> Import Presentation -> Import Bundle -> Save Event Defaults -> Build Source -> Publish Build -> Get Release。发布活动配置(自动补 Runtime)会在缺少默认 runtime 时自动创建 Runtime Binding,再继续发布链。整条链一键验收会继续执行:play -> launch -> start -> finish -> result -> history。</div>
|
|
|
<div class="subpanel">
|
|
|
<div class="muted-note">预期结果</div>
|
|
|
<div class="kv">
|
|
|
@@ -759,20 +937,45 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
<div>总判定 <code id="flow-regression-overall">待执行</code></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="panel" data-modes="common">
|
|
|
- <h2>请求导出</h2>
|
|
|
- <p>最后一次请求会生成一条可复制的 curl,后面做问题复现会方便很多。</p>
|
|
|
- <div class="actions">
|
|
|
- <button id="btn-copy-curl">Copy Last Curl</button>
|
|
|
- <button class="ghost" id="btn-clear-history">Clear History</button>
|
|
|
- </div>
|
|
|
<div class="subpanel">
|
|
|
- <div class="muted-note">Last Curl</div>
|
|
|
- <div id="curl" class="log" style="min-height:120px; max-height:200px;"></div>
|
|
|
+ <div class="muted-note">当前 Launch 实际配置摘要</div>
|
|
|
+ <div class="kv">
|
|
|
+ <div>Config URL <code id="launch-config-url">-</code></div>
|
|
|
+ <div>Release ID <code id="launch-config-release-id">-</code></div>
|
|
|
+ <div>Manifest URL <code id="launch-config-manifest-url">-</code></div>
|
|
|
+ <div>Schema Version <code id="launch-config-schema-version">-</code></div>
|
|
|
+ <div>Playfield Kind <code id="launch-config-playfield-kind">-</code></div>
|
|
|
+ <div>Game Mode <code id="launch-config-game-mode">-</code></div>
|
|
|
+ <div>判定 <code id="launch-config-verdict">待执行</code></div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</section>
|
|
|
+
|
|
|
+ <div class="stack" data-modes="common frontend">
|
|
|
+ <section class="panel" data-modes="common">
|
|
|
+ <h2>请求导出</h2>
|
|
|
+ <p>最后一次请求会生成一条可复制的 curl,后面做问题复现会方便很多。</p>
|
|
|
+ <div class="actions">
|
|
|
+ <button id="btn-copy-curl">Copy Last Curl</button>
|
|
|
+ <button class="ghost" id="btn-clear-history">Clear History</button>
|
|
|
+ </div>
|
|
|
+ <div class="subpanel">
|
|
|
+ <div class="muted-note">Last Curl</div>
|
|
|
+ <div id="curl" class="log" style="min-height:120px; max-height:200px;"></div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="panel" data-modes="common frontend">
|
|
|
+ <h2>前端调试日志</h2>
|
|
|
+ <p>前端可把 launch、manifest、地图页、结果页等调试信息直接打到 backend。这里显示最近日志,便于和 workbench 当前配置对口排查。</p>
|
|
|
+ <div class="actions">
|
|
|
+ <button id="btn-client-logs-refresh">拉取前端日志</button>
|
|
|
+ <button class="ghost" id="btn-client-logs-clear">清空前端日志</button>
|
|
|
+ </div>
|
|
|
+ <div class="muted-note">建议前端至少上报:eventId / releaseId / manifestUrl / game.mode / playfield.kind / 页面阶段。</div>
|
|
|
+ <div id="client-logs" class="log" style="min-height:180px; max-height:420px;"></div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<div class="category-head" id="nav-admin" data-modes="config admin">
|
|
|
@@ -1633,6 +1836,45 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
<div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权</div></div>
|
|
|
</div>
|
|
|
|
|
|
+ <div class="api-item" data-api="dev workbench 工作台 面板 调试">
|
|
|
+ <div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/workbench</span></div>
|
|
|
+ <div class="api-desc">开发态工作台页面,集中提供一键流、日志、配置摘要、API 目录和后台运营联调入口。</div>
|
|
|
+ <div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权</div></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="api-item" data-api="dev client logs 前端 调试 日志 上报">
|
|
|
+ <div class="api-head"><span class="api-method">POST</span><span class="api-path">/dev/client-logs</span></div>
|
|
|
+ <div class="api-desc">接收 frontend 主动上报的调试日志,供 backend 在 workbench 中统一查看。</div>
|
|
|
+ <div class="api-meta">
|
|
|
+ <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
|
|
|
+ <div><strong>关键参数:</strong><code>source</code>、<code>level</code>、<code>category</code>、<code>message</code>、<code>eventId</code>、<code>releaseId</code>、<code>sessionId</code>、<code>manifestUrl</code>、<code>route</code>、<code>details</code></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="api-item" data-api="dev client logs 前端 调试 日志 列表">
|
|
|
+ <div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/client-logs</span></div>
|
|
|
+ <div class="api-desc">获取 frontend 最近上报的调试日志,便于 backend 直接对照排查。</div>
|
|
|
+ <div class="api-meta">
|
|
|
+ <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
|
|
|
+ <div><strong>查询参数:</strong><code>limit</code></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="api-item" data-api="dev client logs 前端 调试 日志 清空">
|
|
|
+ <div class="api-head"><span class="api-method">DELETE</span><span class="api-path">/dev/client-logs</span></div>
|
|
|
+ <div class="api-desc">清空当前内存中的 frontend 调试日志,方便开始新一轮联调。</div>
|
|
|
+ <div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权</div></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="api-item" data-api="dev manifest summary manifest 摘要 代读 调试">
|
|
|
+ <div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/manifest-summary</span></div>
|
|
|
+ <div class="api-desc">由 backend 代读指定 manifest,并返回 <code>schemaVersion</code>、<code>playfield.kind</code>、<code>game.mode</code> 调试摘要。</div>
|
|
|
+ <div class="api-meta">
|
|
|
+ <div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
|
|
|
+ <div><strong>查询参数:</strong><code>url</code></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div class="api-item" data-api="dev config local files 本地 配置 文件 列表">
|
|
|
<div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/config/local-files</span></div>
|
|
|
<div class="api-desc">列出本地配置目录中的 JSON 文件,作为 source config 导入入口。</div>
|
|
|
@@ -2257,6 +2499,159 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
$('flow-regression-overall').textContent = overall;
|
|
|
}
|
|
|
|
|
|
+ function resetLaunchConfigSummary() {
|
|
|
+ $('launch-config-url').textContent = '-';
|
|
|
+ $('launch-config-release-id').textContent = '-';
|
|
|
+ $('launch-config-manifest-url').textContent = '-';
|
|
|
+ $('launch-config-schema-version').textContent = '-';
|
|
|
+ $('launch-config-playfield-kind').textContent = '-';
|
|
|
+ $('launch-config-game-mode').textContent = '-';
|
|
|
+ $('launch-config-verdict').textContent = '待执行';
|
|
|
+ }
|
|
|
+
|
|
|
+ async function resolveLaunchConfigSummary(launchPayload) {
|
|
|
+ const launchData = launchPayload && launchPayload.launch ? launchPayload.launch : {};
|
|
|
+ const config = launchData.config || {};
|
|
|
+ const resolvedRelease = launchData.resolvedRelease || {};
|
|
|
+ const configUrl = config.configUrl || '-';
|
|
|
+ const releaseId = config.releaseId || resolvedRelease.releaseId || '-';
|
|
|
+ const manifestUrl = resolvedRelease.manifestUrl || config.configUrl || '-';
|
|
|
+ const summary = {
|
|
|
+ configUrl: configUrl,
|
|
|
+ releaseId: releaseId,
|
|
|
+ manifestUrl: manifestUrl,
|
|
|
+ schemaVersion: '-',
|
|
|
+ playfieldKind: '-',
|
|
|
+ gameMode: '-',
|
|
|
+ verdict: '未读取 manifest'
|
|
|
+ };
|
|
|
+ const targetUrl = config.configUrl || resolvedRelease.manifestUrl;
|
|
|
+ if (!targetUrl) {
|
|
|
+ summary.verdict = '未通过:launch 未返回 configUrl / manifestUrl';
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const proxy = await request('GET', '/dev/manifest-summary?url=' + encodeURIComponent(targetUrl));
|
|
|
+ const data = proxy && proxy.data ? proxy.data : {};
|
|
|
+ summary.schemaVersion = data.schemaVersion || '-';
|
|
|
+ summary.playfieldKind = data.playfieldKind || '-';
|
|
|
+ summary.gameMode = data.gameMode || '-';
|
|
|
+ if (summary.schemaVersion !== '-' && summary.playfieldKind !== '-' && summary.gameMode !== '-') {
|
|
|
+ summary.verdict = '通过:已读取 launch 实际 manifest 摘要';
|
|
|
+ } else {
|
|
|
+ summary.verdict = '未通过:manifest 已读取,但关键信息不完整';
|
|
|
+ }
|
|
|
+ return summary;
|
|
|
+ } catch (error) {
|
|
|
+ summary.verdict = '未通过:manifest 读取异常';
|
|
|
+ summary.error = error && error.message ? error.message : String(error);
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function setLaunchConfigSummary(summary) {
|
|
|
+ const data = summary || {};
|
|
|
+ $('launch-config-url').textContent = data.configUrl || '-';
|
|
|
+ $('launch-config-release-id').textContent = data.releaseId || '-';
|
|
|
+ $('launch-config-manifest-url').textContent = data.manifestUrl || '-';
|
|
|
+ $('launch-config-schema-version').textContent = data.schemaVersion || '-';
|
|
|
+ $('launch-config-playfield-kind').textContent = data.playfieldKind || '-';
|
|
|
+ $('launch-config-game-mode').textContent = data.gameMode || '-';
|
|
|
+ $('launch-config-verdict').textContent = data.verdict || '待执行';
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderClientLogs(items) {
|
|
|
+ const logs = Array.isArray(items) ? items : [];
|
|
|
+ if (!logs.length) {
|
|
|
+ $('client-logs').textContent = '暂无前端调试日志';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ $('client-logs').textContent = logs.map(function(item) {
|
|
|
+ const lines = [];
|
|
|
+ lines.push('[' + (item.receivedAt || '-') + '] #' + (item.id || '-') + ' ' + (item.level || 'info').toUpperCase() + ' ' + (item.source || 'unknown'));
|
|
|
+ if (item.category) {
|
|
|
+ lines.push('category: ' + item.category);
|
|
|
+ }
|
|
|
+ lines.push('message: ' + (item.message || '-'));
|
|
|
+ if (item.eventId || item.releaseId || item.sessionId) {
|
|
|
+ lines.push('event/release/session: ' + (item.eventId || '-') + ' / ' + (item.releaseId || '-') + ' / ' + (item.sessionId || '-'));
|
|
|
+ }
|
|
|
+ if (item.manifestUrl) {
|
|
|
+ lines.push('manifest: ' + item.manifestUrl);
|
|
|
+ }
|
|
|
+ if (item.route) {
|
|
|
+ lines.push('route: ' + item.route);
|
|
|
+ }
|
|
|
+ if (item.details && Object.keys(item.details).length) {
|
|
|
+ lines.push('details: ' + JSON.stringify(item.details, null, 2));
|
|
|
+ }
|
|
|
+ return lines.join('\n');
|
|
|
+ }).join('\n\n---\n\n');
|
|
|
+ }
|
|
|
+
|
|
|
+ async function refreshClientLogs() {
|
|
|
+ const result = await request('GET', '/dev/client-logs?limit=50');
|
|
|
+ renderClientLogs(result.data);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ function selectBootstrapContextForEvent(bootstrap, eventId) {
|
|
|
+ const data = bootstrap || {};
|
|
|
+ if (eventId && data.scoreOEventId && eventId === data.scoreOEventId) {
|
|
|
+ return {
|
|
|
+ eventId: data.scoreOEventId,
|
|
|
+ releaseId: data.scoreOReleaseId || '',
|
|
|
+ sourceId: data.scoreOSourceId || '',
|
|
|
+ buildId: data.scoreOBuildId || '',
|
|
|
+ courseSetId: data.scoreOCourseSetId || '',
|
|
|
+ courseVariantId: data.scoreOCourseVariantId || '',
|
|
|
+ runtimeBindingId: data.scoreORuntimeBindingId || ''
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (eventId && data.variantManualEventId && eventId === data.variantManualEventId) {
|
|
|
+ return {
|
|
|
+ eventId: data.variantManualEventId,
|
|
|
+ releaseId: data.variantManualReleaseId || '',
|
|
|
+ sourceId: data.sourceId || '',
|
|
|
+ buildId: data.buildId || '',
|
|
|
+ courseSetId: data.courseSetId || '',
|
|
|
+ courseVariantId: data.courseVariantId || '',
|
|
|
+ runtimeBindingId: data.runtimeBindingId || ''
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ eventId: data.eventId || '',
|
|
|
+ releaseId: data.releaseId || '',
|
|
|
+ sourceId: data.sourceId || '',
|
|
|
+ buildId: data.buildId || '',
|
|
|
+ courseSetId: data.courseSetId || '',
|
|
|
+ courseVariantId: data.courseVariantId || '',
|
|
|
+ runtimeBindingId: data.runtimeBindingId || ''
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function applyBootstrapContext(bootstrap, explicitEventId) {
|
|
|
+ const eventId = explicitEventId || $('event-id').value || $('admin-event-ref-id').value || '';
|
|
|
+ const selected = selectBootstrapContextForEvent(bootstrap, eventId);
|
|
|
+ state.sourceId = selected.sourceId || state.sourceId;
|
|
|
+ state.buildId = selected.buildId || state.buildId;
|
|
|
+ state.releaseId = selected.releaseId || state.releaseId;
|
|
|
+ $('admin-pipeline-source-id').value = selected.sourceId || $('admin-pipeline-source-id').value;
|
|
|
+ $('admin-pipeline-build-id').value = selected.buildId || $('admin-pipeline-build-id').value;
|
|
|
+ $('admin-pipeline-release-id').value = selected.releaseId || $('admin-pipeline-release-id').value;
|
|
|
+ $('event-release-id').value = selected.releaseId || $('event-release-id').value;
|
|
|
+ $('prod-runtime-event-id').value = selected.eventId || $('prod-runtime-event-id').value;
|
|
|
+ $('prod-place-id').value = bootstrap.placeId || $('prod-place-id').value;
|
|
|
+ $('prod-map-asset-id').value = bootstrap.mapAssetId || $('prod-map-asset-id').value;
|
|
|
+ $('prod-tile-release-id').value = bootstrap.tileReleaseId || $('prod-tile-release-id').value;
|
|
|
+ $('prod-course-source-id').value = bootstrap.courseSourceId || $('prod-course-source-id').value;
|
|
|
+ $('prod-course-set-id').value = selected.courseSetId || $('prod-course-set-id').value;
|
|
|
+ $('prod-course-variant-id').value = selected.courseVariantId || $('prod-course-variant-id').value;
|
|
|
+ $('prod-runtime-binding-id').value = selected.runtimeBindingId || $('prod-runtime-binding-id').value;
|
|
|
+ syncState();
|
|
|
+ return selected;
|
|
|
+ }
|
|
|
+
|
|
|
function extractList(payload) {
|
|
|
if (Array.isArray(payload)) {
|
|
|
return payload;
|
|
|
@@ -2306,20 +2701,7 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
writeLog(flowTitle + '.step', { step: 'bootstrap-demo' });
|
|
|
const bootstrap = await request('POST', '/dev/bootstrap-demo');
|
|
|
if (bootstrap.data) {
|
|
|
- state.sourceId = bootstrap.data.sourceId || state.sourceId;
|
|
|
- state.buildId = bootstrap.data.buildId || state.buildId;
|
|
|
- state.releaseId = bootstrap.data.releaseId || state.releaseId;
|
|
|
- $('admin-pipeline-source-id').value = bootstrap.data.sourceId || $('admin-pipeline-source-id').value;
|
|
|
- $('admin-pipeline-build-id').value = bootstrap.data.buildId || $('admin-pipeline-build-id').value;
|
|
|
- $('admin-pipeline-release-id').value = bootstrap.data.releaseId || $('admin-pipeline-release-id').value;
|
|
|
- $('prod-runtime-event-id').value = bootstrap.data.eventId || $('prod-runtime-event-id').value;
|
|
|
- $('prod-place-id').value = bootstrap.data.placeId || $('prod-place-id').value;
|
|
|
- $('prod-map-asset-id').value = bootstrap.data.mapAssetId || $('prod-map-asset-id').value;
|
|
|
- $('prod-tile-release-id').value = bootstrap.data.tileReleaseId || $('prod-tile-release-id').value;
|
|
|
- $('prod-course-source-id').value = bootstrap.data.courseSourceId || $('prod-course-source-id').value;
|
|
|
- $('prod-course-set-id').value = bootstrap.data.courseSetId || $('prod-course-set-id').value;
|
|
|
- $('prod-course-variant-id').value = bootstrap.data.courseVariantId || $('prod-course-variant-id').value;
|
|
|
- $('prod-runtime-binding-id').value = bootstrap.data.runtimeBindingId || $('prod-runtime-binding-id').value;
|
|
|
+ applyBootstrapContext(bootstrap.data, eventId);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -2336,6 +2718,9 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
if (eventDetail.data.currentRuntime && eventDetail.data.currentRuntime.runtimeBindingId) {
|
|
|
$('admin-release-runtime-binding-id').value = eventDetail.data.currentRuntime.runtimeBindingId;
|
|
|
$('prod-runtime-binding-id').value = eventDetail.data.currentRuntime.runtimeBindingId;
|
|
|
+ } else {
|
|
|
+ $('admin-release-runtime-binding-id').value = '';
|
|
|
+ $('prod-runtime-binding-id').value = '';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -2539,6 +2924,9 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
launch.data.launch.resolvedRelease &&
|
|
|
launch.data.launch.resolvedRelease.manifestUrl
|
|
|
);
|
|
|
+ const launchConfigSummary = await resolveLaunchConfigSummary(launch.data);
|
|
|
+ setLaunchConfigSummary(launchConfigSummary);
|
|
|
+ writeLog(flowTitle + '.launch-summary', launchConfigSummary);
|
|
|
|
|
|
writeLog(flowTitle + '.step', {
|
|
|
step: 'session-start',
|
|
|
@@ -2618,6 +3006,41 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ function applyFrontendDemoSelection(options) {
|
|
|
+ resetLaunchConfigSummary();
|
|
|
+ $('entry-channel-code').value = 'mini-demo';
|
|
|
+ $('entry-channel-type').value = 'wechat_mini';
|
|
|
+ $('event-id').value = options.eventId;
|
|
|
+ $('event-release-id').value = options.releaseId;
|
|
|
+ $('event-variant-id').value = options.variantId || '';
|
|
|
+ $('config-event-id').value = options.eventId;
|
|
|
+ $('admin-event-ref-id').value = options.eventId;
|
|
|
+ $('local-config-file').value = options.localConfigFile || $('local-config-file').value;
|
|
|
+ if (options.gameModeCode) {
|
|
|
+ $('admin-game-mode-code').value = options.gameModeCode;
|
|
|
+ $('prod-course-mode').value = options.gameModeCode;
|
|
|
+ }
|
|
|
+ $('prod-runtime-event-id').value = options.eventId;
|
|
|
+ $('prod-course-set-id').value = options.courseSetId || $('prod-course-set-id').value;
|
|
|
+ $('prod-course-variant-id').value = options.courseVariantId || $('prod-course-variant-id').value;
|
|
|
+ $('prod-runtime-binding-id').value = options.runtimeBindingId || '';
|
|
|
+ $('admin-release-runtime-binding-id').value = options.runtimeBindingId || '';
|
|
|
+ $('admin-pipeline-source-id').value = options.sourceId || '';
|
|
|
+ $('admin-pipeline-build-id').value = options.buildId || '';
|
|
|
+ state.sourceId = options.sourceId || '';
|
|
|
+ state.buildId = options.buildId || '';
|
|
|
+ state.releaseId = options.releaseId || state.releaseId;
|
|
|
+ localStorage.setItem(MODE_KEY, 'frontend');
|
|
|
+ syncWorkbenchMode();
|
|
|
+ writeLog(options.logTitle, {
|
|
|
+ eventId: $('event-id').value,
|
|
|
+ releaseId: $('event-release-id').value,
|
|
|
+ variantId: $('event-variant-id').value || null,
|
|
|
+ localConfigFile: $('local-config-file').value
|
|
|
+ });
|
|
|
+ setStatus(options.statusText);
|
|
|
+ }
|
|
|
+
|
|
|
function setStatus(text, isError = false) {
|
|
|
statusEl.textContent = text;
|
|
|
statusEl.className = isError ? 'status error' : 'status';
|
|
|
@@ -3485,6 +3908,10 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
}, true);
|
|
|
state.sessionId = result.data.launch.business.sessionId;
|
|
|
state.sessionToken = result.data.launch.business.sessionToken;
|
|
|
+ syncState();
|
|
|
+ const configSummary = await resolveLaunchConfigSummary(result.data);
|
|
|
+ setLaunchConfigSummary(configSummary);
|
|
|
+ writeLog('event-launch.summary', configSummary);
|
|
|
return result;
|
|
|
});
|
|
|
|
|
|
@@ -4221,6 +4648,16 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
setStatus('ok: history cleared');
|
|
|
};
|
|
|
|
|
|
+ $('btn-client-logs-refresh').onclick = () => run('dev/client-logs', async () => {
|
|
|
+ return await refreshClientLogs();
|
|
|
+ });
|
|
|
+
|
|
|
+ $('btn-client-logs-clear').onclick = () => run('dev/client-logs/clear', async () => {
|
|
|
+ const result = await request('DELETE', '/dev/client-logs');
|
|
|
+ renderClientLogs([]);
|
|
|
+ return result;
|
|
|
+ });
|
|
|
+
|
|
|
$('btn-scenario-save').onclick = saveCurrentScenario;
|
|
|
$('btn-scenario-load').onclick = loadSelectedScenario;
|
|
|
$('btn-scenario-delete').onclick = deleteSelectedScenario;
|
|
|
@@ -4228,7 +4665,10 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
$('btn-scenario-import').onclick = importScenarioFromJSON;
|
|
|
|
|
|
$('btn-flow-home').onclick = () => run('flow-home', async () => {
|
|
|
- await request('POST', '/dev/bootstrap-demo');
|
|
|
+ const bootstrap = await request('POST', '/dev/bootstrap-demo');
|
|
|
+ if (bootstrap.data) {
|
|
|
+ applyBootstrapContext(bootstrap.data);
|
|
|
+ }
|
|
|
const login = await request('POST', '/auth/login/wechat-mini', {
|
|
|
code: $('wechat-code').value,
|
|
|
clientType: 'wechat',
|
|
|
@@ -4241,43 +4681,70 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
|
|
|
$('btn-bootstrap').onclick = () => run('bootstrap-demo', async () => {
|
|
|
const result = await request('POST', '/dev/bootstrap-demo');
|
|
|
- state.sourceId = result.data.sourceId || '';
|
|
|
- state.buildId = result.data.buildId || '';
|
|
|
- state.releaseId = result.data.releaseId || state.releaseId || '';
|
|
|
- if (result.data.releaseId) {
|
|
|
- $('event-release-id').value = result.data.releaseId;
|
|
|
- }
|
|
|
- $('prod-runtime-event-id').value = result.data.eventId || $('prod-runtime-event-id').value;
|
|
|
- $('prod-place-id').value = result.data.placeId || $('prod-place-id').value;
|
|
|
- $('prod-map-asset-id').value = result.data.mapAssetId || $('prod-map-asset-id').value;
|
|
|
- $('prod-tile-release-id').value = result.data.tileReleaseId || $('prod-tile-release-id').value;
|
|
|
- $('prod-course-source-id').value = result.data.courseSourceId || $('prod-course-source-id').value;
|
|
|
- $('prod-course-set-id').value = result.data.courseSetId || $('prod-course-set-id').value;
|
|
|
- $('prod-course-variant-id').value = result.data.courseVariantId || $('prod-course-variant-id').value;
|
|
|
- $('prod-runtime-binding-id').value = result.data.runtimeBindingId || $('prod-runtime-binding-id').value;
|
|
|
+ applyBootstrapContext(result.data);
|
|
|
+ return result;
|
|
|
+ });
|
|
|
+
|
|
|
+ $('btn-use-classic-demo').onclick = () => run('use-classic-demo', async () => {
|
|
|
+ const result = await request('POST', '/dev/bootstrap-demo');
|
|
|
+ applyFrontendDemoSelection({
|
|
|
+ eventId: result.data.eventId || 'evt_demo_001',
|
|
|
+ releaseId: result.data.releaseId || 'rel_demo_001',
|
|
|
+ localConfigFile: 'classic-sequential.json',
|
|
|
+ gameModeCode: 'classic-sequential',
|
|
|
+ sourceId: result.data.sourceId || '',
|
|
|
+ buildId: result.data.buildId || '',
|
|
|
+ courseSetId: result.data.courseSetId || '',
|
|
|
+ courseVariantId: result.data.courseVariantId || '',
|
|
|
+ runtimeBindingId: result.data.runtimeBindingId || '',
|
|
|
+ logTitle: 'classic-demo-ready',
|
|
|
+ statusText: 'ok: classic demo loaded'
|
|
|
+ });
|
|
|
+ return result;
|
|
|
+ });
|
|
|
+
|
|
|
+ $('btn-use-score-o-demo').onclick = () => run('use-score-o-demo', async () => {
|
|
|
+ const result = await request('POST', '/dev/bootstrap-demo');
|
|
|
+ applyFrontendDemoSelection({
|
|
|
+ eventId: result.data.scoreOEventId || 'evt_demo_score_o_001',
|
|
|
+ releaseId: result.data.scoreOReleaseId || 'rel_demo_score_o_001',
|
|
|
+ localConfigFile: 'score-o.json',
|
|
|
+ gameModeCode: 'score-o',
|
|
|
+ sourceId: result.data.scoreOSourceId || '',
|
|
|
+ buildId: result.data.scoreOBuildId || '',
|
|
|
+ courseSetId: result.data.scoreOCourseSetId || '',
|
|
|
+ courseVariantId: result.data.scoreOCourseVariantId || '',
|
|
|
+ runtimeBindingId: result.data.scoreORuntimeBindingId || '',
|
|
|
+ logTitle: 'score-o-demo-ready',
|
|
|
+ statusText: 'ok: score-o demo loaded'
|
|
|
+ });
|
|
|
return result;
|
|
|
});
|
|
|
|
|
|
$('btn-use-variant-manual-demo').onclick = () => run('use-variant-manual-demo', async () => {
|
|
|
const result = await request('POST', '/dev/bootstrap-demo');
|
|
|
- $('entry-channel-code').value = 'mini-demo';
|
|
|
- $('entry-channel-type').value = 'wechat_mini';
|
|
|
- $('event-id').value = result.data.variantManualEventId || 'evt_demo_variant_manual_001';
|
|
|
- $('event-release-id').value = result.data.variantManualReleaseId || 'rel_demo_variant_manual_001';
|
|
|
- $('event-variant-id').value = 'variant_b';
|
|
|
- localStorage.setItem(MODE_KEY, 'frontend');
|
|
|
- syncWorkbenchMode();
|
|
|
- writeLog('variant-manual-demo-ready', {
|
|
|
- eventId: $('event-id').value,
|
|
|
- releaseId: $('event-release-id').value,
|
|
|
- variantId: $('event-variant-id').value
|
|
|
+ applyFrontendDemoSelection({
|
|
|
+ eventId: result.data.variantManualEventId || 'evt_demo_variant_manual_001',
|
|
|
+ releaseId: result.data.variantManualReleaseId || 'rel_demo_variant_manual_001',
|
|
|
+ variantId: 'variant_b',
|
|
|
+ localConfigFile: 'classic-sequential.json',
|
|
|
+ gameModeCode: 'classic-sequential',
|
|
|
+ sourceId: result.data.sourceId || '',
|
|
|
+ buildId: result.data.buildId || '',
|
|
|
+ courseSetId: result.data.courseSetId || '',
|
|
|
+ courseVariantId: result.data.courseVariantId || '',
|
|
|
+ runtimeBindingId: '',
|
|
|
+ logTitle: 'variant-manual-demo-ready',
|
|
|
+ statusText: 'ok: manual variant demo loaded'
|
|
|
});
|
|
|
- setStatus('ok: manual variant demo loaded');
|
|
|
return result;
|
|
|
});
|
|
|
|
|
|
$('btn-flow-launch').onclick = () => run('flow-launch', async () => {
|
|
|
- await request('POST', '/dev/bootstrap-demo');
|
|
|
+ const bootstrap = await request('POST', '/dev/bootstrap-demo');
|
|
|
+ if (bootstrap.data) {
|
|
|
+ applyBootstrapContext(bootstrap.data);
|
|
|
+ }
|
|
|
const smsSend = await request('POST', '/auth/sms/send', {
|
|
|
countryCode: $('sms-country').value,
|
|
|
mobile: $('sms-mobile').value,
|
|
|
@@ -4388,6 +4855,7 @@ const devWorkbenchHTML = `<!doctype html>
|
|
|
renderScenarioOptions();
|
|
|
applyAPIFilter();
|
|
|
syncAPICounts();
|
|
|
+ renderClientLogs([]);
|
|
|
writeLog('workbench-ready', { ok: true, hint: 'Use Bootstrap Demo first on a fresh database.' });
|
|
|
</script>
|
|
|
</body>
|