AI创作弹窗对话打磨 v2:单弹窗需求框+输入框+AI全程引导
废弃旧分步向导+跨会话打磨+md载入路径,改为单弹窗 confirmAiScript 重写。
改动:
- public/data-pages.js: confirmAiScript v2(L199-326)= REQ_FIELDS 10 项常量 + 顶部10行需求框常显(空值灰显"待定·由AI按人设自决")+ 中部对话流(u/ai气泡)+ 底部输入行 + 取消/提交创作;无 step/chip/AI按钮/载入按钮/上下步
- public/style.css: 删 .wizard-modal/.wz-/.ai-topic 旧样式;新增 .req-modal/.req-sheet/.req-dialog/.req-msg.mine/.req-msg.ai/.req-bubble/.req-input-row/.req-input/.req-send 全套
- server.js: 新增 POST /api/ai/clarify(紧跟 /api/ai/topics 之后)= 同步阻塞 Dify chat-messages 100s AbortController;入参 {accountName,turn,reqMd,history};query 拼装账号人设+当前需求+最近2轮历史+本轮输入;强制输出"对话+==REQ==+10行需求"模板
- 记忆:MEMORY.md 核心技能段补"AI创作弹窗对话打磨 v2"长期记忆;2026-09-02.md 追加完整收尾日志
关键设计:每轮重发 reqMd + 最近 2 轮 history 做无状态上下文;AI 可更新任意字段但严禁生成脚本正文;reqLines 仅取非空字段 + "未填由AI按人设自决"提示。
冒烟 tmp/cdp-req-dialog-smoke.mjs(CDP 9224)4 步端到端 53s 全绿:结构✓/双轮真实Dify打磨✓/需求框0→6→8字段✓/提交捕获{label:创作指令,skills:[短视频脚本创作],hasPersona:true,reqRows:8,modalClosed:true}✓。
This commit is contained in:
1 parent
07d63a8caf
commit
1390268585
6 files changed
+291
-57
No files matched your search
@@ -156,66 +156,173 @@ function bindBatchBar(container, opts) {
|
||||
}));
|
||||
}
|
||||
|
||||
/* AI写脚本 确认弹窗:按账号是否有设定动态文案;选题输入区(手输 / AI 结合人设生成)→ 确认后执行创作任务 */
|
||||
/* ============================================================
|
||||
AI创作 需求打磨辅助(09-02 v2:弹窗内对话式——需求框 + 输入框,无跳会话/无文件回填/无选题按钮)
|
||||
============================================================ */
|
||||
// 需求单字段顺序:弹窗需求框展示、AI 输出模板、创作 prompt 共用(值空 = 待 AI 按人设自决)
|
||||
const REQ_FIELDS = [
|
||||
['idea', '一句话创意与选题'], ['form', '叙事形态'], ['dur', '时长目标'], ['mood', '情绪基调'], ['aud', '受众'],
|
||||
['plot', '核心情节'], ['end', '结尾落点'], ['sym', '结构性符号'], ['ad', '广告植入'], ['note', '其他约束'],
|
||||
];
|
||||
// 需求 md 行 → 向导字段映射(parseReqSheet 用)
|
||||
const REQ_LABEL_TO_KEY = Object.fromEntries(REQ_FIELDS.map(([k, label]) => [label, k]));
|
||||
// 需求单 markdown 解析 → 状态对象(仅解析已知字段,空值/待填写忽略)
|
||||
function parseReqSheet(md) {
|
||||
const out = {};
|
||||
for (const line of String(md || '').split('\n')) {
|
||||
const m = /^[-*]\s*([^::]+?)\s*[::]\s*(.*)$/.exec(line.trim());
|
||||
if (!m) continue;
|
||||
const key = REQ_LABEL_TO_KEY[m[1].trim()];
|
||||
const val = (m[2] || '').trim();
|
||||
if (key && val && val !== '(待填写)') out[key] = val;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// 状态 → 10 行完整需求清单(供 AI 每轮保持同模板输出;空字段留空值)
|
||||
function reqMdOf(w) {
|
||||
return REQ_FIELDS.map(([k, label]) => `- ${label}:${String(w[k] || '').trim()}`).join('\n');
|
||||
}
|
||||
// 状态 → 已填需求行(创作 prompt 用,非空才输出)
|
||||
function reqLines(w) {
|
||||
return REQ_FIELDS.filter(([k]) => String(w[k] || '').trim()).map(([k, label]) => `- ${label}:${String(w[k]).trim()}`);
|
||||
}
|
||||
// 组装创作 prompt:人设块在前(既有铁律),向导需求在后;空维度不输出由 AI 自主决策
|
||||
function buildCreatePrompt(accountName, w, personaBlock, hasPersona) {
|
||||
const req = reqLines(w);
|
||||
const reqSec = req.length ? `\n【创作需求】\n${req.join('\n')}` : '';
|
||||
const tail = hasPersona
|
||||
? `请贴合上述账号设定(人设与内容规则)创作;【创作需求】中填写的各项为明确要求、必须满足,未填写的维度由你按人设与选题自主决策。`
|
||||
: `该账号暂无账号设定,请先为达人「${accountName}」提取账号设定(生成人设卡),再基于账号设定与【创作需求】创作完整脚本;未填写的维度由你自主决策。`;
|
||||
return `请为达人「${accountName}」创作一条短视频脚本。${personaBlock}${reqSec}\n${tail}`;
|
||||
}
|
||||
|
||||
/* AI创作 弹窗(09-02 v2:需求框 + 输入框 对话式打磨)——
|
||||
顶部=创作需求(10 行字段,随对话逐轮被 AI 更新,空值标「待定·由 AI 按人设自决」)
|
||||
中部=对话流(用户输入 vs AI 整理/追问),底部=输入框 + 提交创作 */
|
||||
async function confirmAiScript(accountName, btn) {
|
||||
// 判断账号是否有有效设定(persona 非空且非测试占位):有 →「基于当前账号设定」,无 →「先进行账号设定分析」
|
||||
const accId = btn && btn.dataset.accId ? btn.dataset.accId : '';
|
||||
let hasPersona = false, personaContent = '';
|
||||
if (accId) {
|
||||
const w = { idea: '', form: '', dur: '', mood: '', aud: '', plot: '', end: '', sym: '', ad: '', note: '' };
|
||||
const convo = []; // 对话流 { role: 'u' | 'ai', text }
|
||||
let pHas = false, pContent = '';
|
||||
let busy = false;
|
||||
const loadPersona = async () => {
|
||||
if (pContent || !accId) return;
|
||||
try {
|
||||
const r = await api('/api/dsh/account-persona?accountId=' + encodeURIComponent(accId));
|
||||
const content = String(r.content || '').trim();
|
||||
hasPersona = !!content && !/测试写入|连通性测试/.test(content);
|
||||
if (hasPersona) personaContent = personaForCreate(content); // 完整拼入,仅剔除「十一、当前需解决的问题」及之后非创作章节
|
||||
} catch (e) { hasPersona = false; }
|
||||
}
|
||||
const desc = hasPersona
|
||||
? `将为达人「${esc(accountName)}」基于当前账号设定生成脚本。可先填写选题方向,或点击右下角 AI 按钮结合人设生成选题:`
|
||||
: `将为达人「${esc(accountName)}」创作一条短视频脚本(先进行账号设定分析,再生成完整脚本)。可先填写选题方向,或点击右下角 AI 按钮结合人设生成选题:`;
|
||||
const c = String(r.content || '').trim();
|
||||
pHas = !!c && !/测试写入|连通性测试/.test(c);
|
||||
if (pHas) pContent = personaForCreate(c); // 完整拼入,仅剔除「十一、当前需解决的问题」及之后非创作章节
|
||||
} catch (e) { pHas = false; }
|
||||
};
|
||||
const personaBlock = () => (pHas && pContent ? `\n【账号设定(人设卡)】\n${pContent}\n` : '');
|
||||
|
||||
const mask = document.createElement('div'); mask.className = 'modal-mask';
|
||||
mask.innerHTML = `
|
||||
<div class="modal">
|
||||
<h3>${svgIcon('pen', 18)} AI创作</h3>
|
||||
<p class="dim">${desc}</p>
|
||||
<div class="ai-topic">
|
||||
<textarea id="aiTopicInput" class="ai-topic-input" placeholder="输入选题方向(可选),如:亲子露营的搞笑瞬间"></textarea>
|
||||
<button class="ai-topic-btn" id="aiTopicGen" title="AI 结合人设生成选题">AI</button>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="ghost" data-cancel>取消</button>
|
||||
<button class="primary" data-ok>确认执行</button>
|
||||
</div>
|
||||
</div>`;
|
||||
mask.innerHTML = `<div class="modal req-modal">
|
||||
<div class="modal-title-row"><span>AI创作 · ${esc(accountName)}</span><button class="btn" data-x title="关闭">${svgIcon('x', 14)}</button></div>
|
||||
<div class="req-head">在下方输入想法,AI 会边聊边把需求整理到「创作需求」框里;聊够了点「提交创作」。</div>
|
||||
<div class="req-sheet" id="reqSheet"></div>
|
||||
<div class="req-dialog" id="reqDialog"></div>
|
||||
<div class="req-input-row">
|
||||
<textarea id="reqInput" class="req-input" rows="2" placeholder="说说你的想法,例如:想拍一个亲子互动的搞笑短剧… (Enter 发送,Shift+Enter 换行)"></textarea>
|
||||
<button type="button" class="primary req-send" id="reqSend">发送</button>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="ghost" data-cancel>取消</button>
|
||||
<button class="primary" data-create>提交创作</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(mask);
|
||||
const close = () => mask.remove();
|
||||
mask.addEventListener('click', (e) => { if (e.target === mask) close(); });
|
||||
mask.querySelector('[data-x]').addEventListener('click', close);
|
||||
mask.querySelector('[data-cancel]').addEventListener('click', close);
|
||||
const genBtn = mask.querySelector('#aiTopicGen');
|
||||
genBtn.addEventListener('click', async () => {
|
||||
if (genBtn.disabled) return;
|
||||
genBtn.disabled = true; genBtn.textContent = '…';
|
||||
const q = (s) => mask.querySelector(s);
|
||||
|
||||
/* —— 需求框渲染:10 行字段,空值灰显 —— */
|
||||
const renderSheet = () => {
|
||||
const rows = REQ_FIELDS.map(([k, label]) => {
|
||||
const v = String(w[k] || '').trim();
|
||||
return `<div class="req-sheet-row${v ? '' : ' empty'}"><span class="rs-label">${label}</span><span class="rs-val">${v ? esc(v) : '(待定 · 由 AI 按人设自决)'}</span></div>`;
|
||||
}).join('');
|
||||
q('#reqSheet').innerHTML = rows;
|
||||
};
|
||||
/* —— 对话流渲染 —— */
|
||||
const renderDialog = () => {
|
||||
const el = q('#reqDialog');
|
||||
el.innerHTML = convo.length
|
||||
? convo.map((c) => (c.role === 'u'
|
||||
? `<div class="req-msg mine"><div class="req-bubble">${esc(c.text)}</div></div>`
|
||||
: `<div class="req-msg ai"><div class="req-bubble">${esc(c.text)}</div></div>`)).join('')
|
||||
: `<div class="req-dialog-empty">在这里输入一个初步想法,AI 会引导你补齐形态 / 时长 / 情绪 / 受众 / 情节 / 结尾 / 植入等需求;每条输入都会即时更新上方的创作需求框。</div>`;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
};
|
||||
const setBusy = (b, txt) => {
|
||||
busy = b;
|
||||
const s = q('#reqSend');
|
||||
if (s) { s.disabled = b; s.textContent = b ? (txt || '整理中…') : '发送'; }
|
||||
};
|
||||
/* —— 发送一轮:调 /api/ai/clarify(同步打磨,返回 reply + ==REQ== 段) —— */
|
||||
const sendTurn = async (raw) => {
|
||||
const text = String(raw || '').trim();
|
||||
if (!text || busy) return;
|
||||
convo.push({ role: 'u', text });
|
||||
q('#reqInput').value = '';
|
||||
renderDialog();
|
||||
setBusy(true);
|
||||
try {
|
||||
const r = await api('/api/ai/topics', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ accountName }) });
|
||||
// 带最近 2 轮问答做上下文(无状态:需求本身在 reqMd 中已完整携带)
|
||||
const history = [];
|
||||
for (let i = convo.length - 2; i >= 0 && history.length < 2; i--) {
|
||||
if (convo[i].role === 'ai') {
|
||||
const u = convo[i - 1] && convo[i - 1].role === 'u' ? convo[i - 1].text : '';
|
||||
history.unshift({ u, a: convo[i].text });
|
||||
i--;
|
||||
}
|
||||
}
|
||||
const r = await api('/api/ai/clarify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accountName, turn: text, reqMd: reqMdOf(w), history }),
|
||||
});
|
||||
if (r.error) throw new Error(r.error);
|
||||
mask.querySelector('#aiTopicInput').value = r.topics || '(未生成内容)';
|
||||
showToast('✅ 已生成 3 个选题方向,可直接使用或修改');
|
||||
const answer = String(r.answer || '').trim();
|
||||
const ri = answer.lastIndexOf('==REQ==');
|
||||
const reply = (ri >= 0 ? answer.slice(0, ri) : answer).trim();
|
||||
const fields = ri >= 0 ? parseReqSheet(answer.slice(ri + '==REQ=='.length)) : {};
|
||||
let updated = 0;
|
||||
for (const k of Object.keys(fields)) {
|
||||
const v = String(fields[k] || '').trim();
|
||||
if (v && w[k] !== v) { w[k] = v; updated++; }
|
||||
}
|
||||
if (reply) convo.push({ role: 'ai', text: reply });
|
||||
renderSheet();
|
||||
renderDialog();
|
||||
showToast(updated > 0 ? `✅ 需求已更新 ${updated} 项` : (ri >= 0 ? '✅ 已记录(本次无新变更)' : '⚠️ 本次未返回结构化需求,可换种说法再试'));
|
||||
} catch (e) {
|
||||
showToast('❌ 选题生成失败: ' + e.message);
|
||||
convo.pop(); // 移除失败的用户消息,恢复输入
|
||||
q('#reqInput').value = text;
|
||||
renderDialog();
|
||||
showToast('❌ 打磨失败: ' + e.message);
|
||||
}
|
||||
genBtn.disabled = false; genBtn.textContent = 'AI';
|
||||
setBusy(false);
|
||||
};
|
||||
/* —— 事件 —— */
|
||||
const inputEl = q('#reqInput');
|
||||
inputEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendTurn(inputEl.value); }
|
||||
});
|
||||
mask.querySelector('[data-ok]').addEventListener('click', () => {
|
||||
const topic = mask.querySelector('#aiTopicInput').value.trim();
|
||||
q('#reqSend').addEventListener('click', () => sendTurn(inputEl.value));
|
||||
mask.querySelector('[data-create]').addEventListener('click', async () => {
|
||||
if (busy) return;
|
||||
await loadPersona();
|
||||
const prompt = buildCreatePrompt(accountName, w, personaBlock(), pHas);
|
||||
close();
|
||||
const personaBlock = personaContent ? `\n【账号设定(人设卡)】\n${personaContent}\n` : '';
|
||||
const act = hasPersona
|
||||
? `请贴合上述账号设定(人设与内容规则)创作。`
|
||||
: `该账号暂无账号设定,请先为达人「${accountName}」提取账号设定(生成人设卡),再基于账号设定创作完整脚本。`;
|
||||
const prompt = topic
|
||||
? `请为达人「${accountName}」创作一条短视频脚本。${personaBlock}\n选题方向:${topic}\n${act}`
|
||||
: `请为达人「${accountName}」创作一条短视频脚本。${personaBlock}\n${act}`;
|
||||
executeTask({ prompt, label: '创作指令', btn, skills: ['短视频脚本创作'] });
|
||||
});
|
||||
|
||||
renderSheet();
|
||||
renderDialog();
|
||||
inputEl.focus();
|
||||
}
|
||||
|
||||
/* ================= 周榜 ================= */
|
||||
|
||||
@@ -206,22 +206,43 @@ main { height: calc(100vh - 54px); }
|
||||
color: var(--dim); font-size: 13px; word-break: break-all;
|
||||
padding: 0 2px 4px; margin-bottom: 6px;
|
||||
}
|
||||
/* AI脚本弹窗:选题输入区 + 右下角 AI 生成按钮 */
|
||||
.ai-topic { position: relative; margin-bottom: 4px; }
|
||||
.ai-topic-input {
|
||||
width: 100%; min-height: 168px; padding: 8px 10px;
|
||||
border: 1px solid var(--border); border-radius: 8px;
|
||||
font-size: 15px; font-family: inherit; line-height: 1.5; resize: vertical;
|
||||
box-sizing: border-box;
|
||||
/* AI创作 弹窗(09-02 v2):需求框 + 对话流 + 输入框 对话式打磨 */
|
||||
.req-modal { width: 720px; max-width: 96vw; }
|
||||
.req-head { color: var(--dim); font-size: 13px; margin-bottom: 10px; line-height: 1.6; }
|
||||
.req-sheet {
|
||||
border: 1px solid var(--border); border-radius: 8px; background: #fbfcfe;
|
||||
max-height: 218px; overflow: auto; padding: 2px 12px;
|
||||
}
|
||||
.ai-topic-input:focus { outline: none; border-color: var(--primary); }
|
||||
.ai-topic-btn {
|
||||
position: absolute; right: 8px; bottom: 8px;
|
||||
border: 1px solid var(--primary); background: var(--primary-soft); color: var(--primary);
|
||||
border-radius: 6px; padding: 3px 12px; font-size: 14px; font-weight: 600; cursor: pointer;
|
||||
.req-sheet-row {
|
||||
display: flex; gap: 10px; padding: 6px 0; border-bottom: 1px dashed #eceff3;
|
||||
font-size: 14px; line-height: 1.55;
|
||||
}
|
||||
.ai-topic-btn:hover { background: var(--primary); color: #fff; }
|
||||
.ai-topic-btn:disabled { opacity: .6; cursor: wait; }
|
||||
.req-sheet-row:last-child { border-bottom: none; }
|
||||
.req-sheet-row.empty .rs-val { color: #c6ccd6; }
|
||||
.rs-label { flex: none; width: 108px; color: var(--dim); font-size: 13px; padding-top: 1px; }
|
||||
.rs-val { flex: 1; word-break: break-word; }
|
||||
.req-dialog {
|
||||
margin-top: 10px; max-height: 172px; overflow: auto;
|
||||
display: flex; flex-direction: column; gap: 6px; padding: 2px;
|
||||
}
|
||||
.req-dialog-empty { color: var(--dim); font-size: 13px; line-height: 1.7; padding: 2px; }
|
||||
.req-msg { display: flex; }
|
||||
.req-msg.mine { justify-content: flex-end; }
|
||||
.req-msg.ai { justify-content: flex-start; }
|
||||
.req-bubble {
|
||||
max-width: 88%; padding: 7px 11px; border-radius: 10px;
|
||||
font-size: 14px; line-height: 1.55; white-space: pre-wrap; word-break: break-word;
|
||||
}
|
||||
.req-msg.mine .req-bubble { background: var(--primary); color: #fff; border-bottom-right-radius: 3px; }
|
||||
.req-msg.ai .req-bubble { background: #f0f2f5; color: var(--text); border-bottom-left-radius: 3px; }
|
||||
.req-input-row { display: flex; gap: 8px; margin-top: 10px; align-items: flex-end; }
|
||||
.req-input {
|
||||
flex: 1; border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px;
|
||||
font-size: 14px; font-family: inherit; line-height: 1.5; box-sizing: border-box;
|
||||
resize: vertical; min-height: 44px; max-height: 132px;
|
||||
}
|
||||
.req-input:focus { outline: none; border-color: var(--primary); }
|
||||
.req-send { min-width: 68px; }
|
||||
.root-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
|
||||
.root-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
|
||||
@@ -650,6 +650,83 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- AI 创作需求打磨(09-02 弹窗内对话:Dify chat-messages 同步、无状态多轮) ----------
|
||||
// body: { accountName, turn(本轮输入), reqMd(当前需求10行md), history(最近轮[{u,a}]) } → { answer }
|
||||
if (p === '/api/ai/clarify' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', (c) => { body += c; if (body.length > 1e6) req.destroy(); });
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const { accountName, turn, reqMd, history } = JSON.parse(body || '{}');
|
||||
if (!accountName || typeof accountName !== 'string') return sendErr(400, '缺少账号名');
|
||||
if (!turn || typeof turn !== 'string' || !turn.trim()) return sendErr(400, '缺少用户输入');
|
||||
// 1) 账号设定摘要(工作台库,与选题接口同源)
|
||||
let personaText = '', contentText = '';
|
||||
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, 300);
|
||||
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, 900);
|
||||
}
|
||||
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(无状态:携带当前需求 + 最近对话,让 AI 输出 reply + ==REQ== 结构化段)
|
||||
const his = Array.isArray(history) ? history.filter((h) => h && h.u).slice(-2) : [];
|
||||
const histText = his.map((h) => '你:' + String(h.a || '') + '\n达人:' + String(h.u || '')).join('\n');
|
||||
const reqMdText = String(reqMd || '').trim();
|
||||
const query = '你是资深短视频编导,正在创作前通过多轮对话,帮达人「' + accountName + '」把创作需求打磨清楚。'
|
||||
+ '你的职责只限需求澄清、整理与追问;严禁生成脚本正文、台词、分镜或完整故事。\n\n'
|
||||
+ '【账号设定摘要】\n' + (personaText || '(暂无可用账号设定,按通用编导经验澄清)') + '\n'
|
||||
+ (contentText ? '【账号定位】\n' + contentText + '\n' : '')
|
||||
+ '【当前需求清单】\n' + (reqMdText || '(空:等待用户先给出初步想法)') + '\n'
|
||||
+ (histText ? '【对话历史】\n' + histText + '\n' : '')
|
||||
+ '【达人本轮输入】\n' + turn.trim() + '\n\n'
|
||||
+ '【行为要求】\n'
|
||||
+ '1. 理解本轮输入:可能是新想法、对上一问的回答、或想修改某一维度。将信息吸收进需求清单,可改写得更专业准确,但保留用户原意与账号结构性符号。\n'
|
||||
+ '2. 若关键维度仍有缺口且本轮输入不足以补齐:最多追问 1~2 个问题(优先给 A/B/C 选项供挑选),不要一次抛出全部问题,也不要重复已明确的维度。\n'
|
||||
+ '3. 若信息已基本齐备:用一两句话总结并提示用户可点击「提交创作」。\n'
|
||||
+ '4. 输出必须严格分两段:第一段是对用户说的话(120 字以内,含必要时的问题);\n'
|
||||
+ '然后另起一行单独输出 ==REQ==;\n'
|
||||
+ '接着输出 10 行需求清单(仍未确定的项留空值,严禁编造),模板:\n'
|
||||
+ '- 一句话创意与选题:\n- 叙事形态:\n- 时长目标:\n- 情绪基调:\n- 受众:\n- 核心情节:\n- 结尾落点:\n- 结构性符号:\n- 广告植入:\n- 其他约束:';
|
||||
// 4) Dify 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, { answer: data.answer || '' });
|
||||
} catch (e) {
|
||||
return sendErr(500, '需求打磨失败: ' + e.message);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 分析产物同步入库(09-01 新增):扫描账号产出目录 → 写工作台库 mcn-plugin.db
|
||||
if (p === '/api/import/account' && req.method === 'POST') {
|
||||
let body = '';
|
||||
|
||||
Reference in new issue
Block a user