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。这是有意为之:

  1. 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。
  2. 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。
  3. Streaming uploads — large .sxssinger files are read with fs.promises.readFile in 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;透传 pagelimitqtagstag_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.透传 qsuggestexactlimit
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.列出文件。查询参数:pagesizeqtags(逗号分隔)、tag_modeand|or)、visibilitymine=1sortorder
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(filedescriptiontagsvisibility 等)或带 ?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 客户端:

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.idON DELETE SET NULL)与 uploaded_by(标签如 user:alicekey:abc123anonymous)。修改/删除(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_nameuploaded_byuploaded_atContent-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>

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_exceededstorage_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 事务(上传 + 删除)→ 429class_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 写操作 → 429d1_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 调用 → 429workers_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/usagecircuit_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_IDB2_APP_KEYB2_BUCKET_NAMEADMIN_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 仓库)。自托管步骤:

  1. Cloudflare credentials — create an API token with Workers Scripts / D1 / KV Storage Edit permissions. Export CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID.Cloudflare 凭据— 创建带 Workers Scripts / D1 / KV Storage Edit 权限的 API token。导出 CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_ID
  2. B2 application key — at Backblaze, create a key with readFiles, writeFiles, deleteFiles, listBuckets, listFiles, readBuckets, writeBuckets. Copy the keyID and applicationKey (shown once).B2 application key— 在 Backblaze 创建含 readFileswriteFilesdeleteFileslistBucketslistFilesreadBucketswriteBuckets 的 key。复制 keyIDapplicationKey(仅显示一次)。
  3. Run setupnpm install && npm run setup. The script creates the D1 DB + KV namespaces (idempotent), patches wrangler.toml, applies migrations, stores the B2 secrets and generates a bootstrap admin key (sf_…).执行 setupnpm install && npm run setup。脚本会创建 D1 数据库 + KV 命名空间(幂等)、回写 wrangler.toml、应用迁移、存储 B2 secret 并生成引导管理员 key(sf_…)。
  4. Deploynpm run deploy. The B2 bucket is auto-created on the first upload.部署npm run deploy。B2 桶在首次上传时自动创建。
  5. Create a permanent admin — open /admin, paste the bootstrap key, create an admin user. Then npx 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 未配置 /healthdegraded. Upload/download → 503 service_unavailable. Other endpoints work./healthdegraded。上传/下载 → 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. /healthunhealthy.所有依赖 DB 的端点 → 500/healthunhealthy
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: adminowner
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.uploadfile.updatefile.deletefile.tagfile.downloadapi_key.createapi_key.revokeuser.createuser.updateuser.deleteuser.loginuser.logoutsession.revokebulk.deletebulk.tagbulk.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:

常见扩展点:

ℹ️

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 付费层即可——无需改代码。