合并远程master,解决ArchiveMessageController冲突,清理target编译文件

This commit is contained in:
qinyong@9artedu.com 2026-06-11 17:11:35 +08:00
commit 926c8a828f
28 changed files with 398 additions and 154 deletions

View File

@ -1,2 +1 @@
target/ target/
target

View File

@ -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());
} }
} }
} }

View File

@ -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);

View File

@ -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");

View File

@ -1,45 +0,0 @@
server:
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
port: 8086
spring:
application:
name: generation-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:ai_assistant}?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
rabbitmq:
host: ${RABBITMQ_HOST:localhost}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:admin}
password: ${RABBITMQ_PASSWORD:admin}
llm:
qianwen:
api-key: sk-39ceb8d746014b349109e76f893acb18
model: qwen-plus
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0

View File

@ -1,24 +0,0 @@
-- 大模型配置表
CREATE TABLE IF NOT EXISTS llm_models (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
provider VARCHAR(32) NOT NULL COMMENT '提供商: bailian(百炼), doubao(豆包), hunyuan(混元)',
model_name VARCHAR(64) NOT NULL COMMENT '模型显示名称',
model_code VARCHAR(64) NOT NULL COMMENT '模型代码(调用API时使用)',
api_key VARCHAR(255) COMMENT 'API密钥',
api_url VARCHAR(255) COMMENT 'API基础地址',
is_active TINYINT DEFAULT 1 COMMENT '是否启用: 1启用 0禁用',
is_default TINYINT DEFAULT 0 COMMENT '是否为默认模型: 1是 0否',
config_json TEXT COMMENT '额外配置JSON',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_provider_model (provider, model_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大模型配置表';
-- 插入默认模型(百炼)
INSERT INTO llm_models (provider, model_name, model_code, api_key, api_url, is_active, is_default, config_json) VALUES
('bailian', '通义千问-Turbo', 'qwen-turbo', 'sk-39ceb8d746014b349109e76f893acb18', 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', 1, 1, '{"maxTokens": 1500, "temperature": 0.7}'),
('bailian', '通义千问-Plus', 'qwen-plus', 'sk-39ceb8d746014b349109e76f893acb18', 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', 1, 0, '{"maxTokens": 2000, "temperature": 0.7}'),
('doubao', '豆包-Pro', 'doubao-pro-32k', '', 'https://ark.cn-beijing.volces.com/api/v3/chat/completions', 1, 0, '{"maxTokens": 1500, "temperature": 0.7}'),
('hunyuan', '混元-Standard', 'hunyuan-standard', '', 'https://hunyuan.tencentcloudapi.com/', 0, 0, '{"maxTokens": 1500, "temperature": 0.7}')
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;

View File

@ -1,3 +0,0 @@
artifactId=generation-service
groupId=com.artedu
version=1.0.0-SNAPSHOT

View File

@ -1,17 +0,0 @@
com\artedu\generation\service\QianwenClient.class
com\artedu\generation\controller\GenerateRequest.class
com\artedu\generation\entity\ArchiveMessage.class
com\artedu\generation\service\LlmModelService.class
com\artedu\generation\client\DoubaoClient.class
com\artedu\generation\mapper\ArchiveMessageMapper.class
com\artedu\generation\client\LLMClientFactory.class
com\artedu\generation\controller\LlmModelController.class
com\artedu\generation\mapper\LlmModelMapper.class
com\artedu\generation\entity\LlmModel.class
com\artedu\generation\controller\GenerationController.class
com\artedu\generation\service\PromptEngine.class
com\artedu\generation\client\BailianClient.class
com\artedu\generation\client\LLMClient.class
com\artedu\generation\client\HunyuanClient.class
com\artedu\generation\GenerationServiceApplication.class
com\artedu\generation\controller\EmbedRequest.class

View File

@ -1,17 +0,0 @@
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\client\BailianClient.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\client\DoubaoClient.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\client\HunyuanClient.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\client\LLMClient.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\client\LLMClientFactory.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\controller\EmbedRequest.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\controller\GenerateRequest.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\controller\GenerationController.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\controller\LlmModelController.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\entity\ArchiveMessage.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\entity\LlmModel.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\GenerationServiceApplication.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\mapper\ArchiveMessageMapper.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\mapper\LlmModelMapper.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\service\LlmModelService.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\service\PromptEngine.java
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\service\QianwenClient.java

338
docker-compose-server.yml Normal file
View 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

View File

@ -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',