实战:SolonCode CLI 核心代码
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);
}
}