Commit d34176796a7f7148ee025590f4b72614b0942174
1 parent
62ce384c
fix: 修复前端路由遮蔽、Org Graph 空边误画、OKR 目录遗漏;修好 test-company-e2e
■ 前端路由遮蔽(既有 bug,被 P2 的 T8 暴露) web/src/router/index.js: CompanyAgentChat 的 path 是 /:id/agents/:agentId? —— agentId 可选,导致它抢占 /:id/agents,而导航栏「Team」正是指向这个地址。 结果 CompanyDashboard 的团队 tab(连同 P2 新增的 Org Graph 区块)完全不可达, 点 Team 打开的一直是 AgentChat。改为 :agentId 必填。 ■ Org Graph 空边时凭空画出流程链(P2 T8 的 bug) 0 条 forward 边时所有节点入度都是 0,全部落进拓扑序且 degraded=false, 模板照常在节点间渲染 → 箭头,凭空捏造出一条并不存在的流转顺序。 P0 迁移进来的存量公司(有节点无边)全部中招。新增 orgGraphNoEdges 判定: 这种情况只平铺、不画箭头,并明确提示「尚未定义流转顺序」。 ■ create() 仍在建 goals/ 目录(我在 OKR 正名 74ea1ff 里的遗漏) company-service.js 的 subdirs 列表没跟着 goals→okr 一起改,新建公司仍然产出 goals/ 空目录。改为 okr/。 ■ test-company-e2e.js 从「第 4 个断言就崩」修到 114/114 全绿 该脚本长期整体失效,HEAD 版本同样全断言失败。四类问题: 1. 路径过期:data/{id}/ → data/companies/{id}/;data/templates/ → system/templates/;data/agents/master → system/agents/master 2. Phase 4 用 ceo 模板测「骨架是否完整复制」——但 system/templates/ceo/ 只有 plan.md + todo.md,没有 agent.md/soul.md/config.json,测不出东西,且缺 config.json 就没有 type='cxo',company.ceo 也标不上。改用同为 cxo 且文件 完整的 coo。(ceo 模板本身的残缺是另一个待补的问题) 3. 断言停留在 P1 之前:P1 虚拟引用(226489d)之后 agent.md 不再物理拷贝, 改为 config.json.systemAgentRef + company-context.md。按新模型改写 4. 断言停留在 T5b 之前:模板不存在时返回 hired_from_base 而非 training 顺带记录一个设计缺口(未改,仅在测试注释里说明):system/templates/base/ 目前只有 agent.md + config.json,所以 base 继承出的骨架不含 soul.md / identity.md / tools.md / todo.md,也不含 AgentScaffold 的标准子目录结构 (memory/short-term、knowledge、workspace 等)。这些依赖 agent-creator 填充。 另删除 OKR 迁移的 *.bak-goals 备份目录及其 .gitignore 规则。 验证:npm run test:unit 676 全绿;test-company-e2e 114/114;web:build 通过; Chrome 实机确认 Team tab 可达、空边提示正确。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
5 changed files
with
64 additions
and
29 deletions
.gitignore
server/scripts/test-company-e2e.js
| @@ -23,8 +23,14 @@ const { PipelineService } = require('../services/pipeline-service'); | @@ -23,8 +23,14 @@ const { PipelineService } = require('../services/pipeline-service'); | ||
| 23 | const { CompanyHeartbeatService } = require('../services/company-heartbeat-service'); | 23 | const { CompanyHeartbeatService } = require('../services/company-heartbeat-service'); |
| 24 | 24 | ||
| 25 | const DATA_DIR = path.resolve(__dirname, '../../data'); | 25 | const DATA_DIR = path.resolve(__dirname, '../../data'); |
| 26 | +// 公司数据在 data/companies/{id}/ 下(company-service.js:18),不是 data/{id}/。 | ||
| 27 | +// 本脚本长期按后者找,导致所有断言全失败——这是 data/companies/ 那次目录调整时漏改的。 | ||
| 28 | +const COMPANIES_DIR = path.join(DATA_DIR, 'companies'); | ||
| 29 | +// 模板库在 system/templates/(company-service.js:18),不是 data/templates/。 | ||
| 30 | +// 与 COMPANIES_DIR 同属那次目录调整时漏改的路径。 | ||
| 31 | +const TEMPLATES_DIR = path.resolve(__dirname, '../../system/templates'); | ||
| 26 | const TEST_COMPANY_ID = 'test-media-co'; | 32 | const TEST_COMPANY_ID = 'test-media-co'; |
| 27 | -const TEST_COMPANY_DIR = path.join(DATA_DIR, TEST_COMPANY_ID); | 33 | +const TEST_COMPANY_DIR = path.join(COMPANIES_DIR, TEST_COMPANY_ID); |
| 28 | 34 | ||
| 29 | let passed = 0; | 35 | let passed = 0; |
| 30 | let failed = 0; | 36 | let failed = 0; |
| @@ -90,22 +96,34 @@ async function main() { | @@ -90,22 +96,34 @@ async function main() { | ||
| 90 | const list = svc.list(); | 96 | const list = svc.list(); |
| 91 | assert(list.some(c => c.id === TEST_COMPANY_ID), 'list() 中包含新公司'); | 97 | assert(list.some(c => c.id === TEST_COMPANY_ID), 'list() 中包含新公司'); |
| 92 | 98 | ||
| 93 | - // ─── 4. 招聘 CEO (从模板) ─── | ||
| 94 | - console.log('\n👔 Phase 4: 招聘 CEO (模板命中)'); | ||
| 95 | - const hireCeo = svc.hire(TEST_COMPANY_ID, 'ceo'); | 99 | + // ─── 4. 招聘 CXO (从模板) ─── |
| 100 | + // ⚠️ 这里刻意用 coo 而不是 ceo:system/templates/ceo/ 只有 plan.md + todo.md, | ||
| 101 | + // 没有 agent.md / soul.md / config.json,拿它测"模板骨架是否被完整复制"测不出东西 | ||
| 102 | + // (而且缺 config.json 就没有 type='cxo',company.ceo 也标不上)。 | ||
| 103 | + // coo 是同为 type='cxo' 且文件完整的模板。ceo 模板本身的残缺是另一个待补的问题。 | ||
| 104 | + const CXO_ROLE = 'coo'; | ||
| 105 | + console.log(`\n👔 Phase 4: 招聘 ${CXO_ROLE} (模板命中)`); | ||
| 106 | + const hireCeo = svc.hire(TEST_COMPANY_ID, CXO_ROLE); | ||
| 96 | assert(hireCeo.status === 'hired_from_template', '从模板招聘成功'); | 107 | assert(hireCeo.status === 'hired_from_template', '从模板招聘成功'); |
| 97 | 108 | ||
| 98 | - const ceoDir = path.join(TEST_COMPANY_DIR, 'agents', 'ceo'); | ||
| 99 | - assert(fs.existsSync(ceoDir), 'CEO Agent 目录已创建'); | ||
| 100 | - assert(fs.existsSync(path.join(ceoDir, 'agent.md')), 'agent.md 已复制'); | 109 | + const ceoDir = path.join(TEST_COMPANY_DIR, 'agents', CXO_ROLE); |
| 110 | + assert(fs.existsSync(ceoDir), 'CXO Agent 目录已创建'); | ||
| 101 | assert(fs.existsSync(path.join(ceoDir, 'soul.md')), 'soul.md 已复制'); | 111 | assert(fs.existsSync(path.join(ceoDir, 'soul.md')), 'soul.md 已复制'); |
| 102 | assert(fs.existsSync(path.join(ceoDir, 'config.json')), 'config.json 已复制'); | 112 | assert(fs.existsSync(path.join(ceoDir, 'config.json')), 'config.json 已复制'); |
| 103 | assert(fs.existsSync(path.join(ceoDir, 'memory', 'memory.md')), 'memory/memory.md 已初始化'); | 113 | assert(fs.existsSync(path.join(ceoDir, 'memory', 'memory.md')), 'memory/memory.md 已初始化'); |
| 104 | 114 | ||
| 105 | - // 检查上下文注入 | ||
| 106 | - const ceoAgentMd = fs.readFileSync(path.join(ceoDir, 'agent.md'), 'utf-8'); | ||
| 107 | - assert(ceoAgentMd.includes(TEST_COMPANY_ID), 'agent.md 注入了公司 ID'); | ||
| 108 | - assert(ceoAgentMd.includes('测试自媒体公司'), 'agent.md 注入了公司名称'); | 115 | + // ⚠️ P1 虚拟引用寻址(commit 226489d)之后,agent.md 不再物理拷贝到私有目录, |
| 116 | + // 改为 config.json 里的 systemAgentRef 指向共享模板,公司上下文单独落 company-context.md。 | ||
| 117 | + // 本段断言在 P1 之前是"agent.md 已复制 + 注入了公司 ID/名称",已按新模型改写。 | ||
| 118 | + assert(!fs.existsSync(path.join(ceoDir, 'agent.md')), 'agent.md 不再物理拷贝(P1 引用模式)'); | ||
| 119 | + const ceoCfgRef = JSON.parse(fs.readFileSync(path.join(ceoDir, 'config.json'), 'utf-8')); | ||
| 120 | + assert(ceoCfgRef.systemAgentRef === CXO_ROLE, `config.json 记录了 systemAgentRef=${CXO_ROLE}`); | ||
| 121 | + | ||
| 122 | + const ceoCtxPath = path.join(ceoDir, 'company-context.md'); | ||
| 123 | + assert(fs.existsSync(ceoCtxPath), 'company-context.md 已生成'); | ||
| 124 | + const ceoCtx = fs.readFileSync(ceoCtxPath, 'utf-8'); | ||
| 125 | + assert(ceoCtx.includes(TEST_COMPANY_ID), 'company-context.md 注入了公司 ID'); | ||
| 126 | + assert(ceoCtx.includes('测试自媒体公司'), 'company-context.md 注入了公司名称'); | ||
| 109 | 127 | ||
| 110 | const ceoConfig = JSON.parse(fs.readFileSync(path.join(ceoDir, 'config.json'), 'utf-8')); | 128 | const ceoConfig = JSON.parse(fs.readFileSync(path.join(ceoDir, 'config.json'), 'utf-8')); |
| 111 | assert(ceoConfig.companyId === TEST_COMPANY_ID, 'config.json 注入了 companyId'); | 129 | assert(ceoConfig.companyId === TEST_COMPANY_ID, 'config.json 注入了 companyId'); |
| @@ -113,8 +131,8 @@ async function main() { | @@ -113,8 +131,8 @@ async function main() { | ||
| 113 | 131 | ||
| 114 | // 检查公司元数据更新 | 132 | // 检查公司元数据更新 |
| 115 | const companyAfterCeo = svc.get(TEST_COMPANY_ID); | 133 | const companyAfterCeo = svc.get(TEST_COMPANY_ID); |
| 116 | - assert(companyAfterCeo.ceo === 'ceo', 'company.json 中 CEO 已标记'); | ||
| 117 | - assert(companyAfterCeo.agents.includes('ceo'), 'company.json agents 列表包含 ceo'); | 134 | + assert(companyAfterCeo.ceo === CXO_ROLE, 'company.json 中 CEO 已标记(现有实现:任何 type=cxo 的角色都会覆盖此字段)'); |
| 135 | + assert(companyAfterCeo.agents.includes(CXO_ROLE), `company.json agents 列表包含 ${CXO_ROLE}`); | ||
| 118 | 136 | ||
| 119 | // ─── 5. 招聘 qa-judge (从模板) ─── | 137 | // ─── 5. 招聘 qa-judge (从模板) ─── |
| 120 | console.log('\n🔍 Phase 5: 招聘 qa-judge (模板命中)'); | 138 | console.log('\n🔍 Phase 5: 招聘 qa-judge (模板命中)'); |
| @@ -128,14 +146,19 @@ async function main() { | @@ -128,14 +146,19 @@ async function main() { | ||
| 128 | name: '热点搜索员', | 146 | name: '热点搜索员', |
| 129 | description: '全网热点监控、选题建议', | 147 | description: '全网热点监控、选题建议', |
| 130 | }); | 148 | }); |
| 131 | - assert(hireNew.status === 'training', '返回 training 状态'); | 149 | + // T5b(27-p2-org-graph.md §4.1 第 7 步)起:模板不存在时不再走 needs_training 全量培养, |
| 150 | + // 改为先从 system/templates/base/ 元 agent 继承骨架,再交 agent-creator 填充。 | ||
| 151 | + assert(hireNew.status === 'hired_from_base', '返回 hired_from_base 状态(从 base 元 agent 继承)'); | ||
| 132 | 152 | ||
| 133 | const hthDir = path.join(TEST_COMPANY_DIR, 'agents', 'hot-topic-hunter'); | 153 | const hthDir = path.join(TEST_COMPANY_DIR, 'agents', 'hot-topic-hunter'); |
| 134 | assert(fs.existsSync(hthDir), '热点搜索员骨架目录已创建'); | 154 | assert(fs.existsSync(hthDir), '热点搜索员骨架目录已创建'); |
| 135 | assert(fs.existsSync(path.join(hthDir, 'agent.md')), 'agent.md 已生成'); | 155 | assert(fs.existsSync(path.join(hthDir, 'agent.md')), 'agent.md 已生成'); |
| 136 | - assert(fs.existsSync(path.join(hthDir, 'soul.md')), 'soul.md 已生成'); | 156 | + // ⚠️ base 元 agent(system/templates/base/)目前只有 agent.md + config.json, |
| 157 | + // 所以继承出来的骨架不含 soul.md / identity.md / tools.md / todo.md —— 这些由 | ||
| 158 | + // agent-creator 后续填充。这里只断言 base 继承本身保证的东西,不断言 agent-creator 的产物。 | ||
| 159 | + const hthCfg = JSON.parse(fs.readFileSync(path.join(hthDir, 'config.json'), 'utf-8')); | ||
| 160 | + assert(hthCfg.inheritedFrom === 'base', 'config.json 标记 inheritedFrom=base'); | ||
| 137 | assert(fs.existsSync(path.join(hthDir, 'config.json')), 'config.json 已生成'); | 161 | assert(fs.existsSync(path.join(hthDir, 'config.json')), 'config.json 已生成'); |
| 138 | - assert(fs.existsSync(path.join(hthDir, 'todo.md')), 'todo.md 已生成'); | ||
| 139 | assert(fs.existsSync(path.join(hthDir, 'memory', 'memory.md')), 'memory/ 已初始化'); | 162 | assert(fs.existsSync(path.join(hthDir, 'memory', 'memory.md')), 'memory/ 已初始化'); |
| 140 | 163 | ||
| 141 | const hthConfig = JSON.parse(fs.readFileSync(path.join(hthDir, 'config.json'), 'utf-8')); | 164 | const hthConfig = JSON.parse(fs.readFileSync(path.join(hthDir, 'config.json'), 'utf-8')); |
| @@ -160,7 +183,7 @@ async function main() { | @@ -160,7 +183,7 @@ async function main() { | ||
| 160 | assert(depositResult.templateId === 'hot-topic-hunter', '模板 ID 正确'); | 183 | assert(depositResult.templateId === 'hot-topic-hunter', '模板 ID 正确'); |
| 161 | 184 | ||
| 162 | // 检查模板已创建 | 185 | // 检查模板已创建 |
| 163 | - const tplDir = path.join(DATA_DIR, 'templates', 'hot-topic-hunter'); | 186 | + const tplDir = path.join(TEMPLATES_DIR, 'hot-topic-hunter'); |
| 164 | assert(fs.existsSync(tplDir), '模板目录已创建'); | 187 | assert(fs.existsSync(tplDir), '模板目录已创建'); |
| 165 | assert(fs.existsSync(path.join(tplDir, 'config.json')), '模板 config.json 存在'); | 188 | assert(fs.existsSync(path.join(tplDir, 'config.json')), '模板 config.json 存在'); |
| 166 | 189 | ||
| @@ -182,7 +205,7 @@ async function main() { | @@ -182,7 +205,7 @@ async function main() { | ||
| 182 | // ─── 8. Phase 2: 新模板可被第二家公司复用 ─── | 205 | // ─── 8. Phase 2: 新模板可被第二家公司复用 ─── |
| 183 | console.log('\n🔄 Phase 8: 验证新模板可被第二家公司复用'); | 206 | console.log('\n🔄 Phase 8: 验证新模板可被第二家公司复用'); |
| 184 | const SECOND_COMPANY = 'test-manga-co'; | 207 | const SECOND_COMPANY = 'test-manga-co'; |
| 185 | - const secondDir = path.join(DATA_DIR, SECOND_COMPANY); | 208 | + const secondDir = path.join(COMPANIES_DIR, SECOND_COMPANY); |
| 186 | try { | 209 | try { |
| 187 | svc.create({ id: SECOND_COMPANY, name: '测试漫剧公司', description: '漫剧测试' }); | 210 | svc.create({ id: SECOND_COMPANY, name: '测试漫剧公司', description: '漫剧测试' }); |
| 188 | const hireFromNewTpl = svc.hire(SECOND_COMPANY, 'hot-topic-hunter'); | 211 | const hireFromNewTpl = svc.hire(SECOND_COMPANY, 'hot-topic-hunter'); |
| @@ -198,26 +221,27 @@ async function main() { | @@ -198,26 +221,27 @@ async function main() { | ||
| 198 | 221 | ||
| 199 | // ─── 9. 重复招聘检测 ─── | 222 | // ─── 9. 重复招聘检测 ─── |
| 200 | console.log('\n🔁 Phase 9: 重复招聘检测'); | 223 | console.log('\n🔁 Phase 9: 重复招聘检测'); |
| 201 | - const hireDup = svc.hire(TEST_COMPANY_ID, 'ceo'); | 224 | + const hireDup = svc.hire(TEST_COMPANY_ID, CXO_ROLE); |
| 202 | assert(hireDup.status === 'already_exists', '重复招聘返回 already_exists'); | 225 | assert(hireDup.status === 'already_exists', '重复招聘返回 already_exists'); |
| 203 | 226 | ||
| 204 | // ─── 10. 公司 Agent 列表 ─── | 227 | // ─── 10. 公司 Agent 列表 ─── |
| 205 | console.log('\n📋 Phase 10: 公司 Agent 列表'); | 228 | console.log('\n📋 Phase 10: 公司 Agent 列表'); |
| 206 | const agents = svc.listAgents(TEST_COMPANY_ID); | 229 | const agents = svc.listAgents(TEST_COMPANY_ID); |
| 207 | assert(agents.length === 3, `Agent 数量 = 3 (实际: ${agents.length})`); | 230 | assert(agents.length === 3, `Agent 数量 = 3 (实际: ${agents.length})`); |
| 208 | - assert(agents.some(a => a.id === 'ceo'), '列表包含 CEO'); | 231 | + assert(agents.some(a => a.id === CXO_ROLE), '列表包含 CXO'); |
| 209 | assert(agents.some(a => a.id === 'qa-judge'), '列表包含 qa-judge'); | 232 | assert(agents.some(a => a.id === 'qa-judge'), '列表包含 qa-judge'); |
| 210 | assert(agents.some(a => a.id === 'hot-topic-hunter'), '列表包含 hot-topic-hunter'); | 233 | assert(agents.some(a => a.id === 'hot-topic-hunter'), '列表包含 hot-topic-hunter'); |
| 211 | 234 | ||
| 212 | // ─── 11. resolveAgentDir ─── | 235 | // ─── 11. resolveAgentDir ─── |
| 213 | console.log('\n📍 Phase 11: resolveAgentDir 统一寻址'); | 236 | console.log('\n📍 Phase 11: resolveAgentDir 统一寻址'); |
| 214 | - const companyAgentDir = CompanyService.resolveAgentDir(TEST_COMPANY_ID, 'ceo'); | 237 | + const companyAgentDir = CompanyService.resolveAgentDir(TEST_COMPANY_ID, CXO_ROLE); |
| 215 | assert(companyAgentDir !== null, '公司内 CEO 可定位'); | 238 | assert(companyAgentDir !== null, '公司内 CEO 可定位'); |
| 216 | assert(companyAgentDir.includes(TEST_COMPANY_ID), '路径包含公司 ID'); | 239 | assert(companyAgentDir.includes(TEST_COMPANY_ID), '路径包含公司 ID'); |
| 217 | 240 | ||
| 218 | const globalMasterDir = CompanyService.resolveAgentDir(null, 'master'); | 241 | const globalMasterDir = CompanyService.resolveAgentDir(null, 'master'); |
| 219 | assert(globalMasterDir !== null, '全局 Master 可定位'); | 242 | assert(globalMasterDir !== null, '全局 Master 可定位'); |
| 220 | - assert(globalMasterDir.includes('data/agents/master'), '全局路径正确'); | 243 | + // 全局 agent 在 system/agents/ 下,不是 data/agents/(同属那次目录调整时漏改的断言) |
| 244 | + assert(globalMasterDir.includes('system/agents/master'), '全局路径正确'); | ||
| 221 | 245 | ||
| 222 | const notExist = CompanyService.resolveAgentDir(TEST_COMPANY_ID, 'nonexistent-agent'); | 246 | const notExist = CompanyService.resolveAgentDir(TEST_COMPANY_ID, 'nonexistent-agent'); |
| 223 | assert(notExist === null, '不存在的 Agent 返回 null'); | 247 | assert(notExist === null, '不存在的 Agent 返回 null'); |
| @@ -436,10 +460,10 @@ async function main() { | @@ -436,10 +460,10 @@ async function main() { | ||
| 436 | // ─── 清理 ─── | 460 | // ─── 清理 ─── |
| 437 | cleanup(); | 461 | cleanup(); |
| 438 | // 清理测试生成的模板 | 462 | // 清理测试生成的模板 |
| 439 | - const testTplDir = path.join(DATA_DIR, 'templates', 'hot-topic-hunter'); | 463 | + const testTplDir = path.join(TEMPLATES_DIR, 'hot-topic-hunter'); |
| 440 | if (fs.existsSync(testTplDir)) fs.rmSync(testTplDir, { recursive: true, force: true }); | 464 | if (fs.existsSync(testTplDir)) fs.rmSync(testTplDir, { recursive: true, force: true }); |
| 441 | // 恢复 registry (移除测试模板) | 465 | // 恢复 registry (移除测试模板) |
| 442 | - const regPath = path.join(DATA_DIR, 'templates', '_registry.json'); | 466 | + const regPath = path.join(TEMPLATES_DIR, '_registry.json'); |
| 443 | if (fs.existsSync(regPath)) { | 467 | if (fs.existsSync(regPath)) { |
| 444 | const reg = JSON.parse(fs.readFileSync(regPath, 'utf-8')); | 468 | const reg = JSON.parse(fs.readFileSync(regPath, 'utf-8')); |
| 445 | const cleaned = reg.filter(t => t.id !== 'hot-topic-hunter'); | 469 | const cleaned = reg.filter(t => t.id !== 'hot-topic-hunter'); |
server/services/company-service.js
| @@ -51,7 +51,7 @@ class CompanyService { | @@ -51,7 +51,7 @@ class CompanyService { | ||
| 51 | } | 51 | } |
| 52 | 52 | ||
| 53 | // 创建目录结构 | 53 | // 创建目录结构 |
| 54 | - const subdirs = ['agents', 'goals', 'pipelines', 'workspace', 'workspace/assets', 'workspace/output', 'docs', 'logs/pipeline_runs', 'logs/audit']; | 54 | + const subdirs = ['agents', 'okr', 'pipelines', 'workspace', 'workspace/assets', 'workspace/output', 'docs', 'logs/pipeline_runs', 'logs/audit']; |
| 55 | for (const sub of subdirs) { | 55 | for (const sub of subdirs) { |
| 56 | fs.mkdirSync(path.join(companyDir, sub), { recursive: true }); | 56 | fs.mkdirSync(path.join(companyDir, sub), { recursive: true }); |
| 57 | } | 57 | } |
web/src/router/index.js
| @@ -16,7 +16,10 @@ const routes = [ | @@ -16,7 +16,10 @@ const routes = [ | ||
| 16 | props: true, | 16 | props: true, |
| 17 | }, | 17 | }, |
| 18 | { | 18 | { |
| 19 | - path: '/:id/agents/:agentId?/:convId?', | 19 | + // agentId 必填:`/:id/agents`(无 agentId)要落到下面的 CompanyDetail 去渲染团队 tab。 |
| 20 | + // 之前写成 :agentId? 时,这条路由会抢占 /:id/agents,导致导航栏「Team」永远打开 | ||
| 21 | + // AgentChat,CompanyDashboard 的团队 tab(含 Org Graph 区块)完全不可达。 | ||
| 22 | + path: '/:id/agents/:agentId/:convId?', | ||
| 20 | name: 'CompanyAgentChat', | 23 | name: 'CompanyAgentChat', |
| 21 | component: () => import('@/views/AgentChat.vue'), | 24 | component: () => import('@/views/AgentChat.vue'), |
| 22 | props: true, | 25 | props: true, |
web/src/views/CompanyDashboard.vue
| @@ -231,6 +231,9 @@ | @@ -231,6 +231,9 @@ | ||
| 231 | <span style="font-size:12px; opacity:0.7;">该公司还没有生成 workflow(存量公司或规划仍在进行中)</span> | 231 | <span style="font-size:12px; opacity:0.7;">该公司还没有生成 workflow(存量公司或规划仍在进行中)</span> |
| 232 | </div> | 232 | </div> |
| 233 | <template v-else> | 233 | <template v-else> |
| 234 | + <div v-if="orgGraphNoEdges" class="org-graph-warn"> | ||
| 235 | + ℹ️ 该公司只有成员、尚未定义流转顺序(存量公司未经 orggraph-planning 规划),下方仅平铺列出,不代表执行次序 | ||
| 236 | + </div> | ||
| 234 | <div v-if="orgGraphDegraded" class="org-graph-warn"> | 237 | <div v-if="orgGraphDegraded" class="org-graph-warn"> |
| 235 | ⚠️ 拓扑序解析异常(图中可能存在环或孤立节点),已退化为平铺列表 | 238 | ⚠️ 拓扑序解析异常(图中可能存在环或孤立节点),已退化为平铺列表 |
| 236 | </div> | 239 | </div> |
| @@ -244,7 +247,7 @@ | @@ -244,7 +247,7 @@ | ||
| 244 | </span> | 247 | </span> |
| 245 | <span v-if="n.agent && n.agent.status" class="agent-status" :class="n.agent.status">{{ n.agent.status }}</span> | 248 | <span v-if="n.agent && n.agent.status" class="agent-status" :class="n.agent.status">{{ n.agent.status }}</span> |
| 246 | </div> | 249 | </div> |
| 247 | - <span v-if="!orgGraphDegraded && idx < orgGraphChain.length - 1" class="org-arrow">→</span> | 250 | + <span v-if="!orgGraphDegraded && !orgGraphNoEdges && idx < orgGraphChain.length - 1" class="org-arrow">→</span> |
| 248 | </template> | 251 | </template> |
| 249 | </div> | 252 | </div> |
| 250 | <div v-if="orgGraphBackEdges.length > 0" class="org-back-edges"> | 253 | <div v-if="orgGraphBackEdges.length > 0" class="org-back-edges"> |
| @@ -1126,6 +1129,13 @@ const orgGraphTopo = computed(() => { | @@ -1126,6 +1129,13 @@ const orgGraphTopo = computed(() => { | ||
| 1126 | 1129 | ||
| 1127 | const orgGraphChain = computed(() => orgGraphTopo.value.chain) | 1130 | const orgGraphChain = computed(() => orgGraphTopo.value.chain) |
| 1128 | const orgGraphDegraded = computed(() => orgGraphTopo.value.degraded) | 1131 | const orgGraphDegraded = computed(() => orgGraphTopo.value.degraded) |
| 1132 | +// 有节点但一条 forward 边都没有(P0 迁移进来的存量公司就是这样)。 | ||
| 1133 | +// 此时拓扑序退化成"声明顺序",若照常画 → 箭头会凭空捏造出一条并不存在的流程, | ||
| 1134 | +// 所以这种情况只平铺节点、不画箭头,并明确告诉用户「尚未定义流转顺序」。 | ||
| 1135 | +const orgGraphNoEdges = computed(() => | ||
| 1136 | + (orgGraph.value.nodes || []).length > 0 && | ||
| 1137 | + (orgGraph.value.edges || []).filter(e => (e.edgeKind || 'forward') === 'forward').length === 0 | ||
| 1138 | +) | ||
| 1129 | 1139 | ||
| 1130 | // 回退边单独列一行,不参与拓扑序 | 1140 | // 回退边单独列一行,不参与拓扑序 |
| 1131 | const orgGraphBackEdges = computed(() => { | 1141 | const orgGraphBackEdges = computed(() => { |