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:
maogeigei committed 2026-08-31 15:27:54 +08:00
1 parent 96fce56fba
commit 53621279a5
35 files changed
+2567 -117

No files matched your search

@@ -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秒内认出"这是谁")
@@ -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标准模板)
```
> 跨段一致性:角色@图片引用在每段重复出现,不需要额外写一致性锚定语句。