AI任务参数级技能挂载:/api/run支持skills参数写入automations.skills_json,前端创作/复盘/分镜/提炼按钮按技能名传参

This commit is contained in:
maogeigei committed 2026-09-01 16:31:40 +08:00
1 parent ef98395c61
commit f459abe2f2
7 files changed
+164 -5

No files matched your search

@@ -95,7 +95,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const resp = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: t.prompt, name: '创作指令' }),
body: JSON.stringify({ prompt: t.prompt, name: '创作指令', skills: ['短视频脚本创作'] }),
});
const d = await resp.json();
results.push({ videoId: t.videoId, id: d.id, ok: d.ok, error: d.error });
@@ -0,0 +1,39 @@
/* poll-review-tasks.cjs — 轮询 6 条复盘任务(/api/run/status,ACCEPTED 已归 done)*/
const TASKS = [
{ rw: 31, id: 'automation-1788251452496' },
{ rw: 30, id: 'automation-1788251455526' },
{ rw: 29, id: 'automation-1788251458549' },
{ rw: 28, id: 'automation-1788251461578' },
{ rw: 27, id: 'automation-1788251464600' },
{ rw: 26, id: 'automation-1788251467630' },
];
const state = {};
TASKS.forEach((t) => (state[t.rw] = 'pending'));
const status = async (id) => {
try {
const r = await fetch('http://localhost:8900/api/run/status?id=' + encodeURIComponent(id));
const d = await r.json();
return d.state || 'unknown';
} catch { return 'unknown'; }
};
console.log('START polling 6 review tasks:', new Date().toLocaleTimeString('zh-CN'));
const started = Date.now();
const t0 = Date.now();
while (Date.now() - started < 60 * 60 * 1000) {
let allDone = true;
for (const t of TASKS) {
const s = await status(t.id);
state[t.rw] = s;
if (s !== 'done' && s !== 'error') allDone = false;
}
console.log(new Date().toLocaleTimeString('zh-CN'), '|',
TASKS.map((t) => 'rw' + t.rw + ':' + state[t.rw]).join(' '));
if (allDone) {
const errs = TASKS.filter((t) => state[t.rw] === 'error');
console.log(errs.length ? 'ALL DONE (with errors): ' + errs.map((t) => 'rw' + t.rw).join(',') : 'ALL DONE ✓');
process.exit(0);
}
await new Promise((r) => setTimeout(r, 30000));
}
console.log('TIMEOUT after 60min, last states:', JSON.stringify(state));
process.exit(1);
@@ -0,0 +1,54 @@
// 解析 app.asar 找 skills_json 消费逻辑
const fs = require('fs');
const path = require('path');
const asarPath = 'C:/Users/maidou/AppData/Local/Programs/WorkBuddy/resources/app.asar';
const buf = fs.readFileSync(asarPath);
// 新版 asar 多重 pickle 头:@0=4, @4=headerSize+4, @8=headerSize, @12=JSON长度, @16=JSON
const headerLen = buf.readUInt32LE(12);
let pos = 16;
const header = JSON.parse(buf.slice(pos, pos + headerLen).toString('utf8'));
// 遍历文件树找含 skills_json 的 js 文件(全量,不限目录)
const hits = [];
function walk(node, prefix) {
if (!node.files) return;
for (const name of Object.keys(node.files)) {
const f = node.files[name];
const full = prefix + '/' + name;
if (f.files) { walk(f, full); }
else if (/\.(js|cjs|mjs|ts)$/.test(name)) {
hits.push({ full, offset: f.offset, size: f.size });
}
}
}
walk(header, '');
// 逐个读取含 skills_json 的文件片段
const TARGET = Buffer.from('skills_json');
const results = [];
for (const h of hits) {
const start = pos + h.offset;
const chunk = buf.slice(start, start + h.size);
if (chunk.includes(TARGET)) {
// 找出 skills_json 前后各 400 字符上下文
let idx = 0;
while ((idx = chunk.indexOf(TARGET, idx)) !== -1) {
const from = Math.max(0, idx - 500);
const to = Math.min(chunk.length, idx + 700);
results.push({ file: h.full, ctx: chunk.slice(from, to).toString('utf8') });
idx += TARGET.length;
if (results.length > 40) break;
}
}
if (results.length > 40) break;
}
console.log('含 skills_json 的文件数:', new Set(results.map(r=>r.file)).size);
console.log('片段数:', results.length);
// 只输出含读写逻辑的片段(排除 DDL/列定义)
const interesting = results.filter(r => /INSERT|SELECT|UPDATE|JSON\.parse|=>|function|await|\.map\(|\.filter\(|include\(|find\(|query|execute/i.test(r.ctx) && !/CREATE TABLE|ALTER TABLE|addSql|statement-breakpoint|"name",\s*$/.test(r.ctx));
console.log('读写逻辑片段数:', interesting.length);
interesting.slice(0, 25).forEach((r, i) => {
console.log('\n########## ' + r.file + ' [' + i + '] ##########');
console.log(r.ctx.replace(/[ \t]+/g, ' ').slice(0, 1600));
});
@@ -43,7 +43,7 @@ async function executeTask(opts) {
const d = await api('/api/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: opts.prompt, name: opts.label || '工作台任务' }),
body: JSON.stringify({ prompt: opts.prompt, name: opts.label || '工作台任务', skills: opts.skills }),
});
if (d.error) throw new Error(d.error);
taskId = d.id;
@@ -206,7 +206,7 @@ async function confirmAiScript(accountName, btn) {
const prompt = topic
? `请为达人「${accountName}」创作一条短视频脚本。${personaBlock}\n选题方向:${topic}\n${act}`
: `请为达人「${accountName}」创作一条短视频脚本。${personaBlock}\n${act}`;
executeTask({ prompt, label: '创作指令', btn });
executeTask({ prompt, label: '创作指令', btn, skills: ['短视频脚本创作'] });
});
}
@@ -507,6 +507,7 @@ async function renderAccountDetail(view, id) {
? `${SKILL_HINT_CREATE}\n请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}${tb.block}${createTail}`
: `${SKILL_HINT_CREATE}\n请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}\n该视频尚未解析,请先解析这个抖音视频并做拆解分析(${r.url ? '视频地址:' + r.url + '\n' : ''}视频标题:《${r.title}》),再基于账号设定与提炼出的选题,创作完整故事脚本。`,
label: '创作指令',
skills: ['短视频脚本创作'],
btn: b,
});
}));
@@ -517,6 +518,7 @@ async function renderAccountDetail(view, id) {
target: acc.account_name,
prompt: `请为达人「${acc.account_name}」提炼账号设定(生成人设卡)。`,
label: '提炼指令',
skills: ['mcn-dou-analysis'],
btn: exBtn,
}));
view._reload = load; // 静默自动刷新(保留当前 tab/翻页)
@@ -647,6 +649,7 @@ async function renderRewrites(view) {
? `${SKILL_HINT_CREATE}\n请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}${tb.block}${createTail}`
: `${SKILL_HINT_CREATE}\n请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}\n该视频尚未解析,请先解析这个抖音视频并做拆解分析(${r.url ? '视频地址:' + r.url + '\n' : ''}视频标题:《${r.title}》),再基于账号设定与提炼出的选题,创作完整故事脚本。`,
label: '创作指令',
skills: ['短视频脚本创作'],
btn: b,
});
}));
@@ -723,6 +726,7 @@ async function renderRewrites(view) {
target: `《${title}》`,
prompt: `请将视频《${title}》的 AI 脚本${regenerate ? '重新' : ''}展开成 AI 分镜提示词(按分镜技能生成镜头级描述)。`,
label: '分镜指令',
skills: ['短视频分镜脚本'],
btn,
});
};
@@ -823,6 +827,7 @@ async function renderReviews(view) {
desc: '将对该 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式),按 mcn-script-review(脚本复盘)技能执行。',
prompt: '请对这条 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式),按 mcn-script-review(脚本复盘)技能执行。',
label: '复盘指令',
skills: ['脚本复盘'],
btn: b,
});
}));
@@ -505,8 +505,11 @@ const server = http.createServer((req, res) => {
req.on('data', (c) => { body += c; if (body.length > 1e6) req.destroy(); });
req.on('end', () => {
try {
const { prompt, name } = JSON.parse(body || '{}');
const { prompt, name, skills } = JSON.parse(body || '{}');
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) return sendErr(400, 'prompt 不能为空');
// 09-01 参数级技能挂载:skills_json 是 automations 表原生字段(default '[]'),
// 客户端调度器创建会话时按该字段挂载技能;前端传技能名数组(如 ['短视频脚本创作'])即完成任务级技能绑定
const skillsArr = Array.isArray(skills) ? skills.filter((s) => typeof s === 'string' && s.trim()).map((s) => s.trim()) : [];
// 09-01 并发方案A:浏览器类任务(账号信息/视频列表/采集/导入)自动注入浏览器锁约束——
// 多个 AI 会话任务并行共享同一 9223 Chrome 实例,必须互斥使用,否则标签页互相导航抢占
const BROWSER_HINT = /账号信息|视频列表|保存并分析|采集|导入账号|网页采集|浏览器/;
@@ -535,7 +538,7 @@ const server = http.createServer((req, res) => {
const ownerUserId = uidRow ? uidRow.user_id : '';
db.prepare(`INSERT INTO automations (id,name,prompt,status,schedule_type,scheduled_at,next_run_at,rrule,cwds,created_at,updated_at,skills_json,connector_ids_json,model_id,permission_mode,owner_user_id,owner_status)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
.run(id, name || '工作台任务', finalPrompt, 'ACTIVE', 'once', scheduledAt, nextRunAt, '', JSON.stringify([cwd]), now, now, '[]', '[]', 'deepseek-v4-flash', 'fullAccess', ownerUserId, 'confirmed');
.run(id, name || '工作台任务', finalPrompt, 'ACTIVE', 'once', scheduledAt, nextRunAt, '', JSON.stringify([cwd]), now, now, JSON.stringify(skillsArr), '[]', 'deepseek-v4-flash', 'fullAccess', ownerUserId, 'confirmed');
db.close();
log('已提交 AI 任务: ' + id + (name ? ' (' + name + ')' : ''));
return sendJSON(200, { ok: true, id, message: '任务已提交,请在左侧会话栏查看执行' });
@@ -0,0 +1,58 @@
/* submit-review-tasks.cjs — 对俊希最新的 AI 脚本逐条提交复盘任务(与诊断页「复盘」按钮一致 + 落库 script_review 指令)
用法: node submit-review-tasks.cjs [rewriteId1,rewriteId2,...] (不传则自动取 account_id=3 的最新 done 记录) */
const { DatabaseSync } = require('node:sqlite');
const path = require('path');
const DB = path.join(__dirname, 'mcn-plugin.db');
const API = 'http://localhost:8900/api/run';
const ACCOUNT_ID = 3;
const db = new DatabaseSync(DB, { readOnly: true });
let ids = process.argv.slice(2).map(Number).filter(Boolean);
if (!ids.length) {
ids = db.prepare("SELECT id FROM rewrite_log WHERE account_id=? AND status='done' AND script_text IS NOT NULL AND script_text <> '' ORDER BY id DESC LIMIT 20").all(ACCOUNT_ID).map((r) => r.id);
}
const rows = [];
for (const id of ids) {
const r = db.prepare('SELECT rl.id, rl.video_id, rl.title, v.video_title FROM rewrite_log rl LEFT JOIN account_videos v ON v.id=rl.video_id WHERE rl.id=?').get(id);
if (r) rows.push(r);
}
db.close();
if (!rows.length) { console.log('NO REWRITES TO REVIEW'); process.exit(1); }
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
(async () => {
for (const r of rows) {
const prompt = `请对这条 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式),按 mcn-script-review(脚本复盘)技能执行。
脚本标题:《${r.title || r.video_title || ''}》(rewrite_log.id=${r.id},video_id=${r.video_id})
【任务完成后必须执行】
将复盘结果保存到工作台数据库 mcn-plugin.db 的 script_review 表(数据库路径:D:\\AgentSkill\\mcn-video-script\\project\\短视频脚本创作\\V1.0\\mcn-work-shop\\mcn-plugin.db),字段:
- type='ai'
- rewrite_id=${r.id}
- video_id=${r.video_id}
- dims_json=各维度评分 JSON(设定承接/框架节奏/钩子设置/叙事逻辑/极致事件/共鸣和共情/格式规范性/台词口语化/内容合规/教程完整链/其他)
- total=总分(数字)
- verdict=结论
- issues_json=问题清单 JSON
- content=复盘完整内容
- summary=摘要
- status='done'
(用 node:sqlite 或 sqlite3 写入;若该表不存在则创建后再写)
完成后回复:评分 + 是否已保存。`;
try {
const resp = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, name: '复盘指令', skills: ['脚本复盘'] }),
});
const d = await resp.json();
console.log(`SUBMIT review rw${r.id} →`, d.ok ? ('id=' + d.id) : ('ERR ' + d.error));
} catch (e) {
console.log(`SUBMIT review rw${r.id} → EXCEPTION ${e.message}`);
}
await sleep(3000);
}
console.log('ALL REVIEW TASKS SUBMITTED');
})();