mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 19:45:08 +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 vip.mate.wiki.service.WikiContextService;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -358,21 +360,29 @@ public class AgentGraphBuilder {
|
|||||||
agent.topP = runtimeModel.getTopP();
|
agent.topP = runtimeModel.getTopP();
|
||||||
agent.toolCallingEnabled = toolCallingEnabled;
|
agent.toolCallingEnabled = toolCallingEnabled;
|
||||||
|
|
||||||
// Agent 级别覆盖优先,否则继承工作区
|
// Agent-level override takes priority; a relative override is resolved
|
||||||
if (entity.getWorkspaceBasePath() != null && !entity.getWorkspaceBasePath().isBlank()) {
|
// under the workspace basePath so admins can express agent directories
|
||||||
agent.workspaceBasePath = entity.getWorkspaceBasePath();
|
// relative to the workspace root (matching the UI hint).
|
||||||
log.info("Agent {} using agent-level basePath: {}", entity.getName(), agent.workspaceBasePath);
|
String workspaceBase = null;
|
||||||
} else if (entity.getWorkspaceId() != null) {
|
if (entity.getWorkspaceId() != null) {
|
||||||
try {
|
try {
|
||||||
var workspace = workspaceService.getById(entity.getWorkspaceId());
|
var workspace = workspaceService.getById(entity.getWorkspaceId());
|
||||||
if (workspace != null && workspace.getBasePath() != null && !workspace.getBasePath().isBlank()) {
|
if (workspace != null) {
|
||||||
agent.workspaceBasePath = workspace.getBasePath();
|
workspaceBase = workspace.getBasePath();
|
||||||
log.info("Agent {} inherited workspace basePath: {}", entity.getName(), agent.workspaceBasePath);
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} 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={})",
|
log.info("Built agent instance: {} (type={}, protocol={}, tools={}, toolCallingEnabled={})",
|
||||||
entity.getName(), entity.getAgentType(), protocol.getId(),
|
entity.getName(), entity.getAgentType(), protocol.getId(),
|
||||||
@ -1150,6 +1160,37 @@ public class AgentGraphBuilder {
|
|||||||
return reordered;
|
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.
|
* Finds the first enabled chat model whose provider is fully configured.
|
||||||
* Used as a fallback when the default model's provider is not available.
|
* 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 表示跟随模型默认 */
|
/** 默认思考深度:off / low / medium / high / max,null 表示跟随模型默认 */
|
||||||
private String defaultThinkingLevel;
|
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)
|
@TableField(value = "workspace_base_path", updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private String workspaceBasePath;
|
private String workspaceBasePath;
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
-- V100: Add workspace_base_path column to mate_agent for Agent-level directory override.
|
-- 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.
|
-- 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;
|
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',
|
maxIterations: 'Max Iterations',
|
||||||
defaultThinkingLevel: 'Default Thinking Level',
|
defaultThinkingLevel: 'Default Thinking Level',
|
||||||
workspaceBasePath: 'Working Directory',
|
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',
|
modelName: 'Model',
|
||||||
modelGlobalDefault: 'Use global default',
|
modelGlobalDefault: 'Use global default',
|
||||||
modelHint: 'Override the global default model for this employee. Leave blank to follow Settings → Models.',
|
modelHint: 'Override the global default model for this employee. Leave blank to follow Settings → Models.',
|
||||||
|
|||||||
@ -1056,7 +1056,7 @@ export default {
|
|||||||
maxIterations: '最大迭代次数',
|
maxIterations: '最大迭代次数',
|
||||||
defaultThinkingLevel: '默认思考深度',
|
defaultThinkingLevel: '默认思考深度',
|
||||||
workspaceBasePath: '工作目录',
|
workspaceBasePath: '工作目录',
|
||||||
workspaceBasePathHint: '可选。为该员工指定独立的工作目录(相对于工作区根目录)。留空则继承工作区默认目录。',
|
workspaceBasePathHint: '可选。为该员工指定独立的工作目录:相对路径会基于工作区根目录解析,也可填写绝对路径。留空则继承工作区默认目录。',
|
||||||
modelName: '模型',
|
modelName: '模型',
|
||||||
modelGlobalDefault: '使用全局默认模型',
|
modelGlobalDefault: '使用全局默认模型',
|
||||||
modelHint: '为该员工单独指定模型,留空则跟随「设置 → 模型」中的全局默认。',
|
modelHint: '为该员工单独指定模型,留空则跟随「设置 → 模型」中的全局默认。',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user