diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c7f6389 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,111 @@ +# Agent Instructions for 九艺AI坐席辅助系统 + +## 部署规则(核心) + +### ⚠️ 任何代码修改后必须同步服务器部署 + +**无论前端、后台还是后端,任何代码修改完成后,都必须立即同步到服务器并重新部署,绝对不能只做本地修改就结束。** + +--- + +## 一、前端 Sidebar 部署 + +**模块路径**: `frontend/sidebar/` +**服务器部署路径**: `/www/wwwroot/deploy-package/frontend/sidebar/` +**容器名称**: `ai-assistant-sidebar` +**访问地址**: `https://ai.9artedu.com/sidebar/` + +### 部署流程 +```bash +# 1. 本地构建 +cd frontend/sidebar && npm run build + +# 2. 同步到服务器 +scp -r -o StrictHostKeyChecking=no -i root.pem frontend/sidebar/dist/* root@8.133.162.25:/www/wwwroot/deploy-package/frontend/sidebar/dist/ + +# 3. SSH 登录服务器,重新构建镜像并重启容器 +ssh -o StrictHostKeyChecking=no -i root.pem root@8.133.162.25 << 'EOF' +cd /www/wwwroot/deploy-package/frontend/sidebar +docker build -t deploy-package-sidebar . +docker stop ai-assistant-sidebar +docker rm ai-assistant-sidebar +cd /www/wwwroot/deploy-package +docker-compose up -d sidebar +EOF +``` + +--- + +## 二、后台 Admin 部署 + +**模块路径**: `frontend/admin/` +**服务器部署路径**: `/www/wwwroot/deploy-package/frontend/admin/` +**容器名称**: `ai-assistant-admin` +**访问地址**: `https://ai.9artedu.com/admin/` + +### 部署流程 +```bash +# 1. 本地构建 +cd frontend/admin && npm run build + +# 2. 同步到服务器 +scp -r -o StrictHostKeyChecking=no -i root.pem frontend/admin/dist/* root@8.133.162.25:/www/wwwroot/deploy-package/frontend/admin/dist/ + +# 3. SSH 登录服务器,重新构建镜像并重启容器 +ssh -o StrictHostKeyChecking=no -i root.pem root@8.133.162.25 << 'EOF' +cd /www/wwwroot/deploy-package/frontend/admin +docker build -t deploy-package-admin . +docker stop ai-assistant-admin +docker rm ai-assistant-admin +cd /www/wwwroot/deploy-package +docker-compose up -d admin +EOF +``` + +--- + +## 三、后端微服务部署 + +**模块路径**: `backend/*-service/` +**服务器部署路径**: `/www/wwwroot/deploy-package/backend/*-service/` +**技术栈**: Spring Boot + Docker Compose + +### 微服务列表 +| 服务名 | 容器名 | 端口 | 部署路径 | +|--------|--------|------|----------| +| gateway | ai-assistant-gateway | 8080 | `backend/gateway/` | +| auth-service | ai-assistant-auth | 8081 | `backend/auth-service/` | +| archive-service | ai-assistant-archive | 8082 | `backend/archive-service/` | +| conversation-service | ai-assistant-conversation | 8083 | `backend/conversation-service/` | +| intent-service | ai-assistant-intent | 8084 | `backend/intent-service/` | +| recommend-service | ai-assistant-recommend | 8085 | `backend/recommendation-service/` | +| generation-service | ai-assistant-generation | 8086 | `backend/generation-service/` | +| kb-admin-service | ai-assistant-kbadmin | 8087 | `backend/kb-admin-service/` | +| analytics-service | ai-assistant-analytics | 8088 | `backend/analytics-service/` | + +### 部署流程(以某个服务为例) +```bash +# 1. 本地构建 JAR 包(通常在对应服务目录下) +cd backend/auth-service && mvn clean package -DskipTests + +# 2. 将 JAR 包同步到服务器的 deploy-package 对应目录 +scp -o StrictHostKeyChecking=no -i root.pem target/*.jar root@8.133.162.25:/www/wwwroot/deploy-package/backend/auth-service/ + +# 3. SSH 登录服务器,重启对应容器 +ssh -o StrictHostKeyChecking=no -i root.pem root@8.133.162.25 << 'EOF' +cd /www/wwwroot/deploy-package +docker-compose up -d --build auth-service +EOF +``` + +--- + +## 服务器信息 + +- **IP**: `8.133.162.25` +- **SSH 私钥**: `root.pem`(项目根目录) +- **部署根目录**: `/www/wwwroot/deploy-package/` +- **Docker Compose 文件**: `/www/wwwroot/deploy-package/docker-compose.yml` +- **前端站点**: `https://ai.9artedu.com/sidebar/` +- **后台站点**: `https://ai.9artedu.com/admin/` +- **代理**: 宝塔 Nginx → Docker Nginx (8089) → 各容器 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c3ca356 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,120 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +九艺AI坐席辅助系统 — A real-time script recommendation system for course consultants/sales reps at 9artedu.com. It captures WeCom (企业微信) conversations, analyzes intent, builds customer profiles, and recommends sales scripts to agents via a sidebar H5 interface. + +## Architecture + +The system uses a microservices architecture with 10 backend services, all sharing a `common` module. An API Gateway routes requests by path prefix to individual services. + +``` +nginx (port 80) + → gateway (port 8080) — Spring Cloud Gateway, routes by /api/v1/* + → auth-service (8081) — WeCom OAuth, JWT, JS-SDK signing + → archive-service (8082) — WeCom archive C SDK via JNA, RSA decrypt, callbacks + → conversation-service (8083) — Session management, context windows (Redis List) + → intent-service (8084) — Intent recognition (40 scenarios), customer profiles + → recommendation-svc (8085) — 3-layer recall, multi-factor ranking, MMR reranking + → generation-service (8086) — Tongyi Qianwen (DashScope) API, prompt engine + → kb-admin-service (8087) — Script CRUD, category management, import/export + → analytics-service (8088) — Statistics, script effectiveness analysis + → admin frontend (port 5174) — React + Ant Design (script library management, dashboards) + → sidebar frontend (port 5173) — React + Ant Design Mobile (agent-facing H5 sidebar) +``` + +### Key Design Patterns + +- **Unified API response**: All services use `Result` from `common` module (`code: 0` = success, `code: -1` = failure, with `message` and `timestamp`) +- **Pagination**: `PageResult` for list responses +- **Database**: MyBatis-Plus with logical delete (`deleted` field: 0/1), auto-fill handler for timestamps +- **Config**: All services use environment variables for infra hosts (`MYSQL_HOST`, `REDIS_HOST`, `RABBITMQ_HOST`), allowing local dev with `localhost` and containerized with service names +- **Data pipeline**: WeCom callback → archive-service → RabbitMQ → conversation-service → intent-service → recommendation-service → WebSocket push to sidebar + +## Development Commands + +### Backend (Java 8 / Spring Boot 2.7.18) + +```bash +# Build all services from backend/ +cd backend && mvn clean package -DskipTests + +# Run a single service locally (e.g., conversation-service) +cd backend/conversation-service && mvn spring-boot:run + +# Run a specific test +cd backend/conversation-service && mvn test -Dtest=ConversationControllerTest + +# Run all tests +cd backend && mvn test +``` + +### Frontend + +```bash +# Sidebar (agent-facing H5) +cd frontend/sidebar && npm install && npm run dev + +# Admin dashboard +cd frontend/admin && npm install && npm run dev + +# Build for production +cd frontend/sidebar && npm run build +cd frontend/admin && npm run build +``` + +### Docker (full stack) + +```bash +# Start all services +docker compose up -d + +# Start only infrastructure (MySQL, Redis, RabbitMQ) +docker compose up -d mysql redis rabbitmq + +# View logs for a service +docker compose logs -f conversation-service + +# Rebuild and restart a specific service +docker compose up -d --build recommendation-service +``` + +### Database + +- `init.sql` — Full schema (15+ tables) + seed data. Loaded into MySQL on first `docker compose up`. +- `backend/utterances_cg.sql` — CG training script seed data +- `add_utterances.sql` — Additional utterance data + +## Service Ports Reference + +| Service | Port | +|---------|------| +| Gateway | 8080 | +| Auth | 8081 | +| Archive | 8082 | +| Conversation | 8083 | +| Intent | 8084 | +| Recommendation | 8085 | +| Generation | 8086 | +| KB Admin | 8087 | +| Analytics | 8088 | +| Admin Frontend | 5174 | +| Sidebar Frontend | 5173 | +| Nginx (unified entry) | 80 | + +## Tech Stack + +- **Backend**: Spring Boot 2.7.18, Spring Cloud 2021.0.8, MyBatis-Plus 3.5.5, Java 8 +- **Infra**: MySQL 5.7, Redis 6.x, RabbitMQ 3.x +- **Frontend**: React 18, TypeScript, Vite 5, Ant Design 5 / Ant Design Mobile 5 +- **LLM**: DashScope (Tongyi Qianwen qwen-turbo/qwen-plus) +- **Auth**: JWT (jjwt 0.11.5), WeCom OAuth2 +- **Deployment**: Docker + Docker Compose, Nginx + +## Important Constraints + +- **MySQL 5.7**: No native JSON functions beyond basics, no vector types. Vectors are stored in Redis. +- **Java 8**: All services compile to Java 1.8 (not the Dockerfile's Temurin 17 JRE, which is forward-compatible) +- **No Spring Cloud Discovery**: Services use static gateway routing, not Eureka/Consul diff --git a/backend/conversation-service/src/main/java/com/artedu/conversation/service/ConversationManager.java b/backend/conversation-service/src/main/java/com/artedu/conversation/service/ConversationManager.java index b096597..0ef87a6 100644 --- a/backend/conversation-service/src/main/java/com/artedu/conversation/service/ConversationManager.java +++ b/backend/conversation-service/src/main/java/com/artedu/conversation/service/ConversationManager.java @@ -257,6 +257,7 @@ public class ConversationManager { return content.length() > maxLength ? content.substring(0, maxLength) + "..." : content; } + @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) public static class ArchiveMessageEvent { private String msgid; private String corpId; diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class index e6258a9..c3f38fa 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class and b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class index 01809f2..d8731ec 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class and b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class differ diff --git a/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar b/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar index 88e2c14..d8daf06 100644 Binary files a/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar and b/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar differ diff --git a/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar.original b/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar.original index a9c9372..221748d 100644 Binary files a/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar.original and b/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar.original differ diff --git a/frontend/sidebar/src/App.tsx b/frontend/sidebar/src/App.tsx index ff74622..a43bb16 100644 --- a/frontend/sidebar/src/App.tsx +++ b/frontend/sidebar/src/App.tsx @@ -215,7 +215,7 @@ function App() { - + diff --git a/frontend/sidebar/src/components/ScriptRecommend.tsx b/frontend/sidebar/src/components/ScriptRecommend.tsx index 864f6ba..b55d195 100644 --- a/frontend/sidebar/src/components/ScriptRecommend.tsx +++ b/frontend/sidebar/src/components/ScriptRecommend.tsx @@ -4,25 +4,66 @@ import type { Recommendation } from '../types' interface Props { userInfo?: any + customerId?: string } -export default function ScriptRecommend({ userInfo }: Props) { +interface ConversationTurn { + turnNumber: number + studentContent: string + seatContent: string +} + +export default function ScriptRecommend({ userInfo, customerId: propCustomerId }: Props) { const [scripts, setScripts] = useState([]) const [loading, setLoading] = useState(false) const [generating, setGenerating] = useState(false) - const [customerMsg, setCustomerMsg] = useState('我想学游戏美术,零基础可以吗?') + const [customerMsg, setCustomerMsg] = useState('') const [generated, setGenerated] = useState('') // 从 localStorage 或企微环境获取真实身份信息 const sessionId = localStorage.getItem('current_session_id') || `session_${Date.now()}` - const customerId = localStorage.getItem('current_customer_id') || 'wx_001' + const customerId = propCustomerId || localStorage.getItem('current_customer_id') || 'wx_001' const staffId = userInfo?.userId || 'staff_001' const corpId = userInfo?.corpId || 'wwd483c2fba24ae30a' + // 动态获取最后一条学员消息 + const fetchLastCustomerMessage = async () => { + if (!customerId || customerId === 'wx_001') return + try { + const res = await fetch(`/api/v1/conversations?customerId=${customerId}&corpId=${corpId}`) + const data = await res.json() + if (data.code === 0 && data.data && data.data.length > 0) { + // 取最新的会话 + const sessions = data.data as Array<{ sessionId: string; startTime: string }> + const latestSession = sessions.sort( + (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime() + )[0] + const turnsRes = await fetch(`/api/v1/conversations/${latestSession.sessionId}/turns`) + const turnsData = await turnsRes.json() + if (turnsData.code === 0 && turnsData.data && turnsData.data.length > 0) { + const turns = turnsData.data as ConversationTurn[] + // 取最后一条有内容的学员消息 + for (let i = turns.length - 1; i >= 0; i--) { + if (turns[i].studentContent?.trim()) { + setCustomerMsg(turns[i].studentContent.trim()) + break + } + } + } + } + } catch (e) { + console.error('获取最后一条学员消息失败:', e) + } + } + useEffect(() => { loadRecommendations() }, []) + useEffect(() => { + fetchLastCustomerMessage() + }, [customerId]) + const loadRecommendations = async () => { setLoading(true) try {