feat(MCN工作台): 移植dsh导入账号功能 + 弹窗按钮/宽度优化 + 分镜技能归入subskill
- 账号列表页新增「导入账号」:两种方式(抖音账号信息/本地文件夹),复用 executeTask 任务链路提交 AI 任务 - 查看改写弹窗:展开源脚本按钮移入 AI脚本 标题右侧(间隔10px),弹窗最大宽度 1400→1600 - 短视频提示词生成 整体移入 短视频脚本创作/V1.0/subskill/mcn-video-prompt(与 mcn-dou-analysis 同构,138 文件 R100 保留历史) - 工作台 dsh-data/server 若干修复 + 记忆更新
This commit is contained in:
1 parent
ad4646bb12
commit
96fce56fba
154 files changed
+1201
-198
No files matched your search
@@ -232,6 +232,20 @@ const server = http.createServer((req, res) => {
|
||||
return dshOk(200, dsh.listAccounts({ page: qInt('page', 1), pageSize: qInt('pageSize', 20), search: String(q.get('search') || ''), track: String(q.get('track') || ''), type: String(q.get('type') || 'all') }));
|
||||
} catch (e) { return sendErr(500, e.message); }
|
||||
}
|
||||
if (p === '/api/dsh/account-favorite' && req.method === 'POST') {
|
||||
if (dshGuard()) return;
|
||||
let body = '';
|
||||
req.on('data', (c) => { body += c; if (body.length > 1e6) req.destroy(); });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { id, fav } = JSON.parse(body || '{}');
|
||||
if (!id) return sendErr(400, '缺少 id');
|
||||
dsh.setFavorite(id, !!fav);
|
||||
return dshOk(200, { id, favorite: !!fav });
|
||||
} catch (e) { return sendErr(500, e.message); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (p === '/api/dsh/account-videos') {
|
||||
if (dshGuard()) return;
|
||||
try {
|
||||
@@ -320,6 +334,99 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询 AI 任务执行状态(供前端轮询恢复按钮):queued / pending / running / done / error / notfound
|
||||
if (p === '/api/run/status' && req.method === 'GET') {
|
||||
const id = q.get('id');
|
||||
if (!id) return sendErr(400, '缺少 id');
|
||||
try {
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const db = new DatabaseSync(process.env.WORKBUDDY_DB || path.join(os.homedir(), '.workbuddy', 'workbuddy.db'), { readOnly: true });
|
||||
const auto = db.prepare('SELECT status, deleted_at, last_run_at FROM automations WHERE id=?').get(id);
|
||||
if (!auto || auto.deleted_at) { db.close(); return sendJSON(200, { ok: true, id, state: 'notfound' }); }
|
||||
const st = db.prepare('SELECT running, last_error FROM automation_runtime_state WHERE automation_id=?').get(id);
|
||||
const runs = db.prepare("SELECT status FROM automation_runs WHERE automation_id=? ORDER BY created_at DESC LIMIT 1").get(id);
|
||||
db.close();
|
||||
let state = 'queued';
|
||||
if (st && Number(st.running) === 1) state = 'running';
|
||||
else if (runs) {
|
||||
const s = String(runs.status || '').toUpperCase();
|
||||
if (s === 'IN_PROGRESS') state = 'running';
|
||||
else if (s === 'PENDING_REVIEW') state = 'pending';
|
||||
else if (['DONE', 'COMPLETED', 'SUCCESS', 'FINISHED'].includes(s)) state = 'done';
|
||||
else if (['ERROR', 'CANCELLED', 'FAILED', 'INTERRUPTED'].includes(s)) state = 'error';
|
||||
else state = 'pending';
|
||||
}
|
||||
return sendJSON(200, { ok: true, id, state, error: (st && st.last_error) || null });
|
||||
} catch (e) {
|
||||
return sendErr(500, '状态查询失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- AI 生成选题(Dify chat-messages 即时调用,参考技能 S5 选题方法论) ----------
|
||||
if (p === '/api/ai/topics' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', (c) => { body += c; if (body.length > 1e6) req.destroy(); });
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const { accountName } = JSON.parse(body || '{}');
|
||||
if (!accountName || typeof accountName !== 'string') return sendErr(400, '缺少账号名');
|
||||
// 1) 查账号定位 + 人设摘要(工作台库)
|
||||
let contentText = '', personaText = '';
|
||||
try {
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const db = new DatabaseSync(path.join(ROOT, 'mcn-plugin.db'), { readOnly: true });
|
||||
const acc = db.prepare('SELECT * FROM hot_accounts WHERE account_name=? LIMIT 1').get(accountName);
|
||||
if (acc) {
|
||||
contentText = String(acc.content || '').slice(0, 500);
|
||||
const p = db.prepare('SELECT content_json, summary FROM account_persona WHERE account_id=? ORDER BY id DESC LIMIT 1').get(acc.id);
|
||||
if (p) personaText = String(p.summary || p.content_json || '').slice(0, 500);
|
||||
}
|
||||
db.close();
|
||||
} catch (e) { /* 库不可用不阻塞,仍可生成 */ }
|
||||
// 2) 读 Dify key:环境变量优先,其次技能脚本内置默认
|
||||
let apiKey = process.env.DIFY_MCN_CYLG_KEY || '';
|
||||
if (!apiKey) {
|
||||
const scriptPath = path.join(ROOT, '..', 'scripts', 'MCN_CYLG_API.py');
|
||||
try {
|
||||
const src = fs.readFileSync(scriptPath, 'utf8');
|
||||
const m = src.match(/DEFAULT_CYLG_KEY\s*=\s*"([^"]+)"/);
|
||||
if (m) apiKey = m[1];
|
||||
} catch (e) { /* 脚本不存在则走 env */ }
|
||||
}
|
||||
if (!apiKey) return sendErr(500, '未配置 DIFY_MCN_CYLG_KEY(环境变量或技能脚本内置 KEY)');
|
||||
// 3) 构造选题 query(参考 references/创作流程/5_生成短视频选题.md 方法论)
|
||||
const query = '你是短视频选题策划师,请为达人「' + accountName + '」结合账号设定生成 3 个爆款选题方案。\n\n'
|
||||
+ '【达人信息】\n' + (contentText || '(无账号定位信息)') + '\n'
|
||||
+ (personaText ? '【人设摘要】\n' + personaText + '\n' : '')
|
||||
+ '\n要求:\n'
|
||||
+ '1. 选题要跳出账号核心行为框架,但保留账号结构性符号(固定机制/口头禅等)\n'
|
||||
+ '2. 每个选题一句话说清主题+切入角度,标注目标体感(如治愈感/爽感/获得感)\n'
|
||||
+ '3. 只输出 3 个选题,每行一个,格式:1. 主题(切入角度|目标体感)\n'
|
||||
+ '4. 不要输出其他解释';
|
||||
// 4) 调 Dify chat-messages(blocking 即时返回)
|
||||
const difyUrl = String(process.env.DIFY_URL || 'https://mydify.youmanvideo.com/v1').replace(/\/+$/, '');
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 100000);
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(difyUrl + '/chat-messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'blocking', user: 'mcn-work-shop' }),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
} finally { clearTimeout(timer); }
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) return sendErr(502, 'AI 接口错误: ' + (data.message || resp.status));
|
||||
log('AI 选题生成: ' + accountName);
|
||||
return dshOk(200, { topics: data.answer || '' });
|
||||
} catch (e) {
|
||||
return sendErr(500, '选题生成失败: ' + e.message);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- 静态文件 ----------
|
||||
let filePath = path.join(PUBLIC_DIR, p === '/' ? 'index.html' : p);
|
||||
// 防穿越
|
||||
|
||||
Reference in new issue
Block a user