exflower-license-server 1.0.4 → 1.0.6
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/README.md +162 -4
- package/admin/index.html +0 -1
- package/index.js +4 -0
- package/migrate-excard-conventions.js +175 -0
- package/package.json +5 -3
- package/routes.js +75 -2
package/README.md
CHANGED
|
@@ -177,11 +177,169 @@ sudo systemctl enable --now exflower-license-server
|
|
|
177
177
|
|
|
178
178
|
## 模板系统
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
模板用于向客户端分发预设配置。管理后台支持创建 `agent`、`crew`、`excard`、`workflow`、`doc` 五种类型的模板。
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
### 通用字段
|
|
183
|
+
|
|
184
|
+
| 字段 | 必填 | 说明 |
|
|
185
|
+
|------|------|------|
|
|
186
|
+
| `type` | 是 | 模板类型:`agent` / `crew` / `excard` / `workflow` / `doc` |
|
|
187
|
+
| `name` | 是 | 模板名称 |
|
|
188
|
+
| `description` | 否 | 模板简介(一句话描述) |
|
|
189
|
+
| `content_json` | 是 | 结构化数据(JSON 对象),各类型字段见下方 |
|
|
190
|
+
| `content_md` | 否 | Markdown 格式说明文档,部分类型必须 |
|
|
191
|
+
| `company_id` | 否 | `NULL` = 全局模板(所有公司可见);指定公司 ID = 仅该公司可见 |
|
|
192
|
+
| `tags` | 否 | 标签数组 JSON,如 `["营销", "运营"]` |
|
|
193
|
+
| `version` | 否 | 版本号,默认 `1.0` |
|
|
194
|
+
|
|
195
|
+
### 各类型填写要求
|
|
196
|
+
|
|
197
|
+
#### `agent` — Agent 成员模板
|
|
198
|
+
|
|
199
|
+
用于创建 CrewAI Agent。
|
|
200
|
+
|
|
201
|
+
**content_json 字段:**
|
|
202
|
+
|
|
203
|
+
| 字段 | 必填 | 类型 | 说明 |
|
|
204
|
+
|------|------|------|------|
|
|
205
|
+
| `name` | 是 | string | Agent 名称 |
|
|
206
|
+
| `role` | 是 | string | 角色定位 |
|
|
207
|
+
| `goal` | 是 | string | 目标描述 |
|
|
208
|
+
| `backstory` | 否 | string | 背景故事(如 content_md 提供了完整 Backstory 章节,可留空) |
|
|
209
|
+
| `experience` | 否 | string | 经验积累(同上) |
|
|
210
|
+
| `tools` | 否 | string[] | 工具列表,如 `["web_search", "file_read"]` |
|
|
211
|
+
| `department` | 否 | string | 所属部门 |
|
|
212
|
+
| `allowDelegation` | 否 | boolean | 允许委派,默认 `true` |
|
|
213
|
+
| `maxIter` | 否 | number | 最大迭代次数,默认 `25` |
|
|
214
|
+
| `memory` | 否 | boolean | 启用记忆,默认 `true` |
|
|
215
|
+
| `allowScriptExecution` | 否 | string | 脚本执行模式:`disabled` / `allow_all`,默认 `allow_all` |
|
|
216
|
+
|
|
217
|
+
**content_md 格式(推荐提供):**
|
|
218
|
+
|
|
219
|
+
```markdown
|
|
220
|
+
# Backstory
|
|
221
|
+
|
|
222
|
+
Agent 的完整背景故事,支持 Markdown 格式...
|
|
223
|
+
|
|
224
|
+
# Experience
|
|
225
|
+
|
|
226
|
+
- 经验积累条目 1
|
|
227
|
+
- 经验积累条目 2
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
客户端导入时会优先从 `content_md` 中提取完整的 `Backstory` 和 `Experience` 章节。
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
#### `crew` — 项目组模板
|
|
235
|
+
|
|
236
|
+
用于创建 Crew(Agent 团队)。
|
|
237
|
+
|
|
238
|
+
**content_json 字段:**
|
|
239
|
+
|
|
240
|
+
| 字段 | 必填 | 类型 | 说明 |
|
|
241
|
+
|------|------|------|------|
|
|
242
|
+
| `name` | 是 | string | 项目组名称 |
|
|
243
|
+
| `description` | 否 | string | 项目组描述 |
|
|
244
|
+
| `agentIds` | 否 | string[] | 成员 Agent ID 列表 |
|
|
245
|
+
| `tasks` | 否 | object[] | 任务列表,每个任务包含: |
|
|
246
|
+
| `tasks[].id` | 是 | string | 任务 ID |
|
|
247
|
+
| `tasks[].agentId` | 是 | string | 执行任务的 Agent ID |
|
|
248
|
+
| `tasks[].description` | 是 | string | 任务描述 |
|
|
249
|
+
| `tasks[].order` | 否 | number | 执行顺序 |
|
|
250
|
+
| `tasks[].expectedOutput` | 否 | string | 期望输出 |
|
|
251
|
+
| `process` | 否 | string | 执行流程:`sequential`(顺序)/ `hierarchical`(层级),默认 `sequential` |
|
|
252
|
+
| `managerAgentId` | 否 | string | Manager Agent ID(层级流程时必填) |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
#### `excard` — ExCard 执行模板
|
|
257
|
+
|
|
258
|
+
用于创建工作流执行的 ExCard 模板。
|
|
259
|
+
|
|
260
|
+
**content_json 字段:**
|
|
261
|
+
|
|
262
|
+
| 字段 | 必填 | 类型 | 说明 |
|
|
263
|
+
|------|------|------|------|
|
|
264
|
+
| `id` | 否 | string | ExCard ID,为空时客户端自动生成 |
|
|
265
|
+
| `name` | 是 | string | ExCard 名称 |
|
|
266
|
+
| `description` | 否 | string | 描述 |
|
|
267
|
+
| `agent` | 否 | string | 默认执行 Agent 名称(为空时客户端自动选用第一个可用 Agent) |
|
|
268
|
+
| `category` | 否 | string | 分类,默认 `未分类` |
|
|
269
|
+
| `tags` | 否 | string[] | 标签列表 |
|
|
270
|
+
| `workflow` | 否 | object[] | 工作流步骤数组 |
|
|
271
|
+
|
|
272
|
+
**content_md 格式(必须提供):**
|
|
273
|
+
|
|
274
|
+
ExCard 的完整 Markdown 文档,包含执行约定、资源依赖、工作流步骤等。
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
#### `workflow` — 工作流模板
|
|
279
|
+
|
|
280
|
+
用于创建 Job + 预设步骤的工作流。
|
|
281
|
+
|
|
282
|
+
**content_json 字段:**
|
|
283
|
+
|
|
284
|
+
| 字段 | 必填 | 类型 | 说明 |
|
|
285
|
+
|------|------|------|------|
|
|
286
|
+
| `title` | 是 | string | Job 标题 |
|
|
287
|
+
| `description` | 否 | string | Job 描述 |
|
|
288
|
+
| `type` | 否 | string | Job 类型:`once`(一次性)/ `recurring`(周期性),默认 `once` |
|
|
289
|
+
| `agent` | 否 | string | 默认 Agent |
|
|
290
|
+
| `steps` | 否 | object[] | 步骤列表,每个步骤包含: |
|
|
291
|
+
| `steps[].stepType` / `type` | 否 | string | 步骤类型:`task` / `review` / `excard` |
|
|
292
|
+
| `steps[].title` | 否 | string | 步骤标题 |
|
|
293
|
+
| `steps[].description` | 否 | string | 步骤描述 |
|
|
294
|
+
| `steps[].agent` | 否 | string | 执行 Agent |
|
|
295
|
+
| `steps[].excardId` / `excard_id` | 否 | string | 关联的 ExCard ID |
|
|
296
|
+
| `steps[].transitionsPass` | 否 | object | 审核通过后的跳转规则 |
|
|
297
|
+
| `steps[].transitionsFail` | 否 | object | 审核驳回后的跳转规则 |
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
#### `doc` — 文档模板
|
|
302
|
+
|
|
303
|
+
用于分发知识库文档。
|
|
304
|
+
|
|
305
|
+
**content_json 字段:**
|
|
306
|
+
|
|
307
|
+
| 字段 | 必填 | 类型 | 说明 |
|
|
308
|
+
|------|------|------|------|
|
|
309
|
+
| `assets` | 否 | object[] | 附件列表,每个附件包含: |
|
|
310
|
+
| `assets[].filename` | 是 | string | 文件名 |
|
|
311
|
+
| `assets[].mimeType` | 是 | string | MIME 类型,如 `image/png` |
|
|
312
|
+
| `assets[].data` | 是 | string | Base64 编码的文件内容 |
|
|
313
|
+
|
|
314
|
+
**content_md 格式(必须提供):**
|
|
315
|
+
|
|
316
|
+
完整的文档 Markdown 内容。客户端导入时会自动创建知识库文档目录,并将 assets 解压到 `assets/` 子目录。
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### 模板创建示例(curl)
|
|
321
|
+
|
|
322
|
+
创建 Agent 模板:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
curl -X POST http://localhost:4001/api/admin/templates \
|
|
326
|
+
-H "x-admin-token: exflower-admin-2026" \
|
|
327
|
+
-H "Content-Type: application/json" \
|
|
328
|
+
-d '{
|
|
329
|
+
"type": "agent",
|
|
330
|
+
"name": "小红书运营专家",
|
|
331
|
+
"description": "提供专业级小红书运营服务",
|
|
332
|
+
"content_json": {
|
|
333
|
+
"name": "小红书运营专家",
|
|
334
|
+
"role": "小红书内容运营与增长策略师",
|
|
335
|
+
"goal": "帮助用户制定并执行高效的小红书内容策略",
|
|
336
|
+
"tools": ["web_search"],
|
|
337
|
+
"allowScriptExecution": "allow_all"
|
|
338
|
+
},
|
|
339
|
+
"content_md": "# Backstory\n\n你是一位资深小红书运营专家...\n\n# Experience\n\n- 曾帮助 50+ 品牌实现小红书粉丝破万...",
|
|
340
|
+
"tags": ["运营", "社交媒体"]
|
|
341
|
+
}'
|
|
342
|
+
```
|
|
185
343
|
|
|
186
344
|
## 构建 Docker 镜像
|
|
187
345
|
|
package/admin/index.html
CHANGED
package/index.js
CHANGED
|
@@ -38,6 +38,10 @@ app.get('/admin', (req, res) => {
|
|
|
38
38
|
res.sendFile(path.join(__dirname, 'admin', 'index.html'));
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
// ── Auto-migrate ExCard conventions on startup ──
|
|
42
|
+
const { autoMigrateExcardConventions } = require('./migrate-excard-conventions');
|
|
43
|
+
autoMigrateExcardConventions();
|
|
44
|
+
|
|
41
45
|
setupRoutes(app);
|
|
42
46
|
|
|
43
47
|
app.listen(PORT, () => {
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* License Server ExCard conventions 迁移脚本
|
|
4
|
+
* 将旧格式(字符串)的 conventions 转换为新格式(数组)
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* cd license-server
|
|
8
|
+
* node migrate-excard-conventions.js [--dry-run]
|
|
9
|
+
* npm run migrate
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const Database = require('better-sqlite3');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
function getDataDir() {
|
|
18
|
+
if (process.env.LICENSE_DATA_DIR) return process.env.LICENSE_DATA_DIR;
|
|
19
|
+
const sysDir = process.platform === 'win32'
|
|
20
|
+
? path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'exflower', 'license-server')
|
|
21
|
+
: '/var/lib/exflower/license-server';
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(sysDir)) fs.mkdirSync(sysDir, { recursive: true });
|
|
24
|
+
fs.accessSync(sysDir, fs.constants.W_OK);
|
|
25
|
+
return sysDir;
|
|
26
|
+
} catch {
|
|
27
|
+
return path.join(os.homedir(), '.exflower', 'license-server');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 解析 conventions 文本块为数组格式
|
|
33
|
+
*/
|
|
34
|
+
function parseConventionsBlock(content) {
|
|
35
|
+
if (!content) return [];
|
|
36
|
+
if (typeof content !== 'string') {
|
|
37
|
+
return Array.isArray(content) ? content : [];
|
|
38
|
+
}
|
|
39
|
+
const blocks = content.split(/\n\s*\n/).filter(Boolean);
|
|
40
|
+
const result = [];
|
|
41
|
+
for (const block of blocks) {
|
|
42
|
+
const lines = block.split('\n').filter(l => l.trim());
|
|
43
|
+
const obj = {};
|
|
44
|
+
for (const l of lines) {
|
|
45
|
+
const match1 = l.match(/^-\s+\*\*(.*?)\*\*:\s*(.*)$/);
|
|
46
|
+
if (match1) {
|
|
47
|
+
obj[match1[1].trim()] = match1[2].trim();
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const match2 = l.match(/^(.+?):\s*(.*)$/);
|
|
51
|
+
if (match2) {
|
|
52
|
+
obj[match2[1].trim()] = match2[2].trim();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(obj).length > 0) result.push(obj);
|
|
56
|
+
}
|
|
57
|
+
return result.length > 0 ? result : [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function migrateConventions(data) {
|
|
61
|
+
if (!data || !data.conventions) return data;
|
|
62
|
+
for (const key of ['input', 'output', 'errorHandling']) {
|
|
63
|
+
const val = data.conventions[key];
|
|
64
|
+
if (typeof val === 'string') {
|
|
65
|
+
data.conventions[key] = parseConventionsBlock(val);
|
|
66
|
+
} else if (!Array.isArray(val)) {
|
|
67
|
+
data.conventions[key] = [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function runMigration(options = {}) {
|
|
74
|
+
const { dryRun = false, silent = false } = options;
|
|
75
|
+
const dataDir = getDataDir();
|
|
76
|
+
const dbFile = path.join(dataDir, 'license.db');
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(dbFile)) {
|
|
79
|
+
if (!silent) console.error(`License database not found: ${dbFile}`);
|
|
80
|
+
return { migrated: 0, skipped: 0, failed: 0, error: 'Database not found' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!silent) {
|
|
84
|
+
console.log(`Database: ${dbFile}`);
|
|
85
|
+
if (dryRun) console.log('[DRY RUN] No changes will be written.\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const db = new Database(dbFile);
|
|
89
|
+
|
|
90
|
+
// Find all excard templates
|
|
91
|
+
const templates = db.prepare("SELECT id, name, content_json FROM templates WHERE type = 'excard'").all();
|
|
92
|
+
|
|
93
|
+
if (!silent) console.log(`Found ${templates.length} ExCard template(s).\n`);
|
|
94
|
+
|
|
95
|
+
let migrated = 0;
|
|
96
|
+
let skipped = 0;
|
|
97
|
+
let failed = 0;
|
|
98
|
+
|
|
99
|
+
for (const t of templates) {
|
|
100
|
+
let data;
|
|
101
|
+
try {
|
|
102
|
+
data = typeof t.content_json === 'string' ? JSON.parse(t.content_json) : t.content_json;
|
|
103
|
+
} catch (e) {
|
|
104
|
+
if (!silent) console.log(` [SKIP] ${t.id} / ${t.name} — invalid JSON: ${e.message}`);
|
|
105
|
+
failed++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!data.conventions) {
|
|
110
|
+
skipped++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const allArrays = ['input', 'output', 'errorHandling'].every(
|
|
115
|
+
key => Array.isArray(data.conventions[key])
|
|
116
|
+
);
|
|
117
|
+
if (allArrays) {
|
|
118
|
+
skipped++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
migrateConventions(data);
|
|
123
|
+
|
|
124
|
+
if (!silent) {
|
|
125
|
+
console.log(` [MIGRATE] ${t.id} / ${t.name}`);
|
|
126
|
+
console.log(` input: → array(${data.conventions.input.length})`);
|
|
127
|
+
console.log(` output: → array(${data.conventions.output.length})`);
|
|
128
|
+
console.log(` errorHandling: → array(${data.conventions.errorHandling.length})`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!dryRun) {
|
|
132
|
+
db.prepare("UPDATE templates SET content_json = ? WHERE id = ?")
|
|
133
|
+
.run(JSON.stringify(data), t.id);
|
|
134
|
+
}
|
|
135
|
+
migrated++;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
db.close();
|
|
139
|
+
|
|
140
|
+
if (!silent) {
|
|
141
|
+
console.log(`\nDone.`);
|
|
142
|
+
console.log(` Migrated: ${migrated}`);
|
|
143
|
+
console.log(` Skipped: ${skipped}`);
|
|
144
|
+
console.log(` Failed: ${failed}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { migrated, skipped, failed };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 自动迁移(启动时调用,静默模式)
|
|
152
|
+
*/
|
|
153
|
+
function autoMigrateExcardConventions() {
|
|
154
|
+
try {
|
|
155
|
+
const result = runMigration({ silent: true });
|
|
156
|
+
if (result.migrated > 0) {
|
|
157
|
+
console.log(`[AutoMigrate] ExCard conventions: ${result.migrated} migrated, ${result.skipped} skipped, ${result.failed} failed`);
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {
|
|
160
|
+
console.error('[AutoMigrate] Error:', e.message);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── CLI entry ──
|
|
165
|
+
if (require.main === module) {
|
|
166
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
167
|
+
runMigration({ dryRun, silent: false });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
runMigration,
|
|
172
|
+
autoMigrateExcardConventions,
|
|
173
|
+
migrateConventions,
|
|
174
|
+
parseConventionsBlock
|
|
175
|
+
};
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exflower-license-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
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",
|
|
11
11
|
"db.js",
|
|
12
12
|
"routes.js",
|
|
13
13
|
"utils.js",
|
|
14
|
+
"migrate-excard-conventions.js",
|
|
14
15
|
"admin/**/*",
|
|
15
16
|
"Dockerfile",
|
|
16
17
|
"docker-compose.yml",
|
|
@@ -18,7 +19,8 @@
|
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"start": "node index.js",
|
|
21
|
-
"dev": "node index.js"
|
|
22
|
+
"dev": "node index.js",
|
|
23
|
+
"migrate": "node migrate-excard-conventions.js"
|
|
22
24
|
},
|
|
23
25
|
"dependencies": {
|
|
24
26
|
"better-sqlite3": "^11.10.0",
|
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
|
});
|
|
@@ -326,14 +327,42 @@ function setupRoutes(app) {
|
|
|
326
327
|
res.json({ success: true, templates });
|
|
327
328
|
});
|
|
328
329
|
|
|
330
|
+
// ── Template Relationship Helpers ──
|
|
331
|
+
function _extractWorkflowExcardRefs(contentJson) {
|
|
332
|
+
try {
|
|
333
|
+
const data = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson;
|
|
334
|
+
const steps = data.steps || [];
|
|
335
|
+
const refs = new Set();
|
|
336
|
+
for (const step of steps) {
|
|
337
|
+
const eid = step.excardId || step.excard_id;
|
|
338
|
+
if (eid) refs.add(eid);
|
|
339
|
+
}
|
|
340
|
+
return Array.from(refs);
|
|
341
|
+
} catch {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function _findWorkflowRefsToExcard(excardId) {
|
|
347
|
+
const allWorkflows = queryAll("SELECT id, name, content_json FROM templates WHERE type = 'workflow'");
|
|
348
|
+
const refs = [];
|
|
349
|
+
for (const wf of allWorkflows) {
|
|
350
|
+
const ids = _extractWorkflowExcardRefs(wf.content_json);
|
|
351
|
+
if (ids.includes(excardId)) {
|
|
352
|
+
refs.push({ id: wf.id, name: wf.name });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return refs;
|
|
356
|
+
}
|
|
357
|
+
|
|
329
358
|
app.post('/api/admin/templates', requireAdmin, (req, res) => {
|
|
330
359
|
const { company_id, type, name, description, content_json, content_md, tags, version } = req.body;
|
|
331
360
|
|
|
332
361
|
if (!type || !name || !content_json) {
|
|
333
362
|
return res.status(400).json({ success: false, message: 'type, name, content_json are required' });
|
|
334
363
|
}
|
|
335
|
-
if (!['agent', '
|
|
336
|
-
return res.status(400).json({ success: false, message: 'type must be agent,
|
|
364
|
+
if (!['agent', 'excard', 'workflow', 'doc'].includes(type)) {
|
|
365
|
+
return res.status(400).json({ success: false, message: 'type must be agent, excard, workflow, or doc' });
|
|
337
366
|
}
|
|
338
367
|
|
|
339
368
|
// If company_id is provided, verify it exists
|
|
@@ -344,6 +373,20 @@ function setupRoutes(app) {
|
|
|
344
373
|
}
|
|
345
374
|
}
|
|
346
375
|
|
|
376
|
+
// Validate workflow excard references
|
|
377
|
+
if (type === 'workflow') {
|
|
378
|
+
const excardRefs = _extractWorkflowExcardRefs(content_json);
|
|
379
|
+
for (const eid of excardRefs) {
|
|
380
|
+
const exists = queryGet('SELECT id FROM templates WHERE id = ? AND type = ?', [eid, 'excard']);
|
|
381
|
+
if (!exists) {
|
|
382
|
+
return res.status(400).json({
|
|
383
|
+
success: false,
|
|
384
|
+
message: `Workflow references excard template '${eid}' which does not exist in the template library`
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
347
390
|
const id = generateId();
|
|
348
391
|
queryRun(
|
|
349
392
|
`INSERT INTO templates (id, company_id, type, name, description, content_json, content_md, tags, version, created_at, updated_at)
|
|
@@ -369,6 +412,22 @@ function setupRoutes(app) {
|
|
|
369
412
|
if (!template) return res.status(404).json({ success: false, message: 'Template not found' });
|
|
370
413
|
|
|
371
414
|
const { company_id, type, name, description, content_json, content_md, tags, version, is_public } = req.body;
|
|
415
|
+
|
|
416
|
+
// Validate workflow excard references when content_json is updated
|
|
417
|
+
const isWorkflow = (type !== undefined ? type : template.type) === 'workflow';
|
|
418
|
+
if (isWorkflow && content_json !== undefined) {
|
|
419
|
+
const excardRefs = _extractWorkflowExcardRefs(content_json);
|
|
420
|
+
for (const eid of excardRefs) {
|
|
421
|
+
const exists = queryGet('SELECT id FROM templates WHERE id = ? AND type = ?', [eid, 'excard']);
|
|
422
|
+
if (!exists) {
|
|
423
|
+
return res.status(400).json({
|
|
424
|
+
success: false,
|
|
425
|
+
message: `Workflow references excard template '${eid}' which does not exist in the template library`
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
372
431
|
const updates = [];
|
|
373
432
|
const params = [];
|
|
374
433
|
|
|
@@ -393,6 +452,20 @@ function setupRoutes(app) {
|
|
|
393
452
|
});
|
|
394
453
|
|
|
395
454
|
app.delete('/api/admin/templates/:id', requireAdmin, (req, res) => {
|
|
455
|
+
const template = queryGet('SELECT * FROM templates WHERE id = ?', [req.params.id]);
|
|
456
|
+
if (!template) return res.status(404).json({ success: false, message: 'Template not found' });
|
|
457
|
+
|
|
458
|
+
// Prevent deleting excard templates referenced by workflow templates
|
|
459
|
+
if (template.type === 'excard') {
|
|
460
|
+
const refs = _findWorkflowRefsToExcard(template.id);
|
|
461
|
+
if (refs.length > 0) {
|
|
462
|
+
return res.status(400).json({
|
|
463
|
+
success: false,
|
|
464
|
+
message: `Cannot delete: this ExCard template is referenced by ${refs.length} workflow template(s): ${refs.map(r => `'${r.name}'`).join(', ')}`
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
396
469
|
queryRun('DELETE FROM templates WHERE id = ?', [req.params.id]);
|
|
397
470
|
res.json({ success: true });
|
|
398
471
|
});
|