exflower-license-server 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/index.html +246 -20
- package/package.json +2 -2
- package/routes.js +89 -6
package/admin/index.html
CHANGED
|
@@ -156,6 +156,7 @@
|
|
|
156
156
|
.template-type-crew { background: #f3e5f5; color: #6a1b9a; }
|
|
157
157
|
.template-type-excard { background: #e8f5e9; color: #2e7d32; }
|
|
158
158
|
.template-type-workflow { background: #fff3e0; color: #ef6c00; }
|
|
159
|
+
.template-type-doc { background: #e0f7fa; color: #00838f; }
|
|
159
160
|
textarea {
|
|
160
161
|
width: 100%;
|
|
161
162
|
padding: 8px 12px;
|
|
@@ -234,6 +235,14 @@
|
|
|
234
235
|
<input type="number" name="max_instances" placeholder="实例数" value="1" id="new-instances">
|
|
235
236
|
<input type="number" name="max_agents" placeholder="Agent数" value="5" id="new-agents">
|
|
236
237
|
<input type="number" name="max_crews" placeholder="Crew数" value="3" id="new-crews">
|
|
238
|
+
<select id="new-validity">
|
|
239
|
+
<option value="1">1 个月</option>
|
|
240
|
+
<option value="3">3 个月</option>
|
|
241
|
+
<option value="6">6 个月</option>
|
|
242
|
+
<option value="12" selected>1 年</option>
|
|
243
|
+
<option value="24">2 年</option>
|
|
244
|
+
<option value="0">永久</option>
|
|
245
|
+
</select>
|
|
237
246
|
<button class="btn btn-primary" onclick="createCompany()">创建</button>
|
|
238
247
|
</div>
|
|
239
248
|
|
|
@@ -294,6 +303,50 @@
|
|
|
294
303
|
</div>
|
|
295
304
|
</div>
|
|
296
305
|
|
|
306
|
+
<div class="modal-overlay" id="company-edit-modal">
|
|
307
|
+
<div class="modal">
|
|
308
|
+
<h3>编辑公司授权</h3>
|
|
309
|
+
<input type="hidden" id="ce-id">
|
|
310
|
+
<div class="form-group">
|
|
311
|
+
<label>公司</label>
|
|
312
|
+
<input type="text" id="ce-name" disabled>
|
|
313
|
+
</div>
|
|
314
|
+
<div class="form-group">
|
|
315
|
+
<label>有效期</label>
|
|
316
|
+
<select id="ce-validity" onchange="document.getElementById('ce-expires').disabled = (this.value !== 'custom')">
|
|
317
|
+
<option value="1">顺延 1 个月</option>
|
|
318
|
+
<option value="3">顺延 3 个月</option>
|
|
319
|
+
<option value="6">顺延 6 个月</option>
|
|
320
|
+
<option value="12">顺延 1 年</option>
|
|
321
|
+
<option value="24">顺延 2 年</option>
|
|
322
|
+
<option value="permanent">永久</option>
|
|
323
|
+
<option value="custom">指定日期</option>
|
|
324
|
+
<option value="keep">不修改</option>
|
|
325
|
+
</select>
|
|
326
|
+
</div>
|
|
327
|
+
<div class="form-group">
|
|
328
|
+
<label>到期日期(仅"指定日期"时可用)</label>
|
|
329
|
+
<input type="date" id="ce-expires" disabled>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="form-group">
|
|
332
|
+
<label>最大实例数</label>
|
|
333
|
+
<input type="number" id="ce-instances" min="1">
|
|
334
|
+
</div>
|
|
335
|
+
<div class="form-group">
|
|
336
|
+
<label>最大 Agent 数</label>
|
|
337
|
+
<input type="number" id="ce-agents" min="1">
|
|
338
|
+
</div>
|
|
339
|
+
<div class="form-group">
|
|
340
|
+
<label>最大 Crew 数</label>
|
|
341
|
+
<input type="number" id="ce-crews" min="1">
|
|
342
|
+
</div>
|
|
343
|
+
<div class="modal-actions">
|
|
344
|
+
<button class="btn" onclick="document.getElementById('company-edit-modal').classList.remove('show')">取消</button>
|
|
345
|
+
<button class="btn btn-primary" onclick="saveCompanyEdit()">保存</button>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
297
350
|
<div class="modal-overlay" id="import-detail-modal">
|
|
298
351
|
<div class="modal">
|
|
299
352
|
<h3 id="import-detail-title">导入记录</h3>
|
|
@@ -320,10 +373,10 @@
|
|
|
320
373
|
<input type="hidden" id="tmpl-id">
|
|
321
374
|
<div class="form-group">
|
|
322
375
|
<label>类型</label>
|
|
323
|
-
<select id="tmpl-type">
|
|
376
|
+
<select id="tmpl-type" onchange="onTemplateTypeChange()">
|
|
324
377
|
<option value="agent">Agent</option>
|
|
325
|
-
<option value="crew">Crew</option>
|
|
326
378
|
<option value="excard">ExCard</option>
|
|
379
|
+
<option value="doc">文档</option>
|
|
327
380
|
<option value="workflow">Workflow</option>
|
|
328
381
|
</select>
|
|
329
382
|
</div>
|
|
@@ -345,13 +398,17 @@
|
|
|
345
398
|
<option value="">全局模板</option>
|
|
346
399
|
</select>
|
|
347
400
|
</div>
|
|
348
|
-
<div class="form-group">
|
|
349
|
-
<label
|
|
350
|
-
<
|
|
401
|
+
<div class="form-group" id="tmpl-md-file-group">
|
|
402
|
+
<label>Markdown 文件(Agent / ExCard / 文档直接上传客户端导出的 .md)</label>
|
|
403
|
+
<input type="file" id="tmpl-md-file" accept=".md,.markdown,text/markdown,text/plain" onchange="onTemplateFileSelected(event)">
|
|
351
404
|
</div>
|
|
352
|
-
<div class="form-group">
|
|
353
|
-
<label
|
|
354
|
-
<textarea id="tmpl-
|
|
405
|
+
<div class="form-group" id="tmpl-json-group">
|
|
406
|
+
<label>内容 (JSON) <span style="color:#86868b" id="tmpl-json-hint"></span></label>
|
|
407
|
+
<textarea id="tmpl-json" rows="8"></textarea>
|
|
408
|
+
</div>
|
|
409
|
+
<div class="form-group" id="tmpl-md-group">
|
|
410
|
+
<label id="tmpl-md-label">Markdown 内容</label>
|
|
411
|
+
<textarea id="tmpl-md" rows="10" oninput="onTemplateMdInput()"></textarea>
|
|
355
412
|
</div>
|
|
356
413
|
<div class="modal-actions">
|
|
357
414
|
<button class="btn" onclick="closeTemplateModal()">取消</button>
|
|
@@ -392,26 +449,43 @@
|
|
|
392
449
|
document.getElementById('stat-instances').textContent = res.active_instances || 0;
|
|
393
450
|
}
|
|
394
451
|
|
|
452
|
+
let companiesCache = [];
|
|
453
|
+
|
|
395
454
|
async function loadCompanies() {
|
|
396
455
|
const res = await api('GET', '/api/admin/companies');
|
|
456
|
+
companiesCache = res.companies || [];
|
|
397
457
|
const tbody = document.getElementById('companies-body');
|
|
398
458
|
tbody.innerHTML = '';
|
|
399
459
|
|
|
400
|
-
for (const c of
|
|
460
|
+
for (const c of companiesCache) {
|
|
401
461
|
const tr = document.createElement('tr');
|
|
402
462
|
const statusClass = c.status === 'active' ? 'badge-active' : c.status === 'suspended' ? 'badge-suspended' : 'badge-expired';
|
|
403
463
|
const expired = c.expires_at && new Date(c.expires_at) < new Date();
|
|
404
464
|
const finalStatus = expired ? 'expired' : c.status;
|
|
405
465
|
const finalClass = expired ? 'badge-expired' : statusClass;
|
|
406
466
|
|
|
467
|
+
let expiresHtml = '永久';
|
|
468
|
+
if (c.expires_at) {
|
|
469
|
+
const dateStr = c.expires_at.split('T')[0];
|
|
470
|
+
const daysLeft = Math.ceil((new Date(c.expires_at) - Date.now()) / 86400000);
|
|
471
|
+
if (daysLeft < 0) {
|
|
472
|
+
expiresHtml = `${dateStr} <span style="color:#ff3b30">已过期</span>`;
|
|
473
|
+
} else if (daysLeft < 30) {
|
|
474
|
+
expiresHtml = `${dateStr} <span style="color:#ff9500">剩 ${daysLeft} 天</span>`;
|
|
475
|
+
} else {
|
|
476
|
+
expiresHtml = `${dateStr} <span style="color:#86868b">剩 ${daysLeft} 天</span>`;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
407
480
|
tr.innerHTML = `
|
|
408
481
|
<td><strong>${c.name}</strong></td>
|
|
409
482
|
<td><span class="license-key" onclick="navigator.clipboard.writeText('${c.license_key}');toast('已复制')">${c.license_key}</span></td>
|
|
410
483
|
<td>${c.max_instances}实例 / ${c.max_agents}Agent / ${c.max_crews}Crew</td>
|
|
411
484
|
<td><span class="badge ${finalClass}">${finalStatus}</span></td>
|
|
412
|
-
<td>${
|
|
485
|
+
<td>${expiresHtml}</td>
|
|
413
486
|
<td>${c.active_instances || 0}</td>
|
|
414
487
|
<td>
|
|
488
|
+
<button class="btn btn-small btn-primary" onclick="openCompanyEdit('${c.id}')">编辑</button>
|
|
415
489
|
<button class="btn btn-small ${c.status === 'active' ? 'btn-danger' : 'btn-primary'}" onclick="toggleStatus('${c.id}', '${c.status}')">
|
|
416
490
|
${c.status === 'active' ? '暂停' : '恢复'}
|
|
417
491
|
</button>
|
|
@@ -449,12 +523,20 @@
|
|
|
449
523
|
const name = document.getElementById('new-name').value.trim();
|
|
450
524
|
if (!name) return toast('请输入公司名称');
|
|
451
525
|
|
|
526
|
+
const months = parseInt(document.getElementById('new-validity').value);
|
|
527
|
+
let expiresAt = null; // 永久
|
|
528
|
+
if (months > 0) {
|
|
529
|
+
const d = new Date();
|
|
530
|
+
d.setMonth(d.getMonth() + months);
|
|
531
|
+
expiresAt = d.toISOString().slice(0, 10);
|
|
532
|
+
}
|
|
533
|
+
|
|
452
534
|
await api('POST', '/api/admin/companies', {
|
|
453
535
|
name,
|
|
454
536
|
max_instances: parseInt(document.getElementById('new-instances').value) || 1,
|
|
455
537
|
max_agents: parseInt(document.getElementById('new-agents').value) || 5,
|
|
456
538
|
max_crews: parseInt(document.getElementById('new-crews').value) || 3,
|
|
457
|
-
expires_at:
|
|
539
|
+
expires_at: expiresAt
|
|
458
540
|
});
|
|
459
541
|
|
|
460
542
|
document.getElementById('new-name').value = '';
|
|
@@ -463,6 +545,56 @@
|
|
|
463
545
|
loadStats();
|
|
464
546
|
}
|
|
465
547
|
|
|
548
|
+
function openCompanyEdit(id) {
|
|
549
|
+
const c = companiesCache.find(x => x.id === id);
|
|
550
|
+
if (!c) return toast('公司未找到');
|
|
551
|
+
document.getElementById('ce-id').value = c.id;
|
|
552
|
+
document.getElementById('ce-name').value = c.name;
|
|
553
|
+
document.getElementById('ce-validity').value = 'keep';
|
|
554
|
+
document.getElementById('ce-expires').value = c.expires_at ? c.expires_at.split('T')[0] : '';
|
|
555
|
+
document.getElementById('ce-expires').disabled = true;
|
|
556
|
+
document.getElementById('ce-instances').value = c.max_instances;
|
|
557
|
+
document.getElementById('ce-agents').value = c.max_agents;
|
|
558
|
+
document.getElementById('ce-crews').value = c.max_crews;
|
|
559
|
+
document.getElementById('company-edit-modal').classList.add('show');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function saveCompanyEdit() {
|
|
563
|
+
const id = document.getElementById('ce-id').value;
|
|
564
|
+
const c = companiesCache.find(x => x.id === id);
|
|
565
|
+
const validity = document.getElementById('ce-validity').value;
|
|
566
|
+
|
|
567
|
+
const payload = {
|
|
568
|
+
max_instances: parseInt(document.getElementById('ce-instances').value) || 1,
|
|
569
|
+
max_agents: parseInt(document.getElementById('ce-agents').value) || 1,
|
|
570
|
+
max_crews: parseInt(document.getElementById('ce-crews').value) || 1,
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
if (validity === 'permanent') {
|
|
574
|
+
payload.expires_at = null;
|
|
575
|
+
} else if (validity === 'custom') {
|
|
576
|
+
const d = document.getElementById('ce-expires').value;
|
|
577
|
+
if (!d) return toast('请选择到期日期');
|
|
578
|
+
payload.expires_at = d;
|
|
579
|
+
} else if (validity !== 'keep') {
|
|
580
|
+
// 顺延:从当前到期日(未过期)或今天(已过期/永久)起算
|
|
581
|
+
const months = parseInt(validity);
|
|
582
|
+
const now = new Date();
|
|
583
|
+
let base = now;
|
|
584
|
+
if (c && c.expires_at) {
|
|
585
|
+
const cur = new Date(c.expires_at);
|
|
586
|
+
if (cur > now) base = cur;
|
|
587
|
+
}
|
|
588
|
+
base.setMonth(base.getMonth() + months);
|
|
589
|
+
payload.expires_at = base.toISOString().slice(0, 10);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
await api('PATCH', `/api/admin/companies/${id}`, payload);
|
|
593
|
+
document.getElementById('company-edit-modal').classList.remove('show');
|
|
594
|
+
toast('已保存');
|
|
595
|
+
loadCompanies();
|
|
596
|
+
}
|
|
597
|
+
|
|
466
598
|
async function toggleStatus(id, current) {
|
|
467
599
|
const next = current === 'active' ? 'suspended' : 'active';
|
|
468
600
|
await api('PATCH', `/api/admin/companies/${id}`, { status: next });
|
|
@@ -538,6 +670,83 @@
|
|
|
538
670
|
}
|
|
539
671
|
}
|
|
540
672
|
|
|
673
|
+
// ── Template type-driven upload ──
|
|
674
|
+
const MD_TYPES = ['agent', 'excard', 'doc'];
|
|
675
|
+
const WORKFLOW_SKELETON = `{
|
|
676
|
+
"title": "工作流程名称",
|
|
677
|
+
"description": "",
|
|
678
|
+
"type": "one-time",
|
|
679
|
+
"agent": "",
|
|
680
|
+
"steps": [
|
|
681
|
+
{
|
|
682
|
+
"stepType": "task",
|
|
683
|
+
"title": "步骤1",
|
|
684
|
+
"description": "",
|
|
685
|
+
"agent": "",
|
|
686
|
+
"excardId": "<excard模板ID>",
|
|
687
|
+
"transitions": { "pass": "next", "fail": "end" }
|
|
688
|
+
}
|
|
689
|
+
]
|
|
690
|
+
}`;
|
|
691
|
+
|
|
692
|
+
function onTemplateTypeChange() {
|
|
693
|
+
const type = document.getElementById('tmpl-type').value;
|
|
694
|
+
const isMd = MD_TYPES.includes(type);
|
|
695
|
+
document.getElementById('tmpl-json-group').classList.toggle('hidden', isMd);
|
|
696
|
+
document.getElementById('tmpl-md-file-group').classList.toggle('hidden', !isMd);
|
|
697
|
+
document.getElementById('tmpl-md-label').textContent = isMd
|
|
698
|
+
? 'Markdown 内容(上传文件后可编辑)'
|
|
699
|
+
: 'Markdown 内容 (可选)';
|
|
700
|
+
if (type === 'workflow') {
|
|
701
|
+
const jsonEl = document.getElementById('tmpl-json');
|
|
702
|
+
jsonEl.placeholder = WORKFLOW_SKELETON;
|
|
703
|
+
document.getElementById('tmpl-json-hint').textContent = '步骤结构,excardId 引用模板库中的 ExCard 模板 ID';
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function parseFrontmatter(md) {
|
|
708
|
+
if (!md || !md.startsWith('---')) return { frontmatter: {}, body: md || '' };
|
|
709
|
+
const end = md.indexOf('\n---', 3);
|
|
710
|
+
if (end === -1) return { frontmatter: {}, body: md };
|
|
711
|
+
const fmText = md.slice(3, end).trim();
|
|
712
|
+
const body = md.slice(end + 4).trim();
|
|
713
|
+
const frontmatter = {};
|
|
714
|
+
for (const line of fmText.split('\n')) {
|
|
715
|
+
const m = line.match(/^([A-Za-z_][\w]*)\s*:\s*(.*)$/);
|
|
716
|
+
if (!m) continue;
|
|
717
|
+
let val = m[2].trim();
|
|
718
|
+
if (/^[[{]/.test(val) || /^(true|false|null|-?\d+(\.\d+)?)$/.test(val)) {
|
|
719
|
+
try { val = JSON.parse(val); } catch { /* keep as string */ }
|
|
720
|
+
}
|
|
721
|
+
frontmatter[m[1]] = val;
|
|
722
|
+
}
|
|
723
|
+
return { frontmatter, body };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function onTemplateFileSelected(event) {
|
|
727
|
+
const file = event.target.files?.[0];
|
|
728
|
+
if (!file) return;
|
|
729
|
+
file.text().then(text => {
|
|
730
|
+
document.getElementById('tmpl-md').value = text;
|
|
731
|
+
onTemplateMdInput();
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function onTemplateMdInput() {
|
|
736
|
+
const type = document.getElementById('tmpl-type').value;
|
|
737
|
+
if (!MD_TYPES.includes(type)) return;
|
|
738
|
+
const md = document.getElementById('tmpl-md').value;
|
|
739
|
+
const nameEl = document.getElementById('tmpl-name');
|
|
740
|
+
if (nameEl.value.trim()) return; // 不覆盖管理员手填的名称
|
|
741
|
+
const { frontmatter, body } = parseFrontmatter(md);
|
|
742
|
+
let auto = typeof frontmatter.name === 'string' ? frontmatter.name : '';
|
|
743
|
+
if (!auto) {
|
|
744
|
+
const h1 = body.match(/^#\s+(.+)$/m);
|
|
745
|
+
if (h1) auto = h1[1].trim();
|
|
746
|
+
}
|
|
747
|
+
if (auto) nameEl.value = auto;
|
|
748
|
+
}
|
|
749
|
+
|
|
541
750
|
function openTemplateModal() {
|
|
542
751
|
document.getElementById('modal-title').textContent = '上传模板';
|
|
543
752
|
document.getElementById('tmpl-id').value = '';
|
|
@@ -547,7 +756,9 @@
|
|
|
547
756
|
document.getElementById('tmpl-version').value = '';
|
|
548
757
|
document.getElementById('tmpl-json').value = '';
|
|
549
758
|
document.getElementById('tmpl-md').value = '';
|
|
759
|
+
document.getElementById('tmpl-md-file').value = '';
|
|
550
760
|
populateCompanySelect();
|
|
761
|
+
onTemplateTypeChange();
|
|
551
762
|
document.getElementById('template-modal').classList.add('show');
|
|
552
763
|
}
|
|
553
764
|
|
|
@@ -576,6 +787,8 @@
|
|
|
576
787
|
}
|
|
577
788
|
document.getElementById('tmpl-json').value = jsonStr;
|
|
578
789
|
document.getElementById('tmpl-md').value = t.content_md || '';
|
|
790
|
+
document.getElementById('tmpl-md-file').value = '';
|
|
791
|
+
onTemplateTypeChange();
|
|
579
792
|
document.getElementById('template-modal').classList.add('show');
|
|
580
793
|
}
|
|
581
794
|
|
|
@@ -592,16 +805,29 @@
|
|
|
592
805
|
const version = document.getElementById('tmpl-version').value.trim() || undefined;
|
|
593
806
|
const companyId = document.getElementById('tmpl-company').value || undefined;
|
|
594
807
|
const jsonRaw = document.getElementById('tmpl-json').value.trim();
|
|
595
|
-
const md = document.getElementById('tmpl-md').value.trim()
|
|
808
|
+
const md = document.getElementById('tmpl-md').value.trim();
|
|
596
809
|
|
|
597
810
|
if (!name) return toast('请输入模板名称');
|
|
598
|
-
if (!jsonRaw) return toast('请输入 JSON 内容');
|
|
599
811
|
|
|
600
|
-
let contentJson;
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
812
|
+
let contentJson, contentMd;
|
|
813
|
+
if (type === 'workflow') {
|
|
814
|
+
if (!jsonRaw) return toast('请输入 JSON 内容');
|
|
815
|
+
try {
|
|
816
|
+
contentJson = JSON.parse(jsonRaw);
|
|
817
|
+
} catch (e) {
|
|
818
|
+
return toast('JSON 格式错误: ' + e.message);
|
|
819
|
+
}
|
|
820
|
+
contentMd = md || undefined;
|
|
821
|
+
} else {
|
|
822
|
+
// agent / excard / doc:Markdown 原生上传
|
|
823
|
+
if (!md) return toast('请上传或粘贴 Markdown 内容');
|
|
824
|
+
const { frontmatter } = parseFrontmatter(md);
|
|
825
|
+
if (type === 'agent' || type === 'excard') {
|
|
826
|
+
contentJson = frontmatter; // 客户端导入时从 frontmatter 读取字段
|
|
827
|
+
} else {
|
|
828
|
+
contentJson = { assets: [] };
|
|
829
|
+
}
|
|
830
|
+
contentMd = md;
|
|
605
831
|
}
|
|
606
832
|
|
|
607
833
|
if (id) {
|
|
@@ -611,7 +837,7 @@
|
|
|
611
837
|
};
|
|
612
838
|
if (version) payload.version = version;
|
|
613
839
|
if (companyId !== undefined) payload.company_id = companyId;
|
|
614
|
-
if (
|
|
840
|
+
if (contentMd !== undefined) payload.content_md = contentMd;
|
|
615
841
|
await api('PATCH', `/api/admin/templates/${id}`, payload);
|
|
616
842
|
closeTemplateModal();
|
|
617
843
|
toast('模板更新成功');
|
|
@@ -624,7 +850,7 @@
|
|
|
624
850
|
description: desc,
|
|
625
851
|
version,
|
|
626
852
|
content_json: contentJson,
|
|
627
|
-
content_md:
|
|
853
|
+
content_md: contentMd,
|
|
628
854
|
});
|
|
629
855
|
closeTemplateModal();
|
|
630
856
|
toast('模板上传成功');
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exflower-license-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "ExFlower License Server - 在线授权验证服务",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"exflower-license-server": "
|
|
7
|
+
"exflower-license-server": "index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"index.js",
|
package/routes.js
CHANGED
|
@@ -109,6 +109,7 @@ function setupRoutes(app) {
|
|
|
109
109
|
max_agents: company.max_agents,
|
|
110
110
|
max_crews: company.max_crews
|
|
111
111
|
},
|
|
112
|
+
active_instances: activeInstances,
|
|
112
113
|
expires_at: company.expires_at
|
|
113
114
|
});
|
|
114
115
|
});
|
|
@@ -169,9 +170,10 @@ function setupRoutes(app) {
|
|
|
169
170
|
const key = generateLicenseKey();
|
|
170
171
|
const id = generateId();
|
|
171
172
|
|
|
173
|
+
// expires_at 显式传 null = 永久授权;不传则默认 1 年
|
|
172
174
|
const defaultExpires = new Date();
|
|
173
175
|
defaultExpires.setFullYear(defaultExpires.getFullYear() + 1);
|
|
174
|
-
const expires = expires_at || defaultExpires.toISOString().slice(0, 10);
|
|
176
|
+
const expires = expires_at === null ? null : (expires_at || defaultExpires.toISOString().slice(0, 10));
|
|
175
177
|
|
|
176
178
|
queryRun(
|
|
177
179
|
`INSERT INTO companies (id, name, license_key, max_instances, max_agents, max_crews, expires_at, status)
|
|
@@ -326,14 +328,49 @@ function setupRoutes(app) {
|
|
|
326
328
|
res.json({ success: true, templates });
|
|
327
329
|
});
|
|
328
330
|
|
|
331
|
+
// ── Template Relationship Helpers ──
|
|
332
|
+
function _extractWorkflowExcardRefs(contentJson) {
|
|
333
|
+
try {
|
|
334
|
+
const data = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson;
|
|
335
|
+
const steps = data.steps || [];
|
|
336
|
+
const refs = new Set();
|
|
337
|
+
for (const step of steps) {
|
|
338
|
+
const eid = step.excardId || step.excard_id;
|
|
339
|
+
if (eid) refs.add(eid);
|
|
340
|
+
}
|
|
341
|
+
return Array.from(refs);
|
|
342
|
+
} catch {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function _findWorkflowRefsToExcard(excardId) {
|
|
348
|
+
const allWorkflows = queryAll("SELECT id, name, content_json FROM templates WHERE type = 'workflow'");
|
|
349
|
+
const refs = [];
|
|
350
|
+
for (const wf of allWorkflows) {
|
|
351
|
+
const ids = _extractWorkflowExcardRefs(wf.content_json);
|
|
352
|
+
if (ids.includes(excardId)) {
|
|
353
|
+
refs.push({ id: wf.id, name: wf.name });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return refs;
|
|
357
|
+
}
|
|
358
|
+
|
|
329
359
|
app.post('/api/admin/templates', requireAdmin, (req, res) => {
|
|
330
360
|
const { company_id, type, name, description, content_json, content_md, tags, version } = req.body;
|
|
331
361
|
|
|
332
|
-
if (!type || !name
|
|
333
|
-
return res.status(400).json({ success: false, message: 'type
|
|
362
|
+
if (!type || !name) {
|
|
363
|
+
return res.status(400).json({ success: false, message: 'type and name are required' });
|
|
364
|
+
}
|
|
365
|
+
if (!['agent', 'excard', 'workflow', 'doc'].includes(type)) {
|
|
366
|
+
return res.status(400).json({ success: false, message: 'type must be agent, excard, workflow, or doc' });
|
|
334
367
|
}
|
|
335
|
-
|
|
336
|
-
|
|
368
|
+
// workflow 必须以 content_json 描述步骤结构;其余类型可用 content_md 原生上传
|
|
369
|
+
if (type === 'workflow' && !content_json) {
|
|
370
|
+
return res.status(400).json({ success: false, message: 'workflow templates require content_json' });
|
|
371
|
+
}
|
|
372
|
+
if (!content_json && !content_md) {
|
|
373
|
+
return res.status(400).json({ success: false, message: 'content_json or content_md is required' });
|
|
337
374
|
}
|
|
338
375
|
|
|
339
376
|
// If company_id is provided, verify it exists
|
|
@@ -344,6 +381,20 @@ function setupRoutes(app) {
|
|
|
344
381
|
}
|
|
345
382
|
}
|
|
346
383
|
|
|
384
|
+
// Validate workflow excard references
|
|
385
|
+
if (type === 'workflow') {
|
|
386
|
+
const excardRefs = _extractWorkflowExcardRefs(content_json);
|
|
387
|
+
for (const eid of excardRefs) {
|
|
388
|
+
const exists = queryGet('SELECT id FROM templates WHERE id = ? AND type = ?', [eid, 'excard']);
|
|
389
|
+
if (!exists) {
|
|
390
|
+
return res.status(400).json({
|
|
391
|
+
success: false,
|
|
392
|
+
message: `Workflow references excard template '${eid}' which does not exist in the template library`
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
347
398
|
const id = generateId();
|
|
348
399
|
queryRun(
|
|
349
400
|
`INSERT INTO templates (id, company_id, type, name, description, content_json, content_md, tags, version, created_at, updated_at)
|
|
@@ -354,7 +405,9 @@ function setupRoutes(app) {
|
|
|
354
405
|
type,
|
|
355
406
|
name,
|
|
356
407
|
description || '',
|
|
357
|
-
|
|
408
|
+
content_json
|
|
409
|
+
? (typeof content_json === 'string' ? content_json : JSON.stringify(content_json))
|
|
410
|
+
: '{}',
|
|
358
411
|
content_md || null,
|
|
359
412
|
tags ? JSON.stringify(tags) : '[]',
|
|
360
413
|
version || '1.0',
|
|
@@ -369,6 +422,22 @@ function setupRoutes(app) {
|
|
|
369
422
|
if (!template) return res.status(404).json({ success: false, message: 'Template not found' });
|
|
370
423
|
|
|
371
424
|
const { company_id, type, name, description, content_json, content_md, tags, version, is_public } = req.body;
|
|
425
|
+
|
|
426
|
+
// Validate workflow excard references when content_json is updated
|
|
427
|
+
const isWorkflow = (type !== undefined ? type : template.type) === 'workflow';
|
|
428
|
+
if (isWorkflow && content_json !== undefined) {
|
|
429
|
+
const excardRefs = _extractWorkflowExcardRefs(content_json);
|
|
430
|
+
for (const eid of excardRefs) {
|
|
431
|
+
const exists = queryGet('SELECT id FROM templates WHERE id = ? AND type = ?', [eid, 'excard']);
|
|
432
|
+
if (!exists) {
|
|
433
|
+
return res.status(400).json({
|
|
434
|
+
success: false,
|
|
435
|
+
message: `Workflow references excard template '${eid}' which does not exist in the template library`
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
372
441
|
const updates = [];
|
|
373
442
|
const params = [];
|
|
374
443
|
|
|
@@ -393,6 +462,20 @@ function setupRoutes(app) {
|
|
|
393
462
|
});
|
|
394
463
|
|
|
395
464
|
app.delete('/api/admin/templates/:id', requireAdmin, (req, res) => {
|
|
465
|
+
const template = queryGet('SELECT * FROM templates WHERE id = ?', [req.params.id]);
|
|
466
|
+
if (!template) return res.status(404).json({ success: false, message: 'Template not found' });
|
|
467
|
+
|
|
468
|
+
// Prevent deleting excard templates referenced by workflow templates
|
|
469
|
+
if (template.type === 'excard') {
|
|
470
|
+
const refs = _findWorkflowRefsToExcard(template.id);
|
|
471
|
+
if (refs.length > 0) {
|
|
472
|
+
return res.status(400).json({
|
|
473
|
+
success: false,
|
|
474
|
+
message: `Cannot delete: this ExCard template is referenced by ${refs.length} workflow template(s): ${refs.map(r => `'${r.name}'`).join(', ')}`
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
396
479
|
queryRun('DELETE FROM templates WHERE id = ?', [req.params.id]);
|
|
397
480
|
res.json({ success: true });
|
|
398
481
|
});
|