# 试卷项目台 · PaperFlow Workspace

面向一至两位试卷编辑的轻量项目流程管理工作台。前端使用原生 HTML/CSS/JavaScript，服务端使用 EdgeOne Makers Cloud Functions（Node.js），数据保存在 EdgeOne Makers Blob 中。

## 已包含的功能

- 工作总览：进行中项目、今日待处理节点、逾期节点、完成项目、近期项目与节点
- 项目管理：新建、编辑、删除、搜索、状态/类别/学科筛选、项目备注与交付日期
- 自由流程：任意添加、编辑、删除、上下调整节点；节点可分别设置经手人、计划日期、实际完成日期、状态和处理记录
- 日期日历：查看每个节点的计划日期、完成状态和逾期情况
- 流程模板：创建、编辑、删除模板；从模板创建项目；也可将已有项目流程另存为模板
- 分类与节点字典：可调整分类名称、选项、分类顺序；可新建/删除分类；可调整常用节点的名称与顺序；项目实际填写值不受字典限制
- 操作记录：记录项目、节点、分类、模板和成员方面的主要变化
- 成员管理：不开放公开注册；管理员只能从内部工作台添加成员、重置密码、停用/启用成员
- 数据导出/恢复：完整 JSON 备份、项目 CSV、节点明细 CSV、单个项目 JSON；支持从 JSON 备份恢复项目数据
- 登录保护：服务端密码哈希、HttpOnly 会话 Cookie、会话签名与过期、首次登录强制改密、简单登录失败锁定

## 关于登录账号

首次初始化时默认账号为 **任鹏程**，初始密码为 **123456**（可用环境变量覆盖）。为避免默认密码被他人使用，首次登录会强制修改密码，至少 8 位。初始账号只会在数据存储首次初始化时创建；后来修改环境变量不会覆盖现有账号密码。

> 请不要让公开访问者长期使用默认密码。上线后第一件事就是用默认账号登录并设置自己的密码。管理员添加的成员也必须有独立密码。

## 部署到 EdgeOne Makers

**重要：`pages.edgeone.ai/drop` 的静态预览与完整应用部署不是一回事。** 本项目的 `?demo=1` 模式适合先通过 Drop 上传体验界面，但登录和数据仅保存在当前浏览器。真实使用需要部署 `cloud-functions/` 与 Blob 服务端代码；为了确保 `@edgeone/pages-blob` 依赖正确安装，推荐将完整源码导入 Git 集成的 Makers 项目后部署，而不是把静态预览包当作正式版。

**服务端代码必须放在 `cloud-functions/`（Cloud Functions · Node.js v20），不要用 `edge-functions/`。** 原因：

- Blob 存储只支持 Cloud Functions（Node.js）。Edge Functions 是 V8 运行时，不支持 npm 包，也不会注入 Blob 凭证；在那里调用 `@edgeone/pages-blob` 会抛 `Missing: deployCredential`，页面只看到 500「服务端发生错误」。
- 密码哈希使用 Web Crypto 的 PBKDF2（12 万次迭代，实测约 60ms）。Edge Functions 单次调用 CPU 限额 200ms，容易超限；Cloud Functions 是 120 秒。

要启用真实登录和数据持久化，请把整个项目部署为 EdgeOne Makers 项目，让静态文件和 `cloud-functions/` 一起部署在同一个站点源下：

1. 将本目录完整上传到你自己的 Git 仓库，然后在 EdgeOne Makers 中选择导入 Git 仓库创建项目；也可使用能够安装项目依赖的 Makers/CLI 部署流程。不要仅上传 `index.html` 或使用下方静态预览包作为正式版。
2. 确认项目根目录能同时找到 `index.html`、`styles.css`、`app.js`、`package.json` 和 `cloud-functions/` 目录。依赖安装使用 `package.json` 中的 `@edgeone/pages-blob`，平台会自动安装；静态文件无需打包，如果控制台要求填写构建命令，保持默认或留空即可。
3. 在 Makers 项目环境变量中设置以下值，然后重新部署：

   | 变量 | 值 |
   |---|---|
   | `APP_SECRET` | **必须设置**：随机生成的长密钥，至少 32 个字符；建议 48 字节以上随机值，不要提交到 Git |
   | `INITIAL_ADMIN_USERNAME` | `任鹏程`（可选；不设置时默认此值） |
   | `INITIAL_ADMIN_PASSWORD` | `123456`（可选；不设置时默认此值，首次登录强制修改） |

   可用 OpenSSL 生成 `APP_SECRET`：`openssl rand -base64 48`。如果没有 OpenSSL，也可以使用密码管理器生成一串至少 48 字符的随机字符串。环境变量修改后**必须重新部署**才会生效。
4. 部署完成后先打开 `https://你的域名/api/diag` 自检：`"ok": true` 表示运行时、crypto、Blob 全部正常；某一项 `false` 时 `detail` 会写出具体原因（例如 `Missing: deployCredential` 表示 Blob 没开通或代码不在 `cloud-functions/`）。加上 `?write=1` 会额外实测一次 Blob 写入并删除临时对象。
5. 首次登录：账号 `任鹏程`，初始密码 `123456`；网页会要求设置新密码。进入「成员与安全」添加其他内部使用者。
6. 首次成功访问 API 后，项目会自动创建 Blob 命名空间 `paperflow-workspace` 和 `workspace/db.json`。此后项目、流程节点、分类、模板和操作记录均保存在服务端。
7. 先创建一份测试项目，改动节点并刷新页面确认数据仍在，再开始录入真实项目。定期使用「导出数据」保存 JSON 备份。

### 登录报「服务端发生错误」怎么查

错误弹窗现在会附带 `code`、`技术细节` 和提示。对照 `/api/diag` 的结果处理：

| 现象 | 原因 | 处理 |
|---|---|---|
| `/api/diag` 中「Blob 凭证」为 `false`，detail 含 `Missing: deployCredential` | 服务端跑在 `edge-functions/`，或项目未启用 Blob 存储 | 代码移到 `cloud-functions/`，在控制台启用 Blob，重新部署 |
| 「APP_SECRET」为 `false` | 未设置或不足 32 位，或改了变量没重新部署 | 设置 48 位以上随机值后重新部署 |
| 「PBKDF2-SHA256」为 `false` 或耗时超过 200ms | 运行时 crypto 不支持或 CPU 超限 | 使用 `cloud-functions/`（Node.js v20） |
| 登录提示「跨站请求已被拒绝」 | EdgeOne 把 `request.url` 换成内部 SCF 域名（如 `*.pages-scf-sg-pro.qcloudteo.com`）和 http，与浏览器 `Origin`（你的公网域名）天然不一致 | 新版本已把 `*.qcloudteo.com`、`*.qcloud.com`、`*.myqcloud.com` 等平台内部域名视为「不可比对」直接放行；仍被拦时用 `PAPERFLOW_ALLOWED_ORIGINS=https://你的域名` 显式放行，并把 `origin=`、`requestUrl=` 发来 |
| 整站返回 HTML 401 `Access Restricted` | 不是应用的错，是平台拦截：预览链接过期、开了访问限制、或 Global 项目网络受限 | 回控制台点「预览」换新链接、关闭访问限制、或绑定自定义域名 |
| 登录成功但马上被踢回登录页 | 浏览器没保存 Cookie：用了 http、或前端与函数不同源 | 统一用站点自己的 https 域名访问 |
| `/api/diag` 本身 404 | 项目没有识别到函数目录 | 确认目录名为 `cloud-functions/`（不是 `functions/`、`edge-functions/`） |

### 静态预览（不用于真实业务）

如果只是想先看设计，可以下载单独的「Makers Drop 静态预览包」，直接上传到 [Makers Drop](https://pages.edgeone.ai/drop)，然后打开网站地址并加上 `?demo=1`，例如 `https://你的演示地址/?demo=1`。登录演示账号：`任鹏程` / `123456`。演示模式的数据只保存在当前浏览器的 `localStorage`，没有真实服务端身份验证，不同设备不共享，也不应录入真实工作资料。

## 数据与安全边界

- 密码只在服务端使用 PBKDF2-SHA-256 哈希保存；服务端不把密码或密码哈希返回给网页。
- 会话 Cookie 带有 `HttpOnly`、`SameSite=Lax` 和 HTTPS 下的 `Secure` 属性。生产站点必须使用 HTTPS。
- 同源校验是纵深防御：EdgeOne 会把函数看到的 `request.url` 改写成内部 SCF 域名，此时无法比对，改为放行；真正兜底的是 `SameSite=Lax` Cookie 和「写操作只接受 `application/json`」（跨站表单无法伪造该 Content-Type，会被 CORS 预检拦住）。
- 数据导出文件不包含成员密码或密码哈希。备份文件应自行妥善保管。
- 整个业务数据保存在一个 JSON Blob 中，适合小团队和低并发使用。写入时有版本号检查，用于拦截大多数过期页面的覆盖操作；它不是关系型数据库的事务锁，不适合多人高并发同时修改。
- Cloud Functions 单次请求体上限约 6MB，本项目在服务端限制为 4MB。仍建议定期导出备份，避免把大量扫描件或附件直接塞进项目备注；当前版本只管理项目流程元数据，不存储试卷文件本体。
- 未分类异常会在响应里带上 `code` 与一行 `技术细节`（错误名 + 错误消息，截断 300 字），只用于排错、不含密钥；如需隐藏，设置环境变量 `PAPERFLOW_HIDE_DETAIL=1`。
- 如果 `APP_SECRET` 丢失或更换，旧会话将失效；持久化的项目数据不会因此删除。

## 本地交互演示

打开 `index.html?demo=1`，使用 `任鹏程 / 123456` 登录。演示包含示例项目数据，便于试用面板、流程、分类、模板、日历与导出；数据可以随时清空或修改，且不会影响正式服务端数据。

## 目录结构

```text
paperflow-workspace/
├── index.html
├── styles.css
├── app.js
├── package.json
└── cloud-functions/          # Node.js v20 云函数（Blob 存储只在这里可用）
    ├── lib/core.js
    └── api/
        ├── auth/login.js
        ├── auth/logout.js
        ├── auth/me.js
        ├── profile.js
        ├── state.js
        ├── users.js
        └── diag.js           # 部署自检：GET /api/diag
```

## 官方文档

- Edge Functions 路由与运行环境：https://pages.edgeone.ai/document/edge-functions
- Cloud Functions（Node.js）：https://pages.edgeone.ai/document/node-functions
- Blob 存储及 SDK：https://pages.edgeone.ai/document/blob-storage
- Makers 环境变量：https://pages.edgeone.ai/document/environment-variable
- Makers Drop 静态上传：https://pages.edgeone.ai/drop
