# PawPlay 私有运营台 周报、预算和收支台账的只读网站。无前端构建、无第三方运行依赖,Node.js 22 及以上可直接运行。数据目录与代码、镜像分离;上传视频、采集数据和生成周报由独立任务完成。 ## 配置 必需环境变量: | 名称 | 内容 | | --- | --- | | `USERNAME` | 管理员账号,无默认值 | | `PASSWORD_HASH` | 下述脚本生成的 scrypt 哈希,无默认密码 | | `SESSION_SECRET` | 随机生成、至少 32 字符的独立会话密钥 | | `PUBLIC_ORIGIN` | 对外 HTTPS 源,例如 `https://pawplay-ops.example.com`,不带路径或结尾斜杠 | `DATA_DIR` 默认 `./data`,容器内默认 `/data`。`PORT` 默认 `8080`,监听 `0.0.0.0`。 密码通过标准输入传给 `node scripts/hash-password.mjs`,不要放进命令参数、Git 或日志。该脚本输出 `scrypt$16384$8$1$$`。要求密码至少 16 字符;环境文件权限应为 `0600`,只由管理员访问。Compose 的 `env_file` 中将哈希值包在单引号内,避免 `$` 被插值。 使用自己的凭据配置环境后,运行 `npm start`。本地 HTTP 预览可以显式设置 `COOKIE_SECURE=false`,同时将 `PUBLIC_ORIGIN` 指向准确的 `http://localhost:` 或 `http://127.0.0.1:`。该开关拒绝非回环地址;生产环境必须保留默认安全 Cookie。 认证会话有效期 4 小时。退出登录会立即撤销当前 Cookie;服务重启会撤销所有会话。更换密码时更新 `PASSWORD_HASH` 并重新启动此服务。网站没有在线重设密码入口,也不记录密码、Cookie 或登录请求体。 ## 数据格式 所有金额使用数值或 `null`。`null` 表示未知,不应以 0 代替。不同币种、现金支出、固定套餐分摊和预估费用分开保存;套餐内 Token 用量不能重复计作现金支出。账户总用量不能直接归因到 PawPlay。 `config.json`: ```json { "title": "PawPlay 运营台", "timezone": "Asia/Shanghai", "currency": "USD", "budget": { "subscription_usd": 200, "billing_period": "pending", "project_allocation_fraction": null, "extra_api_weekly_limit_usd": null }, "links": {"product": "", "repository": ""} } ``` `billing_period` 支持 `monthly`、`weekly`、`pending`;项目分摊比例范围由生成任务校验,未确认时使用 `null`。 `ledger.json`: ```json { "entries": [ { "id": "cost-001", "date": "2026-11-03", "label": "本周套餐分摊", "category": "subscription", "amount": null, "currency": "USD", "basis": "allocated", "source": "计费周期和项目占比待确认" } ] } ``` `basis` 支持 `cash`、`allocated`、`estimate`、`settled`。支持额外元数据,例如 `project`、`status`、`usage`;网站不据此重新计算金额。 `reports/2026-W45.json`: ```json { "id": "2026-W45", "title": "第 45 周运营报告", "period_start": "2026-10-30T20:00:00+08:00", "period_end": "2026-11-06T20:00:00+08:00", "published_at": "2026-11-06T20:00:00+08:00", "summary": "本周进展摘要。", "highlights": ["完成的工作"], "risks": ["需要关注的问题"], "next_actions": ["下一周计划"], "decisions": ["已确认的决策"], "metrics": {"views": null, "downloads": {"value": null, "source": "尚未接入采集"}}, "costs": {"cash_usd": null, "allocated_usd": null, "estimated_usd": null}, "revenue": {"settled_cny": null, "estimated_cny": null} } ``` 文件名必须与 `id` 一致。编号只接受字母、数字、下划线和连字符,长度不超过 80 字符。页面按 `period_end` 倒序展示;`metrics` 支持嵌套对象,页面以可读字段呈现。周报 JSON 可在登录后下载。报告生成任务应使用临时文件写完后原子替换,避免读到半份 JSON。 `period_start` 和 `period_end` 可使用 ISO 日期时间,页面统一以北京时间显示。当前未结束周期的快照添加 `"kind": "snapshot"`,页面会注明“截至当前的部分周期”。 数据中不要放密码、Token、浏览器会话或签名密钥。文件下载受同样的认证保护。缺失文件按待核实展示;损坏 JSON 返回读取失败,不会静默按零处理。 ## Docker 与反向代理 ```sh docker build -t pawplay-ops . ``` 镜像内使用非 root 的 `node` 用户,内部端口 `8080`。生产 Compose 应将私有环境文件通过 `env_file` 注入,将报告目录挂载为 `/data:ro`,并确保该目录对容器的 UID 1000 可读。环境文件不挂载到 `/data`,不复制到镜像。该服务不需要写入报告目录;可设置只读根文件系统、`cap_drop: [ALL]` 和 `no-new-privileges`。 Nginx Proxy Manager 终止 HTTPS,转发到所选 NAS 端口。`PUBLIC_ORIGIN` 必须与用户访问的域名一致。反向代理无需读取报告内容或额外添加认证旁路。`GET /healthz` 可在 NAS 内网使用 HTTP 检查,只返回 `{"ok":true}`;它不代表报告采集已完成。 报告、台账以及配置的所有 API 均在服务端校验会话。报告不会作为静态资源提供。登录静态页面可匿名访问,不能从中读取项目运营数据。登录按连接来源限速;反向代理下来源可能合并,因此单人运营站点优先使用当前的保守限制。 ## 检查 ```sh npm test ``` 覆盖:缺配置时拒绝启动、密码散列、匿名隔离、登录与注销撤销、会话篡改、Secure Cookie、来源限制、登录限速、路径穿越和符号链接防护、异常请求体及金额空值保留。