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 +91 -4
- package/db.js +3 -0
- package/index.js +3 -0
- package/package.json +1 -1
- package/routes.js +20 -6
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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
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: '
|
|
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: '
|
|
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
|
|
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
|
});
|