Singer Market 歌手市场
The Singer Market is a self-hosted file-sharing backend (project name singer-files) plus an in-app Electron window that proxies to it. The backend is a single Cloudflare Worker backed by D1 (metadata), KV (rate-limit + token cache), and Backblaze B2 (object storage). The frontend is src/singerMarket.js + src/singerMarket.html; all HTTP is funneled through src/main/singerMarketIpc.js in the Electron main process.
歌手市场 由自托管的文件分享后端(项目名 singer-files)和应用内 Electron 窗口共同组成。后端是单个 Cloudflare Worker,由 D1(元数据)、KV(限流 + 令牌缓存)和 Backblaze B2(对象存储)支撑。前端为 src/singerMarket.js + src/singerMarket.html;所有 HTTP 流量都经由 Electron 主进程中的 src/main/singerMarketIpc.js 转发。
For end-user usage, see the User Guide — Singer Market. This page covers the architecture, the IPC proxy contract, the REST API the proxy calls, and how to self-host or extend the backend.
面向最终用户的使用说明见 用户指南 — 歌手市场。本页涵盖架构、IPC 代理契约、代理所调用的 REST API,以及如何自托管或扩展后端。
Architecture at a Glance 架构概览
┌────────────────────────────────────────────────────────────────┐
│ SXSEditor (Electron) │
│ │
│ singerMarket window (renderer) ◄──► singerMarketIpc (main) │
│ src/singerMarket.js + .html src/main/singerMarketIpc.js│
│ - toolbar / grid / dialogs - token persistence │
│ - window.electronAPI.singerMarket.* - multipart builder │
│ - never touches the network - Bearer header injection │
│ - native file pickers │
└────────────────────────────────┬───────────────────────────────┘
│ HTTPS (Node https module)
▼
┌────────────────────────────────────────────────────────────────┐
│ Cloudflare Worker (singer-files) │
│ https://singer-files.<acct>.workers.dev │
│ - router.js - lib/auth.js (sessions + API keys + rate) │
│ - lib/b2.js - lib/storage.js (B2 adapter) │
│ - lib/db.js - lib/quota.js (circuit breaker) │
│ - handlers/* - ui_user.js / ui_admin.js (inline SPAs) │
└──────────────┬─────────────────────┬───────────────────┬────────┘
│ │ │
▼ ▼ ▼
┌────────────┐ ┌──────────┐ ┌──────────┐
│ Backblaze │ │ D1 DB │ │ KV │
│ B2 bucket │ │singer_ │ │ rate- │
│singer-files│ │ files │ │ limit + │
│ (blobs) │ │ (8 tables)│ │ B2 cache │
└────────────┘ └──────────┘ └──────────┘
| Layer层 | Tech技术 | Holds承载内容 |
|---|---|---|
| Edge compute边缘计算 | Cloudflare WorkersCloudflare Workers | Routing, auth, rate limit, business logic, inline SPAs.路由、鉴权、限流、业务逻辑、内嵌 SPA。 |
| Blob storage对象存储 | Backblaze B2 (singer-files bucket)Backblaze B2(singer-files 桶) |
Raw file bytes (auto-created on first upload).原始文件字节(首次上传时自动建桶)。 |
| Metadata DB元数据数据库 | Cloudflare D1 (SQLite)Cloudflare D1(SQLite) | 8 tables: files, tags, file_tags, users, sessions, api_keys, audit_log, schema_meta.8 张表:files、tags、file_tags、users、sessions、api_keys、audit_log、schema_meta。 |
| Rate limit + cache限流与缓存 | Cloudflare KV (SINGER_FILES_KV)Cloudflare KV(SINGER_FILES_KV) |
Per-IP-per-minute counters; cached B2 auth/upload tokens.每 IP 每分钟计数器;B2 鉴权/上传令牌缓存。 |
| In-app client应用内客户端 | Electron window + main-process IPC proxyElectron 窗口 + 主进程 IPC 代理 | UI; token persistence; multipart building; native file pickers.UI;令牌持久化;multipart 构造;原生文件选择。 |
Why B2 over R2? B2's free tier gives 10 GB storage + 1 GB/day download, and through the Cloudflare Bandwidth Alliance the B2 → Workers egress leg is free. Workers → user egress is the only metered leg. The Worker streams uploads/downloads through itself so it can enforce auth, audit, and Content-Disposition.
为何选 B2 而非 R2?B2 免费层提供 10 GB 存储 + 每天 1 GB 下载,且通过 Cloudflare Bandwidth Alliance,B2 → Workers 的出口流量免费。只有 Workers → 用户这一段才计费。Worker 把上传/下载都流经自身,以便实施鉴权、审计与 Content-Disposition。
The Electron IPC Proxy Electron IPC 代理
All HTTP traffic from the Singer Market window goes through src/main/singerMarketIpc.js rather than fetch in the renderer. This is deliberate:
歌手市场窗口的所有 HTTP 流量都经由 src/main/singerMarketIpc.js,而非渲染进程中的 fetch。这是有意为之:
- CSP stays tight — the renderer CSP keeps
connect-src 'self'; no need to widen it to the Worker URL.保持 CSP 严格— 渲染进程 CSP 仍为connect-src 'self',无需放宽到 Worker URL。 - Token never enters renderer JS — the Bearer token is held by the main process; the renderer only sees success/error + parsed JSON.令牌不进入渲染进程 JS— Bearer 令牌由主进程持有;渲染进程只能看到成功/失败与解析后的 JSON。
- Streaming uploads — large
.sxssingerfiles are read withfs.promises.readFilein the main process and sent as a multipart body, so the renderer never holds the full buffer.流式上传— 大型.sxssinger文件由主进程用fs.promises.readFile读取并以 multipart body 发送,渲染进程无需持有完整 buffer。
IPC channelsIPC 通道
All channels are prefixed singer-market: and exposed on window.electronAPI.singerMarket via the preload bridge. Every handler resolves to { success: boolean, ...payload } or { success: false, error: string }.
所有通道都以 singer-market: 为前缀,并通过 preload 桥接暴露在 window.electronAPI.singerMarket 上。每个处理器返回 { success: boolean, ...payload } 或 { success: false, error: string }。
| IPC channelIPC 通道 | Backend call后端调用 | Notes说明 |
|---|---|---|
singer-market:register |
POST /api/auth/register |
Auto-logs-in; persists token + user.自动登录;持久化令牌与用户信息。 |
singer-market:login |
POST /api/auth/login |
Persists { token, user } to userData/singer-market-token.json.将 { token, user } 持久化到 userData/singer-market-token.json。 |
singer-market:logout |
POST /api/auth/logout |
Best-effort server revoke; local session file is always deleted.尽力撤销服务端会话;本地会话文件总会被删除。 |
singer-market:me |
GET /api/auth/me |
Refreshes cached user; clears session on 401.刷新缓存的用户信息;401 时清除会话。 |
singer-market:list |
GET /api/files?visibility=public&... |
Forces visibility=public; passes page, limit, q, tags, tag_mode.强制 visibility=public;透传 page、limit、q、tags、tag_mode。 |
singer-market:file-detail |
GET /api/files/:id |
Adds auth header so private files owned by the user are visible.附带鉴权头,使本人私有的文件可见。 |
singer-market:tags |
GET /api/tags?... |
Forwards q, suggest, exact, limit.透传 q、suggest、exact、limit。 |
singer-market:upload |
POST /api/files (multipart) |
Reads file from disk in main process; builds multipart with buildMultipart(); requires auth.在主进程从磁盘读取文件;用 buildMultipart() 构造 multipart;需鉴权。 |
singer-market:download |
GET /api/files/:id/download |
Binary response (encoding: null); returns { buffer, filename, contentType }.二进制响应(encoding: null);返回 { buffer, filename, contentType }。 |
singer-market:pick-file |
— (native dialog)—(原生对话框) | dialog.showOpenDialog filtered to .sxssinger; authorizes path via security.authorizePath.dialog.showOpenDialog 限定 .sxssinger;通过 security.authorizePath 授权路径。 |
singer-market:pick-save-path |
— (native dialog)—(原生对话框) | dialog.showSaveDialog with the original filename as default; authorizes path.dialog.showSaveDialog,默认文件名为原文件名;授权路径。 |
singer-market:health |
GET /health |
Liveness probe (db / b2 / kv binding status).存活探针(db / b2 / kv 绑定状态)。 |
Session persistence会话持久化
The proxy persists the session to userData/singer-market-token.json with mode 0o600 on POSIX (mode is ignored on Windows). The file shape is { token: "sfu_...", user: { id, username, is_admin } }. The token format sfu_ denotes a user session token (vs sf_ for admin API keys).
代理将会话持久化到 userData/singer-market-token.json,POSIX 上权限为 0o600(Windows 忽略权限)。文件结构为 { token: "sfu_...", user: { id, username, is_admin } }。sfu_ 前缀表示用户会话令牌(区别于 sf_ 管理员 API key)。
// singerMarketIpc.js — token persistence helpers
function loadSession() { /* read userData/singer-market-token.json */ }
function saveSession(s) { /* write with mode 0o600 */ }
function clearSession() { /* unlink the file */ }
function getToken() { return loadSession()?.token ?? null; }
function withAuth(h) { /* injects Authorization: Bearer <token> */ }
Multipart builderMultipart 构造
buildMultipart(fields, file) constructs a multipart/form-data body as a single Buffer, using a randomly-generated boundary. The file argument is { filename, data: Buffer, contentType? }. This is intentionally dependency-free — the Worker accepts standard multipart, so we don't pull in form-data.
buildMultipart(fields, file) 将 multipart/form-data body 构造为单个 Buffer,使用随机生成的 boundary。file 参数为 { filename, data: Buffer, contentType? }。这里刻意不引入依赖——Worker 接受标准 multipart,因此无需引入 form-data。
Backend REST API 后端 REST API
Base URL: https://singer-files.<acct>.workers.dev. The Worker exposes three credential kinds, tried in order: user session token (sfu_…), admin API key (sf_…), bootstrap admin key (the ADMIN_BOOTSTRAP_API_KEY secret). All are sent as Authorization: Bearer <token>.
Base URL:https://singer-files.<acct>.workers.dev。Worker 接受三类凭据,按顺序尝试:用户会话令牌(sfu_…)、管理员 API key(sf_…)、引导管理员 key(ADMIN_BOOTSTRAP_API_KEY secret)。均以 Authorization: Bearer <token> 发送。
Endpoints used by the in-app client应用内客户端使用的端点
| Method & path方法与路径 | Auth鉴权 | Purpose用途 |
|---|---|---|
POST /api/auth/register |
none (rate-limited)无(限流) | Create user; auto-login; returns { user, token, expires_at }.创建用户;自动登录;返回 { user, token, expires_at }。 |
POST /api/auth/login |
none (rate-limited)无(限流) | Login; returns { user, token, expires_at, token_type: "Bearer" }.登录;返回 { user, token, expires_at, token_type: "Bearer" }。 |
POST /api/auth/logout |
useruser | Revoke current session. Idempotent.撤销当前会话。幂等。 |
GET /api/auth/me |
useruser | Current user + session expiry.当前用户与会话过期时间。 |
GET /api/files |
optional可选 | List files. Query: page, size, q, tags (csv), tag_mode (and|or), visibility, mine=1, sort, order.列出文件。查询参数:page、size、q、tags(逗号分隔)、tag_mode(and|or)、visibility、mine=1、sort、order。 |
GET /api/files/:id |
optional可选 | File metadata (by id or slug). Private files require owner/admin auth.文件元数据(按 id 或 slug)。私有文件需 owner/admin 鉴权。 |
POST /api/files |
user (or write key)user(或 write key) | Upload. Multipart (file, description, tags, visibility, ...) or raw body with ?filename=.上传。Multipart(file、description、tags、visibility 等)或带 ?filename= 的原始 body。 |
GET /api/files/:id/download |
optional可选 | Stream file bytes. ?inline=1 renders in-browser. Sets Content-Disposition.流式返回文件字节。?inline=1 浏览器内渲染。设置 Content-Disposition。 |
GET /api/tags |
optional可选 | Tag list / fuzzy search / autocomplete (?suggest=1) / exact check.标签列表 / 模糊搜索 / 自动补全(?suggest=1)/ 精确存在性检查。 |
GET /health |
none无 | Liveness; reports binding status (db / b2 / kv).存活探针;上报绑定状态(db / b2 / kv)。 |
Other endpoints (admin / API-only)其他端点(管理员 / 仅 API)
These are not currently invoked by the in-app window but exist on the backend for admin operations and API-only clients:
应用内窗口当前不调用这些端点,但后端提供它们用于管理员操作与仅 API 客户端:
GET /api/auth/sessions,DELETE /api/auth/sessions/:prefix— list / revoke the caller's sessions.GET /api/auth/sessions、DELETE /api/auth/sessions/:prefix— 列出 / 撤销调用者的会话。PATCH /api/files/:id,DELETE /api/files/:id— update metadata / soft-delete (owner or admin).PATCH /api/files/:id、DELETE /api/files/:id— 更新元数据 / 软删除(owner 或 admin)。POST/PUT/DELETE /api/files/:id/tags— add / replace / remove tags.POST/PUT/DELETE /api/files/:id/tags— 添加 / 替换 / 移除标签。GET /api/tags/:name/files— files for a given tag.GET /api/tags/:name/files— 指定标签下的文件。POST /api/bulk/delete,/api/bulk/tag,/api/bulk/untag— owner-scoped bulk ops (max 100 ids).POST /api/bulk/delete、/api/bulk/tag、/api/bulk/untag— owner 范围的批量操作(最多 100 个 id)。GET/POST /api/users,GET/PATCH/DELETE /api/users/:id— user CRUD (admin).GET/POST /api/users、GET/PATCH/DELETE /api/users/:id— 用户 CRUD(admin)。GET/POST /api/keys,GET/DELETE /api/keys/:id— API key management (admin).GET/POST /api/keys、GET/DELETE /api/keys/:id— API key 管理(admin)。GET /api/admin/usage— quota + circuit-breaker dashboard (admin).GET /api/admin/usage— 配额 + 熔断器仪表盘(admin)。GET /api/audit— audit log (admin).GET /api/audit— 审计日志(admin)。GET /f/:slug— short URL download (same as/api/files/:id/downloadby slug).GET /f/:slug— 短链下载(按 slug 等同于/api/files/:id/download)。
Errors are JSON: { "error": { "code", "message", "required_scope"? } }. Common codes map to bad_request (400), unauthorized (401), forbidden (403), not_found (404), payload_too_large (413), too_many_requests (429), quota_exceeded (429/503), internal_error (500).
错误为 JSON:{ "error": { "code", "message", "required_scope"? } }。常见 code 对应:bad_request (400)、unauthorized (401)、forbidden (403)、not_found (404)、payload_too_large (413)、too_many_requests (429)、quota_exceeded (429/503)、internal_error (500)。
Authentication & Permission Model 鉴权与权限模型
Principals, in priority order:
主体(按优先级排序):
| Principal主体 | Can do可执行操作 |
|---|---|
| Anonymous匿名 | Read public files & lists (when ALLOW_PUBLIC_READ=true). Upload only if ALLOW_ANONYMOUS_UPLOAD=true (off by default).读取公开文件与列表(ALLOW_PUBLIC_READ=true 时)。仅当 ALLOW_ANONYMOUS_UPLOAD=true(默认关闭)时可上传。 |
| Logged-in user登录用户 | Read everything. Upload. Manage own files only (metadata, tags, delete). Cannot see other users' private files.读取全部内容。上传。仅管理自己的文件(元数据、标签、删除)。无法查看他人私有文件。 |
| Admin user管理员用户 | Everything a user can do, plus manage all files, users, API keys, audit log, quota dashboard.用户能做的全部,外加管理所有文件、用户、API key、审计日志、配额仪表盘。 |
| API key (read / write / admin)API key(read / write / admin) | read: read all files (incl. private). write: + upload/update/delete on all files (global writer). admin: + manage keys & audit log.read:读取所有文件(含私有)。write:+ 对所有文件上传/更新/删除(全局写者)。admin:+ 管理 key 与审计日志。 |
Every file records user_id (FK to users.id, ON DELETE SET NULL) and uploaded_by (label like user:alice, key:abc123, or anonymous). Modify/delete (PATCH/DELETE, tag ops) requires: owner match, OR admin, OR write-scoped API key. Public files are read-only to everyone except the owner/admin.
每个文件记录 user_id(外键到 users.id,ON DELETE SET NULL)与 uploaded_by(标签如 user:alice、key:abc123 或 anonymous)。修改/删除(PATCH/DELETE、标签操作)需满足:owner 匹配,或 admin,或 write 范围 API key。公开文件对除 owner/admin 外的所有人只读。
Data Model 数据模型
D1 schema (8 tables). Migrations live in migrations/ on the singer-files backend repo:
D1 schema(8 张表)。迁移脚本位于 singer-files 后端仓库的 migrations/:
files id, slug, original_name, storage_key, mime_type, size,
sha256, description, visibility, uploaded_by,
user_id (FK→users.id, ON DELETE SET NULL),
created_at, updated_at, expires_at, download_count,
is_deleted, meta (JSON)
tags id, name (case-insensitive unique), created_at
file_tags file_id, tag_id (many-to-many)
users id, username (unique, case-insensitive),
password_hash (pbkdf2:sha256:...), display_name,
is_admin, is_active, created_at, last_login_at
sessions token (PK, 'sfu_<32>'), user_id (FK→users.id, CASCADE),
created_at, expires_at, last_used_at, user_agent, ip
api_keys id, key_hash (SHA-256), key_prefix, label, scopes,
uploaded_by, created_at, last_used_at, is_active
audit_log id, action, resource_type, resource_id, actor, ip,
user_agent, details (JSON), created_at
schema_meta key, value (version tracking)
B2 storage layout: each file is stored at <file-uuid>/<sanitized-filename> in the singer-files bucket. B2 object X-Bz-Info-* metadata carries original_name, uploaded_by, uploaded_at; Content-Type carries the declared MIME. The bucket is created as allPublic, but downloads go through the Worker so it can enforce auth, audit, and content-disposition.
B2 存储布局:每个文件存于 singer-files 桶的 <file-uuid>/<sanitized-filename> 路径。B2 对象的 X-Bz-Info-* 元数据携带 original_name、uploaded_by、uploaded_at;Content-Type 携带声明的 MIME。桶以 allPublic 创建,但下载仍走 Worker,以便实施鉴权、审计与 content-disposition。
Rate Limiting & Circuit Breaker 速率限制与熔断器
Per-IP-per-minute counters in KV, keyed rl:<bucket>:<ip>:<minute>:
KV 中按 IP 每分钟计数,键为 rl:<bucket>:<ip>:<minute>:
- Read (
RATE_LIMIT_READ, default 600/min) — all GET endpoints. - 读(
RATE_LIMIT_READ,默认 600/分钟)— 所有 GET 端点。 - Write (
RATE_LIMIT_WRITE, default 120/min) — all POST/PUT/PATCH/DELETE. - 写(
RATE_LIMIT_WRITE,默认 120/分钟)— 所有 POST/PUT/PATCH/DELETE。 - Auth (
RATE_LIMIT_AUTH, default 60/min) — login/register attempts. - 登录(
RATE_LIMIT_AUTH,默认 60/分钟)— 登录/注册尝试。
KV is eventually consistent across edges, so the limiter is best-effort; if KV is unavailable it fails open. Over-limit responses are 429 too_many_requests with a Retry-After header.
KV 在各边缘节点间为最终一致,因此限流为尽力而为;KV 不可用时失败开放(请求仍放行)。超限响应为 429 too_many_requests 并带 Retry-After 头。
On top of rate limiting, a daily circuit breaker refuses new write operations when any counter hits its cap. Counters reset at UTC midnight.
在限流之上还有每日 熔断器,当任一计数器达到上限时拒绝新的写操作。计数器在 UTC 午夜重置。
| Check检查项 | Default cap默认上限 | Tripped behavior触发后行为 |
|---|---|---|
storage |
9.8 GB (CB_STORAGE_LIMIT_BYTES)9.8 GB(CB_STORAGE_LIMIT_BYTES) |
New upload would exceed → 503 quota_exceeded (storage_limit).新上传会超限 → 503 quota_exceeded(storage_limit)。 |
class_a |
2,450 / day (CB_CLASS_A_LIMIT)每天 2,450(CB_CLASS_A_LIMIT) |
B2 Class A transactions (uploads + deletes) → 429 (class_a_limit).B2 Class A 事务(上传 + 删除)→ 429(class_a_limit)。 |
d1_writes |
95,000 / day (CB_D1_WRITES_LIMIT)每天 95,000(CB_D1_WRITES_LIMIT) |
D1 write operations → 429 (d1_writes_limit).D1 写操作 → 429(d1_writes_limit)。 |
workers_req |
95,000 / day (CB_WORKERS_REQ_LIMIT)每天 95,000(CB_WORKERS_REQ_LIMIT) |
All Worker invocations → 429 (workers_req_limit); service effectively offline.所有 Worker 调用 → 429(workers_req_limit);服务事实上离线。 |
class_b |
2,450 / day (CB_CLASS_B_LIMIT)每天 2,450(CB_CLASS_B_LIMIT) |
B2 Class B transactions (downloads + lists). Informational; not used to block reads.B2 Class B 事务(下载 + 列表)。仅信息展示,不阻断读。 |
Set any cap to 0 to disable that check. The full state is exposed at GET /api/admin/usage under circuit_breaker.
将任一上限设为 0 即可禁用该项检查。完整状态在 GET /api/admin/usage 的 circuit_breaker 字段中暴露。
Configuration Reference 配置参考
Non-secret config lives in wrangler.toml under [vars]; secrets are set via npx wrangler secret put NAME. Key knobs:
非敏感配置位于 wrangler.toml 的 [vars];secret 通过 npx wrangler secret put NAME 设置。关键项:
| Variable变量 | Default默认 | Description说明 |
|---|---|---|
ALLOW_PUBLIC_READ |
true |
Anonymous can read public files & lists.匿名可读公开文件与列表。 |
ALLOW_USER_REGISTRATION |
true |
Self-register at /api/auth/register. Set false to require admin-created accounts.允许在 /api/auth/register 自助注册。设 false 则需管理员创建账号。 |
ALLOW_ANONYMOUS_UPLOAD |
false |
Anonymous uploads without login. Off by default — uploads must be owned.允许匿名上传。默认关闭 — 上传必须归属某用户。 |
MAX_UPLOAD_BYTES |
104857600 (100 MB) |
Hard upload size cap (Workers free-tier ceiling).上传大小硬上限(Workers 免费层上限)。 |
ALLOWED_MIMES |
* |
Comma-separated allow-list, or * for any.逗号分隔的白名单,或 * 表示任意。 |
DEFAULT_VISIBILITY |
public |
Default visibility for new uploads (public / private).新上传的默认可见性(public / private)。 |
SESSION_TTL_HOURS |
720 (30 days) |
User login session lifetime.用户登录会话生命周期。 |
CB_* |
see table above见上表 | Circuit-breaker caps. 0 disables a check.熔断器上限。0 禁用该项检查。 |
Secrets: B2_KEY_ID, B2_APP_KEY, B2_BUCKET_NAME, ADMIN_BOOTSTRAP_API_KEY (delete after creating a permanent admin user).
Secret:B2_KEY_ID、B2_APP_KEY、B2_BUCKET_NAME、ADMIN_BOOTSTRAP_API_KEY(创建永久管理员账号后应删除)。
Self-Hosting 自托管
The singer-files backend is its own repository (not part of the SXSEditor repo). To run your own instance:
singer-files 后端是独立仓库(不属于 SXSEditor 仓库)。自托管步骤:
- Cloudflare credentials — create an API token with Workers Scripts / D1 / KV Storage Edit permissions. Export
CLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_ID.Cloudflare 凭据— 创建带 Workers Scripts / D1 / KV Storage Edit 权限的 API token。导出CLOUDFLARE_API_TOKEN与CLOUDFLARE_ACCOUNT_ID。 - B2 application key — at Backblaze, create a key with
readFiles,writeFiles,deleteFiles,listBuckets,listFiles,readBuckets,writeBuckets. Copy thekeyIDandapplicationKey(shown once).B2 application key— 在 Backblaze 创建含readFiles、writeFiles、deleteFiles、listBuckets、listFiles、readBuckets、writeBuckets的 key。复制keyID与applicationKey(仅显示一次)。 - Run setup —
npm install && npm run setup. The script creates the D1 DB + KV namespaces (idempotent), patcheswrangler.toml, applies migrations, stores the B2 secrets and generates a bootstrap admin key (sf_…).执行 setup—npm install && npm run setup。脚本会创建 D1 数据库 + KV 命名空间(幂等)、回写wrangler.toml、应用迁移、存储 B2 secret 并生成引导管理员 key(sf_…)。 - Deploy —
npm run deploy. The B2 bucket is auto-created on the first upload.部署—npm run deploy。B2 桶在首次上传时自动创建。 - Create a permanent admin — open
/admin, paste the bootstrap key, create an admin user. Thennpx wrangler secret delete ADMIN_BOOTSTRAP_API_KEY.创建永久管理员— 打开/admin,粘贴引导 key,创建管理员账号。然后npx wrangler secret delete ADMIN_BOOTSTRAP_API_KEY。
Pointing the SXSEditor client at your instance让 SXSEditor 客户端指向你的实例
The in-app client hard-codes the backend URL in src/main/singerMarketIpc.js:
应用内客户端在后端 URL 在 src/main/singerMarketIpc.js 中硬编码:
const API_BASE = 'https://singer-files.15240287482.workers.dev';
To use your own backend, fork the SXSEditor repo, change this constant, rebuild (npm run package), and distribute the resulting build. There is no UI setting for this — it's a build-time constant by design (it keeps the default UX zero-config for end users).
若要使用自己的后端,请 fork SXSEditor 仓库,修改此常量,重新打包(npm run package),分发构建产物。此处没有 UI 设置——刻意做成构建时常量,让默认 UX 对终端用户零配置。
Failure Modes 故障模式
| Scenario场景 | Behavior行为 |
|---|---|
| B2 not configuredB2 未配置 | /health → degraded. Upload/download → 503 service_unavailable. Other endpoints work./health → degraded。上传/下载 → 503 service_unavailable。其他端点正常。 |
| B2 bucket missingB2 桶缺失 | Auto-created as allPublic on first upload. No manual step.首次上传时自动创建为 allPublic。无需手动操作。 |
| B2 object missing (manually deleted)B2 对象缺失(被手动删除) | Download → 404 with audit-log entry file.missing_blob.下载 → 404,审计日志记录 file.missing_blob。 |
| B2 auth token expired (24h)B2 鉴权令牌过期(24 小时) | Auto-refreshed from KV; on a 401, retried once.从 KV 自动刷新;遇 401 时重试一次。 |
| D1 unavailableD1 不可用 | All DB-backed endpoints → 500. /health → unhealthy.所有依赖 DB 的端点 → 500。/health → unhealthy。 |
| KV unavailableKV 不可用 | Rate limiting is skipped (fail-open). B2 tokens re-fetched per call (slower, still correct).限流跳过(失败开放)。B2 令牌每次调用重新获取(更慢但仍然正确)。 |
| Invalid/expired session token无效/过期会话令牌 | 401 unauthorized. The IPC proxy clears the local session on 401 from /api/auth/me.401 unauthorized。IPC 代理在 /api/auth/me 返回 401 时清除本地会话。 |
| Valid token, wrong owner令牌有效但非 owner | 403 forbidden with required_scope: admin or owner.403 forbidden,附 required_scope: admin 或 owner。 |
| DB write fails after B2 uploadB2 上传后 DB 写入失败 | B2 object is asynchronously deleted (rollback).B2 对象被异步删除(回滚)。 |
| Last-admin demotion/deletion降级/删除最后一位管理员 | 409 conflict — cannot demote or delete the last remaining admin.409 conflict— 不能降级或删除最后一位管理员。 |
| Password change修改密码 | All the user's sessions are revoked; they must re-login.该用户所有会话被撤销;需重新登录。 |
All write operations emit audit-log entries (file.upload, file.update, file.delete, file.tag, file.download, api_key.create, api_key.revoke, user.create, user.update, user.delete, user.login, user.logout, session.revoke, bulk.delete, bulk.tag, bulk.untag).
所有写操作都会写审计日志(file.upload、file.update、file.delete、file.tag、file.download、api_key.create、api_key.revoke、user.create、user.update、user.delete、user.login、user.logout、session.revoke、bulk.delete、bulk.tag、bulk.untag)。
Testing the IPC Proxy 测试 IPC 代理
The proxy is unit-tested in test/singerMarketIpc.test.js. The module exports _internal for white-box testing:
代理的单测在 test/singerMarketIpc.test.js。模块导出 _internal 供白盒测试:
module.exports = {
registerSingerMarketIpc,
_internal: { request, buildMultipart, withAuth,
loadSession, saveSession, clearSession },
};
Run the Singer Market tests in isolation:
单独运行歌手市场测试:
npx mocha --require ./test/setup.js "test/singerMarketIpc.test.js" --timeout 30000
See Testing & CLI for the overall test framework layout.
整体测试框架布局见 测试与命令行。
Extending the Market Window 扩展市场窗口
Common extension points:
常见扩展点:
- Add a new backend call — add an
ipcMain.handle('singer-market:<name>', ...)insingerMarketIpc.js, expose it inpreload.jsundersingerMarket, then callwindow.electronAPI.singerMarket.<name>(...)fromsingerMarket.js. - 新增后端调用— 在
singerMarketIpc.js添加ipcMain.handle('singer-market:<name>', ...),在preload.js的singerMarket下暴露,再在singerMarket.js调用window.electronAPI.singerMarket.<name>(...)。 - Owner edit/delete in-window — the backend already supports
PATCH /api/files/:idandDELETE /api/files/:idfor owners. Add IPC handlers + UI buttons that surface them only whenstate.user.id === singer.user_id. - 在窗口内 owner 编辑/删除— 后端已支持 owner 调用
PATCH /api/files/:id与DELETE /api/files/:id。新增 IPC 处理器 + UI 按钮,仅在state.user.id === singer.user_id时显示。 - Tag autocomplete on upload — the backend exposes
GET /api/tags?suggest=1&q=.... Wire the upload dialog's tag input to it for GitHub-style suggestions. - 上传时标签自动补全— 后端已提供
GET /api/tags?suggest=1&q=...。将上传对话框的标签输入接入即可获得 GitHub 风格建议。 - Switch backend — change
API_BASEinsingerMarketIpc.jsand rebuild. No other code change required. - 切换后端— 修改
singerMarketIpc.js的API_BASE并重新打包。无需其他改动。
When adding new IPC channels, also add a unit test in test/singerMarketIpc.test.js and update src/preload.js. The preload bridge is the security boundary — never bypass it by giving the renderer direct access to ipcRenderer.
新增 IPC 通道时,请在 test/singerMarketIpc.test.js 添加单测,并更新 src/preload.js。preload 桥接是安全边界——切勿让渲染进程直接访问 ipcRenderer。
Free-Tier Budget 免费层预算
Default circuit-breaker caps are intentionally 2–5% below the real free-tier limits to absorb KV's eventual-consistency under-counting:
默认熔断上限刻意比真实免费层限制低 2–5%,以吸收 KV 最终一致导致的少计:
| Resource资源 | Free-tier limit免费层限制 | Circuit-breaker default熔断默认 |
|---|---|---|
| Worker requestsWorker 请求 | 100,000 / day每天 100,000 | CB_WORKERS_REQ_LIMIT=95000 |
| Worker request bodyWorker 请求体 | 100 MB100 MB | MAX_UPLOAD_BYTES=104857600 |
| B2 storageB2 存储 | 10 GB10 GB | CB_STORAGE_LIMIT_BYTES=10522669875 (~9.8 GB) |
| B2 Class A transactionsB2 Class A 事务 | 2,500 / day每天 2,500 | CB_CLASS_A_LIMIT=2450 |
| B2 Class B transactionsB2 Class B 事务 | 2,500 / day每天 2,500 | CB_CLASS_B_LIMIT=2450 |
| D1 writesD1 写入 | 100,000 / day每天 100,000 | CB_D1_WRITES_LIMIT=95000 |
| D1 readsD1 读取 | 5,000,000 / day每天 5,000,000 | —— |
| KV reads / writesKV 读 / 写 | 100,000 / 1,000 per day每天 100,000 / 1,000 | — (if KV writes bottleneck, disable the counters via CB_*_LIMIT=0)—(若 KV 写入成为瓶颈,通过 CB_*_LIMIT=0 禁用计数器) |
If you outgrow the free tier, upgrade to Workers Paid ($5/month) and/or B2 paid tier — no code changes required.
若超出免费层,升级到 Workers Paid($5/月)和/或 B2 付费层即可——无需改代码。