feat(agent): optional agent-level workspace basePath override (#212)

* feat(agent): optional agent-level workspace basePath override

Add workspaceBasePath field to AgentEntity that optionally overrides
the workspace-level basePath. When set, the agent uses its own directory;
when null, it inherits the workspace's basePath (existing behavior).

- AgentEntity: new workspaceBasePath field with ALWAYS update strategy
- AgentGraphBuilder: agent-level override takes priority over workspace
- Flyway migration V121 for H2 and MySQL
- UI: form input in basic tab with i18n (zh-CN, en-US)

* fix(agent): rename migration V121→V125 to avoid Flyway conflict with upstream

Upstream already has V121__tool_disclosure_tier.sql. Rename our
migration to V125 (next available after V124).

* fix(agent): make MySQL V125 migration idempotent

Use INFORMATION_SCHEMA check before ADD COLUMN to avoid
"Duplicate column name" error on re-deploy.
This commit is contained in:
倪程伟 2026-05-25 15:42:01 +08:00 committed by GitHub
parent 696646f614
commit cbdd70379b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 39 additions and 3 deletions

View File

@ -358,13 +358,16 @@ public class AgentGraphBuilder {
agent.topP = runtimeModel.getTopP();
agent.toolCallingEnabled = toolCallingEnabled;
// 查找工作区活动目录
if (entity.getWorkspaceId() != null) {
// 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) {
try {
var workspace = workspaceService.getById(entity.getWorkspaceId());
if (workspace != null && workspace.getBasePath() != null && !workspace.getBasePath().isBlank()) {
agent.workspaceBasePath = workspace.getBasePath();
log.info("Agent {} bound to workspace basePath: {}", entity.getName(), agent.workspaceBasePath);
log.info("Agent {} inherited workspace basePath: {}", entity.getName(), agent.workspaceBasePath);
}
} catch (Exception e) {
log.warn("Failed to lookup workspace basePath for agent {}: {}", entity.getName(), e.getMessage());

View File

@ -78,6 +78,10 @@ public class AgentEntity {
/** 默认思考深度off / low / medium / high / maxnull 表示跟随模型默认 */
private String defaultThinkingLevel;
/** Agent 级别的工作目录覆盖,为 null 时继承工作区 basePath */
@TableField(value = "workspace_base_path", updateStrategy = FieldStrategy.ALWAYS)
private String workspaceBasePath;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

View File

@ -0,0 +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.
ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS workspace_base_path VARCHAR(512) DEFAULT NULL;

View File

@ -0,0 +1,12 @@
-- V125: Add workspace_base_path column to mate_agent for Agent-level directory override.
-- Idempotent: checks INFORMATION_SCHEMA before ADD COLUMN.
SET @col_exists := (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'mate_agent'
AND COLUMN_NAME = 'workspace_base_path'
);
SET @stmt := IF(@col_exists = 0,
'ALTER TABLE mate_agent ADD COLUMN workspace_base_path VARCHAR(512) DEFAULT NULL',
'SELECT 1');
PREPARE s FROM @stmt; EXECUTE s; DEALLOCATE PREPARE s;

View File

@ -1163,6 +1163,8 @@ export default {
extraInstructionsHint: 'Optional. Use for output format, process checklists, or boundary rules.',
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.',
modelName: 'Model',
modelGlobalDefault: 'Use global default',
modelHint: 'Override the global default model for this employee. Leave blank to follow Settings → Models.',
@ -1198,6 +1200,7 @@ export default {
backstory: 'e.g. Spent 10 years in data — believes in asking the right question before writing SQL...',
extraInstructions: 'Optional: output format, process checklist, or boundary rules...',
tags: 'tag1,tag2',
workspaceBasePath: 'e.g. projects/code-review',
},
messages: {
noDescription: 'No description',

View File

@ -1055,6 +1055,8 @@ export default {
extraInstructionsHint: '可选。用于细化输出格式、流程清单或边界规则。',
maxIterations: '最大迭代次数',
defaultThinkingLevel: '默认思考深度',
workspaceBasePath: '工作目录',
workspaceBasePathHint: '可选。为该员工指定独立的工作目录(相对于工作区根目录)。留空则继承工作区默认目录。',
modelName: '模型',
modelGlobalDefault: '使用全局默认模型',
modelHint: '为该员工单独指定模型,留空则跟随「设置 → 模型」中的全局默认。',
@ -1090,6 +1092,7 @@ export default {
backstory: '例:在数据里待了十年,相信先问对问题再写 SQL...',
extraInstructions: '可选:补充输出格式、流程清单或边界规则...',
tags: 'tag1,tag2',
workspaceBasePath: '例如projects/code-review',
},
messages: {
noDescription: '暂无描述',

View File

@ -43,6 +43,7 @@ export interface Agent {
enabled: boolean
icon?: string
tags?: string
workspaceBasePath?: string
createTime?: string
updateTime?: string
}

View File

@ -271,6 +271,11 @@
<option value="max">{{ t('agents.thinkingLevels.max') }}</option>
</select>
</div>
<div class="form-group full-width">
<label class="form-label">{{ t('agents.fields.workspaceBasePath') }}</label>
<input v-model="form.workspaceBasePath" class="form-input" :placeholder="t('agents.placeholders.workspaceBasePath')" />
<p class="form-hint">{{ t('agents.fields.workspaceBasePathHint') }}</p>
</div>
<!--
Identity triad: role + goal + backstory map to H2 sections in
the stored systemPrompt. The card tagline is derived from
@ -703,6 +708,7 @@ const defaultForm = (): Partial<Agent> & { name: string; defaultThinkingLevel: s
tags: '',
enabled: true,
defaultThinkingLevel: null,
workspaceBasePath: null,
})
const form = ref(defaultForm())
@ -890,6 +896,7 @@ async function openEditModal(agent: Agent) {
tags: agent.tags || '',
enabled: agent.enabled,
defaultThinkingLevel: (agent as any).defaultThinkingLevel || null,
workspaceBasePath: agent.workspaceBasePath || null,
}
profileForm.value = parsePrompt(agent.systemPrompt)
modalTab.value = 'basic'