diff --git a/.workbuddy/memory/2026-09-01.md b/.workbuddy/memory/2026-09-01.md index dc66366..1ba718c 100644 --- a/.workbuddy/memory/2026-09-01.md +++ b/.workbuddy/memory/2026-09-01.md @@ -154,3 +154,18 @@ - ② 账号详情旧梦留声机 id185(hasTopic=true):含【视频选题】+「# 选题」段(10106 字符)✅ - 未解析分支不变 PASS ✅ - 教训:断言必须按数据实况动态分支(hasTopic true→查【视频选题】,false→查【视频解析内容】+提炼指令),写死单一路径会误报 FAIL + +## 全功能 AI 会话任务输入输出梳理 + 顺序统一(12:05-12:20) +- **用户需求**:①把每个功能对应的 AI 会话任务输入输出搞清楚 ②创作类 prompt 一般「账号设定放前面、选题信息放后面」 +- **梳理产出**:`mcn-work-shop/docs/AI会话任务输入输出对照.md`(新建 docs/ 目录)——8 类功能入口对照表(账号列表 AI创作/账号详情视频列表 AI创作/#/rewrites AI创作/提炼账号设定/生成分镜/复盘评分/保存并分析账号/添加账号)+ 触发链路 + 各功能 prompt 结构模板 + 关键细节 + 验证说明 +- **顺序统一改动(data-pages.js 2 处未解析分支)**:账号详情视频列表 L436 + #/rewrites L566 —— 未解析 prompt 原为「请先解析…再创作:URL\n标题…${personaBlock}」(账号设定在末尾),统一改为「请为达人…创作…。${personaBlock}${noPersonaNote}\n该视频尚未解析,请先解析…(视频地址+标题)…再基于账号设定与提炼出的选题创作」(账号信息前置) +- **补漏**:账号详情分支 data-rewrite JSON 缺 url 字段(#/rewrites 分支有),未解析 prompt 用 r.url 会丢视频地址 → 补齐 `url: v.video_url || ''`(L340) +- **验证**:cdp-verify-aicreate.mjs 升级断言——已解析 topic/content 两模式 + 未解析「账号信息(人设块或缺失提示)位置 < 解析要求位置」顺序断言;全部 PASS(①已解析旧梦留声机 1629 content 模式、②未解析白昼小熊缺失提示 idx26<解析要求 idx75、③账号详情旧梦留声机 topic 模式) +- **已确认无需改**:confirmAiScript(账号列表)本就是 人设→选题方向 顺序;生成分镜/提炼设定/复盘/保存添加为转换/诊断类任务,无账号设定-选题顺序概念 + +## 09-01 20:0x 账号列表「类型」列 签约→内部(badge + 筛选下拉) +- **用户需求**:「把账号列表下的 类型列的签约改为 内部」 +- **改动(data-pages.js 2 处文案)**:L237 badge `${t === 'external' ? '外部' : '签约'}`→`'内部'`;L260 筛选下拉 option「签约」→「内部」;后端 type=signed 值不变 +- **CDP 验证全 PASS**(cdp-verify-typebadge.mjs):badge 文本 ["内部","外部","外部","外部","外部","内部","内部"];下拉 ["全部","内部","外部"];UI(badge+下拉)无「签约」残留 +- **「签约」残留排查边界**:页面含「签约」文本来自账号 bio 数据(如「Oiiioii签约超创」简介),属账号数据不在改名范围——用 CDP 脚本定位(tmp/find-signed.mjs,跑完即删)确认为数据文本而非 UI +- 临时脚本已删;待提交(与顺序统一+docs 文档一起按 2 小时节奏 push) diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-aicreate.mjs b/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-aicreate.mjs index 753b177..3021bb3 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-aicreate.mjs +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-aicreate.mjs @@ -119,8 +119,16 @@ async function main() { await evalJs(ws, `document.querySelectorAll('[data-rewrite]')[${unparsedInfo.idx}].click(); true`); await waitFor(ws, `!!window.__cap`, 30000, 'confirmRun 捕获(未解析)'); const p2 = JSON.parse(await evalJs(ws, `JSON.stringify(window.__cap)`)).prompt || ''; - const ok2 = p2.includes('请先解析这个抖音视频') && !p2.includes('undefined'); - console.log(' 未解析分支(先解析再创作+无 undefined 泄漏):', ok2 ? 'PASS' : 'FAIL'); + // 账号设定在前:账号相关信息(【账号设定(人设卡)】块,或无人设时的「该账号暂无账号设定」提示)必须出现在解析要求之前(09-01 用户要求统一顺序) + const iP2 = p2.indexOf('【账号设定(人设卡)】'); + const iNote = p2.indexOf('该账号暂无账号设定'); + const iUn = p2.indexOf('该视频尚未解析'); + const acctPos = iP2 >= 0 ? iP2 : iNote; // 有 persona 取人设块位置;无 persona 取缺失提示位置 + const ok2 = p2.includes('该视频尚未解析,请先解析') + && acctPos >= 0 && iUn > acctPos // 账号相关信息在解析要求之前 + && !p2.includes('undefined'); + console.log(' 未解析分支:人设块 idx=', iP2, '| 缺失提示 idx=', iNote, '| 解析要求 idx=', iUn); + console.log(' 未解析分支(账号信息前置+先解析再创作+无泄漏):', ok2 ? 'PASS' : 'FAIL'); } else { console.log(' 无未解析视频可测(跳过)'); } diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-typebadge.mjs b/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-typebadge.mjs new file mode 100644 index 0000000..972c954 --- /dev/null +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-typebadge.mjs @@ -0,0 +1,83 @@ +/* cdp-verify-typebadge.mjs — 账号列表「类型」列 签约→内部 改名验证(09-01) + * 断言:①badge 文本(非 external)=「内部」、external=「外部」不变 ②筛选下拉选项「内部」③页面无「签约」残留 + * 运行:node cdp-verify-typebadge.mjs + */ +const CDP = 'http://127.0.0.1:9223'; + +async function getWs(targetId) { + const tabs = await (await fetch(CDP + '/json')).json(); + const t = tabs.find((x) => x.id === targetId); + if (!t) throw new Error('target not found: ' + targetId); + return t.webSocketDebuggerUrl; +} +function cdp(ws, method, params = {}) { + return new Promise((resolve, reject) => { + const id = ++cdp._id; + const w = new WebSocket(ws); + const timer = setTimeout(() => { try { w.close(); } catch (e) {} reject(new Error('timeout: ' + method)); }, 30000); + w.onopen = () => w.send(JSON.stringify({ id, method, params })); + w.onmessage = (ev) => { + const m = JSON.parse(ev.data); + if (m.id !== id) return; + clearTimeout(timer); + try { w.close(); } catch (e) {} + m.error ? reject(new Error(method + ': ' + JSON.stringify(m.error))) : resolve(m.result); + }; + w.onerror = () => { clearTimeout(timer); reject(new Error('ws error: ' + method)); }; + }); +} +cdp._id = 0; + +async function evalJs(ws, expr) { + const r = await cdp(ws, 'Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true }); + if (r.exceptionDetails) throw new Error('eval err: ' + JSON.stringify(r.exceptionDetails).slice(0, 300)); + return r.result && r.result.value; +} + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +async function waitFor(ws, expr, timeout = 40000, label = '') { + const t0 = Date.now(); + while (Date.now() - t0 < timeout) { + try { if (await evalJs(ws, expr)) return true; } catch (e) {} + await sleep(800); + } + throw new Error('waitFor timeout: ' + label + ' -> ' + expr); +} + +async function main() { + const tabs = await (await fetch(CDP + '/json')).json(); + const wb = tabs.find((t) => t.url.includes('localhost:8900') || t.url.includes('127.0.0.1:8900')); + if (!wb) throw new Error('未找到 8900 工作台标签页'); + const ws = await getWs(wb.id); + console.log('▶ 复用标签页:', wb.url); + + await evalJs(ws, `location.hash = '#/accounts'`); + await cdp(ws, 'Page.reload', { ignoreCache: true }); + await waitFor(ws, `!!document.querySelector('[data-create]')`, 40000, '账号列表渲染'); + await sleep(500); + + const r = await evalJs(ws, `(() => { + const badges = [...document.querySelectorAll('.badge')].map((b) => b.textContent.trim()); + const sel = document.querySelector('#accType'); + const opts = sel ? [...sel.options].map((o) => o.textContent.trim()) : []; + // UI 残留检查仅限 badge 与筛选下拉(账号 bio 简介等数据文本含「签约」属账号数据,不在改名范围) + const badgeText = document.querySelector('.table-wrap') ? [...document.querySelectorAll('.badge')].map((b) => b.textContent).join('') : ''; + const selText = sel ? sel.textContent : ''; + return { badges, opts, uiSigned: (badgeText + selText).includes('签约') }; + })()`); + console.log('badge 文本:', JSON.stringify(r.badges)); + console.log('筛选下拉选项:', JSON.stringify(r.opts)); + console.log('UI(badge+下拉)含「签约」残留:', r.uiSigned); + + const extOk = r.badges.includes('外部'); + const intOk = r.badges.some((b) => b === '内部'); + const optOk = r.opts.includes('内部') && !r.opts.includes('签约'); + const noSigned = !r.uiSigned; + console.log('✅ 外部 badge 保留:', extOk ? 'PASS' : 'FAIL'); + console.log('✅ 内部 badge 出现:', intOk ? 'PASS' : 'FAIL'); + console.log('✅ 下拉含「内部」且无「签约」:', optOk ? 'PASS' : 'FAIL'); + console.log('✅ UI 无「签约」残留:', noSigned ? 'PASS' : 'FAIL'); + console.log('\n====', (extOk && intOk && optOk && noSigned) ? '全部 PASS ====' : '存在 FAIL ===='); +} + +main().catch((e) => { console.error('❌', e.message); process.exit(1); }); diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/docs/AI会话任务输入输出对照.md b/project/短视频脚本创作/V1.0/mcn-work-shop/docs/AI会话任务输入输出对照.md new file mode 100644 index 0000000..e3e31e9 --- /dev/null +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/docs/AI会话任务输入输出对照.md @@ -0,0 +1,102 @@ +# MCN 工作台 · AI 会话任务输入输出对照 + +> 更新:2026-09-01 | 范围:工作台所有触发 AI 会话任务(/api/run → 左侧会话栏后台会话)的功能入口 +> 统一规则(用户定):**创作类任务 prompt 一律「账号设定(人设卡)在前,选题信息在后」**;无人设时以「该账号暂无账号设定,请先提取」提示占位(同样前置)。 + +## 触发链路(所有入口共用) + +按钮点击 → 确认弹窗(confirmRun / confirmAiScript / confirmSaveAccount)→ `executeTask()` → `POST /api/run`(server.js 写 automations 表,scheduledAt=now+5s + next_run_at=now+5000)→ 客户端调度器拾取 → 左侧会话栏「mcn-workshop」空间后台会话执行 → 轮询 `/api/run/status`(done/error/review)→ 完成刷新页面。 + +## 功能对照表 + +| # | 功能入口 | 按钮 | 任务名(label) | 输入(prompt 注入) | 输出 | 代码位置 | +|---|---------|------|--------------|--------------------|------|---------| +| 1 | 账号列表(#/accounts) | AI创作 | 创作指令 | 账号名 + 【账号设定(人设卡)】(完整、剔非创作章节) + 选题方向(手输 或 AI 结合人设生成) | 全新故事脚本 | confirmAiScript | +| 2 | 账号详情(#/account/:id)视频列表 | AI创作 | 创作指令 | 账号名 + 【账号设定(人设卡)】+ 视频选题块(有「# 选题」段→直接用;无→【视频解析内容(供提炼选题)】+ 提炼指令)或 未解析→先解析再创作 | 全新故事脚本 | renderAccountDetail data-rewrite | +| 3 | AI写脚本(#/rewrites)视频列表 | AI创作 | 创作指令 | 同 2(按 accountId 拉人设,带 video_url) | 全新故事脚本 | renderRewrites data-rewrite | +| 4 | 账号详情(#/account/:id) | 提炼账号设定 | 提炼指令 | 账号名 | 人设卡(12段账号设定) | renderAccountDetail data-extract | +| 5 | 改写对比弹窗(AI脚本 vs 源脚本) | 生成分镜 / 重新生成 | 分镜指令 | 视频标题 + AI改写脚本 | AI 分镜提示词(按分镜技能) | openRewriteCompare fireGenSb | +| 6 | AI脚本诊断(#/reviews) | 复盘 | 复盘指令 | 该条 AI 脚本(后台按 mcn-script-review 技能执行) | 评分式复盘报告(AI/源/对比三模式) | renderReviews data-review-row | +| 7 | 周榜(#/ranking / 首页) | 分析 / 关注 | 保存指令 | 账号名 + 主页 URL | 账号信息 + 视频列表入库 | confirmSaveAccount | +| 8 | 账号列表(#/accounts) | 添加 | 添加指令 | 账号名称 + 账号地址(可选) | 网页采集账号信息 + 视频列表入库 | openImportAccounts | + +## 各功能 prompt 结构与顺序 + +### 1/2/3 创作类(账号设定前 → 选题后) + +**已解析 + 有「# 选题」段**(mode=topic): +``` +请为达人「{name}」创作一条全新的短视频故事脚本。 +【账号设定(人设卡)】 +{人设卡全文,剔「## 十一、」及之后} +【视频选题】 +标题:《{title}》 +# 选题 +{选题段:赛道/受众/视频主题/开场钩子/中间留人/结尾落点} +请结合账号设定与视频选题,创作一个全新的故事脚本(新故事、新情节,不要复刻原视频),输出完整故事脚本。 +``` + +**已解析 + 无「# 选题」段**(老视频脚本=分镜头表,mode=content): +``` +请为达人「{name}」创作一条全新的短视频故事脚本。 +【账号设定(人设卡)】 +{人设卡全文} +【视频解析内容(供提炼选题)】 +标题:《{title}》 +{解析 content 全文} +请先通读以上【视频解析内容】,提炼出该视频的选题(赛道/受众/视频主题/开场钩子/中间留人/结尾落点等),再结合账号设定与提炼出的选题,创作一个全新的故事脚本(新故事、新情节,不要复刻原视频),输出完整故事脚本。 +``` + +**未解析**(先解析再创作,账号信息仍前置): +``` +请为达人「{name}」创作一条全新的短视频故事脚本。 +【账号设定(人设卡)】 或 「该账号暂无账号设定,请先为达人「{name}」提取账号设定(生成人设卡),再基于账号设定与选题创作。」 +该视频尚未解析,请先解析这个抖音视频并做拆解分析(视频地址:{url} +视频标题:《{title}》),再基于账号设定与提炼出的选题,创作完整故事脚本。 +``` + +**账号列表入口(confirmAiScript,选题方向)**: +``` +请为达人「{name}」创作一条短视频脚本。 +【账号设定(人设卡)】 +{人设卡全文} +选题方向:{topic}(可选) +请贴合上述账号设定(人设与内容规则)创作。 +``` + +### 4 提炼账号设定 +``` +请为达人「{name}」提炼账号设定(生成人设卡)。 +``` + +### 5 生成分镜(转换类,无账号设定/选题概念) +``` +请将视频《{title}》的 AI 改写脚本{重新}展开成 AI 分镜提示词(按分镜技能生成镜头级描述)。 +``` + +### 6 复盘评分(诊断类,由技能定义输入输出) +``` +请对这条 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式),按 mcn-script-review(脚本复盘)技能执行。 +``` + +### 7 保存并分析账号 +``` +请帮我把抖音账号「{name}」保存并分析(账号信息 + 视频列表):{url} +``` + +### 8 添加账号 +``` +请帮我把抖音账号 {账号名称「x」, 账号地址「y」} 添加:通过抖音网页获取账号信息与视频列表,写入账号列表并保存到本地 +``` + +## 关键细节 + +- **人设卡注入**:完整拼入(不截断),仅通过 `personaForCreate()` 剔除「## 十一、当前需解决的问题」及之后章节(分析师视角内容非创作素材);三处创作入口统一。 +- **视频选题提取**:`extractVideoTopic(content)` 取解析 content 的「# 选题」段(到下一个 `\n# ` 标题止);无该段(老脚本为分镜头表格式)→ `buildTopicBlock` mode=content 携带全文 + 提炼指令,AI 先提炼选题再创作。 +- **按钮命名**:全部统一「AI创作」(2026-09-01),弹窗标题同;「提炼账号设定」「生成分镜」「复盘」「分析/关注」「添加」为各自独立任务。 +- **未解析分支**:带 video_url 时 prompt 内嵌「视频地址:{url}」;账号列表视频(#/rewrites)用 r.url,账号详情视频列表用 r.aweme 拼 https://www.douyin.com/video/{aweme}(历史遗留,建议统一为 video_url)。 + +## 验证 + +- 脚本:`cdp-verify-aicreate.mjs`(hook confirmRun 捕获 prompt,断言账号设定在前、选题在后、无 undefined 泄漏、未解析分支账号信息前置) +- 覆盖:①#/rewrites 已解析(topic/content 两模式)②#/rewrites 未解析 ③账号详情已解析 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 4f4e820..a9f4e57 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js @@ -234,7 +234,7 @@ async function renderAccounts(view) { const tracks = (await api('/api/dsh/tracks').catch(() => ({ tracks: [] }))).tracks || []; async function load() { const d = await api('/api/dsh/accounts?type=' + state.type + '&search=' + encodeURIComponent(state.search) + '&track=' + encodeURIComponent(state.track) + '&page=' + state.page + '&pageSize=' + state.pageSize); - const badge = (t) => `${t === 'external' ? '外部' : '签约'}`; + const badge = (t) => `${t === 'external' ? '外部' : '内部'}`; const rows = (d.items || []).map((a) => ` ${badge(a.account_type)} @@ -257,7 +257,7 @@ async function renderAccounts(view) {
@@ -337,7 +337,7 @@ async function renderAccountDetail(view, id) { ${fmtWan(v.comment_count)} ${fmtWan(v.share_count)} ${fmtDuration(v.duration)} - + `).join(''); const tabBtns = [['info', '账号信息'], ['persona', '账号设定'], ['analysis', '数据分析']].map(([k, label]) => ``).join(''); let panel = ''; @@ -433,7 +433,7 @@ async function renderAccountDetail(view, id) { target: `达人「${r.name}」|选题《${r.title}》`, prompt: parsed ? `请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}${tb.block}${createTail}` - : `请先解析这个抖音视频并做拆解分析,再为达人「${r.name}」创作一条全新故事脚本:https://www.douyin.com/video/${r.aweme || ''}\n视频标题:《${r.title}》${personaBlock}${noPersonaNote}`, + : `请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}\n该视频尚未解析,请先解析这个抖音视频并做拆解分析(${r.url ? '视频地址:' + r.url + '\n' : ''}视频标题:《${r.title}》),再基于账号设定与提炼出的选题,创作完整故事脚本。`, label: '创作指令', btn: b, }); @@ -563,7 +563,7 @@ async function renderRewrites(view) { target: `达人「${r.name}」|选题《${r.title}》`, prompt: parsed ? `请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}${tb.block}${createTail}` - : `请先解析这个抖音视频并做拆解分析,再为达人「${r.name}」创作一条全新故事脚本${r.url ? ':' + r.url : ''}\n视频标题:《${r.title}》${personaBlock}${noPersonaNote}`, + : `请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}\n该视频尚未解析,请先解析这个抖音视频并做拆解分析(${r.url ? '视频地址:' + r.url + '\n' : ''}视频标题:《${r.title}》),再基于账号设定与提炼出的选题,创作完整故事脚本。`, label: '创作指令', btn: b, });