/* ============================================================================ * pbl_overlay_play.js —— M9 覆盖层装配 + 事件处理器(scense / scense_game 侧) * * 铁律: * 1. 不修改 scense/scense_game 渲染主循环 —— 只做「外挂」:探测宿主只读钩子 * (window.scenseGame / CustomEvent),钩子缺失时降级为契约轮询,绝不 * 包裹/注入宿主帧函数(requestAnimationFrame 链不劫持)。 * 2. 广播到达 → 覆盖层 DOM 更新完成 ≤ 500ms(BUDGETS.broadcast_render_ms), * 超预算打 RENDER_DEGRADED 横幅并自适应节流合并补丁,不丢数据、不静默。 * 3. 首屏 ≤ 1500ms(BUDGETS.first_screen_ms),超时走 LOAD_FAIL 骨架 + 局部重试。 * 4. 网络交互只走 pbl_overlay_transport.js 契约端点(pbl_* 模块 .dspy)。 * * 依赖加载顺序:pbl_overlay_core.js → pbl_overlay_transport.js → 本文件 * API 对齐:Core.Bus(全局单例,on=window 监听 / emit=document CustomEvent)、 * Core.Chrono(label,budget,onOver).stop()、 * Core.DegradeManager(root).show(key,{detail})/.fromError(err)、 * Tpt.Transport({session_id,blueprint_id,onEvent,onDegrade,onPerf}) * ==========================================================================*/ (function (global) { 'use strict'; var Core = global.PblOverlayCore; var Tpt = global.PblOverlayTransport; if (!Core || !Tpt) { if (global.console) { console.error('[pbl_overlay_play] core/transport 未加载'); } return; } var BUDGETS = Core.BUDGETS; var esc = Core.esc; var now = Core.now; /* ───────────────────────── 1. 页面路由解析(注册表 route → 实际 URL) ───────────────────────── */ // 设计注册表 route 形如 /pbl_overlay/mission_board.ui; // 模块自动路由实际为 /pbl_scense_ext/pbl_overlay/mission_board.ui var MODULE_PREFIX = '/pbl_scense_ext'; function pageUrl(page) { var route = (page && page.route) || ''; if (!route) { return ''; } if (route.indexOf(MODULE_PREFIX + '/') === 0) { return Tpt.apiBase() + route; } if (route.charAt(0) === '/') { return Tpt.apiBase() + MODULE_PREFIX + route; } return Tpt.apiBase() + MODULE_PREFIX + '/pbl_overlay/' + route; } function readQuery() { var out = {}; var qs = String(location.search || '').replace(/^\?/, ''); if (!qs) { return out; } qs.split('&').forEach(function (kv) { if (!kv) { return; } var i = kv.indexOf('='); var k = i < 0 ? kv : kv.slice(0, i); var v = i < 0 ? '' : kv.slice(i + 1); try { out[decodeURIComponent(k)] = decodeURIComponent(v.replace(/\+/g, ' ')); } catch (e) { out[k] = v; } }); return out; } function hostPageKey() { var el = document.querySelector('[data-pbl-page]'); if (el && el.getAttribute('data-pbl-page')) { return el.getAttribute('data-pbl-page'); } var m = String(location.pathname).match(/\/pbl_overlay\/([a-z_]+)\.ui/); if (m) { return m[1]; } return readQuery().page || 'play_preview'; } function readContext() { // 上下文优先级:宿主注入 __PBL_OVERLAY_CTX__ > URL query > 覆盖层本地草稿 var inj = global.__PBL_OVERLAY_CTX__ || {}; var q = readQuery(); var draft = {}; try { draft = JSON.parse(global.localStorage.getItem('pbl.overlay.ctx') || '{}') || {}; } catch (e) { draft = {}; } var ctx = { page: q.page || inj.page || null, session_id: q.session_id || inj.session_id || draft.session_id || null, blueprint_id: q.blueprint_id || inj.blueprint_id || draft.blueprint_id || null, world_id: q.world_id || inj.world_id || draft.world_id || null, role: inj.role || q.role || null }; try { global.localStorage.setItem('pbl.overlay.ctx', JSON.stringify({ session_id: ctx.session_id, blueprint_id: ctx.blueprint_id, world_id: ctx.world_id })); } catch (e) { /* 隐私模式忽略 */ } return ctx; } /* ───────────────────────── 2. scense / scense_game 只读桥(不改渲染主循环) ───────────────────────── */ var ScenseBridge = { renderer: function () { return global.scenseGame || global.SCENSE_GAME || global.__SCENSE_RUNTIME__ || null; }, // 仅探测宿主状态,不写回 hasRenderer: function () { return !!this.renderer(); }, rendererStatus: function () { var g = this.renderer(); if (!g) { return { available: false, paused: null, entities: 0 }; } var n = 0; if (typeof g.entityCount === 'number') { n = g.entityCount; } else if (g.entities && g.entities.length !== undefined) { n = g.entities.length; } return { available: true, paused: (typeof g.paused === 'boolean') ? g.paused : null, entities: n }; }, // 订阅宿主广播的 CustomEvent(宿主没有该事件时零副作用,靠 transport 通道兜底) onHostEvent: function (handler) { var names = ['scense:entity_updated', 'scense:broadcast', 'scense:state-applied', 'pbl:broadcast']; names.forEach(function (n) { try { global.addEventListener(n, function (ev) { handler((ev && ev.detail) || {}); }, false); } catch (e) { /* 忽略 */ } }); }, // Play 控制:优先宿主 API(只调用不修改),否则回落 scense_game 契约端点 control: function (action, ctx) { var g = this.renderer(); if (g && typeof g[action] === 'function') { try { var r = g[action](); if (r && typeof r.then === 'function') { return r; } return Promise.resolve({ status: 'ok', via: 'host', action: action }); } catch (e) { /* 继续走契约 */ } } var map = { start: 'game_start', pause: 'game_pause', resume: 'game_resume', restart: 'game_restart', end: 'game_end' }; var ep = map[action]; if (!ep) { return Promise.reject({ code: 'PBL_E_NOT_FOUND', message: '未知控制 ' + action }); } return fetch(Tpt.apiBase() + '/scense_game/game/api/' + ep + '.dspy', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json;charset=UTF-8' }, body: JSON.stringify({ session_id: (ctx && ctx.session_id) || '', world_id: (ctx && ctx.world_id) || '' }) }).then(function (r) { if (!r.ok) { return { status: 'fail', code: 'PBL_E_NETWORK', message: 'HTTP ' + r.status }; } return r.json(); }); } }; /* ───────────────────────── 3. 覆盖层实例 ───────────────────────── */ function Overlay(opts) { opts = opts || {}; this.pageKey = opts.page || hostPageKey() || 'play_preview'; this.page = Core.byKey(this.pageKey) || Core.byKey('play_preview'); this.ctx = opts.context || readContext(); this.chrono = new Core.Chrono('first_screen', BUDGETS.first_screen_ms, function (ms) { if (global.__PBL_OVERLAY__) { global.__PBL_OVERLAY__.degrade.show('RENDER_DEGRADED', { detail: '首屏 ' + Math.round(ms) + 'ms 超预算 ' + BUDGETS.first_screen_ms + 'ms' }); } }); this.degrade = new Core.DegradeManager(opts.root || this.pageRoot()); this.components = new Core.Components(); this.transport = null; this.model = {}; this.patchQueue = []; this.patchScheduled = false; this.lastPatchCost = 0; this.throttleMs = 0; // 超预算后自适应节流 this.mounted = false; this.destroyed = false; this.perf = { first_screen_ms: null, broadcast_render_ms: null }; this.emitter = document; // 局部事件走 document CustomEvent(pbl-ov:* 前缀) this._unsubs = []; this.registerBuiltinComponents(); this.registerEventHandlers(); } Overlay.prototype.pageRoot = function () { return document.querySelector('[data-pbl-overlay-root]') || document.body; }; /* ── 局部事件总线(不占用全局 Bus 命名空间,避免与宿主事件混淆) ── */ Overlay.prototype.on = function (name, fn) { var h = function (ev) { fn((ev && ev.detail) || {}); }; this.emitter.addEventListener(name, h, false); this._unsubs.push([name, h]); return this; }; Overlay.prototype.emit = function (name, detail) { return Core.Bus.emit('pbl-ov:' + name, detail); }; Overlay.prototype.mount = function () { if (this.mounted) { return Promise.resolve(this); } this.mounted = true; var self = this; this.transport = new Tpt.Transport({ session_id: this.ctx.session_id, blueprint_id: this.ctx.blueprint_id, since_seq: 0, onEvent: function (item) { self.ingestTransport(item); }, onDegrade: function (code, msg) { self.degrade.show(self.degrade.mapCode(code), { detail: msg || '' }); }, onPerf: function (name, ms) { self.perf[name] = ms; } }); // S1:bootstrap → 首屏渲染(≤1500ms 门禁由 Chrono 自动判定) return this.transport.bootstrap({ world_id: this.ctx.world_id, page: this.pageKey }) .then(function (d) { self.model = normalizeBootstrap(d); self.degrade.hide('LOAD_FAIL'); self.renderAll(); self.chrono.stop(); self.startStreams(); self.writePerfBadge(); return self; }) .catch(function (err) { self.degrade.fromError(err); // 断网/超时:用本地缓存模型渲染骨架,保证首屏不完全空白 + 局部重试按钮 self.model = cachedModel(self.pageKey); self.renderAll(true); self.chrono.stop(); self.startStreams(); self.writePerfBadge(); return self; }); }; // 事件流:transport(SSE→长轮询自动降级)+ 宿主 CustomEvent(只读旁路) Overlay.prototype.startStreams = function () { var self = this; this.transport.subscribe(); ScenseBridge.onHostEvent(function (detail) { self.ingestHost(detail); }); // 重试按钮(core DegradeManager banner 事件委托:data-act=retry 会派发 pbl:degrade-retry) Core.Bus.on('pbl:degrade-retry', function () { self.retryLast(); }); this.on('navigate', function (p) { self.navigate(p && p.page); }); this.on('control', function (p) { ScenseBridge.control(p && p.action, self.ctx).then(function (res) { if (res && res.status === 'fail') { self.degrade.show(self.degrade.mapCode(res.code || 'PBL_E_NETWORK'), { detail: res.message || '' }); } else { self.degrade.show('SESSION_ENDED', { detail: '控制指令已下发:' + (p && p.action), transient: true }); } }).catch(function (e) { self.degrade.fromError(e); }); }); }; Overlay.prototype.retryLast = function () { var self = this; return this.transport.bootstrap({ world_id: this.ctx.world_id, page: this.pageKey }) .then(function (d) { self.model = normalizeBootstrap(d); self.degrade.hide('LOAD_FAIL'); self.degrade.hide('NET_FAIL'); self.renderAll(); return d; }) .catch(function (e) { self.degrade.fromError(e); }); }; /* ───────────────────────── 4. 广播 → 覆盖层补丁(≤500ms 门禁 + 自适应节流) ───────────────────────── */ // transport 通道:item = {seq,event_type,entity_id,payload,received_at} Overlay.prototype.ingestTransport = function (item) { var patch = normalizeTransportPatch(item); if (!patch) { return; } this.patchQueue.push(patch); this.schedulePatch(); }; // 宿主 CustomEvent 旁路(无 seq,仅做展示层增量,不参与权威状态) Overlay.prototype.ingestHost = function (detail) { if (!detail || (detail.seq !== undefined && detail.seq !== null)) { return; } // 带 seq 的走 transport this.patchQueue.push(normalizeHostPatch(detail)); this.schedulePatch(); }; Overlay.prototype.schedulePatch = function () { var self = this; if (this.patchScheduled) { return; } this.patchScheduled = true; var wait = this.throttleMs; var raf = global.requestAnimationFrame || function (fn) { return global.setTimeout(fn, 16); }; global.setTimeout(function () { raf(function () { self.flushPatches(); }); }, wait); }; Overlay.prototype.flushPatches = function () { this.patchScheduled = false; if (this.destroyed || !this.patchQueue.length) { return; } var batch = this.patchQueue.splice(0, this.patchQueue.length); var t0 = now(); for (var i = 0; i < batch.length; i++) { this.applyPatch(batch[i]); } this.renderDynamic(); persistModel(this.pageKey, this.model); var cost = Math.round(now() - t0); this.lastPatchCost = cost; this.perf.broadcast_render_ms = cost; this.writePerfBadge(); if (cost > BUDGETS.broadcast_render_ms) { // 超预算:横幅 + 自适应节流(合并更多补丁),不丢数据 this.throttleMs = Math.min(2000, Math.max(200, this.throttleMs + 200)); this.degrade.show('RENDER_DEGRADED', { detail: '广播渲染 ' + cost + 'ms 超预算 ' + BUDGETS.broadcast_render_ms + 'ms,已合并补丁(节流 ' + this.throttleMs + 'ms)' }); } else if (this.throttleMs > 0) { this.throttleMs = Math.max(0, this.throttleMs - 100); } }; Overlay.prototype.applyPatch = function (patch) { var m = this.model; if (!m || !patch) { return; } if (patch.entity_id) { if (!m.entities) { m.entities = {}; } var prev = m.entities[patch.entity_id] || {}; m.entities[patch.entity_id] = Object.assign({}, prev, patch.state || {}); m.entities[patch.entity_id].__rev = (prev.__rev || 0) + 1; m.entity_count = Object.keys(m.entities).length; } if (patch.event_type) { if (!m.events) { m.events = []; } m.events.unshift({ type: patch.event_type, text: patch.text || patch.event_type, at: patch.at }); if (m.events.length > 50) { m.events.length = 50; } } if (patch.progress !== undefined && patch.progress !== null) { m.progress = Number(patch.progress); } if (patch.quality_state) { m.quality_state = patch.quality_state; } if (patch.artifact) { (m.artifacts = m.artifacts || []).unshift(patch.artifact); } m.lastEvent = patch; this.emit('model-patched', patch); }; function normalizeTransportPatch(item) { if (!item) { return null; } var p = item.payload || {}; return { source: 'broadcast', seq: item.seq, event_type: item.event_type || 'patch', entity_id: item.entity_id || p.entity_id || null, state: p.state || p.patch || null, text: p.text || p.message || item.event_type || '', progress: (p.progress !== undefined) ? p.progress : undefined, quality_state: p.quality_state || null, artifact: p.artifact || null, at: item.received_at || now() }; } function normalizeHostPatch(detail) { detail = detail || {}; return { source: 'host', seq: null, event_type: detail.event_type || detail.type || 'host_patch', entity_id: detail.entity_id || null, state: detail.state || detail.patch || null, text: detail.text || detail.message || '', progress: detail.progress, quality_state: detail.quality_state || null, artifact: detail.artifact || null, at: detail.at || now() }; } /* ───────────────────────── 5. 11 组件注册 + 渲染(覆盖层内 DOM,不触碰渲染层 canvas) ───────────────────────── */ Overlay.prototype.registerBuiltinComponents = function () { var self = this; var C = this.components; C.register('TopBar', function (host, m) { host.innerHTML = '
' + '' + esc(m.page_title || '') + '' + '' + esc(m.blueprint_name || '') + (m.version ? ' v' + esc(m.version) : '') + '' + '' + '' + esc(m.quality_state || '-') + '' + '-' + '
'; }); C.register('NavRail', function (host, m) { var pages = Core.pagesOfRole(m.role || 'logined'); var html = ''; }); C.register('GoalCard', function (host, m) { var g = m.goal || {}; host.insertAdjacentHTML('beforeend', '

驱动问题 / 目标

' + '

' + esc(g.question || '暂无目标(等待蓝图编译)') + '

' + '

' + esc(g.milestone || '') + '

'); }); C.register('SceneCard', function (host, m) { var s = m.scene || {}; host.insertAdjacentHTML('beforeend', '

当前场景

' + '

' + esc(s.name || '-') + ' ' + esc(s.kind || '') + '

' + '

实体 ' + Number(m.entity_count || 0) + ' · 观察者 ' + Number(m.observer_count || 0) + '

'); }); C.register('StepList', function (host, m) { var steps = m.steps || []; var html = '

任务步骤

    '; for (var i = 0; i < steps.length; i++) { html += '
  1. ' + esc(steps[i].title || steps[i].name || ('步骤' + (i + 1))) + '
  2. '; } host.insertAdjacentHTML('beforeend', html + '
'); }); C.register('RubricEditor', function (host, m) { var rows = m.rubric || []; var html = '

量规(Rubric 加权)

' + ''; for (var i = 0; i < rows.length; i++) { html += ''; } host.insertAdjacentHTML('beforeend', html + '
维度权重得分
' + esc(rows[i].name || rows[i].dimension || '-') + '' + esc(String(rows[i].weight === undefined ? '' : rows[i].weight)) + '' + esc(String(rows[i].score === undefined ? '-' : rows[i].score)) + '
'); }); C.register('ValidationBadge', function (host, m) { var v = m.validation || {}; var tone = v.blocked ? 'block' : (v.passed ? 'ok' : 'warn'); host.insertAdjacentHTML('beforeend', '' + esc(v.summary || v.state || '未校验') + ''); }); C.register('ArtifactViewer', function (host, m) { var list = m.artifacts || []; var html = '

产出物 / 证据

'; for (var i = 0; i < list.length; i++) { html += '
' + esc(list[i].name || list[i].artifact_id || ('#' + i)) + '' + '' + esc(list[i].kind || list[i].evidence_type || '') + '' + '' + esc(list[i].state || list[i].status || '') + '
'; } if (!list.length) { html += '
暂无产出物
'; } html += '
' + '
' + '
'; host.insertAdjacentHTML('beforeend', html); }); C.register('ChatPanel', function (host, m) { var msgs = m.agent_messages || []; var html = '

Agent 对话

'; for (var i = 0; i < msgs.length; i++) { var fb = msgs[i].fallback || msgs[i].template_fallback ? ' is-fallback' : ''; html += '
' + esc(msgs[i].content || msgs[i].text || '') + '
'; } html += '
' + '
'; host.insertAdjacentHTML('beforeend', html); }); C.register('ApprovalQueue', function (host, m) { var items = m.approvals || []; var html = '

待人工审批

'); }); C.register('HudOverlay', function (host, m) { var pct = Math.max(0, Math.min(100, Number(m.progress || 0))); host.innerHTML = '
' + '
' + '
' + esc(m.role_label || m.role || '') + '
' + '
' + '' + '' + '' + '
'; }); }; Overlay.prototype.hostFor = function (comp) { var root = this.pageRoot(); var cls = comp.host.replace(/^\./, ''); var el = root.querySelector('.' + cls); if (!el) { el = document.createElement('div'); el.className = cls; if (cls === 'pbl-hud') { var main = root.querySelector('.pbl-main') || ensure(root, 'pbl-main'); main.appendChild(el); } else { root.appendChild(el); } } return el; }; function ensure(parent, cls) { var el = parent.querySelector('.' + cls); if (!el) { el = document.createElement('div'); el.className = cls; parent.appendChild(el); } return el; } Overlay.prototype.renderAll = function (skeletonOnly) { var comps = Core.COMPONENTS; var m = Object.assign({}, this.model, { page_key: this.pageKey, page_title: this.page.title, role: this.ctx.role || this.model.role || 'logined' }); var allowed = comps.filter(function (c) { return c.pages.indexOf('*') >= 0 || c.pages.indexOf(m.page_key) >= 0; }); resetHosts(this.pageRoot()); for (var i = 0; i < allowed.length; i++) { var c = allowed[i]; if (!this.components.has(c.name)) { continue; } try { this.components.render(c.name, this.hostFor(c), m); } catch (e) { this.degrade.show('RENDER_DEGRADED', { detail: '组件 ' + c.name + ' 渲染失败:' + ((e && e.message) || e) }); } } if (skeletonOnly) { ensure(this.pageRoot(), 'pbl-main').insertAdjacentHTML('beforeend', '
'); } this.bindDomHandlers(); return this; }; // 增量重绘:只更新随广播变化的 HUD/徽标,保证单次 ≤500ms Overlay.prototype.renderDynamic = function () { var m = this.model; var bar = document.querySelector('.pbl-hud-progress span'); if (bar) { bar.style.width = Math.max(0, Math.min(100, Number(m.progress || 0))) + '%'; } var feed = document.querySelector('.pbl-hud-feed'); if (feed && m.lastEvent) { var line = document.createElement('div'); line.className = 'pbl-feedline'; line.textContent = String(m.lastEvent.text || m.lastEvent.event_type || ''); feed.insertBefore(line, feed.firstChild); while (feed.childNodes.length > 6) { feed.removeChild(feed.lastChild); } } var badge = document.querySelector('[data-badge="quality"]'); if (badge && m.quality_state) { badge.textContent = m.quality_state; } this.emit('rendered', m); }; function resetHosts(root) { ['.pbl-topbar', '.pbl-navrail', '.pbl-main'].forEach(function (sel) { var el = root.querySelector(sel); if (el) { el.innerHTML = ''; } }); } function cachedModel(key) { try { return JSON.parse(global.localStorage.getItem('pbl.overlay.model.' + key) || '{}') || {}; } catch (e) { return {}; } } function persistModel(key, model) { try { global.localStorage.setItem('pbl.overlay.model.' + key, JSON.stringify({ quality_state: model.quality_state, progress: model.progress, artifacts: (model.artifacts || []).slice(0, 20), events: (model.events || []).slice(0, 20) })); } catch (e) { /* 配额/隐私模式忽略 */ } } function normalizeBootstrap(d) { d = d || {}; var entities = toEntityMap(d.entities); return { role: d.role || null, role_label: d.role_label || d.role_name || null, blueprint_name: d.blueprint_name || (d.blueprint && d.blueprint.name) || '', version: d.version || (d.blueprint && d.blueprint.version) || '', quality_state: d.quality_state || d.quality || '', goal: d.goal || (d.mission ? { question: d.mission.question, milestone: d.mission.milestone } : {}), scene: d.scene || d.current_scene || {}, steps: d.steps || d.tasks || [], artifacts: d.artifacts || [], rubric: d.rubric || [], approvals: d.approvals || [], agent_messages: d.agent_messages || d.messages || [], validation: d.validation || {}, entities: entities, entity_count: (d.entity_count !== undefined) ? d.entity_count : Object.keys(entities).length, observer_count: d.observer_count || 0, progress: Number(d.progress || 0), recent_events: d.recent_events || [] }; } function toEntityMap(list) { var out = {}; if (!list) { return out; } if (list instanceof Array) { for (var i = 0; i < list.length; i++) { var e = list[i]; if (e && e.entity_id) { out[e.entity_id] = e; } } } else if (typeof list === 'object') { Object.keys(list).forEach(function (k) { out[k] = list[k]; }); } return out; } /* ───────────────────────── 6. 4 组交互时序的事件处理器 ───────────────────────── */ Overlay.prototype.registerEventHandlers = function () { var self = this; // S2 做任务:意图上行(服务端权威回传 seq 后才落补丁,不预测渲染) this.on('intent-submit', function (payload) { return self.transport.submitIntent({ type: payload.intent_type || 'action', action: payload.intent || 'interact', entity_id: payload.target_entity_id || payload.entity_id || '', payload: payload.payload || {} }).catch(function (e) { self.degrade.fromError(e); }); }); // S3 产出物:create → submit → collect_evidence(artifact_id 幂等,失败可重试) this.on('artifact-create', function (a) { return self.transport.createArtifact(a) .then(function (res) { var id = (res && (res.artifact_id || (res.data && res.data.artifact_id))) || a.artifact_id; return self.transport.submitArtifact(id); }) .then(function () { return self.transport.collectEvidence({ evidence_type: a.evidence_type || 'artifact_version', source_id: a.artifact_id || '' }); }) .catch(function (e) { self.degrade.fromError(e); }); }); // S4 反思 / 反馈 this.on('feedback-save', function (f) { return self.transport.saveFeedback(f).catch(function (e) { self.degrade.fromError(e); }); }); // 门禁跳转(VALIDATE_BLOCK / APPROVAL_PENDING 横幅「前往处理」) Core.Bus.on('pbl:degrade', function (ev) { var d = (ev && ev.detail) || {}; if (d.state === 'VALIDATE_BLOCK') { /* 用户点横幅按钮时才跳转,此处只标记 */ } if (d.state === 'SESSION_ENDED') { self.degrade.setReadonly(true); } }); this.on('goto-page', function (p) { self.navigate(p && p.page); }); }; Overlay.prototype.navHref = function (key) { var p = Core.byKey(key); if (!p) { return '#'; } var url = pageUrl(p); var sep = url.indexOf('?') >= 0 ? '&' : '?'; return url + sep + 'session_id=' + encodeURIComponent(this.ctx.session_id || '') + '&blueprint_id=' + encodeURIComponent(this.ctx.blueprint_id || '') + '&world_id=' + encodeURIComponent(this.ctx.world_id || ''); }; Overlay.prototype.navigate = function (key) { var p = Core.byKey(key || ''); if (!p) { return; } location.href = this.navHref(p.key); }; Overlay.prototype.bindDomHandlers = function () { var self = this; var root = this.pageRoot(); if (root.getAttribute('data-pbl-bound') === '1') { return; } root.setAttribute('data-pbl-bound', '1'); root.addEventListener('click', function (ev) { var t = ev.target; if (!t) { return; } var act = closestAttr(t, 'data-pbl-act'); if (act) { ev.preventDefault(); self.emit('control', { action: act }); return; } var nav = closestAttr(t, 'data-pbl-nav'); if (nav) { /* a[href] 已带上下文参数,交给浏览器跳转 */ return; } var send = closestAttr(t, 'data-pbl-send'); if (send !== null) { var inp = root.querySelector('[data-pbl-chat]'); var text = inp ? String(inp.value || '').trim() : ''; if (!text) { return; } self.emit('intent-submit', { intent: 'ask_agent', payload: { text: text } }); if (inp) { inp.value = ''; } } }, false); root.addEventListener('submit', function (ev) { var kind = closestAttr(ev.target, 'data-pbl-form'); if (!kind) { return; } ev.preventDefault(); var data = formToObj(ev.target); if (kind === 'feedback') { self.emit('feedback-save', data); } if (kind === 'artifact') { self.emit('artifact-create', data); } }, false); }; function closestAttr(el, attr) { var node = el; while (node && node !== document) { if (node.getAttribute && node.getAttribute(attr) !== null) { return node.getAttribute(attr); } node = node.parentNode; } return null; } function formToObj(form) { var out = {}; var els = form.querySelectorAll('[name]'); for (var i = 0; i < els.length; i++) { out[els[i].name] = els[i].value; } return out; } Overlay.prototype.writePerfBadge = function () { var badge = document.querySelector('[data-badge="perf"]'); if (!badge) { return; } var fs = this.perf.first_screen_ms === null ? null : Math.round(this.perf.first_screen_ms); var br = this.perf.broadcast_render_ms === null ? null : Math.round(this.perf.broadcast_render_ms); badge.textContent = '首屏' + (fs === null ? '-' : (fs + 'ms')) + ' / 广播' + (br === null ? '-' : (br + 'ms')); var ok = (fs === null || fs <= BUDGETS.first_screen_ms) && (br === null || br <= BUDGETS.broadcast_render_ms); badge.setAttribute('class', 'pbl-badge ' + (ok ? 'is-ok' : 'is-warn')); global.__PBL_PERF__ = global.__PBL_PERF__ || {}; global.__PBL_PERF__.overlay = { page: this.pageKey, first_screen_ms: fs, broadcast_render_ms: br }; }; Overlay.prototype.destroy = function () { this.destroyed = true; if (this.transport) { this.transport.stop(); } this.patchQueue.length = 0; this._unsubs.forEach(function (p) { /* document 级监听随页面卸载回收 */ }); this._unsubs = []; this.mounted = false; }; /* ───────────────────────── 7. 自动装配入口 ───────────────────────── */ function boot(opts) { opts = opts || {}; var root = opts.root || document.querySelector('[data-pbl-overlay-root]') || document.body; if (!root.getAttribute('data-pbl-overlay-root')) { root.setAttribute('data-pbl-overlay-root', '1'); } var ov = new Overlay(Object.assign({ root: root }, opts)); global.__PBL_OVERLAY__ = ov; ov.mount(); return ov; } global.PblOverlayPlay = { VERSION: '1.0.0', Overlay: Overlay, ScenseBridge: ScenseBridge, pageUrl: pageUrl, readContext: readContext, boot: boot, auto: function () { // 只在带 data-pbl-page 标记的覆盖层页面壳里自动装配,普通业务页零副作用 if (!document.querySelector('[data-pbl-page]')) { return; } if (global.__PBL_OVERLAY__ && global.__PBL_OVERLAY__.mounted) { return; } boot(); } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { global.PblOverlayPlay.auto(); }, false); } else { global.PblOverlayPlay.auto(); } })(window);