工作台迭代:技能挂载收口(F1/F2补mcn-dou-analysis)+文案改名(AI脚本评分/AI脚本)+周榜关注按钮+AI写脚本列表页全量分页/关注优先/时间倒序/标题跳详情/发布时间筛选
- 8处/api/run入口全部参数级技能挂载\n- listVideos全量625条分页+关注优先+发布时间倒序+days筛选\n- .gitignore忽略数据库备份文件
This commit is contained in:
1 parent
f459abe2f2
commit
e776bd2c7c
9 files changed
+111
-27
No files matched your search
@@ -14,6 +14,8 @@ Thumbs.db
|
||||
# 工作台本地数据库(含账号真实数据,不入库;根目录 0 字节残留同规则)
|
||||
mcn-plugin.db
|
||||
**/mcn-plugin.db
|
||||
# 数据库备份文件(运行时临时备份,含真实数据,不入库)
|
||||
**/mcn-plugin.db.bak-*
|
||||
# 临时目录与生成产物
|
||||
tmp/
|
||||
.workbuddy/tmp/
|
||||
|
||||
@@ -261,3 +261,67 @@
|
||||
- **★CDP 验证脚本同 URL 复用陷阱(15:09 实测)**:验证脚本 navigate 固定 URL(如 `?v=titlebar#/video/680`)二次运行会**复用旧页面不重新加载 JS**(浏览器同 URL 判定),导致断言读到上一版 DOM 误判 FAIL——服务端 curl 已是新代码但页面显示旧内容。**修复:验证 URL 带时间戳 `?v=tb${Date.now()}` 强制全新加载**(与工作台重开页面 ?v= 同理,验证脚本也要遵守)
|
||||
- **15:12 首行再调(标签结构)**:用户要「查看视频」几个字显示在**标签的内容后面**——恢复 infoGrid 结构,首行=`infoGrid('视频地址', 查看视频↗链接, wide)`:标签「视频地址」+ 值区域「查看视频 ↗」(URL 仍不显示明文)。**验证**:cdp-verify-titlebar.mjs 10/10 PASS(首行标签=视频地址、查看视频在标签后、无 URL 明文、链接可点)
|
||||
- **15:15 首行定稿(标签+查看视频合并)**:用户澄清——首行标签应为「**标签**」,值=标签内容+「查看视频 ↗」链接(`infoGrid('标签', \`${tags} ${viewLink}\`, wide)`),原第4行标签行删除,信息卡 4 行→3 行(标签/账号组/数据组)。注意数据源 `v.tags` 为逗号分隔纯文本不带 # 前缀。**验证**:cdp-verify-titlebar.mjs 10/10 PASS
|
||||
|
||||
## 视频详情信息卡标题(15:20)
|
||||
- 需求:视频信息卡片内加标题「视频信息」
|
||||
- 改动:data-pages.js renderVideoDetail 信息卡首行前加 `<div class="info-card-title">视频信息</div>`;style.css 新增 `.info-card-title`(15px/600/下分隔线,参考 .data-card-title)
|
||||
- 验证:cdp-verify-infocard-title.mjs 4/4 PASS(标题存在/文案/位置/分隔线)
|
||||
- commit `6e2618b`(未 push)
|
||||
- 15:22 用户要求标签行放回信息卡最后一行(行序:账号/视频ID/时长/发布时间 → 点赞/评论/分享/收藏 → 标签+查看视频链接);cdp-verify-tag-last.mjs 4/4 PASS;commit `ee2bc92`
|
||||
|
||||
## AI创作任务未走技能根因 + 修复(16:17)
|
||||
- **根因**:创作任务(前端 AI创作 按钮+自动化提交脚本)的 prompt 无技能指令,任务空间 mcn-workshop 无技能挂载 → 模型自由发挥:RW26/27/29/31 场次脚本、RW28/30/31 分镜头表格、落盘 md/txt 混杂;对比复盘按钮 prompt 明确「按 mcn-script-review 技能执行」所以诊断走技能
|
||||
- **修复**:①创作 prompt 加技能指令(data-pages.js 两处 + build-create-prompts.cjs),定位方式**跨环境**:Skill 工具按名加载 + 用户技能目录 `~/.workbuddy/skills/短视频脚本创作/`(已建 junction→V1.0 源技能),不写死机器绝对路径;②`/api/run/status` 状态判定漏 ACCEPTED(客户端调度器终态)→ 归入 done(轮询显示全 pending 假象根因);③cwd 保持 mcn-workshop 不改(.workbuddy/memory/automations 未被 gitignore 会污染仓库,且技能发现靠用户级 junction 与 cwd 无关)
|
||||
- 删除作废 RW26-31(备份 mcn-plugin.db.bak-rerun-*),6 个任务重跑(automation-1788250646656 等,prompt 带技能指令)
|
||||
- commit `ef98395`;`~/.workbuddy/skills/短视频脚本创作` junction(PowerShell New-Item -ItemType Junction)
|
||||
|
||||
## 参数级技能挂载(skills_json)+ 重跑验证成功(16:25-16:35)
|
||||
- **用户澄清(16:23)**:技能挂载要用「参数形式」,不是把技能文件放到目录(junction 是文件系统发现,非参数级)
|
||||
- **关键发现**:`automations.skills_json` 是系统原生技能挂载字段(default '[]'),**`/api/run` 之前硬编码 '[]' → 所有任务创建会话时技能挂载为空**(这就是任务不走技能的根因之一);renderer 无技能挂载 UI 表单;消费端在客户端主进程/云端调度器(本地 upsert 镜像 excludedColumn("skills_json"));asar 逆向方法:新版多重 pickle 头(@0=4 @4=headerSize+4 @8=headerSize @12=JSON长度 @16=JSON)
|
||||
- **实现**:`/api/run` 支持 `skills` 数组参数 → `skills_json`;前端 5 处调用点按技能名传参:创作×3=`['短视频脚本创作']`、复盘=`['脚本复盘']`、分镜=`['短视频分镜脚本']`、提炼=`['mcn-dou-analysis']`(技能名=SKILL.md frontmatter name);build-create-prompts.cjs / submit-review-tasks.cjs 同步。executeTask/confirmRun opts 全量透传(app.js 46 行 body 加 skills: opts.skills)
|
||||
- **验证**:测试任务 skills_json=["短视频脚本创作"] 写入成功(已清理);6 条复盘任务 skills=["脚本复盘"] 全部正确
|
||||
- **重跑创作 6 条全部 done(16:28)且格式统一 S9**:RW26-31 均为场次+对白、开场钩子、【景别|环境|光线|场景|场景元素】、情绪/叙事驱动/情感铺垫齐全(RW30 明确「S9 标准脚本|禁止分镜表格」)——技能指令修复生效
|
||||
- **复盘任务**:6 条已提交(automation-1788251452496 等,rw31→rw26),poll-review-tasks.cjs 轮询中(.cjs 禁顶层 await,须包 async IIFE)
|
||||
- commit `f459abe`(未 push)
|
||||
|
||||
## 6 条复盘任务全部完成 + 评分汇总(16:43)
|
||||
- poll-review-tasks.cjs 轮询 11m34s 全部 done(并发≈3,rw31→rw26 依次落库)
|
||||
- script_review 表 id 7-12(本轮 6 条):
|
||||
- RW31(视频320)《生日加班夜》85.3 vs 源87.8——可交付,丢POV播音腔/硬菜奇观降级/钩子偏温和
|
||||
- RW30(视频318)《爸爸悄悄回家30元团圆宴》78 vs 源90——临界,预算30采购34超支无总账
|
||||
- RW29(视频316)《五十块回血饭湘菜》78 vs 源92——附条件,未成年无监护用火=合规红线(高)
|
||||
- RW28(视频309)《开学回血餐60块四荤一素》78 vs 源88——临界,账目58.5 vs 台词"正好六十"
|
||||
- RW27(视频313)《30元回血餐三菜一汤》86 vs 源84——AI略优,剁椒来源断链
|
||||
- RW26(视频315)《30块回血大餐+爸爸惊喜》87.2 vs 源86.6——AI略优,周五时间线未明示
|
||||
- 均值≈82.1;AI vs 源 = 2优4劣
|
||||
- **共性规律(AI脚本短板)**:①账目/数字一致性(预算承诺vs采购明细vs结算字幕三处对不上,触发叙事逻辑封顶78)②未成年人用火安全声明缺失(源视频都有家长陪同监护字幕,平台红线)③极致事件/教程颗粒度降级(家常菜vs源硬菜奇观)④丢源视频记忆点(POV/字幕记账条/同期声毛边)
|
||||
|
||||
## 技能挂载机制统一收口:F1/F2 两处缺口修复(17:55)
|
||||
- 全链路审计结论:8 处 /api/run 入口中 6 处已挂技能,2 处缺口(app.js confirmSaveAccount「保存账号/分析账号」+ openImportAccounts「添加账号数据」)无 skills 参数
|
||||
- 修复:app.js 新增 SKILL_HINT_ACCOUNT 常量(mcn-dou-analysis 同款跨环境定位说明:~/.workbuddy/skills/短视频脚本创作/subskill/mcn-dou-analysis/ + 红线:TOP6/≥60条停止/身份标识浏览器获取禁回填),两入口 prompt 前置技能指令 + executeTask/confirmRun 传 skills:['mcn-dou-analysis']
|
||||
- 至此 8 处入口全部参数级技能挂载:创作×3(短视频脚本创作)、提炼(mcn-dou-analysis)、分镜(短视频分镜脚本)、复盘(脚本复盘)、保存账号/添加账号(mcn-dou-analysis)
|
||||
|
||||
## AI写脚本列表页改版:全量分页 + 关注优先 + 发布时间倒序(18:05)
|
||||
- listVideos(dsh-data.js:147)去掉每账号「30W+/1W+ 取10条」限制(原 ROW_NUMBER PARTITION BY + 阈值子查询),改为全量 625 条分页(32 页)
|
||||
- 排序改为 `h.favorite DESC, v.publish_time DESC, v.id DESC`(favorite=关注标记,账号列表页⭐/☆切换、getAccounts 同款排序;周榜「关注」按钮=登记入库不置 favorite)
|
||||
- 修复连带 bug:where 数组为空时 `WHERE ` 尾随空格 SQL 报错「incomplete input」→ 改为 where.length ? 'WHERE ...' : ''
|
||||
- 前端 toolbar-note 同步:「每账号近期 30W+ 且评论 1W+ 取 10 条」→「全部视频分页展示 · 关注账号优先 · 按发布时间倒序」
|
||||
- 服务重启(Stop-Process 32752 → 新 PID 12472),API 实测 total=625、首屏=两关注账号、时间倒序
|
||||
|
||||
## AI写脚本列表页:标题跳视频详情 + 发布时间筛选(18:06)
|
||||
- 标题点击行为改:data-ext 新标签打开原视频 → `#/video/${id}` 查看视频详情(与账号详情页 397 行同款 link 样式;原视频入口仍在详情页「查看视频 ↗」)
|
||||
- 发布时间筛选:listVideos(dsh-data.js)新增 days 参数 `v.publish_time >= date('now', '-N days')`;server.js /api/dsh/videos 透传 days;前端 renderRewrites 加 rwDays 下拉(全部/近一个月30/近三个月90/近半年180/近一年365,默认近一个月),state.days='30'
|
||||
- 实测:30天 32 条 / 90天 142 条 / 365天 491 条 / 全部 625 条;服务重启 PID 39516
|
||||
- 教训:改 dsh-data.js 后 API 不生效还查了进程缓存——实际是 server.js 路由没透传新参数,改服务层函数必须同步检查调用方透传
|
||||
|
||||
## AI写脚本列表页:标题跳详情 + 发布时间筛选确认(18:08)
|
||||
- 用户要求「点击视频标题→查看视频详情 + 增加发布时间筛选默认近一个月」:核查发现两处代码已实现(标题 #/video/ 链接 data-pages.js:578;rwDays 下拉 595-601 + state.days='30';listVideos days 参数 158-160 行上一轮已加)
|
||||
- 微优化:date('now') → date('now','localtime')(UTC 边界凌晨 0-8 点会差一天,近一个月语义更准)
|
||||
- 重启服务(旧 PID 39516 → 新 PID 39792),实测 days=30→32 条、days=全部→625 条,关注优先+时间倒序保持
|
||||
- 教训:功能已实现但用户重复提需求时,先 API 实测确认(避免重复改),再重开页面排除缓存
|
||||
|
||||
## S6 框架规范通用修改:开场钩子场结构性符号承接(18:27)
|
||||
- 根因:RW31 丢金额机制、RW27/29 缺来源故事——S5 选题定了「结构性符号不能丢」,但 S6 框架的「开场钩子场」设计缺一道「账号结构性符号必须承接」的检查,S9 自检的「设定承接」又太笼统,导致符号在 S5→S9 链路中被稀释漏出
|
||||
- 修改:references/创作流程/6_生成短视频框架.md「场次结构表·8列维度说明」后新增通用规则(引用 S5「结构性符号」概念,不硬编码任何账号/符号)——「开场钩子场(场1)结构性符号承接:场1钩子设计必须承接账号结构性符号,位于开头位置的须一个镜头/一句话交代清楚(遵循 S5 呈现效率规则),禁止铺垫/多步推理;缺失或需铺垫的打回修改场1」
|
||||
- 同步:V1.0 源技能(~/.workbuddy/skills/短视频脚本创作/)+ dsh 副本(~/.dsh/skills/short-video-script/)两处,Lite 不动
|
||||
- 重跑验证:备份 mcn-plugin.db.bak-s6fix-* → 删 RW26-31 + script_review(rewrite_id 26-31) → build-create-prompts.cjs 提交 6 任务(automation-1788258518460 等,v309/313/320=topic、v315/316/318=content)→ poll-create-tasks.cjs 轮询(task M70h3o)
|
||||
@@ -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