exflower-license-server 1.0.6 → 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 -19
- package/package.json +1 -1
- package/routes.js +14 -4
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,9 +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
378
|
<option value="excard">ExCard</option>
|
|
379
|
+
<option value="doc">文档</option>
|
|
326
380
|
<option value="workflow">Workflow</option>
|
|
327
381
|
</select>
|
|
328
382
|
</div>
|
|
@@ -344,13 +398,17 @@
|
|
|
344
398
|
<option value="">全局模板</option>
|
|
345
399
|
</select>
|
|
346
400
|
</div>
|
|
347
|
-
<div class="form-group">
|
|
348
|
-
<label
|
|
349
|
-
<
|
|
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)">
|
|
350
404
|
</div>
|
|
351
|
-
<div class="form-group">
|
|
352
|
-
<label
|
|
353
|
-
<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>
|
|
354
412
|
</div>
|
|
355
413
|
<div class="modal-actions">
|
|
356
414
|
<button class="btn" onclick="closeTemplateModal()">取消</button>
|
|
@@ -391,26 +449,43 @@
|
|
|
391
449
|
document.getElementById('stat-instances').textContent = res.active_instances || 0;
|
|
392
450
|
}
|
|
393
451
|
|
|
452
|
+
let companiesCache = [];
|
|
453
|
+
|
|
394
454
|
async function loadCompanies() {
|
|
395
455
|
const res = await api('GET', '/api/admin/companies');
|
|
456
|
+
companiesCache = res.companies || [];
|
|
396
457
|
const tbody = document.getElementById('companies-body');
|
|
397
458
|
tbody.innerHTML = '';
|
|
398
459
|
|
|
399
|
-
for (const c of
|
|
460
|
+
for (const c of companiesCache) {
|
|
400
461
|
const tr = document.createElement('tr');
|
|
401
462
|
const statusClass = c.status === 'active' ? 'badge-active' : c.status === 'suspended' ? 'badge-suspended' : 'badge-expired';
|
|
402
463
|
const expired = c.expires_at && new Date(c.expires_at) < new Date();
|
|
403
464
|
const finalStatus = expired ? 'expired' : c.status;
|
|
404
465
|
const finalClass = expired ? 'badge-expired' : statusClass;
|
|
405
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
|
+
|
|
406
480
|
tr.innerHTML = `
|
|
407
481
|
<td><strong>${c.name}</strong></td>
|
|
408
482
|
<td><span class="license-key" onclick="navigator.clipboard.writeText('${c.license_key}');toast('已复制')">${c.license_key}</span></td>
|
|
409
483
|
<td>${c.max_instances}实例 / ${c.max_agents}Agent / ${c.max_crews}Crew</td>
|
|
410
484
|
<td><span class="badge ${finalClass}">${finalStatus}</span></td>
|
|
411
|
-
<td>${
|
|
485
|
+
<td>${expiresHtml}</td>
|
|
412
486
|
<td>${c.active_instances || 0}</td>
|
|
413
487
|
<td>
|
|
488
|
+
<button class="btn btn-small btn-primary" onclick="openCompanyEdit('${c.id}')">编辑</button>
|
|
414
489
|
<button class="btn btn-small ${c.status === 'active' ? 'btn-danger' : 'btn-primary'}" onclick="toggleStatus('${c.id}', '${c.status}')">
|
|
415
490
|
${c.status === 'active' ? '暂停' : '恢复'}
|
|
416
491
|
</button>
|
|
@@ -448,12 +523,20 @@
|
|
|
448
523
|
const name = document.getElementById('new-name').value.trim();
|
|
449
524
|
if (!name) return toast('请输入公司名称');
|
|
450
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
|
+
|
|
451
534
|
await api('POST', '/api/admin/companies', {
|
|
452
535
|
name,
|
|
453
536
|
max_instances: parseInt(document.getElementById('new-instances').value) || 1,
|
|
454
537
|
max_agents: parseInt(document.getElementById('new-agents').value) || 5,
|
|
455
538
|
max_crews: parseInt(document.getElementById('new-crews').value) || 3,
|
|
456
|
-
expires_at:
|
|
539
|
+
expires_at: expiresAt
|
|
457
540
|
});
|
|
458
541
|
|
|
459
542
|
document.getElementById('new-name').value = '';
|
|
@@ -462,6 +545,56 @@
|
|
|
462
545
|
loadStats();
|
|
463
546
|
}
|
|
464
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
|
+
|
|
465
598
|
async function toggleStatus(id, current) {
|
|
466
599
|
const next = current === 'active' ? 'suspended' : 'active';
|
|
467
600
|
await api('PATCH', `/api/admin/companies/${id}`, { status: next });
|
|
@@ -537,6 +670,83 @@
|
|
|
537
670
|
}
|
|
538
671
|
}
|
|
539
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
|
+
|
|
540
750
|
function openTemplateModal() {
|
|
541
751
|
document.getElementById('modal-title').textContent = '上传模板';
|
|
542
752
|
document.getElementById('tmpl-id').value = '';
|
|
@@ -546,7 +756,9 @@
|
|
|
546
756
|
document.getElementById('tmpl-version').value = '';
|
|
547
757
|
document.getElementById('tmpl-json').value = '';
|
|
548
758
|
document.getElementById('tmpl-md').value = '';
|
|
759
|
+
document.getElementById('tmpl-md-file').value = '';
|
|
549
760
|
populateCompanySelect();
|
|
761
|
+
onTemplateTypeChange();
|
|
550
762
|
document.getElementById('template-modal').classList.add('show');
|
|
551
763
|
}
|
|
552
764
|
|
|
@@ -575,6 +787,8 @@
|
|
|
575
787
|
}
|
|
576
788
|
document.getElementById('tmpl-json').value = jsonStr;
|
|
577
789
|
document.getElementById('tmpl-md').value = t.content_md || '';
|
|
790
|
+
document.getElementById('tmpl-md-file').value = '';
|
|
791
|
+
onTemplateTypeChange();
|
|
578
792
|
document.getElementById('template-modal').classList.add('show');
|
|
579
793
|
}
|
|
580
794
|
|
|
@@ -591,16 +805,29 @@
|
|
|
591
805
|
const version = document.getElementById('tmpl-version').value.trim() || undefined;
|
|
592
806
|
const companyId = document.getElementById('tmpl-company').value || undefined;
|
|
593
807
|
const jsonRaw = document.getElementById('tmpl-json').value.trim();
|
|
594
|
-
const md = document.getElementById('tmpl-md').value.trim()
|
|
808
|
+
const md = document.getElementById('tmpl-md').value.trim();
|
|
595
809
|
|
|
596
810
|
if (!name) return toast('请输入模板名称');
|
|
597
|
-
if (!jsonRaw) return toast('请输入 JSON 内容');
|
|
598
811
|
|
|
599
|
-
let contentJson;
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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;
|
|
604
831
|
}
|
|
605
832
|
|
|
606
833
|
if (id) {
|
|
@@ -610,7 +837,7 @@
|
|
|
610
837
|
};
|
|
611
838
|
if (version) payload.version = version;
|
|
612
839
|
if (companyId !== undefined) payload.company_id = companyId;
|
|
613
|
-
if (
|
|
840
|
+
if (contentMd !== undefined) payload.content_md = contentMd;
|
|
614
841
|
await api('PATCH', `/api/admin/templates/${id}`, payload);
|
|
615
842
|
closeTemplateModal();
|
|
616
843
|
toast('模板更新成功');
|
|
@@ -623,7 +850,7 @@
|
|
|
623
850
|
description: desc,
|
|
624
851
|
version,
|
|
625
852
|
content_json: contentJson,
|
|
626
|
-
content_md:
|
|
853
|
+
content_md: contentMd,
|
|
627
854
|
});
|
|
628
855
|
closeTemplateModal();
|
|
629
856
|
toast('模板上传成功');
|
package/package.json
CHANGED
package/routes.js
CHANGED
|
@@ -170,9 +170,10 @@ function setupRoutes(app) {
|
|
|
170
170
|
const key = generateLicenseKey();
|
|
171
171
|
const id = generateId();
|
|
172
172
|
|
|
173
|
+
// expires_at 显式传 null = 永久授权;不传则默认 1 年
|
|
173
174
|
const defaultExpires = new Date();
|
|
174
175
|
defaultExpires.setFullYear(defaultExpires.getFullYear() + 1);
|
|
175
|
-
const expires = expires_at || defaultExpires.toISOString().slice(0, 10);
|
|
176
|
+
const expires = expires_at === null ? null : (expires_at || defaultExpires.toISOString().slice(0, 10));
|
|
176
177
|
|
|
177
178
|
queryRun(
|
|
178
179
|
`INSERT INTO companies (id, name, license_key, max_instances, max_agents, max_crews, expires_at, status)
|
|
@@ -358,12 +359,19 @@ function setupRoutes(app) {
|
|
|
358
359
|
app.post('/api/admin/templates', requireAdmin, (req, res) => {
|
|
359
360
|
const { company_id, type, name, description, content_json, content_md, tags, version } = req.body;
|
|
360
361
|
|
|
361
|
-
if (!type || !name
|
|
362
|
-
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' });
|
|
363
364
|
}
|
|
364
365
|
if (!['agent', 'excard', 'workflow', 'doc'].includes(type)) {
|
|
365
366
|
return res.status(400).json({ success: false, message: 'type must be agent, excard, workflow, or doc' });
|
|
366
367
|
}
|
|
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' });
|
|
374
|
+
}
|
|
367
375
|
|
|
368
376
|
// If company_id is provided, verify it exists
|
|
369
377
|
if (company_id) {
|
|
@@ -397,7 +405,9 @@ function setupRoutes(app) {
|
|
|
397
405
|
type,
|
|
398
406
|
name,
|
|
399
407
|
description || '',
|
|
400
|
-
|
|
408
|
+
content_json
|
|
409
|
+
? (typeof content_json === 'string' ? content_json : JSON.stringify(content_json))
|
|
410
|
+
: '{}',
|
|
401
411
|
content_md || null,
|
|
402
412
|
tags ? JSON.stringify(tags) : '[]',
|
|
403
413
|
version || '1.0',
|