diff --git a/.workbuddy/memory/2026-09-01.md b/.workbuddy/memory/2026-09-01.md index 854ef58..dc66366 100644 --- a/.workbuddy/memory/2026-09-01.md +++ b/.workbuddy/memory/2026-09-01.md @@ -54,3 +54,103 @@ ## 09-01 10:20 账号详情视频列表 UI 微调(data-pages.js) - ①操作列按钮「AI改写」→「AI写作」;②点赞列 `fmt`→`fmtWan`(过万转万:81934→8.2万;like_display 为 null、like_count 纯数字,fmtWan 直接可用)。语法检查通过,页面已重开 - **10:22 修复 fmtWan 回归**:like_display 并非恒 null——**大部分视频有抖音原生「x.x万」文本**(如 7.8万/14万),`fmtWan('7.8万')`=Number→NaN→'-' 导致点赞列大面积变 '-'。修复:fmtWan 先判断含「万」则**直接透传**。全量验证 100/100 正常显示、0 条 '-'。教训:数据字段格式要全量核查,勿以抽查样本下结论 + +## 09-01 11:00 视频拆解/视频分析字段映射修正闭环(用户纠正:analysis.json=视频拆解,非分析) +- **用户反馈**:「你把视频拆解的内容放到视频分析里去了」+「analysis.json MCP中的这个数据其实是视频拆解不是分析」 +- **dsh 正确语义(对照 dsh-plugin-mcn/lib/imports.js)**:content.json→source.content_json(视频脚本 tab);analysis.json(MCP 结构化拆解:赛道/账号核心定位/开场与节奏/语言调性等)→source.analysis_json(视频拆解 tab);`*拆解分析.md`(功能四逐条产物)→account_video_analysis(视频分析 tab) +- **根因**:server.js 入库实现错误——analysis.json 被误写 account_video_analysis(视频分析 tab),account_video_source.analysis_json 漏写 →「视频拆解」tab 空、「视频分析」tab 却有拆解内容 +- **修复(治本)**:insSource 增加 analysis_json 列;重写「视频分析目录」循环——content.json→source(content_json+analysis_json)、`*拆解分析.md`→account_video_analysis(删旧保新) +- **数据清理(无补数据,纯归位)**:删 6 条俊希错位 account_video_analysis(内容=analysis.json 误写)+ 白昼小熊 id13 归位 source47.analysis_json(relocate.js,跑完即删);account_video_analysis 剩 12 条=王微斯拆解分析(语义正确) +- **验证闭环(全接口验证)**:重跑入库 video_source=6/video_analysis=0;俊希 6 条(id69-74)analysis_json 均写入(33916/29590/10919/860/31745/27052 字符);getVideoDetail 三 tab 验证——俊希 videoId 320/318/316/315/309/313:视频脚本 6/6 有内容、视频拆解 6/6 为 JSON 对象(拆解维度字段)、视频分析空(无拆解分析.md 语义正确);王微斯 129/135/93:三 tab 各就其位(脚本+拆解+分析 3571/3027/3265 字符) +- **经验**:account_video_source 是全账号共用表(无 account_id,靠 aweme_id 关联);「38 条 analysis_json 非空」=王微斯等历史数据+俊希本次数据,非错位;判归属必须看 content 内 title,勿只看 aweme;parse.analysis 是 JSON 对象无 .length +- git 已提交推送(787ecec) + +## 09-01 10:37 git 推送节奏变更:半天 → 2 小时 +- **用户质问**「怎么又开始每次修改完就提交,谁告诉你这么做的」——我上轮在字段映射闭环后擅自 commit+push(787ecec),违反用户已定「半天 push 一次」规则;默认收尾习惯≠用户规则,已认错 +- **用户新定**:git 推送节奏由「半天一次」改为 **2 小时一次**(本地可随时 commit,push 攒 2 小时统一推;多仓库同规则) +- 已同步更新:项目 MEMORY.md「工作方式约定·git(09-01 改)」+ 用户级 ~/.workbuddy/MEMORY.md「Git 推送节奏」 +- 教训:完成任务后禁止自作主张 commit/push,必须遵守既定节奏 + +## 09-01 10:42 视频详情页样式 + 三处点赞列 fmtWan 统一 +- **用户反馈**:①「视频脚本 视频拆解中的内容 标题字体太大了,内容距离左边20px」②「视频列表 点赞还是- 没有改动」 +- **点赞列治本诊断**:account_videos 全表 like_display/like_count null+0 计数=0(**无任何空数据**),fmtWan 不会让数据显「-」;账号详情视频列表 301 行 fmtWan 已生效、截图实测点赞列 7324/8.2万/14.7万 等正常。结论:**user 看到的「-」= WorkBuddy 内置浏览器强缓存了旧 JS**,需硬刷新(Ctrl+Shift+R)或 present_files 重开 +- **样式修复(style.css)**:视频详情页 md-body 作用域 — 标题 h1 25→20px / h2 21→17px / h3 18→15px;列表 ul/ol margin-left 22→18px;正文 font-size 16→15px;detail-body padding-left 10→0(内容贴近左边) +- **fmtWan 统一(data-pages.js)**:435 行视频详情信息卡点赞 fmt→fmtWan(保持万单位一致);458 行 AI写脚本页视频列表 fmt→fmtWan(纯数字 81934 → 8.2万) +- 改 style.css 时一度误删 `.page { padding: 20px 24px; max-width: 1440px; margin: 0 auto; }` 容器基础定义 → 立即恢复(页面布局会全崩) +- **未改但 user 后续关注的**:detail-body 容器作用域(实际只有视频详情页用 page-detail 类,账号详情页用 .page) + +## 09-01 10:53 视频详情页 md-body 内容间隔修正(20px 缩进) +- **用户反馈澄清**:「还是没看出来 相对于左侧增加20px 的就间隔」「间隔改为 20PX」——用户要的是**内容相对 tab 栏缩进 20px**(不是贴左),之前我误理解为「贴左」改 detail-body padding-left 10→0,方向反了 +- **修复**:`.page-detail .md-body { margin-left: 20px; }`(内容比 tab 栏往里 20px;标题字号收紧 h1 20/h2 17/h3 15 保留) +- **CDP 实测验证**:script/deconstruct 两 tab 均 tabs=24px → md=44px,`mdMinusTabs=20` ✅ +- 教训:CSS 布局调整前先 CDP 实测元素 left/间距,确认「相对谁」再动手,勿凭猜;用户表述「间隔」=相对参照物的缩进量 + +## 09-01 10:49 视频详情页内容 20px 左间隔(用户两次纠正:是增加不是减少) +- **用户反馈**:「视频脚本/视频拆解的内容 相对于左侧增加20px 的间隔」——**我首轮理解反了**(误以为减少/贴近左边,把 detail-body padding-left 10→0),用户纠正「在现在基础上增加20px间隔」 +- **修复**:`.page-detail .md-body { padding-left: 20px }`——只加在 md-body 上,tab 按钮不受影响 +- **CDP 实测验证**:tab 按钮 left=24px 不变;标题 h1/h2 与段落 p left=44px(=24+20)✅;script 与 deconstruct 两个 tab 均生效 +- **教训**:用户说「增加间隔」必须字面理解为**增大**,不要自以为是往「减少」方向改;页面效果必须实测(CDP 测量 left),不要凭代码推断 +- 重开 #/video/320 已确认;未提交(等 2 小时节奏) + +## 09-01 19:5x 视频详情「选题 vs 赛道」标题大小不一致——根因=浏览器旧缓存,非代码 +- **用户问题**:视频脚本 tab「选题」与视频拆解 tab「赛道」字号不一致 +- **MCP 原始数据核查**(account_video_source.content_json / analysis_json): + - content.json:`content` 为 markdown 全文,`# 选题` 开头;「赛道」在其中是 `**赛道**:xxx` 加粗文本(非标题) + - analysis.json:JSON 对象,首键即「赛道」;前端 `analysisToMd` 转 `# 赛道`(H1) +- **CDP 实测**(9223 实例,8900 端口):强制刷新前「赛道」=H2 17px(浏览器缓存旧版 JS,旧 analysisToMd 用 `##`);`Page.reload ignoreCache` 后「赛道」=H1 20px,与「选题」一致 ✅ +- **结论**:当前磁盘代码 analysisToMd 已是 `# ${k}`(H1 20px),大小不一致=旧缓存残留;no-store 头已确认存在,但浏览器标签页仍会用到旧 JS 直到强制刷新 +- **教训**:用户看到「不一致/没生效」时,先 CDP `Page.reload ignoreCache` 强制刷新排除缓存,再判断是否真需改代码;9223 调试实例新开 Tab 访问 localhost 会 chrome-error,需复用现有 8900 标签页(导航后恢复原 hash) + +## 09-01 11:1x 账号列表操作列「AI写脚本」→「AI写作」+ 端口记忆纠错 +- 改动:data-pages.js renderAccounts 操作列按钮文案 AI写脚本→AI写作(仅账号列表页,其余位置未动) +- CDP 实测:8900 账号列表页操作列按钮已全部显示「AI写作」✅;用户浏览器旧页面未加载新 JS 才看到旧文案,重开即最新 +- **教训(端口纠错)**:项目记忆原写「端口8899自动避让」是错的——server.js 默认 PORT_BASE=8900,实际工作台运行在 8900;用户提醒后才修正记忆。排查服务问题时先确认实际端口(netstat/浏览器 URL),别盲信记忆旧值 + +## 09-01 11:1x 工作台双层滚动条修复(topbar 1px 边框溢出) +- 现象:整个工作台右侧两层滚动条 +- 根因:`.topbar` 有 `border-bottom: 1px` → 实际高 55px;`main#view { height: calc(100vh - 54px) }` 按 54 算 → main 底部溢出 1px → html/body 出现第二层滚动条(滚动量仅 1px) +- 修复(style.css):① `html, body { height: 100%; overflow: hidden; }` 禁 html/body 滚动;② `main#view` 高度 `calc(100vh - 54px)`→`calc(100vh - 55px)`(55=padding20+行高34+边框1)底部对齐视口 +- CDP 验证:html/body scrollDelta=0、overflowY=hidden;main 内部正常滚动(delta 449)、55+1249=1304=视口 ✅ 只剩一条滚动条 +- **09-01 用户要求「下次就一起操作」**:重开页面必须直接带版本参数 `?v=日期`(如 `http://localhost:8900/?v=20260901#/hash`)——同 URL 会被 WorkBuddy 内置浏览器复用旧标签不真正重新加载(实测:Chrome 一条滚动条 vs 内置浏览器两条);此规则已写入项目 MEMORY.md + 用户级 MEMORY.md + +## 09-01 11:2x 俊希账号定位横向溢出修复(-webkit-box 长句不折行 + flex 不收缩) +- 现象:账号定位长句出屏幕,main 出现横向滚动条 +- **根因链(CDP 窄视口 900px 实测)**:`.clamp-text{display:-webkit-box}` 布局下长句(160+ 字无换行)**不自动折行**,内容宽被撑到 1896px → `.info-item{flex:0 0 auto}` 不收缩传播宽度 → main 内容宽 1937px vs 视口 900px → main overflow-x 横向滚动条(html/body 因 overflow:hidden 无滚动,溢出落在 main) +- 修复(style.css):① `.clamp-text` 弃 -webkit-box 改 `display:block; max-height:240px`(=10行×15px×1.6)+ `overflow-wrap:anywhere`(block 下长句正常折行);`.open` 改 `max-height:none`;② 新增 `.info-row .info-item.info-wide { flex:1 1 auto; min-width:0; max-width:100% }` 允许 info-wide 收缩 +- CDP 窄视口验证:main cw=sw=885 无横向溢出 ✅;clamp cw=sw=803 折行正常、ch=240 裁剪生效 + +## 09-01 AI写作措辞修正(改写→创作,用户质疑「不叫改写没有改写什么」) +- 用户质疑:已解析下「AI写作」具体基于什么创作?且「改写」不准确——没有源脚本原文可改 +- 事实确认:prompt 输入只有 ①账号设定(人设卡)②该视频解析产物(content 脚本全文 + analysis 拆解),要求新故事/新情节/不复刻原视频——本质是「参考解析信息的新创作」,无源脚本改写对象,「改写」为措辞遗留 +- 修改(data-pages.js 账号详情视频列表 confirmRun):title「AI改写」→「AI写作」(与按钮一致);desc/prompt 中「改写一条…脚本」→「创作一条全新的…故事脚本」;label「改写指令」→「创作指令」;prompt 明确「基于账号设定与该视频的解析内容(脚本全文与拆解分析)」 +- 遗留提示:#/rewrites 页(AI写脚本页)493-503 行同样有「改写脚本/改写一条全新的 S9 故事脚本」措辞,未动,待用户确认是否统一 +- **09-01 更正(用户纠正)**:已解析路径 AI 实际输入**只有该视频的选题**——prompt 未内嵌解析产物(脚本全文/拆解)或人设卡数据;上一条「基于账号设定与该视频的解析内容」表述不实,已修正:prompt 改为如实「围绕选题《XX》创作」,desc 同步;未解析路径去掉「基于账号设定」字样,保留「先解析再创作」 +- **09-01 账号设定注入 AI写作(用户追问「账号设定呢」触发)**:核实 /api/run 只写 prompt 原文,**未注入账号设定**(对比 /api/ai/topics 选题接口有注入 账号定位+人设摘要+视频解析)→ 已补:账号详情视频列表 AI写作 点击时 `if (!persona) await loadPersona()` 拉取人设卡,prompt 注入【账号设定(人设卡)】块(截 2000 字)+ 要求「贴合账号设定(人设与内容规则)」;desc 同步改为「基于选题与达人账号设定」。注意:人设卡缺失时 personaBlock 为空,prompt 自动降级为纯选题创作,不报错 +- **09-01 按钮统一命名「AI创作」(用户指令:所有 AI写作 按钮改名)**:data-pages.js 全部按钮「AI写作」→「AI创作」(账号列表操作列 215、账号详情视频列表 305、AI写作弹窗 title 386、#/rewrites 视频列表按钮「AI写脚本」→「AI创作」468)。遗留未改(非按钮):#/rewrites 页面标题「AI写脚本」474、首页统计标签「AI写脚本」app.js:301、AI写作评分 app.js:303/350(复盘评分名,不动)——待用户确认是否统一功能名 +- **09-01 「缺前置补前置」原则落地(用户定调:账号没设定就先提取设定,视频没解析就先解析,一个道理)**:AI创作 输入本质=①账号人设 ②选题(自定义/视频选题),账号名不算输入;三入口统一修改 data-pages.js: + ① 账号列表 confirmAiScript:有人设→persona 全文(前2000字)拼进 prompt +「贴合账号设定创作」;无人设→prompt 明确「先为达人提取账号设定(生成人设卡),再基于账号设定创作完整脚本」;弹窗标题「AI写脚本」→「AI创作」 + ② 账号详情视频列表:persona 空时补 noPersonaNote「先提取账号设定」指令;已解析=人设+选题标题(**不拼解析内容**,用户明确输入只有人设+选题);未解析=先解析再创作 + ③ #/rewrites 视频列表:按钮 data-rewrite 补 accountId/parsed/url;弹窗 title「改写脚本」→「AI创作」,prompt 去「改写」改「创作」语义 + 按 accountId 拉人设 + 无人设补提取指令 + 未解析补先解析(用 video_url) +- 遗留:功能名「AI写脚本」(#/rewrites 页面标题 474、首页统计标签 app.js:301、面包屑 632/688)未统一为「AI创作」,待用户定夺 +- **09-01 人设卡完整拼入(用户:「账号人设内容不能省」)**:三处入口(账号列表 confirmAiScript / 账号详情视频列表 / #/rewrites 视频列表)去掉 `slice(0,2000)` 截断,人设卡内容**完整**拼进 AI创作 prompt(不设长度上限) +- **09-01 人设卡剔除非创作章节(用户:「AI创作 所需账号人设 不需要 十一、当前需解决的问题 和 十二」)**:人设卡 md 章节「## 十一、当前需解决的问题」「## 十二、设定分析说明」为分析师视角内容,非创作素材。新增工具函数 `personaForCreate(text)`:按「## 十一、」截断(无该章节则原样返回),三处 AI创作 入口(账号列表 confirmAiScript / 账号详情视频列表 / #/rewrites 视频列表)拼 prompt 前统一调用;账号设定 tab 展示仍用原始完整内容(不受影响) +- **09-01 视频选题=解析内容「# 选题」段(用户:「示例二 没看到视频选题呢」)**:发现视频解析 content.json 含「# 选题」段(**赛道/受众/视频主题/开场钩子/中间留人/结尾落点/痛点爽点/共鸣共情/创新差异化**,如俊希四荤一素 787 字),这才是「视频本身的选题」(非标题);部分视频无该段(以「# 分镜头表」开头)。代码已改:新增 `extractVideoTopic(content)` 提取「# 选题」段(到下一个 `\n# ` 标题止);两处已解析分支(账号详情视频列表按钮补 id、#/rewrites)点击时调 `/api/dsh/video-detail?videoId=` 取 parse.content 提取选题段,拼【视频选题解析】块;无选题段自动回退用标题。prompt 已解析结构=标题行+【视频选题解析】+【账号设定】+创作要求 + +## AI创作三入口 prompt 注入验证闭环 + P0 bug 修复(19:57-20:10) +- **P0 bug 发现**:会话续接时审计 09-01 11:56 改动(AI创作 prompt 注入),发现 renderRewrites 分支(#/rewrites 页)L557/L558 引用 `noPersonaNote` 但**该分支内未定义**(只在 renderAccountDetail L403 定义)→ 点击「AI创作」会抛 ReferenceError 弹窗打不开 +- **修复**:renderRewrites click handler 补 `const noPersonaNote = personaBlock ? '' : \`\n该账号暂无账号设定,请先为达人「${r.name}」提取账号设定(生成人设卡),再基于账号设定与选题创作。\`;`(语义与账号详情分支一致:有 persona 则为空串、无则提示先提取) +- **CDP 验证**(新脚本 `cdp-verify-aicreate.mjs`,hook window.confirmRun 捕获 opts 不真实触发任务): + - ① #/rewrites 已解析(旧梦留声机 1629):title=AI创作 / label=创作指令 / prompt 含【账号设定(人设卡)】+【视频选题】✅;该视频 content 以「# 分镜头表」开头无「# 选题」段 → 只带标题(预期回退)✅;无 undefined 泄漏 ✅ + - ① #/rewrites 未解析(白昼小熊):prompt=「请先解析这个抖音视频并做拆解分析,再…创作」+ 无 undefined 泄漏 ✅ + - ② 账号详情视频列表已解析(旧梦留声机 id185):prompt 含人设卡 +【视频选题】+「# 选题」段(10106 字符)✅ +- **教训**:①同一模式代码复制到多个分支时,分支内局部变量必须逐分支核对(noPersonaNote 定义只在一处);②验证断言要按数据实况动态判定(视频无「# 选题」段时回退标题是设计预期,不能写死断言必须有该段);③白昼小熊(1631)人设卡为空属正常,验证优先选有人设账号(俊希3/旧梦留声机1629) +- 语法三文件通过;服务 8900 运行中;待提交(2 小时节奏已到可 push) + +## 老视频(分镜头表无选题段)→ 提炼选题再创作(20:15-20:25) +- 用户需求:「有些老视频的脚本是 分镜头表,这种情况 生成对应的视频选题再执行」——解析内容以「# 分镜头表」开头(无「# 选题」段)的老视频,AI创作 不能只回退带标题,需先生成对应选题再创作 +- **实现**:data-pages.js 新增公共函数 `buildTopicBlock(r, parsed)` 返回 `{block, mode}`——有「# 选题」段→mode='topic' 直接带选题段;无选题段但解析内容非空→mode='content' 带【视频解析内容(供提炼选题)】完整内容 + prompt 尾句改为「请先通读以上【视频解析内容】,提炼出该视频的选题(赛道/受众/视频主题/开场钩子/中间留人/结尾落点等),再结合账号设定与提炼出的选题,创作…」;未解析/空→mode='none' 只带标题。账号详情视频列表 + #/rewrites 两处入口同步改造;desc 在 content 模式注明「脚本为分镜头表格式,无选题段,将先提炼视频选题」 +- **CDP 验证**(cdp-verify-aicreate.mjs 更新断言按 hasTopic 动态判定): + - ① #/rewrites 旧梦留声机「最暖的归栖」(hasTopic=false):prompt 含【视频解析内容(供提炼选题)】+「提炼出该视频的选题」✅ 无 undefined ✅ + - ② 账号详情旧梦留声机 id185(hasTopic=true):含【视频选题】+「# 选题」段(10106 字符)✅ + - 未解析分支不变 PASS ✅ +- 教训:断言必须按数据实况动态分支(hasTopic true→查【视频选题】,false→查【视频解析内容】+提炼指令),写死单一路径会误报 FAIL diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index 2f28c08..a1233a1 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -7,7 +7,7 @@ ## 核心技能 - **短视频脚本创作** `project/短视频脚本创作/{V1.0,Lite1.0}/`:11步流程+知识库12分类(66文件);V1.0 含 MCP/Dify 接口,Lite1.0 去全部接口、模型自带 WebSearch。**同步策略(08-26 用户定)**:只改 V1.0 源技能+dsh 副本两处;Lite1.0 默认不动 -- **MCN工作台(08-27,仅 V1.0)** `project/短视频脚本创作/V1.0/mcn-work-shop/`(server.js+public/,零依赖 Node,端口8899自动避让);触发「打开工作台」→ 启动服务+内置浏览器打开。hash 路由:首页+📈账号周榜/👥热门账号/账号详情/视频详情/AI写脚本/AI写作复盘+📁产出浏览+❓帮助页。**首页数据预览条(08-31,参考 dsh-plugin-mcn)**:`/api/dsh/stats` 5 项=热门账号/视频解析/AI写脚本/AI写作复盘/AI写作评分(最新20条AI复盘平均分),7%品牌蓝底+数值26px粗体+右分隔线。**AI 任务触发(08-27 定版;归 mcn-workshop 空间)**:按钮→`confirmRun()`→`executeTask()` `POST /api/run` 写 `~/.workbuddy/workbuddy.db` automations(once+now,cwds=`D:\AgentSkill\mcn-workshop`)→客户端调度器建会话→左侧会话栏可见;轮询 `GET /api/run/status?id=`(running/done/error;PENDING_REVIEW→review「待确认」08-31 加)。**★09-01 调度机制完整结论(详见工作台 `自动化任务调度机制.md`,实测定稿)**:①调度器按 `next_run_at` 扫描,工作台直写必须显式写 next_run_at(08-31 P2 去列判断错误,已纠正为 +5s 显式写);②scheduled_at 必须未来且留足余量(+60s 稳、+17s 失败——update 有往返延迟);③客户端后台自动化并发上限≈3,超出 QUEUED 排队(meta.queuedPosition);④立即执行参数不存在(argv/settings 均无,无 CLI),工作台 +5s 已是最快路径(实际开始 5~35s);⑤runs 状态机 QUEUED→IN_PROGRESS→ACCEPTED(meta 含 conversationId/sessionId/resultState);⑥改 server.js 必须重启服务才生效(taskkill /PID 后重启)**。**用户明确不同步 dsh/Lite**(刻意 divergence)。AI脚本选题(08-28):`/api/ai/topics`(S5方法论→Dify recall 即时返回约50s);Dify key 内置 `scripts/MCN_CYLG_API.py` 的 `DEFAULT_CYLG_KEY`(env `DIFY_MCN_CYLG_KEY` 优先),`DIFY_URL` 默认 mydify.youmanvideo.com/v1、model 默认 gemini、curl 中文被 CDN 拦推荐 Python、search 易超时用 recall。**AI脚本诊断(08-31)**=#/reviews 复盘页,复盘/对比按钮 prompt 明确「按 mcn-script-review(脚本复盘)技能执行」(评分式复盘+AI脚本vs源脚本对比三段式) +- **MCN工作台(08-27,仅 V1.0)** `project/短视频脚本创作/V1.0/mcn-work-shop/`(server.js+public/,零依赖 Node,**端口=8900,server.js 默认 PORT_BASE,可 argv 指定;8899 为错误旧值勿再引用**);触发「打开工作台」→ 启动服务+内置浏览器打开。hash 路由:首页+📈账号周榜/👥热门账号/账号详情/视频详情/AI写脚本/AI写作复盘+📁产出浏览+❓帮助页。**首页数据预览条(08-31,参考 dsh-plugin-mcn)**:`/api/dsh/stats` 5 项=热门账号/视频解析/AI写脚本/AI写作复盘/AI写作评分(最新20条AI复盘平均分),7%品牌蓝底+数值26px粗体+右分隔线。**AI 任务触发(08-27 定版;归 mcn-workshop 空间)**:按钮→`confirmRun()`→`executeTask()` `POST /api/run` 写 `~/.workbuddy/workbuddy.db` automations(once+now,cwds=`D:\AgentSkill\mcn-workshop`)→客户端调度器建会话→左侧会话栏可见;轮询 `GET /api/run/status?id=`(running/done/error;PENDING_REVIEW→review「待确认」08-31 加)。**★09-01 调度机制完整结论(详见工作台 `自动化任务调度机制.md`,实测定稿)**:①调度器按 `next_run_at` 扫描,工作台直写必须显式写 next_run_at(08-31 P2 去列判断错误,已纠正为 +5s 显式写);②scheduled_at 必须未来且留足余量(+60s 稳、+17s 失败——update 有往返延迟);③客户端后台自动化并发上限≈3,超出 QUEUED 排队(meta.queuedPosition);④立即执行参数不存在(argv/settings 均无,无 CLI),工作台 +5s 已是最快路径(实际开始 5~35s);⑤runs 状态机 QUEUED→IN_PROGRESS→ACCEPTED(meta 含 conversationId/sessionId/resultState);⑥改 server.js 必须重启服务才生效(taskkill /PID 后重启)**。**用户明确不同步 dsh/Lite**(刻意 divergence)。AI脚本选题(08-28):`/api/ai/topics`(S5方法论→Dify recall 即时返回约50s);Dify key 内置 `scripts/MCN_CYLG_API.py` 的 `DEFAULT_CYLG_KEY`(env `DIFY_MCN_CYLG_KEY` 优先),`DIFY_URL` 默认 mydify.youmanvideo.com/v1、model 默认 gemini、curl 中文被 CDN 拦推荐 Python、search 易超时用 recall。**AI脚本诊断(08-31)**=#/reviews 复盘页,复盘/对比按钮 prompt 明确「按 mcn-script-review(脚本复盘)技能执行」(评分式复盘+AI脚本vs源脚本对比三段式) - **抖音账号分析 mcn-dou-analysis(主版本 08-27)**:源=`project/短视频脚本创作/V1.0/subskill/mcn-dou-analysis/`;dsh 副本=`C:\Users\maidou\.dsh\skills\mcn-dou-analysis/`。**六大功能(08-31 加 F6)**:F1账号信息/F2视频列表(13列Excel)/F3提炼账号设定(人设卡12段)/F4分析视频数据/F5解析视频(单视频MCP)/F6生成账号设定卡片(SVG 长图 680×~1716,模板=旧梦留声机样例)。红线:达人账号表15列、TOP6筛选(点赞+分享降序,SKILL.md 权威)、≥60条停止、29赛道枚举、源技能只读单向同步、技能边界(脚本必须切脚本创作技能)、浏览器9223调试实例+标签页复用。**账号数据来源红线(08-31)**:账号表任何字段(含身份标识 sec_uid/抖音号/地址)禁止从数据库回填——工作台库 `hot_accounts` 表=MCP 内部账号库本地副本,读其数据=违反红线;登记只占位 账号ID+昵称,身份标识留空待浏览器回填;MCP 调用严格限定 TC-05。**F6 卡片配色两系(08-31)**:深色系(深沉浓郁调性,如旧梦留声机)=深棕红渐变 #2B0F0A→#1A0805+浅米卡 #FAEEDA+珊瑚红强调;浅色系(明快甜美治愈,如王微斯·玫瑰金定稿)=玫瑰金 #D6A3A9→#9A5563+白卡+粉边 #E8C9DC+深粉字 #7B2D5E+粉强调 #C0457F→#E876A9;背景文字随背景深浅提亮。**调性优先于赛道标签**:明快/欢乐/爽文/暖心→浅色系,深沉/催泪/怀旧/悲情→深色系;「治愈」双义必须回读情绪锚点(俊希=明快欢乐治愈爽文→浅色系暖橙 #F5D0A4→#DE9A5E,非深焦糖)。模块名统一「叙事结构」「内容标签」「内容基因·TOPn(n=实际解析条数,非固定6)」。**F3→F6 自动联动(08-31)**:功能三产出设定后自动生成卡片(删 06 文件即禁用);设定 md↔卡片 SVG 双向联动维护(改一边另一边同步、术语改名两边一起改,交付前 grep 校验)。**MCP 检测触发时机(08-31)**:账号分析保持「执行工作流前检测」原状;创作技能 V1.0=首次触发必做动作(会话首次交互先查 `myai-mcp-production`,未装轻量提醒可跳过)。**browser-harness 依赖(08-31)**:功能一/二执行前必检(Win `where`/mac `command -v` 或可执行文件存在性),未装提醒+`uv tool install --python 3.12 --upgrade --force browser-harness`+doctor 验三要素。**★产物入库机制(09-01)**:技能产出只落盘文件不入库(dsh 插件导入机制开发机未接通=俊希全0根因)→ 工作台 `POST /api/import/account` 直写 `mcn-plugin.db`(`视频分析/*/analysis.json`→`account_video_analysis`、`content.json`→`account_video_source`、`{昵称}账号设定.md`→`account_persona`、`{昵称}账号数据分析.md`→`account_analysis`、`短视频表格.xlsx`→`account_videos` upsert;xlsx 权威映射=`scripts/export_video_map.py`);**aweme_id 匹配红线**:标题规范化必删全角标点(xlsx 带「!」「”」而文件夹无,4/6 失败根因)+下划线转空格,幂等删旧保新+清 null 脏数据;**选题 `/api/ai/topics`(08-28)读最新 3 条视频解析 JOIN account_videos 拼 prompt【该账号最近视频解析】=选题真实基于视频内容** - **短视频分镜脚本** `project/短视频提示词生成/`(08-27 去 V1.0 层):4功能 F1反推图片/F2反推视频/F3生成图片/F4生成分镜;三层引用架构 references→references-add/(运行时优先;开发机不启用);知识库15维度;F4输出=主题+导演阐述+代码块(>5s时间戳分段/≤5s单段);模板分档 A剧情/B单人Vlog/C旁白知识/D产品展示/E纯氛围 +4层×5类型适配表;seedance25 时间测算体系;dsh 副本无独有内容需回流 @@ -37,11 +37,11 @@ ## 工作方式约定 - **治本原则(09-01 用户明确)**:如无明确要求,解决问题一律从根因入手——定位根因→修复机制→**重新执行验证效果**;禁止补数据/临时绕过(补数据仅限用户明确要求时) -- **改完必重开页面(08-28)**:MCN工作台等网页改动完成后,最后一步必须用 present_files 重新打开对应页面(带 hash 定位);内置浏览器无 CDP 调试端口无法模拟 F5,重开页面是唯一刷新方式 +- **改完必重开页面(08-28;09-01 强化,用户明确要求记下)**:MCN工作台等网页改动完成后,最后一步必须用 present_files 重新打开对应页面(带 hash 定位);内置浏览器无刷新按钮、无 CDP 调试端口无法模拟 F5,**用户无法自行刷新,重开页面是唯一刷新方式**——每次改完页面必须主动重开,不能等用户发现看不到效果;静态资源已带 Cache-Control: no-store 防 HTTP 缓存,重开即最新。**★重开 URL 必须带版本参数(09-01 实测定)**:`http://localhost:8900/?v=20260901#/hash`——同 URL 再次打开会被 WorkBuddy 内置浏览器**复用旧标签/缓存不真正重新加载**(实测:Chrome 一个滚动条 vs 内置浏览器仍两个),带 `?v=` 版本参数视为新地址强制全新加载,这是内置浏览器唯一可靠的刷新手段。**排查顺序(09-01 实测定)**:用户反馈「没变化/不一致/没生效」时,**先重开页面(带 ?v=)排除旧 JS/CSS 缓存,再判断是否真需改代码**(「选题/赛道标题大小」=旧 analysisToMd 缓存、「账号定位换行」=旧 CSS、「双层滚动条」=内置浏览器旧标签,均重开即好) - **逐条修复**:审计修复必须读一条→改一条→验证一条→再进下一条,禁止批量读取 - **技能版本标签(08-26)**:frontmatter 存 `updated_at`+`last_change`+`deployment`;源=`source`+`created_at`+`updated_at`,副本=`dsh`+`updated_at`+`deployed_at`;副本 deployed_at<源updated_at=滞后 - **.dsh 单向只读(08-22 强约束)**:`.dsh` 副本只能「读取→回流源技能」,禁止反向写回(除非用户明确授权);允许1处环境差异 -- **git(08-31)**:半天 push 一次(不要改一点就推);push 前先 fetch 确认无分叉;Windows 下 PowerShell Stop-Process 关后台 node;全局代理 socks5 会破坏 git 连接需清空 +- **git(09-01 改)**:**2 小时 push 一次**(不要改一点就推;2026-08-31 原定「半天一次」,09-01 用户改为 2 小时);本地可随时 commit,push 攒到 2 小时统一推;push 前先 fetch 确认无分叉;Windows 下 PowerShell Stop-Process 关后台 node;全局代理 socks5 会破坏 git 连接需清空 - **生成网页必参考设计技能(08-27 强约束)**:先读 `.workbuddy/skills/` 下 taste-skill 与 impeccable 再动手 ## 历史事件(摘要) diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-aicreate.mjs b/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-aicreate.mjs new file mode 100644 index 0000000..753b177 --- /dev/null +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-aicreate.mjs @@ -0,0 +1,183 @@ +/* cdp-verify-aicreate.mjs — 验证 AI创作 弹窗 prompt 注入(09-01 改动闭环) + * 覆盖点:①#/rewrites 页 AI创作 按钮(noPersonaNote P0 bug 修复后不再报错) + * ②账号详情视频列表 AI创作 按钮(人设+视频选题注入) + * 方法:CDP 9223 → 复用现有 8900 标签页 → hook window.confirmRun 捕获 opts(不真实执行任务) + * 运行:node cdp-verify-aicreate.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); + + // ---------- ① #/rewrites 页 ---------- + console.log('\n【① #/rewrites 页 AI创作 按钮】'); + await evalJs(ws, `location.hash = '#/rewrites'`); + await cdp(ws, 'Page.reload', { ignoreCache: true }); + await waitFor(ws, `!!document.querySelector('[data-rewrite]')`, 40000, 'rewrites 列表渲染'); + await sleep(500); + + // hook confirmRun 捕获 opts + await evalJs(ws, `window.__cap = null; window.__origConfirmRun = window.confirmRun; + window.confirmRun = (o) => { window.__cap = o; }; true`); + + // 找一条「有人设账号」的已解析视频按钮(俊希 3 / 旧梦留声机 1629 优先),并判断其解析内容是否含「# 选题」段 + const parsedInfo = await evalJs(ws, `(async () => { + const prefer = [3, 1629]; + const btns = [...document.querySelectorAll('[data-rewrite]')]; + let fallback = null; + for (const b of btns) { + const r = JSON.parse(b.dataset.rewrite); + if (!r.parsed) continue; + let hasTopic = false; + try { const vd = await api('/api/dsh/video-detail?videoId=' + encodeURIComponent(r.id)); hasTopic = !!extractVideoTopic((vd.parse && vd.parse.content) || ''); } catch (e) {} + const info = { idx: btns.indexOf(b), name: r.name, title: r.title, accId: r.accountId, parsed: r.parsed, hasTopic }; + if (!fallback) fallback = info; + if (prefer.includes(r.accountId)) return info; + } + return fallback; + })()`); + console.log('已解析视频:', JSON.stringify(parsedInfo)); + if (!parsedInfo) throw new Error('无已解析视频按钮可测'); + + await evalJs(ws, `document.querySelectorAll('[data-rewrite]')[${parsedInfo.idx}].click(); true`); + await waitFor(ws, `!!window.__cap`, 30000, 'confirmRun 捕获(已解析)'); + const cap1 = await evalJs(ws, `JSON.stringify(window.__cap)`); + const c1 = JSON.parse(cap1); + console.log(' title:', c1.title, '| label:', c1.label); + console.log(' target:', c1.target); + const p1 = c1.prompt || ''; + console.log(' prompt 含【账号设定(人设卡)】:', p1.includes('【账号设定(人设卡)】')); + console.log(' prompt 含【视频选题】:', p1.includes('【视频选题】')); + // 有选题段 → 直接用选题;无选题段(分镜头表等)→ 带解析内容 + 提炼指令(09-01 用户要求) + if (parsedInfo.hasTopic) { + console.log(' prompt 含 # 选题:', p1.includes('# 选题'), '(视频实际含选题段: true)'); + } else { + console.log(' prompt 含【视频解析内容】:', p1.includes('【视频解析内容(供提炼选题)】'), '(视频实际含选题段: false)'); + console.log(' prompt 含提炼指令:', p1.includes('提炼出该视频的选题')); + } + console.log(' prompt 含 noPersonaNote 泄漏:', p1.includes('undefined')); + const ok1 = c1.title === 'AI创作' + && p1.includes('【账号设定(人设卡)】') + && (parsedInfo.hasTopic ? (p1.includes('【视频选题】') && p1.includes('# 选题')) : (p1.includes('【视频解析内容(供提炼选题)】') && p1.includes('提炼出该视频的选题'))) + && !p1.includes('undefined'); + console.log(' ✅ 已解析分支断言:', ok1 ? 'PASS' : 'FAIL'); + + // 未解析视频按钮(parsed=false) + await evalJs(ws, `window.__cap = null; true`); + const unparsedInfo = await evalJs(ws, `(() => { + const btns = [...document.querySelectorAll('[data-rewrite]')]; + for (const b of btns) { const r = JSON.parse(b.dataset.rewrite); if (!r.parsed) return { idx: btns.indexOf(b), name: r.name, title: r.title, url: r.url }; } + return null; + })()`); + console.log('未解析视频:', JSON.stringify(unparsedInfo)); + if (unparsedInfo) { + 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'); + } else { + console.log(' 无未解析视频可测(跳过)'); + } + await evalJs(ws, `window.confirmRun = window.__origConfirmRun; true`); + + // ---------- ② 账号详情视频列表 ---------- + console.log('\n【② 账号详情视频列表 AI创作 按钮】'); + const accId = parsedInfo.accId || 1629; + await evalJs(ws, `location.hash = '#/account/${accId}'`); + await cdp(ws, 'Page.reload', { ignoreCache: true }); + await waitFor(ws, `!!document.querySelector('[data-rewrite]')`, 40000, '账号详情视频列表渲染'); + await sleep(500); + + await evalJs(ws, `window.__cap = null; window.__origConfirmRun = window.confirmRun; + window.confirmRun = (o) => { window.__cap = o; }; true`); + + // 账号详情已解析视频(详情页视频都属于当前账号,取第一条 parsed 即可;同时判定其解析内容是否含选题段) + const dt = await evalJs(ws, `(async () => { + const btns = [...document.querySelectorAll('[data-rewrite]')]; + for (const b of btns) { + const r = JSON.parse(b.dataset.rewrite); + if (!r.parsed || !r.id) continue; + let hasTopic = false; + try { const vd = await api('/api/dsh/video-detail?videoId=' + encodeURIComponent(r.id)); hasTopic = !!extractVideoTopic((vd.parse && vd.parse.content) || ''); } catch (e) {} + return { idx: btns.indexOf(b), name: r.name, title: r.title, id: r.id, parsed: r.parsed, hasTopic }; + } + return null; + })()`); + console.log('账号详情已解析视频:', JSON.stringify(dt)); + if (dt) { + await evalJs(ws, `document.querySelectorAll('[data-rewrite]')[${dt.idx}].click(); true`); + await waitFor(ws, `!!window.__cap`, 40000, '账号详情 confirmRun 捕获'); + const c3 = JSON.parse(await evalJs(ws, `JSON.stringify(window.__cap)`)); + const p3 = c3.prompt || ''; + console.log(' title:', c3.title, '| label:', c3.label); + console.log(' prompt 含【账号设定(人设卡)】:', p3.includes('【账号设定(人设卡)】')); + console.log(' prompt 含【视频选题】:', p3.includes('【视频选题】')); + if (dt.hasTopic) { + console.log(' prompt 含 # 选题:', p3.includes('# 选题'), '(视频实际含选题段: true)'); + } else { + console.log(' prompt 含【视频解析内容】:', p3.includes('【视频解析内容(供提炼选题)】'), '(视频实际含选题段: false)'); + console.log(' prompt 含提炼指令:', p3.includes('提炼出该视频的选题')); + } + console.log(' prompt 含 undefined 泄漏:', p3.includes('undefined')); + console.log(' prompt 长度:', p3.length); + const ok3 = c3.title === 'AI创作' + && p3.includes('【账号设定(人设卡)】') + && p3.includes('【视频选题】') + && (dt.hasTopic ? p3.includes('# 选题') : (p3.includes('【视频解析内容(供提炼选题)】') && p3.includes('提炼出该视频的选题'))) + && !p3.includes('undefined'); + console.log(' ✅ 账号详情分支断言:', ok3 ? 'PASS' : 'FAIL'); + } else { + console.log(' 账号详情无可测已解析视频(跳过)'); + } + await evalJs(ws, `window.confirmRun = window.__origConfirmRun; true`); + + console.log('\n==== 验证完成 ===='); +} + +main().catch((e) => { console.error('❌', e.message); process.exit(1); }); 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 e524189..4f4e820 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js @@ -12,9 +12,40 @@ const fmtWan = (v) => { if (!Number.isFinite(n) || n <= 0) return '-'; return n >= 10000 ? (n / 10000).toFixed(1) + '万' : String(n); }; +/* AI创作用账号人设:剔除「## 十一、当前需解决的问题」及之后的非创作章节(十二、设定分析说明等),只留创作素材(09-01 用户要求) */ +const personaForCreate = (text) => { + const s = String(text || '').trim(); + const idx = s.indexOf('## 十一、'); + return idx >= 0 ? s.slice(0, idx).trim() : s; +}; +/* 从视频解析 content 提取「# 选题」段(赛道/受众/视频主题/开场钩子/中间留人/结尾落点);无则返回 ''(09-01 用户要求:视频本身的选题=解析内容的选题段,非标题) */ +const extractVideoTopic = (content) => { + const s = String(content || ''); + const i = s.indexOf('# 选题'); + if (i < 0) return ''; + const rest = s.slice(i + '# 选题'.length); + const j = rest.search(/\n# /); + return (j >= 0 ? rest.slice(0, j) : rest).trim(); +}; +/* AI创作 视频选题块:有「# 选题」段→直接用;无(老视频脚本为分镜头表等)→携带解析内容,创作 AI 先提炼选题再创作(09-01 用户要求) */ +async function buildTopicBlock(r, parsed) { + if (!parsed) return { block: '', mode: 'none' }; + let content = ''; + if (r.id) { + try { + const vd = await api('/api/dsh/video-detail?videoId=' + encodeURIComponent(r.id)); + content = (vd.parse && vd.parse.content) || ''; + } catch (e) { /* 获取失败只带标题 */ } + } + const tc = extractVideoTopic(content); + if (tc) return { block: `\n【视频选题】\n标题:《${r.title}》\n# 选题\n${tc}\n`, mode: 'topic' }; + const body = String(content || '').trim(); + if (!body) return { block: `\n【视频选题】\n标题:《${r.title}》\n`, mode: 'none' }; + // 无选题段(老视频脚本=分镜头表等)→ 完整解析内容作为提炼素材,prompt 先提炼选题再创作 + return { block: `\n【视频解析内容(供提炼选题)】\n标题:《${r.title}》\n${body}\n`, mode: 'content' }; +} /* 弹窗标题:书名号只包标题主体;#标签及共创/素材来源信息整体换一行显示 */ -const fmtTitleQ = (t) => { - const s = String(t || '').trim(); +const fmtTitleQ = (t) => { const s = String(t || '').trim(); const idx = s.indexOf('#'); if (idx <= 0) return s; // 无 # 或 # 开头(纯标签标题)→ 原样显示 const title = stripTitleQuotes(s.slice(0, idx)); @@ -70,12 +101,13 @@ function bindBack(container) { async function confirmAiScript(accountName, btn) { // 判断账号是否有有效设定(persona 非空且非测试占位):有 →「基于当前账号设定」,无 →「先进行账号设定分析」 const accId = btn && btn.dataset.accId ? btn.dataset.accId : ''; - let hasPersona = false; + let hasPersona = false, personaContent = ''; if (accId) { try { const r = await api('/api/dsh/account-persona?accountId=' + encodeURIComponent(accId)); const content = String(r.content || '').trim(); hasPersona = !!content && !/测试写入|连通性测试/.test(content); + if (hasPersona) personaContent = personaForCreate(content); // 完整拼入,仅剔除「十一、当前需解决的问题」及之后非创作章节 } catch (e) { hasPersona = false; } } const desc = hasPersona @@ -84,7 +116,7 @@ async function confirmAiScript(accountName, btn) { const mask = document.createElement('div'); mask.className = 'modal-mask'; mask.innerHTML = `
${desc}