feat(sidebar&conversation): 新增动态获取客户历史消息功能,完善参数传递
1. 为ScriptRecommend组件新增customerId参数,支持从父组件传入客户ID 2. 添加动态获取最后一条学员消息的逻辑,从后端API拉取会话历史 3. 为ArchiveMessageEvent类添加Jackson忽略未知属性注解 4. 新增部署文档和项目说明文档AGENTS.md、CLAUDE.md
This commit is contained in:
parent
6160691aa9
commit
4d8bc95063
111
AGENTS.md
Normal file
111
AGENTS.md
Normal file
@ -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) → 各容器
|
||||||
120
CLAUDE.md
Normal file
120
CLAUDE.md
Normal file
@ -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<T>` from `common` module (`code: 0` = success, `code: -1` = failure, with `message` and `timestamp`)
|
||||||
|
- **Pagination**: `PageResult<T>` 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
|
||||||
@ -257,6 +257,7 @@ public class ConversationManager {
|
|||||||
return content.length() > maxLength ? content.substring(0, maxLength) + "..." : content;
|
return content.length() > maxLength ? content.substring(0, maxLength) + "..." : content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public static class ArchiveMessageEvent {
|
public static class ArchiveMessageEvent {
|
||||||
private String msgid;
|
private String msgid;
|
||||||
private String corpId;
|
private String corpId;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -215,7 +215,7 @@ function App() {
|
|||||||
|
|
||||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||||
<Tabs.Tab title="话术推荐" key="recommend">
|
<Tabs.Tab title="话术推荐" key="recommend">
|
||||||
<ScriptRecommend userInfo={userInfo} />
|
<ScriptRecommend userInfo={userInfo} customerId={currentCustomerId} />
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
<Tabs.Tab title="对话分析" key="analysis">
|
<Tabs.Tab title="对话分析" key="analysis">
|
||||||
<ConversationAnalysis userInfo={userInfo} />
|
<ConversationAnalysis userInfo={userInfo} />
|
||||||
|
|||||||
@ -4,25 +4,66 @@ import type { Recommendation } from '../types'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
userInfo?: any
|
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<Recommendation[]>([])
|
const [scripts, setScripts] = useState<Recommendation[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [generating, setGenerating] = useState(false)
|
const [generating, setGenerating] = useState(false)
|
||||||
const [customerMsg, setCustomerMsg] = useState('我想学游戏美术,零基础可以吗?')
|
const [customerMsg, setCustomerMsg] = useState('')
|
||||||
const [generated, setGenerated] = useState('')
|
const [generated, setGenerated] = useState('')
|
||||||
|
|
||||||
// 从 localStorage 或企微环境获取真实身份信息
|
// 从 localStorage 或企微环境获取真实身份信息
|
||||||
const sessionId = localStorage.getItem('current_session_id') || `session_${Date.now()}`
|
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 staffId = userInfo?.userId || 'staff_001'
|
||||||
const corpId = userInfo?.corpId || 'wwd483c2fba24ae30a'
|
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(() => {
|
useEffect(() => {
|
||||||
loadRecommendations()
|
loadRecommendations()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLastCustomerMessage()
|
||||||
|
}, [customerId])
|
||||||
|
|
||||||
const loadRecommendations = async () => {
|
const loadRecommendations = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user