Commit f3c9b91ae25c0e639cb0d3593b3622be852f0ef1

Authored by Yi Bo
1 parent 45b2c71d

feat: complete template agent management capabilities

- Added inline editing for template description and type fields
- Implemented Delete Template functionality (removes both file system and registry entry)
- Added confirmation safeguards for the destructive delete action
server/routes/company.js
... ... @@ -47,12 +47,12 @@ router.get('/templates', (req, res) => {
47 47 res.json(companyService.listTemplates());
48 48 });
49 49  
50   -// PATCH /api/company/templates/:templateId — 修改模板元信息 (如名称)
  50 +// PATCH /api/company/templates/:templateId — 修改模板元信息 (名称, 类型, 描述)
51 51 router.patch('/templates/:templateId', (req, res) => {
52 52 try {
53 53 const { templateId } = req.params;
54   - const { name } = req.body;
55   - if (!name || !name.trim()) return res.status(400).json({ error: 'name 为必填项' });
  54 + const { name, type, description } = req.body;
  55 + if (name !== undefined && !name.trim()) return res.status(400).json({ error: 'name 为必填项' });
56 56  
57 57 const path = require('path');
58 58 const fs = require('fs');
... ... @@ -63,29 +63,66 @@ router.patch('/templates/:templateId', (req, res) => {
63 63 return res.status(404).json({ error: `模板 "${templateId}" 不存在` });
64 64 }
65 65  
66   - // 1. 更新 config.json 中的 name
  66 + // 1. 更新 config.json 中的字段
67 67 const configPath = path.join(templateDir, 'config.json');
68 68 if (fs.existsSync(configPath)) {
69 69 const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
70   - config.name = name.trim();
  70 + if (name !== undefined) config.name = name.trim();
  71 + if (type !== undefined) config.type = type.trim();
  72 + if (description !== undefined) config.description = description.trim();
71 73 fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
72 74 }
73 75  
74   - // 2. 更新 _registry.json 中的 name
  76 + // 2. 更新 _registry.json 中的字段
75 77 const registryPath = path.join(TEMPLATES_DIR, '_registry.json');
76 78 if (fs.existsSync(registryPath)) {
77 79 try {
78 80 const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
79 81 const entry = registry.find(t => t.id === templateId);
80 82 if (entry) {
81   - entry.name = name.trim();
  83 + if (name !== undefined) entry.name = name.trim();
  84 + if (type !== undefined) entry.type = type.trim();
  85 + if (description !== undefined) entry.description = description.trim();
82 86 fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf-8');
83 87 }
84 88 } catch {}
85 89 }
86 90  
87   - console.log(`[Template] ✏️ 模板 ${templateId} 名称已更新为: ${name.trim()}`);
88   - res.json({ success: true, id: templateId, name: name.trim() });
  91 + console.log(`[Template] ✏️ 模板 ${templateId} 元信息已更新`);
  92 + res.json({ success: true, id: templateId, name: name?.trim(), type: type?.trim(), description: description?.trim() });
  93 + } catch (err) {
  94 + res.status(500).json({ error: err.message });
  95 + }
  96 +});
  97 +
  98 +// DELETE /api/company/templates/:templateId — 删除模板
  99 +router.delete('/templates/:templateId', (req, res) => {
  100 + try {
  101 + const { templateId } = req.params;
  102 + const path = require('path');
  103 + const fs = require('fs');
  104 + const TEMPLATES_DIR = path.resolve(__dirname, '../../system/templates');
  105 + const templateDir = path.join(TEMPLATES_DIR, templateId);
  106 +
  107 + if (!fs.existsSync(templateDir)) {
  108 + return res.status(404).json({ error: `模板 "${templateId}" 不存在` });
  109 + }
  110 +
  111 + // 1. 删除模版文件夹
  112 + fs.rmSync(templateDir, { recursive: true, force: true });
  113 +
  114 + // 2. 更新 registry
  115 + const registryPath = path.join(TEMPLATES_DIR, '_registry.json');
  116 + if (fs.existsSync(registryPath)) {
  117 + try {
  118 + let registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
  119 + registry = registry.filter(t => t.id !== templateId);
  120 + fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf-8');
  121 + } catch {}
  122 + }
  123 +
  124 + console.log(`[Template] 🗑️ 模板 ${templateId} 已被删除`);
  125 + res.json({ success: true, message: `模板 ${templateId} 已删除` });
89 126 } catch (err) {
90 127 res.status(500).json({ error: err.message });
91 128 }
... ...
web/src/views/TemplateAgents.vue
... ... @@ -96,8 +96,33 @@
96 96 </template>
97 97 <span class="template-id-tag">#{{ selectedTemplate.id }}</span>
98 98 </h2>
  99 + <div class="template-desc-edit">
  100 + <template v-if="editingDesc">
  101 + <textarea v-model="editDescValue" class="desc-edit-input form-textarea" @keyup.esc="cancelEditDesc" placeholder="输入模板描述..."></textarea>
  102 + <div class="desc-edit-actions">
  103 + <button class="name-edit-action save" @click.stop="saveTemplateDesc" title="保存">✓</button>
  104 + <button class="name-edit-action cancel" @click.stop="cancelEditDesc" title="取消">✕</button>
  105 + </div>
  106 + </template>
  107 + <template v-else>
  108 + <div class="desc-display" @click="startEditDesc" title="点击修改描述">
  109 + {{ selectedTemplate.description || '暂无描述' }} <span class="name-edit-hint">✏️</span>
  110 + </div>
  111 + </template>
  112 + </div>
99 113 <div class="meta-row">
100   - <span class="badge badge-type">{{ selectedTemplate.type }}</span>
  114 + <template v-if="editingType">
  115 + <select ref="typeSelectRef" v-model="editTypeValue" class="type-edit-select form-input" @change="saveTemplateType" @blur="cancelEditType" @keyup.esc="cancelEditType">
  116 + <option value="skill">🔧 skill</option>
  117 + <option value="executor">⚡ executor</option>
  118 + <option value="specialist">🔬 specialist</option>
  119 + <option value="cxo">👔 cxo</option>
  120 + <option value="role">🏢 role</option>
  121 + </select>
  122 + </template>
  123 + <template v-else>
  124 + <span class="badge badge-type editable-badge" @click="startEditType" title="点击修改类型">{{ selectedTemplate.type }} <span class="name-edit-hint">✏️</span></span>
  125 + </template>
101 126 <span class="badge badge-used">被使用: {{ selectedTemplate.usedCount }} 次</span>
102 127 <span v-if="selectedTemplate.benchmarkScore" class="badge badge-score">评分: {{ selectedTemplate.benchmarkScore }}</span>
103 128 <span v-if="selectedTemplate.trainedAt" class="badge badge-time">训练于: {{ selectedTemplate.trainedAt }}</span>
... ... @@ -111,6 +136,9 @@
111 136 </div>
112 137 <!-- Action Buttons -->
113 138 <div class="header-actions">
  139 + <button class="action-btn action-delete" :disabled="actionStatus !== null" @click="deleteTemplate">
  140 + 🗑️ 删除骨架
  141 + </button>
114 142 <button class="action-btn action-upload" :disabled="actionStatus === 'uploading'" @click="$refs.skillFileInput.click()">
115 143 {{ actionStatus === 'uploading' ? '⏳ 导入中...' : '📥 导入 Skill (ZIP/文件)' }}
116 144 </button>
... ... @@ -241,6 +269,13 @@ const editingName = ref(false)
241 269 const editNameValue = ref('')
242 270 const nameInputRef = ref(null)
243 271  
  272 +// Inline type/desc editing
  273 +const editingType = ref(false)
  274 +const editTypeValue = ref('')
  275 +const typeSelectRef = ref(null)
  276 +const editingDesc = ref(false)
  277 +const editDescValue = ref('')
  278 +
244 279 const fileDirty = computed(() => editFileContent.value !== null && filePreviewContent.value !== null && editFileContent.value !== filePreviewContent.value)
245 280  
246 281 function toggleSortDir() {
... ... @@ -426,6 +461,79 @@ async function saveTemplateName() {
426 461 }
427 462 }
428 463  
  464 +// ─── Inline Type / Desc Editing ───
  465 +
  466 +function startEditType() {
  467 + if (!selectedTemplate.value) return;
  468 + editTypeValue.value = selectedTemplate.value.type;
  469 + editingType.value = true;
  470 + nextTick(() => { typeSelectRef.value?.focus() });
  471 +}
  472 +
  473 +function cancelEditType() {
  474 + editingType.value = false;
  475 +}
  476 +
  477 +async function saveTemplateType() {
  478 + if (!editingType.value) return;
  479 + const newType = editTypeValue.value.trim();
  480 + if (!newType || newType === selectedTemplate.value?.type) {
  481 + cancelEditType();
  482 + return;
  483 + }
  484 + try {
  485 + await axios.patch(`/api/company/templates/${selectedTemplateId.value}`, { type: newType });
  486 + const tpl = templates.value.find(t => t.id === selectedTemplateId.value);
  487 + if (tpl) tpl.type = newType;
  488 + editingType.value = false;
  489 + } catch (e) {
  490 + alert(`修改类型失败: ${e.response?.data?.error || e.message}`);
  491 + }
  492 +}
  493 +
  494 +function startEditDesc() {
  495 + if (!selectedTemplate.value) return;
  496 + editDescValue.value = selectedTemplate.value.description || '';
  497 + editingDesc.value = true;
  498 +}
  499 +
  500 +function cancelEditDesc() {
  501 + editingDesc.value = false;
  502 +}
  503 +
  504 +async function saveTemplateDesc() {
  505 + if (!editingDesc.value) return;
  506 + const newDesc = editDescValue.value.trim();
  507 + if (newDesc === selectedTemplate.value?.description) {
  508 + cancelEditDesc();
  509 + return;
  510 + }
  511 + try {
  512 + await axios.patch(`/api/company/templates/${selectedTemplateId.value}`, { description: newDesc });
  513 + const tpl = templates.value.find(t => t.id === selectedTemplateId.value);
  514 + if (tpl) tpl.description = newDesc;
  515 + editingDesc.value = false;
  516 + } catch (e) {
  517 + alert(`修改描述失败: ${e.response?.data?.error || e.message}`);
  518 + }
  519 +}
  520 +
  521 +async function deleteTemplate() {
  522 + if (!selectedTemplate.value) return;
  523 + if (!confirm(`⚠️ 确定要彻底删除 Agent 骨架 "${selectedTemplate.value.name || selectedTemplate.value.id}" 吗?此操作将删除所有关单文件,并且无法恢复!`)) return;
  524 + try {
  525 + actionStatus.value = 'uploading'; // reuse this state to show loading
  526 + actionMessage.value = '正在删除模板...';
  527 + await axios.delete(`/api/company/templates/${selectedTemplateId.value}`);
  528 + actionStatus.value = null;
  529 + selectedTemplateId.value = null;
  530 + await loadTemplates();
  531 + } catch (e) {
  532 + actionStatus.value = 'error';
  533 + actionMessage.value = `删除失败: ${e.response?.data?.error || e.message}`;
  534 + }
  535 +}
  536 +
429 537 // ─── Agent Creator & Innovator ───
430 538  
431 539 async function triggerGenerate() {
... ... @@ -785,6 +893,19 @@ onMounted(() =&gt; {
785 893 .action-upload:hover:not(:disabled) { background: rgba(245,158,11,0.2); box-shadow: 0 3px 10px rgba(245,158,11,0.15); }
786 894 .action-provision { background: rgba(20,184,166,0.1); color: #14b8a6; border-color: rgba(20,184,166,0.2); }
787 895 .action-provision:hover:not(:disabled) { background: rgba(20,184,166,0.2); box-shadow: 0 3px 10px rgba(20,184,166,0.15); }
  896 +.action-delete { background: rgba(239, 68, 68, 0.1); color: var(--red-text, #ef4444); border-color: rgba(239, 68, 68, 0.2); }
  897 +.action-delete:hover:not(:disabled) { background: rgba(239, 68, 68, 0.2); box-shadow: 0 3px 10px rgba(239, 68, 68, 0.15); }
  898 +
  899 +/* Field Editing overrides */
  900 +.template-desc-edit { margin-bottom: 12px; margin-top: -2px; }
  901 +.desc-display { font-size: 13px; color: var(--text-secondary); cursor: pointer; display: inline-flex; align-items: flex-start; gap: 6px; }
  902 +.desc-display:hover .name-edit-hint { opacity: 0.6; }
  903 +.desc-display:hover { color: var(--text-primary); }
  904 +.desc-edit-input { width: 100%; max-width: 500px; display: block; margin-bottom: 8px; min-height: 60px; font-size: 13px; }
  905 +.desc-edit-actions { display: flex; gap: 6px; }
  906 +.editable-badge { cursor: pointer; transition: all 0.2s; display: inline-flex; align-items: center; gap: 4px; }
  907 +.editable-badge:hover { background: var(--bg-input-focus); border-color: var(--accent); color: var(--accent); }
  908 +.type-edit-select { width: 140px; }
788 909  
789 910 /* Status Banner */
790 911 .status-banner {
... ...