saiprakash774/workflow-inbox

GitHub: saiprakash774/workflow-inbox

一款基于 TypeScript 的多区域运维工作流收件箱工具,通过服务端状态机和幂等 REST API 统一处理部署审查、基础设施配置与事件响应三类审批任务。

Stars: 0 | Forks: 0

# Workflow 收件箱 一款内部运维工具:提供统一的收件箱,用于审查并处理三种不同工作流类型的待办任务——部署审查、基础设施配置请求以及事件响应——且每种类型均跨越多个 AWS 风格的区域。 本项目是在严格的 60 分钟时间限制下完成的技术测试。任务要求明确指定使用 TypeScript,并明确指出不要添加 auth、Docker、CI/CD 或 Kubernetes——项目中完全没有包含这些内容,这是有意为之,并非疏漏。 ## 快速开始 ``` npm install npm start ``` 然后打开 **http://localhost:3000**。 `npm start` 会运行一个 `prestart` 步骤,在启动后端之前编译前端 TypeScript(`frontend/app.ts` → `public/frontend/app.js`,不使用 bundler),后端使用 `tsx` 启动(直接执行 TypeScript,无需单独的编译步骤)。要求 Node 18+;在 Node 24 上进行开发和测试。 其他脚本: | 命令 | 作用 | |---|---| | `npm run dev` | 与 `start` 相同,但后端会在保存时热重载(`tsx watch`) | | `npm run typecheck` | 使用 `tsc --noEmit` 对后端及共享 contract 进行类型检查 | | `npm run build:frontend` | 从 `frontend/*.ts` 重新构建 `public/frontend/*.js` | **已知缺陷:** `npm run dev` 只会在启动时构建一次前端——对 `frontend/app.ts` 进行修改后,需要手动运行 `npm run build:frontend`(或重启 `dev`)才能生效。请参阅下文的“改进设想”。 ## 包含内容 - **三种工作流类型**,共有 15 个预置模拟项目,涵盖了每种类型生命周期中的所有状态: - **部署审查 (Deployment Review)** —— 批准/拒绝将服务部署到 staging/production 环境,然后将其标记为已部署。 - **基础设施配置 (Infra Provisioning)** —— 批准/拒绝云资源请求(RDS、EKS 节点、VPC peering 等),然后在整个配置过程中进行追踪,直至完成或失败(支持重试)。 - **事件响应 (Incident Response)** —— 确认、升级并解决事件;其中一个预置的事件带有一个模拟的 `aiTriageSuggestion` 字段,以此作为对 LLM 辅助分诊的小小致敬。 - 一个带有**服务端状态机**的 REST API —— 针对给定工作流的一组合法操作由服务端计算并交由前端处理;客户端永远不会重新实现“在这里哪些按钮是合法的”逻辑。 - 在唯一的 mutation endpoint(`POST /:id/actions`)上实现了**严格的幂等性**,具备全局错误处理、带有 request ID 的结构化日志,以及基础的内存级指标。 - 一个**无框架、可访问的前端** —— 采用语义化 HTML、原生表单控件、全程支持键盘操作,并已在真实的无头浏览器中验证通过(绝非仅凭肉眼检查)。 ## 前提假设 - **无 auth。** “代表行事”是一个纯文本字段,用于代替已验证的身份——根据任务说明,这属于范围之外的内容。 - **内存级数据**,每次重启都会重置。没有持久化层,因为明确指出这只是一个供团队成员继续开发的初始版本。 - **“工作流”是具有一个 status 字段和 N 个有效转换的单一资源**,而不是多节点执行图。真正的工作流编排器(参见“未来系统集成”)运行的是由异构节点组成的图;这里进行了范围缩减以适应时间限制。 - **区域只是标签,而非基础设施。** 每个工作流都会列出其涉及的区域,仅用于过滤——实际上并不会发生跨区域复制或路由。 ## 工程选择与权衡 - **全面采用 TypeScript,不使用 bundler。** 后端运行在 `tsx` 上(零配置 TS 执行);前端是直接通过普通的 `tsc` 编译为静态 JS 的 TypeScript(不使用 React/Vite/webpack)。这使得整个工具链仅需 `npm install && npm start`,从而能将时间预算投入到 API/状态机/幂等性逻辑中,而非构建工具上。权衡:与组件框架相比,`frontend/app.ts` 中需要编写更多的手动 DOM 代码——为了缓解这一问题,所有动态渲染都封装在一个小巧的 `el()` 辅助函数背后,该函数只设置 `textContent`,绝不设置 `innerHTML`,因此它也不会成为引发 XSS 的捷径。 - **共享 contract,而非共享逻辑。** `shared/types.ts` 定义了 `Workflow` 联合类型以及 API 请求/响应的数据结构,由前后端共同引入——这是“契约优先”API 设计的一个具体缩影。状态机的*规则*本身**并不**共享——只有后端(`server/services/transitions.ts`)拥有它们,前端通过请求 `GET /api/workflows/meta` 来获取当前合法的操作。在客户端重复业务规则正是持久化工作流 runtime 旨在防范的那种“一致性漂移风险”;这个应用在更小的规模上也做出了同样的选择。 - **内存级存储,而非 Postgres。** 避免了在 Docker/DB 设置上绕道;代价是重启后会丢失数据(参见“前提假设”)。 - **内存级幂等存储,而非 Redis。** 在单进程内是正确的;但在多个实例间或重启后无法保持。这一点在 `server/middleware/idempotency.ts` 中已明确指出。 - **手写验证,而非 zod/joi。** 在仅有三种固定请求结构的情况下是合理的;如果 endpoint 数量超过少许,就不可避免地会面临与 `shared/types.ts` 发生实际漂移的风险。 ## API 设计与幂等性 | 方法与路径 | 用途 | |---|---| | `GET /api/health` | 存活状态、运行时间、内存级指标快照 | | `GET /api/workflows?type=&status=®ion=&q=` | 列出/过滤工作流 | | `GET /api/workflows/:id` | 单个工作流,包含完整历史记录 | | `GET /api/workflows/meta` | 类型、区域,以及完整的 `status → 允许的操作` 表 | | `POST /api/workflows/:id/actions` | 执行操作——请求体为 `{ action, actor, comment? }`,**需要 `Idempotency-Key` 请求头** | 错误返回格式始终为 `{ error: { code, message, requestId, details? } }`,并带有经过精心挑选的 status code(400 表示验证/格式错误的请求体,404 表示未找到,409 表示冲突,500 表示意外错误——绝不泄露内部细节)。 每个响应都带有 `X-Request-Id`(自动生成,或者如果上游传入了 `X-Request-Id` 请求头则对其进行转发,正如真正的 gateway 会做的那样),这样用户报告的错误就可以直接通过检索定位到其服务端日志行。 **幂等性**,仿照 Stripe 风格的 `Idempotency-Key` 请求头约定: - `POST /:id/actions` 不带 key → **400**。 - 新的 key → 操作运行,并且其响应该 key 进行缓存。 - 相同的 key,相同的 payload,在第一次调用完成*后*重播 → **原样返回原始响应**(带有 `Idempotent-Replay: true` 响应头)——处理程序不会再次运行,因此重试“批准”操作绝不会导致重复应用。 - 相同的 key,相同的 payload,但第一次调用*仍在进行中* → **409**(防止双击或重复请求竞争);30 秒的过期检查可防止真正卡住的请求永久阻塞该 key。 - 相同的 key,**不同**的 payload → **409**(在不同的逻辑操作中重用 key 是客户端的 bug,不应被静默允许)。 - **5xx 响应永远不会被缓存**——发生临时故障时,可以使用相同的 key 安全地进行重试。 这是在 HTTP 层面对持久化执行引擎(见下文)免费提供保证的一种手工近似实现。 ## 可访问性 全程使用原生的 `` 控件,而不是自定义控件(完全支持键盘操作,且零自定义 ARIA)、一个跳过链接、用于结果计数和 toast 通知的 `aria-live` 区域、每个按钮的 `aria-label` 都包含其对应的工作流标题(这样使用屏幕阅读器的用户在连续切换“批准”按钮时,能听到每个按钮属于哪个工作流),以及针对单项历史记录使用原生的 `
/`,而不是自定义的展开/折叠控件。已通过脚本化的无头浏览器验证流程(过滤器、操作以及缺少“代表行事”的验证路径)确认,而非仅仅依靠视觉检查。 ## 可观测性 为每个请求/响应以及应用的每一次状态转换生成结构化 JSON 日志(包含时间戳、级别、消息、元数据);每个请求/响应/错误都带有 request ID,以关联客户端和服务端日志;在 `GET /api/health` 上公开轻量级的内存计数器(`requests_total`、客户端/服务端错误计数数、按类型+操作统计的应用操作数、幂等重放/冲突计数);以及进程级的安全防护网(后端的 `unhandledRejection`/`uncaughtException`,前端的 `window.onerror`/`unhandledrejection`),确保不会出现无提示的失败。这些都是在生产环境中由 Micrometer + 真正的 tracing 后端所提供功能的占位符——见下文。 ## 改进设想 - 在相同的服务层接口后方接入真正的持久化层,并辅以由 Testcontainers 支持的集成测试。 - 基于 Redis 的幂等存储,使得该保证在重启后依然有效,或在多个实例间也能正常运行。 - 乐观并发控制(一个 version/ETag 字段),这样两个审查者同时对同一个项目进行操作时,会得到明确的冲突提示,而不是悄无声息地发生“最后写入胜出”。 - 基于 schema 驱动的验证(zod),从单一来源生成 runtime 检查和 TypeScript 类型,而不是使用 `shared/types.ts` 加上可能会产生偏离的手写检查。 - 引入分页,而不是返回完整列表——在 15 行模拟数据时没问题,但在生产规模下则不可行。 - 一套自动化测试套件(针对转换表和服务层的单元测试、API 集成测试,以及将构建期间用于手动验证的 Playwright 脚本转变为真正的 e2e 测试套件)。 - 将 `tsc --watch` 进程接入 `npm run dev`,以实现前端实时重载。 - 真正多步骤、DAG 形状的工作流,而不是单一的 status 字段——见下文。 ## 未来系统集成 这项家庭作业的任务说明有意做到了语言无关,但该任务背后的岗位实际上是一个**基于 Temporal.io 构建的 Kotlin/Spring Boot 工作流编排器 runtime**。有必要明确说明这里的快捷实现方案如何映射到该架构上: - **手工编写的转换表**(`server/services/transitions.ts`)只是对持久化执行引擎免费提供功能的一个简易替代:工作流定义拥有其合法的转换规则,并且由 *runtime*——而非应用程序代码——来执行、持久化,并在崩溃后确定性地重放它们。 - **幂等性中间件**是在 HTTP 层面手工重新实现了 Temporal 通过 activity 重试加上工作流执行历史记录原生提供的保证。在这里,它是一个生命周期短暂的内存级 key-value 存储,无法在重启后存活,也无法扩展到单个实例之外;而在 Temporal 中,“此 activity 是否已经运行过”的问题是由持久化、可重放的历史记录来回答的,而不是靠一个 `Map`。 - **此处的“区域”只是可过滤的标签。** 真正的多区域部署需要具备区域感知能力的 task queue/namespace、工作流状态的跨区域复制,并且需要明确回答“哪个区域拥有此工作流的执行权,以及发生区域故障转移时该怎么办”——这些在本练习中均未进行建模。 - **这里的每种工作流类型都只有一个扁平的 status 字段**,而不是一个图。生产系统执行的是由异构节点组成的图——HTTP 调用、代理工具调用、LLM 调用、人工审查环节。`incident_response` 工作流中的 `aiTriageSuggestion` 字段是对 LLM 形态节点的一点微小致敬,但背后并没有节点图或执行引擎。 - **`shared/types.ts`** 是契约优先 API 设计的种子;在生产环境中,它应该是由 Kotlin 后端和所有 TypeScript/Python 消费者(例如基于 FastAPI 的 worker)共享的 OpenAPI/protobuf contract 生成的客户端,而不是手工维护的文件。 - **此处的可观测性**(结构化日志 + 内存级计数器)在生产环境中将被 Micrometer 加上真正的 tracing 后端(OpenTelemetry → Datadog/Honeycomb)所取代,并配备针对每种工作流类型的仪表板,以及对卡住或失败执行的告警机制。 ## 项目结构 ``` server/ Express + TypeScript backend (run via tsx) data/ In-memory mock data store services/ State machine + business logic controllers/ Request handling routes/ Express routers middleware/ Request context, idempotency, error handling utils/ Logger, AppError, metrics, asyncHandler shared/types.ts API contracts shared by backend and frontend frontend/app.ts Frontend source (compiled by tsc, no bundler) public/ Static assets served by Express (index.html, styles.css, plus the compiled frontend/ and shared/ output — gitignored) ```
标签:MITM代理, TypeScript, 任务看板, 内部工具, 安全插件, 工作流系统, 自动化攻击, 运维管理