From 01a1301a4a30a107e7b31a304d3f838188ab3c62 Mon Sep 17 00:00:00 2001 From: maogeigei Date: Mon, 7 Sep 2026 12:12:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0:=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E5=BC=8F=E9=80=89=E9=A2=98=E5=AE=8C=E5=96=84=20=3D=3DOPTS=3D?= =?UTF-8?q?=3D=20=E5=80=99=E9=80=89=20chips(=E7=BC=96=E5=AF=BC=E8=AF=8D?= =?UTF-8?q?=E5=AE=9A=E7=A8=BF+=E5=88=86=E6=94=AF=E8=BF=BD=E9=97=AE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V1.0/mcn-work-shop/public/data-pages.js | 35 +++++++++++++++---- .../V1.0/mcn-work-shop/public/style.css | 11 ++++++ .../V1.0/mcn-work-shop/server.js | 18 ++++++---- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js index 1e47329..4f5e45a 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js @@ -349,14 +349,29 @@ async function confirmAiScript(accountName, btn) { const n = Math.round(Number(durNum.value)); w.dur = (Number.isFinite(n) && n > 0) ? n + '秒' : ''; }); - /* —— 对话流渲染 —— */ + /* —— 对话流渲染(09-07:AI 消息可携带 ==OPTS== 候选,渲染为可点选 chips;点击即以该候选作为用户输入发送) —— */ const renderDialog = () => { const el = q('#reqDialog'); el.innerHTML = convo.length - ? convo.map((c) => (c.role === 'u' - ? `
${esc(c.text)}
` - : `
${esc(c.text)}
`)).join('') - : `
在这里输入一个初步想法,AI 会引导你补齐创意 / 创新·爆点 / 痛点爽点 / 叙事结构等需求(落点要素可留空,由 AI 按账号设定与选题补齐,共鸣与情节结尾由 AI 依痛点爽点发挥);每条输入都会即时更新上方的创作需求框。支持上传附件。Enter 发送,Shift+Enter 换行。
`; + ? convo.map((c, ci) => { + if (c.role === 'u') return `
${esc(c.text)}
`; + const opts = (Array.isArray(c.opts) && c.opts.length) + ? `
${c.opts.map((o) => ``).join('')}
` + : ''; + return `
${esc(c.text)}
${opts}
`; + }).join('') + : `
在这里输入一个初步想法,AI 会引导你补齐创意 / 创新·爆点 / 痛点爽点 / 叙事结构等需求(落点要素可留空,由 AI 按账号设定与选题补齐,共鸣与情节结尾由 AI 依痛点爽点发挥);每条输入都会即时更新上方的创作需求框,AI 追问时会给出可点选的候选。支持上传附件。Enter 发送,Shift+Enter 换行。
`; + el.querySelectorAll('.req-opt-chip').forEach((btn) => { + btn.addEventListener('click', () => { + if (busy) return; + const ci = Number(btn.dataset.i); + const opt = btn.dataset.opt || ''; + if (!opt || !Number.isFinite(ci) || !convo[ci]) return; + if (convo[ci].role === 'ai') convo[ci].opts = []; // 点过的候选即时收起 + renderDialog(); + sendTurn(opt); + }); + }); el.scrollTop = el.scrollHeight; }; const setBusy = (b) => { @@ -443,14 +458,20 @@ async function confirmAiScript(accountName, btn) { if (r.error) throw new Error(r.error); const answer = String(r.answer || '').trim(); const ri = answer.lastIndexOf('==REQ=='); - const reply = (ri >= 0 ? answer.slice(0, ri) : answer).trim(); + const head = (ri >= 0 ? answer.slice(0, ri) : answer).trim(); + // 09-07:==OPTS== 候选段(位于正文与 ==REQ== 之间,每行一个可点选短句)→ 渲染为 chips + const oi = head.lastIndexOf('==OPTS=='); + const reply = (oi >= 0 ? head.slice(0, oi) : head).trim(); + const opts = oi >= 0 + ? head.slice(oi + '==OPTS=='.length).split('\n').map((s) => s.trim()).filter((s) => s && !s.startsWith('==')).slice(0, 6) + : []; 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 }); + if (reply || opts.length) convo.push({ role: 'ai', text: reply || '(已记录本轮信息,继续补充或点选上方候选)', opts }); renderSheet(); renderDialog(); showToast(updated > 0 ? `✅ 需求已更新 ${updated} 项` : (ri >= 0 ? '✅ 已记录(本次无新变更)' : '⚠️ 本次未返回结构化需求,可换种说法再试')); diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css b/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css index b595910..8999a4e 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css @@ -266,6 +266,17 @@ main { height: calc(100vh - 54px); } } .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; } +/* 09-07:AI 追问候选 chips(==OPTS== 段渲染;置于 AI 气泡下方,点击即作为用户输入发送) */ +.req-msg.ai { flex-wrap: wrap; column-gap: 8px; } +.req-msg.ai .req-bubble + .req-opts { margin-top: 6px; } +.req-opts { display: flex; flex-wrap: wrap; gap: 6px; max-width: 92%; } +.req-opt-chip { + display: inline-flex; align-items: center; + background: #fff; border: 1px solid var(--border); border-radius: 999px; + padding: 4px 12px; font-size: 13px; color: var(--text); cursor: pointer; + line-height: 1.4; transition: border-color .15s, color .15s, background .15s; +} +.req-opt-chip:hover { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); } /* 底部功能条:输入框(圆角块内嵌 textarea + 底部工具栏:回形针 · ✓ 提交创作 · ↑ 发送) */ .req-bar { flex: none; display: flex; align-items: flex-end; gap: 10px; } .req-create { diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/server.js b/project/短视频脚本创作/V1.0/mcn-work-shop/server.js index bea7172..6b9778f 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/server.js +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/server.js @@ -882,13 +882,19 @@ const server = http.createServer((req, res) => { + '【达人本轮输入】\n' + turn.trim() + '\n\n' + '【行为要求】\n' + '1. 理解本轮输入:可能是新想法、对上一问的回答、或想修改某一维度。将信息吸收进需求清单,可改写得更专业准确,但保留用户原意与账号结构性符号。\n' - + '2. 若关键维度仍有缺口且本轮输入不足以补齐:最多追问 1~2 个问题(优先给 A/B/C 选项供挑选),不要一次抛出全部问题,也不要重复已明确的维度。\n' - + '3. 若信息已基本齐备:用一两句话总结并提示用户可点击「提交创作」。\n' - + '4. 输出必须严格分两段:第一段是对用户说的话(120 字以内,含必要时的问题);\n' - + '然后另起一行单独输出 ==REQ==;\n' - + '接着输出 5 行需求清单(仍未确定的项留空值,严禁编造;情节/结尾/时长不由本清单承载,由创作阶段按人设与选题自决;共鸣/共情亦不收集,创作时由 AI 依痛点 / 爽点自然发挥),模板:\n' + + '2. 若关键维度仍有缺口且本轮输入不足以补齐:最多追问 1~2 个问题,不要一次抛出全部问题,也不要重复已明确的维度。追问须给出可点选的候选,供达人一键选用。\n' + + '3. 候选来源受控(只能出自以下词表,严禁编造清单外选项):\n' + + ' - 叙事结构未定 → 候选:钩子前置 / 悬念反转 / 递进铺垫 / 先抑后扬\n' + + ' - 内容形式(叙事形态)未定 → 候选:剧情(场次叙事,多角色对白)/ Vlog 纪实·人物采访(第一视角)/ 口播(干货·教程·盘点)/ 视觉短片(画面+音乐驱动,测评种草或纯氛围,台词极少)\n' + + ' - 达人倾向剧情演绎(含多角色/冲突反转的意向)但人物与关系未明 → 候选追问主要人物身份 / 人物间关系(如:父子 / 夫妻 / 同事 / 陌生冲突),一次只问一个层面\n' + + '4. 若信息已基本齐备:用一两句话总结并提示用户可点击「提交创作」,无需输出候选。\n' + + '5. 输出三段结构,顺序固定:\n' + + ' 第一段:对用户说的话(120 字以内,含必要时的问题);\n' + + ' 若本轮提供了候选:另起一行单独输出 ==OPTS==,每行一个候选、直接是达人可原样回复的短句(≤30 字、不含编号前缀);若未提供候选则不输出该段;\n' + + ' 最后另起一行单独输出 ==REQ==,接着输出 5 行需求清单(仍未确定的项留空值,严禁编造;情节/结尾/时长不由本清单承载,由创作阶段按人设与选题自决;共鸣/共情亦不收集,创作时由 AI 依痛点 / 爽点自然发挥),模板:\n' + '- 一句话创意:\n- 创新 / 爆点:\n- 痛点 / 爽点:\n- 叙事结构:\n- 其他约束:\n' - + '5. 需求框只收集本条视频的选题相关信息;账号级设定(情绪基调 / 目标受众 / 结构性符号 / 广告植入等)已由账号设定自动带入创作,不要向达人重复询问这些维度——仅当达人主动提出与账号设定不同的要求时,将其记入「其他约束」。'; + + ' 达人点击候选后,把该选择吸收进对应字段(如内容形式选「视觉短片」→ 写入叙事结构行并注明画面驱动;人物选择 → 写入一句话创意行的事件或人物信息)。\n' + + '6. 需求框只收集本条视频的选题相关信息;账号级设定(情绪基调 / 目标受众 / 结构性符号 / 广告植入等)已由账号设定自动带入创作,不要向达人重复询问这些维度——仅当达人主动提出与账号设定不同的要求时,将其记入「其他约束」。'; // 4) Dify blocking 调用 const difyUrl = String(process.env.DIFY_URL || 'https://mydify.youmanvideo.com/v1').replace(/\/+$/, ''); const ctrl = new AbortController();