AI任务参数级技能挂载:/api/run支持skills参数写入automations.skills_json,前端创作/复盘/分镜/提炼按钮按技能名传参
This commit is contained in:
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');
|
||||
})();
|
||||
Reference in new issue
Block a user