docs(design): OpenWorkflow dashboard reference for console engine panel

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-27 18:15:30 +08:00
1 parent 9853d11a19
commit 007992d283
2 files changed
+147 -1

No files matched your search

@@ -0,0 +1,146 @@
# OpenWorkflow 面板调研 → `/console/engine` 复刻方案
- 调研对象:本机锁定版本 `[email protected]`(`spikes/openworkflow/node_modules/openworkflow`,`node_modules/.pnpm/[email protected]*`)+ GitHub `openworkflowdev/openworkflow`(main 分支源码,含 `apps/dashboard`、`apps/cli`)+ `npm view`。
- 关联:ADR-002(`docs/adr/ADR-002-task-engine.md`)、`docs/spikes/w0b-openworkflow.md`。
- 结论先说:**核心库(Apache-2.0)不含任何面板代码**;面板是两个独立、**无开源许可**的 npm 包;面板架构极简(2 个页面、无 workers 表、无鉴权、无手动重试原语);我们要做的是"参考信息架构 + 直连同一份 schema + 用自己的组件重写",不是"抄源码"。
## 1. 面板怎么来的、技术栈、鉴权现状
`spikes/openworkflow/node_modules/openworkflow/dist/` 下只有 `client/ core/ worker/ postgres/ sqlite/` 几个目录,纯 TS 库,没有 `bin` 字段、没有 dashboard 相关文件,`grep -r "dashboard"` 全库零命中。README 里那张 `dashboard.png` 配图对应的是另外两个包:
- `@openworkflow/cli`(npm 最新 0.5.2,`bin: openworkflow`)—— scaffold / `worker start` / `dashboard` 三个子命令的入口。
- `@openworkflow/dashboard`(npm 最新 0.1.10,`bin: dashboard`,解包 3.2MB)—— 面板本体。
启动方式:`npx @openworkflow/cli dashboard [--port N]`。源码 `apps/cli/commands.ts::getDashboardSpawnOptions` 显示它只是 `spawn("npx", ["@openworkflow/dashboard"], { env: { ...process.env, PORT } })` 的一层包装;也可以跳过 CLI 直接 `npx @openworkflow/dashboard`。运行前必须在项目根目录有 `openworkflow.config.ts`(否则报错提示先 `init`),面板从里面读 `config.backend`,连到**同一个** Postgres、同一个 `openworkflow` schema。默认端口 3000(与其 `vite dev --port 3000` 脚本一致)。
技术栈:`@tanstack/react-start`(基于 Vite 的全栈 React 19 SSR 框架,文件路由 + server functions,不是独立跑的 REST API 服务)+ shadcn/ui(`@base-ui/react`)+ Tailwind CSS v4 + `@monaco-editor/react`(只读 JSON 查看器)+ `@phosphor-icons/react` + `prom-client`(暴露 `/metrics` Prometheus 文本端点,不在导航里)。面板"后端"就是 TanStack Start 的 `createServerFn`,在服务端直接 `import type { Backend } from "openworkflow/internal"` 调用,**没有对外 REST API**,前端也是通过同构的 server function 调用,不是 fetch 一个公开 JSON 接口。
**无认证的事实**:遍历 `apps/dashboard/src` 下 48 个源文件,没有一个与 auth / login / session 相关;`AppLayout` 只有 logo + 明暗主题切换。任何能访问该端口的人都能看到全部 run 的 input/output/error(可能含业务敏感数据)、发起新 run、取消/重跑任意 run。生产环境必须自己在前面套反向代理鉴权,面板自身零门禁——这也是我们必须把复刻版做成 admin-only 的直接依据。
## 2. 页面与功能清单
路由只有两个文件路由:`/`(runs 列表)、`/runs/$runId`(run 详情)。**没有 workers 页、没有队列页、没有单独的统计页**(统计卡片嵌在列表页顶部),原因见第 3 节:引擎压根没有持久化的 worker 表。另有非 UI 路由 `/metrics`(Prometheus)。
### 2.1 `/` Workflow Runs 列表页
- 标题 + "New Run" 按钮(打开 Dialog,见下)。
- `WorkflowStats`:5 张统计卡片,固定顺序 **pending / running / completed / failed / canceled**(类型上用 `Omit<..., "succeeded"|"sleeping">` 强制排除这两个 legacy 状态),每张显示 count + 状态图标(pending=时钟/暖色,running=转圈图标叠加 spin 动画/蓝色,completed=对勾/绿色,failed=叉/红色,canceled=禁止符/灰色)。数据来自 `countWorkflowRuns()`——**全表** `GROUP BY status`,不受列表分页/筛选影响。
- `RunList`:卡片列表,每行 = 状态图标(running 会转)+ workflow 名 + version 徽章 + run id(等宽全量展示,可复制)+ 状态 badge + 若为子工作流则显示父 run 关系徽章(`[父workflow名] 父runId`,可点击)+ 右侧 Duration(`finishedAt-startedAt`,进行中用 `now-startedAt`)+ Started(相对时间,如 "3m ago")。整行可点进详情。**没有状态筛选下拉、没有按 workflow 名筛选、没有搜索框**——后端 `listWorkflowRuns` 支持 `status`/`workflowName` 参数,但列表页 UI 目前没接筛选控件,只做了分页。
- 分页:`CursorPaginationControls`,基于 `{next, prev}` **双向不透明游标**(不是 offset),页大小 25/50/100(默认 25)。
- "New Run" 弹窗(`CreateRunForm` → `createWorkflowRunServerFn`):手填 workflowName、version(可选)、input(JSON 文本框,前端 `JSON.parse` 校验)、availableAt/deadlineAt(可选)。本质是运维手工投递工作流的后门,**不校验 workflowName 是否有对应 worker 注册**。
- 每 2 秒轮询(`usePolling`:`setInterval` + `router.invalidate()`),切到后台标签页暂停,回前台立即刷新一次再继续。
### 2.2 `/runs/$runId` Run 详情页
- 头部:返回按钮 + workflow 名 + version 徽章 + 若有父 run 则显示 "Parent Workflow Run" 关系条(可跳转)+ 两个动作按钮:
- **Rerun**(仅终态 run 可见,即 completed/succeeded/failed/canceled):确认弹窗后调用 `rerunWorkflowRun(backend, workflowRunId, fromStep?)`,创建新 run,携带**原始 input 和 version**;不指定 `fromStep` 则全部步骤重跑,指定则复用该步骤之前"已成功"的历史结果,该步骤本身/之后步骤/之前失败或未完成的步骤会重新执行,成功后跳到新 run 详情页。
**重要发现**:`rerunWorkflowRun` 是从 `openworkflow/internal` 导出的,但本项目锁定的 `[email protected]`(`dist/internal.d.ts`)里**没有这个导出**(只有 `isWorkflow`、backend 相关类型、`WorkflowRun`/`StepAttempt` 类型)。这是 0.10.1 之后新增的能力,GitHub main 分支的迁移脚本比对下来(0→6 号迁移,新增的是"6 - add step index to steps",没有新表),说明这是纯代码层面加的功能,不涉及 schema 变更,但**当前锁定版本用不了**,重跑要自己实现(见第 4 节)。
- **Cancel**(仅 pending/running/legacy sleeping 可见):确认弹窗,调用 `cancelWorkflowRun`。
- `RunOverviewPanel`:Run ID(等宽可复制)、Status(badge,hover 有 tooltip 解释含义)、Started/Duration/Attempts 三个 metadata 字段、Available At/Deadline At(相对时间 + hover 看 ISO 绝对时间)/Worker ID(当前或最后持有该 run 租约的 worker 标识,无则 "-")。折叠区块 "Workflow Payloads":Input(只读 JSON,Monaco 渲染)、Output(若有 error 则显示 error 且标红,而不是 output;run 失败时该折叠区块默认展开)。
- 左栏 "Steps" 面板:该 run 的 `step_attempts` 列表(同款游标分页组件),做成可键盘导航的 `listbox`(上下键/Home/End),每行 = 状态图标 + step 名 + kind 徽章(function/sleep/workflow/signal-send/signal-wait)+ 耗时。选中后右栏刷出详情。
- 右栏 "Step Inspector":Step Attempt ID、"Rerun from step" 按钮(同 Rerun 但带 `fromStep`)、Attempts for Step Name(同名 step 出现过几次,用于区分重试序号)、Started At/Finished At、若 `kind==="workflow"` 且有 `childWorkflowRunId` 则显示子 run 关系行、Output(同 run 级"有 error 显示 error"逻辑)。**没有单独展示 step 的 input/config**——面板选择不暴露这两个内部字段。
- 非终态 run 同样启用 2 秒轮询。
### 2.3 容易想当然但确实没有的东西
没有 workers/队列/并发页;没有全局搜索、按 workflowName 筛选下拉、导出、signal 发送/查看 UI;没有登录/多租户/RBAC;没有"原地重试单个失败 step"的按钮(Rerun 是整 run 级别产生新 run,不是原地重试)。
## 3. 数据来源:表结构与状态枚举
面板不经过公开 REST API,是服务端函数直接调用 `Backend`(Postgres 实现用 `postgres.js`,不是 pg/Kysely)。以下建表 SQL 摘自 `dist/postgres/postgres.js` 的迁移脚本(与 GitHub main 分支比对,0.10.1 之后只加了一个索引迁移,表结构未变),可以直接用 Kysely 只读查询同一份数据。
### `workflow_runs`(PK: namespace_id + id)
| 列 | 类型 | 说明 |
| --- | --- | --- |
| namespace_id, id | TEXT | 默认 namespace 是 `"default"` |
| workflow_name, version | TEXT(version 可空) | 注册表键是 `(name, version)` |
| status | TEXT | 见下方枚举 |
| idempotency_key | TEXT 可空 | 24h 去重窗口(`DEFAULT_RUN_IDEMPOTENCY_PERIOD_MS`) |
| config / context / input / output / error | JSONB 可空 | error 是 `SerializedError` |
| attempts | INT | 重试是整个 run 重新排期,这个数会随之增长 |
| parent_step_attempt_namespace_id / id | TEXT 可空 | 子工作流指回父 step |
| worker_id | TEXT 可空 | **唯一的"worker 信息"**,当前/最后持有租约的 worker 标识;running 且被主动 park 等待时会置空 |
| available_at / deadline_at / started_at / finished_at / created_at / updated_at | TIMESTAMPTZ | |
状态枚举 `WorkflowRunStatus`:`pending | running | sleeping(legacy) | succeeded(legacy) | completed | failed | canceled`。等待信号/sleep 期间 run 仍是 `running`,只是 `worker_id` 为空、`available_at` 指向唤醒时间(ADR-002 已验证)。
### `step_attempts`(PK: namespace_id + id)
| 列 | 类型 | 说明 |
| --- | --- | --- |
| workflow_run_id, step_name, kind | TEXT | kind ∈ `function\|sleep\|workflow\|signal-send\|signal-wait` |
| status | TEXT | `running\|succeeded(legacy)\|completed\|failed` |
| config / context / output / error | JSONB 可空 | context 按 kind 判别:sleep 有 `resumeAt`,workflow 有 `timeoutAt`,signal-wait 有 `signal`+`timeoutAt` |
| child_workflow_run_namespace_id / id | TEXT 可空 | kind=workflow 时指向子 run |
| started_at / finished_at / created_at / updated_at | TIMESTAMPTZ | |
关键索引:`step_attempts_signal_wait_idx ON (namespace_id, (context->>'signal')) WHERE kind='signal-wait' AND status='running'`——排查"卡在等信号"时可以直接按这个条件查。
### `workflow_signals`、`openworkflow_migrations`
`workflow_signals(namespace_id, id, signal, data, sender_idempotency_key, workflow_run_id, step_attempt_id, created_at)`:只记录**成功投递**的信号(没有等待者时 `sendSignal` 直接丢弃、不落库,ADR-002 已验证)。`openworkflow_migrations(version)`:单列版本表。
### 没有 `workers` 表
对照 GitHub main 分支当前全部迁移(0~6 号)确认:**引擎从未有过持久化的 worker 注册表/心跳表**。`worker_id` 只是 `workflow_runs` 上的一个字符串快照,没有独立生命周期、没有心跳时间戳、没有主机名/版本/并发数信息。面板因此也没有 workers 页——这不是面板功能缺失,是引擎架构层面就没有这个数据源。想在自己面板做"活跃 worker"视图,只能退而求其次:`SELECT DISTINCT worker_id, count(*), min(started_at) FROM workflow_runs WHERE status='running' AND worker_id IS NOT NULL GROUP BY worker_id`,并且要在 UI 上明确标注"这是当前持有租约的快照,不是权威 worker 注册表"。
### Backend 接口(等价于我们要复刻的查询能力)
`listWorkflowRuns({status?, workflowName?, limit?, after?, before?})` → `{data, pagination:{next,prev}}`;`countWorkflowRuns()` → 按 status 分组计数(不含 succeeded/sleeping);`getWorkflowRun({workflowRunId})`;`listStepAttempts({workflowRunId, limit?, after?, before?})`;`getStepAttempt({stepAttemptId})`;`cancelWorkflowRun({workflowRunId})`;`createWorkflowRun(...)`。**没有** `retryWorkflowRun`/`retryStepAttempt` 这类原语——0.10.1 里失败重试完全是引擎内部按 `retryPolicy` 自动做的,不可从外部触发。分页游标是不透明字符串,不要自己臆测格式,原样透传。
## 4. 本项目复刻方案:`/console/engine`
### 路由(`apps/web`,沿用现有 `app/routes/console/*.tsx` 扁平命名)
- `app/routes/console/engine.tsx`:runs 列表 + 统计卡片 + "当前活跃 worker" 小条(见上,标注非权威)。
- `app/routes/console/engine.$runId.tsx`:run 详情(steps 列表 + step inspector,两栏布局)。
- 不单独做 "workers" 三级路由——数据源太弱,做成列表页里的一个衍生区块即可,避免暗示它是一个可信的运维视图。
### 服务端 API(`apps/server`,Hono,挂在既有 `API_PREFIX = "/api/v1"`,鉴权用现成的 `requireScope("admin")`)
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/api/v1/engine/runs` | query: `status?, workflowName?, cursor?, limit?`;内部转换成 openworkflow 的 `before/after` 双游标 |
| GET | `/api/v1/engine/runs/:id` | run 详情 |
| GET | `/api/v1/engine/runs/:id/steps` | 该 run 的 step_attempts,分页 |
| GET | `/api/v1/engine/counts` | 等价 `countWorkflowRuns()`,给统计卡片 |
| GET | `/api/v1/engine/workers` | 见上,`DISTINCT worker_id` 快照,响应里显式带 `note: "非权威注册表"` 字段或注释 |
| POST | `/api/v1/engine/runs/:id/cancel` | 走 `TaskEngine.cancel`(ADR-002 端口),不直连 openworkflow,保持"先写业务侧 cancel_requested 再调引擎 cancel"的既有约定 |
**不做 `retry`/`rerun` 端点**:0.10.1 没有这个引擎原语,失败重试是自动的。如果确实需要面板级"重跑",要自己实现——读目标 run 的 input/version + 已完成 step 历史,调用 `createWorkflowRun` 生成新 run——并且要在 UI 上明确标成"引擎诊断专用重跑,不是业务重试",避免和业务层显式配置的 `retryPolicy`/幂等键混淆(ADR-002 §2.6)。
### 字段映射(引擎表列 → API 字段 → 展示位置)
| 引擎列(`workflow_runs`) | API 字段 | 展示位置 |
| --- | --- | --- |
| id / workflow_name / version | id / workflowName / version | 列表行标题、详情页头部 |
| status | status | 状态徽章 + 统计卡片分组键 |
| attempts | attempts | 详情页 metadata |
| worker_id | workerId | 详情页 metadata;列表页顶部"活跃 worker"聚合源 |
| available_at / deadline_at | availableAt / deadlineAt | 详情页 metadata(相对时间 + hover 绝对时间) |
| started_at / finished_at | startedAt / finishedAt | 列表 Duration 计算、详情页字段 |
| input / output / error | input / output / error | 详情页折叠区"Workflow Payloads"(有 error 优先显示 error) |
| parent_step_attempt_id | parentStepAttemptId → 反查父 run | 详情页"父工作流"关系条 |
| (不映射)config / context | 不对外暴露为"输入" | 仅 `context->>'signal'` 用于排查 signal-wait 卡住原因 |
`step_attempts` 同理:`step_name/kind/status/started_at/finished_at/output/error/child_workflow_run_id` 映射到 Steps 列表行 + Step Inspector,`config` 不展示。
### 需要注意的点
1. **诊断视图与业务视图分开**:ADR-002 已指出 `step_attempts` 会有停在 `running` 的孤儿行(worker 被杀、租约未到期)。engine 面板可以照实展示——这正是它的价值(暴露孤儿 attempt);但面向用户的业务任务页(下载进度等)绝不能读这张表,必须读业务表。两者要物理上分开路由、分开权限,不要合并成一个页面。
2. **权限仅 admin**:理由——官方面板本身零门禁,而 run 的 input/output/error 可能含下载路径、账号、TMDB key 等业务敏感数据,且有 cancel/新建 run 的写操作,风险等同于直接操作引擎。
3. **只读连接单独开**:openworkflow 自己的 postgres.js 连接池不能塞进 Kysely 事务(ADR-002 已验证),诊断查询建议用独立的只读 pg 连接指向 `openworkflow` schema,不占业务写路径的连接数;两边连接数都算进 PG `max_connections`。
4. **不要把 config/context 当输入展示**:这两个字段是引擎内部状态(重试策略、continuation),对运维暴露用 `input/output/error` 三个字段即可,`context` 只在排查 signal-wait 卡住时挖 `context->>'signal'`(注意同一信号名同一时间只能有一个等待者,卡住时先怀疑撞了这个限制)。
5. **统计卡片的轮询代价**:官方面板每 2 秒 `router.invalidate()` 一次,其中 `countWorkflowRuns()` 是全表 `GROUP BY`。已有索引 `workflow_runs_status_created_at_desc_idx` 兜底,但表到百万级后不建议照抄 2 秒轮询频率,建议拉长间隔或改用 SSE(项目已有 `EventBus`/`sse.ts`)只推增量,不要无脑复制这个轮询模式。
6. **游标透传**:openworkflow 的 `{next, prev}` 是双向不透明游标,与本项目 `packages/contracts` 里 `CursorQuery`/`cursorPage`(单向 `cursor` + `nextCursor`)的约定不完全一致。engine 相关端点建议单开一套双向游标类型,不要硬塞进现有单向 `cursorPage` 泛型,否则"上一页"功能会丢失。
## 5. 许可证
- `openworkflow`(核心库,npm `openworkflow`,GitHub 目录 `packages/openworkflow`):**Apache-2.0**,仓库根 `LICENSE.md` 是标准 Apache-2.0 全文。我们已经在用这个库,符合 ADR-002。
- **`@openworkflow/cli` 与 `@openworkflow/dashboard`(面板本体及其 CLI 入口):npm 明确标记 "Proprietary"**——两个包各自的 `package.json` 都没有 `license` 字段(npm 对缺失 license 字段的展示约定就是 Proprietary),且 `apps/cli`、`apps/dashboard` 目录下没有单独 LICENSE 文件覆盖仓库根的 Apache-2.0。也就是说:源码虽在同一个公开仓库可读,但**面板和 CLI 不是开源许可**,不能按 Apache-2.0 处理。
- 因此不是"照抄需要保留 NOTICE"这么宽松,而是**面板 UI 源码本身没有对外许可,不能复制/改写它的 `.tsx`/`.ts` 源文件去用,哪怕保留版权声明也不构成许可**。可以做、也应该做的:
1. 参考它的信息架构、字段取舍、状态配色语义(功能设计层面,不受版权限制,本文档第 2、3 节即用于此)。
2. 直接查询 `openworkflow` schema 的数据(表结构由 Apache-2.0 的核心库定义,数据访问不受面板许可证约束)。
3. UI 组件、路由、页面代码完全用本项目既有设计系统(`uhd_web` 迁移过来的一套)重新实现,不引用、不拷贝 `apps/dashboard/src` 下任何源文件。
4. 若未来想直接复用 `@openworkflow/dashboard` 本体(不改造,只套反代鉴权跑起来),需要先向 OpenWorkflow 官方确认商用许可条款(proprietary 包条款未公开),不能假设"内部用、不对外分发"就自动免责。
+1 -1
View File
@@ -56,7 +56,7 @@ scripts dev/ci 辅助脚本
7. **入库**:verifyAndImport → Copy/Move(临时文件+校验+提交;跨卷 Copy→核验→删源;EXDEV 不静默降级)、路径绑定固定(已绑定目录不因译名变化改变)、organization_record + 文件级结果、**公开整理记录 API**(API Key、游标增量)。 7. **入库**:verifyAndImport → Copy/Move(临时文件+校验+提交;跨卷 Copy→核验→删源;EXDEV 不静默降级)、路径绑定固定(已绑定目录不因译名变化改变)、organization_record + 文件级结果、**公开整理记录 API**(API Key、游标增量)。
8. **持久任务**:ingestFeed、resolveObservation、evaluateAcquisition、submitDownload、verifyAndImport、refreshMetadata、reconcileLibrary(资产存在性)、cleanupDownloadCache、resolveDecision;outbox;重启恢复;幂等键;取消传播。 8. **持久任务**:ingestFeed、resolveObservation、evaluateAcquisition、submitDownload、verifyAndImport、refreshMetadata、reconcileLibrary(资产存在性)、cleanupDownloadCache、resolveDecision;outbox;重启恢复;幂等键;取消传播。
9. **Agent**:Pi AgentSession 封装;能力注册中心(ID/版本/schema/权限/风险/幂等/预览/处理器),首批能力:catalog.lookup、release.search、release.inspect、identity.resolve、download.plan、download.submit、organize.plan、organize.execute、task.inspect、task.retry、task.cancel、policy.preview、policy.apply、inventory.query;授权校验在服务端;agent_run 持久化 + SSE 增量;后台事件触发有界 Agent 任务(识别歧义、下载失败诊断);设置页可配上游/模型/思考档位/上下文预算/压缩/子代理开关。 9. **Agent**:Pi AgentSession 封装;能力注册中心(ID/版本/schema/权限/风险/幂等/预览/处理器),首批能力:catalog.lookup、release.search、release.inspect、identity.resolve、download.plan、download.submit、organize.plan、organize.execute、task.inspect、task.retry、task.cancel、policy.preview、policy.apply、inventory.query;授权校验在服务端;agent_run 持久化 + SSE 增量;后台事件触发有界 Agent 任务(识别歧义、下载失败诊断);设置页可配上游/模型/思考档位/上下文预算/压缩/子代理开关。
10. **控制台页面**(REQ §13):首页六区块(自动化概览/当前工作/需要我处理/缓存与保留/来源健康/最近产出)、媒体库(列表+详情:路径/身份映射/元数据来源/Slot/候选拒绝理由/下载历史)、订阅与收录规则、偏好标签(基础 CRUD+抑制记录)、PT 站点工作台(整理视图:列表/搜索/详情/库存叠加/统一下载入口;原站视图首版只留占位与说明)、来源/Feed/快照、下载与任务(含业务时间线)、整理记录、待处理中心、身份与元数据、系统设置(实例/路径映射/API Key/Pi 上游与预算/自动化开关)、Agent 会话页 + 侧栏、**工作流引擎面板**(用户 2026-09-27 明确:把 OpenWorkflow 自带 Dashboard 的功能用本项目组件复刻并集成进控制台,走管理员登录;runs 列表/筛选、run 详情(步骤/attempts/输入输出/错误/时间线)、workers 与队列统计、cancel/retry 操作;数据经服务端 API 直接查 `openworkflow` schema;参照 `docs/design/openworkflow-dashboard-reference.md`)。 10. **控制台页面**(REQ §13):首页六区块(自动化概览/当前工作/需要我处理/缓存与保留/来源健康/最近产出)、媒体库(列表+详情:路径/身份映射/元数据来源/Slot/候选拒绝理由/下载历史)、订阅与收录规则、偏好标签(基础 CRUD+抑制记录)、PT 站点工作台(整理视图:列表/搜索/详情/库存叠加/统一下载入口;原站视图首版只留占位与说明)、来源/Feed/快照、下载与任务(含业务时间线)、整理记录、待处理中心、身份与元数据、系统设置(实例/路径映射/API Key/Pi 上游与预算/自动化开关)、Agent 会话页 + 侧栏、**工作流引擎面板**(用户 2026-09-27 明确:把 OpenWorkflow 自带 Dashboard 的功能用本项目组件复刻并集成进控制台,走管理员登录;runs 列表/筛选、run 详情(步骤/attempts/输入输出/错误/时间线)、workers 与队列统计、cancel/retry 操作;数据经服务端 API 直接查 `openworkflow` schema;参照 `docs/design/openworkflow-dashboard-reference.md`;注意:官方面板包 @openworkflow/dashboard 是 Proprietary,不能复制源码,只参考信息架构并直查同一 schema 用本项目组件重写;0.10.1 无 rerun API,重跑通过我们自己的 enqueue 实现)。
11. **可选 Emby 兼容查询**:多实例,查有无/季集/版本,未知≠缺失。 11. **可选 Emby 兼容查询**:多实例,查有无/季集/版本,未知≠缺失。
12. **运维**:`scripts/backup.sh`(pg_dump + 映射导出)、恢复后只读对账模式开关、docker 打包:**除 PostgreSQL 外全部塞进一个容器**(单镜像内跑 API + worker + 托管 web 静态产物,单端口对外;compose 只有 app + pg 两个服务)、runbook。(用户 2026-09-27 明确) 12. **运维**:`scripts/backup.sh`(pg_dump + 映射导出)、恢复后只读对账模式开关、docker 打包:**除 PostgreSQL 外全部塞进一个容器**(单镜像内跑 API + worker + 托管 web 静态产物,单端口对外;compose 只有 app + pg 两个服务)、runbook。(用户 2026-09-27 明确)
14. **管理员登录门禁**(用户 2026-09-27 明确):单管理员账号密码登录(首次启动初始化/设置向导),会话 cookie(HttpOnly、SameSite)+ 登录页 + 未登录跳转;密码哈希(argon2/scrypt)、登录限速与失败锁定、会话吊销、可选 TOTP/Passkey 二次验证;控制台走会话鉴权,外部程序走 API Key。实践参考 `/opt/uhd_doc` 项目(见 `docs/design/auth-reference.md` 调研结论)。 14. **管理员登录门禁**(用户 2026-09-27 明确):单管理员账号密码登录(首次启动初始化/设置向导),会话 cookie(HttpOnly、SameSite)+ 登录页 + 未登录跳转;密码哈希(argon2/scrypt)、登录限速与失败锁定、会话吊销、可选 TOTP/Passkey 二次验证;控制台走会话鉴权,外部程序走 API Key。实践参考 `/opt/uhd_doc` 项目(见 `docs/design/auth-reference.md` 调研结论)。