@code2rich/jpage 1.5.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/.claude/settings.local.json +68 -0
- package/.dockerignore +8 -0
- package/.env.example +56 -0
- package/.github/workflows/ci.yml +43 -0
- package/CLAUDE.md +280 -0
- package/Dockerfile +44 -0
- package/LICENSE +21 -0
- package/README.md +433 -0
- package/README_EN.md +399 -0
- package/bin/args.js +64 -0
- package/bin/client.js +93 -0
- package/bin/commands/_shared.js +54 -0
- package/bin/commands/cat.js +23 -0
- package/bin/commands/ls.js +44 -0
- package/bin/commands/mv.js +20 -0
- package/bin/commands/rm.js +22 -0
- package/bin/commands/skills.js +70 -0
- package/bin/commands/star.js +23 -0
- package/bin/commands/tags.js +97 -0
- package/bin/commands/upload.js +84 -0
- package/bin/commands/url.js +25 -0
- package/bin/commands/whoami.js +29 -0
- package/bin/config.js +85 -0
- package/bin/jpage.js +168 -0
- package/build.js +112 -0
- package/docker-compose.yml +26 -0
- package/docs/api.md +438 -0
- package/docs/design/005-custom-modal.md +296 -0
- package/docs/design/013-file-version-history.md +324 -0
- package/docs/design/billing-system.md +600 -0
- package/docs/design/db-index-and-healthcheck.md +176 -0
- package/docs/design/loading-states.md +209 -0
- package/docs/virtual-hosting-feasibility.md +453 -0
- package/eslint.config.mjs +172 -0
- package/lib/auth-state.js +15 -0
- package/lib/categories.js +20 -0
- package/lib/crypto.js +85 -0
- package/lib/csp.js +66 -0
- package/lib/db.js +53 -0
- package/lib/dispatch.js +103 -0
- package/lib/fts.js +81 -0
- package/lib/middleware/auth.js +114 -0
- package/lib/middleware/files.js +42 -0
- package/lib/paths.js +9 -0
- package/lib/render-cache.js +48 -0
- package/lib/render.js +157 -0
- package/lib/templates.js +149 -0
- package/lib/util.js +66 -0
- package/lib/view-counts.js +59 -0
- package/lib/zip.js +192 -0
- package/logger.js +16 -0
- package/mailer.js +34 -0
- package/mcp/constants.js +16 -0
- package/mcp/resources.js +74 -0
- package/mcp/server.js +43 -0
- package/mcp/tools-categories.js +56 -0
- package/mcp/tools-content-templates.js +59 -0
- package/mcp/tools-files.js +245 -0
- package/mcp/tools-tags.js +41 -0
- package/mcp/tools-versions.js +57 -0
- package/mcp/transport.js +183 -0
- package/mcp/util.js +63 -0
- package/mcp-server.js +20 -0
- package/migrations/001_init_schema.js +25 -0
- package/migrations/002_add_share_key.js +33 -0
- package/migrations/003_add_roles_and_tokens.js +28 -0
- package/migrations/004_add_version_history.js +32 -0
- package/migrations/005_tags_starred_categories.js +49 -0
- package/migrations/006_zip_bundle.js +17 -0
- package/migrations/007_add_file_type_uploaded_by_indexes.js +7 -0
- package/migrations/008_add_fts5.js +6 -0
- package/migrations/009_add_link_visits.js +20 -0
- package/migrations/010_add_templates_system.js +34 -0
- package/migrations/011_content_templates.js +233 -0
- package/migrations/012_add_email_and_verification.js +35 -0
- package/migrations/013_add_token_encrypted.js +14 -0
- package/migrations.js +65 -0
- package/package.json +63 -0
- package/public/css/style.css +2915 -0
- package/public/index.html +855 -0
- package/public/js/api.js +22 -0
- package/public/js/app.js +94 -0
- package/public/js/components/dialog.js +106 -0
- package/public/js/components/toast.js +13 -0
- package/public/js/pages/content-templates.js +330 -0
- package/public/js/pages/home.js +1903 -0
- package/public/js/pages/landing.js +158 -0
- package/public/js/pages/login.js +175 -0
- package/public/js/pages/preview.js +713 -0
- package/public/js/theme.js +44 -0
- package/public/js/utils.js +67 -0
- package/routes/admin.js +136 -0
- package/routes/auth.js +365 -0
- package/routes/categories.js +90 -0
- package/routes/content-templates.js +215 -0
- package/routes/files/_shared.js +112 -0
- package/routes/files/associations.js +94 -0
- package/routes/files/crud.js +139 -0
- package/routes/files/detail-serve.js +178 -0
- package/routes/files/index.js +38 -0
- package/routes/files/list.js +200 -0
- package/routes/files/overwrite.js +114 -0
- package/routes/files/upload.js +204 -0
- package/routes/files/versions.js +166 -0
- package/routes/files.js +16 -0
- package/routes/skills.js +93 -0
- package/routes/tags.js +65 -0
- package/routes/tokens.js +110 -0
- package/routes/users.js +120 -0
- package/server.js +372 -0
- package/skills/jpage-content-template/SKILL.md +98 -0
- package/skills/jpage-upload/SKILL.md +247 -0
- package/skills-registry.js +135 -0
- package/templates/academic.html +41 -0
- package/templates/dark-pro.html +41 -0
- package/templates/default.html +56 -0
- package/templates/github.html +67 -0
- package/test/browser-harness.js +125 -0
- package/test/dispatch-bench.js +74 -0
- package/test/helpers/setup.js +45 -0
- package/test/integration/admin.test.js +108 -0
- package/test/integration/auth.test.js +93 -0
- package/test/integration/categories.test.js +103 -0
- package/test/integration/cli.test.js +310 -0
- package/test/integration/content-templates.test.js +147 -0
- package/test/integration/files-security.test.js +248 -0
- package/test/integration/files.test.js +139 -0
- package/test/integration/share.test.js +79 -0
- package/test/integration/skills.test.js +104 -0
- package/test/integration/tags.test.js +84 -0
- package/test/integration/tokens.test.js +89 -0
- package/test/integration/users.test.js +138 -0
- package/test/mcp-harness.js +152 -0
- package/test/perf-bench.js +108 -0
- package/test/perf-harness.js +198 -0
- package/test/run-server.sh +15 -0
- package/test/unit/cli-args.test.js +88 -0
- package/test/unit/cli-config.test.js +89 -0
- package/test/unit/crypto.test.js +100 -0
- package/test/unit/fts.test.js +52 -0
- package/test/unit/render-cache.test.js +76 -0
- package/test/unit/util.test.js +81 -0
- package/test/unit/zip.test.js +164 -0
package/build.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 前端构建:esbuild 打包 + 代码分割 + minify(JS)+ minify(CSS)
|
|
3
|
+
//
|
|
4
|
+
// 产出 public/dist/:
|
|
5
|
+
// - app.[hash].js 入口(路由 + 全局状态/主题)
|
|
6
|
+
// - chunks/*.[hash].js 路由级懒加载 chunk(landing/login/home/preview + 共享 chunk)
|
|
7
|
+
// - style.[hash].css 压缩后的样式
|
|
8
|
+
// - manifest.json 原始名 -> 带哈希文件名 的映射,供 index.html 引用
|
|
9
|
+
//
|
|
10
|
+
// 用法:node build.js (生产:minify + 哈希)
|
|
11
|
+
// node build.js --dev (开发:不 minify、不哈希,便于调试)
|
|
12
|
+
//
|
|
13
|
+
// 设计原则:不改变"无构建即可开发"的约定——public/js 与 public/css 仍是源文件,
|
|
14
|
+
// 可直接被浏览器加载(开发模式 server.js 仍可跑源文件)。本脚本仅用于生产打包到 public/dist。
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { execFileSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const PUBLIC = path.join(__dirname, 'public');
|
|
20
|
+
const DIST = path.join(PUBLIC, 'dist');
|
|
21
|
+
const isDev = process.argv.includes('--dev');
|
|
22
|
+
|
|
23
|
+
// 清理 dist
|
|
24
|
+
fs.rmSync(DIST, { recursive: true, force: true });
|
|
25
|
+
fs.mkdirSync(DIST, { recursive: true });
|
|
26
|
+
|
|
27
|
+
function runEsbuild(args) {
|
|
28
|
+
// 用 npx 调 esbuild(devDependency),保证跨环境一致
|
|
29
|
+
execFileSync('npx', ['esbuild', ...args], { stdio: 'inherit', cwd: __dirname });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- 1. JS:打包 app.js,代码分割 ---
|
|
33
|
+
const entryNames = isDev ? '[name]' : '[name]-[hash]';
|
|
34
|
+
const jsArgs = [
|
|
35
|
+
path.join(PUBLIC, 'js', 'app.js'),
|
|
36
|
+
'--bundle', '--format=esm',
|
|
37
|
+
'--splitting',
|
|
38
|
+
'--outdir=' + path.join(DIST),
|
|
39
|
+
'--outbase=' + path.join(PUBLIC, 'js'),
|
|
40
|
+
'--entry-names=' + entryNames,
|
|
41
|
+
'--chunk-names=chunks/' + entryNames,
|
|
42
|
+
'--metafile=' + path.join(DIST, 'meta.json'),
|
|
43
|
+
];
|
|
44
|
+
if (!isDev) jsArgs.push('--minify', '--target=es2020');
|
|
45
|
+
runEsbuild(jsArgs);
|
|
46
|
+
|
|
47
|
+
// --- 2. CSS:minify style.css ---
|
|
48
|
+
const cssSrc = path.join(PUBLIC, 'css', 'style.css');
|
|
49
|
+
const cssArgs = [
|
|
50
|
+
cssSrc,
|
|
51
|
+
'--outfile=' + path.join(DIST, isDev ? 'style.css' : 'style.css'),
|
|
52
|
+
];
|
|
53
|
+
if (!isDev) cssArgs.push('--minify');
|
|
54
|
+
// esbuild 对单文件 css 用 --outfile;但我们要带哈希名 → 先输出再改名
|
|
55
|
+
runEsbuild([cssSrc, '--outfile=' + path.join(DIST, 'style.tmp.css'), ...(isDev ? [] : ['--minify'])]);
|
|
56
|
+
|
|
57
|
+
// --- 3. 生成 manifest + 重命名 CSS 带哈希 ---
|
|
58
|
+
const meta = JSON.parse(fs.readFileSync(path.join(DIST, 'meta.json'), 'utf8'));
|
|
59
|
+
const manifest = {};
|
|
60
|
+
|
|
61
|
+
// JS outputs:从 metafile 提取(path 是绝对/相对,统一取 basename)
|
|
62
|
+
for (const outPath of Object.keys(meta.outputs)) {
|
|
63
|
+
const base = path.basename(outPath);
|
|
64
|
+
// app*.js -> app.js;chunk 在 chunks/ 下保留
|
|
65
|
+
const inChunks = outPath.includes('chunks/');
|
|
66
|
+
if (base.startsWith('app-') || base === 'app.js') {
|
|
67
|
+
manifest['app.js'] = inChunks ? 'chunks/' + base : base;
|
|
68
|
+
}
|
|
69
|
+
// 其余按 basename 记录(landing/login/home/preview + chunk)
|
|
70
|
+
// 不显式登记,index.html 只需引用 app.js 入口
|
|
71
|
+
}
|
|
72
|
+
fs.unlinkSync(path.join(DIST, 'meta.json'));
|
|
73
|
+
|
|
74
|
+
// CSS 重命名带哈希(用内容 hash)
|
|
75
|
+
const cssContent = fs.readFileSync(path.join(DIST, 'style.tmp.css'));
|
|
76
|
+
let cssName = 'style.css';
|
|
77
|
+
if (!isDev) {
|
|
78
|
+
const crypto = require('crypto');
|
|
79
|
+
const h = crypto.createHash('sha1').update(cssContent).digest('hex').slice(0, 8);
|
|
80
|
+
cssName = `style-${h}.css`;
|
|
81
|
+
}
|
|
82
|
+
fs.writeFileSync(path.join(DIST, cssName), cssContent);
|
|
83
|
+
fs.unlinkSync(path.join(DIST, 'style.tmp.css'));
|
|
84
|
+
manifest['style.css'] = cssName;
|
|
85
|
+
|
|
86
|
+
// 找到 app 入口实际文件名(dist 根目录下 app*.js,不在 chunks/)
|
|
87
|
+
const appFile = fs.readdirSync(DIST).find(f => /^app(-[a-z0-9]+)?\.js$/i.test(f));
|
|
88
|
+
manifest['app.js'] = appFile;
|
|
89
|
+
|
|
90
|
+
fs.writeFileSync(path.join(DIST, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
91
|
+
|
|
92
|
+
// --- 4. 汇总报告 ---
|
|
93
|
+
const files = [];
|
|
94
|
+
function walk(dir, rel = '') {
|
|
95
|
+
for (const f of fs.readdirSync(dir)) {
|
|
96
|
+
const full = path.join(dir, f);
|
|
97
|
+
const r = rel ? rel + '/' + f : f;
|
|
98
|
+
if (fs.statSync(full).isDirectory()) walk(full, r);
|
|
99
|
+
else files.push({ name: r, size: fs.statSync(full).size });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
walk(DIST);
|
|
103
|
+
const gzip = (s) => Math.round(s / 3); // gzip 对 JS/CSS 经验压缩比 ~3x(仅粗略估计)
|
|
104
|
+
|
|
105
|
+
console.log('\n=== 构建完成:public/dist/ ===');
|
|
106
|
+
console.log('manifest:', JSON.stringify(manifest));
|
|
107
|
+
console.log('\n文件清单:');
|
|
108
|
+
for (const f of files.sort((a, b) => b.size - a.size)) {
|
|
109
|
+
console.log(` ${f.name.padEnd(40)} ${(f.size / 1024).toFixed(2).padStart(8)} KB (gzip ~${(gzip(f.size) / 1024).toFixed(2)} KB)`);
|
|
110
|
+
}
|
|
111
|
+
const total = files.reduce((a, f) => a + f.size, 0);
|
|
112
|
+
console.log(`\n合计:${(total / 1024).toFixed(2)} KB (gzip ~${(gzip(total) / 1024).toFixed(2)} KB)`);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
services:
|
|
2
|
+
app:
|
|
3
|
+
build: .
|
|
4
|
+
container_name: jpage
|
|
5
|
+
ports:
|
|
6
|
+
- "8858:8858"
|
|
7
|
+
volumes:
|
|
8
|
+
- ./data:/app/data
|
|
9
|
+
environment:
|
|
10
|
+
- NODE_ENV=${NODE_ENV:-production}
|
|
11
|
+
- PORT=${PORT:-8858}
|
|
12
|
+
- ADMIN_USER=${ADMIN_USER:-admin}
|
|
13
|
+
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
|
|
14
|
+
- SESSION_SECRET=${SESSION_SECRET:-}
|
|
15
|
+
- MCP_TOKEN=${MCP_TOKEN:-}
|
|
16
|
+
- MCP_IP=${MCP_IP:-localhost}
|
|
17
|
+
- TOKEN_ENCRYPTION_KEY=${TOKEN_ENCRYPTION_KEY:-}
|
|
18
|
+
- SMTP_HOST=${SMTP_HOST:-}
|
|
19
|
+
- SMTP_PORT=${SMTP_PORT:-}
|
|
20
|
+
- SMTP_SECURE=${SMTP_SECURE:-}
|
|
21
|
+
- SMTP_USER=${SMTP_USER:-}
|
|
22
|
+
- SMTP_PASS=${SMTP_PASS:-}
|
|
23
|
+
- SMTP_FROM=${SMTP_FROM:-}
|
|
24
|
+
- APP_URL=${APP_URL:-}
|
|
25
|
+
- ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-false}
|
|
26
|
+
restart: unless-stopped
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# 即页 REST API 参考
|
|
2
|
+
|
|
3
|
+
端口 `8858`(`PORT` 可覆盖)。所有写入类端点要求登录(session cookie `jpage.sid`)或 Bearer token。
|
|
4
|
+
|
|
5
|
+
## 通用说明
|
|
6
|
+
|
|
7
|
+
| 项 | 值 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Base URL | `http://localhost:8858` |
|
|
10
|
+
| 内容类型 | 除 `POST /api/files/upload`(multipart)外均为 `application/json` |
|
|
11
|
+
| 鉴权 | 三选一:session cookie `jpage.sid`(登录后获得)**或** 用户级 API Token(`Authorization: Bearer jp_xxx`)**或** 全局 `MCP_TOKEN`(向后兼容) |
|
|
12
|
+
| 字符集 | UTF-8 |
|
|
13
|
+
| 文件大小上限 | 50 MB |
|
|
14
|
+
| 允许扩展名(上传) | `.html` `.htm` `.md` `.markdown` `.zip` |
|
|
15
|
+
| 上传限流 | 50 req / 15 min / IP(`/api/files/upload`、`/api/files/upload-json`、`/api/files/:id/overwrite`、`/api/files/:id/overwrite-json`) |
|
|
16
|
+
| 登录限流 | 10 req / 15 min / IP(`POST /api/auth/login`) |
|
|
17
|
+
|
|
18
|
+
> **权限模型**:admin 可访问全部文件与用户;普通用户只能操作自己的文件与公开文件。下方各端点标注「需登录」表示三种鉴权方式任一即可,额外标注「仅 admin」的还需 admin 角色。
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 鉴权
|
|
23
|
+
|
|
24
|
+
### `GET /api/auth/me`
|
|
25
|
+
|
|
26
|
+
当前登录信息。返回 `{id, username, email, emailVerified, role}` 或 401。
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
curl -b jpage.sid=<cookie> http://localhost:8858/api/auth/me
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `POST /api/auth/login`
|
|
33
|
+
|
|
34
|
+
登录。Body: `{account, password}` 或 `{username, password}`(统一入口,自动识别用户名或邮箱)。成功后写入 session cookie。
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
curl -c jpage.sid -X POST http://localhost:8858/api/auth/login \
|
|
38
|
+
-H "Content-Type: application/json" \
|
|
39
|
+
-d '{"username":"admin","password":"admin1234"}'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `POST /api/auth/register`
|
|
43
|
+
|
|
44
|
+
注册(需 `ALLOW_REGISTRATION=true`)。Body: `{email?, username?, password, confirmPassword}`,至少提供 email 或 username。邮箱注册会自动生成用户名,并发送验证邮件。
|
|
45
|
+
|
|
46
|
+
### `POST /api/auth/logout`
|
|
47
|
+
|
|
48
|
+
销毁 session,清除 cookie。
|
|
49
|
+
|
|
50
|
+
### `POST /api/auth/change-password`
|
|
51
|
+
|
|
52
|
+
修改当前用户密码。Body: `{currentPassword, newPassword}`。所有用户可用。
|
|
53
|
+
|
|
54
|
+
### `POST /api/auth/profile`
|
|
55
|
+
|
|
56
|
+
编辑个人资料(需登录)。Body: `{username?, email?}`。
|
|
57
|
+
|
|
58
|
+
### `GET /api/auth/verify-email?token=...`
|
|
59
|
+
|
|
60
|
+
验证邮箱 token,重定向前端页面。
|
|
61
|
+
|
|
62
|
+
### `POST /api/auth/resend-verification`
|
|
63
|
+
|
|
64
|
+
重发验证邮件(需登录,限流 5/h)。
|
|
65
|
+
|
|
66
|
+
### `POST /api/auth/send-register-code`
|
|
67
|
+
|
|
68
|
+
发送注册验证码(需 `ALLOW_REGISTRATION=true`)。Body: `{email}`,向该邮箱发送 6 位数字验证码(10 分钟有效)。
|
|
69
|
+
|
|
70
|
+
### `GET /api/auth/smtp-status`
|
|
71
|
+
|
|
72
|
+
返回 `{configured: bool}`,SMTP 是否已配置。
|
|
73
|
+
|
|
74
|
+
### `GET /api/auth/registration-status`
|
|
75
|
+
|
|
76
|
+
返回 `{enabled: bool}`,注册是否开放。
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 文件管理
|
|
81
|
+
|
|
82
|
+
### `GET /api/files`
|
|
83
|
+
|
|
84
|
+
列出全部文件(按 `created_at DESC`)。返回 `{files: [...]}`。
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"files": [
|
|
89
|
+
{
|
|
90
|
+
"id": 1,
|
|
91
|
+
"original_name": "report.html",
|
|
92
|
+
"file_type": "html",
|
|
93
|
+
"size": 1234,
|
|
94
|
+
"is_public": 1,
|
|
95
|
+
"created_at": "2026-06-06 12:00:00"
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `GET /api/files/search`
|
|
102
|
+
|
|
103
|
+
全文/文件名搜索(FTS5 + LIKE 合并,一次往返,带分页)。查询参数:`q`(关键词)、`page`、`pageSize`、`tagId`、`categoryId`、`starred` 等过滤项。需登录。
|
|
104
|
+
|
|
105
|
+
### `POST /api/files/upload` — multipart
|
|
106
|
+
|
|
107
|
+
传统上传(`multipart/form-data`)。字段:
|
|
108
|
+
- `file` — 二进制文件(必填,支持 `.html`/`.htm`/`.md`/`.markdown`/`.zip`)
|
|
109
|
+
- `isPublic` — `true` / `false`(可选,默认 `true`)
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
curl -b jpage.sid -X POST http://localhost:8858/api/files/upload \
|
|
113
|
+
-F "file=@report.html" \
|
|
114
|
+
-F "isPublic=true"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
返回 `{id, original_name, file_type, size, is_public}`。
|
|
118
|
+
|
|
119
|
+
`.zip` 走两种模式:(1) **网站包 Bundle**(含 `index.html` + 资源目录),存为解压目录,`is_bundle=1`;(2) **批量上传**(多个独立 HTML/MD),各自建文件记录。同名文件自动覆盖(旧版本备份到 `file_versions`)。
|
|
120
|
+
|
|
121
|
+
### `POST /api/files/upload-json` — JSON
|
|
122
|
+
|
|
123
|
+
**MCP server 调用的入口**,避免构造 multipart。Body:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"name": "report.html",
|
|
128
|
+
"content": "<!doctype html><h1>Hello</h1>",
|
|
129
|
+
"isPublic": true
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
134
|
+
|---|---|---|---|
|
|
135
|
+
| `name` | string | ✓ | 文件名(含扩展名) |
|
|
136
|
+
| `content` | string | ✓ | UTF-8 文本内容 |
|
|
137
|
+
| `isPublic` | boolean | ✗ | 默认 `true` |
|
|
138
|
+
|
|
139
|
+
返回同 multipart 端点。`uploaded_by` 自动取 session 用户 id。同名文件自动覆盖(备份旧版本到 `file_versions`)。
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
curl -b jpage.sid -X POST http://localhost:8858/api/files/upload-json \
|
|
143
|
+
-H "Content-Type: application/json" \
|
|
144
|
+
-d '{"name":"hello.html","content":"<h1>hi</h1>"}'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `POST /api/files/upload-zip-base64` — JSON
|
|
148
|
+
|
|
149
|
+
MCP `upload_file` 调用的 ZIP 入口。Body: `{name, content(base64), ...}`,内部复用 ZIP 解包逻辑。
|
|
150
|
+
|
|
151
|
+
### `PUT /api/files/:id`
|
|
152
|
+
|
|
153
|
+
更新文件名或公开性。Body:
|
|
154
|
+
- `{name}` — 重命名(trim 后非空)
|
|
155
|
+
- `{isPublic}` — 切换公开/私有
|
|
156
|
+
- `{templateId}` — 绑定样式模板 id(可选)
|
|
157
|
+
|
|
158
|
+
至少传一个字段。仅 admin 或文件所有者。
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
curl -b jpage.sid -X PUT http://localhost:8858/api/files/1 \
|
|
162
|
+
-H "Content-Type: application/json" \
|
|
163
|
+
-d '{"name":"new-name.html"}'
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `DELETE /api/files/:id`
|
|
167
|
+
|
|
168
|
+
删除文件(DB + 磁盘)。返回 `{success: true}`。仅 admin 或文件所有者。
|
|
169
|
+
|
|
170
|
+
### `POST /api/files/batch`
|
|
171
|
+
|
|
172
|
+
批量操作。Body: `{action, ids: [...], data?}`,`action` ∈ `delete`/`setPublic`/`setPrivate`/`setCategory`,单次最多 200 个文件。
|
|
173
|
+
|
|
174
|
+
### `GET /api/files/:id`
|
|
175
|
+
|
|
176
|
+
单个文件元数据(经 `loadFileWithPrivacy` 做所有权/公开性校验)。
|
|
177
|
+
|
|
178
|
+
### `GET /api/files/:id/content`
|
|
179
|
+
|
|
180
|
+
返回原始文本。公开文件无需登录,私有文件需 session/token。**网站包(bundle,`is_bundle=1`)不支持此端点**,返回 `400`,请改用 `/api/files/:id/render` 预览或 `/api/files/:id/download` 下载。
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{ "id": 1, "original_name": "r.html", "file_type": "html", "is_public": 1, "content": "..." }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `GET /api/files/:id/render`
|
|
187
|
+
|
|
188
|
+
返回渲染后的 HTML。Markdown 文件走 `marked` 转换并加样式模板;HTML 文件原样返回(自动注入 `<meta charset="UTF-8">`);Bundle 注入 `<base>` 标签使相对路径指向 `/api/files/:id/asset/`。公开/私有访问规则同上。
|
|
189
|
+
|
|
190
|
+
### `GET /api/files/:id/download`
|
|
191
|
+
|
|
192
|
+
文件流式下载,`Content-Disposition` 含 UTF-8 文件名。Bundle 以 ZIP 形式下载。
|
|
193
|
+
|
|
194
|
+
### `GET /api/files/:id/asset/*`
|
|
195
|
+
|
|
196
|
+
Bundle 资源文件访问(带路径穿越校验)。
|
|
197
|
+
|
|
198
|
+
### `POST /api/files/:id/overwrite` — multipart
|
|
199
|
+
|
|
200
|
+
预览页专用覆盖上传,自动把旧版本备份到 `file_versions`。
|
|
201
|
+
|
|
202
|
+
### `POST /api/files/:id/overwrite-json` — JSON
|
|
203
|
+
|
|
204
|
+
MCP 使用的 JSON 覆盖上传,自动版本备份。
|
|
205
|
+
|
|
206
|
+
### `GET /api/files/:id/stats`
|
|
207
|
+
|
|
208
|
+
返回文件访问统计:`{viewCount, daily7, daily30}`。`viewCount` 含未回写的缓冲值,保证读一致。
|
|
209
|
+
|
|
210
|
+
### `GET /s/:key`
|
|
211
|
+
|
|
212
|
+
短链接渲染页面。通过 `share_key` 查找文件并渲染,公开文件无需登录。访问计数累积到内存缓冲,每 30s 批量回写。
|
|
213
|
+
|
|
214
|
+
### 版本历史
|
|
215
|
+
|
|
216
|
+
| 端点 | 方法 | 说明 |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `/api/files/:id/versions` | GET | 版本历史列表 |
|
|
219
|
+
| `/api/files/:id/versions/:ver/content` | GET | 历史版本原文 |
|
|
220
|
+
| `/api/files/:id/versions/:ver/render` | GET | 渲染历史版本 |
|
|
221
|
+
| `/api/files/:id/versions/:ver/restore` | POST | 恢复到指定版本 |
|
|
222
|
+
| `/api/files/:id/versions/:ver` | DELETE | 删除指定历史版本 |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 标签
|
|
227
|
+
|
|
228
|
+
### `GET /api/tags`
|
|
229
|
+
|
|
230
|
+
列出所有标签及其关联文件数量。需登录。
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{ "tags": [{ "id": 1, "name": "报告", "file_count": 3 }] }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### `POST /api/tags`
|
|
237
|
+
|
|
238
|
+
创建标签。若同名标签已存在则返回现有记录。需登录。
|
|
239
|
+
|
|
240
|
+
- Body: `{ "name": "Q3" }`
|
|
241
|
+
- 返回: `{ "id": 2, "name": "Q3" }`
|
|
242
|
+
|
|
243
|
+
### `DELETE /api/tags/:id`
|
|
244
|
+
|
|
245
|
+
删除标签(同时清除所有文件的该标签关联)。需登录。
|
|
246
|
+
|
|
247
|
+
### `PUT /api/files/:id/tags`
|
|
248
|
+
|
|
249
|
+
替换文件的标签列表。需登录。
|
|
250
|
+
|
|
251
|
+
- Body: `{ "tagIds": [1, 2, 3] }`
|
|
252
|
+
- 返回: `{ "success": true, "tags": [{ "id": 1, "name": "报告" }] }`
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 收藏
|
|
257
|
+
|
|
258
|
+
### `POST /api/files/:id/star`
|
|
259
|
+
|
|
260
|
+
收藏文件。需登录。重复收藏不报错。
|
|
261
|
+
|
|
262
|
+
### `DELETE /api/files/:id/star`
|
|
263
|
+
|
|
264
|
+
取消收藏。需登录。
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 分类
|
|
269
|
+
|
|
270
|
+
### `GET /api/categories`
|
|
271
|
+
|
|
272
|
+
列出当前用户的分类及其文件数量。需登录。
|
|
273
|
+
|
|
274
|
+
```json
|
|
275
|
+
{ "categories": [{ "id": 1, "name": "工作", "file_count": 5 }] }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### `POST /api/categories`
|
|
279
|
+
|
|
280
|
+
创建分类。需登录。
|
|
281
|
+
|
|
282
|
+
- Body: `{ "name": "学习" }`
|
|
283
|
+
- 返回: `{ "id": 2, "name": "学习" }`
|
|
284
|
+
|
|
285
|
+
### `PUT /api/categories/:id`
|
|
286
|
+
|
|
287
|
+
重命名分类。**仅 admin**。
|
|
288
|
+
|
|
289
|
+
- Body: `{ "name": "新名称" }`
|
|
290
|
+
|
|
291
|
+
### `DELETE /api/categories/:id`
|
|
292
|
+
|
|
293
|
+
删除分类。文件自动变为未分类(`category_id = NULL`)。**仅 admin**。
|
|
294
|
+
|
|
295
|
+
### `PUT /api/files/:id/category`
|
|
296
|
+
|
|
297
|
+
设置文件的分类。需登录(仅 admin 或文件所有者)。
|
|
298
|
+
|
|
299
|
+
- Body: `{ "categoryId": 1 }` 或 `{ "categoryId": null }`(移除分类)
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 用户管理(仅 admin)
|
|
304
|
+
|
|
305
|
+
| 端点 | 方法 | 说明 |
|
|
306
|
+
|---|---|---|
|
|
307
|
+
| `/api/users` | GET | 列出所有用户(含 email) |
|
|
308
|
+
| `/api/users` | POST | 创建用户 `{username, password, role, email?}` |
|
|
309
|
+
| `/api/users/:id` | PUT | 更新用户名/邮箱/角色或重置密码 |
|
|
310
|
+
| `/api/users/:id` | DELETE | 删除用户(不可删自己,文件转交 admin) |
|
|
311
|
+
|
|
312
|
+
## API Token
|
|
313
|
+
|
|
314
|
+
每用户最多 10 个,格式 `jp_` + 32 位 base62,DB 存 SHA-256 哈希 + 前 8 位前缀,明文仅创建时返回一次。
|
|
315
|
+
|
|
316
|
+
| 端点 | 方法 | 说明 |
|
|
317
|
+
|---|---|---|
|
|
318
|
+
| `/api/tokens` | GET | 列出自己的 Token |
|
|
319
|
+
| `/api/tokens` | POST | 创建 Token `{name}`(明文仅返回一次) |
|
|
320
|
+
| `/api/tokens/:id` | DELETE | 删除 Token(自己的或 admin 删任意) |
|
|
321
|
+
|
|
322
|
+
## 内容模板(Content Templates)
|
|
323
|
+
|
|
324
|
+
公开模板库 + 用户自建模板,用于快速创建文件。
|
|
325
|
+
|
|
326
|
+
| 端点 | 方法 | 说明 |
|
|
327
|
+
|---|---|---|
|
|
328
|
+
| `/api/content-templates/public` | GET | 公开模板列表(无需登录) |
|
|
329
|
+
| `/api/content-templates/public/:id/preview` | GET | 公开模板预览 |
|
|
330
|
+
| `/api/content-templates` | GET | 当前用户模板列表 |
|
|
331
|
+
| `/api/content-templates/scenes` | GET | 模板场景分类 |
|
|
332
|
+
| `/api/content-templates/:id` | GET | 模板详情 |
|
|
333
|
+
| `/api/content-templates/:id/content` | GET | 模板原文内容 |
|
|
334
|
+
| `/api/content-templates` | POST | 创建模板 |
|
|
335
|
+
| `/api/content-templates/:id` | PUT | 更新模板(仅所有者) |
|
|
336
|
+
| `/api/content-templates/:id` | DELETE | 删除模板(仅所有者) |
|
|
337
|
+
| `/api/content-templates/:id/use` | POST | 基于模板创建文件 |
|
|
338
|
+
|
|
339
|
+
> 另有 `GET /api/templates`(样式模板,渲染皮肤)。
|
|
340
|
+
|
|
341
|
+
## 管理后台(仅 admin)
|
|
342
|
+
|
|
343
|
+
| 端点 | 方法 | 说明 |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| `/api/admin/export` | GET | 导出数据库为备份 |
|
|
346
|
+
| `/api/admin/import` | POST | 导入备份(替换连接后重新 `configureDatabase()`) |
|
|
347
|
+
| `/api/admin/stats` | GET | 系统统计 |
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Skills(AI 技能包)
|
|
352
|
+
|
|
353
|
+
技能包是与 jpage MCP server 配套的 [Claude Code / Claude Desktop Skill](https://modelcontextprotocol.io) 仓库。Web UI 的"AI 技能"区域支持浏览与下载。
|
|
354
|
+
|
|
355
|
+
- `GET /api/skills` — 列出 `skills/*/SKILL.md` 中的所有 skill
|
|
356
|
+
- `GET /api/skills/:name` — 返回 skill 详情(含 SKILL.md 正文 + 文件清单)
|
|
357
|
+
- `GET /api/skills/:name/download` — 打包整个 skill 目录为 zip 返回
|
|
358
|
+
|
|
359
|
+
| 项 | 值 |
|
|
360
|
+
|---|---|
|
|
361
|
+
| 鉴权 | session cookie **或** Bearer token(与 `/mcp` 共享 `MCP_TOKEN`) |
|
|
362
|
+
| 数据源 | 仓库内 `skills/<name>/SKILL.md` 及其同目录文件 |
|
|
363
|
+
| SKILL.md 解析 | 读取 YAML frontmatter(`name` / `description` / `version` / `author`),缺失 `name` 时回退到目录名 |
|
|
364
|
+
| zip 库 | `archiver@7`(流式打包) |
|
|
365
|
+
|
|
366
|
+
### `GET /api/skills`
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
curl -b jpage.sid http://localhost:8858/api/skills
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```json
|
|
373
|
+
{
|
|
374
|
+
"skills": [
|
|
375
|
+
{
|
|
376
|
+
"name": "jpage-upload",
|
|
377
|
+
"title": "jpage-upload",
|
|
378
|
+
"description": "将 HTML / Markdown 字符串上传到本地 jpage 服务…",
|
|
379
|
+
"version": "",
|
|
380
|
+
"author": "",
|
|
381
|
+
"fileCount": 1,
|
|
382
|
+
"totalSize": 3094
|
|
383
|
+
}
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### `GET /api/skills/:name`
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
curl -b jpage.sid http://localhost:8858/api/skills/jpage-upload
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
返回:`{name, title, description, version, author, fileCount, totalSize, files: [...], body: "<SKILL.md 正文 markdown>"}`
|
|
395
|
+
|
|
396
|
+
### `GET /api/skills/:name/download`
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
curl -b jpage.sid -OJ http://localhost:8858/api/skills/jpage-upload/download
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
下载 `jpage-upload.zip`,解压后是完整的 skill 目录(顶层目录名为 skill 名),可直接复制到 `~/.claude/skills/`。
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## MCP 端点(`/mcp`)
|
|
407
|
+
|
|
408
|
+
独立的 MCP Streamable HTTP 端点。**全局 `MCP_TOKEN` 或用户级 API Token 任一即可挂载**(未配置任何 Token 时 `/mcp` 禁用)。
|
|
409
|
+
|
|
410
|
+
| 项 | 值 |
|
|
411
|
+
|---|---|
|
|
412
|
+
| 路径 | `POST`/`GET`/`DELETE` `/mcp` |
|
|
413
|
+
| 鉴权 | `Authorization: Bearer <MCP_TOKEN>`(全局,向后兼容)**或** 用户级 API Token(`jp_xxx`) |
|
|
414
|
+
| 协议 | MCP Streamable HTTP(最新规范) |
|
|
415
|
+
| 工具(15 个) | `list_files` / `upload_file` / `get_file_content` / `delete_file` / `rename_file` / `get_file_url` / `list_file_versions` / `restore_file_version` / `list_tags` / `add_tags_to_file` / `star_file` / `unstar_file` / `list_categories` / `create_category` / `set_file_category` |
|
|
416
|
+
| 资源 | `jpage://files` / `jpage://file/{id}`(≤ 256KB) |
|
|
417
|
+
|
|
418
|
+
工具和资源**不走 `fetch('http://127.0.0.1:port/...')` 自调用**,而是通过 `lib/dispatch.js` 的进程内分发器直接调用 `app.handle()`,复用同一 Bearer token。绕过 TCP 序列化与二次鉴权 DB 查询(单次调用约快 80%),同时权限、限流、审计与 HTTP 完全一致。
|
|
419
|
+
|
|
420
|
+
完整 schema 可在 Claude Desktop / Claude Code 连接后查看,或通过 `npx @modelcontextprotocol/inspector http://localhost:8858/mcp` 调试。
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 环境变量
|
|
425
|
+
|
|
426
|
+
| 变量 | 默认 | 说明 |
|
|
427
|
+
|---|---|---|
|
|
428
|
+
| `PORT` | `8858` | HTTP 监听端口 |
|
|
429
|
+
| `NODE_ENV` | `development` | `production` 时强制要求 `SESSION_SECRET`,Cookie 仅 HTTPS 下发送 |
|
|
430
|
+
| `SESSION_SECRET` | 随机生成(开发) | session 签名密钥。生产必设 |
|
|
431
|
+
| `JPAGE_DATA_DIR` | `./data` | 数据目录(数据库、会话、上传文件) |
|
|
432
|
+
| `ADMIN_USER` | `admin` | 启动时若 users 表为空,自动创建该用户名的管理员 |
|
|
433
|
+
| `ADMIN_PASSWORD` | — | 管理员密码(≥ 8 位)。留空则自动生成 16 位随机密码并打到启动日志 |
|
|
434
|
+
| `MCP_TOKEN` | — | 全局 `/mcp` Bearer token(可选)。**未设置时仍可用用户级 API Token 访问 `/mcp`** |
|
|
435
|
+
| `MCP_IP` | `localhost` | `/mcp` 对外暴露的 IP/主机名(用于启动日志和 `.mcp.json` 中的 URL) |
|
|
436
|
+
| `ALLOW_REGISTRATION` | `false` | 设为 `true` 开放用户自助注册 |
|
|
437
|
+
| `SMTP_HOST` 等 | — | SMTP 配置(`SMTP_HOST/PORT/SECURE/USER/PASS/FROM`),用于邮箱验证 |
|
|
438
|
+
| `APP_URL` | `http://localhost:8858` | 应用外部访问地址,用于拼接验证链接 |
|