7.0 KiB
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$<base64url-salt>$<base64url-key>。要求密码至少 16 字符;环境文件权限应为 0600,只由管理员访问。Compose 的 env_file 中将哈希值包在单引号内,避免 $ 被插值。
使用自己的凭据配置环境后,运行 npm start。本地 HTTP 预览可以显式设置 COOKIE_SECURE=false,同时将 PUBLIC_ORIGIN 指向准确的 http://localhost:<port> 或 http://127.0.0.1:<port>。该开关拒绝非回环地址;生产环境必须保留默认安全 Cookie。
认证会话有效期 4 小时。退出登录会立即撤销当前 Cookie;服务重启会撤销所有会话。更换密码时更新 PASSWORD_HASH 并重新启动此服务。网站没有在线重设密码入口,也不记录密码、Cookie 或登录请求体。
数据格式
所有金额使用数值或 null。null 表示未知,不应以 0 代替。不同币种、现金支出、固定套餐分摊和预估费用分开保存;套餐内 Token 用量不能重复计作现金支出。账户总用量不能直接归因到 PawPlay。
config.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。
evolution.json 保存演变路径,登录后在「演变路径」中展开浏览。缺失文件显示空状态;事件按文件中的顺序展示,建议按发生顺序维护。每条事件包含 id、recorded_at、可选 occurred_at、occurred_at_precision、retrospective、trigger、hypothesis、change、evidence、cost(known 和 unknown)、decision、next_step 与 artifact_refs。证据和关联资料可使用可读字符串数组,未知信息应保留而非猜测。写入时应使用临时文件原子替换。
ledger.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:
{
"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 与反向代理
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 均在服务端校验会话。报告不会作为静态资源提供。登录静态页面可匿名访问,不能从中读取项目运营数据。登录按连接来源限速;反向代理下来源可能合并,因此单人运营站点优先使用当前的保守限制。
检查
npm test
覆盖:缺配置时拒绝启动、密码散列、匿名隔离、登录与注销撤销、会话篡改、Secure Cookie、来源限制、登录限速、路径穿越和符号链接防护、异常请求体及金额空值保留。
每日计划与复盘
登录后选择「每日计划与复盘」,只读查看 DATA_DIR/daily/YYYY-MM-DD.json,日期倒序。每个文件为 JSON 对象,date 须与文件名一致,planned、executed、evidence、costs、review、tomorrow 为字符串数组,可省略表示暂无记录;金额未知应明确写待核实,不能默认为零。示例是结构说明,不代表实际工作:
{"date":"2026-11-03","planned":["示例计划"],"executed":[],"evidence":[],"costs":["待核实"],"review":[],"tomorrow":[]}
仅日期文件名参与读取;无效日期、JSON、字段类型或越过数据目录的符号链接将跳过并在页面显示数量。渲染使用文本节点,网页不提供写入或命令执行。每日 Markdown 记录不会自动变成 JSON,须由运营记录流程生成对应 JSON;更新需通过原有挂载数据目录,部署需单独执行。