工作台迭代:技能挂载收口(F1/F2补mcn-dou-analysis)+文案改名(AI脚本评分/AI脚本)+周榜关注按钮+AI写脚本列表页全量分页/关注优先/时间倒序/标题跳详情/发布时间筛选
- 8处/api/run入口全部参数级技能挂载\n- listVideos全量625条分页+关注优先+发布时间倒序+days筛选\n- .gitignore忽略数据库备份文件
This commit is contained in:
1 parent
5d6f78864a
commit
6662cc70d9
9 files changed
+111
-27
No files matched your search
@@ -68,6 +68,7 @@ for (const vid of VIDEO_IDS) {
|
||||
const noPersonaNote = personaText ? '' : `\n该账号暂无账号设定,请先为达人「${ACCOUNT_NAME}」提取账号设定(生成人设卡),再基于账号设定与选题创作。`;
|
||||
|
||||
const prompt = `请调用 Skill 工具加载「短视频脚本创作」技能(技能根目录 = 用户技能目录 ~/.workbuddy/skills/短视频脚本创作/,先读取其 SKILL.md 及 references 规范;若 Skill 工具无法加载,请直接读取该目录下的 SKILL.md 文件)。遵循 S1-S11 完整创作流程,最终输出 S9 标准脚本格式(按场次+对白组织,画面标注【景别|环境|光线|场景|场景元素】),禁止输出分镜头表格。
|
||||
创作时必须逐条读取并严格执行 SKILL.md 的「开场钩子铁律」与【账号设定】的「内容规则」(如果…则…)——二者为硬约束,不得自由发挥、不得跳过,具体标准一律以 SKILL.md 与内容规则原文为准。
|
||||
请为达人「${ACCOUNT_NAME}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}${block}${createTail}
|
||||
|
||||
【任务完成后必须执行】
|
||||
|
||||
@@ -144,28 +144,31 @@ function getAccountAnalysis(accountId) {
|
||||
|
||||
/* ================= 视频 ================= */
|
||||
|
||||
function listVideos({ page = 1, pageSize = 20, search = '', parsed = '' } = {}) {
|
||||
function listVideos({ page = 1, pageSize = 20, search = '', parsed = '', days = '' } = {}) {
|
||||
const d = getDb();
|
||||
if (!d) return { total: 0, page: 1, pageSize, items: [] };
|
||||
const where = [`v.id IN (SELECT id FROM (SELECT id, ROW_NUMBER() OVER (PARTITION BY account_id ORDER BY publish_time DESC, id DESC) rn FROM account_videos WHERE like_count >= 300000 AND comment_count >= 10000) t WHERE rn <= 10)`];
|
||||
// 09-01 用户要求:分页展示全部视频(不再限制每账号 30W+/1W+ 取 10 条)
|
||||
const where = [];
|
||||
const params = [];
|
||||
if (search) { where.push('(v.video_title LIKE ? OR h.account_name LIKE ?)'); params.push(`%${search}%`, `%${search}%`); }
|
||||
if (parsed === '1' || parsed === '0') {
|
||||
const cond = `(v.id IN (SELECT video_id FROM account_video_source) OR v.id IN (SELECT video_id FROM account_video_analysis))`;
|
||||
where.push(parsed === '1' ? cond : `NOT (${cond})`);
|
||||
}
|
||||
const w = 'WHERE ' + where.join(' AND ');
|
||||
// 发布时间筛选:days>0 时取近 N 天(默认近一个月由前端传 30;localtime 避免 UTC 边界差一天)
|
||||
const n = Number(days);
|
||||
if (n > 0) { where.push(`v.publish_time >= date('now', 'localtime', ?)`); params.push(`-${n} days`); }
|
||||
const w = where.length ? 'WHERE ' + where.join(' AND ') : '';
|
||||
const total = d.prepare(`SELECT COUNT(*) c FROM account_videos v LEFT JOIN hot_accounts h ON h.id=v.account_id ${w}`).get(...params).c;
|
||||
const limit = Math.max(1, Math.min(Number(pageSize) || 20, 100));
|
||||
const offset = (Math.max(1, Number(page) || 1) - 1) * limit;
|
||||
const items = d.prepare(`
|
||||
SELECT v.*, h.account_name,
|
||||
SUM(v.like_count) OVER (PARTITION BY v.account_id) AS acc_likes,
|
||||
(SELECT COUNT(*) FROM account_video_source s WHERE s.video_id=v.id OR s.aweme_id=v.aweme_id) AS has_source,
|
||||
(SELECT COUNT(*) FROM account_video_analysis a WHERE a.video_id=v.id OR a.aweme_id=v.aweme_id) AS has_analysis,
|
||||
(SELECT COUNT(*) FROM rewrite_log r WHERE r.video_id=v.id AND r.status='done' AND r.script_text IS NOT NULL AND r.script_text <> '') AS rewrite_count
|
||||
FROM account_videos v LEFT JOIN hot_accounts h ON h.id=v.account_id
|
||||
${w} ORDER BY acc_likes DESC, v.publish_time DESC, v.id DESC LIMIT ? OFFSET ?
|
||||
${w} ORDER BY h.favorite DESC, v.publish_time DESC, v.id DESC LIMIT ? OFFSET ?
|
||||
`).all(...params, limit, offset);
|
||||
return { total, page: Math.max(1, Number(page) || 1), pageSize: limit, items };
|
||||
}
|
||||
@@ -273,7 +276,7 @@ function getStats() {
|
||||
// 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)
|
||||
// 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();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/* poll-create-tasks.cjs — 轮询 6 个创作任务状态,全部完成/失败后退出 */
|
||||
const TASKS = [
|
||||
{ videoId: 309, id: 'automation-1788250646656' },
|
||||
{ videoId: 313, id: 'automation-1788250649673' },
|
||||
{ videoId: 315, id: 'automation-1788250652690' },
|
||||
{ videoId: 316, id: 'automation-1788250655707' },
|
||||
{ videoId: 318, id: 'automation-1788250658730' },
|
||||
{ videoId: 320, id: 'automation-1788250661754' },
|
||||
{ videoId: 309, id: 'automation-1788258518460' },
|
||||
{ videoId: 313, id: 'automation-1788258521485' },
|
||||
{ videoId: 315, id: 'automation-1788258524508' },
|
||||
{ videoId: 316, id: 'automation-1788258527521' },
|
||||
{ videoId: 318, id: 'automation-1788258530542' },
|
||||
{ videoId: 320, id: 'automation-1788258533562' },
|
||||
];
|
||||
const API = 'http://localhost:8900/api/run/status?id=';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
@@ -17,8 +17,8 @@ const status = async (id) => {
|
||||
} catch { return 'unknown'; }
|
||||
};
|
||||
console.log('START polling 6 review tasks:', new Date().toLocaleTimeString('zh-CN'));
|
||||
(async () => {
|
||||
const started = Date.now();
|
||||
const t0 = Date.now();
|
||||
while (Date.now() - started < 60 * 60 * 1000) {
|
||||
let allDone = true;
|
||||
for (const t of TASKS) {
|
||||
@@ -37,3 +37,4 @@ while (Date.now() - started < 60 * 60 * 1000) {
|
||||
}
|
||||
console.log('TIMEOUT after 60min, last states:', JSON.stringify(state));
|
||||
process.exit(1);
|
||||
})();
|
||||
@@ -22,6 +22,10 @@ const EXT_ICON = { md: 'file-text', json: 'file-code', xlsx: 'table', csv: 'tabl
|
||||
流程:点击按钮 → 确认弹窗 → 提交 /api/run → 按钮变「执行中…」禁用
|
||||
→ 轮询 /api/run/status 直到任务完成(done/error)→ 按钮恢复 */
|
||||
|
||||
/* 账号分析技能指令(09-01 修复:F1/F2 入口任务必须按「mcn-dou-analysis」技能执行,与提炼账号设定入口统一。
|
||||
定位方式跨环境:优先用 Skill 工具按技能名加载;技能位于用户技能目录 ~/.workbuddy/skills/短视频脚本创作/subskill/mcn-dou-analysis/(部署到用户环境时同此标准位),不写死机器绝对路径 */
|
||||
const SKILL_HINT_ACCOUNT = '请调用 Skill 工具加载「mcn-dou-analysis」技能(技能根目录 = 用户技能目录 ~/.workbuddy/skills/短视频脚本创作/subskill/mcn-dou-analysis/,先读取其 SKILL.md 及 references 规范;若 Skill 工具无法加载,请直接读取该目录下的 SKILL.md 文件)。严格按技能工作流执行(获取账号信息 → 视频列表抓取 → 登记达人账号表 15 列规范),遵守技能红线:TOP6 筛选、≥60 条停止、账号身份标识(sec_uid/抖音号/地址)必须由浏览器获取、禁止从数据库回填。';
|
||||
|
||||
// 任务完成后自动刷新当前数据页(无需手动刷新即可看到最新数据)
|
||||
function refreshCurrentPage() {
|
||||
const view = $('#view');
|
||||
@@ -97,9 +101,10 @@ function confirmSaveAccount(name, url, btn) {
|
||||
desc: '确认分析该账号并添加到账号列表(账号信息 + 视频列表)?',
|
||||
target: name,
|
||||
url: url || '',
|
||||
prompt: `请帮我把抖音账号「${name}」保存并分析(账号信息 + 视频列表):${url || ''}`,
|
||||
prompt: `${SKILL_HINT_ACCOUNT}\n请帮我把抖音账号「${name}」保存并分析(账号信息 + 视频列表):${url || ''}`,
|
||||
label: '保存指令',
|
||||
btn,
|
||||
skills: ['mcn-dou-analysis'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,9 +137,9 @@ function openImportAccounts(btn) {
|
||||
const url = (mask.querySelector('[data-imp-url]').value || '').trim();
|
||||
if (!name && !url) { showToast('请填写账号名称或账号地址'); return; }
|
||||
const parts = [name ? `账号名称「${name}」` : '', url ? `账号地址「${url}」` : ''].filter(Boolean).join(',');
|
||||
const prompt = `请帮我把抖音账号 ${parts} 添加:通过抖音网页获取账号信息与视频列表,写入账号列表并保存到本地`;
|
||||
const prompt = `${SKILL_HINT_ACCOUNT}\n请帮我把抖音账号 ${parts} 添加:通过抖音网页获取账号信息与视频列表,写入账号列表并保存到本地`;
|
||||
close();
|
||||
executeTask({ prompt, label: '添加指令', btn });
|
||||
executeTask({ prompt, label: '添加指令', btn, skills: ['mcn-dou-analysis'] });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -292,7 +297,7 @@ function renderHome() {
|
||||
const cards = [
|
||||
{ icon: 'trending-up', title: '账号周榜', desc: '三方数据 · 赛道/日期筛选', hash: '#/ranking' },
|
||||
{ icon: 'users', title: '账号列表', desc: '关注的账号 + 视频列表', hash: '#/accounts' },
|
||||
{ icon: 'pen', title: 'AI写脚本', desc: 'AI生成脚本+分镜提示词', hash: '#/rewrites' },
|
||||
{ icon: 'pen', title: 'AI写脚本', desc: 'AI脚本+分镜提示词', hash: '#/rewrites' },
|
||||
{ icon: 'flask', title: 'AI脚本诊断', desc: '检查清单+复盘报告', hash: '#/reviews' },
|
||||
];
|
||||
return `
|
||||
@@ -302,7 +307,7 @@ function renderHome() {
|
||||
<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 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}">
|
||||
@@ -330,7 +335,7 @@ function renderHome() {
|
||||
<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>
|
||||
<div class="recent-head"><h3>AI脚本</h3><a class="link" href="#/rewrites">查看全部</a></div>
|
||||
<div id="recentScriptsList" class="recent-list"><span class="dim">加载中…</span></div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -349,7 +354,7 @@ async function loadHomeStats() {
|
||||
['视频解析', s.analyses],
|
||||
['AI写脚本', s.rewrites],
|
||||
['AI脚本诊断', s.reviews],
|
||||
['AI写作评分', s.writingScore],
|
||||
['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>`
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/* AI创作技能指令(09-01 修复:任务必须按「短视频脚本创作」技能执行,禁止模型自由发挥导致分镜表格/TXT/MD 格式混杂。
|
||||
定位方式跨环境:优先用 Skill 工具按技能名加载;技能位于用户技能目录 ~/.workbuddy/skills/短视频脚本创作/(部署到用户环境时同此标准位),不写死机器绝对路径 */
|
||||
const SKILL_HINT_CREATE = '请调用 Skill 工具加载「短视频脚本创作」技能(技能根目录 = 用户技能目录 ~/.workbuddy/skills/短视频脚本创作/,先读取其 SKILL.md 及 references 规范;若 Skill 工具无法加载,请直接读取该目录下的 SKILL.md 文件)。遵循 S1-S11 完整创作流程(选题→框架→大纲→情绪→结构→脚本),最终输出 S9 标准脚本格式(按场次+对白组织,画面标注【景别|环境|光线|场景|场景元素】),禁止输出分镜头表格、禁止偏离脚本格式。';
|
||||
const SKILL_HINT_CREATE = '请调用 Skill 工具加载「短视频脚本创作」技能(技能根目录 = 用户技能目录 ~/.workbuddy/skills/短视频脚本创作/,先读取其 SKILL.md 及 references 规范;若 Skill 工具无法加载,请直接读取该目录下的 SKILL.md 文件)。遵循 S1-S11 完整创作流程(选题→框架→大纲→情绪→结构→脚本),最终输出 S9 标准脚本格式(按场次+对白组织,画面标注【景别|环境|光线|场景|场景元素】),禁止输出分镜头表格、禁止偏离脚本格式。创作时必须逐条读取并严格执行 SKILL.md 的「开场钩子铁律」与【账号设定】的「内容规则」(如果…则…)——二者为硬约束,不得自由发挥、不得跳过,具体标准一律以 SKILL.md 与内容规则原文为准。';
|
||||
|
||||
const fmt = (v) => (v == null || v === '') ? '-' : String(v);
|
||||
/* 过万数字以"万"为单位,保留一位小数(如 57141 → 5.7万);已是"x.x万"文本(like_display 抖音原生格式)直接透传(09-01 修复:旧实现 Number('7.8万')=NaN→'-') */
|
||||
@@ -232,7 +232,7 @@ async function renderRanking(view) {
|
||||
<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>
|
||||
<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 = `
|
||||
<div class="page">
|
||||
@@ -569,13 +569,13 @@ async function renderVideoDetail(view, id) {
|
||||
|
||||
/* ================= AI写脚本 ================= */
|
||||
async function renderRewrites(view) {
|
||||
let state = { search: '', parsed: '', page: 1, pageSize: 20, batch: false };
|
||||
let state = { search: '', parsed: '', days: '30', page: 1, pageSize: 20, batch: false };
|
||||
async function load() {
|
||||
const d = await api('/api/dsh/videos?search=' + encodeURIComponent(state.search) + '&parsed=' + state.parsed + '&page=' + state.page + '&pageSize=' + state.pageSize);
|
||||
const d = await api('/api/dsh/videos?search=' + encodeURIComponent(state.search) + '&parsed=' + state.parsed + '&days=' + state.days + '&page=' + state.page + '&pageSize=' + state.pageSize);
|
||||
const rows = (d.items || []).map((v) => `<tr>
|
||||
${state.batch ? `<td class="cb-cell"><input type="checkbox" class="batch-cb" data-id="${v.id}"></td>` : ''}
|
||||
<td>${v.account_id ? `<a class="link" href="#/account/${v.account_id}" title="查看账号详情">${esc(v.account_name || '-')}</a>` : esc(v.account_name || '-')}</td>
|
||||
<td class="cell-content">${v.video_url ? `<a class="title-plain" data-ext="${esc(v.video_url)}" title="在新标签打开原视频">${esc(cleanTitle(v.video_title) || '-')}</a>` : esc(cleanTitle(v.video_title) || '-')}</td>
|
||||
<td class="cell-content td-title"><a class="link" href="#/video/${v.id}" title="查看视频详情">${esc(cleanTitle(v.video_title) || '-')}</a></td>
|
||||
<td>${fmtWan(v.like_display || v.like_count)}</td>
|
||||
<td>${fmtWan(v.comment_count)}</td>
|
||||
<td>${fmtWan(v.share_count)}</td>
|
||||
@@ -592,6 +592,13 @@ async function renderRewrites(view) {
|
||||
<span class="page-title">AI写脚本</span>
|
||||
<div class="toolbar-right" style="margin-right:10px">
|
||||
<input id="rwSearch" class="input" placeholder="搜索视频标题 / 账号" value="${esc(state.search)}">
|
||||
<select id="rwDays" class="input-select">
|
||||
<option value="" ${state.days === '' ? 'selected' : ''}>全部时间</option>
|
||||
<option value="30" ${state.days === '30' ? 'selected' : ''}>近一个月</option>
|
||||
<option value="90" ${state.days === '90' ? 'selected' : ''}>近三个月</option>
|
||||
<option value="180" ${state.days === '180' ? 'selected' : ''}>近半年</option>
|
||||
<option value="365" ${state.days === '365' ? 'selected' : ''}>近一年</option>
|
||||
</select>
|
||||
<select id="rwParsed" class="input-select">
|
||||
<option value="" ${state.parsed === '' ? 'selected' : ''}>全部状态</option>
|
||||
<option value="1" ${state.parsed === '1' ? 'selected' : ''}>已解析</option>
|
||||
@@ -599,7 +606,7 @@ async function renderRewrites(view) {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-note">每账号近期 30W+ 且评论 1W+ 取 10 条</div>
|
||||
<div class="toolbar-note">全部视频分页展示 · 关注账号优先 · 按发布时间倒序</div>
|
||||
<div class="table-wrap"><table class="table">
|
||||
<thead><tr>${state.batch ? '<th class="cb-cell"><input type="checkbox" class="batch-cb-all"></th>' : ''}<th>账号</th><th>视频标题</th><th>点赞</th><th>评论</th><th>分享</th><th>时长</th><th>发布时间</th><th>状态</th><th>AI脚本</th><th class="th-act">操作</th></tr></thead>
|
||||
<tbody>${rows || `<tr><td colspan="${state.batch ? 11 : 10}" class="empty">暂无视频数据</td></tr>`}</tbody>
|
||||
@@ -611,6 +618,7 @@ async function renderRewrites(view) {
|
||||
</div>`;
|
||||
bindBack(view);
|
||||
view.querySelector('#rwSearch').addEventListener('keydown', (e) => { if (e.key === 'Enter') { state.search = e.target.value; state.page = 1; load(); } });
|
||||
view.querySelector('#rwDays').addEventListener('change', (e) => { state.days = e.target.value; state.page = 1; load(); });
|
||||
view.querySelector('#rwParsed').addEventListener('change', (e) => { state.parsed = e.target.value; state.page = 1; load(); });
|
||||
bindPager(view, (p) => { state.page = p; load(); });
|
||||
bindBatchBar(view, {
|
||||
|
||||
@@ -460,7 +460,7 @@ const server = http.createServer((req, res) => {
|
||||
}
|
||||
if (p === '/api/dsh/videos') {
|
||||
if (dshGuard()) return;
|
||||
return dshOk(200, dsh.listVideos({ page: qInt('page', 1), pageSize: qInt('pageSize', 20), search: String(q.get('search') || ''), parsed: String(q.get('parsed') || '') }));
|
||||
return dshOk(200, dsh.listVideos({ page: qInt('page', 1), pageSize: qInt('pageSize', 20), search: String(q.get('search') || ''), parsed: String(q.get('parsed') || ''), days: String(q.get('days') || '') }));
|
||||
}
|
||||
if (p === '/api/dsh/video-detail') {
|
||||
if (dshGuard()) return;
|
||||
|
||||
Reference in new issue
Block a user