@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/README_EN.md
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# jpage
|
|
2
|
+
|
|
3
|
+
> Drop a file, get a page — instantly.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/code2rich/jpage/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
**[>>> View Product Introduction <<<](https://jpage.cn/)**
|
|
8
|
+
|
|
9
|
+
**jpage** is a zero-config HTML / Markdown instant preview and sharing tool. Drop in a document and instantly get a clean online page — no deployment pipeline, no server knowledge required. Especially great for one-click sharing of AI-generated content.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
### Core
|
|
16
|
+
|
|
17
|
+
- **Instant Preview** — Upload HTML or Markdown files, get a rendered online page in seconds
|
|
18
|
+
- **Enhanced Markdown Rendering** — Syntax highlighting (highlight.js), math formulas (KaTeX), Mermaid diagrams, with automatic dark/light theme switching
|
|
19
|
+
- **Source View** — Toggle between rendered and source modes for easy comparison
|
|
20
|
+
- **Short Links** — Each file gets an auto-generated 8-character short link (`/s/xxxxxxxx`)
|
|
21
|
+
- **File Management** — Rename, delete, download, toggle public/private — simple and intuitive
|
|
22
|
+
- **Drag & Drop Upload** — Click or drag to upload, up to 50MB per file
|
|
23
|
+
- **Version History** — Overwrite uploads automatically preserve previous versions; rollback anytime
|
|
24
|
+
- **Responsive Design** — Adapts to desktop and mobile; dark mode follows system preference
|
|
25
|
+
|
|
26
|
+
### Organization
|
|
27
|
+
|
|
28
|
+
- **Tags** — Tag files for multi-dimensional categorization and search
|
|
29
|
+
- **Categories** — Organize files into categories for clear hierarchy
|
|
30
|
+
- **Favorites** — One-click bookmark for quick access to frequently used files
|
|
31
|
+
|
|
32
|
+
### Security & Permissions
|
|
33
|
+
|
|
34
|
+
- **Multi-user Support** — Admin can create and manage multiple users; regular users can only access their own files and public files
|
|
35
|
+
- **Open Registration** — Enable self-service registration via `ALLOW_REGISTRATION=true`, with optional SMTP email verification
|
|
36
|
+
- **Session Auth** — Cookie + bcrypt password hashing
|
|
37
|
+
- **API Tokens** — Each user can create multiple API tokens for scripts and AI tools
|
|
38
|
+
- **Public/Private Files** — Toggle visibility on upload; private files are only accessible to the owner and admin
|
|
39
|
+
- **Rate Limiting** — Login and upload endpoints are rate-limited to prevent brute force and abuse
|
|
40
|
+
|
|
41
|
+
### AI Integration
|
|
42
|
+
|
|
43
|
+
- **MCP Protocol** — Built-in MCP Streamable HTTP endpoint for direct AI tool integration
|
|
44
|
+
- **Skills Management** — Auto-discovers Claude Code/Desktop skill packs in the `skills/` directory
|
|
45
|
+
- **JSON Upload API** — `/api/files/upload-json` for programmatic uploads, ideal for AI workflows
|
|
46
|
+
|
|
47
|
+
### Deployment
|
|
48
|
+
|
|
49
|
+
- **Zero-Dependency Runtime** — Single container startup with built-in SQLite storage
|
|
50
|
+
- **Docker One-Click Deploy** — Multi-stage build, env var configuration, volume persistence
|
|
51
|
+
- **Database Migrations** — Automatic schema migration on startup; no manual upgrades needed
|
|
52
|
+
|
|
53
|
+
## Tech Stack
|
|
54
|
+
|
|
55
|
+
- **Backend**: Node.js + Express + express-session (SQLite session store), domain-split Router architecture (routes/ + lib/ shared layer)
|
|
56
|
+
- **Database**: SQLite3 (zero-config, auto-migration)
|
|
57
|
+
- **Frontend**: Vanilla JavaScript (no framework dependencies)
|
|
58
|
+
- **Rendering**: marked.js + highlight.js + KaTeX + Mermaid
|
|
59
|
+
- **Security**: helmet + layered CSP (strict policy for admin UI, iframe sandbox isolation + content-tiered CSP for render pages)
|
|
60
|
+
- **Protocol**: MCP Streamable HTTP (@modelcontextprotocol/sdk)
|
|
61
|
+
- **Testing**: node:test + supertest (unit + integration), GitHub Actions CI
|
|
62
|
+
- **Container**: Docker / Docker Compose
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
### Docker Deploy (Recommended)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://github.com/code2rich/jpage.git
|
|
70
|
+
cd jpage
|
|
71
|
+
cp .env.example .env # Edit .env with ADMIN_PASSWORD and SESSION_SECRET
|
|
72
|
+
docker-compose up -d
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Visit http://localhost:8858 — you'll be redirected to the login page.
|
|
76
|
+
|
|
77
|
+
### Local Development
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm install
|
|
81
|
+
ADMIN_USER=admin ADMIN_PASSWORD=test1234 SESSION_SECRET=dev-secret npm start
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Development mode (hot reload):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm run dev
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Development & Testing
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm test # Unit + integration tests (node:test + supertest)
|
|
94
|
+
npm run test:unit # Unit tests only
|
|
95
|
+
npm run build # Build frontend bundle (esbuild → public/dist)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
End-to-end / benchmarks (start the server first with `npm start`):
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
node test/perf-harness.js 8858 # Core e2e (login/upload/render/short-link/tags)
|
|
102
|
+
node test/mcp-harness.js 8858 # MCP endpoint
|
|
103
|
+
node test/perf-bench.js 8858 # Render/list/cache latency benchmarks
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Auth & Security
|
|
107
|
+
|
|
108
|
+
jpage supports a multi-user system. Admin manages all users and files; regular users can only access their own files and public files. Share links (`/api/files/:id/render`, `/s/:key`, download, source) are anonymously accessible when the file is marked public. Uncheck "Public access" on upload to make the file visible only to the owner and admin.
|
|
109
|
+
|
|
110
|
+
**Content Security (CSP)**: Hardened via helmet + layered policies — the admin UI gets a strict CSP (same-origin scripts only); user-content render pages are isolated by iframe sandbox (without `allow-same-origin`, blocking access to the parent window). Markdown pages use a strict CSP (inline mermaid init script whitelisted via nonce), while HTML pages use a relaxed CSP + sandbox fallback (user HTML often contains legitimate scripts).
|
|
111
|
+
|
|
112
|
+
### Authentication Methods
|
|
113
|
+
|
|
114
|
+
API and MCP endpoints support three authentication methods:
|
|
115
|
+
|
|
116
|
+
1. **Session Cookie** — `jpage.sid` cookie after login, for browser access
|
|
117
|
+
2. **API Token** — User-created `jp_` prefixed tokens, for script integration
|
|
118
|
+
3. **MCP Token** — `MCP_TOKEN` environment variable, for AI tool connections (backward compatible)
|
|
119
|
+
|
|
120
|
+
### Environment Variables
|
|
121
|
+
|
|
122
|
+
| Variable | Required | Description |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `ADMIN_USER` | No | Admin username for first startup when users table is empty; defaults to `admin` |
|
|
125
|
+
| `ADMIN_PASSWORD` | No | Admin password for first startup (≥8 chars); if empty, a random 16-char password is generated and printed to startup logs |
|
|
126
|
+
| `SESSION_SECRET` | Prod | Encrypts session cookies; in dev mode a temporary key is auto-generated (lost on restart) |
|
|
127
|
+
| `NODE_ENV` | No | When `production`, cookies are sent only over HTTPS; missing SESSION_SECRET will refuse to start |
|
|
128
|
+
| `PORT` | No | Default `8858` |
|
|
129
|
+
| `MCP_TOKEN` | No | Global Bearer token for the `/mcp` endpoint (backward compatible); when unset, `/mcp` is still accessible via a user-level API token (`jp_` prefix) |
|
|
130
|
+
| `ALLOW_REGISTRATION` | No | Set to `true` to enable self-service registration; defaults to off (admin-only user creation) |
|
|
131
|
+
| `SMTP_HOST` | No | SMTP server address (e.g. `smtp.qq.com`); enables email verification when configured |
|
|
132
|
+
| `SMTP_PORT` | No | SMTP port (e.g. `465`) |
|
|
133
|
+
| `SMTP_SECURE` | No | Use SSL (`true`/`false`) |
|
|
134
|
+
| `SMTP_USER` | No | SMTP login username |
|
|
135
|
+
| `SMTP_PASS` | No | SMTP login password or authorization code |
|
|
136
|
+
| `SMTP_FROM` | No | Sender address (e.g. `"jpage <user@example.com>"`) |
|
|
137
|
+
| `APP_URL` | No | External app URL used to build verification links (e.g. `https://jpage.cn`) |
|
|
138
|
+
|
|
139
|
+
If both `ADMIN_USER` and `ADMIN_PASSWORD` are left empty, the startup log will output:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
[jpage] Created initial admin: admin
|
|
143
|
+
[jpage] Initial password (save this): 7Hk2mN9pq4rTv8wX
|
|
144
|
+
[jpage] ⚠️ Please change the password after first login
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Copy the password from the log to log in.
|
|
148
|
+
|
|
149
|
+
Recommended `SESSION_SECRET` generation:
|
|
150
|
+
```bash
|
|
151
|
+
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Reset or Change Password
|
|
155
|
+
|
|
156
|
+
All users can change their password in Settings after login. Admin can reset other users' passwords in User Management.
|
|
157
|
+
|
|
158
|
+
Manual method (SQLite command line):
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
node -e "console.log(require('bcryptjs').hashSync('new-password', 10))"
|
|
162
|
+
sqlite3 data/database.sqlite "UPDATE users SET password_hash='<hash-from-above>' WHERE username='admin';"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Project Structure
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
jpage/
|
|
169
|
+
├── server.js # Entry: app assembly + middleware + startup orchestration (logic split out)
|
|
170
|
+
├── routes/ # Domain-split Express Routers
|
|
171
|
+
│ ├── auth.js # Login/register/email verification
|
|
172
|
+
│ ├── users.js # User management (admin)
|
|
173
|
+
│ ├── tokens.js # API tokens
|
|
174
|
+
│ ├── files.js # File CRUD/upload/render/versions/tags/star/stats
|
|
175
|
+
│ ├── tags.js # Tags
|
|
176
|
+
│ ├── categories.js # Categories + template metadata
|
|
177
|
+
│ ├── content-templates.js # Content template marketplace
|
|
178
|
+
│ ├── admin.js # Backup export/import/stats
|
|
179
|
+
│ └── skills.js # Skills + MCP config
|
|
180
|
+
├── lib/ # Shared layer (reused by routes)
|
|
181
|
+
│ ├── db.js # SQLite access (dbRun/dbGet/dbAll + PRAGMA)
|
|
182
|
+
│ ├── paths.js # Data/upload dir constants
|
|
183
|
+
│ ├── util.js # now/shareKey/clientIp/decodeFilename pure helpers
|
|
184
|
+
│ ├── csp.js # Layered CSP policies + nonce
|
|
185
|
+
│ ├── auth-state.js # Shared adminUserId state
|
|
186
|
+
│ ├── templates.js # Template system + marked/hljs/KaTeX pipeline
|
|
187
|
+
│ ├── render.js # File → HTML rendering (with CSP headers)
|
|
188
|
+
│ ├── render-cache.js # Render result LRU cache
|
|
189
|
+
│ ├── fts.js # FTS5 full-text index
|
|
190
|
+
│ ├── categories.js # Category name in-memory cache
|
|
191
|
+
│ ├── view-counts.js # Buffered view-count batched flush
|
|
192
|
+
│ ├── zip.js # ZIP upload (security validation/extraction/classification)
|
|
193
|
+
│ ├── dispatch.js # MCP in-process request dispatch (bypass TCP self-call)
|
|
194
|
+
│ └── middleware/ # Auth + file-loading middleware
|
|
195
|
+
├── logger.js # Structured JSON Lines logger
|
|
196
|
+
├── mailer.js # SMTP mail (verification codes/links)
|
|
197
|
+
├── mcp-server.js # MCP Streamable HTTP endpoint (/mcp)
|
|
198
|
+
├── migrations.js # Database migration runner
|
|
199
|
+
├── migrations/ # Sequential schema migrations (001-012)
|
|
200
|
+
├── skills-registry.js # Scans skills/ dir, provides skill list/details/zip packaging
|
|
201
|
+
├── templates/ # Markdown render style templates (default/github/academic/dark-pro)
|
|
202
|
+
├── package.json
|
|
203
|
+
├── build.js # esbuild bundles frontend → public/dist
|
|
204
|
+
├── Dockerfile
|
|
205
|
+
├── docker-compose.yml
|
|
206
|
+
├── .env.example # Environment variable template
|
|
207
|
+
├── .mcp.json # MCP client config example
|
|
208
|
+
├── docs/
|
|
209
|
+
│ ├── api.md # Complete REST API reference
|
|
210
|
+
│ └── design/ # Design documents
|
|
211
|
+
├── skills/
|
|
212
|
+
│ └── jpage-upload/ # Claude Code / Desktop skill
|
|
213
|
+
│ └── SKILL.md
|
|
214
|
+
├── test/ # Unit + integration tests (node:test + supertest) + e2e harness
|
|
215
|
+
├── data/ # SQLite databases, uploaded files & sessions (auto-created)
|
|
216
|
+
└── public/ # Frontend static assets
|
|
217
|
+
├── index.html
|
|
218
|
+
├── css/style.css
|
|
219
|
+
├── js/ # Page-split ES modules
|
|
220
|
+
└── dist/ # Build output (npm run build, git-ignored)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## REST API
|
|
224
|
+
|
|
225
|
+
Port `8858` (overridable via `PORT`). All write endpoints require login or Bearer token.
|
|
226
|
+
|
|
227
|
+
### Auth
|
|
228
|
+
|
|
229
|
+
| Endpoint | Method | Description |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `/api/auth/me` | GET | Current login info |
|
|
232
|
+
| `/api/auth/login` | POST | Login (`{username, password}`) |
|
|
233
|
+
| `/api/auth/register` | POST | Register (requires `ALLOW_REGISTRATION=true`) |
|
|
234
|
+
| `/api/auth/logout` | POST | Logout |
|
|
235
|
+
| `/api/auth/change-password` | POST | Change password (all users) |
|
|
236
|
+
|
|
237
|
+
### User Management (Admin only)
|
|
238
|
+
|
|
239
|
+
| Endpoint | Method | Description |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| `/api/users` | GET | List all users |
|
|
242
|
+
| `/api/users` | POST | Create user |
|
|
243
|
+
| `/api/users/:id` | PUT | Update role or reset password |
|
|
244
|
+
| `/api/users/:id` | DELETE | Delete user; files transfer to admin |
|
|
245
|
+
|
|
246
|
+
### API Tokens
|
|
247
|
+
|
|
248
|
+
| Endpoint | Method | Description |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| `/api/tokens` | GET | List own tokens |
|
|
251
|
+
| `/api/tokens` | POST | Create token (plaintext returned once only) |
|
|
252
|
+
| `/api/tokens/:id` | DELETE | Delete token |
|
|
253
|
+
|
|
254
|
+
### File Management
|
|
255
|
+
|
|
256
|
+
| Endpoint | Method | Description |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `/api/files` | GET | List files (admin sees all; users see own + public) |
|
|
259
|
+
| `/api/files/upload` | POST | Multipart upload |
|
|
260
|
+
| `/api/files/upload-json` | POST | JSON upload (`{name, content, isPublic?}`) |
|
|
261
|
+
| `/api/files/:id` | PUT | Rename or toggle public/private |
|
|
262
|
+
| `/api/files/:id` | DELETE | Delete file |
|
|
263
|
+
| `/api/files/:id/content` | GET | Return raw text |
|
|
264
|
+
| `/api/files/:id/render` | GET | Return rendered HTML |
|
|
265
|
+
| `/api/files/:id/download` | GET | Stream download file |
|
|
266
|
+
| `/s/:key` | GET | Short link page render |
|
|
267
|
+
|
|
268
|
+
### Tags
|
|
269
|
+
|
|
270
|
+
| Endpoint | Method | Description |
|
|
271
|
+
|---|---|---|
|
|
272
|
+
| `/api/tags` | GET | List all tags (with file_count) |
|
|
273
|
+
| `/api/tags` | POST | Create tag |
|
|
274
|
+
| `/api/tags/:id` | DELETE | Delete tag |
|
|
275
|
+
| `/api/files/:id/tags` | PUT | Replace file's tag list |
|
|
276
|
+
|
|
277
|
+
### Favorites
|
|
278
|
+
|
|
279
|
+
| Endpoint | Method | Description |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| `/api/files/:id/star` | POST | Favorite file |
|
|
282
|
+
| `/api/files/:id/star` | DELETE | Unfavorite file |
|
|
283
|
+
|
|
284
|
+
### Categories
|
|
285
|
+
|
|
286
|
+
| Endpoint | Method | Description |
|
|
287
|
+
|---|---|---|
|
|
288
|
+
| `/api/categories` | GET | List categories (with file_count) |
|
|
289
|
+
| `/api/categories` | POST | Create category |
|
|
290
|
+
| `/api/categories/:id` | PUT | Rename category |
|
|
291
|
+
| `/api/categories/:id` | DELETE | Delete category |
|
|
292
|
+
| `/api/files/:id/category` | PUT | Set file category |
|
|
293
|
+
|
|
294
|
+
### Skills
|
|
295
|
+
|
|
296
|
+
| Endpoint | Method | Description |
|
|
297
|
+
|---|---|---|
|
|
298
|
+
| `/api/skills` | GET | List installed skill packs |
|
|
299
|
+
| `/api/skills/:name` | GET | Skill details |
|
|
300
|
+
| `/api/skills/:name/download` | GET | ZIP download of skill directory |
|
|
301
|
+
|
|
302
|
+
Full API documentation: [docs/api.md](docs/api.md).
|
|
303
|
+
|
|
304
|
+
## MCP / AI Integration
|
|
305
|
+
|
|
306
|
+
jpage includes a built-in [MCP Streamable HTTP](https://modelcontextprotocol.io) endpoint, enabling AI tools like Claude Code and Claude Desktop to directly upload and manage files.
|
|
307
|
+
|
|
308
|
+
### Enable
|
|
309
|
+
|
|
310
|
+
Set the `MCP_TOKEN` environment variable:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
MCP_TOKEN=your-secret-token
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Client Configuration
|
|
317
|
+
|
|
318
|
+
**Claude Code** — Add to your project's `.mcp.json` or merge into `~/.claude.json`:
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
{
|
|
322
|
+
"mcpServers": {
|
|
323
|
+
"jpage": {
|
|
324
|
+
"type": "http",
|
|
325
|
+
"url": "http://localhost:8858/mcp",
|
|
326
|
+
"headers": {
|
|
327
|
+
"Authorization": "Bearer ${env.MCP_TOKEN}"
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Claude Desktop** — Merge the `mcpServers` block into `~/Library/Application Support/Claude/claude_desktop_config.json`.
|
|
335
|
+
|
|
336
|
+
### Capabilities
|
|
337
|
+
|
|
338
|
+
**Tools** (15):
|
|
339
|
+
|
|
340
|
+
| Tool | Purpose |
|
|
341
|
+
|---|---|
|
|
342
|
+
| `upload_file` | Upload HTML or Markdown, return preview link |
|
|
343
|
+
| `list_files` | List all files |
|
|
344
|
+
| `get_file_content` | Read file content |
|
|
345
|
+
| `get_file_url` | Get file preview URL |
|
|
346
|
+
| `rename_file` | Rename file |
|
|
347
|
+
| `delete_file` | Delete file |
|
|
348
|
+
| `list_file_versions` | View file version history |
|
|
349
|
+
| `restore_file_version` | Rollback to specified version |
|
|
350
|
+
| `list_tags` | List tags |
|
|
351
|
+
| `add_tags_to_file` | Add tags to file |
|
|
352
|
+
| `star_file` | Favorite file |
|
|
353
|
+
| `unstar_file` | Unfavorite file |
|
|
354
|
+
| `list_categories` | List categories |
|
|
355
|
+
| `create_category` | Create category |
|
|
356
|
+
| `set_file_category` | Set file category |
|
|
357
|
+
|
|
358
|
+
**Resources** (2):
|
|
359
|
+
|
|
360
|
+
| URI | Description |
|
|
361
|
+
|---|---|
|
|
362
|
+
| `jpage://files` | All file metadata (JSON list) |
|
|
363
|
+
| `jpage://file/{id}` | Single file content (≤ 256KB) |
|
|
364
|
+
|
|
365
|
+
### Companion Skill
|
|
366
|
+
|
|
367
|
+
The repo includes `skills/jpage-upload/SKILL.md`, a ready-to-use skill for Claude Code / Desktop. Once installed, AI-generated HTML, Markdown, reports, and visualizations are automatically uploaded to jpage with a preview link.
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
ln -s "$(pwd)/skills/jpage-upload" ~/.claude/skills/jpage-upload
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Web Management
|
|
374
|
+
|
|
375
|
+
After login, the homepage bottom section shows an **AI Skills** block where you can view details and download zip packages. Add new skills by creating `skills/<name>/SKILL.md` with YAML frontmatter — the service auto-discovers them on restart.
|
|
376
|
+
|
|
377
|
+
### Debug
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
npx -y @modelcontextprotocol/inspector http://localhost:8858/mcp
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Use Cases
|
|
384
|
+
|
|
385
|
+
- **AI Content Sharing** — One-click upload of HTML reports and visualizations generated by Claude Code, Cursor, and similar tools
|
|
386
|
+
- **Technical Documentation** — Markdown notes, meeting minutes, project reports with auto-rendered code highlighting, math formulas, and diagrams
|
|
387
|
+
- **Static Page Hosting** — Single-page HTML demos, prototypes, landing pages — no server configuration needed
|
|
388
|
+
- **Temporary File Sharing** — Any HTML/Markdown file, drag and drop to get a link — no account required
|
|
389
|
+
- **Version Management** — Iterative document updates automatically preserve history; rollback anytime
|
|
390
|
+
|
|
391
|
+
## Why
|
|
392
|
+
|
|
393
|
+
Existing solutions are either too heavy (requiring server setup, domains, CI) or too closed (locked to specific platforms).
|
|
394
|
+
|
|
395
|
+
jpage does one thing: make static content sharing simple again. Drop a file, get a link. An optional multi-user system exists, but the default is zero-friction — drop a file to get a link, share public files anonymously without registering.
|
|
396
|
+
|
|
397
|
+
## License
|
|
398
|
+
|
|
399
|
+
MIT
|
package/bin/args.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// 零依赖 argv 解析器。
|
|
2
|
+
//
|
|
3
|
+
// 约定:
|
|
4
|
+
// - 以 `--` 开头的 token 是选项(长选项);支持 `--key value` 和 `--key=value` 两种形式
|
|
5
|
+
// - 不支持单字母组合短选项(-a),避免歧义;单字母需求用长选项
|
|
6
|
+
// - 布尔标志:出现即为 true(如 --public)。需要显式值时用 --key=value
|
|
7
|
+
// - 其余 token 是位置参数(positional)
|
|
8
|
+
// - 第一个位置参数视作命令(cmd),第二个视作子命令(sub)
|
|
9
|
+
//
|
|
10
|
+
// 返回:{ cmd, sub, opts, positional }
|
|
11
|
+
// cmd 第一个位置参数(字符串,无则 null)
|
|
12
|
+
// sub 第二个位置参数(字符串,无则 null)
|
|
13
|
+
// opts 选项对象(键名去掉 -- 前缀,值默认 true,否则字符串)
|
|
14
|
+
// positional 全部位置参数数组(含 cmd/sub)
|
|
15
|
+
//
|
|
16
|
+
// 设计目标:够用、可预测、无运行时依赖。commander 级别的复杂场景(子命令嵌套、
|
|
17
|
+
// 变长参数类型校验)超出 CLI 需求,故不引入。
|
|
18
|
+
|
|
19
|
+
function parse(argv) {
|
|
20
|
+
const opts = {};
|
|
21
|
+
const positional = [];
|
|
22
|
+
let cmd = null;
|
|
23
|
+
let sub = null;
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
const tok = argv[i];
|
|
27
|
+
|
|
28
|
+
if (tok === '--') {
|
|
29
|
+
// 之后的全部当位置参数
|
|
30
|
+
for (let j = i + 1; j < argv.length; j++) positional.push(argv[j]);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (tok.startsWith('--')) {
|
|
35
|
+
const eq = tok.indexOf('=');
|
|
36
|
+
if (eq > 1) {
|
|
37
|
+
// --key=value
|
|
38
|
+
const key = tok.slice(2, eq);
|
|
39
|
+
const value = tok.slice(eq + 1);
|
|
40
|
+
opts[key] = value;
|
|
41
|
+
} else {
|
|
42
|
+
const key = tok.slice(2);
|
|
43
|
+
const next = argv[i + 1];
|
|
44
|
+
// 下一个 token 不是选项(且存在)→ 当作值;否则当布尔标志
|
|
45
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
46
|
+
opts[key] = next;
|
|
47
|
+
i++;
|
|
48
|
+
} else {
|
|
49
|
+
opts[key] = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
positional.push(tok);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (positional.length >= 1) cmd = positional[0];
|
|
59
|
+
if (positional.length >= 2) sub = positional[1];
|
|
60
|
+
|
|
61
|
+
return { cmd, sub, opts, positional };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { parse };
|
package/bin/client.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// CLI HTTP 客户端:基于全局 fetch 的薄封装,统一鉴权与错误处理。
|
|
2
|
+
//
|
|
3
|
+
// 接口形:{ get, post, postForm, put, del },path 以 /api/... 开头。
|
|
4
|
+
// 非预期 JSON(如下载二进制)用 raw(path, init) 拿原始 Response。
|
|
5
|
+
//
|
|
6
|
+
// 测试注入:createClient 接受可选 fetchImpl 参数。集成测试注入一个打到 supertest
|
|
7
|
+
// app 的 fetch shim,避免起真实 TCP 端口(项目既有测试都是 in-process 模式)。
|
|
8
|
+
//
|
|
9
|
+
// 与 lib/dispatch.js 的区别:dispatch 是「服务端进程内直调」(绕过 TCP),
|
|
10
|
+
// 本文件是「客户端经网络调」(真 fetch),两者职责正交。
|
|
11
|
+
|
|
12
|
+
// 请求失败的统一错误类型。带 status + 可读 message,命令层据此决定退出码与提示。
|
|
13
|
+
class HttpError extends Error {
|
|
14
|
+
constructor(status, message, body) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'HttpError';
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.body = body;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 创建 API 客户端。
|
|
24
|
+
* @param {object} opts
|
|
25
|
+
* @param {string} opts.base - 服务地址(如 http://localhost:8858),无尾斜杠
|
|
26
|
+
* @param {string} [opts.token] - Bearer token;为空时不发 Authorization 头
|
|
27
|
+
* @param {function} [opts.fetchImpl] - 可选 fetch 实现(测试注入)
|
|
28
|
+
* @returns {object}
|
|
29
|
+
*/
|
|
30
|
+
function createClient({ base, token, fetchImpl } = {}) {
|
|
31
|
+
const f = fetchImpl || fetch;
|
|
32
|
+
|
|
33
|
+
async function raw(path, init = {}) {
|
|
34
|
+
const url = base + path;
|
|
35
|
+
const headers = { ...(init.headers || {}) };
|
|
36
|
+
if (token) headers.Authorization = 'Bearer ' + token;
|
|
37
|
+
|
|
38
|
+
let res;
|
|
39
|
+
try {
|
|
40
|
+
res = await f(url, { ...init, headers });
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// 网络层错误(ECONNREFUSED / DNS / 超时)→ 包装成可识别错误
|
|
43
|
+
const err = new Error(`无法连接到 ${base}:${e.message}`);
|
|
44
|
+
err.name = 'NetworkError';
|
|
45
|
+
err.cause = e;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
return res;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// JSON 请求:发 JSON body,期望 JSON 响应。失败抛 HttpError。
|
|
52
|
+
async function json(method, path, body) {
|
|
53
|
+
const init = { method };
|
|
54
|
+
if (body !== undefined) {
|
|
55
|
+
init.headers = { 'content-type': 'application/json' };
|
|
56
|
+
init.body = JSON.stringify(body);
|
|
57
|
+
}
|
|
58
|
+
const res = await raw(path, init);
|
|
59
|
+
return parseJson(res, method, path);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function parseJson(res, method, path) {
|
|
63
|
+
const text = await res.text();
|
|
64
|
+
let data = null;
|
|
65
|
+
if (text) {
|
|
66
|
+
try {
|
|
67
|
+
data = JSON.parse(text);
|
|
68
|
+
} catch {
|
|
69
|
+
data = { raw: text };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (res.status < 200 || res.status >= 300) {
|
|
73
|
+
const msg = (data && data.error) || `HTTP ${res.status}`;
|
|
74
|
+
throw new HttpError(res.status, `REST ${method} ${path} -> ${res.status} ${msg}`, data);
|
|
75
|
+
}
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
raw,
|
|
81
|
+
get: (p) => json('GET', p),
|
|
82
|
+
post: (p, body) => json('POST', p, body),
|
|
83
|
+
put: (p, body) => json('PUT', p, body),
|
|
84
|
+
del: (p) => json('DELETE', p),
|
|
85
|
+
// multipart:调用方传已构造好的 FormData
|
|
86
|
+
postForm: async (p, formData) => {
|
|
87
|
+
const res = await raw(p, { method: 'POST', body: formData });
|
|
88
|
+
return parseJson(res, 'POST', p);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { createClient, HttpError };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// 命令间共享的输出辅助函数。
|
|
2
|
+
//
|
|
3
|
+
// 设计:命令模块只做「调 API + 格式化输出」,错误码由 bin/jpage.js 统一处理。
|
|
4
|
+
// 这里集中放格式化工具(人类可读的字节/时间、表格),避免命令文件互相重复。
|
|
5
|
+
//
|
|
6
|
+
// I/O 注入:out()/err() 默认写 process.stdout/stderr,但 run() 可通过 setIo()
|
|
7
|
+
// 注入自定义流(测试用),这样测试捕获输出时不必 monkeypatch process.stdout
|
|
8
|
+
// (那会破坏 node:test 自身的 TAP 输出)。
|
|
9
|
+
|
|
10
|
+
let _stdout = process.stdout;
|
|
11
|
+
let _stderr = process.stderr;
|
|
12
|
+
|
|
13
|
+
function setIo({ stdout, stderr } = {}) {
|
|
14
|
+
if (stdout) _stdout = stdout;
|
|
15
|
+
if (stderr) _stderr = stderr;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resetIo() {
|
|
19
|
+
_stdout = process.stdout;
|
|
20
|
+
_stderr = process.stderr;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 写一行到 stdout(自动补换行)。
|
|
24
|
+
function out(s) {
|
|
25
|
+
_stdout.write(s);
|
|
26
|
+
}
|
|
27
|
+
// 写一行到 stderr。
|
|
28
|
+
function err(s) {
|
|
29
|
+
_stderr.write(s);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 字节 → 人类可读(B / KB / MB)。
|
|
33
|
+
function formatSize(bytes) {
|
|
34
|
+
if (bytes == null) return '-';
|
|
35
|
+
const n = Number(bytes);
|
|
36
|
+
if (!isFinite(n)) return '-';
|
|
37
|
+
if (n < 1024) return n + 'B';
|
|
38
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + 'K';
|
|
39
|
+
return (n / 1024 / 1024).toFixed(1) + 'M';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ISO/SQLite 时间 → 简短显示。原样返回无法解析的输入。
|
|
43
|
+
function formatTime(t) {
|
|
44
|
+
if (!t) return '-';
|
|
45
|
+
return String(t).replace('T', ' ').replace(/\.\d+$/, '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 从文件元数据拼预览 URL:优先 /s/:key,无私有短链时返回 null。
|
|
49
|
+
function shareUrl(base, file) {
|
|
50
|
+
if (!file || !file.share_key) return null;
|
|
51
|
+
return `${base}/s/${file.share_key}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { setIo, resetIo, out, err, formatSize, formatTime, shareUrl };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// cat 命令:输出文件原始内容到 stdout。
|
|
2
|
+
// 后端:GET /api/files/:id/content(owner/admin 限定)。
|
|
3
|
+
// bundle(is_bundle=1)不支持取「整个内容」,后端会返回入口文件内容,此处原样输出。
|
|
4
|
+
|
|
5
|
+
const { out } = require('./_shared');
|
|
6
|
+
|
|
7
|
+
async function run(client, args) {
|
|
8
|
+
const id = args.sub;
|
|
9
|
+
if (!id) {
|
|
10
|
+
const e = new Error('用法:jpage cat <id>');
|
|
11
|
+
e.name = 'UsageError';
|
|
12
|
+
throw e;
|
|
13
|
+
}
|
|
14
|
+
const data = await client.get(`/api/files/${id}/content`);
|
|
15
|
+
if (typeof data.content === 'string') {
|
|
16
|
+
out(data.content);
|
|
17
|
+
if (!data.content.endsWith('\n')) out('\n');
|
|
18
|
+
} else {
|
|
19
|
+
out(JSON.stringify(data, null, 2) + '\n');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { run };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// ls 命令:列出文件。
|
|
2
|
+
// 后端:GET /api/files(支持 page/limit/sort/order/keyword/category/tag)。
|
|
3
|
+
|
|
4
|
+
const { formatSize, formatTime, out } = require('./_shared');
|
|
5
|
+
|
|
6
|
+
async function run(client, args) {
|
|
7
|
+
const o = args.opts;
|
|
8
|
+
const params = new URLSearchParams();
|
|
9
|
+
if (o.page) params.set('page', o.page);
|
|
10
|
+
if (o.limit) params.set('limit', o.limit);
|
|
11
|
+
if (o.sort) params.set('sort', o.sort);
|
|
12
|
+
if (o.order) params.set('order', o.order);
|
|
13
|
+
if (o.kw || o.keyword) params.set('keyword', o.kw || o.keyword);
|
|
14
|
+
if (o.cat || o.category) params.set('category', o.cat || o.category);
|
|
15
|
+
if (o.tag) params.set('tag', o.tag);
|
|
16
|
+
const qs = params.toString();
|
|
17
|
+
|
|
18
|
+
const data = await client.get('/api/files' + (qs ? '?' + qs : ''));
|
|
19
|
+
const files = data.files || [];
|
|
20
|
+
const pg = data.pagination || {};
|
|
21
|
+
|
|
22
|
+
if (files.length === 0) {
|
|
23
|
+
out('(无文件)\n');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 对齐表格:id / 类型 / 公开 / 大小 / 更新时间 / 文件名 / 标签
|
|
28
|
+
for (const f of files) {
|
|
29
|
+
const pub = f.is_public ? 'pub' : 'pri';
|
|
30
|
+
const tags = (f.tags || []).map((t) => t.name).join(',');
|
|
31
|
+
const bundle = f.is_bundle ? ' 📦' : '';
|
|
32
|
+
out(
|
|
33
|
+
`#${f.id} [${f.file_type || '?'} ${pub}] ${formatSize(f.size).padEnd(7)} ` +
|
|
34
|
+
`${formatTime(f.updated_at)} ${f.original_name}${bundle}` +
|
|
35
|
+
(tags ? ` {${tags}}` : '') +
|
|
36
|
+
'\n'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
out(
|
|
40
|
+
`\n第 ${pg.page || 1} / ${pg.totalPages || 1} 页,共 ${pg.total || files.length} 个\n`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { run };
|