feat: 新增员工姓名绑定、客户信息补全与docker-compose配置
1. 为话术生成服务添加staffName参数,支持生成回复时使用真实员工姓名 2. 优化客户昵称补全逻辑,新增自动创建缺失客户记录功能 3. 重构前端话术推荐组件,调整客户信息展示逻辑 4. 添加完整的docker-compose-server部署配置文件
This commit is contained in:
parent
4e6e673402
commit
5182590fad
1
backend/archive-service/.gitignore
vendored
1
backend/archive-service/.gitignore
vendored
@ -1,2 +1 @@
|
|||||||
target/
|
target/
|
||||||
target
|
|
||||||
@ -270,32 +270,48 @@ public class ArchiveMessageController {
|
|||||||
nameMap.putAll(customerNameMap);
|
nameMap.putAll(customerNameMap);
|
||||||
|
|
||||||
// 对未找到昵称或昵称为ID的外部客户,实时调用企微API获取并更新数据库
|
// 对未找到昵称或昵称为ID的外部客户,实时调用企微API获取并更新数据库
|
||||||
|
log.info("enrichSenderNames 开始处理, userIds={}, nameMap keys={}", userIds, nameMap.keySet());
|
||||||
for (String uid : userIds) {
|
for (String uid : userIds) {
|
||||||
String currentName = nameMap.get(uid);
|
String currentName = nameMap.get(uid);
|
||||||
if (isExternalUserId(uid) && (currentName == null || currentName.equals(uid))) {
|
boolean isExternal = isExternalUserId(uid);
|
||||||
|
boolean needFetch = isExternal && (currentName == null || currentName.equals(uid));
|
||||||
|
log.info("处理uid={}, currentName={}, isExternal={}, needFetch={}", uid, currentName, isExternal, needFetch);
|
||||||
|
if (needFetch) {
|
||||||
try {
|
try {
|
||||||
Map<String, Object> detail = weComApiClient.getExternalContact(uid);
|
Map<String, Object> detail = weComApiClient.getExternalContact(uid);
|
||||||
|
log.info("getExternalContact 返回: uid={}, detail={}", uid, detail);
|
||||||
if (detail != null && !detail.isEmpty()) {
|
if (detail != null && !detail.isEmpty()) {
|
||||||
Map<String, Object> contact = (Map<String, Object>) detail.get("external_contact");
|
Map<String, Object> contact = (Map<String, Object>) detail.get("external_contact");
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
String name = (String) contact.get("name");
|
String name = (String) contact.get("name");
|
||||||
|
log.info("解析到name: uid={}, name={}", uid, name);
|
||||||
if (name != null && !name.isEmpty()) {
|
if (name != null && !name.isEmpty()) {
|
||||||
nameMap.put(uid, name);
|
nameMap.put(uid, name);
|
||||||
// 更新数据库
|
// 更新或创建数据库记录
|
||||||
Customer customer = customerMapper.selectOne(
|
Customer customer = customerMapper.selectOne(
|
||||||
new LambdaQueryWrapper<Customer>()
|
new LambdaQueryWrapper<Customer>()
|
||||||
.eq(Customer::getCustomerId, uid)
|
.eq(Customer::getCustomerId, uid)
|
||||||
.eq(Customer::getCorpId, "wwd483c2fba24ae30a")
|
.eq(Customer::getCorpId, corpId)
|
||||||
);
|
);
|
||||||
if (customer != null) {
|
if (customer != null) {
|
||||||
customer.setName(name);
|
customer.setName(name);
|
||||||
customerMapper.updateById(customer);
|
customerMapper.updateById(customer);
|
||||||
|
log.info("更新客户昵称成功: uid={}, name={}", uid, name);
|
||||||
|
} else {
|
||||||
|
Customer newCustomer = new Customer();
|
||||||
|
newCustomer.setCustomerId(uid);
|
||||||
|
newCustomer.setCorpId(corpId);
|
||||||
|
newCustomer.setName(name);
|
||||||
|
newCustomer.setType("UNKNOWN");
|
||||||
|
newCustomer.setStatus("ACTIVE");
|
||||||
|
customerMapper.insert(newCustomer);
|
||||||
|
log.info("创建客户记录成功: uid={}, name={}", uid, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("实时获取客户昵称失败: {}", uid);
|
log.warn("实时获取客户昵称失败: uid={}, err={}", uid, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -112,11 +112,12 @@ public class GenerationController {
|
|||||||
public Result<Map<String, Object>> generateUtterance(
|
public Result<Map<String, Object>> generateUtterance(
|
||||||
@RequestParam(value = "fromUser", required = false) String fromUser,
|
@RequestParam(value = "fromUser", required = false) String fromUser,
|
||||||
@RequestParam(value = "toUser", required = false) String toUser,
|
@RequestParam(value = "toUser", required = false) String toUser,
|
||||||
@RequestParam(value = "roomid", required = false) String roomid) {
|
@RequestParam(value = "roomid", required = false) String roomid,
|
||||||
|
@RequestParam(value = "staffName", required = false) String staffName) {
|
||||||
|
|
||||||
String chatContext = buildChatContext(fromUser, toUser, roomid);
|
String chatContext = buildChatContext(fromUser, toUser, roomid);
|
||||||
|
|
||||||
String[] promptPair = promptEngine.buildUtterancePrompt(chatContext);
|
String[] promptPair = promptEngine.buildUtterancePrompt(chatContext, staffName);
|
||||||
String systemPrompt = promptPair[0];
|
String systemPrompt = promptPair[0];
|
||||||
String userPrompt = promptPair[1];
|
String userPrompt = promptPair[1];
|
||||||
|
|
||||||
@ -187,7 +188,8 @@ public class GenerationController {
|
|||||||
public SseEmitter streamUtterance(
|
public SseEmitter streamUtterance(
|
||||||
@RequestParam("fromUser") String fromUser,
|
@RequestParam("fromUser") String fromUser,
|
||||||
@RequestParam("toUser") String toUser,
|
@RequestParam("toUser") String toUser,
|
||||||
@RequestParam(value = "roomid", required = false) String roomid) {
|
@RequestParam(value = "roomid", required = false) String roomid,
|
||||||
|
@RequestParam(value = "staffName", required = false) String staffName) {
|
||||||
|
|
||||||
SseEmitter emitter = new SseEmitter(120000L);
|
SseEmitter emitter = new SseEmitter(120000L);
|
||||||
|
|
||||||
@ -208,8 +210,8 @@ public class GenerationController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String systemPrompt = promptEngine.buildSystemPrompt(chatContext);
|
String systemPrompt = promptEngine.buildSystemPrompt(chatContext, staffName);
|
||||||
String userPrompt = promptEngine.buildMultiStyleUserPrompt();
|
String userPrompt = promptEngine.buildMultiStyleUserPrompt(staffName);
|
||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
String generated = qianwenClient.generate(systemPrompt, userPrompt, 1200);
|
String generated = qianwenClient.generate(systemPrompt, userPrompt, 1200);
|
||||||
|
|||||||
@ -10,9 +10,14 @@ public class PromptEngine {
|
|||||||
/**
|
/**
|
||||||
* 构建生成话术的系统提示词(角色设定)
|
* 构建生成话术的系统提示词(角色设定)
|
||||||
*/
|
*/
|
||||||
public String buildSystemPrompt(String chatHistoryContext) {
|
public String buildSystemPrompt(String chatHistoryContext, String staffName) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("资深销售教练,仅输出符合规范的 3 条推荐回复话术,全程禁止输出任何对话分析、客户判断、前置解读、总结说明文字,任何分析类文字一律不写,正文只保留 3 条话术条目。\n");
|
sb.append("资深销售教练,仅输出符合规范的 3 条推荐回复话术,全程禁止输出任何对话分析、客户判断、前置解读、总结说明文字,任何分析类文字一律不写,正文只保留 3 条话术条目。\n");
|
||||||
|
if (staffName != null && !staffName.isEmpty()) {
|
||||||
|
sb.append("## 重要约束\n");
|
||||||
|
sb.append("你的名字是「").append(staffName).append("」,在回复中如果需要自称或介绍自己,必须使用这个名字,绝对禁止编造任何其他名字(如小王、小李、小张等)。\n");
|
||||||
|
}
|
||||||
|
|
||||||
sb.append("## 对话历史\n");
|
sb.append("## 对话历史\n");
|
||||||
sb.append(chatHistoryContext);
|
sb.append(chatHistoryContext);
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
@ -80,8 +85,8 @@ public class PromptEngine {
|
|||||||
* 构建完整话术 Prompt(system + user 分离)
|
* 构建完整话术 Prompt(system + user 分离)
|
||||||
* 返回 String[]{systemPrompt, userPrompt}
|
* 返回 String[]{systemPrompt, userPrompt}
|
||||||
*/
|
*/
|
||||||
public String[] buildUtterancePrompt(String chatHistoryContext) {
|
public String[] buildUtterancePrompt(String chatHistoryContext, String staffName) {
|
||||||
String system = buildSystemPrompt(chatHistoryContext);
|
String system = buildSystemPrompt(chatHistoryContext, staffName);
|
||||||
// String user = buildUserPrompt(message, context, customerProfile, conversationContext,
|
// String user = buildUserPrompt(message, context, customerProfile, conversationContext,
|
||||||
// referenceUtterances, intent, stage, stageLabel, intentLabel);
|
// referenceUtterances, intent, stage, stageLabel, intentLabel);
|
||||||
String user = "";
|
String user = "";
|
||||||
@ -133,8 +138,11 @@ public class PromptEngine {
|
|||||||
* 多风格话术生成的用户提示词
|
* 多风格话术生成的用户提示词
|
||||||
* 要求 LLM 输出 3 条不同风格话术,用 ||| 分隔
|
* 要求 LLM 输出 3 条不同风格话术,用 ||| 分隔
|
||||||
*/
|
*/
|
||||||
public String buildMultiStyleUserPrompt() {
|
public String buildMultiStyleUserPrompt(String staffName) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (staffName != null && !staffName.isEmpty()) {
|
||||||
|
sb.append("你的名字是「").append(staffName).append("」,请用这个名字进行回复,不要编造其他名字。\n\n");
|
||||||
|
}
|
||||||
sb.append("请根据上述对话历史,生成 3 条不同风格的推荐回复话术。\n\n");
|
sb.append("请根据上述对话历史,生成 3 条不同风格的推荐回复话术。\n\n");
|
||||||
sb.append("风格要求(严格按顺序输出):\n");
|
sb.append("风格要求(严格按顺序输出):\n");
|
||||||
sb.append("第1条 - 亲和风格:温暖亲切,像朋友聊天,拉近距离\n");
|
sb.append("第1条 - 亲和风格:温暖亲切,像朋友聊天,拉近距离\n");
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
338
docker-compose-server.yml
Normal file
338
docker-compose-server.yml
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ---------------------- 基础设施 ----------------------
|
||||||
|
mysql:
|
||||||
|
image: mysql:5.7
|
||||||
|
container_name: ai-assistant-mysql
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-ai_assistant}
|
||||||
|
MYSQL_CHARACTER_SET_SERVER: utf8mb4
|
||||||
|
MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
command: >
|
||||||
|
--default-authentication-plugin=mysql_native_password
|
||||||
|
--character-set-server=utf8mb4
|
||||||
|
--collation-server=utf8mb4_unicode_ci
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6-alpine
|
||||||
|
container_name: ai-assistant-redis
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
image: rabbitmq:3-management-alpine
|
||||||
|
container_name: ai-assistant-rabbitmq
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "5672:5672"
|
||||||
|
- "15672:15672"
|
||||||
|
environment:
|
||||||
|
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER:-admin}
|
||||||
|
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS:-admin}
|
||||||
|
volumes:
|
||||||
|
- rabbitmq_data:/var/lib/rabbitmq
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "rabbitmq-diagnostics", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
# ---------------------- 后端服务 ----------------------
|
||||||
|
gateway:
|
||||||
|
build: ./backend/gateway
|
||||||
|
container_name: ai-assistant-gateway
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./gateway-application.yml:/app/config/application.yml
|
||||||
|
depends_on:
|
||||||
|
- auth-service
|
||||||
|
- archive-service
|
||||||
|
- conversation-service
|
||||||
|
- intent-service
|
||||||
|
- recommend-service
|
||||||
|
- generation-service
|
||||||
|
- kb-admin-service
|
||||||
|
- analytics-service
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
auth-service:
|
||||||
|
build: ./backend/auth-service
|
||||||
|
container_name: ai-assistant-auth
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
|
environment:
|
||||||
|
- SERVER_SERVLET_ENCODING_CHARSET=UTF-8
|
||||||
|
- SERVER_SERVLET_ENCODING_ENABLED=true
|
||||||
|
- SERVER_SERVLET_ENCODING_FORCE=true
|
||||||
|
- MYSQL_HOST=sh-cdb-6fzlwnms.sql.tencentcdb.com
|
||||||
|
- MYSQL_USERNAME=assistant_prod
|
||||||
|
- MYSQL_PASSWORD=U%$4Tu_C3+4
|
||||||
|
- MYSQL_PORT=63912
|
||||||
|
- MYSQL_DATABASE=ai_assistant
|
||||||
|
- SPRING_DATASOURCE_USERNAME=assistant_prod
|
||||||
|
- SPRING_DATASOURCE_PASSWORD=U%$4Tu_C3+4
|
||||||
|
- SPRING_DATASOURCE_URL=jdbc:mysql://sh-cdb-6fzlwnms.sql.tencentcdb.com:63912/ai_assistant?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-your-256-bit-secret-key-here-at-least-32-characters}
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
archive-service:
|
||||||
|
build: ./backend/archive-service
|
||||||
|
container_name: ai-assistant-archive
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8082:8082"
|
||||||
|
environment:
|
||||||
|
- SERVER_SERVLET_ENCODING_CHARSET=UTF-8
|
||||||
|
- SERVER_SERVLET_ENCODING_ENABLED=true
|
||||||
|
- SERVER_SERVLET_ENCODING_FORCE=true
|
||||||
|
- MYSQL_HOST=sh-cdb-6fzlwnms.sql.tencentcdb.com
|
||||||
|
- MYSQL_USERNAME=assistant_prod
|
||||||
|
- MYSQL_PASSWORD=U%$4Tu_C3+4
|
||||||
|
- MYSQL_PORT=63912
|
||||||
|
- MYSQL_USERNAME=assistant_prod
|
||||||
|
- MYSQL_PASSWORD=U%$4Tu_C3+4
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- RABBITMQ_HOST=rabbitmq
|
||||||
|
- WECOM_ARCHIVE_SECRET=${WECOM_ARCHIVE_SECRET:-}
|
||||||
|
- WECOM_ARCHIVE_CALLBACK_TOKEN=${WECOM_ARCHIVE_CALLBACK_TOKEN:-}
|
||||||
|
- WECOM_ARCHIVE_CALLBACK_AESKEY=${WECOM_ARCHIVE_CALLBACK_AESKEY:-}
|
||||||
|
- WECOM_ARCHIVE_SDK_PATH=${WECOM_ARCHIVE_SDK_PATH:-/app/sdk}
|
||||||
|
- ARCHIVE_REPAIR_ON_STARTUP=true
|
||||||
|
- WECOM_AGENT_SECRET=nU1xezyHyonS9rOJ9Kwo3i5OM9iuWc1TrjXf-rOYZx0
|
||||||
|
- WECOM_CONTACT_SECRET=zrZkae10k1T7bqrt-jVYjj9LQ4GgMibj-wUMy48OsyY
|
||||||
|
volumes:
|
||||||
|
- ./keys:/app/keys
|
||||||
|
# 如需真实企微SDK,取消注释下行并将Linux .so文件放入 ./sdk/linux/
|
||||||
|
- ./sdk/linux:/app/sdk
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- redis
|
||||||
|
- rabbitmq
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
conversation-service:
|
||||||
|
build: ./backend/conversation-service
|
||||||
|
container_name: ai-assistant-conversation
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8083:8083"
|
||||||
|
environment:
|
||||||
|
- SERVER_SERVLET_ENCODING_CHARSET=UTF-8
|
||||||
|
- SERVER_SERVLET_ENCODING_ENABLED=true
|
||||||
|
- SERVER_SERVLET_ENCODING_FORCE=true
|
||||||
|
- MYSQL_HOST=sh-cdb-6fzlwnms.sql.tencentcdb.com
|
||||||
|
- MYSQL_USERNAME=assistant_prod
|
||||||
|
- MYSQL_PASSWORD=U%$4Tu_C3+4
|
||||||
|
- MYSQL_PORT=63912
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- RABBITMQ_HOST=rabbitmq
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- redis
|
||||||
|
- rabbitmq
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
intent-service:
|
||||||
|
build: ./backend/intent-service
|
||||||
|
container_name: ai-assistant-intent
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8084:8084"
|
||||||
|
environment:
|
||||||
|
- SERVER_SERVLET_ENCODING_CHARSET=UTF-8
|
||||||
|
- SERVER_SERVLET_ENCODING_ENABLED=true
|
||||||
|
- SERVER_SERVLET_ENCODING_FORCE=true
|
||||||
|
- MYSQL_HOST=sh-cdb-6fzlwnms.sql.tencentcdb.com
|
||||||
|
- MYSQL_USERNAME=assistant_prod
|
||||||
|
- MYSQL_PASSWORD=U%$4Tu_C3+4
|
||||||
|
- MYSQL_PORT=63912
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- RABBITMQ_HOST=rabbitmq
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- redis
|
||||||
|
- rabbitmq
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
recommend-service:
|
||||||
|
build: ./backend/recommendation-service
|
||||||
|
container_name: ai-assistant-recommend
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8085:8085"
|
||||||
|
environment:
|
||||||
|
- SERVER_SERVLET_ENCODING_CHARSET=UTF-8
|
||||||
|
- SERVER_SERVLET_ENCODING_ENABLED=true
|
||||||
|
- SERVER_SERVLET_ENCODING_FORCE=true
|
||||||
|
- MYSQL_HOST=sh-cdb-6fzlwnms.sql.tencentcdb.com
|
||||||
|
- MYSQL_USERNAME=assistant_prod
|
||||||
|
- MYSQL_PASSWORD=U%$4Tu_C3+4
|
||||||
|
- MYSQL_PORT=63912
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- RABBITMQ_HOST=rabbitmq
|
||||||
|
- DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY:-}
|
||||||
|
- MYSQL_HOST=sh-cdb-6fzlwnms.sql.tencentcdb.com
|
||||||
|
- MYSQL_PORT=63912
|
||||||
|
- MYSQL_USERNAME=assistant_prod
|
||||||
|
- MYSQL_PASSWORD=U%$4Tu_C3+4
|
||||||
|
- MYSQL_DATABASE=ai_assistant
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- redis
|
||||||
|
- rabbitmq
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
generation-service:
|
||||||
|
build: ./backend/generation-service
|
||||||
|
container_name: ai-assistant-generation
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8086:8086"
|
||||||
|
environment:
|
||||||
|
- SERVER_SERVLET_ENCODING_CHARSET=UTF-8
|
||||||
|
- SERVER_SERVLET_ENCODING_ENABLED=true
|
||||||
|
- SERVER_SERVLET_ENCODING_FORCE=true
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY:-}
|
||||||
|
- MYSQL_HOST=sh-cdb-6fzlwnms.sql.tencentcdb.com
|
||||||
|
- MYSQL_PORT=63912
|
||||||
|
- MYSQL_USERNAME=assistant_prod
|
||||||
|
- MYSQL_PASSWORD=U%$4Tu_C3+4
|
||||||
|
- MYSQL_DATABASE=ai_assistant
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
kb-admin-service:
|
||||||
|
build: ./backend/kb-admin-service
|
||||||
|
container_name: ai-assistant-kbadmin
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8087:8087"
|
||||||
|
environment:
|
||||||
|
- SERVER_SERVLET_ENCODING_CHARSET=UTF-8
|
||||||
|
- SERVER_SERVLET_ENCODING_ENABLED=true
|
||||||
|
- SERVER_SERVLET_ENCODING_FORCE=true
|
||||||
|
- MYSQL_HOST=sh-cdb-6fzlwnms.sql.tencentcdb.com
|
||||||
|
- MYSQL_USERNAME=assistant_prod
|
||||||
|
- MYSQL_PASSWORD=U%$4Tu_C3+4
|
||||||
|
- MYSQL_PORT=63912
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
analytics-service:
|
||||||
|
build: ./backend/analytics-service
|
||||||
|
container_name: ai-assistant-analytics
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8088:8088"
|
||||||
|
environment:
|
||||||
|
- SERVER_SERVLET_ENCODING_CHARSET=UTF-8
|
||||||
|
- SERVER_SERVLET_ENCODING_ENABLED=true
|
||||||
|
- SERVER_SERVLET_ENCODING_FORCE=true
|
||||||
|
- MYSQL_HOST=sh-cdb-6fzlwnms.sql.tencentcdb.com
|
||||||
|
- MYSQL_USERNAME=assistant_prod
|
||||||
|
- MYSQL_PASSWORD=U%$4Tu_C3+4
|
||||||
|
- MYSQL_PORT=63912
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
# ---------------------- 前端服务 ----------------------
|
||||||
|
admin:
|
||||||
|
build: ./frontend/admin
|
||||||
|
container_name: ai-assistant-admin
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "5174:80"
|
||||||
|
depends_on:
|
||||||
|
- gateway
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
sidebar:
|
||||||
|
build: ./frontend/sidebar
|
||||||
|
container_name: ai-assistant-sidebar
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "5173:80"
|
||||||
|
depends_on:
|
||||||
|
- gateway
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
# ---------------------- 统一入口 Nginx ----------------------
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: ai-assistant-nginx
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8089:80"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
depends_on:
|
||||||
|
- gateway
|
||||||
|
- admin
|
||||||
|
- sidebar
|
||||||
|
networks:
|
||||||
|
- ai-assistant-network
|
||||||
|
|
||||||
|
# ---------------------- 数据卷 ----------------------
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
|
redis_data:
|
||||||
|
rabbitmq_data:
|
||||||
|
|
||||||
|
# ---------------------- 网络 ----------------------
|
||||||
|
networks:
|
||||||
|
ai-assistant-network:
|
||||||
|
driver: bridge
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||||
import { Button, Toast, SpinLoading, Tag, Space } from 'antd-mobile'
|
import { Button, Toast, SpinLoading } from 'antd-mobile'
|
||||||
|
|
||||||
interface StyleConfig {
|
interface StyleConfig {
|
||||||
id: string
|
id: string
|
||||||
@ -31,8 +31,7 @@ const STYLES: StyleConfig[] = [
|
|||||||
export default function ScriptRecommendStream({ userInfo, customerId: propCustomerId, chatContext }: Props) {
|
export default function ScriptRecommendStream({ userInfo, customerId: propCustomerId, chatContext }: Props) {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [generating, setGenerating] = useState(false)
|
const [generating, setGenerating] = useState(false)
|
||||||
const [customerMsg, setCustomerMsg] = useState('')
|
const [, setProfileTags] = useState<string[]>([])
|
||||||
const [profileTags, setProfileTags] = useState<string[]>([])
|
|
||||||
const [utterances, setUtterances] = useState<UtteranceItem[]>([])
|
const [utterances, setUtterances] = useState<UtteranceItem[]>([])
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const sseRef = useRef<EventSource | null>(null)
|
const sseRef = useRef<EventSource | null>(null)
|
||||||
@ -123,7 +122,7 @@ export default function ScriptRecommendStream({ userInfo, customerId: propCustom
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建 SSE URL:群聊传 roomid,单聊传 fromUser+toUser
|
// 构建 SSE URL:群聊传 roomid,单聊传 fromUser+toUser+staffName
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (isGroupChat && chatId) {
|
if (isGroupChat && chatId) {
|
||||||
params.append('roomid', chatId)
|
params.append('roomid', chatId)
|
||||||
@ -131,6 +130,9 @@ export default function ScriptRecommendStream({ userInfo, customerId: propCustom
|
|||||||
params.append('fromUser', customerId)
|
params.append('fromUser', customerId)
|
||||||
params.append('toUser', staffId)
|
params.append('toUser', staffId)
|
||||||
}
|
}
|
||||||
|
if (userInfo?.userName) {
|
||||||
|
params.append('staffName', userInfo.userName)
|
||||||
|
}
|
||||||
|
|
||||||
const url = `/api/v1/generation/utterance-stream?${params.toString()}`
|
const url = `/api/v1/generation/utterance-stream?${params.toString()}`
|
||||||
console.log('SSE connect:', url)
|
console.log('SSE connect:', url)
|
||||||
@ -211,12 +213,20 @@ export default function ScriptRecommendStream({ userInfo, customerId: propCustom
|
|||||||
const init = async () => {
|
const init = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
// 单聊获取最后一条消息用于展示(限定当前销售与学员的对话,避免串消息)
|
// 单聊获取画像标签
|
||||||
if (!isGroupChat && customerId && staffId) {
|
if (!isGroupChat && customerId) {
|
||||||
const res = await fetch(`/api/v1/conversations/last-message?customerId=${customerId}&staffId=${staffId}&corpId=${corpId}`)
|
const profileRes = await fetch(`/api/v1/intent/profile/${encodeURIComponent(customerId)}?corpId=${corpId}`)
|
||||||
const data = await res.json()
|
const profileData = await profileRes.json()
|
||||||
if (data.code === 0 && data.data?.content) {
|
if (profileData.code === 0 && profileData.data) {
|
||||||
setCustomerMsg(data.data.content)
|
const p = profileData.data
|
||||||
|
const tags: string[] = []
|
||||||
|
if (p.currentOccupation) tags.push(p.currentOccupation)
|
||||||
|
if (p.studentType) tags.push(p.studentType)
|
||||||
|
if (p.skillLevel) tags.push(p.skillLevel)
|
||||||
|
if (p.intentLevel) tags.push(p.intentLevel)
|
||||||
|
if (p.concernFocus) tags.push(p.concernFocus)
|
||||||
|
if (p.decisionStage) tags.push(p.decisionStage)
|
||||||
|
setProfileTags(tags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -247,30 +257,7 @@ export default function ScriptRecommendStream({ userInfo, customerId: propCustom
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 12, background: '#f5f5f5', minHeight: '100vh' }}>
|
<div style={{ padding: 12, background: '#f5f5f5', minHeight: '100vh' }}>
|
||||||
{/* 学员/群聊消息 + 标签 */}
|
{/* 生成中状态 -->
|
||||||
<div style={{
|
|
||||||
background: '#fff',
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: '12px 16px',
|
|
||||||
marginBottom: 12,
|
|
||||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)'
|
|
||||||
}}>
|
|
||||||
<div style={{ fontSize: 13, color: '#666', marginBottom: 8 }}>
|
|
||||||
💬 <span style={{ color: '#999' }}>
|
|
||||||
{isGroupChat ? '群聊消息' : '学员消息'}:
|
|
||||||
</span>
|
|
||||||
<span style={{ color: '#333', fontWeight: 500 }}>{customerMsg || '暂无'}</span>
|
|
||||||
</div>
|
|
||||||
{profileTags.length > 0 && (
|
|
||||||
<Space wrap>
|
|
||||||
{profileTags.map(tag => (
|
|
||||||
<Tag key={tag} color='primary' fill='outline' style={{ fontSize: 12 }}>{tag}</Tag>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 生成中状态 */}
|
|
||||||
{generating && utterances.every(u => !u.isComplete) && (
|
{generating && utterances.every(u => !u.isComplete) && (
|
||||||
<div style={{
|
<div style={{
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user