视频详情页:去掉视频分析tab;视频脚本/视频拆解未解析加解析按钮、已解析内容右上加刷新按钮(走技能F5 MCP解析任务)
This commit is contained in:
1 parent
1a68637de8
commit
8fe246fb51
5 files changed
+130
-8
No files matched your search
@@ -221,3 +221,10 @@
|
||||
- 串行理由:多会话并发会抢浏览器 9223 调试实例;executeTask 单按钮封装不适配 N 任务,自写轮询
|
||||
- 未验证:首次实点后需核对任务侧能否覆盖更新 hot_accounts 主表(server.js importAccountIntoDb 不写 hot_accounts 粉丝/获赞/作品数——只 upsert 视频解析/人设/视频列表/拆解;技能侧登记链路待实测)
|
||||
- 纯静态改动无需重启;版本 ?v=20260903j
|
||||
|
||||
## R28 视频详情页:去「视频分析」tab + 未解析「解析」/已解析「刷新」按钮(09-03)
|
||||
- 需求:视频详情页(renderVideoDetail)视频脚本/视频拆解 未解析时空态加「解析」按钮;已解析内容右上角加「刷新」按钮;去掉「视频分析」tab(analysis 数据保留不入展示)
|
||||
- data-pages.js:①顶部新增 SKILL_HINT_VIDEO_PARSE(mcn-dou-analysis 功能五 F5:upload_douyin_video→detailId→sleep→short_video_detail→json_tool.py 落盘 content.json/analysis.json→工作台 POST /api/import/account 入库);②tabBtns 删 analysis;③body 分支:scriptMd/deconstructMd 有→.vd-tools(右上刷新 data-parse=refresh)+md-body;无→.empty-state+解析按钮(data-parse=parse);④新增文件级 confirmVideoParse(btn,v,mode):confirmRun+executeTask 单任务,prompt=SKILL_HINT_VIDEO_PARSE+链接/标题/达人,label 解析指令 skills mcn-dou-analysis;tab 绑定后统一绑 [data-parse]
|
||||
- style.css:.vd-tools(flex 右对齐,margin 2px 0 8px 20px 对齐 page-detail md-body 缩进)
|
||||
- 冒烟 cdp-verify-r28.mjs(9225):未解析58=2 tabs(无分析 tab)+双 tab 空态解析按钮+解析弹窗;已解析117=vd-tools 刷新按钮(icon);deconstruct 无 analysis→空态解析(符合分支)全 PASS
|
||||
- 版本 ?v=20260903k;待实测:解析完成后 account_video_source 更新链路(F5 产物在 视频对标/ 目录,import/account 只扫 视频分析/——需看技能侧 backfill 或 import 扩展)
|
||||
@@ -0,0 +1,86 @@
|
||||
// R28 冒烟:视频详情页——去「视频分析」tab;未解析=空态「解析」按钮;已解析=内容右上「刷新」按钮
|
||||
const list = await fetch('http://localhost:9225/json').then((r) => r.json());
|
||||
const page = list.find((t) => t.type === 'page');
|
||||
if (!page) { console.log('ERR: no page'); process.exit(1); }
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
let id = 0; const pending = new Map();
|
||||
const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params })); });
|
||||
ws.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { const p = pending.get(m.id); pending.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.result); } };
|
||||
await new Promise((r) => (ws.onopen = r));
|
||||
const ev = async (expr) => { const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) return { __err: (r.exceptionDetails.exception?.description || r.exceptionDetails.text).slice(0, 200) }; return r.result.value; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
const waitFor = async (expr, ms = 12000) => { const t0 = Date.now(); while (Date.now() - t0 < ms) { if (await ev(expr)) return true; await sleep(300); } return false; };
|
||||
const fail = (m) => { console.log('FAIL@' + m); process.exit(1); };
|
||||
const goto = async (hash) => { await ev(`location.hash = ${JSON.stringify(hash)}`); await sleep(700); };
|
||||
|
||||
await send('Page.navigate', { url: 'http://localhost:8900/#/' });
|
||||
if (!(await waitFor(`!!document.querySelector('#view')`))) fail('app boot');
|
||||
await ev(`document.querySelectorAll('.modal-mask').forEach((m) => m.remove())`); // 清 root 设置弹窗
|
||||
|
||||
const out = {};
|
||||
/* ============ 1) 未解析视频 #/video/58 ============ */
|
||||
await goto('#/video/58');
|
||||
if (!(await waitFor(`!!document.querySelector('#view .tabs')`))) fail('tabs render');
|
||||
out.unparsed = await ev(`(() => {
|
||||
const tabs = [...document.querySelectorAll('#view .tabs .tab')].map((t) => ({ txt: t.textContent.trim(), active: t.classList.contains('tab-active') }));
|
||||
const es = document.querySelector('#view .empty-state');
|
||||
const pb = document.querySelector('#view [data-parse]');
|
||||
return {
|
||||
tabs,
|
||||
hasAnalysisTab: tabs.some((t) => t.txt === '视频分析'),
|
||||
emptyState: !!es,
|
||||
parseBtn: pb ? { mode: pb.dataset.parse, txt: pb.textContent.trim() } : null,
|
||||
};
|
||||
})()`);
|
||||
// 切「视频拆解」tab → 也应空态+解析按钮
|
||||
await ev(`[...document.querySelectorAll('#view .tabs .tab')].find((t) => t.textContent.trim() === '视频拆解').click()`);
|
||||
if (!(await waitFor(`document.querySelector('#view [data-parse]')`))) fail('deconstruct parse btn');
|
||||
out.unparsedDecon = await ev(`(() => {
|
||||
const pb = document.querySelector('#view [data-parse]');
|
||||
return { mode: pb.dataset.parse, empty: !!document.querySelector('#view .empty-state'), activeTab: document.querySelector('#view .tabs .tab-active').textContent.trim() };
|
||||
})()`);
|
||||
// 点「解析」→ confirmRun 弹窗 → 取消(不触发真实任务)
|
||||
await ev(`document.querySelector('#view [data-parse]').click()`);
|
||||
if (!(await waitFor(`[...document.querySelectorAll('.modal-mask .modal')].some((m) => m.textContent.includes('解析'))`))) fail('parse modal');
|
||||
out.parseModal = await ev(`(() => {
|
||||
const m = [...document.querySelectorAll('.modal-mask .modal')].find((x) => x.textContent.includes('视频拆解') || x.querySelector('h3'));
|
||||
const r = { title: m.querySelector('h3') ? m.querySelector('h3').textContent.trim() : '', hasOk: !!m.querySelector('[data-ok]') };
|
||||
m.querySelector('[data-cancel]').click();
|
||||
return r;
|
||||
})()`);
|
||||
await sleep(300);
|
||||
|
||||
/* ============ 2) 已解析视频 #/video/117 ============ */
|
||||
await goto('#/video/117');
|
||||
if (!(await waitFor(`!!document.querySelector('#view .md-body')`))) fail('parsed md-body');
|
||||
out.parsed = await ev(`(() => {
|
||||
const tools = document.querySelector('#view .vd-tools');
|
||||
const pb = tools ? tools.querySelector('[data-parse]') : null;
|
||||
return {
|
||||
hasTools: !!tools,
|
||||
refreshBtn: pb ? { mode: pb.dataset.parse, txt: pb.textContent.trim(), hasIcon: !!pb.querySelector('svg') } : null,
|
||||
mdLen: document.querySelector('#view .md-body').textContent.length,
|
||||
};
|
||||
})()`);
|
||||
// 切「视频拆解」tab:有内容→右上刷新;无内容→空态解析按钮(两种均可,但必须出现 data-parse)
|
||||
await ev(`[...document.querySelectorAll('#view .tabs .tab')].find((t) => t.textContent.trim() === '视频拆解').click()`);
|
||||
if (!(await waitFor(`document.querySelector('#view [data-parse]')`))) fail('parsed decon btn');
|
||||
out.parsedDecon = await ev(`(() => {
|
||||
const pb = document.querySelector('#view [data-parse]');
|
||||
return { mode: pb.dataset.parse, hasTools: !!document.querySelector('#view .vd-tools'), hasMd: !!document.querySelector('#view .md-body') };
|
||||
})()`);
|
||||
|
||||
console.log(JSON.stringify(out, null, 1));
|
||||
const ok = out.unparsed && !out.unparsed.__err
|
||||
&& out.unparsed.tabs.length === 2 && out.unparsed.tabs[0].txt === '视频脚本' && out.unparsed.tabs[1].txt === '视频拆解'
|
||||
&& out.unparsed.hasAnalysisTab === false
|
||||
&& out.unparsed.emptyState === true && out.unparsed.parseBtn && out.unparsed.parseBtn.mode === 'parse' && out.unparsed.parseBtn.txt.includes('解析')
|
||||
&& out.unparsedDecon && out.unparsedDecon.mode === 'parse' && out.unparsedDecon.empty === true && out.unparsedDecon.activeTab === '视频拆解'
|
||||
&& out.parseModal && out.parseModal.hasOk && !out.parseModal.title.includes('刷新')
|
||||
&& out.parsed && !out.parsed.__err && out.parsed.hasTools === true
|
||||
&& out.parsed.refreshBtn && out.parsed.refreshBtn.mode === 'refresh' && out.parsed.refreshBtn.txt.includes('刷新') && out.parsed.refreshBtn.hasIcon
|
||||
&& out.parsedDecon && (out.parsedDecon.mode === 'refresh' || (out.parsedDecon.mode === 'parse' && out.parsedDecon.hasMd === false));
|
||||
console.log(ok ? '\n=== PASS ===' : '\n=== CHECK ===');
|
||||
ws.close();
|
||||
process.exit(ok ? 0 : 1);
|
||||
@@ -14,6 +14,9 @@ const SKILL_HINT_STORYBOARD = '请调用 Skill 工具加载「短视频分镜脚
|
||||
/* 脚本复盘技能声明(09-02 修复:原技能声明混写在业务 prompt 字符串内,拆出独立前缀统一管理;
|
||||
复盘技能=只分析不改写,且工作台复盘必须独立会话一条脚本一上下文) */
|
||||
const SKILL_HINT_REVIEW = '请调用 Skill 工具加载「脚本复盘」技能(技能根目录 = 用户技能目录 ~/.workbuddy/skills/短视频脚本创作/subskills/mcn-script-review/,先读取其 SKILL.md 及 references 规范;若 Skill 工具无法加载,请直接读取该目录下的 SKILL.md 文件)。本任务为工作台复盘任务:严格按技能执行方式判定与复盘流程执行(评分式复盘/对比分析模式由任务说明指定;一条脚本=一个独立上下文,禁止混审多条脚本)。技能边界:只做分析与建议,不修改脚本、不触发创作流程、不改动任何技能文件。';
|
||||
/* 视频解析技能声明(09-03 R28:视频详情页「解析/刷新」按钮=单视频 MCP 解析,任务必须按 mcn-dou-analysis 功能五执行,
|
||||
一次性产出「视频脚本」(content 分镜头表)与「视频拆解」(analysis 结构化拆解)两个数据源 */
|
||||
const SKILL_HINT_VIDEO_PARSE = '请调用 Skill 工具加载「mcn-dou-analysis」技能(技能根目录 = 用户技能目录 ~/.workbuddy/skills/短视频脚本创作/subskills/mcn-dou-analysis/,先读取其 SKILL.md 及 references 规范;若 Skill 工具无法加载,请直接读取该目录下的 SKILL.md 文件)。本任务为技能功能五「解析视频」:对单条抖音视频做 MCP 内容解析。按 references/feature/05_解析视频.md 流程执行:① 调用 MCP 工具 upload_douyin_video 提交视频链接(share_text=该视频抖音链接)获取 detailId;② 等待解析完成(新视频约 3-5 分钟,用 sleep 等待后查询,content 已出而 analysis 未出时稍后重试);③ 用 short_video_detail 查询解析结果 content(视频脚本/分镜头表)与 analysis(视频拆解/结构化JSON);④ 按 references/解析结果格式规范.md 用 scripts/json_tool.py 将 content.json 与 analysis.json 保存到产出目录对应视频文件夹;⑤ 解析结果入库:工作台环境(本机 http://localhost:8900)产物落盘后调用 POST /api/import/account(body {"account":"{达人昵称}"})同步入库,接口不可用则跳过不影响保存。完成后汇报 detailId、content/analysis 是否齐备与保存路径。技能边界:只做该单视频解析,不抓取账号信息、不提炼账号设定、不创作/改写脚本。';
|
||||
/* 09-02 v25:AI创作任务完成后的写库回调指令——否则脚本只落会话/产出目录,工作台「AI写脚本」列表看不到。
|
||||
参考选题创作(有原视频,如视频行 AI创作):传 videoId/awemeId → rewrite_log.video_id 关联原视频;
|
||||
自定义选题创作(无原视频,如账号列表 AI创作 弹窗):不传 videoId → video_id 留空,归「自定义选题创作」tab。
|
||||
@@ -1071,12 +1074,17 @@ async function renderVideoDetail(view, id) {
|
||||
const viewLink = v.video_url ? `<a class="link" data-ext="${esc(v.video_url)}" title="打开视频网页">查看视频 ↗</a>` : '无';
|
||||
const scriptMd = parse.content || null;
|
||||
const deconstructMd = parse.analysis ? analysisToMd(parse.analysis) : null;
|
||||
const analysisMd = analysis && analysis.content ? analysis.content : null;
|
||||
const tabBtns = [['script', '视频脚本'], ['deconstruct', '视频拆解'], ['analysis', '视频分析']].map(([k, label]) => `<button class="tab ${tab === k ? 'tab-active' : ''}" data-tab="${k}">${label}</button>`).join('');
|
||||
/* 解析/刷新按钮(09-03 R28):未解析=空态「解析」;已解析=内容右上角「刷新」(同 F5 解析任务,幂等覆盖) */
|
||||
const parseBtn = (mode) => `<button class="btn btn-sm" data-parse="${mode}" title="${mode === 'refresh' ? '重新解析该视频(覆盖更新视频脚本与视频拆解)' : '对该视频做 MCP 解析(产出视频脚本与视频拆解)'}">${svgIcon(mode === 'refresh' ? 'refresh' : 'zap', 14, 'currentColor')} ${mode === 'refresh' ? '刷新' : '解析'}</button>`;
|
||||
const emptyAct = (icon, title, desc) => `<div class="empty-state"><div class="empty-icon">${svgIcon(icon, 40)}</div><div class="empty-title">${title}</div><div class="empty-desc">${desc}</div><div style="margin-top:12px">${parseBtn('parse')}</div></div>`;
|
||||
const tabBtns = [['script', '视频脚本'], ['deconstruct', '视频拆解']].map(([k, label]) => `<button class="tab ${tab === k ? 'tab-active' : ''}" data-tab="${k}">${label}</button>`).join('');
|
||||
let body = '';
|
||||
if (tab === 'script') body = scriptMd ? `<div class="md-body">${renderMarkdown(scriptMd)}</div>` : `<div class="empty">该视频暂无视频脚本(未做原视频解析)</div>`;
|
||||
else if (tab === 'deconstruct') body = deconstructMd ? `<div class="md-body">${renderMarkdown(deconstructMd)}</div>` : `<div class="empty">该视频暂无视频拆解(MCP 结构化分析)</div>`;
|
||||
else body = analysisMd ? `<div class="md-body">${renderMarkdown(analysisMd)}</div>` : `<div class="empty">该视频暂无视频分析</div>`;
|
||||
if (tab === 'script') body = scriptMd
|
||||
? `<div class="vd-tools">${parseBtn('refresh')}</div><div class="md-body">${renderMarkdown(scriptMd)}</div>`
|
||||
: emptyAct('file-text', '暂无视频脚本', '该视频尚未解析。点击「解析」对该视频做 MCP 内容解析(同时产出「视频脚本」与「视频拆解」,约需数分钟)。');
|
||||
else if (tab === 'deconstruct') body = deconstructMd
|
||||
? `<div class="vd-tools">${parseBtn('refresh')}</div><div class="md-body">${renderMarkdown(deconstructMd)}</div>`
|
||||
: emptyAct('chart', '暂无视频拆解', '该视频尚未完成结构化拆解。点击「解析」对该视频做 MCP 内容解析(同时产出「视频脚本」与「视频拆解」,约需数分钟)。');
|
||||
view.innerHTML = `
|
||||
<div class="page page-detail">
|
||||
<div class="page-toolbar">${toolbarBack()}<span class="page-title">${esc(cleanTitle(v.video_title) || '视频详情')}</span></div>
|
||||
@@ -1095,11 +1103,30 @@ async function renderVideoDetail(view, id) {
|
||||
</div>`;
|
||||
bindBack(view);
|
||||
view.querySelectorAll('[data-tab]').forEach((b) => b.addEventListener('click', () => { tab = b.dataset.tab; load(); }));
|
||||
view.querySelectorAll('[data-parse]').forEach((b) => b.addEventListener('click', () => confirmVideoParse(b, v, b.dataset.parse)));
|
||||
view._reload = load; // 静默自动刷新(保留当前 tab)
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
/* 视频详情「解析/刷新」确认弹窗(09-03 R28):走 confirmRun + executeTask 单任务链路,
|
||||
prompt=技能功能五 F5(SKILL_HINT_VIDEO_PARSE),任务完成 executeTask 自动刷新本页显示新内容 */
|
||||
function confirmVideoParse(btn, v, mode) {
|
||||
const isRefresh = mode === 'refresh';
|
||||
confirmRun({
|
||||
title: isRefresh ? '重新解析视频' : '解析视频',
|
||||
desc: isRefresh
|
||||
? '将重新对该视频做 MCP 内容解析,覆盖更新「视频脚本」与「视频拆解」(约需 3-5 分钟,完成后自动刷新)。'
|
||||
: '将对该视频做 MCP 内容解析,产出「视频脚本」(分镜头表)与「视频拆解」(结构化分析),约需 3-5 分钟,完成后自动刷新。',
|
||||
target: cleanTitle(v.video_title) || '视频',
|
||||
url: v.video_url || '',
|
||||
prompt: `${SKILL_HINT_VIDEO_PARSE}\n待解析视频链接:${v.video_url || ''}\n视频标题:${v.video_title || ''}\n所属达人:${v.account_name || ''}`,
|
||||
label: '解析指令',
|
||||
btn,
|
||||
skills: ['mcn-dou-analysis'],
|
||||
});
|
||||
}
|
||||
|
||||
/* ================= AI写脚本 ================= */
|
||||
async function renderRewrites(view, initAccount) { /* v22:initAccount=账号列表 AI脚本列跳转进入,预筛该账号;09-02 v25:AI写脚本=三类脚本中心(参考选题创作/自定义选题创作/ABC融合创作) */
|
||||
try { initAccount = decodeURIComponent(initAccount || ''); } catch (e) { initAccount = ''; } /* hash 路径段 param 未解码,中文账号名需还原(v22 fix) */
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MCN工作台</title>
|
||||
<link rel="stylesheet" href="style.css?v=20260903j">
|
||||
<link rel="stylesheet" href="style.css?v=20260903k">
|
||||
<script src="vendor/echarts.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js?v=20260903j"></script>
|
||||
<script src="data-pages.js?v=20260903j"></script>
|
||||
<script src="app.js?v=20260903k"></script>
|
||||
<script src="data-pages.js?v=20260903k"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -407,6 +407,8 @@ main#view { height: calc(100vh - 55px); overflow: auto; } /* 55px=topbar 实际
|
||||
.page { padding: 20px 24px; max-width: 1440px; margin: 0 auto; }
|
||||
/* ================= 视频详情页 md-body:内容相对 tab 缩进 20px + 标题字号收紧 ================= */
|
||||
.page-detail .md-body { font-size: 15px; margin-left: 20px; }
|
||||
/* 视频详情内容右上角工具条(09-03 R28):已解析内容顶部右对齐放「刷新」按钮 */
|
||||
.vd-tools { display: flex; justify-content: flex-end; margin: 2px 0 8px 20px; }
|
||||
.page-detail .md-body h1 { font-size: 20px; border-bottom: 1px solid var(--border); padding-bottom: 4px; margin: 14px 0 8px; }
|
||||
.page-detail .md-body h2 { font-size: 17px; border-bottom: 1px solid var(--border); padding-bottom: 3px; margin: 12px 0 6px; }
|
||||
.page-detail .md-body h3 { font-size: 15px; margin: 10px 0 6px; }
|
||||
|
||||
Reference in new issue
Block a user