exflower-license-server 1.0.3 → 1.0.5

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 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
- - **类型**:`agent` / `crew` / `excard` / `workflow`
183
- - **范围**:`company_id = NULL` 为全局模板(所有公司可见);指定 `company_id` 则仅该公司可见
184
- - **内容**:`content_json`(结构化数据)+ `content_md`(Markdown 说明)
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/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,6 +1,6 @@
1
1
  {
2
2
  "name": "exflower-license-server",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "ExFlower License Server - 在线授权验证服务",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@
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
@@ -332,8 +332,8 @@ function setupRoutes(app) {
332
332
  if (!type || !name || !content_json) {
333
333
  return res.status(400).json({ success: false, message: 'type, name, content_json are required' });
334
334
  }
335
- if (!['agent', 'crew', 'excard', 'workflow'].includes(type)) {
336
- return res.status(400).json({ success: false, message: 'type must be agent, crew, excard, or workflow' });
335
+ if (!['agent', 'crew', 'excard', 'workflow', 'doc'].includes(type)) {
336
+ return res.status(400).json({ success: false, message: 'type must be agent, crew, excard, workflow, or doc' });
337
337
  }
338
338
 
339
339
  // If company_id is provided, verify it exists