Commit ae7650e8b82fae1ad75fb89a54a8549b40e265d6
1 parent
9e26bea2
feat(company): target_dir 的 API/UI 配置入口(42 号文档 Phase 3)
39 号文档 P5 落地的是"配了 company.json.target_dir 就生效"这一半, "人怎么把这两个字段填进去"此前只能手工编辑 company.json。 复用已存在的 PUT /api/company/:companyId(companyService.update()), 没有新建端点。新增 target_dir/target_mode 分支,校验直接调 path-resolver.validateLinkedTarget(39 号文档 P5 抽出来的共享函数, 和 graph-engine.js 运行时校验同一份判据):合法路径写入成功、敏感 路径黑名单拒绝、linked-new 非空目录拒绝、linked-existing 不存在拒绝, 校验失败路由层转 400;传空字符串清空配置;不传 target_mode 默认 linked-existing,与 graph-engine.js 的 readCompanyTargetConfig 同口径。 前端:CompanyDashboard.vue 概览 tab 加一张配置卡片,本地表单状态 不直接双向绑定 selectedCompany,保存失败不会冲掉用户刚打的字。 新增测试 8 例,npm run web:build 编译通过。**未做真实浏览器点击 验证**——服务器上有其它并行会话真实在跑的 agent 进程,这次没有重启 服务器(42 号文档 §二记过一次"重启杀掉别人真实进程"的教训),文档 里标记了这个已知缺口。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwG4b7Nq1NLR9eeE3M9Us9
Showing
5 changed files
with
265 additions
and
4 deletions
docs/todo/system-design/42-org-work-graph-reliability-followups.md
| 1 | 1 | # 42. Org/Work Graph 扳撠 39 ﹝ |
| 2 | 2 | |
| 3 | -> ****嚗hase 1-2 撌脣嚗2026-08-30嚗hase 3-4 敺撌乓 | |
| 3 | +> ****嚗hase 1-3 撌脣嚗2026-08-30嚗hase 3 垢蝻箔甈∠撉砲秩hase 4 敺撌乓 | |
| 4 | 4 | > ****嚗2026-08-30 |
| 5 | 5 | > ****嚗39-boss-agent-task-supervision.md](./39-boss-agent-task-supervision.md) P1-P5 摰甈∠垢蝡舀遣 銝 goal `ceo` 銝銝芸揪虜laywright 5/5 撉**1469 銝芸蝏選活瘚撠 3 銝芸瘝∟憸**遢﹝甈∪銝撌仿◇摨葵 phase |
| 6 | 6 | > **霂餉◆**嚗葵 phase 膩甈∠嚗瘚雿辣/銵靘摰 |
| ... | ... | @@ -73,7 +73,15 @@ Phase 4嚗imeline.ndjson planning/reflection 頧刻蕨 蝏扳 |
| 73 | 73 | |
| 74 | 74 | --- |
| 75 | 75 | |
| 76 | -## hase 3嚗target_dir` API/UI | |
| 76 | +## hase 3嚗target_dir` API/UI 撌脣嚗2026-08-30嚗 | |
| 77 | + | |
| 78 | +**蝏**嚗 | |
| 79 | +- 垢憭鈭歇摮 `PUT /api/company/:companyId`嚗companyService.update()`嚗瓷撱箇垢挽霈⊿"憒歇摮撠望撅"餈銝凋update()` 憓 `target_dir`/`target_mode` 嚗撉靚 `path-resolver.validateLinkedTarget`嚗39 ﹝ P5 鈭怠嚗 `graph-engine.js` 餈撉隞賢嚗撉仃韐交嚗楝撅 `try/catch` 頧 400嚗target_dir` 隡征摮泵銝脫征蔭嚗 `target_mode` 暺恕 `linked-existing`嚗 `graph-engine.js` `readCompanyTargetConfig` 敺server/tests/unit/company-target-dir-config.test.js`嚗8 靘linked-existing`/`linked-new` 楝敺恕 mode楝敺linked-new` 征敶linked-existing` 銝征蔭銝嚗 | |
| 80 | +- 垢嚗CompanyDashboard.vue` "璁"tab 撘敶獢 + 璅∪ + 靽嚗銵典 `selectedCompany`嚗仃韐交銝砲霈拍◤ | |
| 81 | + | |
| 82 | +**撉撘**嚗npm run web:build` 蝻 + 垢瘚楝撅 mock service嚗**瘝⊥撉**銝摰僎銵頝 agent 餈ps` 3 銝 `claude` 摮霈拙垢42 ﹝ 禮鈭 撌脩扇餈甈"鈭箇"悌嚗活銝**餈銝銝芸歇撩嚗""**嚗活蝒嚗瓷僎銵頝摨砲銵乩甈∠撉 | |
| 83 | + | |
| 84 | +--- | |
| 77 | 85 | |
| 78 | 86 | ****嚗5 " `company.json.target_dir` 撠梁"餈"鈭箸舅銝芸挾憛怨"餈瓷極蝻敶 JSON 辣 |
| 79 | 87 | ... | ... |
server/routes/company.js
| ... | ... | @@ -191,8 +191,16 @@ router.get('/:companyId', (req, res) => { |
| 191 | 191 | }); |
| 192 | 192 | |
| 193 | 193 | // PUT /api/company/:companyId |
| 194 | +// 42 号文档 Phase 3:body 里带 target_dir 时,companyService.update() 会做真实的路径校验 | |
| 195 | +// (复用 path-resolver.validateLinkedTarget,敏感路径黑名单 / linked-new 非空 / linked-existing | |
| 196 | +// 不存在等都在这一步拦,不用等到真正跑任务分配项目根才发现),校验失败抛出,这里接住转 400。 | |
| 194 | 197 | router.put('/:companyId', (req, res) => { |
| 195 | - const company = companyService.update(req.params.companyId, req.body); | |
| 198 | + let company; | |
| 199 | + try { | |
| 200 | + company = companyService.update(req.params.companyId, req.body); | |
| 201 | + } catch (err) { | |
| 202 | + return res.status(400).json({ error: err.message }); | |
| 203 | + } | |
| 196 | 204 | if (!company) return res.status(404).json({ error: '公司不存在' }); |
| 197 | 205 | res.json({ success: true, company }); |
| 198 | 206 | }); | ... | ... |
server/services/company-service.js
| ... | ... | @@ -357,6 +357,26 @@ class CompanyService { |
| 357 | 357 | } |
| 358 | 358 | if (updates.sop !== undefined) company.sop = updates.sop; |
| 359 | 359 | if (updates.heartbeat !== undefined) company.heartbeat = { ...company.heartbeat, ...updates.heartbeat }; |
| 360 | + | |
| 361 | + // 42 号文档 Phase 3:target_dir 的配置入口。校验复用 path-resolver 的 validateLinkedTarget | |
| 362 | + // ——39 号文档 P5 落地时就是从这里抽出来的共享函数,graph-engine.js 的 ensureProjectRoot | |
| 363 | + // 用它做运行时校验,这里在配置阶段就先做一遍,不用等第一次真的分配项目根才发现路径不对。 | |
| 364 | + // 校验失败直接抛出(调用方 try/catch 转 400),不静默吞掉、不写进一半的脏状态—— | |
| 365 | + // 抛出发生在 _saveCompany() 之前,本次 update 里其它字段也不会被落盘(同一次保存,要么全写要么不写)。 | |
| 366 | + if (updates.target_dir !== undefined) { | |
| 367 | + if (updates.target_dir === null || updates.target_dir === '') { | |
| 368 | + company.target_dir = null; | |
| 369 | + company.target_mode = null; | |
| 370 | + } else { | |
| 371 | + // 缺省 'linked-existing':与 graph-engine.js 的 readCompanyTargetConfig 同一条默认值口径, | |
| 372 | + // 两处判据分叉是这个仓库反复栽过的坑("两套体系不统一"),不能各写一份。 | |
| 373 | + const mode = updates.target_mode === 'linked-new' ? 'linked-new' : 'linked-existing'; | |
| 374 | + const { validateLinkedTarget } = require('./devloop/path-resolver'); | |
| 375 | + company.target_dir = validateLinkedTarget(updates.target_dir, mode); | |
| 376 | + company.target_mode = mode; | |
| 377 | + } | |
| 378 | + } | |
| 379 | + | |
| 360 | 380 | company.updatedAt = new Date().toISOString(); |
| 361 | 381 | |
| 362 | 382 | this._saveCompany(company); | ... | ... |
server/tests/unit/company-target-dir-config.test.js
0 → 100644
| 1 | +/** | |
| 2 | + * Unit Tests — PUT /api/company/:companyId 的 target_dir 配置入口(42 号文档 Phase 3) | |
| 3 | + * | |
| 4 | + * 39 号文档 P5 落地的是"配了 company.json.target_dir 就生效"这一半,"人怎么把这两个字段 | |
| 5 | + * 填进去"这一半此前只能手工编辑 company.json。这条测试钉住新入口的校验分支——校验复用 | |
| 6 | + * path-resolver.validateLinkedTarget(P5 已经抽出来的共享函数),跟 graph-engine.js 运行时 | |
| 7 | + * 用的是同一份判据,不重新写一遍。 | |
| 8 | + * | |
| 9 | + * 走真入口:把 router 挂到真 express 上 listen(0) 再 fetch,不 mock company-service。 | |
| 10 | + */ | |
| 11 | +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; | |
| 12 | +import fs from 'fs'; | |
| 13 | +import os from 'os'; | |
| 14 | +import path from 'path'; | |
| 15 | +import express from 'express'; | |
| 16 | + | |
| 17 | +const COMPANIES_DIR = path.resolve(__dirname, '../../../data/companies'); | |
| 18 | + | |
| 19 | +describe('PUT /api/company/:companyId — target_dir 配置(42 号文档 Phase 3)', () => { | |
| 20 | + let server; | |
| 21 | + let base; | |
| 22 | + const createdCompanies = []; | |
| 23 | + const createdDirs = []; | |
| 24 | + | |
| 25 | + beforeAll(async () => { | |
| 26 | + const app = express(); | |
| 27 | + app.use(express.json()); | |
| 28 | + app.use('/api/company', require('../../routes/company')); | |
| 29 | + await new Promise((resolve) => { server = app.listen(0, resolve); }); | |
| 30 | + base = `http://127.0.0.1:${server.address().port}/api/company`; | |
| 31 | + }); | |
| 32 | + | |
| 33 | + afterAll(() => { | |
| 34 | + if (server) server.close(); | |
| 35 | + for (const cid of createdCompanies) { | |
| 36 | + fs.rmSync(path.join(COMPANIES_DIR, cid), { recursive: true, force: true }); | |
| 37 | + } | |
| 38 | + for (const d of createdDirs) { | |
| 39 | + try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ } | |
| 40 | + } | |
| 41 | + }); | |
| 42 | + | |
| 43 | + function makeCompany(companyId) { | |
| 44 | + const dir = path.join(COMPANIES_DIR, companyId); | |
| 45 | + fs.mkdirSync(dir, { recursive: true }); | |
| 46 | + fs.writeFileSync(path.join(dir, 'company.json'), JSON.stringify({ id: companyId, name: companyId })); | |
| 47 | + createdCompanies.push(companyId); | |
| 48 | + return dir; | |
| 49 | + } | |
| 50 | + | |
| 51 | + async function put(companyId, body) { | |
| 52 | + const r = await fetch(`${base}/${companyId}`, { | |
| 53 | + method: 'PUT', | |
| 54 | + headers: { 'Content-Type': 'application/json' }, | |
| 55 | + body: JSON.stringify(body), | |
| 56 | + }); | |
| 57 | + return [r.status, await r.json()]; | |
| 58 | + } | |
| 59 | + | |
| 60 | + it('linked-existing:合法的已存在空目录,写入成功', async () => { | |
| 61 | + const cid = 'target-dir-cfg-existing'; | |
| 62 | + makeCompany(cid); | |
| 63 | + const target = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-existing-')); | |
| 64 | + createdDirs.push(target); | |
| 65 | + | |
| 66 | + const [status, body] = await put(cid, { target_dir: target, target_mode: 'linked-existing' }); | |
| 67 | + expect(status).toBe(200); | |
| 68 | + expect(body.company.target_dir).toBe(path.resolve(target)); | |
| 69 | + expect(body.company.target_mode).toBe('linked-existing'); | |
| 70 | + | |
| 71 | + // 真的落盘了,不是只回了个内存对象 | |
| 72 | + const onDisk = JSON.parse(fs.readFileSync(path.join(COMPANIES_DIR, cid, 'company.json'), 'utf8')); | |
| 73 | + expect(onDisk.target_dir).toBe(path.resolve(target)); | |
| 74 | + }); | |
| 75 | + | |
| 76 | + it('linked-new:目录不存在时自动创建,写入成功', async () => { | |
| 77 | + const cid = 'target-dir-cfg-new'; | |
| 78 | + makeCompany(cid); | |
| 79 | + const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-new-parent-')); | |
| 80 | + createdDirs.push(parent); | |
| 81 | + const target = path.join(parent, 'brand-new-project'); | |
| 82 | + | |
| 83 | + expect(fs.existsSync(target)).toBe(false); | |
| 84 | + const [status, body] = await put(cid, { target_dir: target, target_mode: 'linked-new' }); | |
| 85 | + expect(status).toBe(200); | |
| 86 | + expect(body.company.target_mode).toBe('linked-new'); | |
| 87 | + expect(fs.existsSync(target)).toBe(true); // validateLinkedTarget 的既有副作用:自动建目录 | |
| 88 | + }); | |
| 89 | + | |
| 90 | + it('不传 target_mode 时默认按 linked-existing 处理(与 graph-engine.js 的默认口径一致)', async () => { | |
| 91 | + const cid = 'target-dir-cfg-default-mode'; | |
| 92 | + makeCompany(cid); | |
| 93 | + const target = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-default-')); | |
| 94 | + createdDirs.push(target); | |
| 95 | + | |
| 96 | + const [status, body] = await put(cid, { target_dir: target }); | |
| 97 | + expect(status).toBe(200); | |
| 98 | + expect(body.company.target_mode).toBe('linked-existing'); | |
| 99 | + }); | |
| 100 | + | |
| 101 | + it('敏感路径命中黑名单 → 400,不写入', async () => { | |
| 102 | + const cid = 'target-dir-cfg-sensitive'; | |
| 103 | + makeCompany(cid); | |
| 104 | + const sensitive = path.join(os.homedir(), '.ssh', 'target-dir-cfg-should-not-write'); | |
| 105 | + | |
| 106 | + const [status, body] = await put(cid, { target_dir: sensitive, target_mode: 'linked-existing' }); | |
| 107 | + expect(status).toBe(400); | |
| 108 | + expect(body.error).toContain('sensitive_path'); | |
| 109 | + | |
| 110 | + const onDisk = JSON.parse(fs.readFileSync(path.join(COMPANIES_DIR, cid, 'company.json'), 'utf8')); | |
| 111 | + expect(onDisk.target_dir).toBeUndefined(); // 校验失败,company.json 没有被改动 | |
| 112 | + }); | |
| 113 | + | |
| 114 | + it('linked-new 但目录已存在且非空 → 400,不写入', async () => { | |
| 115 | + const cid = 'target-dir-cfg-new-nonempty'; | |
| 116 | + makeCompany(cid); | |
| 117 | + const target = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-nonempty-')); | |
| 118 | + createdDirs.push(target); | |
| 119 | + fs.writeFileSync(path.join(target, 'already-here.txt'), 'x'); | |
| 120 | + | |
| 121 | + const [status, body] = await put(cid, { target_dir: target, target_mode: 'linked-new' }); | |
| 122 | + expect(status).toBe(400); | |
| 123 | + expect(body.error).toContain('not empty'); | |
| 124 | + }); | |
| 125 | + | |
| 126 | + it('linked-existing 但目录不存在 → 400,不写入', async () => { | |
| 127 | + const cid = 'target-dir-cfg-existing-missing'; | |
| 128 | + makeCompany(cid); | |
| 129 | + const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-missing-parent-')); | |
| 130 | + createdDirs.push(parent); | |
| 131 | + const target = path.join(parent, 'does-not-exist'); | |
| 132 | + | |
| 133 | + const [status, body] = await put(cid, { target_dir: target, target_mode: 'linked-existing' }); | |
| 134 | + expect(status).toBe(400); | |
| 135 | + expect(body.error).toContain('does not exist'); | |
| 136 | + }); | |
| 137 | + | |
| 138 | + it('target_dir 传空字符串清掉已有配置', async () => { | |
| 139 | + const cid = 'target-dir-cfg-clear'; | |
| 140 | + makeCompany(cid); | |
| 141 | + const target = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-clear-')); | |
| 142 | + createdDirs.push(target); | |
| 143 | + | |
| 144 | + const [s1, b1] = await put(cid, { target_dir: target, target_mode: 'linked-existing' }); | |
| 145 | + expect(s1).toBe(200); | |
| 146 | + expect(b1.company.target_dir).toBeTruthy(); | |
| 147 | + | |
| 148 | + const [s2, b2] = await put(cid, { target_dir: '' }); | |
| 149 | + expect(s2).toBe(200); | |
| 150 | + expect(b2.company.target_dir).toBeNull(); | |
| 151 | + expect(b2.company.target_mode).toBeNull(); | |
| 152 | + }); | |
| 153 | + | |
| 154 | + it('公司不存在 → 404', async () => { | |
| 155 | + const [status] = await put('target-dir-cfg-nope', { target_dir: '/tmp/whatever' }); | |
| 156 | + expect(status).toBe(404); | |
| 157 | + }); | |
| 158 | +}); | ... | ... |
web/src/views/CompanyDashboard.vue
| ... | ... | @@ -123,6 +123,44 @@ |
| 123 | 123 | </div> |
| 124 | 124 | </div> |
| 125 | 125 | |
| 126 | + <!-- Target Dir Card(42 号文档 Phase 3:公司级目标代码目录配置) --> | |
| 127 | + <div class="overview-card"> | |
| 128 | + <div class="overview-card-header"> | |
| 129 | + <h3>📁 目标代码目录</h3> | |
| 130 | + </div> | |
| 131 | + <p style="font-size:13px; color:var(--text-secondary); margin:0 0 12px;"> | |
| 132 | + 配了之后,这家公司所有任务的交付物代码都固定落在这个目录,不再逐任务分配临时沙盒 | |
| 133 | + (类似 Claude Code 选工作目录)。留空则维持默认的沙盒模式。 | |
| 134 | + </p> | |
| 135 | + <div class="overview-info-grid" style="grid-template-columns: 1fr;"> | |
| 136 | + <div class="info-item"> | |
| 137 | + <span class="info-label">目标目录(绝对路径)</span> | |
| 138 | + <input | |
| 139 | + v-model="targetDirForm.target_dir" | |
| 140 | + type="text" | |
| 141 | + placeholder="/Users/you/projects/my-app" | |
| 142 | + style="padding:6px 10px; border:1px solid var(--border-color); border-radius:6px; font-size:13px; width:100%; box-sizing:border-box;" | |
| 143 | + /> | |
| 144 | + </div> | |
| 145 | + <div class="info-item"> | |
| 146 | + <span class="info-label">模式</span> | |
| 147 | + <select v-model="targetDirForm.target_mode" style="padding:6px 10px; border:1px solid var(--border-color); border-radius:6px; font-size:13px;"> | |
| 148 | + <option value="linked-existing">linked-existing(接管一个已存在的目录)</option> | |
| 149 | + <option value="linked-new">linked-new(新建/空目录)</option> | |
| 150 | + </select> | |
| 151 | + </div> | |
| 152 | + </div> | |
| 153 | + <div class="overview-actions"> | |
| 154 | + <button class="btn-sm" @click="saveTargetDir" :disabled="isSavingTargetDir"> | |
| 155 | + {{ isSavingTargetDir ? '⏳ 保存中...' : '💾 保存' }} | |
| 156 | + </button> | |
| 157 | + <span v-if="targetDirError" style="color:var(--status-failed); font-size:13px;">{{ targetDirError }}</span> | |
| 158 | + <span v-else-if="selectedCompany?.target_dir" style="color:var(--text-secondary); font-size:13px;"> | |
| 159 | + 当前生效:{{ selectedCompany.target_dir }}({{ selectedCompany.target_mode }}) | |
| 160 | + </span> | |
| 161 | + </div> | |
| 162 | + </div> | |
| 163 | + | |
| 126 | 164 | <!-- Agent Status Table --> |
| 127 | 165 | <div class="overview-card agents-card"> |
| 128 | 166 | <div class="overview-card-header"> |
| ... | ... | @@ -739,7 +777,7 @@ |
| 739 | 777 | </template> |
| 740 | 778 | |
| 741 | 779 | <script setup> |
| 742 | -import { ref, computed, onMounted, watch, onUnmounted } from 'vue' | |
| 780 | +import { ref, reactive, computed, onMounted, watch, onUnmounted } from 'vue' | |
| 743 | 781 | import { useRouter, useRoute } from 'vue-router' |
| 744 | 782 | import axios from 'axios' |
| 745 | 783 | import AppNav from '@/components/AppNav.vue' |
| ... | ... | @@ -756,6 +794,11 @@ const companyStatus = ref(null) |
| 756 | 794 | const isStarting = ref(false) |
| 757 | 795 | const isStopping = ref(false) |
| 758 | 796 | const selectedCompany = ref(null) |
| 797 | +// 42 号文档 Phase 3:target_dir 配置表单本地状态,不直接双向绑到 selectedCompany—— | |
| 798 | +// 保存失败时输入框不该跟着回滚成服务端的旧值,用户还得看着自己刚打的字重打一遍。 | |
| 799 | +const targetDirForm = reactive({ target_dir: '', target_mode: 'linked-existing' }) | |
| 800 | +const isSavingTargetDir = ref(false) | |
| 801 | +const targetDirError = ref('') | |
| 759 | 802 | const companyAgents = ref([]) |
| 760 | 803 | const pipelines = ref([]) |
| 761 | 804 | const pipelineRuns = ref([]) |
| ... | ... | @@ -972,6 +1015,14 @@ function goToTaskBoard() { |
| 972 | 1015 | router.push(`/${selectedCompany.value.id}/task-board`) |
| 973 | 1016 | } |
| 974 | 1017 | |
| 1018 | +// 42 号文档 Phase 3:公司切换 / 首次加载时,把表单同步成服务端当前值——只在 id 变化时同步, | |
| 1019 | +// 不watch 整个 selectedCompany,否则用户正在改的输入框会被别的字段刷新(比如心跳状态轮询)打断。 | |
| 1020 | +watch(() => selectedCompany.value?.id, () => { | |
| 1021 | + targetDirForm.target_dir = selectedCompany.value?.target_dir || '' | |
| 1022 | + targetDirForm.target_mode = selectedCompany.value?.target_mode || 'linked-existing' | |
| 1023 | + targetDirError.value = '' | |
| 1024 | +}, { immediate: true }) | |
| 1025 | + | |
| 975 | 1026 | watch(() => route?.params, async (params) => { |
| 976 | 1027 | if (!params) return |
| 977 | 1028 | if (route.name === 'CompanyDetail' && params.id) { |
| ... | ... | @@ -1154,6 +1205,22 @@ async function stopCompany() { |
| 1154 | 1205 | isStopping.value = false |
| 1155 | 1206 | } |
| 1156 | 1207 | |
| 1208 | +async function saveTargetDir() { | |
| 1209 | + if (!selectedCompany.value) return | |
| 1210 | + isSavingTargetDir.value = true | |
| 1211 | + targetDirError.value = '' | |
| 1212 | + try { | |
| 1213 | + const { data } = await axios.put(`/api/company/${selectedCompany.value.id}`, { | |
| 1214 | + target_dir: targetDirForm.target_dir, | |
| 1215 | + target_mode: targetDirForm.target_mode, | |
| 1216 | + }) | |
| 1217 | + selectedCompany.value = data.company | |
| 1218 | + } catch (e) { | |
| 1219 | + targetDirError.value = e.response?.data?.error || e.message | |
| 1220 | + } | |
| 1221 | + isSavingTargetDir.value = false | |
| 1222 | +} | |
| 1223 | + | |
| 1157 | 1224 | async function deleteCompany(companyId) { |
| 1158 | 1225 | if (!confirm('确认要删除该公司吗?该操作仅会从列表中移除公司声明。')) return |
| 1159 | 1226 | try { | ... | ... |