exflower-license-server 1.0.7 → 1.3.1

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.
Files changed (113) hide show
  1. package/Dockerfile +9 -11
  2. package/README.md +112 -41
  3. package/activation.js +360 -0
  4. package/admin/index.html +1175 -430
  5. package/admin/vendor/pico.min.css +4 -0
  6. package/central-public.pem +3 -0
  7. package/db.js +135 -1
  8. package/index.js +24 -2
  9. package/package.json +8 -2
  10. package/routes.js +802 -46
  11. package/seed-templates.js +131 -0
  12. package/seeds/agent-templates/agent-1001-douyin.md +355 -0
  13. package/seeds/agent-templates/agent-1002-xiaoho.md +347 -0
  14. package/seeds/agent-templates/agent-1003-zhishi.md +515 -0
  15. package/seeds/agent-templates/agent-1004-weixin.md +362 -0
  16. package/seeds/agent-templates/agent-1005-qiyepe.md +323 -0
  17. package/seeds/agent-templates/agent-1006-fapiao.md +321 -0
  18. package/seeds/agent-templates/agent-1007-zhihu.md +325 -0
  19. package/seeds/agent-templates/agent-1008-chanpi.md +367 -0
  20. package/seeds/agent-templates/agent-1009-zhaopi.md +344 -0
  21. package/seeds/agent-templates/agent-1010-meiguo.md +355 -0
  22. package/seeds/agent-templates/agent-1011-falvwe.md +374 -0
  23. package/seeds/agent-templates/agent-1012-tishici.md +371 -0
  24. package/seeds/agent-templates/agent-1013-diansh.md +371 -0
  25. package/seeds/agent-templates/agent-1014-kuajing.md +347 -0
  26. package/seeds/agent-templates/agent-1015-zhibod.md +398 -0
  27. package/seeds/agent-templates/agent-1016-bizhan.md +347 -0
  28. package/seeds/agent-templates/agent-1017-shipin.md +359 -0
  29. package/seeds/agent-templates/agent-1018-tuxian.md +345 -0
  30. package/seeds/agent-templates/agent-1019-shipint.md +354 -0
  31. package/seeds/agent-templates/agent-1020-dingji.md +347 -0
  32. package/seeds/agent-templates/agent-1021-shangy.md +364 -0
  33. package/seeds/agent-templates/agent-1022-xiaosh.md +337 -0
  34. package/seeds/agent-templates/agent-1023-kehuch.md +356 -0
  35. package/seeds/agent-templates/agent-1024-bujiyu.md +323 -0
  36. package/seeds/agent-templates/agent-1025-shujuyi.md +326 -0
  37. package/seeds/agent-templates/agent-1026-yunyin.md +336 -0
  38. package/seeds/agent-templates/agent-1027-duansh.md +335 -0
  39. package/seeds/agent-templates/agent-1028-kefux.md +329 -0
  40. package/seeds/agent-templates/agent-1029-aisou.md +352 -0
  41. package/seeds/agent-templates/agent-1030-zhengq.md +400 -0
  42. package/seeds/agent-templates/agent-1031-aizhil.md +343 -0
  43. package/seeds/agent-templates/agent-1032-shouqi.md +345 -0
  44. package/seeds/agent-templates/agent-1033-jixiao.md +333 -0
  45. package/seeds/agent-templates/agent-1034-fengxi.md +323 -0
  46. package/seeds/agent-templates/agent-1035-biange.md +318 -0
  47. package/seeds/agent-templates/agent-1036-jijins.md +329 -0
  48. package/seeds/agent-templates/agent-1037-gonggu.md +343 -0
  49. package/seeds/agent-templates/agent-1038-weibo.md +330 -0
  50. package/seeds/agent-templates/agent-1039-kuaish.md +337 -0
  51. package/seeds/agent-templates/agent-1040-uishej.md +330 -0
  52. package/seeds/agent-templates/agent-1041-wendan.md +325 -0
  53. package/seeds/agent-templates/agent-1042-youjia.md +334 -0
  54. package/seeds/agent-templates/agent-1043-gerenc.md +313 -0
  55. package/seeds/agent-templates/agent-1044-baidus.md +337 -0
  56. package/seeds/agent-templates/agent-1045-sousuo.md +326 -0
  57. package/seeds/agent-templates/agent-1046-yingyo.md +334 -0
  58. package/seeds/agent-templates/agent-1047-yonghu.md +326 -0
  59. package/seeds/agent-templates/agent-1048-qiyezh.md +328 -0
  60. package/seeds/agent-templates/agent-1049-bokece.md +337 -0
  61. package/seeds/agent-templates/agent-1050-uxjia.md +334 -0
  62. package/seeds/agent-templates/agent-1051-pinpai.md +329 -0
  63. package/seeds/agent-templates/agent-1052-zhudon.md +330 -0
  64. package/seeds/agent-templates/agent-1053-meiri.md +336 -0
  65. package/seeds/agent-templates/agent-1054-jiudia.md +334 -0
  66. package/seeds/agent-templates/agent-1055-huokce.md +333 -0
  67. package/seeds/agent-templates/agent-1056-tuihuo.md +330 -0
  68. package/seeds/agent-templates/agent-1057-zouzha.md +320 -0
  69. package/seeds/agent-templates/agent-1058-xinlix.md +315 -0
  70. package/seeds/agent-templates/agent-1059-xushix.md +320 -0
  71. package/seeds/agent-templates/agent-1060-renlei.md +328 -0
  72. package/seeds/agent-templates/agent-1061-dilixu.md +329 -0
  73. package/seeds/agent-templates/agent-1062-lishix.md +322 -0
  74. package/seeds/agent-templates/agent-1063-ganggu.md +332 -0
  75. package/seeds/agent-templates/agent-1064-caiwuj.md +321 -0
  76. package/seeds/agent-templates/agent-1065-touziy.md +318 -0
  77. package/seeds/agent-templates/agent-1066-caiwuf.md +305 -0
  78. package/seeds/agent-templates/agent-1067-jinron.md +317 -0
  79. package/seeds/agent-templates/agent-1068-caiwuy.md +330 -0
  80. package/seeds/agent-templates/agent-1069-feishu.md +346 -0
  81. package/seeds/agent-templates/agent-1070-dingdin.md +330 -0
  82. package/seeds/agent-templates/agent-1071-guonei.md +336 -0
  83. package/seeds/agent-templates/agent-1072-gongyi.md +323 -0
  84. package/seeds/agent-templates/agent-1073-fuzhuan.md +329 -0
  85. package/seeds/agent-templates/agent-1074-kucun.md +323 -0
  86. package/seeds/agent-templates/agent-1075-wululu.md +325 -0
  87. package/seeds/agent-templates/agent-1076-gongys.md +338 -0
  88. package/seeds/agent-templates/agent-1077-fufeim.md +309 -0
  89. package/seeds/agent-templates/agent-1078-guangg.md +341 -0
  90. package/seeds/agent-templates/agent-1079-shejia.md +320 -0
  91. package/seeds/agent-templates/agent-1080-ppcjing.md +318 -0
  92. package/seeds/agent-templates/agent-1081-chengxu.md +313 -0
  93. package/seeds/agent-templates/agent-1082-sousc.md +318 -0
  94. package/seeds/agent-templates/agent-1083-zhuizon.md +316 -0
  95. package/seeds/agent-templates/agent-1084-xiangmu.md +345 -0
  96. package/seeds/agent-templates/agent-1085-shiyan.md +327 -0
  97. package/seeds/agent-templates/agent-1086-xingwei.md +310 -0
  98. package/seeds/agent-templates/agent-1087-fankui.md +294 -0
  99. package/seeds/agent-templates/agent-1088-sprint.md +316 -0
  100. package/seeds/agent-templates/agent-1089-qushiya.md +316 -0
  101. package/seeds/agent-templates/agent-1090-xuexig.md +322 -0
  102. package/seeds/agent-templates/agent-1091-zhipian.md +314 -0
  103. package/seeds/agent-templates/agent-1092-weixinx.md +327 -0
  104. package/seeds/agent-templates/agent-1093-duozhin.md +335 -0
  105. package/seeds/agent-templates/agent-1094-anquan.md +314 -0
  106. package/seeds/agent-templates/agent-1095-shijian.md +330 -0
  107. package/seeds/agent-templates/agent-1096-quanzhan.md +325 -0
  108. package/seeds/agent-templates/agent-1097-daimash.md +327 -0
  109. package/seeds/agent-templates/agent-1098-devops.md +318 -0
  110. package/seeds/agent-templates/agent-1099-shujug.md +328 -0
  111. package/seeds/agent-templates/agent-1100-jishuw.md +331 -0
  112. package/seeds/agent-templates/agent-1101-zhilia.md +321 -0
  113. package/seeds/agent-templates/agent-1102-jishux.md +331 -0
package/Dockerfile CHANGED
@@ -1,7 +1,8 @@
1
1
  # ExFlower License Server Docker Image
2
2
  #
3
- # Build:
3
+ # 直接从 npm registry 拉取已发布版本,构建机无需源码:
4
4
  # docker build -t exflower/license-server:latest .
5
+ # docker build --build-arg VERSION=1.3.1 -t exflower/license-server:1.3.1 .
5
6
  #
6
7
  # Run:
7
8
  # docker run -d -p 4001:4001 \
@@ -11,17 +12,13 @@
11
12
 
12
13
  FROM node:18-alpine
13
14
 
14
- # Install build tools for native modules (better-sqlite3)
15
+ # better-sqlite3 原生模块编译工具(alpine/musl 无预编译二进制)
15
16
  RUN apk add --no-cache python3 make g++
16
17
 
17
- WORKDIR /app
18
-
19
- # Install dependencies
20
- COPY package.json package-lock.json ./
21
- RUN npm ci --production
22
-
23
- # Copy application files
24
- COPY . .
18
+ # 从 npm registry 全局安装已发布版本(国内机构建机可换 npmmirror)
19
+ ARG VERSION=latest
20
+ ARG NPM_REGISTRY=https://registry.npmjs.org
21
+ RUN npm install -g --registry=${NPM_REGISTRY} exflower-license-server@${VERSION}
25
22
 
26
23
  # Create data directory
27
24
  RUN mkdir -p /app/data
@@ -39,5 +36,6 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
39
36
  # Default environment
40
37
  ENV PORT=4001
41
38
  ENV NODE_ENV=production
39
+ ENV LICENSE_DATA_DIR=/app/data
42
40
 
43
- CMD ["node", "index.js"]
41
+ CMD ["exflower-license-server"]
package/README.md CHANGED
@@ -12,7 +12,7 @@ npm install -g exflower-license-server
12
12
 
13
13
  # 查看版本
14
14
  exflower-license-server --version
15
- # 输出: 1.0.2
15
+ # 输出: 1.0.7
16
16
 
17
17
  # 运行
18
18
  exflower-license-server
@@ -123,6 +123,7 @@ sudo systemctl enable --now exflower-license-server
123
123
  |------|--------|------|
124
124
  | `PORT` | `4001` | 服务端口 |
125
125
  | `ADMIN_TOKEN` | `exflower-admin-2026` | 管理后台鉴权 Token |
126
+ | `ADMIN_TOKEN_HASH` | — | 管理 Token 的 sha256(hex)。设置后忽略 `ADMIN_TOKEN`,明文不落环境变量(`echo -n "你的token" \| shasum -a 256` 生成) |
126
127
  | `LICENSE_DATA_DIR` | 见下方 | 数据库持久化目录 |
127
128
  | `NODE_ENV` | `production` | 运行环境 |
128
129
 
@@ -140,14 +141,59 @@ sudo systemctl enable --now exflower-license-server
140
141
 
141
142
  **⚠️ 重要**:npm 全局安装(`npm install -g`)或更新时,安装目录会被完全替换。默认不再将数据存放在包安装目录内,升级不会丢失已有数据。
142
143
 
144
+ ### 升级与数据安全
145
+
146
+ 升级(换新 tgz / 重建 Docker 镜像)只替换程序代码,以下内容全部保留:
147
+
148
+ - `license.db`:公司/License/实例记录、**客户上传的模板**、品牌设置、激活状态
149
+ - 上传的文件本体:`packages/`(轻应用)、`library-templates/`(资料图谱)、`updates/`(客户端更新包)
150
+
151
+ 注意事项:
152
+
153
+ 1. **自定义了 `LICENSE_DATA_DIR` 的部署**:升级后的启动脚本/systemd unit 必须继续带同一个值——换了路径等于换了一套空数据库(旧数据仍在原目录,切回即可恢复)。
154
+ 2. **Docker 部署**:数据在命名卷 `license-data` 里,删容器/重建镜像不受影响;但不要随手 `docker compose down -v`(`-v` 会连卷一起删)。
155
+ 3. **官方 Agent 模板播种**(v1.3.0 起随包自带 102 个全局模板):只在首次启动播种一次(settings 落标记),管理员删除的官方模板不会因升级/重启复活;播种用 `INSERT OR IGNORE`,绝不覆盖客户已有数据。
156
+
143
157
  ## 管理后台
144
158
 
145
- 启动后访问 `http://localhost:4001/admin`,使用 `ADMIN_TOKEN` 登录。
159
+ 启动后访问 `http://localhost:4001/admin`,使用 `ADMIN_TOKEN` 登录(登录时会真实校验 Token,错误无法进入)。
160
+
161
+ ### 中央激活(v1.3.0 起,必做)
162
+
163
+ **本服务部署后必须先向 ExFlower 中央授权服务激活,否则将拒绝全部客户端请求**(客户端 verify/心跳、模板、轻应用、更新下载一律 403;管理台不受影响)。
164
+
165
+ 1. 向厂商获取本服务器专属的 `customer_key`(格式 `EXFC-XXXXXX-XXXXXX-XXXXXX-XXXXXX`)
166
+ 2. 管理台首页 →「中央激活」→ 输入 key 激活
167
+ 3. 激活后每日自动向中央复核(主端点 `https://license.exflower.pro`,可配置备端点;中央短暂不可达时靠本地缓存凭证照常服务)
168
+
169
+ 有效期语义:合同到期前 30 天管理台开始提醒;过期后有 7 天宽限期(服务正常);宽限过后停止对客户端服务。同一 key 在新服务器激活会顶替旧服务器(旧服务器立即停服并显示大红横幅)。吊销/顶替/过期时管理台全站横幅提示。
170
+
171
+ **实例配额语义(v1.3.1 起)**:中央签发的 `max_instances` 是客户**创建激活码时可分配的总实例数**——所有激活码的"最大实例数"之和不得超过该额度(创建/编辑时校验,管理台会显示剩余可分配数)。客户端实际激活多少台由各激活码自己的 max_instances 管(7 天无心跳自动释放名额),中央不干预运行时激活。
146
172
 
147
- 管理后台功能:
148
- - **企业管理**:创建/编辑/禁用/删除公司,设置授权配额
149
- - **实例管理**:查看已激活的实例列表
150
- - **模板管理**:上传/编辑/删除模板(agent/crew/excard/workflow),支持按公司分发,查看导入统计(次数/公司/实例)
173
+ 管理后台功能(v1.2.0 起按私有化部署定位组织:一个后台 = 一个组织):
174
+ - **License 管理**:创建 License 时填写备注(如"市场部")并选择有效期(1/3/6 个月、1 年、2 年、永久);列表显示剩余天数(<30 天标黄、过期标红);支持编辑(改有效期/配额)、暂停/恢复、删除、**重置 Key**
175
+ - **品牌(白标)**:全局工作台名称设置(见下方"白标品牌名")
176
+ - **实例监控**:查看已激活的实例列表
177
+ - **模板管理**:按类型选择上传方式(见下方"模板系统"),支持按 License 分发,查看导入统计
178
+ - **轻应用分发**:上传 `.exfapp` 包并按 License 隔离分发;客户端「轻应用」页可浏览/下载/导入(上限 5GB,含图谱的包注意部署机磁盘)
179
+ - **客户端更新**:上传各平台更新包(mac 传 **zip**、win 传 NSIS **exe**),客户端启动自动检查 + 帮助菜单手动检查,electron-updater 静默下载安装(每个包 ~700MB+,注意部署机磁盘余量)
180
+
181
+ ### License Key 显示策略(v1.2.0 起)
182
+
183
+ - 完整 Key **只在创建/重置成功时显示一次**(弹窗 + 复制按钮),关闭后无法再查看
184
+ - 列表中 Key 以掩码显示(`EXF-XXXXXX-······-······-XXXXXX` 首尾各一段),不可点击复制
185
+ - Key 丢失 → 使用「重置 Key」:旧 Key 立即失效(已激活客户端失去授权需重新激活),新 Key 同样只显示一次
186
+
187
+ ### 安全特性(v1.2.0 起)
188
+
189
+ - 登录时真实校验 Token,错误 Token 无法进入后台
190
+ - 管理接口限速 60 次/分/IP,license 验证接口 120 次/分/IP(超限 429)
191
+ - Token 比较使用 timing-safe 方式;支持 `ADMIN_TOKEN_HASH` 哈希配置(明文不落环境变量)
192
+ - 使用默认 Token 启动时打印醒目警告;非 localhost 的 HTTP 访问显示明文传输警示 banner
193
+
194
+ ## 白标品牌名
195
+
196
+ 「品牌」频道设置**全局**工作台名称后,客户端每次 verify/心跳都会收到 `brand_name` 并写入本地 `~/.exflower/openexteam.json` 的 `title`,工作台标题随之热更新。留空则客户端保持默认 ExFlower。
151
197
 
152
198
  ## API 端点
153
199
 
@@ -156,38 +202,58 @@ sudo systemctl enable --now exflower-license-server
156
202
  | 端点 | 方法 | 说明 |
157
203
  |------|------|------|
158
204
  | `/health` | GET | 健康检查 |
159
- | `/api/license/verify` | POST | 验证 License Key |
160
- | `/api/license/heartbeat` | POST | 心跳上报 |
205
+ | `/api/license/verify` | POST | 验证 License Key(响应含 `company.brand_name`,全局品牌设置) |
206
+ | `/api/license/heartbeat` | POST | 心跳上报(响应含 `brand_name`) |
161
207
  | `/api/templates` | GET | 获取模板列表(需 `x-license-key`) |
162
208
  | `/api/templates/:id` | GET | 获取模板详情(需 `x-license-key`) |
209
+ | `/api/packages` | GET | 轻应用包列表,按公司隔离(需 `x-license-key`) |
210
+ | `/api/packages/:id/download` | GET | 下载轻应用包(需 `x-license-key`) |
211
+ | `/api/updates/latest.yml` | GET | Windows 更新清单(electron-updater,需 `x-license-key`) |
212
+ | `/api/updates/latest-mac.yml` | GET | macOS 更新清单(需 `x-license-key`) |
213
+ | `/api/updates/files/:platform/:filename` | GET | 更新包下载(需 `x-license-key`) |
163
214
 
164
215
  ### 管理后台接口
165
216
 
166
217
  | 端点 | 方法 | 说明 |
167
218
  |------|------|------|
168
- | `/api/admin/companies` | GET/POST | 企业管理 |
169
- | `/api/admin/companies/:id` | PATCH/DELETE | 更新/删除公司 |
219
+ | `/api/admin/companies` | GET/POST | License 列表(**key 掩码返回**)/创建(**key 完整返回仅此一次**) |
220
+ | `/api/admin/companies/:id` | PATCH/DELETE | 更新/删除 License |
221
+ | `/api/admin/companies/:id/reset-key` | POST | 重置 License Key(旧 key 立即失效,新 key 完整返回仅此一次) |
222
+ | `/api/admin/brand` | GET/PUT | 全局品牌名(白标)读取/设置 |
170
223
  | `/api/admin/instances` | GET | 实例列表 |
171
224
  | `/api/admin/instances/:id` | DELETE | 删除实例 |
172
225
  | `/api/admin/templates` | GET/POST | 模板列表/创建 |
173
226
  | `/api/admin/templates/:id` | PATCH/DELETE | 更新/删除模板 |
174
227
  | `/api/admin/imports` | GET | 导入统计(支持 `?template_id=` / `?company_id=` 过滤) |
228
+ | `/api/admin/packages` | GET/POST | 轻应用包列表/上传(multipart,字段 `file`+`company_id`) |
229
+ | `/api/admin/packages/:id` | DELETE | 删除轻应用包 |
230
+ | `/api/admin/updates` | GET/POST | 更新包列表/上传(multipart,字段 `file`+`platform`+`version`) |
231
+ | `/api/admin/updates/:id` | DELETE | 删除更新包 |
175
232
 
176
233
  所有管理接口需在 Header 中携带 `x-admin-token`。
177
234
 
178
235
  ## 模板系统
179
236
 
180
- 模板用于向客户端分发预设配置。管理后台支持创建 `agent`、`crew`、`excard`、`workflow`、`doc` 五种类型的模板。
237
+ 模板用于向客户端分发预设配置。管理后台支持创建 `agent`、`excard`、`workflow`、`doc` 四种类型的模板。
238
+
239
+ ### 上传方式(v1.0.7 起)
240
+
241
+ 管理后台根据类型提供不同的上传方式:
242
+
243
+ - **agent / excard / doc**:**Markdown 原生上传**。直接上传客户端导出的 `.md` 文件(或粘贴 Markdown 文本),后台自动解析 frontmatter 预填模板名称并生成 `content_json`,无需手写 JSON。
244
+ - **workflow**:仍以 JSON 描述步骤结构(步骤间的 excardId 引用和分支跳转无法由 Markdown 表达),上传框预填骨架示例。
245
+
246
+ 通过 API 创建模板时,`content_json` 与 `content_md` 至少提供一个(`workflow` 类型必须提供 `content_json`)。
181
247
 
182
248
  ### 通用字段
183
249
 
184
250
  | 字段 | 必填 | 说明 |
185
251
  |------|------|------|
186
- | `type` | 是 | 模板类型:`agent` / `crew` / `excard` / `workflow` / `doc` |
252
+ | `type` | 是 | 模板类型:`agent` / `excard` / `workflow` / `doc` |
187
253
  | `name` | 是 | 模板名称 |
188
254
  | `description` | 否 | 模板简介(一句话描述) |
189
- | `content_json` | | 结构化数据(JSON 对象),各类型字段见下方 |
190
- | `content_md` | | Markdown 格式说明文档,部分类型必须 |
255
+ | `content_json` | 视类型 | 结构化数据(JSON 对象);`workflow` 必填,其余类型可由 `content_md` 代替 |
256
+ | `content_md` | 视类型 | Markdown 内容;`agent` / `excard` / `doc` 推荐以此为主 |
191
257
  | `company_id` | 否 | `NULL` = 全局模板(所有公司可见);指定公司 ID = 仅该公司可见 |
192
258
  | `tags` | 否 | 标签数组 JSON,如 `["营销", "运营"]` |
193
259
  | `version` | 否 | 版本号,默认 `1.0` |
@@ -196,9 +262,9 @@ sudo systemctl enable --now exflower-license-server
196
262
 
197
263
  #### `agent` — Agent 成员模板
198
264
 
199
- 用于创建 CrewAI Agent
265
+ 用于创建 CrewAI Agent。**推荐直接上传客户端 agent 的 `.md` 文件**(YAML frontmatter + Backstory/Experience 正文),frontmatter 字段即 `content_json`。
200
266
 
201
- **content_json 字段:**
267
+ **frontmatter / content_json 字段:**
202
268
 
203
269
  | 字段 | 必填 | 类型 | 说明 |
204
270
  |------|------|------|------|
@@ -217,6 +283,18 @@ sudo systemctl enable --now exflower-license-server
217
283
  **content_md 格式(推荐提供):**
218
284
 
219
285
  ```markdown
286
+ ---
287
+ id: agent-xxx
288
+ name: 小红书运营专家
289
+ role: 小红书内容运营与增长策略师
290
+ goal: 帮助用户制定并执行高效的小红书内容策略
291
+ tools: []
292
+ allowDelegation: false
293
+ maxIter: 25
294
+ memory: true
295
+ allowScriptExecution: disabled
296
+ ---
297
+
220
298
  # Backstory
221
299
 
222
300
  Agent 的完整背景故事,支持 Markdown 格式...
@@ -231,33 +309,11 @@ Agent 的完整背景故事,支持 Markdown 格式...
231
309
 
232
310
  ---
233
311
 
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
312
  #### `excard` — ExCard 执行模板
257
313
 
258
- 用于创建工作流执行的 ExCard 模板。
314
+ 用于创建工作流执行的 ExCard 模板。**推荐直接上传客户端导出的标准 ExCard `.md` 文件**(与客户端"导出 ExCard"产物格式一致):模板只需存 `content_md`,客户端导入时走标准 MD 导入链路(`POST /api/excards/import`),由客户端解析为结构化数据。frontmatter 会被后台自动提取为 `content_json` 用于列表展示。
259
315
 
260
- **content_json 字段:**
316
+ **frontmatter / content_json 字段:**
261
317
 
262
318
  | 字段 | 必填 | 类型 | 说明 |
263
319
  |------|------|------|------|
@@ -271,7 +327,7 @@ Agent 的完整背景故事,支持 Markdown 格式...
271
327
 
272
328
  **content_md 格式(必须提供):**
273
329
 
274
- ExCard 的完整 Markdown 文档,包含执行约定、资源依赖、工作流步骤等。
330
+ ExCard 的完整 Markdown 文档(frontmatter + 资源依赖 + 执行步骤 + 执行约定),与客户端 `~/.exflower/excards-md/` 下的文件格式一致。
275
331
 
276
332
  ---
277
333
 
@@ -341,6 +397,21 @@ curl -X POST http://localhost:4001/api/admin/templates \
341
397
  }'
342
398
  ```
343
399
 
400
+ ### 批量种子导入(部署自带模板)
401
+
402
+ `scripts/seed-templates.js` 用于部署时批量导入 Agent 模板(自动建分类、挂分类)与内置帮助文档,幂等可重复执行(分类按名复用,模板按 type+name 跳过):
403
+
404
+ ```bash
405
+ node scripts/seed-templates.js \
406
+ --url http://127.0.0.1:4001 --token <ADMIN_TOKEN> \
407
+ --agents-dir /path/to/agent-mds \
408
+ [--guide /path/to/guide/README.md]
409
+ ```
410
+
411
+ - Agent 目录中的 `agent-*.md` 按脚本内置的序号→分类映射归入 5 个分类(新媒体内容运营 / 增长与获客 / 销售与客户成功 / 企业职能与合规 / AI 与专业生产力),frontmatter 自动转为 `content_json`。
412
+ - 导入的模板均为**全局模板**(所有 License 可见)。
413
+ - 脚本遵守 admin 接口限速(60 次/分钟),遇到 429 自动退避重试。
414
+
344
415
  ## 构建 Docker 镜像
345
416
 
346
417
  ```bash
package/activation.js ADDED
@@ -0,0 +1,360 @@
1
+ /**
2
+ * 中央授权激活模块(M5)
3
+ *
4
+ * license-server 作为「客户服务器」向中央授权服务(license.exflower.pro)激活并每日复核:
5
+ * - customer_key 永不网传:proof = HMAC_SHA256(sha256(key+'|hmac'), action|prefix|fingerprint|ts)
6
+ * - 中央返回 Ed25519 签名凭证(30 天滑动),本地缓存验签后生效
7
+ * - 主端点不通自动切备端点;双端点同时不通靠缓存凭证撑到 token_expires_at
8
+ * - 未激活 / 被顶替 / 合同过期 / 凭证超期 = 拒绝客户端服务(路由层门禁)
9
+ *
10
+ * 状态文件:DATA_DIR/activation.json
11
+ * 验签逻辑(canonical JSON + Ed25519)与 central-license/lib/credential.js 保持一致,
12
+ * 此处自包含拷贝——license-server 独立打包部署,不能跨目录引用。
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const crypto = require('crypto');
18
+ const { DATA_DIR } = require('./db');
19
+
20
+ const STATE_FILE = path.join(DATA_DIR, 'activation.json');
21
+ const GRACE_MS = 7 * 86400000; // 合同过期宽限期(与中央一致)
22
+ const REMIND_MS = 30 * 86400000; // 到期前提醒起点(与中央一致)
23
+ const REVALIDATE_INTERVAL_MS = 24 * 3600000; // 每日复核
24
+ const RETRY_INTERVAL_MS = 3600000; // 失败后每小时重试
25
+ const HTTP_TIMEOUT_MS = 10000;
26
+
27
+ const DEFAULT_PRIMARY = 'https://license.exflower.pro';
28
+
29
+ // ── 状态读写 ──
30
+
31
+ let state = null;
32
+
33
+ function loadState() {
34
+ if (state) return state;
35
+ try {
36
+ state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
37
+ } catch {
38
+ state = {};
39
+ }
40
+ return state;
41
+ }
42
+
43
+ function saveState() {
44
+ fs.mkdirSync(DATA_DIR, { recursive: true });
45
+ const tmp = STATE_FILE + '.tmp';
46
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
47
+ fs.renameSync(tmp, STATE_FILE);
48
+ }
49
+
50
+ // 服务器指纹:首次生成后持久不变(激活身份)
51
+ function getFingerprint() {
52
+ const s = loadState();
53
+ if (!s.fingerprint) {
54
+ s.fingerprint = crypto.randomUUID();
55
+ saveState();
56
+ }
57
+ return s.fingerprint;
58
+ }
59
+
60
+ // ── 验签(自包含,勿改逻辑,与 central-license/lib/credential.js 对齐) ──
61
+
62
+ const CRED_FORMAT = 'exflower-server-license';
63
+
64
+ function canonicalize(value) {
65
+ if (Array.isArray(value)) {
66
+ return '[' + value.map(canonicalize).join(',') + ']';
67
+ }
68
+ if (value && typeof value === 'object') {
69
+ const keys = Object.keys(value).sort();
70
+ return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalize(value[k])).join(',') + '}';
71
+ }
72
+ return JSON.stringify(value);
73
+ }
74
+
75
+ function loadCentralPublicKey() {
76
+ if (process.env.CENTRAL_PUBLIC_KEY) return process.env.CENTRAL_PUBLIC_KEY.replace(/\\n/g, '\n');
77
+ return fs.readFileSync(path.join(__dirname, 'central-public.pem'), 'utf8');
78
+ }
79
+
80
+ /** 验签凭证,返回明文主体;失败返回 null */
81
+ function verifyCredential(credential) {
82
+ try {
83
+ if (!credential || credential.format !== CRED_FORMAT || !credential.signature) return null;
84
+ const body = { ...credential };
85
+ delete body.signature;
86
+ const sig = Buffer.from(credential.signature, 'base64url');
87
+ const ok = crypto.verify(null, Buffer.from(canonicalize(body), 'utf8'), loadCentralPublicKey(), sig);
88
+ return ok ? body : null;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ // ── HMAC 证明(与 central-license/src/keys.js 协议一致) ──
95
+
96
+ function computeProof(key, action, fingerprint, ts) {
97
+ const prefix = key.split('-').slice(0, 2).join('-');
98
+ const secret = crypto.createHash('sha256').update(key + '|hmac', 'utf8').digest('hex');
99
+ const hmac = crypto.createHmac('sha256', secret)
100
+ .update(`${action}|${prefix}|${fingerprint}|${ts}`, 'utf8')
101
+ .digest('hex');
102
+ return { key_prefix: prefix, fingerprint, ts, hmac };
103
+ }
104
+
105
+ // ── 中央端点调用(主备 failover) ──
106
+
107
+ function getEndpoints() {
108
+ const s = loadState();
109
+ // CENTRAL_PRIMARY_URL:开发/测试覆盖主端点(生产默认 license.exflower.pro)
110
+ const list = [process.env.CENTRAL_PRIMARY_URL || (s.endpoints && s.endpoints.primary) || DEFAULT_PRIMARY];
111
+ if (s.endpoints && s.endpoints.backup) list.push(s.endpoints.backup);
112
+ return list.map(u => u.replace(/\/+$/, ''));
113
+ }
114
+
115
+ async function postJson(url, body) {
116
+ const ctrl = new AbortController();
117
+ const timer = setTimeout(() => ctrl.abort(), HTTP_TIMEOUT_MS);
118
+ try {
119
+ const res = await fetch(url, {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/json' },
122
+ body: JSON.stringify(body),
123
+ signal: ctrl.signal,
124
+ });
125
+ const data = await res.json().catch(() => ({}));
126
+ return { status: res.status, data };
127
+ } finally {
128
+ clearTimeout(timer);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * 调中央 activate / revalidate,主备依次尝试。
134
+ * 返回 { ok, token?, contract_state?, error?, message?, superseded? }
135
+ */
136
+ async function callCentral(action) {
137
+ const s = loadState();
138
+ const key = s.customer_key;
139
+ if (!key) return { ok: false, error: 'no_key' };
140
+ const ts = Date.now();
141
+ const proof = computeProof(key, action, getFingerprint(), ts);
142
+ let lastErr = 'all endpoints unreachable';
143
+ for (const base of getEndpoints()) {
144
+ try {
145
+ const { status, data } = await postJson(`${base}/api/v1/${action}`, proof);
146
+ if (data && data.success && data.token) {
147
+ return { ok: true, token: data.token, contract_state: data.contract_state, endpoint: base };
148
+ }
149
+ // 明确的业务拒绝(revoked/expired/superseded/bad_proof…)不再尝试备机——结果一样
150
+ const err = (data && data.error) || `http_${status}`;
151
+ return { ok: false, error: err, message: data && data.message };
152
+ } catch (e) {
153
+ lastErr = `${base}: ${e.name === 'AbortError' ? 'timeout' : e.message}`;
154
+ }
155
+ }
156
+ return { ok: false, error: 'unreachable', message: lastErr };
157
+ }
158
+
159
+ // ── 激活 / 复核 ──
160
+
161
+ /** 首次激活(或清除后重新激活)。返回 { ok, error?, message? } */
162
+ async function activateNow() {
163
+ const r = await callCentral('activate');
164
+ const s = loadState();
165
+ if (r.ok) {
166
+ const payload = verifyCredential(r.token);
167
+ if (!payload) {
168
+ s.last_error = '中央返回的凭证验签失败';
169
+ saveState();
170
+ return { ok: false, error: 'bad_signature' };
171
+ }
172
+ s.credential = r.token;
173
+ s.superseded = false;
174
+ s.terminal = null;
175
+ s.last_revalidate = new Date().toISOString();
176
+ s.last_error = null;
177
+ saveState();
178
+ return { ok: true, contract_state: r.contract_state };
179
+ }
180
+ s.last_error = r.message || r.error;
181
+ saveState();
182
+ return r;
183
+ }
184
+
185
+ /** 每日复核:凭证滑动顺延;被顶替/吊销/过期落状态。返回 { ok, error? } */
186
+ async function revalidateNow() {
187
+ const s = loadState();
188
+ if (!s.customer_key) return { ok: false, error: 'no_key' };
189
+ if (!s.credential) return activateNow(); // 本地无凭证 → 走激活
190
+ const r = await callCentral('revalidate');
191
+ if (r.ok) {
192
+ const payload = verifyCredential(r.token);
193
+ if (!payload) {
194
+ s.last_error = '中央返回的凭证验签失败';
195
+ saveState();
196
+ return { ok: false, error: 'bad_signature' };
197
+ }
198
+ s.credential = r.token;
199
+ s.superseded = false;
200
+ s.last_revalidate = new Date().toISOString();
201
+ s.last_error = null;
202
+ saveState();
203
+ return { ok: true, contract_state: r.contract_state };
204
+ }
205
+ if (r.error === 'superseded') {
206
+ s.superseded = true;
207
+ s.last_error = r.message || '本服务器的激活已被同 License 的新激活顶替';
208
+ saveState();
209
+ return r;
210
+ }
211
+ if (r.error === 'not_activated') {
212
+ // 中央侧无此激活记录(库被重置等)→ 重新激活
213
+ return activateNow();
214
+ }
215
+ if (r.error === 'revoked' || r.error === 'expired') {
216
+ // 合同终止/超宽限:清凭证,立即进入拒绝服务
217
+ s.credential = null;
218
+ s.terminal = r.error; // 终态:revoked / expired(区别于「从未激活」)
219
+ s.last_error = r.message || (r.error === 'revoked' ? 'License 已被吊销' : '合同已过期且超出宽限期');
220
+ saveState();
221
+ return r;
222
+ }
223
+ // 网络类失败:保留缓存凭证,下个周期再试
224
+ s.last_error = r.message || r.error;
225
+ saveState();
226
+ return r;
227
+ }
228
+
229
+ // ── 执行状态推导(路由门禁用) ──
230
+
231
+ /**
232
+ * 当前执行状态:
233
+ * unactivated 未配置 customer_key,或从未激活成功
234
+ * superseded 被同 License 的新服务器顶替
235
+ * revoked 被中央吊销(复核时确认)
236
+ * expired 合同过期且超宽限期
237
+ * token_expired 缓存凭证超 30 天且复核未成功(双端点失联兜底边界)
238
+ * grace 合同过期宽限中(可服务,提醒)
239
+ * expiring_soon 合同 30 天内到期(可服务,提醒)
240
+ * active 正常
241
+ */
242
+ function getStatus() {
243
+ const s = loadState();
244
+ const base = {
245
+ configured: !!s.customer_key,
246
+ key_prefix: s.customer_key ? s.customer_key.split('-').slice(0, 2).join('-') : null,
247
+ fingerprint: s.fingerprint || null,
248
+ endpoints: { primary: process.env.CENTRAL_PRIMARY_URL || (s.endpoints && s.endpoints.primary) || DEFAULT_PRIMARY, backup: (s.endpoints && s.endpoints.backup) || '' },
249
+ last_revalidate: s.last_revalidate || null,
250
+ last_error: s.last_error || null,
251
+ };
252
+ if (!s.customer_key) return { ...base, state: 'unactivated' };
253
+ if (s.superseded) return { ...base, state: 'superseded' };
254
+ if (!s.credential) return { ...base, state: s.terminal || 'unactivated', message: s.last_error };
255
+
256
+ const payload = verifyCredential(s.credential);
257
+ if (!payload) return { ...base, state: 'unactivated', message: '缓存凭证验签失败' };
258
+
259
+ const now = Date.now();
260
+ const expires = new Date(payload.expires_at).getTime();
261
+ const tokenExpires = new Date(payload.token_expires_at).getTime();
262
+ const info = {
263
+ customer: payload.customer,
264
+ license_id: payload.license_id,
265
+ expires_at: payload.expires_at,
266
+ token_expires_at: payload.token_expires_at,
267
+ days_remaining: Math.max(0, Math.ceil((expires - now) / 86400000)),
268
+ limits: payload.limits || {},
269
+ };
270
+ if (now > expires + GRACE_MS) return { ...base, ...info, state: 'expired' };
271
+ if (now > tokenExpires) return { ...base, ...info, state: 'token_expired' };
272
+ if (now > expires) return { ...base, ...info, state: 'grace' };
273
+ if (expires - now < REMIND_MS) return { ...base, ...info, state: 'expiring_soon' };
274
+ return { ...base, ...info, state: 'active' };
275
+ }
276
+
277
+ /** 是否放行客户端请求。放行时返回 null,否则返回 { status, message } */
278
+ function gateRejection() {
279
+ const st = getStatus();
280
+ switch (st.state) {
281
+ case 'active':
282
+ case 'expiring_soon':
283
+ case 'grace':
284
+ return null;
285
+ case 'superseded':
286
+ return { status: 403, message: '本服务器的激活已被同 License 的新激活顶替,已停止服务' };
287
+ case 'revoked':
288
+ return { status: 403, message: 'License 已被厂商吊销,已停止服务' };
289
+ case 'expired':
290
+ return { status: 403, message: '授权合同已过期且超出宽限期' };
291
+ case 'token_expired':
292
+ return { status: 403, message: '授权凭证已过期且无法联系中央授权服务复核' };
293
+ default:
294
+ return { status: 403, message: '授权服务器未激活:请联系厂商获取 customer_key 并在管理台完成激活' };
295
+ }
296
+ }
297
+
298
+ // ── 配置(admin) ──
299
+
300
+ /** 设置 customer_key / 备端点并立即激活 */
301
+ async function configure({ customer_key, backup_endpoint }) {
302
+ const s = loadState();
303
+ if (customer_key !== undefined) {
304
+ const key = String(customer_key || '').trim().toUpperCase();
305
+ if (!/^EXFC-[0-9A-F]{6}-[0-9A-F]{6}-[0-9A-F]{6}-[0-9A-F]{6}$/.test(key)) {
306
+ return { ok: false, error: 'bad_format', message: 'customer_key 格式应为 EXFC-XXXXXX-XXXXXX-XXXXXX-XXXXXX' };
307
+ }
308
+ if (key !== s.customer_key) {
309
+ // 换 key:旧凭证/顶替/终态作废
310
+ s.customer_key = key;
311
+ s.credential = null;
312
+ s.superseded = false;
313
+ s.terminal = null;
314
+ }
315
+ }
316
+ if (backup_endpoint !== undefined) {
317
+ s.endpoints = { primary: (s.endpoints && s.endpoints.primary) || DEFAULT_PRIMARY, backup: String(backup_endpoint || '').trim() };
318
+ }
319
+ s.last_error = null;
320
+ saveState();
321
+ return activateNow();
322
+ }
323
+
324
+ /** 清除激活配置(回到未激活) */
325
+ function clear() {
326
+ const s = loadState();
327
+ const fingerprint = s.fingerprint; // 指纹保留,换 key 重激活仍同一身份
328
+ state = { fingerprint };
329
+ saveState();
330
+ }
331
+
332
+ // ── 调度器 ──
333
+
334
+ let timer = null;
335
+
336
+ async function tick() {
337
+ const s = loadState();
338
+ if (!s.customer_key) return;
339
+ const r = await revalidateNow();
340
+ const ok = r.ok;
341
+ schedule(ok ? REVALIDATE_INTERVAL_MS : RETRY_INTERVAL_MS);
342
+ }
343
+
344
+ function schedule(delay) {
345
+ if (timer) clearTimeout(timer);
346
+ timer = setTimeout(tick, delay);
347
+ timer.unref();
348
+ }
349
+
350
+ /** 启动时调用:有 key 则立即复核一次,然后按日循环(失败 1 小时重试) */
351
+ function startScheduler() {
352
+ const s = loadState();
353
+ if (!s.customer_key) return;
354
+ tick().catch(e => {
355
+ console.error('[Activation] 复核异常:', e.message);
356
+ schedule(RETRY_INTERVAL_MS);
357
+ });
358
+ }
359
+
360
+ module.exports = { getStatus, gateRejection, configure, clear, activateNow, revalidateNow, startScheduler };