From 1390268585dd54c588d5e4d33589dbf0e194223e Mon Sep 17 00:00:00 2001 From: maogeigei Date: Wed, 2 Sep 2026 16:00:46 +0800 Subject: [PATCH] =?UTF-8?q?AI=E5=88=9B=E4=BD=9C=E5=BC=B9=E7=AA=97=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=89=93=E7=A3=A8=20v2=EF=BC=9A=E5=8D=95=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E9=9C=80=E6=B1=82=E6=A1=86+=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E6=A1=86+AI=E5=85=A8=E7=A8=8B=E5=BC=95=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 废弃旧分步向导+跨会话打磨+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}✓。 --- .workbuddy/memory/2026-09-01.md | 8 + .workbuddy/memory/2026-09-02.md | 13 ++ .workbuddy/memory/MEMORY.md | 8 + .../V1.0/mcn-work-shop/public/data-pages.js | 193 ++++++++++++++---- .../V1.0/mcn-work-shop/public/style.css | 49 +++-- .../V1.0/mcn-work-shop/server.js | 77 +++++++ 6 files changed, 291 insertions(+), 57 deletions(-) diff --git a/.workbuddy/memory/2026-09-01.md b/.workbuddy/memory/2026-09-01.md index 96336e1..0b6dc08 100644 --- a/.workbuddy/memory/2026-09-01.md +++ b/.workbuddy/memory/2026-09-01.md @@ -330,3 +330,11 @@ - 失误:验证视频详情页对齐时误用 agent-browser(system 技能列表里可见),未遵守项目铁律「浏览器操作一律 browser-harness」(user 级技能,Always use browser-harness for any web interaction) - 检查:browser-harness 环境未安装——技能目录只有 SKILL.md,无 subskills/browser-harness/envs/ 环境(可执行文件 browser-harness.exe 不存在) - 规避:涉及浏览器自动化/截图/交互时,先 where browser-harness 或查 C:/Users/maidou/.workbuddy/skills/browser-harness/subskills/browser-harness/envs/browser-harness/Scripts/browser-harness.exe 是否存在;未装则 uv tool install --python 3.12 --upgrade --force browser-harness + doctor 验三要素,不装 agent-browser + +## AI创作 向导(C+A)落地后冒烟验证(16:23) +- 目标:账号列表 AI创作按钮 → 弹向导分 5 步收集创意/选题/设定/叙事框架;A=弹窗正向确认(chips+手动) C=打磨用 /api/run 建空技能会话产《创作需求单.md》→ 人工「载入需求单」一键回填再提交创作(人工点击确认并提交创作) +- 落地位置:data-pages.js confirmAiScript (L267-418) + 辅助 buildClarifyPrompt/parseReqSheet/loadReqSheet/reqLines/buildCreatePrompt (L199-263) + 常量 CREATE_CHIP_GROUPS/CREATE_STEPS/REQ_SHEET_FILE/REQ_SHEET_TEMPLATE/REQ_LABEL_TO_KEY (L163-197) + style.css .wizard-modal 与 .wz-* 样式 (L226-251)——本轮代码已存在于工作区,未做修改 +- 验证方法:启动 Chrome headless CDP 9224(user-data-dir 独立;CDP 9223 在用户内置浏览器实例,本次用 9224 独立验证),Node + WebSocket 写 tmp/cdp-wizard-smoke.mjs 模拟用户点击 5 步 + 注入 window.executeTask 拦截真实任务 +- 结果(report.tmp/wizard-report.json):① step1 5 元素全在 (idea/gen/load/clarify/create) maskN=1 ② 空想法直接创作 skills=['短视频脚本创作'] ③ 5 步填写后 step5 dotActive=5+9 行摘要+skills=['短视频脚本创作']+hasReqSec=true ④ 打磨 skills=[]+hasSheetPath+forbidCreate+hasAccount ⑤ 载入(无文件) toast「未找到《创作需求单.md》」+maskN=1+stillAt1=true ⑥ chips/自由输入互斥 onAfterChip=true→onAfterFree=0+valAfterFree=「微短剧改编」 +- 教训:① 子 shell `cmd &` 启的 Chrome 会被沙箱回收(与 node server.js 同事发现),必须用 run_in_background=true 启任务;② first 版测试未在每节清 modal-mask 导致 wz-5-confirm.png 出现双 modal 重影,强化版每节前 removeAll(.modal-mask) 后 maskN=1 一致;③ user 提出的「人工点击确认并提交创作」铁律体现在:clarify 路径不自动接续 create,强制走 载入需求单 → 人工 review → 直接创作 +- 截图:tmp/wz-A1-idea.png(步骤1)/ tmp/wz-A2-form.png(步骤2 chips)/ tmp/wz-A3-mood.png / tmp/wz-A4-plot.png / tmp/wz-A5-confirm.png(步骤5 摘要9行)/ tmp/wz-A6-load-empty.png(载入无文件 toast) diff --git a/.workbuddy/memory/2026-09-02.md b/.workbuddy/memory/2026-09-02.md index caa18a1..e5da75a 100644 --- a/.workbuddy/memory/2026-09-02.md +++ b/.workbuddy/memory/2026-09-02.md @@ -178,3 +178,16 @@ - 数据源:`/api/dsh/video-script?videoId=` 的 rewrites(同表 rewrite_log.id,与列表 items.id 同体系)+ items 行聚合 review_ai/compare(find 不到时兜底 raw curRow → 显示空态) - CSS 补 `.diag-empty`(style.css,dim 灰 + padding) - 语法 node --check 通过 ×2;服务未重启(纯静态,no-store);git commit afc45b7,与之前 6 个 commit 一起 push(d0cbd41..afc45b7 成功) + +## AI创作弹窗对话打磨 v2 定稿(15:50-16:00) +- **用户反馈**:「跟 AI 打磨需求 和 载入需求单 交互很奇怪,而且不连贯」→ 要求改为「弹窗内 需求框 + 输入框 对话打磨」「AI 按钮去掉,全过程都用 AI」——彻底废弃旧分步向导 + 跨会话打磨 + 需求单 md 载入路径 +- **方案**:单弹窗 v2(`confirmAiScript` 完全重写,data-pages.js:199-326)= 顶部 10 行「创作需求」框(`REQ_FIELDS` 空值灰显"待定·由 AI 按人设自决")+ 中部对话流(u/ai 气泡)+ 底部输入行(Enter 发,Shift+Enter 换行)+ 取消/提交创作;**无 step/chip/AI 按钮/载入按钮/上下步** +- **后端新增** `POST /api/ai/clarify`(server.js,紧跟 `/api/ai/topics` 之后)= 同步阻塞 Dify chat-messages(100s AbortController),request `{ accountName, turn, reqMd, history }`,response `{ answer }`;query 拼接=账号人设摘要+当前需求 md+最近 2 轮历史+用户本轮输入,强制输出"对话回复 + ==REQ== + 10 行需求" +- **关键设计**:每轮重发「当前完整需求 + 最近 2 轮历史」做无状态上下文;AI 可更新任意字段、用户可改写 AI 输出;AI **只澄清需求严禁生成脚本正文**;最终 prompt `reqLines` 仅取非空字段,未填字段加 "由 AI 按人设自决" 提示 +- **冒烟脚本 `tmp/cdp-req-dialog-smoke.mjs`**(CDP 9224,4 步端到端)3 轮迭代定稿——首两轮 2 个脚本 bug: + 1. ④ 提交点击 `querySelector('[data-create]')` 命中背后列表按钮(弹窗内外按钮同名 data-create)→ 改为 `.req-modal [data-create]` 限定作用域;同时 `title` 改为 `.req-modal .modal-title-row span`(原全局 querySelector 命中其他 modal 模板) + 2. `Page.navigate` 同 URL 不真正 reload(旧弹窗残留叠加新弹窗,rows 30=3 个弹窗叠加)→ 改用 `?v=Date.now()` 强制 reload + `waitFor('!document.querySelector(".req-modal")')` 等清场 +- **最终全绿**(53s):①结构 rows:10 / title="AI创作 · 旧梦留声机" ②第一轮 busy=true,AI 回复贴合人设("账号主打【隔辈亲情与温情催泪】...A. 角色换为笨拙爷"),filled 0→6 ③第二轮 filled 6→8 ④提交 label="创作指令"/skills=["短视频脚本创作"]/hasPersona=true(人设卡注入 prompt)/reqRows=8(8 条需求进 prompt)/modalClosed=true +- **截图产出** `tmp/req-1-empty.png` / `req-2-after-turn1.png` / `req-3-after-turn2.png` + 报告 `tmp/req-dialog-report.json` +- **工作台页面** 按"改完必重开"约定 present_files `?v=20260902-req#/accounts` +- **经验沉淀**:①账号列表/弹窗内同名 `data-create` 触发选择器歧义——统一加 modal 作用域或换名(如 `data-confirm-create`)②CDP Page.navigate 同 URL 不可靠——浏览器/CDP 跨会话残留必须强制 reload + 显式等清空;冒烟脚本应假设首状态脏 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index f3b8227..5146ca4 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -11,6 +11,14 @@ - **抖音账号分析 mcn-dou-analysis(主版本 08-27)**:源=`project/短视频脚本创作/V1.0/subskills/mcn-dou-analysis/`(**09-02 主技能 subskill 目录更名 subskills**);dsh 副本=`C:\Users\maidou\.dsh\skills\mcn-dou-analysis/`。**六大功能(08-31 加 F6)**:F1账号信息/F2视频列表(13列Excel)/F3提炼账号设定(人设卡12段)/F4分析视频数据/F5解析视频(单视频MCP)/F6生成账号设定卡片(SVG 长图 680×~1716,模板=旧梦留声机样例)。红线:达人账号表15列、TOP6筛选(点赞+分享降序,SKILL.md 权威)、≥60条停止、29赛道枚举、源技能只读单向同步、技能边界(脚本必须切脚本创作技能)、浏览器9223调试实例+标签页复用。**账号数据来源红线(08-31)**:账号表任何字段(含身份标识 sec_uid/抖音号/地址)禁止从数据库回填——工作台库 `hot_accounts` 表=MCP 内部账号库本地副本,读其数据=违反红线;登记只占位 账号ID+昵称,身份标识留空待浏览器回填;MCP 调用严格限定 TC-05。**F6 卡片配色两系(08-31)**:深色系(深沉浓郁调性,如旧梦留声机)=深棕红渐变 #2B0F0A→#1A0805+浅米卡 #FAEEDA+珊瑚红强调;浅色系(明快甜美治愈,如王微斯·玫瑰金定稿)=玫瑰金 #D6A3A9→#9A5563+白卡+粉边 #E8C9DC+深粉字 #7B2D5E+粉强调 #C0457F→#E876A9;背景文字随背景深浅提亮。**调性优先于赛道标签**:明快/欢乐/爽文/暖心→浅色系,深沉/催泪/怀旧/悲情→深色系;「治愈」双义必须回读情绪锚点(俊希=明快欢乐治愈爽文→浅色系暖橙 #F5D0A4→#DE9A5E,非深焦糖)。模块名统一「叙事结构」「内容标签」「内容基因·TOPn(n=实际解析条数,非固定6)」。**F3→F6 自动联动(08-31)**:功能三产出设定后自动生成卡片(删 06 文件即禁用);设定 md↔卡片 SVG 双向联动维护(改一边另一边同步、术语改名两边一起改,交付前 grep 校验)。**MCP 检测触发时机(08-31)**:账号分析保持「执行工作流前检测」原状;创作技能 V1.0=首次触发必做动作(会话首次交互先查 `myai-mcp-production`,未装轻量提醒可跳过)。**browser-harness 依赖(08-31)**:功能一/二执行前必检(Win `where`/mac `command -v` 或可执行文件存在性),未装提醒+`uv tool install --python 3.12 --upgrade --force browser-harness`+doctor 验三要素。**★产物入库机制(09-01)**:技能产出只落盘文件不入库(dsh 插件导入机制开发机未接通=俊希全0根因)→ 工作台 `POST /api/import/account` 直写 `mcn-plugin.db`(`视频分析/*/analysis.json`→`account_video_analysis`、`content.json`→`account_video_source`、`{昵称}账号设定.md`→`account_persona`、`{昵称}账号数据分析.md`→`account_analysis`、`短视频表格.xlsx`→`account_videos` upsert;xlsx 权威映射=`scripts/export_video_map.py`);**aweme_id 匹配红线**:标题规范化必删全角标点(xlsx 带「!」「”」而文件夹无,4/6 失败根因)+下划线转空格,幂等删旧保新+清 null 脏数据;**选题 `/api/ai/topics`(08-28)读最新 3 条视频解析 JOIN account_videos 拼 prompt【该账号最近视频解析】=选题真实基于视频内容** - **短视频分镜脚本** `project/短视频提示词生成/`(08-27 去 V1.0 层):4功能 F1反推图片/F2反推视频/F3生成图片/F4生成分镜;三层引用架构 references→references-add/(运行时优先;开发机不启用);知识库15维度;F4输出=主题+导演阐述+代码块(>5s时间戳分段/≤5s单段);模板分档 A剧情/B单人Vlog/C旁白知识/D产品展示/E纯氛围 +4层×5类型适配表;seedance25 时间测算体系;dsh 副本无独有内容需回流 +## AI创作弹窗对话打磨 v2(09-02 定稿,废弃旧分步向导+跨会话打磨+md载入) +- **路径**:账号列表 → AI创作 → 单弹窗(`confirmAiScript` 完全重写 data-pages.js:199-326) +- **后端新增** `POST /api/ai/clarify`(server.js 紧跟 /api/ai/topics 之后):同步阻塞 Dify chat-messages,100s AbortController;入参 `{ accountName, turn, reqMd, history }`,返回 `{ answer }`;query 拼装=账号人设摘要+当前需求 md+最近 2 轮 history+本轮输入;强制输出「对话+==REQ==+10 行需求」模板 +- **前端**:REQ_FIELDS 10 项 (idea/form/dur/mood/aud/plot/end/sym/ad/note);顶部 10 行需求框常显(空值灰显"待定·由 AI 按人设自决")+ 中部对话流(u/ai 气泡)+ 底部输入行(Enter 发,Shift+Enter 换行)+ 取消/提交创作;**无 step/chip/AI 按钮/载入按钮/上下步** +- **关键设计**:每轮重发 reqMd + 最近 2 轮 history 做无状态上下文;AI 任何字段都允许更新、严禁生成脚本正文;reqLines 仅取非空字段 + 「未填由 AI 按人设自决」提示 +- **冒烟 CDP 坑**(3 轮迭代定稿):①querySelector('[data-create]') 命中背后列表按钮(弹窗内外同名 data-create)→ 必须 `.req-modal [data-create]` 限定;②Page.navigate 同 URL 不真正 reload 致旧弹窗叠加 → `?v=Date.now()` 强制 reload + `waitFor('!document.querySelector(".req-modal")')` 等清场 +- **冒烟脚本** `tmp/cdp-req-dialog-smoke.mjs`(4 步端到端 CDP 9224)= 结构/双轮真实 Dify 打磨/需求框递增/提交捕获(拦截 executeTask 验 label+skills+persona+reqRows+modalClosed),端到端 ~50s 全绿 + ## 创作技能关键规则(权威源:SKILL.md + references/创作流程规范.md) 1. 三态创作:⚡自动(默认)=静默执行+只落最终交付物;🤝共创=+每确认门询问;🛠️开发=+每步全落盘。7确认门不可合并;角色边界(分析不做创作) 2. 内容总监兜底所有质量检查点;文件 `scripts/{视频标题}/XX_阶段名.md` 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 1149305..803fead 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js @@ -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 = ` - `; + mask.innerHTML = ``; 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 `
${label}${v ? esc(v) : '(待定 · 由 AI 按人设自决)'}
`; + }).join(''); + q('#reqSheet').innerHTML = rows; + }; + /* —— 对话流渲染 —— */ + const renderDialog = () => { + const el = q('#reqDialog'); + el.innerHTML = convo.length + ? convo.map((c) => (c.role === 'u' + ? `
${esc(c.text)}
` + : `
${esc(c.text)}
`)).join('') + : `
在这里输入一个初步想法,AI 会引导你补齐形态 / 时长 / 情绪 / 受众 / 情节 / 结尾 / 植入等需求;每条输入都会即时更新上方的创作需求框。
`; + 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(); } /* ================= 周榜 ================= */ diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css b/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css index 237c417..47bd0b5 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css @@ -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; diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/server.js b/project/短视频脚本创作/V1.0/mcn-work-shop/server.js index e84ca45..4749107 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/server.js +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/server.js @@ -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 = '';