Compare commits
5
Commits
f61c255b9d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a71980e4bf | ||
|
|
a84d386929 | ||
|
|
01dcd83987 | ||
|
|
609e0afb29 | ||
|
|
1ef034cd59 |
+15
-17
@@ -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
|
||||
|
||||
@@ -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 数据采集的通信协议指明“直连”规范。
|
||||
@@ -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
|
||||
|
||||
@@ -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 文件
|
||||
@@ -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 +0,0 @@
|
||||
# 黑手党提案专家长期记忆
|
||||
@@ -1,6 +0,0 @@
|
||||
# Profile
|
||||
|
||||
- 语言:中文
|
||||
- 风格:像资深咨询合伙人一样思考,像教练一样提问,像分析师一样输出
|
||||
- 交互模式:每步先出方案,用户校准后推进
|
||||
- 输出格式:Markdown 表格 + 结构化文档
|
||||
@@ -1,10 +0,0 @@
|
||||
# Soul
|
||||
|
||||
你是「黑手党提案专家」,一个基于 TOC(约束理论)五步思考过程的 AI 战略顾问。
|
||||
|
||||
你的使命是帮助企业设计"让客户无法拒绝的价值主张"。你不做营销策划,不做广告创意。你做的是:通过系统性思考,找到行业惯例背后的隐含假设,打破它,构建双赢方案。
|
||||
|
||||
## 核心价值观
|
||||
- 严谨:UDE 必须通过 10 条标准,冲突图必须互斥
|
||||
- 务实:例外必须是具体案例,不是逻辑推演
|
||||
- 双赢:方案必须同时满足企业和客户的需求
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ""
|
||||
@@ -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 ""
|
||||
@@ -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": []}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 - 揭示冲突背后的惯例**。
|
||||
@@ -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),然后在左上角切换到「黑手党提案专家」开始使用。
|
||||
@@ -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 - 确立新规则、匹配新能力**。
|
||||
@@ -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/ 目录作为案例存档。
|
||||
> 如果在后续使用中发现方法论有需要改进的地方,我会在下次心跳时自动整理迭代建议。"
|
||||
@@ -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 的根因,不是"跟我们的产品最相关的"。产品/方案如何注入到核心问题上,是冲突图和惯例打破要解决的事。
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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% 定价策略',
|
||||
'首月立减冲榜促销习惯'
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, '<')
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
@@ -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
@@ -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
|
||||
@@ -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),原文含义模糊时降低。
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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 "需调参"),
|
||||
}
|
||||
@@ -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` 卡片确认 |
|
||||
@@ -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 测试。*
|
||||
@@ -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> | 必选:<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><br> |
|
||||
| | | 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>基于系统思考解决整体关键问题;(主要矛盾)有效产出成本拆分 |
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### 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> |
|
||||
|
||||
#### 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 |
|
||||
| | |  | 打破假设:<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)
|
||||
|
||||

|
||||
|
||||
##### 匹配新能力
|
||||
|
||||
~~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号晚搞完+报价
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
3+
|
||||
|
||||
| 产品单元 | 运营能力(改变UDE为DE的新增能力) | 运营能力(维持DE的必要能力) |
|
||||
| --- | --- | --- |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
|
||||
# 第三部分:有效产出会计
|
||||
|
||||
## 研讨准备:产品交付的资源与工序
|
||||
|
||||
### 1、充分展开产品交付流程
|
||||
|
||||
| 产品单元 | 工序与资源投入-顺序逻辑 | 每单位交付物消耗 |
|
||||
| --- | --- | --- |
|
||||
| |  | 工序:时间(找瓶颈) \| 资源(评估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
@@ -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
File diff suppressed because it is too large
Load Diff
-1010
File diff suppressed because it is too large
Load Diff
Generated
+1698
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user