Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion internal/agent/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,16 +287,83 @@ func buildEnhancedSystemPromptForAgent(base string, summary *entity.ChatSummary,
if userCtx.TimeStr != "" {
userInfo += "- 当前时间:" + userCtx.TimeStr + "\n"
}
if userCtx.Timezone != "" {
userInfo += "- 用户时区:" + userCtx.Timezone + "\n"
}
if userCtx.Username != "" {
userInfo += "- 用户:" + userCtx.Username + "\n"
}
if userCtx.Role != "" {
userInfo += "- 角色:" + userCtx.Role + "\n"
userInfo += "- 系统角色:" + userCtx.Role + "\n"
}
if userCtx.Department != "" {
userInfo += "- 部门:" + userCtx.Department + "\n"
}
if userCtx.Position != "" {
userInfo += "- 职位:" + userCtx.Position + "\n"
}
if userCtx.Expertise != "" {
userInfo += "- 擅长/关注:" + userCtx.Expertise + "\n"
}
if userCtx.Language != "" {
userInfo += "- 偏好语言:" + userCtx.Language + "\n"
}
if userInfo != "## 当前信息\n" {
extras = append(extras, userInfo)
}

if userCtx.AnswerStyle != "" || userCtx.TableFirst || userCtx.CitationStyle != "" {
var p strings.Builder
p.WriteString("## 用户回答偏好\n")
switch userCtx.AnswerStyle {
case "concise":
p.WriteString("- 回答风格:简洁凝练,直击要点,3~5 句说完,不过度展开\n")
case "detailed":
p.WriteString("- 回答风格:详细展开,先结论再分点论述,必要时给例子和注意事项\n")
case "step_by_step":
p.WriteString("- 回答风格:分步讲解,用 1/2/3…编号或小标题组织步骤\n")
default:
p.WriteString("- 回答风格:平衡简洁与完整,先结论再展开\n")
}
if userCtx.TableFirst {
p.WriteString("- 结构化呈现:对比、列表、映射等数据优先用 Markdown 表格组织\n")
}
switch userCtx.CitationStyle {
case "none":
p.WriteString("- 引用格式:正文不标注引用,引用信息仅由消息底部来源区展示\n")
case "doc_title_only":
p.WriteString("- 引用格式:正文引用时只提「根据《文档名》」,不要章节\n")
default:
p.WriteString("- 引用格式:正文引用时以「根据《文档名》· 章节标题」形式说明来源\n")
}
extras = append(extras, p.String())
}

if strings.TrimSpace(userCtx.RoleTemplatePrompt) != "" {
extras = append(extras, "## 角色模板设定\n"+strings.TrimSpace(userCtx.RoleTemplatePrompt))
}

if userCtx.Language != "" {
langHint := "## 回答语言\n"
switch userCtx.Language {
case "en-US":
langHint += "- 请使用英文回答(美式英语)。\n"
case "ja-JP":
langHint += "- 请使用日语回答。\n"
case "ko-KR":
langHint += "- 请使用韩语回答。\n"
case "fr-FR":
langHint += "- 请使用法语回答。\n"
case "de-DE":
langHint += "- 请使用德语回答。\n"
case "es-ES":
langHint += "- 请使用西班牙语回答。\n"
default:
langHint += "- 请使用简体中文回答。\n"
}
extras = append(extras, langHint)
}

if summary != nil && summary.Summary != "" {
extras = append(extras, "## 本次对话摘要\n"+summary.Summary)
}
Expand Down
17 changes: 13 additions & 4 deletions internal/agent/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@ import (
)

type PromptUserContext struct {
ID string
Username string
Role string
TimeStr string
ID string
Username string
Role string
TimeStr string
Department string
Position string
Expertise string
Language string
Timezone string
AnswerStyle string
TableFirst bool
CitationStyle string
RoleTemplatePrompt string
}

// Request 描述 Agent 执行请求
Expand Down
3 changes: 2 additions & 1 deletion internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ func NewRouter(
toolTypeService service.ToolTypeService,
toolProviderService service.ToolProviderService,
userToolConfigService service.UserToolConfigService,
prefService service.UserPreferenceService,
) *Router {
return &Router{
userCtrl: user.NewController(userService, adminUserService),
userCtrl: user.NewController(userService, adminUserService, prefService),
authCtrl: auth.NewController(authService, userService),
searchCtrl: search.NewController(searchService),
modelCtrl: model.NewController(modelService),
Expand Down
76 changes: 70 additions & 6 deletions internal/api/v1/user/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,98 @@ import (
type Controller struct {
userService service.UserServiceInterface
adminUserService service.AdminUserServiceInterface
prefService service.UserPreferenceService
}

// NewController 创建用户控制器
func NewController(userService service.UserServiceInterface, adminUserService service.AdminUserServiceInterface) *Controller {
func NewController(
userService service.UserServiceInterface,
adminUserService service.AdminUserServiceInterface,
prefService service.UserPreferenceService,
) *Controller {
return &Controller{
userService: userService,
adminUserService: adminUserService,
prefService: prefService,
}
}

// GetProfile 获取当前用户信息
// GetProfile 获取当前用户完整画像(基本信息 + 偏好)
func (ctrl *Controller) GetProfile(c *gin.Context) {
userID, ok := middleware.CurrentUserID(c)
if !ok {
return
}

user, err := ctrl.userService.GetUserByID(userID)
profile, err := ctrl.userService.GetProfile(userID)
if err != nil {
response.BizError(c, err)
return
}

response.Success(c, ctrl.userService.GetUserResponse(user))
response.Success(c, profile)
}

// UpdateProfile 更新当前用户信息
func (ctrl *Controller) UpdateProfile(c *gin.Context) {
// UpdateProfile 更新当前用户画像字段(部门/职位/擅长/语言/时区)
func (ctrl *Controller) UpdateUserProfile(c *gin.Context) {
userID, ok := middleware.CurrentUserID(c)
if !ok {
return
}

var req request.UpdateProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}

if err := ctrl.userService.UpdateProfile(userID, &req); err != nil {
response.BizError(c, err)
return
}

response.Success(c, nil)
}

// GetPreference 获取当前用户偏好设置
func (ctrl *Controller) GetPreference(c *gin.Context) {
userID, ok := middleware.CurrentUserID(c)
if !ok {
return
}
ctx := c.Request.Context()
pref, err := ctrl.prefService.GetByUserID(ctx, userID)
if err != nil {
response.BizError(c, err)
return
}
response.Success(c, ctrl.prefService.ToDTO(pref))
}

// UpdatePreference 更新当前用户偏好设置(Upsert:不存在则创建)
func (ctrl *Controller) UpdatePreference(c *gin.Context) {
userID, ok := middleware.CurrentUserID(c)
if !ok {
return
}

var req request.UpdateUserPreferenceRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数错误")
return
}

ctx := c.Request.Context()
res, err := ctrl.prefService.Upsert(ctx, userID, &req)
if err != nil {
response.BizError(c, err)
return
}
response.Success(c, res)
}

// UpdateBasicInfo 更新当前用户基本信息(头像/邮箱)
func (ctrl *Controller) UpdateBasicInfo(c *gin.Context) {
userID, ok := middleware.CurrentUserID(c)
if !ok {
return
Expand Down
11 changes: 10 additions & 1 deletion internal/api/v1/user/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,19 @@ func (ctrl *Controller) RegisterRoutes(r *gin.RouterGroup) {
// 普通用户:个人资料管理
userGroup := r.Group("/user")
{
// 完整画像(基本信息 + 偏好)
userGroup.GET("/profile", ctrl.GetProfile)
userGroup.PUT("/profile", ctrl.UpdateProfile)
// 更新用户画像字段(部门/职位/擅长/语言/时区)
userGroup.PUT("/profile/detail", ctrl.UpdateUserProfile)
// 更新基本信息(头像/邮箱)
userGroup.PUT("/profile", ctrl.UpdateBasicInfo)
// 头像上传
userGroup.POST("/avatar", ctrl.UploadAvatar)
// 修改密码
userGroup.POST("/password", ctrl.ChangePassword)
// 偏好设置:查询 + 更新
userGroup.GET("/preference", ctrl.GetPreference)
userGroup.PUT("/preference", ctrl.UpdatePreference)
}

// 管理员:用户管理
Expand Down
12 changes: 9 additions & 3 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ func (a *App) initDependencies() {
dingtalkBindingRepo := repository.NewDingTalkBindingRepository(a.postgresqlDB)
storageQuotaRepo := repository.NewStorageQuotaRepository(a.postgresqlDB)
userRepo := repository.NewUserRepository(a.postgresqlDB)
// 阶段二:用户偏好 Repository
userPreferenceRepo := repository.NewUserPreferenceRepository(a.postgresqlDB)

// 模型配置缓存(10 分钟 TTL)
modelCache := cache.New(a.redis, "model:", 10*time.Minute)
Expand Down Expand Up @@ -326,7 +328,9 @@ func (a *App) initDependencies() {
ai := a.initAgentComponents(toolFactory, documentRepo, chunkRepo, knowledgeBaseRepo)

// 初始化 Service
userSvc := service.NewUserService(userRepo)
// 阶段二:创建 UserPreference Service,作为 UserService 依赖
prefSvc := service.NewUserPreferenceService(userPreferenceRepo)
userSvc := service.NewUserService(userRepo, prefSvc)
adminUserSvc := service.NewAdminUserService(userRepo)
adminSessionSvc := service.NewAdminSessionService(chatSessionRepo, chatMessageRepo)
authSvc := service.NewAuthService(userRepo, userSvc, a.redis)
Expand All @@ -347,7 +351,7 @@ func (a *App) initDependencies() {
syncSvc := service.NewSyncService(knowledgeBaseRepo, syncSourceRepo, syncJobRepo, syncItemRepo, syncedDocumentRepo, dingtalkBindingRepo, documentChunkSvc, textExtractor, dingtalkClient, "data/uploads")
storageSvc := service.NewStorageService(storageQuotaRepo)
contextSvc := service.NewContextService(chatMessageRepo, memoryRepo, summaryRepo)
chatSvc := service.NewChatService(chatSessionRepo, chatMessageRepo, ai.Retriever, modelRepo, userModelConfigRepo, userRepo, userModelCache, ai.AgentEngine, contextSvc)
chatSvc := service.NewChatService(chatSessionRepo, chatMessageRepo, ai.Retriever, modelRepo, userModelConfigRepo, userRepo, userModelCache, ai.AgentEngine, contextSvc, prefSvc)
toolTypeService := service.NewToolTypeService(cachedToolTypeRepo)
toolProviderService := service.NewToolProviderService(toolProviderRepo, cachedToolTypeRepo, toolRegistry)
userToolConfigService := service.NewUserToolConfigService(cachedUserToolConfigRepo, cachedToolTypeRepo, toolProviderRepo, toolRegistry)
Expand All @@ -371,7 +375,9 @@ func (a *App) initDependencies() {
chunkRepo,
toolTypeService,
toolProviderService,
userToolConfigService)
userToolConfigService,
prefSvc,
)
}

// prewarmModelClients 启动时预创建所有已启用系统模型的 LLM 客户端
Expand Down
23 changes: 22 additions & 1 deletion internal/model/dto/request/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,32 @@ type LoginRequest struct {
Captcha string `json:"captcha" binding:"required,len=4"`
}

// UpdateUserRequest 更新用户信息请求
// UpdateUserRequest 更新用户基本信息请求(头像/邮箱)
type UpdateUserRequest struct {
Avatar string `json:"avatar" binding:"omitempty,url,max=255"`
Email string `json:"email" binding:"omitempty,email,max=100"`
}

// UpdateProfileRequest 更新用户画像请求(部门/职位/擅长/语言/时区)
type UpdateProfileRequest struct {
Department string `json:"department" binding:"omitempty,max=100"`
Position string `json:"position" binding:"omitempty,max=100"`
Expertise string `json:"expertise" binding:"omitempty,max=255"`
PreferredLanguage string `json:"preferred_language" binding:"omitempty,oneof=zh-CN en-US ja-JP ko-KR fr-FR de-DE es-ES"`
Timezone string `json:"timezone" binding:"omitempty,max=50"`
}

// UpdateUserPreferenceRequest 更新用户偏好请求
type UpdateUserPreferenceRequest struct {
DefaultModelID *string `json:"default_model_id" binding:"omitempty,max=36"`
PreferredKBIDs []string `json:"preferred_kb_ids" binding:"omitempty,max=50"`
AnswerStyle string `json:"answer_style" binding:"omitempty,oneof=concise balanced detailed step_by_step"`
AutoDeepMode *bool `json:"auto_deep_mode"`
AutoDeepThreshold *int `json:"auto_deep_threshold" binding:"omitempty,min=1,max=5"`
UseMarkdownTable *bool `json:"use_markdown_table"`
CitationStyle string `json:"citation_style" binding:"omitempty,oneof=none section_title doc_title_only"`
}

// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
Expand Down Expand Up @@ -60,3 +80,4 @@ type AdminUpdateUserRequest struct {
type AdminResetPasswordRequest struct {
Password string `json:"password" binding:"required,min=6,max=50"`
}

45 changes: 35 additions & 10 deletions internal/model/dto/response/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,41 @@ package response

import "time"

// UserResponse 用户响应
// UserResponse 用户基本信息响应(含画像字段)
type UserResponse struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Avatar string `json:"avatar"`
Status int `json:"status"`
Role int `json:"role"`
LastModel string `json:"lastModel,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Avatar string `json:"avatar"`
Status int `json:"status"`
Role int `json:"role"`
LastModel string `json:"lastModel,omitempty"`
Department string `json:"department,omitempty"`
Position string `json:"position,omitempty"`
Expertise string `json:"expertise,omitempty"`
PreferredLanguage string `json:"preferred_language,omitempty"`
Timezone string `json:"timezone,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// UserPreferenceResponse 用户偏好响应
type UserPreferenceResponse struct {
UserID string `json:"user_id"`
DefaultModelID string `json:"default_model_id,omitempty"`
PreferredKBIDs []string `json:"preferred_kb_ids,omitempty"`
AnswerStyle string `json:"answer_style"`
AutoDeepMode bool `json:"auto_deep_mode"`
AutoDeepThreshold int `json:"auto_deep_threshold"`
UseMarkdownTable bool `json:"use_markdown_table"`
CitationStyle string `json:"citation_style"`
UpdatedAt string `json:"updated_at"`
}

// ProfileResponse 合并返回 基本信息 + 偏好
type ProfileResponse struct {
User UserResponse `json:"user"`
Preference UserPreferenceResponse `json:"preference"`
}

// AdminUserListItem 管理员用户列表项
Expand All @@ -32,3 +56,4 @@ type LoginResponse struct {
Token string `json:"token"`
User UserResponse `json:"user"`
}

Loading
Loading