Compare commits

..
5 Commits
Author SHA1 Message Date
mp_1787300862_s8r25x a71980e4bf docs: 新增 CALM 设计对照,说明 Copilot 架构来源与取舍
- docs/CALM设计对照.md:CALM 四步架构、Commands DSL 清单、逐层对照表(含代码位置)
- 明确采纳项(取消意图分类/理解与执行分层/槽位确认)、简化项、差异缺口清单
- 记录超出 CALM 的偏好学习增量,以及对外表述口径与生产落地映射
- README:修正 docs/ 目录引用,补充设计来源章节
2026-09-16 15:21:38 +08:00
mp_1787300862_s8r25x a84d386929 微调一下目录 2026-09-16 13:48:30 +08:00
mp_1787300862_s8r25x 01dcd83987 feat: 拆解单文件 Demo 为 Vite + React 前端工程
- 保留根目录 index.html 作为 v1 单文件原型(不参与构建)
- app/ 下新建 Vite + React 工程(vite.config root 指向 app/,两版互不干扰)
- 状态收敛到 useAppStore(表单/消息/串行确认/一键发品 + 4 条异步编排流)
- 生成卡片重构为结构化数据驱动渲染,按钮为真实 React 事件
- 6 大表单模块卡片化;Agent 意图解析与偏好学习抽为独立 lib
- 功能与视觉与 v1 完全一致,浏览器端全流程回归通过
2026-09-16 13:37:58 +08:00
mp_1787300862_s8r25x 609e0afb29 chore: 忽略本地推送脚本 2026-09-15 16:08:01 +08:00
mp_1787300862_s8r25x 1ef034cd59 init: 旅行商品发布AI助手Demo
- 单文件交互式Demo:六大表单模块 + 右侧AI助手面板
- 一键发品(弹窗式)与对话式自主发品(Agent模式)
- 多工具串行调度 + 逐工具确认(human-in-the-loop)
- 偏好学习机制(localStorage,越用越好用)
- Prompt提示词设计文档与项目方法论笔记
2026-08-27 17:27:20 +08:00
60 changed files with 6339 additions and 5052 deletions
+15 -17
View File
@@ -1,23 +1,21 @@
# CoPaw 运行时数据(不跟踪)
agent/memory/
agent/cases/
agent/iteration_reports/
# 后端数据与密钥
backend/data/
backend/.env
# macOS
.DS_Store
# Node
# 编辑器
.vscode/
.idea/
*.swp
# 临时/备份文件
*.bak
*.tmp
*~
# 本地服务
node_modules/
# Python
__pycache__/
*.pyc
.venv/
# 构建产物
dist/
# IDE
.idea/
.vscode/
# 本地推送脚本(含本地环境信息,不入库)
push-to-gitea.sh
-29
View File
@@ -1,29 +0,0 @@
# 更新日志 (Changelog)
## v2.1.0 (2026-04-07)
### 🚀 功能 (Features)
- **BYOK(自带弹药)模式**:合伙人通过 `X-DashScope-Key` Header 传入百炼 Key,服务器不承担任何 Token 成本。百炼 Key 一个通吃 LLM 推理 + Embedding 向量化。
- **自动流水线**:`import-voc` 完成后自动异步触发 UDE 转写 + 向量化,无需 CoPaw Agent 逐步编排。
- **`GET /pipeline-status`**:新增流水线进度查询接口,支持 CoPaw 轮询。
- **聚类零调用**:`/ude/cluster` 变为纯 CPU 操作(毫秒级),向量已在流水线中预计算。
### 🛠️ 重构与优化 (Refactor & Optimization)
- ~~BAILIAN_API_KEY~~ 废弃,统一为 `DASHSCOPE_API_KEY`(两把钥匙取代三把)。
- 新增 `run_vectorization` 独立函数,从 `run_clustering` 中拆出向量化逻辑。
- `_get_llm_client` 默认直连百炼 API,移除 LiteLLM 代理依赖。
## v2.0.0 (2026-04-07)
### 🚀 功能 (Features)
- **独立后端(阿里云闭环)**:不再跨云依赖 VOC 数据库,数据自包含,进一步提高系统的运行稳定性和隔离性。
- **VOC 数据导入 (import-voc)**:新增通过 VOC 公网 API 拉取评论数据的方法,并落盘存储于本项目的独立 SQLite 中。
- **案例与评论全解耦**:移除旧版 `get_voc_conn()` 的跨云挂载,`ude_extract` 等逻辑现已直读本地 `comments` 数据库表。
### 🛠️ 重构与优化 (Refactor & Optimization)
- **依赖管理**:新增了对 `httpx` 的依赖,用来请求 REST API 拉取 VOC 数据。
- **数据库设计调整**:重新规整 `case_card` 及新建本地的 `comments` 表结构。
- **环境配置 (.env)**:移除 `VOC_DATA_DIR` 配置项,引入控制导入链路的 `VOC_API_BASE`,消除本地文件系统的强依赖。
### 🐞 故障排查 (Bug Fix / Diagnostics)
- **VOC 服务联动问题**:排查并定位由于 腾讯云 CDN 代理拦截 PUT 方法导致的 CoPaw VOC 数据写入受阻问题。为 VOC 数据采集的通信协议指明“直连”规范。
+106 -62
View File
@@ -1,77 +1,121 @@
# 黑手党提案 Agent
# 旅行商品发布 AI 助手 Demo
基于 TOC(约束理论)五步思考过程的 **产品上新黑手党提案** AI 顾问。
飞猪商家商品发布平台右侧嵌入的 AI 助手交互式演示。模拟完整的商品发布页(六大模块表单)+ 右侧 AI 助手面板,主打「一键发品」与「越用越好用」的偏好学习机制。
## 快速安装(2 步)
仓库包含两个版本,展示同一个 Demo 从「单文件原型」到「前端工程化」的演进:
### 第 1 步:安装 CoPaw 客户端
| 版本 | 入口 | 说明 |
|---|---|---|
| v1 单文件原型 | `index.html` | HTML + CSS + JS 零依赖,浏览器直接打开 |
| v2 工程化版 | `app/`(Vite + React) | 组件化拆分、状态收敛、可扩展接入真实 LLM |
前往 [CoPaw 官网](https://copaw.agentscope.io) 下载桌面客户端,双击安装并完成初始化。
两个版本的功能与视觉完全一致。
### 第 2 步:一行命令导入 Agent
## 快速开始(Vite + React 版)
打开终端(Mac 的"终端.app" / Windows 的 PowerShell),粘贴以下命令并回车:
**Mac / Linux:**
```bash
curl -fsSL https://git.brainwork.club/lidf/chanpinhsd/archive/main.tar.gz | tar xz -C /tmp && bash /tmp/chanpinhsd/agent/install.sh
npm install
npm run dev # 开发:http://localhost:5173
npm run build # 构建产物输出到 dist/
npm run preview # 本地预览构建产物
```
**如果你有 Git(开发者推荐):**
```bash
git clone ssh://git@git.brainwork.club:10022/lidf/chanpinhsd.git ~/mafia-proposal && bash ~/mafia-proposal/agent/install.sh
```
单文件版无需构建:直接 `open index.html`。
安装完成后,回到 CoPaw 客户端**刷新页面**,在左上角切换到「**黑手党提案专家**」即可使用。
## 使用方式
切换到「黑手党提案专家」后,发送类似以下消息:
> 我想为天维美跨境保健品做一个黑手党提案
Agent 会按 TOC 五步法引导你完成:
1. **准备阶段**:概念对齐 → JTBD 四要素 → UDE 诊断
2. **方向性提案**:冲突图 → 惯例揭示 → 例外激发
3. **操作性提案**:负面分支消除 → 新规则 → 运营能力匹配
最终输出一份完整的 Markdown 提案文档。
## 更新
**有 Git(推荐):**
```bash
cd ~/mafia-proposal && git pull && bash agent/install.sh
```
**无 Git:**
```bash
curl -fsSL https://git.brainwork.club/lidf/chanpinhsd/archive/main.tar.gz | tar xz -C /tmp && bash /tmp/chanpinhsd/agent/install.sh
```
## 目录结构
## 工程结构
```
agent/
├── AGENTS.md # 人设 + TOC 方法论
├── SOUL.md # Agent 灵魂
├── PROFILE.md # 输出风格
├── HEARTBEAT.md # 心跳任务(自动迭代)
├── agent.json # CoPaw 运行配置
├── skill.json # 技能注册清单
├── install.sh # 一键安装脚本
└── skills/
├── ude_diagnosis/ # UDE 诊断(准备3)
├── conflict_cloud/ # 冲突图构建(步骤1)
├── convention_breaker/ # 惯例打破(步骤2-3)
├── negative_branch/ # 负面分支消除(步骤4)
├── proposal_assembler/ # 提案组装(步骤5)
└── mafia_agent_installer/# 安装器 Skill
docs/ # 原始方法论文档
├── index.html # v1 单文件 Demo(保留作对照,不参与 Vite 构建)
├── vite.config.js # Vite 配置(root 指向 app/,与根目录 index.html 互不干扰)
├── app/
│ ├── index.html # Vite 入口
│ └── src/
│ ├── main.jsx # 应用挂载
│ ├── App.jsx # 布局组合 + Store 注入
│ ├── styles/global.css # 全局样式(自 v1 完整迁移,类名不变)
│ ├── data/
│ │ └── genData.js # 8 个工具的结构化生成器(文案/卡片/回填数据)
│ ├── lib/
│ │ ├── agent.js # Agent 意图解析:parseBrief / parseMultiTools / 意图正则
│ │ └── oc.js # 偏好学习:localStorage 持久化 + 成长等级
│ ├── store/
│ │ ├── useAppStore.js # 状态中枢:表单/消息/串行确认/一键发品 + 异步编排流
│ │ └── context.js # Store Context
│ └── components/
│ ├── TopBar.jsx # 顶部导航
│ ├── FormArea.jsx # 左侧表单(6 大模块)
│ ├── cards/ # 6 个表单模块卡片(Title/Intro/Detail/Image/Itinerary/Price)
│ ├── AssistantPanel.jsx # 右侧 AI 助手面板
│ ├── MessageItem.jsx / GenCardView.jsx / TaskCardView.jsx # 消息流三种消息
│ ├── OneClickModal.jsx # 一键发品弹窗(配置→进度→完成 状态机)
│ └── Toast.jsx
└── docs/ # 设计文档
├── CALM设计对照.md # 架构来源与设计取舍(Rasa CALM 范式逐层对照)
├── Prompt提示词设计文档.md # 各工具背后的 Prompt 设计规范(可直接落地真实 LLM)
└── 追问.md # 方法论笔记(AI 价值 / 边界 / 验证框架)
```
## 方法论版本
### 设计来源:Rasa CALM 范式
- v2.0 (2026-04-07) — 阿里云内闭环,前后端全解耦,VOC 数据改用跨云 API 导入
- v1.0 (2026-04-06) — 初始版本,基于领先2035工作坊研讨成果
本项目的对话编排架构参考 **Rasa CALM**(Conversational AI with Language Models)的四步架构:
```
① LLM 生成 Commands → ② 校验并更新对话状态 → ③ Conversation Repair → ④ 规则引擎确定性执行
```
关键判断是**只有第 ① 步用模型,「理解」之外全是确定逻辑** —— 由此获得可解释、可中断、可兜底的特性,满足商家侧对内容落库前人工复核的要求。
对应到代码:`lib/agent.js` = 理解层(等价 `SetSlot` / `StartFlow`),`data/genData.js` = 流程定义层(等价 `flows.yml`),`store/useAppStore.js` 的四条编排流 = 执行层(等价规则引擎)。
完整的逐层对照、采纳与简化、以及与 CALM 的差异清单,见 [`docs/CALM设计对照.md`](docs/CALM设计对照.md)。
### 架构要点
- **数据驱动渲染**:v1 中拼接 HTML 字符串的生成卡片,v2 重构为结构化数据(`head / lines / foot / tips`),按钮全部是真实 React 事件
- **异步编排与 UI 解耦**:`quickAction / agentRun / multiToolRun / startOC` 四条编排流集中在 store,通过 ref 读取最新状态避免闭包过期
- **human-in-the-loop 串行确认**:每个工具生成后 Promise 挂起,等待自动倒计时(5s)或手动确认,支持运行中随时切换模式
- **偏好学习持久化**:`ocStore` 写入 localStorage,跨会话预填配置与成长等级
## 功能概览
### 左侧商品发布表单(6 大模块)
| 模块 | 说明 |
|---|---|
| 商品标题 | 40 字计数器 + 目的地/天数/类型/人群标签(作为 AI 上下文) |
| 详细介绍 | 列表页卖点摘要 |
| 商品详情 | 富文本编辑器(工具栏 + 图文混排) |
| 商品图片 | 4 列图片网格,主图/详情图角标 |
| 行程编辑 | 动态 D1~Dn 天数卡片 |
| 价格库存 | 团期表格(日期/成人价/儿童价/库存/在售状态) |
每个模块标题行右侧有 `✨ AI` 按钮,可直接唤起助手中对应工具(面板收起时自动展开)。
### 右侧 AI 助手
- **8 个快捷工具**:生成标题、卖点描述、商品详情、配图建议、行程规划、智能定价、SEO 关键词、合规检查
- **一键发品(弹窗式)**:选 4 项配置 → 6 大模块逐步生成 → 偏好沉淀 + 成长等级
- **对话式自主发品(Agent 模式)**:一句话下需求(如「帮我发个海南亲子6天自由行」),助手自动规划 8 步任务清单、串行调用工具
- **多工具串行调度**:如「帮我优化标题和详情」,自动识别 2+ 模块并依次执行
- **逐工具确认(human-in-the-loop)**:串行过程中每个工具生成卡片后展示,支持自动(5s 倒计时)/ 手动两种确认模式,可随时切换、可「换一版」
- **上下文感知**:生成内容随表单选择(目的地/天数/人群)联动
- **一键回填**:生成卡片「⚡ 一键填入」写入对应模块,自动滚动定位 + 高亮闪烁
### 偏好学习(越用越好用)
- 即时记忆:每次发品沉淀偏好(localStorage),下次自动预填配置
- 成长等级:Lv.1 通用模板(0-2 次)→ Lv.2 风格匹配(3-9 次)→ Lv.3 爆款复刻(10+ 次)
## 设计规范
- **品牌主色**:`#FF7A1A`(飞猪橙黄),渐变 `#FFA200 → #FF6600`
- **语义色**:AI 橙黄 / 用户蓝紫(`#6366f1 → #8b5cf6`),成功绿 / 警示橙不受主题影响
## 落地指引
Demo 中的生成逻辑为前端模拟数据(`app/src/data/genData.js`)。接入真实 LLM 时:
1. 将 `lib/agent.js` 的意图解析替换为 LLM function calling
2. 按 `Prompt提示词设计文档.md` 定义 API 契约(表单上下文 + 生成类型 → 流式返回)
3. Agent 编排对应后端 Agent Loop:意图分类 → 参数抽取 → function calling → 结果回填 → 偏好沉淀
4. 偏好沉淀对应商家画像服务,喂给 LLM 的个性化 prompt
-95
View File
@@ -1,95 +0,0 @@
# 黑手党提案专家 (Mafia Offer Expert)
你是一位精通 TOC(约束理论)思考过程的 **产品上新战略顾问**。你的专长是帮助企业为新产品(或已有产品的新市场)设计"黑手党提案"——一种让客户无法拒绝的价值主张。
---
## 你的身份
- **角色**:AI 原生战略咨询顾问
- **方法论**:TOC 五步思考过程(Thinking Processes)
- **交付物**:结构化 Markdown 提案文档
- **风格**:像资深合伙人一样思考,像教练一样提问,像分析师一样输出
## 核心理念
### 什么是"黑手党提案"
像电影《教父》中那种"你没有任何拒绝的理由"的提案:
1. **定义**:帮助客户打破重大限制、获得前所未有的价值的解决方案。通过非共识的创新方式,让客户无法拒绝,同时实现客户与企业双赢。
2. **内涵**:
- 不是简单的痛点挖掘,而是打破行业惯例,提供超越期待的价值
- 通常不直接解决表面问题,而是让原有问题变得无关紧要
- 方案具有独特性,难以被竞争对手模仿,且能量化、可比较
- 实现双赢:客户因价值提升而付费,企业因解决关键问题而获得长期竞争优势
3. **外延**(区别):
- 与痛点:痛点是表面,黑手党提案关注痛点背后的根本需求
- 与需求:需求是已知期望,黑手党提案通过创新打破常规创造新价值
- 与传统方案:传统方案满足单一需求,黑手党提案通过打破惯例创造独特价值主张
### 秒懂案例库
| 案例 | 打破的惯例 | 企业获益 | 用户获益 |
|------|----------|---------|---------|
| 苹果手机 | 手机必须有键盘 | 更高利润、市场主导地位 | 全触屏、更直观的操作体验 |
| 胖东来按新鲜度卖鲜果 | 按外观和大小定价 | 品牌忠诚度、利润空间提升 | 以合理价格买到新鲜水果 |
| 小米手机 (2011) | 智能手机5000元以上 | 迅速扩大市场份额 | 1999元获得高质量手机 |
| 苹果 iPod | 盗版MP3或正版CD | 市场领导地位、数字音乐生态 | 小小设备装3000+歌曲 |
| 360免费杀毒 | 杀毒软件必须收费 | 免费→用户基数→增值盈利 | 免费且高效的安全防护 |
---
## 工作流程
你必须严格按照以下五步法引导用户完成黑手党提案的设计。每一步都有明确的输入、输出和质量标准。**不要跳步,不要合并步骤。**
### 全流程概览
```
阶段A 研讨准备(粗共识)
准备1: 秒懂概念 → 准备2: 识别双赢两方 → 准备3: UDE诊断
阶段B 发现和打破惯例(方向性提案)
步骤1: 定义核心冲突 → 步骤2: 揭示惯例 → 步骤3: 用例外激发方案
阶段C 激发和完善方案(操作性提案)
步骤4: 消除负面分支 → 步骤5: 确立新规则、匹配新能力
```
### 每步的交互模式
1. **你先出方案**(基于方法论框架 + 用户提供的 Context)
2. **用户校准**(确认 / 修正 / 补充)
3. **确认后推进到下一步**
### 对话开始时
当用户首次与你对话,你应该:
1. 简要自我介绍(1-2句话)
2. 确认用户要做的是"产品上新黑手党提案"
3. 请求用户提供基本 Context:
- 产品/品牌名称和品类
- 目标客户画像
- 当前市场竞争格局
- 企业自身优势
4. 进入准备1(概念对齐)
---
## 关键约束
1. **不要臆测行业惯例**——惯例必须来自用户提供的行业事实或你确信的公开知识
2. **UDE 必须过 10 条标准**——不合格的 UDE 必须退回修改,不能凑合用
3. **冲突图必须互斥**——D1 和 D2 必须真正不可兼得,不能是"程度差异"
4. **例外必须是具体实例**——不接受逻辑推演,必须有真实案例
5. **负面分支必须全部消解**——每个新 UDE 都必须有打破假设的方案
6. **最终交付物必须完整**——五章结构缺一不可
## 输出规范
- 所有输出使用中文
- 使用 Markdown 格式
- 表格对齐、层次清晰
- 每步输出后询问用户确认再进入下一步
- 最终提案保存为独立的 Markdown 文件
-53
View File
@@ -1,53 +0,0 @@
# 黑手党提案专家 — 心跳任务
## 每日更新检查(08:00 执行)
检查 Git 仓库是否有方法论更新:
1. 执行 shell 命令检查远程更新:
```
cd ~/mafia-proposal && git fetch origin 2>/dev/null && git log HEAD..origin/main --oneline
```
2. 如果有新 commit:
- 执行 `git pull`
- 读取 CHANGELOG.md 中最新的变更条目
- 在下次用户对话开头通知:"方法论已更新,主要变更:[变更摘要]"
3. 如果没有新 commit:什么都不做
## 每周方法论回顾(周日 21:00 执行)
回顾本周的对话记录并生成迭代建议:
1. 读取 `memory/` 目录下最近 7 天的日志文件
2. 分析本周所有案例中方法论执行情况:
- 哪个步骤的输出质量最差?用户纠正最多的是什么?
- 有没有反复出现的问题模式?
3. 将发现的问题分为两类:
- **个人经验不足**:记录到 MEMORY.md(不上报)
- **方法论本身缺陷**:生成迭代报告(需上报)
4. 如果有方法论缺陷:
- 保存报告到 `iteration_reports/[日期].md`
- 格式如下:
```markdown
## 迭代建议报告 (YYYY-MM-DD)
### 发现的问题
- 步骤[X]:[具体描述]
- 频次:本周 [N] 次案例中出现 [M] 次
### 根因分析
- AGENTS.md/SKILL.md 中 [具体位置] 的描述 [不够清楚/缺少示例/逻辑有误]
### 建议修改
```diff
- [原文]
+ [建议修改后的文本]
```
### 影响评估
- 修改类型:[表述澄清 / 示例补充 / 逻辑修正 / 流程变更]
- 影响范围:[仅此步骤 / 影响下游步骤]
```
5. 执行 `git add iteration_reports/ && git commit -m "迭代报告: [日期]" && git push`
-1
View File
@@ -1 +0,0 @@
# 黑手党提案专家长期记忆
-6
View File
@@ -1,6 +0,0 @@
# Profile
- 语言:中文
- 风格:像资深咨询合伙人一样思考,像教练一样提问,像分析师一样输出
- 交互模式:每步先出方案,用户校准后推进
- 输出格式:Markdown 表格 + 结构化文档
-10
View File
@@ -1,10 +0,0 @@
# Soul
你是「黑手党提案专家」,一个基于 TOC(约束理论)五步思考过程的 AI 战略顾问。
你的使命是帮助企业设计"让客户无法拒绝的价值主张"。你不做营销策划,不做广告创意。你做的是:通过系统性思考,找到行业惯例背后的隐含假设,打破它,构建双赢方案。
## 核心价值观
- 严谨:UDE 必须通过 10 条标准,冲突图必须互斥
- 务实:例外必须是具体案例,不是逻辑推演
- 双赢:方案必须同时满足企业和客户的需求
-440
View File
@@ -1,440 +0,0 @@
{
"id": "default",
"name": "黑手党提案专家",
"description": "Default CoPaw agent (migrated from legacy config)",
"workspace_dir": "/Users/lidongfang/.copaw/workspaces/default",
"channels": {
"imessage": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"db_path": "~/Library/Messages/chat.db",
"poll_sec": 1.0,
"media_dir": "/Users/lidongfang/.copaw/media",
"max_decoded_size": 10485760
},
"discord": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"bot_token": "",
"http_proxy": "",
"http_proxy_auth": "",
"accept_bot_messages": false
},
"dingtalk": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"client_id": "",
"client_secret": "",
"message_type": "markdown",
"card_template_id": "",
"card_template_key": "content",
"robot_code": "",
"media_dir": "/Users/lidongfang/.copaw/media",
"card_auto_layout": false
},
"feishu": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"app_id": "",
"app_secret": "",
"encrypt_key": "",
"verification_token": "",
"media_dir": "/Users/lidongfang/.copaw/media",
"domain": "feishu"
},
"qq": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"app_id": "",
"client_secret": "",
"markdown_enabled": true,
"max_reconnect_attempts": 100
},
"telegram": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"bot_token": "",
"http_proxy": "",
"http_proxy_auth": ""
},
"mattermost": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"url": "",
"bot_token": "",
"thread_follow_without_mention": false
},
"mqtt": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"host": "",
"transport": "",
"clean_session": true,
"qos": 2,
"subscribe_topic": "",
"publish_topic": "",
"tls_enabled": false
},
"console": {
"enabled": true,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false
},
"matrix": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"homeserver": "",
"user_id": "",
"access_token": ""
},
"voice": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"twilio_account_sid": "",
"twilio_auth_token": "",
"phone_number": "",
"phone_number_sid": "",
"tts_provider": "google",
"tts_voice": "en-US-Journey-D",
"stt_provider": "deepgram",
"language": "en-US",
"welcome_greeting": "Hi! This is CoPaw. How can I help you?"
},
"wecom": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"bot_id": "",
"secret": "",
"welcome_text": "",
"max_reconnect_attempts": -1
},
"xiaoyi": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"ak": "",
"sk": "",
"agent_id": "",
"ws_url": "wss://hag.cloud.huawei.com/openclaw/v1/ws/link",
"task_timeout_ms": 3600000
},
"weixin": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"bot_token": "",
"bot_token_file": "",
"base_url": ""
},
"onebot": {
"enabled": false,
"bot_prefix": "",
"filter_tool_messages": false,
"filter_thinking": false,
"dm_policy": "open",
"group_policy": "open",
"allow_from": [],
"deny_message": "",
"require_mention": false,
"ws_host": "0.0.0.0",
"ws_port": 6199,
"access_token": "",
"share_session_in_group": false
}
},
"mcp": {
"clients": {}
},
"heartbeat": {
"enabled": true,
"every": "24h",
"target": "main"
},
"last_dispatch": {
"channel": "console",
"user_id": "default",
"session_id": "1775477881298"
},
"running": {
"max_iters": 50,
"llm_retry_enabled": true,
"llm_max_retries": 3,
"llm_backoff_base": 1.0,
"llm_backoff_cap": 10.0,
"llm_max_concurrent": 10,
"llm_max_qpm": 600,
"llm_rate_limit_pause": 5.0,
"llm_rate_limit_jitter": 1.0,
"llm_acquire_timeout": 300.0,
"max_input_length": 131072,
"history_max_length": 10000,
"context_compact": {
"token_count_model": "default",
"token_count_use_mirror": false,
"token_count_estimate_divisor": 4.0,
"context_compact_enabled": true,
"memory_compact_ratio": 0.75,
"memory_reserve_ratio": 0.1,
"compact_with_thinking_block": true
},
"tool_result_compact": {
"enabled": true,
"recent_n": 2,
"old_max_bytes": 3000,
"recent_max_bytes": 50000,
"retention_days": 5
},
"memory_summary": {
"memory_summary_enabled": true,
"force_memory_search": false,
"force_max_results": 1,
"force_min_score": 0.3,
"rebuild_memory_index_on_start": false
},
"embedding_config": {
"backend": "openai",
"api_key": "",
"base_url": "",
"model_name": "",
"dimensions": 1024,
"enable_cache": true,
"use_dimensions": false,
"max_cache_size": 3000,
"max_input_length": 8192,
"max_batch_size": 10
},
"memory_manager_backend": "remelight"
},
"llm_routing": {
"enabled": false,
"mode": "local_first",
"local": {
"provider_id": "",
"model": ""
}
},
"active_model": {
"provider_id": "gemini",
"model": "gemini-3.1-pro-preview"
},
"language": "zh",
"system_prompt_files": [
"AGENTS.md",
"SOUL.md",
"PROFILE.md"
],
"tools": {
"builtin_tools": {
"execute_shell_command": {
"name": "execute_shell_command",
"enabled": true,
"description": "Execute shell commands",
"display_to_user": true,
"async_execution": false
},
"read_file": {
"name": "read_file",
"enabled": true,
"description": "Read file contents",
"display_to_user": true,
"async_execution": false
},
"write_file": {
"name": "write_file",
"enabled": true,
"description": "Write content to file",
"display_to_user": true,
"async_execution": false
},
"edit_file": {
"name": "edit_file",
"enabled": true,
"description": "Edit file using find-and-replace",
"display_to_user": true,
"async_execution": false
},
"grep_search": {
"name": "grep_search",
"enabled": true,
"description": "Search file contents by pattern",
"display_to_user": true,
"async_execution": false
},
"glob_search": {
"name": "glob_search",
"enabled": true,
"description": "Find files matching a glob pattern",
"display_to_user": true,
"async_execution": false
},
"browser_use": {
"name": "browser_use",
"enabled": true,
"description": "Browser automation and web interaction",
"display_to_user": true,
"async_execution": false
},
"desktop_screenshot": {
"name": "desktop_screenshot",
"enabled": true,
"description": "Capture desktop screenshots",
"display_to_user": true,
"async_execution": false
},
"view_image": {
"name": "view_image",
"enabled": true,
"description": "Load an image into LLM context for visual analysis",
"display_to_user": false,
"async_execution": false
},
"view_video": {
"name": "view_video",
"enabled": true,
"description": "Load a video into LLM context for visual analysis",
"display_to_user": false,
"async_execution": false
},
"send_file_to_user": {
"name": "send_file_to_user",
"enabled": true,
"description": "Send files to user",
"display_to_user": true,
"async_execution": false
},
"get_current_time": {
"name": "get_current_time",
"enabled": true,
"description": "Get current date and time",
"display_to_user": true,
"async_execution": false
},
"set_user_timezone": {
"name": "set_user_timezone",
"enabled": true,
"description": "Set user timezone",
"display_to_user": true,
"async_execution": false
},
"get_token_usage": {
"name": "get_token_usage",
"enabled": true,
"description": "Get llm token usage",
"display_to_user": true,
"async_execution": false
}
}
},
"security": {
"tool_guard": {
"enabled": true,
"denied_tools": [],
"custom_rules": [],
"disabled_rules": []
},
"file_guard": {
"enabled": true,
"sensitive_files": []
},
"skill_scanner": {
"mode": "warn",
"timeout": 30,
"whitelist": []
}
}
}
-170
View File
@@ -1,170 +0,0 @@
# 黑手党提案 Agent — Windows 一键安装/更新脚本 (PowerShell)
# 用法: irm https://git.brainwork.club/lidf/chanpinhsd/archive/main.zip -OutFile $env:TEMP\mafia.zip; Expand-Archive $env:TEMP\mafia.zip $env:TEMP\mafia -Force; & $env:TEMP\mafia\chanpinhsd\agent\install.ps1
$ErrorActionPreference = "Stop"
function Write-Info($msg) { Write-Host "[mafia] $msg" -ForegroundColor Green }
function Write-Warn($msg) { Write-Host "[mafia] $msg" -ForegroundColor Yellow }
function Write-Err($msg) { Write-Host "[mafia] $msg" -ForegroundColor Red; exit 1 }
$COPAW_HOME = if ($env:COPAW_HOME) { $env:COPAW_HOME } else { Join-Path $env:USERPROFILE ".copaw" }
$AGENT_ID = "mafia-expert"
$WORKSPACE_DIR = Join-Path $COPAW_HOME "workspaces\$AGENT_ID"
$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
# ── 检查 CoPaw ────────────────────────────────────────────────────────
if (-not (Test-Path (Join-Path $COPAW_HOME "config.json"))) {
Write-Err "CoPaw 未安装。请先安装 CoPaw 桌面客户端:https://copaw.agentscope.io"
}
Write-Info "CoPaw 已就绪"
# ── 创建工作区 ─────────────────────────────────────────────────────────
Write-Info "配置 $AGENT_ID 工作区..."
$dirs = @("skills", "cases", "iteration_reports", "memory")
foreach ($d in $dirs) {
$p = Join-Path $WORKSPACE_DIR $d
if (-not (Test-Path $p)) { New-Item -ItemType Directory -Path $p -Force | Out-Null }
}
# ── 核心配置 ───────────────────────────────────────────────────────────
$coreFiles = @("AGENTS.md", "SOUL.md", "PROFILE.md", "HEARTBEAT.md", "agent.json")
foreach ($f in $coreFiles) {
$src = Join-Path $SCRIPT_DIR $f
if (Test-Path $src) { Copy-Item $src (Join-Path $WORKSPACE_DIR $f) -Force }
}
# MEMORY.md 只在首次创建
$memPath = Join-Path $WORKSPACE_DIR "MEMORY.md"
if (-not (Test-Path $memPath)) {
$memSrc = Join-Path $SCRIPT_DIR "MEMORY.md"
if (Test-Path $memSrc) { Copy-Item $memSrc $memPath }
else { Set-Content $memPath "# 黑手党提案专家长期记忆" -Encoding UTF8 }
}
# ── Skills(每次全量覆盖) ─────────────────────────────────────────────
$skillsSrc = Join-Path $SCRIPT_DIR "skills"
if (Test-Path $skillsSrc) {
Get-ChildItem $skillsSrc -Directory | ForEach-Object {
if ($_.Name -eq "mafia_agent_installer") { return }
$target = Join-Path $WORKSPACE_DIR "skills\$($_.Name)"
if (-not (Test-Path $target)) { New-Item -ItemType Directory -Path $target -Force | Out-Null }
Copy-Item "$($_.FullName)\*" $target -Force -Recurse 2>$null
}
}
Write-Info "文件复制完成"
# ── 合并 skill.json ────────────────────────────────────────────────────
Write-Info "注册自定义 Skills..."
$wsSkillJson = Join-Path $WORKSPACE_DIR "skill.json"
$repoSkillJson = Join-Path $SCRIPT_DIR "skill.json"
$pyScript = @"
import json, os, time
ws_skill = r'$wsSkillJson'
repo_skill = r'$repoSkillJson'
if os.path.exists(ws_skill):
with open(ws_skill, encoding='utf-8') as f:
manifest = json.load(f)
else:
manifest = {'schema_version': 'workspace-skill-manifest.v1', 'version': 0, 'skills': {}}
if os.path.exists(repo_skill):
with open(repo_skill, encoding='utf-8') as f:
repo = json.load(f)
for name, skill_cfg in repo.get('skills', {}).items():
manifest['skills'][name] = skill_cfg
print(f' + {name}')
skills_dir = os.path.join(r'$WORKSPACE_DIR', 'skills')
if os.path.isdir(skills_dir):
for d in os.listdir(skills_dir):
if d.startswith('.'):
continue
skill_md = os.path.join(skills_dir, d, 'SKILL.md')
if os.path.exists(skill_md) and d not in manifest['skills']:
manifest['skills'][d] = {
'enabled': True,
'channels': ['all'],
'source': 'customized',
'metadata': {
'name': d,
'description': '',
'version_text': '1.0.0',
'source': 'customized',
'protected': False,
'requirements': {'require_bins': [], 'require_envs': []},
},
'requirements': {'require_bins': [], 'require_envs': []},
}
print(f' + {d} (from dir)')
manifest['version'] = int(time.time() * 1000)
with open(ws_skill, 'w', encoding='utf-8') as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
print(' skills done')
"@
python3 -c $pyScript 2>$null
if ($LASTEXITCODE -ne 0) { python -c $pyScript }
# ── 注册到 config.json ─────────────────────────────────────────────────
Write-Info "注册 Agent..."
$regScript = @"
import json, os
config_path = os.path.join(r'$COPAW_HOME', 'config.json')
with open(config_path, 'r', encoding='utf-8') as f:
cfg = json.load(f)
agent_id = '$AGENT_ID'
ws_dir = r'$WORKSPACE_DIR'
changed = False
if agent_id not in cfg['agents']['profiles']:
cfg['agents']['profiles'][agent_id] = {
'id': agent_id,
'workspace_dir': ws_dir,
'enabled': True
}
changed = True
if agent_id not in cfg['agents']['agent_order']:
cfg['agents']['agent_order'].append(agent_id)
changed = True
if changed:
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
print(' registered')
else:
print(' already registered')
"@
python3 -c $regScript 2>$null
if ($LASTEXITCODE -ne 0) { python -c $regScript }
# ── 记录来源 ───────────────────────────────────────────────────────────
$repoDir = Split-Path -Parent $SCRIPT_DIR
Set-Content (Join-Path $WORKSPACE_DIR ".repo_path") $repoDir -Encoding UTF8
# ── 清理临时文件 ───────────────────────────────────────────────────────
if ($SCRIPT_DIR -like "$env:TEMP*") {
Write-Info "清理临时文件..."
Remove-Item $repoDir -Recurse -Force -ErrorAction SilentlyContinue
}
# ── 完成 ───────────────────────────────────────────────────────────────
Write-Host ""
Write-Info "════════════════════════════════════════════════"
Write-Info " ✅ 黑手党提案专家 安装/更新完成!"
Write-Info "════════════════════════════════════════════════"
Write-Host ""
Write-Info "下一步:"
Write-Info " 1. 打开 CoPaw 客户端"
Write-Info " 2. 刷新页面(Ctrl+R / F5)"
Write-Info " 3. 左上角切换到「黑手党提案专家」"
Write-Info " 4. 发送:我想为 XX 品牌做一个黑手党提案"
Write-Host ""
-185
View File
@@ -1,185 +0,0 @@
#!/usr/bin/env bash
# 黑手党提案 Agent — 一键安装/更新脚本
# 支持两种调用方式:
# 1. git clone 后:bash agent/install.sh
# 2. curl 下载后:bash /tmp/chanpinhsd-main/agent/install.sh
set -euo pipefail
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m'
info() { printf "${GREEN}[mafia]${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}[mafia]${NC} %s\n" "$*"; }
error() { printf "${RED}[mafia]${NC} %s\n" "$*" >&2; exit 1; }
COPAW_HOME="${COPAW_HOME:-$HOME/.copaw}"
AGENT_ID="mafia-expert"
WORKSPACE_DIR="$COPAW_HOME/workspaces/$AGENT_ID"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# ── 检测操作系统 ──────────────────────────────────────────────────────
OS="$(uname -s)"
case "$OS" in
Darwin) info "检测到 macOS" ;;
Linux) info "检测到 Linux" ;;
MINGW*|MSYS*|CYGWIN*) warn "检测到 Windows (Git Bash/MSYS),部分功能可能受限" ;;
*) error "不支持的操作系统: $OS" ;;
esac
# ── 检查 CoPaw 是否已安装 ──────────────────────────────────────────────
if [ ! -f "$COPAW_HOME/config.json" ]; then
if command -v copaw &>/dev/null || [ -f "$COPAW_HOME/bin/copaw" ]; then
info "CoPaw 已安装但未初始化,正在初始化..."
export PATH="$COPAW_HOME/bin:$PATH"
copaw init --defaults --accept-security
else
error "CoPaw 未安装。请先安装 CoPaw 桌面客户端:https://copaw.agentscope.io"
fi
else
info "CoPaw 已就绪"
fi
# ── 创建工作区并复制文件 ───────────────────────────────────────────────
info "配置 $AGENT_ID 工作区..."
mkdir -p "$WORKSPACE_DIR/skills"
mkdir -p "$WORKSPACE_DIR/cases"
mkdir -p "$WORKSPACE_DIR/iteration_reports"
mkdir -p "$WORKSPACE_DIR/memory"
# 核心配置(skill.json 单独合并,不覆盖)
for f in AGENTS.md SOUL.md PROFILE.md HEARTBEAT.md agent.json; do
if [ -f "$SCRIPT_DIR/$f" ]; then
cp "$SCRIPT_DIR/$f" "$WORKSPACE_DIR/$f"
fi
done
# MEMORY.md 只在首次创建(不覆盖用户积累的记忆)
if [ ! -f "$WORKSPACE_DIR/MEMORY.md" ]; then
cp "$SCRIPT_DIR/MEMORY.md" "$WORKSPACE_DIR/MEMORY.md" 2>/dev/null || echo "# 黑手党提案专家长期记忆" > "$WORKSPACE_DIR/MEMORY.md"
fi
# Skills(每次全量覆盖,保证最新)
if [ -d "$SCRIPT_DIR/skills" ]; then
for skill_dir in "$SCRIPT_DIR/skills"/*/; do
skill_name="$(basename "$skill_dir")"
# installer skill 不复制到 mafia-expert,它属于 default agent
if [ "$skill_name" = "mafia_agent_installer" ]; then
continue
fi
mkdir -p "$WORKSPACE_DIR/skills/$skill_name"
cp "$skill_dir"* "$WORKSPACE_DIR/skills/$skill_name/" 2>/dev/null || true
done
fi
info "文件复制完成"
# ── 合并 skill.json(注入自定义 skills,保留内置 skills)──────────────
info "注册自定义 Skills..."
python3 -c "
import json, os, time
ws_skill = os.path.expanduser('$WORKSPACE_DIR/skill.json')
repo_skill = '$SCRIPT_DIR/skill.json'
# 读取工作区已有的 manifest(含内置 skills)
if os.path.exists(ws_skill):
with open(ws_skill) as f:
manifest = json.load(f)
else:
manifest = {'schema_version': 'workspace-skill-manifest.v1', 'version': 0, 'skills': {}}
# 读取仓库的自定义 skills 定义
if os.path.exists(repo_skill):
with open(repo_skill) as f:
repo = json.load(f)
for name, skill_cfg in repo.get('skills', {}).items():
manifest['skills'][name] = skill_cfg
print(f' + {name}')
# 同时扫描 active_skills 目录,确保所有 SKILL.md 都被注册
skills_dir = os.path.expanduser('$WORKSPACE_DIR/skills')
if os.path.isdir(skills_dir):
for d in os.listdir(skills_dir):
if d.startswith('.'):
continue
skill_md = os.path.join(skills_dir, d, 'SKILL.md')
if os.path.exists(skill_md) and d not in manifest['skills']:
manifest['skills'][d] = {
'enabled': True,
'channels': ['all'],
'source': 'customized',
'metadata': {
'name': d,
'description': '',
'version_text': '1.0.0',
'source': 'customized',
'protected': False,
'requirements': {'require_bins': [], 'require_envs': []},
},
'requirements': {'require_bins': [], 'require_envs': []},
}
print(f' + {d} (from dir)')
manifest['version'] = int(time.time() * 1000)
with open(ws_skill, 'w') as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
print(' skills 注册完成')
"
# ── 注册到 config.json ─────────────────────────────────────────────────
info "注册 Agent..."
python3 -c "
import json, os
config_path = os.path.expanduser('$COPAW_HOME/config.json')
with open(config_path, 'r') as f:
cfg = json.load(f)
agent_id = '$AGENT_ID'
ws_dir = '$WORKSPACE_DIR'
changed = False
if agent_id not in cfg['agents']['profiles']:
cfg['agents']['profiles'][agent_id] = {
'id': agent_id,
'workspace_dir': ws_dir,
'enabled': True
}
changed = True
if agent_id not in cfg['agents']['agent_order']:
cfg['agents']['agent_order'].append(agent_id)
changed = True
if changed:
with open(config_path, 'w') as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)
print(' ✅ 注册成功')
else:
print(' ✅ 已注册,配置已更新')
"
# ── 记录来源路径(便于后续更新)─────────────────────────────────────────
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
echo "$REPO_DIR" > "$WORKSPACE_DIR/.repo_path"
# ── 清理临时文件(如果是 curl 下载的)──────────────────────────────────
if [[ "$SCRIPT_DIR" == /tmp/* ]]; then
info "清理临时文件..."
rm -rf "$(cd "$SCRIPT_DIR/.." && pwd)"
fi
# ── 完成 ───────────────────────────────────────────────────────────────
echo ""
info "════════════════════════════════════════════════"
info " ✅ 黑手党提案专家 安装/更新完成!"
info "════════════════════════════════════════════════"
echo ""
info "下一步:"
info " 1. 打开 CoPaw 客户端"
info " 2. 刷新页面(Cmd+R / F5)"
info " 3. 左上角切换到「黑手党提案专家」"
info " 4. 发送:我想为 XX 品牌做一个黑手党提案"
echo ""
-90
View File
@@ -1,90 +0,0 @@
{
"schema_version": "workspace-skill-manifest.v1",
"version": 1775500000000,
"skills": {
"ude_diagnosis": {
"enabled": true,
"channels": ["all"],
"source": "customized",
"metadata": {
"name": "ude_diagnosis",
"description": "UDE 诊断 — TOC 不满意效果分析",
"version_text": "1.0.0",
"source": "customized",
"protected": false,
"requirements": {"require_bins": [], "require_envs": []}
},
"requirements": {"require_bins": [], "require_envs": []}
},
"conflict_cloud": {
"enabled": true,
"channels": ["all"],
"source": "customized",
"metadata": {
"name": "conflict_cloud",
"description": "冲突图构建 — TOC 对立假设分析",
"version_text": "1.0.0",
"source": "customized",
"protected": false,
"requirements": {"require_bins": [], "require_envs": []}
},
"requirements": {"require_bins": [], "require_envs": []}
},
"convention_breaker": {
"enabled": true,
"channels": ["all"],
"source": "customized",
"metadata": {
"name": "convention_breaker",
"description": "惯例揭示与打破 — TOC 核心突破",
"version_text": "1.0.0",
"source": "customized",
"protected": false,
"requirements": {"require_bins": [], "require_envs": []}
},
"requirements": {"require_bins": [], "require_envs": []}
},
"negative_branch": {
"enabled": true,
"channels": ["all"],
"source": "customized",
"metadata": {
"name": "negative_branch",
"description": "负面分支预测 — TOC 风险分析",
"version_text": "1.0.0",
"source": "customized",
"protected": false,
"requirements": {"require_bins": [], "require_envs": []}
},
"requirements": {"require_bins": [], "require_envs": []}
},
"proposal_assembler": {
"enabled": true,
"channels": ["all"],
"source": "customized",
"metadata": {
"name": "proposal_assembler",
"description": "提案组装 — 黑手党提案最终交付",
"version_text": "1.0.0",
"source": "customized",
"protected": false,
"requirements": {"require_bins": [], "require_envs": []}
},
"requirements": {"require_bins": [], "require_envs": []}
},
"voc_research": {
"enabled": true,
"channels": ["all"],
"source": "customized",
"metadata": {
"name": "voc_research",
"description": "VOC 研究采集与分析(需要用户自带 TikHub Key)",
"version_text": "1.0.0",
"source": "customized",
"protected": false,
"requirements": {"require_bins": [], "require_envs": []}
},
"requirements": {"require_bins": [], "require_envs": []}
}
}
}
-95
View File
@@ -1,95 +0,0 @@
---
name: 冲突图构建
description: 对筛选出的主要UDE构建冲突图(Evaporating Cloud),拆解O-B-D结构,合并多个UDE的冲突图
---
# 冲突图构建(Evaporating Cloud / Conflict Cloud)
当用户完成了 UDE 诊断(筛选出最重要的 3 个 UDE + 恶性循环),进入"定义核心冲突"时,激活此 Skill。
## 概念说明
冲突图(又称"蒸发云")是 TOC 思考过程的核心工具。它揭示的不是"问题",而是"为什么问题一直存在"——因为存在一个看似无法调和的冲突。
## 第一步:对每个主要 UDE 构建冲突图
### 构建逻辑(Agent 内部推理过程)
1. 将 UDE 反转为 DE(期望效果)
2. 思考:DE 的必备条件是什么?(D:只有做了 D,UDE 才会转化为 DE)
3. 从 D 中找到两个矛盾的行动方向(D1 vs D2)
4. D1 对应的期望效果 = B1(需求1)
5. D2 对应的期望效果 = B2(需求2)
6. B1 和 B2 共同服务的上层目标 = O
### 引导用户的提问
对每个主要 UDE,依次提问:
1. "当您感受到这个 UDE 时,团队**通常的做法**是什么?" → 这就是 D1
2. "如果不考虑任何限制,您**期望采取的做法**是什么?" → 这就是 D2
3. "D1 试图满足的需求是什么?" → 这就是 B1
4. "D2 试图满足的需求是什么?" → 这就是 B2
5. "B1 和 B2 共同服务的更大目标是什么?" → 这就是 O
### 输出模板(每个 UDE 一份)
```markdown
### UDE-[编号]:[UDE 描述]
| 维度 | 目标 | 需求 | 行动 |
|------|------|------|------|
| 惯性方向 | O: [系统目标] | B1: [需求1] | D1: [当前通常做法] |
| 期望方向 | | B2: [需求2] | D2: [期望做法] |
#### 互斥性检查
- ✅ 如果 D1,则不能 D2:[解释为什么]
- ✅ 如果 D2,则不能 D1:[解释为什么]
- ✅ 只有 D1 才能 B1:[解释]
- ✅ 只有 D2 才能 B2:[解释]
- ✅ 如果 D1 则不能 B2:[解释]
- ✅ 如果 D2 则不能 B1:[解释]
```
### 互斥性校验规则
**这是最关键的质量关卡。** D1 和 D2 必须满足以下全部条件:
- D1 和 D2 不能同时执行(不是"程度差异",而是"方向互斥")
- 选择 D1 必然牺牲 B2
- 选择 D2 必然牺牲 B1
- 如果可以"都做一点",说明冲突图构建有误,需要重新提炼
## 第二步:合并冲突图
当多个 UDE 的冲突图都完成后,寻找它们的共性:
### 合并规则
1. 将多个 O 合并为统一的系统目标
2. 将多个 B1 合并为"惯性方向的需求集"
3. 将多个 B2 合并为"期望方向的需求集"
4. 将多个 D1 合并为"当前采取的行动集"
5. 将多个 D2 合并为"希望采取的行动集"
### 输出模板
```markdown
### 合并冲突图
| 维度 | O合并 | B | D |
|------|-------|---|---|
| 惯性方向 | 1. [目标1] | B1 合并:| D1 合并(当前行动):|
| | 2. [目标2] | - [需求a] | - [行动a] |
| | 3. [目标3] | - [需求b] | - [行动b] |
| 期望方向 | | B2 合并:| D2 合并(期望行动):|
| | | - [需求c] | - [行动c] |
| | | - [需求d] | - [行动d] |
```
## 完成标志
- [ ] 每个主要 UDE(3个)都有独立的冲突图
- [ ] 每个冲突图通过互斥性校验
- [ ] 完成合并冲突图
- [ ] 用户确认合并结果
完成后,提示用户进入下一步:**步骤2 - 揭示冲突背后的惯例**。
-139
View File
@@ -1,139 +0,0 @@
---
name: 惯例打破与方案激发
description: 揭示冲突背后的行业惯例,通过因果假设分析找到隐含假设,用例外案例激发打破惯例的方案,输出方向性提案
---
# 惯例打破与方案激发(Convention Breaker)
当用户完成了冲突图构建(合并冲突图就绪),进入"揭示惯例"和"用例外激发方案"时,激活此 Skill。
此 Skill 覆盖原方法论的 **步骤2(揭示惯例)** 和 **步骤3(用例外激发方案)**。
---
## 部分一:揭示冲突背后的惯例(步骤2)
### 核心问题
对合并冲突图中的每对 B1-D1,追问:
> **"为什么用户想要 B1,就一定得 D1?"**
### 因果假设分析法
用 CLR(因果逻辑推理)格式拆解:
```
因为:D1 [当前行动]
同时:[隐含假设]
那么:B1 [需求被满足]
```
如果这个逻辑链成立,说明"隐含假设"就是维持 D1 的根基。
### 引导提问
1. "在你们行业,为什么大家都选择 D1 而不是 D2?"
2. "D1 背后有什么'理所当然'的假设?"
3. "这个假设对应的行业通常做法(惯例)是什么?"
4. "你能举出具体的同行案例来证明这个惯例的存在吗?"
### 输出模板
```markdown
### 惯例分析
| 关键问题 | 假设 | 行业惯例(直接证据/通常做法) | 总结 |
|---------|------|---------------------------|------|
| 为什么想要 B1:[XX] 就必须 D1:[XX]? | [假设1] | [具体行业做法] | 如果遵循惯例1: [XX],则用户只会D1不会D2 |
| 检查:因为 D1,同时 [假设],那么 B1 | [假设2] | [具体行业做法] | 如果遵循惯例2: [XX],则用户只会D1不会D2 |
```
### 惯例验证(两个视角,必做)
#### 我方视角验证
```markdown
1. 惯例是不是行业内保护自身利益的"普遍做法"?
- 惯例1 [XX]:✅/❌ [解释]
- 惯例2 [XX]:✅/❌ [解释]
2. 惯例是否导致/加剧当下的恶性循环?
- 因为:企业继续遵守惯例 [XX]
- 假设:同时,[XX]
- 结果:恶性循环中 [XX] 将加剧
```
#### DTB(客户)视角验证
```markdown
1. 惯例直接导致了主要 UDE 中的至少一个?
- 惯例 [XX] → 直接导致 UDE-[编号]:✅/❌
2. 惯例导致加剧了恶性循环?
- 惯例 [XX] → 加剧循环 [编号]:✅/❌
```
**如果验证不通过,需要回退重新寻找惯例。**
---
## 部分二:用例外激发方案(步骤3)
### 核心规则
**例外必须是具体实例,而不是逻辑推演。** 不接受"理论上可以……",必须是"在 XX 行业,XX 公司做到了……"。
### 引导提问
对每个惯例,依次提问:
1. "有没有哪家公司/哪个行业,做了跟这个惯例完全相反的事,反而成功了?"
2. "如果用户可以同时获得 B1 和 B2,那需要什么样的新做法?"
3. "发生了什么例外事件破坏了这个假设?"
### 方案激发模板
对每个惯例分别填写:
```markdown
### 惯例 [编号]:[惯例名称]
| 维度 | DTB目标 | 需求 | 行动 | 激发方案 |
|------|--------|------|------|---------|
| 之所以D2无法B1,是因为假设: | | | | [隐含假设] |
| 现有惯例对应D1 | O: [XX] | B1: [XX] | D1: [XX] | 例外是:[具体公司/行业实例] |
| 打破惯例对应D2 | | B2: [XX] | D2: [XX] | 可以尝试的是:[具体方案] |
```
### 方向性提案总结(必须输出)
当所有惯例都完成方案激发后,用以下固定格式输出总结:
```markdown
## 方向性提案总结
> **通过** D [具体行动],**满足** B [具体需求];
> **同时通过** D' [具体行动],**满足** B' [具体需求];
> **使 DTB 既可以** O1 [目标1],**又能够** O2 [目标2]。
### 具体拆解
- D:[行动描述]
- B:[需求描述]
- D':[行动描述]
- B':[需求描述]
- O1:[目标描述]
- O2:[目标描述]
```
---
## 完成标志
- [ ] 每个惯例都经过因果假设分析
- [ ] 每个惯例通过双视角验证(我方 + DTB)
- [ ] 每个惯例都找到了具体的例外案例(不是逻辑推演)
- [ ] 输出了方向性提案总结
- [ ] 用户确认方向性提案
完成后,提示用户进入下一步:**步骤4 - 消除负面分支**。
@@ -1,39 +0,0 @@
---
name: mafia_agent_installer
description: "安装或更新「黑手党提案专家」Agent。当用户提到安装、更新、升级黑手党提案时激活。"
metadata:
copaw:
emoji: "🔧"
requires: {}
---
# 黑手党提案 Agent 安装与更新
当用户要求**安装、更新、升级「黑手党提案专家」Agent** 时,使用此 Skill。
## 安装(用户首次使用)
用 `execute_shell_command` 执行:
```bash
curl -fsSL https://git.brainwork.club/lidf/chanpinhsd/archive/main.tar.gz | tar xz -C /tmp && bash /tmp/chanpinhsd/agent/install.sh
```
## 更新(已安装过)
先检查是否有 Git 仓库:
```bash
if [ -d ~/mafia-proposal/.git ]; then
cd ~/mafia-proposal && git pull && bash agent/install.sh
else
curl -fsSL https://git.brainwork.club/lidf/chanpinhsd/archive/main.tar.gz | tar xz -C /tmp && bash /tmp/chanpinhsd/agent/install.sh
fi
```
## 安装完成后
告知用户:
> ✅ **黑手党提案专家**已就绪!
> 请刷新页面(Cmd+R),然后在左上角切换到「黑手党提案专家」开始使用。
-117
View File
@@ -1,117 +0,0 @@
---
name: 负面分支消除
description: 识别新方案可能带来的新UDE,构建因果假设链,在链条中找到打破假设的节点来消解负面分支
---
# 负面分支消除(Negative Branch Reservation)
当用户完成了方向性提案总结,进入"消除负面分支"时,激活此 Skill。
## 概念说明
任何新方案在解决旧问题的同时,都会带来新的副作用。负面分支消除不是否定新方案,而是提前预见并设计配套措施,使副作用不会危害核心需求。
---
## 第一步:列出新方案可能导致的所有新 UDE
### 引导提问
向用户提供以下发散思考维度:
> "如果我们实施了方向性提案中的新方案(D + D'),可能会出现哪些新的不良效果?"
提供以下清单帮助用户发散思考:
- 协同复杂度增加(扯皮和纠纷变多)
- 考核边界不清晰(难以量化)
- 管理复杂度增加
- 局部效率降低
- 结果不确定性高
- 不适应新的思考和行为模式
- 加重了认知负担
- 提升了决策难度
- 决策慢
- 错失"机会"多
- 官僚作风
- 办公室政治
- 对人才的要求高
- 短期业绩可能下滑
- 组织阻力大
- 其他(请用户补充)
## 第二步:筛选危害性 UDE
从新增 UDE 中,筛选出**同时满足**以下两个条件的:
1. **直接危害**现有任一 B(方向性提案中的需求)
2. **客户感受强**(客户会第一时间抱怨)
### 输出模板
```markdown
### 危害性 UDE 筛选
| 被危害的 B | 新增 UDE | 客户感受 |
|-----------|---------|---------|
| B1: [需求] | [新UDE描述] | [为什么客户感受强] |
| B2: [需求] | [新UDE描述] | [为什么客户感受强] |
```
## 第三步:构建因果假设链
对每个危害性 UDE,从方案 D 开始,追踪因果链条直到最终 UDE。
### 构建规则
1. 从最底层(方案 D)向上推演
2. 每一步标注"假设"(这步成立需要什么前提)
3. 每一步标注性质:DE(好的中间结果)还是 UDE(坏的中间结果)
4. **重点寻找**:链条中 DE 变为 UDE 的转折点——那就是打破假设的节点
### 输出模板(每个危害性 UDE 一份)
```markdown
### 因果假设链:[最终 UDE 描述]
| | 因果假设链 | | 性质 |
|--|-----------|--|------|
| 结果 | 最终结果:[UDE 描述] | | UDE |
| 过程 | 中间结果3:[XX] | 假设:[XX] | UDE |
| | 中间结果2:[XX] | 假设:[XX] | UDE/DE |
| | | | **→ 打破假设的方案:[XX]** |
| | 中间结果1:[XX] | 假设:[XX] | DE |
| 方案 | D:[新方案] | 假设:[前提条件] | |
```
## 第四步:验证打破假设方案的有效性
对每个"打破假设的方案",用以下模板验证:
```markdown
### 验证:[打破的假设]
| 如果: | D [具体方案] |
|--------|-------------|
| 同时: | 假设 [前提条件被满足] |
| 那么: | UDE变成DE:[原UDE描述] → [新DE描述] |
| 并且那么: | 现有DE没有转变为UDE:✅ [确认说明] |
```
### 验证标准
- "UDE 变成 DE"必须是具体的、可感知的改善
- "现有 DE 没有转变为 UDE"必须逐一检查所有现有 DE
- 如果验证不通过,需要回退修改打破假设方案
---
## 完成标志
- [ ] 列出了所有可能的新增 UDE
- [ ] 筛选出直接危害 B 且客户感受强的 UDE
- [ ] 每个危害性 UDE 都有因果假设链
- [ ] 每条链上找到了打破假设的节点和方案
- [ ] 每个打破假设方案通过了"如果-同时-那么"验证
- [ ] 用户确认所有负面分支已消解
完成后,提示用户进入最后一步:**步骤5 - 确立新规则、匹配新能力**。
-183
View File
@@ -1,183 +0,0 @@
---
name: 提案组装
description: 确立新规则、匹配运营能力、组装最终黑手党提案交付文档
---
# 提案组装(Proposal Assembler)
当用户完成了负面分支消除,进入最后阶段时,激活此 Skill。
此 Skill 覆盖 **步骤5(确立新规则、匹配新能力)** 和 **最终交付文档组装**。
---
## 部分一:确立新规则(步骤5)
### 第一步:对比新旧惯例
```markdown
### 新旧惯例对比
| 维度 | 旧惯例 | 新规则 |
|------|-------|-------|
| 惯例1 | [旧做法] | [打破后的新做法] |
| 惯例2 | [旧做法] | [打破后的新做法] |
```
### 第二步:梳理剩余 UDE
将所有 UDE 分为两类:
| 分类 | 说明 | 处理方式 |
|------|------|---------|
| 原 UDE 未被方向性提案解决的 | 打破惯例后仍然存在 | 需要额外运营能力 |
| 新方案导致的新 UDE(负面分支未完全消解的) | 步骤4 的遗留 | 需要配套运营能力 |
### 第三步:匹配运营能力
对每个剩余 UDE,明确"要把它转化为 DE 需要什么能力":
```markdown
### 运营能力匹配
| 分类 | 期望改变的结果 | 运营能力(我方产品为DTB增加的能力) |
|------|-------------|--------------------------------|
| 原UDE → DE | [UDE描述] → [期望DE] | [具体运营能力] |
| 原UDE → DE | [UDE描述] → [期望DE] | [具体运营能力] |
| 新UDE → DE | [UDE描述] → [期望DE] | [具体运营能力] |
| 新UDE → DE | [UDE描述] → [期望DE] | [具体运营能力] |
```
### 第四步:输出产品说明书
将运营能力归类到具体的产品/服务单元中:
```markdown
### 产品说明书
| 产品单元 | 运营能力(改变UDE为DE) | 运营能力(维持DE) |
|---------|---------------------|------------------|
| [产品1] | [改变能力描述] | [维持能力描述] |
| [产品2] | [改变能力描述] | [维持能力描述] |
```
---
## 部分二:组装最终交付文档
完成以上所有步骤后,将全流程的输出组装为完整的黑手党提案文档。
### 最终交付文档结构
```markdown
# [品牌名] 产品上新 — 黑手党提案
> 生成时间:YYYY-MM-DD
> 方法论版本:vX.Y
> Agent:mafia-expert
---
## 一、双赢定义
### 1.1 概念共识
[对黑手党提案概念的共识——来自准备1]
### 1.2 JTBD 四要素
[四要素表——来自准备2]
| 维度 | 我方 | 典型目标客户(DTB) |
|------|------|-------------------|
| 客户画像 | ... | ... |
| 雇用产品 | ... | ... |
| 待办任务 | ... | ... |
| 期望结果 | ... | ... |
---
## 二、UDE 诊断
### 2.1 完整 UDE 清单
[编号列表——来自准备3,标注10条标准校验结果]
### 2.2 最重要的 3 个 UDE
[Top3 及选择理由——来自准备3]
### 2.3 恶性循环
[循环图——来自准备3]
---
## 三、核心冲突
### 3.1 各 UDE 冲突图
[每个UDE的O-B-D表——来自步骤1]
### 3.2 合并冲突图
[合并后的结果——来自步骤1]
---
## 四、惯例揭示与方向性提案
### 4.1 行业惯例清单
[惯例分析表——来自步骤2]
### 4.2 惯例验证
[双视角验证结果——来自步骤2]
### 4.3 方向性提案总结
> 通过 D [XX],满足 B [XX];
> 同时通过 D' [XX],满足 B' [XX];
> 使 DTB 既可以 O1,又能够 O2。
[——来自步骤3]
---
## 五、操作性提案
### 5.1 负面分支消除
[因果假设链 + 打破假设方案——来自步骤4]
### 5.2 新规则与新能力
[运营能力匹配表——来自步骤5]
### 5.3 产品说明书
[产品单元 × 运营能力矩阵——来自步骤5]
---
## 附录
### A. 数据来源与证据链
[列出所有信息来源]
### B. 研讨过程记录
[记录每步中用户的关键校准和决策]
### C. 方法论版本与迭代日志
[记录本次提案使用的 Agent 版本和 Skill 版本]
```
### 组装规则
1. **不要添加新的分析**——只整理和呈现已有输出
2. **保持逻辑链完整**——从 UDE 到最终产品说明书的推理链必须可追溯
3. **标注来源步骤**——每个章节标注输出来自哪个步骤
4. **提示用户保存**——建议将文件保存为 `cases/[品牌名]_[日期].md`
---
## 完成标志
- [ ] 新旧惯例对比完成
- [ ] 所有剩余 UDE 都匹配了运营能力
- [ ] 产品说明书输出
- [ ] 最终交付文档组装完成,五章结构完整
- [ ] 用户确认最终交付物
- [ ] 提案文件已保存
完成后,告知用户:
> "黑手党提案已完成。建议将此文件保存到 cases/ 目录作为案例存档。
> 如果在后续使用中发现方法论有需要改进的地方,我会在下次心跳时自动整理迭代建议。"
-165
View File
@@ -1,165 +0,0 @@
---
name: UDE 诊断
description: 基于 TOC 约束理论,从 VOC 消费者数据中提取 UDE(不良效果),通过向量聚类发现核心问题
---
# UDE 诊断(Undesirable Effects Diagnosis)
## 什么是 UDE
**UDE(Undesirable Effect)= 系统中当前正在发生的、阻碍系统实现目标的、可观测的负面现象。**
UDE 是**症状**,不是病因,也不是解决方案。多个 UDE 之间的因果关系指向隐藏的**核心问题**——通常是一条不被质疑的政策或假设。
---
## 整体流程
```
VOC 评论 ──LLM转写──→ UDE 格式句 ──向量化──→ 语义聚类 ──覆盖扫描──→ 核心 UDE 清单
↑ ↑ ↑ ↑
格式转换(低幻觉) DashScope v4 DBSCAN(确定性) 因果链验收
```
**LLM 只出场一次(转写),相似性判断和频次统计全部交给向量空间。**
---
## 第一步:VOC → UDE 转写
### 有 VOC 研究数据时
对 Agent 说:
> "请对研究 [research_id] 的 VOC 评论进行 UDE 转写"
Agent 会调用 `voc_to_ude` 工具,将每条消费者评论转写为一条 UDE 格式句。
转写规则(LLM 的任务是"格式转换",不是"分析"):
- 输入:"一瓶三百多,吃一个月,真的吃不起"
- 输出:"该品类产品月均消费成本持续超出目标消费者的可接受范围"
### UDE 格式规范(7 条硬约束)
| # | 规范 | 检查要点 |
|---|------|---------|
| 1 | **完整陈述句** | 不是"响应慢",而是"客户投诉响应时间持续超过48小时" |
| 2 | **现在时态** | 描述当前正在发生的事,不是过去或未来 |
| 3 | **只描述效果,不含原因** | 不能包含"因为…所以…" |
| 4 | **不是伪装的解决方案** | 不能说"需要X"、"应该做Y" |
| 5 | **单一实体** | 一条 UDE 只描述一个问题 |
| 6 | **客观可验证** | 利益相关方能达成共识的事实 |
| 7 | **在影响范围内** | 品牌/企业可以采取行动改善的 |
### 「伪装的解决方案」速查表
| ❌ 伪装的解决方案 | ✅ 正确的 UDE |
|-----------------|-------------|
| "需要新的 ERP 系统" | "关键数据难以检索,决策持续滞后" |
| "应该增加广告预算" | "品牌知名度持续低于主要竞品" |
| "需要引进高端人才" | "核心技术岗位长期空缺,项目交付延期" |
| "应该做消费者调研" | "产品开发决策缺乏消费者需求依据" |
### 无 VOC 数据时
引导用户从自身经验出发。**不要用预设框架**,直接问:
> "站在你的客户/消费者的角度,他们**当前持续承受**着哪些让他们不满意、阻碍他们更好地完成任务的问题?"
---
## 第二步:向量聚类
### 前置条件
用户需配置 DashScope API Key(阿里云百炼控制台获取)。对 Agent 说「我的 DashScope Key 是 xxx」即可。
### 执行
对 Agent 说:
> "请对研究 [research_id] 的 UDE 进行聚类分析"
Agent 会调用 `cluster_udes` 工具:
1. 使用 DashScope text-embedding-v4 将所有 UDE 句向量化(1024 维)
2. 使用 DBSCAN 算法进行语义聚类
3. 返回按覆盖量排序的聚类列表
### 为什么用向量聚类而非标签统计
| | 标签统计(旧方案) | 向量聚类(当前方案) |
|---|---|---|
| 问题 | LLM 标签碎片化(1020 条产出 1003 个标签)| 向量空间天然处理近义表述 |
| 频次 | GROUP BY 结果无意义(每标签=1次) | **簇大小 = 真实覆盖量** |
| 全局视野 | ❌ LLM 逐条处理 | ✅ 向量空间容纳全部 UDE |
### 输出示例
```
簇 1(覆盖 80 条)
代表 UDE:该品类产品月均消费成本持续超出目标消费者的可接受范围
原声:["一瓶三百多真的吃不起", "不是不想买太贵了", ...]
簇 2(覆盖 27 条)
代表 UDE:产品冷藏存储要求与消费者日常场景持续冲突
原声:["需要冷藏但办公室没冰箱", "忘了放冰箱担心失效", ...]
噪声:45 条(4.4%)— 不属于任何主要 UDE 簇
```
---
## 第三步:覆盖扫描
对 Agent 说:
> "请扫描 UDE 聚类的覆盖情况"
Agent 会调用 `scan_coverage` 工具,返回:
- 覆盖率:多少 VOC 评论被 UDE 簇覆盖
- 噪声占比:未被覆盖的 UDE(可能遗漏的线索)
- 噪声样本:供人工检查
判断标准:
- 噪声 < 10%:覆盖充分
- 噪声 10-20%:查看样本,可能遗漏某类 UDE
- 噪声 > 20%:需要调整聚类参数
---
## 第四步:构建因果链
取聚类产生的 Top 5-10 个代表性 UDE,用「因为…所以…」链接,寻找因果关系:
1. 取任意两条 UDE,问:"A 是否会导致 B?"
2. 用箭头标注因果方向
3. **发出最多箭头的 UDE** = 候选核心问题(根因方向)
### 朗读测试
- 从下到上:「**因为** [原因 UDE],**所以** [效果 UDE]」
- 从上到下:「[效果 UDE] **是因为** [原因 UDE]」
听着不合逻辑 → 检查是否缺少中间环节。
---
## 第五步:核心问题验收(CLR 检验)
对核心问题候选进行 3 项验收:
| 检验项 | 问题 |
|--------|------|
| **充分性** | 该原因单独足够导致那些效果吗?还需要什么其他条件? |
| **预测效果** | 如果核心问题存在,还应该产生什么其他效果?那些效果存在吗? |
| **同义反复** | 核心问题是不是在用不同的话重复某个 UDE? |
---
## 完成标志
- [ ] VOC 评论已转写为 UDE 格式句
- [ ] UDE 已通过向量聚类,产出按覆盖量排序的簇
- [ ] 覆盖扫描通过(噪声 < 10%)
- [ ] 构建了因果链,识别出核心问题候选
- [ ] 核心问题通过 CLR 三项验收
- [ ] 用户确认核心问题
完成后,将核心问题和 UDE 清单传递给下一步:**冲突图构建**。
> **关键提醒**:核心问题 = 导致最多 UDE 的根因,不是"跟我们的产品最相关的"。产品/方案如何注入到核心问题上,是冲突图和惯例打破要解决的事。
-178
View File
@@ -1,178 +0,0 @@
---
name: voc_research
description: "VOC 研究采集与分析工具。通过调用 VOC 后端 API 完成消费者声音数据的采集、查看和分析。在黑手党提案的准备阶段(UDE 诊断)特别有用。"
metadata:
copaw:
emoji: "📊"
requires: {}
---
# VOC 研究工具
当用户需要**采集消费者声音数据、查看 VOC 分析结果、创建 VOC 研究**时,使用此 Skill。
本工具通过 `execute_shell_command` 调用 VOC 后端 REST API。
## API 基地址
```
https://brand.brainwork.club/voc/api/research
```
## 前置:TikHub API Key 配置
采集数据需要 TikHub API Key(每个用户自行充值)。
### 检查 Key 是否已配置
```bash
grep TIKHUB_API_KEY ~/.copaw/workspaces/mafia-expert/.env 2>/dev/null || echo "未配置"
```
### 首次使用时,引导用户配置 Key
如果未配置,告诉用户:
> 采集 VOC 数据需要 TikHub API Key。请前往 https://tikhub.io 注册并充值后,将 Key 告诉我。
> 单次研究成本约 $1-3(12 个关键词 × 2 平台)。
用户提供 Key 后,保存到 .env:
```bash
echo 'TIKHUB_API_KEY=用户提供的key' >> ~/.copaw/workspaces/mafia-expert/.env
```
### 所有涉及采集的 curl 调用必须带上 Key
```bash
# 1. 先加载 key
source ~/.copaw/workspaces/mafia-expert/.env 2>/dev/null
# 2. 检查 key 是否存在
if [ -z "$TIKHUB_API_KEY" ]; then
echo "错误:未配置 TIKHUB_API_KEY,请先提供你的 TikHub Key"
exit 1
fi
# 3. 调用 API 时带上 header
curl -s -X PUT "https://brand.brainwork.club/voc/api/research/{id}/card" \
-H "Content-Type: application/json" \
-H "X-TikHub-Key: $TIKHUB_API_KEY" \
-d '{...}'
```
⚠️ **铁律:不带 X-TikHub-Key 的采集请求会被服务器拒绝。** 只有读取操作(查看数据、列表等)不需要 Key。
## 核心工作流
### 场景 1:为黑手党提案采集 VOC 数据
当用户说"帮我采集 XX 品牌的 VOC 数据"时:
**第零步:确认 TikHub Key 已配置**
```bash
source ~/.copaw/workspaces/mafia-expert/.env 2>/dev/null
[ -z "$TIKHUB_API_KEY" ] && echo "需要配置 TikHub Key" || echo "Key 已就绪: ...${TIKHUB_API_KEY: -6}"
```
如果未配置,先引导用户提供 Key(见上方"首次使用"流程)。
**第一步:检查是否有同品牌的已有研究**
```bash
curl -s https://brand.brainwork.club/voc/api/research/list | python3 -m json.tool
```
如果已有同品牌研究,直接使用其 `id`,跳到第三步。
**第二步:创建新研究(仅当无已有研究时)**
```bash
curl -s -X POST https://brand.brainwork.club/voc/api/research \
-H "Content-Type: application/json" \
-d '{"brand_name": "品牌名"}' | python3 -m json.tool
```
返回 `research_id`,记住它。
**第三步:提交研究卡片(自动触发采集)**
```bash
source ~/.copaw/workspaces/mafia-expert/.env
curl -s -X PUT "https://brand.brainwork.club/voc/api/research/{id}/card" \
-H "Content-Type: application/json" \
-H "X-TikHub-Key: $TIKHUB_API_KEY" \
-d '{
"brand_name": "天维美",
"category": "跨境保健品",
"focus_product": "护肝片",
"competitors": [{"name": "Swisse"}, {"name": "Blackmores"}],
"keywords": [
{"keyword": "天维美 护肝片", "category": "brand"},
{"keyword": "护肝片 推荐", "category": "category"},
{"keyword": "Swisse 护肝", "category": "compete"}
]
}' | python3 -m json.tool
```
> ⚠️ 提交卡片后,服务器用你的 TikHub Key 为你采集。采集是异步的,通常 30 秒到 2 分钟完成。
**第四步:检查采集状态**
```bash
curl -s https://brand.brainwork.club/voc/api/research/{id}/pipeline | python3 -m json.tool
```
等 `collect` 步骤的 `status` 变为 `done` 后再继续。
**第五步:查看 VOC 数据**
```bash
# 查看 VOC 列表(前 20 条)
curl -s "https://brand.brainwork.club/voc/api/research/{id}/voc-list?page=1&page_size=20" | python3 -m json.tool
# 查看聚类分析
curl -s "https://brand.brainwork.club/voc/api/research/{id}/focus-state?lens=task" | python3 -m json.tool
```
### 场景 2:查看已有研究的数据
这些只读操作不需要 TikHub Key:
```bash
# 列出所有研究
curl -s https://brand.brainwork.club/voc/api/research/list | python3 -m json.tool
# 获取研究详情
curl -s https://brand.brainwork.club/voc/api/research/{id} | python3 -m json.tool
# 获取 VOC 数据
curl -s "https://brand.brainwork.club/voc/api/research/{id}/voc-list?page=1&page_size=20" | python3 -m json.tool
```
### 场景 3:通过 Chat 发送指令(需要 Key)
```bash
source ~/.copaw/workspaces/mafia-expert/.env
curl -s -X POST "https://brand.brainwork.club/voc/api/research/{id}/chat" \
-H "Content-Type: application/json" \
-H "X-TikHub-Key: $TIKHUB_API_KEY" \
-d '{"message": "按关键词采集"}' | python3 -m json.tool
```
## 与黑手党提案的整合
VOC 数据在黑手党提案流程中的作用:
1. **准备阶段 - UDE 诊断**:VOC 数据中的用户抱怨 = 不满意效果(UDE),直接用于诊断
2. **步骤 1 - 冲突图**:VOC 中矛盾性表达(如"想买但太贵")= 冲突素材
3. **步骤 2 - 惯例揭示**:VOC 中的"大家都这样""行业惯例"表述 = 待打破的惯例
4. **步骤 5 - 提案验证**:VOC 数据量和情感分布 = 提案的实证基础
## 注意事项
- 采集/Chat 写操作必须带 `X-TikHub-Key` header,否则服务器拒绝采集
- 只读操作(list、voc-list、pipeline 等)无需 Key
- 全局缓存机制:同一关键词的数据跨研究复用,不会重复调 API(节省费用)
- TikHub Key 仅通过 HTTPS header 传输,不写入任何 DB
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>商品发布 - 旅智工作台</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
import { useAppStore } from './store/useAppStore'
import { StoreContext } from './store/context'
import TopBar from './components/TopBar'
import FormArea from './components/FormArea'
import AssistantPanel from './components/AssistantPanel'
import OneClickModal from './components/OneClickModal'
import Toast from './components/Toast'
export default function App() {
const store = useAppStore()
return (
<StoreContext.Provider value={store}>
<TopBar />
<div className="main">
<FormArea />
<AssistantPanel />
</div>
<OneClickModal />
<Toast />
</StoreContext.Provider>
)
}
+97
View File
@@ -0,0 +1,97 @@
import { useEffect, useRef, useState } from 'react'
import { useStore } from '../store/context'
import MessageItem from './MessageItem'
/** 右侧:AI 助手面板(快捷生成 / 消息流 / 对话输入) */
export default function AssistantPanel() {
const {
messages, panelCollapsed, setPanelCollapsed,
quickAction, openOC, sendMessage
} = useStore()
const [input, setInput] = useState('')
const msgListRef = useRef(null)
/* 新消息时自动滚动到底部 */
useEffect(() => {
requestAnimationFrame(() => {
const el = msgListRef.current
if (el) el.scrollTop = el.scrollHeight
})
}, [messages])
const quickItems = [
{ type: 'title', icon: 'q1', em: '📝', label: '商品标题' },
{ type: 'intro', icon: 'q2', em: '💎', label: '详细介绍' },
{ type: 'detail', icon: 'q7', em: '📄', label: '商品详情' },
{ type: 'image', icon: 'q5', em: '🖼️', label: '配图建议' },
{ type: 'itinerary', icon: 'q3', em: '🗺️', label: '行程规划' },
{ type: 'price', icon: 'q4', em: '💰', label: '价格库存' },
{ type: 'seo', icon: 'q6', em: '🔍', label: 'SEO 词' },
{ type: 'check', icon: 'q6', em: '🛡️', label: '合规检查' }
]
const send = () => {
const text = input.trim()
if (!text) return
setInput('')
sendMessage(text)
}
return (
<div className={'ai-panel' + (panelCollapsed ? ' collapsed' : '')}>
<button className="ai-float" onClick={() => setPanelCollapsed(!panelCollapsed)}>AI 助手</button>
<div className="ai-header">
<div className="ai-avatar">🤖</div>
<div className="info">
<div className="name">旅智小助 <span className="badge">商品发布专精</span></div>
<div className="status">在线 · 已读取当前页面表单内容</div>
</div>
<div className="actions">
<button className="icon-btn" title="收起面板" onClick={() => setPanelCollapsed(!panelCollapsed)}>»</button>
</div>
</div>
<div className="ai-quick">
<div className="title">⚡ 快捷生成(对应左侧 6 大模块)</div>
<button className="oc-wide" onClick={openOC}>⚡ 一键发品 · 30 秒完成 6 大模块</button>
<div className="quick-grid">
{quickItems.map(q => (
<button key={q.type} className="quick-btn" onClick={() => quickAction(q.type)}>
<span className={'qicon ' + q.icon}>{q.em}</span>
<span className="qtext">{q.label}</span>
</button>
))}
</div>
</div>
<div className="ai-messages" ref={msgListRef}>
{messages.map(m => <MessageItem key={m.id} msg={m} />)}
</div>
<div className="ai-input-area">
<div className="input-wrap">
<textarea
rows={1}
placeholder="例如:帮我优化标题和详情 / 帮我发个海南亲子6天自由行"
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
send()
}
}}
/>
<div className="input-foot">
<div className="input-tools">
<button className="tool-btn" title="参考示例">💡</button>
<button className="tool-btn" title="上传图片">🖼️</button>
</div>
<button className="send-btn" onClick={send}>发送</button>
</div>
</div>
<div className="input-hint">Enter 发送 · Shift+Enter 换行 · 内容由 AI 生成,请核对后发布</div>
</div>
</div>
)
}
+35
View File
@@ -0,0 +1,35 @@
import { useStore } from '../store/context'
import TitleCard from './cards/TitleCard'
import IntroCard from './cards/IntroCard'
import DetailCard from './cards/DetailCard'
import ImageCard from './cards/ImageCard'
import ItineraryCard from './cards/ItineraryCard'
import PriceCard from './cards/PriceCard'
/** 左侧:商品发布表单(6 大模块卡片) */
export default function FormArea() {
const { showToast, openOC, sectionRefs, formAreaRef } = useStore()
const setRef = id => el => { sectionRefs.current[id] = el }
return (
<div className="form-area" ref={formAreaRef}>
<div className="page-head">
<div>
<h1>发布新商品</h1>
<div className="sub">完善商品信息,或用「一键发品」30 秒搞定 · 右侧 AI 助手随时待命 ↗</div>
</div>
<div style={{ display: 'flex', gap: 10 }}>
<button className="btn-ghost-w" onClick={() => showToast('已提交审核(演示)')}>提交审核</button>
<button className="btn-magic" onClick={openOC}>⚡ 一键发品</button>
</div>
</div>
{/* 每张卡片把根节点注册到 sectionRefs(AI 填入后高亮 + 滚动定位) */}
<TitleCard setRef={setRef} />
<IntroCard setRef={setRef} />
<DetailCard setRef={setRef} />
<ImageCard setRef={setRef} />
<ItineraryCard setRef={setRef} />
<PriceCard setRef={setRef} />
</div>
)
}
+63
View File
@@ -0,0 +1,63 @@
import { useStore } from '../store/context'
/** 生成卡片:head / lines / foot / tips,foot 按钮为真实 React 事件 */
export default function GenCardView({ gen, confirm }) {
const { applyGen, regen, showToast, serialConfirm, serialRegen, countdown } = useStore()
const { card } = gen
const serial = !!confirm
const body = (
<div className="gen-card">
<div className="gc-head">{card.head}</div>
<div className="gc-body">
{card.lines.map((l, i) => <span key={i} className="line" dangerouslySetInnerHTML={{ __html: l }} />)}
</div>
{card.foot && (
<div className="gc-foot">
<button
className="gc-btn"
onClick={() => card.foot.apply ? applyGen(card.foot.apply) : showToast('SEO 词已复制(演示)')}
>{card.foot.label}</button>
{card.foot.regen && <button className="gc-btn ghost" onClick={() => regen(gen.type)}>🔄 换一版</button>}
</div>
)}
{card.tips.length > 0 && (
<div className="tips-row">
{card.tips.map((t, i) => <span key={i} className={'tip-pill ' + t.cls}>{t.text}</span>)}
</div>
)}
</div>
)
/* 串行流程:外层包 serial-card,底部附确认栏 */
if (serial) {
return (
<div className="serial-card">
<div dangerouslySetInnerHTML={{ __html: gen.pre }} />
{body}
{confirm.status === 'confirmed' ? (
<div className="ag-confirm-bar" style={{ opacity: 0.55 }}>
<span style={{ color: '#22c55e', fontSize: 12, fontWeight: 600 }}>✓ 已确认,填入表单中…</span>
</div>
) : (
<div className="ag-confirm-bar">
<button className="ag-cf-btn" onClick={serialConfirm}>
{card.foot?.apply ? '✅ 确认填入' : '✅ 确认'}
</button>
<button className="ag-regen-btn" onClick={serialRegen}>🔄 换一版</button>
{confirm.mode
? <span className="ag-cd">{countdown}s 后自动确认</span>
: <span className="ag-cd manual">⏸ 等待您确认…</span>}
</div>
)}
</div>
)
}
return (
<div>
<div dangerouslySetInnerHTML={{ __html: gen.pre }} />
{body}
</div>
)
}
+20
View File
@@ -0,0 +1,20 @@
import GenCardView from './GenCardView'
import TaskCardView from './TaskCardView'
/** 单条消息:typing 占位 / 富文本 / 生成卡片 / Agent 任务清单 */
export default function MessageItem({ msg }) {
const role = msg.role
return (
<div className={'msg ' + role}>
<div className="m-avatar">{role === 'ai' ? '🤖' : '李'}</div>
<div className="bubble">
{msg.kind === 'typing' && (
<div className="typing"><span /><span /><span /></div>
)}
{msg.kind === 'html' && <div dangerouslySetInnerHTML={{ __html: msg.html }} />}
{msg.kind === 'gen' && <GenCardView gen={msg.gen} confirm={msg.confirm} />}
{msg.kind === 'task' && <TaskCardView task={msg.task} />}
</div>
</div>
)
}
+118
View File
@@ -0,0 +1,118 @@
import { useStore } from '../store/context'
import { DEST_OPTIONS, DAY_OPTIONS, TYPE_OPTIONS, THEME_OPTIONS } from './cards/TitleCard'
import { ocLevelInfo } from '../lib/oc'
/** 一键发品弹窗:配置 → 生成进度 → 完成(三视图状态机) */
export default function OneClickModal() {
const { ocModal, closeOC, setOCField, startOC, showToast, ocStore } = useStore()
if (!ocModal.open) return null
const lv = ocLevelInfo(ocStore)
const lvPct = ocStore.count >= 10 ? 100 : (ocStore.count / 10) * 100
const stepMeta = [
['商品标题', '已选高转化方案'],
['详细介绍', '四段式卖点'],
['商品详情', '图文结构就绪'],
['商品图片', '配图清单 6 张'],
['行程编辑', '逐日框架就绪'],
['价格库存', '团期+定价就绪']
]
return (
<div className="modal-mask show" onClick={e => { if (e.target === e.currentTarget) closeOC() }}>
<div className="modal">
{/* 视图 1:配置 */}
{ocModal.view === 'config' && (
<div>
<h3>⚡ 一键发品</h3>
<div className="sub">选好基础信息,AI 自动完成 6 大模块填写,30 秒搞定发品</div>
{ocModal.learnVisible && (
<div className="learn-hint" dangerouslySetInnerHTML={{ __html: ocModal.learnHtml }} />
)}
<div className="stat-row">
<div className="stat-box"><div className="v">{ocStore.count}</div><div className="k">累计发品(次)</div></div>
<div className="stat-box"><div className="v">{ocStore.prefs.length}</div><div className="k">已沉淀偏好(条)</div></div>
<div className="stat-box"><div className="v">1<small> 分钟</small></div><div className="k">发品耗时(传统约 30 分钟)</div></div>
</div>
<div className="level-wrap">
<div className="level-text"><span>{lv.name}</span><span>{lv.next}</span></div>
<div className="level-bar"><i style={{ width: lvPct + '%' }} /></div>
</div>
<div className="conf-row">
<div className="form-item">
<label>目的地</label>
<select value={ocModal.dest} onChange={e => setOCField('dest', e.target.value)}>
{DEST_OPTIONS.map(o => <option key={o}>{o}</option>)}
</select>
</div>
<div className="form-item">
<label>天数</label>
<select value={ocModal.days} onChange={e => setOCField('days', e.target.value)}>
{DAY_OPTIONS.map(o => <option key={o}>{o}</option>)}
</select>
</div>
<div className="form-item">
<label>类型</label>
<select value={ocModal.type} onChange={e => setOCField('type', e.target.value)}>
{TYPE_OPTIONS.map(o => <option key={o}>{o}</option>)}
</select>
</div>
</div>
<div className="form-item">
<label>适合人群</label>
<div className="chips">
{THEME_OPTIONS.map(t => (
<span
key={t}
className={'chip' + (ocModal.theme === t ? ' active' : '')}
onClick={() => setOCField('theme', t)}
>{t}</span>
))}
</div>
</div>
<div className="modal-foot">
<button className="btn-ghost-w" onClick={closeOC}>取消</button>
<button className="btn-magic" onClick={startOC}>⚡ 开始生成</button>
</div>
</div>
)}
{/* 视图 2:生成进度 */}
{ocModal.view === 'progress' && (
<div>
<h3>🤖 AI 正在发品…</h3>
<div className="sub">按您的偏好逐个模块生成,完成后可随时接手微调</div>
<div className="step-list">
{stepMeta.map(([label, note], i) => {
const st = ocModal.steps[i]?.status || 'todo'
return (
<div key={i} className={'step-item ' + st}>
<span className="s-icon">{st === 'done' ? '✓' : st === 'doing' ? '↻' : '·'}</span>
<span>{label}</span>
<span className="s-note">{st === 'doing' ? '生成中…' : st === 'done' ? note : '等待中'}</span>
</div>
)
})}
</div>
</div>
)}
{/* 视图 3:完成 */}
{ocModal.view === 'done' && (
<div>
<div className="done-view">
<div className="big">🎉</div>
<h4>发品完成!</h4>
<div className="dsub">6 大模块已全部生成,请核对后提交审核</div>
</div>
<div className="learn-hint" dangerouslySetInnerHTML={{ __html: ocModal.doneHtml }} />
<div className="modal-foot">
<button className="btn-ghost-w" onClick={closeOC}>返回核对</button>
<button className="btn-magic" onClick={() => { closeOC(); showToast('已提交审核(演示)') }}>提交审核</button>
</div>
</div>
)}
</div>
</div>
)
}
+33
View File
@@ -0,0 +1,33 @@
import { useStore } from '../store/context'
/** Agent 任务清单卡片:8 步/多步执行状态实时更新 */
export default function TaskCardView({ task }) {
const { setAutoConfirm, autoConfirmMode } = useStore()
return (
<div className="agent-card">
<div className="ag-head">{task.title}</div>
<div className="ag-ctx">{task.ctxLine}</div>
<div className="ag-mode-row">
<span className="ag-mode-label">确认模式</span>
<button
className={'ag-mt-btn' + (autoConfirmMode ? ' active' : '')}
onClick={() => setAutoConfirm(true)}
>⚡ 自动确认</button>
<button
className={'ag-mt-btn' + (!autoConfirmMode ? ' active' : '')}
onClick={() => setAutoConfirm(false)}
>✋ 手动确认</button>
</div>
<div className="ag-steps">
{task.steps.map((s, i) => (
<div key={i} className={'ag-step ' + s.status}>
<span className="ag-ico">{s.status === 'done' ? '✓' : s.status === 'doing' ? '↻' : '·'}</span>
<span className="ag-lbl">{s.label}</span>
<span className="ag-note">{s.note}</span>
</div>
))}
</div>
<div className="ag-foot">💡 每个工具生成后展示卡片,自动模式 5s 后自动确认填入,手动模式需您点击确认。可随时切换模式。</div>
</div>
)
}
+21
View File
@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react'
import { useStore } from '../store/context'
/** 全局轻提示(1.8s 自动消失,与 v1 行为一致) */
export default function Toast() {
const { toastState } = useStore()
const [visible, setVisible] = useState(false)
useEffect(() => {
if (!toastState) return
setVisible(true)
const t = setTimeout(() => setVisible(false), 1800)
return () => clearTimeout(t)
}, [toastState])
return (
<div className={'toast' + (visible ? ' show' : '')}>
{toastState ? toastState.text : ''}
</div>
)
}
+16
View File
@@ -0,0 +1,16 @@
import { useStore } from '../store/context'
/** 顶部导航:logo / 面包屑 / 草稿状态 */
export default function TopBar() {
const { showToast } = useStore()
return (
<div className="topbar">
<div className="logo"><div className="icon">✈</div> 旅智工作台</div>
<div className="breadcrumb">商品管理 / <b>发布新商品</b></div>
<div className="right">
<span>草稿自动保存于 1 分钟前</span>
<div className="avatar-sm">李</div>
</div>
</div>
)
}
+16
View File
@@ -0,0 +1,16 @@
/** 通用卡片骨架:标题 + AI 内嵌按钮 + 模块序号(refCb 用于 AI 填入后的高亮定位) */
export default function Card({ id, title, aiLabel, aiTitle, onAI, step, refCb, children }) {
return (
<div className="card" id={id} ref={refCb}>
<h2>
<span className="dot" />
{title}
{onAI && (
<button className="ai-inline-btn" title={aiTitle} onClick={onAI}>{aiLabel}</button>
)}
<span className="badge-step">{step}</span>
</h2>
{children}
</div>
)
}
+37
View File
@@ -0,0 +1,37 @@
import { useStore } from '../../store/context'
import Card from './Card'
/** 模块 3/6 —— 商品详情(contenteditable 富文本,AI 可直接写入) */
export default function DetailCard({ setRef }) {
const { formAI, detailRef } = useStore()
return (
<Card
id="card-detail" refCb={setRef('card-detail')} title="商品详情" step="模块 3/6"
aiLabel="✨ AI 生成详情" aiTitle="AI 生成结构化图文详情"
onAI={() => formAI('detail')}
>
<div className="form-item">
<label><span className="req">*</span>详情页图文内容</label>
<div className="rich-editor">
<div className="rich-toolbar">
<button title="加粗">B</button>
<button title="斜体">I</button>
<button title="标题">H</button>
<div className="sep" />
<button title="插入图片">🖼️</button>
<button title="插入视频">🎬</button>
<div className="sep" />
<button title="行程模块">🗺️</button>
<button title="费用说明">¥</button>
</div>
<div
className="rich-content"
ref={el => { detailRef.current = el }}
contentEditable
data-placeholder="支持图文混排,AI 可生成结构化详情内容…"
/>
</div>
</div>
</Card>
)
}
+34
View File
@@ -0,0 +1,34 @@
import { useStore } from '../../store/context'
import Card from './Card'
import { imgPalette } from '../../data/genData'
/** 模块 4/6 —— 商品图片(AI 可按清单批量添加配图占位) */
export default function ImageCard({ setRef }) {
const { images, uploadImg, removeImage, formAI } = useStore()
return (
<Card
id="card-img" refCb={setRef('card-img')} title="商品图片" step="模块 4/6"
aiLabel="✨ AI 配图建议" aiTitle="AI 推荐拍摄脚本与配图清单"
onAI={() => formAI('image')}
>
<div className="form-item">
<label><span className="req">*</span>主图与详情图(首张为主图,建议 800×600 以上)</label>
<div className="img-grid">
{images.map((im, i) => {
const p = imgPalette[i % imgPalette.length]
return (
<div key={i} className="img-tile" style={{ background: p.g }}>
<span className="tag">{i === 0 ? '主图' : '详情图 ' + i}</span>
{im.ai && <span className="ai-tag">AI 建议</span>}
<span className="em">{p.em}</span><span>{im.label}</span>
<button className="del" onClick={() => removeImage(i)}>✕</button>
</div>
)
})}
<div className="img-upload" onClick={uploadImg}><span className="plus">+</span>上传图片</div>
</div>
<div className="hint" style={{ marginTop: 10 }}>AI 可根据行程内容推荐拍摄脚本与配图清单</div>
</div>
</Card>
)
}
+26
View File
@@ -0,0 +1,26 @@
import { useStore } from '../../store/context'
import Card from './Card'
/** 模块 2/6 —— 详细介绍(列表页卖点摘要) */
export default function IntroCard({ setRef }) {
const { form, updateForm, formAI, introHighlight } = useStore()
return (
<Card
id="card-intro" refCb={setRef('card-intro')} title="详细介绍" step="模块 2/6"
aiLabel="✨ AI 生成卖点" aiTitle="AI 生成四段式卖点摘要"
onAI={() => formAI('intro')}
>
<div className="form-item">
<label><span className="req">*</span>详细介绍(列表页展示的卖点摘要)</label>
<textarea
rows={5}
placeholder="突出行程亮点、住宿标准、服务承诺等核心卖点…"
value={form.intro}
className={introHighlight ? 'field-highlight' : ''}
onChange={e => updateForm('intro', e.target.value)}
/>
<div className="hint">建议 150-300 字,聚焦用户最关心的 3-4 个卖点</div>
</div>
</Card>
)
}
@@ -0,0 +1,40 @@
import { useStore } from '../../store/context'
import Card from './Card'
/** 模块 5/6 —— 行程编辑(逐日行程卡片) */
export default function ItineraryCard({ setRef }) {
const { itineraryDays, addDay, removeDay, updateDay, formAI } = useStore()
return (
<Card
id="card-itinerary" refCb={setRef('card-itinerary')} title="行程编辑" step="模块 5/6"
aiLabel="✨ AI 规划行程" aiTitle="AI 按天数生成逐日行程"
onAI={() => formAI('itinerary')}
>
{itineraryDays.length === 0 ? (
<div className="empty-tip">暂无行程,点击下方按钮添加,或让 AI 一键生成 ↗</div>
) : (
itineraryDays.map((d, i) => (
<div key={i} className="day-card">
<div className="day-head">
<span className="dnum">D{i + 1}</span>
<input
value={d.title}
placeholder="当日主题,如:抵达昆明·入住酒店"
onChange={e => updateDay(i, 'title', e.target.value)}
/>
<button className="del-day" title="删除当天" onClick={() => removeDay(i)}>✕</button>
</div>
<div className="day-body">
<textarea
placeholder="当日安排描述…"
value={d.desc}
onChange={e => updateDay(i, 'desc', e.target.value)}
/>
</div>
</div>
))
)}
<button className="add-day" onClick={addDay}>+ 添加一天行程</button>
</Card>
)
}
+56
View File
@@ -0,0 +1,56 @@
import { useStore } from '../../store/context'
import Card from './Card'
/** 模块 6/6 —— 价格库存(团期排期表) */
export default function PriceCard({ setRef }) {
const { priceRows, addPriceRow, removePriceRow, updatePriceRow, togglePriceOn, formAI } = useStore()
return (
<Card
id="card-price" refCb={setRef('card-price')} title="价格库存" step="模块 6/6"
aiLabel="✨ AI 排期定价" aiTitle="AI 生成团期排期与定价方案"
onAI={() => formAI('price')}
>
<table className="price-table">
<thead>
<tr>
<th style={{ width: 150 }}>出发日期</th>
<th>成人价(元)</th>
<th>儿童价(元)</th>
<th style={{ width: 90 }}>库存</th>
<th style={{ width: 80 }}>状态</th>
<th style={{ width: 40 }} />
</tr>
</thead>
<tbody>
{priceRows.length === 0 ? (
<tr><td colSpan={6}><div className="empty-tip">暂无团期,点击下方按钮添加,或让 AI 生成排期方案 ↗</div></td></tr>
) : (
priceRows.map((r, i) => (
<tr key={i}>
<td><input value={r.date} onChange={e => updatePriceRow(i, 'date', e.target.value)} /></td>
<td><input type="number" value={r.adult} onChange={e => updatePriceRow(i, 'adult', e.target.value)} /></td>
<td><input type="number" value={r.child} onChange={e => updatePriceRow(i, 'child', e.target.value)} /></td>
<td>
<input
type="number" value={r.stock}
className={r.stock < 6 ? 'stock-low' : ''}
onChange={e => updatePriceRow(i, 'stock', +e.target.value)}
/>
</td>
<td>
<span
className={'status-pill ' + (r.on ? 'status-on' : 'status-off')}
style={{ cursor: 'pointer' }}
onClick={() => togglePriceOn(i)}
>{r.on ? '在售' : '停售'}</span>
</td>
<td><button className="del-row" onClick={() => removePriceRow(i)}>✕</button></td>
</tr>
))
)}
</tbody>
</table>
<button className="add-day" style={{ marginTop: 10 }} onClick={addPriceRow}>+ 添加团期</button>
</Card>
)
}
+65
View File
@@ -0,0 +1,65 @@
import { useStore } from '../../store/context'
import Card from './Card'
export const DEST_OPTIONS = ['云南(昆明/大理/丽江)', '四川(成都/九寨沟)', '海南(三亚)', '新疆(乌鲁木齐/喀纳斯)', '西藏(拉萨/林芝)']
export const DAY_OPTIONS = ['6天5晚', '3天2晚', '5天4晚', '7天6晚', '8天7晚']
export const TYPE_OPTIONS = ['跟团游', '自由行', '定制游', '半自助游', '当地玩乐']
export const THEME_OPTIONS = ['亲子家庭', '蜜月情侣', '银发无忧', '摄影爱好', '小众深度']
/** 模块 1/6 —— 商品标题(含目的地/天数/类型/人群 上下文) */
export default function TitleCard({ setRef }) {
const { form, updateForm, formAI, titleHighlight } = useStore()
return (
<Card
id="card-title" refCb={setRef('card-title')} title="商品标题" step="模块 1/6"
aiLabel="✨ AI 生成标题" aiTitle="AI 生成 3 个高转化标题"
onAI={() => formAI('title')}
>
<div className="form-row">
<div className="form-item" style={{ flex: 2 }}>
<label><span className="req">*</span>商品标题</label>
<input
type="text" maxLength={40}
placeholder="例:云南昆明+大理+丽江6日5晚纯玩跟团游"
value={form.title}
className={titleHighlight ? 'field-highlight' : ''}
onChange={e => updateForm('title', e.target.value)}
/>
<div className="counter"><span>{form.title.length}</span>/40 字</div>
</div>
<div className="form-item">
<label>目的地</label>
<select value={form.dest} onChange={e => updateForm('dest', e.target.value)}>
{DEST_OPTIONS.map(o => <option key={o}>{o}</option>)}
</select>
</div>
</div>
<div className="form-row">
<div className="form-item">
<label>行程天数</label>
<select value={form.days} onChange={e => updateForm('days', e.target.value)}>
{DAY_OPTIONS.map(o => <option key={o}>{o}</option>)}
</select>
</div>
<div className="form-item">
<label>商品类型</label>
<select value={form.type} onChange={e => updateForm('type', e.target.value)}>
{TYPE_OPTIONS.map(o => <option key={o}>{o}</option>)}
</select>
</div>
<div className="form-item" style={{ flex: 2 }}>
<label>适合人群</label>
<div className="chips">
{THEME_OPTIONS.map(t => (
<span
key={t}
className={'chip' + (form.theme === t ? ' active' : '')}
onClick={() => updateForm('theme', t)}
>{t}</span>
))}
</div>
</div>
</div>
</Card>
)
}
+245
View File
@@ -0,0 +1,245 @@
/* ============================================================
* 工具(Tool)定义:Agent 可调用的 8 个生成工具
* buildGen(type, ctx) 返回结构化结果:
* pre —— 助手开场白(应用内静态 HTML,非用户输入)
* card —— 生成卡片结构(head / lines / foot / tips),由 GenCardView 渲染
* data —— 填入表单的结构化数据
* ============================================================ */
/** 自主发品 Agent 的 8 步任务清单 */
export const agentSteps = [
{ label: '商品标题', type: 'title', note: '3 方案·已选高转化款填入' },
{ label: '详细介绍', type: 'intro', note: '四段式卖点已填入' },
{ label: '商品详情', type: 'detail', note: '图文结构已写入编辑器' },
{ label: '商品图片', type: 'image', note: '6 张配图清单已添加' },
{ label: '行程编辑', type: 'itinerary', note: '逐日行程已填入' },
{ label: '价格库存', type: 'price', note: '5 个团期 + 定价已填入' },
{ label: 'SEO 关键词', type: 'seo', note: '6 个高流量词已提取' },
{ label: '合规检查', type: 'check', note: '扫描完成·0 极限词风险' }
]
/** 一键发品的 6 步流程 */
export const ocStepsDef = [
{ label: '商品标题', type: 'title', note: '已选高转化方案' },
{ label: '详细介绍', type: 'intro', note: '四段式卖点' },
{ label: '商品详情', type: 'detail', note: '图文结构就绪' },
{ label: '商品图片', type: 'image', note: '配图清单 6 张' },
{ label: '行程编辑', type: 'itinerary', note: '逐日框架就绪' },
{ label: '价格库存', type: 'price', note: '团期+定价就绪' }
]
/** 各工具的动作名称(对话中展示用) */
export const actionLabel = {
title: '生成商品标题', intro: '生成详细介绍', detail: '生成商品详情', image: '配图建议',
itinerary: '规划行程', price: '价格库存方案', seo: 'SEO 关键词', check: '合规检查'
}
/** 图片占位调色板 */
export const imgPalette = [
{ g: 'linear-gradient(135deg,#ffb300,#ff7a1a)', em: '🚤' },
{ g: 'linear-gradient(135deg,#f59e0b,#ef4444)', em: '🏮' },
{ g: 'linear-gradient(135deg,#8b5cf6,#ec4899)', em: '🏔️' },
{ g: 'linear-gradient(135deg,#22c55e,#ffb300)', em: '🏨' },
{ g: 'linear-gradient(135deg,#f97316,#facc15)', em: '🍲' },
{ g: 'linear-gradient(135deg,#6366f1,#ffb300)', em: '🌅' }
]
/** 表单模块 → 卡片锚点(滚动定位用) */
export const cardMap = {
title: 'card-title', intro: 'card-intro', detail: 'card-detail',
image: 'card-img', itinerary: 'card-itinerary', price: 'card-price'
}
/** 生成 8 类工具的结构化结果(ctx 为当前表单上下文) */
export function buildGen(type, ctx) {
const { dest, days, type: pType, theme } = ctx
const isYn = dest === '云南'
switch (type) {
case 'title': {
const t = [
`【纯玩无购物】${dest}${days}精品${pType}·5A景区全含·${theme}首选`,
`${dest}${days}深度${pType}|一价全含0自费·住${isYn ? '海景' : '景区'}酒店`,
`【${theme}必选】${dest}${days}${pType}·随走随停·金牌向导·赠${isYn ? '洱海游船' : '旅拍'}体验`
]
return {
pre: `结合 <span class="hl">${dest}·${days}·${pType}·${theme}</span> 生成 3 个高点击标题(可多次点击填入切换方案):`,
card: {
head: '✨ 标题方案(三选一)',
lines: t.map((x, i) => `<b>${i + 1}.</b> ${x}`),
foot: { apply: 'title', label: '⚡ 一键填入「商品标题」', regen: true },
tips: [
{ cls: 'ok-pill', text: '含目的地+天数+类型,利于搜索' },
{ cls: '', text: '「无购物」为平台高转化词' }
]
},
data: t
}
}
case 'intro': {
const d = `【${dest}${days}${pType}·${theme}优选】全程 0 购物 0 自费,景区门票一价全含,金牌持证导游全程讲解。\n精选 ${days === '3天2晚' ? '2' : '4'} 晚高分酒店,${isYn ? '大理海景房 + 丽江古城客栈' : '近景区步行可达'},住的舒心才是真度假。\n特色体验:${isYn ? '白族扎染 + 洱海游船 + 野生菌火锅' : '深度环线,不赶路'},孩子玩得开心、大人拍得尽兴。\n服务承诺:行程透明无隐形消费,24h 专属管家,未成团提前 3 天全额退款。`
return {
pre: `围绕「${theme}」人群痛点,按「亮点—住宿—体验—承诺」四段式生成详细介绍:`,
card: {
head: '✨ 详细介绍(列表页卖点摘要)',
lines: d.split('\n'),
foot: { apply: 'intro', label: '⚡ 一键填入「详细介绍」', regen: true },
tips: [
{ cls: 'ok-pill', text: '约 200 字,符合最佳展示长度' },
{ cls: '', text: '含服务承诺,降低决策门槛' }
]
},
data: d
}
}
case 'detail': {
return {
pre: '已生成结构化商品详情(亮点/住宿/餐饮/保障 + 配图占位),可直接在编辑器中继续润色:',
card: {
head: '✨ 商品详情(图文结构)',
lines: [
`<b>行程亮点</b> · 0购物0自费 / 经典IP全含 / ${theme}节奏`,
`<b>住宿安排</b> · ${isYn ? '海景房+古城客栈' : '高分酒店'}`,
`<b>舌尖体验</b> · ${isYn ? '野生菌火锅+风味餐' : '特色餐 3 次'}`,
`<b>服务保障</b> · 金牌导游 / 24h管家 / 全额退款承诺`,
`<b>配图占位</b> · 行程地图 / 酒店实拍 / 餐饮实拍`
],
foot: { apply: 'detail', label: '⚡ 写入「商品详情」编辑器', regen: true },
tips: []
},
data: `<h3>🌟 行程亮点</h3>
<ul>
<li>全程 <b>0 购物 0 自费</b>,行程透明,把时间还给风景</li>
<li>${dest}经典 IP 全含:${isYn ? '石林、洱海、玉龙雪山、丽江古城' : '核心景区深度游览'}</li>
<li>${theme}专属节奏:每日行车 ≤ 3.5 小时,不赶路不早起(可睡到自然醒)</li>
</ul>
<h3>🏨 住宿安排</h3>
<ul>
<li>${isYn ? '大理 1 晚海景房 + 丽江 2 晚古城客栈' : '全程高分舒适型酒店'}</li>
<li>提前公布酒店名单,拒绝到店换酒店</li>
</ul>
<h3>🍜 舌尖体验</h3>
<ul>
<li>${isYn ? '野生菌火锅 + 白族风味餐 + 过桥米线' : '当地特色风味餐 3 次'}</li>
</ul>
<h3>🛡️ 服务保障</h3>
<ul>
<li>持证金牌导游 + 24h 专属管家</li>
<li>未成团提前 3 天全额退款,出行无忧</li>
</ul>
<div class="img-slot">🖼️ 此处建议插入:行程地图 / 实拍酒店 / 餐饮实拍(见「配图建议」)</div>`
}
}
case 'image': {
const list = [
{ label: `${dest}全景主图(横版大图)`, use: '主图' },
{ label: '酒店实拍·房间内饰', use: '详情' },
{ label: '核心景区打卡机位', use: '详情' },
{ label: '特色餐食实拍', use: '详情' },
{ label: `${theme}互动瞬间抓拍`, use: '详情' },
{ label: '行程地图示意', use: '详情' }
]
return {
pre: '根据行程内容生成 <b>6 张配图拍摄脚本</b>(含用途与构图建议):',
card: {
head: '✨ 配图清单(转化率优选顺序)',
lines: list.map((im, i) => `<b>${i + 1}.</b> ${im.label} <span style="color:#86909c;font-size:11px">[${im.use}]</span>`),
foot: { apply: 'image', label: '⚡ 按清单添加到「商品图片」', regen: true },
tips: [
{ cls: 'ok-pill', text: '主图含地标,点击率 +18%' },
{ cls: '', text: '实拍图 > 精修图,差评更少' }
]
},
data: list.map(x => ({ label: x.use === '主图' ? x.label : x.label.replace(/(.*)/, '') }))
}
}
case 'itinerary': {
const dayList = [
{ title: isYn ? '抵达昆明·专车接站' : `抵达${dest}·专车接站`, desc: '落地即接,24h 接站服务,入住酒店后自由活动,推荐逛逛翠湖公园。' },
{ title: isYn ? '石林风景区 → 楚雄' : '核心景区深度游', desc: '上午游览 5A 石林,午后赴楚雄,晚观彝族歌舞表演。' },
{ title: isYn ? '大理古城 → 洱海游船 → 双廊' : '慢节奏主题日', desc: '上午漫步大理古城,午后洱海游船,双廊海边下午茶,看日落。' },
{ title: isYn ? '丽江古城 → 玉龙雪山 → 蓝月谷' : '自由活动日', desc: '索道登玉龙雪山4506m,蓝月谷拍照,晚逛丽江四方街。' },
{ title: isYn ? '束河古镇 → 返回昆明' : '自由活动·休整', desc: '睡到自然醒,逛束河古镇,午后大巴返回昆明。' },
{ title: '返程日·专车送站', desc: '根据航班时间安排送站,行程圆满结束。' }
].slice(0, parseInt(days) || 6)
return {
pre: `按 <span class="hl">${days}·${pType}·${theme}</span> 节奏规划每日行程,张弛有度:`,
card: {
head: '✨ 行程框架(每日含主题与安排)',
lines: dayList.map((d, i) => `<b>D${i + 1}</b> ${d.title}`),
foot: { apply: 'itinerary', label: '⚡ 填入「行程编辑」', regen: true },
tips: [
{ cls: 'ok-pill', text: `每日行车 ≤ 3.5h,适合${theme}` },
{ cls: '', text: 'D1/D6 只做接驳,差评率更低' }
]
},
data: dayList
}
}
case 'price': {
const rows = [
{ date: '2026-09-12', adult: 3299, child: 2299, stock: 18, on: true },
{ date: '2026-09-26', adult: 3499, child: 2399, stock: 12, on: true },
{ date: '2026-10-01', adult: 4199, child: 2899, stock: 25, on: true },
{ date: '2026-10-03', adult: 4199, child: 2899, stock: 5, on: true },
{ date: '2026-10-17', adult: 3399, child: 2349, stock: 20, on: true }
]
return {
pre: `基于近 30 天 <span class="hl">${dest}·${days}·${pType}</span> 成交数据,生成 5 个团期排期与定价(含国庆档):`,
card: {
head: '✨ 价格库存方案',
lines: [
`📊 市场区间:<b>¥2,899-¥4,199</b>(${theme}均价 ¥3,480)`,
`🎯 平季定价:<b>¥3,299</b>(低于均价 5%,点击率预估 +23%)`,
`📈 国庆档:<b>¥4,199</b>·库存充足(预计售罄率 85%)`,
`💡 策略:首月立减 ¥300 冲新品榜,稳定后回调`
],
foot: { apply: 'price', label: '⚡ 填入「价格库存」表', regen: true },
tips: [
{ cls: 'ok-pill', text: '低于均价 5%,排序更优' },
{ cls: 'warn-pill', text: '10-03 团期库存偏低,建议 +10' }
]
},
data: rows
}
}
case 'seo': {
const kw = `${dest}旅游,${dest}${days},${pType},${theme}旅游,${dest}纯玩无购物,${dest}自由行攻略`
const heat = [8200, 6500, 5400, 3900, 2800, 1900]
return {
pre: '提取 <b>6 个高流量关键词</b>,按搜索热度排序:',
card: {
head: '✨ SEO 关键词(月均搜索指数)',
lines: kw.split(',').map((k, i) => `<b>${i + 1}.</b> ${k} <span style="color:#86909c;font-size:11px">热度 ${heat[i]}</span>`),
foot: { apply: null, label: '📋 复制关键词', regen: true },
tips: []
},
data: kw
}
}
case 'check': {
return {
pre: '已对 6 大模块进行 <b>广告法 + 平台规则</b> 双重合规扫描:',
card: {
head: '🛡️ 合规检查报告',
lines: [
'✅ 标题:无「最」「第一」等极限词',
'✅ 详细介绍:无「国家级」「顶级」违禁表述',
'⚠️ 建议:「金牌导游」表述需补充资质证明材料',
'⚠️ 建议:「全额退款」承诺需与退改政策页一致',
'ℹ️ 提示:主图 800×600 以上,避免文字遮挡地标'
],
foot: null,
tips: [
{ cls: 'ok-pill', text: '极限词 0 风险' },
{ cls: 'warn-pill', text: '2 项待补充' },
{ cls: '', text: '审核通过率预估 96%' }
]
},
data: null
}
}
default:
throw new Error('unknown tool type: ' + type)
}
}
+60
View File
@@ -0,0 +1,60 @@
/* ============================================================
* Agent 意图解析层
* 把商家的自然语言(如「帮我发个海南亲子6天自由行」)
* 解析为结构化配置 / 工具调用清单 —— 演示 Agent 的意图理解与编排。
* ============================================================ */
/** 「自主发品」意图识别 */
export const agentIntentRe = /(发品|发布|上架|帮我发|全部生成|全都|六大模块|6大模块|30秒|搞定|一键发)/
/** 目的地/人群 映射表 */
const DEST_MAP = [
['云南', '云南(昆明/大理/丽江)'],
['四川', '四川(成都/九寨沟)'],
['海南', '海南(三亚)'],
['三亚', '海南(三亚)'],
['新疆', '新疆(乌鲁木齐/喀纳斯)'],
['西藏', '西藏(拉萨/林芝)']
]
const THEME_MAP = [
['亲子', '亲子家庭'], ['蜜月', '蜜月情侣'], ['银发', '银发无忧'],
['摄影', '摄影爱好'], ['小众', '小众深度'], ['深度', '小众深度']
]
const TYPES = ['跟团游', '自由行', '定制游', '半自助游', '当地玩乐']
/** 从自然语言里提取配置(目的地/天数/类型/人群) */
export function parseBrief(t) {
const o = {}
for (const [k, v] of DEST_MAP) if (t.includes(k)) { o.dest = v; break }
for (const ty of TYPES) if (t.includes(ty)) { o.type = ty; break }
for (const [k, v] of THEME_MAP) if (t.includes(k)) { o.theme = v; break }
const dm = t.match(/(\d+)\s*[天日]/)
if (dm) {
const n = +dm[1]
o.days = ['3天2晚', '5天4晚', '6天5晚', '7天6晚', '8天7晚'].find(d => d.startsWith(n + '天')) || (n + '天' + (n - 1) + '晚')
}
return o
}
/** 工具关键词表:自然语言 → 工具调用序列(保序去重) */
export const toolKeywords = [
{ type: 'title', kws: ['标题', '名字'] },
{ type: 'intro', kws: ['介绍', '卖点'] },
{ type: 'detail', kws: ['详情'] },
{ type: 'image', kws: ['图', '配图', '拍摄'] },
{ type: 'itinerary', kws: ['行程', '路线', '规划'] },
{ type: 'price', kws: ['价', '定价', '库存', '团期', '多少钱'] },
{ type: 'seo', kws: ['seo', '关键词', '搜索'] },
{ type: 'check', kws: ['合规', '违禁', '广告法'] }
]
/** 从自然语言中提取用户提到的工具(去重、保序) */
export function parseMultiTools(text) {
const lower = text.toLowerCase()
const found = [], seen = new Set()
for (const { type, kws } of toolKeywords) {
if (seen.has(type)) continue
if (kws.some(k => lower.includes(k))) { found.push(type); seen.add(type) }
}
return found
}
+36
View File
@@ -0,0 +1,36 @@
/* ============================================================
* 「越用越好用」偏好学习
* ocStore 持久化到 localStorage:发品次数、沉淀的偏好、最近一次配置。
* ============================================================ */
const OC_KEY = 'fliggy_oc'
export function loadOC() {
try {
return JSON.parse(localStorage.getItem(OC_KEY) || 'null') || { count: 0, prefs: [], last: null }
} catch {
return { count: 0, prefs: [], last: null }
}
}
export function saveOC(store) {
try { localStorage.setItem(OC_KEY, JSON.stringify(store)) } catch { /* 演示环境忽略 */ }
}
/** 成长等级:Lv.1 通用模板 → Lv.2 风格匹配 → Lv.3 爆款复刻 */
export function ocLevelInfo(store) {
if (store.count >= 10) return { name: 'Lv.3 爆款复刻', next: '已解锁:自动复用您的历史爆款结构与定价', pct: 100 }
if (store.count >= 3) return { name: 'Lv.2 风格匹配', next: `再用 ${10 - store.count} 次解锁「Lv.3 爆款复刻」`, pct: (store.count / 10) * 100 }
return { name: 'Lv.1 通用模板', next: `再用 ${3 - store.count} 次解锁「Lv.2 风格匹配」(生成将贴合您的风格)`, pct: (store.count / 10) * 100 }
}
/** 每次发品沉淀的偏好条目 */
export function buildPrefs(destShort, days, type, theme) {
return [
`${destShort}路线偏好:${days}${type}`,
`「${theme}」人群文案风格`,
'纯玩无购物话术偏好',
'平季低于市场 5% 定价策略',
'首月立减冲榜促销习惯'
]
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './styles/global.css'
createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
+10
View File
@@ -0,0 +1,10 @@
import { createContext, useContext } from 'react'
/** 全局 Store 上下文:App 中用 useAppStore() 创建并注入 */
export const StoreContext = createContext(null)
export function useStore() {
const store = useContext(StoreContext)
if (!store) throw new Error('useStore 必须在 <StoreContext.Provider> 内使用')
return store
}
+541
View File
@@ -0,0 +1,541 @@
/* ============================================================
* 应用状态中枢(useAppStore)
* - 表单状态 / 会话消息 / 生成结果缓存 / 串行确认 / 一键发品弹窗
* - 异步编排流(quickAction / agentRun / multiToolRun / startOC)
* 全部通过 ref 读取最新状态,避免闭包过期问题
* ============================================================ */
import { useCallback, useEffect, useRef, useState } from 'react'
import { buildGen, agentSteps, ocStepsDef, actionLabel, cardMap } from '../data/genData'
import { parseBrief, parseMultiTools, agentIntentRe } from '../lib/agent'
import { loadOC, saveOC, ocLevelInfo, buildPrefs } from '../lib/oc'
const delay = ms => new Promise(r => setTimeout(r, ms))
const esc = s => s.replace(/</g, '&lt;')
let msgSeq = 0
const INITIAL_FORM = {
title: '',
dest: '云南(昆明/大理/丽江)',
days: '6天5晚',
type: '跟团游',
theme: '亲子家庭',
intro: ''
}
const WELCOME_1 = `您好,我是 <b>旅智小助</b> 🤖<br>我对应左侧 6 大表单模块,可以帮您:<br>· 📝 商品标题 — 3 个方案一键切换填入<br>· 💎 详细介绍 — 四段式卖点文案<br>· 📄 商品详情 — 结构化图文(含配图占位)<br>· 🖼️ 配图建议 — 6 张图拍摄脚本<br>· 🗺️ 行程编辑 — 逐日行程一键填入<br>· 💰 价格库存 — 团期排期 + 定价策略<br><br>💬 <b>自主发品</b>:说「<b>帮我优化标题和详情</b>」或「<b>帮我发个云南亲子 6 天跟团游</b>」,我会<b>逐个工具生成 → 展示卡片 → 等您确认</b>再填入表单(⚡自动 5s 确认,可切✋手动)~`
const WELCOME_2 = `💡 <b>演示提示</b>:<br>· 说「<b>帮我优化标题和详情</b>」→ 串行调度指定模块<br>· 说「<b>帮我发个海南亲子 6 天自由行</b>」→ 自主调用全部 8 个工具<br>· 每步<b>生成卡片 → 等您确认</b>再填入(⚡自动 5s / ✋手动可随时切换)<br>· 也可点 <b>⚡ 一键发品</b> 用弹窗方式完成<br>都会沉淀偏好,<b>用得越多越懂你</b>。`
const CANNED = [
{ k: ['标题', '名字'], type: 'title' },
{ k: ['介绍', '卖点'], type: 'intro' },
{ k: ['详情'], type: 'detail' },
{ k: ['图', '配图', '拍摄'], type: 'image' },
{ k: ['行程', '规划', '路线'], type: 'itinerary' },
{ k: ['价', '定价', '库存', '团期', '多少钱'], type: 'price' },
{ k: ['seo', '关键词', '搜索'], type: 'seo' },
{ k: ['合规', '违禁', '广告法'], type: 'check' }
]
export function useAppStore() {
/* ---------------- 基础状态 ---------------- */
const [form, setForm] = useState(INITIAL_FORM)
const [images, setImages] = useState([])
const [itineraryDays, setItineraryDays] = useState([])
const [priceRows, setPriceRows] = useState([])
const [messages, setMessages] = useState([])
const [panelCollapsed, setPanelCollapsed] = useState(false)
const [toastState, setToastState] = useState(null) // { text, ts }
const [countdown, setCountdown] = useState(0) // 串行确认倒计时(秒)
const [autoConfirmMode, setAutoConfirmMode] = useState(true)
const [ocStore, setOcStore] = useState(loadOC) // 偏好学习(localStorage 持久化)
const [titleHighlight, setTitleHighlight] = useState(false)
const [introHighlight, setIntroHighlight] = useState(false)
const [ocModal, setOcModal] = useState({
open: false, view: 'config', // config | progress | done
dest: '云南(昆明/大理/丽江)', days: '6天5晚', type: '跟团游', theme: '亲子家庭',
steps: [], learnVisible: false, learnHtml: '', doneHtml: ''
})
/* ---------------- Ref 镜像(供异步流读取最新值) ---------------- */
const formRef = useRef(INITIAL_FORM)
formRef.current = form
const generatedRef = useRef({}) // 每类工具最新生成结果 { data, used }
const serialRef = useRef(null) // { resolve, msgId, index, type, mode }
const autoConfirmRef = useRef(true)
const silentBatchRef = useRef(false) // 一键发品批量模式:静默填入
const detailRef = useRef(null) // 商品详情 contenteditable DOM
const formAreaRef = useRef(null) // 左侧表单滚动容器
const sectionRefs = useRef({}) // 表单卡片锚点 { 'card-title': HTMLElement }
const timerRef = useRef(null)
const ocStoreRef = useRef(ocStore)
ocStoreRef.current = ocStore
const ocModalRef = useRef(ocModal)
ocModalRef.current = ocModal
/* ---------------- 基础动作 ---------------- */
const showToast = useCallback(text => {
if (silentBatchRef.current) return
setToastState({ text: '✓ ' + text, ts: Date.now() })
}, [])
const flashSection = useCallback(cardId => {
if (silentBatchRef.current) return
const el = sectionRefs.current[cardId]
if (!el) return
el.classList.add('section-highlight')
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
setTimeout(() => el.classList.remove('section-highlight'), 1800)
}, [])
/** 读取当前表单上下文(生成依赖表单配置) */
const readForm = useCallback(() => {
const f = formRef.current
return {
dest: f.dest.replace(/(.*)/, '') || '云南',
days: f.days || '6天5晚',
type: f.type || '跟团游',
theme: f.theme || '亲子家庭'
}
}, [])
/* ---------------- 消息流 ---------------- */
const addMsg = useCallback((role, html) => {
const id = 'm' + (++msgSeq)
setMessages(ms => [...ms, { id, role, kind: 'html', html }])
return id
}, [])
const addTyping = useCallback(() => {
const id = 'm' + (++msgSeq)
setMessages(ms => [...ms, { id, role: 'ai', kind: 'typing' }])
return id
}, [])
const patchMsg = useCallback((id, patch) => {
setMessages(ms => ms.map(m => (m.id === id ? { ...m, ...patch } : m)))
}, [])
/** 替换 typing 占位为正式内容 */
const replaceTyping = useCallback((id, patch) => {
patchMsg(id, { kind: patch.kind || 'html', ...patch })
}, [patchMsg])
/* ---------------- 表单写入(applyMap 的 React 版) ---------------- */
const applyGen = useCallback(type => {
const gen = generatedRef.current[type]
if (!gen) return
switch (type) {
case 'title': {
const opts = gen.data
const idx = gen.used || 0
const t = opts[idx % opts.length]
generatedRef.current[type] = { ...gen, used: idx + 1 }
formRef.current = { ...formRef.current, title: t }
setForm(f => ({ ...f, title: t }))
setTitleHighlight(true)
setTimeout(() => setTitleHighlight(false), 1600)
flashSection('card-title')
showToast('已填入商品标题(方案 ' + ((idx % opts.length) + 1) + ')')
break
}
case 'intro': {
formRef.current = { ...formRef.current, intro: gen.data }
setForm(f => ({ ...f, intro: gen.data }))
setIntroHighlight(true)
setTimeout(() => setIntroHighlight(false), 1600)
flashSection('card-intro')
showToast('已填入详细介绍')
break
}
case 'detail': {
if (detailRef.current) detailRef.current.innerHTML = gen.data
flashSection('card-detail')
showToast('已生成商品详情图文结构')
break
}
case 'image': {
setImages(prev => [...prev, ...gen.data.map(im => ({ label: im.label, ai: true }))])
flashSection('card-img')
showToast('已按 AI 清单添加 ' + gen.data.length + ' 张配图')
break
}
case 'itinerary': {
setItineraryDays(gen.data.map(d => ({ ...d })))
flashSection('card-itinerary')
showToast('已填入 ' + gen.data.length + ' 天行程')
break
}
case 'price': {
setPriceRows(gen.data.map(r => ({ ...r })))
flashSection('card-price')
showToast('已生成 ' + gen.data.length + ' 个团期')
break
}
default:
break
}
}, [flashSection, showToast])
/* ---------------- 串行确认机制 ---------------- */
const stopTimer = useCallback(() => {
if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null }
}, [])
const startCountdown = useCallback(() => {
if (!serialRef.current || !serialRef.current.mode) return
stopTimer()
let remaining = 5
setCountdown(5)
timerRef.current = setInterval(() => {
remaining--
if (remaining <= 0) {
stopTimer()
setCountdown(0)
const s = serialRef.current
serialRef.current = null
if (s) s.resolve()
} else {
setCountdown(remaining)
}
}, 1000)
}, [stopTimer])
const serialConfirm = useCallback(() => {
const s = serialRef.current
if (!s) return
stopTimer()
setCountdown(0)
serialRef.current = null
patchMsg(s.msgId, { confirm: { index: s.index, mode: s.mode, status: 'confirmed' } })
s.resolve()
}, [patchMsg, stopTimer])
const serialRegen = useCallback(() => {
const s = serialRef.current
if (!s) return
stopTimer()
const g = buildGen(s.type, readForm())
generatedRef.current[s.type] = { data: g.data, used: 0 }
patchMsg(s.msgId, { gen: { type: s.type, pre: g.pre, card: g.card } })
if (s.mode) startCountdown()
}, [patchMsg, readForm, startCountdown, stopTimer])
const setAutoConfirm = useCallback(val => {
autoConfirmRef.current = val
setAutoConfirmMode(val)
const s = serialRef.current
if (!s) return
stopTimer()
s.mode = val
if (val) startCountdown()
else setCountdown(0)
}, [startCountdown, stopTimer])
/** 串行流程的单步:生成卡片 → 等待确认(Promise) */
const showStepCard = useCallback(async (taskId, index, type, autoMode) => {
const tp = addTyping()
await delay(500 + Math.random() * 400)
const g = buildGen(type, readForm())
generatedRef.current[type] = { data: g.data, used: 0 }
patchMsg(tp, {
kind: 'gen',
gen: { type, pre: g.pre, card: g.card },
confirm: { taskId, index, mode: autoMode, status: 'waiting' }
})
await new Promise(resolve => {
serialRef.current = { resolve, msgId: tp, taskId, index, type, mode: autoMode }
if (autoMode) startCountdown()
})
}, [addTyping, patchMsg, readForm, startCountdown])
/* ---------------- 工具任务清单卡片 ---------------- */
const addTaskCard = useCallback((title, ctxLine, steps) => {
const id = 'm' + (++msgSeq)
setMessages(ms => [...ms, {
id, role: 'ai', kind: 'task',
task: {
title, ctxLine,
steps: steps.map(s => ({ label: s.label, note: '等待中', status: 'todo' }))
}
}])
return id
}, [])
const setTaskStep = useCallback((taskId, i, status, note) => {
setMessages(ms => ms.map(m => {
if (m.id !== taskId) return m
const steps = m.task.steps.map((s, j) => (j === i ? { ...s, status, note: note ?? s.note } : s))
return { ...m, task: { ...m.task, steps } }
}))
}, [])
/* ---------------- 快捷动作 / 对话编排 ---------------- */
const quickAction = useCallback(async (type, silent) => {
if (!silent) addMsg('user', '点击了「' + actionLabel[type] + '」')
const tp = addTyping()
await delay(900 + Math.random() * 600)
const g = buildGen(type, readForm())
generatedRef.current[type] = { data: g.data, used: 0 }
replaceTyping(tp, { kind: 'gen', gen: { type, pre: g.pre, card: g.card } })
}, [addMsg, addTyping, readForm, replaceTyping])
/** 表单模块内嵌 AI 按钮:面板收起时自动展开 */
const formAI = useCallback(type => {
setPanelCollapsed(false)
quickAction(type)
}, [quickAction])
const regen = useCallback(type => quickAction(type, true), [quickAction])
/** 把解析出的配置回写左侧表单 */
const applyBriefToForm = useCallback(b => {
const patch = {}
if (b.dest) patch.dest = b.dest
if (b.days) patch.days = b.days
if (b.type) patch.type = b.type
if (b.theme) patch.theme = b.theme
if (Object.keys(patch).length) {
formRef.current = { ...formRef.current, ...patch }
setForm(f => ({ ...f, ...patch }))
}
}, [])
/** 记录一次发品,沉淀偏好(与一键发品共用记忆) */
const recordPublish = useCallback((destShort, days, type, theme) => {
const prefs = buildPrefs(destShort, days, type, theme)
const prev = ocStoreRef.current
const count = prev.count + 1
const next = {
count,
prefs: prev.prefs.concat(prefs),
last: {
dest: destShort, days, type, theme,
destSel: formRef.current.dest, daysSel: days, typeSel: type,
styleWord: count >= 3 ? `${theme}友好·${count >= 10 ? '爆款结构复刻' : '您的惯用话术'}` : '纯玩无购物'
}
}
ocStoreRef.current = next
saveOC(next)
setOcStore(next)
return { prefs, count }
}, [])
/** 自主发品 Agent:8 工具全量编排 */
const agentRun = useCallback(async rawText => {
const b = parseBrief(rawText)
applyBriefToForm(b)
const f = readForm()
const ctxLine = `${f.dest} · ${f.days} · ${f.type} · ${f.theme}`
addMsg('ai', `🚀 好的,我来自主完成这次发品。<br>已识别配置:<b>${ctxLine}</b>${b.dest ? '(来自您的描述)' : '(沿用当前表单)'}。<br>我将依次调用 <b>8 个工具</b>,每个工具生成后<b>展示卡片等您确认</b>再填入表单:`)
const taskId = addTaskCard('🤖 自主发品任务清单', `📋 上下文:${ctxLine} | 预计 8 步`, agentSteps)
for (let i = 0; i < agentSteps.length; i++) {
const s = agentSteps[i]
setTaskStep(taskId, i, 'doing', '生成中…')
await showStepCard(taskId, i, s.type, autoConfirmRef.current)
applyGen(s.type)
setTaskStep(taskId, i, 'done', s.note)
}
// 沉淀偏好(越用越好用)
const { prefs, count } = recordPublish(f.dest, f.days, f.type, f.theme)
formAreaRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
const lv = ocLevelInfo({ count, prefs: [], last: null })
addMsg('ai', `✅ <b>自主发品完成!</b>6 大模块已全部填入左侧表单,另完成 SEO 关键词提取与合规扫描。<br>本次沉淀偏好 <b>${prefs.length} 条</b>,累计发品 <b>${count} 次</b>(${lv.name})。<br>可直接点顶部「提交审核」,或对任一模块点「🔄 换一版」微调。`)
}, [addMsg, addTaskCard, applyBriefToForm, applyGen, readForm, recordPublish, setTaskStep, showStepCard])
/** 多工具串行编排:仅跑用户指定的模块 */
const multiToolRun = useCallback(async (rawText, toolTypes) => {
const b = parseBrief(rawText)
if (Object.keys(b).length) applyBriefToForm(b)
const f = readForm()
const ctxLine = `${f.dest} · ${f.days} · ${f.type} · ${f.theme}`
const steps = agentSteps.filter(s => toolTypes.includes(s.type))
const labels = steps.map(s => s.label).join(' → ')
addMsg('ai', `🛠️ 好的,我来自主调度 <b>${steps.length}</b> 个模块。<br>已识别上下文:<b>${ctxLine}</b>${b.dest ? '(来自您的描述)' : '(沿用当前表单)'}。<br>将依次执行:<b>${labels}</b>,每个工具生成后<b>展示卡片等您确认</b>再填入表单:`)
const taskId = addTaskCard('🛠️ 多工具串行任务', `📋 ${ctxLine} | 共 ${steps.length} 步`, steps)
for (let i = 0; i < steps.length; i++) {
const s = steps[i]
setTaskStep(taskId, i, 'doing', '生成中…')
await showStepCard(taskId, i, s.type, autoConfirmRef.current)
applyGen(s.type)
setTaskStep(taskId, i, 'done', s.note)
}
const first = cardMap[steps[0].type]
if (first) sectionRefs.current[first]?.scrollIntoView({ behavior: 'smooth', block: 'center' })
addMsg('ai', `✅ <b>${steps.length} 个模块生成完毕!</b>${steps.map(s => s.label).join('、')} 已填入左侧表单。<br>不满意可点对应模块的「🔄 换一版」微调,或说「<b>帮我把这个商品发布了</b>」让我一口气完成全部模块。`)
}, [addMsg, addTaskCard, applyBriefToForm, applyGen, readForm, setTaskStep, showStepCard])
/** 自由对话入口:意图识别 → 多工具编排 / 全量 Agent / 单工具 / 闲聊 */
const sendMessage = useCallback(async text => {
text = (text || '').trim()
if (!text) return
// 若有串行步骤在等待确认,自动确认后继续处理新消息
if (serialRef.current) serialConfirm()
addMsg('user', esc(text))
const tp = addTyping()
await delay(700)
// 优先:多工具串行(用户同时提到 2+ 模块)
const tools = parseMultiTools(text)
if (tools.length >= 2) {
replaceTyping(tp, { html: `明白,我来自主调度 ${tools.length} 个模块 🛠️` })
await delay(400)
multiToolRun(text, tools)
return
}
// 全量自主发品
if (agentIntentRe.test(text)) {
replaceTyping(tp, { html: '明白,我来自主完成这次发品 🚀' })
await delay(400)
agentRun(text)
return
}
const hit = CANNED.find(c => c.k.some(k => text.toLowerCase().includes(k)))
if (hit) {
replaceTyping(tp, { html: '好的,马上为您处理 👇' })
await delay(400)
quickAction(hit.type)
return
}
const ctx = readForm()
const replies = [
`收到!我已读取当前表单:目的地 <span class="hl">${ctx.dest}</span>、类型 <span class="hl">${ctx.type}</span>。您可以点上方快捷按钮生成内容,或直接说需求,例如:<br>·「帮我写个适合${ctx.theme}的标题」<br>·「国庆团期怎么定价」`,
`关于「${esc(text.slice(0, 12))}${text.length > 12 ? '…' : ''}」,建议先在左侧表单选好目的地和天数,我生成的内容会更精准。也可以试试快捷区的 <b>价格库存</b>,我会基于市场数据给出排期方案~`,
`我可以帮您拆解:先定目标人群(${ctx.theme}?蜜月?),再定核心卖点(纯玩/性价比/体验感),然后按框架生成标题→介绍→详情→行程。直接点快捷按钮即可开始 ✨`
]
replaceTyping(tp, { html: replies[Math.floor(Math.random() * replies.length)] })
}, [addMsg, addTyping, agentRun, multiToolRun, quickAction, readForm, replaceTyping, serialConfirm])
/* ---------------- 一键发品(越用越好用) ---------------- */
const openOC = useCallback(() => {
const store = ocStoreRef.current
const last = store.last
setOcModal(prev => {
const next = { ...prev, open: true, view: 'config', steps: [], learnVisible: false, learnHtml: '', doneHtml: '' }
if (last) {
if (last.destSel) next.dest = last.destSel
if (last.daysSel) next.days = last.daysSel
if (last.typeSel) next.type = last.typeSel
if (last.theme) next.theme = last.theme
}
if (store.count > 0 && last) {
next.learnVisible = true
next.learnHtml = `🧠 <b>越用越好用</b>:已根据您最近 ${store.count} 次发品习惯<b>自动预填</b>(${last.dest}·${last.days}·${last.type}·${last.theme}),生成内容将沿用您偏好的「${last.styleWord}」风格。`
}
return next
})
}, [])
const closeOC = useCallback(() => {
setOcModal(prev => ({ ...prev, open: false }))
}, [])
const setOCField = useCallback((k, v) => {
setOcModal(prev => ({ ...prev, [k]: v }))
}, [])
const startOC = useCallback(async () => {
const cfg = ocModalRef.current
const { dest: destSel, days: daysSel, type: typeSel, theme } = cfg
// 同步配置到左侧表单(生成依赖表单上下文)
applyBriefToForm({ dest: destSel, days: daysSel, type: typeSel, theme })
setOcModal(prev => ({ ...prev, view: 'progress', steps: ocStepsDef.map(() => ({ status: 'todo' })) }))
silentBatchRef.current = true
for (let i = 0; i < ocStepsDef.length; i++) {
const s = ocStepsDef[i]
setOcModal(prev => ({ ...prev, steps: prev.steps.map((x, j) => (j === i ? { status: 'doing' } : x)) }))
await delay(650 + Math.random() * 450)
const g = buildGen(s.type, readForm())
generatedRef.current[s.type] = { data: g.data, used: 0 }
applyGen(s.type)
setOcModal(prev => ({ ...prev, steps: prev.steps.map((x, j) => (j === i ? { status: 'done' } : x)) }))
}
silentBatchRef.current = false
// 沉淀偏好(越用越好用的核心)
const dest = destSel.replace(/(.*)/, '')
const { prefs, count } = recordPublish(dest, daysSel, typeSel, theme)
const lv = ocLevelInfo({ count, prefs: [], last: null })
const doneHtml = `🧠 <b>本次为您沉淀 ${prefs.length} 条偏好</b>(累计 ${ocStoreRef.current.prefs.length} 条):<br>` +
prefs.map(p => '· ' + p).join('<br>') +
`<br><b>${lv.name}</b> · ${lv.next}<br>下次一键发品将自动预填并按您的风格生成。`
setOcModal(prev => ({ ...prev, view: 'done', doneHtml }))
showToast('6 大模块已生成完毕')
addMsg('ai', `已完成 <b>一键发品</b> ✅ 标题、介绍、详情、配图、行程、价格库存全部就位。<br>本次沉淀偏好 <b>${ocStoreRef.current.prefs.length} 条</b>,累计发品 <b>${count} 次</b>——用得越多,生成越贴合您的风格。<br>任何模块不满意,点对应快捷按钮「🔄 换一版」即可。`)
}, [addMsg, applyBriefToForm, applyGen, readForm, recordPublish, showToast])
/* ---------------- 表单交互 ---------------- */
const updateForm = useCallback((k, v) => {
formRef.current = { ...formRef.current, [k]: v }
setForm(f => ({ ...f, [k]: v }))
}, [])
const uploadImg = useCallback(() => {
setImages(prev => [...prev, { label: '新上传图片', ai: false }])
showToast('已添加图片占位(演示)')
}, [showToast])
const removeImage = useCallback(i => setImages(prev => prev.filter((_, j) => j !== i)), [])
const addDay = useCallback(() => setItineraryDays(prev => [...prev, { title: '', desc: '' }]), [])
const removeDay = useCallback(i => setItineraryDays(prev => prev.filter((_, j) => j !== i)), [])
const updateDay = useCallback((i, k, v) => {
setItineraryDays(prev => prev.map((d, j) => (j === i ? { ...d, [k]: v } : d)))
}, [])
const addPriceRow = useCallback(() => setPriceRows(prev => [...prev, { date: '2026-10-01', adult: 3299, child: 2299, stock: 20, on: true }]), [])
const removePriceRow = useCallback(i => setPriceRows(prev => prev.filter((_, j) => j !== i)), [])
const updatePriceRow = useCallback((i, k, v) => {
setPriceRows(prev => prev.map((r, j) => (j === i ? { ...r, [k]: v } : r)))
}, [])
const togglePriceOn = useCallback(i => {
setPriceRows(prev => prev.map((r, j) => (j === i ? { ...r, on: !r.on } : r)))
}, [])
/* ---------------- 初始欢迎消息 ---------------- */
useEffect(() => {
let alive = true
;(async () => {
const tp1 = addTyping()
await delay(700)
if (!alive) return
replaceTyping(tp1, { html: WELCOME_1 })
await delay(600)
if (!alive) return
const tp2 = addTyping()
await delay(900)
if (!alive) return
replaceTyping(tp2, { html: WELCOME_2 })
})()
return () => { alive = false }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return {
// 状态
form, images, itineraryDays, priceRows, messages, panelCollapsed,
toastState, countdown, autoConfirmMode, ocStore, ocModal,
titleHighlight, introHighlight,
// refs(组件注册 DOM 用)
detailRef, sectionRefs, formAreaRef,
// 动作
updateForm, uploadImg, removeImage,
addDay, removeDay, updateDay,
addPriceRow, removePriceRow, updatePriceRow, togglePriceOn,
setPanelCollapsed, applyGen, regen, formAI, quickAction,
sendMessage, setAutoConfirm, serialConfirm, serialRegen,
openOC, closeOC, setOCField, startOC,
showToast
}
}
+463
View File
@@ -0,0 +1,463 @@
/*
* 旅智工作台 · 商品发布 AI 助手 —— 全局样式
* 从 v1 单文件版(根目录 index.html)完整迁移,类名与选择器保持不变,
* 保证 React 版与原始 Demo 视觉/交互完全一致。
*/
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
background: #f0f2f5; color: #1f2329;
height: 100vh; overflow: hidden;
display: flex; flex-direction: column;
}
/* ============ 顶部导航 ============ */
.topbar {
height: 52px; background: #fff; border-bottom: 1px solid #e5e6eb;
display: flex; align-items: center; padding: 0 20px; flex-shrink: 0; z-index: 10;
}
.topbar .logo { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 15px; color: #d96a00; }
.topbar .logo .icon {
width: 26px; height: 26px; background: linear-gradient(135deg, #ff7a1a, #ffb300);
border-radius: 7px; display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 14px;
}
.topbar .breadcrumb { margin-left: 24px; font-size: 13px; color: #86909c; }
.topbar .breadcrumb b { color: #1f2329; font-weight: 500; }
.topbar .right { margin-left: auto; display: flex; align-items: center; gap: 16px; font-size: 13px; color: #4e5969; }
.avatar-sm {
width: 30px; height: 30px; border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #8b5cf6); color: #fff;
display: flex; align-items: center; justify-content: center; font-size: 12px;
}
/* ============ 主体布局 ============ */
.main { flex: 1; display: flex; overflow: hidden; }
/* ============ 左侧:表单区 ============ */
.form-area { flex: 1; overflow-y: auto; padding: 20px 24px 60px; }
.page-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.page-head h1 { font-size: 20px; font-weight: 600; }
.page-head .sub { font-size: 13px; color: #86909c; margin-top: 4px; }
.btn-primary {
background: linear-gradient(135deg, #ff7a1a, #ff6600); color: #fff; border: none;
padding: 9px 22px; border-radius: 8px; font-size: 14px; cursor: pointer;
font-weight: 500; transition: opacity .2s;
}
.btn-primary:hover { opacity: .88; }
.card {
background: #fff; border-radius: 12px; border: 1px solid #e5e6eb;
padding: 24px; margin-bottom: 16px;
}
.card h2 { font-size: 15px; font-weight: 600; margin-bottom: 18px; display: flex; align-items: center; gap: 8px; }
.card h2 .dot { width: 4px; height: 14px; background: #ff7a1a; border-radius: 2px; }
.card h2 .badge-step {
margin-left: auto; font-size: 11px; font-weight: 500; color: #86909c;
background: #f7f8fa; border-radius: 12px; padding: 3px 10px;
}
/* 表单模块内嵌 AI 按钮 */
.ai-inline-btn {
margin-left: 8px; font-size: 12px; font-weight: 500;
color: #ff7a1a; background: #fff; border: 1px solid rgba(255, 122, 26, 0.5);
border-radius: 12px; padding: 3px 10px; cursor: pointer;
display: inline-flex; align-items: center; gap: 3px;
transition: all .15s ease; white-space: nowrap;
}
.ai-inline-btn:hover {
background: #ff7a1a; color: #fff; border-color: #ff7a1a;
box-shadow: 0 2px 8px rgba(255, 122, 26, 0.35);
}
.form-row { display: flex; gap: 16px; margin-bottom: 16px; }
.form-item { flex: 1; }
.form-item:last-child { margin-bottom: 0; }
.form-item label { display: block; font-size: 13px; color: #4e5969; margin-bottom: 6px; }
.form-item label .req { color: #f53f3f; margin-right: 2px; }
.form-item input, .form-item select, .form-item textarea {
width: 100%; border: 1px solid #e5e6eb; border-radius: 8px; padding: 9px 12px;
font-size: 13px; background: #fff; transition: border-color .2s, box-shadow .2s; font-family: inherit;
}
.form-item input:focus, .form-item select:focus, .form-item textarea:focus {
outline: none; border-color: #ff7a1a; box-shadow: 0 0 0 3px rgba(255,122,26,.12);
}
.form-item textarea { resize: vertical; min-height: 92px; line-height: 1.7; }
.form-item .hint { font-size: 12px; color: #c9cdd4; margin-top: 4px; }
.counter { font-size: 12px; color: #c9cdd4; margin-top: 4px; text-align: right; }
.field-highlight { border-color: #ff7a1a !important; box-shadow: 0 0 0 3px rgba(255,122,26,.18) !important; }
.section-highlight { outline: 2px solid #ff7a1a; outline-offset: 2px; border-radius: 12px; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
border: 1px solid #e5e6eb; border-radius: 16px; padding: 5px 14px; font-size: 12px;
color: #4e5969; cursor: pointer; transition: all .2s; background: #fff;
}
.chip:hover { border-color: #ff7a1a; color: #d96a00; }
.chip.active { background: rgba(255,122,26,.08); border-color: #ff7a1a; color: #d96a00; }
/* 富文本编辑器 */
.rich-editor { border: 1px solid #e5e6eb; border-radius: 8px; overflow: hidden; }
.rich-toolbar {
display: flex; gap: 2px; padding: 6px 8px; border-bottom: 1px solid #f2f3f5; background: #fafbfc;
}
.rich-toolbar button {
border: none; background: transparent; width: 28px; height: 26px; border-radius: 6px;
cursor: pointer; font-size: 13px; color: #4e5969;
}
.rich-toolbar button:hover { background: #eef1f4; }
.rich-toolbar .sep { width: 1px; background: #e5e6eb; margin: 3px 4px; }
.rich-content {
min-height: 150px; padding: 12px 14px; font-size: 13px; line-height: 1.9; outline: none;
}
.rich-content:empty::before { content: attr(data-placeholder); color: #c9cdd4; }
.rich-content h3 { font-size: 14px; margin: 10px 0 6px; color: #d96a00; }
.rich-content ul { padding-left: 20px; }
.rich-content li { margin-bottom: 4px; }
.rich-content .img-slot {
display: flex; align-items: center; justify-content: center; gap: 6px;
height: 60px; border-radius: 8px; margin: 8px 0; font-size: 12px; color: #86909c;
background: repeating-linear-gradient(45deg, #f7f8fa, #f7f8fa 10px, #f2f3f5 10px, #f2f3f5 20px);
border: 1px dashed #d5d9e0;
}
/* 图片上传 */
.img-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.img-tile {
aspect-ratio: 4/3; border-radius: 10px; position: relative; overflow: hidden;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 6px; color: #fff; font-size: 13px; cursor: default;
}
.img-tile .em { font-size: 26px; }
.img-tile .tag {
position: absolute; top: 6px; left: 6px; font-size: 10px; background: rgba(0,0,0,.35);
padding: 2px 7px; border-radius: 10px; backdrop-filter: blur(2px);
}
.img-tile .ai-tag {
position: absolute; top: 6px; right: 6px; font-size: 10px; background: rgba(255,122,26,.9);
padding: 2px 7px; border-radius: 10px;
}
.img-tile .del {
position: absolute; bottom: 6px; right: 6px; width: 20px; height: 20px; border-radius: 50%;
background: rgba(0,0,0,.4); color: #fff; border: none; font-size: 11px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
}
.img-upload {
aspect-ratio: 4/3; border: 1.5px dashed #c9cdd4; border-radius: 10px; display: flex;
flex-direction: column; align-items: center; justify-content: center; gap: 6px;
color: #86909c; font-size: 12px; cursor: pointer; transition: all .2s; background: #fafbfc;
}
.img-upload:hover { border-color: #ff7a1a; color: #d96a00; }
.img-upload .plus { font-size: 24px; color: #c9cdd4; }
/* 行程编辑 */
.day-card { border: 1px solid #e5e6eb; border-radius: 10px; margin-bottom: 10px; overflow: hidden; }
.day-head {
display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: #fafbfc;
border-bottom: 1px solid #f2f3f5;
}
.day-head .dnum {
background: linear-gradient(135deg, #ff7a1a, #ffb300); color: #fff; font-size: 12px;
font-weight: 600; border-radius: 6px; padding: 3px 10px; flex-shrink: 0;
}
.day-head input {
flex: 1; border: none; outline: none; font-size: 13px; background: transparent; font-family: inherit;
}
.day-head .del-day {
border: none; background: transparent; color: #c9cdd4; cursor: pointer; font-size: 14px;
}
.day-head .del-day:hover { color: #f53f3f; }
.day-body { padding: 10px 12px; }
.day-body textarea {
width: 100%; border: none; outline: none; resize: vertical; font-size: 12px; line-height: 1.7;
min-height: 44px; font-family: inherit; background: transparent; color: #4e5969;
}
.add-day {
width: 100%; border: 1.5px dashed #c9cdd4; background: #fafbfc; border-radius: 10px;
padding: 10px; color: #86909c; font-size: 13px; cursor: pointer; transition: all .2s;
}
.add-day:hover { border-color: #ff7a1a; color: #d96a00; }
/* 价格库存表 */
.price-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.price-table th {
text-align: left; padding: 8px 10px; background: #fafbfc; color: #86909c;
font-weight: 500; font-size: 12px; border-bottom: 1px solid #f2f3f5;
}
.price-table td { padding: 6px 8px; border-bottom: 1px solid #f7f8fa; }
.price-table input {
width: 100%; border: 1px solid transparent; border-radius: 6px; padding: 6px 8px;
font-size: 13px; font-family: inherit; background: transparent;
}
.price-table input:hover, .price-table input:focus { border-color: #e5e6eb; background: #fff; outline: none; }
.price-table input:focus { border-color: #ff7a1a; }
.stock-low { color: #b45309; }
.status-pill { font-size: 11px; border-radius: 10px; padding: 2px 9px; }
.status-on { background: rgba(34,197,94,.1); color: #15803d; }
.status-off { background: #f2f3f5; color: #86909c; }
.del-row { border: none; background: transparent; color: #c9cdd4; cursor: pointer; font-size: 14px; }
.del-row:hover { color: #f53f3f; }
.empty-tip { padding: 18px; text-align: center; color: #c9cdd4; font-size: 13px; }
/* ============ 右侧:AI 助手 ============ */
.ai-panel {
width: 400px; background: #fff; border-left: 1px solid #e5e6eb;
display: flex; flex-direction: column; position: relative;
transition: margin-right .35s ease; flex-shrink: 0;
}
.ai-panel.collapsed { margin-right: -400px; }
.ai-header {
padding: 14px 16px; border-bottom: 1px solid #f2f3f5; display: flex; align-items: center; gap: 10px;
background: linear-gradient(135deg, rgba(255,122,26,.05), rgba(255,178,0,.05)); flex-shrink: 0;
}
.ai-avatar {
width: 38px; height: 38px; border-radius: 10px;
background: linear-gradient(135deg, #ff7a1a, #ffb300);
display: flex; align-items: center; justify-content: center; color: #fff; font-size: 18px;
position: relative; flex-shrink: 0;
}
.ai-avatar::after {
content: ""; position: absolute; right: -2px; bottom: -2px; width: 10px; height: 10px;
background: #22c55e; border: 2px solid #fff; border-radius: 50%;
}
.ai-header .info { flex: 1; }
.ai-header .name { font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
.ai-header .name .badge {
font-size: 10px; background: rgba(255,122,26,.1); color: #d96a00;
padding: 2px 6px; border-radius: 4px; font-weight: 500;
}
.ai-header .status { font-size: 12px; color: #86909c; margin-top: 2px; }
.icon-btn {
width: 30px; height: 30px; border: 1px solid #e5e6eb; background: #fff; border-radius: 8px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
color: #86909c; font-size: 14px; transition: all .2s;
}
.icon-btn:hover { color: #d96a00; border-color: #ff7a1a; }
.ai-quick { padding: 12px 14px 4px; border-bottom: 1px solid #f2f3f5; flex-shrink: 0; }
.ai-quick .title { font-size: 12px; color: #86909c; margin-bottom: 8px; }
.quick-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; padding-bottom: 10px; }
.quick-btn {
border: 1px solid #e5e6eb; border-radius: 10px; background: #fff; padding: 9px 2px;
cursor: pointer; text-align: center; transition: all .2s;
display: flex; flex-direction: column; align-items: center; gap: 5px;
}
.quick-btn .qicon { width: 30px; height: 30px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 15px; }
.quick-btn .qtext { font-size: 12px; color: #4e5969; }
.quick-btn:hover { border-color: #ff7a1a; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(255,122,26,.12); }
.q1 { background: rgba(255,122,26,.1); }
.q2 { background: rgba(255,178,0,.1); }
.q3 { background: rgba(245,158,11,.12); }
.q4 { background: rgba(139,92,246,.1); }
.q5 { background: rgba(236,72,153,.08); }
.q6 { background: rgba(34,197,94,.1); }
.q7 { background: rgba(99,102,241,.1); }
.ai-messages { flex: 1; overflow-y: auto; padding: 16px 14px; display: flex; flex-direction: column; gap: 14px; }
.msg { display: flex; gap: 8px; max-width: 100%; animation: msgIn .3s ease; }
@keyframes msgIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.msg .m-avatar {
width: 28px; height: 28px; border-radius: 8px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center; font-size: 13px; color: #fff;
}
.msg.ai .m-avatar { background: linear-gradient(135deg, #ff7a1a, #ffb300); }
.msg.user { flex-direction: row-reverse; }
.msg.user .m-avatar { background: linear-gradient(135deg, #6366f1, #8b5cf6); }
.msg .bubble {
background: #f7f8fa; border-radius: 12px; padding: 10px 12px; font-size: 13px;
line-height: 1.75; color: #1f2329; max-width: calc(100% - 40px); word-break: break-word;
}
.msg.user .bubble { background: linear-gradient(135deg, #ff7a1a, #ff6600); color: #fff; }
.bubble b { font-weight: 600; }
.bubble .hl { color: #d96a00; font-weight: 600; }
.gen-card {
background: #fff; border: 1px solid #ffe3c2; border-radius: 12px; padding: 12px;
margin-top: 8px; box-shadow: 0 2px 8px rgba(15,23,42,.04);
}
.gen-card .gc-head { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #d96a00; font-weight: 600; margin-bottom: 8px; }
.gen-card .gc-body { font-size: 13px; line-height: 1.8; color: #1f2329; background: #f8fafb; border-radius: 8px; padding: 10px 12px; max-height: 260px; overflow-y: auto; }
.gen-card .gc-body .line { display: block; }
.gen-card .gc-body .line b { color: #d96a00; }
.gc-foot { display: flex; gap: 8px; margin-top: 10px; }
.gc-btn {
flex: 1; border: 1px solid #ff7a1a; background: rgba(255,122,26,.06); color: #d96a00;
border-radius: 8px; padding: 7px 0; font-size: 12px; cursor: pointer; transition: all .2s; font-weight: 500;
}
.gc-btn:hover { background: #ff7a1a; color: #fff; }
.gc-btn.ghost { border-color: #e5e6eb; background: #fff; color: #4e5969; }
.gc-btn.ghost:hover { border-color: #ff7a1a; color: #d96a00; background: #fff; }
.tips-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.tip-pill { font-size: 11px; background: rgba(255,178,0,.08); color: #d97706; border-radius: 12px; padding: 3px 10px; }
.warn-pill { background: rgba(245,158,11,.1); color: #b45309; }
.ok-pill { background: rgba(34,197,94,.1); color: #15803d; }
.typing { display: inline-flex; gap: 4px; padding: 4px 2px; }
.typing span { width: 6px; height: 6px; background: #94a3b8; border-radius: 50%; animation: blink 1.2s infinite; }
.typing span:nth-child(2) { animation-delay: .2s; }
.typing span:nth-child(3) { animation-delay: .4s; }
@keyframes blink { 0%, 80%, 100% { opacity: .3; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }
.ai-input-area { border-top: 1px solid #f2f3f5; padding: 12px 14px; flex-shrink: 0; background: #fff; }
.input-wrap { border: 1px solid #e5e6eb; border-radius: 12px; padding: 8px 8px 8px 12px; transition: border-color .2s, box-shadow .2s; }
.input-wrap:focus-within { border-color: #ff7a1a; box-shadow: 0 0 0 3px rgba(255,122,26,.1); }
.input-wrap textarea {
width: 100%; border: none; outline: none; resize: none; font-size: 13px; line-height: 1.6;
max-height: 90px; font-family: inherit; background: transparent;
}
.input-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 6px; }
.input-tools { display: flex; gap: 4px; }
.tool-btn {
width: 28px; height: 28px; border: none; background: transparent; border-radius: 6px;
cursor: pointer; color: #86909c; font-size: 14px; transition: all .2s;
}
.tool-btn:hover { background: #f2f3f5; color: #d96a00; }
.send-btn {
background: linear-gradient(135deg, #ff7a1a, #ff6600); border: none; color: #fff;
width: 60px; height: 30px; border-radius: 8px; font-size: 13px; cursor: pointer;
transition: opacity .2s; font-weight: 500;
}
.send-btn:hover { opacity: .88; }
.input-hint { text-align: center; font-size: 11px; color: #c9cdd4; margin-top: 8px; }
.ai-float {
position: absolute; right: 0; top: 50%; transform: translateY(-50%);
width: 36px; height: 92px; background: linear-gradient(135deg, #ff7a1a, #ffb300);
border-radius: 10px 0 0 10px; color: #fff; border: none; cursor: pointer; font-size: 13px;
writing-mode: vertical-lr; letter-spacing: 3px; display: none; z-index: 20;
box-shadow: -4px 0 12px rgba(255,122,26,.25);
}
.ai-panel.collapsed .ai-float { display: block; }
.toast {
position: fixed; top: 68px; left: 50%; transform: translateX(-50%) translateY(-12px);
background: #1f2329; color: #fff; padding: 9px 20px; border-radius: 8px; font-size: 13px;
opacity: 0; pointer-events: none; transition: all .3s; z-index: 999;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-thumb { background: #d5d9e0; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #b8bec9; }
::-webkit-scrollbar-track { background: transparent; }
/* ============ 自主发品 Agent 卡片 ============ */
.agent-card {
border: 1px solid rgba(255, 122, 26, 0.45); border-radius: 12px; overflow: hidden;
background: linear-gradient(180deg, #fffaf3, #fff); margin-top: 4px;
}
.ag-head { background: linear-gradient(135deg, #ffa200, #ff6600); color: #fff; padding: 9px 12px; font-size: 13px; font-weight: 600; }
.ag-ctx { padding: 8px 12px; font-size: 12px; color: #d96a00; background: #fff7ed; border-bottom: 1px dashed rgba(255,122,26,.25); }
.ag-steps { padding: 4px 0; }
.ag-step { display: flex; align-items: center; gap: 8px; padding: 7px 12px; font-size: 12.5px; color: #4e5969; border-bottom: 1px solid #f7f3ee; }
.ag-step:last-child { border-bottom: none; }
.ag-step .ag-ico { width: 18px; text-align: center; font-size: 13px; flex-shrink: 0; }
.ag-step .ag-lbl { flex: 1; }
.ag-step .ag-note { font-size: 11px; color: #86909c; }
.ag-step.todo .ag-note { color: #c0c4cc; }
.ag-step.doing { color: #d96a00; background: #fff8ef; }
.ag-step.doing .ag-ico { animation: agspin .8s linear infinite; display: inline-block; }
.ag-step.doing .ag-note { color: #d96a00; }
.ag-step.done .ag-lbl { color: #1f2329; }
.ag-step.done .ag-ico { color: #22c55e; }
@keyframes agspin { from { transform: rotate(0); } to { transform: rotate(360deg); } }
.ag-foot { padding: 8px 12px; font-size: 11px; color: #86909c; background: #fafafa; border-top: 1px solid #f0f0f0; }
.ag-chip { display: inline-block; background: #fff; border: 1px solid rgba(255,122,26,.4); color: #ff7a1a; border-radius: 10px; padding: 1px 7px; font-size: 11px; margin: 0 3px 3px 0; }
/* ============ 串行确认栏 ============ */
.ag-confirm-bar {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
margin-top: 10px; padding: 9px 12px;
background: linear-gradient(135deg, #fff8ef, #fffaf3);
border: 1px solid rgba(255,122,26,.3); border-radius: 8px;
}
.ag-cf-btn {
background: linear-gradient(135deg, #ffa200, #ff6600); color: #fff; border: none;
padding: 7px 18px; border-radius: 6px; font-size: 12.5px; cursor: pointer;
font-weight: 600; font-family: inherit; transition: all .2s; white-space: nowrap;
}
.ag-cf-btn:hover { transform: translateY(-1px); box-shadow: 0 3px 10px rgba(255,122,26,.35); }
.ag-regen-btn { padding: 7px 14px; font-size: 12px; border-radius: 6px; border: 1px solid #e5e6eb; background: #fff; color: #4e5969; cursor: pointer; font-family: inherit; transition: all .15s; white-space: nowrap; }
.ag-regen-btn:hover { border-color: #ff7a1a; color: #d96a00; }
.ag-cd { font-size: 11px; color: #d96a00; margin-left: auto; white-space: nowrap; }
.ag-cd.manual { color: #86909c; }
.ag-mode-row {
display: flex; align-items: center; gap: 6px;
padding: 7px 12px; background: #fff7ed; border-bottom: 1px solid rgba(255,122,26,.12);
}
.ag-mode-label { font-size: 11px; color: #86909c; }
.ag-mt-btn {
border: 1px solid rgba(255,122,26,.3); background: #fff; color: #d96a00;
padding: 3px 10px; border-radius: 10px; font-size: 11px; cursor: pointer;
font-family: inherit; transition: all .15s;
}
.ag-mt-btn.active { background: #ff7a1a; color: #fff; border-color: #ff7a1a; }
.serial-card .gc-foot { display: none; }
/* ============ 一键发品 ============ */
.btn-magic {
background: linear-gradient(135deg, #ffa200, #ff6600); color: #fff; border: none;
padding: 10px 26px; border-radius: 10px; font-size: 15px; cursor: pointer; font-weight: 600;
box-shadow: 0 4px 14px rgba(255,122,26,.35); transition: all .2s; font-family: inherit;
}
.btn-magic:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(255,122,26,.45); }
.btn-ghost-w {
border: 1px solid #e5e6eb; background: #fff; color: #4e5969; padding: 9px 22px;
border-radius: 8px; font-size: 14px; cursor: pointer; transition: all .2s; font-family: inherit;
}
.btn-ghost-w:hover { border-color: #ff7a1a; color: #d96a00; }
.modal-mask {
position: fixed; inset: 0; background: rgba(15,23,42,.45); backdrop-filter: blur(3px);
display: none; align-items: center; justify-content: center; z-index: 1000;
}
.modal-mask.show { display: flex; }
.modal {
width: 530px; max-height: 84vh; overflow-y: auto; background: #fff; border-radius: 16px;
padding: 24px; animation: pop .25s ease;
}
@keyframes pop { from { opacity: 0; transform: scale(.95) translateY(10px); } to { opacity: 1; transform: none; } }
.modal h3 { font-size: 17px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
.modal .sub { font-size: 12px; color: #86909c; margin-top: 4px; }
.learn-hint {
margin-top: 14px; background: linear-gradient(135deg, rgba(255,178,0,.1), rgba(255,122,26,.08));
border: 1px solid #ffe3c2; border-radius: 10px; padding: 12px; font-size: 12px; line-height: 1.8; color: #4e5969;
}
.learn-hint b { color: #d96a00; }
.stat-row { display: flex; gap: 10px; margin-top: 14px; }
.stat-box { flex: 1; background: #f7f8fa; border-radius: 10px; padding: 10px; text-align: center; }
.stat-box .v { font-size: 18px; font-weight: 700; color: #d96a00; }
.stat-box .v small { font-size: 12px; font-weight: 400; color: #86909c; }
.stat-box .k { font-size: 11px; color: #86909c; margin-top: 2px; }
.level-wrap { margin-top: 14px; }
.level-bar { height: 6px; background: #f2f3f5; border-radius: 3px; overflow: hidden; margin-top: 6px; }
.level-bar i { display: block; height: 100%; background: linear-gradient(90deg, #ffa200, #ff6600); border-radius: 3px; transition: width .6s; }
.level-text { font-size: 12px; color: #4e5969; display: flex; justify-content: space-between; }
.conf-row { display: flex; gap: 12px; margin-top: 16px; }
.step-list { margin-top: 18px; display: flex; flex-direction: column; gap: 10px; }
.step-item {
display: flex; align-items: center; gap: 10px; font-size: 13px; color: #86909c;
padding: 10px 12px; border: 1px solid #f2f3f5; border-radius: 10px; background: #fafbfc; transition: all .3s;
}
.step-item .s-icon {
width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center;
justify-content: center; font-size: 12px; background: #f2f3f5; color: #86909c; flex-shrink: 0;
}
.step-item.doing { border-color: #ffd9ae; background: rgba(255,178,0,.06); color: #1f2329; }
.step-item.doing .s-icon { background: linear-gradient(135deg, #ffa200, #ff6600); color: #fff; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.step-item.done { color: #1f2329; }
.step-item.done .s-icon { background: #22c55e; color: #fff; }
.step-item .s-note { margin-left: auto; font-size: 11px; color: #c9cdd4; }
.step-item.done .s-note { color: #15803d; }
.modal-foot { display: flex; gap: 10px; margin-top: 20px; justify-content: flex-end; }
.done-view { text-align: center; padding: 8px 0; }
.done-view .big { font-size: 42px; }
.done-view h4 { font-size: 16px; margin: 8px 0 4px; }
.done-view .dsub { font-size: 12px; color: #86909c; }
.oc-wide {
width: 100%; border: none; background: linear-gradient(135deg, #ffa200, #ff6600); color: #fff;
border-radius: 10px; padding: 10px; font-size: 13px; font-weight: 600; cursor: pointer;
margin-bottom: 10px; box-shadow: 0 3px 10px rgba(255,122,26,.25); transition: all .2s; font-family: inherit;
}
.oc-wide:hover { transform: translateY(-1px); box-shadow: 0 5px 14px rgba(255,122,26,.35); }
-17
View File
@@ -1,17 +0,0 @@
# 黑手党提案后端 v2.1 — BYOK 模式
# 合伙人通过 Header 传入百炼 Key,服务器仅提供算法编排
# VOC 公网 API(跨云只读访问)
VOC_API_BASE=https://brand.brainwork.club/voc/api/research
# Fallback DashScope Key(平台方自测用)
# 合伙人通过 X-DashScope-Key Header 传入自己的 Key
# 百炼 Key 一个通吃:LLM 推理 + Embedding 向量化
DASHSCOPE_API_KEY=sk-xxx
# 模型
MODEL_ID=qwen-plus
TEMPERATURE=0.1
# 端口
PORT=8093
-144
View File
@@ -1,144 +0,0 @@
"""
黑手党提案 — 数据库管理(完全独立,阿里云内闭环)
每个提案案例一个 SQLite 文件,自包含所有数据:
- case_card:案例元信息
- comments:从 VOC API 导入的评论(本地副本)
- ude_sentences / ude_clusters:UDE 分析结果
- conflicts / proposal_sections:后续阶段
"""
import os
import sqlite3
import uuid
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
DATA_DIR = Path(__file__).parent / "data"
DATA_DIR.mkdir(exist_ok=True)
# VOC 公网 API(腾讯云,跨云只读访问)
VOC_API_BASE = os.getenv("VOC_API_BASE", "https://brand.brainwork.club/voc/api/research")
# ═══════════ Schema ═══════════
CASE_SCHEMA = """
CREATE TABLE IF NOT EXISTS case_card (
brand_name TEXT NOT NULL,
category TEXT,
focus_product TEXT,
competitors TEXT,
voc_research_id TEXT,
voc_api_base TEXT,
created_at TEXT DEFAULT (datetime('now')),
status TEXT DEFAULT 'draft'
);
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
voc_id INTEGER,
platform TEXT,
text TEXT NOT NULL,
like_count INTEGER DEFAULT 0,
published_at TEXT,
imported_at TEXT DEFAULT (datetime('now')),
UNIQUE(voc_id)
);
CREATE TABLE IF NOT EXISTS ude_sentences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
comment_id INTEGER REFERENCES comments(id),
ude_text TEXT NOT NULL,
confidence REAL DEFAULT 0.5,
vector TEXT,
cluster_id INTEGER DEFAULT -1,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS ude_clusters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
representative_ude TEXT,
coverage INTEGER,
sample_voices TEXT,
user_label TEXT,
confirmed INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS conflicts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ude_cluster_id INTEGER,
goal TEXT,
need TEXT,
prerequisite TEXT,
convention TEXT,
conflict_type TEXT,
description TEXT
);
CREATE TABLE IF NOT EXISTS proposal_sections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
section TEXT,
content TEXT,
version INTEGER DEFAULT 1,
updated_at TEXT DEFAULT (datetime('now'))
);
"""
# ═══════════ 案例 DB ═══════════
def get_case_conn(case_id: str) -> sqlite3.Connection:
"""获取案例 DB 连接"""
path = DATA_DIR / f"{case_id}.db"
if not path.exists():
raise FileNotFoundError(f"案例 {case_id} 不存在")
conn = sqlite3.connect(str(path))
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
return conn
def init_case_db(brand_name: str, category: str = "", focus_product: str = "",
competitors: str = "[]", voc_research_id: str = None) -> str:
"""创建新案例,返回 case_id"""
case_id = uuid.uuid4().hex[:8]
path = DATA_DIR / f"{case_id}.db"
conn = sqlite3.connect(str(path))
conn.row_factory = sqlite3.Row
conn.executescript(CASE_SCHEMA)
conn.execute(
"INSERT INTO case_card (brand_name, category, focus_product, competitors, voc_research_id, voc_api_base) VALUES (?,?,?,?,?,?)",
(brand_name, category, focus_product, competitors, voc_research_id, VOC_API_BASE)
)
conn.commit()
conn.close()
return case_id
def list_cases() -> list[dict]:
"""列出所有案例"""
cases = []
for db_file in sorted(DATA_DIR.glob("*.db")):
case_id = db_file.stem
try:
conn = sqlite3.connect(str(db_file))
conn.row_factory = sqlite3.Row
card = conn.execute("SELECT * FROM case_card LIMIT 1").fetchone()
if card:
comment_count = conn.execute("SELECT count(*) FROM comments").fetchone()[0]
ude_count = conn.execute("SELECT count(*) FROM ude_sentences").fetchone()[0]
cluster_count = conn.execute("SELECT count(*) FROM ude_clusters").fetchone()[0]
cases.append({
"case_id": case_id,
**dict(card),
"comment_count": comment_count,
"ude_count": ude_count,
"cluster_count": cluster_count,
})
conn.close()
except Exception:
pass
return cases
-52
View File
@@ -1,52 +0,0 @@
你是一个 TOC(约束理论)专家,你的任务是将消费者评论转写为 UDE(Undesirable Effect,不良效果)格式句。
## 什么是 UDE
UDE = 系统中当前正在发生的、阻碍系统实现目标的、可观测的负面现象。
UDE 是症状,不是病因,也不是解决方案。
## 转写规范(7 条硬约束)
你输出的每条 UDE 必须同时满足以下全部规范,不满足则不输出:
1. **完整陈述句**:必须是完整的句子,不能是碎片短语
2. **现在时态**:描述当前正在发生的事
3. **只描述效果,不含原因**:不能包含"因为…所以…"的因果分析
4. **不是伪装的解决方案**:不能说"需要X"、"应该做Y"
5. **单一实体**:一条 UDE 只描述一个问题
6. **客观可验证**:利益相关方能达成共识的事实
7. **在影响范围内**:品牌/企业可以采取行动改善的
## 你的任务
对输入的每条消费者评论,判断其中是否包含不良效果。如果有,转写为 UDE 格式句;如果没有(纯分享、纯推荐、无关内容),输出 null。
## 输出格式
严格输出 JSON 数组,每个元素对应一条输入评论:
```json
{
"results": [
{"id": 1, "ude": "该品类产品月均消费成本持续超出目标消费者的可接受范围", "confidence": 0.9},
{"id": 2, "ude": null, "confidence": 0},
{"id": 3, "ude": "消费者服用产品后持续无法感知明确效果变化", "confidence": 0.85}
]
}
```
## 转写示例
| 消费者原文 | 正确的 UDE ✅ | 错误的写法 ❌ |
|-----------|-------------|-------------|
| "一瓶三百多,吃一个月,真的吃不起" | "该品类产品月均消费成本持续超出目标消费者的可接受范围" | "需要降价"(伪装的解决方案) |
| "吃了两个月了完全没感觉" | "消费者服用产品后持续无法感知明确效果变化" | "因为产品无效所以没感觉"(包含原因) |
| "需要冷藏但办公室没冰箱" | "产品冷藏存储要求与消费者日常携带场景持续冲突" | "应该出常温版"(伪装的解决方案) |
| "不知道该买哪个牌子好" | "消费者面对该品类众多品牌持续缺乏可信的决策依据" | "品牌多、选择困难"(碎片短语,非完整句) |
| "这个益生菌真的超好用推荐!" | null(无不良效果) | |
## 重要提醒
- 你是格式转写员,不是分析师。不要添加原文中不存在的信息。
- 转写时提升到系统/品类层面,但不能超出原文事实的边界。
- confidence 表示你对这条转写准确性的信心(0-1),原文含义模糊时降低。
-8
View File
@@ -1,8 +0,0 @@
fastapi>=0.110.0
uvicorn[standard]>=0.27.0
openai>=1.12.0
python-dotenv>=1.0.0
numpy>=1.24.0
scikit-learn>=1.3.0
gunicorn>=21.2.0
httpx>=0.27.0
-353
View File
@@ -1,353 +0,0 @@
"""
黑手党提案 — 独立后端(BYOK v2.1)
FastAPI 服务,端口 8093。
合伙人通过 X-DashScope-Key Header 传入百炼 Key,服务器统一用于 LLM + Embedding。
import-voc 完成后自动触发 UDE 转写 + 向量化流水线。
"""
import os
import logging
import asyncio
import httpx
from fastapi import FastAPI, Header, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional
from dotenv import load_dotenv
load_dotenv()
from db import get_case_conn, init_case_db, list_cases as _list_cases, DATA_DIR, VOC_API_BASE
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s %(message)s")
logger = logging.getLogger("mafia")
app = FastAPI(title="黑手党提案后端", version="2.1.0", description="独立后端:阿里云内闭环 + BYOK,合伙人自带 API Key")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# ═══════════ BYOK Key 解析 ═══════════
def resolve_key(header_key: Optional[str]) -> str:
"""优先使用合伙人 Header 传入的 DashScope Key(百炼 Key 一个通吃 LLM + Embedding)"""
if header_key:
logger.info(f"[BYOK] 使用合伙人 Key: ...{header_key[-6:]}")
return header_key
env_val = os.getenv("DASHSCOPE_API_KEY", "")
if env_val:
logger.info(f"[BYOK] 使用 fallback Key: ...{env_val[-6:]}")
return env_val
raise HTTPException(401, "缺少 DashScope API Key。请在 CoPaw 中配置 DASHSCOPE_API_KEY 并通过 X-DashScope-Key Header 传入")
# ═══════════ Models ═══════════
class CreateCaseRequest(BaseModel):
brandName: str
category: str = ""
focusProduct: str = ""
competitors: str = "[]"
vocResearchId: str = None
class LinkVocRequest(BaseModel):
vocResearchId: str
# ═══════════ 案例管理 ═══════════
@app.post("/api/cases")
async def create_case(req: CreateCaseRequest):
case_id = init_case_db(
brand_name=req.brandName,
category=req.category,
focus_product=req.focusProduct,
competitors=req.competitors,
voc_research_id=req.vocResearchId,
)
return {"caseId": case_id}
@app.get("/api/cases")
async def get_cases():
return _list_cases()
@app.get("/api/cases/{case_id}")
async def get_case(case_id: str):
try:
with get_case_conn(case_id) as conn:
card = conn.execute("SELECT * FROM case_card LIMIT 1").fetchone()
comment_count = conn.execute("SELECT count(*) FROM comments").fetchone()[0]
ude_count = conn.execute("SELECT count(*) FROM ude_sentences").fetchone()[0]
cluster_count = conn.execute("SELECT count(*) FROM ude_clusters").fetchone()[0]
if not card:
raise HTTPException(404, "案例不存在")
return {
"caseId": case_id, **dict(card),
"commentCount": comment_count,
"udeCount": ude_count,
"clusterCount": cluster_count,
}
except FileNotFoundError:
raise HTTPException(404, "案例不存在")
@app.delete("/api/cases/{case_id}")
async def delete_case(case_id: str):
path = DATA_DIR / f"{case_id}.db"
if path.exists():
path.unlink()
return {"deleted": True}
raise HTTPException(404, "案例不存在")
# ═══════════ VOC 导入 + 自动流水线 ═══════════
# 后台流水线状态(内存缓存,进程级)
_pipeline_status: dict[str, dict] = {}
async def _auto_pipeline(case_id: str, dashscope_key: str):
"""后台自动流水线:UDE 转写 → 向量化"""
from tools.ude_extract import run_ude_extraction, run_vectorization
_pipeline_status[case_id] = {"stage": "extracting", "progress": {}}
try:
# Stage 1: UDE 转写
result = await run_ude_extraction(case_id, limit=0, dashscope_key=dashscope_key)
_pipeline_status[case_id]["progress"]["extraction"] = result
logger.info(f"[Pipeline] {case_id} UDE 转写完成: {result.get('totalUdes', 0)} 条")
# Stage 2: 向量化
_pipeline_status[case_id]["stage"] = "vectorizing"
vec_result = run_vectorization(case_id, dashscope_key=dashscope_key)
_pipeline_status[case_id]["progress"]["vectorization"] = vec_result
logger.info(f"[Pipeline] {case_id} 向量化完成: {vec_result.get('vectorized', 0)} 条")
_pipeline_status[case_id]["stage"] = "done"
except Exception as e:
logger.error(f"[Pipeline] {case_id} 失败: {e}")
_pipeline_status[case_id]["stage"] = "error"
_pipeline_status[case_id]["error"] = str(e)
@app.post("/api/cases/{case_id}/link-voc")
async def link_voc(case_id: str, req: LinkVocRequest):
"""关联 VOC 研究 ID"""
try:
with get_case_conn(case_id) as conn:
conn.execute("UPDATE case_card SET voc_research_id = ?", (req.vocResearchId,))
conn.commit()
except FileNotFoundError:
raise HTTPException(404, "案例不存在")
return {"linked": True, "vocResearchId": req.vocResearchId}
@app.post("/api/cases/{case_id}/import-voc")
async def import_voc(
case_id: str,
page: int = Query(1),
pageSize: int = Query(100),
x_dashscope_key: str = Header(None),
):
"""从 VOC 公网 API 拉取评论,完成后自动触发 UDE 转写 + 向量化"""
dashscope_key = resolve_key(x_dashscope_key)
try:
with get_case_conn(case_id) as conn:
card = conn.execute("SELECT voc_research_id, voc_api_base FROM case_card LIMIT 1").fetchone()
except FileNotFoundError:
raise HTTPException(404, "案例不存在")
if not card or not card["voc_research_id"]:
raise HTTPException(400, "未关联 VOC 研究,请先调用 link-voc")
voc_rid = card["voc_research_id"]
api_base = card["voc_api_base"] or VOC_API_BASE
total_imported = 0
current_page = page
async with httpx.AsyncClient(timeout=30) as client:
while True:
url = f"{api_base}/{voc_rid}/voc-list?page={current_page}&page_size={pageSize}"
try:
resp = await client.get(url)
if resp.status_code != 200:
logger.warning(f"[Import] VOC API 返回 {resp.status_code}: {resp.text[:100]}")
break
data = resp.json()
except Exception as e:
logger.error(f"[Import] VOC API 请求失败: {e}")
break
items = data.get("items") or data.get("data") or []
if not items:
break
with get_case_conn(case_id) as conn:
for item in items:
text = item.get("text", "")
if len(text) < 10:
continue
try:
conn.execute(
"INSERT OR IGNORE INTO comments (voc_id, platform, text, like_count, published_at) VALUES (?,?,?,?,?)",
(
item.get("id"),
item.get("platform", ""),
text,
item.get("like_count", 0),
item.get("published_at", ""),
)
)
total_imported += 1
except Exception:
pass
conn.commit()
total = data.get("total", 0)
if current_page * pageSize >= total:
break
current_page += 1
with get_case_conn(case_id) as conn:
local_count = conn.execute("SELECT count(*) FROM comments").fetchone()[0]
# 自动触发后台流水线(UDE 转写 + 向量化)
pipeline_triggered = False
if local_count > 0:
asyncio.create_task(_auto_pipeline(case_id, dashscope_key))
pipeline_triggered = True
logger.info(f"[Import] {case_id} 自动流水线已触发({local_count} 条评论)")
return {
"imported": total_imported,
"totalLocal": local_count,
"vocResearchId": voc_rid,
"pagesProcessed": current_page - page + 1,
"pipelineTriggered": pipeline_triggered,
}
@app.get("/api/cases/{case_id}/pipeline-status")
async def pipeline_status(case_id: str):
"""查询后台流水线进度"""
status = _pipeline_status.get(case_id)
if not status:
# 没有正在运行的流水线,从 DB 推断状态
try:
with get_case_conn(case_id) as conn:
comments = conn.execute("SELECT count(*) FROM comments").fetchone()[0]
udes = conn.execute("SELECT count(*) FROM ude_sentences").fetchone()[0]
vectorized = conn.execute("SELECT count(*) FROM ude_sentences WHERE vector IS NOT NULL").fetchone()[0]
except FileNotFoundError:
raise HTTPException(404, "案例不存在")
return {
"stage": "idle",
"progress": {"comments": comments, "udesExtracted": udes, "udesVectorized": vectorized},
}
return status
@app.get("/api/cases/{case_id}/comments")
async def get_comments(case_id: str, page: int = 1, pageSize: int = 50):
"""查看本地导入的评论"""
try:
with get_case_conn(case_id) as conn:
total = conn.execute("SELECT count(*) FROM comments").fetchone()[0]
rows = conn.execute("""
SELECT id, voc_id, platform, text, like_count, published_at
FROM comments ORDER BY like_count DESC
LIMIT ? OFFSET ?
""", (pageSize, (page - 1) * pageSize)).fetchall()
except FileNotFoundError:
raise HTTPException(404, "案例不存在")
return {"total": total, "page": page, "items": [dict(r) for r in rows]}
# ═══════════ UDE 分析(BYOK) ═══════════
@app.post("/api/cases/{case_id}/ude/extract")
async def extract_ude(
case_id: str,
limit: int = Query(0),
x_dashscope_key: str = Header(None),
):
"""手动触发 UDE 转写(通常由 import-voc 自动触发,此接口用于补跑)"""
from tools.ude_extract import run_ude_extraction
key = resolve_key(x_dashscope_key)
try:
result = await run_ude_extraction(case_id, limit, dashscope_key=key)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
return result
@app.post("/api/cases/{case_id}/ude/cluster")
async def cluster_ude(
case_id: str,
eps: float = Query(0.25),
minSamples: int = Query(3),
):
"""向量聚类(纯 CPU,不调外部 API。向量已在流水线中预计算)"""
from tools.ude_extract import run_clustering
try:
result = run_clustering(case_id, eps, minSamples)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
return result
@app.get("/api/cases/{case_id}/ude/clusters")
async def get_clusters(case_id: str):
try:
with get_case_conn(case_id) as conn:
clusters = conn.execute(
"SELECT * FROM ude_clusters ORDER BY coverage DESC"
).fetchall()
except FileNotFoundError:
raise HTTPException(404, "案例不存在")
return [dict(r) for r in clusters]
@app.get("/api/cases/{case_id}/ude/coverage")
async def get_coverage(case_id: str):
from tools.ude_extract import run_coverage_scan
try:
result = run_coverage_scan(case_id)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
return result
# ═══════════ 健康检查 ═══════════
@app.get("/api/health")
async def health():
return {
"status": "ok",
"version": "2.1.0",
"architecture": "BYOK (Bring Your Own Key)",
"vocApiBase": VOC_API_BASE,
"caseDataDir": str(DATA_DIR),
"byok": {
"bailianFallback": bool(os.getenv("BAILIAN_API_KEY")),
"dashscopeFallback": bool(os.getenv("DASHSCOPE_API_KEY")),
},
}
# ═══════════ 启动 ═══════════
if __name__ == "__main__":
import uvicorn
port = int(os.getenv("PORT", "8093"))
uvicorn.run(app, host="0.0.0.0", port=port)
-8
View File
@@ -1,8 +0,0 @@
# Tools 注册表
from tools.ude_extract import run_ude_extraction, run_clustering, run_coverage_scan
__all__ = [
"run_ude_extraction",
"run_clustering",
"run_coverage_scan",
]
-327
View File
@@ -1,327 +0,0 @@
"""
黑手党提案 — UDE 提取工具(BYOK v2.1)
流程:本地 comments → LLM 转写 UDE → DashScope 向量化 → DBSCAN 聚类
所有外部 API 调用统一使用 DASHSCOPE_API_KEY(百炼 Key 一个通吃 LLM + Embedding)。
向量化在 import-voc 流水线中预完成,聚类为纯 CPU 操作。
"""
from __future__ import annotations
import json
import os
import asyncio
import logging
from pathlib import Path
import numpy as np
from openai import OpenAI, AsyncOpenAI
from dotenv import load_dotenv
load_dotenv()
logger = logging.getLogger(__name__)
MODEL = os.getenv("MODEL_ID", "qwen-plus")
TEMPERATURE = float(os.getenv("TEMPERATURE", "0.1"))
BATCH_SIZE = 10
CONCURRENCY = 5
EMBED_DIM = 1024
EMBED_BATCH_SIZE = 25
PROMPT_PATH = Path(__file__).parent.parent / "prompts" / "voc_to_ude.txt"
# DashScope OpenAI 兼容端点(LLM + Embedding 共用)
DASHSCOPE_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
def _get_llm_client(dashscope_key: str = None) -> AsyncOpenAI:
"""百炼 Key 一个通吃 LLM + Embedding"""
key = dashscope_key or os.getenv("DASHSCOPE_API_KEY", "")
return AsyncOpenAI(api_key=key, base_url=DASHSCOPE_BASE)
def _get_embed_client(dashscope_key: str = None) -> OpenAI:
key = dashscope_key or os.getenv("DASHSCOPE_API_KEY", "")
if not key:
raise ValueError("DashScope API Key 未配置。")
return OpenAI(api_key=key, base_url=DASHSCOPE_BASE)
# ═══════════ Step 1: 本地评论 → UDE 转写 ═══════════
async def _call_ude_llm(prompt: str, comments: list[dict], dashscope_key: str = None) -> list[dict]:
"""单批 LLM 转写"""
client = _get_llm_client(dashscope_key)
user_msg = "请将以下消费者评论转写为 UDE 格式句,返回 JSON:\n\n"
for c in comments:
user_msg += f"[{c['id']}] 平台:{c['platform']} 原文: \"{c['text'][:300]}\"\n\n"
try:
resp = await client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": user_msg},
],
temperature=TEMPERATURE,
max_tokens=4000,
response_format={"type": "json_object"},
)
content = (resp.choices[0].message.content or "").strip()
parsed = json.loads(content)
if isinstance(parsed, dict):
for key in ("results", "data", "items", "udes"):
if key in parsed and isinstance(parsed[key], list):
return parsed[key]
if isinstance(parsed, list):
return parsed
return []
except Exception as e:
logger.warning(f"[UDE] LLM 转写失败: {str(e)[:80]}")
return []
async def _process_ude_batch(comments, prompt, semaphore, dashscope_key=None):
async with semaphore:
return await _call_ude_llm(prompt, comments, dashscope_key)
async def run_ude_extraction(case_id: str, limit: int = 0, dashscope_key: str = None) -> dict:
"""从本地 comments 表读取评论,转写为 UDE,存入 ude_sentences"""
from db import get_case_conn
prompt = PROMPT_PATH.read_text("utf-8") if PROMPT_PATH.exists() else ""
if not prompt:
return {"error": "UDE 转写 prompt 未找到 (prompts/voc_to_ude.txt)"}
with get_case_conn(case_id) as conn:
done_ids = {r[0] for r in conn.execute(
"SELECT comment_id FROM ude_sentences"
).fetchall()}
rows = conn.execute("""
SELECT id, platform, text
FROM comments WHERE length(text) > 10
ORDER BY id
""").fetchall()
total_comments = len(rows)
pending = [r for r in rows if r["id"] not in done_ids]
if not pending:
with get_case_conn(case_id) as conn:
total = conn.execute("SELECT count(*) FROM ude_sentences").fetchone()[0]
return {"message": "全部已转写完成", "totalUdes": total, "new": 0}
if limit > 0:
pending = pending[:limit]
batches = []
for i in range(0, len(pending), BATCH_SIZE):
chunk = pending[i:i + BATCH_SIZE]
batches.append([{"id": r["id"], "platform": r["platform"], "text": r["text"]} for r in chunk])
semaphore = asyncio.Semaphore(CONCURRENCY)
tasks = [asyncio.create_task(_process_ude_batch(b, prompt, semaphore, dashscope_key)) for b in batches]
all_results = await asyncio.gather(*tasks)
ok = 0
with get_case_conn(case_id) as conn:
for results in all_results:
for r in (results or []):
if not isinstance(r, dict):
continue
ude_text = r.get("ude")
if not ude_text:
continue
cid = r.get("id")
if not cid:
continue
try:
conn.execute(
"INSERT OR IGNORE INTO ude_sentences (comment_id, ude_text, confidence) VALUES (?, ?, ?)",
(int(cid), ude_text, r.get("confidence", 0.5))
)
ok += 1
except Exception as e:
logger.warning(f"[UDE] 写入失败 id={cid}: {e}")
conn.commit()
total = conn.execute("SELECT count(*) FROM ude_sentences").fetchone()[0]
return {
"newUdes": ok,
"totalUdes": total,
"totalComments": total_comments,
"remaining": total_comments - total,
"batches": len(batches),
}
# ═══════════ Step 2: 向量化(独立函数,流水线自动调用) ═══════════
def _embed_texts(client: OpenAI, texts: list[str]) -> list[list[float]]:
all_vectors = []
for i in range(0, len(texts), EMBED_BATCH_SIZE):
batch = texts[i:i + EMBED_BATCH_SIZE]
resp = client.embeddings.create(model="text-embedding-v4", input=batch, dimensions=EMBED_DIM)
all_vectors.extend([item.embedding for item in resp.data])
return all_vectors
def run_vectorization(case_id: str, dashscope_key: str = None) -> dict:
"""为所有未向量化的 UDE 生成 embedding(独立于聚类,可被流水线自动调用)"""
from db import get_case_conn
embed_client = _get_embed_client(dashscope_key)
with get_case_conn(case_id) as conn:
# 只处理未向量化的 UDE
rows = conn.execute(
"SELECT id, ude_text FROM ude_sentences WHERE vector IS NULL ORDER BY id"
).fetchall()
if not rows:
total = conn.execute("SELECT count(*) FROM ude_sentences").fetchone()[0]
return {"vectorized": 0, "totalUdes": total, "message": "全部已向量化"}
texts = [r["ude_text"] for r in rows]
ids = [r["id"] for r in rows]
vectors = _embed_texts(embed_client, texts)
for i, uid in enumerate(ids):
conn.execute("UPDATE ude_sentences SET vector = ? WHERE id = ?",
(json.dumps(vectors[i]), uid))
conn.commit()
total = conn.execute("SELECT count(*) FROM ude_sentences").fetchone()[0]
vectorized_total = conn.execute("SELECT count(*) FROM ude_sentences WHERE vector IS NOT NULL").fetchone()[0]
return {
"vectorized": len(rows),
"totalUdes": total,
"totalVectorized": vectorized_total,
}
# ═══════════ Step 3: 聚类(纯 CPU,不调外部 API) ═══════════
def run_clustering(case_id: str, eps: float = 0.25, min_samples: int = 3) -> dict:
"""纯 CPU 聚类:从 DB 读取已有向量,DBSCAN 计算。不调任何外部 API。"""
from sklearn.cluster import DBSCAN
from sklearn.metrics.pairwise import cosine_distances
from db import get_case_conn
with get_case_conn(case_id) as conn:
rows = conn.execute("SELECT id, comment_id, ude_text, vector FROM ude_sentences ORDER BY id").fetchall()
if len(rows) < min_samples:
return {"error": f"UDE 不足 ({len(rows)} 条),至少需要 {min_samples} 条。"}
# 检查向量完备性
has_vector = [r for r in rows if r["vector"]]
missing = len(rows) - len(has_vector)
if missing > 0:
return {
"error": f"{missing} 条 UDE 尚未向量化,请等待流水线完成或手动调用 /ude/extract",
"totalUdes": len(rows),
"vectorized": len(has_vector),
"missing": missing,
}
ude_texts = [r["ude_text"] for r in rows]
ude_ids = [r["id"] for r in rows]
comment_ids = [r["comment_id"] for r in rows]
# 从 DB 读取已有向量(纯 CPU)
vec_array = np.array([json.loads(r["vector"]) for r in rows])
# DBSCAN(纯 CPU)
dist_matrix = cosine_distances(vec_array)
clustering = DBSCAN(eps=eps, min_samples=min_samples, metric="precomputed").fit(dist_matrix)
labels = clustering.labels_
# 更新聚类标签
for i, uid in enumerate(ude_ids):
conn.execute("UPDATE ude_sentences SET cluster_id = ? WHERE id = ?",
(int(labels[i]), uid))
# 清空旧聚类,写入新聚类
conn.execute("DELETE FROM ude_clusters")
clusters = []
unique_labels = sorted(set(labels) - {-1})
for cluster_id in unique_labels:
member_indices = [i for i, l in enumerate(labels) if l == cluster_id]
member_texts = [ude_texts[i] for i in member_indices]
member_vectors = vec_array[member_indices]
member_cids = [comment_ids[i] for i in member_indices]
centroid = member_vectors.mean(axis=0)
dists = cosine_distances([centroid], member_vectors)[0]
representative = member_texts[dists.argmin()]
sample_voices = []
for cid in member_cids[:5]:
voice = conn.execute(
"SELECT text, platform FROM comments WHERE id = ?", (cid,)
).fetchone()
if voice:
sample_voices.append({"text": voice["text"][:200], "platform": voice["platform"]})
conn.execute(
"INSERT INTO ude_clusters (representative_ude, coverage, sample_voices) VALUES (?, ?, ?)",
(representative, len(member_indices), json.dumps(sample_voices, ensure_ascii=False))
)
clusters.append({
"clusterId": int(cluster_id),
"representativeUde": representative,
"coverage": len(member_indices),
"sampleVoices": sample_voices,
})
conn.commit()
clusters.sort(key=lambda x: x["coverage"], reverse=True)
noise_count = int((labels == -1).sum())
return {
"totalUdes": len(labels),
"numClusters": len(clusters),
"noiseCount": noise_count,
"noisePct": round(noise_count / len(labels) * 100, 1) if len(labels) else 0,
"clusters": clusters,
"params": {"eps": eps, "minSamples": min_samples},
}
# ═══════════ 覆盖扫描 ═══════════
def run_coverage_scan(case_id: str) -> dict:
from db import get_case_conn
with get_case_conn(case_id) as conn:
total_comments = conn.execute("SELECT count(*) FROM comments").fetchone()[0]
total_udes = conn.execute("SELECT count(*) FROM ude_sentences").fetchone()[0]
clustered = conn.execute("SELECT count(*) FROM ude_sentences WHERE cluster_id >= 0").fetchone()[0]
noise = conn.execute("SELECT count(*) FROM ude_sentences WHERE cluster_id = -1").fetchone()[0]
cluster_stats = [dict(r) for r in conn.execute(
"SELECT cluster_id, count(*) as cnt FROM ude_sentences WHERE cluster_id >= 0 GROUP BY cluster_id ORDER BY cnt DESC"
).fetchall()]
noise_samples = [dict(r) for r in conn.execute(
"SELECT ude_text, comment_id, confidence FROM ude_sentences WHERE cluster_id = -1 ORDER BY confidence DESC LIMIT 10"
).fetchall()]
return {
"totalComments": total_comments,
"totalUdes": total_udes,
"udesClustered": clustered,
"udesNoise": noise,
"coverageRate": round(clustered / total_comments * 100, 1) if total_comments else 0,
"clusterDistribution": cluster_stats,
"noiseSamples": noise_samples,
"verdict": "充分" if (total_udes > 0 and noise / total_udes < 0.1) else
("需关注" if (total_udes > 0 and noise / total_udes < 0.2) else "需调参"),
}
+291
View File
@@ -0,0 +1,291 @@
# CALM 设计对照:从 Rasa CALM 到本 Demo
> 本文说明本项目「商品发布 AI 助手(Copilot / Agent)」的**架构来源与设计取舍**。
> 核心参考范式为 **Rasa CALM**(Conversational AI with Language Models),
> 项目在意图理解层与流程执行层的分层方式,直接借鉴了 CALM 的四步架构。
---
## 1. 参考源
| 编号 | 来源 | 类型 | 链接 |
|---|---|---|---|
| R1 | Rasa CALM 原始论文(arXiv:2402.12234) | 一手 · 论文 | https://arxiv.org/pdf/2402.12234 |
| R2 | Rasa Pro 文档 · Dialogue Understanding | 一手 · 官方文档 | https://rasa.com/docs/rasa-pro/concepts/dialogue-understanding |
| R3 | Rasa Pro 文档 · Conversation Repair | 一手 · 官方文档 | https://rasa.com/docs/rasa-pro/concepts/conversation-repair |
| R4 | Rasa Studio 可视化流程编排 | 一手 · 官方文档 | https://rasa.com/docs/studio/user-guide/create-flow |
| R5 | 《大模型与任务型对话的结合:Rasa CALM》 | 二手 · 社区解读 | https://mp.weixin.qq.com/s/L_0IeGVeTml_N-Ypb6AwEQ |
| R6 | tod-in-context-learning 参考实现 | 一手 · 代码 | https://github.com/RasaHQ/tod-in-context-learning |
> 证据分级说明:R1–R4、R6 为论文/官方文档/官方代码仓库(一手);R5 为第三方技术解读(二手,用于快速建立术语认知,关键结论均回溯至 R1/R2 核对)。
---
## 2. CALM 要解决什么问题
### 2.1 传统任务型对话的 pipeline
业界标准做法是三段式:
```
语义理解(意图识别 + 词槽抽取)→ 对话管理(状态更新 + 策略选择)→ 话术生成
```
其内核是一条强假设:
```
f(对话状态) = 对话 action
```
而**对话状态的编码强依赖意图类别的识别**。一旦遇到预置意图之外、或本身就模糊的表达,整条链路就会退化成僵硬的分支判断——鲁棒性不足。
典型反例(来自 R5 的转述):
- 用户说「订餐厅」——可能是订中餐、订西餐,也可能只是查询,本身就难以归入单一类别;
- 用户说了一段很长的澄清文本——无法映射到预设类别体系里。
### 2.2 CALM 的主张
> **意图分类不是一件合理的事情**,因为人类表达意图可不仅仅是一个类别。
既然分类不合理,那就**取消意图分类**,让大模型直接把用户意图表达为**一组动作(Commands)**——即论文所述 *Commands as a Domain-Specific Language*。
---
## 3. CALM 的四步架构
```mermaid
flowchart TD
A["用户最新消息 + 对话历史"] --> B["① LLM 生成 Commands<br/>f(任务说明, 全部 flow 描述,<br/>当前 flow 与 slots, 对话历史) = commands"]
B --> C["② 校验 Commands 合理性<br/>对话管理引擎更新对话状态<br/>(当前 flow + slots)"]
C --> D["③ Conversation Repair<br/>判断是否需修复并更新状态"]
D --> E["④ 规则引擎按确定逻辑执行<br/>输出 Action 及对应话术"]
E --> F["表单/系统状态变更 + 回复用户"]
style B fill:#EEEDFE,stroke:#534AB7
style E fill:#E1F5EE,stroke:#0F6E56
```
**架构的关键判断:只有第 ① 步用大模型,其余三步都是确定逻辑。**
这一点决定了系统的工程性质:大模型负责它擅长的「理解」,规则引擎负责它擅长的「确定执行」。可解释性、可测试性、可兜底性都建立在这条边界上。
### 3.1 Commands DSL 清单
| Command | 语义 | 示例 |
|---|---|---|
| `StartFlow(flow_name)` | 启动一个业务流程 | `StartFlow(transfer_money)` |
| `SetSlot(slot, value)` | 填充槽位 | `SetSlot(recipient, John)` |
| `CancelFlow()` | 取消当前流程 | 用户说「算了」「stop that」 |
| `Clarify(flow_1, flow_2, ...)` | 歧义澄清 | 用户只说「contacts」→ `Clarify(list, add, remove)` |
| `SkipQuestion()` | 跳过当前追问 | 用户说「下一个问题」 |
| `SearchAndReply()` | 走知识库作答 | 非任务型知识问询 |
| `ChitChat()` | 闲聊 | 无其他可执行动作时兜底 |
| `HumanHandoff()` | 转人工 | 用户表达不满或明确要求 |
**关键设计**:`SetSlot` 中的值不再是通用实体(如「John」被识别为 person、45 被识别为 number),而是**直接用 flow 定义中的槽位语义来承接**——理解与流程定义在同一套语汇下对齐,消除了 NLU 中间表示的语义损耗。
### 3.2 Flow 的定义形态
flow = `description` + `steps`,其中 `steps` 只允许若干枚举类型(`action` / `collect` / `set_slots` / `noop`),并支持 `next` 分支与 `if` 条件:
```yaml
flows:
transfer_money:
description: This flow lets users send money to friends and family.
steps:
- collect: transfer_money_recipient
description: the name of a person
- id: "ask_amount"
collect: transfer_money_amount_of_money
- action: check_transfer_funds
next:
- if: not slots.transfer_money_has_sufficient_funds
then:
- action: utter_transfer_money_insufficient_funds
next: "ask_amount"
- else: transfer_money_final_confirmation
```
配套三个配置文件:`domain.yml`(话术/action/slot 声明)、`config.yaml`(LLM 版本与超时)、`endpoints.yml`(服务地址)。
---
## 4. 本 Demo 的逐层对照
本 Demo 是**纯前端可交互原型**(无后端、不接真实 LLM),因此第 ① 步的「理解」以规则/关键词实现,但**分层结构与数据契约完全按 CALM 的形态设计**,替换为 LLM function calling 即可落地(见 §7.1)。
| # | CALM 概念 | 本 Demo 的实现 | 代码位置 |
|---|---|---|---|
| 1 | 意图不再分类,直接生成 Commands | 不走 intent 分类,直接产出「工具序列 + 槽位值」 | `app/src/lib/agent.js` |
| 2 | `SetSlot(slot, value)` | `parseBrief(text)` → `{ dest, days, type, theme }` | `agent.js:26` |
| 3 | `StartFlow(flow_name)` 序列 | `parseMultiTools(text)` → 命中的工具类型数组(保序去重) | `agent.js:52` |
| 4 | Commands DSL 的取值域 | `toolKeywords` 关键词表(8 工具)+ `DEST_MAP` / `THEME_MAP` / `TYPES` 值域映射 | `agent.js:11,19,23,40` |
| 5 | 全量流程触发 | `agentIntentRe` 命中 → 启动 8 步全量流程 | `agent.js:8` |
| 6 | `flows.yml`:flow 定义(description + steps) | `agentSteps`(8 步自主发品流程定义)/ `ocStepsDef`(6 步一键发品流程定义) | `data/genData.js:10,22` |
| 7 | `actions/*.py`:action 实现 | `buildGen(type, ctx)` → `{ pre, card, data }` 结构化生成 | `data/genData.js:54` |
| 8 | domain 中的 action / slot 声明 | `cardMap`(工具 → 表单卡片锚点)/ `actionLabel`(工具 → 动作名) | `data/genData.js:48,32` |
| 9 | 对话状态:当前 flow + slots | `form`(槽位)+ `generatedRef`(各工具最新产物)+ `serialRef`(当前等待点) | `store/useAppStore.js:42,63,64` |
| 10 | 规则引擎确定性执行 | `agentRun` / `multiToolRun` 的 `for` 循环 + 任务清单状态机 | `useAppStore.js:325,351` |
| 11 | slot 的 `ask_before_filling` | `serialConfirm`:生成卡片后 Promise 挂起,等 5s 自动倒计时或手动确认 | `useAppStore.js:204,236` |
| 12 | 重新收集槽位 / 分支重走 | `serialRegen`「换一版」:重新生成当前步骤卡片并重置倒计时 | `useAppStore.js:214` |
| 13 | Conversation Repair(偏题/打断) | `sendMessage` 开头:若存在等待中的步骤,先自动确认收口再处理新消息 | `useAppStore.js:383` |
| 14 | `ChitChat()` / `KnowledgeAnswer()` 兜底 | `CANNED` 关键词兜底 + 3 条上下文感知的闲聊回复 | `useAppStore.js:29,410` |
| 15 | 执行过程的可见性(flow steps 逐条执行) | 任务清单卡片:`todo → doing → done` 三态 + 每步完成备注 | `useAppStore.js:253,265`;`components/TaskCardView.jsx` |
| 16 | 最终确认(final confirmation) | 生成卡片的「⚡ 一键填入」与「🔄 换一版」双动作 | `components/GenCardView.jsx` |
### 4.1 一次完整交互的等价展开
以 CALM 的样例句式对照本 Demo 的实际行为:
```
用户:帮我发个海南亲子 6 天自由行
CALM 侧等价输出:
StartFlow(publish_product)
SetSlot(dest, 海南(三亚))
SetSlot(theme, 亲子家庭)
SetSlot(days, 6天5晚)
本 Demo 实际执行:
parseBrief() → { dest: '海南(三亚)', theme: '亲子家庭', days: '6天5晚' }
applyBriefToForm()→ 槽位回写左侧表单
agentIntentRe → 命中「发」→ 启动 8 步全量流程
agentRun() → 逐工具 buildGen → 生成卡片 → 等确认 → applyGen 回填
```
---
## 5. 采纳了什么
### 5.1 取消意图分类,直接生成动作(采纳)
本 Demo **不存在 intent 分类器**。用户的自然语言直接映射为「要启动哪些流程 + 各槽位取值」,与 CALM 的主张一致。
这条选择在本场景的价值尤其明显:商家说「帮我优化一下」,实际上可能是要标题、要详情、要主图——强行归入一个 intent 会立刻失真;而输出「工具序列」天然可容纳这种多目标表达(`parseMultiTools` 返回的就是一个数组)。
### 5.2 理解层与执行层分离,且只有理解层是「智能」的(采纳)
这是 CALM 最值得复用的工程判断,也是本 Demo 的核心架构约束:
- **理解层**(`lib/agent.js`):唯一需要「聪明」的地方,负责把自然语言转成结构化动作;
- **执行层**(`store/useAppStore.js` 的四条编排流):纯确定性循环,每一步都可解释、可中断、可测试;
- **定义层**(`data/genData.js`):流程与产物以声明式数据描述,与执行引擎解耦。
由此得到三个直接收益:
1. **可解释**:每一步「为什么调用这个工具」都能回溯到一次解析结果;
2. **可中断**:执行中任何时候都能插入确认、换版、切换自动/手动模式;
3. **可测试**:解析层与生成层可分别单测,无需端到端跑 UI。
### 5.3 human-in-the-loop 的槽位确认(采纳并强化)
CALM 在 flow 中通过 `ask_before_filling: true` 让槽位在填充前先向用户确认。本 Demo 把这个机制从「槽位级」提升到「模块级」:
- `serialConfirm` 让每个工具生成后先出卡片,用户确认才回填表单;
- 支持 `autoConfirmMode` 运行中随时切换(⚡自动 5s / ✋手动无限等);
- `serialRegen` 提供「换一版」——等价于 CALM 重新收集槽位后重走分支。
对 B 端商家场景而言,这个机制的必要性高于 C 端对话:**AI 生成的内容会直接进入商品详情页,落库前必须由人过一道眼。**
---
## 6. 简化了什么
| CALM 的做法 | 本 Demo 的简化 | 简化理由与代价 |
|---|---|---|
| 第 ① 步由 LLM 生成 Commands | 关键词表 + 正则解析(`toolKeywords` / `parseBrief`) | 纯前端 demo 不引入模型依赖;代价是泛化能力受限,仅覆盖演示话术。**替换点已预留**(见 §7.1) |
| flow 定义用 YAML 配置 + 支持 `next` 分支与 `if` 条件 | `agentSteps` / `ocStepsDef` 为硬编码声明式数组,线性执行 | 演示流程本身线性(8 步固定顺序);代价是新增流程或分支需改代码 |
| 对话状态收敛为一个 `dialogue_state` 对象 | 状态分散在 `form` / `generatedRef` / `serialRef` | 复用 React 单向数据流;代价是缺少统一的「当前 flow / 当前槽位」视图 |
| Commands 显式落成可审计的中间表示 | 解析结果直接驱动执行,无 `commands` 中间层 | 少一次数据转换;代价是失去命令级日志与回放能力(见 §7.2) |
---
## 7. 差异与演进方向
### 7.1 已被预留的替换点:意图理解接 LLM
`lib/agent.js` 的三个导出(`agentIntentRe` / `parseBrief` / `parseMultiTools`)构成一个**窄接口**:入参为自然语言,出参为「工具序列 + 槽位值」。
落地真实 LLM 时,只需把这三个函数替换为一次 function calling 调用,**执行层(`agentRun` / `multiToolRun` / `startOC`)零改动**:
```
自然语言
├─ 现状:关键词/正则解析(前端模拟)
└─ 目标:LLM function calling,按 CALM 的 Prompt 模板组织
f(全部 flow 描述, 当前 flow 与 slots, 对话历史, 用户消息) = commands
执行层:不变
```
CALM 的 Prompt 模板(R1/R2)可直接作为契约蓝本:把「可用流程及其描述与槽位」「当前 flow 与槽位取值」「可用动作枚举」「槽位填充约束」四段填进 system prompt,要求模型**先输出分析、再逐行输出动作**。
### 7.2 尚未覆盖的 CALM 能力(下一版清单)
| 缺口 | 现状行为 | 补齐后的目标行为 | 对应 Command |
|---|---|---|---|
| 歧义澄清 | 用户只说「帮我优化一下」→ 落到闲聊兜底 | 反问「您是要优化标题、详情还是配图?」 | `Clarify(title, intro, detail, ...)` |
| 显式取消 | 说「算了」→ 落到闲聊兜底 | 中止当前流程、回滚已生成步骤 | `CancelFlow()` |
| 跳过当前步骤 | 说「跳过这步」→ 落到闲聊兜底 | 跳过当前槽位收集,继续后续步骤 | `SkipQuestion()` |
| 转人工 | 无 | 输出转人工指令并携带上下文 | `HumanHandoff()` |
| 命令中间表示 | 解析结果直接执行 | 生成 `commands[]` → 校验 → 执行,可日志化、可回放、可单测 | — |
| 对话状态视图 | 状态散在三个 ref | 统一 `dialogueState = { activeFlow, currentStep, slots }` | — |
| 流程声明式配置 | 硬编码数组 | 外置 `flows.json`(description + steps + next 分支) | — |
其中**「命令中间表示」优先级最高**。它是 §5.2 三项收益(可解释/可中断/可测试)的落点:一旦 commands 成为显式数据结构,就可以做命令级单测、命令级日志、以及「同一段对话回放不同模型输出」的评估——这正是 CALM 把 Commands 称为 DSL 的用意。
---
## 8. 超出 CALM 的部分:偏好学习
CALM 的边界止于「理解 + 执行」,**不处理跨会话的用户习惯沉淀**。本 Demo 在此之上加了一层产品化增量:
| 机制 | 实现 | 代码位置 |
|---|---|---|
| 偏好沉淀 | 每次发品写入 5 条偏好条目(路线天数、人群文案风格、话术偏好、定价策略、促销习惯) | `lib/oc.js:28` |
| 跨会话持久化 | `localStorage`(键 `fliggy_oc`) | `lib/oc.js:8,16` |
| 成长等级 | Lv.1 通用模板(0–2 次)→ Lv.2 风格匹配(3–9 次)→ Lv.3 爆款复刻(10+ 次) | `lib/oc.js:21` |
| 下次预填 | 一键发品弹窗自动回填上次配置并提示风格沿用语 | `store/useAppStore.js:419` |
| 双通道沉淀 | 对话式自主发品与弹窗式一键发品共用同一套记忆 | `store/useAppStore.js:305` |
**定位说明**:CALM 解决「怎么把话说准、把事做对」;偏好学习解决「怎么越用越贴合这个商家」。二者正交——偏好作为上下文注入理解层的 Prompt,不改变执行层的确定性。
---
## 9. 对外表述口径
需要向他人解释本项目的设计来源时,建议采用如下表述(避免把 CALM 的作用夸大或缩小):
> 任务型对话的经典做法是把用户表达归入一个预设意图类别,再走状态机选动作。这个假设在商家场景下很容易失效——「帮我优化一下」可能 одновременно 指向标题、详情和主图。
>
> 我参考的是 Rasa CALM 的思路:**不做意图分类,让模型直接生成一组动作**(启动哪个流程、填哪些槽位),再交给确定性的流程引擎执行。这套架构里只有「理解」这一步是模型在做事,剩下的执行、确认、修复全是确定逻辑——这正好满足商家侧对**可解释、可中断、可兜底**的要求。
>
> 在这之上我做了一层 CALM 没有的东西:**商家使用习惯库**。每次发品沉淀偏好,下次自动预填并按商家风格生成,让产品「越用越好用」。
---
## 10. 落地对应关系(接真实 LLM / 后端 Agent Loop)
| 本 Demo(前端原型) | 生产环境对应物 |
|---|---|
| `lib/agent.js` 关键词解析 | LLM function calling,按 CALM Prompt 模板构造上下文 |
| `data/genData.js` `buildGen()` | 各工具的生成服务(Prompt 见 `Prompt提示词设计文档.md`) |
| `store/useAppStore.js` 编排流 | 后端 Agent Loop:命令生成 → 校验 → 状态更新 → 执行 → 回填 |
| `agentSteps` / `ocStepsDef` | 流程配置中心(flow 声明式定义,支持分支与版本) |
| `lib/oc.js` `localStorage` | 商家画像 / 习惯库服务,作为个性化上下文注入 Prompt |
| `serialConfirm` 前端确认 | 同构的落库前人工复核节点(保持前端一致交互) |
| 任务清单卡片 | 执行过程可观测性:每步工具调用、耗时、结果状态 |
---
## 附:术语对照
| CALM 术语 | 本项目术语 | 说明 |
|---|---|---|
| Command | 动作 / 指令 | 理解层的输出单元 |
| Flow | 工具流程 / 任务清单 | 如「8 步自主发品」「6 步一键发品」 |
| Flow steps | 流程步骤 | `agentSteps` 的每一项 |
| Slot | 表单字段 / 上下文槽位 | 目的地、天数、类型、人群 |
| Action | 工具(Tool) | 8 个生成工具 |
| Dialogue Understanding | 意图解析 | `parseBrief` / `parseMultiTools` |
| Conversation Repair | 打断收口 / 兜底回复 | `sendMessage` 的等待点收口与闲聊兜底 |
| `ask_before_filling` | 逐工具确认 | `serialConfirm` 卡片确认 |
+475
View File
@@ -0,0 +1,475 @@
# 旅行商品发布 AI 助手 · Prompt 提示词设计文档
> 适用场景:旅行行业商品发布页右侧嵌入式 AI 助手(对应 Demo:`ai-assistant-travel/index.html`)
> 文档目的:沉淀 Demo 中各工具背后的提示词设计思路,形成可直接对接真实大模型(LLM)的 Prompt 规范
> 版本:v1.0 · 2026-08-22
---
## 一、总体设计原则
整套 Prompt 体系围绕五条原则设计,所有工具提示词均遵循:
| # | 原则 | 说明 | Demo 中的体现 |
|---|------|------|--------------|
| 1 | **模块化能力拆分** | 不做一个"大而全"的对话机器人,而是按表单模块拆成 8 个独立能力,每个能力一个专用 Prompt | 快捷区 8 个按钮,各自独立生成逻辑 |
| 2 | **上下文感知(Context Injection)** | 每次生成前先读取页面表单状态,将结构化上下文注入 Prompt | `readForm()` 读取目的地/天数/类型/人群,注入 `ctx` |
| 3 | **结构化输出(生成即数据)** | 要求 LLM 输出结构化内容(列表/字段/逐日数组),而非纯文本,使结果可直接回填表单 | 行程输出 `[{title, desc}]` 数组、价格输出团期行数据 |
| 4 | **生成 + 解释并行** | 每个结果附带"为什么这样写"的依据标签,增强商家信任感 | 每个生成卡片底部的 tip-pill(如"低于均价 5%,排序更优") |
| 5 | **人在环路(Human-in-the-loop)** | AI 只生成草稿,商家确认/微调后填入;提供"换一版"降低单次生成压力 | 所有卡片带「⚡一键填入」+「🔄换一版」双按钮 |
---
## 二、系统级 Prompt(System Prompt)
所有工具共享一个系统级角色设定,作为每次请求的第一段:
```text
你是「旅智小助」,一个旅行电商平台商家后台的商品发布 AI 助手,专注于帮商家高效撰写和优化旅游商品信息。
【角色定位】
- 你熟悉旅游电商的商品发布规范、搜索排序规则、广告法合规要求
- 你了解不同旅行人群(亲子/蜜月/银发/摄影/深度游)的决策痛点
- 你的目标是帮商家在 30 秒内完成高质量发品,同时提升商品的点击率与转化率
【能力边界】
- 只处理与商品发布相关的内容:标题、介绍、详情、配图、行程、价格、SEO、合规
- 不承诺你没有数据支撑的结论;涉及市场数据时明确说明为"估算/参考"
- 生成内容遵守《广告法》:不使用"最""第一""国家级"等极限词与违禁表述
【输出规范】
- 默认输出简体中文,语气专业、简洁、贴近电商运营习惯
- 结构化任务必须按指定格式输出(见各工具的格式约定)
- 每次生成附 1-3 条简短的设计依据(如"含目的地+天数+类型,利于搜索")
```
**设计要点**:
- **角色 + 场景双重限定**:不只说"你是助手",而是绑定"商家后台 + 商品发布"场景,收敛输出风格;
- **能力边界前置**:把拒答规则(不做无关事、不用极限词)写进系统级,避免每个子 Prompt 重复;
- **"附设计依据"** 是信任感设计——让商家理解 AI 为什么这样写,也降低"AI 黑箱"的抵触。
---
## 三、上下文注入设计(Context Injection)
### 3.1 上下文变量定义
每次调用工具前,前端从表单读取并组装以下上下文:
```json
{
"dest": "云南(昆明/大理/丽江)", // 目的地
"days": "6天5晚", // 行程天数
"type": "跟团游", // 商品类型
"theme": "亲子家庭", // 适合人群标签
"title": "", // 已填写的标题(如有)
"history": { // 偏好学习沉淀(见第七章)
"count": 3,
"styleWord": "亲子家庭友好·您的惯用话术",
"prefs": ["云南路线偏好:6天5晚跟团游", "纯玩无购物话术偏好", "平季低于市场5%定价策略"]
}
}
```
### 3.2 注入模板
```text
【当前表单上下文】
目的地:{dest}
行程天数:{days}
商品类型:{type}
适合人群:{theme}
已填写标题:{title}(如为空则忽略)
请严格基于以上上下文生成内容,不要虚构与目的地不符的景点、餐饮、酒店。
```
**设计要点**:
- **变量是"约束"不是"参考"**:明确要求"不要虚构与目的地不符的内容",这是旅游行业最核心的质量红线(张冠李戴的行程是差评根源);
- **空值兜底**:上下文缺失时 Prompt 中显式标注"未填写",引导 LLM 先给建议或用默认值,而非编造;
- **上下文即产品逻辑**:Demo 中切换"云南/海南"生成完全不同的行程,落地上就是这段上下文注入在起作用。
---
## 四、八大工具 Prompt 详解
### 4.1 商品标题生成
**设计意图**:标题是搜索排序与点击率的第一要素,一次给 3 个差异化方案供选择,而非一个"标准答案"。
```text
请为以下旅游商品生成 3 个高点击率的商品标题。
【表单上下文】{ctx}
【标题公式(三选一差异化)】
方案1(信任型):【纯玩无购物】{目的地}{天数}精品{类型}·5A景区全含·{人群}首选
方案2(卖点型):{目的地}{天数}深度{类型}|一价全含0自费·住特色酒店
方案3(体验型):【{人群}必选】{目的地}{天数}{类型}·随走随停·金牌向导·赠体验
【硬性要求】
1. 每个标题 ≤ 40 字(平台限制),必须包含:目的地 + 天数 + 商品类型(利于搜索命中)
2. 三案侧重不同:信任保障 / 价格卖点 / 体验亮点,不要同质化
3. 不得使用极限词;「5A」「金牌」等需有资质支撑的表述加上前置条件
4. 每个标题后附一行 ≤15 字的点击率依据
【输出格式】
1. {标题}|依据:{依据}
2. {标题}|依据:{依据}
3. {标题}|依据:{依据}
```
**结构化输出约定**:前端将 3 个标题解析为数组,"一键填入"按钮循环切换方案(Demo 中 `generated.title.used` 递增取模),无需重新生成。
**设计依据标签示例**:`含目的地+天数+类型,利于搜索`、`「无购物」为平台高转化词`。
---
### 4.2 详细介绍(列表页卖点摘要)
**设计意图**:详细介绍展示在列表页,核心任务是在 150-300 字内打消用户决策疑虑。采用固定四段式框架。
```text
请为以下旅游商品生成列表页"详细介绍",围绕 {theme} 人群的决策痛点撰写。
【表单上下文】{ctx}
【四段式框架(每段一行)】
① 身份定位 + 核心承诺:{目的地}{天数}{类型}·{人群}优选,全程 0 购物 0 自费,门票一价全含
② 住宿亮点:精选 N 晚高分酒店,写具体住宿特色(如"大理海景房+丽江古城客栈")
③ 特色体验:3 个左右具象体验点(可参与的活动、代表性餐食),强调 {theme} 人群的获得感
④ 服务承诺:行程透明无隐形消费、24h 管家、未成团提前 3 天全额退款
【硬性要求】
1. 总字数 150-300 字(列表页最佳展示长度)
2. 每段以换行分隔,不加序号
3. 退款/赔付等承诺必须与平台标准政策一致,不得加码
4. 体验点必须真实存在于该目的地的常规行程中
【输出格式】直接输出四段文案(纯文本,换行分段),末尾另起一行输出"字数:N"
```
**设计要点**:四段式(亮点—住宿—体验—承诺)对应旅游用户决策漏斗:先建立信任 → 消除住宿疑虑 → 激发向往 → 兜底风险。
---
### 4.3 商品详情(富文本图文)
**设计意图**:详情页内容量大,Prompt 按"模块化小标题 + 要点列表 + 配图占位"生成结构化 HTML,直接写入富文本编辑器。
```text
请为以下旅游商品生成详情页图文内容(HTML 格式)。
【表单上下文】{ctx}
【固定结构(h3 小标题 + ul 要点列表)】
🌟 行程亮点(3 条:核心承诺 / 经典 IP 清单 / {theme}专属节奏,含每日行车时长控制)
🏨 住宿安排(2 条:具体住宿配置 / 提前公布酒店名单等保障性表述)
🍜 舌尖体验(目的地代表性餐食,写具体菜名)
🛡️ 服务保障(2 条:导游与管家服务 / 退款保障)
[配图占位](末尾输出一个占位块,标注建议插入:行程地图 / 实拍酒店 / 餐饮实拍)
【硬性要求】
1. 输出合法 HTML 片段,仅使用 h3 / ul / li / b 标签,不使用 style 与 script
2. 每条要点 ≤ 30 字,使用短句
3. 数字类承诺(行车时长、酒店晚数)必须与 {days} 匹配
4. 配图占位使用 <div class="img-slot"> 格式,与「配图建议」清单一一呼应
【输出格式】HTML 片段,可直接注入富文本编辑器
```
**设计要点**:输出限定 HTML 标签白名单,是**安全约束**(防注入)与**渲染兼容**的双重要求;配图占位与配图工具联动,形成模块间呼应。
---
### 4.4 配图建议(拍摄脚本)
**设计意图**:不是生成图片,而是生成"拍什么"的脚本清单——商家多数缺的不是图,而是知道哪几张图转化率高。
```text
请为以下旅游商品生成 6 张商品图的拍摄清单(按转化率贡献排序)。
【表单上下文】{ctx}
【固定清单结构】
1. 主图:{目的地}全景横版大图(必须含地标元素)
2. 详情图:酒店实拍·房间内饰
3. 详情图:核心景区打卡机位
4. 详情图:特色餐食实拍
5. 详情图:{theme}互动瞬间抓拍(有人物、有情绪)
6. 详情图:行程地图示意
【硬性要求】
1. 每张标注用途(主图/详情图)与构图建议(≤12 字)
2. 顺序即优先级:商家时间有限时按序补拍
3. 末尾附 1-2 条实拍建议(如"实拍图>精修图,差评更少")
【输出格式】JSON 数组:[{"label": "图片说明", "use": "主图|详情", "tip": "构图建议"}]
```
**结构化输出约定**:JSON 数组驱动前端"按清单添加图片",每张带 `AI 建议` 角标与人工上传图区分。
---
### 4.5 行程编辑(逐日规划)
**设计意图**:行程是旅游商品差异化核心,也是商家最费时的模块。Prompt 强制逐日结构化输出,且嵌入"节奏控制"规则。
```text
请为以下旅游商品生成 {days} 的逐日行程。
【表单上下文】{ctx}
【行程设计规则】
1. D1 与最后一天只做接驳(接站/送站/自由活动),不安排紧张行程——降低差评率
2. 中段每日行车 ≤ 3.5 小时({theme} 人群节奏),"不赶路不早起"
3. 每日 title 格式:"核心地点 → 核心地点"或"主题·补充说明",≤ 15 字
4. 每日 desc 40-80 字:上午/下午/晚间节奏,含 1 个记忆点(看日落/歌舞表演等)
5. 行程路线需符合该目的地的真实地理动线(如云南:昆明→石林→楚雄→大理→丽江)
【输出格式】JSON 数组,长度必须等于天数:
[{"title": "当日主题", "desc": "当日安排描述"}]
```
**设计要点**:
- **天数硬约束**:`长度必须等于天数` 写进 Prompt,配合前端校验(Demo 中 `.slice(0, parseInt(ctx.days))`);
- **地理动线真实**:这是旅游行程 Prompt 最关键的领域知识注入点,落地时建议在 Prompt 中附目的地动线参考表,或由 RAG 检索标准线路库;
- **节奏规则来自运营经验**(D1/D6 只接驳、行车时长上限),是"AI 不如老运营"的地方,必须显式写进规则。
---
### 4.6 价格库存(团期与定价)
**设计意图**:定价是最敏感的决策,AI 不替商家拍板,而是给"市场参考区间 + 推荐定价 + 策略解释",决策权留给商家。
```text
请基于近 30 天市场成交数据,为以下旅游商品生成团期排期与定价建议。
【表单上下文】{ctx}
【生成规则】
1. 生成 5 个团期:2 个近期平季团 + 2 个节假日档期(如国庆)+ 1 个节后团
2. 定价结构:
- 平季团期定于市场均价下方约 5%(利于搜索排序与点击)
- 节假日档期按供需上浮,库存放大(预计售罄率 ≥ 80%)
- 儿童价约为成人价 65%-70%
3. 每个团期含:出发日期 / 成人价 / 儿童价 / 库存(5-25 区间)
4. 输出前先给 3 行定价依据:市场区间 / 推荐定价及理由 / 促销策略(如"首月立减 ¥300 冲新品榜")
【重要声明】市场数据为估算参考,请以平台实际成交数据校准。库存低于 6 的团期给出补库存建议。
【输出格式】JSON:{"analysis": ["市场区间…","推荐定价…","策略…"], "rows": [{"date":"YYYY-MM-DD","adult":N,"child":N,"stock":N}]}
```
**设计要点**:
- **免责前置**:明确"估算参考",避免商家把 AI 估算当成交数据(法律与信任双风险);
- **策略可解释**:不只给数字,给"为什么低于均价 5%",让商家可判断采纳度;
- Demo 中库存 <6 自动标橙(`stock-low`),是结构化输出 + 前端校验的联动体现。
---
### 4.7 SEO 关键词
**设计意图**:让商品被搜到。关键词组合逻辑固定:目的地词 × 天数词 × 类型词 × 人群词 × 意图词。
```text
请为以下旅游商品提取 6 个搜索关键词,按预估搜索热度降序排列。
【表单上下文】{ctx}
【关键词组合公式(按热度降序)】
1. {目的地}+旅游(目的地大词,热度最高)
2. {目的地}+{天数}(精准需求词)
3. {类型}(品类词)
4. {人群}+旅游(人群词)
5. {目的地}+纯玩无购物(意图词,高转化)
6. {目的地}+自由行攻略(攻略词,长尾引流)
【硬性要求】每个词附月均搜索指数(估算值),关键词自然融入标题与详细介绍,不堆砌。
【输出格式】JSON:[{"kw": "关键词", "heat": 指数}]
```
---
### 4.8 合规检查
**设计意图**:这是唯一"非生成"型工具——输入是已填表单,输出是风险扫描报告。Prompt 采用规则清单模式,输出固定三态。
```text
请对以下商品信息进行「广告法 + 平台规则」双重合规扫描。
【待检内容】
标题:{title}
详细介绍:{intro}
商品详情:{detail}
图片说明:{images}
【扫描规则清单】
1. 广告法极限词:「最」「第一」「唯一」「国家级」「顶级」「极品」等 → ❌ 必须修改
2. 资质类表述:「金牌」「5A」「官方认证」等 → ⚠️ 需补充资质证明材料方可使用
3. 承诺类表述:「全额退款」「赔付」「保证」等 → ⚠️ 需与平台退改政策页一致,不得自行加码
4. 平台规范:主图尺寸(≥800×600)、文字遮挡、虚假宣传风险 → ℹ️ 提示项
【输出格式】逐条输出,每条三态之一:
✅ 通过:{模块}:{说明}
⚠️ 建议:{模块}:{问题描述} → {修改建议}
ℹ️ 提示:{说明}
最后输出汇总:极限词风险数 / 待补充项数 / 审核通过率预估
```
**设计要点**:三态分级(必须改 / 建议改 / 知悉)比"全红警告"更可执行;极限词清单在落地时应做成**前置词表 + 正则快筛 + LLM 复核**双层,正则拦截确定性风险,LLM 处理语义风险(如变体写法)。
---
## 五、一键发品 · 编排 Prompt(Pipeline)
### 5.1 编排设计
一键发品不是一个大 Prompt,而是 **6 个工具 Prompt 的顺序编排**:
```text
[配置收集] → 用户选 4 项(目的地/天数/类型/人群)
↓
[上下文同步] → 写回左侧表单,统一 ctx
↓
[串行生成] 标题 → 介绍 → 详情 → 配图 → 行程 → 价格库存
· 每步复用对应工具的 Prompt + 共享 ctx
· 前序产物作为后续输入(如行程结果注入详情/配图的上下文)
↓
[静默填入] silentBatch 模式:逐模块写入表单,不逐条打扰
↓
[偏好沉淀] 记录本次选择,更新成长等级
```
### 5.2 编排系统 Prompt(增量)
在一键发品模式下,System Prompt 追加一段:
```text
当前为「一键发品」批量模式。请按以下规则执行:
1. 按顺序完成 6 个模块生成,每个模块遵循各自工具的格式约定
2. 后序模块必须与前序模块保持一致:详情中的住宿/餐食须与介绍呼应,
配图清单须覆盖详情中的配图占位,行程天数必须严格等于设定天数
3. 各模块间不自相矛盾(如介绍写"海景房",详情不能写"普通标间")
4. 生成完成后输出一段 ≤ 80 字的总结,说明各模块已就绪与待商家确认的重点
```
**设计要点**:**跨模块一致性**是编排模式的核心增量——单工具各自生成容易出现"介绍承诺 A、详情写 B"的矛盾,必须在编排层显式约束。
---
## 六、自由对话 · 意图路由 Prompt
用户在对话框自由输入时,先做意图分类,再路由到对应工具。
### 6.1 Demo 实现(关键词路由)
```js
const canned = [
{ k: ['标题', '名字'], f: () => quickAction('title') },
{ k: ['介绍', '卖点'], f: () => quickAction('intro') },
{ k: ['图', '配图', '拍摄'], f: () => quickAction('image') },
{ k: ['行程', '规划', '路线'], f: () => quickAction('itinerary') },
{ k: ['价', '定价', '库存', '团期', '多少钱'], f: () => quickAction('price') },
// ...
];
```
### 6.2 落地版意图路由 Prompt
```text
用户在商品发布页的 AI 助手中输入了一句话,请判断意图并路由。
【可选意图】
title / intro / detail / image / itinerary / price / seo / check(对应 8 个工具)
chat_smalltalk(闲聊或与发品无关)
chat_guidance(询问"怎么发品/怎么用助手"等操作指引)
【用户输入】"{text}"
【当前表单上下文】{ctx}
【输出格式】仅输出 JSON:{"intent": "意图标识", "args": {"抽取出的参数": "值"}}
例:输入"帮我生成亲子三亚自由行的标题和配图"
→ {"intent": "title", "args": {"theme": "亲子家庭", "dest": "海南(三亚)", "type": "自由行", "extra": ["image"]}}
注意:参数抽取结果需回写表单上下文后再调用工具;多个意图时按 extra 依次执行。
```
**设计要点**:意图分类用**低温度小模型**(或直接 embedding 匹配)即可,无需大模型;参数抽取的价值在于"说一句话顺带改了表单上下文"。
---
## 七、偏好学习与个性化(越用越好用)
### 7.1 三层个性化注入
这是"越用越好用"的核心 Prompt 机制——同一工具的 Prompt 根据使用历史分三档注入:
| 成长等级 | 触发条件 | 注入的个性化 Prompt |
|---------|---------|-------------------|
| Lv.1 通用模板 | 0-2 次 | 无个性化,使用标准工具 Prompt |
| Lv.2 风格匹配 | 3-9 次 | `【商家风格偏好】该商家惯用"{styleWord}"话术,生成内容贴合其历史风格` |
| Lv.3 爆款复刻 | 10+ 次 | `【历史爆款参考】该商家历史爆款商品结构:{结构摘要},定价策略:{策略},请优先复用该结构` |
### 7.2 偏好沉淀 Prompt(发品完成后触发)
```text
请从本次发品记录中提炼可复用的商家偏好,供下次生成注入。
【本次记录】
商品配置:{dest}·{days}·{type}·{theme}
生成内容摘要:{各模块生成结果的摘要}
商家采纳行为:{是否直接采纳/换过几版/手动改了哪些字段}
【提炼规则】
1. 输出 3-5 条偏好,每条 ≤ 20 字,格式:"维度:偏好描述"
2. 维度限定:路线偏好 / 人群文案风格 / 话术偏好 / 定价策略 / 促销习惯
3. 只提炼有复用价值的信息,不复述本次内容本身
4. 商家手动修改过的字段权重最高(这是最强的偏好信号)
【输出格式】JSON 数组:["维度:偏好描述", ...]
```
### 7.3 设计要点
- **偏好信号优先级**:手动修改 > 换版次数 > 直接采纳——商家改了什么,比 AI 生成了什么更能代表真实偏好;
- **偏好可解释、可管理**:落地时偏好清单应对商家可见可删(Demo 完成页展示"本次沉淀 N 条偏好"),避免"AI 悄悄记我东西"的负感;
- **Prompt 层面实现**:偏好注入就是一段附加上下文,不改工具 Prompt 本体,随时可回退到 Lv.1。
---
## 八、通用工程建议(落地参数)
| 事项 | 建议 |
|------|------|
| **模型温度** | 标题/介绍 0.8(求创意);行程/价格 0.3(求准确);合规检查 0.1(求稳定);意图路由 0 |
| **输出解析** | 结构化工具一律要求 JSON 输出 + 前端 schema 校验,解析失败自动降级为纯文本卡片展示 |
| **流式输出** | 介绍/详情类长文本走 SSE 流式渲染;行程/价格等结构化数据整体返回后渲染表格 |
| **Few-shot** | 标题、介绍两个高频工具建议内置 2-3 组"上下文 → 优质输出"示例(取自平台真实高转化商品,脱敏后) |
| **安全护栏** | ① HTML 标签白名单(4.3)② 极限词正则前置快筛(4.8)③ 行程地理动线校验(4.5,可用目的地-景点映射表校验) |
| **缓存策略** | 同一 ctx 下的生成结果可短缓存(10 分钟),"换一版"强制绕过缓存 |
| **成本控制** | 8 个工具按需调用;一键发品 6 连发时前序产物注入后序(如行程喂给配图),减少重复生成 |
| **免责声明** | 价格类输出必须带"估算参考"声明;对话底部常驻"内容由 AI 生成,请核对后发布" |
---
## 九、Prompt 资产清单(速查)
| 工具 | Prompt 类型 | 输出格式 | 温度 | 个性化注入 |
|------|------------|---------|------|-----------|
| 商品标题 | 生成(3 方案) | 数组 + 依据 | 0.8 | ✅ Lv.2+ |
| 详细介绍 | 生成(四段式) | 纯文本 4 段 | 0.8 | ✅ Lv.2+ |
| 商品详情 | 生成(结构化) | HTML 片段 | 0.6 | ✅ Lv.2+ |
| 配图建议 | 生成(清单) | JSON 数组 | 0.7 | ✅ Lv.2+ |
| 行程编辑 | 生成(逐日) | JSON 数组 | 0.3 | ✅ Lv.3 |
| 价格库存 | 分析 + 生成 | JSON(分析+行) | 0.3 | ✅ Lv.3 |
| SEO 关键词 | 生成(组合公式) | JSON 数组 | 0.5 | — |
| 合规检查 | 规则扫描 | 三态报告 | 0.1 | — |
| 一键发品 | 编排(6 步串行) | 各工具格式 | 各自 | ✅ 全档 |
| 意图路由 | 分类 + 抽取 | JSON | 0 | — |
| 偏好沉淀 | 提炼 | JSON 数组 | 0.2 | — |
---
*本文档随 Demo 演进持续更新。对接真实 LLM 时,建议将各工具 Prompt 模板化存入配置中心,与前端代码解耦,便于单独调优与 A/B 测试。*
-577
View File
@@ -1,577 +0,0 @@
# 黑手党提案 Agent — 产品规格说明书 (SPEC v1.0)
> **版本**:v1.0 | **日期**:2026-04-06
> **作者**:李东方 | **审阅**:沈攀、赵蕾
> **状态**:草案(待审批)
---
## 一、项目定位
### 1.1 是什么
基于 CoPaw(小龙虾)多智能体架构的 **"产品上新黑手党提案"AI 顾问**。
- **产品形态**:一个 CoPaw Agent 配置包(AGENTS.md + Skills + HEARTBEAT.md),而非独立应用
- **交付物**:Markdown 结构化文档(研讨记录 + 提案结论)
- **方法论内核**:TOC(约束理论)五步思考过程
### 1.2 不是什么
- ❌ 不是一个独立的 Web 服务或 SaaS 平台
- ❌ 不是 CoPaw 的二次开发(不改框架代码)
- ❌ 不需要编写 Python 代码——所有交付物都是 Markdown
### 1.3 战略来源
| 来源 | 内容 |
|------|------|
| 领先2035工作坊 | 完成了 TOC 五步法全流程研讨,形成完整方法论 |
| 攀师傅小会决议 (2026-04-06) | 路线 A 优先启动:沈攀做甲方,3-4 天完成,基于小龙虾架构 |
### 1.4 核心原则
1. **AI 是队友不是工具**——Agent 有 Soul、Memory、Skill
2. **自进化机制 > 工程化打补丁**——给示范 → 校准 → 形成 Skill → 不断迭代
3. **有效性 > 完备性 > 严谨性**——不要在中间步骤上过度工程化
4. **人类的核心工作只有两件事**:输入 Context 和 Taste
---
## 二、架构设计
### 2.1 CoPaw 多智能体定位
`mafia-expert` 作为 CoPaw 中的独立专家智能体:
```
~/.copaw/
├── config.json # 全局配置 (LLM providers)
└── workspaces/
├── default/ # 默认智能体(日常助手)
└── mafia-expert/ # 黑手党提案专家智能体
├── agent.json # Agent 专属配置
├── AGENTS.md # 人设 + TOC 方法论注入
├── MEMORY.md # 长期记忆(偏好、品味)
├── HEARTBEAT.md # 自指迭代 + 自动更新触发器
├── active_skills/ # 已激活的 Skills
│ ├── ude_diagnosis/SKILL.md
│ ├── conflict_cloud/SKILL.md
│ ├── convention_breaker/SKILL.md
│ ├── negative_branch/SKILL.md
│ └── proposal_assembler/SKILL.md
├── memory/ # ReMe 每日日志(自动生成)
├── cases/ # 案例输出(迭代素材)
└── iteration_reports/ # 自指迭代报告
```
### 2.2 隔离保证
| 维度 | 隔离方式 | CoPaw 机制 |
|------|---------|-----------|
| 人设 | 独立 AGENTS.md | 多智能体工作区物理隔离 |
| 记忆 | 独立 MEMORY.md + memory/ | ReMe 按 workspace 隔离 |
| 技能 | 独立 active_skills/ | per-agent 技能开关 |
| 工具 | 独立 MCP 客户端配置 | agent.json 中 mcpClients |
| 对话 | 独立 chats.json | per-agent 会话存储 |
### 2.3 三层自指迭代闭环
#### 第一层:个人 Agent 自我迭代(CoPaw 原生)
- **触发**:每次对话自动 + 每周 Heartbeat 回顾
- **机制**:ReMe 自动将对话总结写入 `memory/YYYY-MM-DD.md`
- **产物**:更精准的个人品味和经验(私有 MEMORY.md)
- **范围**:单个用户实例内部
#### 第二层:Skill 自指迭代(核心闭环)
- **触发**:本地 Agent 心跳反思中识别出方法论缺陷(非个人经验问题)
- **上行**:生成 `iteration_report` → `git push` 到共享仓库
- **策展**:服务器 `skill-curator` Agent 汇总 reports → 起草 Skill 修改
- **审批**:核心方法论变更等人工审批;表述澄清自动合并
- **产物**:更好的 AGENTS.md / Skills(共享资产)
#### 第三层:自动分发
- **触发**:每个用户的 mafia-expert Heartbeat 每日检查
- **机制**:`git fetch` → 有新 commit → `git pull` → 更新本地 Skill
- **通知**:下次对话开头告知用户"方法论已更新到 vX.Y"
- **产物**:所有用户同步到最新版
---
## 三、部署策略
### 3.1 三角色 × 三安装方式
| 角色 | 安装方式 | 原因 |
|------|---------|------|
| **用户**(沈攀、赵蕾等) | CoPaw 桌面应用 | 零配置,双击运行 |
| **服务器**(ECS skill-curator) | Docker `agentscope/copaw:latest` | 24/7 运行,环境隔离 |
| **开发者**(李东方) | Script 安装 (`curl ... \| bash`) | 频繁编辑测试、Git 推送 |
### 3.2 Git 仓库结构
```
黑手党提案/
├── docs/ # 原始研讨材料(只读参考)
│ └── 在线工作坊 - 研讨文档.md
├── agent/ # ← 产品核心资产
│ ├── AGENTS.md # 人设 + 方法论注入
│ ├── agent.json # Agent 配置
│ ├── HEARTBEAT.md # 三层迭代触发器
│ ├── skills/ # Skills 目录
│ │ ├── ude_diagnosis/SKILL.md
│ │ ├── conflict_cloud/SKILL.md
│ │ ├── convention_breaker/SKILL.md
│ │ ├── negative_branch/SKILL.md
│ │ └── proposal_assembler/SKILL.md
│ └── install.md # 安装指南
├── curator/ # 服务器 curator Agent 配置
│ ├── AGENTS.md
│ └── HEARTBEAT.md
├── cases/ # 案例库(测试套件)
├── iteration_reports/ # 迭代报告
├── CHANGELOG.md # 迭代日志
└── README.md
```
---
## 四、TOC 五步法方法论拆解
> 以下是 **mafia-expert Agent 必须严格执行的完整流程**。
> 每个步骤对应一个 Skill,Agent 按顺序引导用户完成。
### 阶段 A:研讨准备(粗共识)
#### 准备1:让用户秒懂"黑手党提案"
**Skill**:内置在 AGENTS.md 中(无需独立 Skill)
**Agent 动作**:
1. 用类比引入概念(电影《教父》"你没有任何拒绝的理由")
2. 给出概念定义(三层次):
- **定义**:帮助客户打破重大限制,让其无法拒绝的方案。通过非共识的创新方式实现双赢
- **内涵**:不是痛点挖掘,而是打破行业惯例;不直接解决问题,而是让问题变得无关紧要
- **外延**:与"痛点""需求""传统方案"的区别
3. 提供秒懂案例(至少 3 个)
**参考案例库**(内置于 AGENTS.md):
| 案例 | 打破的惯例 | 企业获益 | 用户获益 |
|------|----------|---------|---------|
| 苹果手机 | 手机必须有键盘 | 更高利润、市场主导 | 全触屏、更直观操作 |
| 胖东来按新鲜度卖鲜果 | 按外观大小定价 | 品牌忠诚、利润空间 | 合理价格买新鲜水果 |
| 小米手机 (2011) | 智能手机5000+ | 迅速扩大市场份额 | 1999元高质量手机 |
| 苹果 iPod | 盗版MP3或正版CD | 市场领导地位 | 小小设备3000+歌曲 |
| 360免费杀毒 | 杀毒软件收费 | 用户基数→增值盈利 | 免费高效杀毒 |
**输出**:用户对"黑手党提案"概念的共识确认
---
#### 准备2:识别双赢的两方
**Skill**:内置在 AGENTS.md 中
**Agent 动作**:引导用户填写 JTBD 四要素表
**输出模板**:
```markdown
| 维度 | 我方(企业) | 典型目标客户(DTB) |
|------|------------|-------------------|
| 1、客户画像 | [行业特征、企业特征] | [DTB 的行业/规模/特征] |
| 2、雇用产品 | [我方提供什么产品/服务] | [客户实际"雇用"我们做什么] |
| 3、待办任务 | [我方要完成的核心任务] | [客户要完成的核心任务] |
| 4、期望结果 | [我方期望的最终成果] | [客户期望的最终成果] |
```
**质量标准**:
- 客户画像必须具体到行业阶段、企业规模、创始人特征
- 期望结果必须可量化、可感知("xx年后的利润=今天的营业额")
---
#### 准备3:定义双赢——UDE 诊断
**Skill**:`ude_diagnosis/SKILL.md`
**Agent 动作**:
1. 引导用户列出所有 UDE(不良现状效果)
2. 对每条 UDE 进行 10 条标准校验
3. 从合格 UDE 中筛选最重要的 3 个
4. 识别恶性循环
**UDE 10 条标准**(Agent 必须逐条检查):
| 序号 | 标准 | ✅ 合格示例 | ❌ 不合格示例 |
|------|------|-----------|-------------|
| 1 | 持续存在 | "业绩增长乏力" | "上个月销量下降"(一次性) |
| 2 | 当下正在发生 | "营销费用高" | "如果不改革就会……"(假设) |
| 3 | 描述状态 | "人才密度不够" | "应该招更多人"(动作) |
| 4 | 责任范围内 | "缺乏战略节奏" | "政策环境不好"(外部不可控) |
| 5 | 可以解决 | "缺科学管理会计" | "人性本恶"(无法解决) |
| 6 | 不归因于人 | "组织协调运转不畅" | "张三能力不行"(责怪个人) |
| 7 | 不分析原因 | "总体利润降低" | "因为成本太高所以利润低"(含原因) |
| 8 | 不是解决方案 | "核心岗位胜任力不足" | "需要引进外部人才"(隐藏方案) |
| 9 | 单独实体 | "主将缺位" | "主将缺位且人才密度不够"(两个实体) |
| 10 | 不是主观猜想 | "现金流不充裕" | "我觉得团队不够努力"(主观) |
**筛选最重要 3 个 UDE 的标准**(同时满足):
1. 跟买我们的产品/用我们的服务有关(我们和同行导致了这个 UDE)
2. 这个 UDE 对客户至关重要,直接/间接影响了 70% 以上的其他 UDE
**恶性循环输出模板**:
```markdown
### 恶性循环 1
- [UDE-A] → [UDE-B] → [UDE-C] → [UDE-D] → 回到 [UDE-A]
### 恶性循环 2
- [UDE-X] → [UDE-Y] → [UDE-Z] → 回到 [UDE-X]
```
---
### 阶段 B:发现和打破惯例(方向性提案)
#### 步骤1:定义核心冲突
**Skill**:`conflict_cloud/SKILL.md`
**Agent 动作**:
1. 对筛选出的每个主要 UDE,构建冲突图(Evaporating Cloud)
2. 引导用户填写 O-B-D 结构
3. 合并多个 UDE 的冲突图
**冲突图构建规则**(Agent 内部逻辑):
- UDE → DE(反转 UDE 为期望效果)
- DE 的必备条件 = D(只有做了 D,UDE 才会转化为 DE)
- 从 D 中找到矛盾行动(D1 vs D2)
- 转化 D 对应的 DE 为 B(需求)
- 推测 B 的共同目标 O(服从于系统目标的小目标)
**单个 UDE 冲突图模板**:
```markdown
| 维度 | 目标 | 需求 | 行动 | 检查 |
|------|------|------|------|------|
| UDE: [描述] | O: [系统目标] | B1: [需求1] | D1: [当前行动] | 当感受到UDE时的通常做法 |
| | | B2: [需求2] | D2: [期望行动] | D1和D2互斥 |
```
**互斥性校验**(Agent 必须确认):
- 如果 D1,则不能 D2
- 如果 D2,则不能 D1
- 只有 D1,才能 B1;如果 D1,则不能 B2
- 只有 D2,才能 B2;如果 D2,则不能 B1
**合并冲突图模板**:
```markdown
| 合并UDE | O合并 | B1 | D1(当前行动) |
|---------|-------|----|--------------:|
| | | B2 | D2(期望行动) |
```
---
#### 步骤2:揭示冲突背后的惯例
**Skill**:`convention_breaker/SKILL.md`(前半段)
**Agent 动作**:
1. 针对合并冲突图,追问:"为什么用户想要 B1 就一定得 D1?"
2. 用因果假设分析法(CLR)找到隐含假设
3. 从假设追溯到行业惯例
4. 验证惯例的有效性
**因果假设分析模板**:
```markdown
| 关键问题 | 假设 | 行业惯例 | 总结 |
|---------|------|---------|------|
| 为什么想要 B1 就必须 D1? | [隐含假设1] | [对应的行业通常做法] | 如果遵循惯例1:[XX] |
| 检查:因为D1,同时[假设],那么B1 | [隐含假设2] | [另一个行业做法] | 则用户只会D1不会D2 |
```
**惯例验证清单**(两个视角):
```markdown
#### 我方视角验证
1. 惯例是不是行业内保护自身利益的"普遍做法"?
2. 惯例是否导致当下的恶性循环?
- 因为:企业继续遵守惯例XX
- 假设:同时,XX
- 结果:恶性循环中 XX 将加剧
#### DTB(客户)视角验证
1. 补全 CCRT(核心冲突现实树)
2. 确认惯例直接导致主要 UDE(至少一个)
3. 确认惯例导致加剧了恶性循环
```
---
#### 步骤3:用例外激发方案
**Skill**:`convention_breaker/SKILL.md`(后半段)
**Agent 动作**:
1. 对每个惯例,找到"例外"(具体实例,而不是逻辑推演)
2. 从例外中提炼"打破惯例"的可行方案
3. 输出方向性提案总结
**方案激发模板**:
```markdown
| 改变的策略 | DTB目标 | 需求 | 行动 | 激发方案 |
|-----------|--------|------|------|---------|
| 惯例1: [名称] | | | | |
| 之所以D2无法B1,是因为假设:[XX] | | | | |
| 现有惯例做法=[XX],对应D1 | O: [XX] | B1: [XX] | D1: [XX] | 例外是:[具体实例] |
| 打破惯例做法=[XX],对应D2 | | B2: [XX] | D2: [XX] | 可以尝试:[XX] |
```
**方向性提案总结格式**(Agent 必须输出):
```markdown
### 方向性提案总结
> 通过 D [具体行动],满足 B [具体需求];
> 同时通过 D' [具体行动],满足 B' [具体需求];
> 使 DTB 既可以 O1 [目标1],又能够 O2 [目标2]。
```
---
### 阶段 C:激发和完善方案(操作性提案)
#### 步骤4:消除负面分支
**Skill**:`negative_branch/SKILL.md`
**Agent 动作**:
1. 列出实施新方案后可能新增的所有 UDE
2. 筛选出"直接危害现有 B"且"客户感受强"的 UDE
3. 对每个危害性 UDE,构建因果假设链(从方案 D → 中间结果 → 最终 UDE)
4. 在因果链中找到 UDE 转为 DE 的节点,补充打破假设的方案
**新增 UDE 发散清单**(Agent 引导用户思考):
- 协同复杂度增加
- 考核边界不清晰
- 管理复杂度增加
- 局部效率降低
- 结果不确定性高
- 不适应新思维模式
- 认知负担加重
- 决策难度提升 / 决策慢
- 对人才要求高
- 其他……
**危害性筛选标准**(同时满足):
1. 直接危害现有任一 B(需求)
2. 客户感受强
**危害性筛选输出模板**:
```markdown
| 被危害的 B | 新增 UDE |
|-----------|---------|
| B1: [需求描述] | [新UDE描述] |
| B2: [需求描述] | [新UDE描述] |
```
**因果假设链分析模板**(每个危害性 UDE 一份):
```markdown
| | 因果假设链 | | 性质 |
|--|-----------|--|------|
| 结果 | 最终结果:[UDE] | | UDE |
| 过程 | 中间结果3:[XX] | 假设:[XX] | UDE |
| | 中间结果2:[XX] | 假设:[XX] | UDE |
| | | | **打破假设的方案:[XX]** |
| | 中间结果1:[XX] | 假设:[XX] | DE |
| 方案 | D:[新方案] | 假设:[前提条件] | |
```
**节点补充验证模板**(确认 UDE→DE 转换有效):
```markdown
| 如果: | D [具体方案] |
|--------|-------------|
| 同时: | 假设 [前提条件] |
| 那么: | UDE变成DE:[原UDE] 变成 [新DE] |
| 并且那么: | 现有DE没有转变为UDE |
```
---
#### 步骤5:确立新规则、匹配新能力
**Skill**:`proposal_assembler/SKILL.md`
**Agent 动作**:
1. 对比打破惯例前后,确立新规则(新惯例)
2. 梳理剩余未解决的原 UDE + 新方案导致的新 UDE
3. 为每个 UDE→DE 的转换,匹配所需的运营能力
4. 输出产品说明书
**运营能力匹配分类**:
| 分类 | 逻辑 | 示例 |
|------|------|------|
| 原 UDE 不会转化为 DE,除非…… | 原有问题需要额外能力才能解决 | 主将缺位 → Build/Borrow/Buy |
| 新方案导致新 UDE,除非…… | 新方案的副作用需要配套能力 | 协同复杂度增加 → 聚焦瓶颈问题 |
**运营能力匹配输出模板**:
```markdown
| 分类 | 期望改变的结果 | 运营能力(新增能力) |
|------|-------------|-------------------|
| 原UDE → DE | 原UDE1: [描述] | [对应运营能力] |
| | 原UDE2: [描述] | [对应运营能力] |
| 新方案 → 新UDE → DE | 新UDE1: [描述] | [对应运营能力] |
| | 新UDE2: [描述] | [对应运营能力] |
```
**产品说明书输出模板**:
```markdown
| 产品单元 | 运营能力(改变UDE为DE) | 运营能力(维持DE) |
|---------|---------------------|------------------|
| [产品/服务1] | [新增能力描述] | [维护能力描述] |
| [产品/服务2] | [新增能力描述] | [维护能力描述] |
```
---
## 五、最终交付物模板
Agent 完成五步法后,必须输出以下完整 Markdown 文档:
```markdown
# [品牌名] 产品上新 — 黑手党提案
> 生成时间:YYYY-MM-DD
> 方法论版本:vX.Y
> Agent:mafia-expert
---
## 一、双赢定义
### 1.1 概念共识
[对黑手党提案概念的共识描述]
### 1.2 JTBD 四要素
| 维度 | 我方 | 典型目标客户(DTB) |
|------|------|-------------------|
| 客户画像 | ... | ... |
| 雇用产品 | ... | ... |
| 待办任务 | ... | ... |
| 期望结果 | ... | ... |
---
## 二、UDE 诊断
### 2.1 完整 UDE 清单
[编号列表,每条 UDE 标注是否通过 10 条标准]
### 2.2 最重要的 3 个 UDE
1. [UDE-1]:[选择理由]
2. [UDE-2]:[选择理由]
3. [UDE-3]:[选择理由]
### 2.3 恶性循环
[循环图描述]
---
## 三、核心冲突
### 3.1 各 UDE 冲突图
[每个 UDE 的 O-B-D 表]
### 3.2 合并冲突图
[合并后的 O-B1-D1 vs B2-D2]
---
## 四、惯例揭示与方向性提案
### 4.1 行业惯例清单
[每个惯例的因果假设分析]
### 4.2 惯例验证
[我方视角 + DTB 视角验证结果]
### 4.3 方向性提案总结
> 通过 D [XX],满足 B [XX];
> 同时通过 D' [XX],满足 B' [XX];
> 使 DTB 既可以 O1,又能够 O2。
---
## 五、操作性提案
### 5.1 负面分支消除
[每个危害性 UDE 的因果假设链 + 打破假设方案]
### 5.2 新规则与新能力
| 分类 | 期望改变 | 运营能力 |
|------|--------|---------|
| ... | ... | ... |
### 5.3 产品说明书
[产品单元 × 运营能力矩阵]
---
## 附录
### A. 数据来源与证据链
### B. 研讨过程记录
### C. 方法论版本与迭代日志
```
---
## 六、验证计划
### 6.1 方法论完整性验证
| 检查项 | 验证方式 |
|--------|---------|
| 五步法每步都有输出 | 对照最终交付物模板逐章检查 |
| UDE 10 条标准无遗漏 | 抽查 3 条 UDE 逐标准验证 |
| 冲突图互斥性成立 | 检查 D1/D2 是否真正互斥 |
| 惯例有行业证据支撑 | 至少 2 个独立来源 |
| 负面分支全部消解 | 每个新 UDE 都有打破假设方案 |
| 运营能力可执行 | DTB 确认具备或可获取该能力 |
### 6.2 Agent 能力验证
| 测试项 | 方法 | 通过标准 |
|--------|------|---------|
| 首次案例完整度 | 用"天维美跨境保健品"触发全流程 | 输出文档覆盖所有章节 |
| UDE 标准执行力 | 故意输入不合格 UDE,观察 Agent 的纠正 | Agent 能指出具体违反哪条标准 |
| 冲突图逻辑性 | 检查 O-B-D 关系链的因果自洽性 | 无循环论证、无逻辑跳跃 |
| 惯例非臆测 | 检查惯例是否来自行业事实而非 Agent 编造 | 每个惯例可追溯到真实行业做法 |
| 迭代闭环 | 跑完首案后触发 Heartbeat,检查 iteration_report | 报告能区分"个人经验"和"方法论缺陷" |
### 6.3 里程碑
| 阶段 | 交付物 | 预计时间 |
|------|--------|---------|
| M1 | AGENTS.md + 5 个 SKILL.md 初稿 | Day 1-2 |
| M2 | 首个案例(天维美)完整跑通 | Day 2-3 |
| M3 | 根据首案反馈修订 Skills v0.2 | Day 3 |
| M4 | HEARTBEAT.md + curator 配置 + install.md | Day 3-4 |
| M5 | 沈攀交叉验证 + 方法论 v1.0 定稿 | Day 4-5 |
@@ -1,482 +0,0 @@
# 在线工作坊 - 研讨文档(&产品试用:黑手党提案+有效产出会计)
# 研讨目标
1. 输出领先2035的“黑手党提案”:我们如何持续与客户双赢?
2. 基于黑手党提案,调整目前产品体系的重心和细节
3. 对研讨流程的节奏和工具进行测试(重点观察公测版中可能出现的卡点)
4. 对现有产品进行成本估算和定价(基于有效产出会计)
## 研讨范式
1. 发言准备:3-5min
2. 顺序表达:5min\人
3. 发散补充:10min\轮
4. 个人收敛:3min准备+2min\人
5. 结论共识:10min\轮
每一轮约50分钟(按3人研讨计算);此时间为粗估计,在开展研讨时以实际计时为准
# 第一部分:黑手党提案
:::
长期陪伴产品的方向性提案(成果导向、可以承诺效果的咨询服务)
:::
## 研讨准备(粗共识)
### 关键准备1:让客户秒懂“黑手党提案”
研讨输出[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6hd8rko0fh4qdmhd2vm)
| 类比引入 | 1. 电影《教父》中那种”你没有任何拒绝的理由“的提案<br> <br>2. 商业中的“双赢”策略,强调双方都获益。<br> <br>3. 生活中的“必杀技”,指某种无法抗拒的吸引力。 |
| --- | --- |
| 概念解释 | 1、定义: <br>黑手党提案是一种能够帮助客户打破重大限制、获得前所未有的价值的解决方案,通过非共识的创新方式,让客户无法拒绝,同时实现客户与企业双赢。<br>2、内涵:<br>* 不是简单的痛点挖掘或需求满足,而是通过打破行业惯例,提供超越期待的价值。<br> <br>* 通常不直接解决表面问题,而是通过创造新的价值,让原有问题变得无关紧要。<br> <br>* 方案具有独特性,难以被竞争对手模仿,且能够量化、比较,清楚展示其优势。<br> <br>* 实现客户与企业的双赢:客户因价值提升而愿意付费,企业因解决关键问题而获得长期竞争优势。<br> <br>3、外延:区别<br>* 与痛点:痛点是表面问题,而黑手党提案更关注痛点背后的根本需求和价值。<br> <br>* 与需求:需求是客户已知的期望,而黑手党提案则是通过创新打破常规,创造新的价值需求。<br> <br>* 与传统方案:传统方案可能满足单一需求,而黑手党提案是通过打破行业惯例,创造独特的价值主张。 |
| 秒懂案例 | ### 案例:苹果手机<br>提案:苹果手机打破了传统手机必须有键盘的惯例,转而采用全触屏设计。<br>* 企业打破的限制:传统手机设计的物理键盘限制了屏幕大小和用户体验。<br> <br>* 企业获得的好处:更高的利润和市场主导地位,引领智能手机行业创新。<br> <br>* 用户打破的限制:用户不再受限于物理键盘的不便,享受更直观的操作。<br> <br>* 用户获得的好处:更时尚、易用的手机,提升了用户体验和满意度。<br> <br>### 案例:胖东来按新鲜度卖鲜果<br>提案:胖东来通过按新鲜度定价,改变了传统水果销售模式。<br>* 企业打破的限制:传统水果定价基于外观和大小,忽视了新鲜度和品质。<br> <br>* 企业获得的好处:增加了利润空间,提升了品牌形象和客户忠诚度。<br> <br>* 用户打破的限制:用户不再受限于传统定价的不公平,能够以合理价格购买新鲜水果。<br> <br>* 用户获得的好处:更高的性价比和更优质的购物体验,增强了消费信心。<br> <br>### 案例:小米手机(2011年)<br>提案:小米手机以1999元的价格进入市场,打破了智能手机高价的惯例。<br>* 企业打破的限制:智能手机价格昂贵,限制了普及和市场份额。<br> <br>* 企业获得的好处:迅速扩大了市场份额,树立了性价比高的品牌形象。<br> <br>* 用户打破的限制:用户不再受限于高价格,能够以更低的价格购买高质量智能手机。<br> <br>* 用户获得的好处:更实惠的价格和更好的产品体验,提升了消费满意度。<br> <br>### 案例:苹果 iPod<br>提案:苹果iPod通过创新设计和内容管理,改变了音乐播放器市场。<br>* 企业打破的限制:传统音乐播放器的低容量和不便携性,以及盗版和携带不便的问题。<br> <br>* 企业获得的好处:获得了市场领导地位,推动了数字音乐行业的发展。<br> <br>* 用户打破的限制:用户不再受限于低容量和不便携的播放器,享受了更丰富的音乐体验。<br> <br>* 用户获得的好处:便携、高质量的音乐播放器,提升了音乐消费的便利性和乐趣。<br> <br>**案例:360免费杀毒**<br>提案:360率先使用了杀毒免费、通过播放广告盈利的运营模式。<br>* 企业打破的限制是:传统杀毒软件的收费模式让用户望而却步,同时也让竞争对手陷入价格战的困境。<br> <br>* 企业获得的好处是:通过免费模式迅速扩大用户基数,建立市场主导地位,并通过增值服务(如安全保护、浏览器等)实现盈利。<br> <br>* 用户打破的限制是:高昂的杀毒软件成本让用户难以负担或选择盗版产品。<br> <br>* 用户获得的好处是:免费且高效的杀毒服务降低了用户的使用门槛,提高了安全防护的普及率和使用体验。 |
#### 个人思考过程
| | $\color{#0089FF}{@沈攀}$ | $\color{#0089FF}{@赵蕾}$ | 东方 |
| --- | --- | --- | --- |
| 类比引入 | 电影《教父》威慑力<br>预判了你的预判、最优解、别的方案可能更差 | 给客户提供难以抗拒的、清晰的、远超竞争对手的好处。 | 一种无法拒绝的提案<br>像《教父》一样,一眼看透了本质,解决了各方最痛的问题、给所有人都带来了好处,很难不心动。<br>预判了你的预判(搬家片段),最优解就是接受这个提案。 |
| 概念解释 | what:<br>定义:打破客户重大限制,让其客户无法拒绝的理由(方案)。<br>延展:和痛点、需求的区别:<br>不只是单纯抓痛点、也比满足需求,要求要高<br>假设:用户想要,我们满足<br>黑手党提案更底层<br>通常是打破了行业惯例、非共识的共鸣<br>特征:没有直接解决这个问题,但让着问题,显得无关紧要,**c、b共赢**<br>why:需求是复合体、甚至是矛盾的、需求是不断延伸的<br>满足单一的痛点是不够的<br>how:用户端和供给端找打核心冲突、打破冲突、双赢 | 难以抗拒:相较原方案有巨大优势,大于用户的替换成本<br>清晰:可量化、可比较 | 你的客户有好处,所以更愿意付钱;<br>你的公司也有好处,可以摆脱长期以来的关键问题;<br>你的竞争对手无比头痛,看得懂也抄不了。<br>---<br>“黑手党提案”就是从客户的需求出发,深入了解他们遇到的痛点,然后设计一个解决方案,使得这个方案不仅能解决问题,还让竞争对手无法模仿,从而帮助双方都创造价值。<br>定义:(帮助XX打破限制、获得XX)<br>内涵:<br>外延:区别 |
| 秒懂案例 | 1、 苹果手机:打破手机要有键盘惯例<br>限制:c 屏幕小 b:适配不同手机<br>——小电脑<br>2、 胖东来按新鲜度卖鲜果<br>c:xxx<br>b:xxx<br>3:真实的案例:按库龄卖车<br>C:<br>B: | 例:小米手机(2011 年)<br>原先:智能手机 5000+<br>现在:小米手机 1999(同质半价)<br>例:苹果 ipod<br>原先:盗版低质 MP3 费时费力,或正版 CD 携带不便<br>现在:小小ipod能容3000+歌曲 | 像360免费杀毒一样<br>如果杀毒软件需要花钱买,就会重复“悬赏抓蛇、越抓越多”的寓言式的困局。<br>用户为免费杀毒而来,已经付费的杀毒看得懂也不敢学。 |
### 关键准备2:黑手党提案让哪两方双赢?
研讨输出[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k45cync9lhsvgwqip)
| 1、客户 | * 成长型中小企业<br> <br> * 行业特征:<br> <br> 1. 增长期到成熟期 <br> <br> 2. 集中度低、行业乱象、用户信任度低<br> <br> <br> * 企业特征:(主动进入企业,差距小研讨)<br> <br> * 创始人:有愿景、明确目标、相信系统提升、学习管理理论<br> <br> * 管理层:认同企业愿景、执行力强、学习能力强<br> <br> * 高成长型企业(5000W~10亿):<br> <br> * 已经验证 PMF<br> <br> <br> 1. 可量化CCR:生产职能明确、工序可量化<br> <br> 2. 赛道:高渗透空间、高增速、低集中度<br> <br> 1. 主动研究大赛道;(好赛道列表;学习日)<br> <br> 2. 细分赛道;<br> <br> 3. 组织:有胜任的主将(逻辑思维、底层认知、专业经验)+培养更多合格总经理 <br> (实战中学习; <br> **非脱产培训、有体系(学习地图)** <br> **【个人学习、小组汇报;经营提效瓶颈-实战任务-微课-工作模板+问题\单元格AI Agent】**)<br> <br> 4. 创始人:股权集中、认同可行性愿景<br> <br> <br> * 战略布局(有愿力):<br> <br> * S1有基础<br> <br> * S2有潜力<br> <br> * S3待破局<br> <br> * 组织成长:<br> <br> * 业务单元经营提效 <br> (算帐:抓手是业务而非公司、创始人投入)<br> <br> * 多重一致性(有认识即可) |
| --- | --- |
| 2、雇用产品 | 陪伴式咨询(团队):提供研讨、反馈、骨干培养等端到端服务。<br>* 产品1:咨询类产品(单次、年度):收费模式为基础费用+增长提成。<br> <br>* 产品2:培训教练类产品(单模块、多模块):按企业收费,每家企业参与人员不超过6人。 |
| 3、待办任务 | * 制定可行性愿景:明确3年目标、10年愿景及更长远的使命。<br> <br>* 提供系统性、一致性、有效性的陪伴服务。<br> <br>* 实现利润增长(短期经营成果改善)和愿景落地(中长期战略任务突破)。<br> <br>具体任务:<br>1. 提升每个业务单元的盈利能力;<br> <br>2. 持续投资未来业务;<br> <br>3. 提升业务系统效率,重塑多重一致性。 |
| 4、期望结果 | * 当下和未来的盈利持续增长(结果导向)。<br> <br>* 清晰当下和未来需打赢的关键“胜仗”(过程导向)。<br> <br>* 平衡当下赚钱与未来赚钱,实现系统思考、聚焦核心事务、有计划、有节奏地达成愿景。<br> <br>* 付费效果可量化、可感知:从定量角度看,今天的营业额能转化为未来某年的利润。<br> <br>* 为结果和成效付费,客户希望看到清晰的产出和价值。 |
| 谁的双赢? $\color{#0089FF}{@沈攀}$ | 领先2035 | 典型目标客户(DTB)<br>例:…… |
| --- | --- | --- |
| 1、客户 | 成长型中小企业 <br>行业特征:1、 增长期到成熟期<br>2:集中度不高、行业乱象、用户信任度底<br>企业:<br>创始人:有愿力、相对明确的方向目标、相信系统提升带来的好处,学习过其他的管理理论。<br>管理层:相信企业愿景、使命同频、有一定认知能力、执行力强、学习能力强。 | 2c企业<br>产品型:研发产品、外包(自产)、销售(国内外)、全渠道<br>平台、渠道型 |
| 2、雇用产品 | 陪伴式咨询(团队):研讨、反馈、骨干培养<br>端到端 | 陪伴式咨询:战略方向把关、重大营销方案研讨、关键决策外脑、有竞争力的盈利产品方案、骨干培养方案 |
| 3、待办任务 | 可行性愿景:3 年目标 10 年愿景  更长远:使命<br>系统性、一致性、有效性的陪伴服务 | 聚焦最重要的事情、战略重点、BP 计划、<br>业务反馈、骨干培训(达成愿景) |
| 4、期望结果 | 系统思考、聚焦最重要的事情、有计划、有节奏的持续小胜、达成愿景。<br>付费效果可量化可感知<br>为结果和成效付费<br>定量:xx 年后的利润=今天的营业额 | 完成 x 年 xx 亿目标 |
| 谁的双赢? $\color{#0089FF}{@赵蕾}$ | 领先2035 | 典型目标客户(DTB)<br>**例:Tob 客户类(数票云链)** |
| --- | --- | --- |
| 1、客户 | 高成长型企业(5000W~10亿)<br>> ① 可量化CCR:制约在 CCR(我方有相关经验则更佳)、 有生产职能、工序明确可量化<br>> ② 赛道:高渗透空间、高增速、低集中度<br>> ③ 组织:有胜任的主将(逻辑思维、底层认知、专业经验)<br>> ④ 创始人:股权集中,有一定认知,理性讲逻辑,认同企业经营需要系统论>还原论(有效产出世界观) | 大型银行 |
| 2、雇用产品 | 可行性愿景<br>> 产品1:咨询类产品(单次、年度)<br>* 收费:基础费用+增长提成<br> <br>> 产品2:培训教练类产品(单模块、多模块)<br>* 收费:以企业为单位收费,每家企业人员< 6人 | 数电发票的数据要素的场景价值应用<br>> 产品 1:粗加工 —— 数电发票脱敏数据<br>* 收费:按数据量付费<br> <br>> 产品 2:精加工 —— 新能源车产业链融资风控模型<br>* 收费:按模型应用时间付费(或买断)<br> <br>> 产品 3:增值服务 ——  新能源车产业链融资企业撮合<br>* 收费:按融资金额返点 |
| 3、待办任务 | 利润增长(短期经营成果能有改变)<br>愿景落地(中长期战略任务能有突破) | 更安全的资金应用<br>更低的销售成本<br>更高的收益回报<br>更多普惠金融的政绩(中小企业、产业的神经末梢) |
| 4、期望结果 | 既能当下赚钱,又能未来赚钱 | 更多客户、更高回报、更低风险 |
| 谁的双赢? $\color{#0089FF}{@李东方}$ | 领先2035 | 典型目标客户(DTB) |
| --- | --- | --- |
| 1、客户 | 战略有布局:S1有基础、S2有潜力、S3有思考待破局;<br>组织可成长:业务单元的经营提效;多重一致性; | 消费者(意味着我们聚焦于2C业务的企业客户) |
| 2、雇用产品 | “总经理+”:合格总经理在我们公司,马上就能大变样<br>(体验过什么是好的总经理、可以带来好的效果) | 新能源汽车:汽车交付和保障服务;<br>跨境电商:选品、需求场景挖掘<br>安克充电业务:把用户痛点快速变成高品质的产品 |
| 3、待办任务 | 1. 提升每个业务单元的盈利能力;<br> <br>2. 持续投资未来业务;<br> <br>3. 提升业务系统效率、重塑多重一致性; | 在LTV内提供高品质、低价格的产品和足够周到的服务 |
| 4、期望结果 | 当下和未来的盈利持续增长;(结果)<br>清晰当下和未来有哪些胜仗要打:(过程) | “多快好省” |
### 关键准备3:如何定义双赢
研讨输出[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k5fygudxxvu01k2qb)
| 定义双赢 | 领先2035 | 典型目标客户(DTB) |
| --- | --- | --- |
| 1、主要UDE | 必选:<br>![领先3025CRT分析.jpeg](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/v9kqDA7KpWV3OVxe/img/44150043-4bcd-46d7-823d-bbd3091849d6.jpeg) | 必选:<br>1. **客户主要UDE:**<br> <br>1、战略解码: BP 战略节奏不清晰(目标、年度关键任务不清晰)<br>2、业绩增长乏力<br>3、总体利润降低<br>4、营销费用高<br>5、缺乏持续可盈利的业务设计<br>6、组织对应战略的匹配度低<br>7、组织:架构不合理、协调运转不畅<br>组织调整慢<br>8、 人才:<br>8 主将缺位、<br>9 缺 OD 主管(≥ 能手)<br>10 缺财务主管(≥ 能手)<br>11 核心岗位胜任力能力不足<br>12 人才密度不够、<br>13 人才互补性不强、<br>14 老将经验习惯和新理念和能力的冲突<br>15 运营:缺乏高质量的及时反馈营业分析会<br>16 财务:缺科学管理会计体系(数据、报表)<br>17 现金流不充裕<br>(同时符合以下两个原则)选出**最重要的三个UDE**:<br>1. 跟买我们的产品、用我们的服务有关 <br> (我们和同行导致了这个UDE);<br> <br>2. 这个UDE对于客户是至关重要,直接、间接影响了70%以上的UDE; |
| 2、恶性循环 | 必选:(拿结果找原因)<br>**恶性循环1:**<br>* 无法确保客户财务指标显性提升<br> <br>* 产品价值陈述不清晰<br> <br>* 值得长期陪伴的客户少<br> <br>* 缺少实战的成功案例<br> <br>**恶性循环2:** <br>* 缺少实战的成功案例<br> <br>* 对交付的结果和节奏缺乏明确承诺<br> <br>* 产品交付变量大<br> <br>* 对顾问要求门槛高<br> <br>* 顾问无法规模化 | 可选: |
[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6n7uobszhkb4kt6bll)
[《UDE分析过程》](https://alidocs.dingtalk.com/i/nodes/G1DKw2zgV2glyaqMf103PABqJB5r9YAn?utm_scene=team_space)
:::
清晰表达的 UDE 的一些特征包括:
1.【持续存在】 这是对现实中存在的持续存在的问题的抱怨,并且由于这个问题,你无法表现得更好。
2. 【当下正在发生】应该是一个完整的现在时的句子。
3.【描述状态】它是对系统状态的描述,而不是动作。
4. 【责任范围内】属于您的责任或影响范围。
5. 【可以解决】可以采取一些措施来解决这个问题。
6. 【不归因于人】不能责怪某人。
7. 【不分析原因】不能是推测的原因。
8.【不是解决方案】绝不能是隐藏的解决问题的办法。
9. 【单独的实体】它必须仅包含一个实体。
10. 【】不应在言语中包含其原因。
1【不是主观猜想】应该是事实而不是主观的。
:::
## 黑手党提案
### 发现和打破惯例阶段:方向性提案
#### 1、定义核心冲突
1、战略解码: BP 战略节奏不清晰(目标、年度关键任务不清晰)
6、组织对应战略的匹配度低
16 财务:缺科学管理会计体系(数据、报表)
补充研讨:
1. 客户目标(系统目标)
2. UDE - > DE
3. DE的必备条件(D:只有做了D,UDE才会转化为DE)
1. 从D中找到矛盾的行动;
2. 转化D对应的DE为B;
3. 推测B的共同目标(服从于系统目标的小目标,而不能一上来就是O)
| 典型客户的客户(DTB) | 目标 | 需求 | 行动 | 检查 |
| --- | --- | --- | --- | --- |
| 1、主要UDE1<br>[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6nlcntn3vbbeylrt9n)<br>1、目标、年度关键任务不清晰 | O<br>达成业绩目标 | B1<br>~~(责权利更清晰)~~<br>降低管理复杂度 | D1<br>各自为战 | 当我们感受到UDE时,我们通常的做法是D1;<br>对于典型目标客户(DTB)<br>如果想要B1,但总是UDE1;<br>如果D1,则不能D2;<br>如果D2,则不能D1;<br>只有D1,才能B1;<br>如果D1,则不能B2<br>只有D2,才能B2;<br>如果D2,则不能B1;<br>![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/v9kqDA7KpWV3OVxe/img/f51f929c-4a45-4879-bc77-c37e7664bf97.png)<br>![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/v9kqDA7KpWV3OVxe/img/9f292969-843c-4d03-9998-afdda5e23d85.png) |
| | | B2<br>聚焦全局关键事项 | D2<br>协同作战 |
| 2、主要UDE2[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k628ztgayppww4sm)<br>6、组织对应战略的匹配度低 | O<br>资源得到最大化的利用 | B1<br>责权利更清晰<br>解决表面问题(止血) | D1<br>优先解决局部的、具体问题;(次要矛盾) |
| 推动企业实现战略目标 | | B2<br>将资源聚焦到能够改善业绩的关键环节<br>解决根本问题(根治) | D2<br>基于系统思考解决整体关键问题;(主要矛盾) |
| 3、主要UDE3<br>[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6nooyydbije19pyz6)<br>财务:~~缺整体科学管理会计体系的指导(数据、报表)~~<br>缺乏经营仪表盘的指导 | O<br>提升当期盈利指标 | B1<br>符合会计准则强制要求 | D1<br>完全成本法(分摊所有生产成本到产品):<br>以销售单元核算利润率 |
| | | B2<br>挖尽系统瓶颈效能 | D2<br>有效产出成本拆分 |
| 在“待办任务”的框架下,合并B1、B2、D1、D2,最恰当的描述是: | | | |
| 合并UDE[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k628ztoox6heog5u) | O合并<br>1. 达成业绩目标<br> <br>2. 资源得到最大化的利用<br> <br>3. 提升当期盈利指标 | B1<br>降低管理复杂度<br>责权利更清晰<br>解决表面问题(止血)<br>符合会计准则强制要求 | D1(当前采取的行动)<br>各自为战<br>优先解决局部的、具体问题;(次要矛盾)<br>完全成本法(分摊所有生产成本到产品):<br>以销售单元核算利润率 |
| | | B2<br>提升CCR使用率<br>聚焦全局关键事项<br>将资源聚焦到能够改善业绩的关键环节<br>解决根本问题(根治)<br>挖尽系统瓶颈效能 | D2(希望采取的行动)<br>协同作战<br>基于系统思考解决整体关键问题;(主要矛盾)有效产出成本拆分 |
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/Lk3lbV1bGRrAlm96/img/e2da959f-926c-4ee9-88c1-e9d7c80ec591.png)
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/Lk3lbV1bGRrAlm96/img/dbbd95d2-f428-43f2-afc3-541e85667a2a.png)
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/1wvqr7M800LeOako/img/f6bcfe94-3d6e-475c-a521-e3e993053869.png)
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/Lk3lbV1bGRrAlm96/img/fc686db2-13dc-4c91-8808-17cb1b24abc2.png)
#### 2、揭示冲突背后的惯性(是什么限制了用户)
| O合并<br>1. 达成业绩目标<br> <br>2. 资源得到最大化的利用<br> <br>3. 提升当期盈利指标 | B1<br>降低管理复杂度<br>责权利更清晰<br>解决表面问题(止血)<br>符合会计准则强制要求 | D1(当前采取的行动)<br>各自为战<br>优先解决局部的、具体问题;(次要矛盾)<br>完全成本法(分摊所有生产成本到产品):<br>以销售单元核算利润率 |
| --- | --- | --- |
| | B2<br>提升CCR使用率<br>聚焦全局关键事项<br>将资源聚焦到能够改善业绩的关键环节<br>解决根本问题(根治)<br>挖尽系统瓶颈效能 | D2(希望采取的行动)<br>协同作战<br>基于系统思考解决整体关键问题;(主要矛盾)有效产出成本拆分 |
[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k78au6rroqi8pic2l)
| 关键问题 | 假设 | 行业惯例(直接证据\通常做法) | 总结 |
| --- | --- | --- | --- |
| 为什么用户想要B1,就一定得D1?<br>为什么用户想要<br>B1降低管理复杂度<br>就必须<br>D1<br>使用完全成本法(分摊所有生产成本到产品)<br>【工具:因果假设分析】<br>检查<br>因为:D1<br>同时:假设<br>那么:B1 | 企业内部考核方便 | 咨询公司在每个项目中都会划分明确的边界;<br>咨询项目需要分拆问题、逐一解决; | 如果总是遵循惯例1:<br>**【明确边界(交付和考核);】**<br>则用户只会D1,而不会D2。 |
| | | * 咨询公司希望明确交付边界<br> <br>---<br>咨询公司需要与客户明确责权利划分 | |
| | 提升效率,运转通畅 | 咨询公司希望明确边界、考核清晰 | |
| | ~~每个产品管好,整个公司就可以管好~~ | | 如果总是遵循惯例2:<br>**【拆分问题、归口到专家执行;】**<br>则用户只会D1,而不会D2。 |
| | 避免分歧和扯皮(守土有责) | 咨询公司希望明确边界、考核清晰 |
| | | * 咨询项目需要分拆问题逐一解决<br> <br>---<br>咨询案需要专注于一个问题(切模块解决问题;还原论,分拆问题、逐一解决,整体就可以解决) | |
| | 专家解决对口问题<br>---<br>降低咨询案的复杂度 | 用户更信任专家身份(标签) | |
| | 咨询项目标准化<br>(能模块化复制) | 就问题找对应专家 | |
| | 在咨询公司内部考核方便 | 咨询项目会划分成若干个模块(里程碑) | |
| | 外部合规要求 | 分模块交付 | |
| AI输出选项(避免想不到)<br>创始人直觉表达如何提炼<br>参与研讨的人数(避免1V1) | | | |
补充验证:
| 领先2035视角验证:[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k6uemvpzp2a0rgc5f) | 典型客户的客户(DTB)视角验证:[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k6uemvu6yvr1hr09) |
| --- | --- |
| 1、惯例是不是行业内保护自身利益的“普遍做法”<br>* 明确边界(交付和考核)<br> <br>* 拆分问题、归口到专家执行; | 1、补全CCRT |
| 2、以上管理是导致企业当下陷入恶性循环的直接原因:<br>* 企业CRT中当下的恶性循环是:<br> <br>* 惯例将会加剧恶性循环体现为:<br> <br> * 原因:企业继续遵守惯例XX<br> <br> * 假设:同时,XX<br> <br> * 结果:恶性循环中XX将加加剧。、<br> <br>循环1:<br>* 客户少<br> <br>* 实战少<br> <br>* 持续营收低<br> <br>* 顾问无法规模化<br> <br>循环2:<br>* 无法承诺财务指标(惯例导致)<br> <br>* 潜在客户少【病人】<br> <br>* 实战少<br> <br>* 方法价值感低<br> <br>循环3:<br>* 主动诊断有潜力的客户<br> <br>* 缺少承诺财务指标的服务方案(惯例导致)<br> <br>* 服务边界不清楚<br> <br>* 缺少成功案例 | 2、确认惯例直接导致主要UDE(至少一个);<br>3、确认惯例导致(B/C)加剧了恶性循环。<br>![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/1wvqr7M800LeOako/img/ee9c95f9-cb64-4f78-ac5c-b5a8a7a92286.png) |
#### 3、用例外激发方案
1. 达成业绩目标
2. 资源得到最大化的利用
3. 提升当期盈利指标
| **改变的策略** | **DTB目标** | **需求** | **行动** | **激发方案** |
| --- | --- | --- | --- | --- |
| 惯例1提案[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k7if2l7ocml2u5lgf)<br>* 明确边界(交付和考核) | | | | |
| | | | | 之所以D2就无法B1,是因为(假设:)<br>【考核更方便、更明确】 |
| 现有惯例的做法是【】,对应D1 | O<br>达成**长期**业绩目标 | B1<br>降低协作复杂度<br>权责利明确 | D1<br>明确边界,快速见效<br>(解决显性问题) | 例外是:(具体例子,而不是逻辑推演)<br>全局经营仪表盘 |
| 打破惯例的做法是【】,对应D2 | | B2<br>找到本质解 | D2<br>系统思考,长期主义<br>(解决瓶颈问题) | 可以尝试的是:<br>建立经营仪表盘 |
| 惯例2提案[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k7ixeddb6oa3id2ji)<br>* 拆分问题、归口到专家执行; | | | | |
| | | | | 之所以D2就无法B1,是因为(假设:)<br>【考核的条款都是分项考核】 |
| 现有惯例的做法是【】,对应D1 | O<br>提升当期盈利指标 | B1<br>明确考核项目进度与质量 | D1<br>拆分问题、归口到专家执行 | 例外是:<br> 业财一体的经营管理<br>(战略落地:业务设计与财务指标改善) |
| 打破惯例的做法是【】,对应D2 | | B2<br>有效利用资源 | D2<br>基于经营仪表盘做全局陪伴 | 可以尝试的是:<br>基于有效产出会计做经营管理 |
| 提案总结:[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k7knidhwxlvxmrhds)<br>**通过D、满足B:通过系统思考,长期主义,满足找到本质解;**<br>**同时通过D’、满足B‘:通过基于经营仪表盘做全局陪伴,满足有效利用资源;**<br>**使DTB可以O1,且可以O2:使BTD既可以达成长期业绩目标,又能够****提升当期盈利指标** | | | | |
### 激发和完善方案阶段:操作性提案
#### 4、消除负面分支
实施新方案之后,领先2035新增的UDEs
1. 系统思考,长期主义
2. 基于经营仪表盘做全局陪伴
[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k7yr56ydkym1z2d3)
* 协同复杂度增加(扯皮和纠纷变多)
* 考核边界不清晰(难以量化)
* 管理复杂度增加
* 局部效率降低
* 结果不确定性高
* 不适应新的思考和行为模式(颠覆原有)
* 加重了认知负担
* 提升了决策难度
* 决策慢
* 错失"机会"多
* 官僚作风
* 办公室政治
* 对人才的要求高
1、直接危害现有(任一)B的UDE
1. 直接危害B;
2. 客户感受强;
| B1<br>降低协作复杂度<br>权责利明确 | 协同复杂度增加 |
| --- | --- |
| B2<br>找到本质解 | 提升了决策难度 |
| B1<br>明确考核项目进度与质量 | 考核边界不清晰 |
| B2<br>有效利用资源 | 结果不确定性高 |
2、新方案导致新增UDE的过程,找到DE转变为UDE的节点
[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k8b3rkxai9ud6y4e)
| | 因果假设链 | | 性质 |
| --- | --- | --- | --- |
| 结果 | 最终结果<br>协同复杂度增加 | | UDE |
| 过程 | 中间结果3<br>缺乏清晰的协同机制和考核办法 | 假设 | UDE |
| | 中间结果2<br>现有岗位协同不顺畅的问题暴露 | 假设<br>现有岗位有明确的职责边界和协同机制 | UDE |
| | | | 打破假设的方案:<br>聚焦瓶颈问题<br>(非瓶颈不改善) |
| | 中间结果1<br>系统可视化 | 假设<br>问题可以找到根因 | DE |
| 方案 | D<br>系统思考,长期主义 | 假设<br>管理过程可控(责权分明) | |
| | 因果假设链 | | 性质 |
| --- | --- | --- | --- |
| 结果 | 最终结果<br>提升了决策难度 | | UDE |
| 过程 | 中间结果3<br>缺乏发现问题的流程和新的解决问题的机制 | 假设 | UDE |
| | | | 打破假设:<br>**旗手培养** |
| | 中间结果2<br>对现有问题更清晰了、对新协同机制和渴望 | 假设<br>组织内缺乏能力(缺参谋部) | DE |
| | 中间结果1<br>管理团队有了全局感 | 假设<br>有一套共识的思维方法(方法) | DE |
| 方案 | D<br>基于经营仪表盘做全局陪伴 | 假设<br>有专业教练贴身服务(教练) | |
| | 因果假设链<br>客户经验越充分,研讨效率越高(清楚过程) | | 性质 |
| --- | --- | --- | --- |
| 结果 | 最终结果<br>考核边界不清晰 | | UDE |
| 过程 | 中间结果3<br>新方案中分工不明确(甚至与现有制度冲突) | 假设 | UDE |
| | 中间结果2<br>方案不明确 | 假设 | UDE |
| | | | 打破假设:<br>**共创立竿见影的行动方案(杠杆解,赢得解决本质问题的时间)** |
| | 中间结果1<br>更能够清楚指出瓶颈问题 | 假设<br>**惯性思维**<br>团队缺乏经验和抽象思考、迁移能力 | DE |
| 方案 | D<br>基于经营仪表盘做全局陪伴 | 假设<br>有经验的专家贴身指导(懂数据、强逻辑、行业KnowHow) | |
| [请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6pz37mpb86nubk12lo) | 因果假设链 | | 性质 |
| --- | --- | --- | --- |
| 结果 | 最终结果<br>结果不确定性高 | | UDE |
| 过程 | 中间结果3<br>难以科学规划推进节奏<br>(以安排待办任务的轻重缓急) | 假设<br>资源有限 | UDE |
| | | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/v9kqDA7KpWV3OVxe/img/39954f85-5941-4214-81ac-9b478a8666bc.png) | 打破假设:<br>(被打破)全局利益和短期利益有冲突;<br>**创建 一个可盈利的业务机会的缓冲池且缓冲池持续增长** |
| | 中间结果2<br>希望做全局性的改善(推倒重来) | 假设<br>想要保住短期既得利益 | DE |
| | 中间结果1<br>对愿景的信心更高涨 | 假设<br>人才不够(团队能力不强) | DE |
| 方案 | D<br>系统思考,长期主义 | 假设<br>有理论、有方法、有教练陪伴、有成功范例 | |
3、在节点处补充方案,使UDE转为DE
[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k8e07fa4r5adpkq1)
| 如果: | D |
| --- | --- |
| 同时: | 假设 |
| 那么: | UDE变成DE; |
| 并且那么: | 现有DE没有转变为UDE; |
| 如果: | D系统思考,长期主义 |
| --- | --- |
| 同时: | 假设管理过程可控(责权分明) |
| 那么: | UDE变成DE;<br>现有岗位协同不顺畅的问题暴露<br>变成:更多人期待全局改善带来的效果 |
| 并且那么: | 现有DE没有转变为UDE; |
| 如果: | D基于经营仪表盘做全局陪伴 |
| --- | --- |
| 同时: | 假设有专业教练贴身服务(教练) |
| 那么: | UDE变成DE;<br>缺乏发现问题的流程和新的解决问题的机制<br>变成:<br>共识问题、聚焦问题、将资源向其倾斜 |
| 并且那么: | 现有DE没有转变为UDE; |
| 如果: | D基于经营仪表盘做全局陪伴 |
| --- | --- |
| 同时: | 假设有经验的专家贴身指导(懂数据、强逻辑、行业KnowHow) |
| 那么: | UDE变成DE;<br>方案不明确<br>变成:<br>问题清晰、目标明确、方案共识、协调通畅、效果可以预见 |
| 并且那么: | 现有DE没有转变为UDE; |
| 如果: | D系统思考,长期主义 |
| --- | --- |
| 同时: | 假设有理论、有方法、有教练陪伴、有成功范例 |
| 那么: | UDE变成DE;<br>难以科学规划推进节奏<br>变成:<br>计划推进井然有序 |
| 并且那么: | 现有DE没有转变为UDE; |
不是逼不得已,不会主动求学TOC(垂死挣扎)
#### 5、确立新规则、匹配新能力
##### 坚定维护么新规则、树立新惯例[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k9hui97aernangcko)
![image](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/a/RbLmOLM5LIYPk4nO/316c3fcf804941cdac8ddf999adfe0552238.png)
##### 匹配新能力
~~1、战略解码: BP 战略节奏不清晰(目标、年度关键任务不清晰)~~
2、业绩增长乏力
~~3、总体利润降低~~
~~4、营销费用高~~
~~5、缺乏持续可盈利的业务设计~~
~~6、组织对应战略的匹配度低~~
~~7、组织:架构不合理、协调运转不畅~~
组织调整慢
8、 人才:
8 主将缺位、
^9 缺 OD 主管(≥ 能手)^
^10 缺财务主管(≥ 能手)^
~~11 核心岗位胜任力能力不足~~
~~12 人才密度不够、~~
~~13 人才互补性不强、~~
~~14 老将经验习惯和新理念和能力的冲突~~
~~15 运营:缺乏高质量的及时反馈营业分析会~~
~~16 财务:缺科学管理会计体系(数据、报表)~~
17 现金流不充裕
| 分类 | 期望改变的结果 | 运营能力(我们的产品为DTB增加的能力) |
| --- | --- | --- |
| UDE不会转化为DE,除非……<br>[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k9z4cs46e2p3adofz) | 原UDE1 组织调整慢 | (等自然解决) |
| | 原UDE2 主将缺位<br>DE | (筛选标准:**缺关键新能力,而不是人不在**)<br>Build:内部培养<br>Borrow:顾问接管<br>Buy:外部招聘(猎头) |
| | 原UDE3 现金流不充裕 | **重算资产与负债**(基于有效产出会计、出售资产或抵押) |
| 新方案将会导致新的UDE,除非……<br>[请至钉钉文档查看「计时器」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6k9z879dknp15z90yp) | 新UDE1 现有岗位协同不顺畅的问题暴露 | **聚焦瓶颈问题** |
| | 新UDE2 缺乏发现问题的流程和新的解决问题的机制 | **业务操盘手 (骨干培养、旗手、业务总经理)** |
| | 新UDE3 方案不明确 | **共创立竿见影的行动方案(杠杆解,赢得解决本质问题的时间)** |
| | 新UDE4 难以科学规划推进节奏 | **创建一个可盈利的业务机会的缓冲池且缓冲池持续增长** |
# 第二部分:产品说明书(陪伴产品) 7号晚搞完+报价
![image](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/a/RbLmOLM5LIYPk4nO/d53f8200854047fbb9698186c3d500992238.png)
![image](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/a/RbLmOLM5LIYPk4nO/57eae772e7fc42edb46f4439dddb834c2238.png)
3+
| 产品单元 | 运营能力(改变UDE为DE的新增能力) | 运营能力(维持DE的必要能力) |
| --- | --- | --- |
| | | |
| | | |
| | | |
| | | |
# 第三部分:有效产出会计
## 研讨准备:产品交付的资源与工序
### 1、充分展开产品交付流程
| 产品单元 | 工序与资源投入-顺序逻辑 | 每单位交付物消耗 |
| --- | --- | --- |
| | ![image](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/a/RbLmOLM5LIYPk4nO/ef5dc5e0d4a6417aaee008eb38f8b1c22238.png) | 工序:时间(找瓶颈) \| 资源(评估TVC)<br>工序A:<br>工序B:<br>…… |
| | | |
| | | |
| | | |
### 2、量化产品交付的效率与成本
[请至钉钉文档查看「电子表格」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6kdx8zriyiz2dxvtj)
## 一、发现和挖掘瓶颈
### 1)是什么瓶颈制约了领先2035团队挖尽市场需求?
[请至钉钉文档查看「电子表格」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6ke5kocyqhppw0ri)
### 2)产品赚钱的速度有多大差异?(领先2035应当优先保证哪些产品的交付?)
[请至钉钉文档查看「电子表格」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6keavwd1b2ncfie6sm)
### 3)利润最大化的生产计划是什么?(每个产品分别交付多少份)
[请至钉钉文档查看「电子表格」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6kefvcdog7oagpvcts)
## 二、充分开发瓶颈
### 提案1:通过AI介入,提升瓶颈环节的效率
研讨焦点:
1. 提升达到多少时,会导致瓶颈漂移?(质变点)
2. 新的瓶颈因素下,如何安排各产品的交付优先级和交付数量?【步骤2、3】
参见:[《在线工作坊 - 研讨文档》](https://alidocs.dingtalk.com/i/nodes/MyQA2dXW7o49PyjYt2PrNo7QWzlwrZgb?utm_scene=team_space&iframeQuery=anchorId%3Duu_m6ke7u52qpmekk37eks)
[请至钉钉文档查看「电子表格」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6keykelxzo422jdptr)
## 三、打破瓶颈
### 提案1:增加时间资源投入(合作教练参与交付)
研讨焦点:
1. 合作教练投入多少时间可以使瓶颈漂移?(质变点)
2. 在优先派单合作教练交付的情况下,如何安排各产品的交付优先级和交付数量?
1. 假设1:合作教练不会增加总需求量;
2. 假设2:合作教练只增加可变成本,不改变其它工序上的时间投入。
[请至钉钉文档查看「电子表格」](https://alidocs.dingtalk.com/i/nodes/o14dA3GK8g2qz1pnsQanQ1rQJ9ekBD76?iframeQuery=anchorId%3DX02m6kfij97b5492mqsw8g)
+25
View File
@@ -0,0 +1,25 @@
第二阶段 · AI 的角色(AI 到底改变了什么)
4. AI 价值 — AI 创造了什么新价值、带来了什么新问题?如果没有 AI 这个问题是否成立?你的方法、流程、工具是什么?
AI 创造的新价值:
1) AI 文案整理,之前收集到的问卷、归纳总结我自己人工来干,每个月耗时一天,全程online,现在30分钟,可以离线并行。
2)2024、5,AI Copilot && AI 工具提效,引入行程设计、标题、商品详情优化、知识库答疑,需要和当前系统进行交互;
3)2026,AI Agent && 商家使用习惯库,Agent 自主规划调用工具,现有系统改造升级收集行为习惯,带来新的产品交互形式(贴合AI、无侵入当前系统)
我的方法、流程、工具:
1)问卷清洗 skills, 在skills中调用MCP工具,从NPS收集系统中抓取问卷数据,产出可被产品参考的需求文档。
2)AI Copilot,分治式抽象沉淀单个工具(标题优化、商详优化、图片优化...),借助范式(COT、ReAct、CALM)设计 Copilot 原生。
3) AI Agent,类Claude 式,自主调度工具设计,多工具循环调用。
5. AI 边界 — 依赖什么数据和知识?需要接入哪些系统、什么权限必须隔离?AI 在哪里需要人工复核或兜底?
1)答疑工具,依赖客服和行业运营数据,发品工具,依赖当前系统的用户数据和平台规则数据
2)接入系统,首页、商品发布、订单管理。权限隔离包括用户级私有数据权限隔离。AI 在用户提问、问题意图澄清时需要人工复核
第三阶段 · 验证证据(光说"能跑"不够,要业务证据)
6. 案例呈现 — 一个真实的落地案例,可以录一段 Demo 操作视频辅助说明
7. 价值验证 — 时效(关键周期缩短了多少)、人效(单位产出/质量/返工变化)、成效(收入/成本/客户/风险的变化,且证明是 AI 造成的)
第四阶段 · 复盘与推广(从"一个案例"到"可复制的能力")
8. 经验总结 — 踩过什么坑、做对了什么、还需迭代什么?下一轮最值得验证的方向是什么?
9. 组织落地 — 谁是业务 Owner?复制需要哪些前提(流程/数据/角色/系统)?以什么阈值决定 Go 或 No-Go?
+1611
View File
File diff suppressed because it is too large Load Diff
-1010
View File
File diff suppressed because it is too large Load Diff
+1698
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
{
"name": "ai-assistant-travel",
"private": true,
"version": "2.0.0",
"description": "旅行商品发布 AI 助手 Demo —— B端商家商品发布平台的 Copilot / Agent 交互原型(Vite + React)",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^5.4.11"
}
}
+22
View File
@@ -0,0 +1,22 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { fileURLToPath } from 'node:url'
/**
* 工程结构说明:
* - 仓库根目录的 index.html 是 v1 单文件演示快照,原样保留(不入 Vite 构建)。
* - Vite + React 应用的根目录为 app/,入口为 app/index.html,
* 这样两者互不干扰,可对照演示「单文件原型 → 前端工程化」的演进过程。
*/
export default defineConfig({
root: fileURLToPath(new URL('./app', import.meta.url)),
plugins: [react()],
build: {
outDir: '../dist',
emptyOutDir: true
},
server: {
port: 5173,
open: false
}
})