Solon v4.0.2

实战:SolonCode CLI 核心代码

</> markdown
2026年6月16日 上午9:25:56

SolonCode CLI 是基于 solon-ai-harness 开发的一个终端编码智能体。

具体参考:SolonCode CLI 的源码(同时也有 soloncode-web, soloncode-desktop 源码可参考)

1、扩展一个新的属性 AgentProperties

package org.noear.solon.codecli.config;

import lombok.Getter;
import lombok.Setter;

import org.noear.solon.ai.chat.ChatConfig;
import org.noear.solon.ai.harness.HarnessExtension;
import org.noear.solon.ai.talents.lsp.LspServerParameters;
import org.noear.solon.codecli.config.entity.ApiSourceDo;
import org.noear.solon.codecli.config.entity.LspServerDo;
import org.noear.solon.codecli.config.entity.McpServerDo;
import org.noear.solon.codecli.config.entity.ModelDo;
import org.noear.solon.core.util.IoUtil;
import org.noear.solon.core.util.ResourceUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.InputStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 代理属性
 */
@Getter
@Setter
public class AgentProperties implements Serializable {
    private static final Logger LOG = LoggerFactory.getLogger(AgentProperties.class);

    public final static String NAME_CONFIG_YML = "config.yml";
    public final static String NAME_SETTINGS_JSON = "settings.json";
    public final static String NAME_AGENTS_MD = "AGENTS.md";


    public final static String X_SESSION_ID = "X-Session-Id";
    public final static String X_SESSION_CWD = "X-Session-Cwd";


    //马具目录
    private final String harnessHome;

    //主代理工具权限
    private List<String> tools = new CopyOnWriteArrayList<>();

    // 禁用工具(全局)
    private List<String> disallowedTools = new CopyOnWriteArrayList<>();

    //最大回合
    private Integer maxTurns;

    //自我反思
    private boolean autoRethink = true;

    private int sessionWindowSize = 8;

    private int summaryWindowSize = 30;
    private int summaryWindowToken = 30000;
    private String summaryModel; //摘要大模型

    private boolean memoryIsolation = true;
    private boolean memoryEnabled = true;

    private boolean sandboxMode = true;
    private boolean sandboxAllowUserHome = true;
    private boolean sandboxSystemRestrict = false;

    private boolean hitlEnabled = false;
    private boolean subagentEnabled = true;
    private boolean bashAsyncEnabled = false;

    private boolean mcpEnabled = true;
    private boolean openApiEnabled = true;
    private boolean lspEnabled = true;

    private String userAgent;
    //defaultModel
    private String defaultModel;

    //api 重试次数
    private int apiRetries = 3;
    //Mcp 重试次数
    private int mcpRetries = 3;
    //模型重试次数
    private int modelRetries = 3;

    //扩展
    private List<HarnessExtension> extensions = new CopyOnWriteArrayList<>();

    //大模型
    private List<ModelDo> models = new CopyOnWriteArrayList<>();
   
    //mcp集
    private Map<String, McpServerDo> mcpServers = new ConcurrentHashMap<>();
    //api集
    private Map<String, ApiSourceDo> apiServers = new ConcurrentHashMap<>();
    //lsp集
    private Map<String, LspServerDo> lspServers = new ConcurrentHashMap<>();

    private boolean thinkPrinted = false;
    private boolean cliPrintSimplified = true;


    public AgentProperties() {
        this.harnessHome =".soloncode/";
    }

    /**
     * 当前目录
     */
    public static String getUserDir() {
        return System.getProperty("user.dir");
    }

    /**
     * 用户主目录
     */
    public static String getUserHome() {
        return System.getProperty("user.home");
    }

    public  String getUserExtensions(){
       return Paths.get(getUserHome(), getHarnessHome(), "extensions").toString();
    }

    public URL getConfigUrl() throws MalformedURLException {
        //1. 资源文件(一般开发时)
        URL tmp = ResourceUtil.getResource(NAME_CONFIG_YML);
        if (tmp != null) {
            return tmp;
        }

        //2. 工作区配置
        Path path = Paths.get(getUserDir(), getHarnessHome(), NAME_CONFIG_YML);
        if (Files.exists(path)) {
            return path.toUri().toURL();
        }

        //3. 用户目录区配置
        path = Paths.get(getUserHome(), getHarnessHome(), NAME_CONFIG_YML);

        if (Files.exists(path)) {
            return path.toUri().toURL();
        }

        //4. 程序边上的配置文件
        tmp = ResourceUtil.getResourceByFile(NAME_CONFIG_YML);
        if (tmp != null) {
            return tmp;
        }

        return null;
    }

    public URL getAgentsUrl() throws MalformedURLException {
        //1. 工作区配置
        Path path = Paths.get(getUserDir(), getHarnessHome(), NAME_AGENTS_MD);
        if (Files.exists(path)) {
            return path.toUri().toURL();
        }

        //2. 用户目录区配置
        path = Paths.get(getUserHome(), getHarnessHome(), NAME_AGENTS_MD);

        if (Files.exists(path)) {
            return path.toUri().toURL();
        }

        //3. 程序边上的配置文件
        URL tmp = ResourceUtil.getResourceByFile(NAME_AGENTS_MD);
        if (tmp != null) {
            return tmp;
        }

        return null;
    }

    public String getAgentsMd() {
        try {
            URL agentsUrl = getAgentsUrl();

            if (agentsUrl != null) {
                try (InputStream is = agentsUrl.openStream()) {
                    String content = IoUtil.transferToString(is, "utf-8").trim();

                    if (content.length() > 10000) { // 例如限制在 1万字符以内
                        LOG.warn("AGENTS.md is too large, truncating...");
                        return content.substring(0, 10000);
                    }
                    return content;
                }
            }
        } catch (Throwable e) {
            LOG.warn("AGENTS.md load failure: {}", e.getMessage(), e);
        }

        return null;
    }


    //---------------

    public List<ModelDo> getModels() {
        return models;
    }


    public boolean isAutoRethink() {
        return autoRethink;
    }


    //--------------------------

    /**
     * 马具主目录
     */
    public final String getHarnessHome() {
        return harnessHome;
    }

    /**
     * 马具会话存放区
     */
    public final String getHarnessSessions() {
        return getHarnessHome() + "sessions/";
    }

    /**
     * 马具技能存放区
     */
    public final String getHarnessSkills() {
        return getHarnessHome() + "skills/";
    }

    /**
     * 马具子代理描述存放区
     */
    public final String getHarnessAgents() {
        return getHarnessHome() + "agents/";
    }

    /**
     * 马具命令描述存放区
     */
    public final String getHarnessCommands() {
        return getHarnessHome() + "commands/";
    }

    /**
     * 马具记忆存放区
     */
    public final String getHarnessMemory() {
        return getHarnessHome() + "memory/";
    }

    /**
     * 马具下载存放区
     */
    public final String getHarnessDownload() {
        return getHarnessHome() + "download/";
    }

    /**
     * 马具连接通道存放区
     */
    public final String getHarnessChannels() {
        return getHarnessHome() + "channels/";
    }
}

2、定制应用启动类

package org.noear.solon.codecli;

import org.noear.solon.Solon;
import org.noear.solon.SolonApp;
import org.noear.solon.codecli.config.AgentFlags;
import org.noear.solon.codecli.config.AgentProperties;
import org.noear.solon.codecli.config.AgentSettings;
import org.noear.solon.core.util.Assert;
import org.noear.solon.scheduling.annotation.EnableScheduling;
import org.noear.solon.web.cors.CrossFilter;
import org.slf4j.bridge.SLF4JBridgeHandler;

import java.net.URL;

/**
 * Cli 应用
 *
 * @author noear
 * @since 3.9.1
 */
@EnableScheduling
public class App {

    public static void main(String[] args) {
        // 1. 移除 JUL 默认的控制台处理器
        SLF4JBridgeHandler.removeHandlersForRootLogger();
        // 2. 添加 SLF4J 处理器
        SLF4JBridgeHandler.install();

        AgentProperties agentProps = new AgentProperties();

        //配置用户扩展目录
        System.setProperty("solon.extend", "!" + agentProps.getUserExtensions());

        Solon.start(App.class, args, app -> {
            initAgentProperties(app, agentProps);
        });
    }

    private static void initAgentProperties(SolonApp app, AgentProperties c) throws Exception {
        //加载配置文件

        URL configUrl = c.getConfigUrl();

        app.cfg().loadAdd(configUrl);

        //获取命令行运行的当前用户工作区
        app.cfg().getProp("soloncode").bindTo(c);

        //兼容旧的模型配置
        if (c.getChatModel() != null) {
            c.getModels().add(c.getChatModel());
        }

        initAgentSettings(app, c);

        //推入容器
        app.context().wrapAndPut(AgentProperties.class, c);

        //-----

        app.enableHttp(false); //默认不启用 http

        String flag = app.cfg().argx().flagAt(0);

        if (AgentFlags.FLAG_SERVE.equals(flag)) {
            enabledWeb(app, c);
            enabledAcp(app, c);
            return;
        }

        if (AgentFlags.FLAG_WEB.equals(flag)) {
            //开始控制台日志
            enabledWeb(app, c);
            return;
        }

        if (AgentFlags.FLAG_ACP.equals(flag)) {
            //开始控制台日志
            enabledAcp(app, c);
            return;
        }
    }

    private static void initAgentSettings(SolonApp app, AgentProperties props) throws Exception {

        AgentSettings agentSettings = AgentSettings.loadFromFile();

        //与 AgentProperties 双向合并
        agentSettings.mergeFrom(props);

        app.context().wrapAndPut(AgentSettings.class, agentSettings);
    }

    private static void enabledWeb(SolonApp app, AgentProperties c) {
        String port = app.cfg().argx().flagAt(1);

        if ("0".equals(port)) {
            port = findAvailablePort();
        }

        if (Assert.isNotEmpty(port) && Assert.isNumber(port)) {
            // soloncode web 1212 //= soloncode web -server.port=1212
            app.cfg().setProperty("server.port", port);
        }

        app.enableHttp(true);
        app.enableWebSocket(true);
        // 允许跨域(桌面端前端通过 localhost 访问 CLI 后端)
        app.router().filter(new CrossFilter().pathPatterns("/ws").allowedOrigins("*"));
        app.router().filter(new CrossFilter().pathPatterns("/chat/**").allowedOrigins("*"));
        app.router().filter(new CrossFilter().pathPatterns("/web/**").allowedOrigins("*"));
    }

    private static void enabledAcp(SolonApp app, AgentProperties c) {
        //开始控制台日志
        if ("stdio".equals(c.getAcpTransport()) == false) {
            app.enableHttp(true);
            app.enableWebSocket(true);
        }
    }

    private static String findAvailablePort() {
        try (java.net.ServerSocket socket = new java.net.ServerSocket(0)) {
            return String.valueOf(socket.getLocalPort());
        } catch (Throwable e) {
            // 如果分配失败,返回一个保底的默认端口
            return null;
        }
    }
}

3、定制配置器

package org.noear.solon.codecli;

import com.agentclientprotocol.sdk.agent.transport.StdioAcpAgentTransport;
import com.agentclientprotocol.sdk.agent.transport.WebSocketSolonAcpAgentTransport;
import com.agentclientprotocol.sdk.spec.AcpAgentTransport;
import io.modelcontextprotocol.json.McpJsonDefaults;
import org.noear.solon.Solon;
import org.noear.solon.ai.agent.AgentSession;
import org.noear.solon.ai.agent.AgentSessionProvider;
import org.noear.solon.ai.agent.session.FileAgentSession;
import org.noear.solon.ai.harness.HarnessEngine;
import org.noear.solon.ai.harness.HarnessExtension;
import org.noear.solon.ai.talents.mount.MountDir;
import org.noear.solon.ai.talents.mount.MountType;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Init;
import org.noear.solon.annotation.Inject;
import org.noear.solon.codecli.command.builtin.*;
import org.noear.solon.codecli.config.AgentFlags;
import org.noear.solon.codecli.config.AgentProperties;
import org.noear.solon.codecli.command.builtin.LoopScheduler;
import org.noear.solon.codecli.channel.Channel;
import org.noear.solon.codecli.config.AgentSettings;
import org.noear.solon.codecli.config.ConfigExtension;
import org.noear.solon.codecli.config.entity.ApiSourceDo;
import org.noear.solon.codecli.config.entity.McpServerDo;
import org.noear.solon.codecli.config.entity.ModelDo;
import org.noear.solon.codecli.config.entity.LspServerDo;
import org.noear.solon.codecli.config.entity.MountDo;
import org.noear.solon.codecli.memory.MemoryFactory;
import org.noear.solon.codecli.portal.*;
import org.noear.solon.codecli.portal.acp.AcpLink;
import org.noear.solon.codecli.portal.cli.CliShell;
import org.noear.solon.codecli.portal.desktop.WsController;
import org.noear.solon.codecli.portal.desktop.WsGate;
import org.noear.solon.codecli.portal.web.WebChannel;
import org.noear.solon.codecli.portal.web.WebController;
import org.noear.solon.codecli.portal.web.WebSettingsController;
import org.noear.solon.codecli.portal.web.WebGate;
import org.noear.solon.codecli.portal.web.WebStreamBuilder;
import org.noear.solon.codecli.portal.desktop.provider.ModelProviderFactory;
import org.noear.solon.core.AppContext;
import org.noear.solon.core.BeanWrap;
import org.noear.solon.core.util.JavaUtil;
import org.noear.solon.core.util.RunUtil;
import org.noear.solon.net.websocket.WebSocketRouter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 *
 * @author noear 2026/4/18 created
 *
 */
@Configuration
public class Configurator {
    private static final Logger LOG = LoggerFactory.getLogger(Configurator.class);

    @Inject
    AppContext appContext;

    @Inject
    HarnessEngine agentRuntime;

    @Inject
    AgentSettings agentSettings;

    @Inject
    ModelProviderFactory modelProviderFactory;

    private LoopScheduler loopScheduler;

    @Bean
    public HarnessEngine agentRuntime(AgentSettings settings) throws Exception {
        String workspace = AgentFlags.getUserDir();
        Map<String, AgentSession> sessionMap = new ConcurrentHashMap<>();

        // 会话数据存到全局目录 ~/.soloncode/sessions/<sessionId>/
        AgentSessionProvider sessionProvider = (sessionId) -> sessionMap.computeIfAbsent(sessionId, key ->
                new FileAgentSession(key, Paths.get(workspace, AgentFlags.getHarnessSessions()).resolve(key).normalize().toFile().toString()));

        HarnessEngine engine = HarnessEngine.of(workspace, AgentFlags.getHarnessHome())
                .userAgent(settings.getGeneral().getUserAgent())
                .systemPrompt(AgentFlags.getAgentsMd())
                .maxTurns(settings.getGeneral().getMaxTurns())
                .autoRethink(settings.getGeneral().getAutoRethink())
                .sessionWindowSize(settings.getGeneral().getSessionWindowSize())
                .sessionProvider(sessionProvider)
                .compressionThreshold(settings.getGeneral().getSummaryWindowSize(), settings.getGeneral().getSummaryWindowToken())
                .compressionModel(settings.getGeneral().getSummaryModel())
                .memoryEnabled(settings.getGeneral().getMemoryEnabled())
                .memoryProvider(new MemoryProvider(agentSettings))
                .sandboxEnabled(settings.getGeneral().getSandboxMode())
                .sandboxAllowUserHome(settings.getGeneral().getSandboxAllowUserHome())
                .sandboxSystemRestrict(settings.getGeneral().getSandboxSystemRestrict())
                .bashAsyncEnabled(settings.getGeneral().getBashAsyncEnabled())
                .subagentEnabled(settings.getGeneral().getSubagentEnabled())
                .hitlEnabled(settings.getGeneral().getHitlEnabled())
                .apiRetries(settings.getGeneral().getApiRetries())
                .modelRetries(settings.getGeneral().getModelRetries())
                .mcpRetries(settings.getGeneral().getModelRetries())
                .toolsAdd(settings.getPermission().getTools())
                .disallowedToolsAdd(settings.getPermission().getDisallowedTools())
                .build();


        engine.setDefaultModel(settings.getDefaultModel());
        for (ModelDo model : agentSettings.getModels().values()) {
            engine.addModel(model);
        }

        for (Map.Entry<String, MountDo> entry : agentSettings.getMountPools().entrySet()) {
            MountDo mount = entry.getValue();
            engine.addMount(MountDir.builder()
                    .alias(entry.getKey())
                    .description(mount.getDescription())
                    .type(mount.getType())
                    .path(mount.getPath())
                    .primary(mount.isPrimary())
                    .enabled(mount.isEnabled())
                    .writeable(mount.isWriteable())
                    .build());
        }

        engine.addMount(MountDir.builder().alias("@global-skills").type(MountType.SKILLS).path("~/" + engine.getHarnessSkills()).primary(true).build());
        engine.addMount(MountDir.builder().alias("@workspace-skills").type(MountType.SKILLS).path("./" + engine.getHarnessSkills()).primary(true).build());

        engine.addMount(MountDir.builder().alias("@global-agents").type(MountType.AGENTS).path("~/" + engine.getHarnessAgents()).primary(true).build());
        engine.addMount(MountDir.builder().alias("@workspace-agents").type(MountType.AGENTS).path("./" + engine.getHarnessAgents()).primary(true).build());

        for (Map.Entry<String, McpServerDo> entry : agentSettings.getMcpServers().entrySet()) {
            engine.addMcpServer(entry.getKey(), entry.getValue());
        }

        for (Map.Entry<String, ApiSourceDo> entry : agentSettings.getApiServers().entrySet()) {
            engine.addApiServer(entry.getValue());
        }

        for (Map.Entry<String, LspServerDo> entry : agentSettings.getLspServers().entrySet()) {
            engine.addLspServer(entry.getKey(), entry.getValue());
        }

        //系统级 LSP 服务器(参考 OpenCode / Claude Code 内置列表,仅注册常见语言)
        addSystemLspServer(engine, agentSettings, "java", Arrays.asList("jdtls"), Arrays.asList(".java"));
        addSystemLspServer(engine, agentSettings, "typescript", Arrays.asList("typescript-language-server", "--stdio"), Arrays.asList(".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"));
        addSystemLspServer(engine, agentSettings, "go", Arrays.asList("gopls"), Arrays.asList(".go"));
        addSystemLspServer(engine, agentSettings, "python", Arrays.asList("pyright-langserver", "--stdio"), Arrays.asList(".py", ".pyi"));
        addSystemLspServer(engine, agentSettings, "rust", Arrays.asList("rust-analyzer"), Arrays.asList(".rs"));
        addSystemLspServer(engine, agentSettings, "c-cpp", Arrays.asList("clangd", "--background-index", "--clang-tidy"), Arrays.asList(".c", ".h", ".cpp", ".hpp", ".cc", ".cxx", ".hxx", ".c++", ".h++", ".hh"));
        addSystemLspServer(engine, agentSettings, "csharp", Arrays.asList("roslyn-language-server", "--stdio", "--autoLoadProjects"), Arrays.asList(".cs", ".csx"));
        addSystemLspServer(engine, agentSettings, "ruby", Arrays.asList("solargraph", "stdio"), Arrays.asList(".rb", ".rake", ".gemspec", ".ru"));
        addSystemLspServer(engine, agentSettings, "php", Arrays.asList("intelephense", "--stdio"), Arrays.asList(".php"));
        addSystemLspServer(engine, agentSettings, "bash", Arrays.asList("bash-language-server", "start"), Arrays.asList(".sh", ".bash", ".zsh", ".ksh"));
        addSystemLspServer(engine, agentSettings, "lua", Arrays.asList("lua-language-server"), Arrays.asList(".lua"));
        addSystemLspServer(engine, agentSettings, "dart", Arrays.asList("dart", "language-server", "--lsp"), Arrays.asList(".dart"));
        addSystemLspServer(engine, agentSettings, "swift", Arrays.asList("sourcekit-lsp"), Arrays.asList(".swift", ".objc", ".objcpp"));
        addSystemLspServer(engine, agentSettings, "kotlin", Arrays.asList("kotlin-language-server"), Arrays.asList(".kt", ".kts"));
        addSystemLspServer(engine, agentSettings, "yaml", Arrays.asList("yaml-language-server", "--stdio"), Arrays.asList(".yaml", ".yml"));

        engine.getCommandRegistry().load(Paths.get(AgentFlags.getUserHome(), engine.getHarnessCommands()));
        engine.getCommandRegistry().load(Paths.get(workspace, engine.getHarnessCommands()));

        engine.getCommandRegistry().register(new ExitCommand());
        engine.getCommandRegistry().register(new ClearCommand());
        engine.getCommandRegistry().register(new ResumeCommand());
        engine.getCommandRegistry().register(new RewindCommand());
        engine.getCommandRegistry().register(new ModelCommand());

        engine.getLspTalent().setEnabled(settings.getGeneral().getLspEnabled());

        // loop scheduler
        this.loopScheduler = new LoopScheduler(engine, AgentFlags.getHarnessLoopWorktrees());
        engine.getCommandRegistry().register(new LoopCommand(loopScheduler));


        engine.addExtension(new ManagerExtension(engine, agentSettings));

        return engine;
    }

    @Init
    public void init() {
        //订阅容器扩展
        appContext.subBeansOfType(HarnessExtension.class, extension -> {
            agentRuntime.addExtension(extension);
        });


        CliShell cliShell = new CliShell(agentRuntime, agentSettings, loopScheduler);
        String flag = Solon.cfg().argx().flagAt(0);

        if (AgentFlags.FLAG_VERSION.equals(flag)) {
            System.out.println(Solon.cfg().appTitle() + " " + AgentFlags.getVersion());
            return;
        }

        checkUpdate();

        //flag
        if (Solon.cfg().argx().flags().size() > 0) {
            if (AgentFlags.FLAG_RUN.equals(flag)) { // java -jar soloncode.jar run '你好' // soloncode run '你好'
                //单次任务态
                String prompt = Solon.cfg().argx().flagAt(1);
                new CliShell(agentRuntime, agentSettings, null).call(prompt);
                Solon.stop();
                return;
            }

            if (AgentFlags.FLAG_SERVE.equals(flag)) { // java -jar soloncode.jar server // soloncode server
                runServe(agentRuntime, agentSettings, cliShell);
                return;
            }

            if (AgentFlags.FLAG_WEB.equals(flag)) { // java -jar soloncode.jar web // soloncode web
                runWeb(agentRuntime, agentSettings, cliShell);
                return;
            }

            if (AgentFlags.FLAG_ACP.equals(flag)) { // java -jar soloncode.jar acp // soloncode acp
                runAcp(agentRuntime, agentSettings, cliShell);
                return;
            }

            //未来可以支持更多控制标记
        }

        //cli - default
        new Thread(cliShell, "CLI-Interactive-Thread").start();
    }

    private void checkUpdate() {
        if (AgentFlags.checkUpdate()) {
            // 使用颜色代码让提示更醒目
            System.out.println("\033[33mDiscover the new version: " + AgentFlags.getLastVersion() + "\033[0m");

            if (JavaUtil.IS_WINDOWS) {
                System.out.println("Update: \033[36mirm https://solon.noear.org/soloncode/setup.ps1 | iex\033[0m");
            } else {
                System.out.println("Update: \033[36mcurl -fsSL https://solon.noear.org/soloncode/setup.sh | bash\033[0m");
            }
            System.out.println();
        }
    }

    private void runServe(HarnessEngine agentRuntime, AgentSettings settings, CliShell cliShell) {
        //serve ws gate
        WebSocketRouter.getInstance().of("/ws", new WsGate(agentRuntime, settings));

        //serve web controller
        BeanWrap webBean = Solon.context().wrapAndPut(WsController.class, new WsController(agentRuntime, modelProviderFactory));
        Solon.app().router().add(webBean);

        //注册第三方渠道(HTTP 端点 + 后台线程)
        WebGate webGate = new WebGate(agentRuntime);
        WebStreamBuilder streamBuilder = new WebStreamBuilder(agentRuntime);
        WebChannel webChannel = new WebChannel(agentRuntime, webGate);
        // 将渠道绑定到 streamBuilder,使 IM 回复能同步
        for (Channel ch : Collections.singletonList(webChannel.getWeChatLink())) {
            streamBuilder.bind(ch);
        }
        streamBuilder.bind(webChannel.getFeishuLink());
        streamBuilder.bind(webChannel.getDingTalkLink());
        BeanWrap channelBean = Solon.context().wrapAndPut(WebChannel.class, webChannel);
        Solon.app().router().add(channelBean);
        RunUtil.async(webChannel);

        //settings controller
        WebSettingsController settingsController = new WebSettingsController(agentRuntime, settings);
        BeanWrap webSettingsController = Solon.context().wrapAndPut(WebSettingsController.class, settingsController);
        Solon.app().router().add(webSettingsController);

        cliShell.printWelcome("Server port: " + Solon.cfg().serverPort());
    }


    private void runWeb(HarnessEngine agentRuntime, AgentSettings settings, CliShell cliShell) {
        //web ws gate
        WebGate webGate = new WebGate(agentRuntime);
        WebSocketRouter.getInstance().of("/web/gate", webGate);

        //web
        BeanWrap webController = Solon.context().wrapAndPut(WebController.class, new WebController(agentRuntime, webGate, loopScheduler));
        Solon.app().router().add(webController);

        WebSettingsController settingsController = new WebSettingsController(agentRuntime, settings);
        BeanWrap webSettingsController = Solon.context().wrapAndPut(WebSettingsController.class, settingsController);
        Solon.app().router().add(webSettingsController);

        BeanWrap webChannel = Solon.context().wrapAndPut(WebChannel.class, new WebChannel(agentRuntime, webGate));
        Solon.app().router().add(webChannel);

        // 启动微信通道
        RunUtil.async((Runnable) webChannel.get());

        // 启动工作区文件变化监听
        try {
            Path workspacePath = Paths.get(agentRuntime.getWorkspace()).toAbsolutePath().normalize();
            WorkspaceWatcher workspaceWatcher = new WorkspaceWatcher(workspacePath);
            workspaceWatcher.addBroadcastHandler(webGate::broadcastRaw);
            workspaceWatcher.start();
        } catch (Exception e) {
            // watcher 启动失败不影响主流程
        }

        if (cliShell == null) {
            return;
        }

        RunUtil.async(() -> {
            try {
                Thread.sleep(500);

                String url = "http://localhost:" + Solon.cfg().serverPort() + "/";

                if (JavaUtil.IS_WINDOWS) {
                    new ProcessBuilder("cmd", "/c", "start", url.replace("&", "^&")).start();
                } else if (JavaUtil.IS_MAC) {
                    new ProcessBuilder("open", url).start();
                } else {
                    new ProcessBuilder("xdg-open", url).start();
                }

                if (cliShell != null) {
                    cliShell.printWelcome("Web interface: " + url);
                }
            } catch (Throwable e) { // 使用 Throwable 捕获更全面
                LOG.warn("Failed to open browser: {}", e.getMessage());
            }
        });
    }


    private void runAcp(HarnessEngine agentRuntime, AgentSettings settings, CliShell cliShell) {
        AcpAgentTransport agentTransport = new StdioAcpAgentTransport();

        new AcpLink(agentRuntime, agentTransport, settings).run();

//        if (cliShell == null) {
//            return;
//        }

        //不能有打印
        //cliShell.printWelcome("Acp interface: stdio");
    }

    /**
     * 添加系统级 LSP 服务器(如果用户未自定义同名配置,则注册)
     */
    private void addSystemLspServer(HarnessEngine engine, AgentSettings settings, String name, List<String> command, List<String> extensions) {
        // 如果用户已自定义同名配置,跳过系统级注册
        if (settings.getLspServers().containsKey(name)) {
            return;
        }

        LspServerDo lspServer = new LspServerDo();
        lspServer.setCommand(command);
        lspServer.setExtensions(extensions);
        lspServer.setEnabled(false); // 默认禁用,用户按需启用
        lspServer.setScope(AgentFlags.SCOPE_LOCAL);

        // 注册到引擎(不启用不会真正加载,仅作为可选项)
        engine.addLspServer(name, lspServer);

        // 同步到 settings 以便前端展示
        settings.getLspServers().put(name, lspServer);
    }
}