exflower-license-server 1.3.2 → 1.4.0

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
@@ -2,6 +2,90 @@
2
2
 
3
3
  ExFlower 在线授权验证服务。管理 License Key、企业授权、实例追踪、模板分发。
4
4
 
5
+ ## 私有化自助部署手册(客户 IT 照此执行)
6
+
7
+ 前提:一台 Linux 服务器(1C1G 即可),已装 Docker;防火墙放行 4001(或反代用的 80/443);能访问 `https://license.exflower.pro`(每日激活复核)。
8
+
9
+ ### 第 1 步:获取部署文件
10
+
11
+ 只需 `Dockerfile` 与 `docker-compose.yml` 两个文件(镜像构建时会自动从 npm 拉取服务本体,无需完整源码):
12
+
13
+ ```bash
14
+ npm pack exflower-license-server
15
+ tar xzf exflower-license-server-*.tgz package/Dockerfile package/docker-compose.yml --strip-components=1
16
+ ```
17
+
18
+ ### 第 2 步:构建镜像
19
+
20
+ ```bash
21
+ # 指定版本构建;国内服务器建议加 npm 镜像源参数
22
+ docker build --build-arg VERSION=1.3.2 \
23
+ --build-arg NPM_REGISTRY=https://registry.npmmirror.com \
24
+ -t exflower/license-server:1.3.2 .
25
+ ```
26
+
27
+ ### 第 3 步:生成管理员 Token(请自行保管,厂商不需要知道)
28
+
29
+ ```bash
30
+ # 生成随机 Token —— 这就是管理后台的登录凭证,只显示这一次,立即妥善保存
31
+ TOKEN=$(openssl rand -hex 24) && echo "Admin Token: $TOKEN"
32
+
33
+ # 计算哈希(部署配置里只放哈希,不落明文)
34
+ echo -n "$TOKEN" | sha256sum | cut -d' ' -f1 # macOS 用: shasum -a 256
35
+ ```
36
+
37
+ ### 第 4 步:启动服务
38
+
39
+ ```bash
40
+ docker run -d --name exflower-license-server --restart unless-stopped \
41
+ -p 4001:4001 \
42
+ -v license-data:/app/data \
43
+ -e NODE_ENV=production \
44
+ -e PORT=4001 \
45
+ -e LICENSE_DATA_DIR=/app/data \
46
+ -e ADMIN_TOKEN_HASH=<第3步算出的哈希> \
47
+ exflower/license-server:1.3.2
48
+ ```
49
+
50
+ ### 第 5 步:验证 + 激活(必做)
51
+
52
+ ```bash
53
+ # 健康检查
54
+ curl http://127.0.0.1:4001/health
55
+ ```
56
+
57
+ 1. 浏览器打开 `http://<服务器IP>:4001/admin`,用第 3 步的 Token 登录
58
+ 2. 进入「中央激活」,输入**厂商签发的 customer_key**(格式 `EXFC-XXXXXX-…`,向厂商索取)并激活
59
+ 3. ⚠️ **未激活前所有客户端接口一律 403**,激活后立即恢复;之后每日自动复核,中央短暂不可达不影响服务
60
+
61
+ ### 第 6 步:发放激活码
62
+
63
+ 管理台「License 管理」→ 新建 License(备注用途、选有效期与实例数)→ 完整 Key **仅显示一次**,复制交付给终端用户在客户端填写。客户端「授权服务器地址」填 `http://<服务器IP>:4001`(配置了域名反代则填域名)。
64
+
65
+ ### 升级版本
66
+
67
+ ```bash
68
+ docker build --build-arg VERSION=<新版本> -t exflower/license-server:<新版本> .
69
+ docker rm -f exflower-license-server
70
+ # 用与第 4 步完全相同的参数重新 docker run(数据在命名卷 license-data 里,不受影响)
71
+ ```
72
+
73
+ ⚠️ 不要使用 `docker compose down -v`——`-v` 会连数据卷一起删除。
74
+
75
+ ### (可选)HTTPS 反代
76
+
77
+ 以 Caddy 为例(`Caddyfile` 一行搞定,自动签发证书):
78
+
79
+ ```
80
+ license.example.com {
81
+ reverse_proxy 127.0.0.1:4001
82
+ }
83
+ ```
84
+
85
+ ⚠️ 境内服务器挂域名须已完成 ICP 备案,否则 80/443 会被云厂商拦截。
86
+
87
+ ---
88
+
5
89
  ## 快速开始
6
90
 
7
91
  ### 方式一:npm 全局安装(推荐)
@@ -12,7 +96,7 @@ npm install -g exflower-license-server
12
96
 
13
97
  # 查看版本
14
98
  exflower-license-server --version
15
- # 输出: 1.3.1
99
+ # 输出: 1.3.2
16
100
 
17
101
  # 运行
18
102
  exflower-license-server
@@ -31,10 +115,10 @@ docker build -t exflower/license-server:latest .
31
115
 
32
116
  # 国内构建机走镜像源(npm registry 与 alpine apk 源都可换)
33
117
  docker build \
34
- --build-arg VERSION=1.3.1 \
118
+ --build-arg VERSION=1.3.2 \
35
119
  --build-arg NPM_REGISTRY=https://registry.npmmirror.com \
36
120
  --build-arg APK_MIRROR=mirrors.cloud.tencent.com \
37
- -t exflower/license-server:1.3.1 .
121
+ -t exflower/license-server:1.3.2 .
38
122
 
39
123
  # 使用 docker-compose(推荐)
40
124
  docker-compose up -d
@@ -238,8 +322,10 @@ sudo systemctl enable --now exflower-license-server
238
322
  | `/api/admin/templates` | GET/POST | 模板列表/创建 |
239
323
  | `/api/admin/templates/:id` | PATCH/DELETE | 更新/删除模板 |
240
324
  | `/api/admin/templates/library` | POST | 资料图谱模板上传(multipart,字段 `file`,`.exflib`) |
241
- | `/api/admin/template-categories` | GET/POST | 模板分类列表/创建(按类型分组) |
325
+ | `/api/admin/templates/doc-upload` | POST | 知识库文档包上传(v1.3.2 起;multipart,字段 `file`,zip 内含 `README.md` + `assets/`) |
326
+ | `/api/admin/template-categories` | GET/POST | 模板分类列表/创建(按类型分组,按 `sort_order` 排序返回) |
242
327
  | `/api/admin/template-categories/:id` | PATCH/DELETE | 分类改名/删除 |
328
+ | `/api/admin/template-categories/order` | PUT | 分类手动排序(v1.3.2 起;body `{type, ids:[...]}`,客户端按此顺序展示分类) |
243
329
  | `/api/admin/imports` | GET | 导入统计(支持 `?template_id=` / `?company_id=` 过滤) |
244
330
  | `/api/admin/packages` | GET/POST | 轻应用包列表/上传(multipart,字段 `file`+`company_id`) |
245
331
  | `/api/admin/packages/:id` | DELETE | 删除轻应用包 |
@@ -259,6 +345,7 @@ sudo systemctl enable --now exflower-license-server
259
345
  管理后台根据类型提供不同的上传方式:
260
346
 
261
347
  - **agent / excard / doc**:**Markdown 原生上传**。直接上传客户端导出的 `.md` 文件(或粘贴 Markdown 文本),后台自动解析 frontmatter 预填模板名称并生成 `content_json`,无需手写 JSON。
348
+ - **doc 附加方式(v1.3.2 起)**:可改传一个 **zip 文档包**(根目录 `README.md` + `assets/` 附件目录),与客户端「发布到模板库」的产物格式一致;名称留空自动取 README 的一级标题。选了 zip 则以包内容为准。
262
349
  - **library**:上传客户端导出的 `.exflib` 包(multipart `POST /api/admin/templates/library`,字段 `file`),服务端解包校验 manifest(`format: exflower-library`)并记录文件大小与 sha256;客户端经 `GET /api/templates/:id/download` 下载导入。
263
350
  - **workflow**(暂不开放):以 JSON 描述步骤结构,上传框预填骨架示例。
264
351
 
package/db.js CHANGED
@@ -239,6 +239,9 @@ db.exec(`
239
239
  CREATE INDEX IF NOT EXISTS idx_client_versions_platform ON client_versions(platform, is_latest)
240
240
  `);
241
241
 
242
+ // 幂等列迁移:CDN 远程地址(有值 = 文件在中央 CDN,本地不落盘;PLAN-CDN-DISTRIBUTION)
243
+ try { db.exec('ALTER TABLE client_versions ADD COLUMN remote_url TEXT'); } catch { /* 已存在 */ };
244
+
242
245
  // ── Server-level settings(品牌等全局配置,私有化部署 = 一个组织) ──
243
246
  db.exec(`
244
247
  CREATE TABLE IF NOT EXISTS settings (
package/index.js CHANGED
@@ -54,6 +54,9 @@ setupRoutes(app);
54
54
  // ── 中央激活调度:已配置 customer_key 则启动即复核,之后每日复核(失败 1 小时重试) ──
55
55
  require('./activation').startScheduler();
56
56
 
57
+ // ── 客户端版本 manifest 同步(启动即拉一次 + 每小时兜底;失败维持本地) ──
58
+ require('./updateSync').startUpdateSync();
59
+
57
60
  app.listen(PORT, () => {
58
61
  const tokenLine = ADMIN_TOKEN_HASH
59
62
  ? 'Admin Token: (hashed via ADMIN_TOKEN_HASH)'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exflower-license-server",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "ExFlower License Server - 在线授权验证服务",
5
5
  "main": "index.js",
6
6
  "bin": {
package/routes.js CHANGED
@@ -10,6 +10,7 @@ const unzipper = require('unzipper');
10
10
  const { queryAll, queryGet, queryRun, getSetting, setSetting, DATA_DIR } = require('./db');
11
11
  const { generateLicenseKey, generateId } = require('./utils');
12
12
  const activation = require('./activation');
13
+ const { ensureFresh } = require('./updateSync');
13
14
 
14
15
  const DEFAULT_ADMIN_TOKEN = 'exflower-admin-2026';
15
16
  // ADMIN_TOKEN_HASH(sha256 hex)优先;否则用明文 ADMIN_TOKEN。二选一。
@@ -154,13 +155,21 @@ function buildLatestYml(mac) {
154
155
  // version 取各行里语义最新(简单按创建时间最新的一行)
155
156
  rows.sort((a, b) => (a.created_at < b.created_at ? 1 : -1));
156
157
  const esc = (s) => String(s).replace(/'/g, "''");
158
+ // 本地文件就绪(存在且大小匹配)→ 本地相对路径(客户端局域网速度);
159
+ // 否则 remote_url(CDN)兜底——后台下载完成后下一轮自动切回本地
160
+ const fileUrl = (r) => {
161
+ if (r.file_path && fs.existsSync(r.file_path) && fs.statSync(r.file_path).size === r.file_size) {
162
+ return `files/${r.platform}/${esc(r.file_name)}`;
163
+ }
164
+ return r.remote_url || `files/${r.platform}/${esc(r.file_name)}`;
165
+ };
157
166
  const lines = [`version: ${rows[0].version}`, 'files:'];
158
167
  for (const r of rows) {
159
- lines.push(` - url: 'files/${r.platform}/${esc(r.file_name)}'`);
168
+ lines.push(` - url: '${fileUrl(r)}'`);
160
169
  lines.push(` sha512: '${r.sha512}'`);
161
170
  lines.push(` size: ${r.file_size}`);
162
171
  }
163
- lines.push(`path: 'files/${rows[0].platform}/${esc(rows[0].file_name)}'`);
172
+ lines.push(`path: '${fileUrl(rows[0])}'`);
164
173
  lines.push(`sha512: '${rows[0].sha512}'`);
165
174
  lines.push(`releaseDate: '${new Date(rows[0].created_at.replace(' ', 'T') + 'Z').toISOString()}'`);
166
175
  return lines.join('\n') + '\n';
@@ -1307,16 +1316,18 @@ function setupRoutes(app) {
1307
1316
  });
1308
1317
 
1309
1318
  // ── Client Updates: client-facing (electron-updater generic provider) ──
1310
- app.get('/api/updates/latest.yml', (req, res) => {
1319
+ app.get('/api/updates/latest.yml', async (req, res) => {
1311
1320
  if (!companyByLicenseKey(req, res)) return;
1321
+ await ensureFresh(); // manifest 过期则先同步中央(失败不阻塞)
1312
1322
  const yml = buildLatestYml(false);
1313
1323
  if (!yml) return res.status(404).json({ success: false, message: 'No win update available' });
1314
1324
  res.setHeader('Content-Type', 'text/yaml; charset=utf-8');
1315
1325
  res.send(yml);
1316
1326
  });
1317
1327
 
1318
- app.get('/api/updates/latest-mac.yml', (req, res) => {
1328
+ app.get('/api/updates/latest-mac.yml', async (req, res) => {
1319
1329
  if (!companyByLicenseKey(req, res)) return;
1330
+ await ensureFresh();
1320
1331
  const yml = buildLatestYml(true);
1321
1332
  if (!yml) return res.status(404).json({ success: false, message: 'No mac update available' });
1322
1333
  res.setHeader('Content-Type', 'text/yaml; charset=utf-8');
@@ -1330,7 +1341,10 @@ function setupRoutes(app) {
1330
1341
  'SELECT * FROM client_versions WHERE platform = ? AND file_name = ?',
1331
1342
  [platform, filename]
1332
1343
  );
1333
- if (!row || !fs.existsSync(row.file_path)) {
1344
+ if (!row) return res.status(404).json({ success: false, message: 'Update file not found' });
1345
+ if (!fs.existsSync(row.file_path)) {
1346
+ // 本地无文件但有 CDN 地址 → 302 兜底(对齐 ensureFresh 未覆盖的旧 yml 场景)
1347
+ if (row.remote_url) return res.redirect(302, row.remote_url);
1334
1348
  return res.status(404).json({ success: false, message: 'Update file not found' });
1335
1349
  }
1336
1350
  res.setHeader('Content-Type', 'application/octet-stream');
@@ -1341,7 +1355,7 @@ function setupRoutes(app) {
1341
1355
  // ── Client Updates: admin ──
1342
1356
  app.get('/api/admin/updates', requireAdmin, (req, res) => {
1343
1357
  const updates = queryAll(
1344
- 'SELECT id, platform, version, file_name, file_size, sha512, release_notes, is_latest, created_at FROM client_versions ORDER BY created_at DESC'
1358
+ 'SELECT id, platform, version, file_name, file_size, sha512, release_notes, is_latest, remote_url, created_at FROM client_versions ORDER BY created_at DESC'
1345
1359
  );
1346
1360
  res.json({ success: true, updates });
1347
1361
  });