fix: duplicate subagents config if qwen-code runs in home dir

This commit is contained in:
tanzhenxin
2025-09-17 11:32:52 +08:00
parent 19950e5b7c
commit 5f90472a7d
3 changed files with 53 additions and 67 deletions

View File

@@ -227,7 +227,7 @@ export const AgentSelectionStep = ({
const textColor = isSelected ? theme.text.accent : theme.text.primary; const textColor = isSelected ? theme.text.accent : theme.text.primary;
return ( return (
<Box key={agent.name} alignItems="center"> <Box key={`${agent.name}-${agent.level}`} alignItems="center">
<Box minWidth={2} flexShrink={0}> <Box minWidth={2} flexShrink={0}>
<Text color={isSelected ? theme.text.accent : theme.text.primary}> <Text color={isSelected ? theme.text.accent : theme.text.primary}>
{isSelected ? '●' : ' '} {isSelected ? '●' : ' '}

View File

@@ -185,6 +185,7 @@ You are a helpful assistant.
const config = manager.parseSubagentContent( const config = manager.parseSubagentContent(
validMarkdown, validMarkdown,
validConfig.filePath, validConfig.filePath,
'project',
); );
expect(config.name).toBe('test-agent'); expect(config.name).toBe('test-agent');
@@ -209,6 +210,7 @@ You are a helpful assistant.
const config = manager.parseSubagentContent( const config = manager.parseSubagentContent(
markdownWithTools, markdownWithTools,
validConfig.filePath, validConfig.filePath,
'project',
); );
expect(config.tools).toEqual(['read_file', 'write_file']); expect(config.tools).toEqual(['read_file', 'write_file']);
@@ -229,6 +231,7 @@ You are a helpful assistant.
const config = manager.parseSubagentContent( const config = manager.parseSubagentContent(
markdownWithModel, markdownWithModel,
validConfig.filePath, validConfig.filePath,
'project',
); );
expect(config.modelConfig).toEqual({ model: 'custom-model', temp: 0.5 }); expect(config.modelConfig).toEqual({ model: 'custom-model', temp: 0.5 });
@@ -249,6 +252,7 @@ You are a helpful assistant.
const config = manager.parseSubagentContent( const config = manager.parseSubagentContent(
markdownWithRun, markdownWithRun,
validConfig.filePath, validConfig.filePath,
'project',
); );
expect(config.runConfig).toEqual({ max_time_minutes: 5, max_turns: 10 }); expect(config.runConfig).toEqual({ max_time_minutes: 5, max_turns: 10 });
@@ -266,6 +270,7 @@ You are a helpful assistant.
const config = manager.parseSubagentContent( const config = manager.parseSubagentContent(
markdownWithNumeric, markdownWithNumeric,
validConfig.filePath, validConfig.filePath,
'project',
); );
expect(config.name).toBe('11'); expect(config.name).toBe('11');
@@ -286,6 +291,7 @@ You are a helpful assistant.
const config = manager.parseSubagentContent( const config = manager.parseSubagentContent(
markdownWithBoolean, markdownWithBoolean,
validConfig.filePath, validConfig.filePath,
'project',
); );
expect(config.name).toBe('true'); expect(config.name).toBe('true');
@@ -301,8 +307,13 @@ You are a helpful assistant.
const projectConfig = manager.parseSubagentContent( const projectConfig = manager.parseSubagentContent(
validMarkdown, validMarkdown,
projectPath, projectPath,
'project',
);
const userConfig = manager.parseSubagentContent(
validMarkdown,
userPath,
'user',
); );
const userConfig = manager.parseSubagentContent(validMarkdown, userPath);
expect(projectConfig.level).toBe('project'); expect(projectConfig.level).toBe('project');
expect(userConfig.level).toBe('user'); expect(userConfig.level).toBe('user');
@@ -313,7 +324,11 @@ You are a helpful assistant.
Just content`; Just content`;
expect(() => expect(() =>
manager.parseSubagentContent(invalidMarkdown, validConfig.filePath), manager.parseSubagentContent(
invalidMarkdown,
validConfig.filePath,
'project',
),
).toThrow(SubagentError); ).toThrow(SubagentError);
}); });
@@ -326,7 +341,11 @@ You are a helpful assistant.
`; `;
expect(() => expect(() =>
manager.parseSubagentContent(markdownWithoutName, validConfig.filePath), manager.parseSubagentContent(
markdownWithoutName,
validConfig.filePath,
'project',
),
).toThrow(SubagentError); ).toThrow(SubagentError);
}); });
@@ -342,39 +361,20 @@ You are a helpful assistant.
manager.parseSubagentContent( manager.parseSubagentContent(
markdownWithoutDescription, markdownWithoutDescription,
validConfig.filePath, validConfig.filePath,
'project',
), ),
).toThrow(SubagentError); ).toThrow(SubagentError);
}); });
it('should warn when filename does not match subagent name', () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const mismatchedPath = '/test/project/.qwen/agents/wrong-filename.md';
const config = manager.parseSubagentContent(
validMarkdown,
mismatchedPath,
);
expect(config.name).toBe('test-agent');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Warning: Subagent file "wrong-filename.md" contains name "test-agent"',
),
);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Consider renaming the file to "test-agent.md"',
),
);
consoleSpy.mockRestore();
});
it('should not warn when filename matches subagent name', () => { it('should not warn when filename matches subagent name', () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const matchingPath = '/test/project/.qwen/agents/test-agent.md'; const matchingPath = '/test/project/.qwen/agents/test-agent.md';
const config = manager.parseSubagentContent(validMarkdown, matchingPath); const config = manager.parseSubagentContent(
validMarkdown,
matchingPath,
'project',
);
expect(config.name).toBe('test-agent'); expect(config.name).toBe('test-agent');
expect(consoleSpy).not.toHaveBeenCalled(); expect(consoleSpy).not.toHaveBeenCalled();

View File

@@ -330,7 +330,10 @@ export class SubagentManager {
* @returns SubagentConfig * @returns SubagentConfig
* @throws SubagentError if parsing fails * @throws SubagentError if parsing fails
*/ */
async parseSubagentFile(filePath: string): Promise<SubagentConfig> { async parseSubagentFile(
filePath: string,
level: SubagentLevel,
): Promise<SubagentConfig> {
let content: string; let content: string;
try { try {
@@ -342,7 +345,7 @@ export class SubagentManager {
); );
} }
return this.parseSubagentContent(content, filePath); return this.parseSubagentContent(content, filePath, level);
} }
/** /**
@@ -353,7 +356,11 @@ export class SubagentManager {
* @returns SubagentConfig * @returns SubagentConfig
* @throws SubagentError if parsing fails * @throws SubagentError if parsing fails
*/ */
parseSubagentContent(content: string, filePath: string): SubagentConfig { parseSubagentContent(
content: string,
filePath: string,
level: SubagentLevel,
): SubagentConfig {
try { try {
// Split frontmatter and content // Split frontmatter and content
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/; const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
@@ -394,31 +401,16 @@ export class SubagentManager {
| undefined; | undefined;
const color = frontmatter['color'] as string | undefined; const color = frontmatter['color'] as string | undefined;
// Determine level from file path using robust, cross-platform check
// A project-level agent lives under <projectRoot>/.qwen/agents
const projectAgentsDir = path.join(
this.config.getProjectRoot(),
QWEN_CONFIG_DIR,
AGENT_CONFIG_DIR,
);
const rel = path.relative(
path.normalize(projectAgentsDir),
path.normalize(filePath),
);
const isProjectLevel =
rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
const level: SubagentLevel = isProjectLevel ? 'project' : 'user';
const config: SubagentConfig = { const config: SubagentConfig = {
name, name,
description, description,
tools, tools,
systemPrompt: systemPrompt.trim(), systemPrompt: systemPrompt.trim(),
level,
filePath, filePath,
modelConfig: modelConfig as Partial<ModelConfig>, modelConfig: modelConfig as Partial<ModelConfig>,
runConfig: runConfig as Partial<RunConfig>, runConfig: runConfig as Partial<RunConfig>,
color, color,
level,
}; };
// Validate the parsed configuration // Validate the parsed configuration
@@ -427,16 +419,6 @@ export class SubagentManager {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`); throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
} }
// Warn if filename doesn't match subagent name (potential issue)
const expectedFilename = `${config.name}.md`;
const actualFilename = path.basename(filePath);
if (actualFilename !== expectedFilename) {
console.warn(
`Warning: Subagent file "${actualFilename}" contains name "${config.name}" but filename suggests "${path.basename(actualFilename, '.md')}". ` +
`Consider renaming the file to "${expectedFilename}" for consistency.`,
);
}
return config; return config;
} catch (error) { } catch (error) {
throw new SubagentError( throw new SubagentError(
@@ -679,14 +661,18 @@ export class SubagentManager {
return BuiltinAgentRegistry.getBuiltinAgents(); return BuiltinAgentRegistry.getBuiltinAgents();
} }
const baseDir = const projectRoot = this.config.getProjectRoot();
level === 'project' const homeDir = os.homedir();
? path.join( const isHomeDirectory = path.resolve(projectRoot) === path.resolve(homeDir);
this.config.getProjectRoot(),
QWEN_CONFIG_DIR, // If project level is requested but project root is same as home directory,
AGENT_CONFIG_DIR, // return empty array to avoid conflicts between project and global agents
) if (level === 'project' && isHomeDirectory) {
: path.join(os.homedir(), QWEN_CONFIG_DIR, AGENT_CONFIG_DIR); return [];
}
let baseDir = level === 'project' ? projectRoot : homeDir;
baseDir = path.join(baseDir, QWEN_CONFIG_DIR, AGENT_CONFIG_DIR);
try { try {
const files = await fs.readdir(baseDir); const files = await fs.readdir(baseDir);
@@ -698,7 +684,7 @@ export class SubagentManager {
const filePath = path.join(baseDir, file); const filePath = path.join(baseDir, file);
try { try {
const config = await this.parseSubagentFile(filePath); const config = await this.parseSubagentFile(filePath, level);
subagents.push(config); subagents.push(config);
} catch (_error) { } catch (_error) {
// Ignore invalid files // Ignore invalid files