Files
pawplay/ops-portal/README.md
T
2026-11-03 17:29:10 +08:00

6.1 KiB
Raw Blame History

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、来源限制、登录限速、路径穿越和符号链接防护、异常请求体及金额空值保留。