diff --git a/.workbuddy/memory/2026-09-03.md b/.workbuddy/memory/2026-09-03.md
index 494591a..29b6274 100644
--- a/.workbuddy/memory/2026-09-03.md
+++ b/.workbuddy/memory/2026-09-03.md
@@ -290,3 +290,32 @@
- **验证**:node --check 3 JS ✓;curl --noproxy GET 状态正常(files=73/lastUpdateAgeMin=124/task=null);POST 空 prompt 400;CDP 冒烟 `tmp/cdp-rank-update-smoke.mjs`(9225)纯图标版 9 项全 PASS(text='' + svg + title + modal 标题/三榜范围/上次更新/按钮文案/取消)
- **服务/工具坑(重踩)**:nohup+& 后台 node 随 bash 退出被回收 → 须 Bash run_in_background 直接托管前台;taskkill //PID Git Bash 语法错 → PowerShell Stop-Process -Force(返回 exit 1 但端口已释放);curl 全局 socks 代理 502 → 必须 --noproxy '*'
- **待实测**:首次真实点击任务链路(调度拾取→技能会话→抓取执行→落盘→页面重建出最新期)
+
+## R34 工作台「脚本详情统一弹窗」三入口共用(09-03 晚间)
+- **需求**:首页 AI脚本卡 / AI写脚本「N 查看」/ 脚本诊断页标题 三处点击后展示同一弹窗;右侧内容由「AI分镜提示词」改为可切换「脚本诊断 | 分镜提示词」两个 tab,默认显示「脚本诊断」(用户指定)
+- **范围**:用户选「三处入口统一升级 (Recommended)」——合并两套重复弹窗代码(`openRewriteCompare` AI写脚本页 / `openScriptDetail` 诊断页)+ 首页 `openRecentScript` 接入
+- **顶层组件** `openScriptModal({ rewriteId, videoId })`(data-pages.js 取代两旧闭包):
+ - 数据装配:`vid>0` ref 走 `Promise.all([/api/dsh/video-script?videoId, /api/dsh/video-detail?videoId])`;`vid===0` custom 走 `/api/dsh/custom-script?id`(拒绝时回退 `/api/dsh/rewrites` 全表定位,若挂 video 则递归转 ref 形态);全空 → toast「该脚本不存在或已删除」
+ - 状态:`stab='diag'`(默认诊断)、`showSrc`、`sbIdx`、`verIdx`;`visibleSbOf(curId)` 按 rewrite_id 归属过滤
+ - 诊断口径:`review-full` 的 ai 或 compare 取一(compare 取 AI 视角 dims/total 与源对比分);11 维 DIAG_DIMS;无诊断 → 「该版本 AI 脚本尚未诊断」+ 开始复盘按钮
+ - 分镜边界:仅 ref 有(storyboard_log 需 video_id);custom 无视频载体 → 空态「该脚本为自定义选题(无关联视频),暂不支持分镜提示词」+ 无生成按钮(禁止 confirmRun 空视频)
+ - 复盘触发:有源 → 三模式复盘(AI/源/对比);无源 → 独立分析仅评 AI 脚本(与诊断页行按钮同文案)
+ - 完整报告:`reportFullReview(reviewId)` 顶层函数副本(原 openReviewDetail 弹窗逻辑抽全局,跨页复用)
+ - 场次滚动联动:仅 `stab==='sb'` 且有分镜时(AI 列滚动 → 分镜列对齐场块,分镜列不反向联动),sideSec 用 `.rw-side-tabs` 存在判定、aiSec 用 head 文本 startsWith('AI脚本') 判定
+- **删除旧闭包**:`openRewriteCompare`(含内嵌 renderSbBlocks)+ `openScriptDetail`(node -e 锚点切割,各 -7139/-6799 字符);衔接点 grep 验证零残留 renderSbBlocks/openRewriteCompare/openScriptDetail 引用
+- **app.js 首页接入**:`openRecentScript(s)` 优先 `window.DataPages.openScriptModal({ rewriteId: s.id, videoId: s.video_id })`;原单列全文弹窗降级兜底(data-pages.js 未就绪时)
+- **style.css 新增**:`.rw-side-head`/`.rw-side-tabs`/`.rw-side-tab`(pill tab 容器,默认透明)
+- **版本**:index.html ?v=20260903u→v(CSS 有 tab 底色修复须强制刷)
+- **CDP 冒烟** `tmp/cdp-r34-modal-smoke.mjs`(9225):reload ignoreCache→首页卡点击 → 弹窗三列 / 两 tab / 默认诊断 / AI列存在 / 源收起 / 诊断渲染 / 切分镜 / 分镜空态 / 关闭 / 诊断页入口同弹窗 / AI写脚本查看同弹窗;JS 版本=20260903v 时 **16 PASS 全绿**
+- **CSS tab 底色修复(09-03 末)**:用户反馈「切换内容的按钮不要用深蓝色底色,文字变色就行」→ `.rw-side-tab.active` 去 `background: var(--primary); color: #fff;`,改 `color: var(--primary); font-weight: 600`;hover 同步去 rgba 浅蓝底;CDP 计算样式验证:active `bg=rgba(0,0,0,0) color=rgb(47,111,237) weight=600`,符合「文字变色就行」
+- **冒烟断言修复(关键)**:初版 C2 断言 `!![data-vnext] || !!.rw-sec-count` 对 videoId=185(仅 1 个 rewrite id=20,单版本)误判 FAIL——组件按 `rewrites.length>1` 才渲染版本切换控件,单版本无控件是正确行为;改为二分合法态:AI列正文渲染 + (无版本区 = 单版本合法) / (有 count = 验证「版本 x/y」格式);重跑全绿
+- **代码删除纪律再踩(教训固化)**:同文件多 Edit 并行执行会互相覆盖(本次 3 处 index.html 版本 bump 并行,只存活 app.js 一处)——**同文件 Edit 必须串行**,并行 Edit 仅适用于不同文件
+
+## R34 样式打磨(09-03 末,w 版)
+- **tab 底色修复**(用户反馈):切换内容的按钮不要用深蓝色的底色,文字变色就行 → `.rw-side-tab.active` 去 `background: var(--primary); color: #fff;`,改 `color: var(--primary); font-weight: 600`;hover 同步去 rgba 浅蓝底;CDP 计算样式验证:active `bg=rgba(0,0,0,0) color=rgb(47,111,237) weight=600`
+- **空态主操作按钮统一**(用户反馈):开始复盘 + 生成分镜 两按钮「一个样式」 → 统一 `btn btn-sm btn-view`(白底蓝字蓝边,复用弹窗主操作家族「展开源脚本」「开始复盘」原有风格);CDP 实测两按钮计算样式完全一致
+- **版本**:data-pages.js ?v=20260903v→w(CSS 没改保持 v,app.js 保持 v)
+- **冒烟回归**:w 资源下 `tmp/cdp-r34-modal-smoke.mjs` 16/16 PASS 无回归
+
+## R34 同步 dsh 副本计划
+- dsh 副本 `C:\Users\maidou\.dsh\skills\short-video-script\`(独立副本,非软链)—— R34 改动 data-pages.js/app.js/style.css/index.html 同步评估后人工复制
diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/app.js b/project/短视频脚本创作/V1.0/mcn-work-shop/public/app.js
index ea300bf..69af401 100644
--- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/app.js
+++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/app.js
@@ -561,6 +561,12 @@ function setRankBoard(board, activeEl) {
loadRankingPreview();
}
function openRecentScript(s) {
+ // R34:首页「AI脚本」卡点击 → 打开与 AI写脚本页查看一致的三列统一弹窗(源脚本可折叠 | AI脚本版本 | 右侧 脚本诊断/分镜提示词 tab,默认脚本诊断)
+ if (window.DataPages && typeof window.DataPages.openScriptModal === 'function') {
+ window.DataPages.openScriptModal({ rewriteId: Number(s.id) || 0, videoId: Number(s.video_id) || 0 });
+ return;
+ }
+ // 兜底(data-pages.js 未就绪时):单列全文弹窗
const title = cleanTitle(s.title || s.video_title) || 'AI 写剧本';
const mask = document.createElement('div'); mask.className = 'modal-mask';
mask.innerHTML = `
diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js
index f1b8281..75bbc87 100644
--- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js
+++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js
@@ -1349,7 +1349,7 @@ async function renderRewrites(view, initAccount) { /* v22:initAccount=账号
confirmMsg: '将删除选中的 {n} 条视频(含视频解析、AI脚本,不可恢复)。确定删除?',
reload: load,
});
- view.querySelectorAll('[data-detail]').forEach((a) => a.addEventListener('click', () => openRewriteCompare(a.dataset.detail)));
+ view.querySelectorAll('[data-detail]').forEach((a) => a.addEventListener('click', () => openScriptModal({ videoId: Number(a.dataset.detail) }))); /* R34:统一脚本详情弹窗(右侧 tab 脚本诊断/分镜,默认诊断) */
view.querySelectorAll('[data-rewrite]').forEach((b) => b.addEventListener('click', () => fireVideoCreate(b)));
} else if (state.tab === 'custom') {
const ipt = view.querySelector('#rwCSearch');
@@ -1404,115 +1404,6 @@ async function renderRewrites(view, initAccount) { /* v22:initAccount=账号
state.stats = st;
load();
}
- async function openRewriteCompare(videoId) {
- const d = await api('/api/dsh/video-script?videoId=' + videoId).catch(() => ({ ok: false }));
- const detail = await api('/api/dsh/video-detail?videoId=' + videoId).catch(() => ({ ok: false }));
- const title = (detail.video && detail.video.video_title) || '';
- const source = d.source || '(暂无源脚本数据)';
- const rewrites = d.rewrites || [];
- let verIdx = 0;
- const stb = await api('/api/dsh/storyboards?videoId=' + videoId).catch(() => ({ items: [] }));
- const storyboards = stb.items || [];
- let sbIdx = 0;
- let showSrc = false; // 源脚本列默认收起
-
- function renderSbBlocks(text) {
- const blocks = String(text || '').split(/\n\s*---+\s*\n/).map((b) => b.trim()).filter(Boolean);
- if (blocks.length <= 1) return renderMarkdown(text || '');
- return blocks.map((b, bi) => {
- const lines = b.split('\n'); const first = lines[0].trim();
- const m = /^段\s*(\d+)\s*[((]\s*([\d.]+)\s*-\s*([\d.]+)\s*秒\s*[))]\s*[||]\s*(.+)$/.exec(first);
- let head = m ? `
${esc(m[4].trim())}${esc(m[2] + ' - ' + m[3] + '秒')}` : `
场${bi + 1}`;
- const content = m ? lines.slice(1).join('\n').trim() : b;
- return `
${head}
${renderMarkdown(content)}
`;
- }).join('');
- }
-
- function renderModal() {
- const cur = rewrites[verIdx];
- const visibleSb = storyboards.filter((s) => !cur || s.rewrite_id === cur.id);
- const sb = visibleSb[Math.min(sbIdx, visibleSb.length - 1)];
- const verCtrl = (p, idx, len, label, genLabel) => {
- const inner = len > 1
- ? `
${label} ${idx + 1}/${len}`
- : (len === 1 ? `
${label} 1/1` : '');
- return ``;
- };
- const mask = document.createElement('div'); mask.className = 'modal-mask';
- mask.innerHTML = `
-
${esc(fmtTitleQ(title))}
-
-
-
源脚本
-
${renderMarkdown(source)}
-
-
-
-
AI脚本
-
${cur ? renderMarkdown(cur.script_text || '(暂无内容)') : '(暂无 AI 脚本)'}
- ${verCtrl('v', verIdx, rewrites.length, '版本')}
-
-
-
AI分镜提示词
-
${sb ? renderSbBlocks(sb.script_text) : '
(暂无分镜提示词)
'}
- ${visibleSb.length ? verCtrl('s', sbIdx, visibleSb.length, '分镜', '重新生成') : ``}
-
-
-
`;
- document.body.appendChild(mask);
- mask.addEventListener('click', (e) => { if (e.target === mask) mask.remove(); });
- mask.querySelector('[data-x]').addEventListener('click', () => mask.remove());
- const q = (s) => mask.querySelector(s);
- const srcToggle = q('[data-src-toggle]'); if (srcToggle) srcToggle.addEventListener('click', () => { showSrc = !showSrc; mask.remove(); renderModal(); });
- const vprev = q('[data-vprev]'); if (vprev) vprev.addEventListener('click', () => { if (verIdx > 0) { verIdx--; sbIdx = 0; mask.remove(); renderModal(); } });
- const vnext = q('[data-vnext]'); if (vnext) vnext.addEventListener('click', () => { if (verIdx < rewrites.length - 1) { verIdx++; sbIdx = 0; mask.remove(); renderModal(); } });
- const sprev = q('[data-sprev]'); if (sprev) sprev.addEventListener('click', () => { if (sbIdx > 0) { sbIdx--; mask.remove(); renderModal(); } });
- const snext = q('[data-snext]'); if (snext) snext.addEventListener('click', () => { if (sbIdx < visibleSb.length - 1) { sbIdx++; mask.remove(); renderModal(); } });
- const fireGenSb = (btn, regenerate) => {
- confirmRun({
- title: regenerate ? '重新生成分镜' : '生成分镜',
- desc: '将基于该 AI 脚本(AI 创作的故事脚本),展开成 AI 分镜提示词(按分镜技能生成镜头级描述)。',
- target: `《${title}》`,
- prompt: `${SKILL_HINT_STORYBOARD}\n请将视频《${title}》的 AI 脚本${regenerate ? '重新' : ''}展开成 AI 分镜提示词(F4 生成分镜,镜头级描述)。`,
- label: '分镜指令',
- skills: ['短视频分镜脚本'],
- btn,
- });
- };
- const genSb = q('[data-gen-sb]'); if (genSb) genSb.addEventListener('click', () => fireGenSb(genSb, false));
- const sgen = q('[data-sgen]'); if (sgen) sgen.addEventListener('click', () => fireGenSb(sgen, true));
- // AI脚本列 ↔ AI分镜列 场次联动:滑动 AI脚本到第 N 场,分镜对应滚动到第 N 场块(仅该脚本有分镜时)
- if (visibleSb.length) {
- const secs = [...mask.querySelectorAll('.rw-stack > .rw-sec')];
- const aiBody = secs[1] && secs[1].querySelector('.rw-sec-body');
- const sbBody = secs[2] && secs[2].querySelector('.rw-sec-body');
- if (aiBody && sbBody) {
- // AI脚本列主导联动:AI脚本列滚动 → 分镜列对齐到对应场块(分镜列滚动不反向联动)
- const syncSb = () => {
- const heads = [...aiBody.querySelectorAll('strong, h1, h2, h3, h4')].filter((el) => /场\s*\d/.test(el.textContent));
- if (!heads.length) return;
- const aiTop = aiBody.getBoundingClientRect().top;
- const limit = aiBody.scrollTop + 60; // 视口上缘往下 60px 内最后一个场标题 = 当前场
- let cur = heads[0];
- for (const h of heads) {
- const t = h.getBoundingClientRect().top - aiTop + aiBody.scrollTop;
- if (t <= limit) cur = h; else break;
- }
- const m = /场\s*(\d+)/.exec(cur.textContent);
- if (!m) return;
- const target = [...sbBody.querySelectorAll('.sb-block')].find((b) => new RegExp('场' + m[1] + '(?![0-9])').test((b.querySelector('.sb-head') || {}).textContent || ''));
- if (target) {
- const t = target.getBoundingClientRect().top - sbBody.getBoundingClientRect().top + sbBody.scrollTop;
- if (Math.abs(sbBody.scrollTop - t) > 1) sbBody.scrollTop = t; // 位置相同不重复设置,避免事件环
- }
- };
- aiBody.addEventListener('scroll', syncSb, { passive: true });
- syncSb(); // 打开弹窗时先对齐一次
- }
- }
- }
- renderModal();
- }
/* 启动:拉取徽标计数;账号级进入(账号列表「AI脚本」列)时若该账号存在自定义选题脚本 → 默认落在「自定义选题创作」tab(否则「参考选题创作」) */
{
@@ -1604,7 +1495,10 @@ async function renderReviews(view) {
`;
bindBack(view);
bindPager(view, (p) => { page = p; render(); });
- view.querySelectorAll('[data-open]').forEach((a) => a.addEventListener('click', () => openScriptDetail(a.dataset.open)));
+ view.querySelectorAll('[data-open]').forEach((a) => a.addEventListener('click', () => { /* R34:统一脚本详情弹窗(右侧 tab 脚本诊断/分镜,默认诊断) */
+ const row = items.find((x) => String(x.id) === String(a.dataset.open));
+ if (row) openScriptModal({ rewriteId: row.id, videoId: Number(row.video_id) || 0 });
+ }));
view.querySelectorAll('[data-review]').forEach((a) => a.addEventListener('click', () => openReviewDetail(a.dataset.review)));
view.querySelectorAll('[data-review-row]').forEach((b) => b.addEventListener('click', () => {
const row = items.find((x) => String(x.id) === String(b.dataset.reviewRow));
@@ -1640,88 +1534,6 @@ async function renderReviews(view) {
}
}
- async function openScriptDetail(rewriteId) {
- // 脚本详情弹窗:样式对齐「AI写脚本」查看弹窗(rw-stack 三列)——源脚本(可折叠) | AI脚本(版本切换) | AI诊断(复盘结果)
- const row = items.find((x) => String(x.id) === String(rewriteId));
- if (!row) return;
- const d2 = await api('/api/dsh/video-script?videoId=' + row.video_id).catch(() => ({ ok: false }));
- const rewrites = (d2.rewrites || []);
- const hasSource = !!(d2.source && String(d2.source).trim());
- let verIdx = Math.max(0, rewrites.findIndex((x) => x.id === row.id));
- let showSrc = false; // 源脚本列默认收起(与查看弹窗一致)
- const DIMS_ALL = ['设定承接', '框架节奏', '钩子设置', '叙事逻辑', '极致事件', '共鸣和共情', '格式规范性', '台词口语化', '内容合规', '教程完整链', '其他'];
- const scoreColor = (v) => (v >= 75 ? '#3a9e6d' : v >= 60 ? '#d99a2b' : '#d9534f');
- // 诊断内容 HTML(取 AI 视角评分:compare 类型取 ai 侧 dims/total 与源对比结论,列表总分口径一致)
- const diagBodyHtml = (dRow) => {
- const rv = reviewOf(dRow);
- if (!rv) return '';
- let dims = {}, total = NaN;
- try { const j = JSON.parse(rv.dims_json || '{}'); dims = rv.type === 'compare' ? (j.ai || {}) : j; } catch (e) {}
- try { const t = JSON.parse(rv.total || '{}'); total = rv.type === 'compare' ? Number(t.ai) : Number(rv.total); } catch (e) { total = Number(rv.total); }
- const srcTotal = (() => { try { const t = JSON.parse(rv.total || '{}'); return rv.type === 'compare' ? Number(t.source) : NaN; } catch (e) { return NaN; } })();
- const scoreBar = DIMS_ALL.map((k) => { const v = dims[k]; const has = typeof v === 'number' && Number.isFinite(v); return `${esc(k)}${has ? v : '—'} `; }).join('');
- let html = `AI 总分 ${fmtScore(Number.isFinite(total) ? total : rv.total)}${rv.type === 'compare' && Number.isFinite(srcTotal) ? `(对比源脚本 ${fmtScore(srcTotal)})` : ''}${rv.verdict ? ` · ${esc(rv.verdict)}` : ''}
${scoreBar}
`;
- let issues = []; try { issues = JSON.parse(rv.issues_json || '[]'); } catch (e) {}
- if (issues.length) html += `问题清单
${issues.map((it) => `
${esc(it.severity || '低')}${esc(it.issue || '')}${it.suggestion ? `→ ${esc(it.suggestion)}` : ''}
`).join('')}
`;
- if (rv.summary) html += `综合评语:${esc(rv.summary)}
`;
- html += ``;
- return html;
- };
- const diagEmptyHtml = (dRow) => (dRow && dRow.review_running)
- ? `复盘运行中…(完成后重新打开本弹窗即可查看评分)
`
- : `该版本 AI 脚本尚未诊断
- `;
-
- function renderModal() {
- const curRow = rewrites.length ? rewrites[Math.min(verIdx, rewrites.length - 1)] : null;
- const dRow = curRow ? (items.find((x) => String(x.id) === String(curRow.id)) || curRow) : null; // items 行带 review_ai/compare;兜底 raw 行(无诊断)
- const diag = dRow ? reviewOf(dRow) : null;
- const srcSec = hasSource
- ? `
-
源脚本
-
${renderMarkdown(d2.source)}
-
-
` : '';
- const srcToggle = hasSource ? `` : '';
- const verCtrl = (() => {
- const len = rewrites.length;
- if (len <= 1) return `${len === 1 ? '版本 1/1' : ''}`;
- return `版本 ${verIdx + 1}/${len}`;
- })();
- const running = !!(dRow && dRow.review_running);
- const diagFoot = diag
- ? ``
- : '';
- const mask = document.createElement('div'); mask.className = 'modal-mask';
- mask.innerHTML = `
-
${esc(fmtTitleQ(row.title || ''))}
-
- ${srcSec}
-
-
AI脚本${srcToggle}
-
${curRow ? renderMarkdown(curRow.script_text || '(暂无内容)') : '(暂无 AI 脚本)'}
- ${verCtrl ? `` : ''}
-
-
-
AI诊断
-
${diag ? diagBodyHtml(dRow) : diagEmptyHtml(dRow)}
- ${diagFoot}
-
-
-
`;
- document.body.appendChild(mask);
- mask.addEventListener('click', (e) => { if (e.target === mask) mask.remove(); });
- mask.querySelector('[data-x]').addEventListener('click', () => mask.remove());
- const q = (s) => mask.querySelector(s);
- const srcT = q('[data-src-toggle]'); if (srcT) srcT.addEventListener('click', () => { showSrc = !showSrc; mask.remove(); renderModal(); });
- const vp = q('[data-vprev]'); if (vp) vp.addEventListener('click', () => { if (verIdx > 0) { verIdx--; mask.remove(); renderModal(); } });
- const vn = q('[data-vnext]'); if (vn) vn.addEventListener('click', () => { if (verIdx < rewrites.length - 1) { verIdx++; mask.remove(); renderModal(); } });
- const diagBtn = q('[data-diag-review]');
- if (diagBtn) diagBtn.addEventListener('click', () => { const t = dRow; if (t) startReview(t, diagBtn); });
- const full = q('[data-review]'); if (full) full.addEventListener('click', () => openReviewDetail(full.dataset.review));
- }
- renderModal();
- }
async function openReviewDetail(reviewId) {
const d3 = await api('/api/dsh/review-detail?id=' + reviewId).catch(() => ({ ok: false }));
@@ -1755,4 +1567,247 @@ async function renderReviews(view) {
}
}
-window.DataPages = { renderRanking, renderAccounts, renderAccountDetail, renderVideoDetail, renderRewrites, renderReviews };
+/* ================= R34 统一「脚本详情」弹窗(首页 AI脚本卡 / AI写脚本「N 查看」/ 脚本诊断 标题 三入口共用) =================
+ 布局同 AI写脚本查看弹窗(rw-stack 横向列):源脚本(可折叠,有原视频时) | AI脚本(版本切换) | 右侧列
+ 右侧列头部 tab:脚本诊断(默认) | 分镜提示词
+ —— 诊断内容 = 当前版本 AI 脚本最新复盘(script_review type=ai 优先;compare 取 AI 视角总分并带源对比分)
+ —— 分镜内容 = 该视频下 storyboard_log 中属当前版本的记录 + 生成/重新生成入口(仅参考选题脚本;自定义选题无视频载体不支持)
+ 数据源:/api/dsh/video-script + /api/dsh/video-detail(参考选题)/ /api/dsh/custom-script(自定义选题)
+ + /api/dsh/review-full?rewriteId=(当前版本诊断)+ /api/dsh/storyboards?videoId=(分镜) */
+const DIAG_DIMS = ['设定承接', '框架节奏', '钩子设置', '叙事逻辑', '极致事件', '共鸣和共情', '格式规范性', '台词口语化', '内容合规', '教程完整链', '其他'];
+const diagScoreColor = (v) => (v >= 75 ? '#3a9e6d' : v >= 60 ? '#d99a2b' : '#d9534f');
+const diagFmtScore = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : '-');
+
+/* 分镜文本渲染:按「段 N(起-止 秒)|标题」分块(继承原 AI写脚本查看弹窗逻辑;分镜时间戳分段格式) */
+function renderSbBlocks(text) {
+ const blocks = String(text || '').split(/\n\s*---+\s*\n/).map((b) => b.trim()).filter(Boolean);
+ if (blocks.length <= 1) return renderMarkdown(text || '');
+ return blocks.map((b, bi) => {
+ const lines = b.split('\n'); const first = lines[0].trim();
+ const m = /^段\s*(\d+)\s*[((]\s*([\d.]+)\s*-\s*([\d.]+)\s*秒\s*[))]\s*[||]\s*(.+)$/.exec(first);
+ let head = m ? `${esc(m[4].trim())}${esc(m[2] + ' - ' + m[3] + '秒')}` : `场${bi + 1}`;
+ const content = m ? lines.slice(1).join('\n').trim() : b;
+ return `${head}
${renderMarkdown(content)}
`;
+ }).join('');
+}
+
+/* 完整复盘报告弹窗(组件内副本:完整报告入口跨页复用;样式与脚本诊断页 openReviewDetail 一致) */
+async function reportFullReview(reviewId) {
+ const d3 = await api('/api/dsh/review-detail?id=' + reviewId).catch(() => ({ ok: false }));
+ if (!d3.ok) { showToast('复盘详情加载失败'); return; }
+ const rv = d3.review;
+ const typeName = rv.type === 'source' ? '原视频脚本复盘' : rv.type === 'compare' ? '对比分析' : 'AI 脚本复盘';
+ let dimsObj = {}; try { dimsObj = JSON.parse(rv.dims_json || '{}'); } catch (e) {}
+ const scoreBar = (dims, prefix) => DIAG_DIMS.map((k) => { const v = dims[k]; const has = typeof v === 'number' && Number.isFinite(v); return `${esc(k)}${has ? v : '—'} `; }).join('');
+ let body = '';
+ if (rv.type === 'compare') {
+ const aiD = dimsObj.ai || {}, srcD = dimsObj.source || {};
+ let t = {}; try { t = JSON.parse(rv.total || '{}'); } catch (e) {}
+ body = `AI 总分 ${diagFmtScore(Number(t.ai))}
${scoreBar(aiD, 'c-ai-')}
源脚本 总分 ${diagFmtScore(Number(t.source))}
${scoreBar(srcD, 'c-src-')}
`;
+ } else {
+ body = `总分 ${diagFmtScore(Number(rv.total))}
${scoreBar(dimsObj, 'd-')}
`;
+ }
+ let issues = []; try { issues = JSON.parse(rv.issues_json || '[]'); } catch (e) {}
+ if (issues.length) body += `问题清单
${issues.map((it) => `
${esc(it.severity || '低')}${esc(it.issue || '')}${it.suggestion ? `→ ${esc(it.suggestion)}` : ''}
`).join('')}
`;
+ if (rv.summary) body += `综合评语:${esc(rv.summary)}
`;
+ if (rv.content) body += `${renderMarkdown(rv.content)}
`;
+ const mask = document.createElement('div'); mask.className = 'modal-mask';
+ mask.innerHTML = `
+
${esc(typeName)}${rv.verdict ? ' · ' + esc(rv.verdict) : ''}
+
${body || '加载中…'}
+
`;
+ document.body.appendChild(mask);
+ mask.addEventListener('click', (e) => { if (e.target === mask) mask.remove(); });
+ mask.querySelector('[data-x]').addEventListener('click', () => mask.remove());
+}
+
+async function openScriptModal({ rewriteId, videoId } = {}) {
+ rewriteId = Number(rewriteId) > 0 ? Number(rewriteId) : 0;
+ const vid = Number(videoId) > 0 ? Number(videoId) : 0;
+ /* ---- 数据装配:版本列表 + 源脚本 + 标题 meta ---- */
+ let rewrites = [], source = null, metaTitle = '', metaAcc = '';
+ if (vid > 0) { /* 参考选题:以视频为容器,展示该视频下全部 AI 脚本版本(含源脚本) */
+ const [vs, vd] = await Promise.all([
+ api('/api/dsh/video-script?videoId=' + vid).catch(() => ({ ok: false, rewrites: [], source: null })),
+ api('/api/dsh/video-detail?videoId=' + vid).catch(() => ({ ok: false, video: null })),
+ ]);
+ rewrites = (vs && vs.rewrites) || [];
+ source = (vs && vs.source) || null;
+ if (vd && vd.video) { metaTitle = vd.video.video_title || ''; metaAcc = vd.video.account_name || ''; }
+ } else if (rewriteId) { /* 自定义选题(video_id 空):单条脚本;custom-script 拒绝则回退全量表定位(该脚本实际挂在视频下) */
+ const cs = await api('/api/dsh/custom-script?id=' + rewriteId).catch(() => ({ ok: false, error: 1 }));
+ if (cs && !cs.error && cs.script) {
+ rewrites = [{ id: cs.script.id, script_text: cs.script.script_text, created_time: cs.script.created_time }];
+ metaTitle = cs.script.title || ''; metaAcc = cs.script.account_name || '';
+ } else {
+ const all = await api('/api/dsh/rewrites').catch(() => ({ items: [] }));
+ const row = ((all && all.items) || []).find((x) => String(x.id) === String(rewriteId));
+ if (row && Number(row.video_id) > 0) { openScriptModal({ rewriteId, videoId: row.video_id }); return; }
+ if (row) { rewrites = [{ id: row.id, script_text: '', created_time: row.created_time }]; metaTitle = row.title || ''; }
+ }
+ }
+ if (!rewrites.length) { showToast('该脚本不存在或已删除'); return; }
+ let verIdx = rewriteId ? Math.max(0, rewrites.findIndex((x) => String(x.id) === String(rewriteId))) : 0;
+ if (verIdx < 0) verIdx = 0;
+ const title = metaTitle || 'AI 脚本';
+ const hasSource = vid > 0 && !!source;
+ let stab = 'diag'; /* 右侧 tab:diag=脚本诊断(默认)/ sb=分镜提示词 */
+ let showSrc = false, sbIdx = 0;
+ const stb = vid > 0 ? await api('/api/dsh/storyboards?videoId=' + vid).catch(() => ({ items: [] })) : { items: [] };
+ const storyboards = ((stb && stb.items) || []);
+ const visibleSbOf = (curId) => storyboards.filter((s) => s.rewrite_id == null || String(s.rewrite_id) === String(curId));
+
+ /* 复盘任务触发:有源脚本 → 三模式(AI/源/对比);无源 → 独立分析仅评 AI 脚本(同脚本诊断页行按钮口径) */
+ const fireReview = (btn) => {
+ if (btn.dataset.lock) return; btn.dataset.lock = '1';
+ try {
+ const [desc, prompt] = hasSource
+ ? ['将对该 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式)。',
+ `${SKILL_HINT_REVIEW}\n请对这条 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式)。`]
+ : ['将对该 AI 脚本做质量复盘评分(该脚本无原视频脚本,仅评 AI 脚本:独立分析模式·10 维评分制)。',
+ `${SKILL_HINT_REVIEW}\n请对这条 AI 脚本做质量复盘评分(独立分析模式·10 维评分制)。该脚本无原视频脚本,只评 AI 脚本本身,不做源脚本复盘、不做对比分析。`];
+ confirmRun({ title: hasSource ? '复盘评分' : 'AI脚本复盘', desc, prompt, label: '复盘指令', skills: ['脚本复盘'], btn });
+ } finally { delete btn.dataset.lock; }
+ };
+ /* 生成 / 重新生成分镜(仅参考选题脚本;自定义选题无视频载体不提供) */
+ const fireGenSb = (btn, regenerate) => {
+ confirmRun({
+ title: regenerate ? '重新生成分镜' : '生成分镜',
+ desc: '将基于该 AI 脚本(AI 创作的故事脚本),展开成 AI 分镜提示词(按分镜技能生成镜头级描述)。',
+ target: `《${title}》`,
+ prompt: `${SKILL_HINT_STORYBOARD}\n请将视频《${title}》的 AI 脚本${regenerate ? '重新' : ''}展开成 AI 分镜提示词(F4 生成分镜,镜头级描述)。`,
+ label: '分镜指令',
+ skills: ['短视频分镜脚本'],
+ btn,
+ });
+ };
+ /* 当前版本诊断(review-full 仅返回已完成复盘;无结果 → null → 空态 + 开始复盘) */
+ const diagOf = async (cur) => {
+ const rf = await api('/api/dsh/review-full?rewriteId=' + cur.id).catch(() => null);
+ const rv = (rf && (rf.ai || rf.compare)) || null;
+ if (!rv) return null;
+ let dims = {}, total = NaN, srcTotal = NaN;
+ try { const j = JSON.parse(rv.dims_json || '{}'); dims = rv.type === 'compare' ? (j.ai || {}) : j; } catch (e) {}
+ if (rv.type === 'compare') { try { const t = JSON.parse(rv.total || '{}'); total = Number(t.ai); srcTotal = Number(t.source); } catch (e) { total = Number(rv.total); } }
+ else { total = Number(rv.total); }
+ return { rv, dims, total, srcTotal };
+ };
+ const diagHtml = (dg) => {
+ const { rv, dims, total, srcTotal } = dg;
+ const totalFin = Number.isFinite(total);
+ const scoreBar = DIAG_DIMS.map((k) => { const v = dims[k]; const has = typeof v === 'number' && Number.isFinite(v); return `${esc(k)}${has ? v : '—'} `; }).join('');
+ let html = `AI 总分 ${diagFmtScore(totalFin ? total : Number(rv.total))}${rv.type === 'compare' && Number.isFinite(srcTotal) ? `(对比源脚本 ${diagFmtScore(srcTotal)})` : ''}${rv.verdict ? ` · ${esc(rv.verdict)}` : ''}
${scoreBar}
`;
+ let issues = []; try { issues = JSON.parse(rv.issues_json || '[]'); } catch (e) {}
+ if (issues.length) html += `问题清单
${issues.map((it) => `
${esc(it.severity || '低')}${esc(it.issue || '')}${it.suggestion ? `→ ${esc(it.suggestion)}` : ''}
`).join('')}
`;
+ if (rv.summary) html += `综合评语:${esc(rv.summary)}
`;
+ html += ``;
+ return html;
+ };
+
+ async function renderModal() {
+ const cur = rewrites[Math.min(verIdx, rewrites.length - 1)] || rewrites[0];
+ if (!cur) return;
+ const visibleSb = visibleSbOf(cur.id);
+ const sb = visibleSb[Math.min(sbIdx, visibleSb.length - 1)];
+ const dg = await diagOf(cur);
+ const verCtrl = (p, idx, len, label, genLabel) => {
+ const inner = len > 1
+ ? `${label} ${idx + 1}/${len}`
+ : (len === 1 ? `${label} 1/1` : '');
+ return ``;
+ };
+ const sideHead = ``;
+ let sideBody = '', sideFoot = '';
+ if (stab === 'diag') {
+ if (dg) {
+ sideBody = diagHtml(dg);
+ sideFoot = ``;
+ } else {
+ sideBody = '该版本 AI 脚本尚未诊断
';
+ sideFoot = ``;
+ }
+ } else if (visibleSb.length) {
+ sideBody = sb ? renderSbBlocks(sb.script_text) : '(暂无分镜提示词)
';
+ sideFoot = verCtrl('s', sbIdx, visibleSb.length, '分镜', '重新生成');
+ } else if (vid > 0) {
+ sideBody = '(暂无分镜提示词)
';
+ sideFoot = ``;
+ } else {
+ sideBody = '该脚本为自定义选题(无关联视频),暂不支持分镜提示词
';
+ sideFoot = '';
+ }
+ const srcSec = hasSource
+ ? `
+
源脚本
+
${renderMarkdown(source)}
+
+
` : '';
+ const srcToggle = hasSource ? `` : '';
+ const mask = document.createElement('div'); mask.className = 'modal-mask';
+ mask.innerHTML = `
+
${svgIcon('pen', 15, 'currentColor')} ${esc(fmtTitleQ(title))}${metaAcc ? ` ${esc(metaAcc)}` : ''}
+
+ ${srcSec}
+
+
AI脚本${srcToggle}
+
${renderMarkdown(cur.script_text || '(暂无内容)')}
+ ${rewrites.length > 1 ? verCtrl('v', verIdx, rewrites.length, '版本') : ''}
+
+
${sideHead}
+
${sideBody}
+ ${sideFoot}
+
+
+
`;
+ document.body.appendChild(mask);
+ mask.addEventListener('click', (e) => { if (e.target === mask) mask.remove(); });
+ const q = (s) => mask.querySelector(s);
+ const xb = q('[data-x]'); if (xb) xb.addEventListener('click', () => mask.remove());
+ const srcT = q('[data-src-toggle]'); if (srcT) srcT.addEventListener('click', () => { showSrc = !showSrc; mask.remove(); renderModal(); });
+ const vp = q('[data-vprev]'); if (vp) vp.addEventListener('click', () => { if (verIdx > 0) { verIdx--; sbIdx = 0; mask.remove(); renderModal(); } });
+ const vn = q('[data-vnext]'); if (vn) vn.addEventListener('click', () => { if (verIdx < rewrites.length - 1) { verIdx++; sbIdx = 0; mask.remove(); renderModal(); } });
+ mask.querySelectorAll('[data-stab]').forEach((t) => t.addEventListener('click', () => { if (stab !== t.dataset.stab) { stab = t.dataset.stab; mask.remove(); renderModal(); } }));
+ const sp = q('[data-sprev]'); if (sp) sp.addEventListener('click', () => { if (sbIdx > 0) { sbIdx--; mask.remove(); renderModal(); } });
+ const sn = q('[data-snext]'); if (sn) sn.addEventListener('click', () => { if (sbIdx < visibleSb.length - 1) { sbIdx++; mask.remove(); renderModal(); } });
+ const genSb = q('[data-gen-sb]'); if (genSb) genSb.addEventListener('click', () => fireGenSb(genSb, false));
+ const sgen = q('[data-sgen]'); if (sgen) sgen.addEventListener('click', () => fireGenSb(sgen, true));
+ const dgBtn = q('[data-diag-review]'); if (dgBtn) dgBtn.addEventListener('click', () => fireReview(dgBtn));
+ const full = q('[data-review]'); if (full) full.addEventListener('click', () => reportFullReview(full.dataset.review));
+ /* AI脚本列滚动 ↔ 分镜列场次对齐(仅右侧为「分镜提示词」且有分镜时生效;分镜列滚动不反向联动) */
+ if (stab === 'sb' && visibleSb.length) {
+ const secs = [...mask.querySelectorAll('.rw-stack > .rw-sec')];
+ const sideSec = secs.find((s2) => s2.querySelector('.rw-side-tabs'));
+ const aiSec = secs.find((s2) => s2 !== sideSec && (s2.querySelector('.rw-sec-head') || {}).textContent && (s2.querySelector('.rw-sec-head') || {}).textContent.trim().startsWith('AI脚本'));
+ const aiBody = aiSec && aiSec.querySelector('.rw-sec-body');
+ const sbBody = sideSec && sideSec.querySelector('.rw-sec-body');
+ if (aiBody && sbBody) {
+ const syncSb = () => {
+ const heads = [...aiBody.querySelectorAll('strong, h1, h2, h3, h4')].filter((el) => /场\s*\d/.test(el.textContent));
+ if (!heads.length) return;
+ const aiTop = aiBody.getBoundingClientRect().top;
+ const limit = aiBody.scrollTop + 60; /* 视口上缘往下 60px 内最后一个场标题 = 当前场 */
+ let curH = heads[0];
+ for (const h of heads) {
+ const t = h.getBoundingClientRect().top - aiTop + aiBody.scrollTop;
+ if (t <= limit) curH = h; else break;
+ }
+ const m = /场\s*(\d+)/.exec(curH.textContent);
+ if (!m) return;
+ const target = [...sbBody.querySelectorAll('.sb-block')].find((b) => new RegExp('场' + m[1] + '(?![0-9])').test((b.querySelector('.sb-head') || {}).textContent || ''));
+ if (target) {
+ const t = target.getBoundingClientRect().top - sbBody.getBoundingClientRect().top + sbBody.scrollTop;
+ if (Math.abs(sbBody.scrollTop - t) > 1) sbBody.scrollTop = t; /* 位置相同不重复设置,避免事件环 */
+ }
+ };
+ aiBody.addEventListener('scroll', syncSb, { passive: true });
+ syncSb(); /* 打开弹窗时先对齐一次 */
+ }
+ }
+ }
+ renderModal();
+}
+
+window.DataPages = { renderRanking, renderAccounts, renderAccountDetail, renderVideoDetail, renderRewrites, renderReviews, openScriptModal };
diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html b/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html
index 70eb626..bc85093 100644
--- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html
+++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html
@@ -4,7 +4,7 @@
MCN工作台
-
+
@@ -41,7 +41,7 @@
-
-
+
+