mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(agent): resolve relative workspaceBasePath under workspace root
This commit is contained in:
parent
cbdd70379b
commit
9e9a96f674
@ -56,6 +56,8 @@ import vip.mate.channel.web.ChatStreamTracker;
|
||||
import vip.mate.wiki.service.WikiContextService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@ -358,21 +360,29 @@ public class AgentGraphBuilder {
|
||||
agent.topP = runtimeModel.getTopP();
|
||||
agent.toolCallingEnabled = toolCallingEnabled;
|
||||
|
||||
// Agent 级别覆盖优先,否则继承工作区
|
||||
if (entity.getWorkspaceBasePath() != null && !entity.getWorkspaceBasePath().isBlank()) {
|
||||
agent.workspaceBasePath = entity.getWorkspaceBasePath();
|
||||
log.info("Agent {} using agent-level basePath: {}", entity.getName(), agent.workspaceBasePath);
|
||||
} else if (entity.getWorkspaceId() != null) {
|
||||
// Agent-level override takes priority; a relative override is resolved
|
||||
// under the workspace basePath so admins can express agent directories
|
||||
// relative to the workspace root (matching the UI hint).
|
||||
String workspaceBase = null;
|
||||
if (entity.getWorkspaceId() != null) {
|
||||
try {
|
||||
var workspace = workspaceService.getById(entity.getWorkspaceId());
|
||||
if (workspace != null && workspace.getBasePath() != null && !workspace.getBasePath().isBlank()) {
|
||||
agent.workspaceBasePath = workspace.getBasePath();
|
||||
log.info("Agent {} inherited workspace basePath: {}", entity.getName(), agent.workspaceBasePath);
|
||||
if (workspace != null) {
|
||||
workspaceBase = workspace.getBasePath();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to lookup workspace basePath for agent {}: {}", entity.getName(), e.getMessage());
|
||||
log.warn("Failed to lookup workspace basePath for agent {}: {}",
|
||||
entity.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
String resolvedBase = resolveAgentBasePath(entity.getWorkspaceBasePath(), workspaceBase);
|
||||
if (resolvedBase != null && !resolvedBase.isBlank()) {
|
||||
agent.workspaceBasePath = resolvedBase;
|
||||
boolean fromOverride = entity.getWorkspaceBasePath() != null
|
||||
&& !entity.getWorkspaceBasePath().isBlank();
|
||||
log.info("Agent {} basePath = {} (source: {})",
|
||||
entity.getName(), resolvedBase, fromOverride ? "agent-override" : "workspace");
|
||||
}
|
||||
|
||||
log.info("Built agent instance: {} (type={}, protocol={}, tools={}, toolCallingEnabled={})",
|
||||
entity.getName(), entity.getAgentType(), protocol.getId(),
|
||||
@ -1150,6 +1160,37 @@ public class AgentGraphBuilder {
|
||||
return reordered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective working directory for an agent.
|
||||
* <p>Precedence:
|
||||
* <ol>
|
||||
* <li>When the agent-level override is set, it wins.</li>
|
||||
* <li>An absolute override is used verbatim.</li>
|
||||
* <li>A relative override is resolved <em>under</em> the workspace basePath
|
||||
* when the workspace has one, matching the UI hint that agent paths
|
||||
* are relative to the workspace root.</li>
|
||||
* <li>A relative override with no workspace basePath is used as-is
|
||||
* (resolves against the JVM working directory at file-tool time).</li>
|
||||
* <li>With no override, the workspace basePath is inherited verbatim;
|
||||
* returns {@code null} when neither side has a value.</li>
|
||||
* </ol>
|
||||
*/
|
||||
static String resolveAgentBasePath(String agentOverride, String workspaceBase) {
|
||||
boolean hasOverride = agentOverride != null && !agentOverride.isBlank();
|
||||
boolean hasWorkspace = workspaceBase != null && !workspaceBase.isBlank();
|
||||
if (!hasOverride) {
|
||||
return hasWorkspace ? workspaceBase : null;
|
||||
}
|
||||
Path overridePath = Paths.get(agentOverride);
|
||||
if (overridePath.isAbsolute()) {
|
||||
return agentOverride;
|
||||
}
|
||||
if (hasWorkspace) {
|
||||
return Paths.get(workspaceBase).resolve(agentOverride).toString();
|
||||
}
|
||||
return agentOverride;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first enabled chat model whose provider is fully configured.
|
||||
* Used as a fallback when the default model's provider is not available.
|
||||
|
||||
@ -78,7 +78,11 @@ public class AgentEntity {
|
||||
/** 默认思考深度:off / low / medium / high / max,null 表示跟随模型默认 */
|
||||
private String defaultThinkingLevel;
|
||||
|
||||
/** Agent 级别的工作目录覆盖,为 null 时继承工作区 basePath */
|
||||
/**
|
||||
* Agent-level working directory override. When non-blank, takes priority
|
||||
* over the workspace's basePath; relative values are resolved under the
|
||||
* workspace basePath. Null/blank means inherit the workspace value.
|
||||
*/
|
||||
@TableField(value = "workspace_base_path", updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String workspaceBasePath;
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
-- V100: Add workspace_base_path column to mate_agent for Agent-level directory override.
|
||||
-- When set, this overrides the workspace-level basePath for this Agent only.
|
||||
-- V125: Add workspace_base_path column to mate_agent for agent-level directory override.
|
||||
-- When set, this overrides the workspace-level basePath for this agent only.
|
||||
ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS workspace_base_path VARCHAR(512) DEFAULT NULL;
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
package vip.mate.agent;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* Verifies the agent-vs-workspace basePath precedence rules used by
|
||||
* {@link AgentGraphBuilder#resolveAgentBasePath(String, String)}.
|
||||
*
|
||||
* <p>The UI advertises agent-level paths as "relative to the workspace root",
|
||||
* so a relative agent override must compose with the workspace basePath rather
|
||||
* than fall through to the JVM working directory.
|
||||
*/
|
||||
class AgentGraphBuilderBasePathResolutionTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Both null/blank → null (no working directory configured)")
|
||||
void noOverrideNoWorkspace_returnsNull() {
|
||||
assertNull(AgentGraphBuilder.resolveAgentBasePath(null, null));
|
||||
assertNull(AgentGraphBuilder.resolveAgentBasePath("", ""));
|
||||
assertNull(AgentGraphBuilder.resolveAgentBasePath(" ", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("No agent override → workspace basePath inherited verbatim")
|
||||
void noOverride_inheritsWorkspace() {
|
||||
assertEquals("/srv/ws-root",
|
||||
AgentGraphBuilder.resolveAgentBasePath(null, "/srv/ws-root"));
|
||||
assertEquals("/srv/ws-root",
|
||||
AgentGraphBuilder.resolveAgentBasePath("", "/srv/ws-root"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Agent override is absolute → used verbatim, workspace ignored")
|
||||
@DisabledOnOs(OS.WINDOWS)
|
||||
void absoluteOverride_usedAsIs_unix() {
|
||||
assertEquals("/opt/agents/code-review",
|
||||
AgentGraphBuilder.resolveAgentBasePath("/opt/agents/code-review", "/srv/ws-root"));
|
||||
assertEquals("/opt/agents/code-review",
|
||||
AgentGraphBuilder.resolveAgentBasePath("/opt/agents/code-review", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Agent override is absolute (Windows) → used verbatim")
|
||||
@EnabledOnOs(OS.WINDOWS)
|
||||
void absoluteOverride_usedAsIs_windows() {
|
||||
assertEquals("C:\\agents\\code-review",
|
||||
AgentGraphBuilder.resolveAgentBasePath("C:\\agents\\code-review", "C:\\ws-root"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Relative agent override + workspace basePath → resolved under workspace")
|
||||
void relativeOverride_resolvedUnderWorkspace() {
|
||||
String expected = Paths.get("/srv/ws-root").resolve("projects/code-review").toString();
|
||||
assertEquals(expected,
|
||||
AgentGraphBuilder.resolveAgentBasePath("projects/code-review", "/srv/ws-root"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Relative agent override with no workspace → used verbatim (legacy fallback)")
|
||||
void relativeOverride_noWorkspace_usedAsIs() {
|
||||
assertEquals("projects/code-review",
|
||||
AgentGraphBuilder.resolveAgentBasePath("projects/code-review", null));
|
||||
assertEquals("projects/code-review",
|
||||
AgentGraphBuilder.resolveAgentBasePath("projects/code-review", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Blank workspace basePath treated like null when override is relative")
|
||||
void relativeOverride_blankWorkspace_usedAsIs() {
|
||||
assertEquals("agent-dir",
|
||||
AgentGraphBuilder.resolveAgentBasePath("agent-dir", " "));
|
||||
}
|
||||
}
|
||||
@ -1164,7 +1164,7 @@ export default {
|
||||
maxIterations: 'Max Iterations',
|
||||
defaultThinkingLevel: 'Default Thinking Level',
|
||||
workspaceBasePath: 'Working Directory',
|
||||
workspaceBasePathHint: 'Optional. Set a dedicated working directory for this employee (relative to workspace root). Leave blank to inherit the workspace default.',
|
||||
workspaceBasePathHint: 'Optional. Set a dedicated working directory for this employee. Relative paths resolve under the workspace root; absolute paths are used as-is. Leave blank to inherit the workspace default.',
|
||||
modelName: 'Model',
|
||||
modelGlobalDefault: 'Use global default',
|
||||
modelHint: 'Override the global default model for this employee. Leave blank to follow Settings → Models.',
|
||||
|
||||
@ -1056,7 +1056,7 @@ export default {
|
||||
maxIterations: '最大迭代次数',
|
||||
defaultThinkingLevel: '默认思考深度',
|
||||
workspaceBasePath: '工作目录',
|
||||
workspaceBasePathHint: '可选。为该员工指定独立的工作目录(相对于工作区根目录)。留空则继承工作区默认目录。',
|
||||
workspaceBasePathHint: '可选。为该员工指定独立的工作目录:相对路径会基于工作区根目录解析,也可填写绝对路径。留空则继承工作区默认目录。',
|
||||
modelName: '模型',
|
||||
modelGlobalDefault: '使用全局默认模型',
|
||||
modelHint: '为该员工单独指定模型,留空则跟随「设置 → 模型」中的全局默认。',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user