This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new eba97f08 feat: add claude-code and qoder agent engines with streaming 
prompt enhance (#954)
eba97f08 is described below

commit eba97f085cded8fcf9fa364b4b84d29cd5727c46
Author: lizhimins <[email protected]>
AuthorDate: Tue Aug 4 15:49:26 2026 +0800

    feat: add claude-code and qoder agent engines with streaming prompt enhance 
(#954)
    
    Introduce an AgentProvider gateway abstraction with two CLI subprocess
    implementations: claude-code (claude -p, streaming via --output-format
    stream-json --verbose --include-partial-messages) and qoder (qodercli
    -p). Credentials reach the subprocess only through the environment
    (RMQ_LLM_TOKEN / RMQ_ANTHROPIC_BASE_URL); the server image now ships
    Node.js plus both CLIs. The gateway resolves the engine per request
    (user preference stored in the browser) falling back to the global LLM
    setting, and when enhance is requested it rewrites the user prompt into
    a structured RocketMQ-expert prompt first, streaming the rewrite to the
    client as incremental enhance SSE events that render as a collapsible
    chain-of-thought block. Also: home submit sends instantly from the AI
    page, the home model list is fixed to the seven gateway models with
    qwen3.8-max recommended, the existing Prompt 增强 toolbar button toggles
    enhancement, the home tab order becomes AI chat first, and vitest runs
    with 32 workers.
---
 deploy/docker-compose.yml                          |    2 +
 server/Dockerfile                                  |    9 +
 .../ai/{AiCommandDTO.java => AgentProvider.java}   |   32 +-
 .../studio/ops/ai/AgentProviderRegistry.java       |   48 +
 .../rocketmq/studio/ops/ai/AiCommandDTO.java       |    1 +
 .../org/apache/rocketmq/studio/ops/ai/ChatDTO.java |    2 +
 .../studio/ops/ai/ClaudeCodeAgentProvider.java     |  206 ++++
 .../rocketmq/studio/ops/ai/CliAgentProvider.java   |  115 +++
 .../rocketmq/studio/ops/ai/LlmConfigService.java   |   69 +-
 .../apache/rocketmq/studio/ops/ai/LlmConfigVO.java |   21 +-
 .../ai/{AiCommandDTO.java => LlmProperties.java}   |   25 +-
 .../studio/ops/ai/OpenAiCompatibleLlmGateway.java  |  121 ++-
 .../rocketmq/studio/ops/ai/QoderAgentProvider.java |   60 ++
 .../studio/settings/GeneralSettingsUpdateDTO.java  |    2 +
 .../studio/settings/GeneralSettingsVO.java         |    1 +
 server/src/main/resources/application.yml          |    3 +
 .../src/main/resources/prompts/enhance-prompt.txt  |    8 +
 .../studio/ops/ai/LlmConfigServiceTest.java        |   28 +-
 .../ops/ai/OpenAiCompatibleLlmGatewayTest.java     |    2 +-
 web/src/api/ai.ts                                  |   29 +-
 web/src/api/llm.ts                                 |    1 +
 web/src/pages/ai/chatDraft.ts                      |    2 +
 web/src/pages/ai/index.tsx                         |  304 +++---
 web/src/pages/home/__tests__/HomePage.test.tsx     |   29 +-
 web/src/pages/home/index.tsx                       |  113 ++-
 web/src/pages/studio/LlmSettings.tsx               | 1008 +++++---------------
 .../__tests__/LlmSettingsAsyncState.test.tsx       |  219 -----
 .../__tests__/LlmSettingsPage.test.tsx}            |   83 +-
 web/src/pages/studio/llmModelOptions.ts            |   10 +-
 .../ai/chatDraft.ts => stores/engineStore.ts}      |   32 +-
 web/vite.config.ts                                 |    3 +
 31 files changed, 1295 insertions(+), 1293 deletions(-)

diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
index a3537356..e7883c06 100644
--- a/deploy/docker-compose.yml
+++ b/deploy/docker-compose.yml
@@ -45,6 +45,8 @@ services:
       SPRING_DATASOURCE_USERNAME: root
       SPRING_DATASOURCE_PASSWORD: studio123
       STUDIO_ROCKETMQ_NAMESRV_ADDR: 
${STUDIO_ROCKETMQ_NAMESRV_ADDR:-nameserver:9876}
+      RMQ_LLM_TOKEN: ${RMQ_LLM_TOKEN:-}
+      RMQ_ANTHROPIC_BASE_URL: ${RMQ_ANTHROPIC_BASE_URL:-}
     expose:
       - "8888"
     extra_hosts:
diff --git a/server/Dockerfile b/server/Dockerfile
index d11ef498..08814ff1 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -12,6 +12,15 @@ RUN mvn package -DskipTests
 # Stage 2: Runtime
 FROM alibabadragonwell/dragonwell:21
 WORKDIR /app
+# Node.js + agent CLIs for the claude-code / qoder agent providers.
+RUN curl -fsSL 
https://npmmirror.com/mirrors/node/v20.19.2/node-v20.19.2-linux-x64.tar.gz | 
tar xz -C /opt \
+    && ln -s /opt/node-v20.19.2-linux-x64/bin/node /usr/local/bin/node \
+    && ln -s /opt/node-v20.19.2-linux-x64/bin/npm /usr/local/bin/npm \
+    && ln -s /opt/node-v20.19.2-linux-x64/bin/npx /usr/local/bin/npx \
+    && npm config set registry https://registry.npmmirror.com \
+    && npm install -g @anthropic-ai/claude-code @qoder-ai/qodercli \
+    && ln -s /opt/node-v20.19.2-linux-x64/bin/claude /usr/local/bin/claude \
+    && ln -s /opt/node-v20.19.2-linux-x64/bin/qodercli /usr/local/bin/qodercli
 COPY --from=build /app/target/*.jar app.jar
 EXPOSE 8888
 ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiCommandDTO.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AgentProvider.java
similarity index 54%
copy from 
server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiCommandDTO.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/ops/ai/AgentProvider.java
index 7c4ce1e0..45e182fe 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiCommandDTO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AgentProvider.java
@@ -16,22 +16,22 @@
  */
 package org.apache.rocketmq.studio.ops.ai;
 
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+/**
+ * Gateway abstraction for agent runtimes. Implementations spawn the vendor CLI
+ * (claude code / qoder) as a subprocess and pass credentials through the child
+ * process environment — never through argv or persisted storage.
+ */
+public interface AgentProvider {
+
+    String engine();
+
+    boolean available();
 
-import java.util.Map;
+    String complete(LlmConfigVO config, String prompt, String modelOverride);
 
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class AiCommandDTO {
-    private String command;
-    private String mode;
-    private String model;
-    private String conversationId;
-    private String prompt;
-    private Map<String, Object> context;
+    /** Streams completion tokens; default falls back to a single chunk via 
complete(). */
+    default void stream(LlmConfigVO config, String prompt, String 
modelOverride,
+                        java.util.function.Consumer<String> tokenConsumer) {
+        tokenConsumer.accept(complete(config, prompt, modelOverride));
+    }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AgentProviderRegistry.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AgentProviderRegistry.java
new file mode 100644
index 00000000..091f6f24
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AgentProviderRegistry.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai;
+
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Selects the CLI agent provider (claude code / qoder) for the configured 
engine.
+ */
+@Component
+public class AgentProviderRegistry {
+
+    private final Map<String, AgentProvider> providers;
+
+    public AgentProviderRegistry(List<AgentProvider> providerList) {
+        this.providers = providerList.stream()
+                .collect(Collectors.toMap(AgentProvider::engine, 
Function.identity()));
+    }
+
+    public AgentProvider forEngine(String engine) {
+        AgentProvider provider = providers.get(engine == null ? "" : 
engine.trim().toLowerCase());
+        if (provider == null) {
+            throw new LlmGatewayException(400, "llm.config.unsupported_engine",
+                    "Agent engine is not supported: " + engine,
+                    "Use one of: claude-code, qoder.");
+        }
+        return provider;
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiCommandDTO.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiCommandDTO.java
index 7c4ce1e0..e93b76a8 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiCommandDTO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiCommandDTO.java
@@ -31,6 +31,7 @@ public class AiCommandDTO {
     private String command;
     private String mode;
     private String model;
+    private String engine;
     private String conversationId;
     private String prompt;
     private Map<String, Object> context;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ChatDTO.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ChatDTO.java
index 7b15e33c..79d7005d 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ChatDTO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ChatDTO.java
@@ -29,5 +29,7 @@ public class ChatDTO {
     private String message;
     private String mode;
     private String model;
+    private String engine;
+    private boolean enhance;
     private String conversationId;
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ClaudeCodeAgentProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ClaudeCodeAgentProvider.java
new file mode 100644
index 00000000..ea7e8a52
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ClaudeCodeAgentProvider.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+/**
+ * Claude Code CLI provider ({@code claude -p}). Credentials are passed to the
+ * child process exclusively via ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL env
+ * entries, mirroring the mq-hub adapter's env-injection approach.
+ */
+@Slf4j
+@Component
+public class ClaudeCodeAgentProvider extends CliAgentProvider {
+
+    public static final String ENGINE = "claude-code";
+    private static final String COMPATIBLE_MODE_SUFFIX = "/compatible-mode/v1";
+    private static final String ANTHROPIC_APP_SUFFIX = "/apps/anthropic";
+    private static final long STREAM_TIMEOUT_SECONDS = 300;
+
+    private final LlmProperties llmProperties;
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    public ClaudeCodeAgentProvider(LlmProperties llmProperties) {
+        this.llmProperties = llmProperties;
+    }
+
+    @Override
+    public String engine() {
+        return ENGINE;
+    }
+
+    @Override
+    protected String binaryName() {
+        return "claude";
+    }
+
+    @Override
+    protected List<String> buildCommand(LlmConfigVO config, String prompt, 
String modelOverride) {
+        List<String> command = new ArrayList<>(List.of("claude", "-p", prompt 
== null ? "" : prompt));
+        String model = StringUtils.hasText(modelOverride) ? 
modelOverride.trim() : config.getModel();
+        if (StringUtils.hasText(model)) {
+            command.add("--model");
+            command.add(model.trim());
+        }
+        return command;
+    }
+
+    @Override
+    public void stream(LlmConfigVO config, String prompt, String 
modelOverride, Consumer<String> tokenConsumer) {
+        if (!available()) {
+            throw new LlmGatewayException(503, "llm.provider.cli_missing",
+                    binaryName() + " CLI is not installed in the server 
runtime",
+                    "Install the CLI into the rocketmq-server image or switch 
the engine to HTTP.");
+        }
+        List<String> command = buildCommand(config, prompt, modelOverride);
+        command.add("--output-format");
+        command.add("stream-json");
+        command.add("--verbose");
+        command.add("--include-partial-messages");
+
+        ProcessBuilder builder = new ProcessBuilder(command);
+        builder.environment().putAll(childEnv(config));
+        builder.redirectErrorStream(false);
+        try {
+            Process process = builder.start();
+            AtomicBoolean emitted = new AtomicBoolean(false);
+            StringBuilder resultText = new StringBuilder();
+            try (BufferedReader reader = new BufferedReader(
+                    new InputStreamReader(process.getInputStream(), 
StandardCharsets.UTF_8))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    parseStreamLine(line, tokenConsumer, emitted, resultText);
+                }
+            }
+            boolean finished = process.waitFor(STREAM_TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+            if (!finished) {
+                process.destroyForcibly();
+                throw new LlmGatewayException(504, "llm.provider.timeout",
+                        binaryName() + " CLI stream timed out after " + 
STREAM_TIMEOUT_SECONDS + "s",
+                        "Retry with a shorter prompt or check the gateway 
latency.");
+            }
+            if (process.exitValue() != 0 && !emitted.get()) {
+                String stderr = new 
String(process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
+                throw new LlmGatewayException(502, "llm.provider.cli_error",
+                        binaryName() + " CLI failed: " + 
(StringUtils.hasText(stderr) ? stderr.trim() : "unknown error"),
+                        "Check the provider credentials, base URL and model 
name.");
+            }
+            if (!emitted.get() && resultText.length() > 0) {
+                tokenConsumer.accept(resultText.toString());
+            }
+        } catch (IOException exception) {
+            throw new LlmGatewayException(502, "llm.provider.io_error",
+                    "Failed to execute " + binaryName() + " CLI",
+                    "Check that the CLI binary is installed and executable.", 
exception);
+        } catch (InterruptedException exception) {
+            Thread.currentThread().interrupt();
+            throw new LlmGatewayException(502, "llm.provider.interrupted",
+                    binaryName() + " CLI execution was interrupted", "Retry 
the request.", exception);
+        }
+    }
+
+    /** Parses one stream-json line: emits text deltas, records the final 
result. */
+    private void parseStreamLine(String line, Consumer<String> tokenConsumer,
+                                 AtomicBoolean emitted, StringBuilder 
resultText) {
+        if (!StringUtils.hasText(line)) {
+            return;
+        }
+        try {
+            JsonNode node = objectMapper.readTree(line);
+            String type = node.path("type").asText("");
+            if ("stream_event".equals(type)) {
+                JsonNode event = node.path("event");
+                if 
("content_block_delta".equals(event.path("type").asText(""))) {
+                    String delta = event.path("delta").path("text").asText("");
+                    if (!delta.isEmpty()) {
+                        emitted.set(true);
+                        tokenConsumer.accept(delta);
+                    }
+                }
+            } else if ("assistant".equals(type)) {
+                for (JsonNode block : node.path("message").path("content")) {
+                    if ("text".equals(block.path("type").asText(""))) {
+                        String text = block.path("text").asText("");
+                        if (!text.isEmpty() && !emitted.get()) {
+                            // No partial messages arrived; use the full 
assistant text once.
+                            resultText.setLength(0);
+                            resultText.append(text);
+                        }
+                    }
+                }
+            } else if ("result".equals(type)) {
+                String result = node.path("result").asText("");
+                if (!result.isEmpty() && !emitted.get()) {
+                    resultText.setLength(0);
+                    resultText.append(result);
+                }
+            }
+        } catch (IOException exception) {
+            log.debug("Skipping unparseable claude stream line: {}", 
line.length() > 200 ? line.substring(0, 200) : line);
+        }
+    }
+
+    @Override
+    protected Map<String, String> childEnv(LlmConfigVO config) {
+        Map<String, String> env = new HashMap<>();
+        String token = StringUtils.hasText(config.getApiKey()) ? 
config.getApiKey().trim() : null;
+        if (token != null) {
+            env.put("ANTHROPIC_AUTH_TOKEN", token);
+        }
+        String baseUrl = anthropicBase(config);
+        if (StringUtils.hasText(baseUrl)) {
+            env.put("ANTHROPIC_BASE_URL", baseUrl);
+        }
+        return env;
+    }
+
+    private String anthropicBase(LlmConfigVO config) {
+        if (llmProperties != null && 
StringUtils.hasText(llmProperties.getAnthropicBaseUrl())) {
+            return llmProperties.getAnthropicBaseUrl().trim();
+        }
+        String apiBase = config.getApiBase();
+        if (!StringUtils.hasText(apiBase)) {
+            return null;
+        }
+        String normalized = apiBase.trim();
+        while (normalized.endsWith("/")) {
+            normalized = normalized.substring(0, normalized.length() - 1);
+        }
+        if (normalized.endsWith(COMPATIBLE_MODE_SUFFIX)) {
+            return normalized.substring(0, normalized.length() - 
COMPATIBLE_MODE_SUFFIX.length())
+                    + ANTHROPIC_APP_SUFFIX;
+        }
+        return normalized;
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/CliAgentProvider.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/CliAgentProvider.java
new file mode 100644
index 00000000..ccec3ec4
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/CliAgentProvider.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Base class for CLI-based agent providers: spawns the vendor CLI in a
+ * subprocess, injects credentials through the child environment and captures
+ * stdout as the completion result.
+ */
+@Slf4j
+public abstract class CliAgentProvider implements AgentProvider {
+
+    private static final long TIMEOUT_SECONDS = 180;
+
+    protected abstract List<String> buildCommand(LlmConfigVO config, String 
prompt, String modelOverride);
+
+    protected abstract Map<String, String> childEnv(LlmConfigVO config);
+
+    protected abstract String binaryName();
+
+    @Override
+    public boolean available() {
+        try {
+            Process process = new ProcessBuilder("sh", "-c", "command -v " + 
binaryName())
+                    .redirectErrorStream(true)
+                    .start();
+            boolean finished = process.waitFor(5, TimeUnit.SECONDS);
+            return finished && process.exitValue() == 0;
+        } catch (IOException | InterruptedException exception) {
+            if (exception instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+            return false;
+        }
+    }
+
+    @Override
+    public String complete(LlmConfigVO config, String prompt, String 
modelOverride) {
+        if (!available()) {
+            throw new LlmGatewayException(503, "llm.provider.cli_missing",
+                    binaryName() + " CLI is not installed in the server 
runtime",
+                    "Install the CLI into the rocketmq-server image or switch 
the engine to HTTP.");
+        }
+        List<String> command = buildCommand(config, prompt, modelOverride);
+        ProcessBuilder builder = new ProcessBuilder(command);
+        Map<String, String> env = builder.environment();
+        env.putAll(childEnv(config));
+        builder.redirectErrorStream(false);
+        try {
+            Process process = builder.start();
+            String stdout = new 
String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+            String stderr = new 
String(process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
+            boolean finished = process.waitFor(TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+            if (!finished) {
+                process.destroyForcibly();
+                throw new LlmGatewayException(504, "llm.provider.timeout",
+                        binaryName() + " CLI timed out after " + 
TIMEOUT_SECONDS + "s",
+                        "Retry with a shorter prompt or check the gateway 
latency.");
+            }
+            if (process.exitValue() != 0) {
+                log.warn("{} CLI failed rc={}, stderr={}", binaryName(), 
process.exitValue(), abbreviate(stderr));
+                throw new LlmGatewayException(502, "llm.provider.cli_error",
+                        binaryName() + " CLI failed: " + abbreviate(
+                                StringUtils.hasText(stderr) ? stderr : stdout),
+                        "Check the provider credentials, base URL and model 
name.");
+            }
+            String result = stdout.trim();
+            if (!StringUtils.hasText(result)) {
+                throw new LlmGatewayException(502, 
"llm.provider.empty_completion",
+                        binaryName() + " CLI returned an empty completion",
+                        "Check the selected model and provider response.");
+            }
+            return result;
+        } catch (IOException exception) {
+            throw new LlmGatewayException(502, "llm.provider.io_error",
+                    "Failed to execute " + binaryName() + " CLI",
+                    "Check that the CLI binary is installed and executable.", 
exception);
+        } catch (InterruptedException exception) {
+            Thread.currentThread().interrupt();
+            throw new LlmGatewayException(502, "llm.provider.interrupted",
+                    binaryName() + " CLI execution was interrupted", "Retry 
the request.", exception);
+        }
+    }
+
+    private String abbreviate(String value) {
+        if (value == null) {
+            return "";
+        }
+        String trimmed = value.trim();
+        return trimmed.length() <= 500 ? trimmed : trimmed.substring(0, 500) + 
"...";
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigService.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigService.java
index f696a1be..75be1dbc 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigService.java
@@ -21,6 +21,7 @@ import org.apache.rocketmq.studio.settings.GeneralSettingsVO;
 import org.apache.rocketmq.studio.settings.SettingsService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import 
org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.stereotype.Service;
 
 import java.net.URI;
@@ -31,10 +32,12 @@ import java.util.Map;
 
 @Service
 @RequiredArgsConstructor
+@EnableConfigurationProperties(LlmProperties.class)
 @Slf4j
 public class LlmConfigService {
 
     private static final String OPENAI = "openai";
+    private static final String DEFAULT_PROVIDER = "tongyi";
     private static final String CHAT_COMPLETIONS_PATH = "/chat/completions";
     private static final int DEFAULT_MAX_TOKENS = 4096;
     private static final double DEFAULT_TEMPERATURE = 0.7;
@@ -51,9 +54,13 @@ public class LlmConfigService {
                     new LlmModelItemVO("deepseek-chat", "DeepSeek Chat"),
                     new LlmModelItemVO("deepseek-reasoner", "DeepSeek 
Reasoner")),
             "tongyi", List.of(
-                    new LlmModelItemVO("qwen-max", "Qwen Max"),
-                    new LlmModelItemVO("qwen-plus", "Qwen Plus"),
-                    new LlmModelItemVO("qwen-turbo", "Qwen Turbo")),
+                    new LlmModelItemVO("qwen3.8-max", "qwen3.8-max"),
+                    new LlmModelItemVO("qwen3.7-max", "qwen3.7-max"),
+                    new LlmModelItemVO("qwen3.7-plus", "qwen3.7-plus"),
+                    new LlmModelItemVO("deepseek-v4-pro", "deepseek-v4-pro"),
+                    new LlmModelItemVO("deepseek-v4-flash", 
"deepseek-v4-flash"),
+                    new LlmModelItemVO("MiniMax-M2.5", "MiniMax-M2.5"),
+                    new LlmModelItemVO("glm-5.2", "glm-5.2")),
             "ollama", List.of(
                     new LlmModelItemVO("llama3", "Llama 3"),
                     new LlmModelItemVO("mistral", "Mistral"),
@@ -65,13 +72,19 @@ public class LlmConfigService {
 
     private final SettingsService settingsService;
     private final OpenAiCompatibleLlmClient llmClient;
+    private final LlmProperties llmProperties;
     private LlmConfigVO overrides;
 
     public synchronized LlmConfigVO getConfig() {
-        if (overrides != null) {
-            return copy(overrides);
+        LlmConfigVO config = overrides != null
+                ? copy(overrides)
+                : fromGeneralSettings(settingsService.getGeneralSettings());
+        String token = envToken();
+        if (!isBlank(token)) {
+            config.setApiKey(token.trim());
+            config.setEnabled(true);
         }
-        return fromGeneralSettings(settingsService.getGeneralSettings());
+        return config;
     }
 
     public synchronized void saveConfig(LlmConfigVO config) {
@@ -81,6 +94,10 @@ public class LlmConfigService {
             throw new LlmGatewayException(400, validation.getCode(), 
validation.getErrMsg(), validation.getHint());
         }
         GeneralSettingsVO current = settingsService.getGeneralSettings();
+        // The env-injected token is authoritative at runtime but must never 
be persisted.
+        String persistedApiKey = isBlank(envToken())
+                ? normalized.getApiKey()
+                : defaultString(current.getApiKey(), "");
         GeneralSettingsVO updated = GeneralSettingsVO.builder()
                 .theme(current.getTheme())
                 .compact(current.isCompact())
@@ -89,7 +106,8 @@ public class LlmConfigService {
                 .sessionTimeout(current.getSessionTimeout())
                 .requireLogin(current.isRequireLogin())
                 .llmProvider(normalized.getProvider())
-                .apiKey(normalized.getApiKey())
+                .llmEngine(normalized.getEngine())
+                .apiKey(persistedApiKey)
                 .model(normalized.getModel())
                 .baseUrl(normalized.getApiBase())
                 .build();
@@ -135,7 +153,8 @@ public class LlmConfigService {
                     "LLM temperature is out of range",
                     "Set temperature to a value between 0 and 2.");
         }
-        boolean keyRequired = !"ollama".equals(provider);
+        boolean keyRequired = !"ollama".equals(provider)
+                && 
LlmConfigVO.ENGINE_HTTP.equalsIgnoreCase(normalized.normalizeEngine());
         if (keyRequired && isBlank(normalized.getApiKey())) {
             return LlmOperationResultVO.failure(
                     "llm.config.missing_api_key",
@@ -160,6 +179,11 @@ public class LlmConfigService {
     public synchronized LlmModelsResultVO listModels() {
         LlmConfigVO config = getConfig();
         String provider = config.getProvider();
+        // The token-plan gateway model set is curated locally; do not query 
the gateway.
+        if (DEFAULT_PROVIDER.equals(provider)) {
+            return new LlmModelsResultVO(0, 
PROVIDER_MODELS.get(DEFAULT_PROVIDER),
+                    LlmModelsResultVO.SOURCE_BUILTIN, null, null, null);
+        }
         if (config.isEnabled() && llmClient.supports(config)) {
             try {
                 List<LlmModelItemVO> models = llmClient.listModels(config);
@@ -179,11 +203,13 @@ public class LlmConfigService {
 
     private LlmConfigVO fromGeneralSettings(GeneralSettingsVO settings) {
         String provider = normalizeProvider(settings.getLlmProvider());
-        String apiKey = defaultString(settings.getApiKey(), "");
+        // Token injected via RMQ_LLM_TOKEN takes precedence over the key 
saved in settings.
+        String apiKey = defaultString(envToken(), 
defaultString(settings.getApiKey(), ""));
         String apiBase = normalizeApiBase(defaultString(settings.getBaseUrl(), 
defaultApiBase(provider)));
         String model = defaultString(settings.getModel(), 
defaultModel(provider));
         return LlmConfigVO.builder()
                 .provider(provider)
+                .engine(normalizeEngine(settings.getLlmEngine()))
                 .apiKey(apiKey)
                 .apiBase(apiBase)
                 .model(model)
@@ -195,10 +221,15 @@ public class LlmConfigService {
                 .build();
     }
 
+    private String envToken() {
+        return llmProperties == null ? null : llmProperties.getToken();
+    }
+
     private LlmConfigVO normalize(LlmConfigVO config) {
         String provider = normalizeProvider(config == null ? null : 
config.getProvider());
         return LlmConfigVO.builder()
                 .provider(provider)
+                .engine(normalizeEngine(config == null ? null : 
config.getEngine()))
                 .apiKey(defaultString(config == null ? null : 
config.getApiKey(), ""))
                 .apiBase(normalizeApiBase(defaultString(config == null ? null 
: config.getApiBase(),
                         defaultApiBase(provider))))
@@ -223,7 +254,13 @@ public class LlmConfigService {
             return normalized;
         }
         if (isBlank(normalized.getApiKey())) {
-            normalized.setApiKey(defaultString(getConfig().getApiKey(), ""));
+            // Fall back to the key stored in settings only; the env-injected 
token
+            // must never be persisted into the settings table.
+            String storedKey = 
settingsService.getGeneralSettings().getApiKey();
+            if (!isBlank(envToken())) {
+                storedKey = defaultString(storedKey, envToken());
+            }
+            normalized.setApiKey(defaultString(storedKey, ""));
         }
         return normalized;
     }
@@ -239,8 +276,16 @@ public class LlmConfigService {
     }
 
     private String normalizeProvider(String provider) {
-        String normalized = defaultString(provider, 
OPENAI).toLowerCase(Locale.ROOT);
-        return PROVIDER_MODELS.containsKey(normalized) ? normalized : OPENAI;
+        String normalized = defaultString(provider, 
DEFAULT_PROVIDER).toLowerCase(Locale.ROOT);
+        return PROVIDER_MODELS.containsKey(normalized) ? normalized : 
DEFAULT_PROVIDER;
+    }
+
+    private String normalizeEngine(String engine) {
+        String normalized = defaultString(engine, 
LlmConfigVO.ENGINE_CLAUDE_CODE).toLowerCase(Locale.ROOT);
+        return switch (normalized) {
+            case LlmConfigVO.ENGINE_HTTP, LlmConfigVO.ENGINE_CLAUDE_CODE, 
LlmConfigVO.ENGINE_QODER -> normalized;
+            default -> LlmConfigVO.ENGINE_HTTP;
+        };
     }
 
     private String defaultModel(String provider) {
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigVO.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigVO.java
index 80151114..23c222c8 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigVO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigVO.java
@@ -30,7 +30,12 @@ import org.springframework.util.StringUtils;
 @NoArgsConstructor
 @AllArgsConstructor
 public class LlmConfigVO {
+    public static final String ENGINE_HTTP = "http";
+    public static final String ENGINE_CLAUDE_CODE = "claude-code";
+    public static final String ENGINE_QODER = "qoder";
+
     private String provider;
+    private String engine;
     @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
     @ToString.Exclude
     private String apiKey;
@@ -50,10 +55,18 @@ public class LlmConfigVO {
 
     @JsonProperty(value = "ready", access = JsonProperty.Access.READ_ONLY)
     public boolean isReady() {
+        if (!enabled || !StringUtils.hasText(model)) {
+            return false;
+        }
+        if (!ENGINE_HTTP.equalsIgnoreCase(normalizeEngine())) {
+            // CLI engines authenticate through the subprocess environment.
+            return true;
+        }
         boolean keyRequired = !"ollama".equalsIgnoreCase(provider);
-        return enabled
-                && StringUtils.hasText(apiBase)
-                && StringUtils.hasText(model)
-                && (!keyRequired || StringUtils.hasText(apiKey));
+        return StringUtils.hasText(apiBase) && (!keyRequired || 
StringUtils.hasText(apiKey));
+    }
+
+    public String normalizeEngine() {
+        return StringUtils.hasText(engine) ? engine.trim().toLowerCase() : 
ENGINE_HTTP;
     }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiCommandDTO.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmProperties.java
similarity index 67%
copy from 
server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiCommandDTO.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmProperties.java
index 7c4ce1e0..62dde47e 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiCommandDTO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmProperties.java
@@ -16,22 +16,17 @@
  */
 package org.apache.rocketmq.studio.ops.ai;
 
-import lombok.AllArgsConstructor;
-import lombok.Builder;
 import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.util.Map;
+import org.springframework.boot.context.properties.ConfigurationProperties;
 
+/**
+ * LLM credentials supplied through the environment. The token is a secret and
+ * must never be persisted or logged; it is injected as RMQ_LLM_TOKEN into the
+ * container and bound here at startup.
+ */
 @Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class AiCommandDTO {
-    private String command;
-    private String mode;
-    private String model;
-    private String conversationId;
-    private String prompt;
-    private Map<String, Object> context;
+@ConfigurationProperties(prefix = "studio.llm")
+public class LlmProperties {
+    private String token;
+    private String anthropicBaseUrl;
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/OpenAiCompatibleLlmGateway.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/OpenAiCompatibleLlmGateway.java
index 993f9900..2ea5eda4 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/OpenAiCompatibleLlmGateway.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/OpenAiCompatibleLlmGateway.java
@@ -27,6 +27,8 @@ import org.springframework.util.StringUtils;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -39,6 +41,7 @@ public class OpenAiCompatibleLlmGateway implements LlmGateway 
{
 
     private final LlmConfigService configService;
     private final OpenAiCompatibleLlmClient llmClient;
+    private final AgentProviderRegistry agentProviders;
     private final ObjectMapper objectMapper;
     private final ExecutorService executor = Executors.newCachedThreadPool();
 
@@ -48,6 +51,12 @@ public class OpenAiCompatibleLlmGateway implements 
LlmGateway {
         if (!hasRunnableConfig(config)) {
             return errorEmitter(incompleteConfigException());
         }
+        String engine = resolveEngine(request == null ? null : 
request.getEngine(), config);
+        if (isCliEngine(engine)) {
+            SseEmitter emitter = new SseEmitter(300_000L);
+            executor.execute(() -> runCliChat(request, config, engine, 
emitter));
+            return emitter;
+        }
         if (!llmClient.supports(config)) {
             return errorEmitter(unsupportedProviderException());
         }
@@ -63,10 +72,116 @@ public class OpenAiCompatibleLlmGateway implements 
LlmGateway {
         if (!hasRunnableConfig(config)) {
             throw incompleteConfigException();
         }
+        String engine = resolveEngine(command == null ? null : 
command.getEngine(), config);
+        if (isCliEngine(engine)) {
+            AgentProvider provider = agentProviders.forEngine(engine);
+            return provider.complete(config, commandPrompt(command), command 
== null ? null : command.getModel());
+        }
         assertSupported(config);
         return llmClient.complete(config, commandPrompt(command), command == 
null ? null : command.getModel());
     }
 
+    /** Request-level engine (per-user preference) overrides the global 
config. */
+    private String resolveEngine(String requestEngine, LlmConfigVO config) {
+        String engine = StringUtils.hasText(requestEngine) ? 
requestEngine.trim().toLowerCase() : null;
+        if (engine == null) {
+            return config.normalizeEngine();
+        }
+        return switch (engine) {
+            case LlmConfigVO.ENGINE_HTTP, LlmConfigVO.ENGINE_CLAUDE_CODE, 
LlmConfigVO.ENGINE_QODER -> engine;
+            default -> config.normalizeEngine();
+        };
+    }
+
+    private boolean isCliEngine(String engine) {
+        return !LlmConfigVO.ENGINE_HTTP.equalsIgnoreCase(engine);
+    }
+
+    private void runCliChat(ChatDTO request, LlmConfigVO config, String 
engine, SseEmitter emitter) {
+        try {
+            AgentProvider provider = agentProviders.forEngine(engine);
+            String prompt = request == null ? null : request.getMessage();
+            if (request != null && request.isEnhance() && 
StringUtils.hasText(prompt)) {
+                prompt = enhanceAndEmit(config, provider, prompt, emitter);
+            }
+            String result = provider.complete(config, prompt, request == null 
? null : request.getModel());
+            sendMessage(emitter, result);
+            emitter.send(SseEmitter.event().name("done").data("[DONE]"));
+            emitter.complete();
+        } catch (LlmGatewayException exception) {
+            log.warn("Agent CLI chat failed: {}", exception.getCode(), 
exception);
+            sendError(emitter, exception);
+        } catch (Exception exception) {
+            log.error("Failed to run agent CLI chat", exception);
+            sendError(emitter, new LlmGatewayException(502, 
"llm.gateway_error",
+                    "Failed to run agent CLI chat", "Check the agent provider 
configuration and retry.", exception));
+        }
+    }
+
+    private static final String ENHANCE_PROMPT_TEMPLATE = 
loadEnhancePromptTemplate();
+
+    private static String loadEnhancePromptTemplate() {
+        try (InputStream in = OpenAiCompatibleLlmGateway.class
+                .getResourceAsStream("/prompts/enhance-prompt.txt")) {
+            if (in == null) {
+                throw new IllegalStateException("prompts/enhance-prompt.txt is 
missing on classpath");
+            }
+            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+        } catch (IOException exception) {
+            throw new IllegalStateException("Failed to load 
prompts/enhance-prompt.txt", exception);
+        }
+    }
+
+    /** Streams the prompt rewrite via the given provider, emitting per-chunk 
"enhance" SSE events. */
+    private String enhanceAndEmit(LlmConfigVO config, AgentProvider provider, 
String rawPrompt, SseEmitter emitter)
+            throws IOException {
+        String metaPrompt = ENHANCE_PROMPT_TEMPLATE.formatted(rawPrompt);
+        StringBuilder accumulated = new StringBuilder();
+        provider.stream(config, metaPrompt, null, chunk -> {
+            accumulated.append(chunk);
+            emitEnhanceChunk(emitter, chunk);
+        });
+        String enhanced = cleanEnhancedPrompt(accumulated.toString());
+        return StringUtils.hasText(enhanced) ? enhanced : rawPrompt;
+    }
+
+    private String enhanceAndEmitHttp(LlmConfigVO config, String rawPrompt, 
SseEmitter emitter)
+            throws IOException {
+        String metaPrompt = ENHANCE_PROMPT_TEMPLATE.formatted(rawPrompt);
+        StringBuilder accumulated = new StringBuilder();
+        llmClient.stream(config, metaPrompt, null, chunk -> {
+            accumulated.append(chunk);
+            emitEnhanceChunk(emitter, chunk);
+        });
+        String enhanced = cleanEnhancedPrompt(accumulated.toString());
+        return StringUtils.hasText(enhanced) ? enhanced : rawPrompt;
+    }
+
+    private void emitEnhanceChunk(SseEmitter emitter, String chunk) {
+        if (!StringUtils.hasText(chunk)) {
+            return;
+        }
+        try {
+            emitter.send(SseEmitter.event()
+                    .name("enhance")
+                    .data(objectMapper.writeValueAsString(Map.of("delta", 
chunk))));
+        } catch (IOException exception) {
+            throw new LlmGatewayException(500, "llm.stream.emit_failed",
+                    "Failed to send enhance event", "Retry the chat request.", 
exception);
+        }
+    }
+
+    private String cleanEnhancedPrompt(String enhanced) {
+        if (enhanced == null) {
+            return "";
+        }
+        String cleaned = enhanced.trim();
+        if (cleaned.startsWith("```")) {
+            cleaned = cleaned.replaceAll("(?s)^```[a-zA-Z]*\\s*", 
"").replaceAll("(?s)```\\s*$", "").trim();
+        }
+        return cleaned;
+    }
+
     @PreDestroy
     void destroy() {
         executor.shutdownNow();
@@ -74,7 +189,11 @@ public class OpenAiCompatibleLlmGateway implements 
LlmGateway {
 
     private void streamChat(ChatDTO request, LlmConfigVO config, SseEmitter 
emitter) {
         try {
-            llmClient.stream(config, request == null ? null : 
request.getMessage(),
+            String prompt = request == null ? null : request.getMessage();
+            if (request != null && request.isEnhance() && 
StringUtils.hasText(prompt)) {
+                prompt = enhanceAndEmitHttp(config, prompt, emitter);
+            }
+            llmClient.stream(config, prompt,
                     request == null ? null : request.getModel(),
                     token -> sendMessage(emitter, token));
             emitter.send(SseEmitter.event().name("done").data("[DONE]"));
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/QoderAgentProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/QoderAgentProvider.java
new file mode 100644
index 00000000..a0b95445
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/QoderAgentProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.ops.ai;
+
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Qoder CLI provider ({@code qodercli -p}). qodercli authenticates through its
+ * own login state in the runtime image, so no credentials are injected here.
+ */
+@Component
+public class QoderAgentProvider extends CliAgentProvider {
+
+    public static final String ENGINE = "qoder";
+
+    @Override
+    public String engine() {
+        return ENGINE;
+    }
+
+    @Override
+    protected String binaryName() {
+        return "qodercli";
+    }
+
+    @Override
+    protected List<String> buildCommand(LlmConfigVO config, String prompt, 
String modelOverride) {
+        List<String> command = new ArrayList<>(List.of("qodercli", "-p", 
prompt == null ? "" : prompt));
+        if (StringUtils.hasText(modelOverride)) {
+            command.add("-m");
+            command.add(modelOverride.trim());
+        }
+        return command;
+    }
+
+    @Override
+    protected Map<String, String> childEnv(LlmConfigVO config) {
+        return Collections.emptyMap();
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/settings/GeneralSettingsUpdateDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/settings/GeneralSettingsUpdateDTO.java
index 7ccefabd..64086905 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/settings/GeneralSettingsUpdateDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/settings/GeneralSettingsUpdateDTO.java
@@ -47,6 +47,7 @@ public class GeneralSettingsUpdateDTO {
     private Boolean requireLogin;
     @NotBlank
     private String llmProvider;
+    private String llmEngine;
     @ToString.Exclude
     private String apiKey;
     private boolean clearApiKey;
@@ -64,6 +65,7 @@ public class GeneralSettingsUpdateDTO {
                 .sessionTimeout(sessionTimeout)
                 .requireLogin(requireLogin)
                 .llmProvider(llmProvider)
+                .llmEngine(llmEngine)
                 .apiKey(apiKey)
                 .clearApiKey(clearApiKey)
                 .model(model)
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/settings/GeneralSettingsVO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/settings/GeneralSettingsVO.java
index 1aade5ae..56075443 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/settings/GeneralSettingsVO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/settings/GeneralSettingsVO.java
@@ -36,6 +36,7 @@ public class GeneralSettingsVO {
     private int sessionTimeout;
     private boolean requireLogin;
     private String llmProvider;
+    private String llmEngine;
     @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
     @ToString.Exclude
     private String apiKey;
diff --git a/server/src/main/resources/application.yml 
b/server/src/main/resources/application.yml
index bb6f7b69..ce721da0 100644
--- a/server/src/main/resources/application.yml
+++ b/server/src/main/resources/application.yml
@@ -44,3 +44,6 @@ studio:
       bearer-token: ${STUDIO_METRICS_PROMETHEUS_BEARER_TOKEN:}
   rocketmq:
     namesrv-addr: ${STUDIO_ROCKETMQ_NAMESRV_ADDR:}
+  llm:
+    token: ${RMQ_LLM_TOKEN:}
+    anthropic-base-url: ${RMQ_ANTHROPIC_BASE_URL:}
diff --git a/server/src/main/resources/prompts/enhance-prompt.txt 
b/server/src/main/resources/prompts/enhance-prompt.txt
new file mode 100644
index 00000000..79f8f204
--- /dev/null
+++ b/server/src/main/resources/prompts/enhance-prompt.txt
@@ -0,0 +1,8 @@
+你是 RocketMQ 消息队列领域的资深运维与架构专家。请把下面用户的原始提问改写成一个结构化、可直接交给大模型回答的高质量 prompt。
+要求:
+1. 忠实保留用户原意,不要直接回答问题本身;
+2. 补充角色设定(RocketMQ 专家)、必要的分析/排查框架,以及输出要求(结论先行、步骤可执行、必要时列出命令或指标);
+3. 只输出改写后的 prompt 文本,不要输出任何解释、标题、引号或代码块标记。
+
+用户原始提问:
+%s
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/LlmConfigServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/LlmConfigServiceTest.java
index 2b40af91..4695f941 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/LlmConfigServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/LlmConfigServiceTest.java
@@ -55,7 +55,7 @@ class LlmConfigServiceTest {
                 .baseUrl("https://api.openai.com/v1";)
                 .build());
         llmClient = mock(OpenAiCompatibleLlmClient.class);
-        llmConfigService = new LlmConfigService(settingsService, llmClient);
+        llmConfigService = new LlmConfigService(settingsService, llmClient, 
new LlmProperties());
     }
 
     @Test
@@ -70,6 +70,29 @@ class LlmConfigServiceTest {
         assertThat(config.isReady()).isTrue();
     }
 
+    @Test
+    void envTokenShouldOverrideApiKeyAtRuntimeButNeverBePersisted() {
+        LlmProperties properties = new LlmProperties();
+        properties.setToken("env-token");
+        LlmConfigService service = new LlmConfigService(settingsService, 
llmClient, properties);
+
+        LlmConfigVO config = service.getConfig();
+        assertThat(config.getApiKey()).isEqualTo("env-token");
+        assertThat(config.isEnabled()).isTrue();
+
+        service.saveConfig(LlmConfigVO.builder()
+                .provider("openai")
+                .apiBase("https://api.openai.com/v1";)
+                .model("gpt-4o")
+                .build());
+
+        ArgumentCaptor<GeneralSettingsVO> captor = 
ArgumentCaptor.forClass(GeneralSettingsVO.class);
+        verify(settingsService).saveGeneralSettings(captor.capture());
+        assertThat(captor.getValue().getApiKey()).isEqualTo("sk-test");
+
+        assertThat(service.getConfig().getApiKey()).isEqualTo("env-token");
+    }
+
     @Test
     void configToStringShouldNotExposeApiKey() {
         LlmConfigVO config = LlmConfigVO.builder()
@@ -248,6 +271,7 @@ class LlmConfigServiceTest {
 
         LlmOperationResultVO result = 
llmConfigService.testConfig(LlmConfigVO.builder()
                 .provider("openai")
+                .engine("http")
                 .apiKey("")
                 .model("gpt-4o")
                 .build());
@@ -375,7 +399,7 @@ class LlmConfigServiceTest {
         LlmModelsResultVO result = llmConfigService.listModels();
 
         assertThat(result.getStatus()).isZero();
-        assertThat(result.getData()).extracting("id").contains("qwen-max", 
"qwen-plus");
+        assertThat(result.getData()).extracting("id").contains("qwen3.8-max", 
"qwen3.7-plus");
     }
 
     @Test
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/OpenAiCompatibleLlmGatewayTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/OpenAiCompatibleLlmGatewayTest.java
index 8b8c5c1f..afff18ff 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/OpenAiCompatibleLlmGatewayTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/OpenAiCompatibleLlmGatewayTest.java
@@ -37,7 +37,7 @@ class OpenAiCompatibleLlmGatewayTest {
     private final LlmConfigService configService = 
mock(LlmConfigService.class);
     private final OpenAiCompatibleLlmClient llmClient = 
mock(OpenAiCompatibleLlmClient.class);
     private final OpenAiCompatibleLlmGateway gateway = new 
OpenAiCompatibleLlmGateway(
-            configService, llmClient, new ObjectMapper());
+            configService, llmClient, new 
AgentProviderRegistry(java.util.List.of()), new ObjectMapper());
 
     @Test
     void chatShouldRejectIncompleteConfigWithoutCallingProvider() {
diff --git a/web/src/api/ai.ts b/web/src/api/ai.ts
index 81f40d08..541cc310 100644
--- a/web/src/api/ai.ts
+++ b/web/src/api/ai.ts
@@ -35,6 +35,7 @@ export interface AiExecuteRequest {
   message: string;
   mode: string;
   model: string;
+  engine?: string;
   tools?: string[];
 }
 
@@ -42,6 +43,8 @@ export interface AiChatRequest {
   message: string;
   mode: string;
   model: string;
+  engine?: string;
+  enhance?: boolean;
   conversationId?: string;
 }
 
@@ -105,13 +108,32 @@ function parseStreamError(payload: string): AiStreamError 
{
   }
 }
 
-function emitEvent(event: string, onChunk: (text: string) => void): boolean {
+function emitEvent(
+  event: string,
+  onChunk: (text: string) => void,
+  onEnhance?: (prompt: string) => void,
+): boolean {
   const payload = getEventData(event);
   if (payload === null) return false;
   if (payload === '[DONE]') return true;
   if (getEventName(event) === 'error') {
     throw parseStreamError(payload);
   }
+  if (getEventName(event) === 'enhance') {
+    try {
+      const parsed = JSON.parse(payload) as { delta?: unknown; prompt?: 
unknown };
+      const delta =
+        typeof parsed.delta === 'string'
+          ? parsed.delta
+          : typeof parsed.prompt === 'string'
+            ? parsed.prompt
+            : null;
+      if (delta !== null) onEnhance?.(delta);
+    } catch {
+      onEnhance?.(payload);
+    }
+    return false;
+  }
 
   try {
     const parsed = JSON.parse(payload) as AiStreamPayload;
@@ -134,6 +156,7 @@ export async function chatStream(
   data: AiChatRequest,
   onChunk: (text: string) => void,
   signal?: AbortSignal,
+  onEnhance?: (prompt: string) => void,
 ) {
   const response = await fetch('/api/ai/chat', {
     method: 'POST',
@@ -162,13 +185,13 @@ export async function chatStream(
     while (boundary) {
       const event = buffer.slice(0, boundary.index);
       buffer = buffer.slice(boundary.index + boundary.length);
-      if (emitEvent(event, onChunk)) return;
+      if (emitEvent(event, onChunk, onEnhance)) return;
       boundary = getEventBoundary(buffer);
     }
   }
 
   buffer += decoder.decode();
-  if (buffer && emitEvent(buffer, onChunk)) return;
+  if (buffer && emitEvent(buffer, onChunk, onEnhance)) return;
 }
 
 export async function executeAiCommand(data: AiExecuteRequest) {
diff --git a/web/src/api/llm.ts b/web/src/api/llm.ts
index 22cf800e..10afb5dd 100644
--- a/web/src/api/llm.ts
+++ b/web/src/api/llm.ts
@@ -19,6 +19,7 @@ import client from './client';
 
 export interface LlmConfig {
   provider: string;
+  engine?: string;
   apiKey?: string;
   apiKeyConfigured?: boolean;
   apiBase: string;
diff --git a/web/src/pages/ai/chatDraft.ts b/web/src/pages/ai/chatDraft.ts
index 2ad1a936..f26f14a9 100644
--- a/web/src/pages/ai/chatDraft.ts
+++ b/web/src/pages/ai/chatDraft.ts
@@ -18,6 +18,7 @@
 export interface ChatDraft {
   prompt: string;
   model?: string;
+  enhance?: boolean;
 }
 
 export function getChatDraft(state: unknown): ChatDraft | null {
@@ -29,5 +30,6 @@ export function getChatDraft(state: unknown): ChatDraft | 
null {
   return {
     prompt: candidate.prompt.trim(),
     ...(model ? { model } : {}),
+    ...(candidate.enhance === true ? { enhance: true } : {}),
   };
 }
diff --git a/web/src/pages/ai/index.tsx b/web/src/pages/ai/index.tsx
index c0463ade..e0023c9d 100644
--- a/web/src/pages/ai/index.tsx
+++ b/web/src/pages/ai/index.tsx
@@ -45,6 +45,7 @@ import { useLang } from '../../i18n/LangContext';
 import { AiStreamError, chatStream, executeTool, listTools, type McpTool } 
from '../../api/ai';
 import { listClusters } from '../../api/cluster';
 import { getLlmConfig, getLlmModels, type LlmConfig } from '../../api/llm';
+import { useEngineStore } from '../../stores/engineStore';
 import { getChatDraft } from './chatDraft';
 
 const { Text } = Typography;
@@ -85,6 +86,8 @@ interface Message {
   stats?: StatItem[];
   descriptions?: DescriptionItem[];
   summary?: string;
+  thinking?: string;
+  pending?: boolean;
   actions?: { label: string; type?: 'primary' | 'default' }[];
 }
 
@@ -274,6 +277,77 @@ export const AiMessage = ({ msg }: { msg: Message }) => (
         </Descriptions>
       )}
 
+      {/* Chain of thought (enhanced prompt) */}
+      {msg.thinking && (
+        <details style={{ marginBottom: 12 }}>
+          <summary
+            style={{
+              cursor: 'pointer',
+              color: '#722ed1',
+              fontSize: 12,
+              fontWeight: 500,
+              userSelect: 'none',
+            }}
+          >
+            思维链:Prompt 增强改写
+          </summary>
+          <div
+            style={{
+              marginTop: 8,
+              padding: '8px 12px',
+              background: '#f9f0ff',
+              border: '1px solid #efdbff',
+              borderRadius: 8,
+              fontSize: 12,
+              lineHeight: 1.7,
+              color: '#595959',
+              whiteSpace: 'pre-wrap',
+            }}
+          >
+            {msg.thinking}
+          </div>
+        </details>
+      )}
+
+      {/* Waiting indicator (inside the bubble) */}
+      {msg.pending && !msg.summary && (
+        <Flex gap={4} align="center" style={{ padding: '2px 0' }}>
+          <span
+            style={{
+              display: 'inline-block',
+              width: 6,
+              height: 6,
+              borderRadius: '50%',
+              background: '#722ed1',
+              animation: 'dotPulse 1.4s infinite ease-in-out',
+            }}
+          />
+          <span
+            style={{
+              display: 'inline-block',
+              width: 6,
+              height: 6,
+              borderRadius: '50%',
+              background: '#722ed1',
+              animation: 'dotPulse 1.4s infinite ease-in-out 0.2s',
+            }}
+          />
+          <span
+            style={{
+              display: 'inline-block',
+              width: 6,
+              height: 6,
+              borderRadius: '50%',
+              background: '#722ed1',
+              animation: 'dotPulse 1.4s infinite ease-in-out 0.4s',
+            }}
+          />
+          <Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>
+            正在思考…
+          </Text>
+        </Flex>
+      )}
+
       {/* Summary text */}
       {msg.summary && (
         <div className="ai-markdown">
@@ -329,6 +403,9 @@ const AiPage = () => {
   const abortControllerRef = useRef<AbortController | null>(null);
   const conversationIdRef = useRef<string | null>(null);
   const consumedDraftRef = useRef(false);
+  const pendingAutoSendRef = useRef<{ prompt: string; model?: string; 
enhance?: boolean } | null>(
+    null,
+  );
 
   const scrollToBottom = useCallback(() => {
     chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -385,8 +462,12 @@ const AiPage = () => {
             : [{ value: draftModel, label: draftModel }, ...options],
         );
       }
+      pendingAutoSendRef.current = {
+        prompt: draft.prompt,
+        model: draft.model,
+        enhance: draft.enhance,
+      };
       navigate('/ai', { replace: true, state: null });
-      textareaRef.current?.focus();
     });
   }, [location.state, navigate]);
 
@@ -408,72 +489,104 @@ const AiPage = () => {
 
   const llmReady = Boolean((llmConfig?.ready ?? llmConfig?.enabled) && 
selectedModel);
 
-  const handleSend = useCallback(async () => {
-    const text = inputValue.trim();
-    if (!text || loading) return;
-    if (!llmReady) {
-      message.warning('请先配置并启用 LLM Provider');
-      return;
-    }
-
-    if (!conversationIdRef.current) {
-      conversationIdRef.current = `conversation-${Date.now()}`;
-    }
+  const handleSend = useCallback(
+    async (textOverride?: string, modelOverride?: string, enhance?: boolean) 
=> {
+      const text = (textOverride ?? inputValue).trim();
+      const model = modelOverride ?? selectedModel;
+      if (!text || loading) return;
+      if (!llmReady) {
+        message.warning('请先配置并启用 LLM Provider');
+        return;
+      }
 
-    const userMsg: Message = {
-      id: `user-${Date.now()}`,
-      role: 'user',
-      text,
-    };
+      if (!conversationIdRef.current) {
+        conversationIdRef.current = `conversation-${Date.now()}`;
+      }
 
-    const responseId = `ai-${Date.now()}`;
-    setMessages((prev) => [...prev, userMsg, { id: responseId, role: 'ai', 
summary: '' }]);
-    setInputValue('');
-    if (textareaRef.current) {
-      textareaRef.current.style.height = 'auto';
-    }
-    setLoading(true);
-    const controller = new AbortController();
-    abortControllerRef.current = controller;
+      const userMsg: Message = {
+        id: `user-${Date.now()}`,
+        role: 'user',
+        text,
+      };
+
+      const responseId = `ai-${Date.now()}`;
+      setMessages((prev) => [
+        ...prev,
+        userMsg,
+        { id: responseId, role: 'ai', summary: '', pending: true },
+      ]);
+      setInputValue('');
+      if (textareaRef.current) {
+        textareaRef.current.style.height = 'auto';
+      }
+      setLoading(true);
+      const controller = new AbortController();
+      abortControllerRef.current = controller;
 
-    try {
-      await chatStream(
-        {
-          message: text,
-          mode: 'chat',
-          model: selectedModel,
-          conversationId: conversationIdRef.current,
-        },
-        (chunk) => {
+      try {
+        await chatStream(
+          {
+            message: text,
+            mode: 'chat',
+            model,
+            engine: useEngineStore.getState().engine,
+            enhance,
+            conversationId: conversationIdRef.current,
+          },
+          (chunk) => {
+            setMessages((prev) =>
+              prev.map((item) =>
+                item.id === responseId
+                  ? { ...item, summary: `${item.summary ?? ''}${chunk}` }
+                  : item,
+              ),
+            );
+          },
+          controller.signal,
+          (enhanceDelta) => {
+            setMessages((prev) =>
+              prev.map((item) =>
+                item.id === responseId
+                  ? { ...item, thinking: `${item.thinking ?? 
''}${enhanceDelta}` }
+                  : item,
+              ),
+            );
+          },
+        );
+      } catch (error) {
+        if (controller.signal.aborted) {
           setMessages((prev) =>
             prev.map((item) =>
-              item.id === responseId ? { ...item, summary: `${item.summary ?? 
''}${chunk}` } : item,
+              item.id === responseId && !item.summary ? { ...item, summary: 
'回答已停止。' } : item,
             ),
           );
-        },
-        controller.signal,
-      );
-    } catch (error) {
-      if (controller.signal.aborted) {
-        setMessages((prev) =>
-          prev.map((item) =>
-            item.id === responseId && !item.summary ? { ...item, summary: 
'回答已停止。' } : item,
-          ),
-        );
-      } else {
-        const errorMessage = error instanceof Error ? error.message : 'AI 
请求失败';
-        const errorHint = error instanceof AiStreamError && error.hint ? 
error.hint : '';
-        const summary = errorHint ? `${errorMessage}\n\n> ${errorHint}` : 
errorMessage;
+        } else {
+          const errorMessage = error instanceof Error ? error.message : 'AI 
请求失败';
+          const errorHint = error instanceof AiStreamError && error.hint ? 
error.hint : '';
+          const summary = errorHint ? `${errorMessage}\n\n> ${errorHint}` : 
errorMessage;
+          setMessages((prev) =>
+            prev.map((item) => (item.id === responseId ? { ...item, summary } 
: item)),
+          );
+          message.error(errorMessage);
+        }
+      } finally {
+        if (abortControllerRef.current === controller) 
abortControllerRef.current = null;
         setMessages((prev) =>
-          prev.map((item) => (item.id === responseId ? { ...item, summary } : 
item)),
+          prev.map((item) => (item.id === responseId ? { ...item, pending: 
false } : item)),
         );
-        message.error(errorMessage);
+        setLoading(false);
       }
-    } finally {
-      if (abortControllerRef.current === controller) 
abortControllerRef.current = null;
-      setLoading(false);
-    }
-  }, [inputValue, llmReady, loading, selectedModel]);
+    },
+    [inputValue, llmReady, loading, selectedModel],
+  );
+
+  /* ─── Auto-send the draft from the home page as soon as runtime is ready 
─── */
+  useEffect(() => {
+    const pending = pendingAutoSendRef.current;
+    if (!pending || loading || !llmReady) return;
+    pendingAutoSendRef.current = null;
+    void handleSend(pending.prompt, pending.model, pending.enhance);
+  }, [llmReady, loading, handleSend]);
 
   const handleStop = useCallback(() => {
     abortControllerRef.current?.abort();
@@ -604,81 +717,6 @@ const AiPage = () => {
             <AiMessage key={msg.id} msg={msg} />
           ),
         )}
-        {loading && (
-          <Flex gap={12} align="flex-start" style={{ marginBottom: 16 }}>
-            <div
-              style={{
-                width: 36,
-                height: 36,
-                borderRadius: '50%',
-                background: 'linear-gradient(135deg, #1677ff 0%, #722ed1 
100%)',
-                flexShrink: 0,
-                display: 'flex',
-                alignItems: 'center',
-                justifyContent: 'center',
-                boxShadow: '0 2px 8px rgba(22, 119, 255, 0.3)',
-              }}
-            >
-              <svg
-                width="20"
-                height="20"
-                viewBox="0 0 24 24"
-                fill="none"
-                stroke="white"
-                strokeWidth="2"
-                strokeLinecap="round"
-                strokeLinejoin="round"
-              >
-                <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 
5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
-                <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 
2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
-                <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
-                <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
-              </svg>
-            </div>
-            <Card
-              size="small"
-              style={{
-                boxShadow: '0 1px 4px rgba(0, 0, 0, 0.06)',
-                borderRadius: 12,
-                borderTopLeftRadius: 4,
-              }}
-              styles={{ body: { padding: '12px 16px' } }}
-            >
-              <Flex gap={4} align="center">
-                <span
-                  style={{
-                    display: 'inline-block',
-                    width: 6,
-                    height: 6,
-                    borderRadius: '50%',
-                    background: '#722ed1',
-                    animation: 'dotPulse 1.4s infinite ease-in-out',
-                  }}
-                />
-                <span
-                  style={{
-                    display: 'inline-block',
-                    width: 6,
-                    height: 6,
-                    borderRadius: '50%',
-                    background: '#722ed1',
-                    animation: 'dotPulse 1.4s infinite ease-in-out 0.2s',
-                  }}
-                />
-                <span
-                  style={{
-                    display: 'inline-block',
-                    width: 6,
-                    height: 6,
-                    borderRadius: '50%',
-                    background: '#722ed1',
-                    animation: 'dotPulse 1.4s infinite ease-in-out 0.4s',
-                  }}
-                />
-              </Flex>
-            </Card>
-          </Flex>
-        )}
         <div ref={chatEndRef} />
       </div>
 
@@ -790,7 +828,7 @@ const AiPage = () => {
                 <div className="shrink-0 flex items-center gap-1">
                   <button
                     className="flex items-center justify-center w-9 h-9 
rounded-full bg-gradient-to-r from-purple-500 to-violet-600 text-white 
shadow-lg hover:shadow-xl transition-all hover:scale-105"
-                    onClick={handleSend}
+                    onClick={() => void handleSend()}
                     disabled={loading || !inputValue.trim() || !llmReady}
                     style={{
                       opacity: loading || !inputValue.trim() || !llmReady ? 
0.5 : 1,
diff --git a/web/src/pages/home/__tests__/HomePage.test.tsx 
b/web/src/pages/home/__tests__/HomePage.test.tsx
index c2ce3cd2..513fe6a5 100644
--- a/web/src/pages/home/__tests__/HomePage.test.tsx
+++ b/web/src/pages/home/__tests__/HomePage.test.tsx
@@ -25,7 +25,6 @@ import HomePage from '../index';
 const navigateMock = vi.hoisted(() => vi.fn());
 const llmApiMocks = vi.hoisted(() => ({
   getLlmConfig: vi.fn(),
-  getLlmModels: vi.fn(),
 }));
 
 vi.mock('react-router-dom', () => ({
@@ -53,19 +52,14 @@ beforeAll(() => {
 beforeEach(() => {
   vi.clearAllMocks();
   llmApiMocks.getLlmConfig.mockResolvedValue({
-    provider: 'openai',
-    apiBase: 'https://api.example.com/v1',
-    model: 'provider-model',
+    provider: 'tongyi',
+    apiBase: 
'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1',
+    model: 'qwen3.8-max',
     maxTokens: 4096,
     temperature: 0.7,
     enabled: true,
     ready: true,
   });
-  llmApiMocks.getLlmModels.mockResolvedValue({
-    status: 0,
-    data: [{ id: 'provider-model' }, { id: 'provider-model-secondary' }],
-    source: 'provider',
-  });
 });
 
 const renderHome = () =>
@@ -78,25 +72,28 @@ const renderHome = () =>
   );
 
 describe('HomePage LLM models', () => {
-  it('loads configured provider models instead of hard-coded options', async 
() => {
+  it('shows the fixed home model list with qwen3.8-max selected', async () => {
     renderHome();
 
-    expect(await screen.findByText('provider-model')).toBeInTheDocument();
-    expect(screen.queryByText('qwen3.7-max')).not.toBeInTheDocument();
+    expect(await screen.findByText('qwen3.8-max')).toBeInTheDocument();
   });
 
-  it('submits the configured provider model to the AI page', async () => {
+  it('submits the selected model and engine to the AI page', async () => {
     const user = userEvent.setup();
     renderHome();
-    await screen.findByText('provider-model');
+    await screen.findByText('qwen3.8-max');
 
-    await user.type(screen.getByPlaceholderText('向 RocketMQ Bot 
提问,全程加密、安全、可信'), '查看集群状态{enter}');
+    await user.type(
+      screen.getByPlaceholderText('向 RocketMQ Bot 提问,全程加密、安全、可信'),
+      '查看集群状态{enter}',
+    );
 
     await waitFor(() => {
       expect(navigateMock).toHaveBeenCalledWith('/ai', {
         state: {
           prompt: '查看集群状态',
-          model: 'provider-model',
+          model: 'qwen3.8-max',
+          engine: 'claude-code',
         },
       });
     });
diff --git a/web/src/pages/home/index.tsx b/web/src/pages/home/index.tsx
index ae7735a8..7a6da0dc 100644
--- a/web/src/pages/home/index.tsx
+++ b/web/src/pages/home/index.tsx
@@ -32,7 +32,8 @@ import {
   MegaphoneSimple,
   Database,
 } from '@phosphor-icons/react';
-import { getLlmConfig, getLlmModels } from '../../api/llm';
+import { getLlmConfig } from '../../api/llm';
+import { useEngineStore } from '../../stores/engineStore';
 import { useLang } from '../../i18n/LangContext';
 
 /* ─── Time-aware greeting key ─── */
@@ -47,10 +48,10 @@ function getGreetingKey(): string {
 
 /* ─── Mode definitions (keys only, labels resolved via t()) ─── */
 const modes = [
-  { key: 'query', labelKey: 'home.mode.query', icon: MagnifyingGlass },
+  { key: 'chat', labelKey: 'home.mode.chat', icon: ChatCircleDots },
   { key: 'diagnose', labelKey: 'home.mode.diagnose', icon: Stethoscope },
   { key: 'manage', labelKey: 'home.mode.manage', icon: Database },
-  { key: 'chat', labelKey: 'home.mode.chat', icon: ChatCircleDots },
+  { key: 'query', labelKey: 'home.mode.query', icon: MagnifyingGlass },
 ];
 
 interface ModelOption {
@@ -58,13 +59,34 @@ interface ModelOption {
   recommended: boolean;
 }
 
+const ENGINE_OPTIONS = [
+  { value: 'claude-code', label: 'Claude Code' },
+  { value: 'qoder', label: 'Qoder' },
+  { value: 'http', label: 'HTTP' },
+];
+
+// 首页只暴露这些模型(token-plan 网关实际可对话的模型集),qwen3.8-max 为推荐项。
+const HOME_MODELS = [
+  'qwen3.8-max',
+  'qwen3.7-max',
+  'qwen3.7-plus',
+  'deepseek-v4-pro',
+  'deepseek-v4-flash',
+  'MiniMax-M2.5',
+  'glm-5.2',
+];
+const RECOMMENDED_MODEL = 'qwen3.8-max';
+
 /* ═══════════════════════════════════════════════════════
    HomePage Component
    ═══════════════════════════════════════════════════════ */
 const HomePage = () => {
-  const [activeMode, setActiveMode] = useState('query');
+  const [activeMode, setActiveMode] = useState('chat');
   const [modelOptions, setModelOptions] = useState<ModelOption[]>([]);
   const [selectedModel, setSelectedModel] = useState('');
+  const engine = useEngineStore((s) => s.engine);
+  const setEnginePreference = useEngineStore((s) => s.setEngine);
+  const [promoteOn, setPromoteOn] = useState(false);
   const [inputValue, setInputValue] = useState('');
   const [indicatorStyle, setIndicatorStyle] = useState({ width: 83, left: 6 });
   const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -76,26 +98,30 @@ const HomePage = () => {
     let cancelled = false;
 
     const loadModels = async () => {
-      const [config, modelsResult] = await Promise.all([
-        getLlmConfig().catch(() => null),
-        getLlmModels().catch(() => null),
-      ]);
+      const config = await getLlmConfig().catch(() => null);
       if (cancelled) return;
 
       const configuredModel = config?.model?.trim() ?? '';
-      const providerModels = modelsResult?.status === 0 && modelsResult.data
-        ? modelsResult.data.map((item) => item.id || item.name || 
'').filter(Boolean)
-        : [];
-      const values = Array.from(new Set([
-        ...(configuredModel ? [configuredModel] : []),
-        ...providerModels,
-      ]));
+      const values = Array.from(
+        new Set([
+          ...HOME_MODELS,
+          ...(configuredModel && !HOME_MODELS.includes(configuredModel) ? 
[configuredModel] : []),
+        ]),
+      );
 
-      setModelOptions(values.map((value, index) => ({
-        value,
-        recommended: configuredModel ? value === configuredModel : index === 0,
-      })));
-      setSelectedModel((current) => (current && values.includes(current) ? 
current : values[0] || ''));
+      setModelOptions(
+        values.map((value) => ({
+          value,
+          recommended:
+            value ===
+            (configuredModel && HOME_MODELS.includes(configuredModel)
+              ? configuredModel
+              : RECOMMENDED_MODEL),
+        })),
+      );
+      setSelectedModel((current) =>
+        current && values.includes(current) ? current : values[0] || '',
+      );
     };
 
     void loadModels();
@@ -154,9 +180,22 @@ const HomePage = () => {
     }
   };
 
+  const handleEngineChange = (value: string) => {
+    setEnginePreference(value as 'claude-code' | 'qoder' | 'http');
+  };
+
   const handlePromptSubmit = () => {
     const prompt = inputValue.trim();
-    navigate('/ai', { state: prompt ? { prompt, ...(selectedModel ? { model: 
selectedModel } : {}) } : null });
+    navigate('/ai', {
+      state: prompt
+        ? {
+            prompt,
+            ...(selectedModel ? { model: selectedModel } : {}),
+            engine,
+            ...(promoteOn ? { enhance: true } : {}),
+          }
+        : null,
+    });
   };
 
   return (
@@ -348,6 +387,17 @@ const HomePage = () => {
                       className="model-selector"
                       style={{ fontSize: '0.893rem' }}
                     />
+                    <Select
+                      size="small"
+                      value={engine}
+                      onChange={(val) => void handleEngineChange(val)}
+                      options={ENGINE_OPTIONS}
+                      variant="borderless"
+                      popupMatchSelectWidth={false}
+                      suffixIcon={<CaretDown size={10} color="#9CA3AF" />}
+                      title={lang === 'zh' ? '执行引擎' : 'Agent engine'}
+                      style={{ fontSize: '0.893rem', minWidth: 110 }}
+                    />
                   </div>
                   <div className="flex shrink-0 items-center gap-1">
                     <button className="p-1 rounded-md text-gray-400 
hover:text-gray-600 hover:bg-gray-50 transition-colors">
@@ -387,8 +437,25 @@ const HomePage = () => {
                             <SlidersHorizontal size={17} />
                             <span>工具</span>
                           </button>
-                          <button className="tool-btn">
-                            <Sparkle size={17} />
+                          <button
+                            className="tool-btn"
+                            onClick={() => setPromoteOn((current) => !current)}
+                            title={
+                              lang === 'zh'
+                                ? '开启后,提交前用 LLM 把提问改写为结构化 prompt'
+                                : 'When enabled, rewrite your prompt with an 
LLM before sending'
+                            }
+                            style={
+                              promoteOn
+                                ? {
+                                    background: '#f9f0ff',
+                                    color: '#722ed1',
+                                    boxShadow: 'inset 0 0 0 1px #d3adf7',
+                                  }
+                                : undefined
+                            }
+                          >
+                            <Sparkle size={17} weight={promoteOn ? 'fill' : 
'regular'} />
                             <span>Prompt 增强</span>
                           </button>
                           <button
diff --git a/web/src/pages/studio/LlmSettings.tsx 
b/web/src/pages/studio/LlmSettings.tsx
index 22e2b522..f56babd3 100644
--- a/web/src/pages/studio/LlmSettings.tsx
+++ b/web/src/pages/studio/LlmSettings.tsx
@@ -15,868 +15,298 @@
  * limitations under the License.
  */
 
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useEffect, useState } from 'react';
 import {
+  Alert,
+  Button,
+  Card,
   Form,
   Input,
+  InputNumber,
   Select,
   Slider,
-  Switch,
-  Button,
-  Card,
   Space,
-  Typography,
-  Divider,
-  Alert,
-  Row,
-  Col,
+  Tag,
   App,
 } from 'antd';
-import {
-  FloppyDisk,
-  Lightning,
-  Cloud,
-  Globe,
-  Key,
-  ShieldCheck,
-  CheckCircle,
-  XCircle,
-} from '@phosphor-icons/react';
 import PageHeader from '../../components/PageHeader';
 import { useLang } from '../../i18n/LangContext';
 import {
   getLlmConfig,
+  getLlmModels,
   saveLlmConfig,
   testLlmConnection,
-  getLlmModels,
   type LlmConfig,
+  type LlmTestResult,
 } from '../../api/llm';
-import { buildLlmFailureResult, type TestResult } from './llmFailureResult';
 import { fallbackModelOptions } from './llmModelOptions';
 
-const { Text } = Typography;
-const MASKED_API_KEY = '••••••••';
-
-interface ProviderDef {
-  key: string;
-  label: string;
-  icon: string;
-  color: string;
-  descKey: string;
-  defaultBaseUrl: string;
-  defaultModel: string;
-  requireApiKey: boolean;
-  requireBaseUrl: boolean;
-  extraFields?: string[];
-}
+const PROVIDER_OPTIONS = [
+  { value: 'tongyi', label: '通义千问(DashScope)' },
+  { value: 'openai', label: 'OpenAI' },
+  { value: 'azure', label: 'Azure OpenAI' },
+  { value: 'deepseek', label: 'DeepSeek' },
+  { value: 'ollama', label: 'Ollama(本地)' },
+  { value: 'bedrock', label: 'AWS Bedrock' },
+];
 
-const PROVIDERS: ProviderDef[] = [
-  {
-    key: 'openai',
-    label: 'OpenAI',
-    icon: '🤖',
-    color: '#10a37f',
-    descKey: 'llm.providerOpenaiDesc',
-    defaultBaseUrl: 'https://api.openai.com/v1',
-    defaultModel: 'gpt-4o',
-    requireApiKey: true,
-    requireBaseUrl: false,
-  },
-  {
-    key: 'azure',
-    label: 'Azure OpenAI',
-    icon: '☁️',
-    color: '#0078d4',
-    descKey: 'llm.providerAzureDesc',
-    defaultBaseUrl: '',
-    defaultModel: 'gpt-4o',
-    requireApiKey: true,
-    requireBaseUrl: true,
-    extraFields: ['deploymentName', 'apiVersion'],
-  },
-  {
-    key: 'deepseek',
-    label: 'DeepSeek',
-    icon: '🔍',
-    color: '#4d6bfe',
-    descKey: 'llm.providerDeepseekDesc',
-    defaultBaseUrl: 'https://api.deepseek.com/v1',
-    defaultModel: 'deepseek-chat',
-    requireApiKey: true,
-    requireBaseUrl: false,
-  },
-  {
-    key: 'tongyi',
-    label: '通义千问',
-    icon: '🧠',
-    color: '#6236ff',
-    descKey: 'llm.providerTongyiDesc',
-    defaultBaseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
-    defaultModel: 'qwen-max',
-    requireApiKey: true,
-    requireBaseUrl: false,
-  },
-  {
-    key: 'ollama',
-    label: 'Ollama',
-    icon: '🦙',
-    color: '#6e40c9',
-    descKey: 'llm.providerOllamaDesc',
-    defaultBaseUrl: 'http://localhost:11434/v1',
-    defaultModel: 'llama3',
-    requireApiKey: false,
-    requireBaseUrl: true,
-  },
-  {
-    key: 'bedrock',
-    label: 'AWS Bedrock',
-    icon: '☁️',
-    color: '#ff9900',
-    descKey: 'llm.providerBedrockDesc',
-    defaultBaseUrl: '',
-    defaultModel: 'anthropic.claude-3-sonnet',
-    requireApiKey: true,
-    requireBaseUrl: false,
-    extraFields: ['awsRegion'],
-  },
+const ENGINE_OPTIONS = [
+  { value: 'claude-code', label: 'Claude Code(默认)' },
+  { value: 'qoder', label: 'Qoder CLI' },
+  { value: 'http', label: 'HTTP(OpenAI 兼容)' },
 ];
 
+const DEFAULT_BASE_URL: Record<string, string> = {
+  tongyi: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
+  openai: 'https://api.openai.com/v1',
+  deepseek: 'https://api.deepseek.com/v1',
+  ollama: 'http://localhost:11434/v1',
+};
+
+interface TestState {
+  success: boolean;
+  msg: string;
+  hint?: string;
+}
+
 const LlmSettingsPage: React.FC = () => {
   const { t } = useLang();
   const { message } = App.useApp();
-
   const [form] = Form.useForm();
-  const [loading, setLoading] = useState(false);
-  const [testLoading, setTestLoading] = useState(false);
-  const [testResult, setTestResult] = useState<TestResult | null>(null);
-  const [enabled, setEnabled] = useState(false);
-  const [selectedProvider, setSelectedProvider] = useState('openai');
-  const [apiKeyMasked, setApiKeyMasked] = useState(true);
-  const [savedApiKey, setSavedApiKey] = useState('');
-  const [modelOptions, setModelOptions] = useState<{ value: string; label: 
string }[]>([]);
-  const [modelsLoading, setModelsLoading] = useState(false);
-
-  const mountedRef = useRef(false);
-  const lifecycleGeneration = useRef(0);
-  const providerInteractionGeneration = useRef(0);
-  const configRequestGeneration = useRef(0);
-  const selectedProviderRef = useRef('openai');
-  const modelRequestGeneration = useRef(0);
-
-  const fetchConfig = useCallback(
-    async (
-      configPromise: Promise<LlmConfig>,
-      requestLifecycle: number,
-      requestGeneration: number,
-      requestProviderGeneration: number,
-    ) => {
-      const ownsRequest = () =>
-        mountedRef.current &&
-        requestLifecycle === lifecycleGeneration.current &&
-        requestGeneration === configRequestGeneration.current &&
-        requestProviderGeneration === providerInteractionGeneration.current;
-      if (ownsRequest()) {
-        setLoading(true);
-      }
-      try {
-        const config = await configPromise;
-        if (!ownsRequest()) return;
-        if (config) {
-          const provider = config.provider || 'openai';
-          selectedProviderRef.current = provider;
-          setSelectedProvider(provider);
-          setEnabled(config.enabled || false);
-          if (config.apiKeyConfigured) {
-            setSavedApiKey(MASKED_API_KEY);
-            form.setFieldsValue({ apiKey: MASKED_API_KEY });
-            setApiKeyMasked(true);
-          }
-          form.setFieldsValue({
-            provider,
-            apiBase: config.apiBase || '',
-            model: config.model || '',
-            maxTokens: config.maxTokens || 4096,
-            temperature: config.temperature !== undefined ? config.temperature 
: 0.7,
-            deploymentName: config.deploymentName || '',
-            apiVersion: config.apiVersion || '2024-02-15-preview',
-            awsRegion: config.awsRegion || 'us-east-1',
-          });
-        }
-        return config;
-      } catch {
-        if (ownsRequest()) {
-          message.error(t('llm.loadFailed'));
-        }
-      } finally {
-        if (ownsRequest()) {
-          setLoading(false);
-        }
-      }
-    },
-    [form, message, t],
-  );
-
-  const fetchModels = useCallback(
-    async (
-      providerOverride?: string,
-      modelOverride?: string,
-      expectedProviderGeneration = providerInteractionGeneration.current,
-    ) => {
-      if (!mountedRef.current) return;
-      const requestedProvider = providerOverride || 
selectedProviderRef.current;
-      if (
-        requestedProvider !== selectedProviderRef.current ||
-        expectedProviderGeneration !== providerInteractionGeneration.current
-      ) {
-        return;
-      }
-      const requestLifecycle = lifecycleGeneration.current;
-      const requestGeneration = ++modelRequestGeneration.current;
-      const ownsRequest = () =>
-        mountedRef.current &&
-        requestLifecycle === lifecycleGeneration.current &&
-        requestGeneration === modelRequestGeneration.current &&
-        expectedProviderGeneration === providerInteractionGeneration.current &&
-        requestedProvider === selectedProviderRef.current;
-      let model = modelOverride;
-
-      setModelsLoading(true);
-      try {
-        const config = await getLlmConfig();
-        if (!ownsRequest()) return;
-
-        const configuredProvider = config?.provider || requestedProvider;
-        if (configuredProvider !== requestedProvider) return;
-
-        model = modelOverride || config?.model || '';
-        if (!config || !config.enabled) {
-          setModelOptions(fallbackModelOptions(requestedProvider, model));
-          return;
-        }
 
-        const result = await getLlmModels();
-        if (!ownsRequest()) return;
+  const [loading, setLoading] = useState(true);
+  const [saving, setSaving] = useState(false);
+  const [testing, setTesting] = useState(false);
+  const [apiKeyConfigured, setApiKeyConfigured] = useState(false);
+  const [modelOptions, setModelOptions] = useState<{ value: string; label: 
string }[]>([]);
+  const [testResult, setTestResult] = useState<TestState | null>(null);
+
+  const buildModelOptions = (
+    nextProvider: string,
+    remoteModels: string[],
+    currentModel?: string,
+  ) => {
+    const source =
+      remoteModels.length > 0
+        ? remoteModels.map((id) => ({ value: id, label: id }))
+        : fallbackModelOptions(nextProvider);
+    if (currentModel && !source.some((option) => option.value === 
currentModel)) {
+      source.unshift({ value: currentModel, label: currentModel });
+    }
+    return source;
+  };
 
-        let models: string[] = [];
-        if (result && result.status === 0 && result.data) {
-          models = result.data.map((m) => m.id || m.name || 
'').filter(Boolean);
-        }
-        if (result?.source === 'fallback') {
-          message.warning(
-            result.hint ||
-              result.warning ||
-              '已回退到内置模型列表,请检查 Provider 凭证或模型接口。',
-          );
-        }
-        if (models.length === 0) {
-          models = fallbackModelOptions(requestedProvider, config.model).map(
-            (option) => option.value,
-          );
-        }
-        setModelOptions(models.map((item) => ({ value: item, label: item })));
-      } catch {
-        if (ownsRequest()) {
-          setModelOptions(fallbackModelOptions(requestedProvider, model));
-        }
-      } finally {
-        if (ownsRequest()) {
-          setModelsLoading(false);
-        }
-      }
-    },
-    [message],
-  );
+  const applyConfig = (config: LlmConfig, remoteModels: string[]) => {
+    const nextProvider = config.provider || 'tongyi';
+    setApiKeyConfigured(Boolean(config.apiKeyConfigured));
+    setModelOptions(buildModelOptions(nextProvider, remoteModels, 
config.model));
+    form.setFieldsValue({
+      engine: config.engine || 'claude-code',
+      provider: nextProvider,
+      model: config.model || undefined,
+      apiBase: config.apiBase || DEFAULT_BASE_URL[nextProvider] || '',
+      maxTokens: config.maxTokens || 4096,
+      temperature: config.temperature ?? 0.7,
+      apiKey: undefined,
+    });
+  };
 
   useEffect(() => {
-    const currentLifecycle = ++lifecycleGeneration.current;
-    const currentConfigRequest = ++configRequestGeneration.current;
-    const currentProviderGeneration = providerInteractionGeneration.current;
-    mountedRef.current = true;
-    const configPromise = getLlmConfig();
-    queueMicrotask(() => {
-      void fetchConfig(
-        configPromise,
-        currentLifecycle,
-        currentConfigRequest,
-        currentProviderGeneration,
-      ).then((config) => {
-        if (
-          !mountedRef.current ||
-          currentLifecycle !== lifecycleGeneration.current ||
-          currentConfigRequest !== configRequestGeneration.current ||
-          currentProviderGeneration !== providerInteractionGeneration.current
-        ) {
-          return;
-        }
-        void fetchModels(
-          config?.provider || selectedProviderRef.current,
-          config?.model,
-          currentProviderGeneration,
-        );
+    let cancelled = false;
+    Promise.all([getLlmConfig(), getLlmModels().catch(() => null)])
+      .then(([config, models]) => {
+        if (cancelled) return;
+        applyConfig(config, models?.data?.map((m) => m.id || 
'').filter(Boolean) ?? []);
+      })
+      .catch(() => {
+        if (!cancelled) message.error(t('llm.loadFailed'));
+      })
+      .finally(() => {
+        if (!cancelled) setLoading(false);
       });
-    });
-
     return () => {
-      mountedRef.current = false;
-      lifecycleGeneration.current += 1;
-      configRequestGeneration.current += 1;
-      modelRequestGeneration.current += 1;
+      cancelled = true;
     };
-  }, [fetchConfig, fetchModels]);
-
-  const maskApiKey = (key: string) => {
-    if (key === MASKED_API_KEY) return MASKED_API_KEY;
-    if (!key || key.length < 8) return key ? '••••••••' : '';
-    return key.slice(0, 4) + '••••••••' + key.slice(-4);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const handleProviderChange = (nextProvider: string) => {
+    setModelOptions(fallbackModelOptions(nextProvider));
+    const fallbackModel = fallbackModelOptions(nextProvider)[0]?.value;
+    form.setFieldsValue({
+      provider: nextProvider,
+      model: fallbackModel,
+      apiBase: DEFAULT_BASE_URL[nextProvider] || form.getFieldValue('apiBase'),
+    });
+    setTestResult(null);
   };
 
-  const currentProvider = PROVIDERS.find((p) => p.key === selectedProvider) || 
PROVIDERS[0];
-
-  const buildConfig = (values: LlmConfig, forceEnabled: boolean): LlmConfig => 
{
-    const apiKey = apiKeyMasked && savedApiKey ? undefined : values.apiKey || 
'';
+  const buildPayload = async (): Promise<LlmConfig | null> => {
+    let values;
+    try {
+      values = await form.validateFields();
+    } catch {
+      return null;
+    }
+    const apiKey = (values.apiKey as string | undefined)?.trim();
     return {
-      ...values,
-      apiKey,
-      enabled: forceEnabled,
+      provider: values.provider,
+      engine: values.engine || 'claude-code',
+      apiBase: values.apiBase,
+      model: values.model,
+      maxTokens: values.maxTokens,
+      temperature: values.temperature,
+      enabled: true,
+      // 留空表示保留服务端已配置的密钥(含环境变量注入的 token)
+      ...(apiKey ? { apiKey } : {}),
     };
   };
 
-  const handleProviderChange = useCallback(
-    (value: string) => {
-      const providerChanged = value !== selectedProviderRef.current;
-      selectedProviderRef.current = value;
-      if (providerChanged) {
-        providerInteractionGeneration.current += 1;
-        configRequestGeneration.current += 1;
-        modelRequestGeneration.current += 1;
-        setLoading(false);
-        setModelsLoading(false);
-      }
-      setSelectedProvider(value);
-      setTestResult(null);
-      const provider = PROVIDERS.find((p) => p.key === value);
-      if (provider) {
-        form.setFieldsValue({
-          apiBase: provider.defaultBaseUrl,
-          model: provider.defaultModel,
-        });
-        setModelOptions(fallbackModelOptions(value, provider.defaultModel));
-        if (providerChanged || !provider.requireApiKey) {
-          form.setFieldsValue({ apiKey: '' });
-          setSavedApiKey('');
-          setApiKeyMasked(true);
-        }
-      }
-    },
-    [form],
-  );
-
-  const handleApiKeyFocus = () => {
-    if (apiKeyMasked && savedApiKey) {
-      form.setFieldsValue({ apiKey: '' });
-      setApiKeyMasked(false);
-    }
-  };
-
-  const handleApiKeyBlur = () => {
-    const val = form.getFieldValue('apiKey');
-    if (!val && savedApiKey) {
-      form.setFieldsValue({ apiKey: maskApiKey(savedApiKey) });
-      setApiKeyMasked(true);
+  const applyTestResult = (result: LlmTestResult) => {
+    if (result.status === 0) {
+      setTestResult({ success: true, msg: result.msg || '连接成功' });
+    } else {
+      setTestResult({
+        success: false,
+        msg: result.errMsg || '连接测试失败',
+        hint: result.hint,
+      });
     }
   };
 
-  const handleTestConnection = () => {
-    const requestProviderGeneration = providerInteractionGeneration.current;
-    setTestLoading(true);
+  const handleTest = async () => {
+    const payload = await buildPayload();
+    if (!payload) return;
+    setTesting(true);
     setTestResult(null);
-    form
-      .validateFields()
-      .then((values) => {
-        const testConfig = buildConfig(values, true);
-        testLlmConnection(testConfig)
-          .then((result) => {
-            if (result && result.status === 0) {
-              setTestResult({ success: true, msg: result.msg || 
t('llm.testSuccessMsg') });
-              message.success(t('llm.testSuccess'));
-              // Auto-save after successful test
-              saveLlmConfig(testConfig)
-                .then(() => {
-                  if (testConfig.apiKey) {
-                    setSavedApiKey(MASKED_API_KEY);
-                    form.setFieldsValue({ apiKey: MASKED_API_KEY });
-                    setApiKeyMasked(true);
-                  }
-                  fetchModels(testConfig.provider, testConfig.model, 
requestProviderGeneration);
-                })
-                .catch(() => {
-                  // auto-save failure is non-critical
-                });
-            } else {
-              setTestResult(buildLlmFailureResult(result, 
t('llm.testFailedMsg')));
-              message.error(t('llm.testFailed'));
-            }
-          })
-          .catch((err) => {
-            setTestResult({
-              success: false,
-              msg: t('llm.testError') + (err.message || ''),
-            });
-            message.error(t('llm.testFailed'));
-          })
-          .finally(() => {
-            setTestLoading(false);
-          });
-      })
-      .catch(() => {
-        setTestLoading(false);
-      });
+    try {
+      applyTestResult(await testLlmConnection(payload));
+    } catch {
+      setTestResult({ success: false, msg: '连接测试请求失败,请稍后重试' });
+    } finally {
+      setTesting(false);
+    }
   };
 
-  const handleSave = () => {
-    const requestProviderGeneration = providerInteractionGeneration.current;
-    setLoading(true);
-    form
-      .validateFields()
-      .then((values) => {
-        const config = buildConfig(values, enabled);
-        saveLlmConfig(config)
-          .then((result) => {
-            if (result && result.status === 0) {
-              message.success(t('llm.saveSuccess'));
-              if (config.apiKey) {
-                setSavedApiKey(MASKED_API_KEY);
-                form.setFieldsValue({ apiKey: MASKED_API_KEY });
-                setApiKeyMasked(true);
-              }
-              fetchModels(config.provider, config.model, 
requestProviderGeneration);
-            } else {
-              message.error((result && result.errMsg) || t('llm.saveFailed'));
-            }
-          })
-          .catch((err) => {
-            if (err.errorFields) {
-              message.error(t('llm.formIncomplete'));
-            } else {
-              message.error(t('llm.saveFailed'));
-            }
-          })
-          .finally(() => {
-            setLoading(false);
-          });
-      })
-      .catch(() => {
-        message.error(t('llm.formIncomplete'));
-        setLoading(false);
-      });
+  const handleSave = async () => {
+    const payload = await buildPayload();
+    if (!payload) return;
+    setSaving(true);
+    try {
+      const result = await saveLlmConfig(payload);
+      if (result.status === 0) {
+        message.success('保存成功');
+        if (payload.apiKey) {
+          setApiKeyConfigured(true);
+          form.setFieldValue('apiKey', undefined);
+        }
+      } else {
+        message.error(result.errMsg || '保存失败');
+      }
+    } catch {
+      message.error('保存请求失败,请稍后重试');
+    } finally {
+      setSaving(false);
+    }
   };
 
-  // ─── Provider Grid ──────────────────────────────────────────
-
-  const renderProviderGrid = () => (
-    <div
-      style={{
-        display: 'grid',
-        gridTemplateColumns: 'repeat(3, 1fr)',
-        gap: 12,
-        marginBottom: 4,
-      }}
-    >
-      {PROVIDERS.map((p) => (
-        <div
-          key={p.key}
-          onClick={() => {
-            form.setFieldsValue({ provider: p.key });
-            handleProviderChange(p.key);
-          }}
-          style={{
-            position: 'relative',
-            display: 'flex',
-            alignItems: 'center',
-            gap: 12,
-            padding: '14px 16px',
-            border: `2px solid ${selectedProvider === p.key ? p.color : 
'#f0f0f0'}`,
-            borderRadius: 10,
-            cursor: 'pointer',
-            background: selectedProvider === p.key ? '#fafbff' : '#ffffff',
-            boxShadow: selectedProvider === p.key ? '0 2px 8px rgba(22, 119, 
255, 0.1)' : 'none',
-            transition: 'all 0.2s ease',
-            userSelect: 'none',
-          }}
-        >
-          <div
-            style={{
-              flexShrink: 0,
-              width: 40,
-              height: 40,
-              display: 'flex',
-              alignItems: 'center',
-              justifyContent: 'center',
-              borderRadius: 8,
-              fontSize: 20,
-              fontWeight: 600,
-              background: p.color + '15',
-              color: p.color,
-            }}
-          >
-            {p.icon.length <= 2 ? p.icon : <Cloud size={20} />}
-          </div>
-          <div style={{ flex: 1, minWidth: 0 }}>
-            <div style={{ fontSize: 14, fontWeight: 600, lineHeight: 1.4 
}}>{p.label}</div>
-            <div
-              style={{
-                fontSize: 11,
-                color: 'rgba(0,0,0,0.45)',
-                lineHeight: 1.4,
-                whiteSpace: 'nowrap',
-                overflow: 'hidden',
-                textOverflow: 'ellipsis',
-              }}
-            >
-              {t(p.descKey)}
-            </div>
-          </div>
-          {selectedProvider === p.key && (
-            <CheckCircle
-              size={16}
-              weight="fill"
-              style={{ position: 'absolute', top: 8, right: 8, color: p.color 
}}
-            />
-          )}
-        </div>
-      ))}
-    </div>
-  );
-
-  // ─── Render ──────────────────────────────────────────────────
-
   return (
-    <div style={{ maxWidth: 960, margin: '0 auto', padding: '0 24px 40px' }}>
-      <PageHeader
-        title={t('llm.title')}
-
-        extra={
-          <Space>
-            <Text type="secondary" style={{ fontSize: 13, marginRight: 8 }}>
-              {t('llm.enable')}
-            </Text>
-            <Switch
-              checked={enabled}
-              onChange={setEnabled}
-              checkedChildren={t('llm.on')}
-              unCheckedChildren={t('llm.off')}
-            />
-          </Space>
-        }
-      />
+    <div style={{ padding: 24 }}>
+      <PageHeader title={t('llm.title')} subtitle="配置 AI 助手使用的模型服务" />
 
-      <Card
-        loading={loading}
-        style={{ borderRadius: 12, boxShadow: '0 1px 4px rgba(0,0,0,0.06)' }}
-        styles={{ body: { padding: '24px 28px' } }}
-      >
+      <Card loading={loading} style={{ maxWidth: 720 }}>
         <Form
           form={form}
           layout="vertical"
-          initialValues={{
-            provider: 'openai',
-            model: 'gpt-4o',
-            apiBase: 'https://api.openai.com/v1',
-            maxTokens: 4096,
-            temperature: 0.7,
-            apiVersion: '2024-02-15-preview',
-            awsRegion: 'us-east-1',
-          }}
+          initialValues={{ provider: 'tongyi', engine: 'claude-code' }}
         >
-          {/* Provider Selection */}
-          <div style={{ fontSize: 15, fontWeight: 600, marginBottom: 16 }}>
-            {t('llm.selectProvider')}
-          </div>
-          {renderProviderGrid()}
-          <Form.Item name="provider" hidden>
-            <Input />
+          <Form.Item
+            label="执行引擎"
+            name="engine"
+            extra="Claude Code / Qoder 引擎在服务器上以 CLI 子进程方式运行,凭据经环境变量注入;HTTP 
引擎直连 OpenAI 兼容接口"
+          >
+            <Select options={ENGINE_OPTIONS} />
           </Form.Item>
 
-          <Divider style={{ margin: '20px 0 16px' }} />
-
-          {/* Connection Config */}
-          <div
-            style={{
-              fontSize: 15,
-              fontWeight: 600,
-              marginBottom: 16,
-              display: 'flex',
-              alignItems: 'center',
-            }}
+          <Form.Item
+            label="模型服务商"
+            name="provider"
+            rules={[{ required: true, message: '请选择模型服务商' }]}
           >
-            <Globe size={16} style={{ marginRight: 6 }} />
-            {t('llm.connectionConfig')}
-          </div>
-
-          <Row gutter={16}>
-            <Col span={12}>
-              <Form.Item
-                name="apiKey"
-                label={
-                  <span>
-                    <Key size={14} style={{ marginRight: 4, verticalAlign: 
'middle' }} />
-                    {t('llm.apiKey')}
-                    {currentProvider.requireApiKey && <Text type="danger"> 
*</Text>}
-                  </span>
-                }
-                rules={
-                  currentProvider.requireApiKey && !savedApiKey
-                    ? [{ required: true, message: t('llm.apiKeyRequired') }]
-                    : []
-                }
-                extra={t('llm.apiKeyEncrypted')}
-              >
-                <Input.Password
-                  placeholder={
-                    currentProvider.requireApiKey
-                      ? t('llm.apiKeyPlaceholder')
-                      : t('llm.apiKeyNoRequired')
-                  }
-                  onFocus={handleApiKeyFocus}
-                  onBlur={handleApiKeyBlur}
-                  visibilityToggle
-                />
-              </Form.Item>
-            </Col>
-            <Col span={12}>
-              <Form.Item
-                name="apiBase"
-                label={
-                  <span>
-                    <Globe size={14} style={{ marginRight: 4, verticalAlign: 
'middle' }} />
-                    {t('llm.apiBase')}
-                    {currentProvider.requireBaseUrl && <Text type="danger"> 
*</Text>}
-                  </span>
-                }
-                rules={
-                  currentProvider.requireBaseUrl
-                    ? [{ required: true, message: t('llm.apiBaseRequired') }]
-                    : []
-                }
-                extra={
-                  currentProvider.requireBaseUrl
-                    ? t('llm.apiBaseRequiredHint')
-                    : t('llm.apiBaseCustom')
-                }
-              >
-                <Input
-                  placeholder={currentProvider.defaultBaseUrl || 
'https://api.openai.com/v1'}
-                />
-              </Form.Item>
-            </Col>
-          </Row>
-
-          {/* Azure extra fields */}
-          {selectedProvider === 'azure' && (
-            <Row gutter={16}>
-              <Col span={12}>
-                <Form.Item
-                  name="deploymentName"
-                  label={t('llm.deploymentName')}
-                  rules={[{ required: true, message: 
t('llm.deploymentNameRequired') }]}
-                >
-                  <Input placeholder="my-gpt4-deployment" />
-                </Form.Item>
-              </Col>
-              <Col span={12}>
-                <Form.Item
-                  name="apiVersion"
-                  label={t('llm.apiVersion')}
-                  rules={[{ required: true, message: 
t('llm.apiVersionRequired') }]}
-                >
-                  <Select placeholder={t('llm.apiVersion')}>
-                    <Select.Option 
value="2024-02-15-preview">2024-02-15-preview</Select.Option>
-                    <Select.Option 
value="2024-08-01-preview">2024-08-01-preview</Select.Option>
-                    <Select.Option 
value="2025-01-01-preview">2025-01-01-preview</Select.Option>
-                  </Select>
-                </Form.Item>
-              </Col>
-            </Row>
-          )}
-
-          {/* AWS Bedrock extra fields */}
-          {selectedProvider === 'bedrock' && (
-            <Form.Item
-              name="awsRegion"
-              label={t('llm.awsRegion')}
-              rules={[{ required: true, message: t('llm.awsRegionRequired') }]}
-            >
-              <Select placeholder={t('llm.awsRegion')}>
-                <Select.Option value="us-east-1">us-east-1</Select.Option>
-                <Select.Option value="us-west-2">us-west-2</Select.Option>
-                <Select.Option value="eu-west-1">eu-west-1</Select.Option>
-                <Select.Option 
value="ap-northeast-1">ap-northeast-1</Select.Option>
-                <Select.Option 
value="ap-southeast-1">ap-southeast-1</Select.Option>
-              </Select>
-            </Form.Item>
-          )}
-
-          <Divider style={{ margin: '20px 0 16px' }} />
+            <Select options={PROVIDER_OPTIONS} onChange={handleProviderChange} 
/>
+          </Form.Item>
 
-          {/* Model Parameters */}
-          <div
-            style={{
-              fontSize: 15,
-              fontWeight: 600,
-              marginBottom: 16,
-              display: 'flex',
-              alignItems: 'center',
-            }}
+          <Form.Item
+            label="模型"
+            name="model"
+            rules={[{ required: true, message: '请选择或输入模型' }]}
+            extra="默认使用 qwen3.8-max"
           >
-            <Lightning size={16} style={{ marginRight: 6 }} />
-            {t('llm.modelParams')}
-          </div>
+            <Select showSearch options={modelOptions} placeholder="选择模型" />
+          </Form.Item>
 
           <Form.Item
-            name="model"
-            label={t('llm.model')}
-            rules={[{ required: true, message: t('llm.modelRequired') }]}
-            extra={t('llm.modelExtra')}
+            label="API Key"
+            name="apiKey"
+            extra={
+              apiKeyConfigured
+                ? '已配置(可能来自环境变量 RMQ_LLM_TOKEN);留空将保留现有密钥'
+                : '请输入 API Key'
+            }
           >
-            <Select
-              showSearch
-              loading={modelsLoading}
-              placeholder={modelsLoading ? t('llm.modelsLoading') : 
currentProvider.defaultModel}
-              filterOption={(input, option) =>
-                (option?.label ?? 
'').toLowerCase().includes(input.toLowerCase())
-              }
-              notFoundContent={modelsLoading ? t('common.loading') : 
t('llm.modelsNotFound')}
-              options={modelOptions}
+            <Input.Password
+              placeholder={apiKeyConfigured ? '••••••••(已配置,留空保留)' : 'sk-...'}
+              autoComplete="new-password"
             />
           </Form.Item>
+          {apiKeyConfigured && (
+            <div style={{ marginTop: -16, marginBottom: 16 }}>
+              <Tag color="green">密钥已配置</Tag>
+            </div>
+          )}
 
-          <Row gutter={16}>
-            <Col span={12}>
-              <Form.Item noStyle shouldUpdate={(prev, cur) => prev.maxTokens 
!== cur.maxTokens}>
-                {({ getFieldValue }) => (
-                  <Form.Item
-                    name="maxTokens"
-                    label={
-                      <span>
-                        {t('llm.maxTokens')}
-                        <Text type="secondary" style={{ marginLeft: 8, 
fontSize: 12 }}>
-                          {getFieldValue('maxTokens') ?? 4096}
-                        </Text>
-                      </span>
-                    }
-                  >
-                    <Slider
-                      min={256}
-                      max={128000}
-                      step={256}
-                      marks={{
-                        2048: { label: '2K', style: { fontSize: 11 } },
-                        8192: { label: '8K', style: { fontSize: 11 } },
-                        32768: { label: '32K', style: { fontSize: 11 } },
-                        128000: { label: '128K', style: { fontSize: 11 } },
-                      }}
-                    />
-                  </Form.Item>
-                )}
-              </Form.Item>
-            </Col>
-            <Col span={12}>
-              <Form.Item noStyle shouldUpdate={(prev, cur) => prev.temperature 
!== cur.temperature}>
-                {({ getFieldValue }) => (
-                  <Form.Item
-                    name="temperature"
-                    label={
-                      <span>
-                        {t('llm.temperature')}
-                        <Text type="secondary" style={{ marginLeft: 8, 
fontSize: 12 }}>
-                          {getFieldValue('temperature') ?? 0.7}
-                        </Text>
-                      </span>
-                    }
-                    extra={t('llm.temperatureExtra')}
-                  >
-                    <Slider
-                      min={0}
-                      max={2}
-                      step={0.1}
-                      marks={{
-                        0: { label: '0', style: { fontSize: 11 } },
-                        0.7: { label: '0.7', style: { fontSize: 11 } },
-                        1: { label: '1', style: { fontSize: 11 } },
-                        2: { label: '2', style: { fontSize: 11 } },
-                      }}
-                    />
-                  </Form.Item>
-                )}
-              </Form.Item>
-            </Col>
-          </Row>
+          <Form.Item
+            label="API Base URL"
+            name="apiBase"
+            rules={[
+              { required: true, message: '请输入 API Base URL' },
+              {
+                pattern: /^https?:\/\/.+/,
+                message: '需为 http/https 地址',
+              },
+            ]}
+          >
+            <Input 
placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1"; />
+          </Form.Item>
 
-          <Divider style={{ margin: '20px 0 16px' }} />
+          <Form.Item label="Temperature" name="temperature">
+            <Slider min={0} max={2} step={0.1} marks={{ 0: '0', 0.7: '0.7', 2: 
'2' }} />
+          </Form.Item>
 
-          {/* Actions */}
-          <div
-            style={{
-              display: 'flex',
-              alignItems: 'center',
-              justifyContent: 'space-between',
-              flexWrap: 'wrap',
-              gap: 12,
-            }}
-          >
-            <Space size="middle">
-              <Button
-                type="primary"
-                icon={<FloppyDisk size={14} />}
-                onClick={handleSave}
-                loading={loading}
-                size="large"
-              >
-                {t('llm.saveConfig')}
-              </Button>
-              <Button
-                icon={<Lightning size={14} />}
-                onClick={handleTestConnection}
-                loading={testLoading}
-                size="large"
-              >
-                {testLoading ? t('llm.testing') : t('llm.testConnection')}
-              </Button>
-            </Space>
-            <div style={{ display: 'flex', alignItems: 'center' }}>
-              <ShieldCheck size={14} style={{ marginRight: 4 }} />
-              <Text type="secondary" style={{ fontSize: 12 }}>
-                {t('llm.securityNote')}
-              </Text>
-            </div>
-          </div>
+          <Form.Item label="Max Tokens" name="maxTokens">
+            <InputNumber min={1} max={200000} style={{ width: 200 }} />
+          </Form.Item>
 
-          {/* Test Result */}
           {testResult && (
             <Alert
-              style={{ marginTop: 16, borderRadius: 8 }}
+              style={{ marginBottom: 16 }}
               type={testResult.success ? 'success' : 'error'}
               showIcon
-              icon={
-                testResult.success ? (
-                  <CheckCircle size={16} weight="fill" />
-                ) : (
-                  <XCircle size={16} weight="fill" />
-                )
-              }
               message={testResult.msg}
-              description={
-                !testResult.success && (testResult.hint || testResult.code) ? (
-                  <Space direction="vertical" size={4}>
-                    {testResult.hint && <Text>{testResult.hint}</Text>}
-                    {testResult.code && <Text code>{testResult.code}</Text>}
-                  </Space>
-                ) : undefined
-              }
-              closable
-              onClose={() => setTestResult(null)}
+              description={testResult.hint}
             />
           )}
+
+          <Form.Item style={{ marginBottom: 0 }}>
+            <Space>
+              <Button type="primary" loading={saving} onClick={() => void 
handleSave()}>
+                保存
+              </Button>
+              <Button loading={testing} onClick={() => void handleTest()}>
+                测试连接
+              </Button>
+            </Space>
+          </Form.Item>
         </Form>
       </Card>
     </div>
diff --git a/web/src/pages/studio/__tests__/LlmSettingsAsyncState.test.tsx 
b/web/src/pages/studio/__tests__/LlmSettingsAsyncState.test.tsx
deleted file mode 100644
index 62290edc..00000000
--- a/web/src/pages/studio/__tests__/LlmSettingsAsyncState.test.tsx
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { App } from 'antd';
-import { act, fireEvent, render, screen, waitFor } from 
'@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { StrictMode } from 'react';
-import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
-import type { LlmModelsResult } from '../../../api/llm';
-import { LangProvider } from '../../../i18n/LangContext';
-import LlmSettingsPage from '../LlmSettings';
-
-const apiMocks = vi.hoisted(() => ({
-  getLlmConfig: vi.fn(),
-  getLlmModels: vi.fn(),
-  saveLlmConfig: vi.fn(),
-  testLlmConnection: vi.fn(),
-}));
-
-vi.mock('../../../api/llm', () => apiMocks);
-
-beforeAll(() => {
-  Object.defineProperty(window, 'matchMedia', {
-    writable: true,
-    value: vi.fn().mockImplementation((query: string) => ({
-      matches: false,
-      media: query,
-      onchange: null,
-      addListener: vi.fn(),
-      removeListener: vi.fn(),
-      addEventListener: vi.fn(),
-      removeEventListener: vi.fn(),
-      dispatchEvent: vi.fn(),
-    })),
-  });
-});
-
-const createDeferred = <T,>() => {
-  let resolve!: (value: T) => void;
-  let reject!: (reason?: unknown) => void;
-  const promise = new Promise<T>((resolvePromise, rejectPromise) => {
-    resolve = resolvePromise;
-    reject = rejectPromise;
-  });
-  return { promise, resolve, reject };
-};
-
-const OPENAI_CONFIG = {
-  provider: 'openai',
-  apiBase: 'https://api.openai.com/v1',
-  model: 'gpt-4o',
-  maxTokens: 4096,
-  temperature: 0.7,
-  enabled: true,
-  apiKeyConfigured: true,
-};
-
-const renderPage = (strict = false) => {
-  const page = (
-    <App>
-      <LangProvider>
-        <LlmSettingsPage />
-      </LangProvider>
-    </App>
-  );
-  return render(strict ? <StrictMode>{page}</StrictMode> : page);
-};
-
-describe('LlmSettingsPage async request ownership', () => {
-  beforeEach(() => {
-    vi.clearAllMocks();
-    apiMocks.getLlmConfig.mockResolvedValue(OPENAI_CONFIG);
-    apiMocks.getLlmModels.mockResolvedValue({
-      status: 0,
-      data: [{ id: 'gpt-4o' }],
-    });
-    apiMocks.saveLlmConfig.mockResolvedValue({ status: 0 });
-    apiMocks.testLlmConnection.mockResolvedValue({ status: 0 });
-  });
-
-  it('does not replace a newly selected provider model list with an older 
response', async () => {
-    const oldProviderModels = createDeferred<LlmModelsResult>();
-    apiMocks.getLlmModels.mockReturnValue(oldProviderModels.promise);
-    const user = userEvent.setup();
-    renderPage();
-
-    await waitFor(() => 
expect(apiMocks.getLlmModels).toHaveBeenCalledTimes(1));
-    await user.click(screen.getByText('DeepSeek', { selector: 'div' }));
-    expect(await screen.findByText('deepseek-chat')).toBeInTheDocument();
-
-    await act(async () => {
-      oldProviderModels.resolve({
-        status: 0,
-        data: [{ id: 'openai-only-late-model' }],
-      });
-    });
-
-    await user.click(screen.getByRole('combobox'));
-    expect(
-      screen.queryByText('openai-only-late-model', {
-        selector: '.ant-select-item-option-content',
-      }),
-    ).not.toBeInTheDocument();
-    expect(
-      await screen.findByText('deepseek-reasoner', {
-        selector: '.ant-select-item-option-content',
-      }),
-    ).toBeInTheDocument();
-  });
-
-  it('keeps the model selector loading while the provider model request is 
pending', async () => {
-    const providerModels = createDeferred<LlmModelsResult>();
-    apiMocks.getLlmModels.mockReturnValue(providerModels.promise);
-    renderPage();
-
-    await waitFor(() => 
expect(apiMocks.getLlmModels).toHaveBeenCalledTimes(1));
-
-    
expect(screen.getByRole('combobox').closest('.ant-select')).toHaveClass('ant-select-loading');
-  });
-
-  it('does not let a delayed initial config replace a provider selected after 
request start', async () => {
-    const initialConfig = createDeferred<typeof OPENAI_CONFIG>();
-    apiMocks.getLlmConfig.mockReturnValueOnce(initialConfig.promise);
-    renderPage();
-
-    expect(apiMocks.getLlmConfig).toHaveBeenCalledTimes(1);
-    fireEvent.click(screen.getByText('DeepSeek', { selector: 'div' }));
-    expect(screen.getByRole('textbox', { name: 'API Base URL' })).toHaveValue(
-      'https://api.deepseek.com/v1',
-    );
-
-    await act(async () => {
-      initialConfig.resolve(OPENAI_CONFIG);
-    });
-
-    expect(screen.getByRole('textbox', { name: 'API Base URL' })).toHaveValue(
-      'https://api.deepseek.com/v1',
-    );
-    expect(
-      screen.getByText('DeepSeek', { selector: 'div' 
}).parentElement?.parentElement,
-    ).toHaveStyle('border: 2px solid rgb(77, 107, 254)');
-    expect(apiMocks.getLlmModels).not.toHaveBeenCalled();
-  });
-
-  it('does not start an old-provider model request after an earlier connection 
test completes', async () => {
-    const oldProviderTest = createDeferred<{ status: number }>();
-    apiMocks.testLlmConnection.mockReturnValue(oldProviderTest.promise);
-    apiMocks.getLlmModels
-      .mockResolvedValueOnce({ status: 0, data: [{ id: 'initial-openai-model' 
}] })
-      .mockResolvedValueOnce({ status: 0, data: [{ id: 'late-openai-model' }] 
});
-    const user = userEvent.setup();
-    renderPage();
-
-    await waitFor(() => 
expect(apiMocks.getLlmModels).toHaveBeenCalledTimes(1));
-    await user.click(screen.getByRole('button', { name: '连接测试' }));
-    await waitFor(() => 
expect(apiMocks.testLlmConnection).toHaveBeenCalledTimes(1));
-    await user.click(screen.getByText('DeepSeek', { selector: 'div' }));
-    expect(await screen.findByText('deepseek-chat')).toBeInTheDocument();
-
-    await act(async () => {
-      oldProviderTest.resolve({ status: 0 });
-    });
-    await waitFor(() => 
expect(apiMocks.saveLlmConfig).toHaveBeenCalledTimes(1));
-
-    await user.click(screen.getByRole('combobox'));
-    expect(
-      screen.queryByText('late-openai-model', {
-        selector: '.ant-select-item-option-content',
-      }),
-    ).not.toBeInTheDocument();
-    expect(
-      await screen.findByText('deepseek-reasoner', {
-        selector: '.ant-select-item-option-content',
-      }),
-    ).toBeInTheDocument();
-    expect(apiMocks.getLlmModels).toHaveBeenCalledTimes(1);
-  });
-
-  it('ignores configuration from the discarded StrictMode lifecycle', async () 
=> {
-    const discardedConfig = createDeferred<typeof OPENAI_CONFIG>();
-    apiMocks.getLlmConfig
-      .mockReturnValueOnce(discardedConfig.promise)
-      .mockResolvedValue(OPENAI_CONFIG);
-    renderPage(true);
-
-    await waitFor(() => 
expect(apiMocks.getLlmModels).toHaveBeenCalledTimes(1));
-
-    await act(async () => {
-      discardedConfig.resolve({
-        ...OPENAI_CONFIG,
-        provider: 'deepseek',
-        apiBase: 'https://api.deepseek.com/v1',
-        model: 'deepseek-chat',
-      });
-    });
-
-    expect(apiMocks.getLlmModels).toHaveBeenCalledTimes(1);
-    expect(screen.getByRole('textbox', { name: 'API Base URL' })).toHaveValue(
-      'https://api.openai.com/v1',
-    );
-    expect(
-      screen.getByText('OpenAI', { selector: 'div' 
}).parentElement?.parentElement,
-    ).toHaveStyle('border: 2px solid rgb(16, 163, 127)');
-  });
-});
diff --git a/web/src/pages/home/__tests__/HomePage.test.tsx 
b/web/src/pages/studio/__tests__/LlmSettingsPage.test.tsx
similarity index 54%
copy from web/src/pages/home/__tests__/HomePage.test.tsx
copy to web/src/pages/studio/__tests__/LlmSettingsPage.test.tsx
index c2ce3cd2..022cfc44 100644
--- a/web/src/pages/home/__tests__/HomePage.test.tsx
+++ b/web/src/pages/studio/__tests__/LlmSettingsPage.test.tsx
@@ -15,23 +15,20 @@
  * limitations under the License.
  */
 
-import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { App } from 'antd';
 import { render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import { App } from 'antd';
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
 import { LangProvider } from '../../../i18n/LangContext';
-import HomePage from '../index';
+import LlmSettingsPage from '../LlmSettings';
 
-const navigateMock = vi.hoisted(() => vi.fn());
 const llmApiMocks = vi.hoisted(() => ({
   getLlmConfig: vi.fn(),
+  saveLlmConfig: vi.fn(),
+  testLlmConnection: vi.fn(),
   getLlmModels: vi.fn(),
 }));
 
-vi.mock('react-router-dom', () => ({
-  useNavigate: () => navigateMock,
-}));
-
 vi.mock('../../../api/llm', () => llmApiMocks);
 
 beforeAll(() => {
@@ -50,55 +47,51 @@ beforeAll(() => {
   });
 });
 
-beforeEach(() => {
-  vi.clearAllMocks();
-  llmApiMocks.getLlmConfig.mockResolvedValue({
-    provider: 'openai',
-    apiBase: 'https://api.example.com/v1',
-    model: 'provider-model',
-    maxTokens: 4096,
-    temperature: 0.7,
-    enabled: true,
-    ready: true,
-  });
-  llmApiMocks.getLlmModels.mockResolvedValue({
-    status: 0,
-    data: [{ id: 'provider-model' }, { id: 'provider-model-secondary' }],
-    source: 'provider',
-  });
-});
-
-const renderHome = () =>
+const renderPage = () =>
   render(
     <App>
       <LangProvider>
-        <HomePage />
+        <LlmSettingsPage />
       </LangProvider>
     </App>,
   );
 
-describe('HomePage LLM models', () => {
-  it('loads configured provider models instead of hard-coded options', async 
() => {
-    renderHome();
+describe('LlmSettingsPage', () => {
+  beforeEach(() => {
+    llmApiMocks.getLlmConfig.mockResolvedValue({
+      provider: 'tongyi',
+      apiBase: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
+      model: 'qwen3.8-max',
+      maxTokens: 4096,
+      temperature: 0.7,
+      enabled: true,
+      apiKeyConfigured: true,
+    });
+    llmApiMocks.getLlmModels.mockResolvedValue({
+      status: 0,
+      data: [{ id: 'qwen3.8-max' }, { id: 'qwen-max' }],
+    });
+    llmApiMocks.saveLlmConfig.mockResolvedValue({ status: 0 });
+    llmApiMocks.testLlmConnection.mockResolvedValue({ status: 0, msg: 'ok' });
+  });
+
+  it('loads the saved config and shows the configured-key badge', async () => {
+    renderPage();
 
-    expect(await screen.findByText('provider-model')).toBeInTheDocument();
-    expect(screen.queryByText('qwen3.7-max')).not.toBeInTheDocument();
+    expect(await screen.findByText('密钥已配置')).toBeInTheDocument();
+    expect(screen.getByText('qwen3.8-max')).toBeInTheDocument();
   });
 
-  it('submits the configured provider model to the AI page', async () => {
+  it('saves without sending an apiKey when the input stays empty', async () => 
{
     const user = userEvent.setup();
-    renderHome();
-    await screen.findByText('provider-model');
+    renderPage();
 
-    await user.type(screen.getByPlaceholderText('向 RocketMQ Bot 
提问,全程加密、安全、可信'), '查看集群状态{enter}');
+    await screen.findByText('密钥已配置');
+    await user.click(screen.getByRole('button', { name: /保\s*存/ }));
 
-    await waitFor(() => {
-      expect(navigateMock).toHaveBeenCalledWith('/ai', {
-        state: {
-          prompt: '查看集群状态',
-          model: 'provider-model',
-        },
-      });
-    });
+    await waitFor(() => 
expect(llmApiMocks.saveLlmConfig).toHaveBeenCalledTimes(1));
+    const payload = llmApiMocks.saveLlmConfig.mock.calls[0][0];
+    expect(payload).toMatchObject({ provider: 'tongyi', model: 'qwen3.8-max' 
});
+    expect(payload.apiKey).toBeUndefined();
   });
 });
diff --git a/web/src/pages/studio/llmModelOptions.ts 
b/web/src/pages/studio/llmModelOptions.ts
index 5e693db8..ad1ebd30 100644
--- a/web/src/pages/studio/llmModelOptions.ts
+++ b/web/src/pages/studio/llmModelOptions.ts
@@ -19,7 +19,15 @@ export const FALLBACK_MODELS: Record<string, string[]> = {
   openai: ['gpt-4o', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'],
   azure: ['gpt-4o', 'gpt-4', 'gpt-3.5-turbo'],
   deepseek: ['deepseek-chat', 'deepseek-reasoner'],
-  tongyi: ['qwen-max', 'qwen-plus', 'qwen-turbo'],
+  tongyi: [
+    'qwen3.8-max',
+    'qwen3.7-max',
+    'qwen3.7-plus',
+    'deepseek-v4-pro',
+    'deepseek-v4-flash',
+    'MiniMax-M2.5',
+    'glm-5.2',
+  ],
   ollama: ['llama3', 'mistral', 'gemma2', 'qwen2.5'],
   bedrock: ['anthropic.claude-3-sonnet', 'anthropic.claude-3-haiku', 
'meta.llama3-70b'],
 };
diff --git a/web/src/pages/ai/chatDraft.ts b/web/src/stores/engineStore.ts
similarity index 59%
copy from web/src/pages/ai/chatDraft.ts
copy to web/src/stores/engineStore.ts
index 2ad1a936..9fec84dc 100644
--- a/web/src/pages/ai/chatDraft.ts
+++ b/web/src/stores/engineStore.ts
@@ -15,19 +15,25 @@
  * limitations under the License.
  */
 
-export interface ChatDraft {
-  prompt: string;
-  model?: string;
-}
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
 
-export function getChatDraft(state: unknown): ChatDraft | null {
-  if (typeof state !== 'object' || state === null) return null;
-  const candidate = state as Record<string, unknown>;
-  if (typeof candidate.prompt !== 'string' || !candidate.prompt.trim()) return 
null;
-  const model = typeof candidate.model === 'string' ? candidate.model.trim() : 
'';
+export type AgentEngine = 'claude-code' | 'qoder' | 'http';
 
-  return {
-    prompt: candidate.prompt.trim(),
-    ...(model ? { model } : {}),
-  };
+interface EngineState {
+  engine: AgentEngine;
+  setEngine: (engine: AgentEngine) => void;
 }
+
+// Per-user agent engine preference, kept in the browser (not in global 
settings).
+export const useEngineStore = create<EngineState>()(
+  persist(
+    (set) => ({
+      engine: 'claude-code',
+      setEngine: (engine) => set({ engine }),
+    }),
+    {
+      name: 'rocketmq-studio-agent-engine',
+    },
+  ),
+);
diff --git a/web/vite.config.ts b/web/vite.config.ts
index 5c6fb649..571f6d5e 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -20,6 +20,9 @@ export default defineConfig(({ mode }) => {
       environment: 'jsdom',
       setupFiles: './src/test/setup.ts',
       css: true,
+      // 32-core box: run test files with full parallelism.
+      maxWorkers: 32,
+      minWorkers: 4,
       // antd interactions driven through userEvent are slow in jsdom, and the 
default
       // 5s budget is exceeded once the whole suite runs in parallel.
       testTimeout: 20000,

Reply via email to