feat(账号分析+工作台): 功能三自动联动生成设定卡+双向联动维护+玫瑰金配色定稿+首页数据预览/周榜优化+CDP验证脚本
- mcn-dou-analysis: 功能三产出设定后自动生成设定卡(Step7); 设定md与卡片SVG双向联动维护规则固化(06文档+03文档+SKILL.md); 浅色系配色定稿玫瑰金(#D6A3A9→#9A5563)+背景文字提亮规则; 新增06生成账号设定卡片文档+模板样例 - mcn-work-shop: 首页数据预览条/周榜预览表加宽/改名AI脚本诊断/tab切换/添加账号弹窗等优化 - 新增 cdp-*.mjs 验证脚本14个(可复用); .gitignore 忽略运行日志/截图/tmp
This commit is contained in:
1 parent
ab97842030
commit
268ce29aae
35 files changed
+2567
-117
No files matched your search
@@ -8,3 +8,10 @@ Thumbs.db
|
||||
**/browser-harness/envs/
|
||||
# WorkBuddy 自动化任务运行数据(本地运行时生成,不提交)
|
||||
.workbuddy/automations/
|
||||
# 工作台运行日志与 CDP 自检截图(本地产物,不提交)
|
||||
**/mcn-work-shop/server.log
|
||||
**/mcn-work-shop/shot-*.png
|
||||
# 临时目录与生成产物
|
||||
tmp/
|
||||
.workbuddy/tmp/
|
||||
generated-images/
|
||||
@@ -160,3 +160,22 @@
|
||||
- **验证**(CDP #/rewrites):按钮在 AI脚本 head 内、与文字 gap=10px 精确、垂直对齐、顶栏无残留 ✅;点击展开源脚本列 + title 切「收起源脚本」✅;宽度 1200→1085 / 1500→1380 / 1700→1564 / **2000→1600 封顶居中** ✅
|
||||
- ⚠️ 左右边距探针:`right:65` 不是 bug——mask(fixed inset:0) 宽=clientWidth 1185(不含滚动条),弹窗 calc(100%-100)=1085 在 mask 内精确居中左右各 50;15px 是页面右侧 classic 滚动条区域,滚动条左缘到弹窗恰 50px;无滚动条页面左右严格 50/50
|
||||
- 验证脚本保留 `cdp-verify-srcbtn1600.mjs`
|
||||
|
||||
## 关闭服务 + 以本地为准同步仓库(18:51-18:55)
|
||||
- **关闭服务**:工作台 node 服务 PID 28424(127.0.0.1:8899)taskkill 停止;0.0.0.0:8899 的 20604 是 MiPCAudio.exe 占位不动;Git Bash `taskkill //PID` 参数会被 MSYS 转换报错,改 PowerShell `Stop-Process`
|
||||
- **提交**(ab97842,155 文件):工作台导入功能/弹窗优化 + **短视频提示词生成 138 文件整体移入 `project/短视频脚本创作/V1.0/subskill/mcn-video-prompt/`**(git 识别为 R100 纯重命名保留历史,与 mcn-dou-analysis 同构);未提交项有意排除:`.workbuddy/tmp/`(edge-cdp 调试临时)、`server.log`、`cdp-wheel*.mjs`(旧滚轮探针)
|
||||
- **以本地为准推送**(用户明确「不用拉取 直接同步」):`git pull` 走 22 端口超时 + 沙箱拦截 ~/.ssh 读取被用户拒 → 改用 `GIT_SSH_COMMAND="ssh -p 222 -o StrictHostKeyChecking=accept-new" git push origin main` ✅ fast-forward 成功(9b0c198..ab97842),**这次 push 后 origin/main 跟踪引用未丢失**(refs/remotes/origin/main 正常)
|
||||
|
||||
## 账号周榜 5 数值列加宽(17:56-18:00)
|
||||
- 用户需求:「账号周榜 列表中 下面的列宽度可以在大一些:综合得分 / 新增粉丝 / 新增点赞 / 新增评论 / 新增分享」
|
||||
- **改 data-pages.js**(renderRanking):`const WIDE = new Set(['comprehensiveScore','newFans','newLikes','newComments','newShares'])` → `sortable()` 工厂给命中 key 的 th 追加 `th-wide` class
|
||||
- **改 style.css**(`.sortable:hover` 后):`.th-wide { min-width: 110px; }`(表格为 auto 布局,数值列加 min-width 即可增宽,不影响内容列)
|
||||
- **验证**(CDP #/ranking 只读测量):5 列均带 th-wide、实测宽 **122px ≥110px** ✅、computed min-width 110px;对照列不受影响(rank 56 / 赛道 90 / followers 98 / 功能 77);20 行数据正常渲染
|
||||
- 验证脚本保留 `cdp-verify-rankwide.mjs`(含列宽测量 + computed style 校验,可复用)
|
||||
|
||||
## 旧梦留声机账号设定卡 + 功能六固化(17:50-18:00 前段多轮迭代,本段收尾)
|
||||
- **SVG 迭代定版**:`桌面/MCNSkill项目/旧梦留声机/旧梦留声机账号设定卡.svg`,viewBox `0 0 680 1716` 深棕红渐变底(`#2B0F0A→#1A0805`)+ 顶部浅米大框(280px 主标题+16px 副题「账号设定卡」)+ 内容路线 6 格浅米卡 + 核心气质 12 chips + 六大内容基因 186×130(GENE 0X 珊瑚红小标签)+ 色彩体系 5 色卡 h=80 + 角色类型库 186×130(中文名+英文代号大写斜体)+ 情绪图条形(76/32/64/112/84/52/72 底边对齐 y=1452)+ 小字总结行 y=1506 + 红线清单 6 条 2×3 + 版权
|
||||
- **功能六固化(生成账号设定卡片)**:新建 `references/feature/06_生成账号设定卡片.md`(输入/产出/五步方式含①.5读模板样例/模块裁剪适配表/配色适配/要求配色规范表/格式/7条避坑/完成汇报)+ `references/样例/`(模板_旧梦留声机账号设定卡.svg + 参考PNG,源+dsh 两副本)+ SKILL.md 功能总览五大→六大 + 新增「八、功能六」章节 + 部署禁用说明(功能六无连带文件)
|
||||
- **收尾完善(本次)**:①SKILL.md 2.2 技能库路径表补录「功能六流程」+「账号设定卡模板样例」两行(源+dsh);②`R. REPayer`→`R. REPAYER` 大小写统一(模板源/dsh/桌面成品三处,其余代号全大写)
|
||||
- **验证**:源技能 vs dsh 副本 SKILL.md/模板SVG/06功能文件三组 diff 全一致;REPayer 全库零残留
|
||||
- **沉淀要点**(供下次生成其他账号设定卡):先读 `references/样例/` 模板(复用版式骨架与样式常量,内容全替换);角色英文代号=大写首字母缩写+单词全大写;情绪图条形底边对齐峰谷差≥40px;文字垂直居中 y=中心;批量移动坐标用脚本统一加减防双重偏移;红线全量;改完必 present_files 预览确认
|
||||
@@ -0,0 +1,337 @@
|
||||
# 2026-08-31 工作日志
|
||||
|
||||
## 工作台首页顶部「数据预览」条带(09:34-09:50)
|
||||
- 用户需求:「工作台首页顶部增加数据预览 参考插件MCN工作台首页顶部的数据预览」→ 参考源确认 = `D:\dshworkspace\plugin_package\dsh-plugin-mcn`(本机源码唯一含 mcnNav_stats 的实现,非线上 agentos)
|
||||
- **参考实现**(dsh-plugin-mcn client.js + index.js):首页 home 视图顶部 stats 条带(浅蓝底 rgba(65,118,230,.07)、圆角 12、padding 22px 0),5 项 statItem(flex:1 竖排 value 28px 粗体品牌蓝 + label 13px 次级色、item 右侧细边框分隔、末项无边框);数据 `GET /mcn/api/stats`(后端 60s 缓存):accounts=hot_accounts del_flag=0 / analyses=account_video_analysis 计数 / rewrites=rewrite_log done+script_text 非空 / reviews=script_review type='ai'+done / writingScore=最新 20 条 AI 复盘 total 平均(1 位小数)
|
||||
- **移植改动**:①dsh-data.js `getStats()` 修正对齐参考——reviews 补 `type='ai'` 限定、writingScore 由「计数」改为「最新 20 条 AI 复盘平均分」(无数据返回 null);②app.js renderHome `.home` 顶部(data-cards 前)加 `#homeStats` 条带占位 + 新增 `loadHomeStats()`(挂载 fetch /api/dsh/stats 一次,5 项 label 与 dsh 一致,失败显示「数据不可用」)+ router home 分支与 _reload 调用;③style.css 新增 `.home-stats/.home-stat-item/.home-stat-value/.home-stat-label`(工作台品牌色 --primary #2f6fed 对应 dsh 的 #4176e6,背景 rgba(47,111,237,.07))
|
||||
- **验证**(CDP #/ 强制刷新):5 项渲染值 热门账号7/视频解析12/AI写脚本17/AI写作复盘1/AI写作评分86.5 与接口一致 ✅;样式 computed 全符合(bg .07 蓝、圆角 12、值 26px 700 品牌蓝、item 右分隔线、末项无)✅
|
||||
- ⚠️ **坑**:CDP `Page.navigate` 到仅 hash 不同的 URL(#/ranking→#/)**不触发文档重载**,CSS/JS 仍是旧版(表现为新增样式全部 computed 为默认值)→ 必须补 `Page.reload({ignoreCache:true})`;服务端 dsh-data.js 改动需**重启 node 服务**(进程级 require 缓存)
|
||||
- 验证脚本保留 `cdp-verify-homestats.mjs`(结构+值+computed 样式测量,可复用)
|
||||
|
||||
## 首页账号周榜预览表:5 数值列加宽 + 功能列「分析」→「关注」(09:42-09:55)
|
||||
- 用户澄清:要加宽的是**工作台首页的账号周榜预览表**(非 #/ranking 功能页——上轮 08-28 加的是功能页,本轮补首页预览表)
|
||||
- **改 style.css**(`.rank-row` grid):`grid-template-columns: 34px 84px minmax(80px,1fr) 52px repeat(4,58px) 72px` → `... 88px repeat(4,84px) 72px`(综合得分 52→88、4 个数值列 58→84)
|
||||
- **改 app.js**(loadRankingPreview 行模板):非 inDb 账号操作按钮「分析」→「关注」(title「分析该账号并加入账号列表」→「关注该账号并加入账号列表」;行为不变仍 confirmSaveAccount 保存并分析);inDb 账号保持「查看」
|
||||
- **验证**(CDP #/ 强制刷新实测):5 列宽 score=88 / 4×rk-num=84 ≥80 ✅;按钮「关注」+ title 正确 ✅;10 行数据正常、首页 stats 条带与快捷卡片不受影响 ✅
|
||||
- ⚠️ 验证脚本坑:flex/grid 行内同类元素取列宽用 `:nth-child(n)`(按子元素序号),**不能用 `:nth-of-type`**(按元素标签类型 span 计数会错位);rk-num 是行内第 5-8 个子元素
|
||||
- 验证脚本保留 `cdp-verify-rankpreview.mjs`
|
||||
|
||||
## 账号周榜全部页面数值列「窄 + 右对齐」(09:50-10:05)
|
||||
- 用户需求:「账号周榜 全部页面 这些列宽度可以窄一些 右对齐:综合得分/粉丝/新增粉丝/新增点赞/新增评论/新增分享」
|
||||
- **功能页 #/ranking**:①data-pages.js WIDE 集合补 `followers`(6 列)、数值列 td 加 `num` 类(综合得分 `num score-cell`);②style.css 根因排查——`width:100%` auto 布局下列需求总和(≈994px) < 容器(1440px),富余空间按比例摊薄拉宽每列,**min-width 只是下限压不住拉伸**(实测 110px 是摊薄结果,thMin 已 90px 却无效)→ 解法:周榜表单独 `table-layout: fixed` + 数值列 `width:90px` 硬约束(fixed 下 width 生效、min-width 忽略),排名 40/赛道 90/功能 85 固定,账号列吃剩余;table 加专属类 `table-rank`(避免影响账号列表等其他 .table 页面);td.num 缩 13px + tabular-nums + padding 8px
|
||||
- **首页预览表**:`.rank-row` grid 列宽 88/84 → **76/76**(grid 本来就是硬约束,仅缩窄;右对齐原本就有)
|
||||
- **验证**(CDP 实测):功能页 6 列全部 **90px + th/td 右对齐** ✅;首页 5 列 76px 右对齐 ✅;行数据/账号列自适应正常
|
||||
- ⚠️ 两个坑:①`.table th { text-align:left }` 特异性(0,1,1) 压过 `.th-wide`(0,1,0),需写 `.table th.th-wide`;②auto 布局富余空间摊薄是「列宽压不住」的真根因,fixed + width 才是硬约束
|
||||
- 验证脚本保留 `cdp-verify-ranknarrow.mjs`(功能页+首页双测)
|
||||
|
||||
## 「AI写作复盘」→「AI脚本诊断」改名 + 首页周榜列表上底色(09:56-10:08)
|
||||
- 用户需求:①「AI写作复盘」包括功能入口和数据预览名称都改为「AI脚本诊断」;②账号周榜列表内容区域上底色(原来透明与页面背景 #f5f6f8 同色很奇怪)
|
||||
- **改名 7 处全量替换**(app.js 6 + data-pages.js 1):首页快捷卡片 title('flask')、数据预览条带 label(占位 HTML + loadHomeStats 两处)、CRUMB_TITLES、router titles、reviews 页 page-title、代码注释;index.html 无「复盘」字样;「AI写作评分」按用户范围保持不改;页面内部功能描述(复盘报告等)保持
|
||||
- **底色**:`.rank-preview-list` 补 `background: var(--panel); box-shadow: var(--shadow);`(白色面板 + 阴影,与快捷卡片一致;功能页 .table-wrap 本就有 panel 白底)
|
||||
- **验证**(CDP 实测):数据预览 label 与快捷卡片均为「AI脚本诊断」✅;全页无「AI写作复盘」残留 ✅;周榜列表背景 rgb(255,255,255) ✅;#/reviews 标题「AI脚本诊断」✅
|
||||
- 验证脚本保留 `cdp-verify-rename.mjs`
|
||||
|
||||
## 周榜功能列按钮统一白底带边框(09:58-10:10)
|
||||
- 用户需求:「账号周榜 功能列的按钮都改为 白底带边框」
|
||||
- **改 style.css** `.btn-import`(功能页「分析」+ 首页「关注」共用):灰底 #f0f2f5 → **白底 #fff + 边框 #d0d4da**,hover 变主色边框+主色字;`.btn-view`(查看)本就是白底+主色边框未动
|
||||
- **验证**(CDP 实测):功能页「分析」白底 rgb(255,255,255)+边框 #d0d4da ✅;首页「关注」同 ✅
|
||||
- 验证脚本保留 `cdp-verify-btnwhite.mjs`
|
||||
|
||||
## 首页周榜预览改 tab 切换 + 新增 3 个空榜(10:02-10:15)
|
||||
- 用户需求:「首页账号周榜 改为 tab切换,新增 作品热榜、点赞日榜、涨粉日榜,功能先空着」
|
||||
- **改 app.js renderHome**:`<h3>账号周榜</h3>` → `.rank-tabs` 4 个 `.rank-tab`(账号周榜 week 默认 active + 作品热榜 hot + 点赞日榜 likes + 涨粉日榜 fans),工具条保留(id=rankTools);内容区新增隐藏的 `#rankPlaceholder` 占位块
|
||||
- **改 app.js router home 分支**:`.rank-tab` 点击切换 active;week → 显示 #rankTools+#rankPreviewList、隐藏占位;其余 → 隐藏工具条和列表、占位显示「『XX』建设中,敬请期待」
|
||||
- **改 style.css**:`.rank-tabs/.rank-tab/.rank-tab.active/.rank-placeholder`(胶囊 tab:白底边框,active 主色蓝底白字;占位:虚线框+白底居中灰字)
|
||||
- **验证**(CDP 实测):4 tab 初始状态 ✅;逐个点击 3 新榜 → 工具条/列表隐藏 + 占位文案正确 ✅;点回账号周榜恢复列表+筛选 ✅
|
||||
- 验证脚本保留 `cdp-verify-ranktabs.mjs`
|
||||
|
||||
## 首页周榜 tab 选中样式:深蓝胶囊 → 浅底色 + 下划线(10:16-10:25)
|
||||
- 用户反馈:「tab 能不能不要用 深蓝色选中,这是哪来参考的样式,太抢眼了,就用下划线效果 浅底色就行」
|
||||
- 首版 `.rank-tab.active` 沿用顶部主导航 `.tab` 的深蓝胶囊样式(白字+深蓝底 #2f6fed),用户嫌抢眼
|
||||
- **改 style.css**:`.rank-tabs` 补 `border-bottom:1px solid var(--border)`(tab 栏底部分隔线,浅色方案基础);`.rank-tab` 透明背景 + `border:none` + `border-bottom:2px solid transparent`(下划线占位防切换抖动)+ 次级色文字;`.rank-tab.active` = `background:var(--primary-soft)`(#eaf1fe 浅蓝底)+ `border-bottom:2px solid var(--primary)`(主色下划线)+ 主色 600 字重;hover 同浅底主色
|
||||
- **验证**(CDP 强制刷新实测):active computed bg=rgb(234,241,254) / color=rgb(47,111,237) / border=2px solid rgb(47,111,237) / weight=600 ✅;非 active 3 个 tab 全透明背景 + 透明下划线占位 + 次级色文字 ✅;4 tab 切换/回切 active 样式均正确 ✅
|
||||
- ⚠️ 验证脚本坑:querySelector 传入元素对象(不是选择器字符串)会报 `'[object HTMLButtonElement]' is not a valid selector`
|
||||
- 验证脚本保留 `cdp-verify-tabstyle.mjs`
|
||||
|
||||
## 首页周榜 tab 改名(10:10-10:15)
|
||||
- 用户需求:「把点赞日榜 和 涨粉日榜 去掉日,作品热榜 改为 视频热榜」
|
||||
- **改 app.js renderHome**:tab 按钮文本 `作品热榜`→`视频热榜`、`点赞日榜`→`点赞榜`、`涨粉日榜`→`涨粉榜`(仅文案,`data-rank-tab` 值 hot/likes/fans 不变)
|
||||
- router 切换按 `data-rank-tab` 匹配 + 占位文案动态取 `t.textContent`,故只改按钮文本、占位自动跟随
|
||||
- **验证**(CDP 强制刷新实测):4 tab 名称 账号周榜/视频热榜/点赞榜/涨粉榜 ✅;逐一点击占位文案「视频热榜/点赞榜/涨粉榜」建设中 ✅;回切账号周榜恢复列表 ✅
|
||||
|
||||
## 账号列表「导入」→「添加」+ 弹窗精简(10:13-10:22)
|
||||
- 用户需求:「账号列表中 导入功能名称 改为添加,点击按钮弹窗 名称改为 添加账号数据,输入项去掉 方式二和抖音号输入」
|
||||
- **改 data-pages.js**:accImport 按钮「导入」→「添加」,title「添加账号数据(抖音账号信息 / 账号地址)」
|
||||
- **改 app.js openImportAccounts**:弹窗标题「添加账号数据」;删除方式二整块(本地文件夹路径输入 + data-imp-dir)与抖音号输入(data-imp-did);说明文字去「方式一:」前缀(只剩一种方式);提交按钮「提交导入」→「提交添加」;提交逻辑清理——去掉 dir/did 分支,校验改「请填写账号名称或账号地址」,prompt 只含名称/地址,executeTask label '导入指令'→'添加指令'
|
||||
- **改 style.css**:注释「导入账号弹窗:方式一账号信息 / 方式二本地文件夹」→「添加账号弹窗:账号信息(名称/地址)」
|
||||
- 保留:账号详情页 infoGrid「抖音号」展示字段(非输入项)、帮助页「导入账号」自然语言用法(用户未要求)
|
||||
- **验证**(CDP #/accounts 强制刷新实测):按钮「添加」+ title ✅;弹窗标题/2 个输入项(账号名称+账号地址可选)/提交添加/无方式二·文件夹·抖音号 ✅
|
||||
- ⚠️ 验证坑:`document.querySelector('.modal')` 取到的是**页面加载时自动弹出的「项目文件地址」设置弹窗**(index.html 既有行为,非本次引入)→ 需取最后一个 `.modal-mask` 才是新开的弹窗
|
||||
|
||||
## 添加按钮图标:download → user-plus(10:15-10:18)
|
||||
- 用户需求:「按钮前面 图标也要改」——「添加」按钮前是下载图标(导入语义残留),需换贴合「添加账号」的图标
|
||||
- **改 app.js**:ICON_PATHS 新增 `'user-plus'`(Lucide 标准:人像 + 右下加号);弹窗标题图标 `svgIcon('download', 18)` → `svgIcon('user-plus', 18)`
|
||||
- **改 data-pages.js**:accImport 按钮图标 `svgIcon('download', 15, 'currentColor')` → `svgIcon('user-plus', 15, 'currentColor')`
|
||||
- **验证**(CDP 实测):按钮/弹窗标题图标均为 user-plus、无 download 残留 ✅
|
||||
- ⚠️ 验证坑:查弹窗图标前必须先 click 打开(否则取到的是页面自动弹出的设置弹窗,无 h3)
|
||||
|
||||
## AI写脚本页操作列按钮:「AI改写」→「AI写脚本」+ 样式统一(10:19-10:22)
|
||||
- 用户需求:「AI写脚本 列表页 操作列改名功能,按钮样式参考 账号列表中功能列按钮样式」
|
||||
- **背景**:账号列表(#/accounts)操作列按钮本就是「AI写脚本」(`btn btn-sm` 白底灰边框 → confirmAiScript);AI写脚本页(#/rewrites)操作列却是「AI改写」(`btn btn-sm btn-view` 主色边框 → confirmRun 改写),两处语义不一致
|
||||
- **改 data-pages.js renderRewrites 行模板**:`<button class="btn btn-sm btn-view" data-rewrite=...>AI改写</button>` → `<button class="btn btn-sm" data-rewrite=...>AI写脚本</button>`(文案统一 + 去掉 btn-view 改白底灰边框,与账号列表操作列一致)
|
||||
- **验证**(CDP #/rewrites 强制刷新实测):按钮文本「AI写脚本」✅、class=btn btn-sm 无 btn-view ✅、白底 rgb(255,255,255)+灰边框 #e3e6ea+灰字 ✅(20 行全部生效)
|
||||
- 未改:视频详情页(298 行)与账号详情页的「AI改写」按钮(用户仅指定 AI写脚本列表页)、弹窗标题「改写脚本」
|
||||
|
||||
## 旧梦留声机账号设定卡(SVG 信息图,10:23-10:30)
|
||||
- 用户需求:参考《权游》影视需求 Skill 信息图,用「旧梦留声机」账号设定生成同款设定卡
|
||||
- **数据源**:`C:\Users\maidou\Desktop\MCNSkill项目\旧梦留声机\旧梦留声机账号设定.md`(群像账号:六内容基因/六段式情绪核弹/角色类型库6类/红线6条/色彩体系/内容路线六步闭环)
|
||||
- **产出**:`旧梦留声机账号设定卡.svg`(同目录,680×990 竖版信息图,show_widget 同步内联渲染)
|
||||
- 结构映射(对齐权游 12 模块):标题+类型+情绪锚点 → 内容路线6步流程条 → 核心气质8chips → 六大内容基因6卡 → 色彩体系5色卡(泛黄旧时光/冷灰乡土/暖黄回忆/冷白危机/明亮都市)→ 角色类型库6卡(含记忆符号)→ 六段式情绪核弹结构(柱状曲线+时长占比)→ 红线清单6条
|
||||
- 视觉语言:暖黄旧时光色系(c-amber #FAEEDA/#854F0B/#633806 主 + c-coral #FAECE7/#993C1D/#712B13 点缀),核爆段 coral 600 高亮;账号美学对齐(非权游深色)
|
||||
- 用户可继续要求:其他账号同款卡 / 调整模块 / 导出 PNG
|
||||
|
||||
## 旧梦留声机设定卡·情绪图条形加高(10:34-10:36)
|
||||
- 用户需求:「情绪图能否整体高度再高一些,各阶段情绪起伏明显」
|
||||
- **改** `桌面\MCNSkill项目\旧梦留声机\旧梦留声机账号设定卡.svg` 六段式情绪核弹结构:7 条条形底边统一对齐 y=812(原 804),高度放大——谷底「绝境施压」14→16、峰顶「核爆段」38→**56**,峰谷高差 24→40px;全部条顶 y 上移,注释行 y 822→836(与下方红线清单标题 y852 无重叠)
|
||||
- 验证:条形文字垂直居中仍贴合条形内;峰谷对比明显
|
||||
|
||||
## 旧梦留声机设定卡·长图版配色升级(10:35-10:40)
|
||||
- 用户需求:「长图版 标题颜色,卡片背景和文字颜色 都更好看」
|
||||
- **重写** `旧梦留声机账号设定卡.svg` 配色体系(布局坐标不变):
|
||||
- 标题:主标题改**渐变** `#8C2F1E→#C05A32` + weight 700;分隔线琥珀金 #E0953C;副标题 #B4642F;标语 #8C2F1E
|
||||
- 模块:标题统一 #5E2A1E 600 + 竖条珊瑚红 #C13B2F;卡片双色系交替——暖奶油 #FFF3E2+边框 #EDCBA4(路线/基因/色彩)与浅珊瑚奶油 #FDEBE2+边框 #E8B7A2(气质/角色/红线)
|
||||
- 文字:卡片标题 #7B2D1B/#8C3B2B,正文暖灰 #6F5B4A(替代原 #5F5E5A 纯灰);情绪图核爆 #D6403A+白字600,琥珀系统一 #F0B25C/#F7D9A8;红线×号 #C13B2F 600
|
||||
- 整图加暖米底 #FDF8F0(原透明)
|
||||
|
||||
## 旧梦留声机设定卡·情绪图条形高度翻倍(10:37-10:42)
|
||||
- 用户需求:「情绪部分 高度继续增加 一倍」
|
||||
- **改** `旧梦留声机账号设定卡.svg`:7 条条形高度全部 ×2(38→76/16→32/32→64/56→112/42→84/26→52/36→72),底边统一对齐 y=880(原 812);因条形变高,注释行 836→904、红线清单整体下移(标题 852→932,卡片行 872/910/948→952/990/1028),viewBox 高度 990→1080
|
||||
- 验证:高度×2 正确、底边全对齐 880、文字仍居中(核爆 768+56=824)
|
||||
|
||||
## 旧梦留声机设定卡·基因卡/角色卡排版升级(10:39-10:45)
|
||||
- 用户需求:六大内容基因与角色类型库卡片文字排版按「GENE 01 标题 内容」「角色中文 英文 内容」样式
|
||||
- **六大内容基因卡**:标题行拆为「GENE 0X」珊瑚红 #C13B2F 10px 600 小标签(x=61/259/457)+ 标题 12px 600 #7B2D1B(x=118/316/514),内容两行不变
|
||||
- **角色类型库卡**:标题行拆为「中文名」12px 600 #8C3B2B(x=61/259/457)+ 英文名 9px #B0715A **右对齐**(text-anchor=end,x=223/421/619),内容行不变;英文名=Elder Guardian/Repaying Child/Suffering Mother/Great-Love Prototype/Guiding Benefactor/Stubborn Dreamer
|
||||
- 验证:SVG 结构完整(1 svg/8 g/53 rect/97 text)、6 GENE + 6 英文名就位、全部标题末字尾 ≤ 卡右缘-8 无溢出
|
||||
|
||||
## 旧梦留声机设定卡·按参考长图版整体重写(10:44-10:55)
|
||||
- 用户反馈:「排版还是不一样呢,仔细看看之前生成 长图版」+ 提供参考图 `桌面\旧梦留声机_账号设定卡_长图版.png`
|
||||
- **重写** `旧梦留声机账号设定卡.svg`(完全推翻前几版,viewBox 680×1560):
|
||||
- 背景:深棕红渐变 `#2B0F0A→#1A0805`(原暖米底)
|
||||
- 顶部大框:浅米色 520×170 圆角 14,框内"旧梦留声机 · 账号 / 设定卡"两行 26px 渐变大标题 + 琥珀金分隔线 + 副标题 + 标语
|
||||
- 装饰线:居中 `— channels requirement board 2026 —`
|
||||
- 模块标题:▍+ 浅米色 14px 加粗
|
||||
- **六大内容基因卡**(186×130,原 72):GENE 0X 标签 letter-spacing 1 + 大标题 14px 700 + 3 行描述(含新增金句"让他撑下去"/"脏一点泪点多一点"等)
|
||||
- **角色类型库卡**(186×130,原 48):PROTOTYPE 标签 + 中文名 14px 700 + **英文代号大写斜体** `E. ELDER / R. REPayer / M. SUFFERING / A. ARCHETYPE / G. GUIDE / D. DREAMER` + 2 行描述
|
||||
- 红线清单:深色 `#3D1810` 卡 + 边框 + 红×号
|
||||
- 底部版权:数据来源 + content requirement board
|
||||
|
||||
## 旧梦留声机设定卡·修复红线清单与情绪图高度(10:47-10:52)
|
||||
- 用户反馈:红线清单内容少了(重写长图版时只保留 2 条)、情绪部分高度又变矮了(重写时条形缩水)
|
||||
- **修复** `旧梦留声机账号设定卡.svg`:
|
||||
- 红线清单 2 条 → 补全 **6 条**(消费苦难/伪造真实素材/美化悲剧/角色没名字/强行好结局/植入打断情绪,2 列×3 行,卡 y=1538/1576/1614)
|
||||
- 情绪图条形恢复到翻倍高度:56→76/26→32/44→64/76→112/54→84/34→52/46→72,底边对齐 y=1452(核爆段 y=1340 与标题基线 1336 差 4px 不重叠)
|
||||
- viewBox 1560→1700,底部版权下移至 1672/1684
|
||||
- 验证:7 条底边全对齐 1452、红线 6 条、无重叠
|
||||
|
||||
## 旧梦留声机设定卡·去 PROTOTYPE + 情绪小字(10:48-10:53)
|
||||
- 用户反馈:① 角色类型库不需要加 PROTOTYPE 这个英文 ② 参考图情绪部分下面有一行小字
|
||||
- **修改** `旧梦留声机账号设定卡.svg`:
|
||||
- **去掉 6 张角色卡的 PROTOTYPE 标签**(珊瑚红小标签)→ 角色卡排版简化为「中文名 + 英文代号 + 3 行描述金句」(描述从 2 行扩到 3 行,新增金句"沉默里把全家扛在肩上"/"朴素到极致是最贵的善良"/"指路的手势比讲理更动人"等)
|
||||
- **情绪图下方加一行小字** y=1506:`情绪节奏总览:压抑蓄力 → 核爆释放 → 破壁落地,每 10-30 秒完成一次情绪反转`(11px 斜体浅米色 #D9BFA3,居中)
|
||||
- 验证:PROTOTYPE 残留 0,6 中文名+6英文代号完整,小字行已嵌入
|
||||
|
||||
## 旧梦留声机设定卡·标题换行+内容路线颜色(10:49-10:53)
|
||||
- 用户反馈:①「旧梦留声机 · 账号 / 设定卡」换行不好看 → 改「旧梦留声机」+「账号设定卡」两行(26px 渐变标题,不带点号)② 内容路线参考图不是深色 → 卡片由 #3D1810 深色改 **#FAEEDA 浅米色 + 边框 #EDCBA4 + 文字 #7B2D1B**(与参考图/基因卡同色系)
|
||||
- 验证:标题两行、内容路线 6 卡填充/边框统一浅米色
|
||||
|
||||
## 旧梦留声机设定卡·标题大小层级(10:51-10:54)
|
||||
- 用户反馈:「账号设定卡 这几个字明显该小一些」
|
||||
- **改** `旧梦留声机账号设定卡.svg` 顶部标题:「旧梦留声机」28px 700 渐变 +「账号设定卡」**16px 600 #8C2F1E letter-spacing 2**(副标题级,缩小 12px),形成大小主次
|
||||
|
||||
## 旧梦留声机设定卡·垂直居中+间距修复(10:52-10:56)
|
||||
- 用户反馈:① 内容路线卡片中文字没有垂直居中 ② 红线清单和情绪下面的文字靠在一起了
|
||||
- **改** `旧梦留声机账号设定卡.svg`:
|
||||
- 内容路线 6 个文字 y 326→**321**(=卡片中心 296+25,垂直居中)
|
||||
- 红线清单整体下移:标题 1522→1538、红线卡 1554/1592/1630、版权 1688/1700;情绪小字(y1506)与红线标题间距 16→**32px**;viewBox 1700→1716
|
||||
- 验证:6 文字全部居中、间距 32px、红线 3 行 1554/1592/1630、viewBox 1716
|
||||
|
||||
## 旧梦留声机设定卡·色彩体系卡片加高(10:53-10:58)
|
||||
- 用户反馈:「色彩体系 卡片 参考图中也高一些」
|
||||
- **改** `旧梦留声机账号设定卡.svg`:色彩卡高度 50→**80**(y=882 h=80,标签文字 954→984);角色类型库整体下移 10px(标题 1000→1010,卡 1018→1028/1162→1172,内部文字同步 +10)
|
||||
- ⚠️ 坑:手动 Edit 改第一张卡 y 后又用脚本整块 +10,导致第一张卡双重偏移(1038),已单独修正回 1028
|
||||
- 验证:色卡 h80、角色卡 y 统一 1028/1172、色卡底 962 vs 角色库标题 1010 间距 48、角色库底 1302 vs 情绪标题 1336 间距 34
|
||||
|
||||
## 账号分析技能新增「功能六:生成账号设定卡片」(10:55-11:05)
|
||||
- 用户需求:把生成账号设定卡片总结成一个功能(方式/要求/格式/注意事项),放入账号分析技能
|
||||
- **新增** `references/feature/06_生成账号设定卡片.md`(源+dsh 副本同步):输入=功能三产出 `{达人昵称}账号设定.md`;产出=`{达人昵称}账号设定卡.svg`(680×~1700 深色长图);四步方式(读设定→12段模块映射→SVG手绘→验证交付);配色规范表(深棕红渐变背景/浅米色卡片/GENE 0X 标签/角色英文代号/核爆红);注意事项含「批量移动坐标防双重偏移」等 7 条避坑
|
||||
- **改 SKILL.md**:功能总览 五大→**六大功能** + 功能六行;新增「八、功能六」章节;部署禁用说明补功能六(无连带文件);章节编号 七/八/九 连续
|
||||
- 验证:源技能与 dsh 副本 06 文件与 SKILL.md diff 一致 ✓
|
||||
|
||||
## 账号分析技能功能六·补充模板样例与适配规则(10:58-11:08)
|
||||
- 用户追问:下次生成别的账号设定卡片也要达到相同效果 → 除流程规范外补充三块
|
||||
- **① 模板样例入技能**(自包含):`references/样例/模板_旧梦留声机账号设定卡.svg`(最佳成品)+ `参考_旧梦留声机_账号设定卡_长图版.png`(渲染效果);功能六「方式」新增 ①.5 步——动手前必读模板,复用版式骨架/样式常量,内容全替换;模板迭代=改模板→同步生成旧梦留声机卡确认→再用于新账号
|
||||
- **② 模块裁剪适配规则**(新章节):按设定实际内容裁剪/增补——无流程删内容路线、无角色库删角色库、基因按条数定列数(2/3列 卡宽=600px/列数-间距)、情绪按实际段数(4-7段)、色彩用真实色值、红线条数不限;每个保留模块须 grep 验证能找到原文
|
||||
- **③ 配色适配规则**(新章节):背景/卡片默认深色长图风保系列一致;色彩体系色块用账号真实色值;强调色跟随账号主色(暖→珊瑚红/琥珀,冷→冷蓝/冰青);冲突时以看得清/有层次/账号辨识度最高准则
|
||||
- 同步:源技能 06 文件+SKILL.md+样例目录 → dsh 副本全部 diff 一致
|
||||
|
||||
## MCN工作台 账号详情 persona tab 卡片/文字切换(11:00-11:35,三轮迭代)
|
||||
- **用户需求**:账号详情「账号设定」tab 改为可切换「卡片版/文字版」,默认显示卡片,无卡片显示文字
|
||||
- **后端新增 2 接口**(server.js):
|
||||
- `/api/dsh/account-card?name=` → 在 allowedRoots() 各根目录下查找 `{账号名}账号设定卡.svg`(规范路径 `{root}/{name}/{name}账号设定卡.svg` → 根下直挂 → 账号目录内扫描含「账号设定卡」的 svg,防目录名/账号名不一致)
|
||||
- `/api/file?path=` → 内联文件输出(无 attachment 头,Content-Type 按 mimeOf),供 `<img>` 渲染 SVG
|
||||
- 新增 `findAccountCard(name)` 工具函数(安全清理 `[\\/:*?"<>|]` 非法字符防路径穿越)
|
||||
- **前端 persona tab 改造**(data-pages.js renderAccountDetail):
|
||||
- 状态:`viewMode='card'`(卡片版默认)+ `personaCard={path|null}`
|
||||
- `loadPersona()` 改 `Promise.all([account-persona, account-card])` 并行加载
|
||||
- panel **三态渲染**:无设定→空态按钮(不变)/ 无卡片→直接文字版 `persona-text` / 有卡片→右上角图标切换组 `.persona-wrap > .persona-vs-icons + .card-view + .persona-text`
|
||||
- 切换事件:直接 DOM 切换 `.card-view/.persona-text` 的 `vs-hidden` 类(不重渲染,保留滚动位置);按钮选择器 `[data-vs]` 兼容图标按钮
|
||||
- **样式三轮迭代**(style.css):
|
||||
1. 整行 segmented control「卡片版|文字版」+ 蓝底激活态(`.view-switch` 块)
|
||||
2. 改为**右上角图标按钮组**:`.persona-wrap { position:relative }` + `.persona-vs-icons { position:absolute; top:0; right:0; z-index:2; display:inline-flex }`,图标 `film`(卡片)+ `file-text`(文字),激活态蓝底白字
|
||||
3. 卡片图片 `max-width: 540px` → **width:100% 自适应内容区域**(去 max-width,依赖父 page max-width 封顶)
|
||||
- **多视口验证**(CDP Emulation 900/1024/1280/1920/2560):
|
||||
- imgW = wrapW,小视口 811→中视口 1191→大视口封顶 1382
|
||||
- 所有视口 `docScrollW < viewport`,**无横向滚动**
|
||||
- 用例 1629 旧梦留声机(有卡):默认卡片版、切文字版、切回卡片版 全过
|
||||
- 用例 1627 王微斯(无卡):直接文字版无切换控件
|
||||
- **服务重启**:旧 23976 任务被停(cNHaFK),新启后台任务 QMmTlH(127.0.0.1:8899);服务 Cache-Control: no-store,改完仍需 `Page.reload({ignoreCache:true})` 强制刷新
|
||||
- 验证脚本保留 `cdp-verify-personacard.mjs`(多视口自检+三态断言+截图,可复用)
|
||||
|
||||
## 功能六配色适配升级:底色跟随账号风格(11:17-11:22)
|
||||
- 用户反馈:「不同风格账号卡片要用对应底色,不是全部都是一个颜色」
|
||||
- **改 06 文件「配色适配」章节**(原「默认沿用深色长图风」→ 强规则「禁止一律模板深棕红底」):
|
||||
- 三步判定:①读设定色彩体系真实色值+赛道判风格 ②风格→底色映射表(7 类:暖怀旧→深棕红/浅米、温情亲子→深焦糖/奶油米、科技冷淡→深灰蓝/冷白、清新三农→深墨绿/米白、潮流都市→深紫黑/浅灰白、活力运动→深海军/冷白、甜美少女→深莓紫/浅粉白) ③强调色=账号主强调色
|
||||
- 底色推导公式(未覆盖风格):主色压暗 15-25% + 降饱和 → 深色渐变底;卡片色取高对比浅色;系列一致性=同账号一致非跨账号一致
|
||||
- **改 ①.5 读模板样例**:「配色体系为基准」→「版式骨架/字号/卡片尺寸/间距为基准,配色不沿用模板」
|
||||
- **改 SKILL.md 八功能六配色适配要点**:底色跟随账号风格,禁止一律模板深棕红底 + 风格映射摘要
|
||||
- 源技能 → dsh 副本同步,06 文件与 SKILL.md diff 全一致
|
||||
|
||||
## 功能六新增「触发场景处理」章节(11:20-11:24)
|
||||
- 用户问「如果用户要求生成账号设定卡片,应该如何处理 分几种情况」→ 输出 7 情况分类 + 三问判定链,用户认可后固化
|
||||
- **06 文件新增「触发场景处理」章节**(输入/产出之后、方式之前):三问判定链(①有无设定→无则功能三或全链路 ②卡片是否已存在→有则询问覆盖/微调 ③模块是否完整→缺则裁剪适配)+ 7 情况表(有设定无卡/有设定有卡/无设定在库/无设定不在库/设定缺模块/参考样式/调整已有卡)+ 环境路径
|
||||
- **SKILL.md 八功能六**补「触发判定」行(指向 06 触发场景处理)
|
||||
- 源技能 → dsh 副本同步,06 文件与 SKILL.md diff 全一致
|
||||
|
||||
## 功能六「内容基因 TOP6 全量」术语澄清(11:25-11:28)
|
||||
- 用户问「账号设定中 TOP6 全量验证 是想说根据TOP6视频分析得出的吗」→ 确认混淆点:模块映射表第 4 行原写「内容基因(TOP6 全量)」,易误读为「TOP6 个基因」
|
||||
- **改 06 文件**模块映射表第 4 行:「内容基因(TOP6 全量)」→「内容基因(设定中全部条目,全量呈现)」+ 新增术语澄清块(第 77 行):本表「内容基因全量」= 设定文件中内容基因条目**全部上卡、不裁剪**(旧梦留声机恰 6 条,非指 TOP6 视频);内容基因条目由**功能三基于 TOP6 视频**(筛选权威参数见 SKILL.md「视频筛选标准」)解析提炼得出——TOP6 视频是分析依据(输入),内容基因是结论呈现(输出),两个「6」层级不同,勿混淆
|
||||
- 源技能 → dsh 副本同步,grep 确认两处第 77 行内容一致,且全文仅此一处含 TOP6 字样
|
||||
|
||||
## 功能六「六大内容基因·TOP6」表述简化(11:29-11:31)
|
||||
- 用户反馈:「简化为 六大内容基因·TOP6 就行了 后续生成卡片也这样描述就行」
|
||||
- **改 06 文件**:模块映射表第 4 行「内容基因(设定中全部条目,全量呈现)」→「内容基因(功能三基于 TOP6 视频提炼的结论)→ 六大内容基因·TOP6」;术语澄清块精简为一行注(保留核心:TOP6 视频=分析依据输入、内容基因=结论输出、卡片全量呈现不裁剪)+ 明确「后续生成卡片统一以『六大内容基因·TOP6』描述该模块」
|
||||
- 源技能 → dsh 副本 cp 同步,diff 一致
|
||||
|
||||
## 功能六「内容基因·TOP6」定名 + 旧梦产出同步修改(11:31-11:33)
|
||||
- 用户两连纠正:「简化为 六大内容基因·TOP6」→ 随即「应该改为 内容基因·TOP6」(去掉「六大」)
|
||||
- **06 文件**(源+dsh):模块映射表第 4 行信息图模块「六大内容基因·TOP6」→「**内容基因·TOP6**」;术语注末句同步改「后续生成卡片统一以『内容基因·TOP6』描述该模块」
|
||||
- **旧梦留声机产出同步**(桌面 MCNSkill项目/旧梦留声机/):
|
||||
- `旧梦留声机账号设定.md`:小标题「六个内容基因标记(TOP6 全量验证)」→「**内容基因·TOP6**」;九差异化壁垒表「六个内容基因是频道级复用资产」→「内容基因·TOP6 是频道级复用资产」
|
||||
- `旧梦留声机账号设定卡.svg`:模块标题「▍六大内容基因(TOP6 全量验证)」→「▍**内容基因·TOP6**」;desc 与代码注释同步改
|
||||
- grep 验证三目录(源/dsh/桌面)无「六大内容基因/六个内容基因/TOP6 全量」残留
|
||||
|
||||
## 「核心气质」→「内容标签」改名(11:32-11:34)
|
||||
- 用户反馈:「核心气质 应该 改为 内容标签」
|
||||
- **06 文件**(源+dsh):模块映射表第 3 行「核心气质/人设标签 | 核心气质(chips)」→「内容标签/核心标签 | 内容标签(chips)」
|
||||
- **模板样例 SVG**(源+dsh):desc/注释/模块标题 3 处「核心气质」→「内容标签」;顺带补上一轮遗漏——desc 里「六大内容基因」→「内容基因·TOP6」、注释+模块标题「▍六大内容基因(TOP6 全量验证)」→「▍内容基因·TOP6」
|
||||
- **06 文件裁剪适配表**(源+dsh):「保留『六大内容基因』」→「保留『内容基因·TOP6』」(上一轮漏改)
|
||||
- **桌面产出**(旧梦留声机):卡片 SVG 3 处「核心气质」→「内容标签」(sed)
|
||||
- 验证:源/dsh 两文件 diff 全一致;三目录 grep「核心气质|六大内容基因|六个内容基因」零残留
|
||||
|
||||
## 「内容路线」→「叙事结构」全链路改名(11:34-11:37)
|
||||
- 用户反馈:「内容路线 改为 叙事结构」——本次术语同时是设定字段名(设定 md 第 16 行)与卡片模块名,按命名一致性全链路统一
|
||||
- **源技能 sed 批量替换 4 文件**(06 文件 4 处:模块映射表第 2 行/触发场景第 5 行/裁剪适配 96/97 行;人设卡生成方法.md 3 处:模板维度定义+校验清单 2 处;模板样例 SVG 3 处:desc/注释/标题;SKILL.md 1 处:功能六「无流程删叙事结构」)→ cp 同步 dsh 副本,diff 4 文件全一致
|
||||
- **桌面产出**:设定 md 第 16 行字段「内容路线」→「叙事结构」;卡片 SVG desc/注释/标题 3 处同步
|
||||
- **不动**:`.bak` 备份、`视频分析/*/analysis.json` 历史解析原始数据(保持数据准确性);dsh tmp/backfill_track_ztq.py 历史回填脚本(其他账号数据文本)
|
||||
- 验证:功能文档三处 grep「内容路线」零残留
|
||||
|
||||
## 生成王微斯账号设定卡(11:45-11:52,功能六首个新账号实例)
|
||||
- 用户需求:「生成 王微斯的 账号设定卡」;触发场景=**有设定无卡→全流程生成**(桌面 MCNSkill项目/王微斯/王微斯账号设定.md,08-13)
|
||||
- **产出** `王微斯账号设定卡.svg`(680×1716,深色长图,同模板版式骨架)
|
||||
- **配色适配**(不沿用模板深棕红):甜宠情感/乙女向→风格映射「甜美少女→深莓紫/浅粉白」→ 背景 #2A0F2E→#18071C 渐变、卡片 #FDF0F6 浅粉白+边框 #F0C9DD、强调玫红 #E0437F/#D84A8C、chips 深莓 #3D1030
|
||||
- **模块裁剪适配**(王微斯 vs 旧梦差异):①内容基因 **4 条**→2×2 卡(卡宽 285=582-12/2,非模板 3 列)②情绪 **四段式**→4 条形(开场76/推拉100/破防128玫红高亮/收口88,底边全对齐 1452)③角色类型库=**女主横卡(582×100)+男主 4 卡 2×2**(无英文代号省略斜体行)④叙事结构 5 步→5 卡 104 宽⑤**删色彩体系模块**(设定无色彩体系段,不硬塞)⑥内容标签 12 chips 两行
|
||||
- **验证**:svg/g 配对(1/1,15/15)、4 条形底边 1452 全对齐、模块间距≥30px、最长文字行(红线19字)不超卡右缘 ✓
|
||||
|
||||
## 配色适配·底色=背景色 修正(11:49-11:53)
|
||||
- 用户澄清:「我说的是改底色就是背景色,不是改里面的元素的颜色」——之前我把映射表写成「背景色/卡片色/强调色」3 列并改了王微斯卡片元素色,理解偏了
|
||||
- **修正 06 文档配色适配章节**(源+dsh):①明确「底色=整卡背景色」,卡片/文字/标签/强调色等内部元素颜色**不随账号变**,统一按下方「配色规范」表(浅米卡+深色字+珊瑚红强调);②映射表去「卡片色」「强调色」列,只留「背景底色」;③强规则加「**只换背景色**:内部元素一律按配色规范表」;④配色规范表标题「旧梦留声机定版」→「模板统一色,所有账号通用;仅背景色随账号风格换」
|
||||
- **改 SKILL.md**(源+dsh)功能六配色要点:明确「底色=整卡背景色,只换背景色,内部元素保持模板统一色」
|
||||
- **修正王微斯卡元素色**(sed 批量):卡片 #FDF0F6→#FAEEDA、边框 #F0C9DD→#EDCBA4、标题 #7B2D5E→#7B2D1B、正文 #6F4A5E→#6F5B4A、GENE #D84A8C→#C13B2F、chips #3D1030→#3D1810+边框 #A8879B→#A88770、标题渐变 #C0457F→#8C2F1E / #E876A9→#C05A32、副题 #8C3B6B→#8C3B2B、情绪条 #F7D9E8→#F7D9A8 / #F2C0DA→#F0B25C / #E0437F→#D6403A / #C22F6A→#A92825、红线 #E0437F→#D6403A、版权 #8A5E7A→#7A5645;**仅保留**背景渐变 #241020→#12070F(深莓紫,甜美/甜宠风格映射)
|
||||
- 验证:svg/g 1/1+15/15、模板色使用 96 处、粉色系残留 0;CDP 截图渲染确认背景深莓紫+元素全模板色(系列感一致)
|
||||
- 脚本保留 cdp-shot-wangweisi.mjs(本地 SVG 截图自检)
|
||||
|
||||
## 深莓紫背景调亮(11:52-11:54)
|
||||
- 用户反馈:「背景色 我看还是黑色呢」——原映射色 #241020→#12070F 压太暗,紫调不可辨,视觉近黑
|
||||
- **调亮**:王微斯卡 bgGrad `#241020→#12070F` → `#3A1E42→#22102A`(顶部 R58/G30/B66 紫调明显、亮度≈旧梦深棕红 2 倍;底部深紫 #22102A,仍保深色底)
|
||||
- **06 文档映射表**(源+dsh)甜美/少女/二次元行同步更新色值
|
||||
- 经验固化:深色渐变底顶部色需保紫/蓝/绿等色相可辨(亮度≈50/255),过暗则视觉归黑;旧梦深棕红 #2B0F0A 亮度 23 但因红棕调暖尚可辨,冷色调更易显黑需提亮
|
||||
|
||||
## 工作台账号设定内容区左右 +20px 间隔(11:42-11:44)
|
||||
- 用户反馈:「账号设定卡 左右可以在间隔20PX」→ 澄清:不是改 SVG,是**工作台账号详情「账号设定」tab 内容区距页面左右再增 20px 间隔**
|
||||
- **改 style.css** `.persona-wrap`: `position: relative` → `position: relative; padding: 0 20px;`(卡片版 .card-view 与文字版 .persona-text 同在 persona-wrap 内,左右各内缩 20px;绝对定位的 .persona-vs-icons 切换图标不受 padding 影响仍贴右上角)
|
||||
- **验证**(CDP #/account/1629 强制刷新 + 点 persona tab):computed padding-left/right=20px;卡片 imgW 1122.7 vs detail-body 1172.7,内缩合计 50px(左 detail-body 10 + persona 20,右 persona 20);文字版 inset 同 50 ✓
|
||||
- 静态 CSS 改动无需重启服务(HTTP 200);验证脚本保留 `cdp-verify-personapad.mjs`
|
||||
|
||||
## 王微斯账号设定卡重写为浅色甜宠风(11:54-11:55)
|
||||
- 用户反馈:「这个账号的调性 应该是没那么深的颜色」——深莓紫 #3A1E42→#22102A 仍偏暗,账号整体偏甜宠少女应走明快调性
|
||||
- **重写** `桌面\MCNSkill项目\王微斯\王微斯账号设定卡.svg` 为浅色甜宠风(布局坐标 680×1716 不变,全量换色):
|
||||
- 背景:`#3A1E42→#22102A` 深莓紫 → **`#F8EDF6→#F1E0EE` 浅粉紫渐变**
|
||||
- 顶部标题框:白 `#FFFFFF` + 粉边 `#E8C9DC`,主标题渐变 `#C0457F→#E876A9`(粉),副标题/标语/模块标题 `#7B2D5E`(深粉)
|
||||
- 五步推拉 / 内容标签 / 基因卡 / 角色库:内部元素全部从「模板统一色 + 粉调」改回「白卡 + 粉边 + 粉 chips」体系(chips `#F7E8F2`/`#D8A9C8`、GENE 标签 `#E0437F`、正文 `#6F4A5E`)
|
||||
- 四段式情绪:浅粉/中粉/破防段 `#E0437F` 高亮(白字 700)/浅粉,注释 `#B98AA6`、小字 `#A98A9C`
|
||||
- 红线清单:浅粉条 `#F7E8F2` + 粉×号 `#E0437F`、文字深粉
|
||||
- **06 文档新增「浅色系配色规范」**(源+dsh 同步):
|
||||
- 配色适配章节重写:底色**分两系**(深色系=深沉浓郁调性,浅色系=明快/甜美/治愈调性),内部元素跟随所选色系,不再「只换背景色」→ 改为「**底色决定色系**」
|
||||
- 关键定义:浅色系 = 白卡 + 粉边 + 深粉文字 + 粉强调
|
||||
- 风格映射表「甜美/少女/二次元」行:深莓紫(调性深沉时)|**浅色甜宠风 `#F8EDF6→#F1E0EE`(调性明快时,如王微斯)**
|
||||
- 新增浅色系推导公式:主色 → 提亮 90%+ 亮度 + 大幅降饱和 → 浅色渐变底;正文用主色加深版(`#7B2D5E` 档)
|
||||
- 配色规范表拆为两套并列:「**深色系(深色长图风,如旧梦留声机)**」+「**浅色系(浅色甜宠风,如王微斯)**」(含背景/标题框/主标题渐变/副标题/模块标题/卡片背景×2类/卡片标题/正文/GENE/英文代号/情绪图破防/琥珀段/小字/红线×号 14 行色值)
|
||||
- 强规则第 2 条「只换背景色」→「底色决定色系」
|
||||
- **验证**:源/dsh 06 文档 diff 全一致;CDP 截图(`shot-wangweisi-card.png`)渲染确认浅粉紫底+白卡+深粉字+破防段粉红高亮,整体调性明快甜宠 ✓
|
||||
- 经验固化:**配色分两系而非单系**——深色系适合剧情/怀旧/深沉调性,浅色系适合甜宠/少女/治愈/明快调性;同一账号必须固定色系,跨账号可不同;未来生成新账号卡片时按风格落系,色值从对应色系表查,禁跨系混用
|
||||
- 脚本升级:`cdp-shot-wangweisi.mjs` 页面匹配 `decodeURIComponent(t.url).includes('王微斯')`(9223 URL 百分号编码坑)+ 补 `Page.reload({ignoreCache:true})`(同 URL navigate 不重载)
|
||||
|
||||
## 王微斯卡片·玫瑰金背景试版(12:01-12:03)
|
||||
- 用户需求:「这个保留 再试试 偏粉的深一点的玫瑰金色」——保留当前浅色甜宠风版式与内部元素,仅换**背景**为偏粉深玫瑰金渐变
|
||||
- **改** `桌面\MCNSkill项目\王微斯\王微斯账号设定卡.svg`:
|
||||
- 渐变:`#F8EDF6→#F1E0EE` 浅粉紫 → **`#D6A3A9→#9A5563`**(顶部偏粉玫瑰金 → 底部深玫瑰金)
|
||||
- 背景上直接书写的元素提亮(背景变深需保可读):6 处模块标题 `#7B2D5E`→`#FDE8EC`(浅粉白)、装饰线+4 处情绪百分比小字 `#B98AA6`→`#EAC6CE`、斜体行+版权 2 行 `#A98A9C`→`#DDBCC4`
|
||||
- **不动**:白卡 #FFFFFF×15、粉 chips #F7E8F2×20、卡片内深粉文字/粉强调等全部内部元素(遵守「改底色=改背景色」原则)
|
||||
- **备份**:原浅粉紫版 → `王微斯账号设定卡_浅粉紫底.bak.svg`(同目录,可回退)
|
||||
- **验证**(CDP 断言+截图 shot-wangweisi-rosegold.png):bg0=#D6A3A9 / bg1=#9A5563 / 6 标题 #FDE8EC / 装饰线 #EAC6CE / 版权 #DDBCC4 / 白卡 15 / 粉 chips 20 全保留 ✓
|
||||
- 验证脚本保留 `cdp-verify-rosegold.mjs`(断言渐变 stop + 背景上文字 + 内部元素计数,可复用)
|
||||
|
||||
## 王微斯卡片·定稿玫瑰金底(12:03-12:05)
|
||||
- 用户反馈:「感觉那个(玫瑰金)更适合 这个账号调性」→ 玫瑰金底定稿,当前 `王微斯账号设定卡.svg`(#D6A3A9→#9A5563)为最终版;浅粉紫备份 `_浅粉紫底.bak.svg` 保留可回退
|
||||
- **06 文档固化**(源+dsh 同步,diff 一致):
|
||||
- 映射表甜美行:深莓紫(深沉时)|**玫瑰金 #D6A3A9→#9A5563(明快时,如王微斯·定稿)**
|
||||
- 浅色系推导改「提亮+降饱和至中浅明度(约 65-90% 亮度)」+ 新增「背景上直接书写的文字随背景深浅提亮保可读」(玫瑰金底:模块标题 #FDE8EC/装饰线注释 #EAC6CE/斜体版权 #DDBCC4)
|
||||
- 浅色系表更新:背景=玫瑰金(更浅可回退浅粉紫)、模块标题=#FDE8EC、情绪小字=#EAC6CE;表后加注释(背景越浅越近深粉字、越深越近浅粉白)
|
||||
- **SKILL.md 功能六配色段更新**(源+dsh diff 一致):「只换背景色」→「底色决定色系」两系完整表述 + 玫瑰金映射 + 浅色系推导 + 背景文字提亮
|
||||
- **MEMORY.md**:浅色系定稿色值更新为玫瑰金
|
||||
- 经验固化:用户试色闭环=「先试浅→再试深一点有色相→选定稿」,配色迭代时**备份每个候选版**(.bak.svg)防回退;色系表以账号定稿为准反哺文档,避免文档停留在试色中间态
|
||||
|
||||
## 王微斯卡片·叙事结构箭头位置修复(12:07-12:08)
|
||||
- 用户反馈:「叙事结构 卡片之间的箭头 都偏到 卡片里面了」
|
||||
- **根因**:卡片缝隙仅 15px,但箭头 line 只画了 2px(如 x1=153 x2=155),marker 宽 6px(refX=8 → 尾部在 x2-4.8),箭头主体伸进左侧卡片约 3px
|
||||
- **修复**:4 条箭头线拉长占满缝隙——154→166 / 273→285 / 392→404 / 511→523(各 12px;卡右缘+1 起、卡左缘-2 止),箭头整体居中于两卡之间,尾部不碰卡1、尖部离卡2 左缘 2px
|
||||
- **验证**(CDP 断言):4 线 x1/x2 全对、线长≥11px(arrowsInGap=true)、背景/标题/白卡15/粉chips20 不受影响 ✓
|
||||
- 经验固化:**卡片间箭头/连线必须按缝隙宽度画线**(缝隙宽-3~4px 的线长),marker 宽度决定最小线长(线长须 ≥ marker 渲染宽 + 1px,否则箭头伸进相邻元素);同款五步卡布局(104 宽×5 + 15px 缝隙)复用时直接套用此坐标
|
||||
|
||||
## 固化:功能三自动联动生成设定卡 + 双向联动维护(12:12-12:15)
|
||||
- 用户需求:「给用户获取账号设定时 自动生成设定卡片,固化到skill中,修改时两边一起修改」——①功能三产出设定后**自动**生成卡片(无需用户开口)②设定 md 与卡片 SVG **双向联动**同步修改,全部固化到技能
|
||||
- **SKILL.md**(源+dsh):功能总览表功能六「功能三之后可选」→「**功能三自动联动(默认)**」;部署禁用说明补「删除功能六文件则不再自动生成(存量可手动)」;功能三核心流程加 **Step 7(自动联动)**;功能六章节加「自动触发(默认)」+「双向联动维护」
|
||||
- **03_提炼账号设定.md**(源+dsh):新增 **Step 7:自动生成账号设定卡片(联动)**——默认自动触发(跳过条件:功能六文件删除/用户明确只要设定)+ 双向联动维护 3 条规则(改设定→卡片同步、改卡片→设定同步、术语改名→两边一起改)+ grep 一致性校验
|
||||
- **06_生成账号设定卡片.md**(源+dsh):使用场景改「默认由功能三自动联动触发」;触发场景处理加「默认自动触发」说明;情况 1 注「自动流程已覆盖」;环境路径后新增「**双向联动维护**」章节(含历史改名案例:内容路线→叙事结构、核心气质→内容标签、六大内容基因→内容基因·TOP6)
|
||||
- 验证:3 文件源=dsh diff 全一致;MEMORY.md 联动规则已固化
|
||||
- 经验固化:功能联动类需求固化落点=SKILL.md(总览/章节)+ 上游功能文档(自动触发的源头)+ 下游功能文档(触发判定与维护规则);「修改时两边一起修改」的准确载体=设定 md ↔ 卡片 SVG 两个产出文件(非源技能/dsh,那是既有同步策略)
|
||||
|
||||
## mcn-video-prompt 提示词输出结构 4层→6层重排(用户定稿,14:29)
|
||||
- **新结构**:①主体列表 → ②画面质感(视觉风格+摄影质感参数,视觉基调前置)→ ③色调光影(光源+主光方向+亮暗区+色调/色温)→ ④主体设定(人物/场景/构图)→ ⑤镜头叙事 → ⑥负面提示词;原「②设定」层拆为画面质感/色调光影/主体设定三层,视觉基调前置(模型对提示词开头权重最高)
|
||||
- **修改文件 3 个**(逐条改+验证):
|
||||
- `subskill/mcn-video-prompt/references/制作流程/分镜提示词制作流程.md`(权威出处,9 处):3.2/3.2.2 标题、改造原则、3.2.1 思考清单层号映射、3.2.2 输出格式块整体重排、6层×5类型适配表(列扩为⑦列)、Step5 校验②结构完整性、总流程 3 处引用、对比表技术参数行
|
||||
- `subskill/mcn-video-prompt/SKILL.md`(8 处):F4 提示词内部结构、F2/F4 详细方法论引用×2、剧本分析前置段、工作流 Step3、提示词模板节×2、自检⑧结构完整性;frontmatter updated_at=2026-08-31+last_change
|
||||
- `subskill/mcn-video-prompt/references/制作流程/长视频分段制作流程.md`(4 处):一致性锁定表(②设定-视觉风格→②画面质感等)、简单段/复杂段输出模板重排为 6 层
|
||||
- 验证:全局 grep 旧层号零残留("4层"仅剩 last_change 变更描述+L189 F2"4层退化策略"无关概念);6层落位 SKILL.md×8/分镜提示词制作流程×7/长视频分段×3
|
||||
- 注:dsh 副本(storyboard-prompt)未同步,按 .dsh 单向只读约定待用户授权
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
## 核心技能
|
||||
- **短视频脚本创作** `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写作复盘(读自有 mcn-plugin.db 副本+dshworkspace 榜单 JSON)+📁产出浏览(桌面 MCNSkill项目/ 账号层直挂)+❓帮助页。**AI 任务触发(08-27 定版;归 mcn-workshop 空间)**:数据页按钮→`confirmRun()` 确认弹窗→`executeTask()` `POST /api/run` 写 `~/.workbuddy/workbuddy.db` automations(once+now,cwds=`D:\AgentSkill\mcn-workshop`)→WorkBuddy 自动执行创建会话→左侧会话栏 mcn-workshop 空间可见;轮询 `GET /api/run/status?id=`(读 automations/automation_runs/automation_runtime_state:running=IN_PROGRESS/running、done=DONE/COMPLETED/SUCCESS、error=ERROR/CANCELLED;5s 间隔 30min 上限)→done/error 恢复按钮(.btn-busy「执行中…」);**用户明确不同步 dsh/Lite**(刻意 divergence);AI脚本选题(08-28):关注账号「创作」→「AI脚本」`confirmAiScript` 选题 textarea→`POST /api/ai/topics`(查库带账号摘要→S5 方法论 prompt→Dify chat-messages recall 即时返回约50s);Dify key 内置 `scripts/MCN_CYLG_API.py` 的 `DEFAULT_CYLG_KEY`(env `DIFY_MCN_CYLG_KEY` 优先;key=app-0vRaxPhvVOqzFQIa5eFzFdjD),`DIFY_URL` 默认 mydify.youmanvideo.com/v1、model 默认 gemini、curl 中文被 CDN 拦推荐 Python、search 易超时用 recall
|
||||
- **抖音账号分析 mcn-dou-analysis(主版本 08-27)**:源=`project/短视频脚本创作/V1.0/subskill/mcn-dou-analysis/`;dsh 副本=`C:\Users\maidou\.dsh\skills\mcn-dou-analysis\`;旧路径 `project/短视频账号分析/case_1/` 已废弃。五大功能:F1账号信息/F2视频列表(13列Excel)/F3提炼账号设定(人设卡12段)/F4分析视频数据(逐条→汇总两段式)/F5解析视频(单视频MCP)。红线:达人账号表15列(仅用户要求增改)、TOP6筛选(点赞+分享降序,权威参数在SKILL.md)、≥60条停止滚动、29赛道枚举、源技能只读单向同步、技能边界(脚本必须切脚本创作技能)、浏览器9223调试实例+标签页复用
|
||||
- **MCN工作台(08-27,仅 V1.0)** `project/短视频脚本创作/V1.0/mcn-work-shop/`(server.js+public/,零依赖 Node,端口8899自动避让);触发「打开工作台」→ 启动服务+内置浏览器打开。hash 路由:首页+📈账号周榜/👥热门账号/账号详情/视频详情/AI写脚本/AI写作复盘(读自有 mcn-plugin.db 副本+dshworkspace 榜单 JSON)+📁产出浏览(桌面 MCNSkill项目/ 账号层直挂)+❓帮助页。**首页顶部数据预览条(08-31,参考 dsh-plugin-mcn 首页 stats 移植)**:`/api/dsh/stats` 5 项=热门账号(accounts)/视频解析(analyses)/AI写脚本(rewrites)/AI写作复盘(reviews type='ai')/AI写作评分(writingScore=最新20条AI复盘平均分),条带=7%品牌蓝底+数值26px粗体+右分隔线(样式在 style.css .home-stats);**AI 任务触发(08-27 定版;归 mcn-workshop 空间)**:数据页按钮→`confirmRun()` 确认弹窗→`executeTask()` `POST /api/run` 写 `~/.workbuddy/workbuddy.db` automations(once+now,cwds=`D:\AgentSkill\mcn-workshop`)→WorkBuddy 自动执行创建会话→左侧会话栏 mcn-workshop 空间可见;轮询 `GET /api/run/status?id=`(读 automations/automation_runs/automation_runtime_state:running=IN_PROGRESS/running、done=DONE/COMPLETED/SUCCESS、error=ERROR/CANCELLED;5s 间隔 30min 上限)→done/error 恢复按钮(.btn-busy「执行中…」);**用户明确不同步 dsh/Lite**(刻意 divergence);AI脚本选题(08-28):关注账号「创作」→「AI脚本」`confirmAiScript` 选题 textarea→`POST /api/ai/topics`(查库带账号摘要→S5 方法论 prompt→Dify chat-messages recall 即时返回约50s);Dify key 内置 `scripts/MCN_CYLG_API.py` 的 `DEFAULT_CYLG_KEY`(env `DIFY_MCN_CYLG_KEY` 优先;key=app-0vRaxPhvVOqzFQIa5eFzFdjD),`DIFY_URL` 默认 mydify.youmanvideo.com/v1、model 默认 gemini、curl 中文被 CDN 拦推荐 Python、search 易超时用 recall
|
||||
- **抖音账号分析 mcn-dou-analysis(主版本 08-27)**:源=`project/短视频脚本创作/V1.0/subskill/mcn-dou-analysis/`;dsh 副本=`C:\Users\maidou\.dsh\skills\mcn-dou-analysis\`;旧路径 `project/短视频账号分析/case_1/` 已废弃。**六大功能(08-31 加 F6)**:F1账号信息/F2视频列表(13列Excel)/F3提炼账号设定(人设卡12段)/F4分析视频数据(逐条→汇总两段式)/F5解析视频(单视频MCP)/**F6生成账号设定卡片**(SVG 长图 680×~1716,模板=旧梦留声机样例)。红线:达人账号表15列(仅用户要求增改)、TOP6筛选(点赞+分享降序,权威参数在SKILL.md)、≥60条停止滚动、29赛道枚举、源技能只读单向同步、技能边界(脚本必须切脚本创作技能)、浏览器9223调试实例+标签页复用。**F6 卡片配色分两系(08-31 定型)**:**深色系(深色长图风,深沉/浓郁调性,如旧梦留声机)**=深棕红渐变底 #2B0F0A→#1A0805 + 浅米卡 #FAEEDA + 深色字 + 珊瑚红强调;**浅色系(浅色甜宠风,明快/甜美/治愈调性,如王微斯·定稿玫瑰金)**=玫瑰金渐变底 #D6A3A9→#9A5563 + 白卡 #FFFFFF + 粉边 #E8C9DC + 深粉字 #7B2D5E + 粉强调 #C0457F→#E876A9;背景上直接书写的文字随背景深浅提亮(玫瑰金底:模块标题 #FDE8EC/注释 #EAC6CE/版权 #DDBCC4);更浅可回退浅粉紫 #F8EDF6→#F1E0EE;底色决定色系,**同一账号必须固定色系**,跨账号可不同;禁跨系混用。模块名统一为「叙事结构」「内容标签」「**内容基因·TOP6**」。F6 触发场景处理(07情况表+三问判定链)在 06 文档。**功能三→功能六自动联动(08-31 固化)**:功能三产出 `{账号名}账号设定.md` 后**自动生成设定卡**(无需用户开口,删除 06 文件即禁用自动);**设定 md ↔ 卡片 SVG 双向联动维护**——改设定→卡片同步(内容级重生成/单点改 SVG)、改卡片→设定同步、术语改名→两边一起改(历史案例:内容路线→叙事结构、核心气质→内容标签),交付前 grep 校验关键术语两文件一致;规则固化于 SKILL.md(功能三 Step7/功能六自动触发)+ 03 文档 Step7 + 06 文档触发场景/双向联动维护
|
||||
- **短视频分镜脚本** `project/短视频提示词生成/`(08-27 去 V1.0 层):4功能 F1反推图片/F2反推视频/F3生成图片/F4生成分镜;三层引用架构 参考skills→references/→references-add/(运行时优先;开发机不启用 references-add/,dsh/用户环境启用);知识库15维度(01-12用户+13音效+14构图+15风格);F4输出=主题+导演阐述+代码块(>5s时间戳分段/≤5s单段);提示词模板类型分档(A剧情/B单人Vlog/C旁白知识/D产品展示/E纯氛围)+4层×5类型适配表;公式只留时间预算表等内部环节,提示词正文只写时长结论;seedance25 时间测算体系(语速6公式+动作14+运镜7+帧级换算+超调6级);dsh 副本无独有内容需回流(唯一独有项 `name: storyboard-prompt` 环境保留)
|
||||
|
||||
## 创作技能关键规则(权威源:SKILL.md 关键规则 + references/创作流程规范.md)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 截图王微斯账号设定卡 SVG 渲染效果(自检配色)
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && (decodeURIComponent(t.url).includes('王微斯') || t.url.includes('8899')));
|
||||
if (!page) { console.log('ERR: no wangweisi/8899 page open'); 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, 300) }; return r.result.value; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
|
||||
// 导航到本地 SVG 文件并强制重载(同一 URL 导航不触发重载)
|
||||
const url = 'file:///C:/Users/maidou/Desktop/MCNSkill项目/王微斯/王微斯账号设定卡.svg';
|
||||
await send('Page.navigate', { url });
|
||||
await sleep(1200);
|
||||
await send('Page.reload', { ignoreCache: true });
|
||||
await sleep(2500);
|
||||
const r = await send('Page.captureScreenshot', { format: 'png' });
|
||||
const fs = await import('node:fs');
|
||||
fs.writeFileSync('shot-wangweisi-card.png', Buffer.from(r.data, 'base64'));
|
||||
console.log('截图完成: shot-wangweisi-card.png');
|
||||
ws.close();
|
||||
@@ -0,0 +1,36 @@
|
||||
// 验证:周榜功能列按钮统一「白底带边框」(btn-view / btn-import)
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && t.url.includes('localhost:8899'));
|
||||
if (!page) { console.log('ERR: no workbuddy page open'); 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 goto = async (hash) => { await send('Page.navigate', { url: 'http://localhost:8899/' + hash }); await sleep(1500); await send('Page.reload', { ignoreCache: true }); await sleep(3000); };
|
||||
|
||||
// 1) 功能页 #/ranking:查看 + 分析按钮
|
||||
await goto('#/ranking');
|
||||
const rank = await ev(`(() => {
|
||||
const btns = [...document.querySelectorAll('.table tbody .cell-act .btn')];
|
||||
return btns.slice(0, 4).map((b) => ({ txt: b.textContent.trim(), bg: getComputedStyle(b).backgroundColor, border: getComputedStyle(b).borderTopColor, color: getComputedStyle(b).color }));
|
||||
})()`);
|
||||
console.log('【功能页功能列】');
|
||||
for (const b of rank) console.log(' ', b.txt, '| bg:', b.bg, '| border:', b.border);
|
||||
|
||||
// 2) 首页预览:查看 + 关注按钮
|
||||
await goto('#/');
|
||||
const home = await ev(`(() => {
|
||||
const btns = [...document.querySelectorAll('#rankPreviewList .rank-row:not(.rank-head) .rk-act .btn')];
|
||||
return btns.slice(0, 4).map((b) => ({ txt: b.textContent.trim(), bg: getComputedStyle(b).backgroundColor, border: getComputedStyle(b).borderTopColor }));
|
||||
})()`);
|
||||
console.log('【首页预览功能列】');
|
||||
for (const b of home) console.log(' ', b.txt, '| bg:', b.bg, '| border:', b.border);
|
||||
|
||||
const allBtns = [...rank, ...home];
|
||||
const ok = allBtns.length > 0 && allBtns.every((b) => b.bg === 'rgb(255, 255, 255)' && b.border !== 'rgb(245, 246, 248)');
|
||||
console.log(ok ? 'PASS: 全部白底带边框' : 'CHECK');
|
||||
ws.close();
|
||||
@@ -0,0 +1,52 @@
|
||||
// 验证「首页顶部数据预览」条带(参考 dsh-plugin-mcn 首页 stats):只读测量
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && t.url.includes('localhost:8899'));
|
||||
if (!page) { console.log('ERR: no workbuddy page open'); 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, 300) }; return r.result.value; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
|
||||
// 1) 导航到首页并强制刷新加载最新代码(纯 hash 变化不触发文档重载,必须 reload)
|
||||
await send('Page.navigate', { url: 'http://localhost:8899/#/' });
|
||||
await sleep(1500);
|
||||
await send('Page.reload', { ignoreCache: true });
|
||||
await sleep(3000);
|
||||
|
||||
// 2) 检查 stats 条带结构 + 渲染值 + 样式
|
||||
const check = await ev(`(() => {
|
||||
const el = document.getElementById('homeStats');
|
||||
if (!el) return { err: 'no #homeStats' };
|
||||
const items = [...el.querySelectorAll('.home-stat-item')];
|
||||
return {
|
||||
count: items.length,
|
||||
values: items.map((i) => ({ v: i.querySelector('.home-stat-value').textContent, l: i.querySelector('.home-stat-label').textContent })),
|
||||
position: el.previousElementSibling ? el.previousElementSibling.className : '(first in .home)',
|
||||
next: el.nextElementSibling ? el.nextElementSibling.className : '',
|
||||
};
|
||||
})()`);
|
||||
console.log('条带结构:', JSON.stringify(check, null, 1));
|
||||
|
||||
// 3) computed 样式核对(条带底色 / 数字色 / item 分隔线)
|
||||
const styleCheck = await ev(`(() => {
|
||||
const el = document.getElementById('homeStats');
|
||||
if (!el) return { err: 'no el' };
|
||||
const cs = getComputedStyle(el);
|
||||
const v = getComputedStyle(el.querySelector('.home-stat-value'));
|
||||
const item = el.querySelector('.home-stat-item');
|
||||
const its = getComputedStyle(item);
|
||||
return {
|
||||
statsBg: cs.backgroundColor, statsRadius: cs.borderRadius, statsPadding: cs.padding,
|
||||
valueColor: v.color, valueSize: v.fontSize, valueWeight: v.fontWeight,
|
||||
itemBorderRight: its.borderRightWidth + ' ' + its.borderRightStyle,
|
||||
itemLast: getComputedStyle(el.querySelector('.home-stat-item:last-child')).borderRightWidth,
|
||||
};
|
||||
})()`);
|
||||
console.log('样式:', JSON.stringify(styleCheck));
|
||||
|
||||
ws.close();
|
||||
console.log('DONE');
|
||||
@@ -0,0 +1,147 @@
|
||||
// 验证「账号设定」卡片版/文字版切换(账号详情页 persona tab)
|
||||
// 用例1:旧梦留声机(1629) 有卡片 → 默认卡片版 + 可切换文字版
|
||||
// 用例2:王微斯(1627) 无卡片 → 直接文字版,无切换控件
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && t.url.includes('localhost:8899'));
|
||||
if (!page) { console.log('ERR: no workbuddy page open'); 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, 300) }; return r.result.value; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
|
||||
const shot = async (name) => {
|
||||
try {
|
||||
const r = await send('Page.captureScreenshot', { format: 'png' });
|
||||
const fs = await import('node:fs');
|
||||
fs.writeFileSync('shot-' + name + '.png', Buffer.from(r.data, 'base64'));
|
||||
console.log('截图:', name);
|
||||
} catch (e) { console.log('截图失败(跳过):', e.message); }
|
||||
};
|
||||
|
||||
const goto = async (url) => {
|
||||
await send('Page.navigate', { url });
|
||||
await sleep(2500);
|
||||
await send('Page.reload', { ignoreCache: true }); // hash 路由 navigate 不重载文档,强制刷新加载最新 JS
|
||||
await sleep(3000);
|
||||
};
|
||||
|
||||
// ---- 用例1:旧梦留声机(有卡片)----
|
||||
console.log('=== 用例1 旧梦留声机 1629 ===');
|
||||
await goto('http://localhost:8899/#/account/1629');
|
||||
// 点击「账号设定」tab
|
||||
const tabClick = await ev(`(() => {
|
||||
const b = document.querySelector('[data-tab="persona"]');
|
||||
if (!b) return { err: 'no persona tab' };
|
||||
b.click();
|
||||
return 'clicked persona tab';
|
||||
})()`);
|
||||
console.log('切tab:', JSON.stringify(tabClick));
|
||||
await sleep(1800); // 等 loadPersona 两个 fetch 完成
|
||||
const s1 = await ev(`(() => {
|
||||
const sw = document.querySelector('.persona-vs-icons');
|
||||
const card = document.querySelector('.card-view');
|
||||
const md = document.querySelector('.persona-text');
|
||||
const img = card && card.querySelector('img');
|
||||
const btns = sw ? [...sw.querySelectorAll('.vi-btn')] : [];
|
||||
const active = btns.find((x) => x.classList.contains('vi-active'));
|
||||
const cardIcon = btns[0] && btns[0].querySelector('svg path');
|
||||
return {
|
||||
hasIcons: !!sw,
|
||||
btnTitles: btns.map((x) => x.title),
|
||||
activeTitle: active ? active.title : null,
|
||||
cardIconIsImage: cardIcon ? cardIcon.getAttribute('d').includes('m21 15-3.086') : null,
|
||||
cardHidden: card ? card.classList.contains('vs-hidden') : null,
|
||||
cardImgSrc: img ? img.getAttribute('src').slice(0, 60) : null,
|
||||
mdHidden: md ? md.classList.contains('vs-hidden') : null,
|
||||
mdLen: md ? md.textContent.length : 0,
|
||||
};
|
||||
})()`);
|
||||
console.log('默认卡片版状态:', JSON.stringify(s1));
|
||||
|
||||
// 切到文字版
|
||||
await ev(`(() => { const b = document.querySelector('[data-vs="text"]'); if (b) b.click(); return 1; })()`);
|
||||
await sleep(400);
|
||||
const s2 = await ev(`(() => {
|
||||
const sw = document.querySelector('.persona-vs-icons');
|
||||
const card = document.querySelector('.card-view');
|
||||
const md = document.querySelector('.persona-text');
|
||||
const active = sw && [...sw.querySelectorAll('.vi-btn')].find((x) => x.classList.contains('vi-active'));
|
||||
return {
|
||||
activeTitle: active ? active.title : null,
|
||||
activeCount: sw ? [...sw.querySelectorAll('.vi-btn.vi-active')].length : null,
|
||||
cardHidden: card ? card.classList.contains('vs-hidden') : null,
|
||||
mdHidden: md ? md.classList.contains('vs-hidden') : null,
|
||||
};
|
||||
})()`);
|
||||
console.log('切文字版后:', JSON.stringify(s2));
|
||||
|
||||
// 切回卡片版
|
||||
await ev(`(() => { const b = document.querySelector('[data-vs="card"]'); if (b) b.click(); return 1; })()`);
|
||||
await sleep(400);
|
||||
const s3 = await ev(`(() => {
|
||||
const card = document.querySelector('.card-view');
|
||||
const md = document.querySelector('.persona-text');
|
||||
const active = document.querySelector('.vi-btn.vi-active');
|
||||
return {
|
||||
activeTitle: active ? active.title : null,
|
||||
activeCount: document.querySelectorAll('.vi-btn.vi-active').length,
|
||||
cardHidden: card ? card.classList.contains('vs-hidden') : null,
|
||||
mdHidden: md ? md.classList.contains('vs-hidden') : null,
|
||||
};
|
||||
})()`);
|
||||
console.log('切回卡片版后:', JSON.stringify(s3));
|
||||
|
||||
// ---- 用例2:王微斯(无卡片)----
|
||||
console.log('=== 用例2 王微斯 1627(无卡片)===');
|
||||
await goto('http://localhost:8899/#/account/1627');
|
||||
await ev(`(() => { const b = document.querySelector('[data-tab="persona"]'); if (b) b.click(); return 1; })()`);
|
||||
await sleep(1800);
|
||||
const s4 = await ev(`(() => {
|
||||
const sw = document.querySelector('.persona-vs-icons');
|
||||
const md = document.querySelector('.persona-text');
|
||||
return {
|
||||
hasIcons: !!sw,
|
||||
mdExists: !!md,
|
||||
mdHidden: md ? md.classList.contains('vs-hidden') : null,
|
||||
mdLen: md ? md.textContent.length : 0,
|
||||
};
|
||||
})()`);
|
||||
console.log('无卡片账号状态:', JSON.stringify(s4));
|
||||
|
||||
// 回到用例1 页面截图卡片版效果(最后统一截图,失败不中断)
|
||||
await goto('http://localhost:8899/#/account/1629');
|
||||
await ev(`(() => { const b = document.querySelector('[data-tab="persona"]'); if (b) b.click(); return 1; })()`);
|
||||
await sleep(1800);
|
||||
|
||||
// 多视口宽度自检:验证图片是否随内容区自适应(不应超出 viewport 引发横向滚动)
|
||||
for (const vw of [1280, 1024, 900, 1920, 2560]) {
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: vw, height: 800, deviceScaleFactor: 1, mobile: false });
|
||||
await sleep(500);
|
||||
const info = await ev(`(() => {
|
||||
const img = document.querySelector('.card-view img');
|
||||
const body = document.querySelector('.detail-body');
|
||||
const wrap = document.querySelector('.persona-wrap');
|
||||
const page = document.querySelector('.page-detail');
|
||||
return {
|
||||
viewport: ${vw},
|
||||
docScrollW: document.documentElement.scrollWidth,
|
||||
bodyW: body ? Math.round(body.getBoundingClientRect().width) : null,
|
||||
wrapW: wrap ? Math.round(wrap.getBoundingClientRect().width) : null,
|
||||
imgW: img ? Math.round(img.getBoundingClientRect().width) : null,
|
||||
pageW: page ? Math.round(page.getBoundingClientRect().width) : null,
|
||||
bodyOverflow: body ? getComputedStyle(body).overflowX : null,
|
||||
};
|
||||
})()`);
|
||||
console.log('视口', vw, ':', JSON.stringify(info));
|
||||
}
|
||||
await send('Emulation.clearDeviceMetricsOverride');
|
||||
await sleep(300);
|
||||
await shot('persona-card-final');
|
||||
|
||||
console.log('DONE');
|
||||
ws.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,70 @@
|
||||
// 验证「账号设定」内容区左右 20px 间隔(persona-wrap padding 0 20px)
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && t.url.includes('localhost:8899'));
|
||||
if (!page) { console.log('ERR: no workbuddy page open'); 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, 300) }; return r.result.value; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
|
||||
const goto = async (url) => {
|
||||
await send('Page.navigate', { url });
|
||||
await sleep(2500);
|
||||
await send('Page.reload', { ignoreCache: true });
|
||||
await sleep(3000);
|
||||
};
|
||||
|
||||
console.log('=== 旧梦留声机 1629(有卡片)===');
|
||||
await goto('http://localhost:8899/#/account/1629');
|
||||
const tabClick = await ev(`(() => {
|
||||
const b = document.querySelector('[data-tab="persona"]');
|
||||
if (!b) return { err: 'no persona tab' };
|
||||
b.click();
|
||||
return 'clicked persona tab';
|
||||
})()`);
|
||||
console.log('切tab:', JSON.stringify(tabClick));
|
||||
await sleep(1800);
|
||||
|
||||
const s1 = await ev(`(() => {
|
||||
const wrap = document.querySelector('.persona-wrap');
|
||||
const card = document.querySelector('.card-view');
|
||||
const img = card && card.querySelector('img');
|
||||
if (!wrap) return { err: 'no .persona-wrap' };
|
||||
const cs = getComputedStyle(wrap);
|
||||
const dbody = document.querySelector('.detail-body');
|
||||
const dw = dbody ? dbody.getBoundingClientRect().width : null;
|
||||
const ww = wrap.getBoundingClientRect().width;
|
||||
const iw = img ? img.getBoundingClientRect().width : null;
|
||||
return {
|
||||
paddingLeft: cs.paddingLeft,
|
||||
paddingRight: cs.paddingRight,
|
||||
detailBodyW: dw,
|
||||
wrapW: ww,
|
||||
imgW: iw,
|
||||
imgInset: iw !== null && dw !== null ? (dw - iw).toFixed(1) : null, // 卡片比内容区窄多少(两侧总和)
|
||||
};
|
||||
})()`);
|
||||
console.log('persona padding 检查:', JSON.stringify(s1, null, 2));
|
||||
|
||||
// 切文字版后再看文字容器
|
||||
const s2 = await ev(`(() => {
|
||||
const btn = document.querySelector('.persona-vs-icons [data-vs="text"]');
|
||||
if (btn) btn.click();
|
||||
return 'switched to text';
|
||||
})()`);
|
||||
await sleep(600);
|
||||
const s3 = await ev(`(() => {
|
||||
const md = document.querySelector('.persona-text');
|
||||
if (!md) return { err: 'no .persona-text' };
|
||||
const dbody = document.querySelector('.detail-body');
|
||||
const dw = dbody ? dbody.getBoundingClientRect().width : null;
|
||||
const mw = md.getBoundingClientRect().width;
|
||||
return { mdW: mw, inset: dw !== null ? (dw - mw).toFixed(1) : null };
|
||||
})()`);
|
||||
console.log('文字版检查:', JSON.stringify(s3, null, 2));
|
||||
ws.close();
|
||||
console.log('DONE');
|
||||
@@ -0,0 +1,58 @@
|
||||
// 验证周榜数值列「窄 + 右对齐」:功能页 #/ranking + 首页预览表,只读测量
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && t.url.includes('localhost:8899'));
|
||||
if (!page) { console.log('ERR: no workbuddy page open'); 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, 300) }; return r.result.value; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
const goto = async (hash) => { await send('Page.navigate', { url: 'http://localhost:8899/' + hash }); await sleep(1500); await send('Page.reload', { ignoreCache: true }); await sleep(3000); };
|
||||
|
||||
// ===== 1) 功能页 #/ranking =====
|
||||
await goto('#/ranking');
|
||||
const rankPage = await ev(`(() => {
|
||||
const ths = [...document.querySelectorAll('.table th')];
|
||||
const wideKeys = ['comprehensiveScore','followers','newFans','newLikes','newComments','newShares'];
|
||||
const colInfo = ths.map((th) => {
|
||||
const rect = th.getBoundingClientRect();
|
||||
return { key: th.dataset.sort || '', label: th.textContent.trim().slice(0,8), wide: th.classList.contains('th-wide'), w: Math.round(rect.width), thAlign: getComputedStyle(th).textAlign };
|
||||
});
|
||||
// td 对齐抽查:第一个数据行
|
||||
const row = document.querySelector('.table tbody tr');
|
||||
const tds = row ? [...row.children] : [];
|
||||
const tdAligns = tds.map((td) => td.className + ':' + getComputedStyle(td).textAlign);
|
||||
return { colInfo, tdAligns, rowCount: document.querySelectorAll('.table tbody tr').length };
|
||||
})()`);
|
||||
console.log('【功能页 #/ranking】');
|
||||
for (const c of rankPage.colInfo) {
|
||||
if (c.wide) console.log(` ${(c.key||c.label).padEnd(20)} w=${String(c.w).padStart(4)} thAlign=${c.thAlign}`);
|
||||
}
|
||||
console.log(' td 对齐:', rankPage.tdAligns.join(' | '));
|
||||
const wideCols = rankPage.colInfo.filter((c) => c.wide);
|
||||
const rankOk = wideCols.length === 6 && wideCols.every((c) => c.w <= 100 && c.thAlign === 'right') && rankPage.tdAligns.every((t) => !t.startsWith('num') || t.endsWith('right'));
|
||||
|
||||
// ===== 2) 首页预览表 =====
|
||||
await goto('#/');
|
||||
const home = await ev(`(() => {
|
||||
const row = document.querySelector('#rankPreviewList .rank-row:not(.rank-head)');
|
||||
if (!row) return { err: 'no row' };
|
||||
const W = (sel) => Math.round(row.querySelector(sel).getBoundingClientRect().width);
|
||||
const A = (sel) => getComputedStyle(row.querySelector(sel)).textAlign;
|
||||
return {
|
||||
score: { w: W('.rk-score'), align: A('.rk-score') },
|
||||
fans: { w: W('.rk-num:nth-child(5)'), align: A('.rk-num:nth-child(5)') },
|
||||
likes: { w: W('.rk-num:nth-child(6)'), align: A('.rk-num:nth-child(6)') },
|
||||
comments: { w: W('.rk-num:nth-child(7)'), align: A('.rk-num:nth-child(7)') },
|
||||
shares: { w: W('.rk-num:nth-child(8)'), align: A('.rk-num:nth-child(8)') },
|
||||
};
|
||||
})()`);
|
||||
console.log('【首页预览表】');
|
||||
console.log(' ', JSON.stringify(home));
|
||||
const homeOk = ['score','fans','likes','comments','shares'].every((k) => home[k] && home[k].w <= 85 && home[k].align === 'right');
|
||||
|
||||
console.log(rankOk && homeOk ? 'PASS' : 'CHECK: 功能页=' + rankOk + ' 首页=' + homeOk);
|
||||
ws.close();
|
||||
@@ -0,0 +1,49 @@
|
||||
// 验证「首页账号周榜预览」5 数值列加宽 + 功能列按钮「分析」→「关注」:只读测量
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && t.url.includes('localhost:8899'));
|
||||
if (!page) { console.log('ERR: no workbuddy page open'); 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, 300) }; return r.result.value; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
|
||||
// 1) 导航首页 + 强制刷新(hash 变化不重载文档)
|
||||
await send('Page.navigate', { url: 'http://localhost:8899/#/' });
|
||||
await sleep(1500);
|
||||
await send('Page.reload', { ignoreCache: true });
|
||||
await sleep(3500);
|
||||
|
||||
// 2) 测量首页预览表各列宽 + 功能列按钮
|
||||
const check = await ev(`(() => {
|
||||
const head = document.querySelector('#rankPreviewList .rank-head');
|
||||
if (!head) return { err: 'no rank preview', html: (document.getElementById('rankPreviewList') || {}).innerHTML ? 'has content' : 'empty' };
|
||||
const row = document.querySelector('#rankPreviewList .rank-row:not(.rank-head)');
|
||||
const W = (sel) => { const el = row && row.querySelector(sel); return el ? Math.round(el.getBoundingClientRect().width) : null; };
|
||||
const cols = [...head.children];
|
||||
const actBtn = row ? row.querySelector('.rk-act .btn') : null;
|
||||
const headTxt = [...head.children].map((c) => c.textContent.trim());
|
||||
return {
|
||||
headCols: headTxt,
|
||||
widths: { score: W('.rk-score'), fans: W('.rk-num:nth-child(5)'), likes: W('.rk-num:nth-child(6)'), comments: W('.rk-num:nth-child(7)'), shares: W('.rk-num:nth-child(8)') },
|
||||
actBtnText: actBtn ? actBtn.textContent.trim() : null,
|
||||
actBtnTitle: actBtn ? actBtn.title : null,
|
||||
actBtnHref: actBtn ? (actBtn.getAttribute('href') || null) : null,
|
||||
rowCount: document.querySelectorAll('#rankPreviewList .rank-row').length - 1,
|
||||
dataCardsStill: !!document.querySelector('.data-cards'),
|
||||
statsStill: !!document.getElementById('homeStats'),
|
||||
};
|
||||
})()`);
|
||||
console.log(JSON.stringify(check, null, 1));
|
||||
|
||||
// 3) 校验:5 列 >= 80px 且按钮为「关注」
|
||||
const widths = check.widths || {};
|
||||
const colsOk = ['score', 'fans', 'likes', 'comments', 'shares'].every((k) => widths[k] >= 80);
|
||||
const btnOk = check.actBtnText === '关注' && (check.actBtnTitle || '').includes('关注');
|
||||
console.log('列宽达标(>=80):', colsOk, '| 按钮为关注:', btnOk);
|
||||
console.log(colsOk && btnOk ? 'PASS' : 'CHECK');
|
||||
|
||||
ws.close();
|
||||
@@ -0,0 +1,51 @@
|
||||
// 验证:首页周榜 tab 切换(账号周榜/作品热榜/点赞日榜/涨粉日榜,后 3 个占位)
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && t.url.includes('localhost:8899'));
|
||||
if (!page) { console.log('ERR: no workbuddy page open'); 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');
|
||||
|
||||
await send('Page.navigate', { url: 'http://localhost:8899/#/' }); await sleep(1500);
|
||||
await send('Page.reload', { ignoreCache: true }); await sleep(3000);
|
||||
|
||||
// 初始状态 + 逐个点击 tab
|
||||
const states = [];
|
||||
const initial = await ev(`(() => ({
|
||||
tabs: [...document.querySelectorAll('.rank-tab')].map((t) => ({ txt: t.textContent.trim(), active: t.classList.contains('active') })),
|
||||
toolsHidden: document.getElementById('rankTools').hidden,
|
||||
listHidden: document.getElementById('rankPreviewList').hidden,
|
||||
phHidden: document.getElementById('rankPlaceholder').hidden,
|
||||
}))()`);
|
||||
states.push({ clicked: '(初始)', ...initial });
|
||||
|
||||
for (const name of ['作品热榜', '点赞日榜', '涨粉日榜', '账号周榜']) {
|
||||
const s = await ev(`(() => {
|
||||
const t = [...document.querySelectorAll('.rank-tab')].find((x) => x.textContent.trim() === ${JSON.stringify(name)});
|
||||
t.click();
|
||||
return {
|
||||
active: [...document.querySelectorAll('.rank-tab')].find((x) => x.classList.contains('active')).textContent.trim(),
|
||||
toolsHidden: document.getElementById('rankTools').hidden,
|
||||
listHidden: document.getElementById('rankPreviewList').hidden,
|
||||
phHidden: document.getElementById('rankPlaceholder').hidden,
|
||||
phText: document.getElementById('rankPlaceholder').textContent.trim(),
|
||||
};
|
||||
})()`);
|
||||
await sleep(150);
|
||||
states.push({ clicked: name, ...s });
|
||||
}
|
||||
console.log(JSON.stringify(states, null, 1));
|
||||
|
||||
const ok = states[0].tabs.length === 4
|
||||
&& states[0].tabs[0].active && !states[0].toolsHidden && !states[0].listHidden && states[0].phHidden
|
||||
&& states[1].clicked === '作品热榜' && states[1].toolsHidden && states[1].listHidden && !states[1].phHidden && states[1].phText.includes('建设中')
|
||||
&& states[2].clicked === '点赞日榜' && states[2].toolsHidden && states[2].phText.includes('建设中')
|
||||
&& states[3].clicked === '涨粉日榜' && states[3].phText.includes('建设中')
|
||||
&& states[4].clicked === '账号周榜' && !states[4].toolsHidden && !states[4].listHidden && states[4].phHidden;
|
||||
console.log(ok ? 'PASS' : 'CHECK');
|
||||
ws.close();
|
||||
@@ -0,0 +1,56 @@
|
||||
// 验证「账号周榜」5 个数值列加宽(.th-wide min-width:110px)
|
||||
// 只读测量,零副作用
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && t.url.includes('#/ranking'));
|
||||
if (!page) page = list.find((t) => t.type === 'page' && t.url.includes('localhost:8899'));
|
||||
if (!page) { console.log('ERR: no workbuddy page open'); 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, 300) }; return r.result.value; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
|
||||
// 1) 导航到周榜页(若不在)并刷新加载最新 CSS
|
||||
if (!page.url.includes('#/ranking')) {
|
||||
await send('Page.navigate', { url: 'http://localhost:8899/#/ranking' });
|
||||
await sleep(2000);
|
||||
}
|
||||
await send('Page.reload', { ignoreCache: true });
|
||||
await sleep(3000);
|
||||
|
||||
// 2) 测量表头各列宽度
|
||||
const cols = await ev(`(() => {
|
||||
const ths = [...document.querySelectorAll('.table th')];
|
||||
const wideKeys = ['comprehensiveScore', 'newFans', 'newLikes', 'newComments', 'newShares'];
|
||||
const rowCount = document.querySelectorAll('.table tbody tr').length;
|
||||
const result = ths.map((th) => {
|
||||
const rect = th.getBoundingClientRect();
|
||||
return { key: th.dataset.sort || '', label: th.textContent.trim().slice(0, 8), wide: th.classList.contains('th-wide'), w: Math.round(rect.width) };
|
||||
});
|
||||
return { rowCount, result };
|
||||
})()`);
|
||||
console.log('行数:', cols.rowCount);
|
||||
console.log('表头列宽:');
|
||||
for (const c of cols.result) {
|
||||
const mark = c.wide ? ' [th-wide]' : '';
|
||||
const match = c.wide ? (c.w >= 110 ? ' >=110 ✓' : ' <110 ✗') : '';
|
||||
console.log(' ' + (c.key || c.label).padEnd(22) + ' width=' + String(c.w).padStart(4) + mark + match);
|
||||
}
|
||||
const wideCols = cols.result.filter((c) => c.wide);
|
||||
const allOk = wideCols.length === 5 && wideCols.every((c) => c.w >= 110);
|
||||
|
||||
// 3) 记录 CSS 规则是否生效
|
||||
const cssCheck = await ev(`(() => {
|
||||
const sheet = [...document.styleSheets].map((s) => s.href || 'inline').join(',');
|
||||
const th = document.querySelector('.table th.th-wide');
|
||||
const cs = th ? getComputedStyle(th) : null;
|
||||
return { sheet, minWidth: cs ? cs.minWidth : 'n/a' };
|
||||
})()`);
|
||||
console.log('样式源:', cssCheck.sheet.slice(0, 160));
|
||||
console.log('computed min-width:', cssCheck.minWidth);
|
||||
|
||||
ws.close();
|
||||
console.log(allOk ? 'PASS: 5 列均已加宽(>=110px)' : 'CHECK: 存在未达标的列');
|
||||
@@ -0,0 +1,41 @@
|
||||
// 验证:「AI写作复盘」→「AI脚本诊断」全量改名 + 首页周榜列表底色
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && t.url.includes('localhost:8899'));
|
||||
if (!page) { console.log('ERR: no workbuddy page open'); 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 goto = async (hash) => { await send('Page.navigate', { url: 'http://localhost:8899/' + hash }); await sleep(1500); await send('Page.reload', { ignoreCache: true }); await sleep(3000); };
|
||||
|
||||
// 1) 首页:数据预览 label + 快捷卡片 + 周榜列表底色 + 残留检查
|
||||
await goto('#/');
|
||||
const home = await ev(`(() => {
|
||||
const stats = [...document.querySelectorAll('#homeStats .home-stat-item')].map((i) => i.querySelector('.home-stat-label').textContent);
|
||||
const cards = [...document.querySelectorAll('.data-card')].map((c) => c.querySelector('.data-card-title').textContent);
|
||||
const rk = document.getElementById('rankPreviewList');
|
||||
const rkBg = rk ? getComputedStyle(rk).backgroundColor : null;
|
||||
const bodyTxt = document.body.textContent;
|
||||
return { stats, cards, rkBg, residualOld: bodyTxt.includes('AI写作复盘'), residualNew: bodyTxt.includes('AI脚本诊断') };
|
||||
})()`);
|
||||
console.log('【首页】');
|
||||
console.log(' 数据预览 label:', home.stats.join(' / '));
|
||||
console.log(' 快捷卡片:', home.cards.join(' / '));
|
||||
console.log(' 周榜列表底色:', home.rkBg);
|
||||
console.log(' 残留旧名:', home.residualOld, '| 出现新名:', home.residualNew);
|
||||
|
||||
// 2) 功能页 #/reviews:页面标题
|
||||
await goto('#/reviews');
|
||||
const rv = await ev(`(() => {
|
||||
const t = document.querySelector('.page-title');
|
||||
return { title: t ? t.textContent : null, hasOld: document.body.textContent.includes('AI写作复盘') };
|
||||
})()`);
|
||||
console.log('【功能页 #/reviews】页面标题:', rv.title, '| 残留旧名:', rv.hasOld);
|
||||
|
||||
ws.close();
|
||||
const ok = !home.residualOld && home.residualNew && home.rkBg && home.rkBg !== 'rgba(0, 0, 0, 0)' && rv.title === 'AI脚本诊断' && !rv.hasOld;
|
||||
console.log(ok ? 'PASS' : 'CHECK');
|
||||
@@ -0,0 +1,45 @@
|
||||
// 验证王微斯卡片玫瑰金背景渲染(断言渐变 stop + 背景上文字提亮 + 截图)
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && (decodeURIComponent(t.url).includes('王微斯') || t.url.includes('8899')));
|
||||
if (!page) { console.log('ERR: no wangweisi/8899 page open'); 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, 300) }; return r.result.value; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
|
||||
const url = 'file:///C:/Users/maidou/Desktop/MCNSkill项目/王微斯/王微斯账号设定卡.svg';
|
||||
await send('Page.navigate', { url });
|
||||
await sleep(1200);
|
||||
await send('Page.reload', { ignoreCache: true });
|
||||
await sleep(2500);
|
||||
|
||||
const check = await ev(`(() => {
|
||||
const g = document.getElementById('bgGrad');
|
||||
const stops = g ? g.querySelectorAll('stop') : [];
|
||||
const titles = [...document.querySelectorAll('text')].filter((t) => t.textContent.includes('▍')).map((t) => t.getAttribute('fill'));
|
||||
const deco = [...document.querySelectorAll('text')].find((t) => t.textContent.includes('channels requirement'))?.getAttribute('fill');
|
||||
const copy = [...document.querySelectorAll('text')].filter((t) => t.textContent.includes('数据来源')).map((t) => t.getAttribute('fill'));
|
||||
const arrows = [...document.querySelectorAll('line')].map((l) => [Number(l.getAttribute('x1')), Number(l.getAttribute('x2'))]);
|
||||
return {
|
||||
bg0: stops[0]?.getAttribute('stop-color'),
|
||||
bg1: stops[1]?.getAttribute('stop-color'),
|
||||
titles,
|
||||
deco,
|
||||
copy,
|
||||
arrows,
|
||||
arrowsInGap: arrows.every(([a, b]) => b - a >= 11), // 线长≥11px(原 2px 贴卡)
|
||||
whiteCards: document.querySelectorAll('rect[fill="#FFFFFF"]').length,
|
||||
pinkChips: document.querySelectorAll('rect[fill="#F7E8F2"]').length
|
||||
};
|
||||
})()`);
|
||||
console.log(JSON.stringify(check, null, 2));
|
||||
|
||||
const r = await send('Page.captureScreenshot', { format: 'png' });
|
||||
const fs = await import('node:fs');
|
||||
fs.writeFileSync('shot-wangweisi-rosegold.png', Buffer.from(r.data, 'base64'));
|
||||
console.log('截图完成: shot-wangweisi-rosegold.png');
|
||||
ws.close();
|
||||
@@ -0,0 +1,42 @@
|
||||
// 验证:首页周榜 tab 选中样式 = 浅底色(primary-soft) + 2px 主色下划线,非选中透明
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
let page = list.find((t) => t.type === 'page' && t.url.includes('localhost:8899'));
|
||||
if (!page) { console.log('ERR: no workbuddy page open'); 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');
|
||||
|
||||
await send('Page.navigate', { url: 'http://localhost:8899/#/' }); await sleep(1500);
|
||||
await send('Page.reload', { ignoreCache: true }); await sleep(3000);
|
||||
|
||||
const stylesOf = `(sel) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return null;
|
||||
const cs = getComputedStyle(el);
|
||||
return { bg: cs.backgroundColor, color: cs.color, border: cs.borderBottomWidth + ' ' + cs.borderBottomStyle + ' ' + cs.borderBottomColor, weight: cs.fontWeight };
|
||||
}`;
|
||||
|
||||
const states = [];
|
||||
// 初始 active = 账号周榜
|
||||
states.push({ clicked: '(初始)', active: '账号周榜', style: await ev(`(${stylesOf})('.rank-tab.active')`) });
|
||||
// 记录非 active 的样式(作品热榜)
|
||||
states.push({ clicked: '(初始)', note: '非active:作品热榜', style: await ev(`(${stylesOf})([...document.querySelectorAll('.rank-tab')].find(x => x.textContent.trim() === '作品热榜'))`) });
|
||||
|
||||
for (const name of ['作品热榜', '点赞日榜', '涨粉日榜', '账号周榜']) {
|
||||
await ev(`(() => { [...document.querySelectorAll('.rank-tab')].find((x) => x.textContent.trim() === ${JSON.stringify(name)}).click(); })()`);
|
||||
await sleep(150);
|
||||
states.push({ clicked: name, active: await ev(`([...document.querySelectorAll('.rank-tab')].find(x => x.classList.contains('active'))?.textContent.trim() || null)`), style: await ev(`(${stylesOf})('.rank-tab.active')`) });
|
||||
}
|
||||
console.log(JSON.stringify(states, null, 1));
|
||||
|
||||
// 预期:active bg=rgb(234, 241, 254) color=rgb(47, 111, 237) border=2px solid rgb(47, 111, 237) weight=600
|
||||
const isActiveStyle = (s) => s && s.bg === 'rgb(234, 241, 254)' && s.color === 'rgb(47, 111, 237)' && s.border === '2px solid rgb(47, 111, 237)' && s.weight === '600';
|
||||
const isIdleStyle = (s) => s && s.bg === 'rgba(0, 0, 0, 0)' && s.border === '2px solid rgba(0, 0, 0, 0)';
|
||||
const ok = states.every((s) => s.active === s.clicked) && isActiveStyle(states[0].style) && isIdleStyle(states[1].style) && states.slice(2).every((s) => isActiveStyle(s.style));
|
||||
console.log(ok ? 'PASS' : 'CHECK');
|
||||
ws.close();
|
||||
@@ -0,0 +1,36 @@
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
const page = list.find((t) => t.type === 'page' && t.url.includes('#/rewrites'));
|
||||
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, 300) }; return r.result.value; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
await send('Page.reload', { ignoreCache: true });
|
||||
await sleep(2000);
|
||||
await ev(`document.querySelector('[data-detail]').click()`);
|
||||
await sleep(1200);
|
||||
// 拿 AI脚本列 body 中心坐标
|
||||
const rect = await ev(`(() => {
|
||||
const ai = [...document.querySelectorAll('.rw-stack > .rw-sec')].find((s) => s.querySelector('.rw-sec-head')?.textContent === 'AI脚本');
|
||||
const r = ai.querySelector('.rw-sec-body').getBoundingClientRect();
|
||||
return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + 200), top: Math.round(r.top), h: Math.round(r.height) };
|
||||
})()`);
|
||||
console.log('aiBody rect:', JSON.stringify(rect));
|
||||
// 真实滚轮:向下滚 6 次(每次 -300 逻辑上内容上移?deltaY 正=向下滚动)
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await send('Input.dispatchMouseEvent', { type: 'mouseWheel', x: rect.x, y: rect.y, deltaX: 0, deltaY: 300 });
|
||||
await sleep(120);
|
||||
}
|
||||
await sleep(300);
|
||||
const st = await ev(`(() => {
|
||||
const secs = [...document.querySelectorAll('.rw-stack > .rw-sec')];
|
||||
const aiBody = secs.find((s) => s.querySelector('.rw-sec-head')?.textContent === 'AI脚本').querySelector('.rw-sec-body');
|
||||
const sbBody = secs.find((s) => s.querySelector('.rw-sec-head')?.textContent === 'AI分镜提示词').querySelector('.rw-sec-body');
|
||||
const vis = [...sbBody.querySelectorAll('.sb-block')].find((b) => b.getBoundingClientRect().top >= sbBody.getBoundingClientRect().top - 2);
|
||||
return { aiScrollTop: Math.round(aiBody.scrollTop), sbScrollTop: Math.round(sbBody.scrollTop), sbVis: vis ? (vis.querySelector('.sb-head')?.textContent.trim() || '').slice(0, 28) : null };
|
||||
})()`);
|
||||
console.log('滚轮后:', JSON.stringify(st));
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,34 @@
|
||||
const list = await fetch('http://localhost:9223/json').then((r) => r.json());
|
||||
const page = list.find((t) => t.type === 'page' && t.url.includes('#/rewrites'));
|
||||
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');
|
||||
// 弹窗可能还在(上轮遗留),先清理再开
|
||||
await ev(`document.querySelectorAll('.modal-mask').forEach((m) => m.remove()); document.querySelector('[data-detail]') && document.querySelector('[data-detail]').click()`);
|
||||
await sleep(1500);
|
||||
const rect = await ev(`(() => {
|
||||
const ai = [...document.querySelectorAll('.rw-stack > .rw-sec')].find((s) => s.querySelector('.rw-sec-head')?.textContent === 'AI脚本');
|
||||
if (!ai) return null;
|
||||
const r = ai.querySelector('.rw-sec-body').getBoundingClientRect();
|
||||
return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + 300) };
|
||||
})()`);
|
||||
console.log('rect:', JSON.stringify(rect));
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await send('Input.dispatchMouseEvent', { type: 'mouseWheel', x: rect.x, y: rect.y, deltaX: 0, deltaY: 400 });
|
||||
await sleep(80);
|
||||
}
|
||||
await sleep(400);
|
||||
const st = await ev(`(() => {
|
||||
const secs = [...document.querySelectorAll('.rw-stack > .rw-sec')];
|
||||
const aiBody = secs.find((s) => s.querySelector('.rw-sec-head')?.textContent === 'AI脚本').querySelector('.rw-sec-body');
|
||||
const sbBody = secs.find((s) => s.querySelector('.rw-sec-head')?.textContent === 'AI分镜提示词').querySelector('.rw-sec-body');
|
||||
const vis = [...sbBody.querySelectorAll('.sb-block')].find((b) => b.getBoundingClientRect().top >= sbBody.getBoundingClientRect().top - 2);
|
||||
return { aiScrollTop: Math.round(aiBody.scrollTop), sbScrollTop: Math.round(sbBody.scrollTop), sbVis: vis ? (vis.querySelector('.sb-head')?.textContent.trim() || '').slice(0, 28) : null };
|
||||
})()`);
|
||||
console.log('滚轮后:', JSON.stringify(st));
|
||||
process.exit(0);
|
||||
@@ -225,12 +225,22 @@ function getReviewFull(rewriteId) {
|
||||
function getStats() {
|
||||
const d = getDb(); if (!d) return null;
|
||||
const c = (sql) => d.prepare(sql).get().c;
|
||||
// AI写作复盘 = 已完成的 AI 脚本复盘数(script_review type='ai')
|
||||
let reviews = 0;
|
||||
try { reviews = c(`SELECT COUNT(*) c FROM script_review WHERE type='ai' AND status='done'`); } catch (e) { /* 统计失败不影响其他 */ }
|
||||
// AI写作评分 = 最新 20 条 AI 复盘总分的平均(保留 1 位小数;无评分返回 null)
|
||||
let writingScore = null;
|
||||
try {
|
||||
const rows = d.prepare(`SELECT total FROM script_review WHERE type='ai' AND status='done' AND total IS NOT NULL AND total <> '' ORDER BY updated_time DESC, id DESC LIMIT 20`).all();
|
||||
const nums = rows.map((r) => Number(r.total)).filter((v) => Number.isFinite(v));
|
||||
if (nums.length > 0) writingScore = Math.round((nums.reduce((s, v) => s + v, 0) / nums.length) * 10) / 10;
|
||||
} catch (e) { /* 评分统计失败不影响其他统计 */ }
|
||||
return {
|
||||
accounts: c(`SELECT COUNT(*) c FROM hot_accounts WHERE del_flag=0`),
|
||||
analyses: c(`SELECT COUNT(*) c FROM account_video_analysis`),
|
||||
rewrites: c(`SELECT COUNT(*) c FROM rewrite_log WHERE status='done' AND script_text IS NOT NULL AND script_text <> ''`),
|
||||
reviews: c(`SELECT COUNT(*) c FROM script_review WHERE status='done'`),
|
||||
writingScore: c(`SELECT COUNT(*) c FROM script_review WHERE type='ai' AND status='done'`),
|
||||
reviews,
|
||||
writingScore,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -100,30 +100,24 @@ function confirmSaveAccount(name, url, btn) {
|
||||
});
|
||||
}
|
||||
|
||||
// 导入外部账号数据(移植自 dsh 插件「热门账号 → 导入」):
|
||||
// 方式一:抖音账号信息(名称/抖音号/地址)→ AI 网页采集账号信息与视频列表入库
|
||||
// 方式二:本地文件夹绝对路径 → AI 读取目录分析文件结构后入库
|
||||
// 添加外部账号数据(移植自 dsh 插件「热门账号 → 导入」):
|
||||
// 填写抖音账号信息(名称/地址)→ AI 网页采集账号信息与视频列表入库
|
||||
// 提交复用 executeTask 任务链路(/api/run → 左侧会话栏可见 → 完成自动刷新)
|
||||
function openImportAccounts(btn) {
|
||||
const mask = document.createElement('div'); mask.className = 'modal-mask';
|
||||
mask.innerHTML = `
|
||||
<div class="modal" style="max-width:520px">
|
||||
<h3>${svgIcon('download', 18)} 导入外部账号数据</h3>
|
||||
<h3>${svgIcon('user-plus', 18)} 添加账号数据</h3>
|
||||
<div class="import-form">
|
||||
<p class="dim" style="margin-bottom:10px">方式一:填写抖音账号信息,AI 通过抖音网页获取账号信息与视频列表。</p>
|
||||
<p class="dim" style="margin-bottom:10px">填写抖音账号信息,AI 通过抖音网页获取账号信息与视频列表。</p>
|
||||
<div class="f-row"><label>账号名称</label>
|
||||
<input class="input" placeholder="如:旧梦留声机" data-imp-name></div>
|
||||
<div class="f-row"><label>抖音号</label>
|
||||
<input class="input" placeholder="如:jiu_meng_ai" data-imp-did></div>
|
||||
<div class="f-row"><label>账号地址(可选)</label>
|
||||
<input class="input" placeholder="https://www.douyin.com/user/..." data-imp-url></div>
|
||||
<div class="divider"></div>
|
||||
<p class="dim" style="margin-bottom:10px">方式二:输入本地数据文件夹绝对路径,AI 读取目录分析文件结构后导入。</p>
|
||||
<div class="f-row"><input class="input" placeholder="本地文件夹路径,如 D:\\账号对标" data-imp-dir></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="ghost" data-cancel>取消</button>
|
||||
<button class="primary" data-ok>提交导入</button>
|
||||
<button class="primary" data-ok>提交添加</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(mask);
|
||||
@@ -131,20 +125,13 @@ function openImportAccounts(btn) {
|
||||
mask.addEventListener('click', (e) => { if (e.target === mask) close(); });
|
||||
mask.querySelector('[data-cancel]').addEventListener('click', close);
|
||||
mask.querySelector('[data-ok]').addEventListener('click', () => {
|
||||
const dir = (mask.querySelector('[data-imp-dir]').value || '').trim();
|
||||
const name = (mask.querySelector('[data-imp-name]').value || '').trim();
|
||||
const did = (mask.querySelector('[data-imp-did]').value || '').trim();
|
||||
const url = (mask.querySelector('[data-imp-url]').value || '').trim();
|
||||
if (!dir && !name && !did && !url) { showToast('请填写抖音账号信息或文件夹路径'); return; }
|
||||
let prompt;
|
||||
if (dir) {
|
||||
prompt = `请帮我把本地文件夹「${dir}」中的账号数据导入:读取目录、分析文件结构,确认后写入账号列表`;
|
||||
} else {
|
||||
const parts = [name ? `账号名称「${name}」` : '', did ? `抖音号「${did}」` : '', url ? `账号地址「${url}」` : ''].filter(Boolean).join(',');
|
||||
prompt = `请帮我把抖音账号 ${parts} 导入:通过抖音网页获取账号信息与视频列表,写入账号列表并保存到本地`;
|
||||
}
|
||||
if (!name && !url) { showToast('请填写账号名称或账号地址'); return; }
|
||||
const parts = [name ? `账号名称「${name}」` : '', url ? `账号地址「${url}」` : ''].filter(Boolean).join(',');
|
||||
const prompt = `请帮我把抖音账号 ${parts} 添加:通过抖音网页获取账号信息与视频列表,写入账号列表并保存到本地`;
|
||||
close();
|
||||
executeTask({ prompt, label: '导入指令', btn });
|
||||
executeTask({ prompt, label: '添加指令', btn });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -168,9 +155,11 @@ const ICON_PATHS = {
|
||||
'file-code': '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="m10 13-2 2 2 2"/><path d="m14 17 2-2-2-2"/>',
|
||||
'table': '<path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/>',
|
||||
'file': '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
|
||||
'image': '<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>',
|
||||
'arrow-left': '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>',
|
||||
'x': '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
||||
'zap': '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
|
||||
'user-plus': '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" x2="19" y1="8" y2="14"/><line x1="22" x2="16" y1="11" y2="11"/>',
|
||||
'download': '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/>',
|
||||
};
|
||||
|
||||
@@ -298,10 +287,17 @@ function renderHome() {
|
||||
{ icon: 'trending-up', title: '账号周榜', desc: '三方数据 · 赛道/日期筛选', hash: '#/ranking' },
|
||||
{ icon: 'users', title: '账号列表', desc: '关注的账号 + 视频列表', hash: '#/accounts' },
|
||||
{ icon: 'pen', title: 'AI写脚本', desc: 'AI生成脚本+分镜提示词', hash: '#/rewrites' },
|
||||
{ icon: 'flask', title: 'AI写作复盘', desc: '检查清单+复盘报告', hash: '#/reviews' },
|
||||
{ icon: 'flask', title: 'AI脚本诊断', desc: '检查清单+复盘报告', hash: '#/reviews' },
|
||||
];
|
||||
return `
|
||||
<div class="home">
|
||||
<div class="home-stats" id="homeStats" aria-label="数据预览">
|
||||
<div class="home-stat-item"><span class="home-stat-value">–</span><span class="home-stat-label">热门账号</span></div>
|
||||
<div class="home-stat-item"><span class="home-stat-value">–</span><span class="home-stat-label">视频解析</span></div>
|
||||
<div class="home-stat-item"><span class="home-stat-value">–</span><span class="home-stat-label">AI写脚本</span></div>
|
||||
<div class="home-stat-item"><span class="home-stat-value">–</span><span class="home-stat-label">AI脚本诊断</span></div>
|
||||
<div class="home-stat-item"><span class="home-stat-value">–</span><span class="home-stat-label">AI写作评分</span></div>
|
||||
</div>
|
||||
<div class="data-cards">
|
||||
${cards.map((c) => `<button class="data-card" data-hash="${c.hash}">
|
||||
<span class="data-card-icon">${svgIcon(c.icon, 22)}</span>
|
||||
@@ -311,8 +307,13 @@ function renderHome() {
|
||||
</div>
|
||||
<div class="rank-preview">
|
||||
<div class="recent-head">
|
||||
<h3>账号周榜</h3>
|
||||
<div class="rank-tools">
|
||||
<div class="rank-tabs" role="tablist">
|
||||
<button class="rank-tab active" data-rank-tab="week" role="tab">账号周榜</button>
|
||||
<button class="rank-tab" data-rank-tab="hot" role="tab">视频热榜</button>
|
||||
<button class="rank-tab" data-rank-tab="likes" role="tab">点赞榜</button>
|
||||
<button class="rank-tab" data-rank-tab="fans" role="tab">涨粉榜</button>
|
||||
</div>
|
||||
<div class="rank-tools" id="rankTools">
|
||||
<select id="rankCat" class="input-select rank-filter" title="赛道筛选"></select>
|
||||
<button class="btn btn-sm" id="rankPrevDate" title="上一期">◀</button>
|
||||
<span id="rankDateLabel" class="rank-date-label"></span>
|
||||
@@ -320,6 +321,7 @@ function renderHome() {
|
||||
</div>
|
||||
</div>
|
||||
<div id="rankPreviewList" class="rank-preview-list"><span class="dim" style="padding:12px">加载中…</span></div>
|
||||
<div id="rankPlaceholder" class="rank-placeholder" hidden></div>
|
||||
</div>
|
||||
<div class="recent-scripts">
|
||||
<div class="recent-head"><h3>AI生成脚本</h3><a class="link" href="#/rewrites">查看全部</a></div>
|
||||
@@ -328,6 +330,29 @@ function renderHome() {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ================= 首页 数据预览(顶部统计条,参考 dsh-plugin-mcn 首页 stats) ================= */
|
||||
// 挂载时拉取一次(后端 SQLite 直查,无缓存),失败时保留占位「–」
|
||||
async function loadHomeStats() {
|
||||
const el = $('#homeStats');
|
||||
if (!el) return;
|
||||
try {
|
||||
const d = await api('/api/dsh/stats');
|
||||
const s = d.stats || {};
|
||||
const items = [
|
||||
['热门账号', s.accounts],
|
||||
['视频解析', s.analyses],
|
||||
['AI写脚本', s.rewrites],
|
||||
['AI脚本诊断', s.reviews],
|
||||
['AI写作评分', s.writingScore],
|
||||
];
|
||||
el.innerHTML = items.map(([label, value]) =>
|
||||
`<div class="home-stat-item"><span class="home-stat-value">${value ?? '–'}</span><span class="home-stat-label">${label}</span></div>`
|
||||
).join('');
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div class="home-stat-item"><span class="home-stat-value">–</span><span class="home-stat-label">数据不可用</span></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 帮助页(功能帮助 + 快速提问) ================= */
|
||||
function renderHelp() {
|
||||
return `
|
||||
@@ -442,7 +467,7 @@ async function loadRankingPreview() {
|
||||
<span class="rk-num">${esc(r.newLikes ?? '-')}</span>
|
||||
<span class="rk-num">${esc(r.newComments ?? '-')}</span>
|
||||
<span class="rk-num">${esc(r.newShares ?? '-')}</span>
|
||||
<span class="rk-act">${r.inDb && r.dbId ? `<a class="btn btn-sm btn-view" href="#/account/${r.dbId}">查看</a>` : `<button class="btn btn-sm btn-import" data-rk-import data-rk-name="${esc(r.accountName)}" data-rk-url="${esc(r.profileUrl || '')}" title="分析该账号并加入账号列表">分析</button>`}</span>
|
||||
<span class="rk-act">${r.inDb && r.dbId ? `<a class="btn btn-sm btn-view" href="#/account/${r.dbId}">查看</a>` : `<button class="btn btn-sm btn-import" data-rk-import data-rk-name="${esc(r.accountName)}" data-rk-url="${esc(r.profileUrl || '')}" title="关注该账号并加入账号列表">关注</button>`}</span>
|
||||
</div>`).join('') : '<span class="dim" style="padding:12px">暂无周榜数据</span>';
|
||||
listEl.querySelectorAll('[data-rk-import]').forEach((b) => b.addEventListener('click', () => {
|
||||
confirmSaveAccount(b.dataset.rkName, b.dataset.rkUrl || '', b);
|
||||
@@ -600,7 +625,7 @@ function openSettings() { renderRootList(); $('#settingsModal').hidden = false;
|
||||
function closeSettings() { $('#settingsModal').hidden = true; }
|
||||
|
||||
/* ================= 路由 ================= */
|
||||
const CRUMB_TITLES = { 'ranking': '账号周榜', 'accounts': '账号列表', 'rewrites': 'AI写脚本', 'reviews': 'AI写作复盘' };
|
||||
const CRUMB_TITLES = { 'ranking': '账号周榜', 'accounts': '账号列表', 'rewrites': 'AI写脚本', 'reviews': 'AI脚本诊断' };
|
||||
|
||||
function parseHash() {
|
||||
const h = (location.hash || '#/').replace(/^#/, '');
|
||||
@@ -622,9 +647,20 @@ async function router() {
|
||||
updateCrumbs('');
|
||||
view.innerHTML = renderHome();
|
||||
view.querySelectorAll('.data-card').forEach((b) => b.addEventListener('click', () => { location.hash = b.dataset.hash; }));
|
||||
// 周榜 tab 切换:账号周榜显示列表+工具条,其余 3 个新榜先占位
|
||||
view.querySelectorAll('.rank-tab').forEach((t) => t.addEventListener('click', () => {
|
||||
view.querySelectorAll('.rank-tab').forEach((x) => x.classList.toggle('active', x === t));
|
||||
const week = t.dataset.rankTab === 'week';
|
||||
view.querySelector('#rankTools').hidden = !week;
|
||||
view.querySelector('#rankPreviewList').hidden = !week;
|
||||
const ph = view.querySelector('#rankPlaceholder');
|
||||
ph.hidden = week;
|
||||
if (!week) ph.textContent = `「${t.textContent.trim()}」建设中,敬请期待`;
|
||||
}));
|
||||
loadHomeStats();
|
||||
loadRankingPreview();
|
||||
loadRecentScripts();
|
||||
view._reload = () => { loadRankingPreview(); loadRecentScripts(); };
|
||||
view._reload = () => { loadHomeStats(); loadRankingPreview(); loadRecentScripts(); };
|
||||
return;
|
||||
}
|
||||
if (name === 'help') {
|
||||
@@ -645,7 +681,7 @@ async function router() {
|
||||
}
|
||||
// 数据功能页
|
||||
if (typeof window.DataPages !== 'undefined') {
|
||||
const titles = { ranking: '账号周榜', accounts: '账号列表', account: '账号详情', video: '视频详情', rewrites: 'AI写脚本', reviews: 'AI写作复盘' };
|
||||
const titles = { ranking: '账号周榜', accounts: '账号列表', account: '账号详情', video: '视频详情', rewrites: 'AI写脚本', reviews: 'AI脚本诊断' };
|
||||
const pages = {
|
||||
'ranking': () => window.DataPages.renderRanking(view),
|
||||
'accounts': () => window.DataPages.renderAccounts(view),
|
||||
|
||||
@@ -132,17 +132,18 @@ async function renderRanking(view) {
|
||||
async function load() {
|
||||
const d = await api('/api/dsh/ranking?date=' + encodeURIComponent(state.date) + '&category=' + encodeURIComponent(state.category) + '&sortBy=' + state.sortBy + '&order=' + state.order + '&page=' + state.page + '&pageSize=' + state.pageSize);
|
||||
const di = state.dates.indexOf(state.date);
|
||||
const sortable = (key, label) => `<th class="sortable" data-sort="${key}">${label}${state.sortBy === key ? (state.order === 'asc' ? ' ↑' : ' ↓') : ''}</th>`;
|
||||
const WIDE = new Set(['comprehensiveScore', 'followers', 'newFans', 'newLikes', 'newComments', 'newShares']); // 数值列(窄+右对齐)
|
||||
const sortable = (key, label) => `<th class="sortable${WIDE.has(key) ? ' th-wide' : ''}" data-sort="${key}">${label}${state.sortBy === key ? (state.order === 'asc' ? ' ↑' : ' ↓') : ''}</th>`;
|
||||
const rows = (d.items || []).map((r) => `<tr>
|
||||
<td>${r.rank ?? '-'}</td>
|
||||
<td>${esc(r.category || '-')}</td>
|
||||
<td>${r.profileUrl ? `<a class="link" data-ext="${esc(r.profileUrl)}" title="在新标签打开账号主页">${esc(r.accountName)}</a>` : esc(r.accountName)}</td>
|
||||
<td class="score-cell">${fmt(r.comprehensiveScore)}</td>
|
||||
<td>${fmt(r.followers)}</td>
|
||||
<td>${fmt(r.newFans)}</td>
|
||||
<td>${fmt(r.newLikes)}</td>
|
||||
<td>${fmt(r.newComments)}</td>
|
||||
<td>${fmt(r.newShares)}</td>
|
||||
<td class="num score-cell">${fmt(r.comprehensiveScore)}</td>
|
||||
<td class="num">${fmt(r.followers)}</td>
|
||||
<td class="num">${fmt(r.newFans)}</td>
|
||||
<td class="num">${fmt(r.newLikes)}</td>
|
||||
<td class="num">${fmt(r.newComments)}</td>
|
||||
<td class="num">${fmt(r.newShares)}</td>
|
||||
<td class="cell-act">${r.inDb ? `<a class="btn btn-sm btn-view" href="#/account/${r.dbId}">查看</a>` : `<button class="btn btn-sm btn-import" data-import='${esc(JSON.stringify(r))}'>分析</button>`}</td>
|
||||
</tr>`).join('');
|
||||
view.innerHTML = `
|
||||
@@ -161,7 +162,7 @@ async function renderRanking(view) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<table class="table table-rank">
|
||||
<thead><tr>
|
||||
${sortable('rank', '排名')}<th>赛道</th><th>账号</th>${sortable('comprehensiveScore', '综合得分')}${sortable('followers', '粉丝')}${sortable('newFans', '新增粉丝')}${sortable('newLikes', '新增点赞')}${sortable('newComments', '新增评论')}${sortable('newShares', '新增分享')}<th class="th-act">功能</th>
|
||||
</tr></thead>
|
||||
@@ -224,7 +225,7 @@ async function renderAccounts(view) {
|
||||
</select>
|
||||
<select id="accTrack" class="input-select"><option value="">全部赛道</option>${tracks.map((t) => `<option value="${esc(t)}" ${t === state.track ? 'selected' : ''}>${esc(t)}</option>`).join('')}</select>
|
||||
<input id="accSearch" class="input" placeholder="搜索账号名 / 内容 / 赛道" value="${esc(state.search)}">
|
||||
<button class="btn" id="accImport" title="导入外部账号数据(抖音账号信息 / 本地文件夹)">${svgIcon('download', 15, 'currentColor')} 导入</button>
|
||||
<button class="btn" id="accImport" title="添加账号数据(抖音账号信息 / 账号地址)">${svgIcon('user-plus', 15, 'currentColor')} 添加</button>
|
||||
<button class="btn" id="accReset" title="重置筛选">${svgIcon('refresh', 15, 'currentColor')}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -272,11 +273,16 @@ async function renderAccountDetail(view, id) {
|
||||
const d = await api('/api/dsh/account-videos?accountId=' + accountId + '&page=1&pageSize=20').catch((e) => ({ ok: false, error: e.message }));
|
||||
if (!d.ok) { view.innerHTML = `<div class="page"><div class="page-toolbar">${toolbarBack()}</div><div class="empty">${esc(d.error || '账号不存在')}</div></div>`; bindBack(view); return; }
|
||||
const acc = d.account;
|
||||
let persona = null, analysis = null;
|
||||
let persona = null, analysis = null, personaCard = null;
|
||||
let viewMode = 'card'; // 账号设定展示模式:card(卡片版,默认)/ text(文字版)
|
||||
|
||||
async function loadPersona() {
|
||||
const r = await api('/api/dsh/account-persona?accountId=' + accountId);
|
||||
persona = r.content || null;
|
||||
const [r, c] = await Promise.all([
|
||||
api('/api/dsh/account-persona?accountId=' + accountId),
|
||||
api('/api/dsh/account-card?name=' + encodeURIComponent(acc.account_name || '')),
|
||||
]);
|
||||
persona = (r && r.content) || null;
|
||||
personaCard = (c && c.exists && c.path) ? { path: c.path } : null;
|
||||
}
|
||||
async function loadAnalysis() {
|
||||
const r = await api('/api/dsh/account-analysis?accountId=' + accountId);
|
||||
@@ -313,12 +319,30 @@ async function renderAccountDetail(view, id) {
|
||||
<div class="table-wrap"><table class="table"><thead><tr><th>发布时间</th><th class="th-id">视频ID</th><th>视频标题</th><th>点赞</th><th>评论</th><th>分享</th><th>时长</th><th class="th-act">操作</th></tr></thead><tbody>${rows || '<tr><td colspan="8" class="empty">暂无视频数据</td></tr>'}</tbody></table></div>
|
||||
${pagerHtml(dv.total, dv.page, 20, (p) => { page = p; load(); })}`;
|
||||
} else if (tab === 'persona') {
|
||||
panel = persona ? `<div class="md-body">${renderMarkdown(persona)}</div>` : `<div class="empty-state">
|
||||
<div class="empty-icon">${svgIcon('users', 40)}</div>
|
||||
<div class="empty-title">该账号暂无账号设定信息</div>
|
||||
<div class="empty-desc">为达人提炼账号设定后,可基于设定生成脚本</div>
|
||||
<button class="btn btn-view" data-extract>提炼账号设定</button>
|
||||
</div>`;
|
||||
if (!persona) {
|
||||
panel = `<div class="empty-state">
|
||||
<div class="empty-icon">${svgIcon('users', 40)}</div>
|
||||
<div class="empty-title">该账号暂无账号设定信息</div>
|
||||
<div class="empty-desc">为达人提炼账号设定后,可基于设定生成脚本</div>
|
||||
<button class="btn btn-view" data-extract>提炼账号设定</button>
|
||||
</div>`;
|
||||
} else if (!personaCard) {
|
||||
// 无卡片:直接文字版(无切换控件)
|
||||
panel = `<div class="md-body persona-text">${renderMarkdown(persona)}</div>`;
|
||||
} else {
|
||||
// 有卡片:右上角图标切换(卡片版 / 文字版),默认卡片版
|
||||
panel = `
|
||||
<div class="persona-wrap">
|
||||
<div class="persona-vs-icons">
|
||||
<button class="vi-btn ${viewMode === 'card' ? 'vi-active' : ''}" data-vs="card" title="卡片版">${svgIcon('image', 16)}</button>
|
||||
<button class="vi-btn ${viewMode === 'text' ? 'vi-active' : ''}" data-vs="text" title="文字版">${svgIcon('file-text', 16)}</button>
|
||||
</div>
|
||||
<div class="card-view ${viewMode === 'card' ? '' : 'vs-hidden'}">
|
||||
<img src="/api/file?path=${encodeURIComponent(personaCard.path)}" alt="${esc(acc.account_name)}账号设定卡">
|
||||
</div>
|
||||
<div class="md-body persona-text ${viewMode === 'text' ? '' : 'vs-hidden'}">${renderMarkdown(persona)}</div>
|
||||
</div>`;
|
||||
}
|
||||
} else {
|
||||
panel = analysis ? `<div class="md-body">${renderMarkdown(analysis)}</div>` : `<div class="empty">该账号暂无数据分析</div>`;
|
||||
}
|
||||
@@ -341,6 +365,15 @@ async function renderAccountDetail(view, id) {
|
||||
const open = text.classList.toggle('open');
|
||||
b.textContent = open ? '收起' : '展开';
|
||||
}));
|
||||
// 账号设定:卡片版 / 文字版 切换(直接 DOM 切换,不重渲染,保留滚动位置)
|
||||
view.querySelectorAll('[data-vs]').forEach((b) => b.addEventListener('click', () => {
|
||||
viewMode = b.dataset.vs;
|
||||
view.querySelectorAll('[data-vs]').forEach((x) => x.classList.toggle('vi-active', x.dataset.vs === viewMode));
|
||||
const cardEl = view.querySelector('.card-view');
|
||||
const mdEl = view.querySelector('.persona-text');
|
||||
if (cardEl) cardEl.classList.toggle('vs-hidden', viewMode !== 'card');
|
||||
if (mdEl) mdEl.classList.toggle('vs-hidden', viewMode !== 'text');
|
||||
}));
|
||||
view.querySelectorAll('[data-rewrite]').forEach((b) => b.addEventListener('click', () => {
|
||||
const r = JSON.parse(b.dataset.rewrite);
|
||||
const parsed = !!r.parsed;
|
||||
@@ -427,7 +460,7 @@ async function renderRewrites(view) {
|
||||
<td>${fmt(v.publish_time)}</td>
|
||||
<td>${(v.has_source || v.has_analysis) ? '✅已解析' : '未解析'}</td>
|
||||
<td>${v.rewrite_count > 0 ? `<button class="btn btn-sm btn-view" data-detail="${v.id}" title="查看改写脚本">${v.rewrite_count} 查看</button>` : '—'}</td>
|
||||
<td class="cell-act"><button class="btn btn-sm btn-view" data-rewrite='${esc(JSON.stringify({ id: v.id, name: v.account_name, title: cleanTitle(v.video_title) }))}'>AI改写</button></td>
|
||||
<td class="cell-act"><button class="btn btn-sm" data-rewrite='${esc(JSON.stringify({ id: v.id, name: v.account_name, title: cleanTitle(v.video_title) }))}'>AI写脚本</button></td>
|
||||
</tr>`).join('');
|
||||
view.innerHTML = `
|
||||
<div class="page">
|
||||
@@ -580,7 +613,7 @@ async function renderRewrites(view) {
|
||||
await load();
|
||||
}
|
||||
|
||||
/* ================= AI写作复盘 ================= */
|
||||
/* ================= AI脚本诊断 ================= */
|
||||
async function renderReviews(view) {
|
||||
const d = await api('/api/dsh/rewrites').catch((e) => ({ ok: false, error: e.message }));
|
||||
const DIMS = ['设定承接', '框架节奏', '钩子设置', '叙事逻辑', '极致事件', '共鸣和共情', '其他', '总分'];
|
||||
@@ -624,7 +657,7 @@ async function renderReviews(view) {
|
||||
|
||||
view.innerHTML = `
|
||||
<div class="page">
|
||||
<div class="page-toolbar">${toolbarBack()}<span class="page-title">AI写作复盘</span><span class="pager-info">按生成时间倒序 · 共 ${items.length} 条</span></div>
|
||||
<div class="page-toolbar">${toolbarBack()}<span class="page-title">AI脚本诊断</span><span class="pager-info">按生成时间倒序 · 共 ${items.length} 条</span></div>
|
||||
<div class="table-wrap"><table class="table">
|
||||
<thead><tr><th>ID</th><th>账号</th><th>生成方式</th><th>脚本标题</th><th>生成时间</th>${DIMS.map((h) => `<th style="text-align:center">${h}</th>`).join('')}<th>操作</th></tr></thead>
|
||||
<tbody>${rows || '<tr><td colspan="13" class="empty">暂无 AI 写脚本(完成改写的脚本会出现在这里)</td></tr>'}</tbody>
|
||||
|
||||
@@ -235,7 +235,7 @@ main { height: calc(100vh - 54px); }
|
||||
.root-add input { flex: 1; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 15px; }
|
||||
.root-msg { margin-top: 8px; font-size: 14px; color: var(--accent); min-height: 16px; }
|
||||
.modal-actions { text-align: right; margin-top: 14px; }
|
||||
/* 导入账号弹窗:方式一账号信息 / 方式二本地文件夹 */
|
||||
/* 添加账号弹窗:账号信息(名称/地址) */
|
||||
.import-form .f-row { margin-bottom: 10px; }
|
||||
.import-form label { display: block; font-size: 13px; color: var(--dim); margin-bottom: 4px; }
|
||||
.import-form .input { width: 100%; box-sizing: border-box; }
|
||||
@@ -265,6 +265,12 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
|
||||
|
||||
/* ================= 首页 ================= */
|
||||
.home { max-width: 960px; margin: 0 auto; padding: 24px; }
|
||||
/* 数据预览条(顶部统计,参考 dsh-plugin-mcn 首页 stats:浅蓝底横条 + 均分竖排数字/标签) */
|
||||
.home-stats { display: flex; width: 100%; box-sizing: border-box; background: rgba(47,111,237,.07); border-radius: 12px; padding: 20px 0; margin-bottom: 6px; }
|
||||
.home-stat-item { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 2px 12px; border-right: 1px solid rgba(47,111,237,.16); }
|
||||
.home-stat-item:last-child { border-right: none; }
|
||||
.home-stat-value { font-size: 26px; font-weight: 700; color: var(--primary); line-height: 1.2; font-variant-numeric: tabular-nums; }
|
||||
.home-stat-label { font-size: 13px; color: var(--dim); }
|
||||
.data-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 14px; margin: 18px 0 24px; }
|
||||
.data-card { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; cursor: pointer; text-align: left; box-shadow: var(--shadow); transition: all .15s; }
|
||||
.data-card:hover { border-color: var(--primary); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(47,111,237,.15); }
|
||||
@@ -290,8 +296,8 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
|
||||
.btn-primary { background: var(--primary); color: #fff; border-color: transparent; }
|
||||
.btn-primary:hover { background: #2a5fd0; color: #fff; }
|
||||
/* 功能列按钮区分:保存=浅灰底深字,查看=白底蓝字 */
|
||||
.btn-import { background: #f0f2f5; border-color: #e1e4e8; color: #495057; }
|
||||
.btn-import:hover { background: #e4e7eb; border-color: #d0d4da; color: #343a40; }
|
||||
.btn-import { background: #fff; border-color: #d0d4da; color: #495057; }
|
||||
.btn-import:hover { background: var(--primary-soft); border-color: var(--primary); color: var(--primary); }
|
||||
.btn-view { background: #fff; border-color: var(--primary); color: var(--primary); }
|
||||
.btn-view:hover { background: var(--primary-soft); color: var(--primary); }
|
||||
.empty { padding: 30px; text-align: center; color: var(--dim); font-size: 15px; }
|
||||
@@ -306,6 +312,15 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
|
||||
.cell-loc { max-width: 110px; } /* 账号定位列缩窄 */
|
||||
.sortable { cursor: pointer; }
|
||||
.sortable:hover { color: var(--primary); }
|
||||
.table th.th-wide { min-width: 90px; text-align: right; } /* 周榜数值列表头(综合得分/粉丝/新增粉丝/点赞/评论/分享):窄 + 右对齐(提特异性压过 .table th 默认左对齐) */
|
||||
.table td.num { text-align: right; font-size: 13px; font-variant-numeric: tabular-nums; padding-left: 8px; padding-right: 8px; } /* 数值列单元格:右对齐 + 紧凑(等宽数字,内容窄于 90px 让 min-width 生效) */
|
||||
/* 周榜表:fixed 布局固定列宽(auto 布局下 100% 表格富余空间会按比例摊薄拉宽列,min-width 压不住;fixed 下 width 为硬约束,账号列吃剩余) */
|
||||
.table.table-rank { table-layout: fixed; }
|
||||
.table.table-rank th, .table.table-rank td { overflow: hidden; text-overflow: ellipsis; }
|
||||
.table.table-rank th.th-wide { width: 90px; }
|
||||
.table.table-rank th:nth-child(1) { width: 40px; } /* 排名 */
|
||||
.table.table-rank th:nth-child(2) { width: 90px; } /* 赛道 */
|
||||
.table.table-rank th:nth-child(10) { width: 85px; } /* 功能 */
|
||||
.link { color: #4a90e2; cursor: pointer; text-decoration: none; }
|
||||
.link:hover { text-decoration: underline; }
|
||||
.link.strong { font-weight: 600; }
|
||||
@@ -329,6 +344,16 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
|
||||
.td-id { font-size: 13px; font-variant-numeric: tabular-nums; padding-left: 8px; padding-right: 8px; }
|
||||
th.th-id { width: 120px; }
|
||||
|
||||
/* ================= 账号设定 卡片版/文字版 切换(右上角图标按钮组) ================= */
|
||||
.persona-wrap { position: relative; padding: 0 20px; } /* 账号设定内容左右各 20px 间隔 */
|
||||
.persona-vs-icons { position: absolute; top: 0; right: 0; z-index: 2; display: inline-flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; background: var(--panel); }
|
||||
.vi-btn { border: none; background: none; padding: 5px 8px; cursor: pointer; color: var(--dim); display: flex; align-items: center; }
|
||||
.vi-btn + .vi-btn { border-left: 1px solid var(--border); }
|
||||
.vi-btn:hover { color: var(--primary); }
|
||||
.vi-btn.vi-active { background: var(--primary); color: #fff; }
|
||||
.card-view img { width: 100%; height: auto; display: block; border-radius: 8px; }
|
||||
.vs-hidden { display: none !important; }
|
||||
|
||||
/* ================= 信息卡 ================= */
|
||||
.info-card { border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin-bottom: 14px; background: var(--panel); }
|
||||
/* 账号信息按行分组:行内项目按内容宽度左对齐,保留行间距 */
|
||||
@@ -396,11 +421,16 @@ th.th-id { width: 120px; }
|
||||
|
||||
/* ================= 首页 账号周榜 预览 ================= */
|
||||
.rank-preview { margin: 4px 0 20px; }
|
||||
.rank-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); padding-bottom: 0; margin-bottom: 10px; align-self: stretch; }
|
||||
.rank-tab { border: none; background: transparent; color: var(--dim); padding: 6px 12px 4px; border-radius: 8px 8px 0 0; cursor: pointer; font-size: 14px; border-bottom: 2px solid transparent; }
|
||||
.rank-tab:hover { color: var(--primary); background: var(--primary-soft); }
|
||||
.rank-tab.active { color: var(--primary); font-weight: 600; background: var(--primary-soft); border-bottom: 2px solid var(--primary); }
|
||||
.rank-placeholder { border: 1px dashed var(--border); border-radius: 10px; padding: 36px 12px; text-align: center; color: var(--dim); background: var(--panel); font-size: 14px; }
|
||||
.rank-tools { display: flex; align-items: center; gap: 6px; margin-left: auto; }
|
||||
.rank-filter { padding: 3px 6px; font-size: 14px; max-width: 110px; }
|
||||
.rank-date-label { font-size: 14px; color: var(--dim); min-width: 62px; text-align: center; white-space: nowrap; }
|
||||
.rank-preview-list { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
|
||||
.rank-row { display: grid; grid-template-columns: 34px 84px minmax(80px, 1fr) 52px repeat(4, 58px) 72px; align-items: center; gap: 6px; padding: 6px 10px; font-size: 14px; border-bottom: 1px solid var(--border); }
|
||||
.rank-preview-list { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: var(--panel); box-shadow: var(--shadow); }
|
||||
.rank-row { display: grid; grid-template-columns: 34px 84px minmax(80px, 1fr) 76px repeat(4, 76px) 72px; align-items: center; gap: 6px; padding: 6px 10px; font-size: 14px; border-bottom: 1px solid var(--border); }
|
||||
.rank-row:last-child { border-bottom: none; }
|
||||
.rank-head { background: var(--panel); color: var(--dim); font-weight: 600; font-size: 13px; }
|
||||
.rk-no { color: var(--dim); font-weight: 600; }
|
||||
|
||||
@@ -55,6 +55,31 @@ function resolveSafe(rawPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- 查找账号设定卡 SVG ----
|
||||
// 产出规范:{产出根}/{达人昵称}/{达人昵称}账号设定卡.svg(功能六产出,与账号设定.md 同目录)
|
||||
// 查找顺序:规范路径 → 根下直挂 → 账号目录内扫描(防目录名/账号名不一致)
|
||||
function findAccountCard(name) {
|
||||
const safeName = String(name || '').replace(/[\\/:*?"<>|]/g, '_').trim();
|
||||
if (!safeName) return null;
|
||||
for (const root of allowedRoots()) {
|
||||
const p1 = path.join(root, safeName, safeName + '账号设定卡.svg');
|
||||
const p2 = path.join(root, safeName + '账号设定卡.svg');
|
||||
for (const c of [p1, p2]) {
|
||||
try { if (fs.existsSync(c) && fs.statSync(c).isFile()) return c; } catch (e) { /* 忽略 */ }
|
||||
}
|
||||
}
|
||||
for (const root of allowedRoots()) {
|
||||
const dir = path.join(root, safeName);
|
||||
try {
|
||||
if (fs.statSync(dir).isDirectory()) {
|
||||
const hit = fs.readdirSync(dir).find((f) => f.toLowerCase().endsWith('.svg') && f.includes('账号设定卡'));
|
||||
if (hit) return path.join(dir, hit);
|
||||
}
|
||||
} catch (e) { /* 忽略 */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- 目录树(递归,跳过隐藏项)----
|
||||
function buildTree(absPath, depth) {
|
||||
if (depth > 4) return null;
|
||||
@@ -206,6 +231,25 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (p === '/api/file') {
|
||||
// 内联输出(无 attachment 头):供 <img>/<object> 直接渲染(如账号设定卡 SVG)
|
||||
const safe = resolveSafe(q.get('path'));
|
||||
if (!safe) return sendErr(403, '路径不在允许范围');
|
||||
try {
|
||||
const st = fs.statSync(safe.abs);
|
||||
if (!st.isFile()) return sendErr(404, '文件不存在');
|
||||
res.writeHead(200, {
|
||||
'Content-Type': mimeOf(safe.abs),
|
||||
'Content-Length': st.size,
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
fs.createReadStream(safe.abs).pipe(res);
|
||||
} catch (e) {
|
||||
return sendErr(500, '读取失败: ' + e.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- 数据功能(只读工作台自有数据库副本) ----------
|
||||
const dshOk = (code, obj) => sendJSON(code, { ok: true, ...obj });
|
||||
const dshGuard = (res2) => { if (!dsh.dbAvailable()) { sendJSON(200, { ok: false, error: '未检测到工作台数据库(mcn-plugin.db),本功能不可用' }); return true; } return false; };
|
||||
@@ -257,6 +301,12 @@ const server = http.createServer((req, res) => {
|
||||
const r = dsh.getPersona(qInt('accountId', 0));
|
||||
return dshOk(200, { content: r ? r.content : null, analysisTime: r ? r.analysis_time : null });
|
||||
}
|
||||
if (p === '/api/dsh/account-card') {
|
||||
// 查询账号设定卡 SVG 文件(桌面产出,允许根目录下查找):{账号名}账号设定卡.svg
|
||||
if (dshGuard()) return;
|
||||
const hit = findAccountCard(q.get('name'));
|
||||
return dshOk(200, { exists: !!hit, path: hit || null });
|
||||
}
|
||||
if (p === '/api/dsh/account-analysis') {
|
||||
if (dshGuard()) return;
|
||||
const r = dsh.getAccountAnalysis(qInt('accountId', 0));
|
||||
|
||||
@@ -12,7 +12,7 @@ description: "MCN 达人爆款视频解析与人设卡生成技能:达人账
|
||||
|
||||
## 功能总览
|
||||
|
||||
本技能包含五大功能,**各功能独立成 MD 文件**(部署时按需手动删除不需要的功能文件即可禁用,无需改代码):
|
||||
本技能包含六大功能,**各功能独立成 MD 文件**(部署时按需手动删除不需要的功能文件即可禁用,无需改代码):
|
||||
|
||||
| # | 功能 | 说明 | 对应步骤 | 功能文件(删除即禁用) |
|
||||
|---|------|------|----------|----------------------|
|
||||
@@ -21,6 +21,7 @@ description: "MCN 达人爆款视频解析与人设卡生成技能:达人账
|
||||
| 3 | 提炼账号设定 | 基于视频 MCP 解析数据,生成人设卡(内容创作维度) | Step 3-6 | `references/feature/03_提炼账号设定.md` |
|
||||
| 4 | 分析视频数据 | 基于近4个月 Excel 数据,每月最好/最差各1条综合分析(商业/市场维度) | Step 7 | `references/feature/04_分析视频数据.md` |
|
||||
| 5 | 解析视频 | 对**单个视频**进行 MCP 解析(上传→查询→保存 content.json/analysis.json),不依赖功能三筛选流程;由账号详情视频列表「解析视频」触发 | 独立 | `references/feature/05_解析视频.md` |
|
||||
| 6 | 生成账号设定卡片 | 基于功能三产出的 `{达人昵称}账号设定.md`,生成**竖版长图 SVG 信息图**(账号设定卡):深色/浅色长图风、模块映射 12 段人设卡、含内容基因/角色类型库/情绪图/红线清单等模块 | **功能三自动联动(默认)**:产出设定后自动生成,无需单独要求 | `references/feature/06_生成账号设定卡片.md` |
|
||||
|
||||
**部署禁用说明**:删除某功能 MD 即禁用对应功能,可连带删除该功能的支撑文件:
|
||||
- 功能一:`references/浏览器搜索抖音账号操作规范.md`(功能一/二共用,**两个功能都删除时**才可连带删除)
|
||||
@@ -28,6 +29,7 @@ description: "MCN 达人爆款视频解析与人设卡生成技能:达人账
|
||||
- 功能三:`references/人设卡生成方法.md`、`references/知识库/账号设定/`、`references/解析结果格式规范.md`(**如功能四/五仍启用,解析结果格式规范需保留**,功能四目录结构以其为准)
|
||||
- 功能四:`references/账号数据分析方法.md`、`references/知识库/视频拆解/`
|
||||
- 功能五:解析结果格式规范(**如功能三/四仍启用需保留**)
|
||||
- 功能六:无连带文件(依赖功能三产出的账号设定.md 作输入,但功能三删了也可单独对有存量账号设定的账号生成卡片)。**默认联动**:功能三产出账号设定后自动生成设定卡;删除功能六文件则功能三产出后不再自动生成(存量账号仍可手动要求生成)
|
||||
|
||||
---
|
||||
|
||||
@@ -83,6 +85,8 @@ description: "MCN 达人爆款视频解析与人设卡生成技能:达人账
|
||||
| 统一JSON工具 | `scripts/json_tool.py`(create-content/create-analysis/inspect/verify-folders 四模式) |
|
||||
| 统一Excel工具 | `scripts/excel_tool.py`(select-top6/query/export-selected 三模式,含三比率计算;select-top6 默认数量按 SKILL.md「视频筛选标准」,可用 `--count N` 调整) |
|
||||
| 视频筛选脚本 | `scripts/select_top6.py`(已整合到 excel_tool.py,保留向后兼容) |
|
||||
| 功能六流程 | `references/feature/06_生成账号设定卡片.md`(生成账号设定卡片五步执行:读设定→读模板样例→模块映射→SVG手绘→验证交付) |
|
||||
| 账号设定卡模板样例 | `references/样例/`(最佳成品模板:`模板_旧梦留声机账号设定卡.svg` + 渲染参考 `参考_旧梦留声机_账号设定卡_长图版.png`;新账号复用版式骨架与样式常量,内容全替换) |
|
||||
| 产出路径配置 | `references/路径配置.md`(🔴 产出路径唯一权威配置:用户自定义路径最高优先,默认桌面 `账号分析/` 兜底) |
|
||||
| 增量库(references-add) | `references-add/`(③ 用户环境场景的增量库机制;当前①开发机/②dsh环境状态仅保留 README 机制说明,原增量已并入标准库:`references/路径配置.md` + `references/铁律避坑规则/`(含功能三`账号设定执行避坑.md` + 功能四`账号数据分析执行避坑.md`)) |
|
||||
|
||||
@@ -131,6 +135,7 @@ description: "MCN 达人爆款视频解析与人设卡生成技能:达人账
|
||||
- **Step 4**:`upload_douyin_video` 提交解析 → 等待 3-5 分钟 → `short_video_detail` 查询(content + analysis)
|
||||
- **Step 5**:`scripts/json_tool.py` 保存 content.json / analysis.json(格式规范见 `references/解析结果格式规范.md`)
|
||||
- **Step 6**:脚本创作技能 + 女娲造人术两个技能库综合生成人设卡(**12段结构**:账号核心定位→内容基因→类型库→情绪结构→视听风格→内容打法→内容规则→系列内容规划→差异化壁垒→红线清单→当前需解决的问题→设定分析说明,不含商业化变现路径分析)
|
||||
- **Step 7(自动联动)**:人设卡 `{达人昵称}账号设定.md` 落盘后,**自动执行功能六生成 `{达人昵称}账号设定卡.svg`**(无需用户单独要求);设定后续被修改(内容/模块/命名)时,**卡片必须同步修改**(双向联动维护,详见 `references/feature/06_生成账号设定卡片.md`「双向联动维护」)
|
||||
|
||||
## 六、功能四:分析视频数据
|
||||
|
||||
@@ -141,9 +146,33 @@ description: "MCN 达人爆款视频解析与人设卡生成技能:达人账
|
||||
- **两段式强制流程**:先逐条拆解(重度调用源技能,拆解文件单独保存到各视频文件夹)→ 拆解全部完成后才进入汇总分析(月度趋势/爆款共性/低表现共性/内容要素关联/差异化壁垒/商业化空间)
|
||||
- **产出**:`{达人昵称}账号数据分析.md`(8章结构:核心结论/五维诊断/月度趋势/最好视频/最差视频/差异化壁垒/商业化空间/行动建议)+ 各视频 `{视频标题}_拆解分析.md`
|
||||
|
||||
## 七、功能五:解析视频
|
||||
|
||||
> **完整执行流程见 `references/feature/05_解析视频.md`**(单视频 MCP 解析:上传→等待→查询→保存 JSON,不依赖功能三 TOP 筛选)
|
||||
|
||||
核心流程概要:
|
||||
- **触发**:账号详情视频列表「解析视频」按钮 / 用户指定单条视频
|
||||
- **流程**:`upload_douyin_video` 提交(3-5 分钟等待)→ `short_video_detail` 查询(content + analysis)→ `scripts/json_tool.py` 保存到 `{产出根目录}/{达人昵称}/视频对标/{视频文件夹}/`
|
||||
- **可选写库**:环境有 MCN 插件接口时补录 `account_video_source` 并提取选题
|
||||
|
||||
## 八、功能六:生成账号设定卡片
|
||||
|
||||
> **完整执行流程见 `references/feature/06_生成账号设定卡片.md`**(读账号设定 → 模块映射 → SVG 手绘 → 验证交付)
|
||||
|
||||
核心流程概要:
|
||||
- **自动触发(默认)**:功能三产出 `{达人昵称}账号设定.md` 后**自动执行本功能**生成设定卡,无需用户单独要求;用户单独要求生成/调整时也可独立触发
|
||||
- **触发判定(先判定再执行)**:三问判定链(①有无账号设定→无则功能三或全链路 ②卡片是否已存在→有则询问覆盖/微调 ③设定模块是否完整→缺则裁剪适配),7 种情况处理详见 `references/feature/06_生成账号设定卡片.md`「触发场景处理」
|
||||
- **双向联动维护**:设定 md 与设定卡**同步修改**(任一边改动另一边必须跟随),规则详见 06 文档「双向联动维护」
|
||||
- **输入**:功能三产出的 `{达人昵称}账号设定.md`(人设卡 12 段)
|
||||
- **产出**:`{达人昵称}账号设定卡.svg`(竖版长图信息图,`viewBox="0 0 680 ~1700"`,深色长图风),与账号设定.md 同目录
|
||||
- **方式**:读设定全文 → **读模板样例 `references/样例/模板_旧梦留声机账号设定卡.svg` + `参考_旧梦留声机_账号设定卡_长图版.png`(复用版式骨架与样式常量,内容全替换)** → 12 段模块映射 → 手写 SVG → 结构+坐标+间距三重校验 → present_files 预览确认
|
||||
- **模块裁剪适配**:按设定文件实际内容裁剪/增补模块(无流程删叙事结构、无角色库删角色库、基因按条数定列数),禁止照搬模板模块清单;每个保留模块须能在设定中找到原文
|
||||
- **配色适配**:**底色 = 整卡背景色**,跟随账号风格,禁止一律模板深棕红底(深棕红+浅米只是旧梦留声机风格);**底色分两系**——深沉/浓郁调性走深色系、明快/甜美/治愈调性走浅色系。按设定「色彩体系」真实色值 + 赛道归入背景色映射(暖怀旧→深棕红、亲子/治愈→深焦糖、科技/冷淡→深灰蓝、清新三农→深墨绿、潮流都市→深紫黑、活力运动→深海军、甜美/甜宠→深莓紫「深沉时」/ 玫瑰金 `#D6A3A9→#9A5563`「明快时,如王微斯定稿」…);深色系公式:主色压暗 15-25% 降饱和 → 深色渐变底;浅色系公式:主色提亮+降饱和至中浅明度 → 浅色渐变底。**底色决定色系**:深色系内部元素=浅米卡+深色字+珊瑚红强调,浅色系内部元素=白卡+粉边+深粉字+粉强调;背景上直接书写的文字随背景深浅提亮保可读。「色彩体系」色块用账号真实色值
|
||||
- **要点**:GENE 0X 小标签 + 角色英文代号;情绪图条形底边对齐、峰谷差 ≥40px;红线全量;文字垂直居中(y=中心);批量移动坐标用脚本统一加减防双重偏移
|
||||
|
||||
---
|
||||
|
||||
## 七、注意事项
|
||||
## 九、注意事项
|
||||
|
||||
1. **MCP解析等待**:新提交的视频解析需3-5分钟,不要立即查询。批量提交后统一等待再批量查询。
|
||||
2. **analysis延迟**:`content`(分镜头表)通常先出,`analysis`(结构化分析)可能延迟1-2分钟。如只有content,稍后重试获取analysis。
|
||||
|
||||
@@ -81,3 +81,18 @@ id = "{detailId}"
|
||||
- 女娲造人术 → `subskills/nuwa-skill-main/`(深度分析方法)
|
||||
|
||||
**人设卡统一 12 段结构**:账号核心定位 → 账号内容基因 → 类型库(按账号类型适配)→ 情绪结构 → 视听风格 → 内容打法 → 内容规则 → 系列内容规划 → 差异化壁垒 → 红线清单 → 当前需解决的问题 → 设定分析说明。**人设卡聚焦内容创作本身(含差异化壁垒的内容侧"为什么别人做不了"),不含商业化变现路径分析**(商业化/市场分析归功能四账号数据分析)。
|
||||
|
||||
---
|
||||
|
||||
## Step 7:自动生成账号设定卡片(联动)
|
||||
|
||||
> 完整方法见 `references/feature/06_生成账号设定卡片.md`(读设定 → 读模板样例 → 模块映射 → SVG 手绘 → 验证交付)
|
||||
|
||||
**默认自动触发**:`{达人昵称}账号设定.md` 落盘后,**无需用户单独要求**,直接执行功能六生成 `{达人昵称}账号设定卡.svg`(与设定 md 同目录)。仅当功能六文件被删除(部署禁用)或用户明确「只要设定不要卡片」时跳过。
|
||||
|
||||
**双向联动维护(修改时两边一起修改)**:
|
||||
- 设定 md 与设定卡是**同一账号信息的两种载体**,任何一方修改,另一方必须同步:
|
||||
- **改设定**(内容/模块/命名)→ 卡片同步更新:内容级改动走功能六「方式」重新生成;单点改动(改文案/换底色)直接改 SVG 对应元素
|
||||
- **改卡片**(单点微调:换底色/微调布局)→ 设定 md 中对应内容同步(底色映射见 06 文档「配色适配」,内容以设定为源)
|
||||
- **术语/模块改名** → 两边一起改(历史案例:「内容路线」→「叙事结构」、「核心气质」→「内容标签」均同步改设定 md + 卡片 SVG)
|
||||
- **一致性校验**:交付前用 grep 核对两文件关键术语一致(模块名「叙事结构」「内容标签」「内容基因·TOP6」等),零残留后再交付
|
||||
@@ -0,0 +1,228 @@
|
||||
# 功能六:生成账号设定卡片
|
||||
|
||||
> 本文件是 MCNDouAnalysis 技能**功能六(生成账号设定卡片)**的完整执行流程。
|
||||
> 独立文件设计目的:部署到用户电脑时,如不需要此功能,**删除本文件即可禁用**(本功能不依赖其他功能文件,无连带删除项;依赖功能三产出的 `{达人昵称}账号设定.md` 作为输入)。
|
||||
> 使用场景:**默认由功能三自动联动触发**——功能三产出 `{达人昵称}账号设定.md` 后自动生成本卡片(无需用户开口);用户单独要求生成/调整已有卡片时也可独立触发。产出为**竖版长图信息图**(SVG 设定卡),可视化展示账号定位/内容基因/情绪结构等核心信息。
|
||||
|
||||
---
|
||||
|
||||
## 输入
|
||||
|
||||
- `{达人昵称}账号设定.md`(功能三产出,人设卡 12 段结构:账号核心定位→内容基因→类型库→情绪结构→视听风格→内容打法→内容规则→系列内容规划→差异化壁垒→红线清单→当前需解决的问题→设定分析说明)
|
||||
- 产出目录:与账号设定.md 同目录(`{产出根目录}/{达人昵称}/`,产出根目录以 `references/路径配置.md` 为准)
|
||||
|
||||
## 产出
|
||||
|
||||
- `{达人昵称}账号设定卡.svg`(竖版长图信息图,默认 `viewBox="0 0 680 1700"` 量级,宽度响应式 100%)
|
||||
- 同一目录可另存参考图:`{达人昵称}_账号设定卡_长图版.png`(导出格式,供快速预览/分享)
|
||||
|
||||
---
|
||||
|
||||
## 触发场景处理(先判定再执行,勿直接开画)
|
||||
|
||||
> **默认自动触发**:功能三产出账号设定后**自动执行本功能**(用户无感,直接落盘卡片);以下判定链适用于「用户单独要求生成/调整卡片」场景,或自动生成后的校验。
|
||||
|
||||
核心前置:**卡片 = f(账号设定.md)**。用户要求生成卡片时,先走判定链再分支处理:
|
||||
|
||||
**判定链(三问)**:
|
||||
```
|
||||
① 账号设定.md 有没有?
|
||||
├─ 没有 → 账号在达人账号表吗?
|
||||
│ ├─ 在库 → 先功能三(提炼账号设定)→ 情况3
|
||||
│ └─ 不在 → 功能一→二→三→六 全链路 → 情况4
|
||||
└─ 有 → ② 卡片 svg 已存在吗?
|
||||
├─ 存在 → 询问覆盖 or 微调 → 情况2 / 7
|
||||
└─ 不存在 → ③ 设定模块完整吗?
|
||||
├─ 完整 → 全模块生成 → 情况1
|
||||
└─ 缺模块 → 裁剪适配生成 → 情况5
|
||||
```
|
||||
|
||||
| # | 情况 | 判定依据 | 处理方式 |
|
||||
|---|------|---------|---------|
|
||||
| 1 | 有设定 + 无卡片 | `{账号名}账号设定.md` 存在,`{账号名}账号设定卡.svg` 不存在 | **自动流程已覆盖**(功能三产出即生成);此处仅当自动生成被跳过(如部署禁用)时手动走「方式」全流程 |
|
||||
| 2 | 有设定 + 已有卡片 | 卡片文件已存在 | **先询问用户**:重新生成覆盖 / 基于现有卡片微调(换底色/加模块/改布局) |
|
||||
| 3 | 无设定(账号在库) | 达人账号表有该账号,但无设定文件 | **先走功能三**提炼账号设定(人设卡)→ 再生成卡片(两步串行) |
|
||||
| 4 | 无设定(账号不在库) | 用户给的是全新账号 | **全链路**:功能一获取信息 → 功能二抓视频 → 功能三提炼设定 → 功能六生成卡片 |
|
||||
| 5 | 设定不完整(缺模块) | 设定缺叙事结构/角色库/色彩体系等某段 | 正常生成 + **模块裁剪适配**(删对应模块,后续模块上移,不留空白) |
|
||||
| 6 | 参考某账号样式 | 用户指定「像 XX 那样做」 | 读被参考账号的卡片做样式参考(版式+底色跟该账号风格),内容仍用本账号设定 |
|
||||
| 7 | 调整已有卡片 | 用户说改现有卡片的某处 | **直接改 SVG 文件**(单点修改,不重走功能三/全流程) |
|
||||
|
||||
**环境路径**:① 开发机 / ③ 用户环境 = `桌面/MCNSkill项目/{账号名}/`;② dsh 环境 = `D:\dshworkspace\解析任务\{达人昵称}/`(以 `references/路径配置.md` 为唯一权威)。
|
||||
|
||||
**双向联动维护(修改时两边一起修改)**:
|
||||
- 设定 md 与设定卡 = **同一账号信息的两种载体**,任一边修改,另一边必须同步:
|
||||
- **改设定**(内容/模块/命名)→ 卡片同步:内容级改动走下方「方式」重新生成;单点改动(改文案/换底色)直接改 SVG 对应元素
|
||||
- **改卡片**(单点微调:换底色/微调布局)→ 设定 md 对应内容同步(底色映射见上方「配色适配」,内容以设定为源)
|
||||
- **术语/模块改名** → 两边一起改(历史案例:「内容路线」→「叙事结构」、「核心气质」→「内容标签」、「六大内容基因」→「内容基因·TOP6」,均同步改设定 md + 卡片 SVG)
|
||||
- **一致性校验**:交付前用 grep 核对关键术语两文件一致(模块名「叙事结构」「内容标签」「内容基因·TOP6」等),零残留后再交付
|
||||
|
||||
---
|
||||
|
||||
## 方式(五步执行)
|
||||
|
||||
### ① 读取账号设定
|
||||
通读 `{达人昵称}账号设定.md` 全文,提取全部模块内容(内容基因/类型库/情绪结构/红线等),**不允许跳读、不允许只挑标题**——卡片内容必须来自设定文件原文,可做金句化压缩,但不得自行编造数据。
|
||||
|
||||
### ①.5 读取模板样例(关键步骤,保证同款效果)
|
||||
动手前**必须先读 `references/样例/模板_旧梦留声机账号设定卡.svg`**(最佳成品模板)与 `references/样例/参考_旧梦留声机_账号设定卡_长图版.png`(渲染效果参考):
|
||||
- 以模板的**版式骨架/字号体系/卡片尺寸/模块间距**为基准,新账号卡片沿用同一套视觉语言;**配色不沿用模板**(模板底只是旧梦留声机风格),按下方「配色适配」章节随账号风格换底色
|
||||
- 模板是「旧梦留声机」实例(内容不可照搬),新账号只复用**版式骨架与样式常量**,内容全部替换
|
||||
- 模板如被反复复用后需整体迭代(配色/布局升级),改模板 → 同步生成一次旧梦留声机卡确认效果 → 再用于新账号
|
||||
|
||||
### ② 模块映射(对齐 12 段人设卡 → 信息图模块)
|
||||
| # | 账号设定内容 | 信息图模块 | 布局 |
|
||||
|---|------------|-----------|------|
|
||||
| 1 | 账号核心定位(赛道/类型/定位语) | 顶部标题区(账号名 + 「账号设定卡」副题 + 定位语) | 浅米色大框 520×170,居中 |
|
||||
| 2 | 内容打法中的路线/流程(如有六步闭环) | 叙事结构(流程条) | 6 卡横排,箭头串联 |
|
||||
| 3 | 内容标签/核心标签 | 内容标签(chips) | 胶囊 86×32,两行 |
|
||||
| 4 | 内容基因(功能三基于 TOP6 视频提炼的结论) | 内容基因·TOP6 | 3 列 × 2 行,卡 186×130 |
|
||||
| 5 | 视听风格/色彩体系 | 色彩体系 | 5 色卡横排(色块 + 标签) |
|
||||
| 6 | 类型库/角色类型 | 角色类型库 | 3 列 × 2 行,卡 186×130 |
|
||||
| 7 | 情绪结构(六段式) | 情绪图(柱状条) | 7 条条形,底边对齐 |
|
||||
| 8 | 红线清单 | 红线清单 | 2 列 × 3 行卡 |
|
||||
| 9 | 差异化壁垒/其他亮点 | 可并入基因/类型卡描述行 | — |
|
||||
| — | 底部版权 | 数据来源 + 版本注记 | 居中两行小字 |
|
||||
|
||||
> **注**:内容基因条目由**功能三基于 TOP6 视频**(筛选权威参数见 SKILL.md「视频筛选标准」)提炼得出——TOP6 视频是分析依据(输入),内容基因是结论(输出),卡片上全量呈现、不裁剪。后续生成卡片统一以「内容基因·TOP6」描述该模块。
|
||||
|
||||
### ③ 排版绘制(SVG 手写)
|
||||
逐模块写 SVG(无模板文件,直接手写 `<rect>`/`<text>`),坐标自上而下递增,模块间留白 ≥30px。
|
||||
|
||||
### ④ 验证交付
|
||||
- 结构校验:`<svg>` 配对、`<g>` 配对、所有文字 x 不超卡片右缘
|
||||
- 坐标校验:条形底边对齐、卡片文字垂直居中(y = rect.y + rect.h/2)
|
||||
- 间距校验:相邻模块最小间距 ≥16px,无重叠
|
||||
- 交付:present_files 打开预览,用户确认后再定稿
|
||||
|
||||
---
|
||||
|
||||
## 模块裁剪适配(不同账号内容不同,禁止照搬模板模块清单)
|
||||
|
||||
模板的 9 个模块以旧梦留声机的设定内容为准。**新账号按设定文件实际有无对应内容裁剪/增补模块**,不得为了凑模块硬塞内容,也不得遗漏设定中已有的核心模块:
|
||||
|
||||
| 设定文件内容 | 模块去留 | 处理 |
|
||||
|------------|---------|------|
|
||||
| 有叙事结构/六步闭环/流程 | 保留「叙事结构」 | 流程条按实际步骤数画(4/5/6 步皆可,等宽均分 600px 宽) |
|
||||
| 无流程类内容 | **删除**「叙事结构」模块 | 后续模块上移,不留空白 |
|
||||
| 有内容基因(≥3 条) | 保留「内容基因·TOP6」 | 按实际条数画 2 列或 3 列(卡宽 = 600px/列数 - 间距) |
|
||||
| 有角色类型/角色库 | 保留「角色类型库」 | 按实际条数画 2 列或 3 列 |
|
||||
| 无角色库 | **删除**「角色类型库」 | — |
|
||||
| 有情绪结构/情绪曲线 | 保留「情绪图」 | 条形按实际段数画(4-7 段),底边对齐 |
|
||||
| 有色彩体系 | 保留「色彩体系」 | 色块按实际色数画(3-6 块),色值用设定中的真实色值 |
|
||||
| 有红线清单 | 保留「红线清单」 | 2 列排布,条数不限(2-6 条) |
|
||||
| 有差异化壁垒/独特点 | 可并入基因卡描述行或单独卡 | — |
|
||||
|
||||
**适配后校验**:每个保留模块必须能在设定文件中找到对应原文(grep 关键字验证);删除模块后 y 坐标整体上移,模块间间距保持 ≥30px。
|
||||
|
||||
---
|
||||
|
||||
## 配色适配(底色 = 背景色,跟随账号风格;内部元素保持模板统一)
|
||||
|
||||
模板配色(深棕红渐变底 + 浅米卡)是旧梦留声机的「暖黄旧时光」色系,**只代表该账号风格,不是默认底色**。**新账号卡片只换底色(整卡背景),必须按账号风格匹配,禁止全部沿用深棕红底。底色分两系:深沉/浓郁调性走深色系(深色渐变底),明快/甜美/治愈调性走浅色系(浅色渐变底),内部元素跟随所选色系规范。**
|
||||
|
||||
> **关键定义**:本节「底色」一律指**整卡背景色**(深色渐变底或浅色渐变底)。卡片块、文字、标签、强调色等**内部元素不随账号风格变**,统一按所选色系的「配色规范」表执行:**深色系(深色长图风)**= 浅米卡片 + 深色文字 + 珊瑚红强调;**浅色系(浅色甜宠风)**= 白卡 + 粉边 + 深粉文字 + 粉强调。同一账号系列卡片必须固定同一色系,保证系列视觉语言一致。
|
||||
|
||||
### ① 判定账号风格(读设定「色彩体系」+ 赛道/类型)
|
||||
以账号设定.md 的「视听风格/色彩体系」模块真实色值为第一依据(主色/强调色),赛道与账号类型辅助判定,归入下表风格。
|
||||
|
||||
### ② 风格 → 背景色映射(示例,可扩展)
|
||||
| 账号风格(色彩体系/赛道特征) | 背景底色(深色渐变) |
|
||||
|----------------------------|--------------------|
|
||||
| 暖怀旧/旧时光(剧情·怀旧,如旧梦留声机) | 深棕红 `#2B0F0A→#1A0805` |
|
||||
| 温情/亲子/治愈(亲子Vlog) | 深焦糖 `#2B1A10→#1A0F08` |
|
||||
| 科技/数码/冷淡(数码科技/商业) | 深灰蓝 `#0E1626→#080D16` |
|
||||
| 清新/田园/三农(三农/户外) | 深墨绿 `#0F1F18→#08130E` |
|
||||
| 潮流/都市/时尚(潮流风尚/颜值) | 深紫黑 `#17101F→#0C0810` |
|
||||
| 活力/运动/热血(身体锻炼/体育) | 深海军 `#0E1B2E→#080F1A` |
|
||||
| 甜美/少女/二次元(甜宠/乙女向) | 深莓紫 `#3A1E42→#22102A`(调性深沉时);**玫瑰金渐变 `#D6A3A9→#9A5563`(调性明快时,如王微斯·定稿)** |
|
||||
|
||||
**背景色推导公式(表中未列的风格)**:取账号色彩体系主色 → 压暗至 15-25% 亮度 + 降饱和 → 得深色渐变底(**仅背景色**,内部元素色不变)。
|
||||
|
||||
**浅色系推导(明快/甜美/治愈调性)**:取账号色彩体系主色 → 提亮+降饱和至中浅明度(约 65-90% 亮度,如王微斯:主粉 → 玫瑰金 `#D6A3A9→#9A5563`)→ 得浅色系渐变底;正文文字用主色加深版(`#7B2D5E` 档)。**背景上直接书写的文字(模块标题/装饰线/注释/版权)随背景深浅提亮保可读**:背景越深文字越亮(如玫瑰金底上模块标题 `#FDE8EC`、装饰线/注释 `#EAC6CE`、斜体/版权 `#DDBCC4`)。
|
||||
|
||||
### ③ 强规则
|
||||
- **禁止**默认沿用模板深棕红底;同一账号每次生成的卡背景色保持一致(系列一致性 = 同账号一致,非跨账号一致)
|
||||
- **底色决定色系**:选深色系则内部元素按「配色规范·深色系」执行,选浅色系则按「配色规范·浅色系」执行;同一色系内部元素色一律固定,不随账号再改
|
||||
- 模块「色彩体系」色块:必须用设定文件中账号的真实色值(模板 5 色块仅示例)
|
||||
- 冲突时:以「看得清、有层次、账号辨识度」为最高准则,模板色优先兜底
|
||||
|
||||
---
|
||||
|
||||
## 要求
|
||||
|
||||
### 配色规范(色系内统一,所有账号通用;底色随账号风格选系)
|
||||
|
||||
**深色系(深色长图风,深沉/浓郁调性,如旧梦留声机)**
|
||||
| 元素 | 色值 |
|
||||
|------|------|
|
||||
| 背景 | 深棕红渐变 `#2B0F0A → #1A0805` |
|
||||
| 顶部标题框 | 浅米色 `#FAEEDA`,圆角 14 |
|
||||
| 主标题 | 渐变 `#8C2F1E → #C05A32`,28px 700 |
|
||||
| 副标题(账号设定卡) | `#8C2F1E`,16px 600,letter-spacing 2 |
|
||||
| 模块标题 | `#FAEEDA`,14px 700,前缀「▍」 |
|
||||
| 卡片背景(内容类) | `#FAEEDA`,边框 `#EDCBA4` |
|
||||
| 卡片背景(强调类) | `#FDEBE2`,边框 `#E8B7A2` |
|
||||
| 卡片标题 | `#7B2D1B`(基因)/ `#8C3B2B`(角色)14px 700 |
|
||||
| 卡片正文 | `#6F5B4A`,11px |
|
||||
| 小标签(GENE 0X) | `#C13B2F`,10px 700,letter-spacing 1 |
|
||||
| 英文代号(角色卡) | `#B0715A`,10px 斜体 |
|
||||
| 情绪图核爆段 | `#D6403A` 底 + 白字 700 |
|
||||
| 情绪图琥珀段 | `#F0B25C` / `#F7D9A8` |
|
||||
| 情绪小字/注释 | `#A88770`,10px |
|
||||
| 红线×号 | `#D6403A` 700 |
|
||||
|
||||
**浅色系(浅色甜宠风,明快/甜美/治愈调性,如王微斯)**
|
||||
| 元素 | 色值 |
|
||||
|------|------|
|
||||
| 背景 | 玫瑰金渐变 `#D6A3A9 → #9A5563`(王微斯定稿;更浅可回退浅粉紫 `#F8EDF6 → #F1E0EE`) |
|
||||
| 顶部标题框 | 白 `#FFFFFF`,粉边 `#E8C9DC`,圆角 14 |
|
||||
| 主标题 | 渐变 `#C0457F → #E876A9`,28px 700 |
|
||||
| 副标题(账号设定卡) | `#7B2D5E`,16px 600,letter-spacing 2 |
|
||||
| 模块标题(背景上) | `#FDE8EC`(玫瑰金底),14px 700,前缀「▍」 |
|
||||
| 卡片背景(内容类) | 白 `#FFFFFF`,边框 `#E8C9DC` |
|
||||
| 卡片背景(强调类/chips) | `#F7E8F2`,边框 `#D8A9C8` |
|
||||
| 卡片标题 | `#7B2D5E`,14px 700 |
|
||||
| 卡片正文 | `#6F4A5E`,11px |
|
||||
| 小标签(GENE 0X) | `#E0437F`,10px 700,letter-spacing 1 |
|
||||
| 英文代号(角色卡) | `#B98AA6`,10px 斜体 |
|
||||
| 情绪图破防段 | `#E0437F` 底 + 白字 700 |
|
||||
| 情绪图琥珀段 | `#E8B7D0` / `#F7E8F2` |
|
||||
| 情绪小字/注释(背景上) | `#EAC6CE`,10px |
|
||||
| 红线×号 | `#E0437F` 700 |
|
||||
|
||||
> 背景上直接书写的装饰线/版权等元素取同色系提亮(`#EAC6CE`/`#DDBCC4`),随背景深浅微调保可读;背景越浅(如浅粉紫)越接近深粉字 `#7B2D5E`,背景越深(如玫瑰金)越接近浅粉白 `#FDE8EC`。
|
||||
|
||||
### 排版要求
|
||||
1. **文字垂直居中**:卡片内文字用 `dominant-baseline="central"` 且 y = 卡片中心;不用则手动算(y = rect.y + rect.h/2),禁止偏移
|
||||
2. **情绪图条形**:底边全部对齐同一基线(峰谷差 ≥ 40px,核爆段最高),文字在条形内居中
|
||||
3. **卡片文字不超界**:标题末字尾 ≤ 卡右缘 - 8px;中文按 12px/字、14px/字粗算宽度
|
||||
4. **角色卡英文代号**:大写首字母缩写 + 单词(如 `E. ELDER` / `M. SUFFERING`),贴合角色记忆符号
|
||||
5. **金句化压缩**:描述行可加 1-2 句传播性金句("沉默里把全家扛在肩上"式),但必须源自设定语义
|
||||
|
||||
---
|
||||
|
||||
## 格式
|
||||
|
||||
- 文件:`.svg`(可另导出 `.png`)
|
||||
- 画布:`viewBox="0 0 680 {高度}"`(宽度固定 680,高度随内容约 1700 量级;内容增加则加高并同步 `height` 与背景 rect)
|
||||
- 背景:全幅渐变 rect(首元素)
|
||||
- 文字:`<text>` 内联属性(font-size/font-weight/fill/letter-spacing),无外部字体依赖
|
||||
- 命名:`{达人昵称}账号设定卡.svg`,与 `{达人昵称}账号设定.md` 同目录
|
||||
|
||||
---
|
||||
|
||||
## 注意事项(执行避坑)
|
||||
|
||||
1. **🔴 批量移动坐标防叠加偏移**:需整体下移某模块时,优先用脚本对该区域所有 `y="…"` 统一加减;若先手动改了一张卡又跑脚本,会造成该卡双重偏移(实测踩坑:手动改 y=1018→1028 后又脚本 +10 → 1038,需单独修正回 1028)。改完必须校验同排卡片 y 一致。
|
||||
2. **模块间最小间距 16px**:情绪小字与红线标题曾贴在一起(16px 内),文字行距需显式拉开(≥32px 为宜)。
|
||||
3. **红线清单全量**:6 条红线必须全部呈现,禁止压缩条数(实测重写时漏到 2 条)。
|
||||
4. **垂直居中靠计算不靠感觉**:`dominant-baseline="central"` 的 y 即中心线;不带该属性时文字基线偏下,需 y = 中心 + 字高/2 左右微调。
|
||||
5. **情绪图高度**:条形高度按用户要求可翻倍(峰顶 112px),底边对齐即可,翻倍后需同步下移注释行与后续模块。
|
||||
6. **交付前必须 present_files 预览**:用户基于预览提出微调(配色/字号/间距),逐条修改逐条验证,禁止批量猜测。
|
||||
7. **导出 PNG**:用户要 PNG 时用支持 SVG 渲染的工具导出(分辨率按 viewBox 宽 680 × 高度),文件放同目录。
|
||||
|
||||
---
|
||||
|
||||
## 完成汇报
|
||||
|
||||
向用户汇报:卡片文件路径、SVG 尺寸(viewBox)、模块数量、与账号设定.md 的一致性说明;如导出 PNG 一并汇报。
|
||||
@@ -46,7 +46,7 @@
|
||||
| 账号类型 | 三分类判定结果(见 5.0) | 单一达人型 / 固定主角+轮换搭档型 / 群像型 + 一句判定依据 |
|
||||
| 核心受众 | 核心观看人群 | 年龄 + 性别 + 核心特征(场景/痛点/兴趣) |
|
||||
| 核心标签 | 区别于同类账号的标签词 | 2-3 个标签短语(如"第一视角沉浸 + 直球互动 + 反差萌破防") |
|
||||
| 内容路线 | 内容推进路径/固定流程 | 用箭头串联的固定流程(如"赛道开场破冰 → 冲突推进 → 情绪升温 → 交心破防 → 收尾升华") |
|
||||
| 叙事结构 | 内容推进路径/固定流程 | 用箭头串联的固定流程(如"赛道开场破冰 → 冲突推进 → 情绪升温 → 交心破防 → 收尾升华") |
|
||||
| 情绪锚点 | 观众获得的核心情绪体验 | 一句话(如"该账号 = 替观众完成 XX 的爽文体验") |
|
||||
|
||||
> 账号名称写在文档标题(`# {达人昵称} 账号人设卡片`)。V2.0 的「变现模式」字段属商业化路径,**人设卡不写**(归功能四账号数据分析);「内容形式」「差异化定位」如需体现,并入赛道/核心标签中表达。
|
||||
@@ -164,7 +164,7 @@
|
||||
|
||||
```
|
||||
账号设定确认清单(按 12 段逐项核对):
|
||||
① 一、账号核心定位 6 维度是否准确(赛道/账号类型/核心受众/核心标签/内容路线/情绪锚点)
|
||||
① 一、账号核心定位 6 维度是否准确(赛道/账号类型/核心受众/核心标签/叙事结构/情绪锚点)
|
||||
② 二、账号内容基因是否提炼到位(频道底色,观众3秒识别)
|
||||
③ 三、类型库/达人设定:按账号类型适配(单一达人=一份6章节 / 固定主角+轮换搭档=主角6章节+搭档库+筛选标准 / 群像=内容基因+角色类型库);达人设定 6 章节是否完整覆盖(身份/性格/外在特质/价值观与底线/说话风格/行为决策)
|
||||
④ 四、情绪结构是否覆盖(段落表+情绪曲线+情绪设计法则)
|
||||
@@ -204,7 +204,7 @@
|
||||
对照 5.1 的 12 段,人设卡按以下顺序排版:
|
||||
|
||||
1. **账号核心定位**
|
||||
(表格:赛道/账号类型/核心受众/核心标签/内容路线/情绪锚点)
|
||||
(表格:赛道/账号类型/核心受众/核心标签/叙事结构/情绪锚点)
|
||||
|
||||
2. **账号内容基因**
|
||||
(N个基因标记,观众3秒内认出"这是谁")
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 971 KiB |
@@ -0,0 +1,252 @@
|
||||
<svg viewBox="0 0 680 1716" width="100%" xmlns="http://www.w3.org/2000/svg" role="img">
|
||||
<title>旧梦留声机 账号设定卡 · 长图版</title>
|
||||
<desc>群像账号旧梦留声机的账号设定信息图:叙事结构、内容标签、内容基因·TOP6、色彩体系、角色类型库、情绪曲线、红线清单</desc>
|
||||
<defs>
|
||||
<linearGradient id="bgGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#2B0F0A"/>
|
||||
<stop offset="1" stop-color="#1A0805"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="titleGrad" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#8C2F1E"/>
|
||||
<stop offset="1" stop-color="#C05A32"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect x="0" y="0" width="680" height="1716" fill="url(#bgGrad)"/>
|
||||
|
||||
<!-- 顶部大框:标题区 -->
|
||||
<rect x="80" y="30" width="520" height="170" rx="14" fill="#FAEEDA"/>
|
||||
<text x="340" y="80" text-anchor="middle" font-size="28" font-weight="700" fill="url(#titleGrad)">旧梦留声机</text>
|
||||
<text x="340" y="110" text-anchor="middle" font-size="16" font-weight="600" fill="#8C2F1E" letter-spacing="2">账号设定卡</text>
|
||||
<rect x="240" y="132" width="200" height="2" rx="1" fill="#E0953C"/>
|
||||
<text x="340" y="155" text-anchor="middle" font-size="12" fill="#8C3B2B">群像账号 · 情感走心短剧 · 公益正能量 · 品牌微电影</text>
|
||||
<text x="340" y="180" text-anchor="middle" font-size="13" font-weight="600" fill="#7B2D1B">把「被遗忘的人、被亏欠的人」的故事,讲给所有人听</text>
|
||||
|
||||
<!-- 装饰线 -->
|
||||
<text x="340" y="228" text-anchor="middle" font-size="11" fill="#A88770" letter-spacing="2">— channels requirement board 2026 —</text>
|
||||
|
||||
<!-- 叙事结构 · 六步闭环 -->
|
||||
<text x="40" y="278" font-size="14" font-weight="700" fill="#FAEEDA">▍叙事结构 · 六步闭环</text>
|
||||
<g font-size="12" font-weight="500" fill="#7B2D1B">
|
||||
<rect x="56" y="296" width="88" height="50" rx="10" fill="#FAEEDA" stroke="#EDCBA4" stroke-width="0.8"/>
|
||||
<text x="100" y="321" text-anchor="middle" dominant-baseline="central">痛点开场</text>
|
||||
<rect x="152" y="296" width="88" height="50" rx="10" fill="#FAEEDA" stroke="#EDCBA4" stroke-width="0.8"/>
|
||||
<text x="196" y="321" text-anchor="middle" dominant-baseline="central">年代闪回</text>
|
||||
<rect x="248" y="296" width="88" height="50" rx="10" fill="#FAEEDA" stroke="#EDCBA4" stroke-width="0.8"/>
|
||||
<text x="292" y="321" text-anchor="middle" dominant-baseline="central">贵人破局</text>
|
||||
<rect x="344" y="296" width="88" height="50" rx="10" fill="#FAEEDA" stroke="#EDCBA4" stroke-width="0.8"/>
|
||||
<text x="388" y="321" text-anchor="middle" dominant-baseline="central">逆袭核爆</text>
|
||||
<rect x="440" y="296" width="88" height="50" rx="10" fill="#FAEEDA" stroke="#EDCBA4" stroke-width="0.8"/>
|
||||
<text x="484" y="321" text-anchor="middle" dominant-baseline="central">时空闭环</text>
|
||||
<rect x="536" y="296" width="88" height="50" rx="10" fill="#FAEEDA" stroke="#EDCBA4" stroke-width="0.8"/>
|
||||
<text x="580" y="321" text-anchor="middle" dominant-baseline="central">公益落版</text>
|
||||
</g>
|
||||
<g stroke="#D98E3F" stroke-width="1.2">
|
||||
<line x1="148" y1="321" x2="150" y2="321" marker-end="url(#arrow)"/>
|
||||
<line x1="244" y1="321" x2="246" y2="321" marker-end="url(#arrow)"/>
|
||||
<line x1="340" y1="321" x2="342" y2="321" marker-end="url(#arrow)"/>
|
||||
<line x1="436" y1="321" x2="438" y2="321" marker-end="url(#arrow)"/>
|
||||
<line x1="532" y1="321" x2="534" y2="321" marker-end="url(#arrow)"/>
|
||||
</g>
|
||||
|
||||
<!-- 内容标签 -->
|
||||
<text x="40" y="392" font-size="14" font-weight="700" fill="#FAEEDA">▍内容标签</text>
|
||||
<g font-size="12" fill="#FAEEDA">
|
||||
<rect x="56" y="406" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="99" y="423" text-anchor="middle" dominant-baseline="central">催泪核弹</text>
|
||||
<rect x="152" y="406" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="195" y="423" text-anchor="middle" dominant-baseline="central">隔辈亲情</text>
|
||||
<rect x="248" y="406" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="291" y="423" text-anchor="middle" dominant-baseline="central">底层逆袭</text>
|
||||
<rect x="344" y="406" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="387" y="423" text-anchor="middle" dominant-baseline="central">真实原型</text>
|
||||
<rect x="440" y="406" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="483" y="423" text-anchor="middle" dominant-baseline="central">公益落版</text>
|
||||
<rect x="536" y="406" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="579" y="423" text-anchor="middle" dominant-baseline="central">群像叙事</text>
|
||||
</g>
|
||||
<g font-size="12" fill="#FAEEDA">
|
||||
<rect x="56" y="446" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="99" y="463" text-anchor="middle" dominant-baseline="central">年代跨度</text>
|
||||
<rect x="152" y="446" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="195" y="463" text-anchor="middle" dominant-baseline="central">无声胜有声</text>
|
||||
<rect x="248" y="446" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="291" y="463" text-anchor="middle" dominant-baseline="central">反差叙事</text>
|
||||
<rect x="344" y="446" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="387" y="463" text-anchor="middle" dominant-baseline="central">细节伏笔</text>
|
||||
<rect x="440" y="446" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="483" y="463" text-anchor="middle" dominant-baseline="central">以小见大</text>
|
||||
<rect x="536" y="446" width="86" height="32" rx="16" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="579" y="463" text-anchor="middle" dominant-baseline="central">朴素美学</text>
|
||||
</g>
|
||||
|
||||
<!-- 内容基因·TOP6 -->
|
||||
<text x="40" y="528" font-size="14" font-weight="700" fill="#FAEEDA">▍内容基因·TOP6</text>
|
||||
|
||||
<g>
|
||||
<rect x="49" y="546" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="61" y="568" font-size="10" font-weight="700" fill="#C13B2F" letter-spacing="1">GENE 01</text>
|
||||
<text x="61" y="592" font-size="14" font-weight="700" fill="#7B2D1B">第一视角沉浸</text>
|
||||
<text x="61" y="618" font-size="11" fill="#6F5B4A">观众的眼睛 = 角色的眼睛</text>
|
||||
<text x="61" y="636" font-size="11" fill="#6F5B4A">门缝窥视 · 缸底仰视 · 晃动失焦</text>
|
||||
<text x="61" y="654" font-size="11" fill="#6F5B4A">让观众不在场,又无处不在</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="247" y="546" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="259" y="568" font-size="10" font-weight="700" fill="#C13B2F" letter-spacing="1">GENE 02</text>
|
||||
<text x="259" y="592" font-size="14" font-weight="700" fill="#7B2D1B">年代跨度闪回</text>
|
||||
<text x="259" y="618" font-size="11" fill="#6F5B4A">年份角标 + 前后期画质断层</text>
|
||||
<text x="259" y="636" font-size="11" fill="#6F5B4A">2011→2025,时间即蓄力池</text>
|
||||
<text x="259" y="654" font-size="11" fill="#6F5B4A">越旧的年代,越催现在的泪</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="445" y="546" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="457" y="568" font-size="10" font-weight="700" fill="#C13B2F" letter-spacing="1">GENE 03</text>
|
||||
<text x="457" y="592" font-size="14" font-weight="700" fill="#7B2D1B">真实感毛边</text>
|
||||
<text x="457" y="618" font-size="11" fill="#6F5B4A">老茧双手 · 污渍 · 方言 · 破碗</text>
|
||||
<text x="457" y="636" font-size="11" fill="#6F5B4A">越粗糙越真实,好看是表演</text>
|
||||
<text x="457" y="654" font-size="11" fill="#6F5B4A">脏一点,泪点多一点</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="49" y="690" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="61" y="712" font-size="10" font-weight="700" fill="#C13B2F" letter-spacing="1">GENE 04</text>
|
||||
<text x="61" y="736" font-size="14" font-weight="700" fill="#7B2D1B">拉扯式情绪结构</text>
|
||||
<text x="61" y="762" font-size="11" fill="#6F5B4A">压抑→微光→破灭→再压抑→核爆</text>
|
||||
<text x="61" y="780" font-size="11" fill="#6F5B4A">每 10-30 秒一次小转折</text>
|
||||
<text x="61" y="798" font-size="11" fill="#6F5B4A">不让观众一次哭尽,留到核爆</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="247" y="690" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="259" y="712" font-size="10" font-weight="700" fill="#C13B2F" letter-spacing="1">GENE 05</text>
|
||||
<text x="259" y="736" font-size="14" font-weight="700" fill="#7B2D1B">动作大于语言</text>
|
||||
<text x="259" y="762" font-size="11" fill="#6F5B4A">不说爱只做事——铺床 · 摔扫帚</text>
|
||||
<text x="259" y="780" font-size="11" fill="#6F5B4A">观众自己脑补的情感更狠</text>
|
||||
<text x="259" y="798" font-size="11" fill="#6F5B4A">一句台词压十秒,比哭更催泪</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="445" y="690" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="457" y="712" font-size="10" font-weight="700" fill="#C13B2F" letter-spacing="1">GENE 06</text>
|
||||
<text x="457" y="736" font-size="14" font-weight="700" fill="#7B2D1B">破壁时刻</text>
|
||||
<text x="457" y="762" font-size="11" fill="#6F5B4A">高潮切真实原型史料 / 原音</text>
|
||||
<text x="457" y="780" font-size="11" fill="#6F5B4A">白芳礼式「你以为这是演的?」</text>
|
||||
<text x="457" y="798" font-size="11" fill="#6F5B4A">「这是真的」——观众瞬间塌房</text>
|
||||
</g>
|
||||
|
||||
<!-- 色彩体系 -->
|
||||
<text x="40" y="862" font-size="14" font-weight="700" fill="#FAEEDA">▍色彩体系 · 冷暖对撞</text>
|
||||
<g font-size="11" fill="#FAEEDA">
|
||||
<rect x="70" y="882" width="100" height="80" rx="10" fill="#E8D5B0" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="120" y="984" text-anchor="middle">泛黄旧时光</text>
|
||||
<rect x="180" y="882" width="100" height="80" rx="10" fill="#8A8F98" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="230" y="984" text-anchor="middle">冷灰乡土</text>
|
||||
<rect x="290" y="882" width="100" height="80" rx="10" fill="#F2C14E" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="340" y="984" text-anchor="middle">暖黄回忆</text>
|
||||
<rect x="400" y="882" width="100" height="80" rx="10" fill="#DCE3EA" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="450" y="984" text-anchor="middle">冷白危机</text>
|
||||
<rect x="510" y="882" width="100" height="80" rx="10" fill="#5B7C99" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="560" y="984" text-anchor="middle">明亮都市</text>
|
||||
</g>
|
||||
|
||||
<!-- 角色类型库 -->
|
||||
<text x="40" y="1010" font-size="14" font-weight="700" fill="#FAEEDA">▍角色类型库 · 专属记忆符号</text>
|
||||
|
||||
<g>
|
||||
<rect x="49" y="1028" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="61" y="1056" font-size="14" font-weight="700" fill="#7B2D1B">隔辈守望者</text>
|
||||
<text x="61" y="1076" font-size="10" font-style="italic" fill="#B0715A" letter-spacing="0.5">E. ELDER</text>
|
||||
<text x="61" y="1104" font-size="11" fill="#6F5B4A">白衬衫 · 老茧手 · 那句「不怕」</text>
|
||||
<text x="61" y="1122" font-size="11" fill="#6F5B4A">替儿孙扛住整个时代的苦</text>
|
||||
<text x="61" y="1140" font-size="11" fill="#6F5B4A">在场即守护,无言胜千言</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="247" y="1028" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="259" y="1056" font-size="14" font-weight="700" fill="#7B2D1B">逆袭反哺者</text>
|
||||
<text x="259" y="1076" font-size="10" font-style="italic" fill="#B0715A" letter-spacing="0.5">R. REPAYER</text>
|
||||
<text x="259" y="1104" font-size="11" fill="#6F5B4A">亏欠→兑现:硬板床→软床</text>
|
||||
<text x="259" y="1122" font-size="11" fill="#6F5B4A">把曾经没得到的,悄悄还回去</text>
|
||||
<text x="259" y="1140" font-size="11" fill="#6F5B4A">反哺路上,每一步都是心疼</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="445" y="1028" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="457" y="1056" font-size="14" font-weight="700" fill="#7B2D1B">苦难中的母亲</text>
|
||||
<text x="457" y="1076" font-size="10" font-style="italic" fill="#B0715A" letter-spacing="0.5">M. SUFFERING</text>
|
||||
<text x="457" y="1104" font-size="11" fill="#6F5B4A">红碎花棉袄 · 玉镯与粗手并存</text>
|
||||
<text x="457" y="1122" font-size="11" fill="#6F5B4A">一辈子没喊过苦的母亲最苦</text>
|
||||
<text x="457" y="1140" font-size="11" fill="#6F5B4A">沉默里,把全家扛在肩上</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="49" y="1172" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="61" y="1200" font-size="14" font-weight="700" fill="#7B2D1B">大爱原型人物</text>
|
||||
<text x="61" y="1220" font-size="10" font-style="italic" fill="#B0715A" letter-spacing="0.5">A. ARCHETYPE</text>
|
||||
<text x="61" y="1248" font-size="11" fill="#6F5B4A">破三轮 · 起皱零钞 · 半个馒头</text>
|
||||
<text x="61" y="1262" font-size="11" fill="#6F5B4A">他没想过自己,谁都想过</text>
|
||||
<text x="61" y="1280" font-size="11" fill="#6F5B4A">朴素到极致,是最贵的善良</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="247" y="1172" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="259" y="1200" font-size="14" font-weight="700" fill="#7B2D1B">贵人引路人</text>
|
||||
<text x="259" y="1220" font-size="10" font-style="italic" fill="#B0715A" letter-spacing="0.5">G. GUIDE</text>
|
||||
<text x="259" y="1248" font-size="11" fill="#6F5B4A">高马尾 · 黑外套 · 指路手势</text>
|
||||
<text x="259" y="1262" font-size="11" fill="#6F5B4A">一句话点醒,一辈子不忘</text>
|
||||
<text x="259" y="1280" font-size="11" fill="#6F5B4A">指路的手势,比讲理更动人</text>
|
||||
</g>
|
||||
<g>
|
||||
<rect x="445" y="1172" width="186" height="130" rx="10" fill="#FAEEDA"/>
|
||||
<text x="457" y="1200" font-size="14" font-weight="700" fill="#7B2D1B">倔强追梦者</text>
|
||||
<text x="457" y="1220" font-size="10" font-style="italic" fill="#B0715A" letter-spacing="0.5">D. DREAMER</text>
|
||||
<text x="457" y="1248" font-size="11" fill="#6F5B4A">不服老 · 执拗 · 圆梦</text>
|
||||
<text x="457" y="1262" font-size="11" fill="#6F5B4A">世界觉得他不行,他偏要行</text>
|
||||
<text x="457" y="1280" font-size="11" fill="#6F5B4A">最倔的人,心里藏着最柔软</text>
|
||||
</g>
|
||||
|
||||
<!-- 六段式情绪核弹结构 -->
|
||||
<text x="40" y="1346" font-size="14" font-weight="700" fill="#FAEEDA">▍六段式情绪核弹结构</text>
|
||||
<g>
|
||||
<rect x="60" y="1376" width="76" height="76" rx="6" fill="#F0B25C" opacity="0.9" stroke="#D98E3F" stroke-width="0.5"/>
|
||||
<text x="98" y="1416" text-anchor="middle" font-size="11" fill="#1A0805" font-weight="600">开场钩子</text>
|
||||
<rect x="144" y="1420" width="76" height="32" rx="6" fill="#F7D9A8" stroke="#D98E3F" stroke-width="0.5"/>
|
||||
<text x="182" y="1440" text-anchor="middle" font-size="11" fill="#1A0805" font-weight="600">绝境施压</text>
|
||||
<rect x="228" y="1388" width="76" height="64" rx="6" fill="#F7D9A8" stroke="#D98E3F" stroke-width="0.5"/>
|
||||
<text x="266" y="1424" text-anchor="middle" font-size="11" fill="#1A0805" font-weight="600">转机拉扯</text>
|
||||
<rect x="312" y="1340" width="76" height="112" rx="6" fill="#D6403A" stroke="#A92825" stroke-width="0.5"/>
|
||||
<text x="350" y="1400" text-anchor="middle" font-size="11" font-weight="700" fill="#FFFFFF">核爆段</text>
|
||||
<rect x="396" y="1368" width="76" height="84" rx="6" fill="#E88E6A" stroke="#C96A48" stroke-width="0.5"/>
|
||||
<text x="434" y="1412" text-anchor="middle" font-size="11" fill="#1A0805" font-weight="600">闭环升华</text>
|
||||
<rect x="480" y="1400" width="76" height="52" rx="6" fill="#F7D9A8" stroke="#D98E3F" stroke-width="0.5"/>
|
||||
<text x="518" y="1428" text-anchor="middle" font-size="11" fill="#1A0805" font-weight="600">落版段</text>
|
||||
<rect x="564" y="1380" width="56" height="72" rx="6" fill="#FAEEDA" stroke="#D9BFA3" stroke-width="0.5"/>
|
||||
<text x="592" y="1420" text-anchor="middle" font-size="11" fill="#8C3B2B" font-weight="600">破壁</text>
|
||||
<text x="98" y="1478" text-anchor="middle" font-size="10" fill="#A88770">3-7秒 揪心</text>
|
||||
<text x="182" y="1478" text-anchor="middle" font-size="10" fill="#A88770">20% 谷底</text>
|
||||
<text x="266" y="1478" text-anchor="middle" font-size="10" fill="#A88770">25% 拉锯</text>
|
||||
<text x="350" y="1478" text-anchor="middle" font-size="10" fill="#A88770">25% 撕心裂肺</text>
|
||||
<text x="434" y="1478" text-anchor="middle" font-size="10" fill="#A88770">15-20% 留白</text>
|
||||
<text x="518" y="1478" text-anchor="middle" font-size="10" fill="#A88770">10-15% 落版</text>
|
||||
<text x="592" y="1478" text-anchor="middle" font-size="10" fill="#A88770">破壁在核爆后</text>
|
||||
<text x="340" y="1506" text-anchor="middle" font-size="11" fill="#D9BFA3" font-style="italic">情绪节奏总览:压抑蓄力 → 核爆释放 → 破壁落地,每 10-30 秒完成一次情绪反转</text>
|
||||
</g>
|
||||
|
||||
<!-- 红线清单 -->
|
||||
<text x="40" y="1538" font-size="14" font-weight="700" fill="#FAEEDA">▍红线清单 · 绝不能做的事</text>
|
||||
<g font-size="11" fill="#FAEEDA">
|
||||
<rect x="49" y="1554" width="286" height="30" rx="8" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="63" y="1573" font-weight="700" fill="#D6403A">×</text><text x="79" y="1573">消费苦难:不消费,只让观众看见人</text>
|
||||
<rect x="345" y="1554" width="286" height="30" rx="8" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="359" y="1573" font-weight="700" fill="#D6403A">×</text><text x="375" y="1573">伪造真实素材:破壁史料必须可溯源</text>
|
||||
<rect x="49" y="1592" width="286" height="30" rx="8" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="63" y="1611" font-weight="700" fill="#D6403A">×</text><text x="79" y="1611">美化悲剧:不加滤镜不配哀伤煽情</text>
|
||||
<rect x="345" y="1592" width="286" height="30" rx="8" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="359" y="1611" font-weight="700" fill="#D6403A">×</text><text x="375" y="1611">角色没有名字:有名字才是一个人</text>
|
||||
<rect x="49" y="1630" width="286" height="30" rx="8" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="63" y="1649" font-weight="700" fill="#D6403A">×</text><text x="79" y="1649">强行好结局:不否定真实的痛苦</text>
|
||||
<rect x="345" y="1630" width="286" height="30" rx="8" fill="#3D1810" stroke="#A88770" stroke-width="0.8"/>
|
||||
<text x="359" y="1649" font-weight="700" fill="#D6403A">×</text><text x="375" y="1649">植入打断情绪:核爆/破壁段不插广告</text>
|
||||
</g>
|
||||
|
||||
<!-- 底部版权 -->
|
||||
<text x="340" y="1688" text-anchor="middle" font-size="10" fill="#7A5645" letter-spacing="1">数据来源:2026-08-26 企微 · 群像账号 · TOP6 数据看板</text>
|
||||
<text x="340" y="1700" text-anchor="middle" font-size="9" fill="#7A5645" letter-spacing="0.5">旧梦留声机 · 账号设定卡 · content requirement board</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,198 @@
|
||||
# Creation Review Method
|
||||
|
||||
> Method: Creation Review | 创作复盘方法
|
||||
> Status: 有效 (2026-08-22)
|
||||
> Scope: 短视频脚本创作全流程(S1-S11)问题复盘与经验沉淀
|
||||
> 本目录存放创作流程中出现的问题复盘方法与经验总结,供后续创作/改写时参考,避免同类问题复现。
|
||||
|
||||
---
|
||||
|
||||
## 复盘方法总览
|
||||
|
||||
创作出现问题 → 按以下三层归因 → 定位环节 → 修改对应步骤 → 沉淀经验。
|
||||
|
||||
### 第一步:问题归因(三层判定)
|
||||
|
||||
| 层 | 判定问题 | 关键问 |
|
||||
|----|---------|--------|
|
||||
| **事件层** | 事件设计本身是否成立 | 这个事件放在任何位置都立得住吗?还是本身就有硬伤? |
|
||||
| **手法层** | 叙事手法使用是否得当 | 手法本身没问题,是选择或执行错了? |
|
||||
| **流程层** | 流程是否有把关环节 | 是 AI 执行偏差,还是流程设计本身缺这道闸? |
|
||||
|
||||
> 归因优先级:先查事件层(设计硬伤),再查手法层(表现方式),最后查流程层(缺把关)。
|
||||
|
||||
### 第二步:定位环节(S5→S11)
|
||||
|
||||
| 环节 | 负责什么 | 问题迹象 |
|
||||
|------|---------|---------|
|
||||
| S5 选题 | 故事前提声明 | 秘密/信息边界从未被定义 |
|
||||
| S6 框架 | 事件设计 + 复盘(主闸) | 事件破坏前提、动机缺失、高潮前置违规 |
|
||||
| S7 大纲 | 事件展开(继承 S6 标注) | 展开时改变前提关系、升级烈度 |
|
||||
| S9 脚本 | 开场演绎(按 S6 已定结构) | 自行决定是否前置、更换开场事件 |
|
||||
| S11 诊断 | 专项兜底(前提/因果/动机) | 脚本成型后仍带逻辑硬伤 |
|
||||
|
||||
### 第三步:修改对应环节
|
||||
|
||||
- 前提问题 → 回 S6 重排事件/曝光节点
|
||||
- 动机问题 → 回 S6/S7 补动机铺垫或调低烈度
|
||||
- 开场问题 → 回 S6 重做开场决策(S9 只演绎不决策)
|
||||
|
||||
---
|
||||
|
||||
## 经验沉淀
|
||||
|
||||
### 经验 1:事件设计必须通过「故事成立性三层校验」
|
||||
|
||||
> 来源:2026-08-22 当众撕包揭穿假快递案例
|
||||
|
||||
**问题**:改写脚本开场 = 新邮递员当众撕包裹示众,喊出「全是假的!寄件人是你自己写的!」。后续场5村民又演「原来如此」——逻辑崩了。
|
||||
|
||||
**三层校验判定**:
|
||||
- 前提一致性 ❌:秘密被公开拆穿 → 十年守护前提崩塌;场5认知倒挂
|
||||
- 因果链条 ❌:无过渡直接进入当众撕包
|
||||
- 动机合理性 ❌:小周与罗爷爷无仇无怨,行为烈度远超动机,零铺垫
|
||||
|
||||
**经验要点**:
|
||||
1. **冲突强 ≠ 事件成立**:一个事件冲突再强,如果破坏故事核心设定(秘密/规则),放在任何位置都立不住
|
||||
2. **事件与前提校验**:设计每个事件时问「这件事发生之后,故事还能按原设定走下去吗?」
|
||||
3. **动机与烈度匹配**:行为烈度 ≤ 动机强度;无仇无怨的角色不能做无理由的激烈行为
|
||||
4. **正确做法参考**(原视频):知情者私下质问「你啷个还在骗这些娃娃哦」——秘密保留、动机成立、烈度匹配
|
||||
|
||||
### 经验 2:高潮前置倒叙——前置的是「结果切片」,不是完整高潮桥段
|
||||
|
||||
> 来源:同案例的手法层归因
|
||||
|
||||
**问题**:AI 把「当众撕包揭穿」(全片最大冲突)前置到开场,导致:
|
||||
- 剧透全部(秘密当场公开)
|
||||
- 情绪峰值提前耗尽(后续蓄力→释放闭环断裂,观众没有理由看下去)
|
||||
|
||||
**经验要点**:
|
||||
1. **前置的是悬念,不是情绪终点**:前置片段让观众产生"怎么发生的/为什么"的好奇(勾起兴趣),不是让观众"已经知道结局/情绪已走完"
|
||||
2. **可前置的高光类型不限**:冲突/情感/治愈瞬间都可以,但必须是"结果感+未解之谜"的切片
|
||||
3. **信息量收敛**:≤1-2秒快闪 + 时间标记(如"十年前"),不演完整桥段/对白/示众
|
||||
4. **拆穿秘密类事件禁止前置**:「当众揭发/拆穿秘密」类事件前置 = 剧透全部 + 破坏前提
|
||||
5. **开场方式选择不设类型禁区**:悬念/冲突/共鸣/反差/高光/提问都行,凡能引起观看兴趣即可——约束只落在「高潮前置倒叙」这一种手法的使用规则上
|
||||
|
||||
### 经验 3:判断问题归属——先事件层,再手法层,最后流程层
|
||||
|
||||
> 来源:案例复盘过程中的方法纠偏
|
||||
|
||||
**纠偏过程**:
|
||||
- 初判:把问题归因于「高潮前置倒叙」手法 → 错(手法是表现层,事件本身才是硬伤)
|
||||
- 修正:问题在「当众撕包揭穿」这个**事件本身**——即使不前置、放在任何位置都会泄密
|
||||
|
||||
**经验要点**:
|
||||
1. **事件设计是根本**:坏事件放哪都是坏的;手法只是放大或收敛它的影响
|
||||
2. **不要把手法问题当事件问题,也不要把事件问题当手法问题**:先确认事件本身成立,再谈表现方式
|
||||
3. **流程把关闭环**:S6 是主闸(事件设计时拦截),S11 是兜底(成型后复核)——中间环节(S7/S9)不重复设检查,只执行 S6 已把关的决策
|
||||
|
||||
### 经验 4:职责分离——结构决策在 S6,演绎执行在 S9
|
||||
|
||||
> 来源:是否前置/如何前置的归属讨论
|
||||
|
||||
**结论**:
|
||||
- **是否前置、前置什么事件、信息是否收敛 → S6 决策**(场次结构表 + 叙事逻辑与动机复盘)
|
||||
- **如何演绎前置片段(画面/台词/节奏)→ S9 执行**(按 S6 已定开场场次)
|
||||
|
||||
**理由**:
|
||||
1. 倒叙开场改变场次结构的时间线 → 属于结构决策,S6 产出场次结构表
|
||||
2. S6 的事件-前提校验天然覆盖「前置是否泄密」
|
||||
3. S9 只负责把 S6 定的开场演出来,不得自行决定是否前置/更换开场事件
|
||||
|
||||
### 经验 5:角色对话信息合法性——「知道的」才能说出口
|
||||
|
||||
> 来源:2026-08-22 新邮递员断言「造假十年」案例(脚本 #23 二次复盘)
|
||||
|
||||
**问题**:场3 老赵退休才把钥匙交给「新来的邮递员小刘」(刚接手),场4 小刘却摔箱断言「十年!你造假造了十年!」——「十年」是只有老赵/村民(长期在场者)才知道的时间跨度,新角色说出即**信息越界**。
|
||||
|
||||
**归因**:流程层缺口(S6 复盘只查「行为符合身份」,不查「对话信息符合角色认知边界」)+ 台词瑕疵。
|
||||
|
||||
**校验要点**:
|
||||
1. **角色的每句关键断言,必须在其认知边界内**(身份×经历×出场时间)——他只能说他能知道的(亲眼所见/亲历/合理推断/他人告知,推断链条要有依据)
|
||||
2. **身份×经历×出场时间 = 认知边界**:新来的→只知道接手后看到的;长期在场→才知道历史;知情者→才知道秘密
|
||||
3. **数量 ≠ 年限**:「退回件堆了一柜子」只能推断「有假件」,不能推断「造假十年」——断言超过证据范围即越界
|
||||
4. **与「行为动机」区分**:动机查「为什么做」,信息合法性查「凭什么知道」——两者都要过
|
||||
5. **修复示范**:新邮递员应说「退回的件堆了一柜子!你造假快递,不是一天两天了吧」(可推断),而非「十年!你造假造了十年!」(不可知)
|
||||
|
||||
### 经验 6:冲突强度来自动机与台词,不是摔得狠、吼得响、围观多
|
||||
|
||||
> 来源:2026-08-22 脚本 #23 场1 揭穿段连发问题(摔箱/吼/围观议论)
|
||||
|
||||
**问题**:场1 开场 = 新邮递员「狠狠摔箱 + 一句吼声砸下来 + 围观村民议论声潮水般涌上来」——三个过度表达叠在一起,但角色动机只是「职责发现退回件」(平和)。表现强度远超动机强度 → 观众感到刻意、假。
|
||||
|
||||
**归因**:共性根因——**AI 用「加烈度、加音量、加围观」堆冲突,但动机撑不起**。这是「动机与烈度匹配」(经验1)在**表达方式维度**的细化,原检查粒度太粗没拦住。
|
||||
|
||||
**校验要点**:
|
||||
1. **冲突强度 = 动机分量 × 台词张力**,不是动作音量围观人群
|
||||
2. **动作烈度需暴力动机支撑**:摔/砸/撕配职责发现 = 不匹配;平和动机配平和动作(放/看/问)
|
||||
3. **音量烈度**:私下发现的问题配私下质问(低声一字一顿),不当众吼
|
||||
4. **公开程度**:秘密型故事的开场揭穿应私下进行——「围观村民议论」暗示事件已公开、秘密已不存,与前提冲突;村民在场保留到高潮
|
||||
5. **台词张力替代音量**:安静地精准戳穿("寄件人查无此人")比吼更有压迫感
|
||||
6. **判别**:观众看完是"这冲突来得自然、有分量"还是"这角色怎么突然这么激动"——后者 = 表达方式过度
|
||||
|
||||
### 经验 7:把关点要收敛,不要摊饼
|
||||
|
||||
> 来源:2026-08-22 故事成立性校验方案设计过程
|
||||
|
||||
**问题**:设计「故事成立性」修复方案时,把同一套校验(前提/因果/动机)摊到 S5/S6/S7/S9/S11 五个环节,每个环节都加一点——**为"每环节都有产出"而摊饼**,导致改动面大、职责重叠、中间环节冗余。后被收敛为:S5 供基准 + S6 主闸 + S11 兜底,中间环节(S7/S9)只执行不重复设检查。
|
||||
|
||||
**经验要点**:
|
||||
1. **把关点收敛到少数关键环节**:源头主闸(事件/结构设计处)+ 兜底(成型后复核),中间环节不重复设检查
|
||||
2. **判断信号**:改动触及 3+ 个环节时,停下来确认是否有单一主责环节可以承担
|
||||
3. **职责不重叠**:每个检查只在一个环节把关一次,避免 S5-S11 各查一遍同一件事
|
||||
4. **每环节有自己的职责**:S5 供基准、S6 定结构/把关、S7 继承展开、S9 演绎、S11 兜底——不是每处都放同一把尺子
|
||||
|
||||
### 经验 9:修表层更要修根因,避免挤牙膏式连环补
|
||||
|
||||
> 来源:2026-08-22 脚本 #23 连环问题(撕包→十年→摔吼围观)
|
||||
|
||||
**问题**:同类问题三次连环暴露——第一次修「当众撕包揭穿」(表层:改台词),随后又暴露「十年信息越界」、再暴露「摔箱/吼/围观议论」。每次只修了**当前看到的表层问题**,没有一次性回溯深层共性根因,导致同类问题换个形式再来。
|
||||
|
||||
**经验要点**:
|
||||
1. **发现问题先问「执行偏差还是流程缺口」**:表层改脚本台词只是执行偏差修复;若是流程缺口(缺把关),必须补流程闸,否则同类问题换形式复发
|
||||
2. **连环暴露的信号**:同一脚本/同一类问题第二次出现时,停止逐条补丁,回溯**共性根因**(本次共性:AI 用过度表达堆冲突、动机撑不起)
|
||||
3. **一次性收敛**:定位到根因后,把该根因的所有表现形态一次性覆盖(动作烈度/音量/公开度/信息跨度),并沉淀为一条经验,避免第三次复发
|
||||
4. **沉淀闭环**:每次修复后问「这个问题下次还会以什么形式出现?」——答案即下一条要补的检查点
|
||||
|
||||
### 经验 10:冷开场切片与后文完整演绎——事件属性必须一致或明示递进(起疑级对白可留、揭穿级对白禁演)
|
||||
|
||||
> 来源:2026-08-22 脚本 #23 复盘(场1 私下平静一问 vs 场4 当众摔箱揭穿)
|
||||
|
||||
**问题**:冷开场(场1)与后文高潮(场4)疑似同一揭穿事件的两次演绎,但公开度/氛围/烈度不一致——场1 私下低声问「这包裹,是假的吧?」,场4 当众摔箱+吼+村民围拢。观众易产生「同一幕两种演法」的错位感;且场1「十年后」字幕与场3 十六岁时间线重叠,冷开场时间定位模糊。
|
||||
|
||||
**归因**:手法层(经验2「前置结果切片」规则细化不足)——经验2 只约束「快闪+不演完整桥段/对白」,未约束「切片与后文完整演绎的事件属性一致性」与「对白分级」。
|
||||
|
||||
**校验要点**:
|
||||
1. **切片=预叙时,事件属性必须与后文一致**:若冷开场是高潮事件的预叙切片,公开度(私下/当众)、围观状态、动作烈度须与后文完整演绎对得上,否则观众错位
|
||||
2. **切片=独立事件时,必须明示递进**:若设计为「私下试探→公开爆发」的递进,需加时间/逻辑锚点(如切片里小刘手中一叠退回件),让观众一眼读懂两场不是重复
|
||||
3. **对白分级**:冷开场对白只允许「起疑级」(秘密的危机开端,如「这包裹,是假的吧?」),禁止「揭穿级」(秘密的总爆发,如「你造假十年!」)——后者必须保留到高潮
|
||||
4. **时间锚点**:冷开场的「十年前/十年后」字幕须与主线时间线自洽,重叠或矛盾时改用模糊标记(「很多年前」)或画面锚点定位
|
||||
|
||||
### 经验 11:剧情脚本混入纪实/素材段时,AI 标识与素材来源标注必须落实
|
||||
|
||||
> 来源:2026 脚本 #23《门卫爷爷的「假包裹」》复盘(AI写脚本 #23,场7 纪实素材段)
|
||||
|
||||
**问题**:剧情段(AI 生成)与真实纪实素材混剪,旁白断言「这不是虚构。在我们看不到的山村里,这样的『假包裹』真实存在过」,但片内未做 AI 生成内容标识、未注明素材来源/情景示意——观众易把剧情素材当真实新闻,存在平台合规(虚假纪实/误导)风险。
|
||||
|
||||
**归因**:流程层缺口——S11 诊断只查故事逻辑/合规词,不查「素材来源与 AI 标识」。
|
||||
|
||||
**校验要点**:
|
||||
1. **混剪必标注**:剧情类脚本混入纪实/新闻素材,必须在字幕注明素材来源,或标注「素材示意/情景再现」,不得让观众误判为真实新闻
|
||||
2. **AI 生成内容显著标识**:按平台规则(抖音等 AI 生成内容标识要求),AI 生成段落需显著标识
|
||||
3. **「真实存在」断言需有依据**:旁白「这不是虚构/真实存在过」需有事实支撑,或降级为软性表达(「据说/在我们看不到的山村」)
|
||||
4. **S11 兜底增加检查项**:素材来源、AI 标识、真实性断言三查,作为剧情类脚本的合规兜底
|
||||
|
||||
---
|
||||
|
||||
## 复盘流程(可执行步骤)
|
||||
|
||||
```
|
||||
发现问题(逻辑/情绪/动机/传播问题)
|
||||
→ 1. 归因三层:事件层?手法层?流程层?
|
||||
→ 2. 定位环节:S5 前提声明?S6 事件/复盘?S7 展开?S9 演绎?S11 兜底?
|
||||
→ 3. 判断是「执行偏差」还是「流程缺口」
|
||||
· 执行偏差 → 修改对应步骤产物,重跑该环节
|
||||
· 流程缺口 → 修改技能流程(S5/S6/S7/S9/S11 或知识库),补把关点
|
||||
→ 4. 沉淀:把案例与经验写入本目录对应文件
|
||||
→ 5. 验证:用新流程重跑同类场景,确认能拦截
|
||||
```
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
name: 脚本复盘
|
||||
description: >
|
||||
短视频脚本复盘技能:只做分析与建议,不修改脚本。
|
||||
【脚本质量复盘】对已有脚本做评分式质量复盘;
|
||||
【对比分析】对比 AI 脚本与视频源脚本的差异;
|
||||
【创作过程复盘】复盘出问题的创作过程,沉淀经验避免再犯。
|
||||
触发场景:复盘脚本、点评脚本、分析脚本问题、对比 AI脚本与源脚本、
|
||||
复盘某次创作过程、总结经验避免再犯。
|
||||
适用平台:抖音/快手/视频号/B站/小红书。
|
||||
version: "1.2"
|
||||
updated_at: "2026-08-22"
|
||||
last_change: "description 精简为基础要求(功能全量实现在正文/references,调用 prompt 不重复实现细节)"
|
||||
agent_created: true
|
||||
---
|
||||
|
||||
# 脚本复盘 Skill
|
||||
|
||||
## 技能概述
|
||||
|
||||
本技能提供脚本复盘的完整能力,与创作技能(short-video-script)解耦:
|
||||
|
||||
- **脚本质量复盘**:对已有脚本做质量点评(复用 short-video-script 的判定标准,只读引用)
|
||||
- **创作过程复盘**:对出问题的创作过程做三层归因与经验沉淀
|
||||
- **经验沉淀**:新问题/新经验写入 `Creation-Review-Method/README.md` 经验库
|
||||
|
||||
> ⚠️ **核心约束**:本技能只做**分析与建议**,不修改脚本、不触发创作流程、不改 short-video-script 的创作步骤文件。
|
||||
|
||||
---
|
||||
|
||||
## 触发条件
|
||||
|
||||
当用户表达以下任一意图时加载本技能:
|
||||
|
||||
- 📝 **脚本复盘**:对已有脚本做质量复盘/点评("复盘这个脚本""点评一下""看看这脚本有什么问题")→ 独立分析模式
|
||||
- 📊 **对比分析**:对比 AI脚本 和 源脚本 的差异("对比 AI脚本和源脚本""分析两者差异")→ 对比分析模式
|
||||
- 📋 **创作复盘**:对某次创作过程做归因复盘("复盘为什么会出现这个问题""总结经验避免再犯")
|
||||
- 💡 **经验沉淀**:把复盘发现的新问题沉淀为经验("把这个教训记下来")
|
||||
|
||||
> 用户要求「对比/差异分析」→ 对比分析模式;否则默认独立分析模式(评分制)。
|
||||
|
||||
---
|
||||
|
||||
## 脚本质量复盘流程(独立分析模式 · 评分制)
|
||||
|
||||
对「已有脚本」(如 AI写作复盘 页保存的脚本)做质量复盘,**每维 100 分制评分,总分等权平均**:
|
||||
|
||||
1. **读取脚本**:从用户提供处读取脚本正文(文本/文件/数据库记录)
|
||||
2. **判定内容类型**(防误用,必做):剧情/悬念/反转类 → 教程/干货类 → 口播/观点/测评类——决定哪些检查项适用(剧情类查故事成立性、教程类查教程完整链,互不误用)
|
||||
2.5 **逻辑硬伤预检(剧情类评分前必做)**:通读全文,对照**逻辑硬伤判定清单**,命中即在问题清单记「高」,并按数量决定叙事逻辑维分数上限:
|
||||
- **判定清单**(任一命中 = 逻辑硬伤 = 严重度「高」):
|
||||
· 时间线/年龄账矛盾(前后数字、岁数、年份对不上)
|
||||
· 动机铺垫不足 / 为反转而反转(事件为道具/转折硬设,无前文推导)
|
||||
· 行为来源穿帮(道具/资源来路不合理,行为无由头)
|
||||
· 逻辑范围失当(范围扩张/收缩无过渡)
|
||||
· 前后事实/信息不符(同一对象状态、位置前后矛盾)
|
||||
- **判定示例(对照用,防边界模糊)**:
|
||||
· "角色夜里意外受伤入院,引出此前毫无铺垫的关键道具" → 为反转而反转/因果跳跃 → **高**(即使可加伏笔补救,仍是硬伤)
|
||||
· "善意行为的作用范围从个体扩到群体,无过渡交代" → 范围失当 → **高**
|
||||
· "角色年龄与工作年限对不上" → 年龄账矛盾 → **高**
|
||||
· "台词含绝对化词'最'" → 合规瑕疵(非逻辑)→ **低**
|
||||
· "场1/场4信息重复" → 表达细节(非逻辑)→ **低**
|
||||
- **叙事逻辑维分数上限(按命中数)**:1 个 → ≤60;2 个 → ≤40;3 个及以上 → ≤20
|
||||
3. **表格维度评分**(与 AI写作复盘 列表对齐,6 维,每维 X/100):
|
||||
- 设定承接:记忆符号/行为决策/内容规则体现
|
||||
- 框架节奏:逻辑链条/爆点贯穿/钩子密度/信息密度/节奏变化/结尾收口
|
||||
- 钩子设置:开场钩子/中间留人/中段不疲/结尾收口(全片留人链路)
|
||||
- 叙事逻辑:前提/因果/动机/信息/表达方式/逻辑链条(仅剧情类强制);**受 2.5 预检分数上限约束**
|
||||
- 极致事件:爆点贯穿/动作描述/场景/可拍性
|
||||
- 共鸣和共情:类型命中/具象场景/自嗨检测/3问自检
|
||||
4. **追加维度评分**(表格外,4 维,每维 X/100):
|
||||
- 格式规范性:画面描述/景别/时长
|
||||
- 台词口语化:书面词/长句/语气词/重复词/人设一致性
|
||||
- 内容合规:绝对化/承诺/数据来源/竞品/违禁词
|
||||
- 教程完整链:仅教程类(成品→材料→步骤→收尾)
|
||||
5. **总分统计**(100 分制):**总分 = (Σ其他适用维得分 + 叙事逻辑×2) / (适用维数 + 1)**(**叙事逻辑权重 2 倍**——故事逻辑是剧情类立身之本;不适用维度标记 null/N-A 不计入分母);**叙事逻辑 <80 时,总分不超过叙事逻辑分**(逻辑是根本,总分不能高于逻辑分);判定 ≥75 可交付 / 60-74 需优化 / <60 需大改
|
||||
6. **输出复盘报告**:
|
||||
- 各维得分 + 档位(90-100 优秀 / 75-89 良好 / 60-74 及格 / 40-59 有问题 / 0-39 严重)
|
||||
- 总分 + 综合判定
|
||||
- 问题清单(按严重度 🔴高/🟡中/🟢低)+ 定位 + 修改建议 + 所属环节(S5-S11)
|
||||
- ⚠️ **严重度强制规则(故事逻辑是根本,命中即「高」,不得降级)**:
|
||||
· **逻辑硬伤判定清单**(任一命中 → 严重度「高」):时间线/年龄账矛盾、动机铺垫不足、行为来源穿帮、逻辑范围失当(扩张/收缩无过渡)、为反转而反转/因果跳跃、前后数字/事实不符
|
||||
· **叙事逻辑维按硬伤数量梯度扣分**:1 个硬伤 → 该维 ≤60;2 个 → ≤40;3 个及以上 → ≤20
|
||||
· **判定边界(禁止降级)**:不要把逻辑问题当"表达细节/可改小瑕疵"标低——只要属清单内(哪怕可改),一律「高」;只有合规瑕疵、表达细节(非逻辑)才可标「低」
|
||||
7. **沉淀**:复盘中发现**新类型问题**(经验库未覆盖)→ 按「经验沉淀」写入经验库
|
||||
|
||||
> 判定标准来源(跨技能只读引用,不复制):
|
||||
> - `short-video-script/references/知识库/09_脚本诊断/脚本质量自动检查清单_八类35项.md`
|
||||
> - `short-video-script/references/知识库/09_脚本诊断/故事成立性校验_通用_逻辑检查.md`
|
||||
> - `short-video-script/references/创作流程/11_脚本检查和诊断.md`(诊断方法参考)
|
||||
|
||||
---
|
||||
|
||||
## 对比分析流程(三段式:AI 脚本 → 源脚本 → 对比)
|
||||
|
||||
对「AI脚本 vs 视频源脚本」做对比复盘(详细见 `references/脚本复盘流程.md` 三、对比分析模式):
|
||||
|
||||
**第 1 段:AI 脚本独立分析**
|
||||
- 数据:`rewrite_log.script_text` + `rewrite_log.topic`(AI 选题)
|
||||
- 10 维逐维评分(同独立分析模式)
|
||||
|
||||
**第 2 段:源脚本独立分析**
|
||||
- 数据:`account_video_source.content_json`
|
||||
- 优先:content 中「# 视频脚本」+「# 选题」(新格式)
|
||||
- 回退:`account_videos.topic`(源选题)+ 分镜头表(旧格式)
|
||||
- 关联:rewrite_id → video_id → account_videos.id
|
||||
- 10 维逐维评分(同独立分析模式)
|
||||
|
||||
**第 3 段:对比(中立评估)**
|
||||
- 逐维对比表:**相同的点 / AI 好的点 / AI 差的点 / 中立结论(AI优于/劣于/持平源)**
|
||||
- 总分对比:AI X/100 vs 源 Y/100
|
||||
- 综合判断:AI 整体优于/劣于/各有优劣 + 建议(保留优势、补回被削弱的)
|
||||
|
||||
**中立原则**:不预设立场——客观列出差异,每维给依据。
|
||||
|
||||
---
|
||||
|
||||
## 创作过程复盘流程
|
||||
|
||||
对出问题的创作过程做归因复盘(方法见 `Creation-Review-Method/README.md`):
|
||||
|
||||
```
|
||||
发现问题(逻辑/情绪/动机/传播问题)
|
||||
→ 1. 归因三层:事件层?手法层?流程层?
|
||||
→ 2. 定位环节:S5 前提声明?S6 事件/复盘?S7 展开?S9 演绎?S11 兜底?
|
||||
→ 3. 判断是「执行偏差」还是「流程缺口」
|
||||
· 执行偏差 → 修改对应步骤产物,重跑该环节
|
||||
· 流程缺口 → 若需改创作技能,先列改动清单等授权(方案与执行分离)
|
||||
→ 4. 沉淀:新问题/新经验写入 Creation-Review-Method/README.md
|
||||
→ 5. 验证:用新流程重跑同类场景,确认能拦截
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 经验沉淀
|
||||
|
||||
- 复盘中发现**经验库未覆盖**的新问题类型 → 按经验格式(来源/问题/归因/校验要点)追加到 `Creation-Review-Method/README.md`
|
||||
- 经验编号顺延(当前至经验 10),保持「创作内容/流程/分析」类经验在 Creation-Review-Method
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
script-review/ ← Skill 根目录
|
||||
├── SKILL.md ← 入口(触发条件 + 复盘流程 + 沉淀规则)
|
||||
├── Creation-Review-Method/
|
||||
│ └── README.md ← 创作过程复盘方法 + 经验库(三层归因 + 9 条经验 + 复盘流程)
|
||||
└── references/
|
||||
└── 脚本复盘流程.md ← 已有脚本质量复盘流程(判定标准跨技能引用)
|
||||
```
|
||||
|
||||
## 与相关技能的关系
|
||||
|
||||
| 技能 | 职责 | 协作 |
|
||||
|------|------|------|
|
||||
| short-video-script | 脚本**创作** + 创作流程内 S11 诊断 | 本技能只读引用其判定标准 |
|
||||
| **script-review(本技能)** | 已有脚本**复盘** + 创作过程**复盘** + 经验沉淀 | 独立运行,不侵入创作流程 |
|
||||
@@ -0,0 +1,295 @@
|
||||
# 脚本复盘流程(已有脚本质量复盘)
|
||||
|
||||
> 对「已有脚本」做质量复盘——独立于创作流程(S1-S11),只分析不修改。
|
||||
> 判定标准**跨技能只读引用** short-video-script 的 `references/知识库/09_脚本诊断/`,不复制内容,保证与创作技能诊断口径一致。
|
||||
|
||||
---
|
||||
|
||||
## 〇、两种执行模式(按需求选择)
|
||||
|
||||
| 模式 | 适用需求 | 输出 |
|
||||
|------|---------|------|
|
||||
| **独立分析** | 「复盘这个脚本」「检查脚本质量」「给脚本打分」 | 每维 100 分制评分 + 总分(10 维等权平均) |
|
||||
| **对比分析** | 「对比 AI脚本 和 源脚本」「分析两者差异」 | 三段式(AI→源→对比),逐维「相同的点/好点/差点」差异评析 |
|
||||
|
||||
> 用户要求「对比/差异分析」→ 对比分析模式;否则默认独立分析模式。
|
||||
|
||||
---
|
||||
|
||||
## 一、适用范围
|
||||
|
||||
| 场景 | 说明 |
|
||||
|------|------|
|
||||
| 复盘已有脚本 | 任意已保存的脚本(如 AI写作复盘 页的脚本 #23、历史改写记录等) |
|
||||
| 点评脚本 | 用户给出脚本文本/文件,要求质量点评(独立分析) |
|
||||
| 对比分析 | 同一选题多版脚本 / AI脚本 vs 源脚本 对比(对比分析) |
|
||||
|
||||
**不适用**:创作流程内的 S11 诊断(那是创作最后一步,对刚生成的脚本做质检,属 short-video-script 职责)。
|
||||
|
||||
---
|
||||
|
||||
## 二、独立分析模式(评分制)
|
||||
|
||||
### 1. 读取脚本
|
||||
从用户提供处读取:脚本文本 / 文件路径 / 数据库记录(如 mcn-plugin.db 的 rewrite_log)。
|
||||
|
||||
### 2. 逐维评分(每维 100 分制,总分等权平均)
|
||||
|
||||
> 判定标准**只读引用** `short-video-script/references/知识库/09_脚本诊断/`(八类35项 + 故事成立性校验),逐维检查并评分。
|
||||
> 维度顺序与 AI写作复盘 表格列对齐(设定承接/框架节奏/钩子设置/叙事逻辑/极致事件/共鸣和共情),表格外维度追加在后。
|
||||
|
||||
#### 评分锚点(每维 100 分制)
|
||||
|
||||
| 分数段 | 档位 | 含义 |
|
||||
|--------|------|------|
|
||||
| 90-100 | 优秀 | 接近爆款水准(满分极少,需有可对标爆款案例支撑) |
|
||||
| 75-89 | 良好 | 达到行业平均水平 |
|
||||
| 60-74 | 及格 | 基本可用但不出彩 |
|
||||
| 40-59 | 有明显问题 | 影响传播效果,需修改 |
|
||||
| 0-39 | 严重缺陷 | 不可用,必须大改 |
|
||||
|
||||
> 评分不是「有没有问题」而是「多接近爆款水准」——高分需证明「好在哪里」,低分需指出「差在哪里」。
|
||||
|
||||
#### 总分(叙事逻辑 2 倍权重)
|
||||
|
||||
```
|
||||
总分 = (Σ其他适用维得分 + 叙事逻辑×2) / (适用维数 + 1)
|
||||
说明:叙事逻辑权重 2 倍(故事逻辑是剧情类立身之本,出问题总分受重大影响);
|
||||
不适用维度标记 null/N-A,不计入分母(如剧情类的教程完整链)
|
||||
判定:≥75 可交付;60-74 需优化;<60 需大改
|
||||
```
|
||||
|
||||
#### 2.1 先判内容类型(防误用,必做)
|
||||
|
||||
> ⚠️ **复盘前必须先判定脚本的内容类型**,再按「适用类型」决定检查哪些项——避免把剧情脚本当教程检查(如"教人发假快递")、或把教程脚本当剧情查故事成立性。
|
||||
|
||||
| 内容类型 | 判定依据 | 检查范围 |
|
||||
|---------|---------|---------|
|
||||
| **剧情/悬念/反转类** | 有角色/故事线/秘密/反转/倒叙(如假快递守护、情感短剧) | 表格6维 + 格式/台词/合规 + **故事成立性(强制)**;教程完整链不适用 |
|
||||
| **教程/干货类** | 教人做事:成品前置→步骤→收尾(如做菜、教学) | 表格6维 + 格式/台词/合规 + **教程完整链(强制)**;故事成立性不适用 |
|
||||
| **口播/观点/测评类** | 口播表达/观点输出/产品测评,无故事线 | 表格6维 + 格式/台词/合规;教程完整链、故事成立性均不适用 |
|
||||
|
||||
**判定错误的风险**:把剧情脚本套教程链检查 → 荒谬误判("教人发假快递");把教程脚本套故事成立性 → 错误打回。判定后按上表执行,不越界检查。
|
||||
|
||||
---
|
||||
|
||||
### 表格维度(6 项,与 AI写作复盘 列表对齐)
|
||||
|
||||
#### 维度1:设定承接
|
||||
|
||||
| 检查项 | 判断标准 | 判定 |
|
||||
|--------|---------|:---:|
|
||||
| 设定承接 | 人设卡记忆符号/行为决策/内容规则在脚本中有体现 → 缺关键机制=不过 | 通过/不过 |
|
||||
|
||||
#### 维度2:框架节奏
|
||||
|
||||
| 检查项 | 判断标准 | 判定 |
|
||||
|--------|---------|:---:|
|
||||
| 逻辑链条 | 前后因果关系清晰,无明显跳跃 → 逻辑混乱=不过 | 通过/不过 |
|
||||
| 爆点贯穿 | 全片至少2次爆点(开头+至少1次中后段)→ 仅开头有=不过 | 通过/不过 |
|
||||
| 钩子密度 | 每15秒至少1个小钩子/转折点 → 平铺直叙段=不过 | 通过/不过 |
|
||||
| 信息密度 | 每10秒有新的有效信息 → 同信息重复=不过 | 通过/不过 |
|
||||
| 节奏变化 | 有快慢交替(短句轰炸↔长句铺陈)→ 全程一个节奏=提醒 | 提醒 |
|
||||
| 结尾收口 | 有明确的总结/话题/互动引导 → 戛然而止=不过 | 通过/不过 |
|
||||
|
||||
#### 维度3:钩子设置(全片留人链路)
|
||||
|
||||
> 不只开场钩子——覆盖「开场抓人 → 中间留人 → 结尾收口」全片钩子体系。
|
||||
|
||||
| 检查项 | 判断标准 | 判定 |
|
||||
|--------|---------|:---:|
|
||||
| 开场钩子 | 前3秒有画面+动作+钩子 → 空泛叙述=不过 | 通过/不过 |
|
||||
| 中间留人钩子 | 每15秒至少1个小钩子/转折点 → 平铺直叙段=不过 | 通过/不过 |
|
||||
| 中段不疲 | 30-50秒持续有新信息注入 → 中段重复前段内容=不过 | 通过/不过 |
|
||||
| 结尾收口 | 有明确的总结/话题/互动引导 → 戛然而止=不过 | 通过/不过 |
|
||||
|
||||
#### 维度4:叙事逻辑
|
||||
|
||||
> 命中**人物行为/故事逻辑硬伤**(行为来源穿帮如"随身携带体积不符的物件"、动机断裂、时间线/年龄账矛盾、逻辑范围失当、为反转而反转/因果跳跃)→ 严重度**「高」**(逻辑有问题其他维度不必再谈),**叙事逻辑维按硬伤数量梯度扣分**:1 个 → ≤60、2 个 → ≤40、3 个及以上 → ≤20——此类问题直接击穿故事成立性,不按普通瑕疵扣分。
|
||||
|
||||
| 检查项 | 判断标准 | 判定 |
|
||||
|--------|---------|:---:|
|
||||
| 前提一致性 | 事件是否破坏核心秘密/设定?有无认知倒挂? | 通过/不过 |
|
||||
| 因果链条 | 事件可否由前文推导?有无跳跃? | 通过/不过 |
|
||||
| 动机合理性 | 行为有无动机铺垫?烈度是否匹配? | 通过/不过 |
|
||||
| **行为来源与时间线** | 行为道具/资源从何而来(携带/使用是否违背常理)?为何"此刻"行动、之前为何不做?临时起意 vs 预先准备是否矛盾? | 通过/不过 |
|
||||
| 信息合法性 | 角色说的每句关键断言是否在其认知边界内? | 通过/不过 |
|
||||
| 表达方式匹配 | 冲突强度来自动机与台词,还是堆砌摔/吼/围观? | 通过/不过 |
|
||||
| 逻辑链条 | 前后因果关系清晰,无明显跳跃 → 逻辑混乱=不过 | 通过/不过 |
|
||||
|
||||
#### 维度5:极致事件
|
||||
|
||||
| 检查项 | 判断标准 | 判定 |
|
||||
|--------|---------|:---:|
|
||||
| 爆点贯穿 | 全片至少2次爆点(开头+至少1次中后段)→ 仅开头有=不过 | 通过/不过 |
|
||||
| 动作描述 | 镜头描述含具体动词(走/拿/放/指/举/翻)→ 没有=不过 | 通过/不过 |
|
||||
| 场景描述 | 有具体场景/道具/环境 → 抽象场景=提醒 | 提醒 |
|
||||
| 不可拍警告 | 含"感觉/氛围/意境/仿佛"等抽象词 → 标记提醒 | 提醒 |
|
||||
|
||||
#### 维度6:共鸣和共情
|
||||
|
||||
| 检查项 | 判断标准 | 判定 |
|
||||
|--------|---------|:---:|
|
||||
| 共鸣类型命中 | 至少命中6种共鸣类型中的1种(经历/情绪/观点/身份/痛点/反差)→ 无法判断=不过 | 通过/不过 |
|
||||
| 具象场景检验 | 至少有1个"零解释秒懂"的具象场景(时间/地点/动作/道具)→ 全是大道理=不过 | 通过/不过 |
|
||||
| 自嗨检测 | 把"我"换成目标人群角色(打工人/宝妈/学生)仍成立 → 只有创作者成立=不过 | 通过/不过 |
|
||||
| 3问自检 | 观众能联想自己经历 + 会评论"我也是" + 不全是空洞大道理 → 3问全不过=不过 | 通过/不过 |
|
||||
|
||||
---
|
||||
|
||||
### 追加维度(表格外,补充检查)
|
||||
|
||||
#### 维度7:格式规范性
|
||||
|
||||
| 检查项 | 判断标准 | 判定 |
|
||||
|--------|---------|:---:|
|
||||
| 画面描述 | 每个镜头有具体动作/场景 → 纯文字叙述=不过 | 通过/不过 |
|
||||
| 景别标注 | 关键镜头有景别(特写/近景/中景/远景)→ 缺失=提醒 | 提醒 |
|
||||
| 时长标注 | 每个镜头段有时长说明 → 缺失=提醒 | 提醒 |
|
||||
|
||||
#### 维度8:台词口语化
|
||||
|
||||
| 检查项 | 判断标准 | 判定 |
|
||||
|--------|---------|:---:|
|
||||
| 书面词汇 | 无"此外/综上所述/由此可见"等书面连接词 → 出现=不过 | 通过/不过 |
|
||||
| 长句检测 | 单句不超过30字 → 超过=不过 | 通过/不过 |
|
||||
| 自然语气词 | 有口语化语气词(呢/吧/啊/哦/嘛)→ 完全没有=不过 | 通过/不过 |
|
||||
| 重复用词 | 同一非关键词3秒内不重复 → 重复=提醒 | 提醒 |
|
||||
| 人设一致性 | 台词逐句对表人设卡(说话风格/口头禅/行为决策)→ 明显不符=不过 | 通过/不过 |
|
||||
|
||||
#### 维度9:内容合规
|
||||
|
||||
| 检查项 | 判断标准 | 判定 |
|
||||
|--------|---------|:---:|
|
||||
| 绝对化词汇 | 命中"第一/最好/唯一/极致/顶级/最X"=不过 | 直接不过 |
|
||||
| 绝对效果承诺 | 命中"绝对有效/100%/立竿见影/保证/根治"=不过 | 直接不过 |
|
||||
| 无来源数据 | "提升X倍/效果翻倍"无来源标注=不过 | 直接不过 |
|
||||
| 竞品名称 | 含竞品品牌名/产品名=警告 | 警告 |
|
||||
| 暗示贬低 | 含"比某些/比别的/比市面上"=警告 | 警告 |
|
||||
| 行业违禁词 | 医美(消除/根治/永久/祛除)/美妆(祛斑/消炎/杀菌/抗皱)/食品(治疗/治愈/预防疾病)=不过 | 直接不过 |
|
||||
|
||||
#### 维度10:教程完整链(**仅教程/干货类适用**)
|
||||
|
||||
| 检查项 | 判断标准 | 判定 |
|
||||
|--------|---------|:---:|
|
||||
| 教程完整链 | 成品/效果前置→材料清单→逐步操作(≥3步,每步含关键技巧)→成品对比收尾;剧情/口播/测评类=不适用,跳过 | 通过/不过 |
|
||||
|
||||
---
|
||||
|
||||
### 总分(最后统计,100 分制)
|
||||
|
||||
| 汇总 | 计算 |
|
||||
|------|------|
|
||||
| 各维得分 | 10 维各得 X/100 分 |
|
||||
| **总分** | **Σ(10 维得分) / 10**(等权平均) |
|
||||
| 综合判定 | ≥75 可交付;60-74 需优化;<60 需大改 |
|
||||
|
||||
> 每维评分参考判定锚点(90-100 优秀 / 75-89 良好 / 60-74 及格 / 40-59 有问题 / 0-39 严重缺陷)。
|
||||
|
||||
### 4. 输出复盘报告
|
||||
|
||||
```
|
||||
## 脚本复盘报告(独立分析)
|
||||
- 脚本:标题 / 来源(id 或文件)
|
||||
- 内容类型判定:剧情/教程/口播测评(决定检查范围)
|
||||
- 表格维度评分(与 AI写作复盘 列对齐,每维 100 分制):
|
||||
| 维度 | 检查项 | 得分 | 档位 | 问题定位 |
|
||||
|------|--------|:---:|:---:|---------|
|
||||
| 设定承接 | 记忆符号/行为决策/内容规则体现 | X/100 | 优秀/良好/及格/有问题/严重 | … |
|
||||
| 框架节奏 | 逻辑链条/爆点贯穿/钩子密度/信息密度/节奏变化/收口 | X/100 | … | … |
|
||||
| 钩子设置 | 开场钩子/中间留人/中段不疲/结尾收口 | X/100 | … | … |
|
||||
| 叙事逻辑 | 前提/因果/动机/信息/表达方式/逻辑链条 | X/100 | …(剧情类) | … |
|
||||
| 极致事件 | 爆点贯穿/动作描述/场景/可拍性 | X/100 | … | … |
|
||||
| 共鸣和共情 | 类型命中/具象场景/自嗨检测/3问自检 | X/100 | … | … |
|
||||
- 追加维度评分(表格外,每维 100 分制):
|
||||
| 维度 | 检查项 | 得分 | 档位 | 问题定位 |
|
||||
|------|--------|:---:|:---:|---------|
|
||||
| 格式规范性 | 画面描述/景别/时长 | X/100 | … | … |
|
||||
| 台词口语化 | 书面词/长句/语气词/重复词/人设一致性 | X/100 | … | … |
|
||||
| 内容合规 | 绝对化/承诺/数据来源/竞品/违禁词 | X/100 | … | … |
|
||||
| 教程完整链 | 仅教程类:成品→材料→步骤→收尾 | X/100 | …(教程类) | … |
|
||||
- 总分统计:
|
||||
| 汇总 | 结果 |
|
||||
|------|------|
|
||||
| 各维得分 | 设定承接 X、框架节奏 X、…、教程链 X |
|
||||
| **总分** | **Σ/10 = X/100**(等权平均) |
|
||||
| 综合判定 | ≥75 可交付 / 60-74 需优化 / <60 需大改 |
|
||||
- 问题清单(按严重度):
|
||||
| # | 严重度 | 定位 | 问题 | 修改建议 | 所属环节 |
|
||||
|---|:---:|------|------|---------|---------|
|
||||
| 1 | 🔴高 | 场X台词 | … | … | S6/S7/S9 |
|
||||
- 总结论:总分 X/100,✅ 可交付 / ⚠️ 需优化 / ❌ 需大改
|
||||
```
|
||||
|
||||
### 5. 沉淀经验
|
||||
复盘发现**经验库未覆盖**的新问题类型 → 追加到 `Creation-Review-Method/README.md`(格式:来源/问题/归因/校验要点,编号顺延)。
|
||||
|
||||
---
|
||||
|
||||
## 三、对比分析模式(三段式:AI 脚本 → 源脚本 → 对比)
|
||||
|
||||
> 适用:用户要求「对比 AI脚本 和 源脚本」「分析两者差异」「对比复盘」。
|
||||
> 先各边独立分析(中立基准),再对比差异——不预设 AI 好或源好,客观评析。
|
||||
|
||||
### 第 1 段:AI 脚本独立分析
|
||||
|
||||
**数据源**:
|
||||
- `rewrite_log.script_text`(AI 故事脚本全文)
|
||||
- `rewrite_log.topic`(AI 选题:赛道/受众/视频主题/开场钩子等)
|
||||
|
||||
**执行**:按「独立分析模式」的 10 维逐维检查并评分(每维 100 分制),输出 AI 脚本评估。
|
||||
|
||||
### 第 2 段:源脚本独立分析
|
||||
|
||||
**数据源**(按优先级):
|
||||
1. **优先**:`account_video_source.content_json` 的 content 中「# 视频脚本」章节(完整源故事脚本)+ 同 content 的「# 选题」(源选题)——**新格式**
|
||||
2. **回退**:只有「# 分镜头表」时(旧格式)——源选题用 `account_videos.topic`(关联 rewrite.video_id → account_videos.id),故事内容用分镜头表
|
||||
|
||||
**数据关联链**:
|
||||
```
|
||||
rewrite_id → video_id → account_videos.id
|
||||
→ account_videos.topic(源选题)
|
||||
→ account_video_source.content_json(源脚本:优先 # 视频脚本,回退分镜头表)
|
||||
```
|
||||
|
||||
**执行**:同一套 10 维逐维检查并评分,输出源脚本评估。
|
||||
|
||||
### 第 3 段:对比(中立评估)
|
||||
|
||||
**方法**:两段评估结果**逐维对比**,不重新评分——列差异,给中立结论。
|
||||
|
||||
| 维度 | AI 脚本 | 源脚本 | 相同的点 | AI 好的点 | AI 差的点 | 中立结论 |
|
||||
|------|---------|--------|---------|----------|----------|---------|
|
||||
| 设定承接 | X/100 | X/100 | … | … | … | AI优于/劣于/持平 |
|
||||
| 框架节奏 | … | … | … | … | … | … |
|
||||
| 钩子设置 | … | … | … | … | … | … |
|
||||
| 叙事逻辑 | … | … | … | … | … | … |
|
||||
| 极致事件 | … | … | … | … | … | … |
|
||||
| 共鸣和共情 | … | … | … | … | … | … |
|
||||
| 格式规范性 | … | … | … | … | … | … |
|
||||
| 台词口语化 | … | … | … | … | … | … |
|
||||
| 内容合规 | … | … | … | … | … | … |
|
||||
| 教程完整链 | … | … | … | … | … | … |
|
||||
|
||||
**对比要点**:
|
||||
- **相同的点**:两脚本共有的设定/结构/钩子/情感点(继承自同一选题)
|
||||
- **AI 好的点**:AI 相对源脚本改进/优化之处(改写的剧情、更强的钩子、更顺的情绪)
|
||||
- **AI 差的点**:AI 相对源脚本退化/缺失之处(丢掉的爆点、变弱的共鸣、逻辑硬伤)
|
||||
- **中立结论**:每维判「AI优于/劣于/持平源脚本」,并给依据——不预设立场
|
||||
|
||||
**对比总结论**:
|
||||
```
|
||||
- 各维中立结论汇总:AI 优于 N 维、劣于 M 维、持平 K 维
|
||||
- 总分对比:AI 总分 X/100 vs 源总分 Y/100
|
||||
- 综合判断:AI 脚本整体 优于/劣于/与源脚本各有优劣 源脚本
|
||||
- 建议:保留 AI 优势维度、补回源脚本被削弱的部分
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、行为约束
|
||||
|
||||
1. **只分析不修改**:复盘输出报告与建议,不直接改脚本正文、不改创作技能文件
|
||||
2. **跨技能只读**:引用 short-video-script 判定标准时只读取,不复制、不修改其文件
|
||||
3. **方案与执行分离**:用户要求"复盘/分析"→ 只输出报告;如需修改脚本/技能 → 先列改动清单等授权
|
||||
4. **不触发创作流程**:复盘不进入 S1-S11,不影响创作技能的任何步骤
|
||||
@@ -8,8 +8,8 @@ description: >
|
||||
角色卡、首帧图、运镜、景别、时间戳分镜、图生视频。
|
||||
version: "1.0"
|
||||
agent_created: true
|
||||
updated_at: "2026-08-26"
|
||||
last_change: "产出结构定稿:①开发机与③用户环境产出根目录由 桌面 MCNSkill项目/分镜提示词/ 改为 桌面 MCNSkill项目/(账号层直挂:{账号名}/提示词{编号}/,去掉「分镜提示词」技能文件夹层,与脚本创作「脚本{编号}」同构);dsh 环境不变 工作区 D:\dshworkspace\创作任务\{账号名}\提示词{编号}\(references-add/路径配置.md 判定表+执行规则+解析规则+示例+变更记录,SKILL.md 声明+示例树)"
|
||||
updated_at: "2026-08-31"
|
||||
last_change: "提示词输出结构 4层→6层重排(用户定稿):①主体列表→②画面质感(视觉风格+摄影质感前置)→③色调光影→④主体设定(人物/场景/构图)→⑤镜头叙事→⑥负面提示词;原「设定」层拆为画面质感/色调光影/主体设定三层,视觉基调前置(模型对提示词开头权重最高);同步 3.2.2 输出格式+3.2.1 思考清单+6层×5类型适配表+Step5 校验②结构完整性+SKILL.md F4/工作流/自检⑧引用(分镜提示词制作流程.md 同步修订)"
|
||||
deployment: source
|
||||
created_at: "2026-08-20"
|
||||
---
|
||||
@@ -202,7 +202,9 @@ F2 在 5 步工作流框架内执行,其中步骤 2-3 包含两轮用户交互
|
||||
> **⚠️ 字符限制冲突说明:** seedance2.0-prompt-skill 实测平台接受上限 2000 字符;Seedance2.0-ShotDesign v2.0 官方规范建议 ≤500 字符(中文)/ ≤1000 词(英文),品质最优区间 60-100 词(中文约 120-200 字符)。**以 500 字符为品质基准,2000 字符为硬性上限。**
|
||||
|
||||
**详细方法论:**
|
||||
- 6步总流程+路径分流+4层结构(主体按类别分行+负面词放最后+镜头朝向规则,11字段降为内部思考清单)+时长速查:`references/制作流程/分镜提示词制作流程.md`
|
||||
- 6步总流程+路径分流+6层结构(主体/画面质感/色调光影/主体设定/镜头叙事/负面词,视觉基调前置,11字段降为内部思考清单)+时长速查:`references/制作流程/分镜提示词制作流程.md`
|
||||
|
||||
**详细方法论:**
|
||||
- 路径C双范式(反应/凝视)+11条核心原则+版权审查SOP:`references/制作流程/图生视频与分镜板制作流程.md`
|
||||
- 四维编码(Z景别/Y高度/X方位/F滤镜):`references/知识库/04_镜头类型/镜头景别词库.md` + `references/知识库/06_焦距视角/焦距视角词库.md`
|
||||
- 运镜安全写法+运动强度分级:`references/知识库/05_运镜方式/运镜方式词库.md`
|
||||
@@ -252,7 +254,7 @@ F2 在 5 步工作流框架内执行,其中步骤 2-3 包含两轮用户交互
|
||||
|
||||
**前置:剧本分析(Step 1,必做)**
|
||||
|
||||
进入路径分流前,先完成剧本类型判定(A叙事剧情/B单人Vlog/C旁白知识/D产品展示/E纯氛围)+ 维度拆解(①节奏全类型必做,②-⑤按类型选做),产出驱动后续路径选择、景别设计、微表情弧线、台词语气修饰、4层格式适配。详见 `references/制作流程/分镜提示词制作流程.md` Step 1。
|
||||
进入路径分流前,先完成剧本类型判定(A叙事剧情/B单人Vlog/C旁白知识/D产品展示/E纯氛围)+ 维度拆解(①节奏全类型必做,②-⑤按类型选做),产出驱动后续路径选择、景别设计、微表情弧线、台词语气修饰、6层格式适配。详见 `references/制作流程/分镜提示词制作流程.md` Step 1。
|
||||
|
||||
**处理流程(按路径分流):**
|
||||
|
||||
@@ -270,7 +272,7 @@ F4 输出 = 主题 + 导演阐述 + 完整提示词(代码块包裹)。多
|
||||
|
||||
**单段(路径 A,≤15s):** 主题 + 导演阐述 + 单块代码块提示词
|
||||
|
||||
提示词内部结构(4层结构,按类型适配):`①主体列表(置顶,按类别分行:人物/场景/道具,@图片引用优先)→ ②设定(人物逐人必写不可遗漏/场景/光线/构图/视觉风格,每项1句,按类型取舍)→ ③镜头叙事(每镜头1段流畅描述:机位/景别/朝向+动作/变化+运镜嵌在动作中+有台词时带语气)→ ④负面提示词(放最后,3.3标准模板)`
|
||||
提示词内部结构(6层结构,按类型适配):`①主体列表(置顶,按类别分行:人物/场景/道具,@图片引用优先)→ ②画面质感(视觉风格+摄影质感参数,视觉基调前置)→ ③色调光影(光源+主光方向+亮暗区+色调/色温,1句)→ ④主体设定(人物逐人必写不可遗漏/场景/构图,每项1句,按类型取舍)→ ⑤镜头叙事(每镜头1段流畅描述:机位/景别/朝向+动作/变化+运镜嵌在动作中+有台词时带语气)→ ⑥负面提示词(放最后,3.3标准模板)`
|
||||
|
||||
> 11字段(一致性锚定/类型质感/人物设定/场景/景深对焦/摄影质感/表演幅度/背景动态/镜头空间/表演指令/负面词)降为**构建时内部思考清单**,不作为输出字段标签。详见制作流程第三节 3.2。
|
||||
|
||||
@@ -287,7 +289,7 @@ F4 输出 = 主题 + 导演阐述 + 完整提示词(代码块包裹)。多
|
||||
**剪辑节奏公式(>15秒必选):** 呼吸式 / 心跳式 / 海浪式 / 子弹时间 / 脉冲式 / 静默锤击
|
||||
|
||||
**详细方法论:**
|
||||
- 创意策略+6步总流程+路径分流+4层结构(主体按类别分行+负面词放最后+镜头朝向规则,11字段降为内部思考清单)+标准负面词:`references/制作流程/分镜提示词制作流程.md`
|
||||
- 创意策略+6步总流程+路径分流+6层结构(主体/画面质感/色调光影/主体设定/镜头叙事/负面词,视觉基调前置,11字段降为内部思考清单)+标准负面词:`references/制作流程/分镜提示词制作流程.md`
|
||||
- 长视频策略+生产流水线(25格聚合矩阵+节奏曲线+景别曲线+P0/P1/P2优先级):`references/制作流程/长视频分段制作流程.md`
|
||||
- 分镜板驱动(路径D格间关系三分类+三层动作分级):`references/制作流程/图生视频与分镜板制作流程.md`
|
||||
- 剪辑节奏(六大节奏公式+AI素材专属5对策+9步工作流):`references/制作流程/多镜头剪辑节奏流程.md`
|
||||
@@ -391,7 +393,7 @@ F4 输出 = 主题 + 导演阐述 + 完整提示词(代码块包裹)。多
|
||||
|:---:|:---|:---|
|
||||
| 1 | 需求解析与参数确认 | 确认时长/比例/生成模式/风格/素材;智能推理原则——能推理的不追问,追问控制在 1-2 个问题内 |
|
||||
| 2 | 视觉诊断与分镜构思 | 三层知识库路由加载参考→构思分镜草案→选定导演风格与视觉方案 |
|
||||
| 3 | 提示词构建 | 按4层结构撰写提示词(按类型适配选层;构建时用11字段思考清单内部过目),时间轴分镜,一镜一动 |
|
||||
| 3 | 提示词构建 | 按6层结构撰写提示词(按类型适配选层;构建时用11字段思考清单内部过目),时间轴分镜,一镜一动 |
|
||||
| 4 | 强制自我校验 | 8 项校验规则逐条检查,有 error 必重写,全部通过才可交付 |
|
||||
| 5 | 专业交付 | 主题+导演阐述+完整提示词(代码块包裹),多段分镜附过渡策略 |
|
||||
|
||||
@@ -403,9 +405,9 @@ F4 输出 = 主题 + 导演阐述 + 完整提示词(代码块包裹)。多
|
||||
|
||||
### 提示词模板
|
||||
|
||||
> 详见 `references/制作流程/分镜提示词制作流程.md` 第三节:3.1 基础六要素(构建思考参考)+ 3.2 4层结构(输出格式,含11字段内部思考清单 3.2.1 + 4层式输出 3.2.2含类型适配表,负面词放最后)+ 3.3 标准负面词模板。
|
||||
> 详见 `references/制作流程/分镜提示词制作流程.md` 第三节:3.1 基础六要素(构建思考参考)+ 3.2 6层结构(输出格式,含11字段内部思考清单 3.2.1 + 6层式输出 3.2.2含类型适配表,负面词放最后)+ 3.3 标准负面词模板。
|
||||
|
||||
4层结构按Step 1类型判定适配:①主体列表(置顶,按类别分行:人物/场景/道具,@图片引用优先)+ ②设定(人物/场景/光线/构图/视觉风格,每项1句,按类型取舍)+ ③镜头叙事(每镜头1段流畅描述,朝向必须写明,连续动作不拆,有台词时带语气)+ ④负面提示词(放最后,3.3标准模板)。构建时用11字段思考清单内部过目确保不遗漏(一致性锚定/类型质感/人物设定/场景/景深对焦/摄影质感/表演幅度/背景动态/镜头空间/表演指令/负面词),但最终输出不出现字段标签。
|
||||
6层结构按Step 1类型判定适配:①主体列表(置顶,按类别分行:人物/场景/道具,@图片引用优先)+ ②画面质感(视觉风格+摄影质感参数,视觉基调前置)+ ③色调光影(光源+主光方向+亮暗区+色调/色温,1句)+ ④主体设定(人物/场景/构图,每项1句,按类型取舍)+ ⑤镜头叙事(每镜头1段流畅描述,朝向必须写明,连续动作不拆,有台词时带语气)+ ⑥负面提示词(放最后,3.3标准模板)。构建时用11字段思考清单内部过目确保不遗漏(一致性锚定/类型质感/人物设定/场景/景深对焦/摄影质感/表演幅度/背景动态/镜头空间/表演指令/负面词),但最终输出不出现字段标签。
|
||||
|
||||
### 光影三层结构
|
||||
|
||||
@@ -478,7 +480,7 @@ F4 输出 = 主题 + 导演阐述 + 完整提示词(代码块包裹)。多
|
||||
- [ ] ⑤ 资产引用限制:图片≤9、视频≤3、音频≤3、混合≤12
|
||||
- [ ] ⑥ 冲突检测:运动冲突(快速+慢动作)、光学冲突(超广角+浅景深)、风格冲突(IMAX vs VHS 等)
|
||||
- [ ] ⑦ 裸英文运镜词检测:Dolly/Aerial/Crane/Pan/Arc/Dutch/Steadicam → 中文或完整短语
|
||||
- [ ] ⑧ 结构完整性:核心层(主体+负面词+镜头叙事)必须存在,②设定按类型取舍(A/B全用/C人物可选/D/E人物省略,详见制作流程第三节 3.2.2 类型适配表)
|
||||
- [ ] ⑧ 结构完整性:核心层(主体+负面词+镜头叙事)必须存在,②画面质感/③色调光影全类型必用,④主体设定按类型取舍(A/B全用/C人物可选/D/E人物省略,详见制作流程第三节 3.2.2 类型适配表)
|
||||
|
||||
**通用:**
|
||||
- [ ] 提示词有清晰主体
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
```
|
||||
剧本分析
|
||||
├─ 类型判定 → 4层格式适配(哪些层省略/变形,见3.2.2类型适配表)
|
||||
├─ 类型判定 → 6层格式适配(哪些层省略/变形,见3.2.2类型适配表)
|
||||
├─①节奏+台词风格 → 路径分流 + 段落时长 + 台词语气修饰
|
||||
├─②爆点/视觉高潮 → 景别递进 + 运镜设计(A/B/C类)
|
||||
├─③反转 → 镜头逻辑 + 首末帧(A类)
|
||||
@@ -61,7 +61,7 @@
|
||||
> - 类型=D → 只做①,②-⑤跳过
|
||||
> - ①节奏=视觉流:产品亮相→质地特写→涂抹过程→效果呈现 / 线性 / 铺陈(亮相)→蓄力(特写)→爆发(涂抹)→余韵(效果) → **路径A + 单段8秒**
|
||||
> - 台词风格=N/A(无对白,可选旁白)
|
||||
> - 驱动4层格式:①主体=护肤品(列道具行) ②设定-人物省略 ③镜头=产品展示流程(非角色动作)
|
||||
> - 驱动6层格式:①主体=护肤品(列道具行) ④主体设定-人物省略 ⑤镜头=产品展示流程(非角色动作)
|
||||
|
||||
### Step 2 需求分析
|
||||
- 明确:时长、画幅(9:16 竖屏 / 16:9 横屏)、单镜头还是多镜头、有无参考素材
|
||||
@@ -79,12 +79,12 @@
|
||||
| D 分镜板驱动 | 分镜脚本/多格规划 | 格间关系三分类 | 《图生视频与分镜板制作流程》 |
|
||||
|
||||
### Step 4 提示词构建
|
||||
按第三节输出格式组装:按Step 1类型判定结果选择4层结构的适用层(见3.2.2类型适配表);构建时用11字段思考清单(3.2.1)内部过目确保不遗漏。词表从 `知识库/15维度` 各取所需。
|
||||
按第三节输出格式组装:按Step 1类型判定结果选择6层结构的适用层(见3.2.2类型适配表);构建时用11字段思考清单(3.2.1)内部过目确保不遗漏。词表从 `知识库/15维度` 各取所需。
|
||||
**剧本分析(Step 1)产出是提示词组装的内容来源**:景别/运镜由爆点驱动、场景调度由人物关系驱动、微表情弧线由情绪弱点驱动、人物设定由角色定位驱动、表演指令由情绪弱点驱动、负面词由场景类型勾选。
|
||||
|
||||
### Step 5 强制校验(8项)
|
||||
1. **字符数**:Python `len()` 实测,禁止估算;品质基准 ≤500 字符,硬性上限 ≤2000 字符(实测)
|
||||
2. **结构完整性**:输出必须包含核心层(①主体 + ②设定 + ③镜头叙事 + ④负面词),②设定层按类型适配(A全用/B全用/C人物可选/D人物省略/E人物省略);顺序固定主体→设定→镜头→负面词
|
||||
2. **结构完整性**:输出必须包含核心层(①主体 + ⑥负面词 + ⑤镜头叙事),②画面质感/③色调光影全类型必用,④主体设定层按类型适配(A全用/B全用/C人物可选/D人物省略/E人物省略);顺序固定主体→画面质感→色调光影→主体设定→镜头→负面词
|
||||
3. **版权扫描**:品牌词/风格借喻/涉灰场景词/夜场BGM词 四类扫描(详见《图生视频与分镜板制作流程》版权审查)
|
||||
4. **导演去名化**:提示词中不得出现导演名/工作室名/IP名,只用参数化描述
|
||||
5. **动作物理合理性**:三层动作分级检查(大动作禁止)
|
||||
@@ -126,10 +126,10 @@
|
||||
| 光影/风格/色彩 | 知识库/07~09、15 |
|
||||
| 音效(原生音效优先) | 知识库/13_音效音频 |
|
||||
|
||||
### 3.2 输出格式(4层结构)
|
||||
### 3.2 输出格式(6层结构)
|
||||
|
||||
> **实战验证**(2026-08-20):编导手写提示词(~350字,叙事驱动)生成效果优于结构化11字段模板(~700字,技术参数驱动)。**模型吃故事,不吃参数表。**
|
||||
> **改造原则**:11字段降为构建时**内部思考清单**(确保不遗漏),最终输出只写**4层结构**——主体置顶按类别分行、设定按类型分项、镜头叙事流畅、负面词放最后。技术参数溶解进设定各分项,表演靠动作暗示而非禁令。按Step 1类型判定结果选择4层的适用层。
|
||||
> **改造原则**:11字段降为构建时**内部思考清单**(确保不遗漏),最终输出只写**6层结构**——主体置顶按类别分行、画面质感与色调光影紧跟主体前置(全局视觉基调前置,模型对提示词开头权重最高)、主体设定按类型分项、镜头叙事流畅、负面词放最后。技术参数溶解进②画面质感/③色调光影/④主体设定各分项,表演靠动作暗示而非禁令。按Step 1类型判定结果选择6层的适用层。
|
||||
|
||||
#### 3.2.1 构建思考清单(内部,不落盘)
|
||||
|
||||
@@ -137,21 +137,21 @@
|
||||
|
||||
| 思考项 | 构建时想什么 | 输出时去哪 |
|
||||
|--------|-------------|-----------|
|
||||
| ①一致性锚定 | 角色服装/道具是否需文字锁定? | ①主体列表@图片引用锁定;无图才在②设定-人物补1句 |
|
||||
| ②类型质感 | 画质/风格/光影基调? | ②设定-视觉风格(1句) |
|
||||
| ③人物设定 | 逐角色外貌/着装/气质? | ①主体列表@图片锁定;无图在②设定-人物补1句(**主体中每个人物必写,不可遗漏**) |
|
||||
| ④场景 | 空间布局/道具? | ②设定-场景(1句空间描述) |
|
||||
| ⑤景深对焦 | 是否需非默认景深? | **默认不写**;特写需浅景深时加进②设定-构图 |
|
||||
| ①一致性锚定 | 角色服装/道具是否需文字锁定? | ①主体列表@图片引用锁定;无图才在④主体设定-人物补1句 |
|
||||
| ②类型质感 | 画质/风格/光影基调? | ②画面质感-视觉风格(1句) |
|
||||
| ③人物设定 | 逐角色外貌/着装/气质? | ①主体列表@图片锁定;无图在④主体设定-人物补1句(**主体中每个人物必写,不可遗漏**) |
|
||||
| ④场景 | 空间布局/道具? | ④主体设定-场景(1句空间描述) |
|
||||
| ⑤景深对焦 | 是否需非默认景深? | **默认不写**;特写需浅景深时加进④主体设定-构图 |
|
||||
| ⑥摄影质感 | 机型/镜头/光圈/快门/ISO/色彩/光影/质感/动态范围? | 按维度自动生成英文摄影参数(从知识库/10_后期细节·摄影质感维度表取值,判断适用维度组合),排除画幅/帧率 |
|
||||
| ⑦表演幅度 | 需禁某种表演? | **默认不写**;用动作暗示替代("鬼鬼祟祟地说"比"禁夸张表情"有效) |
|
||||
| ⑧背景动态 | 需环境微动? | **默认不写**;仅在画面可能静止时加进③镜头动作描述 |
|
||||
| ⑨镜头空间 | 机位/景别/朝向? | 溶进③镜头叙事("中景跟拍男主正面") |
|
||||
| ⑩表演指令 | 逐beat微动作? | 溶进③镜头叙事的动作描述 |
|
||||
| ⑪负面词 | 场景适配禁止项? | ④负面提示词(3.3标准模板) |
|
||||
| ⑧背景动态 | 需环境微动? | **默认不写**;仅在画面可能静止时加进⑤镜头动作描述 |
|
||||
| ⑨镜头空间 | 机位/景别/朝向? | 溶进⑤镜头叙事("中景跟拍男主正面") |
|
||||
| ⑩表演指令 | 逐beat微动作? | 溶进⑤镜头叙事的动作描述 |
|
||||
| ⑪负面词 | 场景适配禁止项? | ⑥负面提示词(3.3标准模板) |
|
||||
|
||||
> **原则:能用@图片引用解决的,绝不写文字;能用动作暗示的,绝不写技术参数。**
|
||||
|
||||
#### 3.2.2 输出格式(4层结构)
|
||||
#### 3.2.2 输出格式(6层结构)
|
||||
|
||||
```
|
||||
①主体列表(置顶,按类别分行)
|
||||
@@ -162,18 +162,24 @@
|
||||
> 有哪类列哪类,无道具/无场景则省略该行;D类主体=产品(列道具行),E类主体=场景(列场景行)
|
||||
> **人物锚定句(仅多角色+跨镜头复用时启用)**:`将 @图片N 中的[身份/外观/服装]定义为 <角色名>`,后续镜头用 `<角色名>` 指代、不再重复写外貌;**识别不到角色名时用角色代称锚定**(`<男主>` `<兄弟>` `<角色A>` 等代称);单人单段保持现状(`人物:男主 @图片2`),不写锚定句
|
||||
|
||||
②设定(分项,每项1句,按类型取舍)
|
||||
②画面质感(视觉基调前置,紧跟主体)
|
||||
视觉风格:[画质+风格+质感1句]
|
||||
摄影质感:模型按维度自动生成英文摄影参数(机型/镜头/光圈/快门/ISO/色彩/光影/质感/动态范围),**用[]框起**,根据剧情场景判断适用维度,从知识库/10_后期细节·摄影质感维度表取值组合(不照搬固定示例);排除画幅/帧率(属平台参数放代码框外)
|
||||
> 风格见知识库/15_视觉风格(去名化)
|
||||
|
||||
③色调光影(光影基调前置)
|
||||
色调光影:[光源来源+主光方向+亮暗区+色调/色温1句]
|
||||
> 光影三层(光源层/光行为层/色调层)见知识库/07_光线时间
|
||||
|
||||
④主体设定(分项,每项1句,按类型取舍)
|
||||
人物:[角色]——[气质/状态1句];[角色]——[气质/状态1句]
|
||||
> **主体中出现的每个人物,此处必须有对应设定,不可遗漏**——逐人写,用分号分隔
|
||||
> @图片已锁定的不重复写外貌,只补气质/状态;无图时按知识库/01_主体描述外貌原则写(宁少写不错写);D/E类省略此行
|
||||
场景:[空间布局+前中后景层次1句]
|
||||
光线:[光源来源+主光方向+亮暗区+色温1句]
|
||||
构图:[景别+前中后景层次+构图法则+视觉重心/留白1句]
|
||||
视觉风格:[画质+风格+质感1句]
|
||||
摄影质感:模型按维度自动生成英文摄影参数(机型/镜头/光圈/快门/ISO/色彩/光影/质感/动态范围),**用[]框起**,根据剧情场景判断适用维度,从知识库/10_后期细节·摄影质感维度表取值组合(不照搬固定示例);排除画幅/帧率(属平台参数放代码框外)
|
||||
> 场景见知识库/03_场景描述(空间锚定三层);光线见知识库/07_光线时间(光影三层);构图见知识库/14_构图布局(五法则+构图几何+雷区规避);风格见知识库/15_视觉风格(去名化);朝向见知识库/06_焦距视角X轴
|
||||
> 场景见知识库/03_场景描述(空间锚定三层);构图见知识库/14_构图布局(五法则+构图几何+雷区规避);朝向见知识库/06_焦距视角X轴
|
||||
|
||||
③镜头(每镜头1段流畅叙事)
|
||||
⑤镜头(每镜头1段流畅叙事)
|
||||
镜头N:[机位/景别/朝向] + [动作/变化流程] + [运镜变化嵌在动作里] + [有台词时:(人物,语气):内容]
|
||||
- 每镜头1段,按动作流自然叙述,不分beat罗列
|
||||
- **朝向必须写明**:正面/背后/固定/侧面——用"正面拍摄""从背后拍摄""固定视角"等明确朝向(词库见知识库/06_焦距视角X轴+知识库/03_场景描述面部朝向/过肩);**主体位置用画面坐标**——写"男主位于画面右侧、四分之三侧朝镜头",不写"从男主右侧拍摄"(画面坐标优于主体坐标,生视频模型更易定位,词库见知识库/03_场景描述演员调度);**避开构图雷区**——默认不用"正面居中",改用 X2 四分之三侧/三分法偏移(雷区见知识库/14_构图布局)
|
||||
@@ -192,28 +198,28 @@
|
||||
- **段末状态(仅多段连续输出时写,单段不写)**:每段末尾写一句"下一段可继承的可见状态"——角色姿态/视线方向/道具位置/光线方向/镜头方向,如"段末男主缩在门框边,薯片袋已被兄弟抢走,镜头停在两人对峙的正面"。用于跨段连续性的锚点
|
||||
> D/E类镜头层写产品展示流程或氛围流变,不写角色动作
|
||||
|
||||
④负面提示词
|
||||
⑥负面提示词
|
||||
负面提示词:[通用项] + [场景勾选项](见3.3)
|
||||
> 负面词放最后——镜头叙事写完后整体排除
|
||||
> BGM禁止由负面词覆盖(通用项已含"背景音乐,BGM,配乐"),不需要单独音频段
|
||||
```
|
||||
|
||||
> **4层×5类型适配表(按Step 1类型判定结果选层):**
|
||||
> **6层×5类型适配表(按Step 1类型判定结果选层):**
|
||||
|
||||
| 类型 | ①主体 | ②设定-人物 | ②设定-其余4项 | ③镜头-台词 | ③镜头-动作 |
|
||||
|------|-------|-----------|-------------|-----------|-----------|
|
||||
| A 叙事剧情 | 人物 | 全用 | 全用 | 对白格式 | 角色动作 |
|
||||
| B 单人Vlog | 人物 | 全用 | 全用 | 口播(非对白) | 人物动作 |
|
||||
| C 旁白知识 | 场景/道具 | 可选 | 全用 | 旁白格式 | 画面变化 |
|
||||
| D 产品展示 | 产品 | 省略 | 全用 | 无(可选旁白) | 产品展示流程 |
|
||||
| E 纯氛围 | 场景 | 省略 | 全用 | 无 | 氛围流变 |
|
||||
| 类型 | ①主体 | ②画面质感 | ③色调光影 | ④主体设定-人物 | ④主体设定-场景/构图 | ⑤镜头-台词 | ⑤镜头-动作 |
|
||||
|------|-------|-----------|-----------|----------------|--------------------|-----------|-----------|
|
||||
| A 叙事剧情 | 人物 | 全用 | 全用 | 全用 | 全用 | 对白格式 | 角色动作 |
|
||||
| B 单人Vlog | 人物 | 全用 | 全用 | 全用 | 全用 | 口播(非对白) | 人物动作 |
|
||||
| C 旁白知识 | 场景/道具 | 全用 | 全用 | 可选 | 全用 | 旁白格式 | 画面变化 |
|
||||
| D 产品展示 | 产品 | 全用 | 全用 | 省略 | 全用 | 无(可选旁白) | 产品展示流程 |
|
||||
| E 纯氛围 | 场景 | 全用 | 全用 | 省略 | 全用 | 无 | 氛围流变 |
|
||||
|
||||
> **编导式 vs 旧11字段版对比:**
|
||||
>
|
||||
> | 对比项 | 编导式(新) | 11字段版(旧) |
|
||||
> |--------|-------------|---------------|
|
||||
> | 字段标签 | 无,信息溶解进叙事 | 【一致性锚定】【景深对焦】等11个标签 |
|
||||
> | 技术参数 | 溶进②设定层各分项 | 独立字段罗列(机型/f值/快门等) |
|
||||
> | 技术参数 | 溶进②画面质感/③色调光影/④主体设定各分项 | 独立字段罗列(机型/f值/快门等) |
|
||||
> | 表演控制 | 动作暗示("鬼鬼祟祟地说") | 禁令约束("禁夸张喜剧化表情") |
|
||||
> | 镜头组织 | 每镜头1段流畅叙事 | 时间戳逐beat分段罗列 |
|
||||
> | 字符量 | ~350字 | ~700字 |
|
||||
|
||||
@@ -56,13 +56,13 @@ Z8→Z5→Z5↔Z4→Z4→Z3→Z2→Z1→Z3→Z7→Z8→Z9(远景开场→中
|
||||
|
||||
## 四、一致性锁定(每段必须执行)
|
||||
|
||||
> 4层结构中,一致性靠@图片引用+设定层统一实现,不需要额外写锁定语句。
|
||||
> 6层结构中,一致性靠@图片引用+②画面质感/③色调光影/④主体设定统一实现,不需要额外写锁定语句。
|
||||
|
||||
| 类型 | 执行方式 | 在4层结构中去哪 |
|
||||
| 类型 | 执行方式 | 在6层结构中去哪 |
|
||||
|------|---------|----------------|
|
||||
| 风格锁定 | 视觉风格跨段统一(首段定调后续段重复或引用) | ②设定-视觉风格 |
|
||||
| 风格锁定 | 视觉风格跨段统一(首段定调后续段重复或引用) | ②画面质感-视觉风格 |
|
||||
| 角色锁定 | @图片引用在每段①主体列表重复出现 | ①主体列表 |
|
||||
| 场景锁定 | 场景延续时②设定-场景写"延续上一段场景",新场景写新描述 | ②设定-场景 |
|
||||
| 场景锁定 | 场景延续时④主体设定-场景写"延续上一段场景",新场景写新描述 | ④主体设定-场景 |
|
||||
|
||||
## 五、分段规则
|
||||
|
||||
@@ -107,18 +107,22 @@ Z8→Z5→Z5↔Z4→Z4→Z3→Z2→Z1→Z3→Z7→Z8→Z9(远景开场→中
|
||||
### 简单段
|
||||
```
|
||||
①主体列表(置顶,按类别分行:人物/场景/道具,@图片引用优先;D类主体=产品,E类主体=场景)
|
||||
②设定(人物逐人必写不可遗漏/场景/光线/构图/视觉风格,每项1句,按类型取舍:D/E类省略人物;摄影质感默认生成参考值·[]框起)
|
||||
③镜头叙事(1段流畅描述:机位/景别/朝向+动作/变化+运镜+有台词时带语气)
|
||||
④负面提示词(放最后,3.3标准模板)
|
||||
②画面质感(视觉风格1句+摄影质感默认生成参考值·[]框起)
|
||||
③色调光影(光源+主光方向+亮暗区+色调/色温,1句)
|
||||
④主体设定(人物逐人必写不可遗漏/场景/构图,每项1句,按类型取舍:D/E类省略人物)
|
||||
⑤镜头叙事(1段流畅描述:机位/景别/朝向+动作/变化+运镜+有台词时带语气)
|
||||
⑥负面提示词(放最后,3.3标准模板)
|
||||
```
|
||||
> 按Step 1类型判定适配4层,详见分镜提示词制作流程3.2.2类型适配表
|
||||
> 按Step 1类型判定适配6层,详见分镜提示词制作流程3.2.2类型适配表
|
||||
|
||||
### 复杂段(多角色/多场景切换时用)
|
||||
```
|
||||
①主体列表(置顶,按类别分行:人物/场景/道具,@图片引用优先,跨段每段重复出现)
|
||||
②设定(人物逐人必写不可遗漏/场景/光线/构图/视觉风格,每项1句;场景变化时写新场景,跨段统一项不重复;摄影质感默认生成参考值·[]框起)
|
||||
③镜头叙事(每镜头1段,多角色互动按动作流叙述,运镜嵌在动作中,朝向必须写明,连续动作不拆,有台词时带语气)
|
||||
④负面提示词(放最后,3.3标准模板)
|
||||
②画面质感(视觉风格1句+摄影质感默认生成参考值·[]框起;跨段统一项不重复)
|
||||
③色调光影(光源+主光方向+亮暗区+色调/色温,1句;跨段统一项不重复)
|
||||
④主体设定(人物逐人必写不可遗漏/场景/构图,每项1句;场景变化时写新场景,跨段统一项不重复)
|
||||
⑤镜头叙事(每镜头1段,多角色互动按动作流叙述,运镜嵌在动作中,朝向必须写明,连续动作不拆,有台词时带语气)
|
||||
⑥负面提示词(放最后,3.3标准模板)
|
||||
```
|
||||
> 跨段一致性:角色@图片引用在每段重复出现,不需要额外写一致性锚定语句。
|
||||
|
||||
|
||||
Reference in new issue
Block a user