You've already forked agentic-coding-workflow
feat(app): wire chat module + LocalFS storage + LLM registry into app.New
This commit is contained in:
@@ -44,3 +44,31 @@ workspace:
|
|||||||
# git CLI 配置(默认走 PATH 里的 git)
|
# git CLI 配置(默认走 PATH 里的 git)
|
||||||
git:
|
git:
|
||||||
binary: "git"
|
binary: "git"
|
||||||
|
|
||||||
|
# Chat 模块(多模态对话 + LLM endpoints)
|
||||||
|
chat:
|
||||||
|
attachment:
|
||||||
|
# 单文件上限(MB)
|
||||||
|
max_file_size_mb: 20
|
||||||
|
# 单条消息中所有附件总和上限(MB)
|
||||||
|
max_message_size_mb: 50
|
||||||
|
# 允许的 MIME 类型白名单。"text/*" 通配任意 text/ 子类型。
|
||||||
|
mime_whitelist:
|
||||||
|
- image/png
|
||||||
|
- image/jpeg
|
||||||
|
- image/webp
|
||||||
|
- image/gif
|
||||||
|
- application/pdf
|
||||||
|
- text/*
|
||||||
|
stream:
|
||||||
|
# SSE ring buffer 在流结束后保留多久,方便前端断线重连。
|
||||||
|
ring_buffer_retention_seconds: 300
|
||||||
|
history:
|
||||||
|
# token 预算的安全余量百分比(5.0 表示在 (context_window - max_output) 上再保留 5%)
|
||||||
|
safety_margin_pct: 5.0
|
||||||
|
|
||||||
|
# 附件存储驱动(当前仅支持 localfs)。生产建议挂载到独立卷。
|
||||||
|
storage:
|
||||||
|
driver: "localfs"
|
||||||
|
localfs:
|
||||||
|
base_path: "./data/uploads"
|
||||||
|
|||||||
@@ -30,12 +30,15 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
||||||
|
"github.com/yan1h/agent-coding-workflow/internal/chat"
|
||||||
"github.com/yan1h/agent-coding-workflow/internal/config"
|
"github.com/yan1h/agent-coding-workflow/internal/config"
|
||||||
"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
|
"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
|
||||||
"github.com/yan1h/agent-coding-workflow/internal/infra/db"
|
"github.com/yan1h/agent-coding-workflow/internal/infra/db"
|
||||||
"github.com/yan1h/agent-coding-workflow/internal/infra/git"
|
"github.com/yan1h/agent-coding-workflow/internal/infra/git"
|
||||||
|
"github.com/yan1h/agent-coding-workflow/internal/infra/llm"
|
||||||
"github.com/yan1h/agent-coding-workflow/internal/infra/logger"
|
"github.com/yan1h/agent-coding-workflow/internal/infra/logger"
|
||||||
"github.com/yan1h/agent-coding-workflow/internal/infra/notify"
|
"github.com/yan1h/agent-coding-workflow/internal/infra/notify"
|
||||||
|
"github.com/yan1h/agent-coding-workflow/internal/infra/storage"
|
||||||
"github.com/yan1h/agent-coding-workflow/internal/project"
|
"github.com/yan1h/agent-coding-workflow/internal/project"
|
||||||
httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http"
|
httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http"
|
||||||
"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
|
"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
|
||||||
@@ -173,6 +176,61 @@ func New(ctx context.Context, cfg *config.Config, dist embed.FS) (*App, error) {
|
|||||||
gopSvc := workspace.NewGitOpsService(wsRepo, auditRec, gitr, pa, credLoader)
|
gopSvc := workspace.NewGitOpsService(wsRepo, auditRec, gitr, pa, credLoader)
|
||||||
workspace.NewHandler(wsSvc, wtSvc, gopSvc, userSvc, userAdminAdapter{svc: userSvc}).Mount(r)
|
workspace.NewHandler(wsSvc, wtSvc, gopSvc, userSvc, userAdminAdapter{svc: userSvc}).Mount(r)
|
||||||
|
|
||||||
|
// ===== chat 模块装配 =====
|
||||||
|
// 依赖图:
|
||||||
|
// pgx.Pool ─ chat.Repository ─┐
|
||||||
|
// ├─ EndpointLoader ─ llm.Registry ─┐
|
||||||
|
// crypto.Encryptor ───────────┘ │
|
||||||
|
// │
|
||||||
|
// storage.LocalFS ─ Storage ──┐ │
|
||||||
|
// ├─ MessageService ─ Handler ──────┤
|
||||||
|
// notify.Dispatcher ─ StreamerHub ─ ConversationService ─ Hub ───┘
|
||||||
|
//
|
||||||
|
// 启动期把卡死的 pending 消息复位为 error,避免行永远卡住。
|
||||||
|
chatRepo := chat.NewRepository(pool)
|
||||||
|
if err := chatRepo.MarkPendingAsErrorOnStartup(ctx); err != nil {
|
||||||
|
log.Warn("mark chat pending messages as error on startup", "err", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointLoader := chat.NewEndpointLoader(chatRepo, enc)
|
||||||
|
llmRegistry := llm.NewRegistry(endpointLoader)
|
||||||
|
|
||||||
|
var attachStorage storage.Storage
|
||||||
|
switch cfg.Storage.Driver {
|
||||||
|
case "localfs", "":
|
||||||
|
s, err := storage.NewLocalFS(cfg.Storage.LocalFS.BasePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("storage localfs: %w", err)
|
||||||
|
}
|
||||||
|
attachStorage = s
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported storage driver %q", cfg.Storage.Driver)
|
||||||
|
}
|
||||||
|
|
||||||
|
// safety_margin_pct 配置为百分比(5.0 表示 5%),构造器期望小数(0.05),故 /100。
|
||||||
|
safetyFraction := cfg.Chat.History.SafetyMarginPct / 100
|
||||||
|
retention := time.Duration(cfg.Chat.Stream.RingBufferRetentionSeconds) * time.Second
|
||||||
|
|
||||||
|
chatEpSvc := chat.NewEndpointService(chatRepo, enc, llmRegistry, auditRec, log)
|
||||||
|
chatTplSvc := chat.NewTemplateService(chatRepo, auditRec, log)
|
||||||
|
chatConvSvc := chat.NewConversationService(chatRepo, llmRegistry, auditRec, log)
|
||||||
|
chatHub := chat.NewStreamerHub(chatRepo, llmRegistry, notifyDispatcher, auditRec,
|
||||||
|
chatConvSvc, log, retention, safetyFraction)
|
||||||
|
chatMsgSvc := chat.NewMessageService(chatRepo, attachStorage, chatHub, auditRec, log,
|
||||||
|
chat.MessageServiceConfig{
|
||||||
|
MimeWhitelist: cfg.Chat.Attachment.MimeWhitelist,
|
||||||
|
MaxFileBytes: cfg.Chat.Attachment.MaxFileSizeMB << 20,
|
||||||
|
MaxMsgBytes: cfg.Chat.Attachment.MaxMessageSizeMB << 20,
|
||||||
|
SafetyPct: safetyFraction,
|
||||||
|
})
|
||||||
|
chatUsageSvc := chat.NewUsageService(chatRepo)
|
||||||
|
chatUploader := chat.NewUploader(chatRepo, attachStorage,
|
||||||
|
cfg.Chat.Attachment.MimeWhitelist,
|
||||||
|
cfg.Chat.Attachment.MaxFileSizeMB<<20,
|
||||||
|
)
|
||||||
|
chat.NewHandler(chatConvSvc, chatMsgSvc, chatTplSvc, chatEpSvc, chatUsageSvc,
|
||||||
|
chatUploader, userSvc, userAdminAdapter{svc: userSvc}).Mount(r)
|
||||||
|
|
||||||
if cfg.Bootstrap.AdminEmail != "" && cfg.Bootstrap.AdminPassword != "" {
|
if cfg.Bootstrap.AdminEmail != "" && cfg.Bootstrap.AdminPassword != "" {
|
||||||
if u, err := userSvc.Bootstrap(ctx, cfg.Bootstrap.AdminEmail, cfg.Bootstrap.AdminPassword); err != nil {
|
if u, err := userSvc.Bootstrap(ctx, cfg.Bootstrap.AdminEmail, cfg.Bootstrap.AdminPassword); err != nil {
|
||||||
log.Warn("bootstrap admin failed", "err", err.Error())
|
log.Warn("bootstrap admin failed", "err", err.Error())
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ type Config struct {
|
|||||||
Bootstrap BootstrapConfig `mapstructure:"bootstrap"`
|
Bootstrap BootstrapConfig `mapstructure:"bootstrap"`
|
||||||
Workspace Workspace `mapstructure:"workspace"`
|
Workspace Workspace `mapstructure:"workspace"`
|
||||||
Git Git `mapstructure:"git"`
|
Git Git `mapstructure:"git"`
|
||||||
|
Chat Chat `mapstructure:"chat"`
|
||||||
|
Storage Storage `mapstructure:"storage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPConfig 描述 HTTP 服务监听参数。
|
// HTTPConfig 描述 HTTP 服务监听参数。
|
||||||
@@ -70,6 +72,41 @@ type Git struct {
|
|||||||
Binary string `mapstructure:"binary"`
|
Binary string `mapstructure:"binary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chat 配置 chat 模块的附件、流式、历史与定时清理策略。
|
||||||
|
type Chat struct {
|
||||||
|
Attachment ChatAttachment `mapstructure:"attachment"`
|
||||||
|
Stream ChatStream `mapstructure:"stream"`
|
||||||
|
History ChatHistory `mapstructure:"history"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatAttachment 控制单文件 / 单消息上限与允许的 MIME 白名单。
|
||||||
|
type ChatAttachment struct {
|
||||||
|
MaxFileSizeMB int64 `mapstructure:"max_file_size_mb"`
|
||||||
|
MaxMessageSizeMB int64 `mapstructure:"max_message_size_mb"`
|
||||||
|
MimeWhitelist []string `mapstructure:"mime_whitelist"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatStream 控制 SSE ring buffer 保留时长与取消宽限期。
|
||||||
|
type ChatStream struct {
|
||||||
|
RingBufferRetentionSeconds int `mapstructure:"ring_buffer_retention_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatHistory 控制 token 预算的安全余量百分比(如 5 表示保留 5% 余量)。
|
||||||
|
type ChatHistory struct {
|
||||||
|
SafetyMarginPct float64 `mapstructure:"safety_margin_pct"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage 选择附件落盘驱动与对应参数。
|
||||||
|
type Storage struct {
|
||||||
|
Driver string `mapstructure:"driver"` // 当前仅支持 "localfs"
|
||||||
|
LocalFS StorageLocalFS `mapstructure:"localfs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageLocalFS 是 driver=localfs 时的根目录。
|
||||||
|
type StorageLocalFS struct {
|
||||||
|
BasePath string `mapstructure:"base_path"`
|
||||||
|
}
|
||||||
|
|
||||||
// Load 从(按优先级递增)默认值 → 配置文件(如有)→ 环境变量 加载配置。
|
// Load 从(按优先级递增)默认值 → 配置文件(如有)→ 环境变量 加载配置。
|
||||||
// configFile 可为空字符串,仅用环境变量。
|
// configFile 可为空字符串,仅用环境变量。
|
||||||
func Load(configFile string) (*Config, error) {
|
func Load(configFile string) (*Config, error) {
|
||||||
@@ -85,6 +122,16 @@ func Load(configFile string) (*Config, error) {
|
|||||||
v.SetDefault("workspace.push_timeout", "5m")
|
v.SetDefault("workspace.push_timeout", "5m")
|
||||||
v.SetDefault("workspace.ops_timeout", "60s")
|
v.SetDefault("workspace.ops_timeout", "60s")
|
||||||
v.SetDefault("git.binary", "git")
|
v.SetDefault("git.binary", "git")
|
||||||
|
v.SetDefault("chat.attachment.max_file_size_mb", 20)
|
||||||
|
v.SetDefault("chat.attachment.max_message_size_mb", 50)
|
||||||
|
v.SetDefault("chat.attachment.mime_whitelist", []string{
|
||||||
|
"image/png", "image/jpeg", "image/webp", "image/gif",
|
||||||
|
"application/pdf", "text/*",
|
||||||
|
})
|
||||||
|
v.SetDefault("chat.stream.ring_buffer_retention_seconds", 300)
|
||||||
|
v.SetDefault("chat.history.safety_margin_pct", 5.0)
|
||||||
|
v.SetDefault("storage.driver", "localfs")
|
||||||
|
v.SetDefault("storage.localfs.base_path", "./data/uploads")
|
||||||
|
|
||||||
if configFile != "" {
|
if configFile != "" {
|
||||||
v.SetConfigFile(configFile)
|
v.SetConfigFile(configFile)
|
||||||
|
|||||||
Reference in New Issue
Block a user