From e12064590a2df282d23b88c8c92dab0a8a92611d Mon Sep 17 00:00:00 2001 From: yibo Date: Tue, 18 Aug 2026 15:26:19 +0800 Subject: [PATCH] refactor(paths): 本体根目录收敛到 locator.AGENTS_ROOT 单一持有者 --- scripts/extract-agent-prompts.js | 2 +- server/routes/company.js | 18 +++++++++--------- server/services/task-service.js | 2 +- server/src/core/devloop/loop-controller.js | 2 +- server/src/core/skill-config-loader.js | 2 +- server/tests/unit/agents-root-single-holder.test.js | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 server/tests/unit/agents-root-single-holder.test.js diff --git a/scripts/extract-agent-prompts.js b/scripts/extract-agent-prompts.js index 9b60ea2..90f47e5 100644 --- a/scripts/extract-agent-prompts.js +++ b/scripts/extract-agent-prompts.js @@ -21,7 +21,7 @@ const path = require('path'); const ROLES = ['pm', 'architect', 'coder', 'tester', 'integrator', 'triager']; const SRC_DIR = path.resolve(__dirname, '../docs/prd/newfunction'); -const OUT_DIR = path.resolve(__dirname, '../system/agents'); +const OUT_DIR = require('../server/services/agent-locator').AGENTS_ROOT; function decodeHtmlEntities(s) { return s diff --git a/server/routes/company.js b/server/routes/company.js index 254e25c..ca61769 100644 --- a/server/routes/company.js +++ b/server/routes/company.js @@ -133,7 +133,7 @@ router.patch('/templates/:templateId', (req, res) => { const path = require('path'); const fs = require('fs'); - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); + const TEMPLATES_DIR = locator.AGENTS_ROOT; const templateDir = path.join(TEMPLATES_DIR, templateId); if (!fs.existsSync(templateDir)) { @@ -178,7 +178,7 @@ router.delete('/templates/:templateId', (req, res) => { const { templateId } = req.params; const path = require('path'); const fs = require('fs'); - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); + const TEMPLATES_DIR = locator.AGENTS_ROOT; const templateDir = path.join(TEMPLATES_DIR, templateId); if (!fs.existsSync(templateDir)) { @@ -257,7 +257,7 @@ router.post('/templates/:templateId/generate', async (req, res) => { const path = require('path'); const fs = require('fs'); const config = require('../config'); - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); + const TEMPLATES_DIR = locator.AGENTS_ROOT; const templateDir = path.join(TEMPLATES_DIR, templateId); if (!fs.existsSync(templateDir)) { @@ -294,7 +294,7 @@ router.post('/templates/:templateId/train', async (req, res) => { const path = require('path'); const fs = require('fs'); const config = require('../config'); - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); + const TEMPLATES_DIR = locator.AGENTS_ROOT; const templateDir = path.join(TEMPLATES_DIR, templateId); if (!fs.existsSync(templateDir)) { @@ -356,7 +356,7 @@ router.post('/templates/:templateId/skills/upload', skillUpload.array('files', 5 try { const { templateId } = req.params; const targetSubdir = req.body.targetDir || '.claude/skills'; // default to .claude/skills/ (Claude Code ACP reads from here) - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); + const TEMPLATES_DIR = locator.AGENTS_ROOT; const templateDir = path.join(TEMPLATES_DIR, templateId); if (!fs.existsSync(templateDir)) { @@ -482,7 +482,7 @@ router.post('/templates/:templateId/provision', async (req, res) => { // 确定工作目录 // 原来写的是 `../../data/system/templates` —— 一个从来不存在的路径(`data/` 多余), // 所以不带 agentDir 的调用一直是 404。顺手改正到合并后的本体目录。 - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); + const TEMPLATES_DIR = locator.AGENTS_ROOT; let workDir; if (agentDir) { // 公司 agent 目录 @@ -595,7 +595,7 @@ router.get('/templates/:templateId/files', (req, res) => { try { const path = require('path'); const fs = require('fs'); - const DATA_DIR = path.resolve(__dirname, '../../system/agents'); + const DATA_DIR = locator.AGENTS_ROOT; const templateDir = path.join(DATA_DIR, req.params.templateId); if (!fs.existsSync(templateDir)) { @@ -651,7 +651,7 @@ router.get('/templates/:templateId/files/content', (req, res) => { try { const path = require('path'); const fs = require('fs'); - const DATA_DIR = path.resolve(__dirname, '../../system/agents'); + const DATA_DIR = locator.AGENTS_ROOT; const filePath = req.query.path; if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); @@ -682,7 +682,7 @@ router.put('/templates/:templateId/files/content', (req, res) => { try { const path = require('path'); const fs = require('fs'); - const DATA_DIR = path.resolve(__dirname, '../../system/agents'); + const DATA_DIR = locator.AGENTS_ROOT; const filePath = req.body.path; if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); diff --git a/server/services/task-service.js b/server/services/task-service.js index 240e86a..5bed5b7 100644 --- a/server/services/task-service.js +++ b/server/services/task-service.js @@ -3,7 +3,7 @@ const path = require('path'); const locator = require('./agent-locator'); const { listCompanyAgents } = require('./company-service'); -const SYSTEM_AGENTS_DIR = path.resolve(__dirname, '../../system/agents'); +const SYSTEM_AGENTS_DIR = require('./agent-locator').AGENTS_ROOT; const TASK_META_DIR = path.resolve(__dirname, '../../system/tasklog'); /** diff --git a/server/src/core/devloop/loop-controller.js b/server/src/core/devloop/loop-controller.js index c02da1c..1f7a8de 100644 --- a/server/src/core/devloop/loop-controller.js +++ b/server/src/core/devloop/loop-controller.js @@ -35,7 +35,7 @@ unitRegistry.attachSseBroker(sseBroker); bugTracker.attachSseBroker(sseBroker); bugTracker.attachTimelineLogger(timelineLogger); -const TEMPLATES_DIR = path.resolve(__dirname, '../../../../system/agents'); +const TEMPLATES_DIR = require('../../../services/agent-locator').AGENTS_ROOT; /** * 新建项目: diff --git a/server/src/core/skill-config-loader.js b/server/src/core/skill-config-loader.js index 53f7537..ca9e266 100644 --- a/server/src/core/skill-config-loader.js +++ b/server/src/core/skill-config-loader.js @@ -23,7 +23,7 @@ const path = require('path'); function loadConfigFromSkills(skillsCodeDir, backendsConfig = {}) { // 扫描所有 Agent 专属的技能挂载目录 (符合 Local File-System First) const dirsToScan = []; - const agentsRoot = path.resolve(__dirname, '../../../system/agents'); + const agentsRoot = require('../../services/agent-locator').AGENTS_ROOT; if (fs.existsSync(agentsRoot)) { const agentFolders = fs.readdirSync(agentsRoot, { withFileTypes: true }); for (const f of agentFolders) { diff --git a/server/tests/unit/agents-root-single-holder.test.js b/server/tests/unit/agents-root-single-holder.test.js new file mode 100644 index 0000000..e5b43ae --- /dev/null +++ b/server/tests/unit/agents-root-single-holder.test.js @@ -0,0 +1,78 @@ +/** + * 「本体根目录」这个事实只能有一个持有者:`agent-locator.AGENTS_ROOT`。 + * + * 为什么需要这条元测试(28 号改造的直接教训): + * 28 号把本体根从 `system/templates/` 搬到 `system/agents/`,搬之前这个路径在 + * 5 个文件里被硬编码了 12 份(`server/routes/company.js` 一个文件里就有 9 份 —— + * 6 个 `TEMPLATES_DIR` + 3 个 `DATA_DIR`,只有 1 处用了权威访问器)。 + * + * 12 份副本的危害不是「不好看」,而是**下一次再搬时改 11 处、漏 1 处**: + * 漏掉的那处会指向一个不存在的目录,而本仓库读这类路径几乎都包在 + * `fs.existsSync()` 守卫里 —— 目录没了不报错,只是静默跳过(并行会话在 + * `scripts/demo-harness-company.js` 实测过这个形状:base 本体没挂上, + * demo 跑出来的 agent 没有四段式骨架,脚本照常打印成功)。 + * + * 同一个病在本仓库已有多个实例:`canDispatch` 的取值域被前端复述过一份、 + * `ACTIVE_ROOT_RUN_STATUS` 在两个文件里逐字重复、`isUngraduatedStatus()` 的取值域 + * 一度按「≠ active 就挡」判而误伤 36 个本体。**同一个事实只能有一处持有者 —— 路径也是事实。** + */ + +const fs = require('fs'); +const path = require('path'); + +const REPO_ROOT = path.resolve(__dirname, '../../..'); + +/** 只扫真正会被执行的源码,跳过依赖、构建产物、测试自身与 locator 本身 */ +const SCAN_DIRS = ['server', 'packages', 'scripts']; +const SKIP_DIR = /(^|\/)(node_modules|dist|\.git|coverage)(\/|$)/; +const SKIP_FILE = /\.test\.js$|agent-locator\.js$/; + +/** + * 「拼出本体根目录」的形状:path.resolve/join 里出现以 ../ 开头、以 system/agents 结尾的字面量。 + * 只认路径构造,不认注释里提到的字符串 —— 注释里写 `system/agents/` 是正常的文档行为。 + */ +const HARDCODED = /path\.(?:resolve|join)\([^)]*['"`][^'"`]*\.\.\/[^'"`]*system\/agents['"`]/; + +function walk(dir, out = []) { + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return out; // 目录不存在(如仓库裁剪过)就跳过,不让本测试成为噪声源 + } + for (const e of entries) { + const full = path.join(dir, e.name); + if (SKIP_DIR.test(full)) continue; + if (e.isDirectory()) walk(full, out); + else if (e.name.endsWith('.js') && !SKIP_FILE.test(e.name)) out.push(full); + } + return out; +} + +describe('本体根目录只有一个持有者(28 §5.1)', () => { + const files = SCAN_DIRS.flatMap((d) => walk(path.join(REPO_ROOT, d))); + + it('扫描样本非空 —— 防止路径写错时本测试退化成永远通过', () => { + // 没有这一条,上面 walk() 的容错会让「一个文件都没扫到」表现为全绿。 + expect(files.length).toBeGreaterThan(100); + }); + + it('没有任何文件自己拼 system/agents 根路径(一律走 locator.AGENTS_ROOT)', () => { + const offenders = []; + for (const f of files) { + const src = fs.readFileSync(f, 'utf-8'); + src.split('\n').forEach((line, i) => { + if (HARDCODED.test(line)) { + offenders.push(`${path.relative(REPO_ROOT, f)}:${i + 1} → ${line.trim()}`); + } + }); + } + expect(offenders).toEqual([]); + }); + + it('locator.AGENTS_ROOT 指向真实存在的目录', () => { + const { AGENTS_ROOT } = require('../../services/agent-locator'); + expect(fs.existsSync(AGENTS_ROOT)).toBe(true); + expect(path.basename(AGENTS_ROOT)).toBe('agents'); + }); +}); -- libgit2 0.21.4