package main import ( "context" "fmt" "os" "os/signal" "syscall" "time" "laodingbot/internal/agent" "laodingbot/internal/config" "laodingbot/internal/knowledge" "laodingbot/internal/llm" "laodingbot/internal/logger" "laodingbot/internal/memory" "laodingbot/internal/runtimews" "laodingbot/internal/toolhost" "laodingbot/internal/tools" "laodingbot/internal/transport/feishu" "laodingbot/internal/transport/telegram" ) func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() isToolhostChild := len(os.Args) > 1 && os.Args[1] == "--toolhost" workspaceRoot, err := runtimews.PrepareFromEnv() if err != nil { panic(fmt.Sprintf("prepare runtime workspace failed: %v", err)) } cfg, err := config.Load() if err != nil { panic(fmt.Sprintf("load config failed: %v", err)) } if isToolhostChild { if err := toolhost.RunChild(ctx, cfg, nil); err != nil && ctx.Err() == nil { panic(fmt.Sprintf("toolhost child failed: %v", err)) } return } appLogger, err := logger.New(cfg.LogLevel) if err != nil { panic(fmt.Sprintf("init logger failed: %v", err)) } appLogger = appLogger.WithComponent("main") appLogger.Infof("config loaded; channel=%s, log_level=%s workspace=%s", cfg.MessageChannel, cfg.LogLevel, workspaceRoot) store, err := memory.NewSQLiteStore(cfg.SQLitePath, appLogger.WithComponent("memory")) if err != nil { appLogger.Errorf("init memory store failed: %v", err) panic(err) } defer store.Close() toolRegistry := tools.NewRegistry(appLogger.WithComponent("tools.registry")) exePath, err := os.Executable() if err != nil { appLogger.Errorf("resolve executable path failed: %v", err) panic(err) } tc, err := toolhost.NewClient(toolhost.ClientConfig{ ExecutablePath: exePath, Args: []string{"--toolhost"}, WorkDir: ".", CallTimeout: time.Duration(cfg.ToolCallTimeoutSec) * time.Second, HeartbeatInterval: 5 * time.Second, MaxConcurrency: 4, }, appLogger.WithComponent("toolhost.client")) if err != nil { appLogger.Errorf("init toolhost client failed: %v", err) panic(err) } defer tc.Close() listCtx, cancel := context.WithTimeout(ctx, 10*time.Second) toolInfos, err := tc.ToolList(listCtx) cancel() if err != nil { appLogger.Errorf("toolhost list failed: %v", err) panic(err) } if len(toolInfos) == 0 { panic("toolhost returned empty tool list") } for _, info := range toolInfos { toolRegistry.Register(toolhost.NewRemoteTool(info.Name, info.Description, time.Duration(cfg.ToolCallTimeoutSec)*time.Second, tc)) } soul, err := knowledge.LoadSoul(cfg.SoulPath) if err != nil { appLogger.Errorf("load soul failed path=%s err=%v", cfg.SoulPath, err) panic(err) } skillSet, err := knowledge.LoadSkillSet(cfg.SkillsDir) if err != nil { appLogger.Errorf("load skill set failed dir=%s err=%v", cfg.SkillsDir, err) panic(err) } appLogger.Infof("knowledge loaded soul_path=%s skills_dir=%s", cfg.SoulPath, cfg.SkillsDir) llmClient := llm.NewOpenAICompatibleClient(cfg.LLM, appLogger.WithComponent("llm")) engine := agent.NewOrchestrator( llmClient, store, toolRegistry, soul, skillSet, cfg.SkillsDir, cfg.ReactMaxSteps, cfg.EnableCapabilityGap, cfg.AutoSkillDir, cfg.GapDraftTriggerCount, time.Duration(cfg.GapClusterLookbackHours)*time.Hour, appLogger.WithComponent("agent"), ) appLogger.Infof("LaodingBot started, channel=%s", cfg.MessageChannel) if err := runMessageChannel(ctx, cfg, engine, appLogger); err != nil && ctx.Err() == nil { appLogger.Errorf("message channel run failed: %v", err) panic(err) } appLogger.Infof("LaodingBot stopped") } func runMessageChannel(ctx context.Context, cfg config.Config, engine *agent.Orchestrator, lg *logger.Logger) error { switch cfg.MessageChannel { case "telegram": tg, err := telegram.NewBot(cfg.Telegram.Token, cfg.Telegram.PollTimeoutSeconds, lg.WithComponent("transport.telegram")) if err != nil { return fmt.Errorf("init telegram bot failed: %w", err) } lg.Infof("starting telegram transport") return tg.Run(ctx, func(ctx context.Context, msg telegram.IncomingMessage) (string, error) { return engine.HandleMessage(ctx, msg.ChatID, msg.UserID, msg.Text) }) case "feishu": fs, err := feishu.NewBot( cfg.Feishu.AppID, cfg.Feishu.AppSecret, cfg.Feishu.VerifyToken, cfg.Feishu.ListenAddr, cfg.Feishu.EventPath, lg.WithComponent("transport.feishu"), ) if err != nil { return fmt.Errorf("init feishu bot failed: %w", err) } lg.Infof("starting feishu transport") return fs.Run(ctx, func(ctx context.Context, msg feishu.IncomingMessage) (string, error) { return engine.HandleMessage(ctx, msg.ChatID, msg.UserID, msg.Text) }) default: return fmt.Errorf("unsupported message channel: %s", cfg.MessageChannel) } }