diff --git a/.gitignore b/.gitignore index 1c1c7d2a..369f6901 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,7 @@ npm-debug.log* .env .env.* !.env.example +provider-*.yml +provider-*.yaml dist/ coverage/ diff --git a/AGENTS.md b/AGENTS.md index 2f7678e9..fc8ce3fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,10 +18,11 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - `bun scripts/cli.ts server start`:以异步 job 启动 database、backend-core、frontend、provider-gateway 和主 server 用户服务,部署规则见 `docs/reference/deployment.md`。 - `bun scripts/cli.ts server status`:查询固定端口、容器状态、健康检查和访问 URL,判定标准见 `docs/reference/deployment.md`。 - `bun scripts/cli.ts server logs`:分页返回文件日志与 Docker 日志尾部,日志规则见 `docs/reference/observability.md`。 -- `bun scripts/cli.ts server rebuild `:以 build-first、Compose lock、no-deps force-recreate 和 post-up validation 的异步 job 重建单个服务,规则见 `docs/reference/deployment.md`。 +- `bun scripts/cli.ts server rebuild `:以 build-first、Compose lock、no-deps force-recreate 和 post-up validation 的异步 job 重建单个服务,规则见 `docs/reference/deployment.md`。 +- `bun scripts/cli.ts provider attach [--master-server URL] [--up] [--force]`:在新增计算节点上生成两项配置的 provider-gateway 挂载包;默认只需要主 server URL(默认 `http://74.48.78.17/`)和唯一 Provider ID,生成的 Compose 固定 Docker socket、`pid: "host"`、`restart: always`、只读 `/workspace` 与 SSH 维护私钥挂载,规则见 `docs/reference/provider-gateway.md`。 - `bun scripts/cli.ts ssh [ssh-like args...]`:通过 provider-gateway 的 Host SSH / WSL SSH 维护桥打开近似原生 ssh 的交互会话或远端命令,并在远端 PATH 注入 `apply_patch`、`glob` 与 `skill-discover`;`apply-patch`、`py`、`skills`、结构化 `find`、`glob` 和 `argv` 子命令用于避免远端补丁、Python stdin、skill 发现与常用只读命令的嵌套转义问题,使用规则见 `docs/reference/cli.md` 和 `docs/reference/provider-gateway.md`。 -- `bun scripts/cli.ts microservice list/status/health/proxy`:管理和验证挂载在主 server 或计算节点 Docker 中的用户服务,Codex Queue/Todo Note on main-server 与 FindJob/Pipeline/MET Nonlinear on D601 的规则见 `docs/reference/microservices.md`。 -- `bun scripts/cli.ts codex task `:按 Codex Queue 任务 ID 查询初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,便于新任务引用历史 session。 +- `bun scripts/cli.ts microservice list/status/health/proxy`:管理和验证挂载在主 server 或计算节点 Docker 中的用户服务,Code Queue/Todo Note/Baidu Netdisk on main-server 与 FindJob/Pipeline/MET Nonlinear on D601 的规则见 `docs/reference/microservices.md`。 +- `bun scripts/cli.ts codex task `:按 Code Queue 任务 ID 查询初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,便于新任务引用历史 session。 - `bun scripts/cli.ts server stop`:以异步 job 停止固定 Compose 项目中的全部 UniDesk 服务,停止后用 `server status` 复核。 - `bun scripts/cli.ts job list` / `bun scripts/cli.ts job status latest`:查询 `.state/jobs/` 中的异步任务状态,job 机制见 `docs/reference/cli.md`。 - `bun scripts/cli.ts debug health` / `bun scripts/cli.ts debug dispatch` / `bun scripts/cli.ts debug task`:通过 Docker 内网 core、真实 HTTP、WebSocket、系统指标、Docker 状态和 Host SSH 维护桥流程调试健康检查、任务下发与任务结果,调试规则见 `docs/reference/cli.md`。 @@ -30,8 +31,8 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 ## Runtime - `bun`:TypeScript 运行时固定使用 Bun,组件入口和 CLI 都直接运行 `.ts` 文件,约束见 `docs/reference/config.md`。 -- `docker-compose.yml`:主 server 统一编排 core、frontend、database、本机 provider gateway、Todo Note 后端和 Codex Queue 后端,且只公开 frontend/provider ingress,服务拓扑见 `docs/reference/deployment.md`。 -- `src/components/frontend`:前端源码固定使用 TypeScript + React,`app.tsx` 只做 shell/router,左侧主模块与顶部子标签统一编译为模块前缀路由:`/ops//`、`/nodes//`、`/tasks//`、`/config//`,只有用户服务使用 `/app//` 深链接,运行总览包含通用性能面板,资源监控含曲线和进程资源排序表,Todo Note、FindJob、Pipeline、MET Nonlinear、Codex Queue 等业务页必须拆到独立 TSX 模块,界面规则见 `docs/reference/frontend.md`。 +- `docker-compose.yml`:主 server 统一编排 core、frontend、database、本机 provider gateway、Todo Note 后端、Baidu Netdisk 后端和 Code Queue 后端,且只公开 frontend/provider ingress,服务拓扑见 `docs/reference/deployment.md`。 +- `src/components/frontend`:前端源码固定使用 TypeScript + React,`app.tsx` 只做 shell/router,左侧主模块与顶部子标签统一编译为模块前缀路由:`/ops//`、`/nodes//`、`/tasks//`、`/config//`,只有用户服务使用 `/app//` 深链接,运行总览包含通用性能面板,资源监控含曲线和进程资源排序表,Todo Note、FindJob、Pipeline、MET Nonlinear、Baidu Netdisk、Code Queue 等业务页必须拆到独立 TSX 模块,界面规则见 `docs/reference/frontend.md`。 - `backend-core / frontend performance`:backend-core 暴露 `/api/performance`,frontend 暴露同源 `/api/frontend-performance` 并在 `/ops/performance/` 汇总组件请求、失败请求、内部操作和慢操作,规则见 `docs/reference/observability.md`。 - `Pipeline OA event flow`:Pipeline 控制、事件取证和甘特图渲染必须最终 100% 由 OA 事件流驱动,交付态不得保留点对点控制、旧审核事件、旧 batch 推进、runner 私有 JSONL 或 frontend/CLI 直写状态,权威规则见 `docs/reference/pipeline-oa-event-flow.md`。 - `src/components/provider-gateway`:当前主 server `74.48.78.17` 也作为 provider gateway 接入 UniDesk,外部节点通过 `ws://74.48.78.17:18082/ws/provider` 接入,必须以 `restart: always` 部署 always-enabled 远程升级、sleep-and-validate 回滚保护和 Host SSH / WSL SSH 透传并完成自测,部署与 Playwright 公网前端验证方法见 `docs/reference/provider-gateway.md`。 @@ -43,7 +44,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - `docs/reference/arch.md`:UniDesk 分布式工作平台的长期架构约束。 - `docs/reference/repo-tree.md`:仓库结构目标与组件边界。 - `docs/reference/observability.md`:服务日志、任务活性、通用性能指标 API 和性能面板的可观测性规则。 -- `docs/reference/microservices.md`:用户服务(兼容命名 `microservice`)的配置、代理、安全边界、Todo Note on main-server、FindJob/Pipeline/MET Nonlinear on D601 和验证规则。 +- `docs/reference/microservices.md`:用户服务(兼容命名 `microservice`)的配置、代理、安全边界、Todo Note/Baidu Netdisk on main-server、FindJob/Pipeline/MET Nonlinear on D601 和验证规则。 - `docs/reference/windows-passthrough.md`:WSL provider 通过 SSH 透传调用 Windows cmd/PowerShell、Keil、COM 串口和 Windows 侧 skill 的长期规则。 - `docs/reference/constar-d601.md`:D601 上 ConStart/constar 固件工作区的 UniDesk SSH 入口、WSL skill wrapper、Keil 编译下载和串口/JSON-RPC 验证简要引导。 - `docs/reference/pipeline-oa-event-flow.md`:Pipeline/OA 事件流、审核/无审核流转、单步调试、甘特图渲染和最终去残留规则。 diff --git a/TEST.md b/TEST.md index c0e4cd5e..32303cfd 100644 --- a/TEST.md +++ b/TEST.md @@ -22,7 +22,7 @@ ## T6 日志第一现场验证 -阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts server logs --tail-bytes 20000`,实际读取输出中列出的 `logs/{YYYYMMDD}/` 文件,确认 backend-core、frontend、provider-gateway、database 都有实时日志;日志不得只有启动行,错误日志必须包含可定位的错误消息或 stack。 +阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts server logs --tail-bytes 20000`,实际读取输出中列出的 `logs/{YYYYMMDD}/` 文件,确认 backend-core、frontend、provider-gateway、database 都有实时日志;backend-core 与 Code Queue/Codex app-server 日志必须按 `logs/{YYYYMMDD}/{startStamp}_{YYYYMMDD}_{HH}_{service}.jsonl` 小时切片,默认日志族总量不得超过 `1GiB`,超过后会删除最旧切片;日志不得只有启动行,错误日志必须包含可定位的错误消息或 stack。 ## T7 停止与端口释放 @@ -34,7 +34,7 @@ ## T8 Playwright 公网前端 E2E -阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:确认 `config.json` 的 `network.publicHost` 是主 server 公网地址,运行 `bun scripts/cli.ts e2e run`,要求 JSON 中 `network:only-frontend-provider-ports`、`network:core-public-blocked`、`network:database-public-blocked`、`network:findjob-public-blocked`、`network:met-nonlinear-public-blocked`、`network:todo-note-public-blocked`、`network:codex-queue-public-blocked`、`core:internal-overview`、`core:pgdata-usage`、`core:performance-api`、`provider:self-node-online`、`provider:gateway-version-label`、`provider:system-status`、`provider:docker-status`、`provider:upgrade-plan`、`provider-ingress:public-health`、`microservice:catalog-findjob`、`microservice:catalog-pipeline`、`microservice:catalog-met-nonlinear`、`microservice:catalog-todo-note`、`microservice:catalog-codex-queue`、`microservice:todo-note-health`、`microservice:todo-note-migrated-data`、`microservice:todo-note-write-path`、`microservice:codex-queue-status`、`microservice:codex-queue-health`、`microservice:codex-queue-tasks`、`microservice:findjob-health`、`microservice:findjob-summary`、`microservice:findjob-jobs-preview`、`microservice:pipeline-status`、`microservice:pipeline-health`、`microservice:pipeline-snapshot`、`microservice:pipeline-oa-event-flow`、`microservice:met-nonlinear-status`、`microservice:met-nonlinear-health`、`microservice:met-nonlinear-queue`、`microservice:met-nonlinear-projects`、`microservice:met-nonlinear-image`、`database:named-volume-write`、`database:todo-note-pg-storage`、`frontend:login-provider-visible`、`frontend:public-provider-info-visible`、`frontend:sidebar-collapse`、`frontend:overview-pgdata-visible`、`frontend:no-naked-json-before-click`、`frontend:performance-panel-visible`、`frontend:system-monitor-visible`、`frontend:upgrade-plan-dispatch`、`frontend:docker-status-visible`、`frontend:gateway-version-records-visible`、`frontend:gateway-duration-subsecond-visible`、`frontend:provider-operation-availability-visible`、`frontend:microservice-catalog-visible`、`frontend:todo-note-integrated-visible`、`frontend:findjob-integrated-visible`、`frontend:codex-queue-integrated-visible`、`frontend:pipeline-integrated-visible`、`frontend:pipeline-react-flow-visible`、`frontend:pipeline-step-timeline-visible`、`frontend:pipeline-oa-event-flow-visible`、`frontend:met-nonlinear-integrated-visible`、`frontend:met-nonlinear-project-tree-detail`、`frontend:met-nonlinear-queue-detail-speed` 全部 passed;打开输出的 screenshotPath,确认 Playwright 访问的是公网 frontend,页面上能看到 `main-server`、`Main Server Provider`、`D601`、`FindJob`、`Pipeline`、`MET Nonlinear`、`Codex Queue`、`性能面板`、`SSH 透传`、`远程更新` 和结构化控件。 +阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:确认 `config.json` 的 `network.publicHost` 是主 server 公网地址,运行 `bun scripts/cli.ts e2e run`,要求 JSON 中 `network:only-frontend-provider-ports`、`network:core-public-blocked`、`network:database-public-blocked`、`network:findjob-public-blocked`、`network:met-nonlinear-public-blocked`、`network:todo-note-public-blocked`、`network:code-queue-public-blocked`、`core:internal-overview`、`core:pgdata-usage`、`core:performance-api`、`provider:self-node-online`、`provider:gateway-version-label`、`provider:system-status`、`provider:docker-status`、`provider:upgrade-plan`、`provider-ingress:public-health`、`microservice:catalog-findjob`、`microservice:catalog-pipeline`、`microservice:catalog-met-nonlinear`、`microservice:catalog-todo-note`、`microservice:catalog-code-queue`、`microservice:todo-note-health`、`microservice:todo-note-migrated-data`、`microservice:todo-note-write-path`、`microservice:code-queue-status`、`microservice:code-queue-health`、`microservice:code-queue-tasks`、`microservice:findjob-health`、`microservice:findjob-summary`、`microservice:findjob-jobs-preview`、`microservice:pipeline-status`、`microservice:pipeline-health`、`microservice:pipeline-snapshot`、`microservice:pipeline-oa-event-flow`、`microservice:met-nonlinear-status`、`microservice:met-nonlinear-health`、`microservice:met-nonlinear-queue`、`microservice:met-nonlinear-projects`、`microservice:met-nonlinear-image`、`database:named-volume-write`、`database:todo-note-pg-storage`、`frontend:login-provider-visible`、`frontend:public-provider-info-visible`、`frontend:sidebar-collapse`、`frontend:overview-pgdata-visible`、`frontend:no-naked-json-before-click`、`frontend:performance-panel-visible`、`frontend:system-monitor-visible`、`frontend:upgrade-plan-dispatch`、`frontend:docker-status-visible`、`frontend:gateway-version-records-visible`、`frontend:gateway-duration-subsecond-visible`、`frontend:provider-operation-availability-visible`、`frontend:microservice-catalog-visible`、`frontend:todo-note-integrated-visible`、`frontend:findjob-integrated-visible`、`frontend:code-queue-integrated-visible`、`frontend:pipeline-integrated-visible`、`frontend:pipeline-react-flow-visible`、`frontend:pipeline-step-timeline-visible`、`frontend:pipeline-oa-event-flow-visible`、`frontend:met-nonlinear-integrated-visible`、`frontend:met-nonlinear-project-tree-detail`、`frontend:met-nonlinear-queue-detail-speed` 全部 passed;打开输出的 screenshotPath,确认 Playwright 访问的是公网 frontend,页面上能看到 `main-server`、`Main Server Provider`、`D601`、`FindJob`、`Pipeline`、`MET Nonlinear`、`Code Queue`、`性能面板`、`SSH 透传`、`远程更新` 和结构化控件。 ## T9 Database 命名卷持久化 @@ -46,11 +46,11 @@ ## T11 资源节点 Docker 状态 -阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts e2e run`,确认 `provider:docker-status` 和 `frontend:docker-status-visible` passed;再用浏览器登录 frontend,进入左侧 `资源节点` 和顶部 `Docker 状态` 子标签,确认可以像 Docker Desktop 一样看到当前节点的 Containers、Images、Volumes、Networks 指标、容器表格、镜像/卷/网络侧栏和 Docker daemon 摘要,并确认在 `main-server` 节点下数据库命名卷 `unidesk_pgdata_10gb` 在 Volumes 区域和数据库命名卷卡片中显式可见;切换到 D518/D601 等计算节点时不应要求存在 pgdata 数据库卷,也不应显示数据库命名卷缺失告警。 +阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts e2e run`,确认 `provider:docker-status`、`provider:gateway-restart-policy` 和 `frontend:docker-status-visible` passed;再用浏览器登录 frontend,进入左侧 `资源节点` 和顶部 `Docker 状态` 子标签,确认可以像 Docker Desktop 一样看到当前节点的 Containers、Images、Volumes、Networks 指标、容器表格、镜像/卷/网络侧栏和 Docker daemon 摘要,所有 running 的 provider-gateway 容器都在 Docker 快照或节点 labels 中显示 `restartPolicy=always` 且 `pidMode=host`,并确认在 `main-server` 节点下数据库命名卷 `unidesk_pgdata_10gb` 在 Volumes 区域和数据库命名卷卡片中显式可见;切换到 D518/D601 等计算节点时不应要求存在 pgdata 数据库卷,也不应显示数据库命名卷缺失告警。 ## T12 前端 TypeScript + React 源码约束 -阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `find src/components/frontend -type f \\( -name '*.js' -o -name '*.jsx' \\) -print`,确认没有手写 frontend JS/JSX 源码;确认 `src/components/frontend/src/app.tsx` 只承担 shell/router,Todo Note、FindJob、Pipeline、MET Nonlinear、ClaudeQQ、Codex Queue 分别在 `src/components/frontend/src/todo-note.tsx`、`src/components/frontend/src/findjob.tsx`、`src/components/frontend/src/pipeline.tsx`、`src/components/frontend/src/met-nonlinear.tsx`、`src/components/frontend/src/claudeqq.tsx`、`src/components/frontend/src/codex-queue.tsx` 中维护;运行 `bun scripts/cli.ts check`,确认这些 TSX 模块全部纳入 TypeScript 检查,且浏览器请求 `/app.js` 由 frontend Bun server 从 TSX imports 转译生成。 +阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `find src/components/frontend -type f \\( -name '*.js' -o -name '*.jsx' \\) -print`,确认没有手写 frontend JS/JSX 源码;确认 `src/components/frontend/src/app.tsx` 只承担 shell/router,Todo Note、FindJob、Pipeline、MET Nonlinear、ClaudeQQ、Code Queue 分别在 `src/components/frontend/src/todo-note.tsx`、`src/components/frontend/src/findjob.tsx`、`src/components/frontend/src/pipeline.tsx`、`src/components/frontend/src/met-nonlinear.tsx`、`src/components/frontend/src/claudeqq.tsx`、`src/components/frontend/src/code-queue.tsx` 中维护;运行 `bun scripts/cli.ts check`,确认这些 TSX 模块全部纳入 TypeScript 检查,且浏览器请求 `/app.js` 由 frontend Bun server 从 TSX imports 转译生成。 ## T13 资源节点任务管理器曲线 @@ -58,7 +58,7 @@ ## T13A 通用性能面板 -阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts e2e run --only core:performance-api,frontend:performance-panel-visible`,确认 backend-core `/api/performance` 返回组件请求统计、内部操作统计、PGDATA 和 Codex Queue PostgreSQL 存储摘要;再用浏览器登录 frontend,进入左侧 `运行总览` 和顶部 `性能面板` 子标签,确认页面能看到 `Bwebui` MB 趋势图、组件汇总、最近失败请求、内部操作汇总和最近慢操作,且没有失败请求时明确显示“最近没有失败请求”;完整性能 JSON 只能通过 `查看原始JSON` 打开。 +阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts e2e run --only core:performance-api,frontend:performance-panel-visible`,确认 backend-core `/api/performance` 返回组件请求统计、内部操作统计、PGDATA 和 Code Queue PostgreSQL 存储摘要;再用浏览器登录 frontend,进入左侧 `运行总览` 和顶部 `性能面板` 子标签,确认页面能看到 `Bwebui` MB 趋势图、组件汇总、最近失败请求、内部操作汇总和最近慢操作,且没有失败请求时明确显示“最近没有失败请求”;完整性能 JSON 只能通过 `查看原始JSON` 打开。 ## T14 Provider Gateway 远程升级 @@ -90,16 +90,16 @@ ## T21 D601 Pipeline User Service -阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts microservice list`,确认 `pipeline` 显示为 `providerId=D601`、`public=false`、`frontendOnly=true`、仓库 URL `https://github.com/pikasTech/pipeline`、commit id、`127.0.0.1:18082` 后端映射、`allowedMethods` 包含 `GET/HEAD/POST` 和 `pipeline-v2-control` 容器摘要;运行 `bun scripts/cli.ts microservice health pipeline`、`bun scripts/cli.ts microservice proxy pipeline '/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3'` 和 `bun scripts/cli.ts microservice proxy pipeline /api/oa-event-flow/diagnostics`,确认链路通过 backend-core、D601 provider-gateway 和 D601 本机 Pipeline 后端,snapshot 返回 `ok=true`、组件 registry、Pipeline run 预览,diagnostics 返回 `mode=oa-event-flow-100`、禁止残留为 0、有无审核与 monitor 审核证据、node 完成事件不携带流程策略字段,且运行代码中不存在 `control-prompts.jsonl`、`monitor-prompts.jsonl`、`monitor-control`、`control-events.jsonl`、monitor stop 文件、`PIPELINE_*_APPEND_FILE`、本地 JSONL append/read helper 或 monitor `/pipeline-state` 挂载等旧文件传输残留;运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 microservice health pipeline`,确认非主 server 也能通过公网 frontend remote CLI 验证同一链路且不需要 `--main-server-key`。随后运行 `bun scripts/cli.ts e2e run --only microservice:pipeline,frontend:pipeline`,确认定向 Pipeline 验收通过;若没有正在运行且已产生 `node-long-running-observation` 的候选 run,E2E 会先通过 D601 SSH 透传启动 `monitor-management-behavior-test` 作为真实长任务观察种子,再由公网 frontend Playwright 验证甘特图连线和实时 running 条。再运行 `bun scripts/cli.ts e2e run`,确认 `microservice:pipeline-oa-event-flow`、`frontend:pipeline-oa-event-flow-visible` 和其他用户服务/frontend Pipeline 检查全部 passed;再登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / 服务目录` 和 `用户服务 / Pipeline`,确认页面以 React 控件显示 D601、仓库引用、私有后端映射、Pipeline 组件矩阵、React Flow 控制图框图、评分器 score 卡片、结构化 `OA 事件流` 诊断面板、epoch 列表、epoch 甘特图和运行材料索引,点击控制图中的 node 后会打开 node 精细控制面板,能通过“抓取过程”读取 node 执行过程,并显示 append prompt、guide 和 redo/restart 操作入口;甘特图必须按纵向时间轴绘制 node 工作竖条,滚动到某个时间窗口时自动隐藏该窗口内无工作的 node 列;点击甘特图执行线后必须显示 `OpenCode Trace`,Trace 正文必须复用 Codex Queue 的公共 Trace 样式和 opencode port,废弃旧 Pipeline step/message/tool 卡片风格;Pipeline OA 事件流验收必须证明有审核和无审核流程都由 OA 从 `node-finished` 事实、config policy、monitor 控制事件和 runner control result 串起,不能保留独立审核请求事件、旧批次推进、runner 私有 JSONL 取证或 frontend/CLI 直写 `.state`;默认没有裸 JSON、JSONL 或逐行日志,只有点击 `查看原始JSON` 才显示原始数据。Pipeline 业务代码开发和调试必须用 `bun scripts/cli.ts ssh D601 ...` 进入 D601 的 `/home/ubuntu/pipeline`,不得把 pipeline 全量代码复制进 UniDesk 仓库,也不得占用主 server 部署调试服务。 +阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts microservice list`,确认 `pipeline` 显示为 `providerId=D601`、`public=false`、`frontendOnly=true`、仓库 URL `https://github.com/pikasTech/pipeline`、commit id、`127.0.0.1:18082` 后端映射、`allowedMethods` 包含 `GET/HEAD/POST` 和 `pipeline-v2-control` 容器摘要;运行 `bun scripts/cli.ts microservice health pipeline`、`bun scripts/cli.ts microservice proxy pipeline '/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3'` 和 `bun scripts/cli.ts microservice proxy pipeline /api/oa-event-flow/diagnostics`,确认链路通过 backend-core、D601 provider-gateway 和 D601 本机 Pipeline 后端,snapshot 返回 `ok=true`、组件 registry、Pipeline run 预览,diagnostics 返回 `mode=oa-event-flow-100`、禁止残留为 0、有无审核与 monitor 审核证据、node 完成事件不携带流程策略字段,且运行代码中不存在 `control-prompts.jsonl`、`monitor-prompts.jsonl`、`monitor-control`、`control-events.jsonl`、monitor stop 文件、`PIPELINE_*_APPEND_FILE`、本地 JSONL append/read helper 或 monitor `/pipeline-state` 挂载等旧文件传输残留;运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 microservice health pipeline`,确认非主 server 也能通过公网 frontend remote CLI 验证同一链路且不需要 `--main-server-key`。随后运行 `bun scripts/cli.ts e2e run --only microservice:pipeline,frontend:pipeline`,确认定向 Pipeline 验收通过;若没有正在运行且已产生 `node-long-running-observation` 的候选 run,E2E 会先通过 D601 SSH 透传启动 `monitor-management-behavior-test` 作为真实长任务观察种子,再由公网 frontend Playwright 验证甘特图连线和实时 running 条。再运行 `bun scripts/cli.ts e2e run`,确认 `microservice:pipeline-oa-event-flow`、`frontend:pipeline-oa-event-flow-visible` 和其他用户服务/frontend Pipeline 检查全部 passed;再登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / 服务目录` 和 `用户服务 / Pipeline`,确认页面以 React 控件显示 D601、仓库引用、私有后端映射、Pipeline 组件矩阵、React Flow 控制图框图、评分器 score 卡片、结构化 `OA 事件流` 诊断面板、epoch 列表、epoch 甘特图和运行材料索引,点击控制图中的 node 后会打开 node 精细控制面板,能通过“抓取过程”读取 node 执行过程,并显示 append prompt、guide 和 redo/restart 操作入口;甘特图必须按纵向时间轴绘制 node 工作竖条,滚动到某个时间窗口时自动隐藏该窗口内无工作的 node 列;点击甘特图执行线后必须显示 `OpenCode Trace`,Trace 正文必须复用 Code Queue 的公共 Trace 样式和 opencode port,废弃旧 Pipeline step/message/tool 卡片风格;Pipeline OA 事件流验收必须证明有审核和无审核流程都由 OA 从 `node-finished` 事实、config policy、monitor 控制事件和 runner control result 串起,不能保留独立审核请求事件、旧批次推进、runner 私有 JSONL 取证或 frontend/CLI 直写 `.state`;默认没有裸 JSON、JSONL 或逐行日志,只有点击 `查看原始JSON` 才显示原始数据。Pipeline 业务代码开发和调试必须用 `bun scripts/cli.ts ssh D601 ...` 进入 D601 的 `/home/ubuntu/pipeline`,不得把 pipeline 全量代码复制进 UniDesk 仓库,也不得占用主 server 部署调试服务。 ## T22 Main Server Todo Note User Service 阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:确认 D518 `/mnt/d/work/todo_note` 已复制到主 server `/root/todo_note`,运行 `bun scripts/cli.ts microservice list`,确认 `todo-note` 显示为 `providerId=main-server`、`public=false`、`frontendOnly=true`、仓库 URL `https://gitee.com/Lyon1998/todo_note`、`todo-note:4211` 后端映射和 `todo-note-backend` 容器摘要;运行 `bun scripts/cli.ts microservice health todo-note`,确认返回 `storage=postgres`;运行 `bun scripts/cli.ts microservice proxy todo-note /api/instances --max-body-bytes 8000`,确认能看到 `CONSTAR`、`大论文`、`找工作`、`小论文`、`事务` 五个迁移清单,总任务数不低于 100。随后通过 backend-core 或 `bun scripts/cli.ts e2e run` 执行临时清单 create/add/toggle/undo/delete 写入循环,确认 Todo Note 写入真实经过 backend-core、main-server provider-gateway、`todo-note-backend` 和主 PostgreSQL,且删除前必须按唯一临时清单名称重新选中临时清单,不能误删迁移清单。最后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / Todo Note`,确认清单、树形任务、筛选、提醒、移动、撤销/重做、字号控制都以 React 控件展示,默认没有裸 JSON,只有点击 `查看原始JSON` 才显示原始数据。 -## T23 Main Server Codex Queue User Service +## T23 Main Server Code Queue User Service -阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts microservice list`,确认 `codex-queue` 显示为 `providerId=main-server`、`public=false`、`frontendOnly=true`、仓库 URL `https://github.com/pikasTech/unidesk`、`codex-queue:4222` 后端映射和 `codex-queue-backend` 容器摘要;运行 `bun scripts/cli.ts server rebuild codex-queue` 并用 `bun scripts/cli.ts job status ` 等待该 job 成功且输出 post-up validation,再运行 `bun scripts/cli.ts microservice health codex-queue`、`bun scripts/cli.ts microservice proxy codex-queue /api/dev-ready --raw`、`bun scripts/cli.ts microservice proxy codex-queue /api/tasks` 和 `bun scripts/cli.ts codex task <已有taskId>`,确认链路通过 backend-core、main-server provider-gateway 和 Codex Queue 后端,且 task id 查询返回初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,且 `queue.devReady.ok=true`、`devReady.missingTools=[]`、`docker.versionOk=true`、`docker.composeOk=true`,必需工具包含 `docker`、`docker-compose`、`jq`、`ssh`、`rsync`、`pip3` 和 `unzip`;提交会产生较多命令输出的小任务后,`/health` 和 `/api/tasks` 仍必须在常规 CLI 超时内返回,容器内不得堆积无超时 healthcheck 进程。随后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / Codex Queue`,确认页面显示默认模型 `gpt-5.5`、默认工作目录 `/root/unidesk`、模型下拉菜单包含 `gpt-5.4-mini`/`gpt-5.4`/`gpt-5.5`、入队份数、队列指标、任务 ID、复制任务 ID、引用按钮、任务耗时、引用任务 ID、清空输入、创建成功提示、任务提交表单、Codex CLI-like 输出、attempt 表、MiniMax/fallback judge 状态、追加 prompt、打断和重试控件;通过页面提交一个小任务,确认任务进入 queued/running/succeeded 或可解释的 failed 状态,并且输出区能看到运行中的 Codex 消息。批量验收时设置 `入队份数=5` 或用 `---` 分隔 5 段 prompt,一次性入队 5 条任务,确认 5 条任务按顺序运行并全部进入 succeeded 或可解释的非成功终态,不能只运行第一条后停止;其中任一任务被 judge 判定 `fail` 时只能把当前任务标为 failed,后续 queued 任务仍必须继续推进。测试异常中断时可以提交长任务后点击 `打断`,确认任务变为 canceled 或被 judge 标记为非成功终态;自动重试只应在服务端/传输异常、任务正常结束但 execution record 显示未完成、或 judge 判定 retry 时发生;retry 必须复用已有 Codex thread 并 append 继续执行 prompt,只有当前任务 complete 后才推进队列中的下一个任务。MiniMax judge 必须能处理 Markdown fence/夹杂文本等 JSON 去噪;若去噪后仍失败,必须把解析错误和上一轮去噪前原始回答反馈给 MiniMax 修复后重试,日志中应出现 `judge_json_parse_retry`,且 repair 成功时仍以 `source=minimax` 返回。Codex provider key 只能通过 `OPENAI_API_KEY`、`CRS_OAI_KEY` 这类运行时环境透传,MiniMax API key 只能通过 `UNIDESK_CODEX_QUEUE_MINIMAX_API_KEY` 这类运行时环境传入,禁止写入 `config.json`、Dockerfile、源码或测试文档。 +阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts microservice list`,确认 `code-queue` 显示为 `providerId=main-server`、`public=false`、`frontendOnly=true`、仓库 URL `https://github.com/pikasTech/unidesk`、`code-queue:4222` 后端映射和 `code-queue-backend` 容器摘要;运行 `bun scripts/cli.ts server rebuild code-queue` 并用 `bun scripts/cli.ts job status ` 等待该 job 成功且输出 post-up validation,再运行 `bun scripts/cli.ts microservice health code-queue`、`bun scripts/cli.ts microservice proxy code-queue /api/dev-ready --raw`、`bun scripts/cli.ts microservice proxy code-queue /api/tasks` 和 `bun scripts/cli.ts codex task <已有taskId>`,确认链路通过 backend-core、main-server provider-gateway 和 Code Queue 后端,且 task id 查询返回初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,且 `queue.devReady.ok=true`、`devReady.missingTools=[]`、`docker.versionOk=true`、`docker.composeOk=true`,必需工具包含 `docker`、`docker-compose`、`jq`、`ssh`、`rsync`、`pip3` 和 `unzip`;提交会产生较多命令输出的小任务后,`/health` 和 `/api/tasks` 仍必须在常规 CLI 超时内返回,容器内不得堆积无超时 healthcheck 进程。运行 `bun scripts/cli.ts microservice proxy code-queue /api/dev-containers/D601/start --method POST --raw`(或等价同源 POST),确认 Code Queue 拉起 D601 `unidesk-codex-dev-D601` 开发容器,返回 JSON 中 `masterProxy.mode=ssh-tun-nat`、`verification.pingGoogleOk=true`、`directPingEvidence` 表明建隧道前直连失败、`pingGoogleLog` 包含 `PING google.com` 和 `0% packet loss`,且 `masterProxyEvidenceAfter` 中 `UNIDESK-CODEX-DEV-D601` NAT 链或 `tun601` 计数比 ping 前增长,证明开发容器网络经 master server 全局代理而不是 D601 本地出网。再用 `bun scripts/cli.ts microservice proxy code-queue /api/tasks --raw` 确认返回的 `queue.executionProviders` 至少包含 `main-server` 和 `D601`,`main-server` 的 `defaultWorkdir=/root/unidesk`,`D601` 的 `defaultWorkdir=/home/ubuntu`;通过 API 提交任务时 `providerId=main-server` 必须在本机容器执行,`providerId=D601` 必须自动复用/拉起 D601 开发容器并在任务 JSON、Trace summary 和日志中显示 `providerId=D601`、`cwd=/home/ubuntu`。随后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / Code Queue`,确认页面显示默认模型 `gpt-5.5`、默认执行 Provider `main-server`、默认工作目录 `/root/unidesk`、Provider 下拉菜单包含 `D601 · /home/ubuntu`、模型下拉菜单包含 `gpt-5.4-mini`/`gpt-5.4`/`gpt-5.5`、入队份数、队列指标、任务 ID、复制任务 ID、引用按钮、任务耗时、引用任务 ID、清空输入、创建成功提示、任务提交表单、Codex CLI-like 输出、attempt 表、MiniMax/fallback judge 状态、追加 prompt、打断和重试控件;通过页面提交一个小任务,确认任务进入 queued/running/succeeded 或可解释的 failed 状态,并且输出区能看到运行中的 Codex 消息。批量验收时设置 `入队份数=5` 或用 `---` 分隔 5 段 prompt,一次性入队 5 条任务,确认 5 条任务按顺序运行并全部进入 succeeded 或可解释的非成功终态,不能只运行第一条后停止;其中任一任务被 judge 判定 `fail` 时只能把当前任务标为 failed,后续 queued 任务仍必须继续推进。测试异常中断时可以提交长任务后点击 `打断`,确认任务变为 canceled 或被 judge 标记为非成功终态;自动重试只应在服务端/传输异常、任务正常结束但 execution record 显示未完成、或 judge 判定 retry 时发生;retry 必须复用已有 Codex thread 并 append 继续执行 prompt,只有当前任务 complete 后才推进队列中的下一个任务。MiniMax judge 必须能处理 Markdown fence/夹杂文本等 JSON 去噪;若去噪后仍失败,必须把解析错误和上一轮去噪前原始回答反馈给 MiniMax 修复后重试,日志中应出现 `judge_json_parse_retry`,且 repair 成功时仍以 `source=minimax` 返回。Codex provider key 只能通过 `OPENAI_API_KEY`、`CRS_OAI_KEY` 这类运行时环境透传,MiniMax API key 只能通过 `UNIDESK_CODE_QUEUE_MINIMAX_API_KEY` 这类运行时环境传入,禁止写入 `config.json`、Dockerfile、源码或测试文档。 ## T24 MET Nonlinear D601 GPU User Service diff --git a/config.json b/config.json index e5a48692..c37e0ec7 100644 --- a/config.json +++ b/config.json @@ -321,22 +321,22 @@ } }, { - "id": "codex-queue", - "name": "Codex Queue", + "id": "baidu-netdisk", + "name": "Baidu Netdisk", "providerId": "main-server", - "description": "Codex Queue 是主 server 承载的 Codex app-server 编排用户服务,用于串行任务队列、运行中输出、追加 prompt、打断会话、异常中断判定和自动重试。", + "description": "容器化百度网盘存储用户服务,提供 OAuth 设备码登录、应用目录浏览和 staging 目录上传下载任务。", "repository": { "url": "https://github.com/pikasTech/unidesk", - "commitId": "2aaf0447a62c336f3a488d77516edbf05ff1d742", - "dockerfile": "src/components/microservices/codex-queue/Dockerfile", + "commitId": "ae462ed9ef8057909fee9eabfadce5ed55e958a2", + "dockerfile": "src/components/microservices/baidu-netdisk/Dockerfile", "composeFile": "docker-compose.yml", - "composeService": "codex-queue", - "containerName": "codex-queue-backend" + "composeService": "baidu-netdisk", + "containerName": "baidu-netdisk-backend" }, "backend": { - "nodeBaseUrl": "http://codex-queue:4222", - "nodeBindHost": "codex-queue", - "nodePort": 4222, + "nodeBaseUrl": "http://baidu-netdisk:4244", + "nodeBindHost": "baidu-netdisk", + "nodePort": 4244, "proxyMode": "provider-gateway-http", "frontendOnly": true, "public": false, @@ -352,6 +352,139 @@ "/api/" ], "healthPath": "/health", + "timeoutMs": 120000 + }, + "development": { + "providerId": "main-server", + "sshPassthrough": true, + "worktreePath": "/root/unidesk" + }, + "frontend": { + "route": "/apps/baidu-netdisk", + "integrated": true + } + }, + { + "id": "filebrowser", + "name": "File Browser D518", + "providerId": "D518", + "description": "File Browser WebUI 用户服务,部署在 D518 WSL provider Docker 中,挂载 provider host / 到 /srv,包含 /mnt/c 等 Windows 盘符;主 server 不再运行 File Browser 容器以节省资源。", + "repository": { + "url": "https://github.com/filebrowser/filebrowser", + "commitId": "ca5e249e3c0c94159c2136a0cd431a424eb18472", + "dockerfile": "docker.io/filebrowser/filebrowser:v2.63.3", + "composeFile": "docker run --restart unless-stopped", + "composeService": "unidesk-filebrowser-d518", + "containerName": "unidesk-filebrowser-d518" + }, + "backend": { + "nodeBaseUrl": "http://host.docker.internal:4251", + "nodeBindHost": "0.0.0.0", + "nodePort": 4251, + "proxyMode": "provider-gateway-http", + "frontendOnly": true, + "public": false, + "allowedMethods": [ + "GET", + "HEAD", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "allowedPathPrefixes": [ + "/" + ], + "healthPath": "/health", + "timeoutMs": 120000 + }, + "development": { + "providerId": "D518", + "sshPassthrough": true, + "worktreePath": "/" + }, + "frontend": { + "route": "/apps/filebrowser", + "integrated": true + } + }, + { + "id": "filebrowser-d601", + "name": "File Browser D601", + "providerId": "D601", + "description": "File Browser WebUI 用户服务,部署在 D601 WSL provider Docker 中,挂载 provider host / 到 /srv,包含 /mnt/c 等 Windows 盘符。", + "repository": { + "url": "https://github.com/filebrowser/filebrowser", + "commitId": "ca5e249e3c0c94159c2136a0cd431a424eb18472", + "dockerfile": "docker.io/filebrowser/filebrowser:v2.63.3", + "composeFile": "docker run --restart unless-stopped", + "composeService": "unidesk-filebrowser-d601", + "containerName": "unidesk-filebrowser-d601" + }, + "backend": { + "nodeBaseUrl": "http://host.docker.internal:4251", + "nodeBindHost": "127.0.0.1", + "nodePort": 4251, + "proxyMode": "provider-gateway-http", + "frontendOnly": true, + "public": false, + "allowedMethods": [ + "GET", + "HEAD", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "allowedPathPrefixes": [ + "/" + ], + "healthPath": "/health", + "timeoutMs": 120000 + }, + "development": { + "providerId": "D601", + "sshPassthrough": true, + "worktreePath": "/" + }, + "frontend": { + "route": "/apps/filebrowser", + "integrated": true + } + }, + { + "id": "code-queue", + "name": "Code Queue", + "providerId": "main-server", + "description": "Code Queue 是主 server 承载的平台无关代码代理队列用户服务,通过 port 层适配 Codex 与 OpenCode,用于串行任务队列、运行中输出、追加 prompt、打断会话、异常中断判定和自动重试。", + "repository": { + "url": "https://github.com/pikasTech/unidesk", + "commitId": "2aaf0447a62c336f3a488d77516edbf05ff1d742", + "dockerfile": "src/components/microservices/code-queue/Dockerfile", + "composeFile": "docker-compose.yml", + "composeService": "code-queue", + "containerName": "code-queue-backend" + }, + "backend": { + "nodeBaseUrl": "http://code-queue:4222", + "nodeBindHost": "code-queue", + "nodePort": 4222, + "proxyMode": "provider-gateway-http", + "frontendOnly": true, + "public": false, + "allowedMethods": [ + "GET", + "HEAD", + "POST", + "DELETE", + "PATCH" + ], + "allowedPathPrefixes": [ + "/health", + "/logs", + "/api/" + ], + "healthPath": "/health", "timeoutMs": 30000 }, "development": { @@ -360,7 +493,7 @@ "worktreePath": "/root/unidesk" }, "frontend": { - "route": "/apps/codex-queue", + "route": "/apps/code-queue", "integrated": true } } diff --git a/docker-compose.yml b/docker-compose.yml index a3b3284a..d59bd895 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: - "-c" - "logging_collector=on" - "-c" - - "log_directory=/var/log/unidesk" + - "log_directory=/var/log/unidesk/${UNIDESK_LOG_DAY}" - "-c" - "log_filename=${UNIDESK_LOG_PREFIX}_database.log" - "-c" @@ -56,7 +56,8 @@ services: DATABASE_VOLUME_NAME: "${UNIDESK_DATABASE_VOLUME}" DATABASE_VOLUME_SIZE: "${UNIDESK_DATABASE_VOLUME_SIZE}" MICROSERVICES_JSON: "${UNIDESK_MICROSERVICES_JSON:-[]}" - LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_PREFIX}_backend-core.jsonl" + LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_backend-core.jsonl" + UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}" volumes: - ${UNIDESK_LOG_DIR}:/var/log/unidesk healthcheck: @@ -81,8 +82,8 @@ services: TODO_NOTE_BACKEND_ONLY: "1" DATABASE_URL: "postgres://${UNIDESK_DATABASE_USER}:${UNIDESK_DATABASE_PASSWORD}@database:5432/${UNIDESK_DATABASE_NAME}" TODO_NOTE_LOGS_DIR: "logs" - LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_PREFIX}_todo-note.jsonl" - TODO_NOTE_LOG_PATH: "/var/log/unidesk/${UNIDESK_LOG_PREFIX}_todo-note.jsonl" + LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_todo-note.jsonl" + TODO_NOTE_LOG_PATH: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_todo-note.jsonl" TODO_NOTE_REMINDER_CLAUDEQQ_ENABLED: "${UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_ENABLED:-true}" TODO_NOTE_REMINDER_CLAUDEQQ_BASE_URL: "${UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_BASE_URL:-http://backend-core:8080/api/microservices/claudeqq/proxy}" TODO_NOTE_REMINDER_CLAUDEQQ_TARGET_TYPE: "${UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TARGET_TYPE:-private}" @@ -100,12 +101,14 @@ services: timeout: 3s retries: 20 - codex-queue: + code-queue: build: context: . - dockerfile: src/components/microservices/codex-queue/Dockerfile - container_name: codex-queue-backend + dockerfile: src/components/microservices/code-queue/Dockerfile + container_name: code-queue-backend restart: unless-stopped + mem_limit: 600m + memswap_limit: 1536m depends_on: - database - backend-core @@ -115,32 +118,46 @@ services: HOST: "0.0.0.0" PORT: "4222" DATABASE_URL: "postgres://${UNIDESK_DATABASE_USER}:${UNIDESK_DATABASE_PASSWORD}@database:5432/${UNIDESK_DATABASE_NAME}" - CODEX_QUEUE_DATA_DIR: "/var/lib/unidesk/codex-queue" - CODEX_QUEUE_WORKDIR: "/root/unidesk" - CODEX_QUEUE_CODEX_HOME: "/var/lib/unidesk/codex-queue/codex-home" - CODEX_QUEUE_SOURCE_CODEX_CONFIG: "/root/.codex/config.toml" - CODEX_QUEUE_DEFAULT_MODEL: "gpt-5.5" - CODEX_QUEUE_MODELS: "gpt-5.5,gpt-5.4-mini,gpt-5.4" - CODEX_QUEUE_MODEL_REASONING_EFFORTS: "gpt-5.5=xhigh" - CODEX_QUEUE_SANDBOX: "danger-full-access" - CODEX_QUEUE_APPROVAL_POLICY: "never" - CODEX_QUEUE_MAX_ATTEMPTS: "99" - CODEX_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: "${UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_ENABLED:-true}" - CODEX_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: "${UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL:-http://backend-core:8080/api/microservices/claudeqq/proxy}" - CODEX_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE: "${UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE:-private}" - CODEX_QUEUE_NOTIFY_CLAUDEQQ_USER_ID: "${UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_USER_ID:-645275593}" - CODEX_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID: "${UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID:-}" - CODEX_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS: "${UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS:-12000}" - CODEX_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS: "${UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS:-15000}" - CODEX_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS: "${UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS:-3}" + CODE_QUEUE_DATA_DIR: "/var/lib/unidesk/code-queue" + CODE_QUEUE_WORKDIR: "/root/unidesk" + CODE_QUEUE_CODEX_HOME: "/var/lib/unidesk/code-queue/codex-home" + CODE_QUEUE_OPENCODE_XDG_DIR: "/var/lib/unidesk/code-queue/opencode-xdg" + CODE_QUEUE_SOURCE_CODEX_CONFIG: "/root/.codex/config.toml" + CODE_QUEUE_DEFAULT_MODEL: "gpt-5.5" + CODE_QUEUE_MODELS: "gpt-5.5,gpt-5.4-mini,gpt-5.4,minimax-m2.7" + CODE_QUEUE_MODEL_REASONING_EFFORTS: "gpt-5.5=xhigh" + CODE_QUEUE_SANDBOX: "danger-full-access" + CODE_QUEUE_APPROVAL_POLICY: "never" + CODE_QUEUE_MAX_ATTEMPTS: "99" + CODE_QUEUE_MAX_ACTIVE_QUEUES: "${UNIDESK_CODE_QUEUE_MAX_ACTIVE_QUEUES:-1}" + NODE_OPTIONS: "${UNIDESK_CODE_QUEUE_NODE_OPTIONS:---max-old-space-size=768}" + CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS: "${UNIDESK_CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS:-10}" + CODE_QUEUE_IN_MEMORY_EVENT_RECORDS: "${UNIDESK_CODE_QUEUE_IN_MEMORY_EVENT_RECORDS:-10}" + CODE_QUEUE_CODEX_SQLITE_LOG_EXPORT_BATCH_SIZE: "${UNIDESK_CODE_QUEUE_CODEX_SQLITE_LOG_EXPORT_BATCH_SIZE:-500}" + CODE_QUEUE_MAIN_PROVIDER_ID: "main-server" + CODE_QUEUE_REMOTE_WORKDIR: "${UNIDESK_CODE_QUEUE_REMOTE_WORKDIR:-/home/ubuntu}" + CODE_QUEUE_EXECUTION_PROVIDER_IDS: "${UNIDESK_CODE_QUEUE_EXECUTION_PROVIDER_IDS:-D601}" + CODE_QUEUE_DEV_CONTAINER_MASTER_HOST: "${UNIDESK_PUBLIC_HOST}" + CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID: "${UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID:-D601}" + CODE_QUEUE_DEV_CONTAINER_IMAGE: "${UNIDESK_CODE_QUEUE_DEV_CONTAINER_IMAGE:-}" + CODE_QUEUE_DEV_CONTAINER_WORKDIR: "${UNIDESK_CODE_QUEUE_DEV_CONTAINER_WORKDIR:-/home/ubuntu}" + CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: "${UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED:-true}" + CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: "${UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL:-http://backend-core:8080/api/microservices/claudeqq/proxy}" + CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE: "${UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE:-private}" + CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID: "${UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID:-645275593}" + CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID: "${UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID:-}" + CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS: "${UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS:-12000}" + CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS: "${UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS:-15000}" + CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS: "${UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS:-3}" OPENAI_API_KEY: "${OPENAI_API_KEY:-}" CRS_OAI_KEY: "${CRS_OAI_KEY:-}" - MINIMAX_API_KEY: "${UNIDESK_CODEX_QUEUE_MINIMAX_API_KEY:-}" - MINIMAX_API_BASE: "${UNIDESK_CODEX_QUEUE_MINIMAX_API_BASE:-https://api.minimaxi.com/v1}" - MINIMAX_MODEL: "${UNIDESK_CODEX_QUEUE_MINIMAX_MODEL:-MiniMax-M2.7}" - MINIMAX_JUDGE_TIMEOUT_MS: "${UNIDESK_CODEX_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS:-60000}" - MINIMAX_JUDGE_REPAIR_ATTEMPTS: "${UNIDESK_CODEX_QUEUE_MINIMAX_JUDGE_REPAIR_ATTEMPTS:-2}" - LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_PREFIX}_codex-queue.jsonl" + MINIMAX_API_KEY: "${UNIDESK_CODE_QUEUE_MINIMAX_API_KEY:-}" + MINIMAX_API_BASE: "${UNIDESK_CODE_QUEUE_MINIMAX_API_BASE:-https://api.minimaxi.com/v1}" + MINIMAX_MODEL: "${UNIDESK_CODE_QUEUE_MINIMAX_MODEL:-MiniMax-M2.7}" + MINIMAX_JUDGE_TIMEOUT_MS: "${UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS:-60000}" + MINIMAX_JUDGE_REPAIR_ATTEMPTS: "${UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_REPAIR_ATTEMPTS:-2}" + LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_code-queue.jsonl" + UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}" volumes: - /var/run/docker.sock:/var/run/docker.sock - .:/root/unidesk @@ -148,7 +165,7 @@ services: - /root/.codex/config.toml:/root/.codex/config.toml:ro - ${UNIDESK_HOST_ROOT_SSH_DIR:-/root/.ssh}:/root/.ssh:ro - ${UNIDESK_LOG_DIR}:/var/log/unidesk - - ./.state/codex-queue:/var/lib/unidesk/codex-queue + - ./.state/code-queue:/var/lib/unidesk/code-queue extra_hosts: - "host.docker.internal:host-gateway" healthcheck: @@ -172,7 +189,8 @@ services: HOST: "0.0.0.0" PORT: "4233" DATABASE_URL: "postgres://${UNIDESK_DATABASE_USER}:${UNIDESK_DATABASE_PASSWORD}@database:5432/${UNIDESK_DATABASE_NAME}" - LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_PREFIX}_project-manager.jsonl" + LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_project-manager.jsonl" + UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}" volumes: - ${UNIDESK_LOG_DIR}:/var/log/unidesk healthcheck: @@ -181,6 +199,37 @@ services: timeout: 3s retries: 20 + baidu-netdisk: + image: baidu-netdisk + build: + context: . + dockerfile: src/components/microservices/baidu-netdisk/Dockerfile + container_name: baidu-netdisk-backend + restart: unless-stopped + depends_on: + - database + expose: + - "4244" + environment: + HOST: "0.0.0.0" + PORT: "4244" + DATABASE_URL: "postgres://${UNIDESK_DATABASE_USER}:${UNIDESK_DATABASE_PASSWORD}@database:5432/${UNIDESK_DATABASE_NAME}" + BAIDU_NETDISK_CLIENT_ID: "${UNIDESK_BAIDU_NETDISK_CLIENT_ID:-}" + BAIDU_NETDISK_CLIENT_SECRET: "${UNIDESK_BAIDU_NETDISK_CLIENT_SECRET:-}" + BAIDU_NETDISK_TOKEN_KEY: "${UNIDESK_BAIDU_NETDISK_TOKEN_KEY:-}" + BAIDU_NETDISK_APP_ROOT: "${UNIDESK_BAIDU_NETDISK_APP_ROOT:-/apps/UniDeskBaiduNetdisk}" + BAIDU_NETDISK_STAGING_DIR: "/data/staging" + LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_baidu-netdisk.jsonl" + UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}" + volumes: + - ${UNIDESK_LOG_DIR}:/var/log/unidesk + - ./.state/baidu-netdisk/staging:/data/staging + healthcheck: + test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:4244/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 5s + timeout: 3s + retries: 20 + frontend: build: context: . @@ -200,7 +249,8 @@ services: AUTH_PASSWORD: "${UNIDESK_AUTH_PASSWORD}" SESSION_SECRET: "${UNIDESK_SESSION_SECRET}" SESSION_TTL_SECONDS: "${UNIDESK_SESSION_TTL_SECONDS}" - LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_PREFIX}_frontend.jsonl" + LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_frontend.jsonl" + UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}" volumes: - ${UNIDESK_LOG_DIR}:/var/log/unidesk healthcheck: @@ -237,7 +287,8 @@ services: PROVIDER_UPGRADE_COMPOSE_PROJECT: "${UNIDESK_PROVIDER_UPGRADE_COMPOSE_PROJECT}" PROVIDER_UPGRADE_SERVICE: "${UNIDESK_PROVIDER_UPGRADE_SERVICE}" PROVIDER_UPGRADE_RUNNER_IMAGE: "${UNIDESK_PROVIDER_UPGRADE_RUNNER_IMAGE}" - LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_PREFIX}_provider-gateway.jsonl" + LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_provider-gateway.jsonl" + UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}" HOST_SSH_HOST: "${UNIDESK_HOST_SSH_HOST}" HOST_SSH_PORT: "${UNIDESK_HOST_SSH_PORT}" HOST_SSH_USER: "${UNIDESK_HOST_SSH_USER}" diff --git a/docs/issue/baidu-netdisk-env-setup.md b/docs/issue/baidu-netdisk-env-setup.md new file mode 100644 index 00000000..5faa8d88 --- /dev/null +++ b/docs/issue/baidu-netdisk-env-setup.md @@ -0,0 +1,127 @@ +# Baidu Netdisk 环境变量配置说明 + +Date: 2026-05-11 + +## 我能补什么,不能补什么 + +我可以在本机生成并持久化这个本地密钥: + +- `UNIDESK_BAIDU_NETDISK_TOKEN_KEY`:UniDesk 本地随机密钥,只用于把百度 `access_token` / `refresh_token` 加密后写入 PostgreSQL。 + +我不能代替你生成下面两个值,因为它们属于你的百度网盘开放平台应用: + +- `UNIDESK_BAIDU_NETDISK_CLIENT_ID`:百度 OAuth `client_id`,在部分百度控制台页面也叫 AppKey/API Key。 +- `UNIDESK_BAIDU_NETDISK_CLIENT_SECRET`:百度 OAuth `client_secret`,在部分百度控制台页面也叫 Secret Key。 + +截至 2026-05-11,本机已经在 `.state/docker-compose.env` 中生成并写入了 `UNIDESK_BAIDU_NETDISK_TOKEN_KEY`。百度应用的 client id 和 client secret 仍保持未配置状态,因为它们是账号归属的密钥,不能猜测,也不能提交到仓库。 + +## 获取百度应用凭证 + +1. 打开百度网盘开放平台控制台,为 UniDesk 创建一个应用,或复用你已有的应用。 +2. 开启 OAuth 和网盘文件 API 所需权限;当前服务请求的 scope 是 `basic,netdisk`。 +3. 复制应用的 OAuth client id/AppKey 和 client secret/Secret Key。 +4. 妥善保管密钥,不要把它粘贴到 `config.json`、文档、源码、git commit、截图、issue 评论或前端代码里。 + +官方参考: + +- 开放平台概览:https://pan.baidu.com/union/doc/nksg0sbfs +- 设备码模式:https://pan.baidu.com/union/doc/fl1x114ti +- 授权码模式:https://pan.baidu.com/union/doc/al0rwqzzl + +## 配置这台主机 + +UniDesk CLI 会把 Docker 运行时变量写到 `.state/docker-compose.env`;该文件已被 git 忽略,适合放本机密钥。请把下面占位值替换为真实百度凭证: + +```bash +cd /root/unidesk +python3 - <<'PY' +from pathlib import Path + +updates = { + "UNIDESK_BAIDU_NETDISK_CLIENT_ID": "paste-baidu-client-id-here", + "UNIDESK_BAIDU_NETDISK_CLIENT_SECRET": "paste-baidu-client-secret-here", + # Optional. Keep the existing value unless you intentionally want another app-folder name. + "UNIDESK_BAIDU_NETDISK_APP_ROOT": "/apps/UniDeskBaiduNetdisk", +} + +path = Path(".state/docker-compose.env") +lines = path.read_text("utf-8").splitlines() if path.exists() else [] +seen = set() +for index, line in enumerate(lines): + if "=" not in line: + continue + key = line.split("=", 1)[0] + if key in updates: + lines[index] = f"{key}={updates[key]}" + seen.add(key) +for key, value in updates.items(): + if key not in seen: + lines.append(f"{key}={value}") +path.parent.mkdir(parents=True, exist_ok=True) +path.write_text("\n".join(lines) + "\n", "utf-8") +PY +``` + +也可以在当前 shell 里临时 export,然后运行会调用 `writeComposeEnv` 的 UniDesk CLI 命令,例如 `server rebuild baidu-netdisk`。CLI 会把这些 shell 变量持久化到 `.state/docker-compose.env`: + +```bash +cd /root/unidesk +export UNIDESK_BAIDU_NETDISK_CLIENT_ID='paste-baidu-client-id-here' +export UNIDESK_BAIDU_NETDISK_CLIENT_SECRET='paste-baidu-client-secret-here' +export UNIDESK_BAIDU_NETDISK_APP_ROOT='/apps/UniDeskBaiduNetdisk' +bun scripts/cli.ts server rebuild baidu-netdisk +``` + +## 应用并验证 + +填好凭证后,只重建 Baidu Netdisk 后端,并等待异步 job 完成: + +```bash +cd /root/unidesk +bun scripts/cli.ts server rebuild baidu-netdisk +bun scripts/cli.ts job status latest +``` + +然后验证服务并启动二维码/设备码登录流程: + +```bash +bun scripts/cli.ts microservice health baidu-netdisk +bun scripts/cli.ts microservice proxy baidu-netdisk /api/auth/device/start --method POST --raw +``` + +手动扫码授权前的预期状态: + +- `health.body.auth.configured` 为 `true`。 +- `health.body.auth.clientIdConfigured` 为 `true`。 +- `health.body.auth.clientSecretConfigured` 为 `true`。 +- `health.body.auth.tokenKeyConfigured` 为 `true`。 +- `/api/auth/device/start` 返回设备登录会话、二维码/用户码元数据,不返回原始百度 token。 + +扫码并授权后: + +```bash +bun scripts/cli.ts microservice proxy baidu-netdisk '/api/account' --raw +bun scripts/cli.ts microservice proxy baidu-netdisk '/api/files?dir=/apps/UniDeskBaiduNetdisk&limit=20' --raw +bun scripts/cli.ts microservice proxy baidu-netdisk /api/self-test --method POST --raw +``` + +`/api/files` 首次访问空应用目录时应返回 `ok=true` 和文件数组;如果远端应用目录还不存在,后端会先创建 `UNIDESK_BAIDU_NETDISK_APP_ROOT` 指向的 `/apps/...` 目录。`/api/self-test` 会生成小文本、上传、列表确认、下载并比较 MD5,适合授权完成后的端到端验收。 + +## Token Key 轮换 + +只应在首次登录前,或明确退出登录并丢弃旧加密 token 记录后,才轮换 `UNIDESK_BAIDU_NETDISK_TOKEN_KEY`。这个 key 用于解密 `baidu_netdisk_tokens` 和 `baidu_netdisk_auth_sessions` 中的既有加密记录;如果已有加密记录后再改 key,旧记录会不可读,需要重新扫码登录。 + +如果测试登录后必须轮换,先正常退出登录: + +```bash +bun scripts/cli.ts microservice proxy baidu-netdisk /api/auth/logout --method POST --raw +``` + +然后更新 `UNIDESK_BAIDU_NETDISK_TOKEN_KEY`,重建 `baidu-netdisk`,再重新发起二维码登录。 + +## 安全检查清单 + +- 百度凭证只放在 `.state/docker-compose.env`、root 管理的密钥系统或临时 shell 环境里。 +- 不要提交 `.env`、`.state/docker-compose.env`、access token、refresh token、dlink 或包含密钥的截图。 +- 不要把 `4244` 暴露到公网;Baidu Netdisk 必须继续走 UniDesk microservice proxy。 +- 成功登录后不要随意轮换 token key,除非已经准备重新登录。 diff --git a/docs/issue/baidu-netdisk-user-service.md b/docs/issue/baidu-netdisk-user-service.md new file mode 100644 index 00000000..ecf11e34 --- /dev/null +++ b/docs/issue/baidu-netdisk-user-service.md @@ -0,0 +1,255 @@ +# Baidu Netdisk User Service Research + +Date: 2026-05-11 + +Implementation note: the first UniDesk-integrated version now lives in this repo as a main-server private user service, with backend source at `src/components/microservices/baidu-netdisk`, Compose service `baidu-netdisk`, config id `baidu-netdisk`, and frontend page `src/components/frontend/src/baidu-netdisk.tsx`. It keeps the recommended v1 boundary: JSON control API through UniDesk microservice proxy, OAuth Device Code login, PostgreSQL-backed encrypted token/task state, and staging-directory upload/download jobs instead of browser byte streaming. + +Environment setup note: Baidu app credentials are account-owned secrets and must be supplied out of band. The local encryption key and exact host configuration steps are documented in `docs/issue/baidu-netdisk-env-setup.md`. + +## Goal + +Create a UniDesk user service that connects to Baidu Netdisk in a containerized way and exposes file storage operations such as login, browse, upload, download, move, rename and delete. The user-facing login should feel similar to the ClaudeQQ page: the UniDesk frontend shows a login card/QR, backend state, recent transfer jobs and explicit raw JSON buttons, while the business backend remains private behind the UniDesk microservice proxy. + +## Recommended Approach + +Build a small pure-backend user service named `baidu-netdisk` and integrate it as a UniDesk user service. Use Baidu Netdisk official OAuth/API directly in the backend for the first version; optionally add AList or CLI tools later as transfer workers, not as the primary auth or frontend. + +Why this route: + +- The official API supports OAuth scopes `basic,netdisk`, QR-like login flows, file listing, metadata/dlink retrieval, multipart upload and file management. +- A ClaudeQQ-like login UI maps well to Baidu's Device Code flow: backend requests a `device_code`, frontend displays `qrcode_url` and `user_code`, backend polls token status at the documented interval. +- The UniDesk proxy currently handles text JSON request/response bodies, with a 1 MiB incoming body limit and an 8 MiB response body limit. Large file bytes should therefore not be pushed through `/api/microservices/*/proxy` in v1. +- Keeping tokens and jobs in PostgreSQL gives restart recovery and avoids storing credentials in local JSON files. + +## Important UniDesk Constraint + +Current `microservice.http` is suitable for control-plane JSON, not bulk binary file transfer: + +- backend-core reads non-GET bodies using `req.text()` and rejects bodies larger than 1 MiB. +- provider-gateway reads upstream responses using `response.text()` and caps returned body text at 8 MiB. +- The proxy only forwards `content-type`, not `range`, `content-disposition` or arbitrary binary headers. + +So v1 should expose APIs such as `POST /api/transfers/upload-from-path` and `POST /api/transfers/download-to-path`, where the backend container reads/writes files from a mounted staging directory. If browser-to-local uploads/downloads are required, add a separate binary streaming capability to backend-core/provider-gateway in a future change set. That future gateway change would trigger the provider-gateway version-bump rule. + +## Baidu Netdisk Access Model + +### Login + +Use Device Code as the default container login: + +1. `POST /api/auth/device/start` calls `GET https://openapi.baidu.com/oauth/2.0/device/code?response_type=device_code&client_id=&scope=basic,netdisk`. +2. Backend stores `device_code`, `user_code`, `verification_url`, `qrcode_url`, `expires_in` and `interval` in PostgreSQL. +3. UniDesk frontend displays the QR code and user code. +4. `GET /api/auth/device/status?sessionId=...` returns current login state; backend polls Baidu token endpoint no more frequently than the returned interval and at least 5 seconds. +5. On success, backend stores `access_token`, `refresh_token`, `expires_in`, `scope`, account metadata and refresh timestamps. + +Authorization Code mode is also possible and can pass `qrcode=1`, but it requires a redirect URI and callback endpoint. Device Code is simpler for a private container behind UniDesk because the browser only needs to display a QR URL and poll backend state. + +Token handling requirements: + +- Access token lifetime is 30 days in the Baidu docs. +- Refresh token is long-lived but the Netdisk docs say it is single-use: after a refresh, store the new refresh token immediately and never retry with the old one in a loop. +- Use a PostgreSQL row lock or advisory lock around refresh to avoid two workers spending the same refresh token concurrently. +- Never log tokens or dlinks; health/status endpoints should only expose redacted auth state. + +### Scope and App Folder + +Use `scope=basic,netdisk`. Official docs describe third-party app data under `/apps/` and visible to users as `/我的应用数据/`. The service should default to a configured app root, for example `/apps/UniDeskBaiduNetdisk`, and reject paths outside that root unless the final approved app permission explicitly allows broader access. + +### Browse and Metadata + +Useful official endpoints: + +- User info: `GET /rest/2.0/xpan/nas?method=uinfo`. +- Quota: `GET /api/quota`. +- List directory: `GET /rest/2.0/xpan/file?method=list` with `dir`, paging and sort parameters. +- File metadata and download URL: `GET /rest/2.0/xpan/multimedia?method=filemetas&fsids=[...]&dlink=1`. + +### Upload + +Official multipart upload sequence: + +1. Compute full-file MD5 and per-part MD5 list. For normal users, part size is fixed at 4 MiB. Docs list higher part and total file limits for paid membership tiers. +2. `POST /rest/2.0/xpan/file?method=precreate` with `path`, `size`, `isdir=0`, `autoinit=1`, `rtype` and `block_list`. +3. `GET /rest/2.0/pcs/file?method=locateupload&appid=250528&uploadid=...&upload_version=2.0` and choose an HTTPS upload domain from `servers`. +4. `POST https:///rest/2.0/pcs/superfile2?method=upload&type=tmpfile&path=...&uploadid=...&partseq=N` as multipart form `file=@chunk` for each required part. +5. `POST /rest/2.0/xpan/file?method=create` with the same `path`, `size`, `isdir`, `rtype`, `uploadid` and ordered `block_list`. + +For small files, the official single-step upload endpoint can be a convenience path, but the multipart path is enough for all sizes and gives progress/resume semantics. + +### Download + +Official download sequence: + +1. Get `fs_id` from list/search. +2. Request file metadata with `dlink=1`. +3. Fetch `dlink&access_token=` using `User-Agent: pan.baidu.com`. +4. Respect `302` redirects, `Range` for resume, and the documented 8-hour dlink lifetime. + +Because browsers cannot safely set the required `User-Agent` and current UniDesk proxy cannot stream large binary responses, the backend should download to staging storage in v1. Browser download can be offered later via a binary streaming proxy endpoint or a backend-owned short-lived internal file endpoint if the gateway/core are upgraded to stream bytes safely. + +## Third-Party Technology Options + +1. Official API in custom backend (recommended for v1) + - Best fit for UniDesk security and UI conventions. + - Precise control over token rotation, path sandboxing, transfer jobs and PostgreSQL persistence. + - Needs implementation of multipart upload/download resume, but the API flow is straightforward. + +2. AList as a sidecar or reference implementation + - AList already has a Baidu Netdisk driver and supports storage mounting through its own server. + - Useful if we want WebDAV-like access or want to validate behavior quickly. + - Treat it as an internal sidecar behind the `baidu-netdisk` backend; do not expose AList WebUI as the UniDesk frontend. + - Watch license/upgrade/security posture before embedding it into production. + +3. bypy as a Python worker + - Good for app-folder upload/download automation and quick scripts. + - Can run in a worker container for batch operations if we accept Python dependency and app-folder assumptions. + - Less ideal as the primary service API because UniDesk still needs its own auth state, job model and structured frontend. + +4. BaiduPCS-Go as a worker + - Strong CLI for batch transfers and resume behavior. + - Could be invoked from the service for jobs after controlled login/config injection. + - Avoid making CLI config files the credential authority; PostgreSQL should remain authoritative. + +5. Unofficial or cracked web APIs + - Avoid. They are unstable, hard to validate, and may violate Baidu terms or trigger account risk controls. + +## Proposed User Service Contract + +### Backend APIs + +Expose a pure JSON control API first: + +- `GET /health`: service, storage, auth, queue and Baidu API reachability summary. +- `GET /api/auth/status`: redacted configured/logged-in/auth-session summary. +- `POST /api/auth/device/start`: start QR/device login. +- `GET /api/auth/device/status?sessionId=...`: login state and QR metadata. OAuth `authorization_pending` and `slow_down` responses are normal pending states and must not be surfaced as frontend HTTP errors. +- `POST /api/auth/refresh`: force token refresh for diagnostics. +- `POST /api/auth/logout`: revoke local tokens and stop jobs. +- `GET /api/account`: user info and quota. +- `GET /api/files?dir=/apps/UniDeskBaiduNetdisk&start=0&limit=100`: directory listing. +- `GET /api/files/meta?fsids=...&dlink=0|1`: metadata, optionally dlink redacted by default. +- `POST /api/folders`: create folder through `method=create&isdir=1`. +- `POST /api/files/manage`: copy/move/rename/delete using `method=filemanager`. +- `POST /api/transfers/upload-from-path`: read a file inside the mounted staging directory and upload it to Baidu. +- `POST /api/transfers/download-to-path`: download a Baidu file to the staging directory. +- `POST /api/self-test`: create a tiny staging fixture, upload it, verify it appears in `/api/files`, download it back to staging and compare MD5. +- `GET /api/transfers`: list transfer jobs. +- `GET /api/transfers/{id}`: job detail, progress, retry and last error. +- `POST /api/transfers/{id}/cancel` and `POST /api/transfers/{id}/retry`. +- `GET /logs`: recent structured service logs with tokens/dlinks redacted. + +If a future binary proxy is added, extend with: + +- `POST /api/uploads/sessions` + chunk PUT/POST endpoints. +- `GET /api/downloads/{jobId}/stream` with Range support. + +### PostgreSQL Tables + +Minimum schema: + +- `baidu_netdisk_accounts(id, baidu_uid, username, avatar_url, vip_type, root_path, created_at, updated_at)`. +- `baidu_netdisk_tokens(account_id, access_token_ciphertext, refresh_token_ciphertext, expires_at, scope, generation, last_refresh_at)`. +- `baidu_netdisk_auth_sessions(id, device_code_ciphertext, user_code, verification_url, qrcode_url, expires_at, poll_interval_seconds, status, error, created_at, updated_at)`. +- `baidu_netdisk_transfer_jobs(id, account_id, direction, status, local_path, remote_path, fs_id, size_bytes, bytes_done, part_size, block_list_json, uploadid, retry_count, error, created_at, updated_at)`. +- `baidu_netdisk_transfer_events(id, job_id, level, message, data_json, created_at)`. + +Token encryption key should come from an environment variable such as `BAIDU_NETDISK_TOKEN_KEY`; no secrets should be committed. + +### Container and Deployment + +If deployed on D601, use the normal compute-node user-service boundary: + +```json +{ + "id": "baidu-netdisk", + "name": "Baidu Netdisk", + "providerId": "D601", + "description": "Containerized Baidu Netdisk storage gateway with QR/device login and transfer jobs.", + "repository": { + "url": "https://github.com/pikasTech/baidu-netdisk-unidesk", + "commitId": "", + "dockerfile": "Dockerfile", + "composeFile": "docker-compose.unidesk.yml", + "composeService": "baidu-netdisk", + "containerName": "baidu-netdisk-backend" + }, + "backend": { + "nodeBaseUrl": "http://host.docker.internal:3295", + "nodeBindHost": "127.0.0.1", + "nodePort": 3295, + "proxyMode": "provider-gateway-http", + "frontendOnly": true, + "public": false, + "allowedMethods": ["GET", "HEAD", "POST", "DELETE"], + "allowedPathPrefixes": ["/health", "/logs", "/api/"], + "healthPath": "/health", + "timeoutMs": 30000 + }, + "development": { + "providerId": "D601", + "sshPassthrough": true, + "worktreePath": "/home/ubuntu/baidu-netdisk-unidesk" + }, + "frontend": { + "route": "/apps/baidu-netdisk", + "integrated": true + } +} +``` + +If deployed on main server, use a Compose service name such as `http://baidu-netdisk:4244` and add a root `docker-compose.yml` service. Main-server deployment is justified only if UniDesk needs central storage on the main server; otherwise D601 or another compute node is cleaner. + +### Frontend Page + +Add a dedicated `src/components/frontend/src/baidu-netdisk.tsx` page and route tab: + +- Login panel: QR image from `qrcode_url`, user code, expires timer, poll status, refresh QR and logout buttons. +- Account cards: username, UID, quota used/total, VIP state, app root path. +- File browser: breadcrumb rooted at `/apps/`, paginated table, folder creation, rename/delete/move controls. +- Transfer panel: upload-from-path form, download-to-path form, job rows, progress bars, speed, ETA, retry/cancel buttons. +- Safety text: private backend mapping, token storage redacted, no direct public Baidu token exposure. +- Raw JSON only behind explicit buttons, following existing user-service conventions. + +## Acceptance Plan + +Focused checks after implementation: + +- `bun scripts/cli.ts microservice list` shows `baidu-netdisk`, private backend, target provider and container summary. +- `bun scripts/cli.ts microservice health baidu-netdisk` returns `ok=true`, `service=baidu-netdisk`, `storage=postgres` and redacted auth state. +- `bun scripts/cli.ts microservice proxy baidu-netdisk /api/auth/device/start --method POST` returns a login session with QR/user-code metadata but no token. +- After manual QR authorization, `/api/account` and `/api/files?dir=` return user/quota/list data. +- Upload/download tests can use `bun scripts/cli.ts microservice proxy baidu-netdisk /api/self-test --method POST --raw`; the response must include a remote path in the app root, an `fsId`, succeeded upload/download jobs, and matching `expectedMd5`/`downloadedMd5`. +- Public port probes must fail for the service port; frontend access only through UniDesk. +- Playwright verifies `/app/baidu-netdisk/` renders shell, login card, account/quota, file browser, transfer panel and no naked JSON. + +Full regression should later add `microservice:catalog-baidu-netdisk`, `microservice:baidu-netdisk-health`, login-state checks and `frontend:baidu-netdisk-integrated-visible` to `scripts/src/e2e.ts`. + +## Open Risks + +- Baidu app review/permissions may block upload/download until the app is approved and scoped correctly. +- Device Code QR expires quickly; frontend needs clear countdown and refresh behavior. +- Refresh token is single-use per Netdisk docs; a race can force re-login if token rotation is not serialized. +- Browser direct download is not a v1 fit because official dlink download requires `User-Agent: pan.baidu.com` and current UniDesk proxy cannot stream large binary safely. +- Large upload/download jobs need resumable local job records and cleanup of temporary chunks/staged files. +- Using AList/BaiduPCS-Go/byPy may introduce third-party license and maintenance risk; keep their configs/token caches derived from UniDesk PostgreSQL, not authoritative. + +## Sources + +- Baidu Netdisk Open Platform overview: https://pan.baidu.com/union/doc/nksg0sbfs +- Authorization Code mode: https://pan.baidu.com/union/doc/al0rwqzzl +- Device Code mode: https://pan.baidu.com/union/doc/fl1x114ti +- User info: https://pan.baidu.com/union/doc/pksg0s9ns +- Quota: https://pan.baidu.com/union/doc/Cksg0s9ic +- File list: https://pan.baidu.com/union/doc/nksg0sat9 +- File metadata/dlink: https://pan.baidu.com/union/doc/Fksg0sbcm +- Precreate upload: https://pan.baidu.com/union/doc/3ksg0s9r7 +- Multipart upload: https://pan.baidu.com/union/doc/nksg0s9vi +- Create file/folder: https://pan.baidu.com/union/doc/rksg0sa17 +- Single-step upload: https://pan.baidu.com/union/doc/olkuuy5kz +- Locate upload domain: https://pan.baidu.com/union/doc/Mlvw5hfnr +- Download: https://pan.baidu.com/union/doc/pkuo3snyp +- File manager: https://pan.baidu.com/union/doc/mksg0s9l4 +- AList Baidu Netdisk driver docs: https://alist-repo.github.io/docs/guide/drivers/baidu.html +- bypy project page: https://pypi.org/project/bypy/ +- BaiduPCS-Go repository: https://github.com/qjfoidnh/BaiduPCS-Go diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 56f91bdb..04950d3b 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -12,16 +12,17 @@ UniDesk 的统一 CLI 入口是根目录 `scripts/cli.ts`,运行方式固定 - `server stop` 创建异步 job,在后台停止固定 Compose project 中的全部 UniDesk 服务。 - `server status` 查询公开端口、内部端口、Compose 容器、core/frontend/provider/database 健康检查和访问 URL。 - `server logs` 返回 `logs/` 文件日志和 Docker 容器日志的尾部,默认限制输出大小,避免日志爆炸。 -- `server rebuild ` 创建异步 job,先构建目标服务镜像,随后在 `.state/locks/server-compose.lock` 串行保护下用 `--no-deps --force-recreate` 替换目标 service 并等待容器 `healthy/running`;该命令用于替代手工删除容器的兜底流程,其中 `todo-note`、`codex-queue` 和 `project-manager` 只重建主 server 承载的对应后端,不会重建或删除 database 命名卷。 +- `server rebuild ` 创建异步 job,先构建目标服务镜像,随后在 `.state/locks/server-compose.lock` 串行保护下用 `--no-deps --force-recreate` 替换目标 service 并等待容器 `healthy/running`;该命令用于替代手工删除容器的兜底流程,其中 `todo-note`、`code-queue`、`project-manager` 和 `baidu-netdisk` 只重建主 server 承载的对应后端,不会重建或删除 database 命名卷。 +- `provider attach [--master-server URL] [--up] [--force]` 在新计算节点生成两项配置的 provider-gateway 挂载包:`.state/provider-.env` 默认只包含 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID`,`provider-.yml` 固定 Docker socket、`pid: "host"`、`restart: always`、只读 `/workspace` 和 SSH 维护私钥挂载;`--up` 会立即执行生成的 `docker compose up -d --build`。 - `ssh [ssh-like args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点;无后续参数时进入远端登录 shell,有后续参数时按 ssh 远端命令体验执行并返回远端 exit code。 - `ssh apply-patch [tool args...] < patch.diff` 直接调用远端注入的 `apply_patch` 工具,并把本地 stdin 中的标准 `*** Begin Patch` / `*** End Patch` patch 流透传给目标节点。 - `ssh py [script-args...] < script.py` 把本地 stdin 落到远端临时 `.py` 文件后再以 `python3 -u` 执行并自动清理,避免再手写 `'python3 -'`、heredoc 或多层引号;`script-args` 会按 argv 安全透传给远端脚本。 - `ssh skills [--scope all|wsl|windows] [--limit N]` 发现目标节点上的 WSL/Linux skill 根目录;当 provider 是 WSL 时同一次调用还会扫描 Windows 用户目录下的 `.agents/skills` 与 `.codex/skills`。 - `microservice list/status/health/proxy` 通过 backend-core 内网 API 管理挂载在计算节点 Docker 中的用户服务(底层命令名仍为 microservice);`health` 和 `proxy` 会走真实 backend-core -> provider-gateway -> 节点本机后端链路,`proxy` 对超大 body 默认输出有界预览,规则见 `docs/reference/microservices.md`。 -- `codex task ` 通过 Codex Queue 私有代理按任务 ID 查询结构化执行摘要;默认只返回有界 prompt/response 预览、最后 assistant message、最近工具调用摘要、attempt、judge、错误、耗时和 trace 翻页提示,适合在新队列任务中引用历史 session 且避免噪声爆炸。 -- `codex task --trace --tail|--from-start|--after-seq N|--before-seq N --limit N` 按页拉取 Codex Queue 的逻辑 trace;响应会返回 `nextAfterSeq`、`previousBeforeSeq`、`hasMore`、`hasBefore` 和下一页/上一页命令,默认 `--trace` 取最新一页,需要完整 prompt/最后 response 时加 `--full`。 +- `codex task ` 通过 Code Queue 私有代理按任务 ID 查询结构化执行摘要;默认只返回有界 prompt/response 预览、执行 Provider、工作目录、最后 assistant message、最近工具调用摘要、attempt、judge、错误、耗时和 trace 翻页提示,适合在新队列任务中引用历史 session 且避免噪声爆炸。 +- `codex task --trace --tail|--from-start|--after-seq N|--before-seq N --limit N` 按页拉取 Code Queue 的逻辑 trace;响应会返回 `nextAfterSeq`、`previousBeforeSeq`、`hasMore`、`hasBefore` 和下一页/上一页命令,默认 `--trace` 取最新一页,需要完整 prompt/最后 response 时加 `--full`。 - `codex output --tail|--from-start|--after-seq N|--before-seq N --limit N [--full-text]` 按原始 output seq 分页读取底层记录;当 trace 行提示 `commandOmittedLines`、`bodyOmittedLines` 或 `rawSeqs` 时,用该命令按 seq 补取完整信息,默认仍有单条文本预览上限,显式 `--full-text` 才返回该页全文。 -- `codex queues`、`codex queue create `、`codex move --queue ` 管理 Codex Queue 的多队列 lane;同一个 queue 内部串行执行,不同 queue 之间并行执行,迁移 queued/retry_wait 任务后会立即调度目标 queue。 +- Code Queue 多队列 lane 由 `codex` 命令命名空间管理:`queues` 列表、`queue create ` 创建、`move --queue ` 迁移;同一个 queue 内部串行执行,不同 queue 之间并行执行,迁移 queued/retry_wait 任务后会立即调度目标 queue。 - `job list` 与 `job status` 查询 `.state/jobs/` 文件系统状态,是异步命令的可观测入口。 - `debug health`、`debug dispatch` 与 `debug task` 走真实内部 core、WebSocket、数据库、provider、系统指标、Docker 状态和 Host SSH 维护桥流程,只用于开发调试,不写入 `TEST.md` 的正式验收步骤。 - `e2e run [--only pattern[,pattern...]] [--skip pattern[,pattern...]]` 使用 publicHost 派生的公开 frontend/provider ingress URL,并通过 Docker 内网验证 core API、PostgreSQL、provider self-connection、系统指标曲线、Docker 状态快照、provider.upgrade 预检和 Playwright 前端页面,是交付前的自动化 E2E 门禁;CLI 默认输出 check 状态摘要,完整诊断写入 `resultPath`,日常迭代应优先用 `--only` / `--skip` 跑最小必要集合。 @@ -30,13 +31,13 @@ UniDesk 的统一 CLI 入口是根目录 `scripts/cli.ts`,运行方式固定 长时操作采用 Fire-and-Forget 模式:CLI 创建 `.state/jobs/{jobId}.json`,后台进程执行真实命令,并将 stdout、stderr 分别写入 `.state/jobs/{jobId}.stdout.log` 与 `.state/jobs/{jobId}.stderr.log`。调用者通过 `bun scripts/cli.ts job status ` 查询进度和尾部输出。 -`server rebuild` 与 `server start`、`server stop` 一样必须通过返回的 job id 确认结果;不要把 `server rebuild codex-queue && server rebuild frontend` 理解成“前一个重建已完成”,因为两个命令只是在快速创建异步 job。重建 frontend 的标准流程是运行 `bun scripts/cli.ts server rebuild frontend`,随后轮询 `bun scripts/cli.ts job status ` 到 `succeeded`,再用 `server status` 或 `e2e run` 验证公网 frontend;重建 Todo Note 后端使用 `bun scripts/cli.ts server rebuild todo-note`,随后用 `microservice health todo-note` 和 `microservice proxy todo-note /api/instances` 验证;重建 Codex Queue 后端使用 `bun scripts/cli.ts server rebuild codex-queue`,随后用 `microservice health codex-queue` 和 `microservice proxy codex-queue /api/tasks` 验证;重建 Project Manager 后端使用 `bun scripts/cli.ts server rebuild project-manager`,随后用 `microservice health project-manager` 和 `microservice proxy project-manager /api/projects` 验证。不得把 `docker rm` 手工兜底当成正式交付步骤。 +`server rebuild` 与 `server start`、`server stop` 一样必须通过返回的 job id 确认结果;不要把 `server rebuild code-queue && server rebuild frontend` 理解成“前一个重建已完成”,因为两个命令只是在快速创建异步 job。重建 frontend 的标准流程是运行 `bun scripts/cli.ts server rebuild frontend`,随后轮询 `bun scripts/cli.ts job status ` 到 `succeeded`,再用 `server status` 或 `e2e run` 验证公网 frontend;重建 Todo Note 后端使用 `bun scripts/cli.ts server rebuild todo-note`,随后用 `microservice health todo-note` 和 `microservice proxy todo-note /api/instances` 验证;重建 Code Queue 后端使用 `bun scripts/cli.ts server rebuild code-queue`,随后用 `microservice health code-queue` 和 `microservice proxy code-queue /api/tasks` 验证;重建 Project Manager 后端使用 `bun scripts/cli.ts server rebuild project-manager`,随后用 `microservice health project-manager` 和 `microservice proxy project-manager /api/projects` 验证;重建 Baidu Netdisk 后端使用 `bun scripts/cli.ts server rebuild baidu-netdisk`,随后用 `microservice health baidu-netdisk` 和 `microservice proxy baidu-netdisk /api/transfers` 验证;重建 main server File Browser 使用 `bun scripts/cli.ts server rebuild filebrowser`,随后用 `microservice health filebrowser` 和 `microservice proxy filebrowser / --max-body-bytes 2000` 验证。不得把 `docker rm` 手工兜底当成正式交付步骤。 ## Output Contract 每条命令的最外层 JSON 包含 `ok`、`command` 和 `data` 或 `error`。失败时 CLI 设置非零退出码,但仍然输出 JSON 错误对象;错误对象应包含 `name`、`message` 和可用的 `stack`。 -`microservice proxy` 是面向人工验证的私有后端读取入口。正式写入型用户服务操作由 frontend 同源代理或 E2E 直接调用 backend-core 完成,并由 config 中的 `allowedMethods` 限制;CLI `proxy` 默认仍作为 GET/HEAD 读取验证入口。为了避免 Pipeline snapshot 这类超大业务 JSON 造成 CLI 输出爆炸,响应 body 超过默认阈值时会返回 `bodyOmitted=true`、`bodyPreview`、`bodyBytes` 和 `rawHint`;需要完整 body 时显式添加 `--raw`,或用 `--max-body-bytes ` 调整预览阈值。正式 frontend 展示仍应优先使用业务控件和 `__unideskArrayLimit` 这类展示级裁剪参数,而不是默认倾倒完整 JSON。 +`microservice proxy` 是面向人工验证的私有后端读取入口。正式写入型用户服务操作由 frontend 同源代理或 E2E 直接调用 backend-core 完成,并由 config 中的 `allowedMethods` 限制;CLI `proxy` 默认仍作为 GET/HEAD 读取验证入口,必要时可显式加 `--method POST|PUT|PATCH|DELETE` 调用无需自定义请求体的受控调试/自测端点,例如 `bun scripts/cli.ts microservice proxy baidu-netdisk /api/self-test --method POST --raw`。为了避免 Pipeline snapshot 这类超大业务 JSON 造成 CLI 输出爆炸,响应 body 超过默认阈值时会返回 `bodyOmitted=true`、`bodyPreview`、`bodyBytes` 和 `rawHint`;需要完整 body 时显式添加 `--raw`,或用 `--max-body-bytes ` 调整预览阈值。正式 frontend 展示仍应优先使用业务控件和 `__unideskArrayLimit` 这类展示级裁剪参数,而不是默认倾倒完整 JSON。 ## Debug Contract diff --git a/docs/reference/config.md b/docs/reference/config.md index 00588e62..db4e692e 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -16,7 +16,7 @@ TypeScript 运行时固定为 Bun。根目录 CLI、backend-core、frontend 和 ## Provider Gateway Metrics And Upgrade -`providerGateway.metrics.diskPath` 指定资源监控页的硬盘采样路径,默认是 `/`。`providerGateway.upgrade` 定义远程升级 provider-gateway 所需的 Compose project、service、仓库挂载路径、派生 env 文件和 updater runner 镜像;这些字段由 CLI 写入 `.state/docker-compose.env`,provider-gateway 只通过 WebSocket 接受 `provider.upgrade` 调度,不从隐藏环境或默认值静默补齐。远程升级没有 `enabled` 开关,长期接入节点必须具备 `mode: "schedule"` 一键升级能力;如果节点不满足升级前置条件,应修正 `PROVIDER_UPGRADE_*`、Docker socket 或仓库挂载,而不是在配置中禁用升级。 +`providerGateway.metrics.diskPath` 指定主 server 本机 provider 的硬盘采样路径,默认是 `/`。`providerGateway.upgrade` 定义主 server provider 远程升级所需的 Compose project、service、仓库挂载路径、派生 env 文件和 updater runner 镜像;这些字段由 CLI 写入 `.state/docker-compose.env`,主 server Compose 仍保持显式配置。外部计算节点可以使用 `provider attach` 的简化 env:只写 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID`,provider-gateway 会派生公网 provider ingress URL、默认 labels、心跳、Docker socket、监控路径和升级 Compose 名称,并通过 Docker inspect 反查 `/workspace` 的宿主挂载源;显式 `PROVIDER_*` 环境变量仍优先级最高。远程升级没有 `enabled` 开关,长期接入节点必须具备 `mode: "schedule"` 一键升级能力;如果节点不满足升级前置条件,应修正 Docker socket、仓库挂载或显式覆盖 `PROVIDER_UPGRADE_*`,而不是在配置中禁用升级。 ## SSH Forwarding @@ -26,7 +26,7 @@ TypeScript 运行时固定为 Bun。根目录 CLI、backend-core、frontend 和 `microservices` 定义挂载在计算节点或主 server Docker 中的非核心用户服务。用户服务是挂在 UniDesk 核心服务上的用户业务能力,缺少这些服务时 UniDesk 核心仍必须能运行。该数组只保存业务仓库 URL、commit id、业务仓库自身 Dockerfile/docker-compose 引用、provider 映射、节点后端端口和 UniDesk frontend 集成入口;不得把业务全量代码复制进 UniDesk。`backend.public` 必须为 `false`,`backend.frontendOnly` 必须为 `true`,`backend.allowedPathPrefixes` 必须限制到业务 API 前缀,`backend.allowedMethods` 必须显式列出允许代理的 HTTP 方法;浏览器只能通过 frontend 同源代理访问这些后端。详细规则见 `docs/reference/microservices.md`。 -主 server 承载的 Todo Note 用户服务使用 `providerId=main-server`、`nodeBaseUrl=http://todo-note:4211` 和 `allowedMethods=["GET","HEAD","POST","DELETE"]`,数据库使用主 PostgreSQL;Codex Queue 使用 `providerId=main-server`、`nodeBaseUrl=http://codex-queue:4222` 和 `allowedPathPrefixes=["/health","/logs","/api/"]`,只通过 frontend/backend/provider-gateway 私有代理访问 Compose 内网服务名;D601 的 FindJob 只允许 `GET/HEAD` 展示型读取路径;D601 的 Pipeline 允许 `GET/HEAD/POST`,其中 `POST` 只用于 Pipeline 后端 `/api/node-control/...` 的 append prompt、guide 和 redo/restart 等受控 node 操作。 +主 server 承载的 Todo Note 用户服务使用 `providerId=main-server`、`nodeBaseUrl=http://todo-note:4211` 和 `allowedMethods=["GET","HEAD","POST","DELETE"]`,数据库使用主 PostgreSQL;Code Queue 使用 `providerId=main-server`、`nodeBaseUrl=http://code-queue:4222` 和 `allowedPathPrefixes=["/health","/logs","/api/"]`,只通过 frontend/backend/provider-gateway 私有代理访问 Compose 内网服务名;D601 的 FindJob 只允许 `GET/HEAD` 展示型读取路径;D601 的 Pipeline 允许 `GET/HEAD/POST`,其中 `POST` 只用于 Pipeline 后端 `/api/node-control/...` 的 append prompt、guide 和 redo/restart 等受控 node 操作。 ## Compose Env Generation diff --git a/docs/reference/deployment.md b/docs/reference/deployment.md index 673a5fd5..d80fd419 100644 --- a/docs/reference/deployment.md +++ b/docs/reference/deployment.md @@ -9,8 +9,9 @@ - `frontend` 是唯一公开 Web 控制台,提供登录、从 TSX 转译出的 React 应用资产和到 backend-core 的同源代理。 - `provider-gateway` 是当前主 server 的本机计算节点代理,通过 WebSocket 主动连到 provider ingress,挂载 `/var/run/docker.sock` 作为自动任务执行主路径,使用 `pid: "host"` 读取节点级进程资源,并周期性上报系统资源指标、进程占用与 Docker daemon 状态;维护用 Host SSH / WSL SSH 私钥目录只读挂载到 `/run/host-ssh`,不得作为自动任务调度主路径。 - `todo-note` 是主 server 承载的 Todo Note 纯后端用户服务,容器名 `todo-note-backend`,只在 Compose 内网暴露 `4211/tcp`,使用主 PostgreSQL 存储迁移后的 Todo Note 数据。 -- `codex-queue` 是主 server 承载的 Codex app-server 队列用户服务,容器名 `codex-queue-backend`,仅在 Compose 内网暴露 `4222/tcp` 给本机 provider-gateway 私有代理访问,任务、queue、未读状态、控制状态和通知 outbox 一律写入主 PostgreSQL,不保留本地状态文件 fallback,浏览器只能通过 UniDesk frontend 同源代理查看运行输出、追加 prompt、打断和重试。 +- `code-queue` 是主 server 承载的 Codex app-server 队列用户服务,容器名 `code-queue-backend`,仅在 Compose 内网暴露 `4222/tcp` 给本机 provider-gateway 私有代理访问,容器硬上限为 `300m` memory/swap;任务、queue、未读状态、控制状态和通知 outbox 一律写入主 PostgreSQL,不保留本地状态文件 fallback,浏览器只能通过 UniDesk frontend 同源代理查看运行输出、追加 prompt、打断和重试。 - `project-manager` 是主 server 承载的项目管理用户服务,容器名 `project-manager-backend`,仅在 Compose 内网暴露 `4233/tcp`,项目清单写入主 PostgreSQL,浏览器只能通过 UniDesk frontend 同源代理执行增删改查、Excel 导入和 Excel 导出。 +- `baidu-netdisk` 是主 server 承载的百度网盘存储用户服务,容器名 `baidu-netdisk-backend`,仅在 Compose 内网暴露 `4244/tcp`,OAuth/token/transfer 状态写入主 PostgreSQL,浏览器只能通过 UniDesk frontend 同源代理执行设备码登录、文件浏览和 staging 传输任务控制。 ## Public Exposure Boundary @@ -26,19 +27,29 @@ Compose v2 安装后仍然必须遵守 UniDesk 的服务控制入口:全栈生 ## Start And Stop -`bun scripts/cli.ts server start` 与 `bun scripts/cli.ts server stop` 都是异步 job。启动 job 只执行固定 Compose project 的 `up -d --build --remove-orphans`,不得先 `down`,避免在 provider-gateway 旧容器或网络冲突时把 `codex-queue-backend` 等长任务容器先删掉又启动失败;停止 job 才允许执行 `down --remove-orphans`。启动和停止流程都禁止删除 Docker named volume。所有会改变主 server Compose 状态的 job 必须通过 `.state/locks/server-compose.lock` 串行化;`server rebuild codex-queue && server rebuild frontend` 这类连续命令只代表连续创建异步 job,不能代表第一个 job 已结束,实际容器变更仍必须由 Compose lock 串行执行。 +`bun scripts/cli.ts server start` 与 `bun scripts/cli.ts server stop` 都是异步 job。启动 job 只执行固定 Compose project 的 `up -d --build --remove-orphans`,不得先 `down`,避免在 provider-gateway 旧容器或网络冲突时把 `code-queue-backend` 等长任务容器先删掉又启动失败;停止 job 才允许执行 `down --remove-orphans`。启动和停止流程都禁止删除 Docker named volume。所有会改变主 server Compose 状态的 job 必须通过 `.state/locks/server-compose.lock` 串行化;`server rebuild code-queue && server rebuild frontend` 这类连续命令只代表连续创建异步 job,不能代表第一个 job 已结束,实际容器变更仍必须由 Compose lock 串行执行。 ## Single Service Rebuild -前端、backend-core、本机 provider-gateway 或主 server 承载的 Todo Note/Codex Queue/Project Manager 用户服务需要重建时,统一使用 `bun scripts/cli.ts server rebuild `,其中 `` 只能是 `backend-core`、`frontend`、`provider-gateway`、`todo-note`、`codex-queue` 或 `project-manager`。该命令先执行目标服务镜像构建,构建成功后才通过 `up -d --no-deps --force-recreate ` 替换目标容器,避免构建失败导致运行中的服务被提前停掉。 +前端、backend-core、本机 provider-gateway 或主 server 承载的 Todo Note/Code Queue/Project Manager/Baidu Netdisk 用户服务需要重建时,统一使用 `bun scripts/cli.ts server rebuild `,其中 `` 只能是 `backend-core`、`frontend`、`provider-gateway`、`todo-note`、`code-queue`、`project-manager` 或 `baidu-netdisk`。File Browser 部署在 D518/D601 计算节点,不属于主 server Compose 可重建服务。该命令先执行目标服务镜像构建,构建成功后才通过 `up -d --no-deps --force-recreate ` 替换目标容器,避免构建失败导致运行中的服务被提前停掉。 + +frontend 改动必须明确上线到公网:修改 `src/components/frontend/src/`、`src/components/frontend/public/style.css`、frontend 使用的共享 TSX/TS 模块或 WebUI 导航后,必须在同一变更集中执行 `bun scripts/cli.ts server rebuild frontend`,并等待 job 成功。公网 WebUI 的 `/app.js` 是 `unidesk-frontend` 容器启动时从镜像内源码转译生成的运行时 bundle;只改工作区文件、只跑 `bun run check`、只跑 `Bun.build` 或只刷新浏览器都不会替换已经运行的容器。 + +frontend 的 Docker 上线顺序为:先运行必要的本地校验,例如 `bun scripts/cli.ts check` 或对应的 frontend bundle 检查;然后运行 `bun scripts/cli.ts server rebuild frontend`;再用 `bun scripts/cli.ts job status latest` 或具体 job id 确认 `status=succeeded`;最后用 `bun scripts/cli.ts server status`、`curl -fsS http://74.48.78.17:18081/health` 和真实页面/深链接验证。若必须手工复现 CLI 行为,等价 Docker Compose 命令只能是固定 project/env 的 `docker compose --env-file .state/docker-compose.env -f docker-compose.yml -p unidesk build frontend`,随后 `docker compose --env-file .state/docker-compose.env -f docker-compose.yml -p unidesk up -d --no-deps --force-recreate frontend`;手工路径仍必须做同样的健康检查,不得改用全栈 `up --build`、`down`、删除容器或重启 database/backend-core 来“顺手上线”前端。 单服务重建必须由 CLI 解析出的 Compose 命令执行,且只能影响目标 service,不得连带重启 database、backend-core 或其他未指定服务。重建后 job 必须按 Docker Compose label 校验目标容器:`com.docker.compose.project` 等于 `config.json` 中的固定 project name,`com.docker.compose.service` 等于目标服务名,并等待容器进入 `healthy`;没有 healthcheck 的服务至少要进入 `running`。如果验证失败,job 必须失败并输出目标容器状态,禁止把“无输出”或“只完成 build”当作成功。 -正式流程不得依赖人工 `docker rm` 兜底;手工删除旧容器后若 job、Docker client 或 daemon 在 `up` 前中断,会直接造成 `direct microservice proxy failed`。`server rebuild ` 必须是可观测 job:build-first、Compose lock、no-deps force-recreate、post-up validation、保留 named volume。Codex Queue 等长任务服务即使被重建也必须依赖服务自身 restart-recovery 恢复任务,不能用“避免重建”掩盖恢复缺陷。 +紧急灾备或数据迁移期间如需手工启动单个 Compose service,也必须保持与 CLI 相同的隔离语义:使用固定 `--env-file .state/docker-compose.env` 和 `up -d --no-deps `,只启动目标容器;如果需要刷新 backend-core 的服务目录或环境变量,应把 `backend-core` 作为显式目标单独重建/替换,不能依赖 `up` 的依赖解析顺手重建 database、backend-core 或其他服务。 + +正式流程不得依赖人工 `docker rm` 兜底;手工删除旧容器后若 job、Docker client 或 daemon 在 `up` 前中断,会直接造成 `direct microservice proxy failed`。`server rebuild ` 必须是可观测 job:build-first、Compose lock、no-deps force-recreate、post-up validation、保留 named volume。Code Queue 等长任务服务即使被重建也必须依赖服务自身 restart-recovery 恢复任务,不能用“避免重建”掩盖恢复缺陷。 ## Health Criteria -服务跑通的最低标准是:backend-core 内网 `/health` 返回 ok,frontend 公网 `/health` 返回 ok,provider ingress 公网 `/health` 返回 ok,database 在容器内 `pg_isready` 可用,Todo Note 后端 `/api/health` 返回 `storage=postgres`,Codex Queue `/health` 返回队列摘要、默认模型和 `queue.storage`,Project Manager `/health` 返回 `storage.primary=postgres` 和项目数量,backend-core `/api/performance` 返回性能指标,`/api/nodes` 中出现 `main-server` provider 且状态为 `online`,`/api/nodes/system-status` 中出现 `main-server` 的 CPU/内存/硬盘采样,`/api/nodes/docker-status` 中出现 `main-server` 的 Docker 快照。交付前还必须运行 `bun scripts/cli.ts e2e run`,并以 `docs/reference/e2e.md` 的门禁作为最终判定。 +服务跑通的最低标准是:backend-core 内网 `/health` 返回 ok,frontend 公网 `/health` 返回 ok,provider ingress 公网 `/health` 返回 ok,database 在容器内 `pg_isready` 可用,Todo Note 后端 `/api/health` 返回 `storage=postgres`,Code Queue `/health` 返回队列摘要、默认模型和 `queue.storage`,Project Manager `/health` 返回 `storage.primary=postgres` 和项目数量,backend-core `/api/performance` 返回性能指标,`/api/nodes` 中出现 `main-server` provider 且状态为 `online`,`/api/nodes/system-status` 中出现 `main-server` 的 CPU/内存/硬盘采样,`/api/nodes/docker-status` 中出现 `main-server` 的 Docker 快照。交付前还必须运行 `bun scripts/cli.ts e2e run`,并以 `docs/reference/e2e.md` 的门禁作为最终判定。 + +## Main Server Memory Budget + +主 server 内存预算按稀缺资源管理,不能把用户服务当作无限内存的 worker 节点使用。`code-queue-backend` 必须保持 `300m` memory/swap 硬上限,默认只允许 `CODE_QUEUE_MAX_ACTIVE_QUEUES=1`,并保持 `CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS=10`、`CODE_QUEUE_IN_MEMORY_EVENT_RECORDS=10` 这类小热窗口;任务历史、队列统计、Trace/output 读取和 `/health` 摘要必须优先从 PostgreSQL 直读或聚合,不能为了性能便利在 Bun 进程内缓存全量历史。任何提高 Code Queue 并发度、热窗口、日志缓冲、Playwright/Codex 子进程常驻规模或容器上限的变更,都必须在同一任务里说明内存预算来源,并通过 `docker inspect code-queue-backend`、`docker stats --no-stream code-queue-backend`、`microservice health code-queue` 和对应 E2E 证明未重新引入内存爆炸风险。 ## Database Volume diff --git a/docs/reference/e2e.md b/docs/reference/e2e.md index c4d021fc..8ce89da0 100644 --- a/docs/reference/e2e.md +++ b/docs/reference/e2e.md @@ -31,19 +31,19 @@ Typical targeted commands: - `bun scripts/cli.ts e2e run --only frontend --skip frontend:todo-note-integrated-visible,frontend:findjob-integrated-visible` - `bun scripts/cli.ts e2e run --only network,provider-ingress` -- Public exposure: Docker port summary must show only frontend and provider ingress host mappings; public core、public database and known private user-service ports such as FindJob `3254`, MET Nonlinear `3288`, Todo Note `4211` and Codex Queue host port `14222` probes must fail. -- Core API: `docker exec unidesk-backend-core` calls internal `GET /api/overview`, which must report `dbReady: true`, `pgdata.volumeName=unidesk_pgdata_10gb`, a positive PostgreSQL database byte count, and at least one online node; internal `GET /api/performance` must report component request statistics, internal operation statistics, PGDATA usage and Codex Queue PostgreSQL storage metadata. -- Provider self-connection: internal `GET /api/nodes` must contain `main-server` with `status: online`, `labels.providerGatewayVersion` equal to `src/components/provider-gateway/package.json` and `labels.providerGatewayUpgradePolicy: "always-enabled"`; internal `GET /api/nodes/system-status` must contain CPU/memory/disk samples plus a non-empty process resource list sorted by memory by default; internal `GET /api/nodes/docker-status` must contain a Docker snapshot for `main-server`; public provider ingress `/health` must return ok. +- Public exposure: Docker port summary must show only frontend and provider ingress host mappings; public core、public database and known private user-service ports such as FindJob `3254`, MET Nonlinear `3288`, Todo Note `4211`, Code Queue host port `14222` and File Browser provider port `4251` probes must fail. +- Core API: `docker exec unidesk-backend-core` calls internal `GET /api/overview`, which must report `dbReady: true`, `pgdata.volumeName=unidesk_pgdata_10gb`, a positive PostgreSQL database byte count, and at least one online node; internal `GET /api/performance` must report component request statistics, internal operation statistics, PGDATA usage and Code Queue PostgreSQL storage metadata. +- Provider self-connection: internal `GET /api/nodes` must contain `main-server` with `status: online`, `labels.providerGatewayVersion` equal to `src/components/provider-gateway/package.json`, `labels.providerGatewayUpgradePolicy: "always-enabled"`, `labels.providerGatewayRestartPolicyOk: true`, `labels.providerGatewayPidModeOk: true`, and `labels.providerGatewayRuntimeGuardOk: true`; internal `GET /api/nodes/system-status` must contain CPU/memory/disk samples plus a non-empty process resource list sorted by memory by default; internal `GET /api/nodes/docker-status` must contain a Docker snapshot for `main-server`; every running `provider-gateway` container visible in Docker snapshots must report `restartPolicy: "always"` and `pidMode: "host"`; public provider ingress `/health` must return ok. - Provider remote control: internal `/api/dispatch` must successfully complete a real `provider.upgrade` task in `mode: "plan"` so the upgrade path is validated without recreating the running gateway during E2E. -- User services: internal `/api/microservices` must include `todo-note` and `codex-queue` on `main-server` plus `findjob`, `pipeline` and `met-nonlinear` on `D601` with `public=false`; `/api/microservices/todo-note/health` must report `storage=postgres`, `/api/microservices/todo-note/proxy/api/instances` must expose the migrated Todo Note lists, and a temporary Todo Note list create/add/toggle/undo/delete cycle must succeed through the real provider-gateway proxy; `/api/microservices/codex-queue/health` must return a Codex Queue summary with default model `gpt-5.5`, and `/api/microservices/codex-queue/proxy/api/tasks` must return queue state through the same proxy; `/api/microservices/findjob/health` and `/api/microservices/findjob/proxy/api/summary` must succeed through the real provider-gateway proxy; `/api/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:5` must return a bounded preview with `_unidesk.arrayLimits` metadata; `/api/microservices/pipeline/health`, `/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3` and `/api/microservices/pipeline/proxy/api/oa-event-flow/diagnostics` must return Pipeline health, registry/run previews and OA event-flow evidence; `/api/microservices/met-nonlinear/health`, `/api/microservices/met-nonlinear/proxy/api/queue`, `/api/microservices/met-nonlinear/proxy/api/projects?root=projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects?root=ex_projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects/config?path=` and `/api/microservices/met-nonlinear/proxy/api/images` must return the D601 TS backend health, queue/GPU policy, full project tree inputs, structured project detail and ready `met-nonlinear-ml:tf26` image status. +- User services: internal `/api/microservices` must include `todo-note` and `code-queue` on `main-server`, canonical `filebrowser` on `D518`, plus `findjob`, `pipeline`, `met-nonlinear` and `filebrowser-d601` on `D601` with `public=false`; `/api/microservices/todo-note/health` must report `storage=postgres`, `/api/microservices/todo-note/proxy/api/instances` must expose the migrated Todo Note lists, and a temporary Todo Note list create/add/toggle/undo/delete cycle must succeed through the real provider-gateway proxy; `/api/microservices/code-queue/health` must return a Code Queue summary with default model `gpt-5.5`, and `/api/microservices/code-queue/proxy/api/tasks` must return queue state through the same proxy; `/api/microservices/filebrowser/health`, `/api/microservices/filebrowser-d601/health` and `/api/microservices/filebrowser/proxy/` must prove File Browser health and WebUI access through UniDesk proxy; `/api/microservices/findjob/health` and `/api/microservices/findjob/proxy/api/summary` must succeed through the real provider-gateway proxy; `/api/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:5` must return a bounded preview with `_unidesk.arrayLimits` metadata; `/api/microservices/pipeline/health`, `/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3` and `/api/microservices/pipeline/proxy/api/oa-event-flow/diagnostics` must return Pipeline health, registry/run previews and OA event-flow evidence; `/api/microservices/met-nonlinear/health`, `/api/microservices/met-nonlinear/proxy/api/queue`, `/api/microservices/met-nonlinear/proxy/api/projects?root=projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects?root=ex_projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects/config?path=` and `/api/microservices/met-nonlinear/proxy/api/images` must return the D601 TS backend health, queue/GPU policy, full project tree inputs, structured project detail and ready `met-nonlinear-ml:tf26` image status. - Database: the command writes an `unidesk_e2e_markers` row through `docker exec unidesk-database psql`, confirms provider state is stored in PostgreSQL, and checks Todo Note rows exist in `todo_note_instances` using the same named volume. - Pipeline OA event flow: `microservice:pipeline-oa-event-flow` must prove both no-audit and monitor-audit runs are driven by OA events end to end. The event stream must show `node-finished` as a neutral fact with `pipeline:{pipelineId}` and `epoch:{runId}` tags, OA policy as the source of downstream/audit decisions, monitor decisions as OA control events, and runner control-result evidence. E2E must fail if delivery still depends on a legacy detail audit policy flag as policy authority, independent legacy audit-request points, a legacy batch completion gate, direct monitor-to-runner calls, or frontend/CLI writes to Pipeline `.state`. - The same Pipeline OA diagnostics must fail on legacy file-transport residuals. Procedure containers, monitor sessions, UI/Gantt DTO builders and CLI fetches must consume prompt/control/stop/display evidence only from the OA event ledger and normalized HTTP read APIs; `control-prompts.jsonl`, `monitor-prompts.jsonl`, `monitor-control`, `control-events.jsonl`, monitor stop files, `.state/pipeline-runs/{runId}/control/commands/`, `PIPELINE_*_APPEND_FILE`, local JSONL append/read helpers, and monitor `/pipeline-state` mounts are forbidden in runtime source. - Pipeline live Gantt setup: when `frontend:pipeline-gantt-observation-live-running` is selected, E2E first looks for a current Pipeline run that already contains both a `node-long-running-observation` marker and a still-running execution interval. If no such candidate exists, the E2E setup starts the D601 `monitor-management-behavior-test` pipeline through `bun scripts/cli.ts ssh D601 ...` and polls the private backend proxy until the observation candidate exists; the acceptance assertion itself still opens the public frontend with Playwright and verifies the rendered arrows, absence of observation source pseudo-points, target arrow inset, and live flashing running bar through React DOM controls. -- Frontend: Playwright must open the public frontend URL derived from `network.publicHost`, not localhost or a Docker-internal URL; it logs in with the configured account, waits for `核心在线`, asserts that `main-server` and `Main Server Provider` are visible, verifies desktop sidebar collapse and `PGDATA` overview metric, opens `运行总览 / 性能面板` to verify `Bwebui`、组件汇总、最近失败请求、内部操作汇总和最近慢操作, clicks `查看原始JSON` to verify Provider data from the frontend, confirms no raw JSON is visible before that click, opens task history to verify duration and failure diagnostics, opens resource nodes `资源监控` to verify CPU/Memory/Disk curves, the structured process resource table, default memory-desc sorting, sortable CPU column and provider upgrade precheck dispatch, opens `Docker 状态`, switches to `main-server`, and verifies the Docker Desktop-style container view including the database named volume `unidesk_pgdata_10gb`, opens `网关版本` and verifies the provider-gateway version, SSH 透传可用性、远程更新可用性 plus structured remote update records for `provider.upgrade`, then opens `用户服务 / 服务目录`、`用户服务 / Todo Note`、`用户服务 / Codex Queue`、`用户服务 / FindJob`、`用户服务 / Pipeline` and `用户服务 / MET Nonlinear` to verify 主 server Todo Note/Codex Queue、D601、仓库引用、私有后端映射、Todo Note 迁移清单和树形任务、Codex Queue 队列/模型/输出/初始 `Submitted prompt`/终态任务自动加载完整 Trace/追加 prompt/打断控件、FindJob 指标和岗位预览、Pipeline 组件矩阵、MiniMax 限额卡片、结构化 OA 事件流诊断面板、React Flow 控制图、epoch 甘特图、甘特图渲染图导出、monitor 首列排序、长任务观察连线、无观察来源伪点、running node 实时闪动执行条和 OpenCode Trace、MET Nonlinear 项目库/Fork/待启动队列/当前队列/已完成/失败诊断/GPU/镜像都通过 React 控件展示。Playwright 还必须验证深链接直达路由,例如公网 `http://:/app/pipeline/` 能直接落到 Pipeline 页面,随后切到 `资源节点 / Docker 状态` 时地址栏更新为 `/nodes/docker/`,并且浏览器 history 返回链路仍能回到 `/app/pipeline/`;还必须直开 `/app/codex-queue/` 验证页面存在 `app-shell`、左侧主模块边栏、顶部状态栏、顶部子标签和 `codex-queue-page`,防止用户服务 deep link 退化成缺 shell 的 standalone 页面;同时 `态势总览` 这类非用户服务页面应落在自己的模块前缀下,例如 `/ops/status/`。Playwright 必须覆盖默认可见时间按北京时间显示,至少包括顶部 `北京时间` 时钟、任务历史/网关版本更新时间和用户服务刷新时间,不得随浏览器本地时区漂移。Task history and provider upgrade records must not display a real sub-second duration as `0s`; MET Nonlinear running rows must show an ETA derived from backend progress or from `startedAt` plus epoch progress, and queue/completed rows must show training speed as `epoch/h`. -- Frontend dense-layout regression gate: whenever a frontend change touches Pipeline 右侧边栏、Trace timeline、详情抽屉、甘特图坐标或其他高信息密度面板, Playwright acceptance must inspect both `总高度` and `横向滚动条`. For Pipeline specifically, the OpenCode Trace session head must carry shared agent/model/session facts and the Trace body must use the same Codex Queue `TraceView` styling; Playwright must fail if old `.pipeline-opencode-step`, `.pipeline-opencode-flow`, `.pipeline-step-message-card` or `.pipeline-opencode-part` user-visible styles reappear, if the Trace container introduces an internal horizontal scrollbar, or if `frontend:pipeline-gantt-frontend-y-accuracy` fails to prove the frontend `frontend-y` layout maps ticks, markers and execution bars from timestamps to y coordinates within tolerance. -- OpenCode Trace must use Codex Queue Trace styling and must not render the deprecated Pipeline continuous step connector; Playwright should fail if `.pipeline-opencode-flow`, `.pipeline-opencode-step` or any equivalent continuous connector/card returns to the user-visible Trace. -- User service frontend assertions must wait for real backend data, not only the page skeleton. For Todo Note this means the page must show the migrated lists `CONSTAR`、`大论文`、`找工作`、`小论文`、`事务`, support creating a temporary list and task through the frontend, and delete that temporary list afterwards. The temporary list must be selected again by its unique generated name before deletion so E2E never deletes a migrated source list by accident. For FindJob this means the page must show a numeric `岗位总量`, `HEALTH OK`, and a non-empty `PREVIEW` count such as `40/1463 PREVIEW`; for Pipeline this means the page must show `Pipeline v2 工作台`, `Health OK`, a numeric component count, a non-empty React Flow control graph, `控制图`, `Epoch 甘特图`, and after clicking a Gantt execution line it must show `OpenCode Trace` rendered by the shared Codex Queue-style Trace component with messages and tool-call groups; for MET Nonlinear this means the page must show `MET Nonlinear 训练编排`, `Health OK`, `Fork Project`, `加入待启动队列`, `启动队列`, `当前队列`, 最大并发设置、task queue and GPU/image panels, and must not show the removed hard-coded `创建10个10轮任务` frontend entry. The MET Nonlinear project library must render `projects/` and `ex_projects/` as a true path tree with folder Project counts; clicking a project row must open a structured detail panel containing `config.json`, `data/ 训练状态`, `模型参数`, `指标` and a parameter count such as `Total Params`; clicking a completed/current/failed job row must open a structured job detail and both the row and detail must show `epoch/h`. Full MET Nonlinear acceptance is driven by public frontend controls: choose a visible source Project, set batch size, epochs and max concurrency in inputs, fork into `projects/unidesk_forks/`, stage the selected forks, start the queue, and verify completed rows plus automatic `metnl-train-*` container removal; loading placeholders like `--` or empty states are not sufficient for E2E success. +- Frontend: Playwright must open the public frontend URL derived from `network.publicHost`, not localhost or a Docker-internal URL; it logs in with the configured account, waits for `核心在线`, asserts that `main-server` and `Main Server Provider` are visible, verifies desktop sidebar collapse and `PGDATA` overview metric, opens `运行总览 / 性能面板` to verify `Bwebui`、组件汇总、最近失败请求、内部操作汇总和最近慢操作, clicks `查看原始JSON` to verify Provider data from the frontend, confirms no raw JSON is visible before that click, opens task history to verify duration and failure diagnostics, opens resource nodes `资源监控` to verify CPU/Memory/Disk curves, the structured process resource table, default memory-desc sorting, sortable CPU column and provider upgrade precheck dispatch, opens `Docker 状态`, switches to `main-server`, and verifies the Docker Desktop-style container view including the database named volume `unidesk_pgdata_10gb`, opens `网关版本` and verifies the provider-gateway version, SSH 透传可用性、远程更新可用性 plus structured remote update records for `provider.upgrade`, then opens `用户服务 / 服务目录`、`用户服务 / Todo Note`、`用户服务 / Code Queue`、`用户服务 / FindJob`、`用户服务 / Pipeline` and `用户服务 / MET Nonlinear` to verify 主 server Todo Note/Code Queue、D601、仓库引用、私有后端映射、Todo Note 迁移清单和树形任务、Code Queue 队列/模型/输出/初始 `Submitted prompt`/终态任务自动加载完整 Trace/追加 prompt/打断控件、FindJob 指标和岗位预览、Pipeline 组件矩阵、MiniMax 限额卡片、结构化 OA 事件流诊断面板、React Flow 控制图、epoch 甘特图、甘特图渲染图导出、monitor 首列排序、长任务观察连线、无观察来源伪点、running node 实时闪动执行条和 OpenCode Trace、MET Nonlinear 项目库/Fork/待启动队列/当前队列/已完成/失败诊断/GPU/镜像都通过 React 控件展示。Playwright 还必须验证深链接直达路由,例如公网 `http://:/app/pipeline/` 能直接落到 Pipeline 页面,随后切到 `资源节点 / Docker 状态` 时地址栏更新为 `/nodes/docker/`,并且浏览器 history 返回链路仍能回到 `/app/pipeline/`;还必须直开 `/app/code-queue/` 验证页面存在 `app-shell`、左侧主模块边栏、顶部状态栏、顶部子标签和 `code-queue-page`,防止用户服务 deep link 退化成缺 shell 的 standalone 页面;同时 `态势总览` 这类非用户服务页面应落在自己的模块前缀下,例如 `/ops/status/`。Playwright 必须覆盖默认可见时间按北京时间显示,至少包括顶部 `北京时间` 时钟、任务历史/网关版本更新时间和用户服务刷新时间,不得随浏览器本地时区漂移。Task history and provider upgrade records must not display a real sub-second duration as `0s`; MET Nonlinear running rows must show an ETA derived from backend progress or from `startedAt` plus epoch progress, and queue/completed rows must show training speed as `epoch/h`. +- Frontend dense-layout regression gate: whenever a frontend change touches Pipeline 右侧边栏、Trace timeline、详情抽屉、甘特图坐标或其他高信息密度面板, Playwright acceptance must inspect both `总高度` and `横向滚动条`. For Pipeline specifically, the OpenCode Trace session head must carry shared agent/model/session facts and the Trace body must use the same Code Queue `TraceView` styling; Playwright must fail if old `.pipeline-opencode-step`, `.pipeline-opencode-flow`, `.pipeline-step-message-card` or `.pipeline-opencode-part` user-visible styles reappear, if the Trace container introduces an internal horizontal scrollbar, or if `frontend:pipeline-gantt-frontend-y-accuracy` fails to prove the frontend `frontend-y` layout maps ticks, markers and execution bars from timestamps to y coordinates within tolerance. +- OpenCode Trace must use Code Queue Trace styling and must not render the deprecated Pipeline continuous step connector; Playwright should fail if `.pipeline-opencode-flow`, `.pipeline-opencode-step` or any equivalent continuous connector/card returns to the user-visible Trace. +- User service frontend assertions must wait for real backend data, not only the page skeleton. For Todo Note this means the page must show the migrated lists `CONSTAR`、`大论文`、`找工作`、`小论文`、`事务`, support creating a temporary list and task through the frontend, and delete that temporary list afterwards. The temporary list must be selected again by its unique generated name before deletion so E2E never deletes a migrated source list by accident. For FindJob this means the page must show a numeric `岗位总量`, `HEALTH OK`, and a non-empty `PREVIEW` count such as `40/1463 PREVIEW`; for Pipeline this means the page must show `Pipeline v2 工作台`, `Health OK`, a numeric component count, a non-empty React Flow control graph, `控制图`, `Epoch 甘特图`, and after clicking a Gantt execution line it must show `OpenCode Trace` rendered by the shared Code Queue-style Trace component with messages and tool-call groups; for MET Nonlinear this means the page must show `MET Nonlinear 训练编排`, `Health OK`, `Fork Project`, `加入待启动队列`, `启动队列`, `当前队列`, 最大并发设置、task queue and GPU/image panels, and must not show the removed hard-coded `创建10个10轮任务` frontend entry. The MET Nonlinear project library must render `projects/` and `ex_projects/` as a true path tree with folder Project counts; clicking a project row must open a structured detail panel containing `config.json`, `data/ 训练状态`, `模型参数`, `指标` and a parameter count such as `Total Params`; clicking a completed/current/failed job row must open a structured job detail and both the row and detail must show `epoch/h`. Full MET Nonlinear acceptance is driven by public frontend controls: choose a visible source Project, set batch size, epochs and max concurrency in inputs, fork into `projects/unidesk_forks/`, stage the selected forks, start the queue, and verify completed rows plus automatic `metnl-train-*` container removal; loading placeholders like `--` or empty states are not sufficient for E2E success. ## Frontend JSON Rule @@ -53,7 +53,7 @@ Remote update records in the frontend are covered by the same rule: `provider.up Provider operation availability is also covered by the structured rendering rule. `host.ssh` availability must be displayed as badges or equivalent controls derived from capabilities and `hostSsh*` labels, and remote update availability must be displayed from `provider.upgrade` capability plus the `always-enabled` policy; these fields must not require opening raw Provider JSON. -User service pages are covered by the same rule. `Todo Note` must show lists, task tree, filters, reminder input, movement controls, undo/redo and metrics as controls; `Codex Queue` must show queue cards, live transcript, model/cwd/max attempt inputs, judge decision, attempt table, append prompt, interrupt and retry controls; `FindJob` must show metrics, jobs and drafts as cards/tables; `Pipeline` must show component classes, React Flow graph nodes/edges, run cards, Gantt execution lines and OpenCode Trace timelines as controls; `MET Nonlinear` must show queue rows, GPU/image cards, a real path tree for the project library, structured project/job detail panels, project config preview, `data/` training state, model parameter count, metrics, progress bars, ETA, `epoch/h` speed and history diagnostics as controls; the full user-service config, summary, snapshot, jobs preview, drafts and run JSON can only appear after an explicit `查看原始JSON` click. +User service pages are covered by the same rule. `Todo Note` must show lists, task tree, filters, reminder input, movement controls, undo/redo and metrics as controls; `Code Queue` must show queue cards, live transcript, model/cwd/max attempt inputs, judge decision, attempt table, append prompt, interrupt and retry controls; `File Browser` must show D518 as the default target, D601 as an alternate target, a screenshot export action, and an embedded upstream WebUI frame served through `/api/microservices//proxy/` with compact file rows that do not let material-icon fallback text cover file metadata; `FindJob` must show metrics, jobs and drafts as cards/tables; `Pipeline` must show component classes, React Flow graph nodes/edges, run cards, Gantt execution lines and OpenCode Trace timelines as controls; `MET Nonlinear` must show queue rows, GPU/image cards, a real path tree for the project library, structured project/job detail panels, project config preview, `data/` training state, model parameter count, metrics, progress bars, ETA, `epoch/h` speed and history diagnostics as controls; the full user-service config, summary, snapshot, jobs preview, drafts and run JSON can only appear after an explicit `查看原始JSON` click. ## Public Boundary Rule @@ -75,6 +75,6 @@ Before claiming delivery, run these checks and keep their JSON output or screens ## Provider Upgrade Gate -When delivery explicitly includes upgrading or rebuilding a compute-node `provider-gateway` such as D601 or D518, the automated E2E plan check is not sufficient. The operator must first bootstrap any legacy provider only from a node-local terminal, node-owned web terminal, systemd, scheduled task, or detached shell if it cannot yet schedule upgrades; SSH passthrough carried by the same provider-gateway must not be used for synchronous self-rebuilds. Then run `provider.upgrade` with `mode: "schedule"` against that Provider ID, confirm the task succeeds, confirm the sleep-and-validate candidate gateway reconnects in the public frontend, confirm the final container restart policy is `always`, and finally verify any required `host.ssh` capability with `bun scripts/cli.ts ssh hostname`. This schedule check is a node-upgrade gate, not a replacement for the standard public frontend Playwright E2E gate. +When delivery explicitly includes upgrading or rebuilding a compute-node `provider-gateway` such as D601 or D518, the automated E2E plan check is not sufficient. The operator must first bootstrap any legacy provider only from a node-local terminal, node-owned web terminal, systemd, scheduled task, or detached shell if it cannot yet schedule upgrades; SSH passthrough carried by the same provider-gateway must not be used for synchronous self-rebuilds. Then run `provider.upgrade` with `mode: "schedule"` against that Provider ID, confirm the task succeeds, confirm the sleep-and-validate candidate gateway reconnects in the public frontend, confirm `docker inspect` reports final restart policy `always` and PID mode `host`, record whether systemd/PM2/Windows scheduled task/Docker Desktop autostart is the daemon-level supervisor, and finally verify any required `host.ssh` capability with `bun scripts/cli.ts ssh hostname`. This schedule check is a node-upgrade gate, not a replacement for the standard public frontend Playwright E2E gate. External compute nodes should run that schedule check through the remote main-server passthrough form: `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch provider.upgrade --mode schedule --wait-ms 15000`. The default remote transport logs in to the public frontend and does not require a main server SSH key; this proves the node can validate itself without direct access to backend-core REST or PostgreSQL. diff --git a/docs/reference/frontend.md b/docs/reference/frontend.md index 4b50b669..47893996 100644 --- a/docs/reference/frontend.md +++ b/docs/reference/frontend.md @@ -6,7 +6,15 @@ UniDesk 前端是 React 组件化工业控制台,不追求展示型大屏效 frontend 应用源码必须使用 TypeScript + React,禁止在 `src/components/frontend` 中维护手写 `.js` / `.jsx` 应用源码。浏览器请求的 `/app.js` 只能由 frontend Bun server 从 `src/components/frontend/src/app.tsx` 及其 TSX imports 转译生成;`public/` 目录只保存 HTML/CSS 等静态资产,不提交手写 `app.js`。 -`src/components/frontend/src/app.tsx` 只承担应用 shell、登录、全局数据加载、主模块/子标签路由和通用控制台页面。用户服务前端必须模块化到独立 TSX 文件,禁止继续把所有业务页面堆进 `app.tsx`。当前长期固定入口为:`todo-note.tsx` 承载 Todo Note 工作台,`findjob.tsx` 承载 FindJob 工作台,`pipeline.tsx` 承载 Pipeline 工作台,`met-nonlinear.tsx` 承载 MET Nonlinear 训练编排工作台,`claudeqq.tsx` 承载 ClaudeQQ QQ 消息网关工作台,`codex-queue.tsx` 承载 Codex Queue 控制台;新增用户服务也必须按同样规则新增独立页面模块,并由 `app.tsx` 只做导入和路由分发。 +`src/components/frontend/src/app.tsx` 只承担应用 shell、登录、全局数据加载、主模块/子标签路由和通用控制台页面。用户服务前端必须模块化到独立 TSX 文件,禁止继续把所有业务页面堆进 `app.tsx`。当前长期固定入口为:`todo-note.tsx` 承载 Todo Note 工作台,`findjob.tsx` 承载 FindJob 工作台,`pipeline.tsx` 承载 Pipeline 工作台,`met-nonlinear.tsx` 承载 MET Nonlinear 训练编排工作台,`claudeqq.tsx` 承载 ClaudeQQ QQ 消息网关工作台,`baidu-netdisk.tsx` 承载 Baidu Netdisk 存储工作台,`code-queue.tsx` 承载 Code Queue 控制台;新增用户服务也必须按同样规则新增独立页面模块,并由 `app.tsx` 只做导入和路由分发。 + +## Deployment Contract + +任何会影响公网 WebUI 的 frontend 源码、样式或导航变更,都必须在同一任务内完成 Docker 上线;只运行 TypeScript 检查、`Bun.build` 或修改仓库文件不代表公网生效。frontend 容器启动时才从镜像内复制的 `src/components/frontend/src/app.tsx` 及其 TSX imports 构建 `/app.js`,当前 Compose 不对 frontend 源码做宿主机 bind mount,因此运行中的 `unidesk-frontend` 不会自动读取工作区新文件。 + +标准上线命令固定为在仓库根目录执行 `bun scripts/cli.ts server rebuild frontend`。该命令会先构建 `frontend` 镜像,再在固定 Compose project 下通过 `up -d --no-deps --force-recreate frontend` 替换单个容器,并等待健康检查通过;不要用“本地 bundle 校验通过”代替这一步。上线后必须用 `bun scripts/cli.ts job status latest` 或具体 job id 确认 `status=succeeded`,再用 `bun scripts/cli.ts server status`、公网 `http://74.48.78.17:18081/health` 和对应页面/深链接验证真实公网页面已经加载新 bundle。 + +如果用户报告公网仍是旧界面,优先排查两件事:第一,确认最近一次 `server rebuild frontend` 的 job 已成功且 `unidesk-frontend` 容器创建时间晚于代码修改时间;第二,浏览器可能保留旧 `/app.js`,应使用无缓存刷新或重新打开无痕窗口,并可用 `curl -H 'Cache-Control: no-cache' http://74.48.78.17:18081/app.js` 检查新 bundle 标记。涉及右键/中键打开新标签页的导航改动,还必须在公网页面 DOM 中确认左侧主模块和顶部子标签是带 `href` 的 ``,普通左键仍走 SPA 路由。 ## Time Zone Contract @@ -14,7 +22,7 @@ frontend 所有默认可见的时间、日期、时钟、更新时间、心跳 ## Layout -左侧边栏只切换主模块:运行总览、资源节点、任务调度、用户服务、系统配置。顶部标签只切换当前主模块内的子功能;例如资源节点下的节点清单、资源标签、心跳状态只属于资源节点,用户服务下的服务目录、Todo Note、FindJob、Pipeline、MET Nonlinear、Codex Queue 只属于用户服务,和运行总览、任务调度、系统配置没有重复或共享语义。桌面端左侧边栏必须支持收起,只保留模块 code 和展开按钮,以便最大化主面板空间;移动端左侧边栏会转为顶部横向主模块条,但高度必须在不同主模块之间保持一致,并保持窄条、单行、不换行;主内容区无论内容多少都必须从顶部向下排列,空状态也不得上下居中制造大块留白。 +左侧边栏只切换主模块:运行总览、资源节点、任务调度、用户服务、系统配置。顶部标签只切换当前主模块内的子功能;例如资源节点下的节点清单、资源标签、心跳状态只属于资源节点,用户服务下的服务目录、Todo Note、FindJob、Pipeline、MET Nonlinear、ClaudeQQ、Baidu Netdisk、Code Queue 只属于用户服务,和运行总览、任务调度、系统配置没有重复或共享语义。桌面端左侧边栏必须支持收起,只保留模块 code 和展开按钮,以便最大化主面板空间;移动端左侧边栏会转为顶部横向主模块条,但高度必须在不同主模块之间保持一致,并保持窄条、单行、不换行;主内容区无论内容多少都必须从顶部向下排列,空状态也不得上下居中制造大块留白。 ## Route Model @@ -26,7 +34,7 @@ frontend shell 必须把左侧主模块与顶部子标签编译为统一的 URL - 主模块根路径如 `/ops/`、`/nodes/`、`/tasks/`、`/app/`、`/config/` 只作为默认子标签或最近活动子标签的入口别名,实际当前页面仍应落到某个具体子标签;浏览器地址栏不能停留在“只有主模块,没有具体页面”的模糊状态。 - route segment 生成顺序固定为:显式 `routeSegment` > ASCII-safe `id` > 由 label 派生的 Unicode-safe slug > 稳定 hash fallback。这样新增 Unicode 标签时默认仍可得到稳定路径,而不要求每个标签单独写一段路由代码。 - 浏览器直开、刷新、`history.back()` / `history.forward()`、点击总览 drilldown 卡片、点击左侧边栏、点击顶部标签都必须走同一个路由状态机;不得出现“页面内容切换了,但 URL 没变”或“URL 变了,但 shell 仍停在旧 tab”的分裂状态。 -- frontend Bun server 必须把这些模块前缀下的深链接路由作为 SPA 入口返回同一个 `index.html` 和同一个 UniDesk shell;实现上允许统一把非静态资源路径都回到同一个 shell,但判定标准是公网直开 `/ops/status/`、`/nodes/docker/`、`/app/pipeline/`、`/app/codex-queue/` 等深链接时都不得 404,且必须和从主页导航进入时一样显示左侧主模块边栏、顶部状态栏、顶部子标签和完整业务页面。禁止为某个用户服务 deep link 返回缺少 UniDesk shell 的独立/standalone bundle;新增应用服务、普通页面和性能优化入口也必须满足“直开 URL 与站内导航同壳同页”的一致性要求。 +- frontend Bun server 必须把这些模块前缀下的深链接路由作为 SPA 入口返回同一个 `index.html` 和同一个 UniDesk shell;实现上允许统一把非静态资源路径都回到同一个 shell,但判定标准是公网直开 `/ops/status/`、`/nodes/docker/`、`/app/pipeline/`、`/app/code-queue/` 等深链接时都不得 404,且必须和从主页导航进入时一样显示左侧主模块边栏、顶部状态栏、顶部子标签和完整业务页面。禁止为某个用户服务 deep link 返回缺少 UniDesk shell 的独立/standalone bundle;新增应用服务、普通页面和性能优化入口也必须满足“直开 URL 与站内导航同壳同页”的一致性要求。 ## Overview Task Drilldown @@ -69,10 +77,11 @@ frontend shell 必须把左侧主模块与顶部子标签编译为统一的 URL - `Todo Note` 子标签必须把主 server `todo-note-backend` 后端渲染为 UniDesk React 控件,包括迁移清单、树形任务、筛选、提醒、拖放/移动、撤销/重做、字号控制和显式原始 JSON 按钮。 - `FindJob` 子标签必须把 D601 findjob 后端渲染为 UniDesk React 控件,包括岗位指标、岗位预览、草稿报告和显式原始 JSON 按钮。 - `ClaudeQQ` 子标签必须把 D601 ClaudeQQ 后端渲染为 UniDesk React 控件,包括 NapCat 容器登录二维码、NapCat HTTP/WS 状态、事件缓存、QQ 事件订阅表、订阅创建表单、消息推送表单、主用户私聊账号 `645275593` 标记、最近 QQ 事件、已发送记录和显式原始 JSON 按钮。 - - `Codex Queue` 子标签必须把主 server `codex-queue-backend` 后端渲染为 UniDesk React 控件,包括多 queue lane、queue 内串行、queue 间并行、任务 ID/复制任务 ID、引用按钮、任务耗时、任务提交/批量提交、引用任务 ID、创建成功提示、清空输入、模型下拉、显式入队份数、默认模型 `gpt-5.5`、MiniMax judge 状态、Codex CLI-like 输出流、attempt 终态、运行中追加 prompt、打断、手动重试和显式原始 JSON 按钮;Codex CLI-like 输出流必须始终保留任务的初始 `Submitted prompt` 和运行中 `Steer prompt`;整个 agent loop 消息流统一命名为专有名词 `Trace`,`Trace` 包含 assistant message、user prompt、system event 和 tool call;Codex Queue 与 Pipeline/OpenCode messages 必须共用 `src/components/frontend/src/trace.tsx` 的 Trace 公共组件、统一 Trace item 接口和 codex/opencode port 适配层;连续 read/edit/run 工具调用只是在 Trace 内折叠为可展开工具调用组,汇总格式至少包含 `xx read, xx edit, xx run`,并展示读取文件、编辑文件、运行命令和耗时摘要;最近 3 个工具调用保持展开,工具调用内容不得自动换行且必须在工具调用块内部横向滚动,工具调用组展开后不得再增加额外左侧缩进;message 与 prompt 必须自动换行,普通 message 不显示左侧项目符号缩进且永不折叠;Trace 首屏可以是摘要预览,但终态任务被选中后必须自动在后台加载完整 Trace,手动“加载完整 Trace”也必须从 Codex Queue output archive 分页补齐早期 trace,不得把 preview 的 `hasMore=false` 当成完整历史;即使热状态为控制体积裁剪了早期 raw output,也要从结构化 `basePrompt/displayPrompt/promptHistory` 和 archive 合成完整用户输入与 agent trace,并且初始 prompt 默认显示注入前 prompt 而不是引用注入全文;当初始 prompt 含引用注入时,引用内容必须默认折叠,并只在 Trace 的初始消息中提供可展开的“最终传入 Codex 的真实完整 prompt”,不得再渲染独立 Prompt 全量卡片;多轮引用注入必须按上游/最早上下文在前、直接引用在后的顺序排列,每一轮必须有明确 `Reference Round N/M` 分割线和时间范围,不能用固定 6 轮截断引用链;点击队列引用按钮必须自动把该任务 ID 写入提交表单的引用输入框,引用任务 ID 创建新任务时必须自动注入 `bun scripts/cli.ts codex task ` 的提示;连续执行同一 prompt 应通过入队份数一次性生成多条任务,避免快速连点造成操作员误判。 - - `Codex Queue` 前端改进必须在同一任务内重建并上线公网 frontend,不能只修改源码或本地 bundle;重建 frontend 是无状态 WebUI 替换,不会导致 Codex Queue 长期任务失败。已结束未读任务只能在 task card 边角显示类似未读消息的 `codex-unread-badge` 圆点和“标为已读”操作,不得把整张卡片改成红色/琥珀色失败态边框、背景或胶囊标签;状态栏的“结束未读”提示也不得使用失败态红色。 - - `Codex Queue` 前端必须把 PostgreSQL-backed backend API 作为 task、queue、readAt/未读状态和 attempt 状态的唯一数据来源;不得用 `localStorage`、`sessionStorage` 或 IndexedDB 持久化这些业务状态,也不得在后端标记已读失败时伪造本地成功。前端允许保留 React 内存态、请求 in-flight guard 和本轮页面缓存,但刷新页面或切换设备后的状态必须完全由后端 PostgreSQL 数据恢复。 - - `Codex Queue` 的 queue/session 左侧边栏必须采用顶部对齐和内容高度优先布局:列表、分组和 task card 都不得用居中、space-between、stretch 或隐式等高网格去拉满侧栏高度;item 少时允许下半部分留空,不能把单个 item 拉高来铺满。提交任务时必须立即锁定 prompt、引用 ID、queue、模型、工作目录、最大尝试和入队份数等输入控件,显示等待状态,并用前端 in-flight guard 阻止重复点击造成重复入队;当解析到多个待入队任务时必须显式要求用户勾选批量确认,防止 `---` 分隔或入队份数误操作导致错误传入多个任务。Trace 面板的主滚动条使用全站细窄现代滚动条;工具调用块内部的横向滚动必须可滚动但隐藏横向滚动条,避免移动端阅读被滚动条占用。公共 `TraceView` 的自动滚动必须采用 follow-tail 语义:只有当前滚动位置在底部附近时才跟随新增输出;用户手动向上滚动后立即暂停自动滚动,异步刷新不得把视图拉回底部,直到用户再次滚动到最底部才恢复自动跟随。 + - `Baidu Netdisk` 子标签必须把主 server `baidu-netdisk-backend` 后端渲染为 UniDesk React 控件,包括 OAuth 设备码二维码/用户码登录、账号容量、应用目录文件浏览、staging 目录上传/下载任务、上传/下载自测按钮与 MD5 结果、脱敏安全说明、日志摘要和显式原始 JSON 按钮;不得把 access token、refresh token、dlink 或 staging 文件字节流裸露到浏览器。 + - `Code Queue` 子标签必须把主 server `code-queue-backend` 后端渲染为 UniDesk React 控件,包括多 queue lane、queue 内串行、queue 间并行、任务 ID/复制任务 ID、引用按钮、任务耗时、任务提交/批量提交、引用任务 ID、创建成功提示、清空输入、模型下拉、显式入队份数、默认模型 `gpt-5.5`、MiniMax judge 状态、Codex CLI-like 输出流、attempt 终态、运行中追加 prompt、打断、手动重试和显式原始 JSON 按钮;Codex CLI-like 输出流必须始终保留任务的初始 `Submitted prompt` 和运行中 `Steer prompt`;整个 agent loop 消息流统一命名为专有名词 `Trace`,`Trace` 包含 assistant message、user prompt、system event 和 tool call;Code Queue 与 Pipeline/OpenCode messages 必须共用 `src/components/frontend/src/trace.tsx` 的 Trace 公共组件、统一 Trace item 接口和 codex/opencode port 适配层;连续 read/edit/run 工具调用只是在 Trace 内折叠为可展开工具调用组,汇总格式至少包含 `xx read, xx edit, xx run`,并展示读取文件、编辑文件、运行命令和耗时摘要;最近 3 个工具调用保持展开,工具调用内容不得自动换行且必须在工具调用块内部横向滚动,工具调用组展开后不得再增加额外左侧缩进;message 与 prompt 必须自动换行,普通 message 不显示左侧项目符号缩进且永不折叠;Trace 首屏可以是摘要预览,但终态任务被选中后必须自动在后台加载完整 Trace,手动“加载完整 Trace”也必须从 Code Queue output archive 分页补齐早期 trace,不得把 preview 的 `hasMore=false` 当成完整历史;即使热状态为控制体积裁剪了早期 raw output,也要从结构化 `basePrompt/displayPrompt/promptHistory` 和 archive 合成完整用户输入与 agent trace,并且初始 prompt 默认显示注入前 prompt 而不是引用注入全文;当初始 prompt 含引用注入时,引用内容必须默认折叠,并只在 Trace 的初始消息中提供可展开的“最终传入 Codex 的真实完整 prompt”,不得再渲染独立 Prompt 全量卡片;多轮引用注入必须按上游/最早上下文在前、直接引用在后的顺序排列,每一轮必须有明确 `Reference Round N/M` 分割线和时间范围,不能用固定 6 轮截断引用链;点击队列引用按钮必须自动把该任务 ID 写入提交表单的引用输入框,引用任务 ID 创建新任务时必须自动注入 `bun scripts/cli.ts codex task ` 的提示;连续执行同一 prompt 应通过入队份数一次性生成多条任务,避免快速连点造成操作员误判。 + - `Code Queue` 前端改进必须在同一任务内重建并上线公网 frontend,不能只修改源码或本地 bundle;重建 frontend 是无状态 WebUI 替换,不会导致 Code Queue 长期任务失败。已结束未读任务只能在 task card 边角显示类似未读消息的 `codex-unread-badge` 圆点和“标为已读”操作,不得把整张卡片改成红色/琥珀色失败态边框、背景或胶囊标签;状态栏的“结束未读”提示也不得使用失败态红色。 + - `Code Queue` 前端必须把 PostgreSQL-backed backend API 作为 task、queue、readAt/未读状态和 attempt 状态的唯一数据来源;不得用 `localStorage`、`sessionStorage` 或 IndexedDB 持久化这些业务状态,也不得在后端标记已读失败时伪造本地成功。前端允许保留 React 内存态、请求 in-flight guard 和本轮页面缓存,但刷新页面或切换设备后的状态必须完全由后端 PostgreSQL 数据恢复。 + - `Code Queue` 的 queue/session 左侧边栏必须提供 task 关键词搜索,并采用顶部对齐和内容高度优先布局:搜索栏、列表、分组和 task card 都不得用居中、space-between、stretch 或隐式等高网格去拉满侧栏高度;item 少时允许下半部分留空,不能把单个 item 拉高来铺满;每个 task card 必须显示 `最近更新: ...前` 这类相对更新时间,便于判断运行中的 Trace 是否卡住。提交任务时必须立即锁定 prompt、引用 ID、queue、模型、工作目录、最大尝试和入队份数等输入控件,显示等待状态,并用前端 in-flight guard 阻止重复点击造成重复入队;当解析到多个待入队任务时必须显式要求用户勾选批量确认,防止 `---` 分隔或入队份数误操作导致错误传入多个任务。Trace 面板的主滚动条使用全站细窄现代滚动条;工具调用块内部的横向滚动必须可滚动但隐藏横向滚动条,避免移动端阅读被滚动条占用。公共 `TraceView` 的自动滚动必须采用 follow-tail 语义:只有当前滚动位置在底部附近时才跟随新增输出;用户手动向上滚动后立即暂停自动滚动,异步刷新不得把视图拉回底部,直到用户再次滚动到最底部才恢复自动跟随。 - 用户服务页面不得 iframe 业务旧前端、Todo Note 原 Vite 前端或 Pipeline 自身 WebUI,不得把用户服务后端端口暴露为浏览器直连 URL,也不得把业务 API 的 JSON 裸铺在页面上。 - `Pipeline` 子标签是 D601 `/home/ubuntu/pipeline` 的 UniDesk host UI。 - Pipeline 仓库自带 WebUI 前端已经废弃;UniDesk frontend 是唯一用户可见的 Pipeline UI。 @@ -119,8 +128,8 @@ frontend shell 必须把左侧主模块与顶部子标签编译为统一的 URL - 甘特图箭头的末端必须在目标点外回缩约一个箭头长度,箭头尖不能插入 prompt 点、控制点或 monitor 观察点内部,避免把“连线”和“真实行为点”混在一起。 - 仍处于 `running` 的 node 必须显示从实际开始时间延伸到当前时间的实时执行条,并用明确的闪动/扫描效果标识“仍在执行”;不得把 running node 渲染成只有起始点或 1s 极短条线。 - 点击甘特图中的执行线、prompt 点或控制点后,右侧边栏必须从默认收起状态展开,并展示结构化事件字段、匹配的 procedure/attempt、以及对应 OpenCode step 的摘要与展开详情,而不是在主界面直接铺 raw JSON、JSONL、worker log 或 control event 文本。 - - OpenCode step/message 展示必须进入公共 `TraceView`,视觉与交互以 `Codex Queue` 的 Trace 为唯一标准;Pipeline 原有 `pipeline-opencode-step`、`pipeline-step-message-card`、`pipeline-opencode-part` 等 step/message/tool 卡片风格已废弃,不得继续作为用户可见 Trace。 - - 右侧边栏中的 OpenCode Trace 必须把公共 session 信息(agent、model、session id)聚合到 Trace 头部,不得在每个 step 重复;Trace 正文必须由 `src/components/frontend/src/trace.tsx` 的 opencode port 转换后统一渲染,工具调用折叠、摘要、横向滚动、message 去缩进规则与 Codex Queue 完全一致。 + - OpenCode step/message 展示必须进入公共 `TraceView`,视觉与交互以 `Code Queue` 的 Trace 为唯一标准;Pipeline 原有 `pipeline-opencode-step`、`pipeline-step-message-card`、`pipeline-opencode-part` 等 step/message/tool 卡片风格已废弃,不得继续作为用户可见 Trace。 + - 右侧边栏中的 OpenCode Trace 必须把公共 session 信息(agent、model、session id)聚合到 Trace 头部,不得在每个 step 重复;Trace 正文必须由 `src/components/frontend/src/trace.tsx` 的 opencode port 转换后统一渲染,工具调用折叠、摘要、横向滚动、message 去缩进规则与 Code Queue 完全一致。 - 右侧边栏排版必须优先保护横向可读宽度:时间放在 step 顶部 header,而不是单独占用左侧窄列;默认摘要不得引入右侧边栏内部横向滚动条,也不得因为窄列挤压把 step 高度拉得过高。 - OpenCode Trace 不能使用 Pipeline 旧连续 step 装饰线或旧 step 卡片;相邻 step 之间若存在真实时间空闲区间,不得被任何连续连接线误渲染为持续执行。 - 调整任何高信息密度右侧边栏布局时,都必须把 `总高度` 与 `横向滚动条` 作为显式验收指标,用 Playwright 打开真实页面验证,而不是只看静态代码或本地想象。 @@ -142,6 +151,10 @@ frontend shell 必须把左侧主模块与顶部子标签编译为统一的 URL 前端必须把 backend-core 返回的 JSON 渲染为合适的控件:状态徽标、指标卡、表格列、标签 chip、字段摘要、任务结果卡、日志行和表单控件。默认页面禁止暴露裸 JSON、`pre` JSON 或整段 `JSON.stringify` 文本;只有用户明确点击 `查看原始JSON` 按钮后,才允许在弹窗或高级编辑区展示原始 JSON。 +## Loading State Title Indicator + +所有 UniDesk 前端模块只要处于加载、刷新、提交、抓取详情、导入导出、远程调度或其他异步等待状态,必须在对应卡片/面板/侧栏的标题框内显示公共加载小图标,避免操作员把“仍在加载”误判为“数据确实为 0”。加载图标统一复用 `src/components/frontend/src/loading-indicator.tsx` 中的 `LoadingTitle` / `LoadingIndicator`,视觉为简洁旋转圆环,不包含鼠标指针或其他额外符号;禁止各页面临时手写不同 spinner、只在按钮文案写“刷新中”或只在空状态里提示加载。新增面板组件必须透传 `loading` 属性到标题框,已有数据列表在刷新期间也应保留旧数据并在标题框显示加载图标。 + ## Login frontend 提供账号密码登录,默认账号为 `admin`,默认密码为 `Liang6516.`。登录会话使用 frontend 容器签发的 HttpOnly Cookie;浏览器后续只访问同源 frontend API,frontend 再通过 Docker 内网代理 backend-core。 diff --git a/docs/reference/microservices.md b/docs/reference/microservices.md index 64dfef2f..5d085375 100644 --- a/docs/reference/microservices.md +++ b/docs/reference/microservices.md @@ -9,7 +9,7 @@ UniDesk 用户服务是挂载到 UniDesk 核心服务上的、面向用户使用 - 用户服务后端端口默认只绑定计算节点本机地址,例如 `127.0.0.1:`,不得直接暴露公网。 - 浏览器只访问 UniDesk frontend;frontend 通过同源 `/api/microservices/*` 代理到 backend-core,backend-core 再通过目标 provider-gateway 的 `microservice.http` 能力访问计算节点本机后端。 - backend-core REST API、database 和计算节点用户服务后端都不得新增公网端口;公网入口仍只有 frontend 和 provider ingress。 -- `microservice.http` 只允许 provider-gateway 访问 `http://127.0.0.1`、`http://localhost`、`http://host.docker.internal` 这类节点本地地址;主 server 内置用户服务可使用同一 Compose 网络内的显式服务名,例如 `todo-note:4211` 或 `codex-queue:4222`。backend-core 还必须用 `allowedPathPrefixes` 和 `allowedMethods` 同时限制可代理路径和 HTTP 方法。 +- `microservice.http` 只允许 provider-gateway 访问 `http://127.0.0.1`、`http://localhost`、`http://host.docker.internal` 这类节点本地地址;主 server 内置用户服务可使用同一 Compose 网络内的显式服务名,例如 `todo-note:4211` 或 `code-queue:4222`。backend-core 还必须用 `allowedPathPrefixes` 和 `allowedMethods` 同时限制可代理路径和 HTTP 方法。 ## Config Contract @@ -65,28 +65,63 @@ Todo Note 数据迁移后必须验证:`microservice proxy todo-note /api/insta Project Manager 在 UniDesk 语境中按纯后端服务管理:不得将 `4233` 映射为公网端口。浏览器只能通过 UniDesk frontend 的 `/api/microservices/project-manager/health` 和 `/api/microservices/project-manager/proxy/...` 同源代理访问项目管理后端。 -### Codex Queue On Main Server +### Baidu Netdisk On Main Server -当前 Codex Queue 作为 `id=codex-queue` 的用户服务登记在 `config.json`: +当前 Baidu Netdisk 作为 `id=baidu-netdisk` 的用户服务登记在 `config.json`: -- Provider:`main-server`,由本机 provider-gateway 通过 `microservice.http` 访问同一 Compose 网络内的 `http://codex-queue:4222`。 -- 代码引用:`https://github.com/pikasTech/unidesk` 与配置中的 `repository.commitId`;服务源码位于 `src/components/microservices/codex-queue`,属于 UniDesk 自有控制面组件。 -- 部署引用:UniDesk 根仓库 `docker-compose.yml` 中的 `codex-queue` service,Dockerfile 为 `src/components/microservices/codex-queue/Dockerfile`,容器名为 `codex-queue-backend`。 -- 上线纪律:Codex Queue 相关的前端或后端改进必须在同一任务内正式上线并验证公网 frontend 或 live API,不能只停留在源码、构建产物或“后续再上线”。重建 `frontend` 只替换无状态 WebUI 容器,不会触碰 `codex-queue-backend`、PostgreSQL 队列或运行中 Codex thread,不能以“可能影响长期任务”为由延迟前端上线;`codex-queue-backend` 本身带有 restart-recovery,允许按 `server rebuild codex-queue` 或 Compose 重启/替换,停止、重启或重建后必须从持久化状态恢复运行中和排队任务。 -- Codex 认证:容器只从主 server 的 `/root/.codex/config.toml` 同步 Codex provider 配置到 `.state/codex-queue/codex-home`,并通过运行时环境透传 `OPENAI_API_KEY`、`CRS_OAI_KEY` 等 provider 所需变量;这些 provider 环境变量必须由 `writeComposeEnv` 写入 `.state/docker-compose.env` 并由 Compose 注入,确保 `server rebuild codex-queue` 的外部 Docker job runner、自重建和容器重启后不会丢失认证。新增 provider 的 `env_key` 时必须增加同类运行时透传和 Compose env 持久化,禁止把 Codex 或 MiniMax 密钥写入仓库文件。Codex Queue 开发容器必须只读挂载 host 的 root SSH 目录到 `/root/.ssh`(默认 `${UNIDESK_HOST_ROOT_SSH_DIR:-/root/.ssh}`),让容器内 `git push`、`ssh -T git@github.com` 与 host 使用同一套 GitHub SSH key/known_hosts;不得把私钥复制进镜像或仓库。 -- Develop-ready 镜像:Codex Queue 镜像必须在启动前预装 UniDesk/Pipeline 调试所需工具,至少包含 `codex`、`bun`、`node`、`npm`/`npx`、`git`、`rg`、`curl`、`python3`/`pip3`、`docker`、`docker compose`、`docker-compose`、`jq`、`ssh`、`rsync`、`make`、`gcc`/`g++`、`tar`、`gzip` 和 `unzip`;不得依赖 Codex 任务运行时再 `apt-get install` 这些基础环境。 +- Provider:`main-server`,由 backend-core 直接访问同一 Compose 网络内的 `http://baidu-netdisk:4244`,公网不发布 `4244`。 +- 代码引用:`https://github.com/pikasTech/unidesk` 与配置中的 `repository.commitId`;服务源码位于 `src/components/microservices/baidu-netdisk`,属于 UniDesk 自有主 server 用户服务。 +- 部署引用:UniDesk 根仓库 `docker-compose.yml` 中的 `baidu-netdisk` service,Dockerfile 为 `src/components/microservices/baidu-netdisk/Dockerfile`,容器名为 `baidu-netdisk-backend`。 +- 配置密钥:Compose 只透传 `UNIDESK_BAIDU_NETDISK_CLIENT_ID`、`UNIDESK_BAIDU_NETDISK_CLIENT_SECRET`、`UNIDESK_BAIDU_NETDISK_TOKEN_KEY` 与可选 `UNIDESK_BAIDU_NETDISK_APP_ROOT`;不得把百度 AppSecret、token key、access token 或 refresh token 写入仓库文件。 +- 配置步骤:`UNIDESK_BAIDU_NETDISK_TOKEN_KEY` 可由本机生成;百度 `client_id` 和 `client_secret` 必须由账号拥有者在百度网盘开放平台创建应用后提供,操作清单见 `docs/issue/baidu-netdisk-env-setup.md`。 +- 数据库:OAuth 设备码会话、账号摘要、加密 token、传输任务和事件写入主 PostgreSQL 表 `baidu_netdisk_*`;服务启动时自动创建/补齐 schema,不依赖仅首次生效的 database init SQL。 +- 文件边界:v1 只支持容器 staging 目录 `/data/staging` 与百度网盘应用目录之间的后台上传/下载任务;staging 目录由主 server `.state/baidu-netdisk/staging` 挂载,`.state/` 只保存可重建文件缓存,不作为 token 或任务权威状态。授权成功、账号刷新、文件列表和上传任务都会确保 `UNIDESK_BAIDU_NETDISK_APP_ROOT` 指向的 `/apps/...` 应用目录存在;目录已存在时必须返回/记录 `errno=-8` 并继续,禁止使用会重命名的策略重复创建 `_YYYYMMDD_...` 目录。 +- API:`GET /health`;`GET /api/auth/status`;`POST /api/auth/device/start`;`GET /api/auth/device/status`;`POST /api/auth/refresh`;`POST /api/auth/logout`;`GET /api/account`;`GET /api/files`;`GET /api/files/meta`;`POST /api/folders`;`POST /api/files/manage`;`POST /api/transfers/upload-from-path`;`POST /api/transfers/download-to-path`;`POST /api/self-test`;`GET /api/transfers`;`GET|POST /api/transfers/{id}/cancel|retry`;`GET /logs`。 +- 授权轮询:百度设备码轮询返回的 `authorization_pending` 和 `slow_down` 是正常中间态,后端必须把它们更新为 pending session(`slow_down` 增加轮询间隔)而不是向前端抛 HTTP 错误;只有拒绝、过期或未知 OAuth 错误才进入 rejected/expired/failed。 +- 自测:`POST /api/self-test` 会在 staging 生成小文本、上传到应用目录、通过 `/api/files` 找到 `fs_id`、下载回 staging 并校验 MD5;该端点不得回显 token/dlink,适合 CLI、前端按钮和交付验收使用。 +- 代理路径:只允许 `/health`、`/logs` 和 `/api/` 前缀;允许方法为 `GET`、`HEAD`、`POST`、`DELETE`。 +- UniDesk 前端:`用户服务 / Baidu Netdisk` React 页面负责展示设备码登录卡、账号容量、应用目录文件表、staging 上传/下载任务、上传/下载自测按钮与结果、脱敏日志和显式原始 JSON 按钮。 + +Baidu Netdisk 在 UniDesk 语境中按纯后端服务管理:不得暴露百度 token、dlink 或 staging 文件字节流给浏览器;浏览器只能通过 UniDesk frontend 的 `/api/microservices/baidu-netdisk/health` 和 `/api/microservices/baidu-netdisk/proxy/...` 同源代理访问控制面 JSON。 + +### File Browser Host Files + +当前 File Browser 作为两组用户服务登记在 `config.json`,共用上游 `https://github.com/filebrowser/filebrowser` 的 `filebrowser/filebrowser:v2.63.3` 镜像和 commit `ca5e249e3c0c94159c2136a0cd431a424eb18472`;主 server 不再运行 File Browser 容器,避免占用主 server CPU/内存和主机根目录遍历资源: + +- `id=filebrowser`:Provider 为 `D518`,服务在 D518 节点本机绑定 `4251`,provider-gateway 容器内通过 `http://host.docker.internal:4251` 访问;容器名为 `unidesk-filebrowser-d518`,挂载 D518 WSL host `/` 到 `/srv`,因此可浏览 `/mnt/c` 等 Windows 盘符。D518 Docker Desktop 的 `host.docker.internal` 指向 Windows host IP,实际部署需使用 `0.0.0.0:4251->8080` 才能让 provider-gateway 容器访问。 +- `id=filebrowser-d601`:Provider 为 `D601`,服务在 D601 节点本机绑定 `127.0.0.1:4251`,provider-gateway 容器内通过 `http://host.docker.internal:4251` 访问;容器名为 `unidesk-filebrowser-d601`,挂载 D601 WSL host `/` 到 `/srv`,因此可浏览 `/mnt/c` 等 Windows 盘符。 +- 启动参数必须包含 `--baseURL /api/microservices//proxy`、`--noauth`、`--disableExec`、`--disableTypeDetectionByHeader`、`--disableImageResolutionCalc` 和 `--disableThumbnails`;容器必须使用 `user: "0:0"` 或 `--user 0:0`,否则 upstream 镜像默认非 root 用户会导致 `/database/filebrowser.db` 写入失败。禁用头部类型探测、图片尺寸计算和缩略图可避免 Windows 盘根目录中的 `hiberfil.sys`、`pagefile.sys` 等受保护文件导致整个目录浏览失败。 +- 代理路径允许 `/`,允许方法为 `GET`、`HEAD`、`POST`、`PUT`、`PATCH` 和 `DELETE`;File Browser 的 `/api/login`、`/api/resources` 和上传 API 需要透传 `X-Auth`、`Range`、`Tus-Resumable` 等请求头。 +- provider-gateway 容器必须配置 `extra_hosts: ["host.docker.internal:host-gateway"]`,确保 D601/D518 Linux Docker 容器内能访问 WSL host 侧 `127.0.0.1:4251` 映射。 +- UniDesk 前端:`用户服务 / File Browser` React 页面展示 D518 主目标和 D601 备用目标的健康状态、仓库引用、私有后端映射,并以 iframe 嵌入对应 File Browser WebUI;页面必须提供截图导出入口,并对上游 WebUI 注入紧凑布局样式,避免 material icon 字体异常时 `folder` 文本遮挡文件名;File Browser 自身端口不得直接暴露公网。 + +### Code Queue On Main Server + +当前 Code Queue 作为 `id=code-queue` 的用户服务登记在 `config.json`: + +- Provider:`main-server`,由本机 provider-gateway 通过 `microservice.http` 访问同一 Compose 网络内的 `http://code-queue:4222`。 +- 代码引用:`https://github.com/pikasTech/unidesk` 与配置中的 `repository.commitId`;服务源码位于 `src/components/microservices/code-queue`,属于 UniDesk 自有控制面组件。 +- 部署引用:UniDesk 根仓库 `docker-compose.yml` 中的 `code-queue` service,Dockerfile 为 `src/components/microservices/code-queue/Dockerfile`,容器名为 `code-queue-backend`。 +- 上线纪律:Code Queue 相关的前端或后端改进必须在同一任务内正式上线并验证公网 frontend 或 live API,不能只停留在源码、构建产物或“后续再上线”。重建 `frontend` 只替换无状态 WebUI 容器,不会触碰 `code-queue-backend`、PostgreSQL 队列或运行中 Codex thread,不能以“可能影响长期任务”为由延迟前端上线;`code-queue-backend` 本身带有 restart-recovery,允许按 `server rebuild code-queue` 或 Compose 重启/替换,停止、重启或重建后必须从持久化状态恢复运行中和排队任务。修改 Code Queue 自身时不得等待当前 Code Queue task 结束、等待 queue idle 或等待 `0 running` 后才重启;这会等待自己退出形成自锁。应直接执行受 Compose lock、build-first、no-deps force-recreate 和 post-up validation 保护的重启/重建路径,并用恢复后的 live API 或公网 frontend 证明任务和队列仍可读可继续。 +- 更名与灾备恢复:旧版 Codex 队列服务名只允许作为兼容诊断和一次性迁移来源;`code-queue-backend` 容器自身 `/health` 正常但 `microservice health code-queue` 返回 `microservice not found`、或服务目录仍只出现旧服务 ID 时,优先判定为 backend-core 仍加载旧 `MICROSERVICES_JSON`,必须刷新 `.state/docker-compose.env` 并显式重建/重建替换 `backend-core`,随后用 `microservice list` 验证 `id=code-queue`、`nodeBaseUrl=http://code-queue:4222` 和容器摘要。若更名后 `unidesk_code_queue_*` 为空而历史 `unidesk_codex_queue_*` 表仍有队列数据,恢复前必须先停止 `code-queue-backend`,备份 `.state/code-queue` 与当前 `unidesk_code_queue_*` 表,再把历史本地状态目录合并到 `.state/code-queue/`,并用 `docker exec -i unidesk-database psql ...` 这类保持 stdin 的方式把 `unidesk_codex_queue_tasks`、`unidesk_codex_queue_queues` 和 `unidesk_codex_queue_notifications` 迁移到对应 `unidesk_code_queue_*` 表;不得在确认 `/api/tasks`、`/api/queues` 和 output archive 可读前删除历史本地状态目录或旧 PostgreSQL 表。迁移完成后只允许用 `docker compose --env-file .state/docker-compose.env up -d --no-deps code-queue` 或 `server rebuild code-queue` 启动目标服务,禁止在灾备窗口里无意执行会连带重建 database/backend-core 的裸 `up -d code-queue`。 +- Codex 认证:容器只从主 server 的 `/root/.codex/config.toml` 同步 Codex provider 配置到 `.state/code-queue/codex-home`,并通过运行时环境透传 `OPENAI_API_KEY`、`CRS_OAI_KEY` 等 provider 所需变量;这些 provider 环境变量必须由 `writeComposeEnv` 写入 `.state/docker-compose.env` 并由 Compose 注入,确保 `server rebuild code-queue` 的外部 Docker job runner、自重建和容器重启后不会丢失认证。新增 provider 的 `env_key` 时必须增加同类运行时透传和 Compose env 持久化,禁止把 Codex 或 MiniMax 密钥写入仓库文件。Code Queue 开发容器必须只读挂载 host 的 root SSH 目录到 `/root/.ssh`(默认 `${UNIDESK_HOST_ROOT_SSH_DIR:-/root/.ssh}`),让容器内 `git push`、`ssh -T git@github.com` 与 host 使用同一套 GitHub SSH key/known_hosts;不得把私钥复制进镜像或仓库。 +- Develop-ready 镜像:Code Queue 镜像必须在启动前预装 UniDesk/Pipeline 调试所需工具,至少包含 `codex`、`bun`、`node`、`npm`/`npx`、`git`、`rg`、`curl`、`python3`/`pip3`、`docker`、`docker compose`、`docker-compose`、`jq`、`ssh`、`rsync`、`make`、`gcc`/`g++`、`tar`、`gzip` 和 `unzip`;不得依赖 Codex 任务运行时再 `apt-get install` 这些基础环境。 +- 远程开发容器与任务执行 Provider:Code Queue 必须能通过 live API 拉起 D601 等计算节点上的开发容器,入口为 `POST /api/dev-containers//start`,默认 Provider 为 `D601`。该流程由 Code Queue 调用 UniDesk `ssh ` 维护桥在目标节点创建 `unidesk-codex-dev-`,并在主 server 与开发容器之间建立 `ssh -w` TUN 点对点链路;主 server 负责对开发容器的 TUN 源地址做 NAT/MASQUERADE,开发容器默认路由和 DNS 改走该 TUN,从而让 `ping google.com`、DNS、HTTP(S) 等出网都经主 server 全局代理,而不是依赖 D601 本地网络。提交 Code Queue 任务时必须支持选择执行 Provider:`main-server` 在本机 Code Queue 容器中执行且默认工作目录保持 `/root/unidesk`;其他 Provider 在对应 `unidesk-codex-dev-` 容器中执行,默认工作目录为 `/home/ubuntu`,可按任务覆盖 `cwd`。远程任务启动前必须自动复用或拉起该 Provider 的开发容器、同步 Codex 配置和允许的运行时 provider 环境变量,并通过同一 master TUN/NAT 链路出网。验收必须保留三类日志:容器直连 `google.com` 在建隧道前失败、容器建隧道后 `ping google.com` 成功、主 server 上对应 `UNIDESK-CODEX-DEV-` NAT 链或 `tun` 计数在 ping 前后增长。开发容器代理密钥只生成到 `.state/code-queue/dev-proxy/` 与目标节点用户目录,不得提交到仓库。 - Codex 控制:服务内部启动 `codex app-server --listen stdio://`,用 JSON-RPC 调用 `thread/start`、`turn/start`、`turn/steer` 和 `turn/interrupt`,并监听 `turn/completed`、assistant delta、reasoning delta、command output delta、file diff delta 等通知生成前端可轮询的 transcript。 - 用户输入持久化:任务初始 prompt 以 `basePrompt/displayPrompt` 作为结构化来源,运行中追加的 `turn/steer` prompt 必须写入 `promptHistory`;transcript 构建时从这些结构化字段合成 `Submitted prompt` 和 `Steer prompt`,不能只依赖有 600 条上限的 raw output,否则长任务输出增长后会丢失关键人工指令。 - 队列语义:`POST /api/tasks` 或 `/api/tasks/batch` 入队,服务始终只运行一个 Codex turn;当前任务真正终止后才推进下一个任务。`GET /api/tasks` 与 `GET /api/tasks/{id}` 返回队列、attempt、judge 和输出;`GET /api/tasks/{id}/summary` 返回按任务 ID 查询的结构化摘要,包括初始 prompt、最后 assistant message、工具调用摘要、attempt、judge、错误和耗时;CLI 入口是 `bun scripts/cli.ts codex task `。`POST /api/tasks/{id}/steer` 向运行中 turn 推入 prompt;`POST /api/tasks/{id}/interrupt` 或 `DELETE /api/tasks/{id}` 打断/取消;`POST /api/tasks/{id}/retry` 手动重试。队列 worker 必须隔离单个 task 的异常,不能因为某个 app-server、judge 异常或 judge 判定 `fail` 让后续 queued 任务停止;`fail` 只把当前任务标为 failed,随后必须继续扫描并推进下一个 queued/retry_wait 任务。当存在 queued/retry_wait 且 worker 空闲时,watchdog 必须自动重新调度。 -- 稳定性与重启恢复:Codex Queue 的第一目标是长期稳定可用;部署修复或运维排障时不得因为担心容器重启会打断任务而拒绝重启、重建或替换 `codex-queue-backend`。容器重启、服务进程重启和镜像替换后,队列、`promptHistory`、running/judging/retry_wait 任务和 active session 元数据必须从 PostgreSQL 恢复,并在已有 `codexThreadId` 可用时用 `thread/resume` 和 continuation prompt 无缝继续当前任务;如果原 app-server turn 已丢失,也必须把当前任务恢复到可 retry/continue 的状态,不能错误推进下一个任务或永久卡住。主 server 侧重建必须走 `server rebuild codex-queue`,该 job 受 `.state/locks/server-compose.lock` 串行化约束,并且必须在 build 后执行 no-deps force-recreate 与 post-up health validation;禁止在 job 中先手工 `docker rm` 再依赖后续命令补救,因为中断窗口会让容器消失并触发 frontend `direct microservice proxy failed`。重启后出现 active task 丢失、手动 steer/interrupt 记录丢失、running 任务卡死、误判完成、跳过当前任务、容器消失或阻塞队列,均属于 Codex Queue 的 P0 核心缺陷,必须先修复并补充 restart-recovery 验收,不能把“避免重启”作为交付策略。 -- 完成判定:app-server `turn/completed` 的 `turn.status=completed|interrupted|failed` 只代表 Codex turn 已结束;即使 `completed` 也必须把原始任务、assistant 最终回复、command/file-change 事件、stderr tail 和 recent events 组成 execution record 交给 judge 判断是否真的完成。配置了 `UNIDESK_CODEX_QUEUE_MINIMAX_API_KEY` 且 MiniMax 可用时,MiniMax `MiniMax-M2.7` 对 `complete|retry|fail` 的判定是权威结果;任何非 LLM 判断,包括字符串匹配、正则、硬编码 safety override,都不得覆盖、降级或提升一次成功的 MiniMax 判定。非 LLM/fallback 判断只允许在 MiniMax 未配置、额度/限流/网络/超时不可用,或 JSON 去噪与 repair 全部耗尽后启用。MiniMax 返回必须先做 JSON 去噪,支持去除 Markdown fence、`json` 标签和从夹杂文本中提取平衡 JSON object;如果去噪后仍无法解析,服务必须把解析错误和上一轮去噪前原始回答反馈给 MiniMax 做 JSON repair 重试,重试次数由 `UNIDESK_CODEX_QUEUE_MINIMAX_JUDGE_REPAIR_ATTEMPTS` 控制,默认 `2`,耗尽后才进入 fallback,并在 fallback 原因中保留 MiniMax 失败信息。 -- Retry/推进语义:`retry` 不是新开一个独立任务或完全新 session;只要已有 `codexThreadId`,服务必须 `thread/resume` 原 thread 并 append 一个继续执行 prompt。continuation/judge feedback prompt 只应携带本轮缺口、恢复原因、验收要求和有界原始任务摘要,禁止重新注入完整引用上下文、历史 transcript 或长 JSON;服务重启恢复类 feedback 尤其必须保持短 prompt,依赖现有 thread 上文继续。只有 judge 判定 `complete` 后,队列 worker 才把当前任务标为成功并推进下一个 queued/retry_wait 任务。非 LLM/fallback 判定产生的 `retry` 最多累计 `3` 次;达到上限后当前任务必须转为 `failed` 并记录原因,worker 继续推进后续 queued/retry_wait 任务,避免 fallback safety override 或硬编码判断造成无限循环。 +- 稳定性与重启恢复:Code Queue 的第一目标是长期稳定可用;部署修复或运维排障时不得因为担心容器重启会打断任务而拒绝重启、重建或替换 `code-queue-backend`。容器重启、服务进程重启和镜像替换后,队列、`promptHistory`、running/judging/retry_wait 任务和 active session 元数据必须从 PostgreSQL 恢复,并在已有 `codexThreadId` 可用时用 `thread/resume` 和 continuation prompt 无缝继续当前任务;如果原 app-server turn 已丢失,也必须把当前任务恢复到可 retry/continue 的状态,不能错误推进下一个任务或永久卡住。主 server 侧重建必须走 `server rebuild code-queue`,该 job 受 `.state/locks/server-compose.lock` 串行化约束,并且必须在 build 后执行 no-deps force-recreate 与 post-up health validation;禁止在 job 中先手工 `docker rm` 再依赖后续命令补救,因为中断窗口会让容器消失并触发 frontend `direct microservice proxy failed`。重启后出现 active task 丢失、手动 steer/interrupt 记录丢失、running 任务卡死、误判完成、跳过当前任务、容器消失或阻塞队列,均属于 Code Queue 的 P0 核心缺陷,必须先修复并补充 restart-recovery 验收,不能把“避免重启”作为交付策略。 +- 调度与 active run slot:Code Queue 必须把“queue processor 正在等待/退避/轮询”和“实际占用 Codex/OpenCode 子进程运行槽”分开建模;`CODE_QUEUE_MAX_ACTIVE_QUEUES` 只限制真实 active run slot,不能把 retry backoff、等待内存下降或等待前序任务的 `processingQueues` 计入 active slot,否则默认 `maxActiveQueues=1` 时一个空等队列会把其他 runnable queue 永久饿死。多个 queue 同时等待 active slot 时必须显式维护 FIFO waiter 队列,避免某个长 retry/backoff 队列刚释放 slot 就立刻重抢,导致更早进入等待的 `retry_wait` 任务长期饥饿;`/health` 必须同时暴露真实 `activeQueueIds`、`activeRunSlotCount`、等待中的 `processingQueueIds` 和 active slot waiters,排障时以 active run slot 与 waiter 顺序判断是否真的有任务在跑、谁应下一个启动。restart-recovery 后的 `retry_wait` 任务若缺失 `codexThreadId`/OpenCode session id,不得无限拒绝 retry;必须用紧凑 recovery prompt 和原始任务摘要重新开一个 agent thread/session,让任务继续推进并在 Trace 中留下 recovery 证据。任何修改 scheduler、retry backoff、queue move、manual retry、shutdown recovery 或内存等待逻辑时,都必须保留“空等 processor 不占 active run slot”、“等待者 FIFO 不饥饿”和“缺失 thread/session 可恢复”的自测或 live 验证。 +- 内存优化过程与防回归:主 server 内存预算很小,Code Queue 的内存治理必须按“PostgreSQL 权威源优先、进程热状态最小化、容器硬上限兜底”的顺序设计。长期可复用的优化路径是:先确认任务、queue、readAt、promptHistory、active session 和通知 outbox 均可从 PostgreSQL 恢复;再把历史任务列表、详情、统计、Trace/output 和 `/health` 的只读查询改为 PostgreSQL 直读或聚合查询;随后只把 `queued`、`running`、`judging`、`retry_wait` 等调度必需任务载入 Bun 堆,并在 PostgreSQL 查询侧裁剪 hot `output`/`events`;最后用 dirty-only flush、append-only 输出归档、Codex SQLite 小批量导出、`bun --smol`、`mem_limit=600m`、`memswap_limit=1536m`、`NODE_OPTIONS=--max-old-space-size=768` 和 cgroup memory watchdog 作为运行时防线。PostgreSQL 到进程的单次读取足够快,不能为了减少 SQL 查询把全部历史 `task_json`、Trace、output 或统计摘要常驻内存;任何新增缓存都必须有默认较小的环境变量上限、明确淘汰策略、可从 PostgreSQL 或 append-only 归档重建,且不得影响重启恢复。新增或修改 `/api/tasks`、overview、stats、summary、transcript、output、trace、health、flush、scheduler 和通知路径时,禁止在常规请求中调用会物化全量历史任务 JSON 的代码,禁止启动后无条件重写全量历史 task JSON,禁止用未设上限的 `Map`/数组保存历史 output/event/Trace,禁止把 `CODE_QUEUE_MAX_ACTIVE_QUEUES` 在 600M 容器默认值下调高到大于 `1`;如确需更大热窗口或并发度,必须同时提高容器内存预算并补充内存压测验收。memory watchdog 必须以 cgroup working set 为主要判断,且在 swap 仍有余量时不得提前杀掉唯一 active run;否则 TypeScript/Playwright 这类短时高内存验证会被错误中断并让 retry 队列反复震荡。 +- 完成判定:app-server `turn/completed` 的 `turn.status=completed|interrupted|failed` 只代表 Codex turn 已结束;即使 `completed` 也必须把原始任务、assistant 最终回复、command/file-change 事件、stderr tail 和 current attempt events 组成 execution record 交给 judge 判断是否真的完成。配置了 `UNIDESK_CODE_QUEUE_MINIMAX_API_KEY` 且 MiniMax 可用时,MiniMax `MiniMax-M2.7` 对 `complete|retry|fail` 的判定是权威结果;当且仅当 MiniMax LLM 调用失效(未配置、额度/限流/网络/超时不可用、JSON 去噪与 repair 全部耗尽、或返回超预算反馈且修复耗尽)时,才允许启用非 LLM/fallback 判断。任何字符串匹配、正则、硬编码 safety override、`hardCompletionBlockers`/`retryRequiredReasons`、`recentOutput` 中旧 attempt 的 429/exceeded retry limit 证据、或面向特定任务的保护逻辑,都不得覆盖、降级、提升或重写一次成功的 MiniMax 判定;尤其不能因为 attempt 1 的限流中断仍在历史输出里,就禁止 MiniMax 把 attempt 2 的正常完成判为 `complete`。MiniMax 返回必须先做 JSON 去噪,支持去除 Markdown fence、`json` 标签和从夹杂文本中提取平衡 JSON object;如果去噪后仍无法解析,服务必须把解析错误和上一轮去噪前原始回答反馈给 MiniMax 做 JSON repair 重试,重试次数由 `UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_REPAIR_ATTEMPTS` 控制,默认 `2`,耗尽后才进入 fallback,并在 fallback 原因中保留 MiniMax 失败信息。 +- Judge 权威边界:MiniMax 成功返回可解析、预算内的 judge JSON 后,Code Queue 必须直接采用该 `decision/reason/continuePrompt`,不得再执行本地 post-validation、协议级完成门禁或 safety override;`hardCompletionBlockers`、`retryRequiredReasons` 这类本地门禁字段不得出现在发送给 MiniMax 的 `executionRecord` 中。只有 MiniMax 不可用或修复耗尽进入 fallback 时,才允许基于字符串/正则做保守 retry。 +- Retry/推进语义:`retry` 不是新开一个独立任务或完全新 session;只要已有 `codexThreadId`,服务必须 `thread/resume` 原 thread 并 append 一个继续执行 prompt。continuation/judge feedback prompt 只应携带本轮缺口、恢复原因、验收要求和有界原始任务摘要,禁止重新注入完整引用上下文、历史 transcript 或长 JSON;服务重启恢复类 feedback 尤其必须保持短 prompt,依赖现有 thread 上文继续。超长 prompt 必须在 prompt 合成源头解决:每个 feedback/recovery/judge 生成器都要从结构化字段选择必要信息、去重合并缺口并提供按需查询入口,禁止先合成超长 prompt 再在末端用 substring/safePreview 一刀切硬截断;硬截断会静默丢失验收信息,风险高于长 prompt 本身。若 MiniMax `continuePrompt` 超出预算,必须要求 MiniMax 基于原始 judge 输入重新合成紧凑反馈,repair 耗尽后才可进入 fallback;不得把已生成的长 prompt 截尾后发送给 Codex。若 MiniMax 成功返回了预算内 `continuePrompt`,必须原样使用该反馈,不得再用 71-Freq、`period_sum/mpu_read_num`、`mpu_read_num`、历史限流中断等字符串识别把它覆盖成“简洁原始需求 continuation”。只有 judge 判定 `complete` 后,队列 worker 才把当前任务标为成功并推进下一个 queued/retry_wait 任务。非 LLM/fallback 判定产生的 `retry` 最多累计 `3` 次;达到上限后当前任务必须转为 `failed` 并记录原因,worker 继续推进后续 queued/retry_wait 任务,避免 fallback safety override 或硬编码判断造成无限循环。 - Judge 探针:`GET|POST /api/judge/probe` 使用同一套 judge 逻辑跑内置 synthetic execution records,覆盖正常完成、正常结束但只给计划、未上线/未部署的服务或 WebUI 改动、传输中断和用户打断等样本,返回 `hits`、`total`、`hitRate`、每例 `expected` 与 `decision`;该接口不得回显 MiniMax API key。 -- 模型选择:默认 Codex 模型是 `gpt-5.5`,内置模型队列包含 `gpt-5.5`、`gpt-5.4-mini`、`gpt-5.4`;`gpt-5.5` 的默认 reasoning effort 必须是 `xhigh`,可通过 `CODEX_QUEUE_MODEL_REASONING_EFFORTS` 追加或覆盖模型级默认值;每个入队任务可通过前端模型下拉菜单或 API 覆盖 `model`、`cwd`、`reasoningEffort` 和 `maxAttempts`,`maxAttempts` 上限为 `99`。Judge 判定 `retry` 或非用户取消类 `fail` 时必须继续已有 `codexThreadId`,不能新建 session;重试间隔使用指数退避,从 `1s` 开始,最大 `10min`。429、Too Many Requests、exceeded retry limit、overloaded、stream disconnected 等服务/限流错误一律判定为 `retry`,不能当作完成。 -- 状态与日志:默认工作目录为容器内 `/root/unidesk`,该路径映射主 server 的 `~/unidesk`;同时保留 `/workspace` 映射以兼容历史任务。Codex Queue 的任务、queue、`readAt`/未读状态、attempt、judge、`promptHistory`、active session 元数据、控制状态和 ClaudeQQ 通知 outbox 一律以主 PostgreSQL 为权威,分别写入 `unidesk_codex_queue_tasks`、`unidesk_codex_queue_queues` 与 `unidesk_codex_queue_notifications`;`DATABASE_URL` 是必需配置,服务不得在 PostgreSQL 缺失或不可用时进入文件存储模式。`.state/codex-queue/state.json` 不再作为任务或 queue 状态存储,不得重新引入本地 JSON fallback;服务启动必须以 PostgreSQL 为唯一来源恢复队列,并把 running/judging 任务恢复为 retry_wait。WebUI 不得用 browser `localStorage`、`sessionStorage` 或 IndexedDB 持久化 task/queue/readAt/unread 等业务状态;浏览器只能保留临时 UI 内存缓存,刷新后必须重新从后端读取 PostgreSQL 权威数据。Codex CLI-like output/Trace 的完整记录可以使用 append-only 文件作为日志型归档,但任务状态、未读状态和列表摘要不得依赖这些文件作为权威来源;`/api/tasks//transcript` 与 `/api/tasks//output` 必须能分页重建完整历史,不得因为热状态裁剪而丢失早期 trace。热 task JSON 只保留可配置窗口(默认 600 条 output、400 条 event)以保证 `/health`、`/api/tasks` 和 PostgreSQL flush 不被长任务拖死。WebUI 必须支持多 queue 查看、显式创建 queue、提交时下拉选择 queue,并支持把已创建且非 active 的任务移动到其他 queue;queue 内串行,queue 间并行。Codex Queue 镜像必须内置 Playwright Chromium 浏览器与系统依赖,保证队列任务能直接执行公网 frontend Playwright 回归,不得只在宿主机临时安装。日志写入 UniDesk `logs/{YYYYMMDD}/..._codex-queue.jsonl`,`/logs` 端点返回最近结构化日志。`/health` 的 `queue.storage.primary` 必须恒为 `postgres`,并通过 `queue.storage.postgresReady`、`queue.devReady` 和 `/api/dev-ready` 暴露 PostgreSQL 可用性、develop-ready 自检、必需工具、Docker socket、`docker compose`、默认工作目录、Codex config 状态和 `/root/.ssh` 共享 SSH key 状态。Codex CLI-like 输出可能很大,服务必须节流状态持久化,禁止对每个 output delta 同步重写完整 state 导致 `/health` 和控制 API 卡死;容器 healthcheck 必须使用带超时的 HTTP 探针,不能留下堆积的无超时探针进程。 -- ClaudeQQ 通知:Codex Queue 可通过 backend-core 的 `claudeqq` 用户服务代理调用 `POST /api/push/text`,在每个任务进入 `succeeded`、`failed` 或 `canceled` 终态后向配置目标发送最终 response,并附带 task id、queue、状态、模型、attempt、当前 running/queued/retry_wait 数和任务总耗时;当所有 queue 进入 `0 running / 0 queued` 空闲态时,必须单独发送一次空闲提醒。通知由 `CODEX_QUEUE_NOTIFY_CLAUDEQQ_ENABLED` 控制,目标由 `CODEX_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE=private|group`、`CODEX_QUEUE_NOTIFY_CLAUDEQQ_USER_ID`、`CODEX_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID` 配置,默认私聊 `645275593`;代理基址、最终 response 最大字符数、单次超时和发送尝试次数分别由 `CODEX_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL`、`CODEX_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS`、`CODEX_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS` 和 `CODEX_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS` 配置。任务终态和队列空闲通知必须先写入 PostgreSQL outbox 表 `unidesk_codex_queue_notifications` 再异步发送;不得使用 `.state/codex-queue/claudeqq-notifications.json`、`CODEX_QUEUE_NOTIFY_CLAUDEQQ_OUTBOX_PATH` 或任何本地 JSON 作为通知权威存储。发送失败、NapCat 离线、代理 502 或容器重启时不能丢通知,必须按 `CODEX_QUEUE_NOTIFY_CLAUDEQQ_RETRY_INTERVAL_MS` 指数退避重试并跨进程/容器重启保留。`/health` 的 `queue.notifications.claudeqq` 必须暴露非敏感配置、目标配置状态和 PostgreSQL outbox 统计;`GET /api/notifications/claudeqq` 返回 outbox 明细,`POST /api/notifications/claudeqq/drain` 手动触发发送,`POST /api/notifications/claudeqq/backfill` 可按 `since` 补入某次故障窗口内已终态任务,确保 QQ/NapCat 超时或离线不会让任务完成通知永久丢失。 -- 代理路径:只允许 `/health`、`/logs` 和 `/api/` 前缀;允许方法为 `GET`、`HEAD`、`POST`、`DELETE`。Codex Queue 只在 Compose 内网暴露 `4222/tcp`,不得映射或开放到公网。 -- UniDesk 前端:`用户服务 / Codex Queue` React 页面负责展示队列卡片、任务 ID、复制任务 ID、引用按钮、任务耗时、默认模型、模型下拉、显式入队份数、引用任务 ID、清空输入、创建成功提示、MiniMax judge 状态、Codex CLI-like 输出流、attempt 终态、追加 prompt、打断和手动重试控件;整个 agent loop 消息流统一命名为专有名词 `Trace`,`Trace` 包含 assistant message、user prompt、system event 和 tool call;Codex Queue 与 Pipeline/OpenCode messages 必须共用 `src/components/frontend/src/trace.tsx` 的 Trace 公共组件、统一 Trace item 接口和 codex/opencode port 适配层;连续 read/edit/run 工具调用只是在 Trace 内折叠为可展开工具调用组,汇总格式至少包含 `xx read, xx edit, xx run`,并展示读取文件、编辑文件、运行命令和耗时摘要;最近 3 个工具调用保持展开,工具调用内容不得自动换行且必须在工具调用块内部横向滚动,工具调用组展开后不得再增加额外左侧缩进;message 与 prompt 必须自动换行,普通 message 不显示左侧项目符号缩进且永不折叠;点击队列卡片引用按钮必须自动把该任务 ID 写入提交表单的引用任务 ID 输入框;引用任务 ID 创建新任务时必须自动注入 `bun scripts/cli.ts codex task ` 的提示,让 Codex 读取初始 prompt、最后消息和工具摘要后继续;连续执行同一 prompt 应使用 `入队份数` 一次性生成多条队列任务,而不是依赖快速连点按钮;原始任务 JSON 只能通过显式 `查看原始JSON` 打开。 +- 模型选择:默认 Codex 模型是 `gpt-5.5`,内置模型队列包含 `gpt-5.5`、`gpt-5.4-mini`、`gpt-5.4`;`gpt-5.5` 的默认 reasoning effort 必须是 `xhigh`,可通过 `CODE_QUEUE_MODEL_REASONING_EFFORTS` 追加或覆盖模型级默认值;每个入队任务可通过前端模型下拉菜单或 API 覆盖 `model`、`cwd`、`reasoningEffort` 和 `maxAttempts`,`maxAttempts` 上限为 `99`。Judge 判定 `retry` 或非用户取消类 `fail` 时必须继续已有 `codexThreadId`,不能新建 session;重试间隔使用指数退避,从 `1s` 开始,最大 `10min`。MiniMax 不可用而进入 fallback/non-LLM 判定时,当前 attempt 的 429、Too Many Requests、exceeded retry limit、overloaded、stream disconnected 等服务/限流错误应判定为 `retry`,不能当作完成;MiniMax 可用时,这些内容只能作为当前 attempt 的 factual evidence 提供给 MiniMax,不能通过硬编码覆盖 MiniMax 结果。 +- 状态与日志:`main-server` 默认工作目录为容器内 `/root/unidesk`,该路径映射主 server 的 `~/unidesk`;同时保留 `/workspace` 映射以兼容历史任务。非主 server Provider 的任务默认工作目录为 `/home/ubuntu`,任务 JSON、列表、Trace 摘要和 CLI 查询都必须显示 `providerId` 与最终 `cwd`。Code Queue 的任务、queue、`readAt`/未读状态、attempt、judge、`promptHistory`、active session 元数据、控制状态和 ClaudeQQ 通知 outbox 一律以主 PostgreSQL 为权威,分别写入 `unidesk_code_queue_tasks`、`unidesk_code_queue_queues` 与 `unidesk_code_queue_notifications`;`DATABASE_URL` 是必需配置,服务不得在 PostgreSQL 缺失或不可用时进入文件存储模式。`.state/code-queue/state.json` 不再作为任务或 queue 状态存储,不得重新引入本地 JSON fallback;服务启动必须以 PostgreSQL 为唯一来源恢复队列,并把 running/judging 任务恢复为 retry_wait。主 server 内存很少,Code Queue 必须把“内存是稀缺资源”作为核心设计约束:历史任务列表、详情、统计和只读 Trace 查询优先从 PostgreSQL 直读,进程内只保留当前 running/judging、queued、retry_wait 等调度必需热任务,不得把全部历史 task JSON 长期缓存到 Bun 堆;需要短期热缓存时必须有严格上限、可裁剪、可从 PostgreSQL 和 append-only 输出归档重建。WebUI 不得用 browser `localStorage`、`sessionStorage` 或 IndexedDB 持久化 task/queue/readAt/unread 等业务状态;浏览器只能保留临时 UI 内存缓存,刷新后必须重新从后端读取 PostgreSQL 权威数据。Codex CLI-like output/Trace 的完整记录可以使用 append-only 文件作为日志型归档,但任务状态、未读状态和列表摘要不得依赖这些文件作为权威来源;`/api/tasks//transcript` 与 `/api/tasks//output` 必须能分页重建完整历史,不得因为热状态裁剪而丢失早期 trace。热 task JSON 只保留可配置窗口以保证 `/health`、`/api/tasks` 和 PostgreSQL flush 不被长任务拖死;主 server 为 Code Queue 放宽到 600M 容器预算后仍默认 `CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS=10`、`CODE_QUEUE_IN_MEMORY_EVENT_RECORDS=10`,启动时必须在 PostgreSQL 查询侧裁剪 hot output/events,并只 flush dirty task,禁止启动后无条件重写全量历史 task JSON;更高预算才允许调大热窗口。WebUI 必须支持多 queue 查看、显式创建 queue、提交时下拉选择 queue、提交时下拉选择执行 Provider,并支持把已创建且非 active 的任务移动到其他 queue;queue 内串行,queue 间可并行,但并行度必须受 `CODE_QUEUE_MAX_ACTIVE_QUEUES` 全局上限约束,600M 容器默认仍只运行 1 个 active queue,避免多个 Codex app-server 同时把容器推过内存上限。Code Queue 镜像必须内置 Playwright Chromium 浏览器与系统依赖,并使用 `bun --smol` 运行后端,保证队列任务能直接执行公网 frontend Playwright 回归且主进程内存可控,不得只在宿主机临时安装。日志写入 UniDesk `logs/{YYYYMMDD}/{startStamp}_{YYYYMMDD}_{HH}_code-queue.jsonl`,按小时切片并按日志族默认保留 `1GiB`;Codex app-server 上游产生的 `logs_*.sqlite` 只能作为短暂缓冲,必须由 Code Queue 周期性导出为 `logs/{YYYYMMDD}/{startStamp}_{YYYYMMDD}_{HH}_codex-app-server.jsonl`,导出后删除/压缩已导出的 SQLite 行,避免重新形成 `logs_2.sqlite` 大文件;`/logs` 端点返回最近结构化日志。`/health` 的 `queue.storage.primary` 必须恒为 `postgres`,并通过 `queue.storage.postgresReady`、`queue.devReady` 和 `/api/dev-ready` 暴露 PostgreSQL 可用性、develop-ready 自检、必需工具、Docker socket、`docker compose`、默认工作目录、Codex config 状态和 `/root/.ssh` 共享 SSH key 状态。Codex CLI-like 输出可能很大,服务必须节流状态持久化,禁止对每个 output delta 同步重写完整 state 导致 `/health` 和控制 API 卡死;容器 healthcheck 必须使用带超时的 HTTP 探针,不能留下堆积的无超时探针进程。 +- ClaudeQQ 通知:Code Queue 可通过 backend-core 的 `claudeqq` 用户服务代理调用 `POST /api/push/text`,在每个任务进入 `succeeded`、`failed` 或 `canceled` 终态后向配置目标发送最终 response,并附带 task id、queue、状态、模型、attempt、当前 running/queued/retry_wait 数和任务总耗时;当所有 queue 进入 `0 running / 0 queued` 空闲态时,必须单独发送一次空闲提醒。通知由 `CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED` 控制,目标由 `CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE=private|group`、`CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID`、`CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID` 配置,默认私聊 `645275593`;代理基址、最终 response 最大字符数、单次超时和发送尝试次数分别由 `CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL`、`CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS`、`CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS` 和 `CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS` 配置。任务终态和队列空闲通知必须先写入 PostgreSQL outbox 表 `unidesk_code_queue_notifications` 再异步发送;不得使用 `.state/code-queue/claudeqq-notifications.json`、`CODE_QUEUE_NOTIFY_CLAUDEQQ_OUTBOX_PATH` 或任何本地 JSON 作为通知权威存储。发送失败、NapCat 离线、代理 502 或容器重启时不能丢通知,必须按 `CODE_QUEUE_NOTIFY_CLAUDEQQ_RETRY_INTERVAL_MS` 指数退避重试并跨进程/容器重启保留。`/health` 的 `queue.notifications.claudeqq` 必须暴露非敏感配置、目标配置状态和 PostgreSQL outbox 统计;`GET /api/notifications/claudeqq` 返回 outbox 明细,`POST /api/notifications/claudeqq/drain` 手动触发发送,`POST /api/notifications/claudeqq/backfill` 可按 `since` 补入某次故障窗口内已终态任务,确保 QQ/NapCat 超时或离线不会让任务完成通知永久丢失。 +- 代理路径:只允许 `/health`、`/logs` 和 `/api/` 前缀;允许方法为 `GET`、`HEAD`、`POST`、`DELETE`。Code Queue 只在 Compose 内网暴露 `4222/tcp`,不得映射或开放到公网。 +- UniDesk 前端:`用户服务 / Code Queue` React 页面负责展示队列卡片、任务 ID、复制任务 ID、引用按钮、任务耗时、默认模型、模型下拉、执行 Provider 下拉、Provider 对应默认工作目录、显式入队份数、引用任务 ID、清空输入、创建成功提示、MiniMax judge 状态、Codex CLI-like 输出流、attempt 终态、追加 prompt、打断和手动重试控件;整个 agent loop 消息流统一命名为专有名词 `Trace`,`Trace` 包含 assistant message、user prompt、system event 和 tool call;Code Queue 与 Pipeline/OpenCode messages 必须共用 `src/components/frontend/src/trace.tsx` 的 Trace 公共组件、统一 Trace item 接口和 codex/opencode port 适配层;连续 read/edit/run 工具调用只是在 Trace 内折叠为可展开工具调用组,汇总格式至少包含 `xx read, xx edit, xx run`,并展示读取文件、编辑文件、运行命令和耗时摘要;最近 3 个工具调用保持展开,工具调用内容不得自动换行且必须在工具调用块内部横向滚动,工具调用组展开后不得再增加额外左侧缩进;message 与 prompt 必须自动换行,普通 message 不显示左侧项目符号缩进且永不折叠;点击队列卡片引用按钮必须自动把该任务 ID 写入提交表单的引用任务 ID 输入框;引用任务 ID 创建新任务时必须自动注入 `bun scripts/cli.ts codex task ` 的提示,让 Codex 读取初始 prompt、最后消息和工具摘要后继续;连续执行同一 prompt 应使用 `入队份数` 一次性生成多条队列任务,而不是依赖快速连点按钮;原始任务 JSON 只能通过显式 `查看原始JSON` 打开。 ## D601 User Services @@ -176,14 +211,15 @@ ClaudeQQ 在 UniDesk 语境中按消息网关后端服务管理:不得直接 - `bun scripts/cli.ts microservice proxy met-nonlinear '/api/projects?root=projects&limit=500'` 与 `bun scripts/cli.ts microservice proxy met-nonlinear '/api/projects/config?path=projects/' --raw`:验证项目库文件树输入和结构化项目详情;详情应包含 config、progress、data、model、metrics 字段,供前端渲染训练状态、模型参数量和指标。 - `bun scripts/cli.ts microservice health claudeqq`、`bun scripts/cli.ts microservice proxy claudeqq /api/napcat/login`、`bun scripts/cli.ts microservice proxy claudeqq /api/events/recent` 和 `bun scripts/cli.ts microservice proxy claudeqq /api/events/subscriptions`:验证 ClaudeQQ 后端、NapCat 容器登录、事件订阅和私有代理链路;消息推送使用 `POST /api/push/text`,不得开放 D601 `3290/3000/3001/6099` 公网端口。 - `bun scripts/cli.ts microservice health todo-note` 与 `bun scripts/cli.ts microservice proxy todo-note /api/instances`:验证主 server Todo Note 后端、PostgreSQL 存储和本机 provider-gateway 私有代理链路。 -- `bun scripts/cli.ts microservice health codex-queue` 与 `bun scripts/cli.ts microservice proxy codex-queue /api/tasks`:验证主 server Codex Queue 后端、PostgreSQL 强制持久化和本机 provider-gateway 私有代理链路;写入、追加 prompt、打断和 readAt/未读状态都必须由 backend 写入 PostgreSQL,frontend 不得用本地存储伪造成功状态。 +- `bun scripts/cli.ts microservice health code-queue` 与 `bun scripts/cli.ts microservice proxy code-queue /api/tasks`:验证主 server Code Queue 后端、PostgreSQL 强制持久化和本机 provider-gateway 私有代理链路;写入、追加 prompt、打断和 readAt/未读状态都必须由 backend 写入 PostgreSQL,frontend 不得用本地存储伪造成功状态。 +- `bun scripts/cli.ts microservice health filebrowser`、`bun scripts/cli.ts microservice health filebrowser-d601` 与 `bun scripts/cli.ts microservice proxy filebrowser / --max-body-bytes 2000`:验证 D518 主 File Browser 和 D601 备用 File Browser 私有代理链路;浏览器 WebUI 必须通过 `/api/microservices/filebrowser/proxy/` 或 `/api/microservices/filebrowser-d601/proxy/` 访问,不得直接开放 `4251` 公网端口。 - `bun scripts/cli.ts --main-server-ip 74.48.78.17 microservice health findjob`:在计算节点或其他非主 server 主机上通过公网 frontend remote CLI 进行同一验证,不需要主 server SSH key。 `debug dispatch D601 microservice.http --payload-json ...` 仅用于开发调试 provider-gateway 代理能力;正式验收和用户入口应优先使用 `microservice` 命令与 frontend 用户服务页面。 ## Frontend Rules -用户服务前端必须整合到 `src/components/frontend/src/` 下的 TypeScript + React 模块中。`app.tsx` 只做 shell/router 和导入分发,业务页面必须拆成独立 TSX,例如 `todo-note.tsx`、`findjob.tsx`、`pipeline.tsx`、`met-nonlinear.tsx`、`codex-queue.tsx`。默认展示必须是业务控件:指标卡、状态徽标、表格、草稿卡片、运行卡片、树形任务、表单控件、结构化材料索引、链接和字段摘要;只有操作员点击 `查看原始JSON` 时才允许打开原始 JSON 弹窗。日志、JSONL 和大块 JSON 不得在主界面按行展示,避免把裸数据伪装成 UI。 +用户服务前端必须整合到 `src/components/frontend/src/` 下的 TypeScript + React 模块中。`app.tsx` 只做 shell/router 和导入分发,业务页面必须拆成独立 TSX,例如 `todo-note.tsx`、`findjob.tsx`、`pipeline.tsx`、`met-nonlinear.tsx`、`code-queue.tsx`。默认展示必须是业务控件:指标卡、状态徽标、表格、草稿卡片、运行卡片、树形任务、表单控件、结构化材料索引、链接和字段摘要;只有操作员点击 `查看原始JSON` 时才允许打开原始 JSON 弹窗。日志、JSONL 和大块 JSON 不得在主界面按行展示,避免把裸数据伪装成 UI。 对于超大业务 JSON,backend-core 可把 `__unideskArrayLimit=:` 作为 frontend-only 代理参数传给 provider-gateway,由 provider-gateway 在返回前裁剪指定 JSON 数组并写入 `_unidesk.arrayLimits` 元数据。该参数只用于控制 UniDesk 展示预览,不能替代业务后端自身分页 API 的长期设计。CLI 的 `microservice proxy` 还会对超过默认阈值的 body 做二次有界预览,防止人工验证时输出爆炸;只有显式 `--raw` 才允许倾倒完整 body。 @@ -195,13 +231,16 @@ ClaudeQQ 在 UniDesk 语境中按消息网关后端服务管理:不得直接 - 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `pipeline` 的 `providerId=D601`、`public=false`、`frontendOnly=true`、仓库 URL、commit id、`127.0.0.1:18082` 映射和 `pipeline-v2-control` 容器摘要可见。 - 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `met-nonlinear` 的 `providerId=D601`、`public=false`、`frontendOnly=true`、仓库 URL、commit id、`127.0.0.1:3288` 映射和 `met-nonlinear-ts` 容器摘要可见。 - 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `claudeqq` 的 `providerId=D601`、`public=false`、`frontendOnly=true`、仓库 URL、commit id、`127.0.0.1:3290` 映射和 `claudeqq-backend` 容器摘要可见。 -- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `codex-queue` 的 `providerId=main-server`、`public=false`、`frontendOnly=true`、UniDesk 仓库 URL、`codex-queue:4222` 映射和 `codex-queue-backend` 容器摘要可见。 +- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `code-queue` 的 `providerId=main-server`、`public=false`、`frontendOnly=true`、UniDesk 仓库 URL、`code-queue:4222` 映射和 `code-queue-backend` 容器摘要可见。 +- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `filebrowser` 和 `filebrowser-d601` 分别显示为 `providerId=D518` 和 `providerId=D601`,均为 `public=false`、`frontendOnly=true`,仓库 URL 为 `https://github.com/filebrowser/filebrowser`,后端映射为 `host.docker.internal:4251`,容器摘要分别为 `unidesk-filebrowser-d518` 和 `unidesk-filebrowser-d601`;列表中不得再出现主 server `filebrowser-main` 容器。 - 运行 `bun scripts/cli.ts microservice health findjob` 与 `bun scripts/cli.ts microservice proxy findjob /api/summary`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 FindJob 后端。 - 运行 `bun scripts/cli.ts microservice health pipeline` 与 `bun scripts/cli.ts microservice proxy pipeline '/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3'`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 Pipeline 后端,且 run/procedure 摘要包含甘特图所需时间字段。 - 运行 `bun scripts/cli.ts microservice health met-nonlinear`、`bun scripts/cli.ts microservice proxy met-nonlinear /api/queue`、`bun scripts/cli.ts microservice proxy met-nonlinear '/api/projects?root=projects&limit=20'` 和 `bun scripts/cli.ts microservice proxy met-nonlinear /api/images`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 MET Nonlinear TS 后端。 - 运行 `bun scripts/cli.ts microservice health claudeqq`、`bun scripts/cli.ts microservice proxy claudeqq /api/napcat/login`、`bun scripts/cli.ts microservice proxy claudeqq /api/events/recent` 和 `bun scripts/cli.ts microservice proxy claudeqq /api/events/subscriptions`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 ClaudeQQ 后端;在 D601 上 `curl http://127.0.0.1:3290/health` 应显示 `service=claudeqq`、`pureBackend=true`、`napcat.containerized=true`、NapCat HTTP/WS 状态、二维码状态和订阅计数。 - 运行 `bun scripts/cli.ts microservice health todo-note` 与 `bun scripts/cli.ts microservice proxy todo-note /api/instances`,确认真实链路经过 backend-core、WebSocket、main-server provider-gateway 和主 server `todo-note-backend` 后端;输出中必须包含五个迁移清单和 PostgreSQL 存储健康状态。 -- 运行 `bun scripts/cli.ts microservice health codex-queue` 与 `bun scripts/cli.ts microservice proxy codex-queue /api/tasks`,确认真实链路经过 backend-core、WebSocket、main-server provider-gateway 和主 server `codex-queue-backend` 后端,并且 `/health` 的 `queue.storage.primary=postgres`、`queue.storage.postgresReady=true`,不得出现 file fallback;`queue.notifications.claudeqq.outbox.storage=postgres` 且暴露 pending/failed/sent 统计。再通过公网 frontend 提交一个 `gpt-5.5` 小任务,确认队列串行推进、输出实时更新、结束后有 judge 判定,且运行中可追加 prompt 或打断。Codex Queue 的重启恢复必须作为验收项:运行中任务存在时重启或重建 `codex-queue-backend` 后,任务必须从 PostgreSQL 恢复到可继续执行状态,不能丢失 active task、`promptHistory`、后续 queued 任务、readAt/未读状态或已入 outbox 的 ClaudeQQ 通知;ClaudeQQ/NapCat 离线期间结束的任务必须能在 `/api/notifications/claudeqq` 中看到 pending/failed,并在登录恢复后通过 `POST /api/notifications/claudeqq/drain` 发送。批量验收必须通过公网 frontend 设置 `入队份数=5` 或使用多段 prompt 分隔,一次性入队 5 条任务,并确认 5 条任务按顺序进入 running/judging/succeeded,而不是只运行第一条。 +- 运行 `bun scripts/cli.ts microservice health code-queue` 与 `bun scripts/cli.ts microservice proxy code-queue /api/tasks`,确认真实链路经过 backend-core、WebSocket、main-server provider-gateway 和主 server `code-queue-backend` 后端,并且 `/health` 的 `queue.storage.primary=postgres`、`queue.storage.postgresReady=true`,不得出现 file fallback;`queue.notifications.claudeqq.outbox.storage=postgres` 且暴露 pending/failed/sent 统计。再通过公网 frontend 提交一个 `gpt-5.5` 小任务,确认队列串行推进、输出实时更新、结束后有 judge 判定,且运行中可追加 prompt 或打断。Code Queue 的重启恢复必须作为验收项:运行中任务存在时重启或重建 `code-queue-backend` 后,任务必须从 PostgreSQL 恢复到可继续执行状态,不能丢失 active task、`promptHistory`、后续 queued 任务、readAt/未读状态或已入 outbox 的 ClaudeQQ 通知;ClaudeQQ/NapCat 离线期间结束的任务必须能在 `/api/notifications/claudeqq` 中看到 pending/failed,并在登录恢复后通过 `POST /api/notifications/claudeqq/drain` 发送。Code Queue 服务名、表名前缀或持久化目录发生迁移后,还必须运行 `bun scripts/cli.ts e2e run --only microservice:catalog-code-queue,microservice:code-queue-status,microservice:code-queue-health,microservice:code-queue-tasks`,证明 backend-core catalog、私有代理、PostgreSQL 队列和任务列表都指向 `code-queue`。批量验收必须通过公网 frontend 设置 `入队份数=5` 或使用多段 prompt 分隔,一次性入队 5 条任务,并确认 5 条任务按顺序进入 running/judging/succeeded,而不是只运行第一条。 +- Code Queue 内存防回归验收:凡是改动 Code Queue 的持久化、scheduler、输出/Trace、health、列表/详情查询、日志导出或容器运行参数,交付前必须用 `docker compose --env-file .state/docker-compose.env config` 或 `docker inspect code-queue-backend` 确认 memory 硬上限为 `629145600` 字节、memory+swap 上限为 `1610612736` 字节,运行 `docker stats --no-stream code-queue-backend` 确认常驻内存低于 `600MiB` 且 `OOMKilled=false`、`RestartCount` 未异常增长,再运行 `bun scripts/cli.ts microservice health code-queue` 确认 `/health` 通过 PostgreSQL 汇总队列而不是物化全量历史任务,并能看到 active run slot 与 waiter 状态。验收还必须覆盖有历史任务存在时的 `/api/tasks`、单任务详情和 output/transcript 查询,证明热状态裁剪不会丢历史输出、也不会重新把全部历史 `task_json` 缓存在进程内;涉及 TypeScript/frontend 验证的任务应能在该 600M memory + 1536M memswap 预算中完成 `bun run --cwd src/components/frontend check` 这类短时高内存命令,而不是被 memory watchdog 反复 SIGTERM。 +- 运行 `bun scripts/cli.ts microservice health filebrowser`、`bun scripts/cli.ts microservice health filebrowser-d601` 和 `bun scripts/cli.ts microservice proxy filebrowser / --max-body-bytes 2000`,确认 File Browser health 返回 `status=OK`,WebUI HTML 包含 `File Browser`,D518/D601 通过 provider-gateway 访问节点本机 `4251`;随后在公网 frontend 的 `用户服务 / File Browser` 中确认 D518 为默认目标、可导出截图、iframe 紧凑布局不再有巨大 `folder` 标记遮挡文件名,并可浏览 `/mnt/c`。 - 在 D601 上用 `bun scripts/cli.ts ssh D601 ...` 调试业务仓库和容器,确认 `curl http://127.0.0.1:3254/api/health` 可用;不要把调试服务部署到主 server。 - 在 D601 上用 `bun scripts/cli.ts ssh D601 ...` 调试业务仓库和容器,确认 `curl http://127.0.0.1:18082/health` 和 `curl http://127.0.0.1:18082/api/snapshot` 可用;不要把 Pipeline 调试服务部署到主 server。 - 在 D601 上用 `bun scripts/cli.ts ssh D601 ...` 调试 `~/met_nonlinear`,确认 `curl http://127.0.0.1:3288/health` 可用;最终验收必须回到公网 UniDesk frontend,通过项目库选择、Fork、加入待启动队列和启动队列完成,不要把 MET Nonlinear 后端、Docker build 或训练任务部署到主 server。 diff --git a/docs/reference/observability.md b/docs/reference/observability.md index 3033680e..fab30114 100644 --- a/docs/reference/observability.md +++ b/docs/reference/observability.md @@ -8,7 +8,13 @@ UniDesk 的可观测性优先级高于静默成功。CLI、服务日志、Docker ## Service Logs -服务日志位于 `logs/{YYYYMMDD}/`,每次 `server start` 都生成新的本地时间戳前缀。backend-core、frontend 和 provider-gateway 输出 JSONL 文件;database 通过 PostgreSQL logging collector 写入同一目录。 +服务日志位于 `logs/{YYYYMMDD}/`,每次 `server start` 都生成新的本地时间戳前缀。新写入的 UniDesk JSONL 日志必须按小时切片:`logs/{YYYYMMDD}/{startStamp}_{YYYYMMDD}_{HH}_{service}.jsonl`,一天一个目录,禁止长期追加到单个巨大 JSONL。所有 UniDesk Bun 服务(backend-core、frontend、provider-gateway、Code Queue、project-manager、baidu-netdisk 以及后续新增服务)必须复用 `src/components/shared/src/rotating-jsonl.ts` 中的 `createHourlyJsonlWriter`;`LOG_FILE` 只作为推导 `logs` 根目录、启动前缀和 service 后缀的 base path,不得直接 `appendFileSync(LOG_FILE, ...)` 长期追加。database 通过 PostgreSQL logging collector 写入同一日期目录。 + +日志保留默认按日志族限制为 `1GiB`:服务写入或 Code Queue 导出日志时必须扫描同一 service 后缀的历史文件,超过上限后自动删除最旧切片;当前活跃切片不能被保留清理删除。全局上限由 `UNIDESK_LOG_RETENTION_BYTES` 控制,服务级上限使用 `UNIDESK__LOG_MAX_BYTES`(如 `UNIDESK_FRONTEND_LOG_MAX_BYTES`、`UNIDESK_PROVIDER_GATEWAY_LOG_MAX_BYTES`),历史兼容变量只允许作为过渡入口。Codex app-server 的 `logs_*.sqlite` 仅作为 Codex 上游运行时的短暂缓冲,Code Queue 必须周期性导出为同样按小时切片的 `codex-app-server` JSONL,并删除/压缩已导出的 SQLite 行,避免 `logs_2.sqlite` 成为长期大文件。 + +新增或迁移服务的长期规范:Dockerfile 必须把 `src/components/shared` 复制到与仓库相同的相对路径,TypeScript 配置必须能解析 shared 引用,Compose 必须传入 `LOG_FILE` 和 `UNIDESK_LOG_RETENTION_BYTES`;如果服务需要在内存中暴露 `/logs`,可以继续维护有限 `recentLogs`,但落盘只能通过统一 hourly writer。业务归档日志(例如 Code Queue task output archive)可以保留 append-only 文件,但不得复用 UniDesk service JSONL 命名族,也不得替代 `/logs` 的结构化服务日志。 + +`bun scripts/cli.ts check` 必须包含日志轮转门禁:核心 Bun 服务源码不得直接向 `LOG_FILE` append,且必须引用统一 hourly writer。新增服务如果进入主 Compose,也要纳入该门禁的 checked file 列表。 ## Log Access @@ -20,6 +26,6 @@ backend-core 必须把 queued、dispatched、running 视为待处理任务,并 ## Performance Metrics -backend-core 必须提供 `/api/performance`,返回滚动窗口内的 HTTP 组件请求统计、最近失败请求、内部操作统计、最近慢操作、进程内存、PGDATA 用量和 Codex Queue PostgreSQL 存储摘要。组件统计必须包含请求数、失败数、失败率、平均延迟和 P95,内部操作统计必须包含服务名、操作名、次数、平均延迟和 P95;失败和慢操作记录必须保留时间、状态、耗时、路径或细节,避免只给汇总数字而无法定位。 +backend-core 必须提供 `/api/performance`,返回滚动窗口内的 HTTP 组件请求统计、最近失败请求、内部操作统计、最近慢操作、进程内存、PGDATA 用量和 Code Queue PostgreSQL 存储摘要。组件统计必须包含请求数、失败数、失败率、平均延迟和 P95,内部操作统计必须包含服务名、操作名、次数、平均延迟和 P95;失败和慢操作记录必须保留时间、状态、耗时、路径或细节,避免只给汇总数字而无法定位。 frontend Bun server 必须提供同源 `/api/frontend-performance`,记录 webui 静态资源、登录/session、API 代理和 frontend->core 代理操作耗时。浏览器中的 `运行总览 / 性能面板` 必须把 frontend 与 backend-core 指标合并展示为 Bwebui 曲线、组件汇总、最近失败请求、内部操作汇总和最近慢操作;完整性能 JSON 只能通过显式 `查看原始JSON` 打开。 diff --git a/docs/reference/provider-gateway.md b/docs/reference/provider-gateway.md index dc91a457..f64281fe 100644 --- a/docs/reference/provider-gateway.md +++ b/docs/reference/provider-gateway.md @@ -18,9 +18,11 @@ Provider Gateway 是计算节点侧容器。它只主动连出到主 server 暴 ## Deployment Method -当前主 server 公网 IP 是 `74.48.78.17`,`config.json` 中的 `network.publicHost` 必须保持为该地址;公网 frontend 入口是 `http://74.48.78.17:18081/`,provider gateway 对外接入入口是 `ws://74.48.78.17:18082/ws/provider`,provider ingress 健康检查是 `http://74.48.78.17:18082/health`。主 server 本机 provider 由根目录 `docker-compose.yml` 的 `provider-gateway` 服务启动,容器内使用 Docker 内网地址 `ws://backend-core:8081/ws/provider` 自接入;外部计算节点部署 provider-gateway 时必须改用公网 provider ingress URL,并复用 `config.json` / `.state/docker-compose.env` 中的 provider token、心跳间隔和重连参数。 +当前主 server 公网 IP 是 `74.48.78.17`,`config.json` 中的 `network.publicHost` 必须保持为该地址;公网 frontend 入口是 `http://74.48.78.17:18081/`,provider gateway 对外接入入口是 `ws://74.48.78.17:18082/ws/provider`,provider ingress 健康检查是 `http://74.48.78.17:18082/health`。主 server 本机 provider 由根目录 `docker-compose.yml` 的 `provider-gateway` 服务启动,容器内使用 Docker 内网地址 `ws://backend-core:8081/ws/provider` 自接入;外部计算节点部署 provider-gateway 时必须改用公网 provider ingress URL。 -计算节点部署 provider-gateway 的最小方法是:准备可运行 `unidesk_provider-gateway` 镜像的 Docker 环境,为节点分配唯一 `PROVIDER_ID` 与可读 `PROVIDER_NAME`,设置 `PROVIDER_SERVER_URL=ws://74.48.78.17:18082/ws/provider`、`PROVIDER_TOKEN`、`PROVIDER_LABELS_JSON`、`HEARTBEAT_INTERVAL_MS`、`RECONNECT_BASE_MS` 和 `RECONNECT_MAX_MS`,并挂载 `/var/run/docker.sock:/var/run/docker.sock` 作为 Docker 状态采集、任务执行和远程升级的唯一自动化通道。为了让 `资源监控` 能看到节点级进程占用,provider-gateway 容器还必须运行在宿主 PID namespace:Compose 写法是 `pid: "host"`,`docker run` 写法是 `--pid host`;缺少该配置时只能看到 provider 容器命名空间内的进程,不能视为完整节点资源监控。所有长期接入节点都必须配置 `PROVIDER_UPGRADE_*` 环境变量,把节点上的 UniDesk 仓库只读挂载到 `PROVIDER_UPGRADE_WORKSPACE_PATH`,并确保升级命令只重建 `provider-gateway` service,不影响 database、backend-core、frontend。provider-gateway 容器必须使用 Docker restart policy `always`,Compose 写法是 `restart: always`,`docker run` 写法是 `--restart always`。provider-gateway 部署必须同时交付 Host SSH / WSL SSH 透传维护桥;WSL 节点应设置 `HOST_SSH_HOST=host.docker.internal`、`HOST_SSH_PORT=22`、`HOST_SSH_USER=`、`HOST_SSH_KEY=/run/host-ssh/id_ed25519`、`HOST_REMOTE_CWD=/home/`,并把只含维护私钥的宿主目录只读挂载到 `/run/host-ssh`。 +新增计算节点推荐使用两项配置的简化挂载流程:在目标节点的 UniDesk 仓库根目录运行 `bun scripts/cli.ts provider attach --master-server http://74.48.78.17/ --up`;如果主 server 仍是默认地址,`--master-server` 可省略。该命令生成 `.state/provider-.env` 和 `provider-.yml`,env 文件默认只保留 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID` 两项,Compose 固定 `restart: always`、`pid: "host"`、Docker socket、只读 `/workspace` 仓库挂载、日志目录和 `/run/host-ssh` 维护私钥目录。provider-gateway 会从 `UNIDESK_MASTER_SERVER=http://74.48.78.17/` 自动派生 `ws://74.48.78.17:18082/ws/provider`,并自动补齐 `PROVIDER_NAME`、默认 labels、心跳/重连参数、`DOCKER_SOCKET_PATH`、`MONITOR_DISK_PATH`、`PROVIDER_UPGRADE_*`、runner image 和日志路径;远程升级所需的宿主仓库路径会优先通过 Docker inspect 反查当前容器 `/workspace` 挂载源,避免手写 `/home/ubuntu/unidesk`、Compose project、env file 或 runner image 时出错。显式环境变量仍可覆盖这些默认值;如果主 server 的 provider token 已改成非默认值,挂载时只额外传一次 `--provider-token ` 或在 env 文件中加入 `PROVIDER_TOKEN`。 + +手写 Compose 仍必须满足同一部署约束:挂载 `/var/run/docker.sock:/var/run/docker.sock` 作为 Docker 状态采集、任务执行和远程升级的唯一自动化通道,并让 provider-gateway 容器运行在宿主 PID namespace;Compose 写法是 `pid: "host"`,`docker run` 写法是 `--pid host`。缺少该配置时只能看到 provider 容器命名空间内的进程,不能视为完整节点资源监控。provider-gateway 容器必须使用 Docker restart policy `always`;`unless-stopped`、空 restart policy、手动 `docker stop` 后期待 Docker daemon 重启自动拉起,都不符合长期接入要求。长期接入节点必须保留只读 `/workspace` 仓库挂载,使 `provider.upgrade mode=schedule` 能构建候选 gateway 且只重建 `provider-gateway` service,不影响 database、backend-core、frontend 或业务用户服务。provider-gateway 部署必须同时交付 Host SSH / WSL SSH 透传维护桥;WSL 节点默认把私钥目录挂载到 `/run/host-ssh` 后,gateway 会在发现 `/run/host-ssh/id_ed25519` 时自动使用 `host.docker.internal:22`、从仓库路径推断 WSL 用户与默认工作目录,必要时仍可用 `HOST_SSH_HOST`、`HOST_SSH_PORT`、`HOST_SSH_USER`、`HOST_SSH_KEY`、`HOST_REMOTE_CWD` 显式覆盖。 ## Mandatory SSH Passthrough Bundle @@ -36,12 +38,14 @@ WSL 计算节点和普通外部计算节点使用同一套 provider ingress 协 WSL 节点应优先使用 WSL 内部原生 Docker Engine 和 `/var/run/docker.sock`,让资源采样、Docker 状态和任务执行都反映 WSL 计算环境本身,而不是 Windows 或 Docker Desktop 的代理上下文。如果当前 Docker CLI 实际连接 Docker Desktop daemon,也可以先作为可用计算节点接入,但必须在 `PROVIDER_LABELS_JSON` 中显式标注 `dockerContext=docker-desktop` 或等价标签,避免在 frontend 中把 Docker Desktop 资源误判为 WSL 原生资源。UniDesk 仓库建议放在 WSL 原生文件系统,例如 `/home/ubuntu/unidesk`,再按需从 Windows 工作区同步源码;长期运行和升级挂载应使用 WSL 原生路径只读挂到容器内 `/workspace`,减少 `/mnt/c` 权限、性能和路径转换问题。 -WSL provider 的最小环境文件应放在节点本地私有路径,例如 `/home/ubuntu/unidesk/.state/provider-.env`,并由 `docker run --env-file` 读取。`PROVIDER_LABELS_JSON` 在 Docker env-file 中可以写成单行 JSON;如果临时用 shell `source` 方式调试,必须对整段 JSON 加引号,否则 shell 会按 `{}` 和逗号拆分导致 JSON 解析失败。WSL 节点建议至少包含这些 labels:`host`、`role=wsl-provider`、`wsl=true`、`distro`、`docker=true`;运行时 provider-gateway 会自动追加 `runtime`、`dockerSocketPresent` 和 `gatewayUptimeSeconds`。`.state/provider-.env`、`logs/provider-/` 和容器日志属于节点本地运行态,必须保持在 `.gitignore` 覆盖范围内,不能提交 provider token、登录态或运行日志。 +WSL provider 的最小环境文件应放在节点本地私有路径,例如 `/home/ubuntu/unidesk/.state/provider-.env`,并由生成的 `provider-.yml` 或 `docker run --env-file` 读取。新挂载默认只写 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID`;如果需要覆盖 labels,`PROVIDER_LABELS_JSON` 在 Docker env-file 中可以写成单行 JSON,临时用 shell `source` 调试时必须对整段 JSON 加引号,否则 shell 会按 `{}` 和逗号拆分导致 JSON 解析失败。不写 labels 时 provider-gateway 会根据 Provider ID、Docker、WSL 内核和 `/etc/os-release` 自动生成 `host`、`role`、`docker`、`wsl`、`distro` 与 `attachMode=simple`,并在运行时追加 `runtime`、`dockerSocketPresent`、gateway 版本和 `gatewayUptimeSeconds`。`.state/provider-.env`、`provider-.yml`、`logs/provider-/` 和容器日志属于节点本地运行态,必须保持在 `.gitignore` 覆盖范围内,不能提交 provider token、登录态或运行日志。 长期运行推荐用 systemd 管理 provider-gateway 容器,而不是只在交互 shell 中运行 Bun 进程。systemd unit 的稳定形态是:`ExecStartPre=-docker rm -f unidesk-provider-gateway-` 清理同名旧容器,`ExecStart=docker run --restart always --pid host --name unidesk-provider-gateway- --env-file ... -v /var/run/docker.sock:/var/run/docker.sock -v /home/ubuntu/unidesk:/workspace:ro -v /home/ubuntu/unidesk/logs/provider-:/var/log/unidesk -v :/run/host-ssh:ro unidesk_provider-gateway:`,`ExecStop=docker stop unidesk-provider-gateway-`,并设置 `Restart=always`。临时部署也必须使用 `docker run -d --restart always --pid host`,并保证容器名、env 文件、日志目录、SSH 私钥只读挂载和镜像 tag 都带上节点 ID,便于 frontend、Docker 状态、SSH 透传、进程资源表和本地排障互相对应。`provider.upgrade` 是长期接入节点的必备能力,provider-gateway 不提供 `PROVIDER_UPGRADE_ENABLED` 或等价禁用开关;如果节点缺少升级环境变量或 SSH 透传环境变量,必须修正节点部署,而不是在服务端接受只能预检、不能升级或不能维护透传的半成品状态。 WSL 本身会在没有前台进程时被 Windows 回收;如果该节点要作为长期在线算力,必须通过 Windows 启动项、计划任务或后台 `wsl.exe -d -u root -- bash -lc "systemctl start docker unidesk-provider-gateway-.service; exec sleep infinity"` 这类 keepalive 进程保持发行版运行。仅启用 WSL 内 systemd service 不等价于 Windows 层面的常驻守护。 +Docker daemon 重启后的恢复验收必须看两个层级。第一层是 Docker 自身:`docker inspect --format '{{.HostConfig.RestartPolicy.Name}} {{.HostConfig.PidMode}} {{.State.Status}}' unidesk-provider-gateway-` 必须返回 `always host running`;D601/D518 这类 Docker Desktop daemon 上还要记录 `docker info --format '{{.Name}} {{.LiveRestoreEnabled}}'`,当前 `LiveRestore=false` 意味着 daemon 重启会停止容器,恢复完全依赖 restart policy,因此不能把容器曾经是 running 当作已具备重启恢复能力。第二层是宿主/WSL 常驻:节点应有 systemd、Windows 计划任务、Docker Desktop 自启动或等价 keepalive 证明 Docker daemon 会随机器/WSL 启动;如果 `systemctl list-unit-files '*unidesk*'` 为空,必须在验收记录中明确该节点暂时只依赖 Docker Desktop daemon 自身,不能声称有 WSL 内 systemd watchdog。provider-gateway 新版本会在启动与 heartbeat 中自检当前容器 restart policy,发现不是 `always` 时通过 Docker socket 尝试 `docker update --restart always ` 并在 labels 中上报 `providerGatewayRestartPolicyOk`、`providerGatewayPidModeOk` 与 `providerGatewayRuntimeGuardOk`;这只是最后一道自愈,不能替代 Compose/systemd 的正确配置。 + ## WSL Network And Proxy Bootstrap WSL 节点出网异常时,先把代理设置固化到目标 WSL 用户的 `~/.bashrc`,而不是只在当前 shell 临时 `export`。长期可复用的写法是在每次交互 shell 启动时从 `/etc/resolv.conf` 读取 Windows 宿主在 WSL NAT 中的 nameserver IP,再导出 `http_proxy`、`https_proxy`、`HTTP_PROXY`、`HTTPS_PROXY`、`all_proxy` 和 `ALL_PROXY` 指向 `http://:7890` / `socks5://:7890`,并保留 `no_proxy=localhost,127.0.0.1,::1,host.docker.internal`。不要把某次启动看到的宿主 IP 当成永远不变的常量;动态读取可以避免 WSL 网络重建后代理失效。 @@ -80,7 +84,7 @@ provider ingress 是唯一允许公网暴露的 provider 连接接口,当前 ## User Service HTTP Proxy -`microservice.http` 是 provider-gateway 给 UniDesk 用户服务使用的私有后端访问能力。backend-core 通过真实 WebSocket dispatch 下发目标 service id、节点本机 `targetBaseUrl`、path、query、method、request body、timeout 和可选 JSON 数组裁剪参数;provider-gateway 支持 `GET`、`HEAD`、`POST`、`PUT`、`PATCH`、`DELETE`,但最终允许方法必须由每个用户服务的 `backend.allowedMethods` 显式配置。provider-gateway 只允许访问 `http://127.0.0.1`、`http://localhost`、`http://host.docker.internal` 这些节点本地地址;主 server 内置 Todo Note 与 Codex Queue 后端可分别使用 Compose 服务名 `http://todo-note:4211` 与 `http://codex-queue:4222`。该能力不打开 provider-gateway 入站端口,也不替代业务仓库自身 Dockerfile/docker-compose。 +`microservice.http` 是 provider-gateway 给 UniDesk 用户服务使用的私有后端访问能力。backend-core 通过真实 WebSocket dispatch 下发目标 service id、节点本机 `targetBaseUrl`、path、query、method、request body、timeout 和可选 JSON 数组裁剪参数;provider-gateway 支持 `GET`、`HEAD`、`POST`、`PUT`、`PATCH`、`DELETE`,但最终允许方法必须由每个用户服务的 `backend.allowedMethods` 显式配置。provider-gateway 只允许访问 `http://127.0.0.1`、`http://localhost`、`http://host.docker.internal` 这些节点本地地址;主 server 内置 Todo Note 与 Code Queue 后端可分别使用 Compose 服务名 `http://todo-note:4211` 与 `http://code-queue:4222`。该能力不打开 provider-gateway 入站端口,也不替代业务仓库自身 Dockerfile/docker-compose。 超大 JSON 响应可以使用 `jsonArrayLimits` 在 provider-gateway 返回前裁剪指定数组,并在响应体中写入 `_unidesk.arrayLimits` 元数据,便于 UniDesk frontend 预览列表而不展示裸 JSON。长期应优先推动业务后端提供分页 API;裁剪只是 UniDesk 集成层的展示保护。 diff --git a/scripts/cli.ts b/scripts/cli.ts index f22af123..d6212f52 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -8,7 +8,8 @@ import { runChecks } from "./src/check"; import { runSsh } from "./src/ssh"; import { extractRemoteCliOptions, runRemoteCli } from "./src/remote"; import { runMicroserviceCommand } from "./src/microservices"; -import { runCodexQueueCommand } from "./src/codex-queue"; +import { runCodeQueueCommand } from "./src/code-queue"; +import { runProviderCommand } from "./src/provider-attach"; const remoteOptions = extractRemoteCliOptions(process.argv.slice(2)); const args = remoteOptions.args; @@ -27,7 +28,8 @@ function help(): unknown { { command: "server stop", description: "Fire-and-forget docker-compose down for the fixed UniDesk stack." }, { command: "server status", description: "Show fixed ports, containers, service health, and public URLs." }, { command: "server logs [--tail-bytes N]", description: "Return bounded tails from file logs and docker logs." }, - { command: "server rebuild ", description: "Build first, then serialize, force-recreate, and validate one Compose service." }, + { command: "server rebuild ", description: "Build first, then serialize, force-recreate, and validate one Compose service." }, + { command: "provider attach [--master-server URL] [--up] [--force]", description: "Generate the minimal external provider-gateway env/compose bundle; only master server URL and provider id are required." }, { command: "ssh [ssh-like args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge with built-in remote helper tools in PATH." }, { command: "ssh apply-patch [tool args...] < patch.diff", description: "Invoke the injected remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." }, { command: "ssh py [script-args...] < script.py", description: "Run remote Python from local stdin through SSH passthrough without nested shell quoting; extra args become script argv." }, @@ -38,10 +40,10 @@ function help(): unknown { { command: "microservice list", description: "List UniDesk-managed user services and their provider/runtime mapping." }, { command: "microservice status ", description: "Show one user service config, repository reference, backend mapping, and runtime status." }, { command: "microservice health ", description: "Probe one user service through backend-core -> provider-gateway HTTP proxy." }, - { command: "microservice proxy [--raw] [--max-body-bytes N]", description: "GET a private user-service backend path through the same frontend-only proxy used by WebUI; large bodies are summarized unless --raw is set." }, - { command: "codex task [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", description: "Fetch a compact Codex Queue task summary; trace rows are opt-in and paged with next/previous commands to avoid output explosion." }, - { command: "codex output [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]", description: "Fetch paged raw Codex Queue output records by seq when a trace row has omitted command/output text." }, - { command: "codex queues | codex queue create | codex move --queue ", description: "List/create Codex Queue lanes and move a queued task so each queue runs serially while queues run in parallel." }, + { command: "microservice proxy [--method GET|POST|PUT|PATCH|DELETE] [--raw] [--max-body-bytes N]", description: "Access a private user-service backend path through the same frontend-only proxy used by WebUI; large bodies are summarized unless --raw is set." }, + { command: "codex task [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", description: "Fetch a compact Code Queue task summary; trace rows are opt-in and paged with next/previous commands to avoid output explosion." }, + { command: "codex output [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]", description: "Fetch paged raw Code Queue output records by seq when a trace row has omitted command/output text." }, + { command: "codex (queues | queue create | move --queue )", description: "List/create Code Queue lanes and move a queued task so each queue runs serially while queues run in parallel." }, { command: "job list", description: "List async jobs from .state/jobs." }, { command: "job status [--tail-bytes N]", description: "Show job state with bounded stdout/stderr tails." }, { command: "debug health", description: "Probe internal core, nodes, system/Docker status, frontend, provider ingress, and public boundary." }, @@ -159,7 +161,7 @@ async function main(): Promise { } if (sub === "rebuild") { if (!isRebuildableService(third)) { - throw new Error("server rebuild requires one of: backend-core, frontend, provider-gateway, todo-note, codex-queue, project-manager"); + throw new Error("server rebuild requires one of: backend-core, frontend, provider-gateway, todo-note, code-queue, project-manager, baidu-netdisk"); } emitJson(commandName, rebuildService(config, third)); return; @@ -171,8 +173,13 @@ async function main(): Promise { return; } + if (top === "provider") { + emitJson(commandName, await runProviderCommand(config, args.slice(1))); + return; + } + if (top === "codex") { - emitJson(commandName, await runCodexQueueCommand(config, args.slice(1))); + emitJson(commandName, await runCodeQueueCommand(config, args.slice(1))); return; } diff --git a/scripts/src/check.ts b/scripts/src/check.ts index 068b9273..752e3a14 100644 --- a/scripts/src/check.ts +++ b/scripts/src/check.ts @@ -1,4 +1,4 @@ -import { existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { runCommand } from "./command"; import { type UniDeskConfig, repoRoot, rootPath } from "./config"; import { composeConfig } from "./docker"; @@ -28,6 +28,32 @@ function commandItem(name: string, command: string[]): CheckItem { }; } +function unifiedLogRotationItem(): CheckItem { + const serviceFiles = [ + "src/components/backend-core/src/index.ts", + "src/components/frontend/src/index.ts", + "src/components/provider-gateway/src/index.ts", + "src/components/microservices/code-queue/src/index.ts", + "src/components/microservices/project-manager/src/index.ts", + "src/components/microservices/baidu-netdisk/src/index.ts", + ]; + const offenders = serviceFiles.flatMap((path) => { + const text = readFileSync(rootPath(path), "utf8"); + const directLogAppend = /appendFileSync\(\s*(?:config\.)?logFile\b/u.test(text) || /appendFileSync\(\s*process\.env\.LOG_FILE\b/u.test(text); + const missingWriter = !text.includes("createHourlyJsonlWriter"); + return directLogAppend || missingWriter ? [{ path, directLogAppend, missingWriter }] : []; + }); + return { + name: "logs:unified-hourly-rotation", + ok: offenders.length === 0, + detail: { + sharedWriter: "src/components/shared/src/rotating-jsonl.ts", + checkedFiles: serviceFiles, + offenders, + }, + }; +} + export function runChecks(config: UniDeskConfig): { ok: boolean; items: CheckItem[] } { const items: CheckItem[] = [ { name: "config:validated", ok: true, detail: { project: config.project.name, runtime: config.runtime } }, @@ -39,6 +65,7 @@ export function runChecks(config: UniDeskConfig): { ok: boolean; items: CheckIte fileItem("src/components/frontend/src/index.ts"), fileItem("src/components/provider-gateway/src/index.ts"), fileItem("scripts/src/e2e.ts"), + unifiedLogRotationItem(), commandItem("bun:version", ["bun", "--version"]), commandItem("typescript:scripts", ["bunx", "tsc", "-p", "scripts/tsconfig.json", "--noEmit", "--pretty", "false"]), commandItem("typescript:components", ["bunx", "tsc", "-p", "src/tsconfig.check.json", "--pretty", "false"]), diff --git a/scripts/src/codex-queue-perf.ts b/scripts/src/code-queue-perf.ts similarity index 93% rename from scripts/src/codex-queue-perf.ts rename to scripts/src/code-queue-perf.ts index 0c98c199..90438b4f 100644 --- a/scripts/src/codex-queue-perf.ts +++ b/scripts/src/code-queue-perf.ts @@ -3,7 +3,7 @@ import { existsSync } from "node:fs"; import { resolve } from "node:path"; import { readConfig } from "./config"; -interface CodexQueuePerfOptions { +interface CodeQueuePerfOptions { url: string; username: string; password: string; @@ -40,7 +40,7 @@ function normalizeBaseUrl(value: string): string { return value.replace(/\/+$/u, ""); } -function readOptions(): CodexQueuePerfOptions { +function readOptions(): CodeQueuePerfOptions { const config = readConfig(); const args = process.argv.slice(2); return { @@ -54,7 +54,7 @@ function readOptions(): CodexQueuePerfOptions { }; } -async function authenticateSession(context: BrowserContext, options: CodexQueuePerfOptions): Promise { +async function authenticateSession(context: BrowserContext, options: CodeQueuePerfOptions): Promise { const response = await fetch(`${options.url}/login`, { method: "POST", headers: { "content-type": "application/json" }, @@ -89,7 +89,7 @@ function parseNumberAttr(value: string | null): number | null { return Number.isFinite(parsed) ? parsed : null; } -async function runCodexQueuePerf(options: CodexQueuePerfOptions): Promise> { +async function runCodeQueuePerf(options: CodeQueuePerfOptions): Promise> { const browsersPath = process.env.PLAYWRIGHT_BROWSERS_PATH || ".state/playwright-browsers"; const fullChromePath = resolve(browsersPath, "chromium-1217/chrome-linux64/chrome"); const launchArgs = [ @@ -105,7 +105,7 @@ async function runCodexQueuePerf(options: CodexQueuePerfOptions): Promise(); const apiTimings: ApiTiming[] = []; const consoleErrors: string[] = []; - const targetUrl = `${options.url}/app/codex-queue/`; + const targetUrl = `${options.url}/app/code-queue/`; const measuredAt = new Date().toISOString(); try { @@ -136,10 +136,10 @@ async function runCodexQueuePerf(options: CodexQueuePerfOptions): Promise { - const pageElement = document.querySelector('[data-testid="codex-queue-page"]') as HTMLElement | null; + const pageElement = document.querySelector('[data-testid="code-queue-page"]') as HTMLElement | null; const output = document.querySelector('[data-testid="codex-output"]') as HTMLElement | null; const tasks = document.querySelectorAll('[data-testid^="codex-task-codex_"]'); return { @@ -212,7 +212,7 @@ async function runCodexQueuePerf(options: CodexQueuePerfOptions): Promise String(item ?? "")).filter((item) => item.length > 0); +} + +function fmtDuration(ms: unknown): string { + const value = Number(ms); + if (!Number.isFinite(value) || value < 0) return "--"; + const totalSeconds = Math.floor(value / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`; + return `${seconds}s`; +} + function upstreamError(response: unknown): string { const record = asRecord(response); if (record === null) return String(response); @@ -168,7 +186,10 @@ function compactSummary(summary: unknown, options: CodexTaskOptions, taskId: str id: record.id ?? taskId, queueId: record.queueId ?? null, status: record.status ?? null, + providerId: record.providerId ?? null, model: record.model ?? null, + agentPort: record.agentPort ?? null, + agentPortInfo: record.agentPortInfo ?? null, reasoningEffort: record.reasoningEffort ?? null, cwd: record.cwd ?? null, attempts: { @@ -177,7 +198,7 @@ function compactSummary(summary: unknown, options: CodexTaskOptions, taskId: str currentMode: record.currentMode ?? null, judgeFailCount: record.judgeFailCount ?? null, judgeFailRetryLimit: record.judgeFailRetryLimit ?? null, - attemptRecords: record.attempts ?? [], + attemptRecords: asArray(record.attempts).map((attempt) => compactAttemptCycle(attempt, options.full)), }, thread: { codexThreadId: record.codexThreadId ?? null, @@ -204,6 +225,7 @@ function compactSummary(summary: unknown, options: CodexTaskOptions, taskId: str }, traceDisclosure: { included: options.trace, + renderer: "shared trace-summary/trace-steps progressive abstraction; CLI and WebUI diverge only at final rendering", total: record.transcriptCount ?? null, maxSeq: transcriptMaxSeq, defaultPage: `bun scripts/cli.ts codex task ${taskId} --trace --limit ${defaultTraceLimit}`, @@ -216,22 +238,144 @@ function compactSummary(summary: unknown, options: CodexTaskOptions, taskId: str }; } -function compactTracePage(body: Record, taskId: string, limit: number): Record { - const transcript = asArray(body.transcript); +function traceKindLabel(kind: unknown): string { + const value = String(kind || ""); + if (value === "ran") return "Ran"; + if (value === "explored") return "Explored"; + if (value === "edited") return "Edited"; + if (value === "error") return "Error"; + if (value === "system") return "System"; + return "Message"; +} + +function transcriptLineToStep(line: unknown): Record { + const record = asRecord(line) ?? {}; + const summaryLines = stringList(record.summaryLines); + const fallbackSummary = [record.commandPreview, record.bodyPreview, record.title].map((value) => asString(value).trim()).filter(Boolean); + return { + seq: record.seq ?? null, + at: record.at ?? null, + kind: record.kind ?? "message", + title: record.title ?? "", + status: record.status ?? null, + durationMs: record.durationMs ?? null, + rawSeqs: record.rawSeqs ?? [], + summaryLines: summaryLines.length > 0 ? summaryLines : fallbackSummary.slice(0, 4), + hasDetail: true, + }; +} + +function compactTraceStep(step: unknown, taskId: string): Record { + const record = asRecord(step) ?? {}; + const seq = record.seq ?? null; + return { + seq, + at: record.at ?? null, + kind: record.kind ?? "message", + label: traceKindLabel(record.kind), + title: record.title ?? "", + status: record.status ?? null, + durationMs: record.durationMs ?? null, + duration: fmtDuration(record.durationMs), + rawSeqs: record.rawSeqs ?? [], + summaryLines: stringList(record.summaryLines), + hasDetail: asBoolean(record.hasDetail), + detailCommand: seq === null ? null : `bun scripts/cli.ts microservice proxy code-queue /api/tasks/${encodeURIComponent(taskId)}/trace-step?seq=${encodeURIComponent(String(seq))} --raw`, + }; +} + +function compactAttemptCycle(value: unknown, full: boolean): Record { + const record = asRecord(value) ?? {}; + return { + index: record.index ?? null, + synthetic: record.synthetic ?? false, + label: record.label ?? null, + mode: record.mode ?? null, + terminalStatus: record.terminalStatus ?? null, + appServerExitCode: record.appServerExitCode ?? null, + appServerSignal: record.appServerSignal ?? null, + error: record.error ?? null, + stderrTail: textView(asString(record.stderrTail), full, 1200), + startedAt: record.startedAt ?? null, + finishedAt: record.finishedAt ?? null, + startSeq: record.startSeq ?? null, + endSeq: record.endSeq ?? null, + execution: record.execution ?? null, + finalResponse: textView(asString(record.finalResponsePreview ?? record.finalResponse), full, 3000), + judge: record.judge ?? null, + feedbackPrompt: textView(asString(record.feedbackPromptPreview), full, 1800), + }; +} + +function traceStepCounts(steps: Record[]): Record { + return steps.reduce>((counts, step) => { + const kind = String(step.kind || "message"); + counts[kind] = (counts[kind] ?? 0) + 1; + return counts; + }, {}); +} + +function renderTraceConsoleRows(summary: Record, steps: Record[]): string[] { + const rows: string[] = []; + const execution = asRecord(summary.execution) ?? {}; + const counts = traceStepCounts(steps); + rows.push([ + `task=${summary.id ?? ""}`, + `status=${summary.status ?? ""}`, + `port=${summary.agentPort ?? "codex"}`, + `model=${summary.model ?? ""}`, + `updated=${summary.updatedAt ?? ""}`, + ].filter((item) => !item.endsWith("=")).join(" ")); + rows.push(`progressive steps=${steps.length} tools=${execution.toolCallCount ?? 0} read=${execution.readCount ?? counts.explored ?? 0} edit=${execution.editCount ?? counts.edited ?? 0} run=${execution.runCount ?? counts.ran ?? 0} errors=${counts.error ?? 0}`); + for (const step of steps) { + const head = [`#${step.seq ?? "?"}`, `[${step.label ?? traceKindLabel(step.kind)}]`, step.title ?? ""] + .filter((item) => String(item).length > 0) + .join(" "); + rows.push(`${head}${step.status ? ` (${step.status})` : ""}${step.duration && step.duration !== "--" ? ` ${step.duration}` : ""}`); + for (const line of stringList(step.summaryLines).slice(0, 4)) rows.push(` ${line}`); + } + return rows; +} + +function compactProgressiveTrace(summaryBody: Record, steps: Record[], taskId: string, full: boolean): Record { + const summary = asRecord(summaryBody.summary) ?? summaryBody; + return { + taskId: summary.id ?? taskId, + queueId: summary.queueId ?? null, + status: summary.status ?? null, + updatedAt: summary.updatedAt ?? null, + model: summary.model ?? null, + agentPort: summary.agentPort ?? null, + agentPortInfo: summary.agentPortInfo ?? null, + prompt: summary.prompt ?? null, + execution: summary.execution ?? null, + attempts: asArray(summary.attempts).map((attempt) => compactAttemptCycle(attempt, full)), + displayedStepSeqs: steps.map((step) => step.seq ?? null), + renderedRows: renderTraceConsoleRows(summary, steps), + }; +} + +function compactTracePage(body: Record, taskId: string, limit: number, summaryBody: Record | null, options: CodexTaskOptions): Record { + const stepsSource = asArray(body.steps).length > 0 ? asArray(body.steps) : asArray(body.transcript).map(transcriptLineToStep); + const steps = stepsSource.map((step) => compactTraceStep(step, taskId)); + const summary = summaryBody === null ? null : asRecord(summaryBody.summary) ?? summaryBody; const nextAfterSeq = body.nextAfterSeq ?? null; const previousBeforeSeq = body.previousBeforeSeq ?? null; - const omittedInPage = transcript.some((item) => { + const omittedInPage = steps.some((item) => { const line = asRecord(item) ?? {}; return asNumber(line.bodyOmittedLines, 0) > 0 || asNumber(line.commandOmittedLines, 0) > 0; }); + const hasRawSeqs = steps.some((item) => asArray(item.rawSeqs).length > 0); return { taskId: body.taskId ?? taskId, queueId: body.queueId ?? null, status: body.status ?? null, updatedAt: body.updatedAt ?? null, + agentPort: body.agentPort ?? summary?.agentPort ?? null, + agentPortInfo: body.agentPortInfo ?? summary?.agentPortInfo ?? null, mode: body.mode ?? null, limit, - returned: transcript.length, + returned: steps.length, total: body.total ?? null, maxSeq: body.maxSeq ?? null, afterSeq: body.afterSeq ?? null, @@ -240,13 +384,15 @@ function compactTracePage(body: Record, taskId: string, limit: previousBeforeSeq, hasMore: body.hasMore ?? false, hasBefore: body.hasBefore ?? false, - transcript, + steps, + progressive: summaryBody === null ? null : compactProgressiveTrace(summaryBody, steps, taskId, options.full), commands: { next: body.hasMore === true && nextAfterSeq !== null ? `bun scripts/cli.ts codex task ${taskId} --trace --after-seq ${nextAfterSeq} --limit ${limit}` : null, previous: body.hasBefore === true && previousBeforeSeq !== null ? `bun scripts/cli.ts codex task ${taskId} --trace --before-seq ${previousBeforeSeq} --limit ${limit}` : null, tail: `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${limit}`, first: `bun scripts/cli.ts codex task ${taskId} --trace --from-start --limit ${limit}`, - rawOutput: omittedInPage ? `Use rawSeqs on each transcript line, e.g. bun scripts/cli.ts codex output ${taskId} --after-seq --limit ${defaultOutputLimit}` : null, + stepDetailTemplate: `bun scripts/cli.ts microservice proxy code-queue /api/tasks/${taskId}/trace-step?seq= --raw`, + rawOutput: omittedInPage || hasRawSeqs ? `Use rawSeqs on each trace step, e.g. bun scripts/cli.ts codex output ${taskId} --after-seq --limit ${defaultOutputLimit}` : null, }, }; } @@ -294,7 +440,7 @@ function queryString(params: Record = { @@ -307,8 +453,9 @@ function codexTaskSummary(taskId: string, options: CodexTaskOptions, fetcher: Co if (options.traceMode === "tail") traceParams.tail = 1; if (options.traceMode === "after") traceParams.afterSeq = options.afterSeq; if (options.traceMode === "before") traceParams.beforeSeq = options.beforeSeq; - const traceResponse = unwrapCodexResponse(fetcher(`/api/microservices/codex-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/transcript${queryString(traceParams)}`)); - result.trace = compactTracePage(traceResponse.body, taskId, options.traceLimit); + const traceSummaryResponse = unwrapCodexResponse(fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/trace-summary`)); + const traceResponse = unwrapCodexResponse(fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/trace-steps${queryString(traceParams)}`)); + result.trace = compactTracePage(traceResponse.body, taskId, options.traceLimit, traceSummaryResponse.body, options); } return result; } @@ -353,7 +500,7 @@ function codexTaskOutput(taskId: string, options: CodexOutputOptions, fetcher: C if (options.mode === "tail") params.tail = 1; if (options.mode === "after") params.afterSeq = options.afterSeq; if (options.mode === "before") params.beforeSeq = options.beforeSeq; - const response = unwrapCodexResponse(fetcher(`/api/microservices/codex-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/output${queryString(params)}`)); + const response = unwrapCodexResponse(fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/output${queryString(params)}`)); return { upstream: response.upstream, outputPage: compactOutputPage(response.body, taskId, options.limit) }; } @@ -366,7 +513,7 @@ export function codexOutputQuery(taskId: string, optionArgs: string[], fetcher: } async function codexTaskSummaryAsync(taskId: string, options: CodexTaskOptions, fetcher: AsyncCodexResponseFetcher): Promise { - const summaryPath = `/api/microservices/codex-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: options.toolLimit })}`; + const summaryPath = `/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: options.toolLimit })}`; const summaryResponse = unwrapCodexResponse(await fetcher(summaryPath)); const summary = summaryResponse.body.summary; const result: Record = { @@ -379,8 +526,9 @@ async function codexTaskSummaryAsync(taskId: string, options: CodexTaskOptions, if (options.traceMode === "tail") traceParams.tail = 1; if (options.traceMode === "after") traceParams.afterSeq = options.afterSeq; if (options.traceMode === "before") traceParams.beforeSeq = options.beforeSeq; - const traceResponse = unwrapCodexResponse(await fetcher(`/api/microservices/codex-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/transcript${queryString(traceParams)}`)); - result.trace = compactTracePage(traceResponse.body, taskId, options.traceLimit); + const traceSummaryResponse = unwrapCodexResponse(await fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/trace-summary`)); + const traceResponse = unwrapCodexResponse(await fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/trace-steps${queryString(traceParams)}`)); + result.trace = compactTracePage(traceResponse.body, taskId, options.traceLimit, traceSummaryResponse.body, options); } return result; } @@ -394,7 +542,7 @@ async function codexTaskOutputAsync(taskId: string, options: CodexOutputOptions, if (options.mode === "tail") params.tail = 1; if (options.mode === "after") params.afterSeq = options.afterSeq; if (options.mode === "before") params.beforeSeq = options.beforeSeq; - const response = unwrapCodexResponse(await fetcher(`/api/microservices/codex-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/output${queryString(params)}`)); + const response = unwrapCodexResponse(await fetcher(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/output${queryString(params)}`)); return { upstream: response.upstream, outputPage: compactOutputPage(response.body, taskId, options.limit) }; } @@ -413,19 +561,19 @@ function requireQueueId(args: string[], command: string): string { return raw.trim(); } -function codexQueues(): unknown { - return unwrapCodexResponse(coreInternalFetch("/api/microservices/codex-queue/proxy/api/queues")); +function codeQueues(): unknown { + return unwrapCodexResponse(coreInternalFetch("/api/microservices/code-queue/proxy/api/queues")); } function codexCreateQueue(queueId: string): unknown { - return unwrapCodexResponse(coreInternalFetch("/api/microservices/codex-queue/proxy/api/queues", { method: "POST", body: { queueId } })); + return unwrapCodexResponse(coreInternalFetch("/api/microservices/code-queue/proxy/api/queues", { method: "POST", body: { queueId } })); } function codexMoveTask(taskId: string, queueId: string): unknown { - return unwrapCodexResponse(coreInternalFetch(`/api/microservices/codex-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/move`, { method: "POST", body: { queueId } })); + return unwrapCodexResponse(coreInternalFetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/move`, { method: "POST", body: { queueId } })); } -export async function runCodexQueueCommand(_config: UniDeskConfig, args: string[]): Promise { +export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]): Promise { const [action = "task", taskIdArg] = args; if (action === "task" || action === "summary" || action === "show") { const taskId = requireTaskId(taskIdArg, `codex ${action}`); @@ -435,11 +583,11 @@ export async function runCodexQueueCommand(_config: UniDeskConfig, args: string[ const taskId = requireTaskId(taskIdArg, "codex output"); return codexOutputQuery(taskId, args.slice(2)); } - if (action === "queues") return codexQueues(); + if (action === "queues") return codeQueues(); if (action === "queue") { const sub = taskIdArg ?? "list"; - if (sub === "list") return codexQueues(); - if (sub === "create") return codexCreateQueue(requireQueueId(args.slice(2), "codex queue create")); + if (sub === "list") return codeQueues(); + if (sub === "create") return codexCreateQueue(requireQueueId(args.slice(2), "queue create")); } if (action === "move") { const taskId = requireTaskId(taskIdArg, "codex move"); diff --git a/scripts/src/docker.ts b/scripts/src/docker.ts index 7b4d1cd1..5700afd8 100644 --- a/scripts/src/docker.ts +++ b/scripts/src/docker.ts @@ -1,5 +1,5 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; -import { basename, join, resolve } from "node:path"; +import { basename, dirname, join, resolve } from "node:path"; import { commandOk, runCommand, tailFile } from "./command"; import { type UniDeskConfig, repoRoot, rootPath } from "./config"; import { startJob } from "./jobs"; @@ -7,6 +7,7 @@ import { startJob } from "./jobs"; export interface ComposeRuntimeEnv { envFile: string; logDir: string; + logDay: string; logPrefix: string; } @@ -18,7 +19,7 @@ export interface ContainerStatus { ports: string; } -const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note", "codex-queue", "project-manager"] as const; +const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note", "code-queue", "project-manager", "baidu-netdisk"] as const; export type RebuildableService = typeof rebuildableServices[number]; export function isRebuildableService(value: string | undefined): value is RebuildableService { @@ -58,18 +59,25 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean): const previousRaw = existsSync(envFile) ? readFileSync(envFile, "utf8") : ""; const previousValue = (key: string): string => previousRaw.match(new RegExp(`^${key}=(.*)$`, "m"))?.[1]?.replace(/^"|"$/g, "") ?? ""; const runtimeSecret = (key: string): string => process.env[key] ?? previousValue(key); - let logDir: string; + let logRoot: string; + let logDay: string; let logPrefix: string; if (!freshLogPrefix && previousRaw.length > 0) { - logDir = previousValue("UNIDESK_LOG_DIR") || rootPath(config.paths.logsDir); + const previousLogDir = previousValue("UNIDESK_LOG_DIR"); + const previousLogRoot = previousLogDir && /^\d{8}$/u.test(basename(previousLogDir)) ? dirname(previousLogDir) : previousLogDir; + logRoot = previousLogRoot || rootPath(config.paths.logsDir); logPrefix = previousValue("UNIDESK_LOG_PREFIX") || localDateParts(new Date()).stamp; + logDay = previousValue("UNIDESK_LOG_DAY") || logPrefix.match(/^\d{8}/u)?.[0] || localDateParts(new Date()).day; } else { const parts = localDateParts(new Date()); - logDir = resolve(rootPath(config.paths.logsDir, parts.day)); + logRoot = resolve(rootPath(config.paths.logsDir)); + logDay = parts.day; logPrefix = parts.stamp; } - mkdirSync(logDir, { recursive: true }); - chmodSync(logDir, 0o777); + logRoot = resolve(logRoot); + mkdirSync(join(logRoot, logDay), { recursive: true }); + chmodSync(logRoot, 0o777); + chmodSync(join(logRoot, logDay), 0o777); const labels = JSON.stringify(config.providerGateway.labels); const microservices = JSON.stringify(config.microservices); const lines = { @@ -105,8 +113,10 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean): UNIDESK_PROVIDER_UPGRADE_COMPOSE_PROJECT: config.providerGateway.upgrade.composeProject, UNIDESK_PROVIDER_UPGRADE_SERVICE: config.providerGateway.upgrade.service, UNIDESK_PROVIDER_UPGRADE_RUNNER_IMAGE: config.providerGateway.upgrade.runnerImage, - UNIDESK_LOG_DIR: logDir, + UNIDESK_LOG_DIR: logRoot, + UNIDESK_LOG_DAY: logDay, UNIDESK_LOG_PREFIX: logPrefix, + UNIDESK_LOG_RETENTION_BYTES: runtimeSecret("UNIDESK_LOG_RETENTION_BYTES") || "1GiB", UNIDESK_HOST_ROOT_SSH_DIR: process.env.UNIDESK_HOST_ROOT_SSH_DIR || "/root/.ssh", UNIDESK_HOST_SSH_KEY_DIR: config.sshForwarding.keyDir, UNIDESK_HOST_SSH_HOST: config.sshForwarding.host, @@ -121,23 +131,32 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean): UNIDESK_TODO_NOTE_REMINDER_SCAN_INTERVAL_MS: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_SCAN_INTERVAL_MS") || "30000", UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TIMEOUT_MS: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TIMEOUT_MS") || "15000", UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS") || "3", - UNIDESK_CODEX_QUEUE_MINIMAX_API_KEY: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_API_KEY"), - UNIDESK_CODEX_QUEUE_MINIMAX_MODEL: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_MODEL") || "MiniMax-M2.7", - UNIDESK_CODEX_QUEUE_MINIMAX_API_BASE: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_API_BASE") || "https://api.minimaxi.com/v1", - UNIDESK_CODEX_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS") || "60000", - UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_ENABLED") || "true", - UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL") || "http://backend-core:8080/api/microservices/claudeqq/proxy", - UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE") || "private", - UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_USER_ID: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_USER_ID") || "645275593", - UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID"), - UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS") || "12000", - UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS") || "15000", - UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS") || "3", + UNIDESK_CODE_QUEUE_MINIMAX_API_KEY: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_API_KEY") || runtimeSecret("MINIMAX_API_KEY"), + UNIDESK_CODE_QUEUE_MINIMAX_MODEL: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_MODEL") || runtimeSecret("MINIMAX_MODEL") || "MiniMax-M2.7", + UNIDESK_CODE_QUEUE_MINIMAX_API_BASE: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_API_BASE") || runtimeSecret("MINIMAX_API_BASE") || "https://api.minimaxi.com/v1", + UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS") || "60000", + UNIDESK_CODE_QUEUE_REMOTE_WORKDIR: runtimeSecret("UNIDESK_CODE_QUEUE_REMOTE_WORKDIR") || "/home/ubuntu", + UNIDESK_CODE_QUEUE_EXECUTION_PROVIDER_IDS: runtimeSecret("UNIDESK_CODE_QUEUE_EXECUTION_PROVIDER_IDS") || "D601", + UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID") || "D601", + UNIDESK_CODE_QUEUE_DEV_CONTAINER_IMAGE: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_IMAGE"), + UNIDESK_CODE_QUEUE_DEV_CONTAINER_WORKDIR: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_WORKDIR") || "/home/ubuntu", + UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED") || "true", + UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL") || "http://backend-core:8080/api/microservices/claudeqq/proxy", + UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE") || "private", + UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID") || "645275593", + UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID"), + UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS") || "12000", + UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS") || "15000", + UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS") || "3", + UNIDESK_BAIDU_NETDISK_CLIENT_ID: runtimeSecret("UNIDESK_BAIDU_NETDISK_CLIENT_ID"), + UNIDESK_BAIDU_NETDISK_CLIENT_SECRET: runtimeSecret("UNIDESK_BAIDU_NETDISK_CLIENT_SECRET"), + UNIDESK_BAIDU_NETDISK_TOKEN_KEY: runtimeSecret("UNIDESK_BAIDU_NETDISK_TOKEN_KEY"), + UNIDESK_BAIDU_NETDISK_APP_ROOT: runtimeSecret("UNIDESK_BAIDU_NETDISK_APP_ROOT") || "/apps/UniDeskBaiduNetdisk", OPENAI_API_KEY: runtimeSecret("OPENAI_API_KEY"), CRS_OAI_KEY: runtimeSecret("CRS_OAI_KEY"), }; writeFileSync(envFile, Object.entries(lines).map(([key, value]) => `${key}=${envValue(value)}`).join("\n") + "\n", "utf8"); - return { envFile, logDir, logPrefix }; + return { envFile, logDir: logRoot, logDay, logPrefix }; } export function composeConfig(config: UniDeskConfig): { runtimeEnv: ComposeRuntimeEnv; command: string[]; result: ReturnType } { @@ -226,7 +245,7 @@ export function rebuildService(config: UniDeskConfig, service: RebuildableServic validateScript, ].join("\n"); const command = ["bash", "-lc", composeLockedScript(script)]; - const jobRunner = service === "codex-queue" ? { runner: "docker" as const, dockerImage: "unidesk-codex-queue:latest" } : {}; + const jobRunner = service === "code-queue" ? { runner: "docker" as const, dockerImage: "unidesk-code-queue:latest" } : {}; const job = startJob("server_rebuild", command, `Rebuild and validate UniDesk ${service} with serialized Docker Compose mutation`, jobRunner); return { job, @@ -242,7 +261,7 @@ export function rebuildService(config: UniDeskConfig, service: RebuildableServic noDeps: true, forceRecreate: true, composeMutationLock: rootPath(".state", "locks", "server-compose.lock"), - jobRunner: service === "codex-queue" ? "docker" : "local", + jobRunner: service === "code-queue" ? "docker" : "local", postUpValidation: true, namedVolumesPreserved: true, }, @@ -352,8 +371,9 @@ export async function stackStatus(config: UniDeskConfig): Promise { internalPorts: [ { name: "backend-core", containerPort: config.network.core.containerPort, hostPort: null }, { name: "database", containerPort: config.network.database.containerPort, hostPort: null }, - { name: "codex-queue", containerPort: 4222, hostPort: null }, + { name: "code-queue", containerPort: 4222, hostPort: null }, { name: "project-manager", containerPort: 4233, hostPort: null }, + { name: "baidu-netdisk", containerPort: 4244, hostPort: null }, ], containers: dockerContainers(config), health: { @@ -391,7 +411,7 @@ export function stackLogs(config: UniDeskConfig, tailBytes: number): unknown { const currentFiles = allFiles.filter((path) => basename(path).startsWith(runtimeEnv.logPrefix)); const selectedFiles = (currentFiles.length > 0 ? currentFiles : allFiles.slice(-12)).slice(-12); const files = selectedFiles.map((path) => ({ path, name: basename(path), tail: tailFile(path, tailBytes) })); - const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend", "codex-queue-backend", "project-manager-backend"]; + const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend", "code-queue-backend", "project-manager-backend", "baidu-netdisk-backend"]; const docker = containerNames.map((name) => { const result = runCommand(["docker", "logs", "--tail", "40", name], repoRoot); return { name, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-tailBytes), stderrTail: result.stderr.slice(-tailBytes) }; diff --git a/scripts/src/e2e.ts b/scripts/src/e2e.ts index 27ee250d..c88f8170 100644 --- a/scripts/src/e2e.ts +++ b/scripts/src/e2e.ts @@ -37,7 +37,8 @@ const NETWORK_CHECK_NAMES = [ "network:met-nonlinear-public-blocked", "network:claudeqq-public-blocked", "network:todo-note-public-blocked", - "network:codex-queue-public-blocked", + "network:code-queue-public-blocked", + "network:filebrowser-public-blocked", ] as const; const SERVICE_CHECK_NAMES = [ @@ -49,6 +50,7 @@ const SERVICE_CHECK_NAMES = [ "provider:system-status", "provider:process-resource-status", "provider:docker-status", + "provider:gateway-restart-policy", "provider:upgrade-plan", "provider-ingress:public-health", "microservice:catalog-findjob", @@ -56,7 +58,11 @@ const SERVICE_CHECK_NAMES = [ "microservice:catalog-met-nonlinear", "microservice:catalog-claudeqq", "microservice:catalog-todo-note", - "microservice:catalog-codex-queue", + "microservice:catalog-code-queue", + "microservice:catalog-filebrowser", + "microservice:filebrowser-health", + "microservice:filebrowser-webui", + "microservice:filebrowser-d601-health", "microservice:findjob-status", "microservice:findjob-health", "microservice:findjob-summary", @@ -79,9 +85,9 @@ const SERVICE_CHECK_NAMES = [ "microservice:todo-note-health", "microservice:todo-note-migrated-data", "microservice:todo-note-write-path", - "microservice:codex-queue-status", - "microservice:codex-queue-health", - "microservice:codex-queue-tasks", + "microservice:code-queue-status", + "microservice:code-queue-health", + "microservice:code-queue-tasks", ] as const; const DATABASE_CHECK_NAMES = [ @@ -112,10 +118,12 @@ const FRONTEND_CHECK_NAMES = [ "frontend:microservice-catalog-visible", "frontend:todo-note-integrated-visible", "frontend:findjob-integrated-visible", - "frontend:codex-queue-integrated-visible", - "frontend:codex-queue-initial-prompt-full-expand", - "frontend:codex-queue-trace-full-load", - "frontend:codex-queue-judge-wrap", + "frontend:code-queue-integrated-visible", + "frontend:code-queue-summary-mobile-wrap", + "frontend:code-queue-initial-prompt-full-expand", + "frontend:code-queue-trace-full-load", + "frontend:code-queue-judge-wrap", + "frontend:code-queue-error-red-markers", "frontend:claudeqq-integrated-visible", "frontend:url-route-deeplink", "frontend:pipeline-integrated-visible", @@ -269,13 +277,18 @@ type OverflowProbe = { ok: boolean; documentOverflowX: number; offenders: Array<{ + overflowKind: string; + overflowPx: number; tag: string; + selector: string; className: string; testId: string; text: string; left: number; right: number; width: number; + clientWidth: number; + scrollWidth: number; parentClassName: string; }>; }; @@ -294,7 +307,7 @@ const LAYOUT_OVERFLOW_PAGE_TEST_IDS: Record = { "/app/pipeline/": "pipeline-page", "/app/met-nonlinear/": "met-nonlinear-page", "/app/claudeqq/": "claudeqq-page", - "/app/codex-queue/": "codex-queue-page", + "/app/code-queue/": "code-queue-page", }; function layoutOverflowTargets(): Array<{ label: string; path: string; testId: string }> { @@ -315,42 +328,94 @@ async function collectLayoutOverflow(page: Page, viewport: { width: number; heig for (const target of targets) { await page.goto(`${origin}${target.path}`, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector(`[data-testid="${target.testId}"]`, { timeout: 15000 }).catch(() => undefined); - await page.waitForTimeout(160); + await page.waitForLoadState("networkidle", { timeout: 1200 }).catch(() => undefined); + await page.waitForTimeout(180); const result = await page.evaluate(({ viewportName: currentViewport, label, path, testId }) => { + const overflowTolerancePx = 6; const viewportWidth = document.documentElement.clientWidth; + const checkNestedContainerOverflow = currentViewport === "mobile" || viewportWidth <= 600; const documentOverflowX = Math.max(0, document.documentElement.scrollWidth - viewportWidth, document.body.scrollWidth - viewportWidth); - const allowed = (element: Element): boolean => { - const html = element as HTMLElement; - if (html.closest(".raw-dialog, .raw-json, .performance-table-wrap, .process-table-wrap, .docker-table-wrap, .table-wrap, .met-project-table, .pipeline-flow-frame, .react-flow, .codex-transcript, .codex-task-list, .todo-tree-scroll, .tabs, .rail")) return true; - const style = getComputedStyle(html); - return ["auto", "scroll"].includes(style.overflowX); + const intentionalHorizontalScrollSelector = [ + ".raw-json", + ".performance-table-wrap", + ".process-table-wrap", + ".docker-table-wrap", + ".table-wrap", + ".met-project-table", + ".pipeline-flow-frame", + ".react-flow", + ".pipeline-gantt-viewport", + ".codex-edit-diff", + ".tabs", + ".rail", + ".status-strip", + ".top-status-bar", + ".codex-transcript-item.ran .codex-transcript-command", + ".codex-transcript-item.ran .codex-transcript-body", + ".codex-transcript-item.explored .codex-transcript-command", + ".codex-transcript-item.explored .codex-transcript-body", + ".codex-transcript-item.edited .codex-transcript-command", + ".codex-transcript-item.edited .codex-transcript-body", + ].join(","); + const insideIntentionalHorizontalScroll = (element: Element): boolean => Boolean(element.closest(intentionalHorizontalScrollSelector)); + const selectorFor = (html: HTMLElement): string => { + const tag = html.tagName.toLowerCase(); + const test = html.getAttribute("data-testid"); + const id = html.id ? `#${html.id}` : ""; + const className = String(html.className || ""); + const classes = className + .split(/\s+/) + .map((item) => item.trim()) + .filter(Boolean) + .slice(0, 3) + .map((item) => `.${item}`) + .join(""); + return `${tag}${id}${classes}${test ? `[data-testid="${test}"]` : ""}`; }; - const offenders = Array.from(document.body.querySelectorAll("*")).flatMap((element) => { - const html = element as HTMLElement; - if (!(html instanceof HTMLElement) || allowed(html)) return []; + const offenderFor = (html: HTMLElement, overflowKind: string, overflowPx: number) => { const rect = html.getBoundingClientRect(); - if (rect.width <= 0 || rect.height <= 0) return []; - const excessLeft = rect.left < -2; - const excessRight = rect.right > viewportWidth + 2; - if (!excessLeft && !excessRight) return []; const parent = html.parentElement; - return [{ + return { + overflowKind, + overflowPx: Math.round(overflowPx), tag: html.tagName.toLowerCase(), + selector: selectorFor(html).slice(0, 180), className: String(html.className || "").slice(0, 160), testId: html.getAttribute("data-testid") || "", text: (html.innerText || html.textContent || "").replace(/\s+/g, " ").trim().slice(0, 160), left: Math.round(rect.left), right: Math.round(rect.right), width: Math.round(rect.width), + clientWidth: Math.round(html.clientWidth), + scrollWidth: Math.round(html.scrollWidth), parentClassName: String(parent?.className || "").slice(0, 160), - }]; - }).slice(0, 20); + }; + }; + const offenders = Array.from(document.body.querySelectorAll("*")).flatMap((element) => { + const html = element as HTMLElement; + if (!(html instanceof HTMLElement) || insideIntentionalHorizontalScroll(html)) return []; + const style = getComputedStyle(html); + const rect = html.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) return []; + const excessLeft = rect.left < -overflowTolerancePx; + const excessRight = rect.right > viewportWidth + overflowTolerancePx; + const viewportOverflowPx = Math.max(0, -rect.left, rect.right - viewportWidth); + const containerOverflowPx = Math.max(0, html.scrollWidth - html.clientWidth); + const formControl = ["INPUT", "SELECT", "TEXTAREA"].includes(html.tagName); + const clippedOverflow = ["clip", "hidden"].includes(style.overflowX); + const found: ReturnType[] = []; + if (excessLeft || excessRight) found.push(offenderFor(html, "viewport", viewportOverflowPx)); + if (checkNestedContainerOverflow && !formControl && !clippedOverflow && html.clientWidth > 0 && containerOverflowPx > overflowTolerancePx) found.push(offenderFor(html, "container", containerOverflowPx)); + return found; + }) + .sort((a, b) => b.overflowPx - a.overflowPx) + .slice(0, 20); return { viewport: currentViewport, label, path, testId, - ok: documentOverflowX <= 2 && offenders.length === 0, + ok: documentOverflowX <= overflowTolerancePx && offenders.length === 0, documentOverflowX: Math.round(documentOverflowX), offenders, }; @@ -620,7 +685,7 @@ function dockerPortSummary(): unknown { } function dockerStatusCheckDetail(dockerStatus: unknown, providerId: string): unknown { - const response = dockerStatus as { ok?: boolean; status?: number; body?: { dockerStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; dockerStatus?: { ok?: boolean; socketPresent?: boolean; collectedAt?: string; counts?: unknown; daemon?: unknown; containers?: Array<{ id?: string; name?: string; image?: string; state?: string; status?: string; ports?: string }> } }> } }; + const response = dockerStatus as { ok?: boolean; status?: number; body?: { dockerStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; dockerStatus?: { ok?: boolean; socketPresent?: boolean; collectedAt?: string; counts?: unknown; daemon?: unknown; containers?: Array<{ id?: string; name?: string; image?: string; state?: string; status?: string; ports?: string; restartPolicy?: string; pidMode?: string }> } }> } }; const item = response.body?.dockerStatuses?.find((entry) => entry.providerId === providerId); return { ok: response.ok, @@ -641,11 +706,57 @@ function dockerStatusCheckDetail(dockerStatus: unknown, providerId: string): unk state: container.state, status: container.status, ports: container.ports, + restartPolicy: container.restartPolicy, + pidMode: container.pidMode, })), }, }; } +function providerGatewayRuntimeGuardDetail(dockerStatus: unknown): { ok: boolean; gateways: unknown[]; violations: unknown[] } { + const response = dockerStatus as { + body?: { + dockerStatuses?: Array<{ + providerId?: string; + nodeStatus?: string; + dockerStatus?: { + containers?: Array<{ + id?: string; + name?: string; + image?: string; + state?: string; + restartPolicy?: string; + pidMode?: string; + composeService?: string; + composeProject?: string; + }>; + }; + }>; + }; + }; + const gateways = (response.body?.dockerStatuses ?? []).flatMap((entry) => + (entry.dockerStatus?.containers ?? []) + .filter((container) => String(container.name || "").includes("provider-gateway") || container.composeService === "provider-gateway") + .map((container) => ({ + providerId: entry.providerId, + nodeStatus: entry.nodeStatus, + id: container.id, + name: container.name, + image: container.image, + state: container.state, + restartPolicy: container.restartPolicy, + pidMode: container.pidMode, + composeProject: container.composeProject, + composeService: container.composeService, + })) + ); + const violations = gateways.filter((container) => + (container as { state?: unknown }).state === "running" + && ((container as { restartPolicy?: unknown }).restartPolicy !== "always" || (container as { pidMode?: unknown }).pidMode !== "host") + ); + return { ok: gateways.length > 0 && violations.length === 0, gateways, violations }; +} + function systemStatusCheckDetail(systemStatus: unknown, providerId: string): unknown { const response = systemStatus as { ok?: boolean; status?: number; body?: { systemStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; current?: { ok?: boolean; collectedAt?: string; cpu?: unknown; memory?: unknown; disk?: unknown }; history?: unknown[] }> } }; const item = response.body?.systemStatuses?.find((entry) => entry.providerId === providerId); @@ -676,7 +787,8 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E const metNonlinearPublic = await fetchProbe(`http://${config.network.publicHost}:3288/health`, 2500); const claudeqqPublic = await fetchProbe(`http://${config.network.publicHost}:3290/health`, 2500); const todoNotePublic = await fetchProbe(`http://${config.network.publicHost}:4211/api/health`, 2500); - const codexQueuePublic = await fetchProbe(`http://${config.network.publicHost}:14222/health`, 2500); + const codeQueuePublic = await fetchProbe(`http://${config.network.publicHost}:14222/health`, 2500); + const filebrowserPublic = await fetchProbe(`http://${config.network.publicHost}:4251/health`, 2500); addSelectedCheck(checks, options, "network:only-frontend-provider-ports", !portsText.includes(`:${config.network.core.port}->`) && !portsText.includes(`:${config.network.database.port}->`) && !portsText.includes(":14222->"), portSummary); addSelectedCheck(checks, options, "network:core-public-blocked", (corePublic as { reachable?: boolean }).reachable === false, corePublic); addSelectedCheck(checks, options, "network:database-public-blocked", (databasePublic as { reachable?: boolean }).reachable === false, databasePublic); @@ -684,7 +796,8 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E addSelectedCheck(checks, options, "network:met-nonlinear-public-blocked", (metNonlinearPublic as { reachable?: boolean }).reachable === false, metNonlinearPublic); addSelectedCheck(checks, options, "network:claudeqq-public-blocked", (claudeqqPublic as { reachable?: boolean }).reachable === false, claudeqqPublic); addSelectedCheck(checks, options, "network:todo-note-public-blocked", (todoNotePublic as { reachable?: boolean }).reachable === false, todoNotePublic); - addSelectedCheck(checks, options, "network:codex-queue-public-blocked", (codexQueuePublic as { reachable?: boolean }).reachable === false, codexQueuePublic); + addSelectedCheck(checks, options, "network:code-queue-public-blocked", (codeQueuePublic as { reachable?: boolean }).reachable === false, codeQueuePublic); + addSelectedCheck(checks, options, "network:filebrowser-public-blocked", (filebrowserPublic as { reachable?: boolean }).reachable === false, filebrowserPublic); } async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[], options: E2ERunOptions): Promise { @@ -715,9 +828,12 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 const todoNoteStatus = dockerCoreJson("/api/microservices/todo-note/status"); const todoNoteHealth = dockerCoreJson("/api/microservices/todo-note/health"); const todoNoteInstances = dockerCoreJson("/api/microservices/todo-note/proxy/api/instances"); - const codexQueueStatus = dockerCoreJson("/api/microservices/codex-queue/status"); - const codexQueueHealth = dockerCoreJson("/api/microservices/codex-queue/health"); - const codexQueueTasks = dockerCoreJson("/api/microservices/codex-queue/proxy/api/tasks?limit=5"); + const codeQueueStatus = dockerCoreJson("/api/microservices/code-queue/status"); + const codeQueueHealth = dockerCoreJson("/api/microservices/code-queue/health"); + const codeQueueTasks = dockerCoreJson("/api/microservices/code-queue/proxy/api/tasks?limit=5"); + const filebrowserHealth = dockerCoreJson("/api/microservices/filebrowser/health"); + const filebrowserWebui = dockerCoreJson("/api/microservices/filebrowser/proxy/"); + const filebrowserD601Health = dockerCoreJson("/api/microservices/filebrowser-d601/health"); const todoE2eName = `E2E Todo ${Date.now()}`; const todoNoteCreate = dockerCoreJson("/api/microservices/todo-note/proxy/api/instances", { method: "POST", body: { name: todoE2eName } }); const todoCreatedId = (todoNoteCreate as { body?: { id?: string } }).body?.id ?? ""; @@ -737,7 +853,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 : { ok: false, error: "missing created todo note id" }; const providerIngress = await fetchProbe(urls.providerIngressHealthUrl); const overviewBody = (coreOverview as { body?: { ok?: boolean; dbReady?: boolean; onlineNodeCount?: number; pgdata?: { volumeName?: string; databaseBytes?: number } } }).body; - const corePerformanceBody = (corePerformance as { body?: { ok?: boolean; requests?: { componentSummary?: unknown[] }; operations?: { summary?: unknown[] }; database?: { pgdata?: { volumeName?: string }; codexQueueStorage?: { table?: string } } } }).body; + const corePerformanceBody = (corePerformance as { body?: { ok?: boolean; requests?: { componentSummary?: unknown[] }; operations?: { summary?: unknown[] }; database?: { pgdata?: { volumeName?: string }; codeQueueStorage?: { table?: string } } } }).body; const nodeList = (coreNodes as { body?: { nodes?: Array<{ providerId?: string; status?: string; labels?: Record }> } }).body?.nodes ?? []; const mainNode = nodeList.find((node) => node.providerId === config.providerGateway.id); const expectedGatewayVersion = providerGatewayPackageVersion(); @@ -747,21 +863,25 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 const processMemoryDescending = mainProcesses.length < 2 || mainProcesses.every((row, index, rows) => index === 0 || Number(rows[index - 1]?.rssBytes ?? 0) >= Number(row.rssBytes ?? 0)); const dockerStatuses = (dockerStatus as { body?: { dockerStatuses?: Array<{ providerId?: string; dockerStatus?: { counts?: { containers?: number }; containers?: unknown[] } }> } }).body?.dockerStatuses ?? []; const mainDocker = dockerStatuses.find((item) => item.providerId === config.providerGateway.id); + const providerGatewayRuntimeGuard = providerGatewayRuntimeGuardDetail(dockerStatus); addSelectedCheck(checks, options, "core:internal-overview", (coreOverview as { ok?: boolean }).ok === true && overviewBody?.ok === true && overviewBody.dbReady === true && (overviewBody.onlineNodeCount ?? 0) >= 1, coreOverview); addSelectedCheck(checks, options, "core:pgdata-usage", (coreOverview as { ok?: boolean }).ok === true && overviewBody?.pgdata?.volumeName === config.database.volume && Number(overviewBody.pgdata.databaseBytes ?? 0) > 0, coreOverview); - addSelectedCheck(checks, options, "core:performance-api", (corePerformance as { ok?: boolean }).ok === true && corePerformanceBody?.ok === true && Array.isArray(corePerformanceBody.requests?.componentSummary) && Array.isArray(corePerformanceBody.operations?.summary) && corePerformanceBody.database?.pgdata?.volumeName === config.database.volume && corePerformanceBody.database?.codexQueueStorage?.table === "unidesk_codex_queue_tasks", corePerformance); + addSelectedCheck(checks, options, "core:performance-api", (corePerformance as { ok?: boolean }).ok === true && corePerformanceBody?.ok === true && Array.isArray(corePerformanceBody.requests?.componentSummary) && Array.isArray(corePerformanceBody.operations?.summary) && corePerformanceBody.database?.pgdata?.volumeName === config.database.volume && corePerformanceBody.database?.codeQueueStorage?.table === "unidesk_code_queue_tasks", corePerformance); addSelectedCheck(checks, options, "provider:self-node-online", nodeList.some((node) => node.providerId === config.providerGateway.id && node.status === "online"), coreNodes); addSelectedCheck(checks, options, "provider:gateway-version-label", mainNode?.labels?.providerGatewayVersion === expectedGatewayVersion && mainNode?.labels?.providerGatewayUpgradePolicy === "always-enabled", { providerId: config.providerGateway.id, expectedGatewayVersion, labels: mainNode?.labels ?? null }); addSelectedCheck(checks, options, "provider:system-status", (systemStatus as { ok?: boolean }).ok === true && mainSystem?.current !== undefined && Number.isFinite(mainSystem.current.cpu?.percent) && Number.isFinite(mainSystem.current.memory?.percent) && mainSystem.current.memory?.mode === "actual_without_cache" && Number.isFinite(mainSystem.current.memory?.cacheBytes) && Number.isFinite(mainSystem.current.disk?.percent) && (mainSystem.history?.length ?? 0) > 0, systemStatusCheckDetail(systemStatus, config.providerGateway.id)); addSelectedCheck(checks, options, "provider:process-resource-status", mainProcesses.length > 0 && mainSystem?.current?.processSummary?.defaultSort === "memory_desc" && processMemoryDescending && mainProcesses.some((row) => Number.isFinite(row.pid) && Number.isFinite(row.rssBytes) && Number.isFinite(row.cpuPercent) && typeof row.command === "string"), { providerId: config.providerGateway.id, processSummary: mainSystem?.current?.processSummary, sample: mainProcesses.slice(0, 5) }); addSelectedCheck(checks, options, "provider:docker-status", (dockerStatus as { ok?: boolean }).ok === true && mainDocker?.dockerStatus !== undefined && ((mainDocker.dockerStatus.counts?.containers ?? 0) > 0 || (mainDocker.dockerStatus.containers?.length ?? 0) > 0), dockerStatusCheckDetail(dockerStatus, config.providerGateway.id)); + addSelectedCheck(checks, options, "provider:gateway-restart-policy", providerGatewayRuntimeGuard.ok, providerGatewayRuntimeGuard); const microserviceList = (microservices as { body?: { microservices?: Array<{ id?: string; providerId?: string; backend?: { public?: boolean }; runtime?: { providerStatus?: string; container?: { name?: string; state?: string } } }> } }).body?.microservices ?? []; const findjob = microserviceList.find((service) => service.id === "findjob"); const pipeline = microserviceList.find((service) => service.id === "pipeline"); const metNonlinear = microserviceList.find((service) => service.id === "met-nonlinear"); const claudeqq = microserviceList.find((service) => service.id === "claudeqq"); const todoNote = microserviceList.find((service) => service.id === "todo-note"); - const codexQueue = microserviceList.find((service) => service.id === "codex-queue"); + const codeQueue = microserviceList.find((service) => service.id === "code-queue"); + const filebrowser = microserviceList.find((service) => service.id === "filebrowser"); + const filebrowserD601 = microserviceList.find((service) => service.id === "filebrowser-d601"); const findjobSummaryBody = (findjobSummary as { body?: { totalJobs?: number; prioritizedJobs?: number } }).body; const findjobJobs = (findjobJobsPreview as { body?: { jobs?: unknown[]; _unidesk?: { arrayLimits?: { jobs?: { returnedLength?: number; originalLength?: number } } } } }).body; const todoNoteRows = (todoNoteInstances as { body?: { instances?: Array<{ id?: string; name?: string; todoCount?: number; completedCount?: number }> } }).body?.instances ?? []; @@ -776,8 +896,11 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 const claudeqqNapcatLoginBody = (claudeqqNapcatLogin as { body?: { ok?: boolean; napcat?: { containerized?: boolean; loginState?: string; qrcode?: { available?: boolean; dataUrl?: string } }; login?: { ready?: boolean; state?: string } } }).body; const claudeqqEventsBody = (claudeqqEvents as { body?: { ok?: boolean; events?: unknown[]; count?: number } }).body; const claudeqqSubscriptionsBody = (claudeqqSubscriptions as { body?: { ok?: boolean; subscriptions?: unknown[]; count?: number } }).body; - const codexQueueHealthBody = (codexQueueHealth as { body?: { ok?: boolean; queue?: { defaultModel?: string; judgeConfigured?: boolean; modelReasoningEfforts?: Record } } }).body; - const codexQueueTasksBody = (codexQueueTasks as { body?: { ok?: boolean; queue?: { defaultModel?: string; modelReasoningEfforts?: Record }; tasks?: unknown[] } }).body; + const codeQueueHealthBody = (codeQueueHealth as { body?: { ok?: boolean; queue?: { defaultModel?: string; judgeConfigured?: boolean; modelReasoningEfforts?: Record } } }).body; + const codeQueueTasksBody = (codeQueueTasks as { body?: { ok?: boolean; queue?: { defaultModel?: string; modelReasoningEfforts?: Record }; tasks?: unknown[] } }).body; + const filebrowserHealthBody = (filebrowserHealth as { body?: { status?: string } }).body; + const filebrowserD601HealthBody = (filebrowserD601Health as { body?: { status?: string } }).body; + const filebrowserWebuiText = String((filebrowserWebui as { body?: { text?: string } }).body?.text || ""); const firstPipelineRun = Array.isArray(pipelineSnapshotBody?.runs) ? pipelineSnapshotBody.runs[0] as { runId?: string; pipelineId?: string; status?: string; updatedAt?: string } | undefined : undefined; @@ -804,7 +927,16 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 addSelectedCheck(checks, options, "microservice:catalog-met-nonlinear", (microservices as { ok?: boolean }).ok === true && metNonlinear?.providerId === "D601" && metNonlinear.backend?.public === false && metNonlinear.runtime?.container?.name === "met-nonlinear-ts", { microservices }); addSelectedCheck(checks, options, "microservice:catalog-claudeqq", (microservices as { ok?: boolean }).ok === true && claudeqq?.providerId === "D601" && claudeqq.backend?.public === false && claudeqq.runtime?.container?.name === "claudeqq-backend", { microservices }); addSelectedCheck(checks, options, "microservice:catalog-todo-note", (microservices as { ok?: boolean }).ok === true && todoNote?.providerId === config.providerGateway.id && todoNote.backend?.public === false && todoNote.runtime?.container?.name === "todo-note-backend", { microservices }); - addSelectedCheck(checks, options, "microservice:catalog-codex-queue", (microservices as { ok?: boolean }).ok === true && codexQueue?.providerId === config.providerGateway.id && codexQueue.backend?.public === false && codexQueue.runtime?.container?.name === "codex-queue-backend", { microservices }); + addSelectedCheck(checks, options, "microservice:catalog-code-queue", (microservices as { ok?: boolean }).ok === true && codeQueue?.providerId === config.providerGateway.id && codeQueue.backend?.public === false && codeQueue.runtime?.container?.name === "code-queue-backend", { microservices }); + addSelectedCheck(checks, options, "microservice:catalog-filebrowser", (microservices as { ok?: boolean }).ok === true + && filebrowser?.providerId === "D518" + && filebrowser.backend?.public === false + && filebrowser.runtime?.container?.name === "unidesk-filebrowser-d518" + && filebrowserD601?.providerId === "D601", + { filebrowser, filebrowserD601 }); + addSelectedCheck(checks, options, "microservice:filebrowser-health", (filebrowserHealth as { ok?: boolean }).ok === true && filebrowserHealthBody?.status === "OK", filebrowserHealth); + addSelectedCheck(checks, options, "microservice:filebrowser-webui", (filebrowserWebui as { ok?: boolean; status?: number }).ok === true && (filebrowserWebui as { status?: number }).status === 200 && filebrowserWebuiText.includes("File Browser"), { status: (filebrowserWebui as { status?: number }).status, textPreview: filebrowserWebuiText.slice(0, 600) }); + addSelectedCheck(checks, options, "microservice:filebrowser-d601-health", (filebrowserD601Health as { ok?: boolean }).ok === true && filebrowserD601HealthBody?.status === "OK", filebrowserD601Health); addSelectedCheck(checks, options, "microservice:findjob-status", (findjobStatus as { ok?: boolean }).ok === true && (findjobStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", findjobStatus); addSelectedCheck(checks, options, "microservice:findjob-health", (findjobHealth as { ok?: boolean; body?: { ok?: boolean } }).ok === true && (findjobHealth as { body?: { ok?: boolean } }).body?.ok === true, findjobHealth); addSelectedCheck(checks, options, "microservice:findjob-summary", (findjobSummary as { ok?: boolean }).ok === true && Number.isFinite(findjobSummaryBody?.totalJobs) && Number.isFinite(findjobSummaryBody?.prioritizedJobs), findjobSummary); @@ -847,9 +979,9 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 addSelectedCheck(checks, options, "microservice:todo-note-health", (todoNoteHealth as { ok?: boolean; body?: { ok?: boolean; storage?: string } }).ok === true && (todoNoteHealth as { body?: { ok?: boolean; storage?: string } }).body?.ok === true && (todoNoteHealth as { body?: { storage?: string } }).body?.storage === "postgres", todoNoteHealth); addSelectedCheck(checks, options, "microservice:todo-note-migrated-data", (todoNoteInstances as { ok?: boolean }).ok === true && todoNoteRows.length >= 5 && ["CONSTAR", "大论文", "找工作", "小论文", "事务"].every((name) => todoNoteNames.includes(name)) && todoNoteRows.reduce((sum, row) => sum + Number(row.todoCount ?? 0), 0) >= 100, { todoNoteInstances }); addSelectedCheck(checks, options, "microservice:todo-note-write-path", (todoNoteCreate as { ok?: boolean }).ok === true && (todoNoteAdd as { ok?: boolean }).ok === true && (todoNoteToggle as { ok?: boolean }).ok === true && (todoNoteUndo as { ok?: boolean }).ok === true && (todoNoteDelete as { ok?: boolean }).ok === true, { todoNoteCreate, todoNoteAdd, todoNoteToggle, todoNoteUndo, todoNoteDelete }); - addSelectedCheck(checks, options, "microservice:codex-queue-status", (codexQueueStatus as { ok?: boolean }).ok === true && (codexQueueStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === config.providerGateway.id, codexQueueStatus); - addSelectedCheck(checks, options, "microservice:codex-queue-health", (codexQueueHealth as { ok?: boolean }).ok === true && codexQueueHealthBody?.ok === true && codexQueueHealthBody.queue?.defaultModel === "gpt-5.5" && codexQueueHealthBody.queue?.modelReasoningEfforts?.["gpt-5.5"] === "xhigh", codexQueueHealth); - addSelectedCheck(checks, options, "microservice:codex-queue-tasks", (codexQueueTasks as { ok?: boolean }).ok === true && codexQueueTasksBody?.ok === true && Array.isArray(codexQueueTasksBody.tasks) && codexQueueTasksBody.queue?.defaultModel === "gpt-5.5" && codexQueueTasksBody.queue?.modelReasoningEfforts?.["gpt-5.5"] === "xhigh", codexQueueTasks); + addSelectedCheck(checks, options, "microservice:code-queue-status", (codeQueueStatus as { ok?: boolean }).ok === true && (codeQueueStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === config.providerGateway.id, codeQueueStatus); + addSelectedCheck(checks, options, "microservice:code-queue-health", (codeQueueHealth as { ok?: boolean }).ok === true && codeQueueHealthBody?.ok === true && codeQueueHealthBody.queue?.defaultModel === "gpt-5.5" && codeQueueHealthBody.queue?.modelReasoningEfforts?.["gpt-5.5"] === "xhigh", codeQueueHealth); + addSelectedCheck(checks, options, "microservice:code-queue-tasks", (codeQueueTasks as { ok?: boolean }).ok === true && codeQueueTasksBody?.ok === true && Array.isArray(codeQueueTasksBody.tasks) && codeQueueTasksBody.queue?.defaultModel === "gpt-5.5" && codeQueueTasksBody.queue?.modelReasoningEfforts?.["gpt-5.5"] === "xhigh", codeQueueTasks); const upgradeDispatch = dockerCoreJson("/api/dispatch", { method: "POST", body: { providerId: config.providerGateway.id, command: "provider.upgrade", payload: { source: "cli-e2e", mode: "plan" } }, @@ -947,12 +1079,14 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 const needMicroserviceCatalog = wants("frontend:microservice-catalog-visible"); const needTodoNote = wants("frontend:todo-note-integrated-visible"); const needFindJob = wants("frontend:findjob-integrated-visible"); - const needCodexQueue = wantsAny([ - "frontend:codex-queue-integrated-visible", - "frontend:codex-queue-initial-prompt-full-expand", - "frontend:codex-queue-trace-full-load", - "frontend:codex-queue-judge-wrap", - ]); + const needCodeQueue = wantsAny([ + "frontend:code-queue-integrated-visible", + "frontend:code-queue-summary-mobile-wrap", + "frontend:code-queue-initial-prompt-full-expand", + "frontend:code-queue-trace-full-load", + "frontend:code-queue-judge-wrap", + "frontend:code-queue-error-red-markers", + ]); const needClaudeqq = wants("frontend:claudeqq-integrated-visible"); const needRouteDeepLink = wants("frontend:url-route-deeplink"); const needPipeline = wantsAny([ @@ -1027,20 +1161,24 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 let microserviceCatalogText = ""; let todoNoteText = ""; let findjobText = ""; - let codexQueueText = ""; - let codexQueueOutputText = ""; - let codexQueueTaskCount = 0; - let codexQueueOptions: string[] = []; - let codexQueueSwitchMetrics: any = { optionCount: 0, switched: false }; - let codexQueueSubmitQueueControl: any = { tagName: "", createButtonVisible: false, oldInputMissing: false }; - let codexQueueTracePlacement: any = { firstChildIsTrace: false, noPageTopStatus: false, filterInsideTracePanel: false, traceStatusVisible: false, markAllReadVisible: false }; - let codexQueueGlobalStatus: any = { activeMicroserviceVisible: false }; - let codexQueuePromptDefaultEmpty = false; - let codexQueueSubmitGuard: any = { batchRowVisible: false, disabledBeforeConfirm: false, enabledAfterConfirm: false, waitElementMissingBeforeSubmit: false }; - let codexQueueScrollbarMetrics: any = { transcriptThin: false, toolHorizontalHidden: true }; + let codeQueueText = ""; + let codeQueueOutputText = ""; + let codeQueueTaskCount = 0; + let codeQueueOptions: string[] = []; + let codeQueueSwitchMetrics: any = { optionCount: 0, switched: false }; + let codeQueueSubmitQueueControl: any = { tagName: "", createButtonVisible: false, oldInputMissing: false }; + let codeQueueTracePlacement: any = { firstChildIsTrace: false, noPageTopStatus: false, filterInsideTracePanel: false, traceStatusVisible: false, markAllReadVisible: false }; + let codeQueueGlobalStatus: any = { activeMicroserviceVisible: false }; + let codeQueueSidebarUpdateMetrics: any = { cardCount: 0, labels: [], hasRecentUpdateLabel: false }; + let codeQueueHtmlGuard: any = { rootAttrMissing: false, sourceAttrMissing: false, sourceNoBasePrompt: false }; + let codeQueueSummaryMobileMetrics: any = { checked: false, summaryCount: 0, ok: false }; + let codeQueuePromptDefaultEmpty = false; + let codeQueueSubmitGuard: any = { batchRowVisible: false, disabledBeforeConfirm: false, enabledAfterConfirm: false, waitElementMissingBeforeSubmit: false }; + let codeQueueScrollbarMetrics: any = { transcriptThin: false, toolHorizontalHidden: true }; let codexInitialPromptFullMetrics: any = { candidateFound: false }; let codexTraceFullMetrics: any = { candidateFound: false }; let codexJudgeWrapMetrics: any = { checked: false }; + let codeQueueErrorHighlightMetrics: any = { checked: false, candidateFound: false }; let claudeqqText = ""; let routeDeepLinkText = ""; let routeInitialPath = ""; @@ -1237,9 +1375,9 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 } } - if (needMicroserviceCatalog || needTodoNote || needFindJob || needCodexQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) { + if (needMicroserviceCatalog || needTodoNote || needFindJob || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) { await page.getByRole("button", { name: /用户服务/ }).click(); - if (needMicroserviceCatalog || needTodoNote || needFindJob || needCodexQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) { + if (needMicroserviceCatalog || needTodoNote || needFindJob || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) { await page.waitForSelector('[data-testid="microservice-catalog-page"]', { timeout: 10000 }); } if (needMicroserviceCatalog) { @@ -1248,7 +1386,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 await page.waitForSelector('[data-testid="microservice-row-met-nonlinear"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="microservice-row-claudeqq"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="microservice-row-todo-note"]', { timeout: 10000 }); - await page.waitForSelector('[data-testid="microservice-row-codex-queue"]', { timeout: 10000 }); + await page.waitForSelector('[data-testid="microservice-row-code-queue"]', { timeout: 10000 }); microserviceCatalogText = await page.locator('[data-testid="microservice-catalog-page"]').innerText({ timeout: 5000 }); } if (needTodoNote) { @@ -1304,13 +1442,13 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 }, undefined, { timeout: 30000 }); findjobText = await page.locator('[data-testid="findjob-page"]').innerText({ timeout: 5000 }); } - if (needCodexQueue) { - await page.getByRole("button", { name: /Codex Queue/ }).click(); - await page.waitForSelector('[data-testid="codex-queue-page"]', { timeout: 10000 }); + if (needCodeQueue) { + await page.getByRole("button", { name: /Code Queue/ }).click(); + await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 10000 }); await page.waitForFunction(() => { const text = document.body.innerText; const lower = text.toLowerCase(); - return lower.includes("codex queue") + return lower.includes("code queue") && text.includes("gpt-5.4-mini") && text.includes("gpt-5.4") && text.includes("gpt-5.5") @@ -1321,24 +1459,27 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 && text.includes("打断") && lower.includes("attempts"); }, undefined, { timeout: 30000 }); - await page.waitForSelector('[data-testid="codex-queue-filter-select"]', { timeout: 10000 }); - codexQueueTracePlacement = await page.evaluate(() => ({ - firstChildIsTrace: Boolean(document.querySelector('[data-testid="codex-queue-page"] > .codex-session-stage:first-child .codex-output-panel')), - noPageTopStatus: document.querySelector('[data-testid="codex-queue-page"] > [data-testid="codex-top-status"]') === null, - filterInsideTracePanel: Boolean(document.querySelector('.codex-output-panel [data-testid="codex-queue-filter-select"]')), + await page.waitForSelector('[data-testid="code-queue-filter-select"]', { timeout: 10000 }); + codeQueueTracePlacement = await page.evaluate(() => ({ + firstChildIsTrace: Boolean(document.querySelector('[data-testid="code-queue-page"] > .codex-session-stage:first-child .codex-output-panel')), + noPageTopStatus: document.querySelector('[data-testid="code-queue-page"] > [data-testid="codex-top-status"]') === null, + filterInsideTracePanel: Boolean(document.querySelector('.codex-output-panel [data-testid="code-queue-filter-select"]')), + taskSearchVisible: Boolean(document.querySelector('[data-testid="codex-task-search-input"]')), traceStatusVisible: /排队\s*\d+.*运行\s*\d+.*结束未读\s*\d+/s.test(document.querySelector('[data-testid="codex-trace-status-summary"]')?.textContent || ""), markAllReadVisible: Boolean(document.querySelector('.codex-output-panel [data-testid="codex-mark-all-read-button"]')), })); - codexQueueGlobalStatus = await page.evaluate(() => { + codeQueueGlobalStatus = await page.evaluate(() => { const text = document.querySelector('[data-testid="active-microservice-status"]')?.textContent || ""; - return { activeMicroserviceVisible: /Codex Queue.*在线|codex queue.*在线/i.test(text), text }; + return { activeMicroserviceVisible: /Code Queue.*在线|code queue.*在线/i.test(text), text }; }); - await page.waitForSelector('[data-testid="codex-queue-id-select"]', { timeout: 10000 }); + await page.waitForSelector('[data-testid="code-queue-id-select"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="codex-create-queue-button"]', { timeout: 10000 }); - codexQueueSubmitQueueControl = await page.evaluate(() => { - const select = document.querySelector('[data-testid="codex-queue-id-select"]') as HTMLSelectElement | null; + codeQueueSubmitQueueControl = await page.evaluate(() => { + const select = document.querySelector('[data-testid="code-queue-id-select"]') as HTMLSelectElement | null; const button = document.querySelector('[data-testid="codex-create-queue-button"]') as HTMLButtonElement | null; - const prompt = document.querySelector('[data-testid="codex-queue-task-form"] textarea') as HTMLTextAreaElement | null; + const prompt = document.querySelector('[data-testid="code-queue-task-form"] textarea') as HTMLTextAreaElement | null; + const provider = document.querySelector('[data-testid="codex-provider-select"]') as HTMLSelectElement | null; + const cwd = document.querySelector('[data-testid="codex-cwd-input"]') as HTMLInputElement | null; const maxAttempts = document.querySelector('[data-testid="codex-max-attempts-input"]') as HTMLInputElement | null; const moveSelect = document.querySelector('[data-testid="codex-task-queue-move-select"]') as HTMLSelectElement | null; const moveButton = document.querySelector('[data-testid="codex-task-queue-move-button"]') as HTMLButtonElement | null; @@ -1346,17 +1487,20 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 tagName: select?.tagName.toLowerCase() || "", optionCount: select?.options.length ?? 0, createButtonVisible: Boolean(button && button.offsetParent !== null), - oldInputMissing: document.querySelector('[data-testid="codex-queue-id-input"]') === null, + oldInputMissing: document.querySelector('[data-testid="code-queue-id-input"]') === null, promptDefaultEmpty: (prompt?.value || "") === "", + providerValue: provider?.value || "", + providerOptions: Array.from(provider?.options || []).map((option) => ({ value: option.value, text: option.textContent || "" })), + cwdValue: cwd?.value || "", maxAttemptsMax: maxAttempts?.max || "", maxAttemptsValue: maxAttempts?.value || "", moveQueueVisible: Boolean(moveSelect && moveSelect.offsetParent !== null && moveButton && moveButton.offsetParent !== null), }; }); - codexQueuePromptDefaultEmpty = Boolean(codexQueueSubmitQueueControl.promptDefaultEmpty); - await page.locator('[data-testid="codex-queue-task-form"] textarea').fill("e2e batch guard one\n---\ne2e batch guard two"); + codeQueuePromptDefaultEmpty = Boolean(codeQueueSubmitQueueControl.promptDefaultEmpty); + await page.locator('[data-testid="code-queue-task-form"] textarea').fill("e2e batch guard one\n---\ne2e batch guard two"); await page.waitForSelector('[data-testid="codex-batch-confirm-row"]', { timeout: 5000 }); - codexQueueSubmitGuard = await page.evaluate(() => { + codeQueueSubmitGuard = await page.evaluate(() => { const row = document.querySelector('[data-testid="codex-batch-confirm-row"]') as HTMLElement | null; const checkbox = document.querySelector('[data-testid="codex-batch-confirm-checkbox"]') as HTMLInputElement | null; const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null; @@ -1374,8 +1518,8 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null; return button !== null && !button.disabled; }, undefined, { timeout: 5000 }); - codexQueueSubmitGuard = { - ...codexQueueSubmitGuard, + codeQueueSubmitGuard = { + ...codeQueueSubmitGuard, ...(await page.evaluate(() => { const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null; return { @@ -1385,8 +1529,8 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 })), }; await page.locator('[data-testid="codex-clear-input-button"]').click(); - codexQueueOptions = await page.locator('[data-testid="codex-queue-filter-select"] option').evaluateAll((options) => options.map((option) => (option as HTMLOptionElement).textContent || "")); - codexQueueSwitchMetrics = await page.locator('[data-testid="codex-queue-filter-select"] option').evaluateAll((options) => ({ + codeQueueOptions = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => options.map((option) => (option as HTMLOptionElement).textContent || "")); + codeQueueSwitchMetrics = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => ({ optionCount: options.length, queueValues: options.map((option) => (option as HTMLOptionElement).value).filter((value) => value !== "__all__"), switched: false, @@ -1397,7 +1541,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 const output = document.querySelector('[data-testid="codex-output"]')?.textContent || ""; return taskCount === 0 || output.includes("Submitted prompt"); }, undefined, { timeout: 15000 }); - codexQueueScrollbarMetrics = await page.evaluate(() => { + codeQueueScrollbarMetrics = await page.evaluate(() => { const transcript = document.querySelector('.codex-transcript') as HTMLElement | null; const toolBlock = document.querySelector('.codex-transcript-item.ran .codex-transcript-command, .codex-transcript-item.ran .codex-transcript-body, .codex-transcript-item.explored .codex-transcript-command, .codex-transcript-item.explored .codex-transcript-body, .codex-transcript-item.edited .codex-transcript-command, .codex-transcript-item.edited .codex-transcript-body') as HTMLElement | null; const transcriptStyle = transcript ? getComputedStyle(transcript) : null; @@ -1411,7 +1555,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 toolOverflowX: toolStyle?.overflowX || "", }; }); - if (wants("frontend:codex-queue-judge-wrap")) { + if (wants("frontend:code-queue-judge-wrap")) { codexJudgeWrapMetrics = await page.evaluate(() => { const measure = (element: HTMLElement | null): any => { if (element === null) return { found: false }; @@ -1489,15 +1633,77 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 }; }); } - codexQueueTaskCount = await page.locator('[data-testid^="codex-task-codex_"]').count(); - if (wants("frontend:codex-queue-initial-prompt-full-expand")) { + codeQueueSidebarUpdateMetrics = await page.evaluate(() => { + const cards = Array.from(document.querySelectorAll('[data-testid="codex-session-sidebar"] .codex-task-card')) as HTMLElement[]; + const labels = cards + .map((card) => card.querySelector('[data-testid^="codex-task-recent-update-"]')?.textContent || "") + .filter(Boolean); + return { + cardCount: cards.length, + labels, + hasRecentUpdateLabel: cards.length === 0 || labels.some((text) => /^最近更新:\s*(刚刚|--|\d+秒前|\d+分钟\d+秒前|\d+小时|\d+天)/u.test(text)), + }; + }); + if (wantsAny(["frontend:code-queue-integrated-visible", "frontend:code-queue-summary-mobile-wrap"])) { + await page.setViewportSize({ width: 390, height: 860 }); + await page.waitForTimeout(120); + codeQueueSummaryMobileMetrics = await page.evaluate(() => { + const viewportWidth = document.documentElement.clientWidth; + const summaries = Array.from(document.querySelectorAll(".codex-execution-summary")) as HTMLElement[]; + const items = summaries.slice(0, 8).map((summary, index) => { + const header = summary.querySelector(".codex-progressive-card-head") as HTMLElement | null; + const meta = header?.querySelector("code") as HTMLElement | null; + const headerRect = header?.getBoundingClientRect(); + const metaRect = meta?.getBoundingClientRect(); + const rootOverflowPx = Math.max(0, summary.scrollWidth - summary.clientWidth); + const headerOverflowPx = header ? Math.max(0, header.scrollWidth - header.clientWidth) : 0; + const metaOverflowPx = meta ? Math.max(0, meta.scrollWidth - meta.clientWidth) : 0; + return { + index, + text: (header?.textContent || "").replace(/\s+/g, " ").trim().slice(0, 180), + viewportWidth, + rootClientWidth: Math.round(summary.clientWidth), + rootScrollWidth: Math.round(summary.scrollWidth), + headerClientWidth: header ? Math.round(header.clientWidth) : 0, + headerScrollWidth: header ? Math.round(header.scrollWidth) : 0, + metaClientWidth: meta ? Math.round(meta.clientWidth) : 0, + metaScrollWidth: meta ? Math.round(meta.scrollWidth) : 0, + rootOverflowPx: Math.round(rootOverflowPx), + headerOverflowPx: Math.round(headerOverflowPx), + metaOverflowPx: Math.round(metaOverflowPx), + headerRight: headerRect ? Math.round(headerRect.right) : 0, + metaRight: metaRect ? Math.round(metaRect.right) : 0, + metaWhiteSpace: meta ? getComputedStyle(meta).whiteSpace : "", + metaOverflowWrap: meta ? getComputedStyle(meta).overflowWrap : "", + }; + }); + return { + checked: true, + viewportWidth, + summaryCount: summaries.length, + items, + ok: summaries.length === 0 || items.every((item) => + item.rootOverflowPx <= 1 + && item.headerOverflowPx <= 1 + && item.metaOverflowPx <= 1 + && item.headerRight <= viewportWidth + 2 + && item.metaRight <= viewportWidth + 2 + && item.metaWhiteSpace !== "nowrap" + && (item.metaOverflowWrap === "anywhere" || item.metaOverflowWrap === "break-word")), + }; + }); + await page.setViewportSize({ width: 1440, height: 920 }); + await page.waitForTimeout(120); + } + codeQueueTaskCount = await page.locator('[data-testid^="codex-task-codex_"]').count(); + if (wants("frontend:code-queue-initial-prompt-full-expand")) { codexInitialPromptFullMetrics = await page.evaluate(async () => { - const tasksResponse = await fetch("/api/microservices/codex-queue/proxy/api/tasks?limit=300&lite=1&devReady=0", { credentials: "same-origin" }); + const tasksResponse = await fetch("/api/microservices/code-queue/proxy/api/tasks?limit=300&lite=1&devReady=0", { credentials: "same-origin" }); const tasksPayload = await tasksResponse.json().catch(() => null); const tasks = Array.isArray(tasksPayload?.tasks) ? tasksPayload.tasks : []; const candidate = tasks.find((task: any) => Array.isArray(task?.referenceTaskIds) && task.referenceTaskIds.length > 0); if (!candidate?.id) return { candidateFound: false }; - const metaResponse = await fetch(`/api/microservices/codex-queue/proxy/api/tasks/${encodeURIComponent(String(candidate.id))}?meta=1`, { credentials: "same-origin" }); + const metaResponse = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(candidate.id))}?meta=1`, { credentials: "same-origin" }); const metaPayload = await metaResponse.json().catch(() => null); const fullPrompt = String(metaPayload?.task?.prompt || ""); const displayPrompt = String(metaPayload?.task?.displayPrompt || ""); @@ -1506,7 +1712,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 taskId: String(candidate.id), promptChars: fullPrompt.length, displayPromptChars: displayPrompt.length, - hasResolvedReference: fullPrompt.includes("# Codex Queue 已解析引用上下文"), + hasResolvedReference: fullPrompt.includes("# Code Queue 已解析引用上下文"), hasCurrentTaskMarker: fullPrompt.includes("# 本次任务"), hasReferenceTaskId: /codex_\\d+_[A-Za-z0-9_-]+/u.test(fullPrompt), }; @@ -1521,7 +1727,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 await page.waitForFunction(() => (document.querySelector('[data-testid="codex-initial-prompt-full"]') as HTMLDetailsElement | null)?.open === true, undefined, { timeout: 5000 }); const initialFullText = await page.getByTestId("codex-initial-prompt-full-text").innerText({ timeout: 5000 }); codexInitialPromptFullMetrics.initialExpanded = await page.getByTestId("codex-initial-prompt-full").evaluate((element) => (element as HTMLDetailsElement).open); - codexInitialPromptFullMetrics.initialFullHasReference = initialFullText.includes("# Codex Queue 已解析引用上下文") || initialFullText.includes("引用 Codex Queue"); + codexInitialPromptFullMetrics.initialFullHasReference = initialFullText.includes("# Code Queue 已解析引用上下文") || initialFullText.includes("引用 Code Queue"); codexInitialPromptFullMetrics.initialFullHasCurrentTask = initialFullText.includes("# 本次任务") || initialFullText.includes("本次任务:"); codexInitialPromptFullMetrics.initialFullChars = initialFullText.length; codexInitialPromptFullMetrics.legacyPromptPanelMissing = await page.evaluate(() => @@ -1531,16 +1737,16 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 ); } } - if (wants("frontend:codex-queue-trace-full-load")) { + if (wants("frontend:code-queue-trace-full-load")) { codexTraceFullMetrics = await page.evaluate(async () => { - const tasksResponse = await fetch("/api/codex-queue-direct/api/tasks?limit=300&lite=1&devReady=0", { credentials: "same-origin" }); + const tasksResponse = await fetch("/api/code-queue-direct/api/tasks?limit=300&lite=1&devReady=0", { credentials: "same-origin" }); const tasksPayload = await tasksResponse.json().catch(() => null); const tasks = Array.isArray(tasksPayload?.tasks) ? tasksPayload.tasks : []; const terminal = new Set(["succeeded", "failed", "canceled"]); for (const task of tasks) { const taskId = String(task?.id || ""); if (!taskId || !terminal.has(String(task?.status || "")) || Number(task?.outputCount || 0) < 20) continue; - const transcriptResponse = await fetch(`/api/codex-queue-direct/api/tasks/${encodeURIComponent(taskId)}/transcript?afterSeq=0&limit=120&fullText=1`, { credentials: "same-origin" }); + const transcriptResponse = await fetch(`/api/code-queue-direct/api/tasks/${encodeURIComponent(taskId)}/transcript?afterSeq=0&limit=120&fullText=1`, { credentials: "same-origin" }); const transcriptPayload = await transcriptResponse.json().catch(() => null); const transcript = Array.isArray(transcriptPayload?.transcript) ? transcriptPayload.transcript : []; const toolCount = transcript.filter((line: any) => ["ran", "explored", "edited"].includes(String(line?.kind || ""))).length; @@ -1569,7 +1775,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 await traceTaskCard.scrollIntoViewIfNeeded({ timeout: 10000 }); await traceTaskCard.click(); await page.waitForFunction(() => { - const pageElement = document.querySelector('[data-testid="codex-queue-page"]') as HTMLElement | null; + const pageElement = document.querySelector('[data-testid="code-queue-page"]') as HTMLElement | null; const output = document.querySelector('[data-testid="codex-output"]') as HTMLElement | null; const toolCount = output?.querySelectorAll('.codex-transcript-item.ran, .codex-transcript-item.explored, .codex-transcript-item.edited').length ?? 0; return pageElement?.getAttribute("data-load-state") === "complete" && toolCount >= 8; @@ -1577,7 +1783,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 codexTraceFullMetrics = { ...codexTraceFullMetrics, ...(await page.evaluate(() => { - const pageElement = document.querySelector('[data-testid="codex-queue-page"]') as HTMLElement | null; + const pageElement = document.querySelector('[data-testid="code-queue-page"]') as HTMLElement | null; const output = document.querySelector('[data-testid="codex-output"]') as HTMLElement | null; const text = output?.textContent || ""; return { @@ -1594,13 +1800,144 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 }; } } - codexQueueOutputText = await page.locator('[data-testid="codex-output"]').innerText({ timeout: 5000 }); - codexQueueText = await page.locator('[data-testid="codex-queue-page"]').innerText({ timeout: 5000 }); - codexQueueOptions = await page.locator('[data-testid="codex-queue-filter-select"] option').evaluateAll((options) => options.map((option) => (option as HTMLOptionElement).textContent || "")); - codexQueueSubmitQueueControl = await page.evaluate(() => { - const select = document.querySelector('[data-testid="codex-queue-id-select"]') as HTMLSelectElement | null; + if (wants("frontend:code-queue-error-red-markers")) { + const authCookies = await page.context().cookies(urls.frontendUrl); + const cookieHeader = authCookies.map(({ name, value }) => `${name}=${value}`).join("; "); + const authHeaders: Record = { accept: "application/json" }; + if (cookieHeader) authHeaders.cookie = cookieHeader; + const fetchJson = async (path: string): Promise => { + const response = await fetch(new URL(path, urls.frontendUrl), { headers: authHeaders }); + return response.json().catch(() => null); + }; + const tasksPayload = await fetchJson("/api/code-queue-direct/api/tasks?limit=300&lite=1&devReady=0"); + const tasks = Array.isArray(tasksPayload?.tasks) ? tasksPayload.tasks : []; + const prioritized = tasks + .filter((task: any) => ["failed", "canceled", "running", "judging", "succeeded"].includes(String(task?.status || ""))) + .concat(tasks); + for (const task of prioritized) { + const taskId = String(task?.id || ""); + if (!taskId) continue; + const summaryPayload = await fetchJson(`/api/code-queue-direct/api/tasks/${encodeURIComponent(taskId)}/trace-summary`); + const summary = summaryPayload?.summary || null; + const attempts = Array.isArray(summary?.attempts) ? summary.attempts : []; + const errorAttempt = attempts.find((attempt: any) => Number(attempt?.errorCount ?? 0) > 0) || null; + const errorCount = Number(errorAttempt?.errorCount ?? summary?.errorCount ?? 0); + const attemptIndex = Number(errorAttempt?.index ?? 0); + if (Number.isFinite(errorCount) && errorCount > 0 && Number.isFinite(attemptIndex) && attemptIndex > 0) { + codeQueueErrorHighlightMetrics = { + checked: true, + candidateFound: true, + taskId, + errorCount, + attemptIndex, + status: String(task?.status || summary?.status || ""), + }; + break; + } + } + if (!codeQueueErrorHighlightMetrics) { + codeQueueErrorHighlightMetrics = { checked: true, candidateFound: false, taskCount: tasks.length }; + } + if (codeQueueErrorHighlightMetrics.candidateFound) { + const errorTaskSearch = page.getByTestId("codex-task-search-input"); + if (await errorTaskSearch.count()) { + await errorTaskSearch.fill(codeQueueErrorHighlightMetrics.taskId); + await page.waitForTimeout(1000); + } + for (let index = 0; index < 8; index += 1) { + if (await page.getByTestId(`codex-task-${codeQueueErrorHighlightMetrics.taskId}`).count()) break; + const moreButton = page.getByTestId("codex-load-more-tasks-button"); + if (!(await moreButton.count())) break; + await moreButton.scrollIntoViewIfNeeded().catch(() => undefined); + await moreButton.click(); + await page.waitForTimeout(500); + } + const errorTaskCard = page.getByTestId(`codex-task-${codeQueueErrorHighlightMetrics.taskId}`); + await errorTaskCard.scrollIntoViewIfNeeded({ timeout: 10000 }); + await errorTaskCard.click(); + await page.waitForSelector('[data-testid="codex-execution-summary"]', { timeout: 15000 }); + const errorSummaryTestId = Number(codeQueueErrorHighlightMetrics.attemptIndex || 0) > 1 + ? `codex-execution-summary-attempt-${Number(codeQueueErrorHighlightMetrics.attemptIndex)}` + : "codex-execution-summary"; + const errorPillTestId = `${errorSummaryTestId}-error-count`; + const errorSummaryLocator = page.getByTestId(errorSummaryTestId); + const errorPillLocator = page.getByTestId(errorPillTestId); + await errorPillLocator.waitFor({ state: "visible", timeout: 30000 }); + const collapsedMetrics = await page.evaluate(({ summaryTestId, pillTestId }) => { + const summary = document.querySelector(`[data-testid="${summaryTestId}"]`) as HTMLDetailsElement | null; + const pill = document.querySelector(`[data-testid="${pillTestId}"]`) as HTMLElement | null; + const pillStyle = pill ? getComputedStyle(pill) : null; + const dangerColor = /207,\s*106,\s*84/u; + return { + summaryOpen: Boolean(summary?.open), + pillVisible: Boolean(pill && pill.offsetParent !== null), + pillText: String(pill?.textContent || "").trim(), + pillClass: String(pill?.className || ""), + pillColor: pillStyle?.color || "", + pillBorderColor: pillStyle?.borderColor || "", + pillBackgroundColor: pillStyle?.backgroundColor || "", + hasDangerColor: dangerColor.test(pillStyle?.color || "") && dangerColor.test(pillStyle?.borderColor || ""), + }; + }, { summaryTestId: errorSummaryTestId, pillTestId: errorPillTestId }); + await errorSummaryLocator.locator("summary").click(); + await page.waitForFunction((testId) => (document.querySelector(`[data-testid="${testId}"]`) as HTMLDetailsElement | null)?.open === true, errorSummaryTestId, { timeout: 5000 }); + await page.waitForFunction((testId) => document.querySelectorAll(`[data-testid="${testId}"] .codex-trace-step.error`).length > 0, errorSummaryTestId, { timeout: 15000 }); + const expandedMetrics = await page.evaluate(({ summaryTestId }) => { + const summary = document.querySelector(`[data-testid="${summaryTestId}"]`) as HTMLDetailsElement | null; + const errorSteps = Array.from(summary?.querySelectorAll(".codex-trace-step.error") || []) as HTMLElement[]; + const first = errorSteps[0] || null; + const channel = first?.querySelector(".codex-output-channel") as HTMLElement | null; + const code = first?.querySelector("code") as HTMLElement | null; + const firstStyle = first ? getComputedStyle(first) : null; + const channelStyle = channel ? getComputedStyle(channel) : null; + const codeStyle = code ? getComputedStyle(code) : null; + const dangerColor = /207,\s*106,\s*84/u; + return { + errorStepCount: errorSteps.length, + firstStepClass: String(first?.className || ""), + firstStepBorderColor: firstStyle?.borderColor || "", + firstStepBackgroundColor: firstStyle?.backgroundColor || "", + firstChannelColor: channelStyle?.color || "", + firstCodeColor: codeStyle?.color || "", + hasDangerColor: dangerColor.test(firstStyle?.borderColor || "") && dangerColor.test(channelStyle?.color || "") && dangerColor.test(codeStyle?.color || ""), + }; + }, { summaryTestId: errorSummaryTestId }); + codeQueueErrorHighlightMetrics = { + ...codeQueueErrorHighlightMetrics, + collapsed: collapsedMetrics, + expanded: expandedMetrics, + ok: collapsedMetrics.pillVisible === true + && collapsedMetrics.pillText.startsWith("Error ") + && collapsedMetrics.hasDangerColor === true + && collapsedMetrics.summaryOpen === false + && expandedMetrics.errorStepCount > 0 + && expandedMetrics.firstStepClass.includes("error") + && expandedMetrics.hasDangerColor === true, + }; + } + } + codeQueueOutputText = await page.locator('[data-testid="codex-output"]').innerText({ timeout: 5000 }); + codeQueueText = await page.locator('[data-testid="code-queue-page"]').innerText({ timeout: 5000 }); + codeQueueHtmlGuard = await page.evaluate(async () => { + const root = document.getElementById("root"); + const overviewAttr = root?.getAttribute("data-codex-overview") || ""; + const response = await fetch(window.location.pathname, { credentials: "same-origin" }); + const html = await response.text(); + return { + rootAttrLength: overviewAttr.length, + rootAttrMissing: overviewAttr.length === 0, + sourceBytes: html.length, + sourceAttrMissing: !html.includes("data-codex-overview"), + sourceNoBasePrompt: !html.includes("basePrompt"), + }; + }); + codeQueueOptions = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => options.map((option) => (option as HTMLOptionElement).textContent || "")); + codeQueueSubmitQueueControl = await page.evaluate(() => { + const select = document.querySelector('[data-testid="code-queue-id-select"]') as HTMLSelectElement | null; const button = document.querySelector('[data-testid="codex-create-queue-button"]') as HTMLButtonElement | null; - const prompt = document.querySelector('[data-testid="codex-queue-task-form"] textarea') as HTMLTextAreaElement | null; + const prompt = document.querySelector('[data-testid="code-queue-task-form"] textarea') as HTMLTextAreaElement | null; + const provider = document.querySelector('[data-testid="codex-provider-select"]') as HTMLSelectElement | null; + const cwd = document.querySelector('[data-testid="codex-cwd-input"]') as HTMLInputElement | null; const maxAttempts = document.querySelector('[data-testid="codex-max-attempts-input"]') as HTMLInputElement | null; const moveSelect = document.querySelector('[data-testid="codex-task-queue-move-select"]') as HTMLSelectElement | null; const moveButton = document.querySelector('[data-testid="codex-task-queue-move-button"]') as HTMLButtonElement | null; @@ -1608,22 +1945,25 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 tagName: select?.tagName.toLowerCase() || "", optionCount: select?.options.length ?? 0, createButtonVisible: Boolean(button && button.offsetParent !== null), - oldInputMissing: document.querySelector('[data-testid="codex-queue-id-input"]') === null, + oldInputMissing: document.querySelector('[data-testid="code-queue-id-input"]') === null, promptDefaultEmpty: (prompt?.value || "") === "", + providerValue: provider?.value || "", + providerOptions: Array.from(provider?.options || []).map((option) => ({ value: option.value, text: option.textContent || "" })), + cwdValue: cwd?.value || "", maxAttemptsMax: maxAttempts?.max || "", maxAttemptsValue: maxAttempts?.value || "", moveQueueVisible: Boolean(moveSelect && moveSelect.offsetParent !== null && moveButton && moveButton.offsetParent !== null), }; }); - codexQueuePromptDefaultEmpty = Boolean(codexQueueSubmitQueueControl.promptDefaultEmpty); - codexQueueSwitchMetrics = await page.locator('[data-testid="codex-queue-filter-select"] option').evaluateAll((options) => ({ + codeQueuePromptDefaultEmpty = Boolean(codeQueueSubmitQueueControl.promptDefaultEmpty); + codeQueueSwitchMetrics = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => ({ optionCount: options.length, queueValues: options.map((option) => (option as HTMLOptionElement).value).filter((value) => value !== "__all__"), switched: false, })); - if (codexQueueSwitchMetrics.queueValues.length > 0) { - const targetQueueId = String(codexQueueSwitchMetrics.queueValues.find((value: string) => value !== "default") || codexQueueSwitchMetrics.queueValues[0]); - await page.getByTestId("codex-queue-filter-select").selectOption(targetQueueId); + if (codeQueueSwitchMetrics.queueValues.length > 0) { + const targetQueueId = String(codeQueueSwitchMetrics.queueValues.find((value: string) => value !== "default") || codeQueueSwitchMetrics.queueValues[0]); + await page.getByTestId("code-queue-filter-select").selectOption(targetQueueId); await page.waitForFunction((queueId) => { const text = document.body.innerText; return text.includes(`view=${queueId}`) || text.includes(`${queueId} ·`); @@ -1632,8 +1972,8 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 const cards = Array.from(document.querySelectorAll('[data-testid^="codex-task-codex_"]')).map((node) => (node as HTMLElement).innerText); return cards.length === 0 || cards.every((text) => text.includes(`queue=${queueId}`)); }, targetQueueId, { timeout: 15000 }); - codexQueueSwitchMetrics = { ...codexQueueSwitchMetrics, targetQueueId, switched: true }; - await page.getByTestId("codex-queue-filter-select").selectOption("__all__"); + codeQueueSwitchMetrics = { ...codeQueueSwitchMetrics, targetQueueId, switched: true }; + await page.getByTestId("code-queue-filter-select").selectOption("__all__"); } } if (needClaudeqq) { @@ -1674,19 +2014,19 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 await page.waitForSelector('[data-testid="overview-page"]', { timeout: 10000 }); routeOverviewPath = new URL(page.url()).pathname; routeOverviewText = await page.locator('[data-testid="overview-page"]').innerText({ timeout: 5000 }); - await page.goto(`${urls.frontendUrl}/app/codex-queue/`, { waitUntil: "domcontentloaded", timeout: 15000 }); + await page.goto(`${urls.frontendUrl}/app/code-queue/`, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector('[data-testid="app-shell"]', { timeout: 10000 }); - await page.waitForSelector('[data-testid="codex-queue-page"]', { timeout: 15000 }); + await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 15000 }); routeCodexPath = new URL(page.url()).pathname; routeCodexShellMetrics = await page.evaluate(() => ({ appShell: Boolean(document.querySelector('[data-testid="app-shell"]')), - standalone: Boolean(document.querySelector('[data-testid="codex-queue-standalone"]')), + standalone: Boolean(document.querySelector('[data-testid="code-queue-standalone"]')), railText: document.querySelector(".rail")?.textContent || "", topbar: Boolean(document.querySelector(".topbar")), tabsText: document.querySelector(".tabs")?.textContent || "", - codexPage: Boolean(document.querySelector('[data-testid="codex-queue-page"]')), + codexPage: Boolean(document.querySelector('[data-testid="code-queue-page"]')), })); - await page.locator('.rail button[title="用户服务"]').click(); + await page.locator('.rail [role="button"][title="用户服务"]').click(); await page.getByRole("button", { name: /^服务目录$/ }).click(); await page.waitForSelector('[data-testid="microservice-catalog-page"]', { timeout: 10000 }); } @@ -2050,7 +2390,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 const microserviceCatalogTextLower = microserviceCatalogText.toLowerCase(); const todoNoteTextLower = todoNoteText.toLowerCase(); const findjobTextLower = findjobText.toLowerCase(); - const codexQueueTextLower = codexQueueText.toLowerCase(); + const codeQueueTextLower = codeQueueText.toLowerCase(); const claudeqqTextLower = claudeqqText.toLowerCase(); const pipelineTextLower = pipelineText.toLowerCase(); const activePipeline = Array.isArray(pipelineSnapshotForFrontend?.pipelines) @@ -2084,13 +2424,15 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 addSelectedCheck(checks, options, "frontend:gateway-duration-subsecond-visible", gatewayHasSubsecondDuration && !gatewayHasRoundedZeroDuration, { gatewayHasSubsecondDuration, gatewayHasRoundedZeroDuration, gatewayTextPreview: gatewayText.slice(0, 900) }); addSelectedCheck(checks, options, "frontend:provider-operation-availability-visible", sshAvailabilityTexts.length >= 1 && upgradeAvailabilityTexts.length >= 1 && sshAvailabilityTexts.every((text) => text.includes("SSH 透传")) && upgradeAvailabilityTexts.every((text) => text.includes("远程更新")) && upgradeAvailabilityTexts.some((text) => text.includes("always-enabled")), { sshAvailabilityTexts, upgradeAvailabilityTexts }); addSelectedCheck(checks, options, "frontend:overview-pgdata-visible", bodyText.includes("PGDATA") && bodyText.includes(config.database.volume), { bodyPreview: bodyText.slice(0, 800) }); - addSelectedCheck(checks, options, "frontend:microservice-catalog-visible", microserviceCatalogTextLower.includes("findjob") && microserviceCatalogTextLower.includes("pipeline") && microserviceCatalogTextLower.includes("todo note") && microserviceCatalogTextLower.includes("met nonlinear") && microserviceCatalogTextLower.includes("claudeqq") && microserviceCatalogTextLower.includes("codex queue") && microserviceCatalogText.includes("D601") && microserviceCatalogText.includes(config.providerGateway.id) && microserviceCatalogTextLower.includes("private") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/findjob") && microserviceCatalogText.includes("https://github.com/pikasTech/pipeline") && microserviceCatalogText.includes("https://github.com/pikasTech/met_nonlinear") && microserviceCatalogText.includes("https://gitee.com/lyon1998/agent_skills") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/todo_note") && microserviceCatalogText.includes("https://github.com/pikasTech/unidesk"), { microserviceCatalogPreview: microserviceCatalogText.slice(0, 2000) }); + addSelectedCheck(checks, options, "frontend:microservice-catalog-visible", microserviceCatalogTextLower.includes("findjob") && microserviceCatalogTextLower.includes("pipeline") && microserviceCatalogTextLower.includes("todo note") && microserviceCatalogTextLower.includes("met nonlinear") && microserviceCatalogTextLower.includes("claudeqq") && microserviceCatalogTextLower.includes("code queue") && microserviceCatalogText.includes("D601") && microserviceCatalogText.includes(config.providerGateway.id) && microserviceCatalogTextLower.includes("private") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/findjob") && microserviceCatalogText.includes("https://github.com/pikasTech/pipeline") && microserviceCatalogText.includes("https://github.com/pikasTech/met_nonlinear") && microserviceCatalogText.includes("https://gitee.com/lyon1998/agent_skills") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/todo_note") && microserviceCatalogText.includes("https://github.com/pikasTech/unidesk"), { microserviceCatalogPreview: microserviceCatalogText.slice(0, 2000) }); addSelectedCheck(checks, options, "frontend:todo-note-integrated-visible", todoNoteTextLower.includes("todo note 工作台") && todoNoteText.includes("CONSTAR") && todoNoteText.includes("大论文") && todoNoteText.includes("UI E2E smoke task") && todoNoteText.includes("撤销") && todoNoteText.includes("重做") && todoNoteText.includes("全部展开") && todoNoteText.includes("仅 UniDesk frontend 代理访问"), { todoNoteTextPreview: todoNoteText.slice(0, 1400) }); addSelectedCheck(checks, options, "frontend:findjob-integrated-visible", findjobTextLower.includes("findjob 工作台".toLowerCase()) && findjobText.includes("岗位总量") && findjobText.includes("D601") && findjobText.includes("近期岗位") && findjobText.includes("仅 UniDesk frontend 代理访问") && /岗位总量\s+\d+/.test(findjobText) && /health\s+ok/i.test(findjobText) && /[1-9]\d*\/[1-9]\d*\s+preview/i.test(findjobText), { findjobTextPreview: findjobText.slice(0, 1200) }); - addSelectedCheck(checks, options, "frontend:codex-queue-integrated-visible", codexQueueTextLower.includes("codex queue") && codexQueueText.includes("gpt-5.4-mini") && codexQueueText.includes("gpt-5.4") && codexQueueText.includes("gpt-5.5") && codexQueueText.includes("提交任务") && codexQueueText.includes("入队份数") && codexQueueText.includes("追加 prompt") && codexQueueText.includes("打断") && codexQueueTextLower.includes("查看 queue") && codexQueueText.includes("创建 queue") && codexQueueOptions.some((text) => text.includes("All queues")) && codexQueueTracePlacement.firstChildIsTrace === true && codexQueueTracePlacement.noPageTopStatus === true && codexQueueTracePlacement.filterInsideTracePanel === true && codexQueueTracePlacement.traceStatusVisible === true && codexQueueTracePlacement.markAllReadVisible === true && codexQueueGlobalStatus.activeMicroserviceVisible === true && codexQueueSubmitQueueControl.tagName === "select" && codexQueueSubmitQueueControl.createButtonVisible === true && codexQueueSubmitQueueControl.oldInputMissing === true && codexQueueSubmitQueueControl.maxAttemptsMax === "99" && codexQueueSubmitQueueControl.maxAttemptsValue === "99" && codexQueueSubmitQueueControl.moveQueueVisible === true && codexQueuePromptDefaultEmpty === true && codexQueueSubmitGuard.batchRowVisible === true && codexQueueSubmitGuard.checkboxVisible === true && codexQueueSubmitGuard.disabledBeforeConfirm === true && codexQueueSubmitGuard.enabledAfterConfirm === true && codexQueueSubmitGuard.waitElementMissingBeforeSubmit === true && codexQueueScrollbarMetrics.transcriptThin === true && codexQueueScrollbarMetrics.toolHorizontalHidden === true && (codexQueueSwitchMetrics.optionCount <= 1 || codexQueueSwitchMetrics.switched === true) && codexQueueTextLower.includes("attempts") && codexQueueText.includes("仅 UniDesk frontend 代理访问") && (codexQueueTaskCount === 0 || codexQueueOutputText.includes("Submitted prompt")), { codexQueueTaskCount, codexQueueOptions, codexQueueSwitchMetrics, codexQueueSubmitQueueControl, codexQueueSubmitGuard, codexQueueScrollbarMetrics, codexQueuePromptDefaultEmpty, codexQueueTracePlacement, codexQueueGlobalStatus, codexQueueOutputPreview: codexQueueOutputText.slice(0, 900), codexQueueTextPreview: codexQueueText.slice(0, 1400) }); - addSelectedCheck(checks, options, "frontend:codex-queue-initial-prompt-full-expand", - codexInitialPromptFullMetrics.candidateFound === false - || ( + addSelectedCheck(checks, options, "frontend:code-queue-integrated-visible", codeQueueTextLower.includes("code queue") && codeQueueText.includes("gpt-5.4-mini") && codeQueueText.includes("gpt-5.4") && codeQueueText.includes("gpt-5.5") && codeQueueText.includes("提交任务") && codeQueueText.includes("执行 Provider") && codeQueueText.includes("入队份数") && codeQueueText.includes("追加 prompt") && codeQueueText.includes("打断") && codeQueueTextLower.includes("查看 queue") && codeQueueText.includes("创建 queue") && codeQueueOptions.some((text) => text.includes("All queues")) && codeQueueTracePlacement.firstChildIsTrace === true && codeQueueTracePlacement.noPageTopStatus === true && codeQueueTracePlacement.filterInsideTracePanel === true && codeQueueTracePlacement.taskSearchVisible === true && codeQueueTracePlacement.traceStatusVisible === true && codeQueueTracePlacement.markAllReadVisible === true && codeQueueGlobalStatus.activeMicroserviceVisible === true && codeQueueSidebarUpdateMetrics.hasRecentUpdateLabel === true && codeQueueHtmlGuard.rootAttrMissing === true && codeQueueHtmlGuard.sourceAttrMissing === true && codeQueueHtmlGuard.sourceNoBasePrompt === true && codeQueueSubmitQueueControl.tagName === "select" && codeQueueSubmitQueueControl.createButtonVisible === true && codeQueueSubmitQueueControl.oldInputMissing === true && codeQueueSubmitQueueControl.providerValue === "main-server" && codeQueueSubmitQueueControl.cwdValue === "/root/unidesk" && Array.isArray(codeQueueSubmitQueueControl.providerOptions) && codeQueueSubmitQueueControl.providerOptions.some((item: any) => item.value === "D601" && String(item.text || "").includes("/home/ubuntu")) && codeQueueSubmitQueueControl.maxAttemptsMax === "99" && codeQueueSubmitQueueControl.maxAttemptsValue === "99" && codeQueueSubmitQueueControl.moveQueueVisible === true && codeQueuePromptDefaultEmpty === true && codeQueueSubmitGuard.batchRowVisible === true && codeQueueSubmitGuard.checkboxVisible === true && codeQueueSubmitGuard.disabledBeforeConfirm === true && codeQueueSubmitGuard.enabledAfterConfirm === true && codeQueueSubmitGuard.waitElementMissingBeforeSubmit === true && codeQueueScrollbarMetrics.transcriptThin === true && codeQueueScrollbarMetrics.toolHorizontalHidden === true && (codeQueueSwitchMetrics.optionCount <= 1 || codeQueueSwitchMetrics.switched === true) && codeQueueTextLower.includes("attempts") && codeQueueText.includes("仅 UniDesk frontend 代理访问") && (codeQueueTaskCount === 0 || codeQueueOutputText.includes("Submitted prompt")), { codeQueueTaskCount, codeQueueOptions, codeQueueSwitchMetrics, codeQueueSubmitQueueControl, codeQueueSubmitGuard, codeQueueScrollbarMetrics, codeQueuePromptDefaultEmpty, codeQueueTracePlacement, codeQueueGlobalStatus, codeQueueSidebarUpdateMetrics, codeQueueHtmlGuard, codeQueueOutputPreview: codeQueueOutputText.slice(0, 900), codeQueueTextPreview: codeQueueText.slice(0, 1400) }); + addSelectedCheck(checks, options, "frontend:code-queue-summary-mobile-wrap", codeQueueSummaryMobileMetrics.checked === true && (codeQueueSummaryMobileMetrics.summaryCount === 0 || codeQueueSummaryMobileMetrics.ok === true), { codeQueueSummaryMobileMetrics }); + addSelectedCheck(checks, options, "frontend:code-queue-error-red-markers", codeQueueErrorHighlightMetrics.checked === true && codeQueueErrorHighlightMetrics.candidateFound === true && codeQueueErrorHighlightMetrics.ok === true, { codeQueueErrorHighlightMetrics }); + addSelectedCheck(checks, options, "frontend:code-queue-initial-prompt-full-expand", + codexInitialPromptFullMetrics.candidateFound === false + || ( codexInitialPromptFullMetrics.promptChars > codexInitialPromptFullMetrics.displayPromptChars && codexInitialPromptFullMetrics.initialDefaultOpen === false && codexInitialPromptFullMetrics.initialExpanded === true @@ -2099,7 +2441,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 && codexInitialPromptFullMetrics.legacyPromptPanelMissing === true ), { codexInitialPromptFullMetrics }); - addSelectedCheck(checks, options, "frontend:codex-queue-trace-full-load", + addSelectedCheck(checks, options, "frontend:code-queue-trace-full-load", codexTraceFullMetrics.candidateFound === false || ( codexTraceFullMetrics.apiTotal >= 20 @@ -2113,11 +2455,11 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 && codexTraceFullMetrics.loadPartial !== "true" ), { codexTraceFullMetrics }); - addSelectedCheck(checks, options, "frontend:codex-queue-judge-wrap", + addSelectedCheck(checks, options, "frontend:code-queue-judge-wrap", codexJudgeWrapMetrics.checked === true && codexJudgeWrapMetrics.ok === true, { codexJudgeWrapMetrics }); addSelectedCheck(checks, options, "frontend:claudeqq-integrated-visible", claudeqqTextLower.includes("claudeqq 工作台") && claudeqqText.includes("D601") && claudeqqText.includes("QQ 事件订阅") && claudeqqText.includes("消息推送") && claudeqqText.includes("事件缓存") && claudeqqText.includes("主用户私聊账号") && claudeqqText.includes("645275593") && claudeqqTextLower.includes("napcat 容器登录") && (claudeqqText.includes("二维码") || claudeqqText.includes("QR SOURCE") || claudeqqText.includes("QR Source") || claudeqqText.includes("已登录")) && claudeqqText.includes("仅 UniDesk frontend 代理访问") && !claudeqqText.includes("{\n"), { claudeqqTextPreview: claudeqqText.slice(0, 1400) }); - addSelectedCheck(checks, options, "frontend:url-route-deeplink", routeInitialPath === "/app/pipeline/" && routeDockerPath === "/nodes/docker/" && routeBackPath === "/app/pipeline/" && routeOverviewPath === "/ops/status/" && routeCodexPath === "/app/codex-queue/" && routeDeepLinkText.toLowerCase().includes("pipeline v2 工作台".toLowerCase()) && routeOverviewText.includes("核心指标") && routeCodexShellMetrics.appShell === true && routeCodexShellMetrics.standalone === false && routeCodexShellMetrics.topbar === true && routeCodexShellMetrics.codexPage === true && String(routeCodexShellMetrics.railText || "").includes("用户服务") && String(routeCodexShellMetrics.tabsText || "").includes("Codex Queue"), { routeInitialPath, routeDockerPath, routeBackIntermediatePath, routeBackPath, routeOverviewPath, routeCodexPath, routeCodexShellMetrics, routeDeepLinkPreview: routeDeepLinkText.slice(0, 1200), routeOverviewPreview: routeOverviewText.slice(0, 800) }); + addSelectedCheck(checks, options, "frontend:url-route-deeplink", routeInitialPath === "/app/pipeline/" && routeDockerPath === "/nodes/docker/" && routeBackPath === "/app/pipeline/" && routeOverviewPath === "/ops/status/" && routeCodexPath === "/app/code-queue/" && routeDeepLinkText.toLowerCase().includes("pipeline v2 工作台".toLowerCase()) && routeOverviewText.includes("核心指标") && routeCodexShellMetrics.appShell === true && routeCodexShellMetrics.standalone === false && routeCodexShellMetrics.topbar === true && routeCodexShellMetrics.codexPage === true && String(routeCodexShellMetrics.railText || "").includes("用户服务") && String(routeCodexShellMetrics.tabsText || "").includes("Code Queue"), { routeInitialPath, routeDockerPath, routeBackIntermediatePath, routeBackPath, routeOverviewPath, routeCodexPath, routeCodexShellMetrics, routeDeepLinkPreview: routeDeepLinkText.slice(0, 1200), routeOverviewPreview: routeOverviewText.slice(0, 800) }); addSelectedCheck(checks, options, "frontend:pipeline-integrated-visible", pipelineTextLower.includes("pipeline v2 工作台".toLowerCase()) && pipelineText.includes("D601") @@ -2194,7 +2536,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 { pipelineObservationGanttMetrics }); addSelectedCheck(checks, options, "frontend:pipeline-step-timeline-visible", pipelineStepTimelineText.includes("OpenCode Trace") - && pipelineStepTimelineText.includes("Codex Queue") + && pipelineStepTimelineText.includes("Code Queue") && pipelineStepTimelineText.toLowerCase().includes("tools") && pipelineStepTimelineText.includes("Trace") && !firstPipelineStepSummaryText.includes("{\n") diff --git a/scripts/src/jobs.ts b/scripts/src/jobs.ts index fb7acd7e..ed87cc73 100644 --- a/scripts/src/jobs.ts +++ b/scripts/src/jobs.ts @@ -97,7 +97,7 @@ export function startJob(name: string, command: string[], note: string, options: repoRoot, "--entrypoint", "bun", - options.dockerImage ?? "unidesk-codex-queue:latest", + options.dockerImage ?? "unidesk-code-queue:latest", rootPath("scripts", "cli.ts"), "internal", "run-job", diff --git a/scripts/src/microservices.ts b/scripts/src/microservices.ts index b2cabe83..d9422d4e 100644 --- a/scripts/src/microservices.ts +++ b/scripts/src/microservices.ts @@ -50,6 +50,20 @@ function numberOption(args: string[], name: string, defaultValue: number): numbe return value; } +function stringOption(args: string[], name: string): string | undefined { + const index = args.indexOf(name); + if (index === -1) return undefined; + const raw = args[index + 1]; + if (raw === undefined || raw.length === 0) throw new Error(`${name} requires a non-empty value`); + return raw; +} + +function methodOption(args: string[]): string { + const method = (stringOption(args, "--method") ?? "GET").toUpperCase(); + if (!["GET", "HEAD", "POST", "DELETE", "PUT", "PATCH"].includes(method)) throw new Error(`unsupported --method ${method}`); + return method; +} + export function summarizeMicroserviceProxyResponse(response: unknown, args: string[]): unknown { if (args.includes("--raw")) return response; const maxBodyBytes = numberOption(args, "--max-body-bytes", 60_000); @@ -84,7 +98,7 @@ export async function runMicroserviceCommand(_config: UniDeskConfig, args: strin if (action === "proxy") { const id = requireId(idArg, "microservice proxy"); const path = requireProxyPath(pathArg); - return summarizeMicroserviceProxyResponse(coreInternalFetch(`/api/microservices/${encodeId(id)}/proxy${path}`), args); + return summarizeMicroserviceProxyResponse(coreInternalFetch(`/api/microservices/${encodeId(id)}/proxy${path}`, { method: methodOption(args) }), args); } throw new Error("microservice command must be one of: list, status, health, proxy"); } diff --git a/scripts/src/provider-attach.ts b/scripts/src/provider-attach.ts new file mode 100644 index 00000000..70f3c612 --- /dev/null +++ b/scripts/src/provider-attach.ts @@ -0,0 +1,222 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { type UniDeskConfig, repoRoot, rootPath } from "./config"; +import { runCommand } from "./command"; + +interface ProviderAttachOptions { + providerId: string; + masterServer: string; + envFile: string; + composeFile: string; + logDir: string; + hostSshKeyDir: string; + token: string | null; + force: boolean; + up: boolean; +} + +interface ProviderAttachContainerValidation { + ok: boolean; + containerName: string; + restartPolicy: string; + restartPolicyOk: boolean; + pidMode: string; + pidModeOk: boolean; + state: string; + command: string[]; + exitCode: number | null; + stderr: string; +} + +function optionValue(args: string[], name: string): string | null { + const index = args.indexOf(name); + if (index === -1) return null; + const value = args[index + 1]; + if (value === undefined || value.length === 0) throw new Error(`${name} requires a non-empty value`); + return value; +} + +function hasFlag(args: string[], name: string): boolean { + return args.includes(name); +} + +function safeProviderSlug(providerId: string): string { + return providerId.replace(/[^a-zA-Z0-9_.-]/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "provider"; +} + +function defaultMasterServer(config: UniDeskConfig): string { + return `http://${config.network.publicHost}/`; +} + +function defaultHostSshKeyDir(): string { + const home = process.env.HOME || "/root"; + return join(home, ".unidesk", "host-ssh"); +} + +function parseAttachOptions(config: UniDeskConfig, args: string[]): ProviderAttachOptions { + const valueOptions = new Set(["--env-file", "--compose-file", "--log-dir", "--master-server", "--master", "--host-ssh-key-dir", "--provider-token", "--token"]); + let providerId: string | undefined; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index] ?? ""; + if (valueOptions.has(arg)) { + index += 1; + continue; + } + if (!arg.startsWith("--")) { + providerId = arg; + break; + } + } + if (providerId === undefined) { + throw new Error("provider attach requires provider id, for example: bun scripts/cli.ts provider attach D518"); + } + const envFile = optionValue(args, "--env-file") ?? rootPath(".state", `provider-${providerId}.env`); + const composeFile = optionValue(args, "--compose-file") ?? rootPath(`provider-${providerId}.yml`); + const logDir = optionValue(args, "--log-dir") ?? rootPath("logs", `provider-${providerId}`); + return { + providerId, + masterServer: optionValue(args, "--master-server") ?? optionValue(args, "--master") ?? defaultMasterServer(config), + envFile, + composeFile, + logDir, + hostSshKeyDir: optionValue(args, "--host-ssh-key-dir") ?? defaultHostSshKeyDir(), + token: optionValue(args, "--provider-token") ?? optionValue(args, "--token"), + force: hasFlag(args, "--force"), + up: hasFlag(args, "--up"), + }; +} + +function envContent(options: ProviderAttachOptions): string { + const lines = [ + "# Generated by: bun scripts/cli.ts provider attach", + "# Required attach inputs are intentionally limited to master server and provider id.", + `UNIDESK_MASTER_SERVER=${options.masterServer}`, + `PROVIDER_ID=${options.providerId}`, + ]; + if (options.token !== null) lines.push(`PROVIDER_TOKEN=${options.token}`); + return `${lines.join("\n")}\n`; +} + +function composeContent(options: ProviderAttachOptions): string { + const slug = safeProviderSlug(options.providerId); + const relativeEnv = pathForCompose(options.envFile); + const relativeLogDir = pathForCompose(options.logDir); + return [ + "services:", + " provider-gateway:", + ` image: unidesk_provider-gateway:${slug}`, + " build:", + " context: .", + " dockerfile: src/components/provider-gateway/Dockerfile", + ` container_name: unidesk-provider-gateway-${slug}`, + " restart: always", + ' pid: "host"', + " env_file:", + ` - ${relativeEnv}`, + " volumes:", + " - /var/run/docker.sock:/var/run/docker.sock", + " - .:/workspace:ro", + ` - ${relativeLogDir}:/var/log/unidesk`, + ` - ${options.hostSshKeyDir}:/run/host-ssh:ro`, + " extra_hosts:", + ' - "host.docker.internal:host-gateway"', + "", + ].join("\n"); +} + +function pathForCompose(path: string): string { + if (path.startsWith(repoRoot)) { + const relative = path.slice(repoRoot.length).replace(/^\/+/, ""); + return relative.length === 0 ? "." : `./${relative}`; + } + return path; +} + +function writeGeneratedFile(path: string, content: string, force: boolean): "created" | "updated" | "unchanged" | "skipped" { + if (existsSync(path)) { + const existing = readFileSync(path, "utf8"); + if (existing === content) return "unchanged"; + if (!force) return "skipped"; + writeFileSync(path, content, "utf8"); + return "updated"; + } + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, content, "utf8"); + return "created"; +} + +function inspectAttachedContainer(options: ProviderAttachOptions): ProviderAttachContainerValidation { + const containerName = `unidesk-provider-gateway-${safeProviderSlug(options.providerId)}`; + const command = [ + "docker", + "inspect", + "--format", + "{{.HostConfig.RestartPolicy.Name}}\t{{.HostConfig.PidMode}}\t{{.State.Status}}", + containerName, + ]; + const result = runCommand(command, repoRoot); + const [restartPolicy = "", pidMode = "", state = ""] = result.stdout.trim().split("\t"); + const restartPolicyOk = restartPolicy === "always"; + const pidModeOk = pidMode === "host"; + return { + ok: result.exitCode === 0 && restartPolicyOk && pidModeOk && state === "running", + containerName, + restartPolicy, + restartPolicyOk, + pidMode, + pidModeOk, + state, + command, + exitCode: result.exitCode, + stderr: result.stderr.trim(), + }; +} + +export async function runProviderCommand(config: UniDeskConfig, args: string[]): Promise { + const [sub] = args; + if (sub !== "attach") { + throw new Error("provider requires subcommand: attach"); + } + const options = parseAttachOptions(config, args.slice(1)); + mkdirSync(options.logDir, { recursive: true }); + mkdirSync(options.hostSshKeyDir, { recursive: true, mode: 0o700 }); + const envStatus = writeGeneratedFile(options.envFile, envContent(options), options.force); + const composeStatus = writeGeneratedFile(options.composeFile, composeContent(options), options.force); + const composeCommand = [ + "docker", + "compose", + "--env-file", + options.envFile, + "-f", + options.composeFile, + "-p", + `unidesk-${safeProviderSlug(options.providerId)}`, + "up", + "-d", + "--build", + "--remove-orphans", + ]; + const result = options.up ? runCommand(composeCommand, repoRoot) : null; + const containerValidation = options.up ? inspectAttachedContainer(options) : null; + return { + providerId: options.providerId, + masterServer: options.masterServer, + envFile: options.envFile, + envFileStatus: envStatus, + composeFile: options.composeFile, + composeFileStatus: composeStatus, + logDir: options.logDir, + hostSshKeyDir: options.hostSshKeyDir, + hostSshKeyPath: join(options.hostSshKeyDir, "id_ed25519"), + startCommand: composeCommand, + started: options.up, + result, + containerValidation, + notes: [ + "The generated env file keeps only UNIDESK_MASTER_SERVER and PROVIDER_ID unless --provider-token is supplied.", + "Place a maintenance SSH private key at hostSshKeyPath if this provider should expose host.ssh.", + "When --up is used, containerValidation must show restartPolicy=always and pidMode=host before the node is accepted as durable across Docker daemon restarts.", + envStatus === "skipped" || composeStatus === "skipped" ? "Existing generated files differed and were not overwritten; rerun with --force to replace them." : "Generated files are ready.", + ], + }; +} diff --git a/scripts/src/remote.ts b/scripts/src/remote.ts index 677e7900..10f3ed57 100644 --- a/scripts/src/remote.ts +++ b/scripts/src/remote.ts @@ -3,7 +3,7 @@ import { type UniDeskConfig } from "./config"; import { type DebugDispatchCommand, isDebugDispatchCommand } from "./debug"; import { summarizeMicroserviceProxyResponse } from "./microservices"; import { isSshSkillDiscoveryArgs, parseSshArgs } from "./ssh"; -import { codexOutputQueryAsync, codexTaskQueryAsync } from "./codex-queue"; +import { codexOutputQueryAsync, codexTaskQueryAsync } from "./code-queue"; export interface RemoteCliOptions { host: string | null; @@ -468,7 +468,7 @@ async function remoteMicroservice(session: FrontendSession, args: string[]): Pro throw new Error("remote microservice command must be: microservice list | status | health | proxy "); } -async function remoteCodexQueue(session: FrontendSession, args: string[]): Promise { +async function remoteCodeQueue(session: FrontendSession, args: string[]): Promise { const action = args[1] ?? "task"; if (action !== "task" && action !== "summary" && action !== "show" && action !== "output") { throw new Error("remote codex command must be: codex task or codex output "); @@ -559,7 +559,7 @@ async function runRemoteCliOverFrontend(options: RemoteCliOptions, config: UniDe return 0; } if (top === "codex") { - emitRemoteJson(name, await remoteCodexQueue(session, args)); + emitRemoteJson(name, await remoteCodeQueue(session, args)); return 0; } if (top === "ssh") { diff --git a/src/components/backend-core/src/index.ts b/src/components/backend-core/src/index.ts index 5c82cf4c..5e35e2f7 100644 --- a/src/components/backend-core/src/index.ts +++ b/src/components/backend-core/src/index.ts @@ -1,7 +1,6 @@ -import { appendFileSync, mkdirSync } from "node:fs"; -import { dirname } from "node:path"; import type { Server, ServerWebSocket } from "bun"; import postgres from "postgres"; +import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../shared/src/rotating-jsonl"; import { type ApiEvent, type ApiNode, @@ -145,6 +144,21 @@ const microserviceProxyRefreshes = new Map>(); let lastTaskSweepAt = 0; let taskSweepInFlight: Promise | null = null; const microserviceProxyMaxBodyTextLength = 8 * 1024 * 1024; +const microserviceForwardRequestHeaders = [ + "accept", + "content-type", + "range", + "x-auth", + "x-requested-with", + "destination", + "overwrite", + "tus-resumable", + "upload-concat", + "upload-defer-length", + "upload-length", + "upload-metadata", + "upload-offset", +] as const; function requiredEnv(name: string): string { const value = process.env[name]; @@ -272,7 +286,12 @@ function readConfig(): RuntimeConfig { } function createLogger(service: string, logFile: string) { - mkdirSync(dirname(logFile), { recursive: true }); + const writer = createHourlyJsonlWriter({ + baseLogFile: logFile, + service, + maxBytes: logRetentionBytesForService(service), + }); + writer.prune(); return (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue): void => { const entry = data === undefined ? { ts: new Date().toISOString(), service, level, message } @@ -281,7 +300,7 @@ function createLogger(service: string, logFile: string) { while (recentLogs.length > 500) recentLogs.shift(); const line = `${JSON.stringify(entry)}\n`; try { - appendFileSync(logFile, line, "utf8"); + writer.appendLine(line, new Date(entry.ts)); } catch (error) { console.error(JSON.stringify({ ts: new Date().toISOString(), service, level: "error", message: "log_write_failed", data: String(error) })); } @@ -1515,6 +1534,24 @@ function contentTypeIsJson(contentType: string): boolean { return contentType.toLowerCase().includes("json"); } +function readMicroserviceRequestHeaders(req: Request): Record { + const requestHeaders: Record = {}; + for (const name of microserviceForwardRequestHeaders) { + const value = req.headers.get(name); + if (value !== null && value.length > 0) requestHeaders[name] = value.slice(0, 4096); + } + return requestHeaders; +} + +function headersFromMicroserviceRequest(requestHeaders: Record): Headers { + const headers = new Headers(); + for (const name of microserviceForwardRequestHeaders) { + const value = requestHeaders[name]; + if (typeof value === "string" && value.length > 0) headers.set(name, value); + } + return headers; +} + function boundedMicroserviceBodyText( bodyText: string, contentType: string, @@ -1620,7 +1657,7 @@ function microserviceCacheTtlMs(serviceId: string, targetPath: string): number { if (serviceId === "findjob" && (targetPath === "/api/summary" || targetPath === "/api/jobs" || targetPath === "/api/drafts")) return 8_000; if (serviceId === "met-nonlinear" && (targetPath === "/api/images" || targetPath === "/api/projects")) return 15_000; if (serviceId === "met-nonlinear" && (targetPath === "/api/queue" || targetPath === "/api/summary" || targetPath === "/api/history")) return 5_000; - if (serviceId === "codex-queue" && targetPath.includes("/transcript")) return 1_000; + if (serviceId === "code-queue" && targetPath.includes("/transcript")) return 1_000; return 750; } @@ -1728,9 +1765,7 @@ async function directMicroserviceResponse( const baseUrl = new URL(service.backend.nodeBaseUrl); const upstreamUrl = new URL(targetPath, baseUrl); upstreamUrl.search = proxyOptions.query; - const headers = new Headers(); - const contentType = typeof requestHeaders["content-type"] === "string" ? requestHeaders["content-type"] : ""; - if (contentType.length > 0) headers.set("content-type", contentType); + const headers = headersFromMicroserviceRequest(requestHeaders); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), Math.max(1000, service.backend.timeoutMs)); try { @@ -1857,9 +1892,7 @@ async function microserviceRoute(req: Request, url: URL): Promise { if (bodyText.length > 1024 * 1024) { return jsonResponse({ ok: false, error: "microservice request body is too large", maxBytes: 1024 * 1024 }, 413); } - const requestHeaders: Record = {}; - const contentType = req.headers.get("content-type"); - if (contentType !== null) requestHeaders["content-type"] = contentType.slice(0, 200); + const requestHeaders = readMicroserviceRequestHeaders(req); if (method === "GET" || method === "HEAD") { const stale = readStaleMicroserviceCache(cacheKey); if (stale !== null) { @@ -1920,7 +1953,7 @@ function recordFromJson(value: JsonValue | null): Record { async function runHostSshPerfCommand(providerId: string, command: string, timeoutMs = 18_000): Promise<{ ok: boolean; taskId: string; task: RawTaskRow | null; stdout: string; stderr: string; exitCode: number | null; timedOut: boolean }> { const { taskId, providerOnline } = await createAndSendTask(providerId, "host.ssh", { - source: "codex-queue-performance-panel", + source: "code-queue-performance-panel", mode: "exec", cwd: "/root/unidesk", timeoutMs: 15_000, @@ -1974,7 +2007,7 @@ function parsePerfJson(body: string): Record | null { async function codexQueueLoadTest(req: Request): Promise { const body = req.method === "POST" ? (await req.json().catch(() => ({}))) as Record : {}; - const codexService = microserviceById("codex-queue"); + const codexService = microserviceById("code-queue"); const providerId = typeof body.providerId === "string" && body.providerId.length > 0 ? body.providerId : codexService?.providerId ?? "main-server"; @@ -1985,7 +2018,7 @@ async function codexQueueLoadTest(req: Request): Promise { const timeoutMs = numberFromUnknown(body.timeoutMs, 90_000, 5_000, 180_000); const targetMs = numberFromUnknown(body.targetMs, 1_000, 100, 60_000); const runId = safePerfRunId(); - const dir = ".state/codex-queue-perf"; + const dir = ".state/code-queue-perf"; const browsersPath = ".state/playwright-browsers"; const outputPath = `${dir}/${runId}.json`; const stderrPath = `${dir}/${runId}.stderr`; @@ -1999,7 +2032,7 @@ async function codexQueueLoadTest(req: Request): Promise { const startCommand = [ `mkdir -p ${shellQuote(dir)}`, `rm -f ${shellQuote(outputPath)} ${shellQuote(stderrPath)} ${shellQuote(exitPath)}`, - `(${playwrightSetup}; PLAYWRIGHT_BROWSERS_PATH=${shellQuote(browsersPath)} bun scripts/src/codex-queue-perf.ts --json --timeout-ms ${timeoutMs} --target-ms ${targetMs}${urlArg} > ${shellQuote(outputPath)}; printf '%s' "$?" > ${shellQuote(exitPath)}) > ${shellQuote(stderrPath)} 2>&1 & printf '%s\\n' ${shellQuote(runId)}`, + `(${playwrightSetup}; PLAYWRIGHT_BROWSERS_PATH=${shellQuote(browsersPath)} bun scripts/src/code-queue-perf.ts --json --timeout-ms ${timeoutMs} --target-ms ${targetMs}${urlArg} > ${shellQuote(outputPath)}; printf '%s' "$?" > ${shellQuote(exitPath)}) > ${shellQuote(stderrPath)} 2>&1 & printf '%s\\n' ${shellQuote(runId)}`, ].join("; "); const startedAt = Date.now(); const start = await runHostSshPerfCommand(providerId, startCommand); @@ -2058,7 +2091,7 @@ async function codexQueueLoadTest(req: Request): Promise { runId, stage: "timeout", elapsedMs: Date.now() - startedAt, - error: `Codex Queue Playwright benchmark did not finish within ${timeoutMs}ms`, + error: `Code Queue Playwright benchmark did not finish within ${timeoutMs}ms`, latestTaskId: latestPoll?.taskId ?? start.taskId, latestTask: rawTaskJson(latestPoll?.task ?? start.task), }, 200); @@ -2197,7 +2230,7 @@ async function routeInner(req: Request, server: Server): Promise getMicroservices()) }); if (url.pathname === "/api/performance") return jsonResponse(await getPerformance()); - if (url.pathname === "/api/codex-queue-load-test" && (req.method === "GET" || req.method === "POST")) return withPerformanceOperation("performance", "codex_queue_load_test", url.pathname, () => codexQueueLoadTest(req)); + if (url.pathname === "/api/code-queue-load-test" && (req.method === "GET" || req.method === "POST")) return withPerformanceOperation("performance", "code_queue_load_test", url.pathname, () => codexQueueLoadTest(req)); if (url.pathname.startsWith("/api/microservices/")) return withPerformanceOperation("microservices", "route", url.pathname, () => microserviceRoute(req, url)); if (url.pathname === "/api/dispatch" && req.method === "POST") return withPerformanceOperation("scheduler", "dispatch", url.pathname, () => dispatchTask(req)); if (url.pathname === "/logs") return jsonResponse({ ok: true, logs: recentLogs.slice(-readLimit(url, 100)) }); diff --git a/src/components/frontend/Dockerfile b/src/components/frontend/Dockerfile index 88823d18..910d4608 100644 --- a/src/components/frontend/Dockerfile +++ b/src/components/frontend/Dockerfile @@ -2,6 +2,8 @@ FROM oven/bun:1-alpine WORKDIR /app/src/components/frontend COPY src/components/frontend/package.json ./package.json RUN bun install --production +COPY src/components/shared /app/src/components/shared +COPY docs /app/docs COPY src/components/frontend/src ./src COPY src/components/frontend/public ./public CMD ["bun", "run", "src/index.ts"] diff --git a/src/components/frontend/public/app.js b/src/components/frontend/public/app.js index ee7d3c56..cab60f53 100644 --- a/src/components/frontend/public/app.js +++ b/src/components/frontend/public/app.js @@ -1,86 +1,287 @@ -(()=>{var oE=Object.create;var{getPrototypeOf:aE,defineProperty:YQ,getOwnPropertyNames:dE}=Object;var eE=Object.prototype.hasOwnProperty;function fH(f){return this[f]}var uH,_H,Sf=(f,u,_)=>{var y=f!=null&&typeof f==="object";if(y){var l=u?uH??=new WeakMap:_H??=new WeakMap,$=l.get(f);if($)return $}_=f!=null?oE(aE(f)):{};let j=u||!f||!f.__esModule?YQ(_,"default",{value:f,enumerable:!0}):_;for(let J of dE(f))if(!eE.call(j,J))YQ(j,J,{get:fH.bind(f,J),enumerable:!0});if(y)l.set(f,j);return j};var Pu=(f,u)=>()=>(u||f((u={exports:{}}).exports,u),u.exports);var pf=((f)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(f,{get:(u,_)=>(typeof require<"u"?require:u)[_]}):f)(function(f){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+f+'" is not supported')});var vQ=Pu((bf)=>{var T3=Symbol.for("react.element"),yH=Symbol.for("react.portal"),lH=Symbol.for("react.fragment"),$H=Symbol.for("react.strict_mode"),jH=Symbol.for("react.profiler"),JH=Symbol.for("react.provider"),FH=Symbol.for("react.context"),QH=Symbol.for("react.forward_ref"),AH=Symbol.for("react.suspense"),UH=Symbol.for("react.memo"),WH=Symbol.for("react.lazy"),BQ=Symbol.iterator;function GH(f){if(f===null||typeof f!=="object")return null;return f=BQ&&f[BQ]||f["@@iterator"],typeof f==="function"?f:null}var TQ={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},rQ=Object.assign,MQ={};function ll(f,u,_){this.props=f,this.context=u,this.refs=MQ,this.updater=_||TQ}ll.prototype.isReactComponent={};ll.prototype.setState=function(f,u){if(typeof f!=="object"&&typeof f!=="function"&&f!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,f,u,"setState")};ll.prototype.forceUpdate=function(f){this.updater.enqueueForceUpdate(this,f,"forceUpdate")};function PQ(){}PQ.prototype=ll.prototype;function D2(f,u,_){this.props=f,this.context=u,this.refs=MQ,this.updater=_||TQ}var w2=D2.prototype=new PQ;w2.constructor=D2;rQ(w2,ll.prototype);w2.isPureReactComponent=!0;var DQ=Array.isArray,SQ=Object.prototype.hasOwnProperty,T2={current:null},CQ={key:!0,ref:!0,__self:!0,__source:!0};function RQ(f,u,_){var y,l={},$=null,j=null;if(u!=null)for(y in u.ref!==void 0&&(j=u.ref),u.key!==void 0&&($=""+u.key),u)SQ.call(u,y)&&!CQ.hasOwnProperty(y)&&(l[y]=u[y]);var J=arguments.length-2;if(J===1)l.children=_;else if(1{bQ.exports=vQ()});var tQ=Pu(($0)=>{function C2(f,u){var _=f.length;f.push(u);f:for(;0<_;){var y=_-1>>>1,l=f[y];if(0>>1;y<$;){var j=2*(y+1)-1,J=f[j],F=j+1,A=f[F];if(0>o6(J,_))Fo6(A,J)?(f[y]=A,f[F]=_,y=F):(f[y]=J,f[j]=_,y=j);else if(Fo6(A,_))f[y]=A,f[F]=_,y=F;else break f}}return u}function o6(f,u){var _=f.sortIndex-u.sortIndex;return _!==0?_:f.id-u.id}if(typeof performance==="object"&&typeof performance.now==="function")R2=performance,$0.unstable_now=function(){return R2.now()};else a6=Date,x2=a6.now(),$0.unstable_now=function(){return a6.now()-x2};var R2,a6,x2,Y1=[],U_=[],OH=1,tu=null,uu=3,u8=!1,Uy=!1,M3=!1,kQ=typeof setTimeout==="function"?setTimeout:null,mQ=typeof clearTimeout==="function"?clearTimeout:null,pQ=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function v2(f){for(var u=F1(U_);u!==null;){if(u.callback===null)f8(U_);else if(u.startTime<=f)f8(U_),u.sortIndex=u.expirationTime,C2(Y1,u);else break;u=F1(U_)}}function h2(f){if(M3=!1,v2(f),!Uy)if(F1(Y1)!==null)Uy=!0,c2(I2);else{var u=F1(U_);u!==null&&p2(h2,u.startTime-f)}}function I2(f,u){Uy=!1,M3&&(M3=!1,mQ(P3),P3=-1),u8=!0;var _=uu;try{v2(u);for(tu=F1(Y1);tu!==null&&(!(tu.expirationTime>u)||f&&!nQ());){var y=tu.callback;if(typeof y==="function"){tu.callback=null,uu=tu.priorityLevel;var l=y(tu.expirationTime<=u);u=$0.unstable_now(),typeof l==="function"?tu.callback=l:tu===F1(Y1)&&f8(Y1),v2(u)}else f8(Y1);tu=F1(Y1)}if(tu!==null)var $=!0;else{var j=F1(U_);j!==null&&p2(h2,j.startTime-u),$=!1}return $}finally{tu=null,uu=_,u8=!1}}var _8=!1,d6=null,P3=-1,iQ=5,gQ=-1;function nQ(){return $0.unstable_now()-gQf||125y?(f.sortIndex=_,C2(U_,f),F1(Y1)===null&&f===F1(U_)&&(M3?(mQ(P3),P3=-1):M3=!0,p2(h2,_-y))):(f.sortIndex=l,C2(Y1,f),Uy||u8||(Uy=!0,c2(I2))),f};$0.unstable_shouldYield=nQ;$0.unstable_wrapCallback=function(f){var u=uu;return function(){var _=uu;uu=u;try{return f.apply(this,arguments)}finally{uu=_}}}});var oQ=Pu((GP,sQ)=>{sQ.exports=tQ()});var dW=Pu((bu)=>{var XH=c0(),xu=oQ();function Ff(f){for(var u="https://reactjs.org/docs/error-decoder.html?invariant="+f,_=1;_"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),A9=Object.prototype.hasOwnProperty,NH=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,aQ={},dQ={};function LH(f){if(A9.call(dQ,f))return!0;if(A9.call(aQ,f))return!1;if(NH.test(f))return dQ[f]=!0;return aQ[f]=!0,!1}function YH(f,u,_,y){if(_!==null&&_.type===0)return!1;switch(typeof u){case"function":case"symbol":return!0;case"boolean":if(y)return!1;if(_!==null)return!_.acceptsBooleans;return f=f.toLowerCase().slice(0,5),f!=="data-"&&f!=="aria-";default:return!1}}function BH(f,u,_,y){if(u===null||typeof u>"u"||YH(f,u,_,y))return!0;if(y)return!1;if(_!==null)switch(_.type){case 3:return!u;case 4:return u===!1;case 5:return isNaN(u);case 6:return isNaN(u)||1>u}return!1}function Ku(f,u,_,y,l,$,j){this.acceptsBooleans=u===2||u===3||u===4,this.attributeName=y,this.attributeNamespace=l,this.mustUseProperty=_,this.propertyName=f,this.type=u,this.sanitizeURL=$,this.removeEmptyString=j}var t0={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(f){t0[f]=new Ku(f,0,!1,f,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(f){var u=f[0];t0[u]=new Ku(u,1,!1,f[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(f){t0[f]=new Ku(f,2,!1,f.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(f){t0[f]=new Ku(f,2,!1,f,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(f){t0[f]=new Ku(f,3,!1,f.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(f){t0[f]=new Ku(f,3,!0,f,null,!1,!1)});["capture","download"].forEach(function(f){t0[f]=new Ku(f,4,!1,f,null,!1,!1)});["cols","rows","size","span"].forEach(function(f){t0[f]=new Ku(f,6,!1,f,null,!1,!1)});["rowSpan","start"].forEach(function(f){t0[f]=new Ku(f,5,!1,f.toLowerCase(),null,!1,!1)});var l7=/[\-:]([a-z])/g;function $7(f){return f[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(f){var u=f.replace(l7,$7);t0[u]=new Ku(u,1,!1,f,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(f){var u=f.replace(l7,$7);t0[u]=new Ku(u,1,!1,f,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(f){var u=f.replace(l7,$7);t0[u]=new Ku(u,1,!1,f,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(f){t0[f]=new Ku(f,1,!1,f.toLowerCase(),null,!1,!1)});t0.xlinkHref=new Ku("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(f){t0[f]=new Ku(f,1,!1,f.toLowerCase(),null,!0,!0)});function j7(f,u,_,y){var l=t0.hasOwnProperty(u)?t0[u]:null;if(l!==null?l.type!==0:y||!(2J||l[j]!==$[J]){var F=` -`+l[j].replace(" at new "," at ");return f.displayName&&F.includes("")&&(F=F.replace("",f.displayName)),F}while(1<=j&&0<=J);break}}}finally{m2=!1,Error.prepareStackTrace=_}return(f=f?f.displayName||f.name:"")?I3(f):""}function DH(f){switch(f.tag){case 5:return I3(f.type);case 16:return I3("Lazy");case 13:return I3("Suspense");case 19:return I3("SuspenseList");case 0:case 2:case 15:return f=i2(f.type,!1),f;case 11:return f=i2(f.type.render,!1),f;case 1:return f=i2(f.type,!0),f;default:return""}}function z9(f){if(f==null)return null;if(typeof f==="function")return f.displayName||f.name||null;if(typeof f==="string")return f;switch(f){case Fl:return"Fragment";case Jl:return"Portal";case U9:return"Profiler";case J7:return"StrictMode";case W9:return"Suspense";case G9:return"SuspenseList"}if(typeof f==="object")switch(f.$$typeof){case yU:return(f.displayName||"Context")+".Consumer";case _U:return(f._context.displayName||"Context")+".Provider";case F7:var u=f.render;return f=f.displayName,f||(f=u.displayName||u.name||"",f=f!==""?"ForwardRef("+f+")":"ForwardRef"),f;case Q7:return u=f.displayName||null,u!==null?u:z9(f.type)||"Memo";case G_:u=f._payload,f=f._init;try{return z9(f(u))}catch(_){}}return null}function wH(f){var u=f.type;switch(f.tag){case 24:return"Cache";case 9:return(u.displayName||"Context")+".Consumer";case 10:return(u._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return f=u.render,f=f.displayName||f.name||"",u.displayName||(f!==""?"ForwardRef("+f+")":"ForwardRef");case 7:return"Fragment";case 5:return u;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return z9(u);case 8:return u===J7?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof u==="function")return u.displayName||u.name||null;if(typeof u==="string")return u}return null}function D_(f){switch(typeof f){case"boolean":case"number":case"string":case"undefined":return f;case"object":return f;default:return""}}function $U(f){var u=f.type;return(f=f.nodeName)&&f.toLowerCase()==="input"&&(u==="checkbox"||u==="radio")}function TH(f){var u=$U(f)?"checked":"value",_=Object.getOwnPropertyDescriptor(f.constructor.prototype,u),y=""+f[u];if(!f.hasOwnProperty(u)&&typeof _<"u"&&typeof _.get==="function"&&typeof _.set==="function"){var{get:l,set:$}=_;return Object.defineProperty(f,u,{configurable:!0,get:function(){return l.call(this)},set:function(j){y=""+j,$.call(this,j)}}),Object.defineProperty(f,u,{enumerable:_.enumerable}),{getValue:function(){return y},setValue:function(j){y=""+j},stopTracking:function(){f._valueTracker=null,delete f[u]}}}}function l8(f){f._valueTracker||(f._valueTracker=TH(f))}function jU(f){if(!f)return!1;var u=f._valueTracker;if(!u)return!0;var _=u.getValue(),y="";return f&&(y=$U(f)?f.checked?"true":"false":f.value),f=y,f!==_?(u.setValue(f),!0):!1}function T8(f){if(f=f||(typeof document<"u"?document:void 0),typeof f>"u")return null;try{return f.activeElement||f.body}catch(u){return f.body}}function K9(f,u){var _=u.checked;return E0({},u,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:_!=null?_:f._wrapperState.initialChecked})}function fA(f,u){var _=u.defaultValue==null?"":u.defaultValue,y=u.checked!=null?u.checked:u.defaultChecked;_=D_(u.value!=null?u.value:_),f._wrapperState={initialChecked:y,initialValue:_,controlled:u.type==="checkbox"||u.type==="radio"?u.checked!=null:u.value!=null}}function JU(f,u){u=u.checked,u!=null&&j7(f,"checked",u,!1)}function Z9(f,u){JU(f,u);var _=D_(u.value),y=u.type;if(_!=null)if(y==="number"){if(_===0&&f.value===""||f.value!=_)f.value=""+_}else f.value!==""+_&&(f.value=""+_);else if(y==="submit"||y==="reset"){f.removeAttribute("value");return}u.hasOwnProperty("value")?q9(f,u.type,_):u.hasOwnProperty("defaultValue")&&q9(f,u.type,D_(u.defaultValue)),u.checked==null&&u.defaultChecked!=null&&(f.defaultChecked=!!u.defaultChecked)}function uA(f,u,_){if(u.hasOwnProperty("value")||u.hasOwnProperty("defaultValue")){var y=u.type;if(!(y!=="submit"&&y!=="reset"||u.value!==void 0&&u.value!==null))return;u=""+f._wrapperState.initialValue,_||u===f.value||(f.value=u),f.defaultValue=u}_=f.name,_!==""&&(f.name=""),f.defaultChecked=!!f._wrapperState.initialChecked,_!==""&&(f.name=_)}function q9(f,u,_){if(u!=="number"||T8(f.ownerDocument)!==f)_==null?f.defaultValue=""+f._wrapperState.initialValue:f.defaultValue!==""+_&&(f.defaultValue=""+_)}var c3=Array.isArray;function Hl(f,u,_,y){if(f=f.options,u){u={};for(var l=0;l<_.length;l++)u["$"+_[l]]=!0;for(_=0;_"+u.valueOf().toString()+"";for(u=$8.firstChild;f.firstChild;)f.removeChild(f.firstChild);for(;u.firstChild;)f.appendChild(u.firstChild)}});function l$(f,u){if(u){var _=f.firstChild;if(_&&_===f.lastChild&&_.nodeType===3){_.nodeValue=u;return}}f.textContent=u}var t3={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},rH=["Webkit","ms","Moz","O"];Object.keys(t3).forEach(function(f){rH.forEach(function(u){u=u+f.charAt(0).toUpperCase()+f.substring(1),t3[u]=t3[f]})});function UU(f,u,_){return u==null||typeof u==="boolean"||u===""?"":_||typeof u!=="number"||u===0||t3.hasOwnProperty(f)&&t3[f]?(""+u).trim():u+"px"}function WU(f,u){f=f.style;for(var _ in u)if(u.hasOwnProperty(_)){var y=_.indexOf("--")===0,l=UU(_,u[_],y);_==="float"&&(_="cssFloat"),y?f.setProperty(_,l):f[_]=l}}var MH=E0({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function V9(f,u){if(u){if(MH[f]&&(u.children!=null||u.dangerouslySetInnerHTML!=null))throw Error(Ff(137,f));if(u.dangerouslySetInnerHTML!=null){if(u.children!=null)throw Error(Ff(60));if(typeof u.dangerouslySetInnerHTML!=="object"||!("__html"in u.dangerouslySetInnerHTML))throw Error(Ff(61))}if(u.style!=null&&typeof u.style!=="object")throw Error(Ff(62))}}function O9(f,u){if(f.indexOf("-")===-1)return typeof u.is==="string";switch(f){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var X9=null;function A7(f){return f=f.target||f.srcElement||window,f.correspondingUseElement&&(f=f.correspondingUseElement),f.nodeType===3?f.parentNode:f}var N9=null,Vl=null,Ol=null;function lA(f){if(f=X$(f)){if(typeof N9!=="function")throw Error(Ff(280));var u=f.stateNode;u&&(u=y4(u),N9(f.stateNode,f.type,u))}}function GU(f){Vl?Ol?Ol.push(f):Ol=[f]:Vl=f}function zU(){if(Vl){var f=Vl,u=Ol;if(Ol=Vl=null,lA(f),u)for(f=0;f>>=0,f===0?32:31-(pH(f)/kH|0)|0}var j8=64,J8=4194304;function p3(f){switch(f&-f){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return f&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return f&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return f}}function S8(f,u){var _=f.pendingLanes;if(_===0)return 0;var y=0,l=f.suspendedLanes,$=f.pingedLanes,j=_&268435455;if(j!==0){var J=j&~l;J!==0?y=p3(J):($&=j,$!==0&&(y=p3($)))}else j=_&~l,j!==0?y=p3(j):$!==0&&(y=p3($));if(y===0)return 0;if(u!==0&&u!==y&&(u&l)===0&&(l=y&-y,$=u&-u,l>=$||l===16&&($&4194240)!==0))return u;if((y&4)!==0&&(y|=_&16),u=f.entangledLanes,u!==0)for(f=f.entanglements,u&=y;0_;_++)u.push(f);return u}function V$(f,u,_){f.pendingLanes|=u,u!==536870912&&(f.suspendedLanes=0,f.pingedLanes=0),f=f.eventTimes,u=31-G1(u),f[u]=_}function nH(f,u){var _=f.pendingLanes&~u;f.pendingLanes=u,f.suspendedLanes=0,f.pingedLanes=0,f.expiredLanes&=u,f.mutableReadLanes&=u,f.entangledLanes&=u,u=f.entanglements;var y=f.eventTimes;for(f=f.expirationTimes;0<_;){var l=31-G1(_),$=1<=o3),GA=String.fromCharCode(32),zA=!1;function CU(f,u){switch(f){case"keyup":return OV.indexOf(u.keyCode)!==-1;case"keydown":return u.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function RU(f){return f=f.detail,typeof f==="object"&&"data"in f?f.data:null}var Ql=!1;function NV(f,u){switch(f){case"compositionend":return RU(u);case"keypress":if(u.which!==32)return null;return zA=!0,GA;case"textInput":return f=u.data,f===GA&&zA?null:f;default:return null}}function LV(f,u){if(Ql)return f==="compositionend"||!E7&&CU(f,u)?(f=PU(),H8=K7=q_=null,Ql=!1,f):null;switch(f){case"paste":return null;case"keypress":if(!(u.ctrlKey||u.altKey||u.metaKey)||u.ctrlKey&&u.altKey){if(u.char&&1=u)return{node:_,offset:u-f};f=y}f:{for(;_;){if(_.nextSibling){_=_.nextSibling;break f}_=_.parentNode}_=void 0}_=qA(_)}}function hU(f,u){return f&&u?f===u?!0:f&&f.nodeType===3?!1:u&&u.nodeType===3?hU(f,u.parentNode):("contains"in f)?f.contains(u):f.compareDocumentPosition?!!(f.compareDocumentPosition(u)&16):!1:!1}function IU(){for(var f=window,u=T8();u instanceof f.HTMLIFrameElement;){try{var _=typeof u.contentWindow.location.href==="string"}catch(y){_=!1}if(_)f=u.contentWindow;else break;u=T8(f.document)}return u}function H7(f){var u=f&&f.nodeName&&f.nodeName.toLowerCase();return u&&(u==="input"&&(f.type==="text"||f.type==="search"||f.type==="tel"||f.type==="url"||f.type==="password")||u==="textarea"||f.contentEditable==="true")}function SV(f){var u=IU(),_=f.focusedElem,y=f.selectionRange;if(u!==_&&_&&_.ownerDocument&&hU(_.ownerDocument.documentElement,_)){if(y!==null&&H7(_)){if(u=y.start,f=y.end,f===void 0&&(f=u),"selectionStart"in _)_.selectionStart=u,_.selectionEnd=Math.min(f,_.value.length);else if(f=(u=_.ownerDocument||document)&&u.defaultView||window,f.getSelection){f=f.getSelection();var l=_.textContent.length,$=Math.min(y.start,l);y=y.end===void 0?$:Math.min(y.end,l),!f.extend&&$>y&&(l=y,y=$,$=l),l=EA(_,$);var j=EA(_,y);l&&j&&(f.rangeCount!==1||f.anchorNode!==l.node||f.anchorOffset!==l.offset||f.focusNode!==j.node||f.focusOffset!==j.offset)&&(u=u.createRange(),u.setStart(l.node,l.offset),f.removeAllRanges(),$>y?(f.addRange(u),f.extend(j.node,j.offset)):(u.setEnd(j.node,j.offset),f.addRange(u)))}}u=[];for(f=_;f=f.parentNode;)f.nodeType===1&&u.push({element:f,left:f.scrollLeft,top:f.scrollTop});typeof _.focus==="function"&&_.focus();for(_=0;_=document.documentMode,Al=null,T9=null,d3=null,r9=!1;function HA(f,u,_){var y=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;r9||Al==null||Al!==T8(y)||(y=Al,("selectionStart"in y)&&H7(y)?y={start:y.selectionStart,end:y.selectionEnd}:(y=(y.ownerDocument&&y.ownerDocument.defaultView||window).getSelection(),y={anchorNode:y.anchorNode,anchorOffset:y.anchorOffset,focusNode:y.focusNode,focusOffset:y.focusOffset}),d3&&A$(d3,y)||(d3=y,y=x8(T9,"onSelect"),0Gl||(f.current=b9[Gl],b9[Gl]=null,Gl--)}function j0(f,u){Gl++,b9[Gl]=f.current,f.current=u}var w_={},$u=r_(w_),Xu=r_(!1),Vy=w_;function Bl(f,u){var _=f.type.contextTypes;if(!_)return w_;var y=f.stateNode;if(y&&y.__reactInternalMemoizedUnmaskedChildContext===u)return y.__reactInternalMemoizedMaskedChildContext;var l={},$;for($ in _)l[$]=u[$];return y&&(f=f.stateNode,f.__reactInternalMemoizedUnmaskedChildContext=u,f.__reactInternalMemoizedMaskedChildContext=l),l}function Nu(f){return f=f.childContextTypes,f!==null&&f!==void 0}function b8(){U0(Xu),U0($u)}function BA(f,u,_){if($u.current!==w_)throw Error(Ff(168));j0($u,u),j0(Xu,_)}function sU(f,u,_){var y=f.stateNode;if(u=u.childContextTypes,typeof y.getChildContext!=="function")return _;y=y.getChildContext();for(var l in y)if(!(l in u))throw Error(Ff(108,wH(f)||"Unknown",l));return E0({},_,y)}function h8(f){return f=(f=f.stateNode)&&f.__reactInternalMemoizedMergedChildContext||w_,Vy=$u.current,j0($u,f),j0(Xu,Xu.current),!0}function DA(f,u,_){var y=f.stateNode;if(!y)throw Error(Ff(169));_?(f=sU(f,u,Vy),y.__reactInternalMemoizedMergedChildContext=f,U0(Xu),U0($u),j0($u,f)):U0(Xu),j0(Xu,_)}var p1=null,l4=!1,u9=!1;function oU(f){p1===null?p1=[f]:p1.push(f)}function kV(f){l4=!0,oU(f)}function M_(){if(!u9&&p1!==null){u9=!0;var f=0,u=f0;try{var _=p1;for(f0=1;f<_.length;f++){var y=_[f];do y=y(!0);while(y!==null)}p1=null,l4=!1}catch(l){throw p1!==null&&(p1=p1.slice(f+1)),OU(U7,M_),l}finally{f0=u,u9=!1}}return null}var zl=[],Kl=0,I8=null,c8=0,su=[],ou=0,Oy=null,k1=1,m1="";function Gy(f,u){zl[Kl++]=c8,zl[Kl++]=I8,I8=f,c8=u}function aU(f,u,_){su[ou++]=k1,su[ou++]=m1,su[ou++]=Oy,Oy=f;var y=k1;f=m1;var l=32-G1(y)-1;y&=~(1<>=j,l-=j,k1=1<<32-G1(u)+l|_<D?(x=N,N=null):x=N.sibling;var c=W(z,N,Z[D],V);if(c===null){N===null&&(N=x);break}f&&N&&c.alternate===null&&u(z,N),q=$(c,q,D),r===null?L=c:r.sibling=c,r=c,N=x}if(D===Z.length)return _(z,N),G0&&Gy(z,D),L;if(N===null){for(;DD?(x=N,N=null):x=N.sibling;var v=W(z,N,c.value,V);if(v===null){N===null&&(N=x);break}f&&N&&v.alternate===null&&u(z,N),q=$(v,q,D),r===null?L=v:r.sibling=v,r=v,N=x}if(c.done)return _(z,N),G0&&Gy(z,D),L;if(N===null){for(;!c.done;D++,c=Z.next())c=G(z,c.value,V),c!==null&&(q=$(c,q,D),r===null?L=c:r.sibling=c,r=c);return G0&&Gy(z,D),L}for(N=y(z,N);!c.done;D++,c=Z.next())c=K(N,z,D,c.value,V),c!==null&&(f&&c.alternate!==null&&N.delete(c.key===null?D:c.key),q=$(c,q,D),r===null?L=c:r.sibling=c,r=c);return f&&N.forEach(function(C){return u(z,C)}),G0&&Gy(z,D),L}function O(z,q,Z,V){if(typeof Z==="object"&&Z!==null&&Z.type===Fl&&Z.key===null&&(Z=Z.props.children),typeof Z==="object"&&Z!==null){switch(Z.$$typeof){case y8:f:{for(var L=Z.key,r=q;r!==null;){if(r.key===L){if(L=Z.type,L===Fl){if(r.tag===7){_(z,r.sibling),q=l(r,Z.props.children),q.return=z,z=q;break f}}else if(r.elementType===L||typeof L==="object"&&L!==null&&L.$$typeof===G_&&rA(L)===r.type){_(z,r.sibling),q=l(r,Z.props),q.ref=v3(z,r,Z),q.return=z,z=q;break f}_(z,r);break}else u(z,r);r=r.sibling}Z.type===Fl?(q=Hy(Z.props.children,z.mode,V,Z.key),q.return=z,z=q):(V=w8(Z.type,Z.key,Z.props,null,z.mode,V),V.ref=v3(z,q,Z),V.return=z,z=V)}return j(z);case Jl:f:{for(r=Z.key;q!==null;){if(q.key===r)if(q.tag===4&&q.stateNode.containerInfo===Z.containerInfo&&q.stateNode.implementation===Z.implementation){_(z,q.sibling),q=l(q,Z.children||[]),q.return=z,z=q;break f}else{_(z,q);break}else u(z,q);q=q.sibling}q=Q9(Z,z.mode,V),q.return=z,z=q}return j(z);case G_:return r=Z._init,O(z,q,r(Z._payload),V)}if(c3(Z))return E(z,q,Z,V);if(S3(Z))return H(z,q,Z,V);z8(z,Z)}return typeof Z==="string"&&Z!==""||typeof Z==="number"?(Z=""+Z,q!==null&&q.tag===6?(_(z,q.sibling),q=l(q,Z),q.return=z,z=q):(_(z,q),q=F9(Z,z.mode,V),q.return=z,z=q),j(z)):_(z,q)}return O}var wl=fW(!0),uW=fW(!1),p8=r_(null),k8=null,Zl=null,N7=null;function L7(){N7=Zl=k8=null}function Y7(f){var u=p8.current;U0(p8),f._currentValue=u}function c9(f,u,_){for(;f!==null;){var y=f.alternate;if((f.childLanes&u)!==u?(f.childLanes|=u,y!==null&&(y.childLanes|=u)):y!==null&&(y.childLanes&u)!==u&&(y.childLanes|=u),f===_)break;f=f.return}}function Nl(f,u){k8=f,N7=Zl=null,f=f.dependencies,f!==null&&f.firstContext!==null&&((f.lanes&u)!==0&&(Ou=!0),f.firstContext=null)}function eu(f){var u=f._currentValue;if(N7!==f)if(f={context:f,memoizedValue:u,next:null},Zl===null){if(k8===null)throw Error(Ff(308));Zl=f,k8.dependencies={lanes:0,firstContext:f}}else Zl=Zl.next=f;return u}var Zy=null;function B7(f){Zy===null?Zy=[f]:Zy.push(f)}function _W(f,u,_,y){var l=u.interleaved;return l===null?(_.next=_,B7(u)):(_.next=l.next,l.next=_),u.interleaved=_,t1(f,y)}function t1(f,u){f.lanes|=u;var _=f.alternate;_!==null&&(_.lanes|=u),_=f;for(f=f.return;f!==null;)f.childLanes|=u,_=f.alternate,_!==null&&(_.childLanes|=u),_=f,f=f.return;return _.tag===3?_.stateNode:null}var z_=!1;function D7(f){f.updateQueue={baseState:f.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function yW(f,u){f=f.updateQueue,u.updateQueue===f&&(u.updateQueue={baseState:f.baseState,firstBaseUpdate:f.firstBaseUpdate,lastBaseUpdate:f.lastBaseUpdate,shared:f.shared,effects:f.effects})}function i1(f,u){return{eventTime:f,lane:u,tag:0,payload:null,callback:null,next:null}}function N_(f,u,_){var y=f.updateQueue;if(y===null)return null;if(y=y.shared,(gf&2)!==0){var l=y.pending;return l===null?u.next=u:(u.next=l.next,l.next=u),y.pending=u,t1(f,_)}return l=y.interleaved,l===null?(u.next=u,B7(y)):(u.next=l.next,l.next=u),y.interleaved=u,t1(f,_)}function X8(f,u,_){if(u=u.updateQueue,u!==null&&(u=u.shared,(_&4194240)!==0)){var y=u.lanes;y&=f.pendingLanes,_|=y,u.lanes=_,W7(f,_)}}function MA(f,u){var{updateQueue:_,alternate:y}=f;if(y!==null&&(y=y.updateQueue,_===y)){var l=null,$=null;if(_=_.firstBaseUpdate,_!==null){do{var j={eventTime:_.eventTime,lane:_.lane,tag:_.tag,payload:_.payload,callback:_.callback,next:null};$===null?l=$=j:$=$.next=j,_=_.next}while(_!==null);$===null?l=$=u:$=$.next=u}else l=$=u;_={baseState:y.baseState,firstBaseUpdate:l,lastBaseUpdate:$,shared:y.shared,effects:y.effects},f.updateQueue=_;return}f=_.lastBaseUpdate,f===null?_.firstBaseUpdate=u:f.next=u,_.lastBaseUpdate=u}function m8(f,u,_,y){var l=f.updateQueue;z_=!1;var{firstBaseUpdate:$,lastBaseUpdate:j}=l,J=l.shared.pending;if(J!==null){l.shared.pending=null;var F=J,A=F.next;F.next=null,j===null?$=A:j.next=A,j=F;var U=f.alternate;U!==null&&(U=U.updateQueue,J=U.lastBaseUpdate,J!==j&&(J===null?U.firstBaseUpdate=A:J.next=A,U.lastBaseUpdate=F))}if($!==null){var G=l.baseState;j=0,U=A=F=null,J=$;do{var{lane:W,eventTime:K}=J;if((y&W)===W){U!==null&&(U=U.next={eventTime:K,lane:0,tag:J.tag,payload:J.payload,callback:J.callback,next:null});f:{var E=f,H=J;switch(W=u,K=_,H.tag){case 1:if(E=H.payload,typeof E==="function"){G=E.call(K,G,W);break f}G=E;break f;case 3:E.flags=E.flags&-65537|128;case 0:if(E=H.payload,W=typeof E==="function"?E.call(K,G,W):E,W===null||W===void 0)break f;G=E0({},G,W);break f;case 2:z_=!0}}J.callback!==null&&J.lane!==0&&(f.flags|=64,W=l.effects,W===null?l.effects=[J]:W.push(J))}else K={eventTime:K,lane:W,tag:J.tag,payload:J.payload,callback:J.callback,next:null},U===null?(A=U=K,F=G):U=U.next=K,j|=W;if(J=J.next,J===null)if(J=l.shared.pending,J===null)break;else W=J,J=W.next,W.next=null,l.lastBaseUpdate=W,l.shared.pending=null}while(1);if(U===null&&(F=G),l.baseState=F,l.firstBaseUpdate=A,l.lastBaseUpdate=U,u=l.shared.interleaved,u!==null){l=u;do j|=l.lane,l=l.next;while(l!==u)}else $===null&&(l.shared.lanes=0);Ny|=j,f.lanes=j,f.memoizedState=G}}function PA(f,u,_){if(f=u.effects,u.effects=null,f!==null)for(u=0;u_?_:4,f(!0);var y=y9.transition;y9.transition={};try{f(!1),u()}finally{f0=_,y9.transition=y}}function VW(){return f1().memoizedState}function nV(f,u,_){var y=Y_(f);if(_={lane:y,action:_,hasEagerState:!1,eagerState:null,next:null},OW(f))XW(u,_);else if(_=_W(f,u,_,y),_!==null){var l=zu();z1(_,f,y,l),NW(_,u,y)}}function tV(f,u,_){var y=Y_(f),l={lane:y,action:_,hasEagerState:!1,eagerState:null,next:null};if(OW(f))XW(u,l);else{var $=f.alternate;if(f.lanes===0&&($===null||$.lanes===0)&&($=u.lastRenderedReducer,$!==null))try{var j=u.lastRenderedState,J=$(j,_);if(l.hasEagerState=!0,l.eagerState=J,K1(J,j)){var F=u.interleaved;F===null?(l.next=l,B7(u)):(l.next=F.next,F.next=l),u.interleaved=l;return}}catch(A){}finally{}_=_W(f,u,l,y),_!==null&&(l=zu(),z1(_,f,y,l),NW(_,u,y))}}function OW(f){var u=f.alternate;return f===q0||u!==null&&u===q0}function XW(f,u){e3=g8=!0;var _=f.pending;_===null?u.next=u:(u.next=_.next,_.next=u),f.pending=u}function NW(f,u,_){if((_&4194240)!==0){var y=u.lanes;y&=f.pendingLanes,_|=y,u.lanes=_,W7(f,_)}}var n8={readContext:eu,useCallback:_u,useContext:_u,useEffect:_u,useImperativeHandle:_u,useInsertionEffect:_u,useLayoutEffect:_u,useMemo:_u,useReducer:_u,useRef:_u,useState:_u,useDebugValue:_u,useDeferredValue:_u,useTransition:_u,useMutableSource:_u,useSyncExternalStore:_u,useId:_u,unstable_isNewReconciler:!1},sV={readContext:eu,useCallback:function(f,u){return D1().memoizedState=[f,u===void 0?null:u],f},useContext:eu,useEffect:CA,useImperativeHandle:function(f,u,_){return _=_!==null&&_!==void 0?_.concat([f]):null,L8(4194308,4,KW.bind(null,u,f),_)},useLayoutEffect:function(f,u){return L8(4194308,4,f,u)},useInsertionEffect:function(f,u){return L8(4,2,f,u)},useMemo:function(f,u){var _=D1();return u=u===void 0?null:u,f=f(),_.memoizedState=[f,u],f},useReducer:function(f,u,_){var y=D1();return u=_!==void 0?_(u):u,y.memoizedState=y.baseState=u,f={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:f,lastRenderedState:u},y.queue=f,f=f.dispatch=nV.bind(null,q0,f),[y.memoizedState,f]},useRef:function(f){var u=D1();return f={current:f},u.memoizedState=f},useState:SA,useDebugValue:R7,useDeferredValue:function(f){return D1().memoizedState=f},useTransition:function(){var f=SA(!1),u=f[0];return f=gV.bind(null,f[1]),D1().memoizedState=f,[u,f]},useMutableSource:function(){},useSyncExternalStore:function(f,u,_){var y=q0,l=D1();if(G0){if(_===void 0)throw Error(Ff(407));_=_()}else{if(_=u(),k0===null)throw Error(Ff(349));(Xy&30)!==0||JW(y,u,_)}l.memoizedState=_;var $={value:_,getSnapshot:u};return l.queue=$,CA(QW.bind(null,y,$,f),[f]),y.flags|=2048,E$(9,FW.bind(null,y,$,_,u),void 0,null),_},useId:function(){var f=D1(),u=k0.identifierPrefix;if(G0){var _=m1,y=k1;_=(y&~(1<<32-G1(y)-1)).toString(32)+_,u=":"+u+"R"+_,_=Z$++,0<_&&(u+="H"+_.toString(32)),u+=":"}else _=iV++,u=":"+u+"r"+_.toString(32)+":";return f.memoizedState=u},unstable_isNewReconciler:!1},oV={readContext:eu,useCallback:qW,useContext:eu,useEffect:C7,useImperativeHandle:ZW,useInsertionEffect:GW,useLayoutEffect:zW,useMemo:EW,useReducer:l9,useRef:WW,useState:function(){return l9(q$)},useDebugValue:R7,useDeferredValue:function(f){var u=f1();return HW(u,S0.memoizedState,f)},useTransition:function(){var f=l9(q$)[0],u=f1().memoizedState;return[f,u]},useMutableSource:$W,useSyncExternalStore:jW,useId:VW,unstable_isNewReconciler:!1},aV={readContext:eu,useCallback:qW,useContext:eu,useEffect:C7,useImperativeHandle:ZW,useInsertionEffect:GW,useLayoutEffect:zW,useMemo:EW,useReducer:$9,useRef:WW,useState:function(){return $9(q$)},useDebugValue:R7,useDeferredValue:function(f){var u=f1();return S0===null?u.memoizedState=f:HW(u,S0.memoizedState,f)},useTransition:function(){var f=$9(q$)[0],u=f1().memoizedState;return[f,u]},useMutableSource:$W,useSyncExternalStore:jW,useId:VW,unstable_isNewReconciler:!1};function A1(f,u){if(f&&f.defaultProps){u=E0({},u),f=f.defaultProps;for(var _ in f)u[_]===void 0&&(u[_]=f[_]);return u}return u}function p9(f,u,_,y){u=f.memoizedState,_=_(y,u),_=_===null||_===void 0?u:E0({},u,_),f.memoizedState=_,f.lanes===0&&(f.updateQueue.baseState=_)}var j4={isMounted:function(f){return(f=f._reactInternals)?By(f)===f:!1},enqueueSetState:function(f,u,_){f=f._reactInternals;var y=zu(),l=Y_(f),$=i1(y,l);$.payload=u,_!==void 0&&_!==null&&($.callback=_),u=N_(f,$,l),u!==null&&(z1(u,f,l,y),X8(u,f,l))},enqueueReplaceState:function(f,u,_){f=f._reactInternals;var y=zu(),l=Y_(f),$=i1(y,l);$.tag=1,$.payload=u,_!==void 0&&_!==null&&($.callback=_),u=N_(f,$,l),u!==null&&(z1(u,f,l,y),X8(u,f,l))},enqueueForceUpdate:function(f,u){f=f._reactInternals;var _=zu(),y=Y_(f),l=i1(_,y);l.tag=2,u!==void 0&&u!==null&&(l.callback=u),u=N_(f,l,y),u!==null&&(z1(u,f,y,_),X8(u,f,y))}};function RA(f,u,_,y,l,$,j){return f=f.stateNode,typeof f.shouldComponentUpdate==="function"?f.shouldComponentUpdate(y,$,j):u.prototype&&u.prototype.isPureReactComponent?!A$(_,y)||!A$(l,$):!0}function LW(f,u,_){var y=!1,l=w_,$=u.contextType;return typeof $==="object"&&$!==null?$=eu($):(l=Nu(u)?Vy:$u.current,y=u.contextTypes,$=(y=y!==null&&y!==void 0)?Bl(f,l):w_),u=new u(_,$),f.memoizedState=u.state!==null&&u.state!==void 0?u.state:null,u.updater=j4,f.stateNode=u,u._reactInternals=f,y&&(f=f.stateNode,f.__reactInternalMemoizedUnmaskedChildContext=l,f.__reactInternalMemoizedMaskedChildContext=$),u}function xA(f,u,_,y){f=u.state,typeof u.componentWillReceiveProps==="function"&&u.componentWillReceiveProps(_,y),typeof u.UNSAFE_componentWillReceiveProps==="function"&&u.UNSAFE_componentWillReceiveProps(_,y),u.state!==f&&j4.enqueueReplaceState(u,u.state,null)}function k9(f,u,_,y){var l=f.stateNode;l.props=_,l.state=f.memoizedState,l.refs={},D7(f);var $=u.contextType;typeof $==="object"&&$!==null?l.context=eu($):($=Nu(u)?Vy:$u.current,l.context=Bl(f,$)),l.state=f.memoizedState,$=u.getDerivedStateFromProps,typeof $==="function"&&(p9(f,u,$,_),l.state=f.memoizedState),typeof u.getDerivedStateFromProps==="function"||typeof l.getSnapshotBeforeUpdate==="function"||typeof l.UNSAFE_componentWillMount!=="function"&&typeof l.componentWillMount!=="function"||(u=l.state,typeof l.componentWillMount==="function"&&l.componentWillMount(),typeof l.UNSAFE_componentWillMount==="function"&&l.UNSAFE_componentWillMount(),u!==l.state&&j4.enqueueReplaceState(l,l.state,null),m8(f,_,l,y),l.state=f.memoizedState),typeof l.componentDidMount==="function"&&(f.flags|=4194308)}function rl(f,u){try{var _="",y=u;do _+=DH(y),y=y.return;while(y);var l=_}catch($){l=` -Error generating stack: `+$.message+` -`+$.stack}return{value:f,source:u,stack:l,digest:null}}function j9(f,u,_){return{value:f,source:null,stack:_!=null?_:null,digest:u!=null?u:null}}function m9(f,u){try{console.error(u.value)}catch(_){setTimeout(function(){throw _})}}var dV=typeof WeakMap==="function"?WeakMap:Map;function YW(f,u,_){_=i1(-1,_),_.tag=3,_.payload={element:null};var y=u.value;return _.callback=function(){s8||(s8=!0,f7=y),m9(f,u)},_}function BW(f,u,_){_=i1(-1,_),_.tag=3;var y=f.type.getDerivedStateFromError;if(typeof y==="function"){var l=u.value;_.payload=function(){return y(l)},_.callback=function(){m9(f,u)}}var $=f.stateNode;return $!==null&&typeof $.componentDidCatch==="function"&&(_.callback=function(){m9(f,u),typeof y!=="function"&&(L_===null?L_=new Set([this]):L_.add(this));var j=u.stack;this.componentDidCatch(u.value,{componentStack:j!==null?j:""})}),_}function vA(f,u,_){var y=f.pingCache;if(y===null){y=f.pingCache=new dV;var l=new Set;y.set(u,l)}else l=y.get(u),l===void 0&&(l=new Set,y.set(u,l));l.has(_)||(l.add(_),f=WO.bind(null,f,u,_),u.then(f,f))}function bA(f){do{var u;if(u=f.tag===13)u=f.memoizedState,u=u!==null?u.dehydrated!==null?!0:!1:!0;if(u)return f;f=f.return}while(f!==null);return null}function hA(f,u,_,y,l){if((f.mode&1)===0)return f===u?f.flags|=65536:(f.flags|=128,_.flags|=131072,_.flags&=-52805,_.tag===1&&(_.alternate===null?_.tag=17:(u=i1(-1,1),u.tag=2,N_(_,u,1))),_.lanes|=1),f;return f.flags|=65536,f.lanes=l,f}var eV=o1.ReactCurrentOwner,Ou=!1;function Gu(f,u,_,y){u.child=f===null?uW(u,null,_,y):wl(u,f.child,_,y)}function IA(f,u,_,y,l){_=_.render;var $=u.ref;if(Nl(u,l),y=P7(f,u,_,y,$,l),_=S7(),f!==null&&!Ou)return u.updateQueue=f.updateQueue,u.flags&=-2053,f.lanes&=~l,s1(f,u,l);return G0&&_&&V7(u),u.flags|=1,Gu(f,u,y,l),u.child}function cA(f,u,_,y,l){if(f===null){var $=_.type;if(typeof $==="function"&&!k7($)&&$.defaultProps===void 0&&_.compare===null&&_.defaultProps===void 0)return u.tag=15,u.type=$,DW(f,u,$,y,l);return f=w8(_.type,null,y,u,u.mode,l),f.ref=u.ref,f.return=u,u.child=f}if($=f.child,(f.lanes&l)===0){var j=$.memoizedProps;if(_=_.compare,_=_!==null?_:A$,_(j,y)&&f.ref===u.ref)return s1(f,u,l)}return u.flags|=1,f=B_($,y),f.ref=u.ref,f.return=u,u.child=f}function DW(f,u,_,y,l){if(f!==null){var $=f.memoizedProps;if(A$($,y)&&f.ref===u.ref)if(Ou=!1,u.pendingProps=y=$,(f.lanes&l)!==0)(f.flags&131072)!==0&&(Ou=!0);else return u.lanes=f.lanes,s1(f,u,l)}return i9(f,u,_,y,l)}function wW(f,u,_){var y=u.pendingProps,l=y.children,$=f!==null?f.memoizedState:null;if(y.mode==="hidden")if((u.mode&1)===0)u.memoizedState={baseLanes:0,cachePool:null,transitions:null},j0(El,Su),Su|=_;else{if((_&1073741824)===0)return f=$!==null?$.baseLanes|_:_,u.lanes=u.childLanes=1073741824,u.memoizedState={baseLanes:f,cachePool:null,transitions:null},u.updateQueue=null,j0(El,Su),Su|=f,null;u.memoizedState={baseLanes:0,cachePool:null,transitions:null},y=$!==null?$.baseLanes:_,j0(El,Su),Su|=y}else $!==null?(y=$.baseLanes|_,u.memoizedState=null):y=_,j0(El,Su),Su|=y;return Gu(f,u,l,_),u.child}function TW(f,u){var _=u.ref;if(f===null&&_!==null||f!==null&&f.ref!==_)u.flags|=512,u.flags|=2097152}function i9(f,u,_,y,l){var $=Nu(_)?Vy:$u.current;if($=Bl(u,$),Nl(u,l),_=P7(f,u,_,y,$,l),y=S7(),f!==null&&!Ou)return u.updateQueue=f.updateQueue,u.flags&=-2053,f.lanes&=~l,s1(f,u,l);return G0&&y&&V7(u),u.flags|=1,Gu(f,u,_,l),u.child}function pA(f,u,_,y,l){if(Nu(_)){var $=!0;h8(u)}else $=!1;if(Nl(u,l),u.stateNode===null)Y8(f,u),LW(u,_,y),k9(u,_,y,l),y=!0;else if(f===null){var{stateNode:j,memoizedProps:J}=u;j.props=J;var F=j.context,A=_.contextType;typeof A==="object"&&A!==null?A=eu(A):(A=Nu(_)?Vy:$u.current,A=Bl(u,A));var U=_.getDerivedStateFromProps,G=typeof U==="function"||typeof j.getSnapshotBeforeUpdate==="function";G||typeof j.UNSAFE_componentWillReceiveProps!=="function"&&typeof j.componentWillReceiveProps!=="function"||(J!==y||F!==A)&&xA(u,j,y,A),z_=!1;var W=u.memoizedState;j.state=W,m8(u,y,j,l),F=u.memoizedState,J!==y||W!==F||Xu.current||z_?(typeof U==="function"&&(p9(u,_,U,y),F=u.memoizedState),(J=z_||RA(u,_,J,y,W,F,A))?(G||typeof j.UNSAFE_componentWillMount!=="function"&&typeof j.componentWillMount!=="function"||(typeof j.componentWillMount==="function"&&j.componentWillMount(),typeof j.UNSAFE_componentWillMount==="function"&&j.UNSAFE_componentWillMount()),typeof j.componentDidMount==="function"&&(u.flags|=4194308)):(typeof j.componentDidMount==="function"&&(u.flags|=4194308),u.memoizedProps=y,u.memoizedState=F),j.props=y,j.state=F,j.context=A,y=J):(typeof j.componentDidMount==="function"&&(u.flags|=4194308),y=!1)}else{j=u.stateNode,yW(f,u),J=u.memoizedProps,A=u.type===u.elementType?J:A1(u.type,J),j.props=A,G=u.pendingProps,W=j.context,F=_.contextType,typeof F==="object"&&F!==null?F=eu(F):(F=Nu(_)?Vy:$u.current,F=Bl(u,F));var K=_.getDerivedStateFromProps;(U=typeof K==="function"||typeof j.getSnapshotBeforeUpdate==="function")||typeof j.UNSAFE_componentWillReceiveProps!=="function"&&typeof j.componentWillReceiveProps!=="function"||(J!==G||W!==F)&&xA(u,j,y,F),z_=!1,W=u.memoizedState,j.state=W,m8(u,y,j,l);var E=u.memoizedState;J!==G||W!==E||Xu.current||z_?(typeof K==="function"&&(p9(u,_,K,y),E=u.memoizedState),(A=z_||RA(u,_,A,y,W,E,F)||!1)?(U||typeof j.UNSAFE_componentWillUpdate!=="function"&&typeof j.componentWillUpdate!=="function"||(typeof j.componentWillUpdate==="function"&&j.componentWillUpdate(y,E,F),typeof j.UNSAFE_componentWillUpdate==="function"&&j.UNSAFE_componentWillUpdate(y,E,F)),typeof j.componentDidUpdate==="function"&&(u.flags|=4),typeof j.getSnapshotBeforeUpdate==="function"&&(u.flags|=1024)):(typeof j.componentDidUpdate!=="function"||J===f.memoizedProps&&W===f.memoizedState||(u.flags|=4),typeof j.getSnapshotBeforeUpdate!=="function"||J===f.memoizedProps&&W===f.memoizedState||(u.flags|=1024),u.memoizedProps=y,u.memoizedState=E),j.props=y,j.state=E,j.context=F,y=A):(typeof j.componentDidUpdate!=="function"||J===f.memoizedProps&&W===f.memoizedState||(u.flags|=4),typeof j.getSnapshotBeforeUpdate!=="function"||J===f.memoizedProps&&W===f.memoizedState||(u.flags|=1024),y=!1)}return g9(f,u,_,y,$,l)}function g9(f,u,_,y,l,$){TW(f,u);var j=(u.flags&128)!==0;if(!y&&!j)return l&&DA(u,_,!1),s1(f,u,$);y=u.stateNode,eV.current=u;var J=j&&typeof _.getDerivedStateFromError!=="function"?null:y.render();return u.flags|=1,f!==null&&j?(u.child=wl(u,f.child,null,$),u.child=wl(u,null,J,$)):Gu(f,u,J,$),u.memoizedState=y.state,l&&DA(u,_,!0),u.child}function rW(f){var u=f.stateNode;u.pendingContext?BA(f,u.pendingContext,u.pendingContext!==u.context):u.context&&BA(f,u.context,!1),w7(f,u.containerInfo)}function kA(f,u,_,y,l){return Dl(),X7(l),u.flags|=256,Gu(f,u,_,y),u.child}var n9={dehydrated:null,treeContext:null,retryLane:0};function t9(f){return{baseLanes:f,cachePool:null,transitions:null}}function MW(f,u,_){var y=u.pendingProps,l=Z0.current,$=!1,j=(u.flags&128)!==0,J;if((J=j)||(J=f!==null&&f.memoizedState===null?!1:(l&2)!==0),J)$=!0,u.flags&=-129;else if(f===null||f.memoizedState!==null)l|=1;if(j0(Z0,l&1),f===null){if(I9(u),f=u.memoizedState,f!==null&&(f=f.dehydrated,f!==null))return(u.mode&1)===0?u.lanes=1:f.data==="$!"?u.lanes=8:u.lanes=1073741824,null;return j=y.children,f=y.fallback,$?(y=u.mode,$=u.child,j={mode:"hidden",children:j},(y&1)===0&&$!==null?($.childLanes=0,$.pendingProps=j):$=Q4(j,y,0,null),f=Hy(f,y,_,null),$.return=u,f.return=u,$.sibling=f,u.child=$,u.child.memoizedState=t9(_),u.memoizedState=n9,f):x7(u,j)}if(l=f.memoizedState,l!==null&&(J=l.dehydrated,J!==null))return fO(f,u,j,y,J,l,_);if($){$=y.fallback,j=u.mode,l=f.child,J=l.sibling;var F={mode:"hidden",children:y.children};return(j&1)===0&&u.child!==l?(y=u.child,y.childLanes=0,y.pendingProps=F,u.deletions=null):(y=B_(l,F),y.subtreeFlags=l.subtreeFlags&14680064),J!==null?$=B_(J,$):($=Hy($,j,_,null),$.flags|=2),$.return=u,y.return=u,y.sibling=$,u.child=y,y=$,$=u.child,j=f.child.memoizedState,j=j===null?t9(_):{baseLanes:j.baseLanes|_,cachePool:null,transitions:j.transitions},$.memoizedState=j,$.childLanes=f.childLanes&~_,u.memoizedState=n9,y}return $=f.child,f=$.sibling,y=B_($,{mode:"visible",children:y.children}),(u.mode&1)===0&&(y.lanes=_),y.return=u,y.sibling=null,f!==null&&(_=u.deletions,_===null?(u.deletions=[f],u.flags|=16):_.push(f)),u.child=y,u.memoizedState=null,y}function x7(f,u){return u=Q4({mode:"visible",children:u},f.mode,0,null),u.return=f,f.child=u}function K8(f,u,_,y){return y!==null&&X7(y),wl(u,f.child,null,_),f=x7(u,u.pendingProps.children),f.flags|=2,u.memoizedState=null,f}function fO(f,u,_,y,l,$,j){if(_){if(u.flags&256)return u.flags&=-257,y=j9(Error(Ff(422))),K8(f,u,j,y);if(u.memoizedState!==null)return u.child=f.child,u.flags|=128,null;return $=y.fallback,l=u.mode,y=Q4({mode:"visible",children:y.children},l,0,null),$=Hy($,l,j,null),$.flags|=2,y.return=u,$.return=u,y.sibling=$,u.child=y,(u.mode&1)!==0&&wl(u,f.child,null,j),u.child.memoizedState=t9(j),u.memoizedState=n9,$}if((u.mode&1)===0)return K8(f,u,j,null);if(l.data==="$!"){if(y=l.nextSibling&&l.nextSibling.dataset,y)var J=y.dgst;return y=J,$=Error(Ff(419)),y=j9($,y,void 0),K8(f,u,j,y)}if(J=(j&f.childLanes)!==0,Ou||J){if(y=k0,y!==null){switch(j&-j){case 4:l=2;break;case 16:l=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:l=32;break;case 536870912:l=268435456;break;default:l=0}l=(l&(y.suspendedLanes|j))!==0?0:l,l!==0&&l!==$.retryLane&&($.retryLane=l,t1(f,l),z1(y,f,l,-1))}return p7(),y=j9(Error(Ff(421))),K8(f,u,j,y)}if(l.data==="$?")return u.flags|=128,u.child=f.child,u=GO.bind(null,f),l._reactRetry=u,null;return f=$.treeContext,Cu=X_(l.nextSibling),Ru=u,G0=!0,W1=null,f!==null&&(su[ou++]=k1,su[ou++]=m1,su[ou++]=Oy,k1=f.id,m1=f.overflow,Oy=u),u=x7(u,y.children),u.flags|=4096,u}function mA(f,u,_){f.lanes|=u;var y=f.alternate;y!==null&&(y.lanes|=u),c9(f.return,u,_)}function J9(f,u,_,y,l){var $=f.memoizedState;$===null?f.memoizedState={isBackwards:u,rendering:null,renderingStartTime:0,last:y,tail:_,tailMode:l}:($.isBackwards=u,$.rendering=null,$.renderingStartTime=0,$.last=y,$.tail=_,$.tailMode=l)}function PW(f,u,_){var y=u.pendingProps,l=y.revealOrder,$=y.tail;if(Gu(f,u,y.children,_),y=Z0.current,(y&2)!==0)y=y&1|2,u.flags|=128;else{if(f!==null&&(f.flags&128)!==0)f:for(f=u.child;f!==null;){if(f.tag===13)f.memoizedState!==null&&mA(f,_,u);else if(f.tag===19)mA(f,_,u);else if(f.child!==null){f.child.return=f,f=f.child;continue}if(f===u)break f;for(;f.sibling===null;){if(f.return===null||f.return===u)break f;f=f.return}f.sibling.return=f.return,f=f.sibling}y&=1}if(j0(Z0,y),(u.mode&1)===0)u.memoizedState=null;else switch(l){case"forwards":_=u.child;for(l=null;_!==null;)f=_.alternate,f!==null&&i8(f)===null&&(l=_),_=_.sibling;_=l,_===null?(l=u.child,u.child=null):(l=_.sibling,_.sibling=null),J9(u,!1,l,_,$);break;case"backwards":_=null,l=u.child;for(u.child=null;l!==null;){if(f=l.alternate,f!==null&&i8(f)===null){u.child=l;break}f=l.sibling,l.sibling=_,_=l,l=f}J9(u,!0,_,null,$);break;case"together":J9(u,!1,null,null,void 0);break;default:u.memoizedState=null}return u.child}function Y8(f,u){(u.mode&1)===0&&f!==null&&(f.alternate=null,u.alternate=null,u.flags|=2)}function s1(f,u,_){if(f!==null&&(u.dependencies=f.dependencies),Ny|=u.lanes,(_&u.childLanes)===0)return null;if(f!==null&&u.child!==f.child)throw Error(Ff(153));if(u.child!==null){f=u.child,_=B_(f,f.pendingProps),u.child=_;for(_.return=u;f.sibling!==null;)f=f.sibling,_=_.sibling=B_(f,f.pendingProps),_.return=u;_.sibling=null}return u.child}function uO(f,u,_){switch(u.tag){case 3:rW(u),Dl();break;case 5:lW(u);break;case 1:Nu(u.type)&&h8(u);break;case 4:w7(u,u.stateNode.containerInfo);break;case 10:var y=u.type._context,l=u.memoizedProps.value;j0(p8,y._currentValue),y._currentValue=l;break;case 13:if(y=u.memoizedState,y!==null){if(y.dehydrated!==null)return j0(Z0,Z0.current&1),u.flags|=128,null;if((_&u.child.childLanes)!==0)return MW(f,u,_);return j0(Z0,Z0.current&1),f=s1(f,u,_),f!==null?f.sibling:null}j0(Z0,Z0.current&1);break;case 19:if(y=(_&u.childLanes)!==0,(f.flags&128)!==0){if(y)return PW(f,u,_);u.flags|=128}if(l=u.memoizedState,l!==null&&(l.rendering=null,l.tail=null,l.lastEffect=null),j0(Z0,Z0.current),y)break;else return null;case 22:case 23:return u.lanes=0,wW(f,u,_)}return s1(f,u,_)}var SW,s9,CW,RW;SW=function(f,u){for(var _=u.child;_!==null;){if(_.tag===5||_.tag===6)f.appendChild(_.stateNode);else if(_.tag!==4&&_.child!==null){_.child.return=_,_=_.child;continue}if(_===u)break;for(;_.sibling===null;){if(_.return===null||_.return===u)return;_=_.return}_.sibling.return=_.return,_=_.sibling}};s9=function(){};CW=function(f,u,_,y){var l=f.memoizedProps;if(l!==y){f=u.stateNode,qy(r1.current);var $=null;switch(_){case"input":l=K9(f,l),y=K9(f,y),$=[];break;case"select":l=E0({},l,{value:void 0}),y=E0({},y,{value:void 0}),$=[];break;case"textarea":l=E9(f,l),y=E9(f,y),$=[];break;default:typeof l.onClick!=="function"&&typeof y.onClick==="function"&&(f.onclick=v8)}V9(_,y);var j;_=null;for(A in l)if(!y.hasOwnProperty(A)&&l.hasOwnProperty(A)&&l[A]!=null)if(A==="style"){var J=l[A];for(j in J)J.hasOwnProperty(j)&&(_||(_={}),_[j]="")}else A!=="dangerouslySetInnerHTML"&&A!=="children"&&A!=="suppressContentEditableWarning"&&A!=="suppressHydrationWarning"&&A!=="autoFocus"&&(y$.hasOwnProperty(A)?$||($=[]):($=$||[]).push(A,null));for(A in y){var F=y[A];if(J=l!=null?l[A]:void 0,y.hasOwnProperty(A)&&F!==J&&(F!=null||J!=null))if(A==="style")if(J){for(j in J)!J.hasOwnProperty(j)||F&&F.hasOwnProperty(j)||(_||(_={}),_[j]="");for(j in F)F.hasOwnProperty(j)&&J[j]!==F[j]&&(_||(_={}),_[j]=F[j])}else _||($||($=[]),$.push(A,_)),_=F;else A==="dangerouslySetInnerHTML"?(F=F?F.__html:void 0,J=J?J.__html:void 0,F!=null&&J!==F&&($=$||[]).push(A,F)):A==="children"?typeof F!=="string"&&typeof F!=="number"||($=$||[]).push(A,""+F):A!=="suppressContentEditableWarning"&&A!=="suppressHydrationWarning"&&(y$.hasOwnProperty(A)?(F!=null&&A==="onScroll"&&A0("scroll",f),$||J===F||($=[])):($=$||[]).push(A,F))}_&&($=$||[]).push("style",_);var A=$;if(u.updateQueue=A)u.flags|=4}};RW=function(f,u,_,y){_!==y&&(u.flags|=4)};function b3(f,u){if(!G0)switch(f.tailMode){case"hidden":u=f.tail;for(var _=null;u!==null;)u.alternate!==null&&(_=u),u=u.sibling;_===null?f.tail=null:_.sibling=null;break;case"collapsed":_=f.tail;for(var y=null;_!==null;)_.alternate!==null&&(y=_),_=_.sibling;y===null?u||f.tail===null?f.tail=null:f.tail.sibling=null:y.sibling=null}}function yu(f){var u=f.alternate!==null&&f.alternate.child===f.child,_=0,y=0;if(u)for(var l=f.child;l!==null;)_|=l.lanes|l.childLanes,y|=l.subtreeFlags&14680064,y|=l.flags&14680064,l.return=f,l=l.sibling;else for(l=f.child;l!==null;)_|=l.lanes|l.childLanes,y|=l.subtreeFlags,y|=l.flags,l.return=f,l=l.sibling;return f.subtreeFlags|=y,f.childLanes=_,u}function _O(f,u,_){var y=u.pendingProps;switch(O7(u),u.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return yu(u),null;case 1:return Nu(u.type)&&b8(),yu(u),null;case 3:if(y=u.stateNode,Tl(),U0(Xu),U0($u),r7(),y.pendingContext&&(y.context=y.pendingContext,y.pendingContext=null),f===null||f.child===null)G8(u)?u.flags|=4:f===null||f.memoizedState.isDehydrated&&(u.flags&256)===0||(u.flags|=1024,W1!==null&&(y7(W1),W1=null));return s9(f,u),yu(u),null;case 5:T7(u);var l=qy(K$.current);if(_=u.type,f!==null&&u.stateNode!=null)CW(f,u,_,y,l),f.ref!==u.ref&&(u.flags|=512,u.flags|=2097152);else{if(!y){if(u.stateNode===null)throw Error(Ff(166));return yu(u),null}if(f=qy(r1.current),G8(u)){y=u.stateNode,_=u.type;var $=u.memoizedProps;switch(y[w1]=u,y[G$]=$,f=(u.mode&1)!==0,_){case"dialog":A0("cancel",y),A0("close",y);break;case"iframe":case"object":case"embed":A0("load",y);break;case"video":case"audio":for(l=0;l",f=f.removeChild(f.firstChild)):typeof y.is==="string"?f=j.createElement(_,{is:y.is}):(f=j.createElement(_),_==="select"&&(j=f,y.multiple?j.multiple=!0:y.size&&(j.size=y.size))):f=j.createElementNS(f,_),f[w1]=u,f[G$]=y,SW(f,u,!1,!1),u.stateNode=f;f:{switch(j=O9(_,y),_){case"dialog":A0("cancel",f),A0("close",f),l=y;break;case"iframe":case"object":case"embed":A0("load",f),l=y;break;case"video":case"audio":for(l=0;lMl&&(u.flags|=128,y=!0,b3($,!1),u.lanes=4194304)}else{if(!y)if(f=i8(j),f!==null){if(u.flags|=128,y=!0,_=f.updateQueue,_!==null&&(u.updateQueue=_,u.flags|=4),b3($,!0),$.tail===null&&$.tailMode==="hidden"&&!j.alternate&&!G0)return yu(u),null}else 2*w0()-$.renderingStartTime>Ml&&_!==1073741824&&(u.flags|=128,y=!0,b3($,!1),u.lanes=4194304);$.isBackwards?(j.sibling=u.child,u.child=j):(_=$.last,_!==null?_.sibling=j:u.child=j,$.last=j)}if($.tail!==null)return u=$.tail,$.rendering=u,$.tail=u.sibling,$.renderingStartTime=w0(),u.sibling=null,_=Z0.current,j0(Z0,y?_&1|2:_&1),u;return yu(u),null;case 22:case 23:return c7(),y=u.memoizedState!==null,f!==null&&f.memoizedState!==null!==y&&(u.flags|=8192),y&&(u.mode&1)!==0?(Su&1073741824)!==0&&(yu(u),u.subtreeFlags&6&&(u.flags|=8192)):yu(u),null;case 24:return null;case 25:return null}throw Error(Ff(156,u.tag))}function yO(f,u){switch(O7(u),u.tag){case 1:return Nu(u.type)&&b8(),f=u.flags,f&65536?(u.flags=f&-65537|128,u):null;case 3:return Tl(),U0(Xu),U0($u),r7(),f=u.flags,(f&65536)!==0&&(f&128)===0?(u.flags=f&-65537|128,u):null;case 5:return T7(u),null;case 13:if(U0(Z0),f=u.memoizedState,f!==null&&f.dehydrated!==null){if(u.alternate===null)throw Error(Ff(340));Dl()}return f=u.flags,f&65536?(u.flags=f&-65537|128,u):null;case 19:return U0(Z0),null;case 4:return Tl(),null;case 10:return Y7(u.type._context),null;case 22:case 23:return c7(),null;case 24:return null;default:return null}}var Z8=!1,lu=!1,lO=typeof WeakSet==="function"?WeakSet:Set,qf=null;function ql(f,u){var _=f.ref;if(_!==null)if(typeof _==="function")try{_(null)}catch(y){Y0(f,u,y)}else _.current=null}function o9(f,u,_){try{_()}catch(y){Y0(f,u,y)}}var iA=!1;function $O(f,u){if(S9=C8,f=IU(),H7(f)){if("selectionStart"in f)var _={start:f.selectionStart,end:f.selectionEnd};else f:{_=(_=f.ownerDocument)&&_.defaultView||window;var y=_.getSelection&&_.getSelection();if(y&&y.rangeCount!==0){_=y.anchorNode;var{anchorOffset:l,focusNode:$}=y;y=y.focusOffset;try{_.nodeType,$.nodeType}catch(V){_=null;break f}var j=0,J=-1,F=-1,A=0,U=0,G=f,W=null;u:for(;;){for(var K;;){if(G!==_||l!==0&&G.nodeType!==3||(J=j+l),G!==$||y!==0&&G.nodeType!==3||(F=j+y),G.nodeType===3&&(j+=G.nodeValue.length),(K=G.firstChild)===null)break;W=G,G=K}for(;;){if(G===f)break u;if(W===_&&++A===l&&(J=j),W===$&&++U===y&&(F=j),(K=G.nextSibling)!==null)break;G=W,W=G.parentNode}G=K}_=J===-1||F===-1?null:{start:J,end:F}}else _=null}_=_||{start:0,end:0}}else _=null;C9={focusedElem:f,selectionRange:_},C8=!1;for(qf=u;qf!==null;)if(u=qf,f=u.child,(u.subtreeFlags&1028)!==0&&f!==null)f.return=u,qf=f;else for(;qf!==null;){u=qf;try{var E=u.alternate;if((u.flags&1024)!==0)switch(u.tag){case 0:case 11:case 15:break;case 1:if(E!==null){var{memoizedProps:H,memoizedState:O}=E,z=u.stateNode,q=z.getSnapshotBeforeUpdate(u.elementType===u.type?H:A1(u.type,H),O);z.__reactInternalSnapshotBeforeUpdate=q}break;case 3:var Z=u.stateNode.containerInfo;Z.nodeType===1?Z.textContent="":Z.nodeType===9&&Z.documentElement&&Z.removeChild(Z.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(Ff(163))}}catch(V){Y0(u,u.return,V)}if(f=u.sibling,f!==null){f.return=u.return,qf=f;break}qf=u.return}return E=iA,iA=!1,E}function f$(f,u,_){var y=u.updateQueue;if(y=y!==null?y.lastEffect:null,y!==null){var l=y=y.next;do{if((l.tag&f)===f){var $=l.destroy;l.destroy=void 0,$!==void 0&&o9(u,_,$)}l=l.next}while(l!==y)}}function J4(f,u){if(u=u.updateQueue,u=u!==null?u.lastEffect:null,u!==null){var _=u=u.next;do{if((_.tag&f)===f){var y=_.create;_.destroy=y()}_=_.next}while(_!==u)}}function a9(f){var u=f.ref;if(u!==null){var _=f.stateNode;switch(f.tag){case 5:f=_;break;default:f=_}typeof u==="function"?u(f):u.current=f}}function xW(f){var u=f.alternate;u!==null&&(f.alternate=null,xW(u)),f.child=null,f.deletions=null,f.sibling=null,f.tag===5&&(u=f.stateNode,u!==null&&(delete u[w1],delete u[G$],delete u[v9],delete u[cV],delete u[pV])),f.stateNode=null,f.return=null,f.dependencies=null,f.memoizedProps=null,f.memoizedState=null,f.pendingProps=null,f.stateNode=null,f.updateQueue=null}function vW(f){return f.tag===5||f.tag===3||f.tag===4}function gA(f){f:for(;;){for(;f.sibling===null;){if(f.return===null||vW(f.return))return null;f=f.return}f.sibling.return=f.return;for(f=f.sibling;f.tag!==5&&f.tag!==6&&f.tag!==18;){if(f.flags&2)continue f;if(f.child===null||f.tag===4)continue f;else f.child.return=f,f=f.child}if(!(f.flags&2))return f.stateNode}}function d9(f,u,_){var y=f.tag;if(y===5||y===6)f=f.stateNode,u?_.nodeType===8?_.parentNode.insertBefore(f,u):_.insertBefore(f,u):(_.nodeType===8?(u=_.parentNode,u.insertBefore(f,_)):(u=_,u.appendChild(f)),_=_._reactRootContainer,_!==null&&_!==void 0||u.onclick!==null||(u.onclick=v8));else if(y!==4&&(f=f.child,f!==null))for(d9(f,u,_),f=f.sibling;f!==null;)d9(f,u,_),f=f.sibling}function e9(f,u,_){var y=f.tag;if(y===5||y===6)f=f.stateNode,u?_.insertBefore(f,u):_.appendChild(f);else if(y!==4&&(f=f.child,f!==null))for(e9(f,u,_),f=f.sibling;f!==null;)e9(f,u,_),f=f.sibling}var g0=null,U1=!1;function W_(f,u,_){for(_=_.child;_!==null;)bW(f,u,_),_=_.sibling}function bW(f,u,_){if(T1&&typeof T1.onCommitFiberUnmount==="function")try{T1.onCommitFiberUnmount(e8,_)}catch(J){}switch(_.tag){case 5:lu||ql(_,u);case 6:var y=g0,l=U1;g0=null,W_(f,u,_),g0=y,U1=l,g0!==null&&(U1?(f=g0,_=_.stateNode,f.nodeType===8?f.parentNode.removeChild(_):f.removeChild(_)):g0.removeChild(_.stateNode));break;case 18:g0!==null&&(U1?(f=g0,_=_.stateNode,f.nodeType===8?f9(f.parentNode,_):f.nodeType===1&&f9(f,_),F$(f)):f9(g0,_.stateNode));break;case 4:y=g0,l=U1,g0=_.stateNode.containerInfo,U1=!0,W_(f,u,_),g0=y,U1=l;break;case 0:case 11:case 14:case 15:if(!lu&&(y=_.updateQueue,y!==null&&(y=y.lastEffect,y!==null))){l=y=y.next;do{var $=l,j=$.destroy;$=$.tag,j!==void 0&&(($&2)!==0?o9(_,u,j):($&4)!==0&&o9(_,u,j)),l=l.next}while(l!==y)}W_(f,u,_);break;case 1:if(!lu&&(ql(_,u),y=_.stateNode,typeof y.componentWillUnmount==="function"))try{y.props=_.memoizedProps,y.state=_.memoizedState,y.componentWillUnmount()}catch(J){Y0(_,u,J)}W_(f,u,_);break;case 21:W_(f,u,_);break;case 22:_.mode&1?(lu=(y=lu)||_.memoizedState!==null,W_(f,u,_),lu=y):W_(f,u,_);break;default:W_(f,u,_)}}function nA(f){var u=f.updateQueue;if(u!==null){f.updateQueue=null;var _=f.stateNode;_===null&&(_=f.stateNode=new lO),u.forEach(function(y){var l=zO.bind(null,f,y);_.has(y)||(_.add(y),y.then(l,l))})}}function Q1(f,u){var _=u.deletions;if(_!==null)for(var y=0;y<_.length;y++){var l=_[y];try{var $=f,j=u,J=j;f:for(;J!==null;){switch(J.tag){case 5:g0=J.stateNode,U1=!1;break f;case 3:g0=J.stateNode.containerInfo,U1=!0;break f;case 4:g0=J.stateNode.containerInfo,U1=!0;break f}J=J.return}if(g0===null)throw Error(Ff(160));bW($,j,l),g0=null,U1=!1;var F=l.alternate;F!==null&&(F.return=null),l.return=null}catch(A){Y0(l,u,A)}}if(u.subtreeFlags&12854)for(u=u.child;u!==null;)hW(u,f),u=u.sibling}function hW(f,u){var{alternate:_,flags:y}=f;switch(f.tag){case 0:case 11:case 14:case 15:if(Q1(u,f),B1(f),y&4){try{f$(3,f,f.return),J4(3,f)}catch(H){Y0(f,f.return,H)}try{f$(5,f,f.return)}catch(H){Y0(f,f.return,H)}}break;case 1:Q1(u,f),B1(f),y&512&&_!==null&&ql(_,_.return);break;case 5:if(Q1(u,f),B1(f),y&512&&_!==null&&ql(_,_.return),f.flags&32){var l=f.stateNode;try{l$(l,"")}catch(H){Y0(f,f.return,H)}}if(y&4&&(l=f.stateNode,l!=null)){var $=f.memoizedProps,j=_!==null?_.memoizedProps:$,J=f.type,F=f.updateQueue;if(f.updateQueue=null,F!==null)try{J==="input"&&$.type==="radio"&&$.name!=null&&JU(l,$),O9(J,j);var A=O9(J,$);for(j=0;jl&&(l=j),y&=~$}if(y=l,y=w0()-y,y=(120>y?120:480>y?480:1080>y?1080:1920>y?1920:3000>y?3000:4320>y?4320:1960*JO(y/1960))-y,10f?16:f,E_===null)var y=!1;else{if(f=E_,E_=null,o8=0,(gf&6)!==0)throw Error(Ff(331));var l=gf;gf|=4;for(qf=f.current;qf!==null;){var $=qf,j=$.child;if((qf.flags&16)!==0){var J=$.deletions;if(J!==null){for(var F=0;Fw0()-h7?Ey(f,0):b7|=_),Lu(f,u)}function gW(f,u){u===0&&((f.mode&1)===0?u=1:(u=J8,J8<<=1,(J8&130023424)===0&&(J8=4194304)));var _=zu();f=t1(f,u),f!==null&&(V$(f,u,_),Lu(f,_))}function GO(f){var u=f.memoizedState,_=0;u!==null&&(_=u.retryLane),gW(f,_)}function zO(f,u){var _=0;switch(f.tag){case 13:var{stateNode:y,memoizedState:l}=f;l!==null&&(_=l.retryLane);break;case 19:y=f.stateNode;break;default:throw Error(Ff(314))}y!==null&&y.delete(u),gW(f,_)}var nW;nW=function(f,u,_){if(f!==null)if(f.memoizedProps!==u.pendingProps||Xu.current)Ou=!0;else{if((f.lanes&_)===0&&(u.flags&128)===0)return Ou=!1,uO(f,u,_);Ou=(f.flags&131072)!==0?!0:!1}else Ou=!1,G0&&(u.flags&1048576)!==0&&aU(u,c8,u.index);switch(u.lanes=0,u.tag){case 2:var y=u.type;Y8(f,u),f=u.pendingProps;var l=Bl(u,$u.current);Nl(u,_),l=P7(null,u,y,f,l,_);var $=S7();return u.flags|=1,typeof l==="object"&&l!==null&&typeof l.render==="function"&&l.$$typeof===void 0?(u.tag=1,u.memoizedState=null,u.updateQueue=null,Nu(y)?($=!0,h8(u)):$=!1,u.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,D7(u),l.updater=j4,u.stateNode=l,l._reactInternals=u,k9(u,y,f,_),u=g9(null,u,y,!0,$,_)):(u.tag=0,G0&&$&&V7(u),Gu(null,u,l,_),u=u.child),u;case 16:y=u.elementType;f:{switch(Y8(f,u),f=u.pendingProps,l=y._init,y=l(y._payload),u.type=y,l=u.tag=ZO(y),f=A1(y,f),l){case 0:u=i9(null,u,y,f,_);break f;case 1:u=pA(null,u,y,f,_);break f;case 11:u=IA(null,u,y,f,_);break f;case 14:u=cA(null,u,y,A1(y.type,f),_);break f}throw Error(Ff(306,y,""))}return u;case 0:return y=u.type,l=u.pendingProps,l=u.elementType===y?l:A1(y,l),i9(f,u,y,l,_);case 1:return y=u.type,l=u.pendingProps,l=u.elementType===y?l:A1(y,l),pA(f,u,y,l,_);case 3:f:{if(rW(u),f===null)throw Error(Ff(387));y=u.pendingProps,$=u.memoizedState,l=$.element,yW(f,u),m8(u,y,null,_);var j=u.memoizedState;if(y=j.element,$.isDehydrated)if($={element:y,isDehydrated:!1,cache:j.cache,pendingSuspenseBoundaries:j.pendingSuspenseBoundaries,transitions:j.transitions},u.updateQueue.baseState=$,u.memoizedState=$,u.flags&256){l=rl(Error(Ff(423)),u),u=kA(f,u,y,_,l);break f}else if(y!==l){l=rl(Error(Ff(424)),u),u=kA(f,u,y,_,l);break f}else for(Cu=X_(u.stateNode.containerInfo.firstChild),Ru=u,G0=!0,W1=null,_=uW(u,null,y,_),u.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling;else{if(Dl(),y===l){u=s1(f,u,_);break f}Gu(f,u,y,_)}u=u.child}return u;case 5:return lW(u),f===null&&I9(u),y=u.type,l=u.pendingProps,$=f!==null?f.memoizedProps:null,j=l.children,R9(y,l)?j=null:$!==null&&R9(y,$)&&(u.flags|=32),TW(f,u),Gu(f,u,j,_),u.child;case 6:return f===null&&I9(u),null;case 13:return MW(f,u,_);case 4:return w7(u,u.stateNode.containerInfo),y=u.pendingProps,f===null?u.child=wl(u,null,y,_):Gu(f,u,y,_),u.child;case 11:return y=u.type,l=u.pendingProps,l=u.elementType===y?l:A1(y,l),IA(f,u,y,l,_);case 7:return Gu(f,u,u.pendingProps,_),u.child;case 8:return Gu(f,u,u.pendingProps.children,_),u.child;case 12:return Gu(f,u,u.pendingProps.children,_),u.child;case 10:f:{if(y=u.type._context,l=u.pendingProps,$=u.memoizedProps,j=l.value,j0(p8,y._currentValue),y._currentValue=j,$!==null)if(K1($.value,j)){if($.children===l.children&&!Xu.current){u=s1(f,u,_);break f}}else for($=u.child,$!==null&&($.return=u);$!==null;){var J=$.dependencies;if(J!==null){j=$.child;for(var F=J.firstContext;F!==null;){if(F.context===y){if($.tag===1){F=i1(-1,_&-_),F.tag=2;var A=$.updateQueue;if(A!==null){A=A.shared;var U=A.pending;U===null?F.next=F:(F.next=U.next,U.next=F),A.pending=F}}$.lanes|=_,F=$.alternate,F!==null&&(F.lanes|=_),c9($.return,_,u),J.lanes|=_;break}F=F.next}}else if($.tag===10)j=$.type===u.type?null:$.child;else if($.tag===18){if(j=$.return,j===null)throw Error(Ff(341));j.lanes|=_,J=j.alternate,J!==null&&(J.lanes|=_),c9(j,_,u),j=$.sibling}else j=$.child;if(j!==null)j.return=$;else for(j=$;j!==null;){if(j===u){j=null;break}if($=j.sibling,$!==null){$.return=j.return,j=$;break}j=j.return}$=j}Gu(f,u,l.children,_),u=u.child}return u;case 9:return l=u.type,y=u.pendingProps.children,Nl(u,_),l=eu(l),y=y(l),u.flags|=1,Gu(f,u,y,_),u.child;case 14:return y=u.type,l=A1(y,u.pendingProps),l=A1(y.type,l),cA(f,u,y,l,_);case 15:return DW(f,u,u.type,u.pendingProps,_);case 17:return y=u.type,l=u.pendingProps,l=u.elementType===y?l:A1(y,l),Y8(f,u),u.tag=1,Nu(y)?(f=!0,h8(u)):f=!1,Nl(u,_),LW(u,y,l),k9(u,y,l,_),g9(null,u,y,!0,f,_);case 19:return PW(f,u,_);case 22:return wW(f,u,_)}throw Error(Ff(156,u.tag))};function tW(f,u){return OU(f,u)}function KO(f,u,_,y){this.tag=f,this.key=_,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=u,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=y,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function au(f,u,_,y){return new KO(f,u,_,y)}function k7(f){return f=f.prototype,!(!f||!f.isReactComponent)}function ZO(f){if(typeof f==="function")return k7(f)?1:0;if(f!==void 0&&f!==null){if(f=f.$$typeof,f===F7)return 11;if(f===Q7)return 14}return 2}function B_(f,u){var _=f.alternate;return _===null?(_=au(f.tag,u,f.key,f.mode),_.elementType=f.elementType,_.type=f.type,_.stateNode=f.stateNode,_.alternate=f,f.alternate=_):(_.pendingProps=u,_.type=f.type,_.flags=0,_.subtreeFlags=0,_.deletions=null),_.flags=f.flags&14680064,_.childLanes=f.childLanes,_.lanes=f.lanes,_.child=f.child,_.memoizedProps=f.memoizedProps,_.memoizedState=f.memoizedState,_.updateQueue=f.updateQueue,u=f.dependencies,_.dependencies=u===null?null:{lanes:u.lanes,firstContext:u.firstContext},_.sibling=f.sibling,_.index=f.index,_.ref=f.ref,_}function w8(f,u,_,y,l,$){var j=2;if(y=f,typeof f==="function")k7(f)&&(j=1);else if(typeof f==="string")j=5;else f:switch(f){case Fl:return Hy(_.children,l,$,u);case J7:j=8,l|=8;break;case U9:return f=au(12,_,u,l|2),f.elementType=U9,f.lanes=$,f;case W9:return f=au(13,_,u,l),f.elementType=W9,f.lanes=$,f;case G9:return f=au(19,_,u,l),f.elementType=G9,f.lanes=$,f;case lU:return Q4(_,l,$,u);default:if(typeof f==="object"&&f!==null)switch(f.$$typeof){case _U:j=10;break f;case yU:j=9;break f;case F7:j=11;break f;case Q7:j=14;break f;case G_:j=16,y=null;break f}throw Error(Ff(130,f==null?f:typeof f,""))}return u=au(j,_,u,l),u.elementType=f,u.type=y,u.lanes=$,u}function Hy(f,u,_,y){return f=au(7,f,y,u),f.lanes=_,f}function Q4(f,u,_,y){return f=au(22,f,y,u),f.elementType=lU,f.lanes=_,f.stateNode={isHidden:!1},f}function F9(f,u,_){return f=au(6,f,null,u),f.lanes=_,f}function Q9(f,u,_){return u=au(4,f.children!==null?f.children:[],f.key,u),u.lanes=_,u.stateNode={containerInfo:f.containerInfo,pendingChildren:null,implementation:f.implementation},u}function qO(f,u,_,y,l){this.tag=u,this.containerInfo=f,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=n2(0),this.expirationTimes=n2(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=n2(0),this.identifierPrefix=y,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function m7(f,u,_,y,l,$,j,J,F){return f=new qO(f,u,_,J,F),u===1?(u=1,$===!0&&(u|=8)):u=0,$=au(3,null,null,u),f.current=$,$.stateNode=f,$.memoizedState={element:y,isDehydrated:_,cache:null,transitions:null,pendingSuspenseBoundaries:null},D7($),f}function EO(f,u,_){var y=3{function eW(){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=="function")return;try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(eW)}catch(f){console.error(f)}}eW(),fG.exports=dW()});var _G=Pu((s7)=>{var uG=t7();s7.createRoot=uG.createRoot,s7.hydrateRoot=uG.hydrateRoot;var NO});var zz=Pu((I4)=>{var SN=c0(),CN=Symbol.for("react.element"),RN=Symbol.for("react.fragment"),xN=Object.prototype.hasOwnProperty,vN=SN.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,bN={key:!0,ref:!0,__self:!0,__source:!0};function Gz(f,u,_){var y,l={},$=null,j=null;_!==void 0&&($=""+_),u.key!==void 0&&($=""+u.key),u.ref!==void 0&&(j=u.ref);for(y in u)xN.call(u,y)&&!bN.hasOwnProperty(y)&&(l[y]=u[y]);if(f&&f.defaultProps)for(y in u=f.defaultProps,u)l[y]===void 0&&(l[y]=u[y]);return{$$typeof:CN,type:f,key:$,ref:j,props:l,_owner:vN.current}}I4.Fragment=RN;I4.jsx=Gz;I4.jsxs=Gz});var Zz=Pu((kP,Kz)=>{Kz.exports=zz()});var nK=Pu((gK)=>{var F3=c0();function uD(f,u){return f===u&&(f!==0||1/f===1/u)||f!==f&&u!==u}var _D=typeof Object.is==="function"?Object.is:uD,yD=F3.useState,lD=F3.useEffect,$D=F3.useLayoutEffect,jD=F3.useDebugValue;function JD(f,u){var _=u(),y=yD({inst:{value:_,getSnapshot:u}}),l=y[0].inst,$=y[1];return $D(function(){l.value=_,l.getSnapshot=u,DF(l)&&$({inst:l})},[f,_,u]),lD(function(){return DF(l)&&$({inst:l}),f(function(){DF(l)&&$({inst:l})})},[f]),jD(_),_}function DF(f){var u=f.getSnapshot;f=f.value;try{var _=u();return!_D(f,_)}catch(y){return!0}}function FD(f,u){return u()}var QD=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?FD:JD;gK.useSyncExternalStore=F3.useSyncExternalStore!==void 0?F3.useSyncExternalStore:QD});var sK=Pu((xb,tK)=>{tK.exports=nK()});var aK=Pu((oK)=>{var S5=c0(),AD=sK();function UD(f,u){return f===u&&(f!==0||1/f===1/u)||f!==f&&u!==u}var WD=typeof Object.is==="function"?Object.is:UD,GD=AD.useSyncExternalStore,zD=S5.useRef,KD=S5.useEffect,ZD=S5.useMemo,qD=S5.useDebugValue;oK.useSyncExternalStoreWithSelector=function(f,u,_,y,l){var $=zD(null);if($.current===null){var j={hasValue:!1,value:null};$.current=j}else j=$.current;$=ZD(function(){function F(K){if(!A){if(A=!0,U=K,K=y(K),l!==void 0&&j.hasValue){var E=j.value;if(l(E,K))return G=E}return G=K}if(E=G,WD(U,K))return E;var H=y(K);if(l!==void 0&&l(E,H))return U=K,E;return U=K,G=H}var A=!1,U,G,W=_===void 0?null:_;return[function(){return F(u())},W===null?void 0:function(){return F(W())}]},[u,_,y,l]);var J=GD(f,$[0],$[1]);return KD(function(){j.hasValue=!0,j.value=J},[J]),qD(J),J}});var eK=Pu((bb,dK)=>{dK.exports=aK()});var r6=Sf(c0(),1);var s6="北京时间";var EH={timeZone:"Asia/Shanghai",hour12:!1},HH={timeZone:"Asia/Shanghai",hour12:!1},VH=new Intl.DateTimeFormat("en-CA",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",hourCycle:"h23"});function M2(f){if(f===null||f===void 0||f==="")return null;let u=f instanceof Date?f:new Date(f);return Number.isNaN(u.getTime())?null:u}function hQ(f){let u=M2(f);if(!u)return null;return VH.formatToParts(u).reduce((_,y)=>{if(y.type!=="literal")_[y.type]=y.value;return _},{})}function zf(f){let u=M2(f);return u?u.toLocaleString("zh-CN",EH):"--"}function L0(f){let u=M2(f);return u?u.toLocaleTimeString("zh-CN",HH):"--"}function P2(f){let u=hQ(f);if(!u)return"";let _=u.hour==="24"?"00":u.hour;return`${u.year}-${u.month}-${u.day}T${_}:${u.minute}`}function IQ(f=new Date){let u=hQ(f);if(!u)return"";return`${u.year}-${u.month}-${u.day}`}function cQ(f){if(!f)return null;let u=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(f);if(!u)return null;let[,_,y,l,$,j,J="00"]=u,F=Date.UTC(Number(_),Number(y)-1,Number(l),Number($)-8,Number(j),Number(J)),A=new Date(F),U=P2(A);return Number.isNaN(A.getTime())||U!==`${_}-${y}-${l}T${$}:${j}`?null:A.toISOString()}var NE=Sf(_G(),1);var q4=Sf(c0(),1);class Cl extends Error{unideskRequestError=!0;meta;constructor(f,u){super(f);this.name="UniDeskRequestError",this.meta=u}}function LO(f){return new Promise((u)=>setTimeout(u,f))}function B$(f,u="操作失败"){return f instanceof Error?f.message:String(f||u)}function z4(f,u=500){if(f===null||f===void 0)return"";let _=typeof f==="string"?f:JSON.stringify(f),y=String(_||"").replace(/\s+/gu," ").trim();return y.length>u?`${y.slice(0,u)}...`:y}function YO(f){try{let u=typeof location<"u"&&location.origin?location.origin:"http://localhost";return new URL(f,u).toString()}catch{return f}}function yG(f){return String(f.method||"GET").toUpperCase()}function BO(f){if(f===null||f===void 0)return!1;if(typeof f!=="object")return!1;if(typeof Blob<"u"&&f instanceof Blob)return!1;if(typeof FormData<"u"&&f instanceof FormData)return!1;if(typeof URLSearchParams<"u"&&f instanceof URLSearchParams)return!1;if(typeof ArrayBuffer<"u"&&f instanceof ArrayBuffer)return!1;return!0}function lG(f){let u=new Headers(f.headers||{}),_=BO(f.body)?JSON.stringify(f.body):f.body;if(_&&!u.has("content-type")&&typeof _==="string")u.set("content-type","application/json");return{...f,credentials:f.credentials||"same-origin",body:_,headers:u}}function $G(f){if(f?.error&&typeof f.error==="object"&&typeof f.error.message==="string")return f.error.message;if(typeof f?.error==="string")return f.error;if(typeof f?.message==="string")return f.message;if(typeof f?.detail==="string")return f.detail;return""}function DO(f,u){if(!f||typeof f!=="object"||Array.isArray(f))return!1;return u.some((_)=>_!==!1&&f[_]===!1)}function L$(f,u,_,y,l={}){return{kind:f,method:_,url:YO(u),occurredAt:y.toISOString(),...l}}function Y$(f,u){if(!f)return"请求失败";return`HTTP ${f}${u?` ${u}`:""}`}function jG(f){try{return{body:f?JSON.parse(f):null,parseError:""}}catch(u){return{body:{text:f},parseError:B$(u,"parse failed")}}}async function wf(f,u={},_=0){let{failureFields:y=["ok"],strictJson:l=!1,retryInvalidJson:$=0,retryDelayMs:j=120,invalidJsonPrefix:J="服务返回了无效 JSON",invalidJsonPreview:F=!1,responsePreviewLength:A=500,...U}=u,G=yG(U),W=new Date,K;try{K=await fetch(f,lG(U))}catch(O){let z=B$(O,"网络请求失败");throw new Cl(z,L$("network",f,G,W,{upstreamMessage:z}))}let E=await K.text(),H=jG(E);if(H.parseError){if(l&&G==="GET"&&_<$)return await LO(j),wf(f,u,_+1);if(l){let O=F?`;响应预览:${z4(E,180)}`:"";throw new Cl(`${J}(${E.length} bytes):${H.parseError}${O}`,L$("parse",f,G,W,{status:K.status,statusText:K.statusText,parseError:H.parseError,responsePreview:z4(E,A)}))}}if(!K.ok||DO(H.body,y)){let O=$G(H.body),z=O||Y$(K.status,K.statusText);throw new Cl(z,L$("http",f,G,W,{status:K.status,statusText:K.statusText,upstreamMessage:O,responsePreview:z4(H.parseError?E:H.body,A)}))}return H.body}async function JG(f,u={}){let _=yG(u),y=new Date,l;try{l=await fetch(f,lG(u))}catch(A){let U=B$(A,"网络请求失败");throw new Cl(U,L$("network",f,_,y,{upstreamMessage:U}))}if(l.ok)return l.blob();let $=await l.text(),j=jG($),J=$G(j.body),F=J||Y$(l.status,l.statusText);throw new Cl(F,L$("http",f,_,y,{status:l.status,statusText:l.statusText,upstreamMessage:J,responsePreview:z4(j.parseError?$:j.body),parseError:j.parseError||void 0}))}function FG(f){return Boolean(f&&typeof f==="object"&&f.unideskRequestError===!0&&f.meta)}function wO(f){if(!f)return"";let u=new Date(f);if(Number.isNaN(u.getTime()))return f;return`${zf(u)} ${s6}`}function o7(f,u="操作失败"){if(FG(f)){let l=f.meta.kind==="parse"?"响应解析失败":f.meta.kind==="network"?"网络请求失败":f.meta.status&&(f.meta.status<200||f.meta.status>=300)?Y$(f.meta.status,f.meta.statusText):"应用请求失败",$=f.meta.status?Y$(f.meta.status):"",j=(F)=>!F||F===l||F===$,J=!j(f.message)?f.message:j(f.meta.upstreamMessage)?"":f.meta.upstreamMessage||"";return{title:l,message:J,status:f.meta.status,statusText:f.meta.statusText,method:f.meta.method,url:f.meta.url,occurredAt:wO(f.meta.occurredAt),responsePreview:f.meta.responsePreview,parseError:f.meta.parseError,structured:!0}}let y=B$(f,u).split(/\r?\n/u);return{title:y[0]||u,message:y.slice(1).join(` -`),structured:y.length>1}}function TO(f,u="操作失败"){let _=o7(f,u),y=[_.title];if(_.message)y.push(`原因: ${_.message}`);if(_.method||_.url)y.push(`请求: ${[_.method,_.url].filter(Boolean).join(" ")}`);if(_.status)y.push(`状态: ${Y$(_.status,_.statusText)}`);if(_.occurredAt)y.push(`时间: ${_.occurredAt}`);if(_.parseError)y.push(`解析错误: ${_.parseError}`);if(_.responsePreview&&_.responsePreview!==_.message)y.push(`响应预览: ${_.responsePreview}`);return y.filter(Boolean).join(` -`)}function Pf(f,u="操作失败"){return FG(f)?TO(f,u):B$(f,u)}var QG=Sf(c0(),1);var P_=QG.default.createElement;function D$(f,u){return u?[P_("dt",{key:`${f}-label`},f),P_("dd",{key:f},u)]:null}function H0({error:f,wide:u=!1,fallback:_="操作失败",className:y=""}){if(!f)return null;let l=o7(f,_),$=[D$("请求",[l.method,l.url].filter(Boolean).join(" ")),D$("状态",l.status?`HTTP ${l.status}${l.statusText?` ${l.statusText}`:""}`:""),D$("时间",l.occurredAt),D$("解析错误",l.parseError),D$("响应预览",l.responsePreview)].filter(Boolean);return P_("div",{className:`form-error unidesk-error${u?" wide":""}${y?` ${y}`:""}`,role:"alert","data-testid":"unidesk-error"},P_("div",{className:"unidesk-error-title"},P_("strong",null,l.title),l.status?P_("span",{className:"unidesk-error-code"},`HTTP ${l.status}`):null),l.message?P_("pre",{className:"unidesk-error-message"},l.message):null,$.length>0?P_("dl",{className:"unidesk-error-details"},$):null)}var m=q4.default.createElement,{useEffect:rO}=q4.default,K4=q4.default.useState,Dy={label:"主用户私聊账号",userId:645275593};function a7(f){let u=Number(f);return Number.isFinite(u)?u.toLocaleString("zh-CN"):"--"}async function a1(f,u={}){return wf(f,{failureFields:["ok","success"],...u})}function Z4({status:f,children:u}){let _=String(f||"unknown").toLowerCase();return m("span",{className:`status-badge ${_}`},u||f||"unknown")}function Rl({label:f,value:u,hint:_,tone:y}){return m("article",{className:`metric-card ${y||""}`},m("div",{className:"metric-label"},f),m("div",{className:"metric-value"},u),m("div",{className:"metric-hint"},_))}function xl({title:f,eyebrow:u,actions:_,children:y,className:l}){return m("section",{className:`panel ${l||""}`},m("div",{className:"panel-head"},m("div",null,u?m("p",{className:"panel-eyebrow"},u):null,m("h2",null,f)),_?m("div",{className:"panel-actions"},_):null),m("div",{className:"panel-body"},y))}function w$({title:f,data:u,onOpen:_,testId:y}){return m("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:(l)=>{l?.stopPropagation?.(),_(f,u)}},"查看原始JSON")}function T$({title:f,text:u}){return m("div",{className:"empty-state"},m("strong",null,f),m("span",null,u))}function MO(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function PO(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function SO(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function S_(f,u){return`${f}/microservices/claudeqq/proxy${u}`}function CO(f){return Array.isArray(f?.events)?f.events.slice(0,80):[]}function RO(f){return Array.isArray(f?.subscriptions)?f.subscriptions.slice(0,50):[]}function xO(f){return Array.isArray(f?.messages)?f.messages.slice(0,30):[]}function AG(f){let u=f?.text??f?.message??f?.raw?.raw_message;if(typeof u!=="string")return"--";return u.length>180?`${u.slice(0,177)}...`:u}function UG(f){let u=f?.groupId??f?.group_id??(f?.message_type==="group"?f?.target_id:void 0),_=f?.userId??f?.user_id??(f?.message_type==="private"?f?.target_id:void 0);if(u)return`群 ${u}`;if(_)return`私聊 ${_}`;return"--"}function WG({microservices:f,onRaw:u,apiBaseUrl:_="/api"}){let y=f.find((w)=>w.id==="claudeqq")||null,[l,$]=K4({loading:!1,qrLoading:!1,error:"",health:null,status:null,napcatLogin:null,napcatQrcode:null,qrcodeFetched:!1,qrcodeRefreshedAt:null,events:null,subscriptions:null,sent:null,refreshedAt:null}),[j,J]=K4({targetType:"private",targetId:String(Dy.userId),message:""}),[F,A]=K4({name:"unidesk-callback",targetUrl:"",eventTypes:"message",secret:""}),[U,G]=K4("");async function W(){if(!y)return;$((w)=>({...w,loading:!0,error:""}));try{let[w,Y,R,k,p]=await Promise.all([a1(`${_}/microservices/claudeqq/health`),a1(S_(_,"/api/server/status")),a1(S_(_,"/api/events/recent?limit=60")),a1(S_(_,"/api/events/subscriptions")),a1(S_(_,"/api/messages/sent?limit=20"))]);if($((n)=>({...n,loading:!1,error:"",health:w,status:Y,events:R,subscriptions:k,sent:p,refreshedAt:new Date})),!l.qrcodeFetched)K(!1)}catch(w){$((Y)=>({...Y,loading:!1,error:Pf(w,"ClaudeQQ 加载失败")}))}}async function K(w=!0){if(!y)return;$((Y)=>({...Y,qrLoading:!0,error:w?"":Y.error}));try{let Y=await a1(S_(_,"/api/napcat/login")),R=Y?.napcat?.qrcode||Y?.qrcode||null;$((k)=>({...k,qrLoading:!1,error:"",napcatLogin:Y,napcatQrcode:R,qrcodeFetched:!0,qrcodeRefreshedAt:new Date}))}catch(Y){$((R)=>({...R,qrLoading:!1,error:w||!R.napcatQrcode?Pf(Y,"NapCat 二维码加载失败"):R.error}))}}async function E(w){w.preventDefault(),G("");let Y=Number(j.targetId);if(!Number.isFinite(Y)||Y<=0||j.message.trim().length===0){$((R)=>({...R,error:"请填写 QQ 目标和消息内容"}));return}try{await a1(S_(_,"/api/push/text"),{method:"POST",body:JSON.stringify({userId:j.targetType==="private"?Y:void 0,groupId:j.targetType==="group"?Y:void 0,message:j.message})}),J((R)=>({...R,targetType:"private",targetId:String(Dy.userId),message:""})),G("消息推送请求已提交"),await W()}catch(R){$((k)=>({...k,error:Pf(R,"发送失败")}))}}async function H(w){if(w.preventDefault(),G(""),F.targetUrl.trim().length===0){$((Y)=>({...Y,error:"请填写订阅回调 URL"}));return}try{await a1(S_(_,"/api/events/subscriptions"),{method:"POST",body:JSON.stringify({name:F.name,targetUrl:F.targetUrl,eventTypes:F.eventTypes.split(",").map((Y)=>Y.trim()).filter(Boolean),secret:F.secret||void 0,enabled:!0})}),G("事件订阅已创建"),await W()}catch(Y){$((R)=>({...R,error:Pf(Y,"订阅失败")}))}}async function O(w){if(!w)return;G("");try{await a1(S_(_,`/api/events/subscriptions/${encodeURIComponent(w)}`),{method:"DELETE"}),G("事件订阅已删除"),await W()}catch(Y){$((R)=>({...R,error:Pf(Y,"删除订阅失败")}))}}if(rO(()=>{if(!y)return;W();return},[y?.id,y?.runtime?.providerStatus]),!y)return m(T$,{title:"ClaudeQQ 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=claudeqq"});let z=MO(y),q=SO(y),Z=PO(y),V=l.health||{},L=l.status||{},r=l.napcatLogin||{},N=V.napcat||L.napcat||{},D={...r.napcat||{},...N,qrcode:l.napcatQrcode||{},webui:N.webui||r.napcat?.webui},x=r.login||{},c=l.napcatQrcode||{},v=CO(l.events),C=RO(l.subscriptions),S=xO(l.sent),B=Boolean(D.httpConnected||x.ready),P=String(D.loginState||x.state||(B?"logged_in":"unknown")),M=Boolean(c.available&&c.dataUrl);return m("div",{className:"claudeqq-page","data-testid":"claudeqq-page"},m(xl,{title:"ClaudeQQ 工作台",eyebrow:"D601 QQ Event Gateway",actions:m("div",{className:"panel-actions"},m("button",{type:"button",className:"ghost-btn",onClick:W,disabled:l.loading,"data-testid":"claudeqq-refresh-button"},l.loading?"刷新中":"刷新"),m(w$,{title:"ClaudeQQ 用户服务",data:y,onOpen:u,testId:"raw-claudeqq-service"}))},m("div",{className:"findjob-hero"},m("div",null,m("div",{className:"node-version-line"},m(Z4,{status:z.providerStatus==="online"?"online":"warn"},z.providerStatus||"unknown"),m("span",null,y.providerId),m("span",null,Z.public?"公网暴露":"仅 UniDesk frontend 代理访问")),m("p",{className:"muted paragraph"},y.description)),m("div",{className:"microservice-ref-card"},m("span",null,"Repo"),m("strong",null,q.url||"--"),m("code",null,q.commitId||"--")),m("div",{className:"microservice-ref-card"},m("span",null,"D601 Docker"),m("strong",null,`${Z.nodeBindHost||"--"}:${Z.nodePort||"--"}`),m("code",null,`${q.composeFile||"--"} / ${q.composeService||"--"}`))),m(H0,{error:l.error,wide:!0}),U?m("div",{className:"form-success wide"},U):null),m("div",{className:"metric-grid"},m(Rl,{label:"Health",value:V.ok||V.status==="ok"?"OK":"--",hint:"D601 /health",tone:V.ok||V.status==="ok"?"ok":"warn"}),m(Rl,{label:"NapCat HTTP",value:D.httpConnected||D.http?.connected?"OK":"离线",hint:`${D.httpHost||V.napcat?.httpHost||"--"}:${D.httpPort||V.napcat?.httpPort||"--"}`}),m(Rl,{label:"NapCat WS",value:D.wsConnected||D.ws?.connected?"OK":"离线",hint:`${D.wsHost||V.napcat?.wsHost||"--"}:${D.wsPort||V.napcat?.wsPort||"--"}`}),m(Rl,{label:"事件缓存",value:a7(l.events?.count??v.length),hint:"recent QQ events"}),m(Rl,{label:"订阅",value:a7(l.subscriptions?.count??C.length),hint:"webhook subscribers"}),m(Rl,{label:"已发送",value:a7(l.sent?.count??S.length),hint:"sent message log"})),m("div",{className:"findjob-grid"},m(xl,{title:"NapCat 容器登录",eyebrow:"QR Login",className:"claudeqq-login-panel",actions:m("div",{className:"panel-actions inline-actions"},m("button",{type:"button",className:"ghost-btn",onClick:()=>K(!0),disabled:l.qrLoading,"data-testid":"claudeqq-napcat-refresh"},l.qrLoading?"刷新中":"手动刷新二维码"),m(w$,{title:"NapCat Login",data:l.napcatLogin,onOpen:u,testId:"raw-claudeqq-napcat-login"}))},m("div",{className:"claudeqq-login-card","data-testid":"claudeqq-napcat-login"},m("div",{className:"claudeqq-qr-frame"},M?m("img",{src:c.dataUrl,alt:"NapCat QQ 登录二维码","data-testid":"claudeqq-napcat-qrcode"}):m(T$,{title:"等待二维码",text:"NapCat 容器启动后会把登录二维码写入 cache/qrcode.png"})),m("div",{className:"claudeqq-login-copy"},m("div",{className:"node-version-line"},m(Z4,{status:B?"online":M?"warn":"unknown"},B?"已登录":M?"待扫码":"等待二维码"),m("span",null,P),m("span",null,"D601 containerized")),m("p",{className:"muted paragraph"},B?"NapCat 已登录,ClaudeQQ 可通过容器内 HTTP/WS 链路收发 QQ 消息。":"用手机 QQ 扫描二维码授权登录。二维码只在首次加载或手动刷新时更新,D601 的 NapCat 端口仍只绑定 127.0.0.1。"),m("div",{className:"microservice-ref-card"},m("span",null,"NapCat WebUI"),m("strong",null,D.webui?.url||"http://napcat:6099/webui"),m("code",null,"local-only / proxied QR login")),m("div",{className:"microservice-ref-card"},m("span",null,"QR Source"),m("strong",null,c.modifiedAt?zf(c.modifiedAt):l.qrcodeRefreshedAt?zf(l.qrcodeRefreshedAt):"--"),m("code",null,c.file||"/napcat/cache/qrcode.png"))))),m(xl,{title:"消息推送",eyebrow:"Push API"},m("div",{className:"microservice-ref-card"},m("span",null,Dy.label),m("strong",null,String(Dy.userId)),m("code",null,"private userId / 默认推送测试目标")),m("form",{className:"stack-form",onSubmit:E,"data-testid":"claudeqq-push-form"},m("label",null,"目标类型",m("select",{value:j.targetType,onChange:(w)=>J((Y)=>({...Y,targetType:w.target.value}))},m("option",{value:"private"},"私聊 userId"),m("option",{value:"group"},"群 groupId"))),m("label",null,"QQ ID",m("input",{value:j.targetId,onChange:(w)=>J((Y)=>({...Y,targetId:w.target.value})),placeholder:String(Dy.userId)})),m("label",null,"消息内容",m("textarea",{value:j.message,onChange:(w)=>J((Y)=>({...Y,message:w.target.value})),rows:4,placeholder:"通过 ClaudeQQ 推送一条 QQ 消息"})),m("button",{type:"submit",className:"primary-btn"},"发送 QQ 消息")),m("p",{className:"muted paragraph"},`主 server 和其他用户服务可通过 UniDesk 同源代理调用 /api/push/text;当前人工推送测试默认使用 ${Dy.label} ${Dy.userId},不需要暴露 D601 后端端口。`)),m(xl,{title:"QQ 事件订阅",eyebrow:"Webhook Subscription"},m("form",{className:"stack-form",onSubmit:H,"data-testid":"claudeqq-subscription-form"},m("label",null,"订阅名称",m("input",{value:F.name,onChange:(w)=>A((Y)=>({...Y,name:w.target.value}))})),m("label",null,"回调 URL",m("input",{value:F.targetUrl,onChange:(w)=>A((Y)=>({...Y,targetUrl:w.target.value})),placeholder:"http://host.docker.internal:18080/..."})),m("label",null,"事件类型",m("input",{value:F.eventTypes,onChange:(w)=>A((Y)=>({...Y,eventTypes:w.target.value})),placeholder:"message,notice"})),m("label",null,"签名密钥",m("input",{value:F.secret,onChange:(w)=>A((Y)=>({...Y,secret:w.target.value})),placeholder:"可选,生成 x-claudeqq-signature"})),m("button",{type:"submit",className:"primary-btn"},"创建订阅")),C.length===0?m(T$,{title:"暂无订阅",text:"可以为 main server 或其他用户服务注册 HTTP webhook"}):m("div",{className:"table-wrap","data-testid":"claudeqq-subscription-table"},m("table",null,m("thead",null,m("tr",null,m("th",null,"名称"),m("th",null,"状态"),m("th",null,"事件"),m("th",null,"回调"),m("th",null,"最近投递"),m("th",null,"操作"))),m("tbody",null,C.map((w)=>m("tr",{key:w.id},m("td",null,m("strong",null,w.name||w.id),m("code",null,w.id||"--")),m("td",null,m(Z4,{status:w.enabled?"online":"warn"},w.enabled?"enabled":"disabled")),m("td",null,Array.isArray(w.eventTypes)?w.eventTypes.join(", "):"message"),m("td",null,w.targetUrl||"--"),m("td",null,w.lastDelivery?`${w.lastDelivery.ok?"OK":"FAIL"} ${zf(w.lastDelivery.at)}`:"--"),m("td",null,m("button",{type:"button",className:"ghost-btn",onClick:()=>O(w.id)},"删除"))))))),m("div",{className:"panel-actions inline-actions"},m(w$,{title:"ClaudeQQ Subscriptions",data:l.subscriptions,onOpen:u,testId:"raw-claudeqq-subscriptions"}))),m(xl,{title:"最近 QQ 事件",eyebrow:l.refreshedAt?`Updated ${L0(l.refreshedAt)}`:"Event Stream"},v.length===0?m(T$,{title:"暂无事件",text:"等待 NapCat WebSocket 上报 QQ 消息事件,或通过订阅 API 消费后续事件"}):m("div",{className:"table-wrap","data-testid":"claudeqq-event-list"},m("table",null,m("thead",null,m("tr",null,m("th",null,"时间"),m("th",null,"类型"),m("th",null,"会话"),m("th",null,"消息"),m("th",null,"ID"))),m("tbody",null,v.map((w)=>m("tr",{key:w.id},m("td",null,zf(w.receivedAt||w.timestamp)),m("td",null,m(Z4,{status:w.postType||w.eventType},w.postType||w.eventType||"--")),m("td",null,UG(w)),m("td",null,AG(w)),m("td",null,m("code",null,w.messageId||w.id||"--"))))))),m("div",{className:"panel-actions inline-actions"},m(w$,{title:"ClaudeQQ Events",data:l.events,onOpen:u,testId:"raw-claudeqq-events"}))),m(xl,{title:"已发送消息",eyebrow:`${S.length} Sent`},S.length===0?m(T$,{title:"暂无发送记录",text:"发送日志来自 ClaudeQQ bot_workspace/messages/sent_messages.jsonl"}):m("div",{className:"table-wrap"},m("table",null,m("thead",null,m("tr",null,m("th",null,"时间"),m("th",null,"目标"),m("th",null,"消息"),m("th",null,"结果"))),m("tbody",null,S.map((w,Y)=>m("tr",{key:w.id||Y},m("td",null,zf(w.timestamp||w.sentAt||w.createdAt)),m("td",null,UG(w)),m("td",null,AG(w)),m("td",null,w.status||w.messageId||w.message_id||"--")))))),m("div",{className:"panel-actions inline-actions"},m(w$,{title:"ClaudeQQ Sent Messages",data:l.sent,onOpen:u,testId:"raw-claudeqq-sent"})))))}var T4=Sf(c0(),1);var jj=Sf(c0(),1);var Lf=jj.default.createElement,{useEffect:vO,useRef:GG}=jj.default;function bO(f,u){return TG(f.toTrace(u))}function hO(f){let u=Number(f);if(!Number.isFinite(u)||u<0)return"--";let _=Math.floor(u/1000),y=Math.floor(_/3600),l=Math.floor(_%3600/60),$=_%60;if(y>0)return`${y}h ${String(l).padStart(2,"0")}m`;if(l>0)return`${l}m ${String($).padStart(2,"0")}s`;return`${$}s`}function wy(f){let u=Number(f);return Number.isFinite(u)&&u>=0?u:null}function OG(f,u=180){let _=String(f||"").replace(/\s+/gu," ").trim();return _.length>u?`${_.slice(0,u-1)}…`:_}function IO(f){if(!f)return 0;return f.split(/\r?\n/u).length}function _j(f){return{ran:"Ran",explored:"Explored",edited:"Edited",toolGroup:"Tool calls",plan:"Plan",message:"Message",system:"System",error:"Error"}[f]||"Message"}function yj(f){let u=Number(f||0);return Number.isFinite(u)&&u>0?`… +${Math.floor(u)} lines`:""}function cO(f){return(Array.isArray(f)?f:[]).reduce((u,_)=>Math.max(u,Number(_?.seq??0)),0)}function zG(f){return["explored","edited","ran"].includes(String(f?.kind||""))}function XG(f){let u={read:0,edit:0,run:0};for(let _ of f){let y=String(_?.kind||"");if(y==="explored")u.read+=1;else if(y==="edited")u.edit+=1;else if(y==="ran")u.run+=1}return u}function NG(f){let u=XG(f);return`${u.read} read, ${u.edit} edit, ${u.run} run`}function LG(f){return f.replace(/^['"`([{<]+/u,"").replace(/['"`)\]}>.,;:]+$/u,"").replace(/:\d+(?::\d+)?$/u,"").trim()}function KG(f){let _=String(f||"").match(/(?:~|\.{1,2}|\/)?(?:[A-Za-z0-9_.@+-]+\/)+[A-Za-z0-9_.@+-]+|[A-Za-z0-9_.@+-]+\.(?:c|cc|cpp|h|hpp|js|jsx|ts|tsx|json|md|py|sh|toml|ya?ml|txt|log|lock)/gu)||[],y=[];for(let l of _){let $=LG(l);if($.length<2||$.includes("..."))continue;if(/^(http|https|status|method)$/iu.test($))continue;if(!y.includes($))y.push($)}return y}function d7(f,u=4){if(f.length===0)return"--";let _=f.slice(0,u).join(", ");return f.length>u?`${_} +${f.length-u}`:_}function ZG(f){let u="";for(let _ of f){if(_.length===0)continue;if(u.length>0&&!u.endsWith(` -`)&&!_.startsWith(` +(()=>{var oH=Object.create;var{getPrototypeOf:aH,defineProperty:SJ,getOwnPropertyNames:dH}=Object;var eH=Object.prototype.hasOwnProperty;function fO(f){return this[f]}var uO,lO,cf=(f,u,l)=>{var y=f!=null&&typeof f==="object";if(y){var r=u?uO??=new WeakMap:lO??=new WeakMap,_=r.get(f);if(_)return _}l=f!=null?oH(aH(f)):{};let $=u||!f||!f.__esModule?SJ(l,"default",{value:f,enumerable:!0}):l;for(let j of dH(f))if(!eH.call($,j))SJ($,j,{get:fO.bind(f,j),enumerable:!0});if(y)r.set(f,$);return $};var h0=(f,u)=>()=>(u||f((u={exports:{}}).exports,u),u.exports);var sf=((f)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(f,{get:(u,l)=>(typeof require<"u"?require:u)[l]}):f)(function(f){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+f+'" is not supported')});var IJ=h0((pf)=>{var J$=Symbol.for("react.element"),yO=Symbol.for("react.portal"),rO=Symbol.for("react.fragment"),_O=Symbol.for("react.strict_mode"),$O=Symbol.for("react.profiler"),jO=Symbol.for("react.provider"),AO=Symbol.for("react.context"),FO=Symbol.for("react.forward_ref"),JO=Symbol.for("react.suspense"),UO=Symbol.for("react.memo"),QO=Symbol.for("react.lazy"),PJ=Symbol.iterator;function WO(f){if(f===null||typeof f!=="object")return null;return f=PJ&&f[PJ]||f["@@iterator"],typeof f==="function"?f:null}var iJ={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},RJ=Object.assign,xJ={};function nr(f,u,l){this.props=f,this.context=u,this.refs=xJ,this.updater=l||iJ}nr.prototype.isReactComponent={};nr.prototype.setState=function(f,u){if(typeof f!=="object"&&typeof f!=="function"&&f!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,f,u,"setState")};nr.prototype.forceUpdate=function(f){this.updater.enqueueForceUpdate(this,f,"forceUpdate")};function vJ(){}vJ.prototype=nr.prototype;function z5(f,u,l){this.props=f,this.context=u,this.refs=xJ,this.updater=l||iJ}var G5=z5.prototype=new vJ;G5.constructor=z5;RJ(G5,nr.prototype);G5.isPureReactComponent=!0;var CJ=Array.isArray,bJ=Object.prototype.hasOwnProperty,K5={current:null},hJ={key:!0,ref:!0,__self:!0,__source:!0};function mJ(f,u,l){var y,r={},_=null,$=null;if(u!=null)for(y in u.ref!==void 0&&($=u.ref),u.key!==void 0&&(_=""+u.key),u)bJ.call(u,y)&&!hJ.hasOwnProperty(y)&&(r[y]=u[y]);var j=arguments.length-2;if(j===1)r.children=l;else if(1{gJ.exports=IJ()});var lU=h0((Qu)=>{function O5(f,u){var l=f.length;f.push(u);f:for(;0>>1,r=f[y];if(0>>1;y<_;){var $=2*(y+1)-1,j=f[$],A=$+1,F=f[A];if(0>i6(j,l))Ai6(F,j)?(f[y]=F,f[A]=l,y=A):(f[y]=j,f[$]=l,y=$);else if(Ai6(F,l))f[y]=F,f[A]=l,y=A;else break f}}return u}function i6(f,u){var l=f.sortIndex-u.sortIndex;return l!==0?l:f.id-u.id}if(typeof performance==="object"&&typeof performance.now==="function")q5=performance,Qu.unstable_now=function(){return q5.now()};else R6=Date,V5=R6.now(),Qu.unstable_now=function(){return R6.now()-V5};var q5,R6,V5,bl=[],S1=[],OO=1,$l=null,j0=3,h6=!1,cy=!1,Q$=!1,aJ=typeof setTimeout==="function"?setTimeout:null,dJ=typeof clearTimeout==="function"?clearTimeout:null,oJ=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function L5(f){for(var u=Vl(S1);u!==null;){if(u.callback===null)b6(S1);else if(u.startTime<=f)b6(S1),u.sortIndex=u.expirationTime,O5(bl,u);else break;u=Vl(S1)}}function X5(f){if(Q$=!1,L5(f),!cy)if(Vl(bl)!==null)cy=!0,w5(Y5);else{var u=Vl(S1);u!==null&&D5(X5,u.startTime-f)}}function Y5(f,u){cy=!1,Q$&&(Q$=!1,dJ(W$),W$=-1),h6=!0;var l=j0;try{L5(u);for($l=Vl(bl);$l!==null&&(!($l.expirationTime>u)||f&&!uU());){var y=$l.callback;if(typeof y==="function"){$l.callback=null,j0=$l.priorityLevel;var r=y($l.expirationTime<=u);u=Qu.unstable_now(),typeof r==="function"?$l.callback=r:$l===Vl(bl)&&b6(bl),L5(u)}else b6(bl);$l=Vl(bl)}if($l!==null)var _=!0;else{var $=Vl(S1);$!==null&&D5(X5,$.startTime-u),_=!1}return _}finally{$l=null,j0=l,h6=!1}}var m6=!1,x6=null,W$=-1,eJ=5,fU=-1;function uU(){return Qu.unstable_now()-fUf||125y?(f.sortIndex=l,O5(S1,f),Vl(bl)===null&&f===Vl(S1)&&(Q$?(dJ(W$),W$=-1):Q$=!0,D5(X5,l-y))):(f.sortIndex=r,O5(bl,f),cy||h6||(cy=!0,w5(Y5))),f};Qu.unstable_shouldYield=uU;Qu.unstable_wrapCallback=function(f){var u=j0;return function(){var l=j0;j0=u;try{return f.apply(this,arguments)}finally{j0=l}}}});var rU=h0((sP,yU)=>{yU.exports=lU()});var $z=h0((t0)=>{var qO=Yu(),g0=rU();function Wf(f){for(var u="https://reactjs.org/docs/error-decoder.html?invariant="+f,l=1;l"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),a5=Object.prototype.hasOwnProperty,VO=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,_U={},$U={};function LO(f){if(a5.call($U,f))return!0;if(a5.call(_U,f))return!1;if(VO.test(f))return $U[f]=!0;return _U[f]=!0,!1}function BO(f,u,l,y){if(l!==null&&l.type===0)return!1;switch(typeof u){case"function":case"symbol":return!0;case"boolean":if(y)return!1;if(l!==null)return!l.acceptsBooleans;return f=f.toLowerCase().slice(0,5),f!=="data-"&&f!=="aria-";default:return!1}}function XO(f,u,l,y){if(u===null||typeof u>"u"||BO(f,u,l,y))return!0;if(y)return!1;if(l!==null)switch(l.type){case 3:return!u;case 4:return u===!1;case 5:return isNaN(u);case 6:return isNaN(u)||1>u}return!1}function O0(f,u,l,y,r,_,$){this.acceptsBooleans=u===2||u===3||u===4,this.attributeName=y,this.attributeNamespace=r,this.mustUseProperty=l,this.propertyName=f,this.type=u,this.sanitizeURL=_,this.removeEmptyString=$}var l0={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(f){l0[f]=new O0(f,0,!1,f,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(f){var u=f[0];l0[u]=new O0(u,1,!1,f[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(f){l0[f]=new O0(f,2,!1,f.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(f){l0[f]=new O0(f,2,!1,f,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(f){l0[f]=new O0(f,3,!1,f.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(f){l0[f]=new O0(f,3,!0,f,null,!1,!1)});["capture","download"].forEach(function(f){l0[f]=new O0(f,4,!1,f,null,!1,!1)});["cols","rows","size","span"].forEach(function(f){l0[f]=new O0(f,6,!1,f,null,!1,!1)});["rowSpan","start"].forEach(function(f){l0[f]=new O0(f,5,!1,f.toLowerCase(),null,!1,!1)});var I9=/[\-:]([a-z])/g;function g9(f){return f[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(f){var u=f.replace(I9,g9);l0[u]=new O0(u,1,!1,f,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(f){var u=f.replace(I9,g9);l0[u]=new O0(u,1,!1,f,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(f){var u=f.replace(I9,g9);l0[u]=new O0(u,1,!1,f,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(f){l0[f]=new O0(f,1,!1,f.toLowerCase(),null,!1,!1)});l0.xlinkHref=new O0("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(f){l0[f]=new O0(f,1,!1,f.toLowerCase(),null,!0,!0)});function k9(f,u,l,y){var r=l0.hasOwnProperty(u)?l0[u]:null;if(r!==null?r.type!==0:y||!(2j||r[$]!==_[j]){var A=` +`+r[$].replace(" at new "," at ");return f.displayName&&A.includes("")&&(A=A.replace("",f.displayName)),A}while(1<=$&&0<=j);break}}}finally{n5=!1,Error.prepareStackTrace=l}return(f=f?f.displayName||f.name:"")?O$(f):""}function YO(f){switch(f.tag){case 5:return O$(f.type);case 16:return O$("Lazy");case 13:return O$("Suspense");case 19:return O$("SuspenseList");case 0:case 2:case 15:return f=M5(f.type,!1),f;case 11:return f=M5(f.type.render,!1),f;case 1:return f=M5(f.type,!0),f;default:return""}}function u9(f){if(f==null)return null;if(typeof f==="function")return f.displayName||f.name||null;if(typeof f==="string")return f;switch(f){case Cr:return"Fragment";case Pr:return"Portal";case d5:return"Profiler";case t9:return"StrictMode";case e5:return"Suspense";case f9:return"SuspenseList"}if(typeof f==="object")switch(f.$$typeof){case UQ:return(f.displayName||"Context")+".Consumer";case JQ:return(f._context.displayName||"Context")+".Provider";case s9:var u=f.render;return f=f.displayName,f||(f=u.displayName||u.name||"",f=f!==""?"ForwardRef("+f+")":"ForwardRef"),f;case o9:return u=f.displayName||null,u!==null?u:u9(f.type)||"Memo";case C1:u=f._payload,f=f._init;try{return u9(f(u))}catch(l){}}return null}function wO(f){var u=f.type;switch(f.tag){case 24:return"Cache";case 9:return(u.displayName||"Context")+".Consumer";case 10:return(u._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return f=u.render,f=f.displayName||f.name||"",u.displayName||(f!==""?"ForwardRef("+f+")":"ForwardRef");case 7:return"Fragment";case 5:return u;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return u9(u);case 8:return u===t9?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof u==="function")return u.displayName||u.name||null;if(typeof u==="string")return u}return null}function s1(f){switch(typeof f){case"boolean":case"number":case"string":case"undefined":return f;case"object":return f;default:return""}}function WQ(f){var u=f.type;return(f=f.nodeName)&&f.toLowerCase()==="input"&&(u==="checkbox"||u==="radio")}function DO(f){var u=WQ(f)?"checked":"value",l=Object.getOwnPropertyDescriptor(f.constructor.prototype,u),y=""+f[u];if(!f.hasOwnProperty(u)&&typeof l<"u"&&typeof l.get==="function"&&typeof l.set==="function"){var{get:r,set:_}=l;return Object.defineProperty(f,u,{configurable:!0,get:function(){return r.call(this)},set:function($){y=""+$,_.call(this,$)}}),Object.defineProperty(f,u,{enumerable:l.enumerable}),{getValue:function(){return y},setValue:function($){y=""+$},stopTracking:function(){f._valueTracker=null,delete f[u]}}}}function I6(f){f._valueTracker||(f._valueTracker=DO(f))}function zQ(f){if(!f)return!1;var u=f._valueTracker;if(!u)return!0;var l=u.getValue(),y="";return f&&(y=WQ(f)?f.checked?"true":"false":f.value),f=y,f!==l?(u.setValue(f),!0):!1}function K4(f){if(f=f||(typeof document<"u"?document:void 0),typeof f>"u")return null;try{return f.activeElement||f.body}catch(u){return f.body}}function l9(f,u){var l=u.checked;return Lu({},u,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:l!=null?l:f._wrapperState.initialChecked})}function AU(f,u){var l=u.defaultValue==null?"":u.defaultValue,y=u.checked!=null?u.checked:u.defaultChecked;l=s1(u.value!=null?u.value:l),f._wrapperState={initialChecked:y,initialValue:l,controlled:u.type==="checkbox"||u.type==="radio"?u.checked!=null:u.value!=null}}function GQ(f,u){u=u.checked,u!=null&&k9(f,"checked",u,!1)}function y9(f,u){GQ(f,u);var l=s1(u.value),y=u.type;if(l!=null)if(y==="number"){if(l===0&&f.value===""||f.value!=l)f.value=""+l}else f.value!==""+l&&(f.value=""+l);else if(y==="submit"||y==="reset"){f.removeAttribute("value");return}u.hasOwnProperty("value")?r9(f,u.type,l):u.hasOwnProperty("defaultValue")&&r9(f,u.type,s1(u.defaultValue)),u.checked==null&&u.defaultChecked!=null&&(f.defaultChecked=!!u.defaultChecked)}function FU(f,u,l){if(u.hasOwnProperty("value")||u.hasOwnProperty("defaultValue")){var y=u.type;if(!(y!=="submit"&&y!=="reset"||u.value!==void 0&&u.value!==null))return;u=""+f._wrapperState.initialValue,l||u===f.value||(f.value=u),f.defaultValue=u}l=f.name,l!==""&&(f.name=""),f.defaultChecked=!!f._wrapperState.initialChecked,l!==""&&(f.name=l)}function r9(f,u,l){if(u!=="number"||K4(f.ownerDocument)!==f)l==null?f.defaultValue=""+f._wrapperState.initialValue:f.defaultValue!==""+l&&(f.defaultValue=""+l)}var q$=Array.isArray;function gr(f,u,l,y){if(f=f.options,u){u={};for(var r=0;r"+u.valueOf().toString()+"";for(u=g6.firstChild;f.firstChild;)f.removeChild(f.firstChild);for(;u.firstChild;)f.appendChild(u.firstChild)}});function x$(f,u){if(u){var l=f.firstChild;if(l&&l===f.lastChild&&l.nodeType===3){l.nodeValue=u;return}}f.textContent=u}var D$={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},TO=["Webkit","ms","Moz","O"];Object.keys(D$).forEach(function(f){TO.forEach(function(u){u=u+f.charAt(0).toUpperCase()+f.substring(1),D$[u]=D$[f]})});function EQ(f,u,l){return u==null||typeof u==="boolean"||u===""?"":l||typeof u!=="number"||u===0||D$.hasOwnProperty(f)&&D$[f]?(""+u).trim():u+"px"}function HQ(f,u){f=f.style;for(var l in u)if(u.hasOwnProperty(l)){var y=l.indexOf("--")===0,r=EQ(l,u[l],y);l==="float"&&(l="cssFloat"),y?f.setProperty(l,r):f[l]=r}}var nO=Lu({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function j9(f,u){if(u){if(nO[f]&&(u.children!=null||u.dangerouslySetInnerHTML!=null))throw Error(Wf(137,f));if(u.dangerouslySetInnerHTML!=null){if(u.children!=null)throw Error(Wf(60));if(typeof u.dangerouslySetInnerHTML!=="object"||!("__html"in u.dangerouslySetInnerHTML))throw Error(Wf(61))}if(u.style!=null&&typeof u.style!=="object")throw Error(Wf(62))}}function A9(f,u){if(f.indexOf("-")===-1)return typeof u.is==="string";switch(f){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var F9=null;function a9(f){return f=f.target||f.srcElement||window,f.correspondingUseElement&&(f=f.correspondingUseElement),f.nodeType===3?f.parentNode:f}var J9=null,kr=null,tr=null;function QU(f){if(f=y3(f)){if(typeof J9!=="function")throw Error(Wf(280));var u=f.stateNode;u&&(u=p4(u),J9(f.stateNode,f.type,u))}}function OQ(f){kr?tr?tr.push(f):tr=[f]:kr=f}function qQ(){if(kr){var f=kr,u=tr;if(tr=kr=null,QU(f),u)for(f=0;f>>=0,f===0?32:31-(hO(f)/mO|0)|0}var k6=64,t6=4194304;function V$(f){switch(f&-f){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return f&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return f&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return f}}function H4(f,u){var l=f.pendingLanes;if(l===0)return 0;var y=0,r=f.suspendedLanes,_=f.pingedLanes,$=l&268435455;if($!==0){var j=$&~r;j!==0?y=V$(j):(_&=$,_!==0&&(y=V$(_)))}else $=l&~r,$!==0?y=V$($):_!==0&&(y=V$(_));if(y===0)return 0;if(u!==0&&u!==y&&(u&r)===0&&(r=y&-y,_=u&-u,r>=_||r===16&&(_&4194240)!==0))return u;if((y&4)!==0&&(y|=l&16),u=f.entangledLanes,u!==0)for(f=f.entanglements,u&=y;0l;l++)u.push(f);return u}function u3(f,u,l){f.pendingLanes|=u,u!==536870912&&(f.suspendedLanes=0,f.pingedLanes=0),f=f.eventTimes,u=31-wl(u),f[u]=l}function kO(f,u){var l=f.pendingLanes&~u;f.pendingLanes=u,f.suspendedLanes=0,f.pingedLanes=0,f.expiredLanes&=u,f.mutableReadLanes&=u,f.entangledLanes&=u,u=f.entanglements;var y=f.eventTimes;for(f=f.expirationTimes;0=n$),OU=String.fromCharCode(32),qU=!1;function hQ(f,u){switch(f){case"keyup":return Oq.indexOf(u.keyCode)!==-1;case"keydown":return u.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function mQ(f){return f=f.detail,typeof f==="object"&&"data"in f?f.data:null}var cr=!1;function Vq(f,u){switch(f){case"compositionend":return mQ(u);case"keypress":if(u.which!==32)return null;return qU=!0,OU;case"textInput":return f=u.data,f===OU&&qU?null:f;default:return null}}function Lq(f,u){if(cr)return f==="compositionend"||!_7&&hQ(f,u)?(f=vQ(),$4=l7=x1=null,cr=!1,f):null;switch(f){case"paste":return null;case"keypress":if(!(u.ctrlKey||u.altKey||u.metaKey)||u.ctrlKey&&u.altKey){if(u.char&&1=u)return{node:l,offset:u-f};f=y}f:{for(;l;){if(l.nextSibling){l=l.nextSibling;break f}l=l.parentNode}l=void 0}l=BU(l)}}function kQ(f,u){return f&&u?f===u?!0:f&&f.nodeType===3?!1:u&&u.nodeType===3?kQ(f,u.parentNode):("contains"in f)?f.contains(u):f.compareDocumentPosition?!!(f.compareDocumentPosition(u)&16):!1:!1}function tQ(){for(var f=window,u=K4();u instanceof f.HTMLIFrameElement;){try{var l=typeof u.contentWindow.location.href==="string"}catch(y){l=!1}if(l)f=u.contentWindow;else break;u=K4(f.document)}return u}function $7(f){var u=f&&f.nodeName&&f.nodeName.toLowerCase();return u&&(u==="input"&&(f.type==="text"||f.type==="search"||f.type==="tel"||f.type==="url"||f.type==="password")||u==="textarea"||f.contentEditable==="true")}function Sq(f){var u=tQ(),l=f.focusedElem,y=f.selectionRange;if(u!==l&&l&&l.ownerDocument&&kQ(l.ownerDocument.documentElement,l)){if(y!==null&&$7(l)){if(u=y.start,f=y.end,f===void 0&&(f=u),"selectionStart"in l)l.selectionStart=u,l.selectionEnd=Math.min(f,l.value.length);else if(f=(u=l.ownerDocument||document)&&u.defaultView||window,f.getSelection){f=f.getSelection();var r=l.textContent.length,_=Math.min(y.start,r);y=y.end===void 0?_:Math.min(y.end,r),!f.extend&&_>y&&(r=y,y=_,_=r),r=XU(l,_);var $=XU(l,y);r&&$&&(f.rangeCount!==1||f.anchorNode!==r.node||f.anchorOffset!==r.offset||f.focusNode!==$.node||f.focusOffset!==$.offset)&&(u=u.createRange(),u.setStart(r.node,r.offset),f.removeAllRanges(),_>y?(f.addRange(u),f.extend($.node,$.offset)):(u.setEnd($.node,$.offset),f.addRange(u)))}}u=[];for(f=l;f=f.parentNode;)f.nodeType===1&&u.push({element:f,left:f.scrollLeft,top:f.scrollTop});typeof l.focus==="function"&&l.focus();for(l=0;l=document.documentMode,ir=null,K9=null,S$=null,N9=!1;function YU(f,u,l){var y=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;N9||ir==null||ir!==K4(y)||(y=ir,("selectionStart"in y)&&$7(y)?y={start:y.selectionStart,end:y.selectionEnd}:(y=(y.ownerDocument&&y.ownerDocument.defaultView||window).getSelection(),y={anchorNode:y.anchorNode,anchorOffset:y.anchorOffset,focusNode:y.focusNode,focusOffset:y.focusOffset}),S$&&I$(S$,y)||(S$=y,y=V4(K9,"onSelect"),0vr||(f.current=B9[vr],B9[vr]=null,vr--)}function Wu(f,u){vr++,B9[vr]=f.current,f.current=u}var o1={},U0=d1(o1),D0=d1(!1),Iy=o1;function er(f,u){var l=f.type.contextTypes;if(!l)return o1;var y=f.stateNode;if(y&&y.__reactInternalMemoizedUnmaskedChildContext===u)return y.__reactInternalMemoizedMaskedChildContext;var r={},_;for(_ in l)r[_]=u[_];return y&&(f=f.stateNode,f.__reactInternalMemoizedUnmaskedChildContext=u,f.__reactInternalMemoizedMaskedChildContext=r),r}function T0(f){return f=f.childContextTypes,f!==null&&f!==void 0}function B4(){Nu(D0),Nu(U0)}function PU(f,u,l){if(U0.current!==o1)throw Error(Wf(168));Wu(U0,u),Wu(D0,l)}function yW(f,u,l){var y=f.stateNode;if(u=u.childContextTypes,typeof y.getChildContext!=="function")return l;y=y.getChildContext();for(var r in y)if(!(r in u))throw Error(Wf(108,wO(f)||"Unknown",r));return Lu({},l,y)}function X4(f){return f=(f=f.stateNode)&&f.__reactInternalMemoizedMergedChildContext||o1,Iy=U0.current,Wu(U0,f),Wu(D0,D0.current),!0}function CU(f,u,l){var y=f.stateNode;if(!y)throw Error(Wf(169));l?(f=yW(f,u,Iy),y.__reactInternalMemoizedMergedChildContext=f,Nu(D0),Nu(U0),Wu(U0,f)):Nu(D0),Wu(D0,l)}var j1=null,I4=!1,h5=!1;function rW(f){j1===null?j1=[f]:j1.push(f)}function mq(f){I4=!0,rW(f)}function e1(){if(!h5&&j1!==null){h5=!0;var f=0,u=ru;try{var l=j1;for(ru=1;f>=$,r-=$,A1=1<<32-wl(u)+r|l<X?(i=V,V=null):i=V.sibling;var m=W(z,V,N[X],H);if(m===null){V===null&&(V=i);break}f&&V&&m.alternate===null&&u(z,V),Z=_(m,Z,X),w===null?Y=m:w.sibling=m,w=m,V=i}if(X===N.length)return l(z,V),Eu&&Ry(z,X),Y;if(V===null){for(;XX?(i=V,V=null):i=V.sibling;var M=W(z,V,m.value,H);if(M===null){V===null&&(V=i);break}f&&V&&M.alternate===null&&u(z,V),Z=_(M,Z,X),w===null?Y=M:w.sibling=M,w=M,V=i}if(m.done)return l(z,V),Eu&&Ry(z,X),Y;if(V===null){for(;!m.done;X++,m=N.next())m=Q(z,m.value,H),m!==null&&(Z=_(m,Z,X),w===null?Y=m:w.sibling=m,w=m);return Eu&&Ry(z,X),Y}for(V=y(z,V);!m.done;X++,m=N.next())m=G(V,z,X,m.value,H),m!==null&&(f&&m.alternate!==null&&V.delete(m.key===null?X:m.key),Z=_(m,Z,X),w===null?Y=m:w.sibling=m,w=m);return f&&V.forEach(function(c){return u(z,c)}),Eu&&Ry(z,X),Y}function O(z,Z,N,H){if(typeof N==="object"&&N!==null&&N.type===Cr&&N.key===null&&(N=N.props.children),typeof N==="object"&&N!==null){switch(N.$$typeof){case p6:f:{for(var Y=N.key,w=Z;w!==null;){if(w.key===Y){if(Y=N.type,Y===Cr){if(w.tag===7){l(z,w.sibling),Z=r(w,N.props.children),Z.return=z,z=Z;break f}}else if(w.elementType===Y||typeof Y==="object"&&Y!==null&&Y.$$typeof===C1&&RU(Y)===w.type){l(z,w.sibling),Z=r(w,N.props),Z.ref=Z$(z,w,N),Z.return=z,z=Z;break f}l(z,w);break}else u(z,w);w=w.sibling}N.type===Cr?(Z=py(N.props.children,z.mode,H,N.key),Z.return=z,z=Z):(H=G4(N.type,N.key,N.props,null,z.mode,H),H.ref=Z$(z,Z,N),H.return=z,z=H)}return $(z);case Pr:f:{for(w=N.key;Z!==null;){if(Z.key===w)if(Z.tag===4&&Z.stateNode.containerInfo===N.containerInfo&&Z.stateNode.implementation===N.implementation){l(z,Z.sibling),Z=r(Z,N.children||[]),Z.return=z,z=Z;break f}else{l(z,Z);break}else u(z,Z);Z=Z.sibling}Z=o5(N,z.mode,H),Z.return=z,z=Z}return $(z);case C1:return w=N._init,O(z,Z,w(N._payload),H)}if(q$(N))return K(z,Z,N,H);if(z$(N))return E(z,Z,N,H);u4(z,N)}return typeof N==="string"&&N!==""||typeof N==="number"?(N=""+N,Z!==null&&Z.tag===6?(l(z,Z.sibling),Z=r(Z,N),Z.return=z,z=Z):(l(z,Z),Z=s5(N,z.mode,H),Z.return=z,z=Z),$(z)):l(z,Z)}return O}var u_=AW(!0),FW=AW(!1),D4=d1(null),T4=null,mr=null,J7=null;function U7(){J7=mr=T4=null}function Q7(f){var u=D4.current;Nu(D4),f._currentValue=u}function w9(f,u,l){for(;f!==null;){var y=f.alternate;if((f.childLanes&u)!==u?(f.childLanes|=u,y!==null&&(y.childLanes|=u)):y!==null&&(y.childLanes&u)!==u&&(y.childLanes|=u),f===l)break;f=f.return}}function or(f,u){T4=f,J7=mr=null,f=f.dependencies,f!==null&&f.firstContext!==null&&((f.lanes&u)!==0&&(w0=!0),f.firstContext=null)}function Ul(f){var u=f._currentValue;if(J7!==f)if(f={context:f,memoizedValue:u,next:null},mr===null){if(T4===null)throw Error(Wf(308));mr=f,T4.dependencies={lanes:0,firstContext:f}}else mr=mr.next=f;return u}var by=null;function W7(f){by===null?by=[f]:by.push(f)}function JW(f,u,l,y){var r=u.interleaved;return r===null?(l.next=l,W7(u)):(l.next=r.next,r.next=l),u.interleaved=l,W1(f,y)}function W1(f,u){f.lanes|=u;var l=f.alternate;l!==null&&(l.lanes|=u),l=f;for(f=f.return;f!==null;)f.childLanes|=u,l=f.alternate,l!==null&&(l.childLanes|=u),l=f,f=f.return;return l.tag===3?l.stateNode:null}var c1=!1;function z7(f){f.updateQueue={baseState:f.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function UW(f,u){f=f.updateQueue,u.updateQueue===f&&(u.updateQueue={baseState:f.baseState,firstBaseUpdate:f.firstBaseUpdate,lastBaseUpdate:f.lastBaseUpdate,shared:f.shared,effects:f.effects})}function J1(f,u){return{eventTime:f,lane:u,tag:0,payload:null,callback:null,next:null}}function I1(f,u,l){var y=f.updateQueue;if(y===null)return null;if(y=y.shared,(fu&2)!==0){var r=y.pending;return r===null?u.next=u:(u.next=r.next,r.next=u),y.pending=u,W1(f,l)}return r=y.interleaved,r===null?(u.next=u,W7(y)):(u.next=r.next,r.next=u),y.interleaved=u,W1(f,l)}function F4(f,u,l){if(u=u.updateQueue,u!==null&&(u=u.shared,(l&4194240)!==0)){var y=u.lanes;y&=f.pendingLanes,l|=y,u.lanes=l,e9(f,l)}}function xU(f,u){var{updateQueue:l,alternate:y}=f;if(y!==null&&(y=y.updateQueue,l===y)){var r=null,_=null;if(l=l.firstBaseUpdate,l!==null){do{var $={eventTime:l.eventTime,lane:l.lane,tag:l.tag,payload:l.payload,callback:l.callback,next:null};_===null?r=_=$:_=_.next=$,l=l.next}while(l!==null);_===null?r=_=u:_=_.next=u}else r=_=u;l={baseState:y.baseState,firstBaseUpdate:r,lastBaseUpdate:_,shared:y.shared,effects:y.effects},f.updateQueue=l;return}f=l.lastBaseUpdate,f===null?l.firstBaseUpdate=u:f.next=u,l.lastBaseUpdate=u}function n4(f,u,l,y){var r=f.updateQueue;c1=!1;var{firstBaseUpdate:_,lastBaseUpdate:$}=r,j=r.shared.pending;if(j!==null){r.shared.pending=null;var A=j,F=A.next;A.next=null,$===null?_=F:$.next=F,$=A;var U=f.alternate;U!==null&&(U=U.updateQueue,j=U.lastBaseUpdate,j!==$&&(j===null?U.firstBaseUpdate=F:j.next=F,U.lastBaseUpdate=A))}if(_!==null){var Q=r.baseState;$=0,U=F=A=null,j=_;do{var{lane:W,eventTime:G}=j;if((y&W)===W){U!==null&&(U=U.next={eventTime:G,lane:0,tag:j.tag,payload:j.payload,callback:j.callback,next:null});f:{var K=f,E=j;switch(W=u,G=l,E.tag){case 1:if(K=E.payload,typeof K==="function"){Q=K.call(G,Q,W);break f}Q=K;break f;case 3:K.flags=K.flags&-65537|128;case 0:if(K=E.payload,W=typeof K==="function"?K.call(G,Q,W):K,W===null||W===void 0)break f;Q=Lu({},Q,W);break f;case 2:c1=!0}}j.callback!==null&&j.lane!==0&&(f.flags|=64,W=r.effects,W===null?r.effects=[j]:W.push(j))}else G={eventTime:G,lane:W,tag:j.tag,payload:j.payload,callback:j.callback,next:null},U===null?(F=U=G,A=Q):U=U.next=G,$|=W;if(j=j.next,j===null)if(j=r.shared.pending,j===null)break;else W=j,j=W.next,W.next=null,r.lastBaseUpdate=W,r.shared.pending=null}while(1);if(U===null&&(A=Q),r.baseState=A,r.firstBaseUpdate=F,r.lastBaseUpdate=U,u=r.shared.interleaved,u!==null){r=u;do $|=r.lane,r=r.next;while(r!==u)}else _===null&&(r.shared.lanes=0);ty|=$,f.lanes=$,f.memoizedState=Q}}function vU(f,u,l){if(f=u.effects,u.effects=null,f!==null)for(u=0;ul?l:4,f(!0);var y=p5.transition;p5.transition={};try{f(!1),u()}finally{ru=l,p5.transition=y}}function wW(){return Ql().memoizedState}function kq(f,u,l){var y=k1(f);if(l={lane:y,action:l,hasEagerState:!1,eagerState:null,next:null},DW(f))TW(u,l);else if(l=JW(f,u,l,y),l!==null){var r=H0();Dl(l,f,y,r),nW(l,u,y)}}function tq(f,u,l){var y=k1(f),r={lane:y,action:l,hasEagerState:!1,eagerState:null,next:null};if(DW(f))TW(u,r);else{var _=f.alternate;if(f.lanes===0&&(_===null||_.lanes===0)&&(_=u.lastRenderedReducer,_!==null))try{var $=u.lastRenderedState,j=_($,l);if(r.hasEagerState=!0,r.eagerState=j,Tl(j,$)){var A=u.interleaved;A===null?(r.next=r,W7(u)):(r.next=A.next,A.next=r),u.interleaved=r;return}}catch(F){}finally{}l=JW(f,u,r,y),l!==null&&(r=H0(),Dl(l,f,y,r),nW(l,u,y))}}function DW(f){var u=f.alternate;return f===Vu||u!==null&&u===Vu}function TW(f,u){P$=S4=!0;var l=f.pending;l===null?u.next=u:(u.next=l.next,l.next=u),f.pending=u}function nW(f,u,l){if((l&4194240)!==0){var y=u.lanes;y&=f.pendingLanes,l|=y,u.lanes=l,e9(f,l)}}var P4={readContext:Ul,useCallback:A0,useContext:A0,useEffect:A0,useImperativeHandle:A0,useInsertionEffect:A0,useLayoutEffect:A0,useMemo:A0,useReducer:A0,useRef:A0,useState:A0,useDebugValue:A0,useDeferredValue:A0,useTransition:A0,useMutableSource:A0,useSyncExternalStore:A0,useId:A0,unstable_isNewReconciler:!1},sq={readContext:Ul,useCallback:function(f,u){return ml().memoizedState=[f,u===void 0?null:u],f},useContext:Ul,useEffect:hU,useImperativeHandle:function(f,u,l){return l=l!==null&&l!==void 0?l.concat([f]):null,U4(4194308,4,VW.bind(null,u,f),l)},useLayoutEffect:function(f,u){return U4(4194308,4,f,u)},useInsertionEffect:function(f,u){return U4(4,2,f,u)},useMemo:function(f,u){var l=ml();return u=u===void 0?null:u,f=f(),l.memoizedState=[f,u],f},useReducer:function(f,u,l){var y=ml();return u=l!==void 0?l(u):u,y.memoizedState=y.baseState=u,f={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:f,lastRenderedState:u},y.queue=f,f=f.dispatch=kq.bind(null,Vu,f),[y.memoizedState,f]},useRef:function(f){var u=ml();return f={current:f},u.memoizedState=f},useState:bU,useDebugValue:q7,useDeferredValue:function(f){return ml().memoizedState=f},useTransition:function(){var f=bU(!1),u=f[0];return f=gq.bind(null,f[1]),ml().memoizedState=f,[u,f]},useMutableSource:function(){},useSyncExternalStore:function(f,u,l){var y=Vu,r=ml();if(Eu){if(l===void 0)throw Error(Wf(407));l=l()}else{if(l=u(),tu===null)throw Error(Wf(349));(ky&30)!==0||GW(y,u,l)}r.memoizedState=l;var _={value:l,getSnapshot:u};return r.queue=_,hU(NW.bind(null,y,_,f),[f]),y.flags|=2048,e$(9,KW.bind(null,y,_,l,u),void 0,null),l},useId:function(){var f=ml(),u=tu.identifierPrefix;if(Eu){var l=F1,y=A1;l=(y&~(1<<32-wl(y)-1)).toString(32)+l,u=":"+u+"R"+l,l=a$++,0",f=f.removeChild(f.firstChild)):typeof y.is==="string"?f=$.createElement(l,{is:y.is}):(f=$.createElement(l),l==="select"&&($=f,y.multiple?$.multiple=!0:y.size&&($.size=y.size))):f=$.createElementNS(f,l),f[pl]=u,f[t$]=y,bW(f,u,!1,!1),u.stateNode=f;f:{switch($=A9(l,y),l){case"dialog":Ku("cancel",f),Ku("close",f),r=y;break;case"iframe":case"object":case"embed":Ku("load",f),r=y;break;case"video":case"audio":for(r=0;rr_&&(u.flags|=128,y=!0,E$(_,!1),u.lanes=4194304)}else{if(!y)if(f=M4($),f!==null){if(u.flags|=128,y=!0,l=f.updateQueue,l!==null&&(u.updateQueue=l,u.flags|=4),E$(_,!0),_.tail===null&&_.tailMode==="hidden"&&!$.alternate&&!Eu)return F0(u),null}else 2*Su()-_.renderingStartTime>r_&&l!==1073741824&&(u.flags|=128,y=!0,E$(_,!1),u.lanes=4194304);_.isBackwards?($.sibling=u.child,u.child=$):(l=_.last,l!==null?l.sibling=$:u.child=$,_.last=$)}if(_.tail!==null)return u=_.tail,_.rendering=u,_.tail=u.sibling,_.renderingStartTime=Su(),u.sibling=null,l=qu.current,Wu(qu,y?l&1|2:l&1),u;return F0(u),null;case 22:case 23:return w7(),y=u.memoizedState!==null,f!==null&&f.memoizedState!==null!==y&&(u.flags|=8192),y&&(u.mode&1)!==0?(m0&1073741824)!==0&&(F0(u),u.subtreeFlags&6&&(u.flags|=8192)):F0(u),null;case 24:return null;case 25:return null}throw Error(Wf(156,u.tag))}function yV(f,u){switch(A7(u),u.tag){case 1:return T0(u.type)&&B4(),f=u.flags,f&65536?(u.flags=f&-65537|128,u):null;case 3:return l_(),Nu(D0),Nu(U0),N7(),f=u.flags,(f&65536)!==0&&(f&128)===0?(u.flags=f&-65537|128,u):null;case 5:return K7(u),null;case 13:if(Nu(qu),f=u.memoizedState,f!==null&&f.dehydrated!==null){if(u.alternate===null)throw Error(Wf(340));f_()}return f=u.flags,f&65536?(u.flags=f&-65537|128,u):null;case 19:return Nu(qu),null;case 4:return l_(),null;case 10:return Q7(u.type._context),null;case 22:case 23:return w7(),null;case 24:return null;default:return null}}var y4=!1,J0=!1,rV=typeof WeakSet==="function"?WeakSet:Set,Vf=null;function pr(f,u){var l=f.ref;if(l!==null)if(typeof l==="function")try{l(null)}catch(y){wu(f,u,y)}else l.current=null}function i9(f,u,l){try{l()}catch(y){wu(f,u,y)}}var eU=!1;function _V(f,u){if(H9=O4,f=tQ(),$7(f)){if("selectionStart"in f)var l={start:f.selectionStart,end:f.selectionEnd};else f:{l=(l=f.ownerDocument)&&l.defaultView||window;var y=l.getSelection&&l.getSelection();if(y&&y.rangeCount!==0){l=y.anchorNode;var{anchorOffset:r,focusNode:_}=y;y=y.focusOffset;try{l.nodeType,_.nodeType}catch(H){l=null;break f}var $=0,j=-1,A=-1,F=0,U=0,Q=f,W=null;u:for(;;){for(var G;;){if(Q!==l||r!==0&&Q.nodeType!==3||(j=$+r),Q!==_||y!==0&&Q.nodeType!==3||(A=$+y),Q.nodeType===3&&($+=Q.nodeValue.length),(G=Q.firstChild)===null)break;W=Q,Q=G}for(;;){if(Q===f)break u;if(W===l&&++F===r&&(j=$),W===_&&++U===y&&(A=$),(G=Q.nextSibling)!==null)break;Q=W,W=Q.parentNode}Q=G}l=j===-1||A===-1?null:{start:j,end:A}}else l=null}l=l||{start:0,end:0}}else l=null;O9={focusedElem:f,selectionRange:l},O4=!1;for(Vf=u;Vf!==null;)if(u=Vf,f=u.child,(u.subtreeFlags&1028)!==0&&f!==null)f.return=u,Vf=f;else for(;Vf!==null;){u=Vf;try{var K=u.alternate;if((u.flags&1024)!==0)switch(u.tag){case 0:case 11:case 15:break;case 1:if(K!==null){var{memoizedProps:E,memoizedState:O}=K,z=u.stateNode,Z=z.getSnapshotBeforeUpdate(u.elementType===u.type?E:Bl(u.type,E),O);z.__reactInternalSnapshotBeforeUpdate=Z}break;case 3:var N=u.stateNode.containerInfo;N.nodeType===1?N.textContent="":N.nodeType===9&&N.documentElement&&N.removeChild(N.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(Wf(163))}}catch(H){wu(u,u.return,H)}if(f=u.sibling,f!==null){f.return=u.return,Vf=f;break}Vf=u.return}return K=eU,eU=!1,K}function C$(f,u,l){var y=u.updateQueue;if(y=y!==null?y.lastEffect:null,y!==null){var r=y=y.next;do{if((r.tag&f)===f){var _=r.destroy;r.destroy=void 0,_!==void 0&&i9(u,l,_)}r=r.next}while(r!==y)}}function t4(f,u){if(u=u.updateQueue,u=u!==null?u.lastEffect:null,u!==null){var l=u=u.next;do{if((l.tag&f)===f){var y=l.create;l.destroy=y()}l=l.next}while(l!==u)}}function R9(f){var u=f.ref;if(u!==null){var l=f.stateNode;switch(f.tag){case 5:f=l;break;default:f=l}typeof u==="function"?u(f):u.current=f}}function pW(f){var u=f.alternate;u!==null&&(f.alternate=null,pW(u)),f.child=null,f.deletions=null,f.sibling=null,f.tag===5&&(u=f.stateNode,u!==null&&(delete u[pl],delete u[t$],delete u[L9],delete u[bq],delete u[hq])),f.stateNode=null,f.return=null,f.dependencies=null,f.memoizedProps=null,f.memoizedState=null,f.pendingProps=null,f.stateNode=null,f.updateQueue=null}function IW(f){return f.tag===5||f.tag===3||f.tag===4}function fQ(f){f:for(;;){for(;f.sibling===null;){if(f.return===null||IW(f.return))return null;f=f.return}f.sibling.return=f.return;for(f=f.sibling;f.tag!==5&&f.tag!==6&&f.tag!==18;){if(f.flags&2)continue f;if(f.child===null||f.tag===4)continue f;else f.child.return=f,f=f.child}if(!(f.flags&2))return f.stateNode}}function x9(f,u,l){var y=f.tag;if(y===5||y===6)f=f.stateNode,u?l.nodeType===8?l.parentNode.insertBefore(f,u):l.insertBefore(f,u):(l.nodeType===8?(u=l.parentNode,u.insertBefore(f,l)):(u=l,u.appendChild(f)),l=l._reactRootContainer,l!==null&&l!==void 0||u.onclick!==null||(u.onclick=L4));else if(y!==4&&(f=f.child,f!==null))for(x9(f,u,l),f=f.sibling;f!==null;)x9(f,u,l),f=f.sibling}function v9(f,u,l){var y=f.tag;if(y===5||y===6)f=f.stateNode,u?l.insertBefore(f,u):l.appendChild(f);else if(y!==4&&(f=f.child,f!==null))for(v9(f,u,l),f=f.sibling;f!==null;)v9(f,u,l),f=f.sibling}var f0=null,Xl=!1;function P1(f,u,l){for(l=l.child;l!==null;)gW(f,u,l),l=l.sibling}function gW(f,u,l){if(Il&&typeof Il.onCommitFiberUnmount==="function")try{Il.onCommitFiberUnmount(v4,l)}catch(j){}switch(l.tag){case 5:J0||pr(l,u);case 6:var y=f0,r=Xl;f0=null,P1(f,u,l),f0=y,Xl=r,f0!==null&&(Xl?(f=f0,l=l.stateNode,f.nodeType===8?f.parentNode.removeChild(l):f.removeChild(l)):f0.removeChild(l.stateNode));break;case 18:f0!==null&&(Xl?(f=f0,l=l.stateNode,f.nodeType===8?b5(f.parentNode,l):f.nodeType===1&&b5(f,l),m$(f)):b5(f0,l.stateNode));break;case 4:y=f0,r=Xl,f0=l.stateNode.containerInfo,Xl=!0,P1(f,u,l),f0=y,Xl=r;break;case 0:case 11:case 14:case 15:if(!J0&&(y=l.updateQueue,y!==null&&(y=y.lastEffect,y!==null))){r=y=y.next;do{var _=r,$=_.destroy;_=_.tag,$!==void 0&&((_&2)!==0?i9(l,u,$):(_&4)!==0&&i9(l,u,$)),r=r.next}while(r!==y)}P1(f,u,l);break;case 1:if(!J0&&(pr(l,u),y=l.stateNode,typeof y.componentWillUnmount==="function"))try{y.props=l.memoizedProps,y.state=l.memoizedState,y.componentWillUnmount()}catch(j){wu(l,u,j)}P1(f,u,l);break;case 21:P1(f,u,l);break;case 22:l.mode&1?(J0=(y=J0)||l.memoizedState!==null,P1(f,u,l),J0=y):P1(f,u,l);break;default:P1(f,u,l)}}function uQ(f){var u=f.updateQueue;if(u!==null){f.updateQueue=null;var l=f.stateNode;l===null&&(l=f.stateNode=new rV),u.forEach(function(y){var r=zV.bind(null,f,y);l.has(y)||(l.add(y),y.then(r,r))})}}function Ll(f,u){var l=u.deletions;if(l!==null)for(var y=0;yr&&(r=$),y&=~_}if(y=r,y=Su()-y,y=(120>y?120:480>y?480:1080>y?1080:1920>y?1920:3000>y?3000:4320>y?4320:1960*jV(y/1960))-y,10f?16:f,v1===null)var y=!1;else{if(f=v1,v1=null,i4=0,(fu&6)!==0)throw Error(Wf(331));var r=fu;fu|=4;for(Vf=f.current;Vf!==null;){var _=Vf,$=_.child;if((Vf.flags&16)!==0){var j=_.deletions;if(j!==null){for(var A=0;ASu()-X7?my(f,0):B7|=l),n0(f,u)}function fz(f,u){u===0&&((f.mode&1)===0?u=1:(u=t6,t6<<=1,(t6&130023424)===0&&(t6=4194304)));var l=H0();f=W1(f,u),f!==null&&(u3(f,u,l),n0(f,l))}function WV(f){var u=f.memoizedState,l=0;u!==null&&(l=u.retryLane),fz(f,l)}function zV(f,u){var l=0;switch(f.tag){case 13:var{stateNode:y,memoizedState:r}=f;r!==null&&(l=r.retryLane);break;case 19:y=f.stateNode;break;default:throw Error(Wf(314))}y!==null&&y.delete(u),fz(f,l)}var uz;uz=function(f,u,l){if(f!==null)if(f.memoizedProps!==u.pendingProps||D0.current)w0=!0;else{if((f.lanes&l)===0&&(u.flags&128)===0)return w0=!1,uV(f,u,l);w0=(f.flags&131072)!==0?!0:!1}else w0=!1,Eu&&(u.flags&1048576)!==0&&_W(u,w4,u.index);switch(u.lanes=0,u.tag){case 2:var y=u.type;Q4(f,u),f=u.pendingProps;var r=er(u,U0.current);or(u,l),r=E7(null,u,y,f,r,l);var _=H7();return u.flags|=1,typeof r==="object"&&r!==null&&typeof r.render==="function"&&r.$$typeof===void 0?(u.tag=1,u.memoizedState=null,u.updateQueue=null,T0(y)?(_=!0,X4(u)):_=!1,u.memoizedState=r.state!==null&&r.state!==void 0?r.state:null,z7(u),r.updater=k4,u.stateNode=r,r._reactInternals=u,T9(u,y,f,l),u=S9(null,u,y,!0,_,l)):(u.tag=0,Eu&&_&&j7(u),E0(null,u,r,l),u=u.child),u;case 16:y=u.elementType;f:{switch(Q4(f,u),f=u.pendingProps,r=y._init,y=r(y._payload),u.type=y,r=u.tag=KV(y),f=Bl(y,f),r){case 0:u=M9(null,u,y,f,l);break f;case 1:u=oU(null,u,y,f,l);break f;case 11:u=tU(null,u,y,f,l);break f;case 14:u=sU(null,u,y,Bl(y.type,f),l);break f}throw Error(Wf(306,y,""))}return u;case 0:return y=u.type,r=u.pendingProps,r=u.elementType===y?r:Bl(y,r),M9(f,u,y,r,l);case 1:return y=u.type,r=u.pendingProps,r=u.elementType===y?r:Bl(y,r),oU(f,u,y,r,l);case 3:f:{if(RW(u),f===null)throw Error(Wf(387));y=u.pendingProps,_=u.memoizedState,r=_.element,UW(f,u),n4(u,y,null,l);var $=u.memoizedState;if(y=$.element,_.isDehydrated)if(_={element:y,isDehydrated:!1,cache:$.cache,pendingSuspenseBoundaries:$.pendingSuspenseBoundaries,transitions:$.transitions},u.updateQueue.baseState=_,u.memoizedState=_,u.flags&256){r=y_(Error(Wf(423)),u),u=aU(f,u,y,l,r);break f}else if(y!==r){r=y_(Error(Wf(424)),u),u=aU(f,u,y,l,r);break f}else for(p0=p1(u.stateNode.containerInfo.firstChild),I0=u,Eu=!0,Yl=null,l=FW(u,null,y,l),u.child=l;l;)l.flags=l.flags&-3|4096,l=l.sibling;else{if(f_(),y===r){u=z1(f,u,l);break f}E0(f,u,y,l)}u=u.child}return u;case 5:return QW(u),f===null&&Y9(u),y=u.type,r=u.pendingProps,_=f!==null?f.memoizedProps:null,$=r.children,q9(y,r)?$=null:_!==null&&q9(y,_)&&(u.flags|=32),iW(f,u),E0(f,u,$,l),u.child;case 6:return f===null&&Y9(u),null;case 13:return xW(f,u,l);case 4:return G7(u,u.stateNode.containerInfo),y=u.pendingProps,f===null?u.child=u_(u,null,y,l):E0(f,u,y,l),u.child;case 11:return y=u.type,r=u.pendingProps,r=u.elementType===y?r:Bl(y,r),tU(f,u,y,r,l);case 7:return E0(f,u,u.pendingProps,l),u.child;case 8:return E0(f,u,u.pendingProps.children,l),u.child;case 12:return E0(f,u,u.pendingProps.children,l),u.child;case 10:f:{if(y=u.type._context,r=u.pendingProps,_=u.memoizedProps,$=r.value,Wu(D4,y._currentValue),y._currentValue=$,_!==null)if(Tl(_.value,$)){if(_.children===r.children&&!D0.current){u=z1(f,u,l);break f}}else for(_=u.child,_!==null&&(_.return=u);_!==null;){var j=_.dependencies;if(j!==null){$=_.child;for(var A=j.firstContext;A!==null;){if(A.context===y){if(_.tag===1){A=J1(-1,l&-l),A.tag=2;var F=_.updateQueue;if(F!==null){F=F.shared;var U=F.pending;U===null?A.next=A:(A.next=U.next,U.next=A),F.pending=A}}_.lanes|=l,A=_.alternate,A!==null&&(A.lanes|=l),w9(_.return,l,u),j.lanes|=l;break}A=A.next}}else if(_.tag===10)$=_.type===u.type?null:_.child;else if(_.tag===18){if($=_.return,$===null)throw Error(Wf(341));$.lanes|=l,j=$.alternate,j!==null&&(j.lanes|=l),w9($,l,u),$=_.sibling}else $=_.child;if($!==null)$.return=_;else for($=_;$!==null;){if($===u){$=null;break}if(_=$.sibling,_!==null){_.return=$.return,$=_;break}$=$.return}_=$}E0(f,u,r.children,l),u=u.child}return u;case 9:return r=u.type,y=u.pendingProps.children,or(u,l),r=Ul(r),y=y(r),u.flags|=1,E0(f,u,y,l),u.child;case 14:return y=u.type,r=Bl(y,u.pendingProps),r=Bl(y.type,r),sU(f,u,y,r,l);case 15:return CW(f,u,u.type,u.pendingProps,l);case 17:return y=u.type,r=u.pendingProps,r=u.elementType===y?r:Bl(y,r),Q4(f,u),u.tag=1,T0(y)?(f=!0,X4(u)):f=!1,or(u,l),MW(u,y,r),T9(u,y,r,l),S9(null,u,y,!0,f,l);case 19:return vW(f,u,l);case 22:return cW(f,u,l)}throw Error(Wf(156,u.tag))};function lz(f,u){return DQ(f,u)}function GV(f,u,l,y){this.tag=f,this.key=l,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=u,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=y,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Fl(f,u,l,y){return new GV(f,u,l,y)}function T7(f){return f=f.prototype,!(!f||!f.isReactComponent)}function KV(f){if(typeof f==="function")return T7(f)?1:0;if(f!==void 0&&f!==null){if(f=f.$$typeof,f===s9)return 11;if(f===o9)return 14}return 2}function t1(f,u){var l=f.alternate;return l===null?(l=Fl(f.tag,u,f.key,f.mode),l.elementType=f.elementType,l.type=f.type,l.stateNode=f.stateNode,l.alternate=f,f.alternate=l):(l.pendingProps=u,l.type=f.type,l.flags=0,l.subtreeFlags=0,l.deletions=null),l.flags=f.flags&14680064,l.childLanes=f.childLanes,l.lanes=f.lanes,l.child=f.child,l.memoizedProps=f.memoizedProps,l.memoizedState=f.memoizedState,l.updateQueue=f.updateQueue,u=f.dependencies,l.dependencies=u===null?null:{lanes:u.lanes,firstContext:u.firstContext},l.sibling=f.sibling,l.index=f.index,l.ref=f.ref,l}function G4(f,u,l,y,r,_){var $=2;if(y=f,typeof f==="function")T7(f)&&($=1);else if(typeof f==="string")$=5;else f:switch(f){case Cr:return py(l.children,r,_,u);case t9:$=8,r|=8;break;case d5:return f=Fl(12,l,u,r|2),f.elementType=d5,f.lanes=_,f;case e5:return f=Fl(13,l,u,r),f.elementType=e5,f.lanes=_,f;case f9:return f=Fl(19,l,u,r),f.elementType=f9,f.lanes=_,f;case QQ:return o4(l,r,_,u);default:if(typeof f==="object"&&f!==null)switch(f.$$typeof){case JQ:$=10;break f;case UQ:$=9;break f;case s9:$=11;break f;case o9:$=14;break f;case C1:$=16,y=null;break f}throw Error(Wf(130,f==null?f:typeof f,""))}return u=Fl($,l,u,r),u.elementType=f,u.type=y,u.lanes=_,u}function py(f,u,l,y){return f=Fl(7,f,y,u),f.lanes=l,f}function o4(f,u,l,y){return f=Fl(22,f,y,u),f.elementType=QQ,f.lanes=l,f.stateNode={isHidden:!1},f}function s5(f,u,l){return f=Fl(6,f,null,u),f.lanes=l,f}function o5(f,u,l){return u=Fl(4,f.children!==null?f.children:[],f.key,u),u.lanes=l,u.stateNode={containerInfo:f.containerInfo,pendingChildren:null,implementation:f.implementation},u}function NV(f,u,l,y,r){this.tag=u,this.containerInfo=f,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=P5(0),this.expirationTimes=P5(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=P5(0),this.identifierPrefix=y,this.onRecoverableError=r,this.mutableSourceEagerHydrationData=null}function n7(f,u,l,y,r,_,$,j,A){return f=new NV(f,u,l,j,A),u===1?(u=1,_===!0&&(u|=8)):u=0,_=Fl(3,null,null,u),f.current=_,_.stateNode=f,_.memoizedState={element:y,isDehydrated:l,cache:null,transitions:null,pendingSuspenseBoundaries:null},z7(_),f}function ZV(f,u,l){var y=3{function jz(){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=="function")return;try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(jz)}catch(f){console.error(f)}}jz(),Az.exports=$z()});var Jz=h0((c7)=>{var Fz=C7();c7.createRoot=Fz.createRoot,c7.hydrateRoot=Fz.hydrateRoot;var VV});var gG=h0((Y8)=>{var WX=Yu(),zX=Symbol.for("react.element"),GX=Symbol.for("react.fragment"),KX=Object.prototype.hasOwnProperty,NX=WX.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,ZX={key:!0,ref:!0,__self:!0,__source:!0};function IG(f,u,l){var y,r={},_=null,$=null;l!==void 0&&(_=""+l),u.key!==void 0&&(_=""+u.key),u.ref!==void 0&&($=u.ref);for(y in u)KX.call(u,y)&&!ZX.hasOwnProperty(y)&&(r[y]=u[y]);if(f&&f.defaultProps)for(y in u=f.defaultProps,u)r[y]===void 0&&(r[y]=u[y]);return{$$typeof:zX,type:f,key:_,ref:$,props:r,_owner:NX.current}}Y8.Fragment=GX;Y8.jsx=IG;Y8.jsxs=IG});var tG=h0((hC,kG)=>{kG.exports=gG()});var BN=h0((LN)=>{var x_=Yu();function CD(f,u){return f===u&&(f!==0||1/f===1/u)||f!==f&&u!==u}var cD=typeof Object.is==="function"?Object.is:CD,iD=x_.useState,RD=x_.useEffect,xD=x_.useLayoutEffect,vD=x_.useDebugValue;function bD(f,u){var l=u(),y=iD({inst:{value:l,getSnapshot:u}}),r=y[0].inst,_=y[1];return xD(function(){r.value=l,r.getSnapshot=u,YF(r)&&_({inst:r})},[f,l,u]),RD(function(){return YF(r)&&_({inst:r}),f(function(){YF(r)&&_({inst:r})})},[f]),vD(l),l}function YF(f){var u=f.getSnapshot;f=f.value;try{var l=u();return!cD(f,l)}catch(y){return!0}}function hD(f,u){return u()}var mD=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?hD:bD;LN.useSyncExternalStore=x_.useSyncExternalStore!==void 0?x_.useSyncExternalStore:mD});var YN=h0((Cb,XN)=>{XN.exports=BN()});var DN=h0((wN)=>{var H2=Yu(),pD=YN();function ID(f,u){return f===u&&(f!==0||1/f===1/u)||f!==f&&u!==u}var gD=typeof Object.is==="function"?Object.is:ID,kD=pD.useSyncExternalStore,tD=H2.useRef,sD=H2.useEffect,oD=H2.useMemo,aD=H2.useDebugValue;wN.useSyncExternalStoreWithSelector=function(f,u,l,y,r){var _=tD(null);if(_.current===null){var $={hasValue:!1,value:null};_.current=$}else $=_.current;_=oD(function(){function A(G){if(!F){if(F=!0,U=G,G=y(G),r!==void 0&&$.hasValue){var K=$.value;if(r(K,G))return Q=K}return Q=G}if(K=Q,gD(U,G))return K;var E=y(G);if(r!==void 0&&r(K,E))return U=G,K;return U=G,Q=E}var F=!1,U,Q,W=l===void 0?null:l;return[function(){return A(u())},W===null?void 0:function(){return A(W())}]},[u,l,y,r]);var j=kD(f,_[0],_[1]);return sD(function(){$.hasValue=!0,$.value=j},[j]),aD(j),j}});var nN=h0((ib,TN)=>{TN.exports=DN()});var Yy=cf(Yu(),1);var c6="北京时间";var ZO={timeZone:"Asia/Shanghai",hour12:!1},EO={timeZone:"Asia/Shanghai",hour12:!1},HO=new Intl.DateTimeFormat("en-CA",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",hourCycle:"h23"});function Z5(f){if(f===null||f===void 0||f==="")return null;let u=f instanceof Date?f:new Date(f);return Number.isNaN(u.getTime())?null:u}function kJ(f){let u=Z5(f);if(!u)return null;return HO.formatToParts(u).reduce((l,y)=>{if(y.type!=="literal")l[y.type]=y.value;return l},{})}function Kf(f){let u=Z5(f);return u?u.toLocaleString("zh-CN",ZO):"--"}function Uu(f){let u=Z5(f);return u?u.toLocaleTimeString("zh-CN",EO):"--"}function E5(f){let u=kJ(f);if(!u)return"";let l=u.hour==="24"?"00":u.hour;return`${u.year}-${u.month}-${u.day}T${l}:${u.minute}`}function tJ(f=new Date){let u=kJ(f);if(!u)return"";return`${u.year}-${u.month}-${u.day}`}function sJ(f){if(!f)return null;let u=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(f);if(!u)return null;let[,l,y,r,_,$,j="00"]=u,A=Date.UTC(Number(l),Number(y)-1,Number(r),Number(_)-8,Number($),Number(j)),F=new Date(A),U=E5(F);return Number.isNaN(F.getTime())||U!==`${l}-${y}-${r}T${_}:${$}`?null:F.toISOString()}var uH=cf(Jz(),1);var l8=cf(Yu(),1);var Uz=cf(Yu(),1),_3=Uz.default.createElement;function LV({active:f=!0,label:u="正在加载"}){if(!f)return null;return _3("span",{className:"loading-spinner-indicator",role:"status","aria-label":u,title:u,"data-testid":"loading-title-indicator"},_3("span",{className:"loading-spinner-ring","aria-hidden":!0}))}function _u({title:f,children:u,loading:l,level:y=2,className:r="",label:_="正在加载"}){return _3(y===3?"h3":"h2",{className:`loading-title ${l?"is-loading":""} ${r}`.trim()},_3("span",{className:"loading-title-text"},u??f),_3(LV,{active:Boolean(l),label:_}))}class j_ extends Error{unideskRequestError=!0;meta;constructor(f,u){super(f);this.name="UniDeskRequestError",this.meta=u}}function BV(f){return new Promise((u)=>setTimeout(u,f))}function A3(f,u="操作失败"){return f instanceof Error?f.message:String(f||u)}function u8(f,u=500){if(f===null||f===void 0)return"";let l=typeof f==="string"?f:JSON.stringify(f),y=String(l||"").replace(/\s+/gu," ").trim();return y.length>u?`${y.slice(0,u)}...`:y}function XV(f){try{let u=typeof location<"u"&&location.origin?location.origin:"http://localhost";return new URL(f,u).toString()}catch{return f}}function Qz(f){return String(f.method||"GET").toUpperCase()}function YV(f){if(f===null||f===void 0)return!1;if(typeof f!=="object")return!1;if(typeof Blob<"u"&&f instanceof Blob)return!1;if(typeof FormData<"u"&&f instanceof FormData)return!1;if(typeof URLSearchParams<"u"&&f instanceof URLSearchParams)return!1;if(typeof ArrayBuffer<"u"&&f instanceof ArrayBuffer)return!1;return!0}function Wz(f){let u=new Headers(f.headers||{}),l=YV(f.body)?JSON.stringify(f.body):f.body;if(l&&!u.has("content-type")&&typeof l==="string")u.set("content-type","application/json");return{...f,credentials:f.credentials||"same-origin",body:l,headers:u}}function zz(f){if(f?.error&&typeof f.error==="object"&&typeof f.error.message==="string")return f.error.message;if(typeof f?.error==="string")return f.error;if(typeof f?.message==="string")return f.message;if(typeof f?.detail==="string")return f.detail;return""}function wV(f,u){if(!f||typeof f!=="object"||Array.isArray(f))return!1;return u.some((l)=>l!==!1&&f[l]===!1)}function $3(f,u,l,y,r={}){return{kind:f,method:l,url:XV(u),occurredAt:y.toISOString(),...r}}function j3(f,u){if(!f)return"请求失败";return`HTTP ${f}${u?` ${u}`:""}`}function Gz(f){try{return{body:f?JSON.parse(f):null,parseError:""}}catch(u){return{body:{text:f},parseError:A3(u,"parse failed")}}}async function Df(f,u={},l=0){let{failureFields:y=["ok"],strictJson:r=!1,retryInvalidJson:_=0,retryDelayMs:$=120,invalidJsonPrefix:j="服务返回了无效 JSON",invalidJsonPreview:A=!1,responsePreviewLength:F=500,...U}=u,Q=Qz(U),W=new Date,G;try{G=await fetch(f,Wz(U))}catch(O){let z=A3(O,"网络请求失败");throw new j_(z,$3("network",f,Q,W,{upstreamMessage:z}))}let K=await G.text(),E=Gz(K);if(E.parseError){if(r&&Q==="GET"&&l<_)return await BV($),Df(f,u,l+1);if(r){let O=A?`;响应预览:${u8(K,180)}`:"";throw new j_(`${j}(${K.length} bytes):${E.parseError}${O}`,$3("parse",f,Q,W,{status:G.status,statusText:G.statusText,parseError:E.parseError,responsePreview:u8(K,F)}))}}if(!G.ok||wV(E.body,y)){let O=zz(E.body),z=O||j3(G.status,G.statusText);throw new j_(z,$3("http",f,Q,W,{status:G.status,statusText:G.statusText,upstreamMessage:O,responsePreview:u8(E.parseError?K:E.body,F)}))}return E.body}async function Kz(f,u={}){let l=Qz(u),y=new Date,r;try{r=await fetch(f,Wz(u))}catch(F){let U=A3(F,"网络请求失败");throw new j_(U,$3("network",f,l,y,{upstreamMessage:U}))}if(r.ok)return r.blob();let _=await r.text(),$=Gz(_),j=zz($.body),A=j||j3(r.status,r.statusText);throw new j_(A,$3("http",f,l,y,{status:r.status,statusText:r.statusText,upstreamMessage:j,responsePreview:u8($.parseError?_:$.body),parseError:$.parseError||void 0}))}function Nz(f){return Boolean(f&&typeof f==="object"&&f.unideskRequestError===!0&&f.meta)}function DV(f){if(!f)return"";let u=new Date(f);if(Number.isNaN(u.getTime()))return f;return`${Kf(u)} ${c6}`}function i7(f,u="操作失败"){if(Nz(f)){let r=f.meta.kind==="parse"?"响应解析失败":f.meta.kind==="network"?"网络请求失败":f.meta.status&&(f.meta.status<200||f.meta.status>=300)?j3(f.meta.status,f.meta.statusText):"应用请求失败",_=f.meta.status?j3(f.meta.status):"",$=(A)=>!A||A===r||A===_,j=!$(f.message)?f.message:$(f.meta.upstreamMessage)?"":f.meta.upstreamMessage||"";return{title:r,message:j,status:f.meta.status,statusText:f.meta.statusText,method:f.meta.method,url:f.meta.url,occurredAt:DV(f.meta.occurredAt),responsePreview:f.meta.responsePreview,parseError:f.meta.parseError,structured:!0}}let y=A3(f,u).split(/\r?\n/u);return{title:y[0]||u,message:y.slice(1).join(` +`),structured:y.length>1}}function TV(f,u="操作失败"){let l=i7(f,u),y=[l.title];if(l.message)y.push(`原因: ${l.message}`);if(l.method||l.url)y.push(`请求: ${[l.method,l.url].filter(Boolean).join(" ")}`);if(l.status)y.push(`状态: ${j3(l.status,l.statusText)}`);if(l.occurredAt)y.push(`时间: ${l.occurredAt}`);if(l.parseError)y.push(`解析错误: ${l.parseError}`);if(l.responsePreview&&l.responsePreview!==l.message)y.push(`响应预览: ${l.responsePreview}`);return y.filter(Boolean).join(` +`)}function wf(f,u="操作失败"){return Nz(f)?TV(f,u):A3(f,u)}var Zz=cf(Yu(),1);var fy=Zz.default.createElement;function F3(f,u){return u?[fy("dt",{key:`${f}-label`},f),fy("dd",{key:f},u)]:null}function Au({error:f,wide:u=!1,fallback:l="操作失败",className:y=""}){if(!f)return null;let r=i7(f,l),_=[F3("请求",[r.method,r.url].filter(Boolean).join(" ")),F3("状态",r.status?`HTTP ${r.status}${r.statusText?` ${r.statusText}`:""}`:""),F3("时间",r.occurredAt),F3("解析错误",r.parseError),F3("响应预览",r.responsePreview)].filter(Boolean);return fy("div",{className:`form-error unidesk-error${u?" wide":""}${y?` ${y}`:""}`,role:"alert","data-testid":"unidesk-error"},fy("div",{className:"unidesk-error-title"},fy("strong",null,r.title),r.status?fy("span",{className:"unidesk-error-code"},`HTTP ${r.status}`):null),r.message?fy("pre",{className:"unidesk-error-message"},r.message):null,_.length>0?fy("dl",{className:"unidesk-error-details"},_):null)}var v=l8.default.createElement,{useEffect:R7}=l8.default,A_=l8.default.useState;function Q0(f,u={}){return Df(f,{failureFields:["ok","success"],...u})}function q0(f,u){return`${f}/microservices/baidu-netdisk/proxy${u}`}function nV(f){let u=Number(f);return Number.isFinite(u)?u.toLocaleString("zh-CN"):"--"}function uy(f){let u=Number(f);if(!Number.isFinite(u)||u<=0)return"--";let l=["B","KB","MB","GB","TB"],y=u,r=0;while(y>=1024&&r{r?.stopPropagation?.(),l(f,u)}},"查看原始JSON")}function J_({title:f,text:u}){return v("div",{className:"empty-state"},v("strong",null,f),v("span",null,u))}function U_({title:f,text:u,href:l,badge:y,testId:r}){return v("a",{className:"doc-link-card",href:l,target:"_blank",rel:"noreferrer","data-testid":r},v("span",null,y||"DOC"),v("strong",null,f),v("p",null,u),v("code",null,l))}function MV(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function SV(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function PV(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function CV(f){return Array.isArray(f?.files)?f.files:[]}function cV(f){return Array.isArray(f?.jobs)?f.jobs:[]}function iV(f,u){if(!f||f===u)return u;let l=f.replace(/\/+$/u,""),y=l.slice(0,l.lastIndexOf("/"))||u;return y.lengthS.id==="baidu-netdisk")||null,[r,_]=A_({loading:!1,actionLoading:!1,error:"",message:"",health:null,account:null,files:null,transfers:null,logs:null,selfTest:null,refreshedAt:null}),[$,j]=A_("/apps/UniDeskBaiduNetdisk"),[A,F]=A_(null),[U,Q]=A_(""),[W,G]=A_({localPath:"sample.txt",remotePath:"/apps/UniDeskBaiduNetdisk/sample.txt"}),[K,E]=A_({fsId:"",localPath:"downloads/"}),O=r.health?.baidu?.appRoot||r.account?.rootPath||"/apps/UniDeskBaiduNetdisk";async function z(S=$){let $f=await Q0(q0(l,`/api/files?dir=${encodeURIComponent(S||O)}&limit=100`));_((Qf)=>({...Qf,files:$f}))}async function Z(){let S=await Q0(q0(l,"/api/transfers?limit=80"));_((e)=>({...e,transfers:S}))}async function N(){if(!y)return;_((S)=>({...S,loading:!0,error:"",message:""}));try{let S=await Q0(`${l}/microservices/baidu-netdisk/health`),e=S?.baidu?.appRoot||O,$f=null,Qf=null;if(S?.auth?.loggedIn){$f=await Q0(q0(l,"/api/account?refresh=1"));let Hf=$&&$.startsWith(e)?$:e;j(Hf),Qf=await Q0(q0(l,`/api/files?dir=${encodeURIComponent(Hf)}&limit=100`))}else j(e);let Af=await Q0(q0(l,"/api/transfers?limit=80")),zf=await Q0(q0(l,"/logs?limit=60"));_((Hf)=>({...Hf,loading:!1,health:S,account:$f?.account||null,files:Qf,transfers:Af,logs:zf,refreshedAt:new Date}))}catch(S){_((e)=>({...e,loading:!1,error:wf(S,"百度网盘服务加载失败")}))}}async function H(){_((S)=>({...S,actionLoading:!0,error:"",message:""}));try{let S=await Q0(q0(l,"/api/auth/device/start"),{method:"POST",body:{}});F(S.session||null),_((e)=>({...e,actionLoading:!1,message:"设备码已生成,请扫码授权"}))}catch(S){_((e)=>({...e,actionLoading:!1,error:wf(S,"创建设备码失败")}))}}async function Y(S=!1){if(!A?.id)return;if(S)_((e)=>({...e,actionLoading:!0,error:""}));try{let e=await Q0(q0(l,`/api/auth/device/status?sessionId=${encodeURIComponent(A.id)}`));if(F(e.session||null),e.session?.status==="succeeded")_(($f)=>({...$f,actionLoading:!1,message:"授权成功,正在刷新账号与文件列表"})),await N();else if(S)_(($f)=>({...$f,actionLoading:!1}))}catch(e){_(($f)=>({...$f,actionLoading:!1,error:wf(e,"轮询登录状态失败")}))}}async function w(){_((S)=>({...S,actionLoading:!0,error:"",message:""}));try{await Q0(q0(l,"/api/auth/logout"),{method:"POST",body:{}}),F(null),_((S)=>({...S,actionLoading:!1,account:null,files:null,message:"本地 token 已清除"})),await N()}catch(S){_((e)=>({...e,actionLoading:!1,error:wf(S,"退出登录失败")}))}}async function V(S){S.preventDefault();let e=U.trim();if(!e)return;_(($f)=>({...$f,actionLoading:!0,error:"",message:""}));try{await Q0(q0(l,"/api/folders"),{method:"POST",body:{path:RV($,e)}}),Q(""),_(($f)=>({...$f,actionLoading:!1,message:"文件夹已创建"})),await z($)}catch($f){_((Qf)=>({...Qf,actionLoading:!1,error:wf($f,"创建文件夹失败")}))}}async function X(S){if(!S)return;_((e)=>({...e,actionLoading:!0,error:"",message:""}));try{await Q0(q0(l,"/api/files/manage"),{method:"POST",body:{opera:"delete",filelist:[{path:S}],async:1}}),_((e)=>({...e,actionLoading:!1,message:"删除任务已提交"})),await z($)}catch(e){_(($f)=>({...$f,actionLoading:!1,error:wf(e,"删除失败")}))}}async function i(S){S.preventDefault(),_((e)=>({...e,actionLoading:!0,error:"",message:""}));try{await Q0(q0(l,"/api/transfers/upload-from-path"),{method:"POST",body:W}),_((e)=>({...e,actionLoading:!1,message:"上传任务已入队"})),await Z()}catch(e){_(($f)=>({...$f,actionLoading:!1,error:wf(e,"上传任务创建失败")}))}}async function m(S){S.preventDefault(),_((e)=>({...e,actionLoading:!0,error:"",message:""}));try{await Q0(q0(l,"/api/transfers/download-to-path"),{method:"POST",body:K}),_((e)=>({...e,actionLoading:!1,message:"下载任务已入队"})),await Z()}catch(e){_(($f)=>({...$f,actionLoading:!1,error:wf(e,"下载任务创建失败")}))}}async function M(S,e){_(($f)=>({...$f,actionLoading:!0,error:"",message:""}));try{await Q0(q0(l,`/api/transfers/${encodeURIComponent(S)}/${e}`),{method:"POST",body:{}}),_(($f)=>({...$f,actionLoading:!1,message:e==="cancel"?"已请求取消任务":"任务已重新入队"})),await Z()}catch($f){_((Qf)=>({...Qf,actionLoading:!1,error:wf($f,"任务操作失败")}))}}async function c(){_((S)=>({...S,actionLoading:!0,error:"",message:"正在运行上传/下载自测..."}));try{let S=await Q0(q0(l,"/api/self-test"),{method:"POST",body:{}});_((e)=>({...e,actionLoading:!1,selfTest:S,message:`上传/下载自测通过:${S.remotePath||""}`})),await z($),await Z()}catch(S){_((e)=>({...e,actionLoading:!1,error:wf(S,"上传/下载自测失败")}))}}if(R7(()=>{if(!y)return;N();return},[y?.id,y?.runtime?.providerStatus]),R7(()=>{if(!A?.id||A.status!=="pending")return;let S=window.setInterval(()=>void Y(!1),Math.max(5000,Number(A.pollIntervalSeconds||5)*1000));return()=>window.clearInterval(S)},[A?.id,A?.status,A?.pollIntervalSeconds]),R7(()=>{if(!y)return;let S=window.setInterval(()=>void Z(),5000);return()=>window.clearInterval(S)},[y?.id]),!y)return v(J_,{title:"Baidu Netdisk 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=baidu-netdisk"});let C=MV(y),T=PV(y),R=SV(y),P=r.health||{},n=r.account||P.auth?.account||null,B=P.auth||{},D=CV(r.files),I=cV(r.transfers),p=n?.quota||{},k=Boolean(B.loggedIn||n),_f=Boolean(B.configured);return v("div",{className:"baidu-netdisk-page","data-testid":"baidu-netdisk-page"},v(ey,{title:"Baidu Netdisk 工作台",eyebrow:"Containerized Storage Gateway",loading:r.loading,actions:v("div",{className:"panel-actions"},v("a",{className:"ghost-btn",href:"/docs/issue/baidu-netdisk-env-setup.md",target:"_blank",rel:"noreferrer","data-testid":"baidu-netdisk-config-doc-link"},"配置文档"),v("button",{type:"button",className:"ghost-btn",onClick:N,disabled:r.loading,"data-testid":"baidu-netdisk-refresh"},r.loading?"刷新中":"刷新"),v(K1,{title:"Baidu Netdisk 用户服务",data:y,onOpen:u,testId:"raw-baidu-netdisk-service"}))},v("div",{className:"baidu-netdisk-hero"},v("div",null,v("div",{className:"node-version-line"},v(dy,{status:C.providerStatus==="online"?"online":"warn"},C.providerStatus||"unknown"),v("span",null,y.providerId),v(dy,{status:R.public?"warn":"private"},R.public?"公网暴露":"仅 UniDesk frontend 代理访问")),v("p",{className:"muted paragraph"},y.description)),v("div",{className:"microservice-ref-card"},v("span",null,"Repo"),v("strong",null,T.url||"--"),v("code",null,T.commitId||"--")),v("div",{className:"microservice-ref-card"},v("span",null,"Private Backend"),v("strong",null,`${R.nodeBindHost||"--"}:${R.nodePort||"--"}`),v("code",null,`${T.composeFile||"--"} / ${T.composeService||"--"}`))),v(Au,{error:r.error,wide:!0}),r.message?v("div",{className:"form-success wide"},r.message):null),v("div",{className:"metric-grid"},v(F_,{label:"Health",value:P.ok?"OK":"--",hint:P.storage?.postgres||"postgres",tone:P.ok?"ok":"warn"}),v(F_,{label:"OAuth",value:_f?"已配置":"待配置",hint:_f?"client + secret + token key":"需要设置 UNIDESK_BAIDU_NETDISK_*",tone:_f?"ok":"warn"}),v(F_,{label:"Login",value:k?"已登录":"未登录",hint:n?.username||"Device Code QR",tone:k?"ok":"warn"}),v(F_,{label:"App Root",value:O.split("/").pop()||"apps",hint:O}),v(F_,{label:"Quota",value:uy(p.used),hint:p.total?`${p.usedPercent||0}% / ${uy(p.total)}`:"授权后刷新"}),v(F_,{label:"Transfers",value:nV(I.length),hint:`running ${r.transfers?.counts?.running||0} / failed ${r.transfers?.counts?.failed||0}`})),v("div",{className:"baidu-netdisk-grid"},v(ey,{title:"配置与文档",eyebrow:"Deployment References",className:"baidu-docs-panel",actions:v("div",{className:"panel-actions inline-actions"},v("a",{className:"ghost-btn",href:"/docs/issue/baidu-netdisk-env-setup.md",target:"_blank",rel:"noreferrer"},"打开环境配置"),v("a",{className:"ghost-btn",href:"/docs/issue/baidu-netdisk-user-service.md",target:"_blank",rel:"noreferrer"},"打开服务方案"))},v("p",{className:"muted paragraph"},_f?"OAuth 运行时变量已配置;如需轮换密钥、迁移部署或排查代理边界,可直接打开下面的项目内文档。":"首次使用请先按环境变量配置文档填入百度应用 client id / secret,然后重建 baidu-netdisk 服务并刷新本页。"),v("div",{className:"baidu-doc-grid","data-testid":"baidu-netdisk-doc-links"},v(U_,{title:"环境变量配置",text:"填写 UNIDESK_BAIDU_NETDISK_CLIENT_ID、CLIENT_SECRET、TOKEN_KEY,并执行重建与健康检查。",href:"/docs/issue/baidu-netdisk-env-setup.md",badge:"SETUP",testId:"baidu-netdisk-env-doc-card"}),v(U_,{title:"服务方案与 API",text:"说明 OAuth Device Code、应用目录、staging 上传下载任务和后端 API 设计。",href:"/docs/issue/baidu-netdisk-user-service.md",badge:"DESIGN"}),v(U_,{title:"用户服务安全边界",text:"查看 UniDesk microservice 私有代理、允许路径、frontendOnly 和密钥边界规则。",href:"/docs/reference/microservices.md",badge:"REF"}),v(U_,{title:"部署与重建流程",text:"查看 server rebuild、Compose 编排、健康检查和交付验证的长期规则。",href:"/docs/reference/deployment.md",badge:"DEPLOY"}),v(U_,{title:"CLI 验证命令",text:"查看 microservice health/proxy、server rebuild、job status 等命令入口。",href:"/docs/reference/cli.md",badge:"CLI"}),v(U_,{title:"百度设备码模式",text:"打开百度官方 OAuth Device Code 文档,对照扫码登录和轮询参数。",href:"https://pan.baidu.com/union/doc/fl1x114ti",badge:"OFFICIAL"}))),v(ey,{title:"设备码登录",eyebrow:"OAuth Device Code",className:"baidu-login-panel",loading:r.actionLoading,actions:v("div",{className:"panel-actions inline-actions"},v("button",{type:"button",className:"primary-btn",onClick:H,disabled:r.actionLoading||!_f,"data-testid":"baidu-netdisk-start-login"},"生成二维码"),A?.id?v("button",{type:"button",className:"ghost-btn",onClick:()=>Y(!0),disabled:r.actionLoading},"检查状态"):null,k?v("button",{type:"button",className:"ghost-btn",onClick:w,disabled:r.actionLoading},"清除本地登录"):null,v(K1,{title:"Baidu Device Session",data:A||B.latestSession,onOpen:u,testId:"raw-baidu-device-session"}))},v("div",{className:"baidu-login-card","data-testid":"baidu-netdisk-login-card"},v("div",{className:"baidu-qr-frame"},A?.qrcodeUrl?v("img",{src:A.qrcodeUrl,alt:"百度网盘设备码授权二维码","data-testid":"baidu-netdisk-qrcode"}):v(J_,{title:_f?"等待二维码":"OAuth 未配置",text:_f?"点击生成二维码后使用百度网盘或百度 App 扫码":"设置 client id、secret 和 token key 后重建服务"})),v("div",{className:"claudeqq-login-copy"},v("div",{className:"node-version-line"},v(dy,{status:k?"online":A?.status==="pending"?"warn":"unknown"},k?"已登录":A?.status||"未开始"),v("span",null,A?.secondsRemaining!==void 0?`${A.secondsRemaining}s`:"--"),v("span",null,"scope basic,netdisk")),v("p",{className:"muted paragraph"},k?"access token / refresh token 已加密保存到 PostgreSQL;前端只看到脱敏登录态。":"后端使用百度 OAuth Device Code 轮询换取 token;二维码过期后重新生成即可。"),v("div",{className:"microservice-ref-card"},v("span",null,"User Code"),v("strong",null,A?.userCode||"--"),v("code",null,A?.verificationUrl||"https://openapi.baidu.com/device")),v("div",{className:"microservice-ref-card"},v("span",null,"Expires"),v("strong",null,A?.expiresAt?Kf(A.expiresAt):"--"),v("code",null,A?.error||"no token exposed"))))),v(ey,{title:"账号与容量",eyebrow:r.refreshedAt?`Updated ${Uu(r.refreshedAt)}`:"Account",loading:r.loading,actions:v("div",{className:"panel-actions inline-actions"},v(K1,{title:"Baidu Account",data:n,onOpen:u,testId:"raw-baidu-account"}))},n?v("div",{className:"baidu-account-card"},v("div",{className:"node-version-line"},v(dy,{status:"online"},"connected"),v("span",null,n.baiduUid||"--"),v("span",null,`VIP ${n.vipType??"--"}`)),v("h3",null,n.username||"Baidu Netdisk"),v("p",{className:"muted paragraph"},`应用目录固定在 ${n.rootPath||O};v1 上传/下载只读写容器 staging 目录,不把大文件字节流穿过 UniDesk proxy。`),v("div",{className:"quota-bar"},v("span",{style:{width:`${Math.max(0,Math.min(100,Number(p.usedPercent||0)))}%`}})),v("div",{className:"microservice-ref-card"},v("span",null,"Quota"),v("strong",null,`${uy(p.used)} / ${uy(p.total)}`),v("code",null,`${p.usedPercent||0}% used`))):v(J_,{title:"尚未登录",text:"扫码授权后这里会显示账号、UID、会员状态和容量"})),v(ey,{title:"文件浏览器",eyebrow:$,className:"baidu-files-panel",loading:r.loading,actions:v("div",{className:"panel-actions inline-actions"},v("button",{type:"button",className:"ghost-btn",onClick:()=>{let S=iV($,O);j(S),z(S)},disabled:!k||$===O},"上级"),v("button",{type:"button",className:"ghost-btn",onClick:()=>z($),disabled:!k},"刷新文件"),v(K1,{title:"Baidu Files",data:r.files,onOpen:u,testId:"raw-baidu-files"}))},v("form",{className:"baidu-pathbar",onSubmit:(S)=>{S.preventDefault(),z($)}},v("input",{value:$,onChange:(S)=>j(S.target.value),disabled:!k}),v("button",{type:"submit",className:"ghost-btn",disabled:!k},"打开路径")),v("form",{className:"baidu-pathbar",onSubmit:V},v("input",{value:U,onChange:(S)=>Q(S.target.value),placeholder:"新文件夹名称",disabled:!k}),v("button",{type:"submit",className:"primary-btn",disabled:!k||!U.trim()},"新建文件夹")),!k?v(J_,{title:"等待授权",text:"登录后通过 /api/files 读取应用目录文件列表"}):D.length===0?v(J_,{title:"目录为空",text:"可以从 staging 目录上传文件或新建文件夹"}):v("div",{className:"table-wrap","data-testid":"baidu-netdisk-file-table"},v("table",null,v("thead",null,v("tr",null,v("th",null,"名称"),v("th",null,"类型"),v("th",null,"大小"),v("th",null,"修改时间"),v("th",null,"fs_id"),v("th",null,"操作"))),v("tbody",null,D.map((S)=>v("tr",{key:S.fsId||S.path},v("td",null,v("strong",null,S.serverFilename||S.path),v("code",null,S.path||"--")),v("td",null,v(dy,{status:S.isDir?"queued":"private"},S.isDir?"DIR":"FILE")),v("td",null,S.isDir?"--":uy(S.size)),v("td",null,S.serverMtime?Kf(S.serverMtime*1000):"--"),v("td",null,v("code",null,S.fsId||"--")),v("td",null,v("div",{className:"inline-actions"},S.isDir?v("button",{type:"button",className:"ghost-btn",onClick:()=>{j(S.path),z(S.path)}},"打开"):v("button",{type:"button",className:"ghost-btn",onClick:()=>E((e)=>({...e,fsId:S.fsId}))},"填入下载"),v("button",{type:"button",className:"ghost-btn",onClick:()=>X(S.path),disabled:r.actionLoading},"删除"))))))))),v(ey,{title:"传输任务",eyebrow:"staging path jobs",className:"baidu-transfers-panel",loading:r.actionLoading,actions:v("div",{className:"panel-actions inline-actions"},v("button",{type:"button",className:"primary-btn",onClick:c,disabled:!k||r.actionLoading,"data-testid":"baidu-netdisk-self-test"},"运行自测"),v("button",{type:"button",className:"ghost-btn",onClick:Z},"刷新任务"),v(K1,{title:"Baidu Transfers",data:r.transfers,onOpen:u,testId:"raw-baidu-transfers"}))},v("div",{className:"baidu-transfer-forms"},v("form",{className:"stack-form",onSubmit:i,"data-testid":"baidu-upload-form"},v("label",null,"容器 staging 文件",v("input",{value:W.localPath,onChange:(S)=>G((e)=>({...e,localPath:S.target.value})),placeholder:"sample.txt"})),v("label",null,"百度网盘目标路径",v("input",{value:W.remotePath,onChange:(S)=>G((e)=>({...e,remotePath:S.target.value})),placeholder:`${O}/sample.txt`})),v("button",{type:"submit",className:"primary-btn",disabled:!k||r.actionLoading},"上传 staging 文件")),v("form",{className:"stack-form",onSubmit:m,"data-testid":"baidu-download-form"},v("label",null,"文件 fs_id",v("input",{value:K.fsId,onChange:(S)=>E((e)=>({...e,fsId:S.target.value})),placeholder:"从文件表填入"})),v("label",null,"保存到 staging 路径",v("input",{value:K.localPath,onChange:(S)=>E((e)=>({...e,localPath:S.target.value})),placeholder:"downloads/"})),v("button",{type:"submit",className:"primary-btn",disabled:!k||!K.fsId||r.actionLoading},"下载到 staging"))),r.selfTest?v("div",{className:"baidu-account-card","data-testid":"baidu-netdisk-self-test-result"},v("div",{className:"node-version-line"},v(dy,{status:r.selfTest.ok?"online":"warn"},r.selfTest.ok?"self-test ok":"self-test"),v("span",null,uy(r.selfTest.sizeBytes))),v("h3",null,r.selfTest.remotePath||"Baidu self-test"),v("div",{className:"microservice-ref-card"},v("span",null,"fs_id"),v("strong",null,r.selfTest.fsId||"--"),v("code",null,r.selfTest.downloadedPath||"--")),v("div",{className:"microservice-ref-card"},v("span",null,"MD5"),v("strong",null,r.selfTest.downloadedMd5||"--"),v("code",null,r.selfTest.expectedMd5||"--")),v(K1,{title:"Baidu Self Test",data:r.selfTest,onOpen:u,testId:"raw-baidu-self-test"})):null,I.length===0?v(J_,{title:"暂无传输任务",text:"上传/下载任务会在后端容器内执行,避免大文件穿过 UniDesk proxy"}):v("div",{className:"table-wrap","data-testid":"baidu-transfer-table"},v("table",null,v("thead",null,v("tr",null,v("th",null,"状态"),v("th",null,"方向"),v("th",null,"路径"),v("th",null,"进度"),v("th",null,"时间"),v("th",null,"操作"))),v("tbody",null,I.map((S)=>v("tr",{key:S.id},v("td",null,v(dy,{status:S.status},S.status)),v("td",null,S.direction),v("td",null,v("strong",null,S.remotePath||S.fsId||"--"),v("code",null,S.localPath||"--"),S.error?v("span",{className:"form-error"},S.error):null),v("td",null,v(xV,{percent:S.progressPercent}),v("span",{className:"muted"},`${uy(S.bytesDone)} / ${uy(S.sizeBytes)}`)),v("td",null,Kf(S.updatedAt)),v("td",null,v("div",{className:"inline-actions"},["queued","running"].includes(S.status)?v("button",{type:"button",className:"ghost-btn",onClick:()=>M(S.id,"cancel")},"取消"):null,["failed","canceled"].includes(S.status)?v("button",{type:"button",className:"ghost-btn",onClick:()=>M(S.id,"retry")},"重试"):null,v(K1,{title:`Transfer ${S.id}`,data:S,onOpen:u}))))))))),v(ey,{title:"安全与日志",eyebrow:"redacted diagnostics",className:"baidu-wide-panel",loading:r.loading,actions:v("div",{className:"panel-actions inline-actions"},v(K1,{title:"Baidu Health",data:P,onOpen:u,testId:"raw-baidu-health"}),v(K1,{title:"Baidu Logs",data:r.logs,onOpen:u,testId:"raw-baidu-logs"}))},v("div",{className:"policy-grid"},v("article",null,v("b",null,"私有后端"),v("span",null,"4244 只在 Compose 网络 expose,浏览器经 UniDesk 同源代理访问")),v("article",null,v("b",null,"Token 加密"),v("span",null,"access/refresh token 使用 BAIDU_NETDISK_TOKEN_KEY 加密后写入 PostgreSQL")),v("article",null,v("b",null,"无浏览器大文件流"),v("span",null,"上传/下载以容器 staging 目录为边界,避免 proxy 文本通道传输大字节流"))))))}var _8=cf(Yu(),1);var s=_8.default.createElement,{useEffect:vV}=_8.default,y8=_8.default.useState,fr={label:"主用户私聊账号",userId:645275593};function x7(f){let u=Number(f);return Number.isFinite(u)?u.toLocaleString("zh-CN"):"--"}async function N1(f,u={}){return Df(f,{failureFields:["ok","success"],...u})}function r8({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return s("span",{className:`status-badge ${l}`},u||f||"unknown")}function Q_({label:f,value:u,hint:l,tone:y}){return s("article",{className:`metric-card ${y||""}`},s("div",{className:"metric-label"},f),s("div",{className:"metric-value"},u),s("div",{className:"metric-hint"},l))}function W_({title:f,eyebrow:u,actions:l,children:y,className:r,loading:_}){return s("section",{className:`panel ${r||""}`},s("div",{className:"panel-head"},s("div",null,u?s("p",{className:"panel-eyebrow"},u):null,s(_u,{title:f,loading:_})),l?s("div",{className:"panel-actions"},l):null),s("div",{className:"panel-body"},y))}function J3({title:f,data:u,onOpen:l,testId:y}){return s("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:(r)=>{r?.stopPropagation?.(),l(f,u)}},"查看原始JSON")}function U3({title:f,text:u}){return s("div",{className:"empty-state"},s("strong",null,f),s("span",null,u))}function bV(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function hV(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function mV(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function ly(f,u){return`${f}/microservices/claudeqq/proxy${u}`}function pV(f){return Array.isArray(f?.events)?f.events.slice(0,80):[]}function IV(f){return Array.isArray(f?.subscriptions)?f.subscriptions.slice(0,50):[]}function gV(f){return Array.isArray(f?.messages)?f.messages.slice(0,30):[]}function Hz(f){let u=f?.text??f?.message??f?.raw?.raw_message;if(typeof u!=="string")return"--";return u.length>180?`${u.slice(0,177)}...`:u}function Oz(f){let u=f?.groupId??f?.group_id??(f?.message_type==="group"?f?.target_id:void 0),l=f?.userId??f?.user_id??(f?.message_type==="private"?f?.target_id:void 0);if(u)return`群 ${u}`;if(l)return`私聊 ${l}`;return"--"}function qz({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let y=f.find((n)=>n.id==="claudeqq")||null,[r,_]=y8({loading:!1,qrLoading:!1,error:"",health:null,status:null,napcatLogin:null,napcatQrcode:null,qrcodeFetched:!1,qrcodeRefreshedAt:null,events:null,subscriptions:null,sent:null,refreshedAt:null}),[$,j]=y8({targetType:"private",targetId:String(fr.userId),message:""}),[A,F]=y8({name:"unidesk-callback",targetUrl:"",eventTypes:"message",secret:""}),[U,Q]=y8("");async function W(){if(!y)return;_((n)=>({...n,loading:!0,error:""}));try{let[n,B,D,I,p]=await Promise.all([N1(`${l}/microservices/claudeqq/health`),N1(ly(l,"/api/server/status")),N1(ly(l,"/api/events/recent?limit=60")),N1(ly(l,"/api/events/subscriptions")),N1(ly(l,"/api/messages/sent?limit=20"))]);if(_((k)=>({...k,loading:!1,error:"",health:n,status:B,events:D,subscriptions:I,sent:p,refreshedAt:new Date})),!r.qrcodeFetched)G(!1)}catch(n){_((B)=>({...B,loading:!1,error:wf(n,"ClaudeQQ 加载失败")}))}}async function G(n=!0){if(!y)return;_((B)=>({...B,qrLoading:!0,error:n?"":B.error}));try{let B=await N1(ly(l,"/api/napcat/login")),D=B?.napcat?.qrcode||B?.qrcode||null;_((I)=>({...I,qrLoading:!1,error:"",napcatLogin:B,napcatQrcode:D,qrcodeFetched:!0,qrcodeRefreshedAt:new Date}))}catch(B){_((D)=>({...D,qrLoading:!1,error:n||!D.napcatQrcode?wf(B,"NapCat 二维码加载失败"):D.error}))}}async function K(n){n.preventDefault(),Q("");let B=Number($.targetId);if(!Number.isFinite(B)||B<=0||$.message.trim().length===0){_((D)=>({...D,error:"请填写 QQ 目标和消息内容"}));return}try{await N1(ly(l,"/api/push/text"),{method:"POST",body:JSON.stringify({userId:$.targetType==="private"?B:void 0,groupId:$.targetType==="group"?B:void 0,message:$.message})}),j((D)=>({...D,targetType:"private",targetId:String(fr.userId),message:""})),Q("消息推送请求已提交"),await W()}catch(D){_((I)=>({...I,error:wf(D,"发送失败")}))}}async function E(n){if(n.preventDefault(),Q(""),A.targetUrl.trim().length===0){_((B)=>({...B,error:"请填写订阅回调 URL"}));return}try{await N1(ly(l,"/api/events/subscriptions"),{method:"POST",body:JSON.stringify({name:A.name,targetUrl:A.targetUrl,eventTypes:A.eventTypes.split(",").map((B)=>B.trim()).filter(Boolean),secret:A.secret||void 0,enabled:!0})}),Q("事件订阅已创建"),await W()}catch(B){_((D)=>({...D,error:wf(B,"订阅失败")}))}}async function O(n){if(!n)return;Q("");try{await N1(ly(l,`/api/events/subscriptions/${encodeURIComponent(n)}`),{method:"DELETE"}),Q("事件订阅已删除"),await W()}catch(B){_((D)=>({...D,error:wf(B,"删除订阅失败")}))}}if(vV(()=>{if(!y)return;W();return},[y?.id,y?.runtime?.providerStatus]),!y)return s(U3,{title:"ClaudeQQ 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=claudeqq"});let z=bV(y),Z=mV(y),N=hV(y),H=r.health||{},Y=r.status||{},w=r.napcatLogin||{},V=H.napcat||Y.napcat||{},X={...w.napcat||{},...V,qrcode:r.napcatQrcode||{},webui:V.webui||w.napcat?.webui},i=w.login||{},m=r.napcatQrcode||{},M=pV(r.events),c=IV(r.subscriptions),C=gV(r.sent),T=Boolean(X.httpConnected||i.ready),R=String(X.loginState||i.state||(T?"logged_in":"unknown")),P=Boolean(m.available&&m.dataUrl);return s("div",{className:"claudeqq-page","data-testid":"claudeqq-page"},s(W_,{title:"ClaudeQQ 工作台",eyebrow:"D601 QQ Event Gateway",loading:r.loading,actions:s("div",{className:"panel-actions"},s("button",{type:"button",className:"ghost-btn",onClick:W,disabled:r.loading,"data-testid":"claudeqq-refresh-button"},r.loading?"刷新中":"刷新"),s(J3,{title:"ClaudeQQ 用户服务",data:y,onOpen:u,testId:"raw-claudeqq-service"}))},s("div",{className:"findjob-hero"},s("div",null,s("div",{className:"node-version-line"},s(r8,{status:z.providerStatus==="online"?"online":"warn"},z.providerStatus||"unknown"),s("span",null,y.providerId),s("span",null,N.public?"公网暴露":"仅 UniDesk frontend 代理访问")),s("p",{className:"muted paragraph"},y.description)),s("div",{className:"microservice-ref-card"},s("span",null,"Repo"),s("strong",null,Z.url||"--"),s("code",null,Z.commitId||"--")),s("div",{className:"microservice-ref-card"},s("span",null,"D601 Docker"),s("strong",null,`${N.nodeBindHost||"--"}:${N.nodePort||"--"}`),s("code",null,`${Z.composeFile||"--"} / ${Z.composeService||"--"}`))),s(Au,{error:r.error,wide:!0}),U?s("div",{className:"form-success wide"},U):null),s("div",{className:"metric-grid"},s(Q_,{label:"Health",value:H.ok||H.status==="ok"?"OK":"--",hint:"D601 /health",tone:H.ok||H.status==="ok"?"ok":"warn"}),s(Q_,{label:"NapCat HTTP",value:X.httpConnected||X.http?.connected?"OK":"离线",hint:`${X.httpHost||H.napcat?.httpHost||"--"}:${X.httpPort||H.napcat?.httpPort||"--"}`}),s(Q_,{label:"NapCat WS",value:X.wsConnected||X.ws?.connected?"OK":"离线",hint:`${X.wsHost||H.napcat?.wsHost||"--"}:${X.wsPort||H.napcat?.wsPort||"--"}`}),s(Q_,{label:"事件缓存",value:x7(r.events?.count??M.length),hint:"recent QQ events"}),s(Q_,{label:"订阅",value:x7(r.subscriptions?.count??c.length),hint:"webhook subscribers"}),s(Q_,{label:"已发送",value:x7(r.sent?.count??C.length),hint:"sent message log"})),s("div",{className:"findjob-grid"},s(W_,{title:"NapCat 容器登录",eyebrow:"QR Login",className:"claudeqq-login-panel",loading:r.qrLoading,actions:s("div",{className:"panel-actions inline-actions"},s("button",{type:"button",className:"ghost-btn",onClick:()=>G(!0),disabled:r.qrLoading,"data-testid":"claudeqq-napcat-refresh"},r.qrLoading?"刷新中":"手动刷新二维码"),s(J3,{title:"NapCat Login",data:r.napcatLogin,onOpen:u,testId:"raw-claudeqq-napcat-login"}))},s("div",{className:"claudeqq-login-card","data-testid":"claudeqq-napcat-login"},s("div",{className:"claudeqq-qr-frame"},P?s("img",{src:m.dataUrl,alt:"NapCat QQ 登录二维码","data-testid":"claudeqq-napcat-qrcode"}):s(U3,{title:"等待二维码",text:"NapCat 容器启动后会把登录二维码写入 cache/qrcode.png"})),s("div",{className:"claudeqq-login-copy"},s("div",{className:"node-version-line"},s(r8,{status:T?"online":P?"warn":"unknown"},T?"已登录":P?"待扫码":"等待二维码"),s("span",null,R),s("span",null,"D601 containerized")),s("p",{className:"muted paragraph"},T?"NapCat 已登录,ClaudeQQ 可通过容器内 HTTP/WS 链路收发 QQ 消息。":"用手机 QQ 扫描二维码授权登录。二维码只在首次加载或手动刷新时更新,D601 的 NapCat 端口仍只绑定 127.0.0.1。"),s("div",{className:"microservice-ref-card"},s("span",null,"NapCat WebUI"),s("strong",null,X.webui?.url||"http://napcat:6099/webui"),s("code",null,"local-only / proxied QR login")),s("div",{className:"microservice-ref-card"},s("span",null,"QR Source"),s("strong",null,m.modifiedAt?Kf(m.modifiedAt):r.qrcodeRefreshedAt?Kf(r.qrcodeRefreshedAt):"--"),s("code",null,m.file||"/napcat/cache/qrcode.png"))))),s(W_,{title:"消息推送",eyebrow:"Push API"},s("div",{className:"microservice-ref-card"},s("span",null,fr.label),s("strong",null,String(fr.userId)),s("code",null,"private userId / 默认推送测试目标")),s("form",{className:"stack-form",onSubmit:K,"data-testid":"claudeqq-push-form"},s("label",null,"目标类型",s("select",{value:$.targetType,onChange:(n)=>j((B)=>({...B,targetType:n.target.value}))},s("option",{value:"private"},"私聊 userId"),s("option",{value:"group"},"群 groupId"))),s("label",null,"QQ ID",s("input",{value:$.targetId,onChange:(n)=>j((B)=>({...B,targetId:n.target.value})),placeholder:String(fr.userId)})),s("label",null,"消息内容",s("textarea",{value:$.message,onChange:(n)=>j((B)=>({...B,message:n.target.value})),rows:4,placeholder:"通过 ClaudeQQ 推送一条 QQ 消息"})),s("button",{type:"submit",className:"primary-btn"},"发送 QQ 消息")),s("p",{className:"muted paragraph"},`主 server 和其他用户服务可通过 UniDesk 同源代理调用 /api/push/text;当前人工推送测试默认使用 ${fr.label} ${fr.userId},不需要暴露 D601 后端端口。`)),s(W_,{title:"QQ 事件订阅",eyebrow:"Webhook Subscription",loading:r.loading},s("form",{className:"stack-form",onSubmit:E,"data-testid":"claudeqq-subscription-form"},s("label",null,"订阅名称",s("input",{value:A.name,onChange:(n)=>F((B)=>({...B,name:n.target.value}))})),s("label",null,"回调 URL",s("input",{value:A.targetUrl,onChange:(n)=>F((B)=>({...B,targetUrl:n.target.value})),placeholder:"http://host.docker.internal:18080/..."})),s("label",null,"事件类型",s("input",{value:A.eventTypes,onChange:(n)=>F((B)=>({...B,eventTypes:n.target.value})),placeholder:"message,notice"})),s("label",null,"签名密钥",s("input",{value:A.secret,onChange:(n)=>F((B)=>({...B,secret:n.target.value})),placeholder:"可选,生成 x-claudeqq-signature"})),s("button",{type:"submit",className:"primary-btn"},"创建订阅")),c.length===0?s(U3,{title:"暂无订阅",text:"可以为 main server 或其他用户服务注册 HTTP webhook"}):s("div",{className:"table-wrap","data-testid":"claudeqq-subscription-table"},s("table",null,s("thead",null,s("tr",null,s("th",null,"名称"),s("th",null,"状态"),s("th",null,"事件"),s("th",null,"回调"),s("th",null,"最近投递"),s("th",null,"操作"))),s("tbody",null,c.map((n)=>s("tr",{key:n.id},s("td",null,s("strong",null,n.name||n.id),s("code",null,n.id||"--")),s("td",null,s(r8,{status:n.enabled?"online":"warn"},n.enabled?"enabled":"disabled")),s("td",null,Array.isArray(n.eventTypes)?n.eventTypes.join(", "):"message"),s("td",null,n.targetUrl||"--"),s("td",null,n.lastDelivery?`${n.lastDelivery.ok?"OK":"FAIL"} ${Kf(n.lastDelivery.at)}`:"--"),s("td",null,s("button",{type:"button",className:"ghost-btn",onClick:()=>O(n.id)},"删除"))))))),s("div",{className:"panel-actions inline-actions"},s(J3,{title:"ClaudeQQ Subscriptions",data:r.subscriptions,onOpen:u,testId:"raw-claudeqq-subscriptions"}))),s(W_,{title:"最近 QQ 事件",eyebrow:r.refreshedAt?`Updated ${Uu(r.refreshedAt)}`:"Event Stream",loading:r.loading},M.length===0?s(U3,{title:"暂无事件",text:"等待 NapCat WebSocket 上报 QQ 消息事件,或通过订阅 API 消费后续事件"}):s("div",{className:"table-wrap","data-testid":"claudeqq-event-list"},s("table",null,s("thead",null,s("tr",null,s("th",null,"时间"),s("th",null,"类型"),s("th",null,"会话"),s("th",null,"消息"),s("th",null,"ID"))),s("tbody",null,M.map((n)=>s("tr",{key:n.id},s("td",null,Kf(n.receivedAt||n.timestamp)),s("td",null,s(r8,{status:n.postType||n.eventType},n.postType||n.eventType||"--")),s("td",null,Oz(n)),s("td",null,Hz(n)),s("td",null,s("code",null,n.messageId||n.id||"--"))))))),s("div",{className:"panel-actions inline-actions"},s(J3,{title:"ClaudeQQ Events",data:r.events,onOpen:u,testId:"raw-claudeqq-events"}))),s(W_,{title:"已发送消息",eyebrow:`${C.length} Sent`,loading:r.loading},C.length===0?s(U3,{title:"暂无发送记录",text:"发送日志来自 ClaudeQQ bot_workspace/messages/sent_messages.jsonl"}):s("div",{className:"table-wrap"},s("table",null,s("thead",null,s("tr",null,s("th",null,"时间"),s("th",null,"目标"),s("th",null,"消息"),s("th",null,"结果"))),s("tbody",null,C.map((n,B)=>s("tr",{key:n.id||B},s("td",null,Kf(n.timestamp||n.sentAt||n.createdAt)),s("td",null,Oz(n)),s("td",null,Hz(n)),s("td",null,n.status||n.messageId||n.message_id||"--")))))),s("div",{className:"panel-actions inline-actions"},s(J3,{title:"ClaudeQQ Sent Messages",data:r.sent,onOpen:u,testId:"raw-claudeqq-sent"})))))}var N3=cf(Yu(),1);var Xz=cf(Yu(),1),Fu=Xz.default.createElement;function Yz({markdown:f,className:u,testId:l}){let y=String(f??"").trimEnd(),r=["markdown-body",u].filter(Boolean).join(" ");return Fu("div",{className:r,"data-testid":l},wz(y,"md"))}function wz(f,u){let l=kV(f).split(` +`),y=[],r=0;while(r\s?/u.test(_)){let Q=[];while(r\s?(.*)$/u);if(G!==null){Q.push(G[1]),r+=1;continue}if(W.trim().length===0){Q.push(""),r+=1;continue}break}y.push(Fu("blockquote",{key:`${u}-quote-${r}`},wz(Q.join(` +`),`${u}-quote-${r}`)));continue}if(Tz(l,r)){let Q=r,W=Q3(l[r]??""),G=Q3(l[r+1]??"");r+=2;let K=[];while(r0)K.push(Q3(l[r]??"")),r+=1;y.push(dV(W,G,K,`${u}-table-${Q}`));continue}let A=$8(_);if(A!==null){let Q=r,W=A.ordered,G=A.start,K=[];while(roV(O,`${u}-list-${Q}-${z}`))));continue}let F=r,U=[];while(r0&&!sV(l,r))U.push(l[r].trim()),r+=1;if(U.length===0)U.push(_.trim()),r+=1;y.push(Fu("p",{key:`${u}-p-${F}`},kl(U.join(` +`),`${u}-p-${F}`)))}return y}function kV(f){return String(f||"").replace(/\r\n/gu,` +`).replace(/\r/gu,` +`).trimEnd()}function Dz(f){let u=f.match(/^\s*(```+|~~~+)\s*([A-Za-z0-9_-]+)?\s*$/u);if(u===null)return null;let l=u[1];return{marker:l.startsWith("`")?"`":"~",length:l.length,language:u[2]||""}}function tV(f,u){let l=f.trim();return l.length>=u.length&&l.split("").every((y)=>y===u.marker)}function v7(f){return/^(?: {4}|\t)/u.test(f)}function Vz(f,u,l){let y=u.trim().length>0?`language-${fL(u)}`:void 0;return Fu("pre",{key:l,className:"markdown-code-block"},Fu("code",{className:y},f))}function sV(f,u){let l=f[u]??"";if(l.trim().length===0)return!0;return Dz(l)!==null||v7(l)||/^(#{1,6})\s+.+$/u.test(l)||/^\s*(?:---+|\*\*\*+|___+)\s*$/u.test(l)||/^\s*>\s?/u.test(l)||Tz(f,u)||$8(l)!==null}function $8(f){let u=f.match(/^\s{0,3}[-*+]\s+(.+)$/u);if(u!==null)return{ordered:!1,text:u[1]};let l=f.match(/^\s{0,3}(\d+)[.)]\s+(.+)$/u);if(l!==null)return{ordered:!0,start:Number(l[1]),text:l[2]};return null}function oV(f,u){let l=f.match(/^\[([ xX])\]\s+(.+)$/u);if(l!==null){let y=l[1].toLowerCase()==="x";return Fu("li",{key:u,className:"task-list-item"},Fu("input",{type:"checkbox",checked:y,readOnly:!0,tabIndex:-1}),Fu("span",null,kl(l[2],`${u}-task`)))}return Fu("li",{key:u},kl(f,u))}function Tz(f,u){let l=f[u]??"",y=f[u+1]??"";if(!l.includes("|")||!y.includes("|"))return!1;let r=Q3(l),_=Q3(y);return r.length>1&&_.length===r.length&&_.every(($)=>/^:?-{3,}:?$/u.test($.trim()))}function Q3(f){let u=f.trim();if(u.startsWith("|"))u=u.slice(1);if(u.endsWith("|"))u=u.slice(0,-1);return u.split("|").map((l)=>l.trim())}function aV(f){let u=f.trim();if(u.startsWith(":")&&u.endsWith(":"))return"center";if(u.endsWith(":"))return"right";if(u.startsWith(":"))return"left";return}function dV(f,u,l,y){let r=u.map(aV);return Fu("div",{key:y,className:"markdown-table-wrap"},Fu("table",null,Fu("thead",null,Fu("tr",null,f.map((_,$)=>Fu("th",{key:`${y}-h-${$}`,style:r[$]?{textAlign:r[$]}:void 0},kl(_,`${y}-h-${$}`))))),Fu("tbody",null,l.map((_,$)=>Fu("tr",{key:`${y}-r-${$}`},f.map((j,A)=>Fu("td",{key:`${y}-r-${$}-${A}`,style:r[A]?{textAlign:r[A]}:void 0},kl(_[A]||"",`${y}-r-${$}-${A}`))))))))}function kl(f,u){let l=[],y=/`([^`\n]+)`|\[([^\]\n]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|(https?:\/\/[^\s<>)]+)|\*\*([^*\n]+)\*\*|__([^_\n]+)__|~~([^~\n]+)~~|\*([^*\n]+)\*|_([^_\n]+)_/gu,r=0,_=0;for(let $ of f.matchAll(y)){let j=$[0],A=$.index??0;Lz(l,f.slice(r,A),`${u}-text-${_}`),r=A+j.length;let F=`${u}-inline-${_}`;if(_+=1,$[1]!==void 0){l.push(Fu("code",{key:F},$[1]));continue}if($[2]!==void 0&&$[3]!==void 0){l.push(Bz($[2],$[3],F));continue}if($[4]!==void 0){l.push(Bz($[4],$[4],F));continue}let U=$[5]??$[6];if(U!==void 0){l.push(Fu("strong",{key:F},kl(U,`${F}-strong`)));continue}if($[7]!==void 0){l.push(Fu("del",{key:F},kl($[7],`${F}-del`)));continue}let Q=$[8]??$[9];if(Q!==void 0)l.push(Fu("em",{key:F},kl(Q,`${F}-em`)))}return Lz(l,f.slice(r),`${u}-text-tail`),l}function Lz(f,u,l){if(u.length===0)return;u.split(` +`).forEach((r,_)=>{if(_>0)f.push(Fu("br",{key:`${l}-br-${_}`}));if(r.length>0)f.push(r)})}function Bz(f,u,l){let y=eV(u);if(y===null)return Fu("span",{key:l},f);let r=/^(?:https?:|mailto:)/iu.test(y);return Fu("a",{key:l,href:y,target:r?"_blank":void 0,rel:r?"noreferrer":void 0},kl(f,`${l}-label`))}function eV(f){let u=String(f||"").trim();if(/^(?:https?:|mailto:)/iu.test(u))return u;if(u.startsWith("/")&&!u.startsWith("//"))return u;if(u.startsWith("#"))return u;return null}function fL(f){return String(f||"").toLowerCase().replace(/[^a-z0-9_-]+/gu,"-").replace(/^-+|-+$/gu,"")||"text"}var s7=cf(Yu(),1);var Tf=s7.default.createElement,{useEffect:uL,useRef:nz}=s7.default;function lL(f,u){return kz(f.toTrace(u))}function yL(f){let u=Number(f);if(!Number.isFinite(u)||u<0)return"--";let l=Math.floor(u/1000),y=Math.floor(l/3600),r=Math.floor(l%3600/60),_=l%60;if(y>0)return`${y}h ${String(r).padStart(2,"0")}m`;if(r>0)return`${r}m ${String(_).padStart(2,"0")}s`;return`${_}s`}function ur(f){let u=Number(f);return Number.isFinite(u)&&u>=0?u:null}function xz(f,u=180){let l=String(f||"").replace(/\s+/gu," ").trim();return l.length>u?`${l.slice(0,u-1)}…`:l}function rL(f){if(!f)return 0;return f.split(/\r?\n/u).length}function I7(f){return{ran:"Ran",explored:"Explored",edited:"Edited",toolGroup:"Tool calls",plan:"Plan",message:"Message",system:"System",error:"Error"}[f]||"Message"}function g7(f){let u=Number(f||0);return Number.isFinite(u)&&u>0?`… +${Math.floor(u)} lines`:""}function _L(f){return(Array.isArray(f)?f:[]).reduce((u,l)=>Math.max(u,Number(l?.seq??0)),0)}function Mz(f){return["explored","edited","ran"].includes(String(f?.kind||""))}function vz(f){let u={read:0,edit:0,run:0};for(let l of f){let y=String(l?.kind||"");if(y==="explored")u.read+=1;else if(y==="edited")u.edit+=1;else if(y==="ran")u.run+=1}return u}function bz(f){let u=vz(f);return`${u.read} read, ${u.edit} edit, ${u.run} run`}function hz(f){return f.replace(/^['"`([{<]+/u,"").replace(/['"`)\]}>.,;:]+$/u,"").replace(/:\d+(?::\d+)?$/u,"").trim()}function Sz(f){let l=String(f||"").match(/(?:~|\.{1,2}|\/)?(?:[A-Za-z0-9_.@+-]+\/)+[A-Za-z0-9_.@+-]+|[A-Za-z0-9_.@+-]+\.(?:c|cc|cpp|h|hpp|js|jsx|ts|tsx|json|md|py|sh|toml|ya?ml|txt|log|lock)/gu)||[],y=[];for(let r of l){let _=hz(r);if(_.length<2||_.includes("..."))continue;if(/^(http|https|status|method)$/iu.test(_))continue;if(!y.includes(_))y.push(_)}return y}function b7(f,u=4){if(f.length===0)return"--";let l=f.slice(0,u).join(", ");return f.length>u?`${l} +${f.length-u}`:l}function Pz(f){let u="";for(let l of f){if(l.length===0)continue;if(u.length>0&&!u.endsWith(` +`)&&!l.startsWith(` `))u+=` -`;u+=_}return u}function YG(f){let u=String(f||"").replace(/\r\n/gu,` +`;u+=l}return u}function mz(f){let u=String(f||"").replace(/\r\n/gu,` `).replace(/\r/gu,` `).trimEnd();return u.length>0?u.split(` -`):[]}function lj(f){let u=String(f.status||"").trim();if(u.length>0)return u;let _=String(f.bodyPreview||"");return/^(item\/[A-Za-z]+(?:\/[A-Za-z]+)?):/u.exec(_)?.[1]||"item/fileChange"}function pO(f){let u=String(f.bodyPreview||"");return/file changes status=([A-Za-z0-9_-]+)/u.exec(u)?.[1]}function kO(f){return/^item\/(?:started|completed): file changes status=/u.test(String(f||"").trim())}function BG(f){if(String(f.kind||"")!=="edited")return!1;let u=String(f.status||""),_=String(f.title||""),y=String(f.bodyPreview||""),l=String(f.commandPreview||"");if(_==="Edited files")return!0;if(/^item\/fileChange\//u.test(u))return!0;if((u==="item/started"||u==="item/completed")&&/file changes status=/u.test(y))return!0;if(/^Success\. Updated the following files:/mu.test(y))return!0;if(/^diff --git /mu.test(y))return!0;return l.length===0&&/^([AMDRCU?]{1,2})\s+\S+/mu.test(y)}function vl(f){return LG(String(f||"").replace(/^[ab]\//u,"").trim())}function Jj(f){let u=/^([AMDRCU?]{1,2})\s+(.+)$/u.exec(f);if(!u)return null;let _=vl(u[2]||"");return _.length>0?{status:u[1]||"M",path:_}:null}function Fj(f){let u=/^\*\*\*\s+(Add|Update|Delete)\s+File:\s+(.+)$/u.exec(f);if(u){let y=u[1]==="Add"?"A":u[1]==="Delete"?"D":"M",l=vl(u[2]||"");return l.length>0?{status:y,path:l}:null}let _=/^\*\*\*\s+Move to:\s+(.+)$/u.exec(f);if(_){let y=vl(_[1]||"");return y.length>0?{status:"R",path:y}:null}return null}function mO(f){let u=[],_=(l,$)=>{let j=vl($);if(j.length===0||j==="/dev/null")return;let J=u.find((F)=>F.path===j);if(J){if(J.status==="M"&&l!=="M")J.status=l;return}u.push({status:l,path:j})},y="";for(let l of YG(f)){let $=Jj(l)||Fj(l);if($!==null){_($.status,$.path),y=$.path;continue}let j=/^diff --git a\/(.+?) b\/(.+)$/u.exec(l);if(j){let G=j[2]||j[1]||"";_("M",G),y=vl(G);continue}let J=/^\+\+\+ b\/(.+)$/u.exec(l);if(J&&J[1]!=="/dev/null"){_("M",J[1]||""),y=vl(J[1]||"");continue}if(/^new file mode /u.exec(l)&&y)_("A",y);if(/^deleted file mode /u.exec(l)&&y)_("D",y);let U=/^rename to (.+)$/u.exec(l);if(U)_("R",U[1]||"")}return u}function iO(f){if(Jj(f)!==null||Fj(f)!==null)return"file";if(/^(diff --git |index |--- |\+\+\+ |\*\*\* Begin Patch|\*\*\* End Patch)/u.test(f))return"meta";if(/^@@ /u.test(f))return"hunk";if(/^\+/u.test(f))return"add";if(/^-/u.test(f))return"del";if(/^(Success\.|No changes|Updated\b|Created\b|Deleted\b|Added\s+\d+\s+lines?|Wrote\s+\d+\s+lines?|Read\s+\d+\s+files?|\.\.\.\[patch content truncated)/iu.test(f))return"note";return"context"}function gO(f){return YG(f).map((u)=>{let _=Jj(u)||Fj(u);if(_!==null)return{text:u,kind:"file",path:_.path,status:_.status};return{text:u,kind:iO(u)}})}function nO(f){return f.reduce((u,_)=>{if(_.kind==="add")u.added+=1;else if(_.kind==="del")u.removed+=1;return u},{added:0,removed:0})}function qG(f,u){return`${u} ${f} line${f===1?"":"s"}`}function tO(f,u){let _=[];if(f>0)_.push(qG(f,"Added"));if(u>0)_.push(qG(u,"removed"));return _.join(", ")}function sO(f){for(let _=f.length-1;_>=0;_-=1){let y=String(f[_]?.status||"").trim();if(y.length>0)return y}let u=String(f[f.length-1]?.method||"").trim();if(u==="item/fileChange/outputDelta")return"updated";if(u==="item/started")return"started";if(u==="item/completed")return"completed";return u.replace(/^item\//u,"")||"changed"}function oO(f){return`${f} file${f===1?"":"s"}`}function DG(f){let u=f.length>0?f:[],_=ZG(u.map((W)=>String(W.bodyPreview||""))),l=ZG(u.map((W)=>String(W.bodyPreview||"")).filter((W)=>W.trim().length>0&&!kO(W)))||_,$=mO(l||_),j=u.map((W)=>({method:lj(W),status:pO(W),at:W.at})),J=gO(l||_),F=nO(J),A=tO(F.added,F.removed),U=$.length>0?oO($.length):"",G=A.length>0?`${A}${U?` in ${U}`:""}`:$.length>0?U:OG(l||_||"File changes",72);return{status:sO(j),summary:G,files:$,stages:j,lines:J,addedLines:F.added,removedLines:F.removed,rawText:_}}function aO(f){let u=f[0],_=f[f.length-1]||u,y=DG(f);return{...u,seq:Number.isFinite(Number(_?.seq))?Number(_?.seq):Number(u?.seq??0),at:_?.at||u?.at,title:y.files.length>0?`Edited ${y.summary}`:"Edited files",status:y.status,commandPreview:"",commandOmittedLines:void 0,bodyPreview:y.rawText,bodyOmittedLines:f.reduce((l,$)=>l+Number($.bodyOmittedLines||0),0)||void 0,rawSeqs:f.flatMap((l)=>Array.isArray(l?.rawSeqs)?l.rawSeqs:[l?.seq]).filter((l)=>l!==void 0),editObservation:y}}function dO(f){let u=Array.isArray(f)?f:[],_=[],y=[],l=()=>{if(y.length===0)return;_.push(aO(y)),y=[]};for(let $ of u){if(BG($)){if(lj($)==="item/started"&&y.length>0)l();if(y.push($),lj($)==="item/completed")l();continue}l(),_.push($)}return l(),_}function wG(f){let u=[],_=[],y=[],l=(A,U)=>{for(let G of U)if(!A.includes(G))A.push(G)};for(let A of f){let U=String(A?.kind||""),G=[A?.commandPreview,A?.bodyPreview,A?.title].map((W)=>String(W||"")).join(` -`);if(U==="explored")l(u,KG(G));else if(U==="edited")l(_,KG(G));else if(U==="ran"){let W=String(A?.commandPreview||A?.title||"").trim();if(W.length>0&&!y.includes(W))y.push(OG(W,90))}}let $=f.map((A)=>Date.parse(String(A?.at||""))).filter((A)=>Number.isFinite(A)),j=$.length>=2?Math.max(0,Math.max(...$)-Math.min(...$)):0,J=f.reduce((A,U)=>A+(wy(U?.durationMs)??wy(U?.elapsedMs)??0),0),F=j>0?j:J;return{readFiles:u,editedFiles:_,runCommands:y,durationLabel:hO(F)}}function eO(f,u=3){let _=Array.isArray(f)?f:[],y=[],l=[],$=Math.max(0,u),j=new Set;for(let F=_.length-1;F>=0&&$>0;F-=1){let A=_[F];if(!zG(A))continue;j.add(A),$-=1}let J=()=>{if(l.length>=2){let F=XG(l);y.push({seq:Number(l[0]?.seq??0),at:l[0]?.at||l.at(-1)?.at,kind:"toolGroup",title:NG(l),status:`${l.length} calls`,items:l,counts:F,digest:wG(l),rawSeqs:l.flatMap((A)=>Array.isArray(A?.rawSeqs)?A.rawSeqs:[A?.seq]).filter((A)=>A!==void 0)})}else y.push(...l);l=[]};for(let F of _){if(zG(F)&&!j.has(F)){l.push(F);continue}J(),y.push(F)}return J(),y}function TG(f){return(Array.isArray(f)?f:[]).map((u,_)=>({...u,seq:Number.isFinite(Number(u?.seq))?Number(u.seq):_+1,kind:String(u?.kind||"message"),at:u?.at===void 0?void 0:String(u.at),durationMs:wy(u?.durationMs)??void 0,title:u?.title===void 0?void 0:String(u.title),status:u?.status===void 0?void 0:String(u.status)}))}function e7(f){return wy(f?.durationMs)??wy(f?.elapsedMs)??wy(f?.timing?.durationMs)??wy(f?.metadata?.durationMs)??void 0}function fj(f,u){return f?.createdAt||f?.updatedAt||f?.completedAt||u||void 0}function uj(f,u){return f?.id||f?.messageId||u}function $j(f,u){let _=new Set(u.map((y)=>y.toLowerCase()));for(let y of Array.isArray(f?.inputFields)?f.inputFields:[]){let l=String(y?.key||"").toLowerCase();if(_.has(l))return String(y?.value||"")}return""}function fX(f){let u=String(f?.tool||f?.title||"").toLowerCase();if(/read|grep|glob|list|ls|find|search|view|cat|sed|rg/u.test(u))return"explored";if(/edit|write|patch|apply|update|create|delete/u.test(u))return"edited";let _=$j(f,["command","cmd"]);if(/\b(rg|grep|find|ls|cat|sed|tail|head|git status|git diff|ps)\b/u.test(_))return"explored";if(/\b(apply_patch|git apply|cat >|tee .*<<|sed -i|python3? .*write_text)\b/u.test(_))return"edited";return"ran"}function uX(f){let u=[],_=1;for(let y of Array.isArray(f)?f:[]){let l=y?.createdAt||y?.updatedAt||y?.completedAt,$=String(y?.role||"assistant").toLowerCase(),j=Array.isArray(y?.parts)?y.parts:[];for(let J of j){let F=String(J?.type||"").toLowerCase();if(F==="step-start"||F==="step-finish")continue;if(F==="text"||F==="reasoning"){let U=String(J?.textPreview||y?.textPreview||"").trim();if(U.length===0)continue;u.push({seq:_++,at:fj(J,l),kind:"message",title:F==="reasoning"?"Reasoning":$==="user"?"User message":$==="system"?"System message":"Assistant message",status:F==="reasoning"?"reasoning":$,bodyPreview:U,durationMs:e7(J),rawSeqs:[uj(J,_)]});continue}if(F==="tool"){let U=$j(J,["command","cmd"])||$j(J,["filePath","filepath","path"])||String(J?.title||J?.tool||"tool"),G=String(J?.outputPreview&&J.outputPreview!=="--"?J.outputPreview:J?.textPreview||"");u.push({seq:_++,at:fj(J,l),kind:fX(J),title:String(J?.title||J?.tool||"tool"),status:String(J?.status||""),commandPreview:U,bodyPreview:G,durationMs:e7(J),rawSeqs:[uj(J,_)]});continue}let A=String(J?.textPreview||J?.title||F||"").trim();if(A)u.push({seq:_++,at:fj(J,l),kind:"system",title:F||"part",bodyPreview:A,status:String(J?.status||""),durationMs:e7(J),rawSeqs:[uj(J,_)]})}if(j.length===0&&y?.textPreview)u.push({seq:_++,at:l,kind:"message",title:`${$||"assistant"} message`,status:$,bodyPreview:String(y.textPreview),rawSeqs:[y?.messageId||_]})}return u}var rG={source:"opencode",toTrace:uX};function _X(f){return String(f||"unknown").toLowerCase().replace(/[^a-z0-9_-]+/gu,"-")||"unknown"}function EG(f){let u=String(f||"M").toUpperCase();if(u.startsWith("A")||u==="??")return"added";if(u.startsWith("D"))return"deleted";if(u.startsWith("R"))return"renamed";return"modified"}function yX(f){if(f==="item/fileChange/outputDelta")return"delta";return f.replace(/^item\//u,"")}function lX(f,u){if(f.kind==="file"){let l=String(f.status||"M");return Lf("div",{key:`${u}-${f.text}`,className:`codex-edit-diff-line file ${EG(l)}`},Lf("span",{className:`codex-edit-file-status ${EG(l)}`},l),Lf("code",null,f.path||f.text.replace(/^([AMDRCU?]{1,2})\s+/u,"")))}let _=f.kind==="add"||f.kind==="del"?f.text.slice(0,1):f.kind==="hunk"?"@@":f.kind==="note"?"ok":"",y=f.kind==="add"||f.kind==="del"?f.text.slice(1):f.text;return Lf("div",{key:`${u}-${f.text}`,className:`codex-edit-diff-line ${f.kind}`},Lf("span",{className:"codex-edit-diff-sign"},_),Lf("code",null,y||" "))}function $X(f,u){let _=f.lines.length>0?f.lines:f.files.map((l)=>({text:`${l.status} ${l.path}`,kind:"file",path:l.path,status:l.status})),y=Number(f.addedLines||0)+Number(f.removedLines||0)>0;return Lf("div",{className:"codex-edit-observation","data-testid":"codex-edit-observation"},Lf("div",{className:"codex-edit-observation-head"},Lf("span",{className:"codex-edit-window-controls","aria-hidden":"true"},Lf("i",null),Lf("i",null),Lf("i",null)),Lf("strong",null,y?"git diff":"git diff --stat"),Lf("code",null,f.summary||"File changes")),f.stages.length>0?Lf("div",{className:"codex-edit-stage-strip"},f.stages.map((l,$)=>Lf("span",{key:`${l.method}-${$}`,className:`codex-edit-stage ${_X(l.status||l.method)}`},Lf("b",null,yX(l.method)),l.status?Lf("em",null,l.status):null))):null,_.length>0?Lf("div",{className:"codex-edit-diff",role:"list"},_.map(lX)):null,u?Lf("div",{className:"codex-edit-omitted"},`${u} (查看原始JSON获取完整记录)`):null)}function HG(f,u,_){let y=yj(_);return Lf("div",{className:`codex-transcript-stream ${f}`,"data-testid":`codex-trace-${f}`},Lf("span",{className:"codex-transcript-stream-label"},f),Lf("pre",{className:"codex-transcript-body"},u,y?` -${y} (查看原始JSON获取完整记录)`:""))}function MG(f,u=!1){let _=String(f.kind||"message"),y=["ran","explored","edited"].includes(_),l=yj(f.commandOmittedLines),$=yj(f.bodyOmittedLines),j=String(f.commandPreview||(y?f.title||"":"")),J=String(f.stdoutPreview||""),F=String(f.stderrPreview||""),A=J.length>0||F.length>0,U=Boolean(f.foldedReferencePrompt)&&String(f.fullPrompt||"").length>0,G=_==="edited"&&(f.editObservation!==void 0||BG(f))?f.editObservation||DG([f]):null;return Lf("article",{key:`${f.seq}-${_}`,className:`codex-transcript-item ${_} ${u?"nested":""}`},Lf("div",{className:"codex-transcript-main"},Lf("div",{className:"codex-transcript-title"},Lf("span",{className:"codex-output-channel"},_j(_)),y&&G===null?null:Lf("strong",null,G!==null?"File changes":String(f.title||_j(_))),f.status?Lf("code",null,String(G?.status||f.status)):null,Lf("time",null,zf(f.at))),j&&G===null?Lf("pre",{className:"codex-transcript-command"},j,l?` -${l}`:""):null,G!==null?$X(G,$):A?Lf("div",{className:"codex-transcript-streams"},J.length>0?HG("stdout",J,f.stdoutOmittedLines):null,F.length>0?HG("stderr",F,f.stderrOmittedLines):null):f.bodyPreview?Lf("pre",{className:"codex-transcript-body"},String(f.bodyPreview),$?` -${$} (查看原始JSON获取完整记录)`:""):null,U?Lf("details",{className:"codex-initial-prompt-full","data-testid":"codex-initial-prompt-full"},Lf("summary",null,Lf("span",null,"引用注入已折叠,点击查看最终传入 Codex 的完整 prompt"),Lf("code",null,`${f.fullPromptLines||IO(String(f.fullPrompt||""))} lines / ${f.fullPromptChars||String(f.fullPrompt||"").length} chars`)),Lf("pre",{className:"codex-transcript-body codex-transcript-full-prompt","data-testid":"codex-initial-prompt-full-text"},String(f.fullPrompt||""))):null))}function jX(f){let u=Array.isArray(f.items)?f.items:[],_=f.digest&&typeof f.digest==="object"?f.digest:wG(u);return Lf("article",{key:`${f.seq}-toolGroup`,className:"codex-transcript-item toolGroup"},Lf("div",{className:"codex-transcript-main"},Lf("details",{className:"codex-tool-group","data-testid":"codex-tool-group"},Lf("summary",null,Lf("div",{className:"codex-tool-group-head"},Lf("span",{className:"codex-output-channel"},_j("toolGroup")),Lf("strong",null,String(f.title||NG(u))),Lf("code",null,String(f.status||`${u.length} calls`)),Lf("time",null,zf(f.at)))),Lf("div",{className:"codex-tool-group-digest"},Lf("span",null,`read: ${d7(Array.isArray(_.readFiles)?_.readFiles:[])}`),Lf("span",null,`edit: ${d7(Array.isArray(_.editedFiles)?_.editedFiles:[])}`),Lf("span",null,`run: ${d7(Array.isArray(_.runCommands)?_.runCommands:[],2)}`),Lf("span",null,`duration: ${_.durationLabel||"--"}`)),Lf("div",{className:"codex-tool-group-items"},u.map((y)=>MG(y,!0))))))}var JX=16;function VG(f){return f.scrollHeight-f.scrollTop-f.clientHeight<=JX}function E4({items:f,input:u,port:_,autoScroll:y=!1,loading:l=!1,hasDetail:$=!0,emptyText:j="等待 Trace 输出...",loadingText:J="正在加载完整 Trace...",testId:F="trace-output",className:A="codex-transcript",keepRecentToolCalls:U=3,collapseTools:G=!0}){let W=GG(null),K=GG(!0),E=dO(_?bO(_,u):TG(f)),H=G?eO(E,U):E,O=cO(E);vO(()=>{let Z=W.current;if(!y||!Z)return;if(!K.current&&!VG(Z))return;Z.scrollTop=Z.scrollHeight,K.current=!0},[y,E.length,O]);let q={className:A,ref:W,onScroll:(Z)=>{let V=Z.currentTarget;K.current=VG(V)},"data-testid":F};if(l&&!$)return Lf("div",q,Lf("div",{className:"codex-output-empty"},J));return Lf("div",q,H.length===0?Lf("div",{className:"codex-output-empty"},j):H.map((Z)=>String(Z.kind||"")==="toolGroup"?jX(Z):MG(Z)))}var T=T4.default.createElement,{useEffect:P$,useMemo:PG,useRef:Zu}=T4.default,u0=T4.default.useState,FX=120,zj=24,QX=48,AX=1200;function SG(){return typeof document>"u"||document.visibilityState!=="hidden"}function C_(f,u="操作失败"){return Pf(f,u)}function X4(f){let u=Number(f);if(!Number.isFinite(u)||u<0)return"--";let _=Math.floor(u/1000),y=Math.floor(_/3600),l=Math.floor(_%3600/60),$=_%60;if(y>0)return`${y}h ${String(l).padStart(2,"0")}m`;if(l>0)return`${l}m ${String($).padStart(2,"0")}s`;return`${$}s`}function mG(f){if(f===null||f===void 0||f==="")return null;let u=f instanceof Date?f.getTime():new Date(f).getTime();return Number.isFinite(u)?u:null}function UX(f,u=Date.now()){let _=mG(f);if(_===null)return"--";let y=Math.max(0,Math.floor((u-_)/1000));if(y<1)return"刚刚";let l=Math.floor(y/86400),$=Math.floor(y%86400/3600),j=Math.floor(y%3600/60),J=y%60;if(l>0)return`${l}天${$>0?`${$}小时`:""}前`;if($>0)return`${$}小时${j>0?`${j}分钟`:""}前`;if(j>0)return`${j}分钟${J}秒前`;return`${J}秒前`}function WX(...f){let u="",_=-1/0;for(let y of f){let l=String(y||"");if(l.length===0)continue;let $=mG(y);if($!==null&&$>=_)u=l,_=$;else if(u.length===0)u=l}return u}function Qj(f){let u=Number(f);if(!Number.isFinite(u)||u<0)return"--";if(u<1000)return`${Math.round(u)}ms`;return`${(u/1000).toFixed(u<1e4?2:1)}s`}function iG(f,u=180){let _=String(f||"").replace(/\s+/gu," ").trim();return _.length>u?`${_.slice(0,u-1)}…`:_}async function M0(f,u={}){return wf(f,{strictJson:!0,retryInvalidJson:1,invalidJsonPrefix:"Codex Queue 返回了无效 JSON",invalidJsonPreview:!0,responsePreviewLength:AX,...u})}function x_({status:f,children:u}){let _=String(f||"unknown").toLowerCase();return T("span",{className:`status-badge ${_}`},u||f||"unknown")}function Ty({label:f,value:u,hint:_,tone:y}){return T("article",{className:`metric-card ${y||""}`},T("div",{className:"metric-label"},f),T("div",{className:"metric-value"},u),T("div",{className:"metric-hint"},_))}function bl({title:f,eyebrow:u,summary:_,actions:y,children:l,className:$}){return T("section",{className:`panel ${$||""}`},T("div",{className:"panel-head"},T("div",null,u?T("p",{className:"panel-eyebrow"},u):null,T("h2",null,f),_?T("div",{className:"panel-summary"},_):null),y?T("div",{className:"panel-actions"},y):null),T("div",{className:"panel-body"},l))}function CG({title:f,data:u,onOpen:_,testId:y}){return T("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:()=>_(f,u)},"查看原始JSON")}function Il({title:f,text:u}){return T("div",{className:"empty-state"},T("strong",null,f),T("span",null,u))}function GX(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function zX(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function KX(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function P0(f,u){return`${f}/codex-queue-direct${u}`}function Z1(f){return Array.isArray(f?.tasks)?f.tasks:[]}function r$(f){return f?.pagination&&typeof f.pagination==="object"&&!Array.isArray(f.pagination)?f.pagination:{}}function RG(f){let u=Date.parse(String(f?.updatedAt||f?.createdAt||""));return Number.isFinite(u)?u:0}function gG(f,u=""){let _=new Map;for(let y of f)for(let l of y){let $=String(l?.id||"");if($.length>0&&!_.has($))_.set($,l)}return Array.from(_.values()).sort((y,l)=>{let $=cG(y)-cG(l);if($!==0)return $;let j=String(y?.id||"")===u?0:1,J=String(l?.id||"")===u?0:1;if(j!==J)return j-J;return RG(l)-RG(y)})}function H4(f,u=""){let _=new Map;for(let y of f)for(let l of y){let $=String(l?.id||"");if($.length===0)continue;_.set($,{..._.get($)||{},...l})}return gG([Array.from(_.values())],u)}function N4(f){return Array.isArray(f?.activeTaskIds)?f.activeTaskIds.map((u)=>String(u||"")).filter(Boolean):[String(f?.activeTaskId||"")].filter(Boolean)}var R_="__all__",ZX="(max-width: 760px)",qX="(min-width: 761px)";function Yu(f){return!f||f===R_}function EX(){return typeof window<"u"&&window.matchMedia(ZX).matches}function Oj(f){return Yu(f)?"":`&queueId=${encodeURIComponent(f)}`}function V4(f,u){return Number(f?.counts?.[u]||0)}function xG(f,u=""){let _=new Map;for(let l of Array.isArray(f?.queues)?f.queues:[]){let $=String(l?.id||"").trim();if($.length>0)_.set($,l)}for(let l of[String(f?.defaultQueueId||"default"),u].map(($)=>$.trim()).filter(Boolean))if(!_.has(l))_.set(l,{id:l,total:0,counts:{},activeTaskId:null,runnableTaskId:null,processing:!1});return Array.from(_.values()).sort((l,$)=>{let j=String(l?.id||"")===String(f?.defaultQueueId||"default")?0:1,J=String($?.id||"")===String(f?.defaultQueueId||"default")?0:1;if(j!==J)return j-J;return String(l?.id||"").localeCompare(String($?.id||""))})}function Kj(f){let u=String(f?.id||"default"),_=V4(f,"running")+V4(f,"judging"),y=V4(f,"queued")+V4(f,"retry_wait"),l=Number(f?.total||0),$=[`${u}`,`${l} tasks`];if(_>0)$.push(`${_} running`);if(y>0)$.push(`${y} queued`);return $.join(" · ")}function Y4(f,u){if(Yu(u))return null;return f.find((_)=>String(_?.id||"")===u)||null}function vG(f,u,_,y){if(Yu(_)){let $=N4(f);return String(f?.activeTaskId||$[0]||y.find((j)=>IG(j))?.id||"")}let l=Y4(u,_);return String(l?.activeTaskId||y.find(($)=>IG($))?.id||"")}function HX(f,u,_){if(!Yu(u)){let y=Y4(f,u);return String(y?.runnableTaskId||_.find((l)=>String(l?.status||"")==="queued"||String(l?.status||"")==="retry_wait")?.id||"")}return String(_.find((y)=>String(y?.status||"")==="queued"||String(y?.status||"")==="retry_wait")?.id||"")}async function VX(f,u,_=R_){let y=Oj(_);try{return await M0(P0(f,`/api/tasks?limit=${zj}&lite=1&devReady=0${y}`))}catch{let $=await Promise.all(["running","judging","retry_wait","queued"].map(async(A)=>{try{return await M0(P0(f,`/api/tasks?status=${encodeURIComponent(A)}&limit=80&lite=1&devReady=0${y}`))}catch{return null}})),j=await M0(P0(f,`/api/tasks?limit=${zj}&lite=1&devReady=0${y}`)).catch(()=>null),J=$.find((A)=>A?.queue)?.queue||j?.queue||u?.queue||u?.body?.queue||{},F=gG([...$.map((A)=>Z1(A)),Z1(j)],String(J?.activeTaskId||""));if(F.length>0)return{ok:!0,queue:J,tasks:F};return M0(P0(f,`/api/tasks?limit=5&lite=1&devReady=0${y}`))}}async function OX(f,u,_=0,y=R_){return M0(P0(f,`/api/tasks/overview?limit=${zj}&transcriptLimit=3&compact=1&afterSeq=${encodeURIComponent(String(Math.max(0,_)))}&preferId=${encodeURIComponent(u)}${Oj(y)}`))}async function XX(f,u,_,y=QX){return M0(P0(f,`/api/tasks?limit=${encodeURIComponent(String(y))}&lite=1&devReady=0&includeActive=0&beforeId=${encodeURIComponent(_)}${Oj(u)}`))}async function NX(f,u){return M0(P0(f,`/api/tasks/${encodeURIComponent(u)}/trace-summary`))}async function LX(f,u,_,y=null){let l=y===null||y===void 0||String(y).length===0?"":`&attempt=${encodeURIComponent(String(y))}`;return M0(P0(f,`/api/tasks/${encodeURIComponent(u)}/prompt?part=${encodeURIComponent(_)}${l}`))}async function YX(f,u,_=0,y=500,l=null){let $=l===null||l===void 0||String(l).length===0?"":`&attempt=${encodeURIComponent(String(l))}`;return M0(P0(f,`/api/tasks/${encodeURIComponent(u)}/trace-steps?afterSeq=${encodeURIComponent(String(_))}&limit=${encodeURIComponent(String(y))}${$}`))}async function BX(f,u,_){return M0(P0(f,`/api/tasks/${encodeURIComponent(u)}/trace-step?seq=${encodeURIComponent(String(_))}`))}async function DX(f,u){return M0(P0(f,`/api/tasks/${encodeURIComponent(u)}/read`),{method:"POST",body:{}})}async function wX(f){return M0(P0(f,"/api/tasks/read-all"),{method:"POST",body:{}})}function TX(f){return Array.isArray(f?.output)?f.output:[]}function nG(f){return Array.isArray(f?.attempts)?f.attempts:[]}function Aj(f){return f?.counts&&typeof f.counts==="object"&&!Array.isArray(f.counts)?f.counts:{}}function rX(f){return f.split(/^\s*---+\s*$/gmu).map((u)=>u.trim()).filter(Boolean)}function bG(f){let u=Number(f);return Number.isFinite(u)?Math.max(1,Math.min(50,Math.floor(u))):1}function L4(f){let u=[];for(let _ of f.split(/[\s,,;;]+/u)){let y=_.trim();if(/^codex_\d+_[A-Za-z0-9_-]+$/u.test(y)&&!u.includes(y))u.push(y)}return u}function MX(f,u){let _=L4(u);if(_.length===0)return f;return[`引用 Codex Queue 任务 ${_.join(" ")}。后端会在入队时只注入这些任务的 initial prompt 和 final response 全文;中间执行过程不注入,如需补充核查可运行:${_.map((y)=>`bun scripts/cli.ts codex task ${y}`).join(";")}`,"","本次任务:",f].join(` -`)}function PX(f){let y=f.trimStart();if(!y.startsWith("# Codex Queue 已解析引用上下文"))return{hasInjection:!1,reference:"",userPrompt:f};let l=f.length-y.length,$=f.lastIndexOf(` +`):[]}function k7(f){let u=String(f.status||"").trim();if(u.length>0)return u;let l=String(f.bodyPreview||"");return/^(item\/[A-Za-z]+(?:\/[A-Za-z]+)?):/u.exec(l)?.[1]||"item/fileChange"}function $L(f){let u=String(f.bodyPreview||"");return/file changes status=([A-Za-z0-9_-]+)/u.exec(u)?.[1]}function jL(f){return/^item\/(?:started|completed): file changes status=/u.test(String(f||"").trim())}function pz(f){if(String(f.kind||"")!=="edited")return!1;let u=String(f.status||""),l=String(f.title||""),y=String(f.bodyPreview||""),r=String(f.commandPreview||"");if(l==="Edited files")return!0;if(/^item\/fileChange\//u.test(u))return!0;if((u==="item/started"||u==="item/completed")&&/file changes status=/u.test(y))return!0;if(/^Success\. Updated the following files:/mu.test(y))return!0;if(/^diff --git /mu.test(y))return!0;return r.length===0&&/^([AMDRCU?]{1,2})\s+\S+/mu.test(y)}function z_(f){return hz(String(f||"").replace(/^[ab]\//u,"").trim())}function o7(f){let u=/^([AMDRCU?]{1,2})\s+(.+)$/u.exec(f);if(!u)return null;let l=z_(u[2]||"");return l.length>0?{status:u[1]||"M",path:l}:null}function a7(f){let u=/^\*\*\*\s+(Add|Update|Delete)\s+File:\s+(.+)$/u.exec(f);if(u){let y=u[1]==="Add"?"A":u[1]==="Delete"?"D":"M",r=z_(u[2]||"");return r.length>0?{status:y,path:r}:null}let l=/^\*\*\*\s+Move to:\s+(.+)$/u.exec(f);if(l){let y=z_(l[1]||"");return y.length>0?{status:"R",path:y}:null}return null}function AL(f){let u=[],l=(r,_)=>{let $=z_(_);if($.length===0||$==="/dev/null")return;let j=u.find((A)=>A.path===$);if(j){if(j.status==="M"&&r!=="M")j.status=r;return}u.push({status:r,path:$})},y="";for(let r of mz(f)){let _=o7(r)||a7(r);if(_!==null){l(_.status,_.path),y=_.path;continue}let $=/^diff --git a\/(.+?) b\/(.+)$/u.exec(r);if($){let Q=$[2]||$[1]||"";l("M",Q),y=z_(Q);continue}let j=/^\+\+\+ b\/(.+)$/u.exec(r);if(j&&j[1]!=="/dev/null"){l("M",j[1]||""),y=z_(j[1]||"");continue}if(/^new file mode /u.exec(r)&&y)l("A",y);if(/^deleted file mode /u.exec(r)&&y)l("D",y);let U=/^rename to (.+)$/u.exec(r);if(U)l("R",U[1]||"")}return u}function FL(f){if(o7(f)!==null||a7(f)!==null)return"file";if(/^(diff --git |index |--- |\+\+\+ |\*\*\* Begin Patch|\*\*\* End Patch)/u.test(f))return"meta";if(/^@@ /u.test(f))return"hunk";if(/^\+/u.test(f))return"add";if(/^-/u.test(f))return"del";if(/^(Success\.|No changes|Updated\b|Created\b|Deleted\b|Added\s+\d+\s+lines?|Wrote\s+\d+\s+lines?|Read\s+\d+\s+files?|\.\.\.\[patch content truncated)/iu.test(f))return"note";return"context"}function JL(f){return mz(f).map((u)=>{let l=o7(u)||a7(u);if(l!==null)return{text:u,kind:"file",path:l.path,status:l.status};return{text:u,kind:FL(u)}})}function UL(f){return f.reduce((u,l)=>{if(l.kind==="add")u.added+=1;else if(l.kind==="del")u.removed+=1;return u},{added:0,removed:0})}function Cz(f,u){return`${u} ${f} line${f===1?"":"s"}`}function QL(f,u){let l=[];if(f>0)l.push(Cz(f,"Added"));if(u>0)l.push(Cz(u,"removed"));return l.join(", ")}function WL(f){for(let l=f.length-1;l>=0;l-=1){let y=String(f[l]?.status||"").trim();if(y.length>0)return y}let u=String(f[f.length-1]?.method||"").trim();if(u==="item/fileChange/outputDelta")return"updated";if(u==="item/started")return"started";if(u==="item/completed")return"completed";return u.replace(/^item\//u,"")||"changed"}function zL(f){return`${f} file${f===1?"":"s"}`}function Iz(f){let u=f.length>0?f:[],l=Pz(u.map((W)=>String(W.bodyPreview||""))),r=Pz(u.map((W)=>String(W.bodyPreview||"")).filter((W)=>W.trim().length>0&&!jL(W)))||l,_=AL(r||l),$=u.map((W)=>({method:k7(W),status:$L(W),at:W.at})),j=JL(r||l),A=UL(j),F=QL(A.added,A.removed),U=_.length>0?zL(_.length):"",Q=F.length>0?`${F}${U?` in ${U}`:""}`:_.length>0?U:xz(r||l||"File changes",72);return{status:WL($),summary:Q,files:_,stages:$,lines:j,addedLines:A.added,removedLines:A.removed,rawText:l}}function GL(f){let u=f[0],l=f[f.length-1]||u,y=Iz(f);return{...u,seq:Number.isFinite(Number(l?.seq))?Number(l?.seq):Number(u?.seq??0),at:l?.at||u?.at,title:y.files.length>0?`Edited ${y.summary}`:"Edited files",status:y.status,commandPreview:"",commandOmittedLines:void 0,bodyPreview:y.rawText,bodyOmittedLines:f.reduce((r,_)=>r+Number(_.bodyOmittedLines||0),0)||void 0,rawSeqs:f.flatMap((r)=>Array.isArray(r?.rawSeqs)?r.rawSeqs:[r?.seq]).filter((r)=>r!==void 0),editObservation:y}}function KL(f){let u=Array.isArray(f)?f:[],l=[],y=[],r=()=>{if(y.length===0)return;l.push(GL(y)),y=[]};for(let _ of u){if(pz(_)){if(k7(_)==="item/started"&&y.length>0)r();if(y.push(_),k7(_)==="item/completed")r();continue}r(),l.push(_)}return r(),l}function gz(f){let u=[],l=[],y=[],r=(F,U)=>{for(let Q of U)if(!F.includes(Q))F.push(Q)};for(let F of f){let U=String(F?.kind||""),Q=[F?.commandPreview,F?.bodyPreview,F?.title].map((W)=>String(W||"")).join(` +`);if(U==="explored")r(u,Sz(Q));else if(U==="edited")r(l,Sz(Q));else if(U==="ran"){let W=String(F?.commandPreview||F?.title||"").trim();if(W.length>0&&!y.includes(W))y.push(xz(W,90))}}let _=f.map((F)=>Date.parse(String(F?.at||""))).filter((F)=>Number.isFinite(F)),$=_.length>=2?Math.max(0,Math.max(..._)-Math.min(..._)):0,j=f.reduce((F,U)=>F+(ur(U?.durationMs)??ur(U?.elapsedMs)??0),0),A=$>0?$:j;return{readFiles:u,editedFiles:l,runCommands:y,durationLabel:yL(A)}}function NL(f,u=3){let l=Array.isArray(f)?f:[],y=[],r=[],_=Math.max(0,u),$=new Set;for(let A=l.length-1;A>=0&&_>0;A-=1){let F=l[A];if(!Mz(F))continue;$.add(F),_-=1}let j=()=>{if(r.length>=2){let A=vz(r);y.push({seq:Number(r[0]?.seq??0),at:r[0]?.at||r.at(-1)?.at,kind:"toolGroup",title:bz(r),status:`${r.length} calls`,items:r,counts:A,digest:gz(r),rawSeqs:r.flatMap((F)=>Array.isArray(F?.rawSeqs)?F.rawSeqs:[F?.seq]).filter((F)=>F!==void 0)})}else y.push(...r);r=[]};for(let A of l){if(Mz(A)&&!$.has(A)){r.push(A);continue}j(),y.push(A)}return j(),y}function kz(f){return(Array.isArray(f)?f:[]).map((u,l)=>({...u,seq:Number.isFinite(Number(u?.seq))?Number(u.seq):l+1,kind:String(u?.kind||"message"),at:u?.at===void 0?void 0:String(u.at),durationMs:ur(u?.durationMs)??void 0,title:u?.title===void 0?void 0:String(u.title),status:u?.status===void 0?void 0:String(u.status)}))}function h7(f){return ur(f?.durationMs)??ur(f?.elapsedMs)??ur(f?.timing?.durationMs)??ur(f?.metadata?.durationMs)??void 0}function m7(f,u){return f?.createdAt||f?.updatedAt||f?.completedAt||u||void 0}function p7(f,u){return f?.id||f?.messageId||u}function t7(f,u){let l=new Set(u.map((y)=>y.toLowerCase()));for(let y of Array.isArray(f?.inputFields)?f.inputFields:[]){let r=String(y?.key||"").toLowerCase();if(l.has(r))return String(y?.value||"")}return""}function ZL(f){let u=String(f?.tool||f?.title||"").toLowerCase();if(/read|grep|glob|list|ls|find|search|view|cat|sed|rg/u.test(u))return"explored";if(/edit|write|patch|apply|update|create|delete/u.test(u))return"edited";let l=t7(f,["command","cmd"]);if(/\b(rg|grep|find|ls|cat|sed|tail|head|git status|git diff|ps)\b/u.test(l))return"explored";if(/\b(apply_patch|git apply|cat >|tee .*<<|sed -i|python3? .*write_text)\b/u.test(l))return"edited";return"ran"}function EL(f){let u=[],l=1;for(let y of Array.isArray(f)?f:[]){let r=y?.createdAt||y?.updatedAt||y?.completedAt,_=String(y?.role||"assistant").toLowerCase(),$=Array.isArray(y?.parts)?y.parts:[];for(let j of $){let A=String(j?.type||"").toLowerCase();if(A==="step-start"||A==="step-finish")continue;if(A==="text"||A==="reasoning"){let U=String(j?.textPreview||y?.textPreview||"").trim();if(U.length===0)continue;u.push({seq:l++,at:m7(j,r),kind:"message",title:A==="reasoning"?"Reasoning":_==="user"?"User message":_==="system"?"System message":"Assistant message",status:A==="reasoning"?"reasoning":_,bodyPreview:U,durationMs:h7(j),rawSeqs:[p7(j,l)]});continue}if(A==="tool"){let U=t7(j,["command","cmd"])||t7(j,["filePath","filepath","path"])||String(j?.title||j?.tool||"tool"),Q=String(j?.outputPreview&&j.outputPreview!=="--"?j.outputPreview:j?.textPreview||"");u.push({seq:l++,at:m7(j,r),kind:ZL(j),title:String(j?.title||j?.tool||"tool"),status:String(j?.status||""),commandPreview:U,bodyPreview:Q,durationMs:h7(j),rawSeqs:[p7(j,l)]});continue}let F=String(j?.textPreview||j?.title||A||"").trim();if(F)u.push({seq:l++,at:m7(j,r),kind:"system",title:A||"part",bodyPreview:F,status:String(j?.status||""),durationMs:h7(j),rawSeqs:[p7(j,l)]})}if($.length===0&&y?.textPreview)u.push({seq:l++,at:r,kind:"message",title:`${_||"assistant"} message`,status:_,bodyPreview:String(y.textPreview),rawSeqs:[y?.messageId||l]})}return u}var tz={source:"opencode",toTrace:EL};function HL(f){return String(f||"unknown").toLowerCase().replace(/[^a-z0-9_-]+/gu,"-")||"unknown"}function cz(f){let u=String(f||"M").toUpperCase();if(u.startsWith("A")||u==="??")return"added";if(u.startsWith("D"))return"deleted";if(u.startsWith("R"))return"renamed";return"modified"}function OL(f){if(f==="item/fileChange/outputDelta")return"delta";return f.replace(/^item\//u,"")}function qL(f,u){if(f.kind==="file"){let r=String(f.status||"M");return Tf("div",{key:`${u}-${f.text}`,className:`codex-edit-diff-line file ${cz(r)}`},Tf("span",{className:`codex-edit-file-status ${cz(r)}`},r),Tf("code",null,f.path||f.text.replace(/^([AMDRCU?]{1,2})\s+/u,"")))}let l=f.kind==="add"||f.kind==="del"?f.text.slice(0,1):f.kind==="hunk"?"@@":f.kind==="note"?"ok":"",y=f.kind==="add"||f.kind==="del"?f.text.slice(1):f.text;return Tf("div",{key:`${u}-${f.text}`,className:`codex-edit-diff-line ${f.kind}`},Tf("span",{className:"codex-edit-diff-sign"},l),Tf("code",null,y||" "))}function VL(f,u){let l=f.lines.length>0?f.lines:f.files.map((r)=>({text:`${r.status} ${r.path}`,kind:"file",path:r.path,status:r.status})),y=Number(f.addedLines||0)+Number(f.removedLines||0)>0;return Tf("div",{className:"codex-edit-observation","data-testid":"codex-edit-observation"},Tf("div",{className:"codex-edit-observation-head"},Tf("span",{className:"codex-edit-window-controls","aria-hidden":"true"},Tf("i",null),Tf("i",null),Tf("i",null)),Tf("strong",null,y?"git diff":"git diff --stat"),Tf("code",null,f.summary||"File changes")),f.stages.length>0?Tf("div",{className:"codex-edit-stage-strip"},f.stages.map((r,_)=>Tf("span",{key:`${r.method}-${_}`,className:`codex-edit-stage ${HL(r.status||r.method)}`},Tf("b",null,OL(r.method)),r.status?Tf("em",null,r.status):null))):null,l.length>0?Tf("div",{className:"codex-edit-diff",role:"list"},l.map(qL)):null,u?Tf("div",{className:"codex-edit-omitted"},`${u} (查看原始JSON获取完整记录)`):null)}function iz(f,u,l){let y=g7(l);return Tf("div",{className:`codex-transcript-stream ${f}`,"data-testid":`codex-trace-${f}`},Tf("span",{className:"codex-transcript-stream-label"},f),Tf("pre",{className:"codex-transcript-body"},u,y?` +${y} (查看原始JSON获取完整记录)`:""))}function sz(f,u=!1){let l=String(f.kind||"message"),y=["ran","explored","edited"].includes(l),r=g7(f.commandOmittedLines),_=g7(f.bodyOmittedLines),$=String(f.commandPreview||(y?f.title||"":"")),j=String(f.stdoutPreview||""),A=String(f.stderrPreview||""),F=j.length>0||A.length>0,U=Boolean(f.foldedReferencePrompt)&&String(f.fullPrompt||"").length>0,Q=l==="edited"&&(f.editObservation!==void 0||pz(f))?f.editObservation||Iz([f]):null;return Tf("article",{key:`${f.seq}-${l}`,className:`codex-transcript-item ${l} ${u?"nested":""}`},Tf("div",{className:"codex-transcript-main"},Tf("div",{className:"codex-transcript-title"},Tf("span",{className:"codex-output-channel"},I7(l)),y&&Q===null?null:Tf("strong",null,Q!==null?"File changes":String(f.title||I7(l))),f.status?Tf("code",null,String(Q?.status||f.status)):null,Tf("time",null,Kf(f.at))),$&&Q===null?Tf("pre",{className:"codex-transcript-command"},$,r?` +${r}`:""):null,Q!==null?VL(Q,_):F?Tf("div",{className:"codex-transcript-streams"},j.length>0?iz("stdout",j,f.stdoutOmittedLines):null,A.length>0?iz("stderr",A,f.stderrOmittedLines):null):f.bodyPreview?Tf("pre",{className:"codex-transcript-body"},String(f.bodyPreview),_?` +${_} (查看原始JSON获取完整记录)`:""):null,U?Tf("details",{className:"codex-initial-prompt-full","data-testid":"codex-initial-prompt-full"},Tf("summary",null,Tf("span",null,"引用注入已折叠,点击查看最终传入 Codex 的完整 prompt"),Tf("code",null,`${f.fullPromptLines||rL(String(f.fullPrompt||""))} lines / ${f.fullPromptChars||String(f.fullPrompt||"").length} chars`)),Tf("pre",{className:"codex-transcript-body codex-transcript-full-prompt","data-testid":"codex-initial-prompt-full-text"},String(f.fullPrompt||""))):null))}function LL(f){let u=Array.isArray(f.items)?f.items:[],l=f.digest&&typeof f.digest==="object"?f.digest:gz(u);return Tf("article",{key:`${f.seq}-toolGroup`,className:"codex-transcript-item toolGroup"},Tf("div",{className:"codex-transcript-main"},Tf("details",{className:"codex-tool-group","data-testid":"codex-tool-group"},Tf("summary",null,Tf("div",{className:"codex-tool-group-head"},Tf("span",{className:"codex-output-channel"},I7("toolGroup")),Tf("strong",null,String(f.title||bz(u))),Tf("code",null,String(f.status||`${u.length} calls`)),Tf("time",null,Kf(f.at)))),Tf("div",{className:"codex-tool-group-digest"},Tf("span",null,`read: ${b7(Array.isArray(l.readFiles)?l.readFiles:[])}`),Tf("span",null,`edit: ${b7(Array.isArray(l.editedFiles)?l.editedFiles:[])}`),Tf("span",null,`run: ${b7(Array.isArray(l.runCommands)?l.runCommands:[],2)}`),Tf("span",null,`duration: ${l.durationLabel||"--"}`)),Tf("div",{className:"codex-tool-group-items"},u.map((y)=>sz(y,!0))))))}var BL=16;function Rz(f){return f.scrollHeight-f.scrollTop-f.clientHeight<=BL}function j8({items:f,input:u,port:l,autoScroll:y=!1,loading:r=!1,hasDetail:_=!0,emptyText:$="等待 Trace 输出...",loadingText:j="正在加载完整 Trace...",testId:A="trace-output",className:F="codex-transcript",keepRecentToolCalls:U=3,collapseTools:Q=!0}){let W=nz(null),G=nz(!0),K=KL(l?lL(l,u):kz(f)),E=Q?NL(K,U):K,O=_L(K);uL(()=>{let N=W.current;if(!y||!N)return;if(!G.current&&!Rz(N))return;N.scrollTop=N.scrollHeight,G.current=!0},[y,K.length,O]);let Z={className:F,ref:W,onScroll:(N)=>{let H=N.currentTarget;G.current=Rz(H)},"data-testid":A};if(r&&!_)return Tf("div",Z,Tf("div",{className:"codex-output-empty"},j));return Tf("div",Z,E.length===0?Tf("div",{className:"codex-output-empty"},$):E.map((N)=>String(N.kind||"")==="toolGroup"?LL(N):sz(N)))}var L=N3.default.createElement,{useEffect:ry,useMemo:oz,useRef:vu}=N3.default,If=N3.default.useState,XL=120,yj=24,JG=48,YL=1200;function az(){return typeof document>"u"||document.visibilityState!=="hidden"}function Wl(f,u="操作失败"){return wf(f,u)}function Ml(f){let u=Number(f);if(!Number.isFinite(u)||u<0)return"--";let l=Math.floor(u/1000),y=Math.floor(l/3600),r=Math.floor(l%3600/60),_=l%60;if(y>0)return`${y}h ${String(r).padStart(2,"0")}m`;if(r>0)return`${r}m ${String(_).padStart(2,"0")}s`;return`${_}s`}function UG(f){if(f===null||f===void 0||f==="")return null;let u=f instanceof Date?f.getTime():new Date(f).getTime();return Number.isFinite(u)?u:null}function QG(f,u=Date.now()){let l=UG(f);if(l===null)return"--";let y=Math.max(0,Math.floor((u-l)/1000));if(y<1)return"刚刚";let r=Math.floor(y/86400),_=Math.floor(y%86400/3600),$=Math.floor(y%3600/60),j=y%60;if(r>0)return`${r}天${_>0?`${_}小时`:""}前`;if(_>0)return`${_}小时${$>0?`${$}分钟`:""}前`;if($>0)return`${$}分钟${j}秒前`;return`${j}秒前`}function WG(...f){let u="",l=-1/0;for(let y of f){let r=String(y||"");if(r.length===0)continue;let _=UG(y);if(_!==null&&_>=l)u=r,l=_;else if(u.length===0)u=r}return u}function wL(f){let u=Number(f);if(!Number.isFinite(u)||u<0)return"--";if(u<1000)return`${Math.round(u)}ms`;return`${(u/1000).toFixed(u<1e4?2:1)}s`}function rj(f,u=180){let l=String(f||"").replace(/\s+/gu," ").trim();return l.length>u?`${l.slice(0,u-1)}…`:l}async function Du(f,u={}){return Df(f,{strictJson:!0,retryInvalidJson:1,invalidJsonPrefix:"Code Queue 返回了无效 JSON",invalidJsonPreview:!0,responsePreviewLength:YL,...u})}function _r({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return L("span",{className:`status-badge ${l}`},u||f||"unknown")}function K_({title:f,eyebrow:u,summary:l,actions:y,children:r,className:_,loading:$}){return L("section",{className:`panel ${_||""}`},L("div",{className:"panel-head"},L("div",null,u?L("p",{className:"panel-eyebrow"},u):null,L(_u,{title:f,loading:$}),l?L("div",{className:"panel-summary"},l):null),y?L("div",{className:"panel-actions"},y):null),L("div",{className:"panel-body"},r))}function zG({title:f,data:u,onOpen:l,testId:y}){return L("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:()=>l(f,u)},"查看原始JSON")}function $r({title:f,text:u}){return L("div",{className:"empty-state"},L("strong",null,f),L("span",null,u))}function DL(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function TL(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function Tu(f,u){return`${f}/code-queue-direct${u}`}function y0(f){return Array.isArray(f?.tasks)?f.tasks:[]}function yy(f){return f?.pagination&&typeof f.pagination==="object"&&!Array.isArray(f.pagination)?f.pagination:{}}function dz(f){let u=Date.parse(String(f?.updatedAt||f?.createdAt||""));return Number.isFinite(u)?u:0}function GG(f,u=""){let l=new Map;for(let y of f)for(let r of y){let _=String(r?.id||"");if(_.length>0&&!l.has(_))l.set(_,r)}return Array.from(l.values()).sort((y,r)=>{let _=jG(y)-jG(r);if(_!==0)return _;let $=String(y?.id||"")===u?0:1,j=String(r?.id||"")===u?0:1;if($!==j)return $-j;return dz(r)-dz(y)})}function G_(f,u=""){let l=new Map;for(let y of f)for(let r of y){let _=String(r?.id||"");if(_.length===0)continue;l.set(_,{...l.get(_)||{},...r})}return GG([Array.from(l.values())],u)}function z3(f){return Array.isArray(f?.activeTaskIds)?f.activeTaskIds.map((u)=>String(u||"")).filter(Boolean):[String(f?.activeTaskId||"")].filter(Boolean)}var $y="__all__",nL="(max-width: 760px)",ML="(min-width: 761px)";function M0(f){return!f||f===$y}function SL(){return typeof window<"u"&&window.matchMedia(nL).matches}function PL(f){return M0(f)?"":`&queueId=${encodeURIComponent(f)}`}function _j(f){return String(f||"").trim().replace(/\s+/gu," ").slice(0,200)}function CL(f){let u=_j(f);return u.length===0?"":`&search=${encodeURIComponent(u)}`}function zj(f,u=""){return`${PL(f)}${CL(u)}`}function A8(f,u){return Number(f?.counts?.[u]||0)}function ez(f,u=""){let l=new Map;for(let r of Array.isArray(f?.queues)?f.queues:[]){let _=String(r?.id||"").trim();if(_.length>0)l.set(_,{...r,name:String(r?.name||_).trim()||_})}for(let r of[String(f?.defaultQueueId||"default"),u].map((_)=>_.trim()).filter(Boolean))if(!l.has(r))l.set(r,{id:r,name:r,total:0,counts:{},activeTaskId:null,runnableTaskId:null,processing:!1});return Array.from(l.values()).sort((r,_)=>{let $=String(r?.id||"")===String(f?.defaultQueueId||"default")?0:1,j=String(_?.id||"")===String(f?.defaultQueueId||"default")?0:1;if($!==j)return $-j;return String(r?.id||"").localeCompare(String(_?.id||""))})}function KG(f){let u=String(f?.id||"default"),l=String(f?.name||"").trim();return l.length>0?l:u}function $j(f){let u=String(f?.id||"default"),l=KG(f);return l===u?u:`${l} (${u})`}function jj(f){let u=A8(f,"running")+A8(f,"judging"),l=A8(f,"queued")+A8(f,"retry_wait"),y=Number(f?.total||0),r=[$j(f),`${y} tasks`];if(u>0)r.push(`${u} running`);if(l>0)r.push(`${l} queued`);return r.join(" · ")}function G3(f,u){if(M0(u))return null;return f.find((l)=>String(l?.id||"")===u)||null}function fG(f,u,l,y){if(M0(l)){let _=z3(f);return String(f?.activeTaskId||_[0]||y.find(($)=>_G($))?.id||"")}let r=G3(u,l);return String(r?.activeTaskId||y.find((_)=>_G(_))?.id||"")}function cL(f,u,l){if(!M0(u)){let y=G3(f,u);return String(y?.runnableTaskId||l.find((r)=>String(r?.status||"")==="queued"||String(r?.status||"")==="retry_wait")?.id||"")}return String(l.find((y)=>String(y?.status||"")==="queued"||String(y?.status||"")==="retry_wait")?.id||"")}async function uG(f,u,l=$y,y=""){let r=zj(l,y);try{return await Du(Tu(f,`/api/tasks?limit=${yj}&lite=1&devReady=0${r}`))}catch{let $=await Promise.all(["running","judging","retry_wait","queued"].map(async(U)=>{try{return await Du(Tu(f,`/api/tasks?status=${encodeURIComponent(U)}&limit=80&lite=1&devReady=0${r}`))}catch{return null}})),j=await Du(Tu(f,`/api/tasks?limit=${yj}&lite=1&devReady=0${r}`)).catch(()=>null),A=$.find((U)=>U?.queue)?.queue||j?.queue||u?.queue||u?.body?.queue||{},F=GG([...$.map((U)=>y0(U)),y0(j)],String(A?.activeTaskId||""));if(F.length>0)return{ok:!0,queue:A,statistics:j?.statistics||$.find((U)=>U?.statistics)?.statistics||null,tasks:F};return Du(Tu(f,`/api/tasks?limit=5&lite=1&devReady=0${r}`))}}async function iL(f,u,l=0,y=$y,r=""){return Du(Tu(f,`/api/tasks/overview?limit=${yj}&transcriptLimit=3&compact=1&afterSeq=${encodeURIComponent(String(Math.max(0,l)))}&preferId=${encodeURIComponent(u)}${zj(y,r)}`))}async function lG(f,u,l,y=JG,r=""){return Du(Tu(f,`/api/tasks?limit=${encodeURIComponent(String(y))}&lite=1&devReady=0&includeActive=0&beforeId=${encodeURIComponent(l)}${zj(u,r)}`))}async function RL(f,u){return Du(Tu(f,`/api/tasks/${encodeURIComponent(u)}/trace-summary`))}async function xL(f,u,l,y=null){let r=y===null||y===void 0||String(y).length===0?"":`&attempt=${encodeURIComponent(String(y))}`;return Du(Tu(f,`/api/tasks/${encodeURIComponent(u)}/prompt?part=${encodeURIComponent(l)}${r}`))}async function vL(f,u,l=0,y=500,r=null){let _=r===null||r===void 0||String(r).length===0?"":`&attempt=${encodeURIComponent(String(r))}`;return Du(Tu(f,`/api/tasks/${encodeURIComponent(u)}/trace-steps?afterSeq=${encodeURIComponent(String(l))}&limit=${encodeURIComponent(String(y))}${_}`))}async function bL(f,u,l){return Du(Tu(f,`/api/tasks/${encodeURIComponent(u)}/trace-step?seq=${encodeURIComponent(String(l))}`))}async function hL(f,u){return Du(Tu(f,`/api/tasks/${encodeURIComponent(u)}/read`),{method:"POST",body:{}})}async function mL(f){return Du(Tu(f,"/api/tasks/read-all"),{method:"POST",body:{}})}function pL(f){return Array.isArray(f?.output)?f.output:[]}function NG(f){return Array.isArray(f?.attempts)?f.attempts:[]}function d7(f){return f?.counts&&typeof f.counts==="object"&&!Array.isArray(f.counts)?f.counts:{}}function IL(f){return f.split(/^\s*---+\s*$/gmu).map((u)=>u.trim()).filter(Boolean)}function yG(f){let u=Number(f);return Number.isFinite(u)?Math.max(1,Math.min(50,Math.floor(u))):1}function lr(f){let u=[];for(let l of f.split(/[\s,,;;]+/u)){let y=l.trim();if(/^codex_\d+_[A-Za-z0-9_-]+$/u.test(y)&&!u.includes(y))u.push(y)}return u}function gL(f,u){let l=lr(u);if(l.length===0)return f;return[`引用 Code Queue 任务 ${l.join(" ")}。后端会在入队时只注入这些任务的 initial prompt 和 final response 全文;中间执行过程不注入,如需补充核查可运行:${l.map((y)=>`bun scripts/cli.ts codex task ${y}`).join(";")}`,"","本次任务:",f].join(` +`)}function kL(f){let y=f.trimStart();if(!y.startsWith("# Code Queue 已解析引用上下文"))return{hasInjection:!1,reference:"",userPrompt:f};let r=f.length-y.length,_=f.lastIndexOf(` # 本次任务 -`);if($0?f.split(/\r\n|\r|\n/u).length:0}function tG(f){let u=String(f?.displayPrompt||"");if(u.length>0)return u;let _=String(f?.prompt||"");return SX(PX(_).userPrompt)}function b_(f){return f?._traceSummary&&typeof f._traceSummary==="object"&&!Array.isArray(f._traceSummary)?f._traceSummary:null}function B4(f){return f?._promptDetails&&typeof f._promptDetails==="object"&&!Array.isArray(f._promptDetails)?f._promptDetails:{}}function Xj(f){let u=b_(f)?.prompt;return u&&typeof u==="object"&&!Array.isArray(u)?u:{}}function Zj(f){let u=b_(f)?.execution;return u&&typeof u==="object"&&!Array.isArray(u)?u:{}}function CX(f){let u=Xj(f),_=String(u.basePrompt||"");return _.length>0?_:tG(f)}function qj(f){let u=b_(f);return String(u?.finalResponse||f?.finalResponse||"").trimEnd()}function Ej(f){let _=b_(f)?.lastJudge||f?.lastJudge;return _&&typeof _==="object"&&!Array.isArray(_)?_:null}function v_(f){return f&&typeof f==="object"&&!Array.isArray(f)?f:null}function RX(f){let u=b_(f)?.attempts;if(Array.isArray(u)&&u.length>0)return u;let _=nG(f);if(_.length>0)return _.map((j,J)=>({...j,index:Number(j?.index||J+1),execution:J===_.length-1?Zj(f):v_(j?.execution)||{},finalResponse:String(j?.finalResponse||j?.finalResponsePreview||(J===_.length-1?qj(f):"")),judge:v_(j?.judge)||(J===_.length-1?Ej(f):null)}));let y=Zj(f),l=qj(f),$=Ej(f);if(Object.keys(y).length===0&&l.length===0&&$===null)return[];return[{index:Number(f?.currentAttempt||1),mode:f?.currentMode||"initial",startedAt:f?.startedAt,finishedAt:f?.finishedAt,terminalStatus:f?.status,execution:y,finalResponse:l,finalResponseChars:l.length,judge:$}]}function xX(f,u){return v_(u?.execution)||Zj(f)}function vX(f,u,_,y){let l=b_(f),$=Number(l?.currentAttempt||f?.currentAttempt||0),j=Number(_),J=Number.isFinite(j)&&j>0&&j===$,F=WX(f?.updatedAt,l?.updatedAt);if(J&&!u?.finishedAt&&F.length>0)return F;return String(u?.updatedAt||u?.finishedAt||y.effectiveEndAt||(J?F:"")||F||f?.finishedAt||f?.startedAt||"")}function bX(f,u){let _=String(u?.finalResponse||u?.finalResponsePreview||"");if(Object.prototype.hasOwnProperty.call(u||{},"finalResponse")||Object.prototype.hasOwnProperty.call(u||{},"finalResponsePreview"))return _.trimEnd();return _.length>0?_.trimEnd():qj(f)}function sG(f,u){if(Object.prototype.hasOwnProperty.call(u||{},"judge"))return v_(u?.judge);return Ej(f)}function oG(f){return`feedback:${String(f||"latest")}`}function hX(f,u,_){let y=String(u?.feedbackPrompt||"").trimEnd(),l=String(u?.feedbackPromptPreview||y||"").trimEnd(),$=Number(u?.feedbackPromptChars||y.length||l.length||0),j=Number(u?.feedbackPromptLines||ry(y||l));if(y.length>0||l.length>0||$>0)return{text:y,preview:l,chars:$,lines:j,source:u?.feedbackPromptSource||"judge-feedback",forAttempt:u?.feedbackPromptForAttempt||Number(_||0)+1,truncated:Boolean(u?.feedbackPromptTruncated)};let J=sG(f,u),F=String(J?.continuePrompt||"").trimEnd();if(J?.decision==="retry"&&F.length>0)return{text:"",preview:F,chars:F.length,lines:ry(F),source:"judge-continue-prompt",forAttempt:Number(_||0)+1,truncated:!1};return null}function IX(f){let u=Xj(f);return Boolean(u.hasReferenceInjection||Number(u.referencePromptChars||0)>0||f?.referenceInjection||f?.referenceInjectionSummary)}function cX(f,u=null){if(u!==null&&u!==void 0){let y=(v_(f?._traceStepsByAttempt)||{})[String(u)];return Array.isArray(y)?y:[]}return Array.isArray(f?._traceSteps)?f._traceSteps:[]}function cl(f){return(Array.isArray(f?.summaryLines)?f.summaryLines:[]).map((u)=>String(u||""))}function D4(f){let u=String(f?.status||"").trim();if(u.length>0)return u;let _=cl(f).join(` -`);return/^(item\/[A-Za-z]+(?:\/[A-Za-z]+)?):/u.exec(_)?.[1]||""}function hG(f){return/^item\/(?:started|completed): file changes status=/u.test(String(f||"").trim())}function pX(f){let u=cl(f);for(let y=u.length-1;y>=0;y-=1){let l=/file changes status=([A-Za-z0-9_-]+)/u.exec(u[y]||"")?.[1];if(l)return l}let _=D4(f);if(_==="item/fileChange/outputDelta")return"updated";if(_==="item/started")return"started";if(_==="item/completed")return"completed";return _.replace(/^item\//u,"")||String(f?.status||"changed")}function kX(f){if(String(f?.kind||"")!=="edited")return!1;let u=String(f?.title||""),_=String(f?.status||""),y=cl(f).join(` -`);if(u==="Edited files")return!0;if(/^item\/fileChange\//u.test(_))return!0;if((_==="item/started"||_==="item/completed")&&/file changes status=/u.test(y))return!0;if(/^Success\. Updated the following files:/mu.test(y))return!0;if(/^diff --git /mu.test(y))return!0;return/^([AMDRCU?]{1,2})\s+\S+/mu.test(y)}function mX(f){if(f.length<=1)return f[0];let u=f.find(($)=>D4($)==="item/fileChange/outputDelta")||f.find(($)=>cl($).some((j)=>!hG(j)))||f.at(-1)||f[0],_=f.flatMap(($)=>Array.isArray($?.rawSeqs)?$.rawSeqs:[$?.seq]).filter(($)=>$!==void 0),y=f.flatMap(cl).filter(($)=>$.trim().length>0&&!hG($)),l=f[f.length-1]||u;return{...u,at:u?.at||l?.at,title:String(u?.title||"Edited files"),status:pX(l),summaryLines:y.length>0?y:cl(u),rawSeqs:_}}function iX(f){let u=Array.isArray(f)?f:[],_=[],y=[],l=()=>{if(y.length>0)_.push(mX(y));y=[]};for(let $ of u){if(kX($)){if(D4($)==="item/started"&&y.length>0)l();if(y.push($),D4($)==="item/completed")l();continue}l(),_.push($)}return l(),_}function gX(f,u){if(u.length===0)return f;let _=u.reduce((y,l)=>{let $=String(l?.kind||"");if($==="explored")y.readCount+=1;else if($==="edited")y.editCount+=1;else if($==="ran")y.runCount+=1;return y},{readCount:0,editCount:0,runCount:0});return{...f,..._,toolCallCount:_.readCount+_.editCount+_.runCount,stepCount:u.length}}function aG(f,u=null){if(u!==null&&u!==void 0){let _=v_(f?._traceStepsLoadedByAttempt)||{};return Boolean(_[String(u)])}return Boolean(f?._traceStepsLoaded)}function Hj(f){return f?._traceStepDetails&&typeof f._traceStepDetails==="object"&&!Array.isArray(f._traceStepDetails)?f._traceStepDetails:{}}function nX(f,u){let _=Number(f?.index);return Number.isFinite(_)?_:u+1}function dG(f,u){return Boolean(f?.synthetic)||Number(u)<=0}function r4(f){let u=Number(f);return Number.isFinite(u)?String(u):void 0}function tX(f){let u=f?.timing&&typeof f.timing==="object"?f.timing:{},_=String(f?.status||"");if(["queued"].includes(_))return`等待 ${X4(u.queueWaitMs??u.totalElapsedMs)}`;if(["running","judging","retry_wait"].includes(_))return`耗时 ${X4(u.durationMs??u.totalElapsedMs)}`;return`耗时 ${X4(u.durationMs??u.totalElapsedMs)}`}function w4(f){return String(f?.queueId||"default")}function sX(f){return{system:"SYS",user:"YOU",assistant:"GPT",reasoning:"THINK",command:"CMD",diff:"DIFF",tool:"TOOL",error:"ERR"}[f]||f.toUpperCase()}function IG(f){return["running","judging","retry_wait"].includes(String(f?.status||""))}function d1(f){return["succeeded","failed","canceled"].includes(String(f?.status||""))}function hl(f){if(!d1(f))return!1;if(f?.terminalUnread===!0)return!0;if(f?.terminalUnread===!1)return!1;return!f?.readAt}function S$(f){let u=Number(f||0);return Number.isFinite(u)?u:0}function oX(f){return S$(f.queued)+S$(f.retry_wait)}function aX(f){return S$(f.running)+S$(f.judging)}function cG(f){if(hl(f))return 0;return{running:1,judging:2,retry_wait:3,queued:4,succeeded:8,failed:8,canceled:8}[String(f?.status||"")]??9}function M$(f){if(!f)return!1;if(f?._traceSummaryLoaded===!0)return!1;return f?.summaryOnly===!0||f?._metaLoaded!==!0}function dX(f){return Boolean(f?._metaLoaded)||f?.summaryOnly===!1}function eX(f,u,_){let y=String(f?.[_]||""),l=String(u?.[_]||"");return y.length>l.length?y:l}function Vj(f,u,_){let y=Array.isArray(f?.[_])?f[_]:[],l=Array.isArray(u?.[_])?u[_]:[];if(l.length===0&&y.length>0)return y;return y.length>l.length?y:l}function fN(f,u){let _=u?.summaryOnly===!0&&dX(f),y={...f,...u};if(!_)return y;for(let l of["prompt","basePrompt","displayPrompt","finalResponse"])y[l]=eX(f,u,l);for(let l of["promptHistory","attempts","output","events"])y[l]=Vj(f,u,l);if(f?.referenceInjection?.items&&!u?.referenceInjection?.items)y.referenceInjection=f.referenceInjection;if(f?.referenceInjectionSummary&&!u?.referenceInjectionSummary)y.referenceInjectionSummary=f.referenceInjectionSummary;y.summaryOnly=f?.summaryOnly===!1?!1:u.summaryOnly,y._metaLoaded=f?._metaLoaded,y._detailLoaded=f?._detailLoaded,y._transcriptComplete=f?._transcriptComplete,y._transcriptPreview=Object.prototype.hasOwnProperty.call(u,"_transcriptPreview")?u._transcriptPreview:f?._transcriptPreview;for(let l of["_traceSummary","_traceSummaryLoaded","_traceSteps","_traceStepsLoaded","_traceStepsByAttempt","_traceStepsLoadedByAttempt","_traceStepDetails","_promptDetails"])if(!Object.prototype.hasOwnProperty.call(u,l)&&Object.prototype.hasOwnProperty.call(f||{},l))y[l]=f[l];return y}function uN(f){let u=f?.selected,_=u?.task&&typeof u.task==="object"?u.task:null;if(_!==null){let l=Boolean(u?.preview);return{..._,transcript:Array.isArray(u?.transcript)?u.transcript:[],_detailLoaded:Array.isArray(u?.transcript)&&u.transcript.length>0,_transcriptComplete:Boolean(!l&&!u?.hasMore&&d1(_)),_transcriptPreview:l,_summaryLoaded:!0}}let y=Z1(f)[0];return y?{...y,_summaryLoaded:!0}:null}function Uj(f,u){let _=new Map;for(let y of[...Array.isArray(f)?f:[],...Array.isArray(u)?u:[]]){let l=`${Number(y?.seq??0)}:${String(y?.kind||"message")}`,$=_.get(l);if(!$){_.set(l,y);continue}let j={...$,...y};for(let[J,F]of[["bodyPreview","bodyOmittedLines"],["commandPreview","commandOmittedLines"]]){let A=String($?.[J]||""),U=String(y?.[J]||"");if(A.length>U.length)j[J]=$[J],j[F]=$[F]}_.set(l,j)}return Array.from(_.values()).sort((y,l)=>Number(y?.seq??0)-Number(l?.seq??0))}function O4(f){return(Array.isArray(f)?f:[]).reduce((u,_)=>Math.max(u,Number(_?.seq??0)),0)}function pG(f,u=8){let _=Array.from(new Set((Array.isArray(f)?f:[]).map((l)=>Number(l?.seq??0)).filter((l)=>Number.isFinite(l)&&l>0))).sort((l,$)=>l-$);if(_.length===0)return 0;let y=_[Math.max(0,_.length-u)]??0;return Math.max(0,y-0.001)}function Wj(f,u){let _=Number(f[u]??0);return Number.isFinite(_)?String(_):"0"}function _N(f,u){let _=Array.isArray(f?.codexModels)?f.codexModels:[],y=["gpt-5.5","gpt-5.4-mini","gpt-5.4"];return Array.from(new Set([..._,...y,u].map((l)=>String(l||"").trim()).filter(Boolean)))}function yN({task:f,selected:u,onSelect:_,onCopy:y,onReference:l,onMarkRead:$,copied:j,markingRead:J}){let F=f?.lastJudge||{},A=String(f?.id||""),U=hl(f);return T("article",{role:"button",tabIndex:0,className:`codex-task-card ${u?"selected":""} ${U?"unread-terminal":""}`,onClick:_,onKeyDown:(G)=>{if(G.key==="Enter"||G.key===" ")G.preventDefault(),_()},"data-unread-terminal":U?"true":"false","data-testid":`codex-task-${f?.id||"unknown"}`},U?T("span",{className:"codex-unread-badge",title:"待读","aria-label":"待读","data-testid":`codex-unread-task-${A||"unknown"}`}):null,T("div",{className:"codex-task-card-head"},T("div",{className:"codex-task-status-line"},T(x_,{status:f?.status},f?.status||"unknown")),T("span",{className:"mono-text"},`${f?.currentAttempt||0}/${f?.maxAttempts||0}`)),T("div",{className:"codex-task-id-row"},T("code",{title:A},A||"unknown"),T("div",{className:"codex-task-id-actions"},T("button",{type:"button",className:"codex-copy-id-btn",onClick:(G)=>{G.stopPropagation(),l(A)},"data-testid":`codex-reference-task-${A||"unknown"}`},"引用"),T("button",{type:"button",className:"codex-copy-id-btn",onClick:(G)=>{G.stopPropagation(),y(A)},"data-testid":`codex-copy-task-id-${A||"unknown"}`},j?"已复制":"复制ID"),U?T("button",{type:"button",className:"codex-copy-id-btn codex-mark-read-btn",disabled:Boolean(J),onClick:(G)=>{G.stopPropagation(),$(A)},"data-testid":`codex-mark-task-read-${A||"unknown"}`},J?"标记中":"标为已读"):null)),T("strong",null,iG(tG(f),120)||"空任务"),T("div",{className:"codex-task-meta"},T("span",null,`queue=${w4(f)}`),T("span",null,f?.model||"--"),T("span",null,tX(f))),T("div",{className:"codex-task-meta"},T("span",null,zf(f?.updatedAt))),F?.decision?T("div",{className:"codex-judge-line"},`judge=${F.decision} ${Math.round(Number(F.confidence||0)*100)}%`):null)}function Gj({title:f,tasks:u,selectedId:_,onSelect:y,onCopy:l,onReference:$,onMarkRead:j,copiedTaskId:J,markingReadTaskId:F,emptyText:A}){let U=Array.isArray(u)?u:[];return T("section",{className:"codex-task-section"},T("div",{className:"codex-task-section-head"},T("span",null,f),T("code",null,String(U.length))),U.length===0?T("p",{className:"codex-task-section-empty"},A):T("div",{className:"codex-task-section-list"},U.map((G)=>T(yN,{key:G.id,task:G,selected:_===G.id,onSelect:()=>y(G.id),onCopy:l,onReference:$,onMarkRead:j,copied:J===G.id,markingRead:F===G.id}))))}function lN({task:f,queueRows:u,busy:_,onMove:y}){let l=String(f?.id||""),$=w4(f),[j,J]=u0($);P$(()=>{J($)},[l,$]);let F=!l||_||["running","judging","retry_wait"].includes(String(f?.status||""));return T("div",{className:"codex-task-move-control","data-testid":"codex-task-queue-move-control"},T("label",null,"任务 queue",T("select",{value:j,disabled:!l||_,onChange:(A)=>J(String(A.target.value||$)),"data-testid":"codex-task-queue-move-select"},u.map((A)=>T("option",{key:String(A?.id||""),value:String(A?.id||"")},Kj(A))))),T("button",{type:"button",className:"ghost-btn",disabled:F||j===$,onClick:()=>y(j),title:F?"运行中 / judging / retry_wait 的任务不能移动;请先打断或等当前 turn 结束":"移动已创建任务到另一个 queue","data-testid":"codex-task-queue-move-button"},"移动"))}function kG(f,u=4){let _=(Array.isArray(f)?f:[]).map((l)=>String(l||"").trim()).filter(Boolean);if(_.length===0)return"--";let y=_.slice(0,u).join(" / ");return _.length>u?`${y} +${_.length-u}`:y}function $N({task:f,loading:u,onLoadPromptPart:_,testId:y="codex-initial-prompt-full",textTestId:l="codex-initial-prompt-full-text",baseTextTestId:$="codex-initial-prompt-base"}){let j=Xj(f),J=B4(f),F=CX(f).trimEnd(),A=String(J.full?.text||""),U=IX(f),G=Number(j.promptChars||f?.promptChars||A.length),W=Number(j.basePromptLines||ry(F)),K=Number(j.promptLines||ry(A));return T("section",{className:"codex-progressive-card codex-progressive-prompt","data-testid":"codex-progressive-prompt"},T("div",{className:"codex-progressive-card-head"},T("span",{className:"codex-output-channel"},"Prompt"),T("strong",null,"Submitted prompt / 原始用户 prompt"),T("code",null,`${W||ry(F)} lines / ${F.length} chars`)),T("pre",{className:"codex-prompt-full","data-testid":$},F||"空 prompt"),U?T("details",{className:"codex-reference-injection codex-progressive-full-prompt","data-testid":y,onToggle:(E)=>{if(E.currentTarget?.open&&!A)_?.("full")}},T("summary",null,T("span",null,"引用注入已折叠,点击按需拉取最终进入 opencode 的完整 prompt"),T("code",null,A?`${K||ry(A)} lines / ${A.length} chars`:`${Number.isFinite(G)&&G>0?G:"--"} chars`)),T("pre",{className:"codex-prompt-full codex-prompt-final-full","data-testid":l},A||(u?"正在按需拉取完整 prompt...":"展开后将只请求 full prompt,不拉取完整 transcript。"))):null)}function eG({task:f,attempt:u,attemptIndex:_,loading:y,onLoadSteps:l,onLoadStep:$,testId:j="codex-execution-summary"}){let J=iX(cX(f,_)),F=gX(xX(f,u),J),A=Hj(f),U=aG(f,_),G=Number(F.toolCallCount||0),W=Array.isArray(F.editedFiles)?F.editedFiles:[],K=Array.isArray(F.commands)?F.commands:[],H=dG(u,_)?` · ${String(u?.label||"recovered thread execution")}`:_?` #${_}`:"",O=vX(f,u,_,F),z=`最近更新: ${UX(O)}`;return T("details",{className:"codex-progressive-card codex-execution-summary","data-testid":j,"data-attempt-index":r4(_),onToggle:(q)=>{if(q.currentTarget?.open&&!U)l?.(_)}},T("summary",null,T("div",{className:"codex-progressive-card-head"},T("span",{className:"codex-output-channel"},"Summary"),T("strong",null,`执行过程摘要${H}`),T("code",{title:O?`最近更新: ${zf(O)}`:z},`${X4(F.durationMs??F.totalElapsedMs)} / ${G} tools / ${z}`)),T("div",{className:"codex-execution-digest"},T("span",null,`read ${Number(F.readCount||0)}`),T("span",null,`edit ${Number(F.editCount||0)}`),T("span",null,`run ${Number(F.runCount||0)}`),T("span",null,`${Number(F.stepCount||J.length||0)} steps`))),T("div",{className:"codex-execution-digest expanded"},T("span",null,`修改文件:${kG(W,6)}`),T("span",null,`执行命令:${kG(K,4)}`)),J.length===0?T("div",{className:"codex-output-empty"},y?"正在按需拉取步骤 summary...":"展开后将只请求执行步骤 summary,不拉取单步骤全量。"):T("div",{className:"codex-trace-step-list"},J.map((q)=>{let Z=String(q?.seq??""),V=A[Z],L=Array.isArray(q?.summaryLines)?q.summaryLines.slice(0,4):[];return T("details",{key:Z||`${q?.title}-${q?.at}`,className:`codex-trace-step ${String(q?.kind||"message")}`,"data-testid":`codex-trace-step-${Z||"unknown"}`,onToggle:(r)=>{if(r.currentTarget?.open&&!V)$?.(q?.seq)}},T("summary",null,T("span",{className:"codex-output-channel"},jN(q?.kind)),T("strong",null,String(q?.title||"Trace step")),q?.status?T("code",null,String(q.status)):null,T("time",null,zf(q?.at))),T("div",{className:"codex-trace-step-summary"},L.length>0?L.map((r,N)=>T("pre",{key:`${Z}-${N}`},String(r||""))):T("span",null,"无 summary")),V?.line?T(E4,{items:[V.line],autoScroll:!1,loading:!1,hasDetail:!0,emptyText:"无步骤详情",testId:`codex-trace-step-detail-${Z||"unknown"}`,className:"codex-transcript codex-step-detail-transcript",collapseTools:!1}):T("div",{className:"codex-output-empty"},y?"正在按需拉取这个步骤的全量数据...":"展开后将只请求这个单步骤的全量数据。"))})))}function jN(f){let u=String(f||"");if(u==="ran")return"Ran";if(u==="explored")return"Explored";if(u==="edited")return"Edited";if(u==="error")return"Error";if(u==="system")return"System";return"Message"}function fz({task:f,attempt:u,attemptIndex:_,testId:y="codex-final-response"}){let l=bX(f,u),$=Number(u?.finalResponseChars||l.length),j=_?` #${_}`:"";return T("section",{className:"codex-progressive-card codex-final-response","data-testid":y,"data-attempt-index":r4(_)},T("div",{className:"codex-progressive-card-head"},T("span",{className:"codex-output-channel"},"Final"),T("strong",null,`最终 response${j}`),T("code",null,`${Number.isFinite($)?$:l.length} chars`)),T("pre",{className:"codex-transcript-body"},l||"暂无最终 response"))}function uz({task:f,attempt:u,attemptIndex:_,testId:y="codex-progressive-judge"}){let l=sG(f,u),$=_?` #${_}`:"";return T("section",{className:"codex-progressive-card codex-progressive-judge","data-testid":y,"data-attempt-index":r4(_)},T("div",{className:"codex-progressive-card-head"},T("span",{className:"codex-output-channel"},"Judge"),T("strong",null,`完成判定${$}`),l?.decision?T("code",null,`${l.decision} ${Math.round(Number(l.confidence||0)*100)}%`):null),l?T("div",{className:"codex-judge-card","data-testid":`${y}-card`},T(x_,{status:l.decision},l.decision),T("strong",null,`${Math.round(Number(l.confidence||0)*100)}% confidence`),T("p",{"data-testid":`${y}-reason`},l.reason||"--"),l.continuePrompt?T("pre",{"data-testid":`${y}-continue-prompt`},String(l.continuePrompt||"")):null):T("div",{className:"codex-output-empty"},"尚未判定"))}function JN({task:f,attempt:u,attemptIndex:_,loading:y,onLoadPromptPart:l,testId:$="codex-judge-feedback-prompt"}){let j=hX(f,u,_);if(j===null)return null;let J=oG(_),A=B4(f)[J],U=String(A?.text||"").trimEnd(),G=String(j.preview||j.text||"").trimEnd(),W=U||String(j.text||"").trimEnd(),K=Number(A?.chars||j.chars||W.length||G.length),E=Number(A?.lines||j.lines||ry(W||G)),H=A?.forAttempt||j.forAttempt||Number(_||0)+1;return T("details",{className:"codex-progressive-card codex-judge-feedback-prompt","data-testid":$,"data-attempt-index":r4(_),onToggle:(O)=>{if(O.currentTarget?.open&&!U)l?.("feedback",_)}},T("summary",null,T("div",{className:"codex-progressive-card-head"},T("span",{className:"codex-output-channel"},"Prompt"),T("strong",null,`judge feedback prompt #${_} -> #${H}`),T("code",null,`${E||"--"} lines / ${Number.isFinite(K)?K:G.length} chars`)),T("p",{className:"codex-feedback-preview","data-testid":`${$}-preview`},G||"展开后按需拉取 judge feedback prompt。")),T("pre",{className:"codex-prompt-full codex-feedback-full","data-testid":`${$}-text`},W||(y?"正在按需拉取 judge feedback prompt...":"展开后将只请求这一次 judge feedback prompt。")))}function FN({task:f,attempt:u,position:_,loading:y,onLoadPromptPart:l,onLoadSteps:$,onLoadStep:j}){let J=nX(u,_),F=_===0,A=dG(u,J),U=A?String(u?.label||"Recovered thread execution"):`Attempt ${J}`;return T("section",{className:"codex-attempt-cycle","data-testid":`codex-attempt-cycle-${J}`},T("div",{className:"codex-attempt-cycle-head"},T("span",{className:"codex-output-channel"},U),T("strong",null,String(u?.mode||(J<=1?"initial":"retry"))),u?.terminalStatus?T(x_,{status:u.terminalStatus},u.terminalStatus):null,T("code",null,`${zf(u?.startedAt)} -> ${zf(u?.finishedAt)}`)),T(eG,{task:f,attempt:u,attemptIndex:J,loading:y,onLoadSteps:$,onLoadStep:j,testId:F?"codex-execution-summary":`codex-execution-summary-attempt-${J}`}),A?null:T(fz,{task:f,attempt:u,attemptIndex:J,testId:F?"codex-final-response":`codex-final-response-attempt-${J}`}),A?null:T(uz,{task:f,attempt:u,attemptIndex:J,testId:F?"codex-progressive-judge":`codex-progressive-judge-attempt-${J}`}),A?null:T(JN,{task:f,attempt:u,attemptIndex:J,loading:y,onLoadPromptPart:l,testId:F?"codex-judge-feedback-prompt":`codex-judge-feedback-prompt-attempt-${J}`}))}function QN({task:f,loading:u,onLoadPromptPart:_,onLoadSteps:y,onLoadStep:l}){if(!f)return T(Il,{title:"未选择任务",text:"从左侧队列选择任务,或提交新 Codex 任务。"});let $=RX(f);return T("div",{className:"codex-transcript codex-progressive-trace","data-testid":"codex-output"},u&&!b_(f)?T("div",{className:"codex-output-empty"},"正在加载 Trace Summary..."):null,T($N,{task:f,loading:u,onLoadPromptPart:_}),$.length>0?$.map((j,J)=>T(FN,{key:`${j?.index||J+1}-${j?.startedAt||J}`,task:f,attempt:j,position:J,loading:u,onLoadPromptPart:_,onLoadSteps:y,onLoadStep:l})):[T(eG,{key:"execution",task:f,loading:u,onLoadSteps:y,onLoadStep:l}),T(fz,{key:"final",task:f}),T(uz,{key:"judge",task:f})])}function AN({task:f}){let u=TX(f);if(!f||u.length===0)return T(Il,{title:"暂无原始消息",text:"原始 Codex app-server 消息会保留在任务 JSON 中。"});return T("details",{className:"codex-raw-output"},T("summary",null,`原始 messages (${u.length})`),T("div",null,u.map((_)=>T("article",{key:`${_.seq}-${_.channel}`,className:`codex-output-line ${_.channel||"system"}`},T("div",{className:"codex-output-meta"},T("span",{className:"codex-output-channel"},sX(String(_.channel||"system"))),T("span",null,zf(_.at)),_.method?T("code",null,_.method):null),T("pre",null,String(_.text||""))))))}function UN({task:f}){let u=nG(f).slice().reverse();if(u.length===0)return T(Il,{title:"尚无 attempt",text:"任务开始运行后,这里会记录 Codex 终态、传输中断和 stderr tail。"});return T("div",{className:"table-wrap codex-attempt-table"},T("table",null,T("thead",null,T("tr",null,T("th",null,"#"),T("th",null,"模式"),T("th",null,"终态"),T("th",null,"传输"),T("th",null,"退出"),T("th",null,"完成时间"))),T("tbody",null,u.map((_)=>T("tr",{key:`${_.index}-${_.startedAt}`},T("td",null,_.index),T("td",null,_.mode),T("td",null,T(x_,{status:_.terminalStatus||"unknown"},_.terminalStatus||"unknown")),T("td",null,_.transportClosedBeforeTerminal?T(x_,{status:"failed"},"closed-before-terminal"):T(x_,{status:"succeeded"},"normal")),T("td",null,`code=${_.appServerExitCode??"--"} signal=${_.appServerSignal??"--"}`),T("td",null,zf(_.finishedAt)))))))}function _z({microservices:f,onRaw:u,apiBaseUrl:_="/api",initialTasksData:y=null,standalone:l=!1}){let $=f.find((g)=>g.id==="codex-queue")||null,j=uN(y),J=String(j?.id||""),F=new Map;if(j!==null&&J.length>0)F.set(J,{task:j,maxSeq:O4(Array.isArray(j.transcript)?j.transcript:[]),complete:Boolean(j._transcriptComplete),completeUpdatedAt:j._transcriptComplete?String(j.updatedAt||""):""});let A=typeof performance>"u"?0:performance.now(),U=Zu(J),G=Zu(0),W=Zu(0),K=Zu(!1),E=Zu(!1),H=Zu(null),O=Zu(new Map),z=Zu(new Map),q=Zu(new Map),Z=Zu(new Map),V=Zu(new Set),L=Zu(!1),r=Zu(Boolean(y)),N=Zu(F),D=Zu(y),[x,c]=u0(null),[v,C]=u0(y),[S,B]=u0(J),[P,M]=u0(j),[w,Y]=u0(!1),[R,k]=u0(""),[p,n]=u0(""),[_f,s]=u0("default"),[ff,Kf]=u0(R_),[Gf,jf]=u0("gpt-5.5"),[Wf,Of]=u0("/root/unidesk"),[Zf,h]=u0(99),[i,I]=u0(1),[lf,$f]=u0(!1),[Af,Yf]=u0(!1),[xf,of]=u0(""),[F0,y0]=u0(!0),[T0,Qu]=u0(()=>typeof window>"u"?!0:window.matchMedia(qX).matches),[X0,v0]=u0(!1),[iu,K0]=u0(""),[Au,uf]=u0(""),[vf,o0]=u0(""),[Bf,b0]=u0(""),[i0,a0]=u0(!1),[nf,d0]=u0(y?{phase:"complete",taskId:J,queueMs:0,detailMs:0,totalMs:A,chunks:j?1:0,transcriptRows:Array.isArray(j?.transcript)?j.transcript.length:0,partial:Boolean(y?.selected?.hasMore||M$(j)),completedAt:new Date}:null),[Hu,oy]=u0(y?new Date:null),[J_,ay]=u0(!1),t=Z1(v),Hf=t.filter(hl),Df=t.filter((g)=>!d1(g)),If=t.filter((g)=>d1(g)&&!hl(g)),Rf=v?.queue||x?.body?.queue||x?.queue||{},Q0=r$(v),af=xG(Rf,_f),h0=Y4(af,ff),e0=Number((Yu(ff)?Rf?.total:h0?.total)??Q0.total??t.length),S6=Q0.hasMore===!0&&String(Q0.nextBeforeId||"").length>0,dy=N4(Rf),ey=Yu(ff)?dy:[String(Y4(af,ff)?.activeTaskId||"")].filter(Boolean),$y=vG(Rf,af,ff,t),F_=Yu(ff)?Aj(Rf):Aj(h0||{}),C6=Aj(Rf),W2=oX(C6),G2=Math.max(aX(C6),dy.length),Q_=S$((Yu(ff)?Rf?.unreadTerminal:h0?.unreadTerminal)??Hf.length),jy=Yu(ff)?"All queues":ff,V3=$?GX($):{},R6=$?KX($):{},O3=$?zX($):{},X3=PG(()=>rX(R),[R]),j1=PG(()=>{let g=bG(i);return X3.flatMap((d)=>Array.from({length:g},()=>MX(d,p)))},[X3,i,p]),Jy=j1.length,x6=Jy>1&&!lf,z2=Af||X0||Jy===0||x6,N3=_N(Rf,Gf),L3=P?.id&&P?.activeTurnId&&String(P?.status)==="running",K2=P?.id&&!["succeeded","failed","canceled"].includes(String(P?.status||"")),Z2=P?.id&&["succeeded","failed","canceled"].includes(String(P?.status||""));function I1(g){let d=typeof g==="function"?g(D.current):g;return D.current=d,C(d),d}function v6(g,d,Qf=null,Ef=null){let Tf=new Set(g.map((Nf)=>String(Nf||"")).filter(Boolean));if(Tf.size===0&&Ef===null&&Qf===null)return;I1((Nf)=>{if(!Nf)return Nf;let rf=Z1(Nf).map((df)=>{let cf=String(df?.id||"");if(!Tf.has(cf))return df;let tf=Ef&&String(Ef?.id||"")===cf?Ef:{};return{...df,...tf,readAt:d,terminalUnread:!1}});return{...Nf,queue:Qf||Nf.queue,tasks:Tf.size>0?H4([rf],$y):rf}});for(let Nf of Tf){let rf=N.current.get(Nf);if(rf?.task){let df=Ef&&String(Ef?.id||"")===Nf?Ef:{},cf={...rf.task,...df,readAt:d,terminalUnread:!1};if(N.current.set(Nf,{...rf,task:cf}),U.current===Nf)M(cf)}}}P$(()=>{$f(!1)},[R,i,p]);function N1(g,d,Qf){let Ef=N.current.get(g)||{},Tf=Ef.task||{},Nf=Array.isArray(Tf.transcript)?Tf.transcript:[],rf=fN(Tf,d),df=Object.prototype.hasOwnProperty.call(d,"transcript")?Uj(Nf,Array.isArray(d.transcript)?d.transcript:[]):Nf,cf={...Tf,...rf,transcript:df,output:Array.isArray(rf.output)?Vj(Tf,rf,"output"):Array.isArray(Tf.output)?Tf.output:[],events:Array.isArray(rf.events)?Vj(Tf,rf,"events"):Array.isArray(Tf.events)?Tf.events:[]},tf=String(cf?.updatedAt||""),ef=Boolean(d._transcriptComplete)&&d1(cf),l0=Boolean(Ef.complete)&&d1(cf)&&String(Ef.completeUpdatedAt||"")===tf,fu=ef||l0,J1={...Ef,task:cf,maxSeq:O4(df),complete:fu,completeUpdatedAt:fu?tf:""};if(N.current.set(g,J1),Qf===W.current&&U.current===g)M(cf);return J1}async function fl(g,d=!1,Qf,Ef){if(!$||!g)return;let Nf=N.current.get(g)?.task,rf=String(Nf?._traceSummaryUpdatedAt||""),df=String(Nf?.updatedAt||"");if(!d&&Nf?._traceSummaryLoaded===!0&&rf===df)return;let cf=g,tf=O.current.get(cf);if(tf)return tf;let ef=W.current,l0=performance.now();if(U.current===g)Y(!0);let fu=(async()=>{try{let J1=await NX(_,g);if(ef!==W.current||U.current!==g)return;let N0=J1?.summary||{};N1(g,{id:g,status:N0.status,updatedAt:N0.updatedAt,startedAt:N0.startedAt,finishedAt:N0.finishedAt,currentAttempt:N0.currentAttempt,maxAttempts:N0.maxAttempts,finalResponse:N0.finalResponse,lastJudge:N0.lastJudge,lastError:N0.lastError,attempts:Array.isArray(N0.attempts)?N0.attempts:[],timing:N0.timing,_traceSummary:N0,_traceSummaryLoaded:!0,_traceSummaryUpdatedAt:String(N0.updatedAt||""),_detailLoaded:!0},ef),d0({phase:"complete",taskId:g,queueMs:Ef??0,detailMs:performance.now()-l0,totalMs:Qf===void 0?performance.now()-l0:performance.now()-Qf,chunks:1,transcriptRows:Number(N0?.execution?.stepCount||0),partial:!1,completedAt:new Date})}finally{if(O.current.delete(cf),ef===W.current&&U.current===g)Y(!1)}})();O.current.set(cf,fu),await fu}async function b6(g,d=null){let Qf=U.current;if(!$||!Qf||!g)return;let Ef=N.current.get(Qf)?.task,Tf=B4(Ef),Nf=g==="feedback"||g==="judge-feedback"?oG(d):g;if(Tf[Nf]?.text)return;let rf=`${Qf}:${Nf}`,df=z.current.get(rf);if(df)return df;let cf=W.current;if(U.current===Qf)Y(!0);let tf=(async()=>{try{let ef=await LX(_,Qf,g,d);if(cf!==W.current||U.current!==Qf)return;let l0=N.current.get(Qf)?.task,fu=B4(l0);N1(Qf,{...g==="full"?{prompt:String(ef?.text||""),promptChars:Number(ef?.chars||0)}:{},_promptDetails:{...fu,[Nf]:ef}},cf)}finally{if(z.current.delete(rf),cf===W.current&&U.current===Qf)Y(!1)}})();z.current.set(rf,tf),await tf}async function q2(g=null){let d=U.current;if(!$||!d)return;let Qf=N.current.get(d)?.task,Ef=g===null||g===void 0||String(g).length===0?"":String(g);if(aG(Qf,Ef||null))return;let Tf=`${d}:${Ef||"all"}`,Nf=q.current.get(Tf);if(Nf)return Nf;let rf=W.current;if(U.current===d)Y(!0);let df=(async()=>{try{let cf=await YX(_,d,0,500,Ef||null);if(rf!==W.current||U.current!==d)return;let tf=Array.isArray(cf?.steps)?cf.steps:[];if(Ef){let ef=N.current.get(d)?.task,l0=v_(ef?._traceStepsByAttempt)||{},fu=v_(ef?._traceStepsLoadedByAttempt)||{};N1(d,{_traceStepsByAttempt:{...l0,[Ef]:tf},_traceStepsLoadedByAttempt:{...fu,[Ef]:!0}},rf)}else N1(d,{_traceSteps:tf,_traceStepsLoaded:!0,_traceStepsHasMore:Boolean(cf?.hasMore),_traceStepsNextAfterSeq:cf?.nextAfterSeq},rf)}finally{if(q.current.delete(Tf),rf===W.current&&U.current===d)Y(!1)}})();q.current.set(Tf,df),await df}async function E2(g){let d=U.current,Qf=String(g??"");if(!$||!d||Qf.length===0)return;let Ef=N.current.get(d)?.task;if(Hj(Ef)[Qf]?.line)return;let Nf=`${d}:${Qf}`,rf=Z.current.get(Nf);if(rf)return rf;let df=W.current;if(U.current===d)Y(!0);let cf=(async()=>{try{let tf=await BX(_,d,g);if(df!==W.current||U.current!==d)return;let ef=N.current.get(d)?.task,l0=Hj(ef);N1(d,{_traceStepDetails:{...l0,[Qf]:tf}},df)}finally{if(Z.current.delete(Nf),df===W.current&&U.current===d)Y(!1)}})();Z.current.set(Nf,cf),await cf}async function EQ(g,d,Qf){if(!$||!g)return;let Ef=performance.now(),Tf=W.current,Nf=N.current.get(g);if(Nf?.task){if(M(Nf.task),Y(M$(Nf.task)||!Nf.complete),!M$(Nf.task)&&Nf.complete&&d1(Nf.task)&&String(Nf.completeUpdatedAt||"")===String(Nf.task?.updatedAt||"")){d0({phase:"complete",taskId:g,queueMs:Qf??0,detailMs:0,totalMs:d===void 0?0:performance.now()-d,chunks:0,transcriptRows:Array.isArray(Nf.task.transcript)?Nf.task.transcript.length:0,completedAt:new Date});return}}else Y(!0);let rf=H.current;if(rf?.taskId===g&&rf.token===Tf)return rf.promise;let df=(async()=>{try{let cf=await M0(P0(_,`/api/tasks/${encodeURIComponent(g)}?meta=1`));if(Tf!==W.current||U.current!==g)return;let tf=N.current.get(g),ef=Array.isArray(tf?.task?.transcript)?tf.task.transcript:[],l0=cf?.task||{},fu=Boolean(tf?.complete)&&String(tf?.completeUpdatedAt||"")===String(l0?.updatedAt||"");N1(g,{...l0,summaryOnly:!1,_metaLoaded:!0,transcript:ef,_detailLoaded:ef.length>0,_transcriptComplete:fu},Tf);let J1=M$(tf?.task)||Boolean(tf?.task?._transcriptPreview),N0=J1?0:ef.length>0?pG(ef):0,A_=!J1&&tf?.complete&&d1(l0)&&String(tf?.completeUpdatedAt||"")===String(l0?.updatedAt||"")?O4(ef):N0,_l=!0,p6=0,k6=ef.length;while(_l){let nu=await M0(P0(_,`/api/tasks/${encodeURIComponent(g)}/transcript?afterSeq=${encodeURIComponent(String(A_))}&limit=${FX}&fullText=1`));if(Tf!==W.current||U.current!==g)return;let L1=N.current.get(g),Fy=Array.isArray(L1?.task?.transcript)?L1.task.transcript:[],Qy=Uj(Fy,Array.isArray(nu?.transcript)?nu.transcript:[]);p6+=1,k6=Qy.length;let I0=Boolean(!nu?.hasMore);if(N1(g,{status:nu?.status||l0.status,updatedAt:nu?.updatedAt||l0.updatedAt,transcript:Qy,_detailLoaded:I0||Qy.length>0,_transcriptComplete:I0,_transcriptPreview:J1&&!I0},Tf),_l=Boolean(nu?.hasMore),A_=Number(nu?.nextAfterSeq??O4(Qy)),!_l)break;await new Promise((LQ)=>window.setTimeout(LQ,0))}d0({phase:"complete",taskId:g,queueMs:Qf??0,detailMs:performance.now()-Ef,totalMs:d===void 0?performance.now()-Ef:performance.now()-d,chunks:p6,transcriptRows:k6,completedAt:new Date})}finally{if(H.current?.taskId===g&&H.current?.token===Tf)H.current=null;if(Tf===W.current&&U.current===g)Y(!1)}})();H.current={taskId:g,token:Tf,promise:df},await df}async function gu(g=U.current,d=!0,Qf=ff){if(!$)return;if(!d&&L.current)return;let Ef=performance.now();if(d)L.current=!0;if(d)d0({phase:"loading",taskId:String(g||U.current||""),startedAt:new Date});let Tf=G.current+1;G.current=Tf;let Nf=String(g||U.current||""),rf=Nf?N.current.get(Nf):null,df=Array.isArray(rf?.task?.transcript)?rf.task.transcript:[],cf=pG(df),tf=x||{},ef=null;try{ef=await OX(_,Nf,cf,Qf)}catch{ef=await VX(_,tf,Qf)}if(Tf!==G.current){if(d)L.current=!1;return}let l0=performance.now()-Ef;c(tf);let fu=ef?.queue||{},J1=String(fu?.activeTaskId||N4(fu)[0]||""),N0=ef;I1((Uu)=>{let w3=Z1(ef),Ay=Z1(Uu),yl=Ay.length>0?H4([Ay,w3],J1):H4([w3],J1),Y2=r$(ef),i6=r$(Uu),tE=Ay.length>w3.length&&(i6.hasMore===!1||String(i6.nextBeforeId||"").length>0),sE={...Y2,...tE?{hasMore:i6.hasMore,nextBeforeId:i6.nextBeforeId}:{},returned:yl.length};return N0={...ef,tasks:yl,pagination:sE},N0});let A_=Z1(N0),_l=xG(fu,_f),p6=vG(fu,_l,Qf,A_),k6=HX(_l,Qf,A_),nu=Nf||U.current,L1=N0?.selected||null,Fy=L1?.task||null,Qy=Array.isArray(L1?.transcript)?L1.transcript:null,I0=nu&&(A_.some((Uu)=>Uu.id===nu)||String(Fy?.id||"")===nu)?nu:p6||k6||A_[0]?.id||"";if(U.current!==I0)W.current+=1;U.current=I0,B(I0);let m6=A_.find((Uu)=>Uu.id===I0);if(m6){let Uu=N.current.get(I0);if(Uu?.task)N.current.set(I0,{...Uu,task:{...m6,...Uu.task,status:m6.status,updatedAt:m6.updatedAt}})}if(Fy?.id===I0&&Qy!==null){let Uu=N.current.get(I0),w3=Array.isArray(Uu?.task?.transcript)?Uu.task.transcript:[],Ay=Uj(w3,Qy),yl=Boolean(L1?.preview);if(N1(I0,{...Fy,_summaryLoaded:!0,transcript:Ay,_detailLoaded:!L1?.hasMore||Ay.length>0,_transcriptComplete:!yl&&!L1?.hasMore&&d1(Fy),_transcriptPreview:yl},W.current),Y(!1),d)d0({phase:"complete",taskId:I0,queueMs:l0,detailMs:Math.max(0,performance.now()-Ef-l0),totalMs:performance.now()-Ef,chunks:1,transcriptRows:Ay.length,partial:Boolean(yl||L1?.hasMore||M$(Fy)),completedAt:new Date});if(oy(new Date),d)L.current=!1;fl(I0,!1,d?Ef:void 0,d?l0:void 0).catch((Y2)=>K0(C_(Y2,"加载 Codex Trace Summary 失败")));return}if(d)d0({phase:"session",taskId:I0,queueMs:l0,totalMs:l0,startedAt:new Date(Date.now()-l0)});if(I0)fl(I0,!0,d?Ef:void 0,d?l0:void 0).catch((Uu)=>K0(C_(Uu,"加载 Codex Trace Summary 失败")));else if(W.current+=1,M(null),Y(!1),d)d0({phase:"complete",taskId:"",queueMs:l0,detailMs:0,totalMs:performance.now()-Ef,chunks:0,transcriptRows:0,completedAt:new Date});if(oy(new Date),d)L.current=!1}async function h6(){if(!$||J_||E.current)return;let g=String(r$(v).nextBeforeId||"");if(!g)return;E.current=!0,ay(!0),K0("");try{let d=await XX(_,ff,g),Qf=Z1(d),Ef=d?.queue||Rf||{},Tf=String(Ef?.activeTaskId||N4(Ef)[0]||$y||"");I1((Nf)=>{let rf=H4([Z1(Nf),Qf],Tf),df=r$(d);return{...Nf||{},queue:Ef,tasks:rf,pagination:{...df,returned:rf.length}}})}catch(d){K0(C_(d,"加载更早 Codex tasks 失败"))}finally{E.current=!1,ay(!1)}}function H2(g){let d=g.currentTarget;if(!d||J_||!S6)return;if(d.scrollHeight-d.scrollTop-d.clientHeight<120)h6()}async function Mu(g,d){v0(!0),K0("");try{await g()}catch(Qf){K0(C_(Qf,d))}finally{v0(!1)}}async function Y3(g){if(!g)return;try{let d=!1;try{if(navigator.clipboard?.writeText)await navigator.clipboard.writeText(g),d=!0}catch{d=!1}if(!d){let Qf=document.createElement("textarea");Qf.value=g,Qf.style.position="fixed",Qf.style.opacity="0",document.body.appendChild(Qf),Qf.select(),d=document.execCommand("copy"),document.body.removeChild(Qf)}if(!d)throw Error("browser clipboard rejected the copy request");o0(g),uf(`已复制任务 ID:${g}`),window.setTimeout(()=>o0((Qf)=>Qf===g?"":Qf),1600)}catch(d){K0(`复制任务 ID 失败:${C_(d)}`)}}function ul(g){if(!g)return;n(g),uf(`已引用任务 ID:${g};提交时后端会读取并注入该任务上下文`)}async function B3(g){if(!$||!g)return;b0(g),await Mu(async()=>{let d=await DX(_,g),Qf=d?.task||{id:g,readAt:new Date().toISOString(),terminalUnread:!1},Ef=String(Qf?.readAt||new Date().toISOString());v6([g],Ef,d?.queue||null,Qf),uf(`已将任务 ${g} 标为已读`)},"标记 Codex task 已读失败"),b0((d)=>d===g?"":d)}async function I6(){if(!$||i0)return;a0(!0),await Mu(async()=>{let g=await wX(_),d=String(g?.readAt||new Date().toISOString()),Qf=Z1(D.current).filter(hl).map((rf)=>String(rf?.id||"")).filter(Boolean),Ef=Array.from(N.current.entries()).filter(([,rf])=>hl(rf?.task)).map(([rf])=>rf),Tf=Array.from(new Set([...Qf,...Ef]));v6(Tf,d,g?.queue||null);let Nf=Number(g?.count||Tf.length);uf(`已将 ${Nf} 个已结束未读任务标为已读`)},"全部标为已读失败"),a0(!1)}function V2(g){let d=g||R_;if(Kf(d),!Yu(d))s(d);if(I1(null),!(Yu(d)?U.current:""))U.current="",W.current+=1,B(""),M(null),Y(!0)}async function O2(){let g=typeof window>"u"?"":window.prompt("输入新的 Codex queue ID(字母/数字/._-,最长 64)","new-lane"),d=String(g||"").trim();if(!d)return;await Mu(async()=>{let Qf=await M0(P0(_,"/api/queues"),{method:"POST",body:{queueId:d}}),Ef=String(Qf?.queue?.id||d);s(Ef),Kf(Ef),I1(null),U.current="",W.current+=1,B(""),M(null),uf(`已创建并切换到 queue:${Ef}`),await gu("",!0,Ef)},"创建 Codex queue 失败")}async function D3(g){if(g.preventDefault(),K.current){uf("任务正在提交中,请等待当前请求完成,已阻止重复提交。");return}if(j1.length>1&&!lf){K0(`检测到将创建 ${j1.length} 个任务;请先勾选“确认批量入队”,避免误传多个任务。`);return}K.current=!0,Yf(!0),uf("正在提交 Codex Queue 任务,请等待后端确认,输入已临时锁定。"),await Mu(async()=>{if(j1.length===0)throw Error("prompt 不能为空");let d=L4(p),Qf=_f.trim()||"default",Ef=[...j1],Tf=(tf)=>({prompt:tf,queueId:Qf,model:Gf,cwd:Wf,maxAttempts:Number(Zf),...d.length>0?{referenceTaskIds:d}:{}}),Nf=Ef.length===1?Tf(Ef[0]):{tasks:Ef.map(Tf)},rf=await M0(P0(_,Ef.length===1?"/api/tasks":"/api/tasks/batch"),{method:"POST",body:Nf}),df=rf?.tasks?.[0]?.id||"",cf=Array.isArray(rf?.tasks)?rf.tasks.map((tf)=>String(tf?.id||"")).filter(Boolean):[];if(uf(`已创建 ${cf.length||Ef.length} 个任务${cf.length>0?`:${cf.join(" / ")}`:""}`),k(""),n(""),$f(!1),U.current=df,ff!==Qf)I1(null);Kf(Qf),s(Qf),await gu(df,!0,Qf)},"Codex 任务入队失败"),K.current=!1,Yf(!1)}async function X2(g){if(g.preventDefault(),!P?.id)return;await Mu(async()=>{await M0(P0(_,`/api/tasks/${encodeURIComponent(P.id)}/steer`),{method:"POST",body:{prompt:xf}}),of(""),await gu(P.id)},"追加 prompt 失败")}async function N2(){if(!P?.id)return;await Mu(async()=>{await M0(P0(_,`/api/tasks/${encodeURIComponent(P.id)}/interrupt`),{method:"POST",body:{}}),await gu(P.id)},"打断 Codex session 失败")}async function c6(){if(!P?.id)return;await Mu(async()=>{await M0(P0(_,`/api/tasks/${encodeURIComponent(P.id)}/retry`),{method:"POST",body:{}}),await gu(P.id)},"重新入队失败")}async function cE(g){let d=String(P?.id||""),Qf=String(g||"").trim();if(!d||!Qf)return;let Ef=w4(P);if(Qf===Ef){uf(`任务 ${d} 已在 queue=${Qf}`);return}await Mu(async()=>{let Nf=(await M0(P0(_,`/api/tasks/${encodeURIComponent(d)}/move`),{method:"POST",body:{queueId:Qf}}))?.task||{...P,queueId:Qf};if(N.current.set(d,{...N.current.get(d)||{},task:Nf}),U.current=d,M(Nf),B(d),s(Qf),!Yu(ff))I1(null),Kf(Qf);uf(`已将任务 ${d} 从 ${Ef} 移动到 ${Qf}`),await gu(d,!0,Yu(ff)?R_:Qf)},"移动任务 queue 失败")}async function pE(){let g=U.current;if(!g)return;let d=performance.now();await Mu(async()=>{d0({phase:"session",taskId:g,queueMs:0,totalMs:0,partial:!0,startedAt:new Date}),await fl(g,!0,d,0)},"刷新 Trace Summary 失败")}function kE(g){U.current=g,W.current+=1,B(g);let d=N.current.get(g);if(d?.task)M(d.task),Y(!1);else{Y(!0);let Qf=t.find((Ef)=>Ef.id===g);if(Qf)M(Qf);else M(null)}gu(g).catch((Qf)=>K0(C_(Qf,"切换 Codex session 失败")))}function L2(g){if(kE(g),EX())Qu(!1)}P$(()=>{if(r.current){r.current=!1;return}Mu(()=>gu(U.current),"Codex Queue 加载失败")},[$?.id,ff]),P$(()=>{if(!$)return;let g=()=>{if(!SG())return;gu(U.current,!1).catch((Ef)=>K0(C_(Ef,"Codex Queue 轮询失败")))},d=window.setInterval(()=>{g()},1500),Qf=()=>{if(SG())g()};return document.addEventListener("visibilitychange",Qf),()=>{window.clearInterval(d),document.removeEventListener("visibilitychange",Qf)}},[$?.id,ff]),P$(()=>{if(!$||!P||w)return;let g=String(P.id||"");if(!g)return;let d=String(P.updatedAt||""),Qf=String(P._traceSummaryUpdatedAt||"");if(P._traceSummaryLoaded===!0&&Qf===d)return;let Ef=`${g}:${d||"unknown"}`;if(V.current.has(Ef))return;V.current.add(Ef),fl(g,!0).catch((Tf)=>K0(C_(Tf,"自动加载 Trace Summary 失败")))},[$?.id,P?.id,P?.updatedAt,P?._traceSummaryUpdatedAt,P?._traceSummaryLoaded,w]);let mE=t.length===0?T(Il,{title:"队列为空",text:"提交一个任务后,Codex 会串行执行并保存输出。"}):[Hf.length>0?T(Gj,{key:"unread",title:"已结束未读",tasks:Hf,selectedId:S,emptyText:"暂无已结束未读任务。",onSelect:L2,onCopy:Y3,onReference:ul,onMarkRead:B3,copiedTaskId:vf,markingReadTaskId:Bf}):null,T(Gj,{key:"active",title:"运行 / 排队",tasks:Df,selectedId:S,emptyText:"当前没有运行或排队任务。",onSelect:L2,onCopy:Y3,onReference:ul,onMarkRead:B3,copiedTaskId:vf,markingReadTaskId:Bf}),T(Gj,{key:"history",title:"历史 session",tasks:If,selectedId:S,emptyText:"最近没有完成、失败或取消的 session。",onSelect:L2,onCopy:Y3,onReference:ul,onMarkRead:B3,copiedTaskId:vf,markingReadTaskId:Bf}),T("div",{key:"pagination",className:"codex-task-pagination","data-testid":"codex-task-pagination"},T("span",null,`已加载 ${t.length} / ${Number.isFinite(e0)?e0:t.length}`),S6?T("button",{type:"button",className:"ghost-btn",disabled:J_,onClick:()=>void h6(),"data-testid":"codex-load-more-tasks-button"},J_?"加载中":"加载更早任务"):T("code",null,"已到队列末尾"))],HQ=(g,d=!1)=>T("label",{className:`codex-queue-switcher ${d?"compact":""}`},T("span",null,d?"Queue":"查看 queue"),T("select",{value:ff,onChange:(Qf)=>V2(String(Qf.target.value||R_)),"data-testid":g},T("option",{value:R_},`All queues · ${Number.isFinite(e0)?e0:t.length} tasks · ${dy.length} running`),af.map((Qf)=>T("option",{key:String(Qf?.id||""),value:String(Qf?.id||"")},Kj(Qf))))),iE=T("div",{className:"codex-trace-status","data-testid":"codex-trace-status-summary"},T("span",{className:"codex-trace-status-chip queued"},T("b",null,"排队"),String(W2)),T("span",{className:"codex-trace-status-chip running"},T("b",null,"运行"),String(G2)),T("span",{className:`codex-trace-status-chip unread ${Q_>0?"warn":""}`},T("b",null,"结束未读"),String(Q_))),gE=T(bl,{title:P?`Trace ${String(P.id).slice(0,22)}`:"Trace 输出",eyebrow:P?`${P.status} / view=${jy} / task queue=${w4(P)} / ${P.model} / agent loop trace`:`Agent loop trace / view=${jy}`,summary:iE,actions:T("div",{className:"panel-actions"},HQ("codex-queue-filter-select"),T("button",{type:"button",className:"ghost-btn codex-mark-all-read-btn",disabled:Q_===0||X0||i0,onClick:()=>void I6(),"data-testid":"codex-mark-all-read-button"},i0?"标记中":`全部标已读${Q_>0?` (${Q_})`:""}`),P?T("button",{type:"button",className:"ghost-btn",disabled:w||X0,onClick:()=>void pE(),"data-testid":"codex-load-full-trace-button"},w?"加载中":b_(P)?"刷新 Summary":"加载 Summary"):null,T("button",{type:"button",className:"codex-session-title-toggle",onClick:()=>Qu((g)=>!g),"data-testid":"codex-queue-sidebar-toggle"},T0?"收起队列":"展开队列"),T("label",{className:"inline-check"},T("input",{type:"checkbox",checked:F0,onChange:(g)=>y0(Boolean(g.target.checked))}),"自动滚动"),T("button",{type:"button",className:"ghost-btn",disabled:!K2||X0,onClick:()=>void N2(),"data-testid":"codex-interrupt-button"},"打断"),T("button",{type:"button",className:"ghost-btn",disabled:!Z2||X0,onClick:()=>void c6()},"重试"),P?T(CG,{title:"Codex Task",data:P,onOpen:u,testId:"raw-codex-task"}):null),className:"codex-output-panel"},T("div",{className:`codex-session-shell ${T0?"":"queue-collapsed"}`},T0?T("aside",{className:"codex-session-sidebar","data-testid":"codex-session-sidebar"},T("div",{className:"codex-session-sidebar-head"},T("div",null,T("span",null,Yu(ff)?"All queues":"Queue lane"),T("strong",null,`${jy} · ${t.length}/${Number.isFinite(e0)?e0:t.length} sessions · 未读 ${Q_}`)),T("button",{type:"button",className:"ghost-btn",onClick:()=>Qu(!1)},"收起")),HQ("codex-queue-filter-sidebar",!0),T("div",{className:"codex-task-list codex-task-list-session",onScroll:H2,"data-testid":"codex-task-list-scroll"},mE)):null,T("div",{className:"codex-session-main"},T("div",{className:"codex-output-stack"},T(QN,{task:P,loading:w,onLoadPromptPart:b6,onLoadSteps:q2,onLoadStep:E2}),T(AN,{task:P})))));if(!$)return T(Il,{title:"Codex Queue 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=codex-queue"});let VQ=Number(nf?.totalMs),OQ=Number(nf?.queueMs),XQ=Number(nf?.detailMs),NQ=Number(nf?.transcriptRows),nE=nf?.phase==="complete"?"complete":String(nf?.phase||"idle");return T("div",{className:`codex-queue-page ${l?"codex-standalone-page":""}`,"data-testid":"codex-queue-page","data-load-state":nE,"data-load-total-ms":Number.isFinite(VQ)?String(Math.round(VQ*10)/10):"","data-load-queue-ms":Number.isFinite(OQ)?String(Math.round(OQ*10)/10):"","data-load-detail-ms":Number.isFinite(XQ)?String(Math.round(XQ*10)/10):"","data-load-transcript-rows":Number.isFinite(NQ)?String(NQ):"","data-load-task-id":String(nf?.taskId||S||""),"data-load-partial":nf?.partial?"true":"false"},T(H0,{error:iu,wide:!0}),Au?T("div",{className:"form-success wide","data-testid":"codex-create-success"},Au):null,T("div",{className:"codex-session-stage codex-session-stage-top"},gE),T("div",{className:"codex-queue-layout"},T("div",{className:"codex-left-rail"},T(bl,{title:"提交任务",eyebrow:Af?"Submitting...":j1.length>1?`${j1.length} tasks`:"Single or Batch",className:"codex-compose-panel"},T("form",{className:`codex-task-form ${Af?"is-submitting":""}`,onSubmit:D3,"data-testid":"codex-queue-task-form","aria-busy":Af?"true":"false"},T("label",null,"Prompt / 多任务用单独一行 --- 分隔",T("textarea",{value:R,rows:8,disabled:Af,onChange:(g)=>k(g.target.value),placeholder:"写入 Codex 任务;多个任务之间用 --- 分隔。"})),T("label",{className:"codex-reference-field"},"引用任务 ID(可选)",T("input",{value:p,disabled:Af,onChange:(g)=>n(g.target.value),placeholder:"codex_...;支持空格/逗号分隔多个 ID","data-testid":"codex-reference-task-id"}),L4(p).length>0?T("code",null,`后端将解析并注入:${L4(p).join(" / ")}`):null),T("div",{className:"codex-form-grid"},T("label",{className:"codex-submit-queue-field"},"Queue",T("div",{className:"codex-submit-queue-row"},T("select",{value:_f,disabled:Af,onChange:(g)=>s(String(g.target.value||"default")),"data-testid":"codex-queue-id-select"},af.map((g)=>T("option",{key:String(g?.id||""),value:String(g?.id||"")},Kj(g)))),T("button",{type:"button",className:"ghost-btn codex-create-queue-btn",onClick:()=>void O2(),disabled:X0||Af,"data-testid":"codex-create-queue-button"},"创建 queue"))),T("label",null,"模型",T("select",{value:Gf,disabled:Af,onChange:(g)=>jf(g.target.value),"data-testid":"codex-model-select"},N3.map((g)=>T("option",{key:g,value:g},g)))),T("label",null,"工作目录",T("input",{value:Wf,disabled:Af,onChange:(g)=>Of(g.target.value),placeholder:Rf?.defaultWorkdir||"/root/unidesk"})),T("label",null,"最大尝试",T("input",{type:"number",min:1,max:99,value:Zf,disabled:Af,onChange:(g)=>h(Number(g.target.value)),"data-testid":"codex-max-attempts-input"})),T("label",null,"入队份数",T("input",{type:"number",min:1,max:50,value:i,disabled:Af,onChange:(g)=>I(Number(g.target.value)),"data-testid":"codex-repeat-count-input"}))),Jy>1?T("label",{className:`codex-batch-confirm ${lf?"confirmed":""}`,"data-testid":"codex-batch-confirm-row"},T("input",{type:"checkbox",checked:lf,disabled:Af,onChange:(g)=>$f(Boolean(g.target.checked)),"data-testid":"codex-batch-confirm-checkbox"}),T("span",null,`确认批量入队 ${Jy} 个任务(prompt 分段 ${X3.length} × 入队份数 ${bG(i)})`)):null,Af?T("div",{className:"codex-submit-wait","data-testid":"codex-submit-wait"},"正在提交到后端,已锁定输入以防重复提交..."):null,T("div",{className:"codex-form-actions"},T("button",{type:"button",className:"ghost-btn",disabled:X0||Af||R.length===0&&p.length===0,onClick:()=>{k(""),n(""),$f(!1),uf("已清空任务输入栏")},"data-testid":"codex-clear-input-button"},"清空输入"),T("button",{type:"submit",className:"primary-btn",disabled:z2,"data-testid":"codex-enqueue-button"},Af?"提交中,请等待...":x6?`请确认批量入队 ${Jy} 个任务`:j1.length>1?`批量入队 ${j1.length} 个任务`:"入队并运行"))))),T("div",{className:"codex-main-stage"},T("div",{className:"codex-detail-grid"},T(bl,{title:"运行控制",eyebrow:L3?"Active turn steer":"Steer when running"},T("div",{className:"codex-run-control-stack"},T(lN,{task:P,queueRows:af,busy:X0,onMove:cE}),T("form",{className:"codex-steer-form",onSubmit:X2},T("label",null,"追加 prompt",T("textarea",{value:xf,rows:4,onChange:(g)=>of(g.target.value),placeholder:"给正在运行的 Codex session 推入新的指令或纠偏。",disabled:!L3})),T("button",{type:"submit",className:"primary-btn",disabled:!L3||X0||xf.trim().length===0,"data-testid":"codex-steer-button"},"推入运行中 session")))),T(bl,{title:"完成判定",eyebrow:P?.lastJudge?P.lastJudge.source:"judge"},P?.lastJudge?T("div",{className:"codex-judge-card","data-testid":"codex-task-judge-card"},T(x_,{status:P.lastJudge.decision},P.lastJudge.decision),T("strong",null,`${Math.round(Number(P.lastJudge.confidence||0)*100)}% confidence`),T("p",{"data-testid":"codex-task-judge-reason"},P.lastJudge.reason||"--"),P.lastJudge.continuePrompt?T("code",{"data-testid":"codex-task-judge-continue-prompt"},iG(P.lastJudge.continuePrompt,220)):null):T(Il,{title:"尚未判定",text:"Codex turn 结束后会由 MiniMax M2.7 或 fallback judge 判定 complete/retry/fail;retry 会在已有 thread 追加继续执行 prompt。"}))),T(bl,{title:"Attempts",eyebrow:"terminal vs interruption"},T(UN,{task:P})))),T(bl,{title:"运行概要",eyebrow:"用户服务",actions:T("div",{className:"panel-actions"},T("button",{type:"button",className:"ghost-btn",onClick:()=>void Mu(()=>gu(S),"刷新失败"),disabled:X0,"data-testid":"codex-refresh-button"},X0?"同步中":"刷新"),T(CG,{title:"Codex Queue 用户服务",data:$,onOpen:u,testId:"raw-codex-queue-service"}))},T("div",{className:"codex-queue-hero"},T("div",null,T("div",{className:"node-version-line"},T(x_,{status:V3.providerStatus==="online"?"online":"warn"},V3.providerStatus||"unknown"),T("span",null,$.providerId),T("span",null,O3.public?"公网暴露":"仅 UniDesk frontend 代理访问"),T("span",null,Rf?.judgeConfigured?`MiniMax ${Rf?.minimaxModel||"M2.7"}`:"Fallback judge")),T("p",{className:"muted paragraph"},$.description)),T("div",{className:"microservice-ref-card"},T("span",null,"Queue view"),T("strong",null,jy),T("code",null,`${t.length}/${Number.isFinite(e0)?e0:t.length} loaded / ${dy.length} active lanes`),T("code",null,`models: ${N3.join(" / ")}`)),T("div",{className:"microservice-ref-card"},T("span",null,"Backend"),T("strong",null,`${O3.nodeBindHost||"--"}:${O3.nodePort||"--"}`),T("code",null,R6.containerName||"codex-queue-backend")))),T("div",{className:"codex-queue-metrics"},T(Ty,{label:"Queues",value:String(Rf?.queueCount??af.length??1),hint:`${Number(ey.length||0)} active lanes`,tone:ey.length>1?"warn":""}),T(Ty,{label:"排队",value:Wj(F_,"queued"),hint:"waiting turns"}),T(Ty,{label:"运行",value:Wj(F_,"running"),hint:ey.length>1?`${ey.length} parallel`:$y?`active ${String($y).slice(0,16)}`:"idle",tone:$y?"warn":"ok"}),T(Ty,{label:"成功",value:Wj(F_,"succeeded"),hint:"completed tasks",tone:"ok"}),T(Ty,{label:"异常/取消",value:String(Number(F_.failed||0)+Number(F_.canceled||0)),hint:"terminal non-success",tone:Number(F_.failed||0)>0?"fail":""}),T(Ty,{label:"加载耗时",value:Qj(nf?.totalMs),hint:nf?.phase==="complete"?`queue ${Qj(nf?.queueMs)} / session ${Qj(nf?.detailMs)} / ${nf?.chunks??0} chunks${nf?.partial?" / preview":""}`:`${nf?.phase||"idle"}...`,tone:Number(nf?.totalMs||0)>1000?"warn":"ok"}),T(Ty,{label:"最近刷新",value:Hu?L0(Hu):"--",hint:"1.5s polling"})))}var C4=Sf(c0(),1);var Jf=C4.default.createElement,{useEffect:WN}=C4.default,GN=C4.default.useState;function M4({status:f,children:u}){let _=String(f||"unknown").toLowerCase();return Jf("span",{className:`status-badge ${_}`},u||f||"unknown")}function h_({label:f,value:u,hint:_,tone:y}){return Jf("article",{className:`metric-card ${y||""}`},Jf("div",{className:"metric-label"},f),Jf("div",{className:"metric-value"},u),Jf("div",{className:"metric-hint"},_))}function P4({title:f,eyebrow:u,actions:_,children:y,className:l}){return Jf("section",{className:`panel ${l||""}`},Jf("div",{className:"panel-head"},Jf("div",null,u?Jf("p",{className:"panel-eyebrow"},u):null,Jf("h2",null,f)),_?Jf("div",{className:"panel-actions"},_):null),Jf("div",{className:"panel-body"},y))}function S4({title:f,data:u,onOpen:_,testId:y}){return Jf("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:()=>_(f,u)},"查看原始JSON")}function Nj({title:f,text:u}){return Jf("div",{className:"empty-state"},Jf("strong",null,f),Jf("span",null,u))}function zN(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function KN(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function ZN(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function My(f,u){let _=f&&typeof f==="object"?f[u]:void 0;return Number.isFinite(Number(_))?String(_):"--"}function qN(f){return(Array.isArray(f?.jobs)?f.jobs:[]).slice(0,40)}function EN(f){return(Array.isArray(f?.drafts)?f.drafts:[]).slice(0,12)}function yz({microservices:f,onRaw:u,apiBaseUrl:_="/api"}){let y=f.find((E)=>E.id==="findjob")||null,[l,$]=GN({loading:!1,error:"",health:null,summary:null,jobs:null,drafts:null,refreshedAt:null});async function j(){if(!y)return;$((E)=>({...E,loading:!0,error:""}));try{let[E,H,O,z]=await Promise.all([wf(`${_}/microservices/findjob/health`),wf(`${_}/microservices/findjob/proxy/api/summary`),wf(`${_}/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:40`),wf(`${_}/microservices/findjob/proxy/api/drafts`)]);$({loading:!1,error:"",health:E,summary:H,jobs:O,drafts:z,refreshedAt:new Date})}catch(E){$((H)=>({...H,loading:!1,error:Pf(E,"FindJob 加载失败")}))}}if(WN(()=>{j()},[y?.id,y?.runtime?.providerStatus]),!y)return Jf(Nj,{title:"FindJob 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=findjob"});let J=zN(y),F=ZN(y),A=KN(y),U=l.summary||{},G=qN(l.jobs),W=EN(l.drafts),K=l.jobs?._unidesk?.arrayLimits?.jobs;return Jf("div",{className:"findjob-page","data-testid":"findjob-page"},Jf(P4,{title:"FindJob 工作台",eyebrow:"D601 用户服务",actions:Jf("div",{className:"panel-actions"},Jf("button",{type:"button",className:"ghost-btn",onClick:j,disabled:l.loading,"data-testid":"findjob-refresh-button"},l.loading?"刷新中":"刷新"),Jf(S4,{title:"FindJob 用户服务",data:y,onOpen:u,testId:"raw-findjob-service"}))},Jf("div",{className:"findjob-hero"},Jf("div",null,Jf("div",{className:"node-version-line"},Jf(M4,{status:J.providerStatus==="online"?"online":"warn"},J.providerStatus||"unknown"),Jf("span",null,y.providerId),Jf("span",null,A.public?"公网暴露":"仅 UniDesk frontend 代理访问")),Jf("p",{className:"muted paragraph"},y.description)),Jf("div",{className:"microservice-ref-card"},Jf("span",null,"Repo"),Jf("strong",null,F.url||"--"),Jf("code",null,F.commitId||"--")),Jf("div",{className:"microservice-ref-card"},Jf("span",null,"D601 Docker"),Jf("strong",null,`${A.nodeBindHost||"--"}:${A.nodePort||"--"}`),Jf("code",null,`${F.composeFile||"--"} / ${F.composeService||"--"}`))),Jf(H0,{error:l.error,wide:!0})),Jf("div",{className:"findjob-grid"},Jf(P4,{title:"岗位指标",eyebrow:l.refreshedAt?`Updated ${L0(l.refreshedAt)}`:"Summary"},Jf("div",{className:"metric-grid"},Jf(h_,{label:"岗位总量",value:My(U,"totalJobs"),hint:"tracked jobs",tone:"ok"}),Jf(h_,{label:"原始岗位",value:My(U,"rawJobs"),hint:"raw queue"}),Jf(h_,{label:"已验证",value:My(U,"verifiedJobs"),hint:"verified set"}),Jf(h_,{label:"优先处理",value:My(U,"prioritizedJobs"),hint:"prioritized"}),Jf(h_,{label:"过期",value:My(U,"staleJobs"),hint:"stale jobs",tone:"warn"}),Jf(h_,{label:"无效",value:My(U,"invalidJobs"),hint:"invalid jobs",tone:"warn"}),Jf(h_,{label:"上海",value:My(U,"shanghaiJobs"),hint:"city filter"}),Jf(h_,{label:"Health",value:l.health?.ok?"OK":"--",hint:"D601 /api/health"})),Jf("div",{className:"panel-actions inline-actions"},Jf(S4,{title:"FindJob Summary",data:U,onOpen:u,testId:"raw-findjob-summary"}))),Jf(P4,{title:"近期岗位",eyebrow:K?`${K.returnedLength}/${K.originalLength} Preview`:`${G.length} Preview`},G.length===0?Jf(Nj,{title:"暂无岗位预览",text:"等待 D601 findjob backend 返回 /api/jobs"}):Jf("div",{className:"table-wrap findjob-job-table"},Jf("table",null,Jf("thead",null,Jf("tr",null,Jf("th",null,"优先级"),Jf("th",null,"状态"),Jf("th",null,"单位"),Jf("th",null,"职位"),Jf("th",null,"城市"),Jf("th",null,"阶段"),Jf("th",null,"截止"),Jf("th",null,"证据"))),Jf("tbody",null,G.map((E)=>Jf("tr",{key:E.id},Jf("td",null,Jf(M4,{status:String(E.priority||"").toLowerCase()||"unknown"},E.priority||"--")),Jf("td",null,Jf(M4,{status:String(E.status||"").toLowerCase()||"unknown"},E.status||"--")),Jf("td",null,E.organization_name||"--",Jf("code",null,E.id||"--")),Jf("td",null,E.display_title||E.title||"--"),Jf("td",null,E.display_city||E.city||"--"),Jf("td",null,E.workflow_stage||"--"),Jf("td",null,E.deadline||"--"),Jf("td",null,E.evidence_url?Jf("a",{href:E.evidence_url,target:"_blank",rel:"noreferrer"},"打开"):Jf("span",{className:"muted"},"无"))))))),Jf("div",{className:"panel-actions inline-actions"},Jf(S4,{title:"FindJob Jobs Preview",data:l.jobs,onOpen:u,testId:"raw-findjob-jobs"}))),Jf(P4,{title:"草稿与报告",eyebrow:`${W.length} Drafts`},W.length===0?Jf(Nj,{title:"暂无草稿",text:"D601 findjob backend 未返回 drafts"}):Jf("div",{className:"draft-list"},W.map((E)=>Jf("article",{key:E.id,className:"draft-card"},Jf("div",{className:"node-card-head"},Jf("strong",null,E.id),Jf(M4,{status:E.status},E.status||"--")),Jf("div",{className:"docker-meta compact"},Jf("span",null,E.workflow_stage||"--"),Jf("span",null,`jobs ${E.counts?.jobs??0}`),Jf("span",null,`reports ${E.counts?.reports??0}`)),Jf("span",null,E.latestReportPath||"暂无报告"),Jf("code",null,zf(E.updated_at||E.updatedAt))))),Jf("div",{className:"panel-actions inline-actions"},Jf(S4,{title:"FindJob Drafts",data:l.drafts,onOpen:u,testId:"raw-findjob-drafts"})))))}var v$=Sf(c0(),1);var b=v$.default.createElement,{useEffect:HN}=v$.default,Lj=v$.default.useState;function C$(f){let u=Number(f);return Number.isFinite(u)?`${Math.max(0,Math.min(100,u)).toFixed(1)}%`:"--"}function Bj(f){if(f===null||f===void 0||f==="")return"--";let u=Number(f);if(!Number.isFinite(u))return"--";if(u<60)return`${Math.max(0,Math.round(u))}s`;if(u<3600)return`${Math.floor(u/60)}m ${Math.round(u%60)}s`;return`${Math.floor(u/3600)}h ${Math.floor(u%3600/60)}m`}function Dj(f,u=2){let _=Number(f);if(!Number.isFinite(_))return f===!1?"false":f===!0?"true":"--";let y=Math.abs(_);if(Number.isInteger(_)||y>=1000)return _.toLocaleString("zh-CN",{maximumFractionDigits:0});if(y>=1)return _.toLocaleString("zh-CN",{maximumFractionDigits:u});return _.toLocaleString("zh-CN",{maximumFractionDigits:Math.max(u,6)})}function x$(f){if(f===null||f===void 0||f==="")return"--";if(typeof f==="boolean")return f?"true":"false";if(typeof f==="number")return Dj(f,4);if(Array.isArray(f))return f.map((u)=>x$(u)).join(" x ");if(typeof f==="object")return"已上报";return String(f)}function R4(f){let u=Number(f);if(!Number.isFinite(u)||u<=0)return"--";let _=u>=100?0:u>=10?1:2;return`${u.toLocaleString("zh-CN",{maximumFractionDigits:_})} epoch/h`}function x4(f){return f.replace(/[^a-zA-Z0-9_-]/g,"-")}function Bu(f){return f&&typeof f==="object"&&!Array.isArray(f)?f:{}}function R$({status:f,children:u}){let _=String(f||"unknown").toLowerCase();return b("span",{className:`status-badge ${_}`},u||f||"unknown")}function I_({label:f,value:u,hint:_,tone:y}){return b("article",{className:`metric-card ${y||""}`},b("div",{className:"metric-label"},f),b("div",{className:"metric-value"},u),b("div",{className:"metric-hint"},_))}function Yj({title:f,eyebrow:u,actions:_,children:y,className:l}){return b("section",{className:`panel ${l||""}`},b("div",{className:"panel-head"},b("div",null,u?b("p",{className:"panel-eyebrow"},u):null,b("h2",null,f)),_?b("div",{className:"panel-actions"},_):null),b("div",{className:"panel-body"},y))}function pl({title:f,data:u,onOpen:_,testId:y}){return b("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:(l)=>{l?.stopPropagation?.(),_(f,u)}},"查看原始JSON")}function e1({title:f,text:u}){return b("div",{className:"empty-state"},b("strong",null,f),b("span",null,u))}function VN(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function ON(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function XN(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function NN(f){return f?.counts&&typeof f.counts==="object"&&!Array.isArray(f.counts)?f.counts:{}}function LN(f){return Array.isArray(f?.jobs)?f.jobs.slice(0,240):[]}function YN(f){return Array.isArray(f?.projects)?f.projects.slice(0,1000):[]}function v4(f){return Array.isArray(f?.projects)?f.projects:[]}function BN(f,u){if(Array.isArray(u?.gpu))return u.gpu;if(Array.isArray(f?.gpu))return f.gpu;return[]}function hu(f,u){return`${f}/microservices/met-nonlinear/proxy${u}`}function lz(f){return f.startedAt&&f.finishedAt?Bj((Date.parse(f.finishedAt)-Date.parse(f.startedAt))/1000):"--"}function DN(f){let u=f.progress||{};if(u.etaSeconds!==null&&u.etaSeconds!==void 0&&u.etaSeconds!==""){let j=Number(u.etaSeconds);if(Number.isFinite(j))return Math.max(0,j)}let _=Number(u.currentEpoch),y=Number(u.epochTarget??f.epochTarget),l=Date.parse(f.startedAt||"");if(!Number.isFinite(_)||_<=0||!Number.isFinite(y)||y<=_||!Number.isFinite(l))return null;let $=Math.max(0,(Date.now()-l)/1000);if($<=0)return null;return Math.max(0,$/_*(y-_))}function $z(f){let u=f.progress||{},_=Number(u.epochPerHour);if(Number.isFinite(_)&&_>0)return _;let y=Date.parse(f.startedAt||""),l=["succeeded","failed","canceled"].includes(f.status)?Date.parse(f.finishedAt||""):Date.now();if(!Number.isFinite(y)||!Number.isFinite(l)||l<=y)return null;let $=Number(u.currentEpoch??f.epochTarget);if(!Number.isFinite($)||$<=0)return null;return $/((l-y)/3600000)}function jz(f){if(f==="staged")return"待启动";if(f==="queued")return"排队中";if(f==="running")return"训练中";if(f==="succeeded")return"已完成";if(f==="failed")return"失败";if(f==="canceled")return"已取消";return f||"unknown"}function Jz(f,u,_){return{name:f,path:u,depth:_,count:0,children:[],project:null}}function wN(f){let u=Jz("","",-1);for(let y of f){let $=String(y?.projectPath||"").replace(/\\/g,"/").split("/").filter(Boolean);if($.length===0)continue;let j=u,J=[];for(let[F,A]of $.entries()){J.push(A);let U=J.join("/"),G=j.children.find((W)=>W.path===U);if(!G)G=Jz(A,U,F),j.children.push(G);if(F===$.length-1)G.project=y;j=G}}let _=(y)=>{let l=y.children.reduce(($,j)=>$+_(j),0);return y.count=(y.project?1:0)+l,y.children.sort(($,j)=>{if(Boolean($.project)!==Boolean(j.project))return $.project?1:-1;return $.name.localeCompare(j.name,"zh-CN",{numeric:!0,sensitivity:"base"})}),y.count};return _(u),u}function TN(f){let u=Bu(f.data);return Bu(u.project).projectPath?Bu(u.project):u}function rN(f){return Bu(Bu(f.data).job)}function Fz({microservices:f,onRaw:u,apiBaseUrl:_="/api"}){let y=f.find((h)=>h.id==="met-nonlinear")||null,[l,$]=Lj({loading:!1,actionBusy:!1,error:"",health:null,summary:null,queue:null,projects:null,history:null,images:null,refreshedAt:null}),[j,J]=Lj({loading:!1,error:"",kind:"",key:"",title:"",data:null}),[F,A]=Lj(()=>({activeTab:"projects",selectedProjects:{},expandedProjectDirs:{},sourceProject:"",forkCount:1,forkEpochs:200,forkPrefix:`ui_fork_${Date.now()}`,maxConcurrency:3,targetGpuName:"2080 Ti",actionMessage:""}));function U(h){A((i)=>({...i,...h}))}async function G(h=F.activeTab){if(!y)return;$((i)=>({...i,loading:!0,error:""}));try{let i=[["health",wf(`${_}/microservices/met-nonlinear/health`)],["summary",wf(hu(_,"/api/summary"))]];if(h==="projects")i.push(["projectsRoot",wf(hu(_,"/api/projects?root=projects&limit=500"))]),i.push(["exProjectsRoot",wf(hu(_,"/api/projects?root=ex_projects&limit=500"))]);if(h==="current"||h==="completed"||h==="failed")i.push(["queue",wf(hu(_,"/api/queue"))]);if(h==="completed"||h==="failed")i.push(["history",wf(hu(_,"/api/history"))]);if(h==="gpu")i.push(["images",wf(hu(_,"/api/images"))]);let I=Object.fromEntries(await Promise.all(i.map(async([$f,Af])=>[$f,await Af]))),lf={loading:!1,actionBusy:!1,error:"",health:I.health,summary:I.summary,refreshedAt:new Date};if(I.projectsRoot||I.exProjectsRoot){let{projectsRoot:$f,exProjectsRoot:Af}=I;lf.projects={ok:$f?.ok!==!1&&Af?.ok!==!1,roots:[{root:"projects",count:v4($f).length},{root:"ex_projects",count:v4(Af).length}],projects:[...v4($f),...v4(Af)]}}if(I.queue)lf.queue=I.queue;if(I.history)lf.history=I.history;if(I.images)lf.images=I.images;$(($f)=>({...$f,...lf}))}catch(i){$((I)=>({...I,loading:!1,actionBusy:!1,error:Pf(i,"MET Nonlinear 加载失败")}))}}async function W(h,i){$((I)=>({...I,actionBusy:!0,error:""})),U({actionMessage:`${h}...`});try{let I=await i();U({actionMessage:I||`${h}完成`}),await G()}catch(I){$((lf)=>({...lf,actionBusy:!1,error:Pf(I,`${h}失败`)}))}}async function K(){await W("保存并发设置",async()=>{await wf(hu(_,"/api/queue/settings"),{method:"PUT",body:JSON.stringify({maxConcurrency:Number(F.maxConcurrency),targetGpuName:F.targetGpuName})})})}function E(){return Object.entries(F.selectedProjects).filter(([,h])=>h).map(([h])=>h)}async function H(){let h=E();if(h.length===0)throw Error("请先选择至少一个 project");await W("加入待启动队列",async()=>{await wf(hu(_,"/api/queue"),{method:"POST",body:JSON.stringify({projectPaths:h,maxConcurrency:Number(F.maxConcurrency),targetGpuName:F.targetGpuName,start:!1})}),U({activeTab:"current",selectedProjects:{}})})}async function O(){let h=F.sourceProject||S[0]?.projectPath;if(!h)throw Error("请先选择源 project");await W("Fork Project",async()=>{let i=await wf(hu(_,"/api/projects/fork"),{method:"POST",body:JSON.stringify({sourceProject:h,count:Number(F.forkCount),epochs:Number(F.forkEpochs),prefix:F.forkPrefix})}),I=Array.isArray(i.projectPaths)?i.projectPaths:[],lf=I.reduce(($f,Af)=>{return $f[Af]=!0,$f},{...F.selectedProjects});return U({selectedProjects:lf}),`已 fork ${I.length} 个 project,并已自动勾选;请确认后点击加入待启动队列。`})}async function z(){await W("启动队列",async()=>{await wf(hu(_,"/api/queue/start"),{method:"POST",body:JSON.stringify({maxConcurrency:Number(F.maxConcurrency),targetGpuName:F.targetGpuName})}),U({activeTab:"current"})})}async function q(h){await W("取消任务",async()=>{await wf(hu(_,`/api/jobs/${encodeURIComponent(h.id)}/cancel`),{method:"POST",body:JSON.stringify({})})})}async function Z(h){let i=String(h?.projectPath||"");if(!i)return;J({loading:!0,error:"",kind:"project",key:i,title:i,data:null});try{let I=await wf(hu(_,`/api/projects/config?path=${encodeURIComponent(i)}`));J({loading:!1,error:"",kind:"project",key:i,title:i,data:I})}catch(I){J({loading:!1,error:Pf(I,"Project 详情加载失败"),kind:"project",key:i,title:i,data:null})}}async function V(h){let i=String(h?.id||"");if(!i)return;J({loading:!0,error:"",kind:"job",key:i,title:h.projectPath||i,data:null});try{let I=await wf(hu(_,`/api/jobs/${encodeURIComponent(i)}`));J({loading:!1,error:"",kind:"job",key:i,title:I?.job?.projectPath||h.projectPath||i,data:I})}catch(I){J({loading:!1,error:Pf(I,"Job 详情加载失败"),kind:"job",key:i,title:h.projectPath||i,data:null})}}if(HN(()=>{G(F.activeTab)},[y?.id,y?.runtime?.providerStatus,F.activeTab]),!y)return b(e1,{title:"MET Nonlinear 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=met-nonlinear"});let L=VN(y),r=XN(y),N=ON(y),D=NN(l.queue?.queue||l.summary?.queue),x=BN(l.health,l.queue),c=l.health?.targetGpu||l.summary?.targetGpu||x.find((h)=>String(h.name||"").includes("2080")),v=l.images?.mlImage||l.health?.image||{},C=LN(l.queue),S=YN(l.projects),B=wN(S),P=F.sourceProject||S[0]?.projectPath||"",M=C.filter((h)=>["staged","queued","running"].includes(h.status)),w=C.filter((h)=>h.status==="succeeded"),Y=C.filter((h)=>["failed","canceled"].includes(h.status)),R=Array.isArray(l.history?.jobs)?l.history.jobs.slice(0,120):[],k=[{id:"projects",label:"项目库",count:S.length},{id:"current",label:"当前队列",count:M.length||Number(D.staged||0)+Number(D.queued||0)+Number(D.running||0)},{id:"completed",label:"已完成",count:w.length||Number(D.succeeded||0)},{id:"failed",label:"失败诊断",count:Y.length||Number(D.failed||0)+Number(D.canceled||0)},{id:"gpu",label:"GPU/镜像",count:x.length}];function p(h,i){if(h.length===0)return b(e1,{title:i==="current"?"当前队列为空":"暂无记录",text:i==="current"?"从项目库选择或 fork project 后先加入待启动队列,再启动队列。":"终态任务会显示耗时、exit code 和失败诊断。"});return b("div",{className:"table-wrap met-job-table"},b("table",null,b("thead",null,b("tr",null,b("th",null,"状态"),b("th",null,"Project"),b("th",null,"Epoch"),b("th",null,"速度"),b("th",null,"ETA/耗时"),b("th",null,"GPU"),b("th",null,"Exit"),b("th",null,"更新时间"),b("th",null,"操作"))),b("tbody",null,h.map((I)=>{let lf=I.progress||{},$f=["staged","queued","running"].includes(I.status),Af=j.kind==="job"&&j.key===I.id;return b("tr",{key:I.id,className:`met-click-row ${Af?"active":""}`,onClick:()=>V(I),"data-testid":`met-job-row-${x4(I.id)}`},b("td",null,b(R$,{status:I.status},jz(I.status))),b("td",null,b("button",{type:"button",className:"met-inline-link",onClick:(Yf)=>{Yf.stopPropagation(),V(I)}},I.projectPath),b("code",null,I.id)),b("td",null,b("span",null,`${lf.currentEpoch??"--"} / ${lf.epochTarget??I.epochTarget??"--"}`),b("div",{className:"met-progress"},b("span",{style:{width:C$(lf.progressPercent)}}))),b("td",null,b("strong",null,R4($z(I)))),b("td",null,I.status==="succeeded"||I.status==="failed"||I.status==="canceled"?lz(I):I.status==="running"?`ETA ${Bj(DN(I))}`:"--"),b("td",null,I.gpuName||"--"),b("td",null,I.exitCode??"--"),b("td",null,zf(I.updatedAt)),b("td",null,$f?b("button",{type:"button",className:"ghost-btn mini",onClick:(Yf)=>{Yf.stopPropagation(),q(I)},disabled:l.actionBusy},"取消"):null,b(pl,{title:`MET Job ${I.id}`,data:I,onOpen:u,testId:`raw-met-job-${I.id}`})))}))))}function n(){return b("div",{className:"met-queue-summary","data-testid":"met-current-summary"},b(R$,{status:"staged"},`待启动 ${D.staged??0}`),b(R$,{status:"queued"},`排队中 ${D.queued??0}`),b(R$,{status:"running"},`训练中 ${D.running??0}`),b("span",null,`最大并发 ${l.summary?.queue?.maxConcurrency??l.queue?.queue?.maxConcurrency??F.maxConcurrency}`),b("span",null,`目标 GPU ${l.summary?.queue?.targetGpuName??l.queue?.queue?.targetGpuName??F.targetGpuName}`))}function _f(h,i){let I=F.expandedProjectDirs[h];return I===void 0?i<2:Boolean(I)}function s(h,i){let I=_f(h,i);U({expandedProjectDirs:{...F.expandedProjectDirs,[h]:!I}})}function ff(h){let i=8+Math.max(0,h.depth)*16;if(Boolean(h.project)){let $f=h.project,Af=Boolean(F.selectedProjects[$f.projectPath]),Yf=j.kind==="project"&&j.key===$f.projectPath;return b("div",{key:h.path,className:`met-tree-row project ${Af?"selected":""} ${Yf?"active":""}`,style:{paddingLeft:i},onClick:()=>Z($f),"data-testid":`met-project-node-${x4($f.projectPath)}`},b("div",{className:"met-tree-name"},b("input",{type:"checkbox",checked:Af,onClick:(xf)=>xf.stopPropagation(),onChange:(xf)=>U({selectedProjects:{...F.selectedProjects,[$f.projectPath]:xf.target.checked}}),"data-testid":`met-project-checkbox-${x4($f.projectPath)}`}),b("button",{type:"button",className:"met-inline-link project-path",onClick:(xf)=>{xf.stopPropagation(),Z($f)}},h.name)),b("span",null,$f.useModel||"--"),b("span",null,$f.epochTrain??"--"),b("span",null,C$($f.progress?.progressPercent)),b("span",null,R4($f.progress?.epochPerHour)))}let lf=_f(h.path,h.depth);return b(v$.default.Fragment,{key:h.path},b("div",{className:"met-tree-row folder",style:{paddingLeft:i},"data-testid":`met-project-folder-${x4(h.path)}`},b("button",{type:"button",className:"met-tree-toggle",onClick:()=>s(h.path,h.depth),"aria-label":lf?`折叠 ${h.path}`:`展开 ${h.path}`},lf?"-":"+"),b("strong",null,h.name),b("span",{className:"met-tree-count"},`${h.count} projects`)),lf?h.children.map(($f)=>ff($f)):null)}function Kf(h){return b("div",{className:"met-detail-kv"},h.map((i)=>b("div",{key:i.label,className:"met-detail-kv-item"},b("span",null,i.label),b("strong",null,x$(i.value)),i.hint?b("small",null,i.hint):null)))}function Gf(h,i){return b("div",{className:"met-detail-section"},b("h3",null,h),Kf(i))}function jf(h){if(!Array.isArray(h)||h.length===0)return b(e1,{title:"模型层未上报",text:"等待 data/model_info.json 或 compute_analysis.json 生成。"});return b("div",{className:"table-wrap met-layer-table"},b("table",null,b("thead",null,b("tr",null,b("th",null,"Layer"),b("th",null,"Type"),b("th",null,"Params"),b("th",null,"Trainable"),b("th",null,"Compute"))),b("tbody",null,h.slice(0,18).map((i,I)=>b("tr",{key:`${i.name||"layer"}-${I}`},b("td",null,i.name||`#${I+1}`),b("td",null,i.type||"--"),b("td",null,Dj(i.num_params)),b("td",null,i.trainable===void 0?"--":String(Boolean(i.trainable))),b("td",null,Dj(i.compute?.total??i.estimated_cost?.weighted_units?.total)))))))}function Wf(h){let i=Array.isArray(h)?h:[];if(i.length===0)return b(e1,{title:"data/ 暂无文件",text:"训练或评估完成后会生成 training_state、metrics、model_info 等文件。"});return b("div",{className:"met-file-chip-grid"},i.slice(0,48).map((I)=>b("span",{key:I},I)),i.length>48?b("span",null,`+${i.length-48}`):null)}function Of(h){let i=String(h||"").replace(/\x1b\[[0-9;]*[A-Za-z]/g,"").split(/\r?\n/).map((I)=>I.trim()).filter(Boolean).slice(-12);if(i.length===0)return b(e1,{title:"暂无日志尾部",text:"该任务未上报 logTail 或日志已轮转。"});return b("div",{className:"met-log-lines"},i.map((I,lf)=>b("div",{key:`${lf}-${I.slice(0,16)}`},I)))}function Zf(){if(j.loading)return b("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},b(e1,{title:"详情加载中",text:j.title||"正在读取 D601 data/ 和 config.json"}));if(j.error)return b("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},b(H0,{error:j.error,wide:!0}));if(!j.data)return b("section",{className:"met-detail-panel muted","data-testid":"met-detail-panel"},b(e1,{title:"选择一个项目或任务查看详情",text:"项目库、当前队列、已完成和失败诊断中的行都可以点击;默认只展示结构化字段,原始 JSON 需显式点击按钮。"}));let h=TN(j),i=rN(j),I=Bu(h.config),lf=Bu(h.progress||i.progress),$f=Bu(h.data),Af=Bu(h.metrics||$f.metrics||lf.trainingInfo?.evaluation_metrics),Yf=Bu($f.trainingInfo||lf.trainingInfo),xf=Bu($f.trainingState),of=Bu(h.model||$f.model),F0=Array.isArray(of.modelSummary)&&of.modelSummary.length>0?of.modelSummary:of.computeLayers,y0=Bu(Yf.evaluation_metrics),T0=j.kind==="job"?"训练任务详情":"Project 详情";return b("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},b("div",{className:"panel-head compact"},b("div",null,b("p",{className:"panel-eyebrow"},j.kind==="job"?"Job + Project Detail":"Project Library Detail"),b("h2",null,T0),b("code",null,h.projectPath||i.projectPath||j.title)),b("div",{className:"panel-actions"},b(pl,{title:`MET ${T0}`,data:j.data,onOpen:u,testId:"raw-met-detail"}))),j.kind==="job"?Gf("任务状态",[{label:"Job ID",value:i.id},{label:"状态",value:jz(i.status)},{label:"GPU",value:i.gpuName},{label:"Exit Code",value:i.exitCode},{label:"耗时",value:lz(i)},{label:"训练速度",value:R4($z({...i,progress:lf}))}]):null,Gf("config.json",[{label:"use_model",value:I.use_model},{label:"epoch_train",value:I.epoch_train},{label:"step_per_epoch",value:I.step_per_epoch},{label:"learning_rate",value:I.learning_rate},{label:"using_gpu",value:I.using_gpu},{label:"use_points",value:I.use_points},{label:"sample_rate",value:I.sample_rate},{label:"time_clipped_s",value:I.time_clipped_s},{label:"H_UNITS",value:I.H_UNITS},{label:"INNER_KAN_UNITS",value:I.INNER_KAN_UNITS},{label:"INNER_KAN_LAYERS",value:I.INNER_KAN_LAYERS},{label:"GRID_SIZE",value:I.GRID_SIZE},{label:"SPLINE_ORDER",value:I.SPLINE_ORDER},{label:"USE_FAST_MODEL",value:I.USE_FAST_MODEL},{label:"IIR_TRAINABLE",value:I.IIR_TRAINABLE}]),Gf("data/ 训练状态",[{label:"Epoch",value:`${lf.currentEpoch??xf.current_epoch??xf.completed_epoch??"--"} / ${lf.epochTarget??I.epoch_train??"--"}`},{label:"Progress",value:C$(lf.progressPercent)},{label:"Last Loss",value:lf.lastLoss??xf.loss},{label:"Last Val Loss",value:lf.lastValLoss??xf.val_loss},{label:"Min Loss",value:Yf.min_loss??xf.min_loss},{label:"Min Val Loss",value:Yf.min_val_loss??xf.min_val_loss},{label:"Log Lines",value:lf.logLineCount},{label:"ETA",value:Bj(lf.etaSeconds??xf.remaining_time)},{label:"训练速度",value:R4(lf.epochPerHour??xf.smoothed_speed)},{label:"Training Alive",value:xf.training_alive}]),Gf("模型参数",[{label:"Model Type",value:of.modelType??I.use_model},{label:"Total Params",value:of.totalParams,hint:of.totalParams===null||of.totalParams===void 0?"未上报":"data/model_info.json"},{label:"Trainable",value:of.trainableParams},{label:"Non-trainable",value:of.nonTrainableParams},{label:"Compute Cost",value:of.computeCost},{label:"Estimate Status",value:of.estimateStatus},{label:"Unsupported Layers",value:of.unsupportedLayerCount}]),Gf("指标",[{label:"train_loss",value:Af.train_loss??y0.train_loss},{label:"val_loss",value:Af.val_loss??y0.val_loss},{label:"train_mae",value:Af.train_mae??y0.train_mae},{label:"val_mae",value:Af.val_mae??y0.val_mae},{label:"train_afmae",value:Af.train_afmae??y0.train_afmae},{label:"val_afmae",value:Af.val_afmae??y0.val_afmae},{label:"freq_drift_hz",value:Af.freq_drift_hz},{label:"sens_drift_percent",value:Af.sens_drift_percent},{label:"linearity_percent",value:Af.linearity_percent},{label:"weights_source",value:Af.weights_source??y0.weights_source},{label:"lr min/mean/max",value:`${x$(Yf.learning_rate_min)} / ${x$(Yf.learning_rate_mean)} / ${x$(Yf.learning_rate_max)}`}]),b("div",{className:"met-detail-section"},b("h3",null,"模型层"),jf(F0)),b("div",{className:"met-detail-section"},b("h3",null,"data/ 文件"),Wf($f.files)),j.kind==="job"?b("div",{className:"met-detail-section"},b("h3",null,"日志尾部"),Of(Bu(j.data).logTail)):null)}return b("div",{className:"met-page","data-testid":"met-nonlinear-page"},b(Yj,{title:"MET Nonlinear 训练编排",eyebrow:"D601 GPU 用户服务",actions:b("div",{className:"panel-actions"},b("button",{type:"button",className:"ghost-btn",onClick:G,disabled:l.loading,"data-testid":"met-refresh-button"},l.loading?"刷新中":"刷新"),b(pl,{title:"MET Nonlinear 用户服务",data:y,onOpen:u,testId:"raw-met-service"}))},b("div",{className:"findjob-hero"},b("div",null,b("div",{className:"node-version-line"},b(R$,{status:L.providerStatus==="online"?"online":"warn"},L.providerStatus||"unknown"),b("span",null,y.providerId),b("span",null,N.public?"公网暴露":"仅 UniDesk frontend 代理访问")),b("p",{className:"muted paragraph"},y.description)),b("div",{className:"microservice-ref-card"},b("span",null,"Repo"),b("strong",null,r.url||"--"),b("code",null,r.commitId||"--")),b("div",{className:"microservice-ref-card"},b("span",null,"D601 Docker"),b("strong",null,`${N.nodeBindHost||"--"}:${N.nodePort||"--"}`),b("code",null,`${r.composeFile||"--"} / ${r.containerName||"--"}`))),b(H0,{error:l.error,wide:!0}),F.actionMessage?b("div",{className:"met-action-log","data-testid":"met-action-message"},F.actionMessage):null),b("div",{className:"met-grid"},b(Yj,{title:"核心状态",eyebrow:l.refreshedAt?`Updated ${L0(l.refreshedAt)}`:"Queue + GPU"},b("div",{className:"metric-grid"},b(I_,{label:"Staged",value:D.staged??0,hint:"加入队列未开始",tone:Number(D.staged||0)>0?"warn":""}),b(I_,{label:"Queued",value:D.queued??0,hint:"排队等待调度",tone:Number(D.queued||0)>0?"warn":""}),b(I_,{label:"Running",value:D.running??0,hint:`max ${l.summary?.queue?.maxConcurrency??l.queue?.queue?.maxConcurrency??"--"}`,tone:Number(D.running||0)>0?"ok":""}),b(I_,{label:"Succeeded",value:D.succeeded??0,hint:"已完成"}),b(I_,{label:"Failed",value:D.failed??0,hint:"需要诊断",tone:Number(D.failed||0)>0?"warn":""}),b(I_,{label:"2080Ti Free",value:c?C$(Number(c.freeRatio)*100):"--",hint:c?`${c.memoryFreeMiB}/${c.memoryTotalMiB} MiB`:"等待 GPU 上报"}),b(I_,{label:"ML Image",value:v.present?"READY":"MISSING",hint:v.image||"met-nonlinear-ml:tf26",tone:v.present?"ok":"warn"}),b(I_,{label:"Health",value:l.health?.ok?"OK":"--",hint:"D601 /health"}))),b(Yj,{title:"队列控制",eyebrow:"Downloader-like staging"},b("div",{className:"met-control-strip"},b("label",null,"最大并发",b("input",{type:"number",min:1,max:16,value:F.maxConcurrency,"data-testid":"met-max-concurrency-input",onChange:(h)=>U({maxConcurrency:h.target.value})})),b("label",null,"目标 GPU",b("input",{value:F.targetGpuName,"data-testid":"met-target-gpu-input",onChange:(h)=>U({targetGpuName:h.target.value})})),b("button",{type:"button",className:"ghost-btn",onClick:K,disabled:l.actionBusy,"data-testid":"met-save-settings-button"},"保存设置"),b("button",{type:"button",className:"primary-btn",onClick:z,disabled:l.actionBusy||Number(D.staged||0)===0,"data-testid":"met-start-queue-button"},"启动队列")),b("p",{className:"muted paragraph"},"Project 先进入待启动队列,不会立即训练;点击启动队列后才切换为排队中,并由 D601 scheduler 按最大并发和 2080Ti 显存策略调度。")),b("section",{className:"panel met-workspace"},b("div",{className:"met-tabs",role:"tablist"},k.map((h)=>b("button",{key:h.id,type:"button",className:F.activeTab===h.id?"active":"",onClick:()=>U({activeTab:h.id}),"data-testid":`met-tab-${h.id}`},`${h.label} ${h.count}`))),b("div",{className:"panel-body"},F.activeTab==="projects"?b("div",{className:"met-form-grid","data-testid":"met-projects-pane"},b("div",{className:"met-fork-card"},b("h3",null,"Fork Project"),b("label",null,"源 Project",b("select",{value:P,"data-testid":"met-source-project-select",onChange:(h)=>U({sourceProject:h.target.value})},S.map((h)=>b("option",{key:h.projectPath,value:h.projectPath},`${h.projectPath} · ${h.useModel||"model?"}`)))),b("label",null,"Fork 数量",b("input",{type:"number",min:1,max:100,value:F.forkCount,"data-testid":"met-fork-count-input",onChange:(h)=>U({forkCount:h.target.value})})),b("label",null,"训练轮数",b("input",{type:"number",min:1,max:1e5,value:F.forkEpochs,"data-testid":"met-fork-epochs-input",onChange:(h)=>U({forkEpochs:h.target.value})})),b("label",null,"目标前缀",b("input",{value:F.forkPrefix,"data-testid":"met-fork-prefix-input",onChange:(h)=>U({forkPrefix:h.target.value})})),b("button",{type:"button",className:"primary-btn",onClick:O,disabled:l.actionBusy||!P,"data-testid":"met-fork-button"},"Fork Project"),b("p",{className:"muted paragraph"},"Fork 只创建新 Project 并自动勾选,不会直接训练;需要在右侧确认后加入待启动队列。")),b("div",{className:"met-project-list"},b("div",{className:"panel-head compact"},b("div",null,b("p",{className:"panel-eyebrow"},`Existing Projects · ${(l.projects?.roots||[]).map((h)=>`${h.root} ${h.count}`).join(" / ")}`),b("h2",null,"选择已有 Project")),b("button",{type:"button",className:"ghost-btn",onClick:H,disabled:l.actionBusy||E().length===0,"data-testid":"met-stage-selected-button"},`加入待启动队列 (${E().length})`)),S.length===0?b(e1,{title:"暂无 project",text:"等待 D601 返回 /api/projects"}):b("div",{className:"met-project-table","data-testid":"met-project-tree"},b("div",{className:"met-tree-header"},b("span",null,"文件树 Project"),b("span",null,"Model"),b("span",null,"Epochs"),b("span",null,"Progress"),b("span",null,"速度")),B.children.map((h)=>ff(h)))),Zf()):null,F.activeTab==="current"?b("div",{"data-testid":"met-current-pane"},n(),p(M,"current"),Zf(),b("div",{className:"panel-actions inline-actions"},b(pl,{title:"MET Queue",data:l.queue,onOpen:u,testId:"raw-met-queue"}))):null,F.activeTab==="completed"?b("div",{"data-testid":"met-completed-pane"},p(w.length>0?w:R.filter((h)=>h.status==="succeeded"),"completed"),Zf()):null,F.activeTab==="failed"?b("div",{"data-testid":"met-failed-pane"},p(Y.length>0?Y:R.filter((h)=>["failed","canceled"].includes(h.status)),"failed"),Zf(),b("div",{className:"panel-actions inline-actions"},b(pl,{title:"MET History",data:l.history,onOpen:u,testId:"raw-met-history"}))):null,F.activeTab==="gpu"?b("div",{className:"met-gpu-pane","data-testid":"met-gpu-pane"},x.length===0?b(e1,{title:"暂无 GPU 上报",text:"等待 D601 met-nonlinear-ts 或 ML image 提供 nvidia-smi 数据"}):b("div",{className:"table-wrap"},b("table",null,b("thead",null,b("tr",null,b("th",null,"Index"),b("th",null,"Name"),b("th",null,"Free"),b("th",null,"Policy"))),b("tbody",null,x.map((h)=>b("tr",{key:h.index},b("td",null,h.index),b("td",null,h.name),b("td",null,`${h.memoryFreeMiB} / ${h.memoryTotalMiB} MiB`,b("div",{className:"met-progress"},b("span",{style:{width:C$(Number(h.freeRatio)*100)}}))),b("td",null,String(h.name||"").includes("2080")?"target 2080Ti, <20% 限制并发":"non-target")))))),b("div",{className:"panel-actions inline-actions"},b(pl,{title:"MET Images",data:l.images,onOpen:u,testId:"raw-met-images"}))):null))))}var h4=[{id:"ops",label:"运行总览",code:"OPS",tabs:[{id:"status",label:"态势总览"},{id:"performance",label:"性能面板"},{id:"events",label:"事件摘要"},{id:"logs",label:"服务日志"}]},{id:"nodes",label:"资源节点",code:"NODE",tabs:[{id:"list",label:"节点清单"},{id:"monitor",label:"资源监控"},{id:"docker",label:"Docker 状态"},{id:"gateway",label:"网关版本"},{id:"labels",label:"资源标签"},{id:"heartbeats",label:"心跳状态"}]},{id:"tasks",label:"任务调度",code:"TASK",tabs:[{id:"dispatch",label:"下发任务"},{id:"pending",label:"待处理任务"},{id:"history",label:"任务历史"},{id:"results",label:"执行结果"}]},{id:"apps",label:"用户服务",code:"APP",routeSegment:"app",tabs:[{id:"catalog",label:"服务目录"},{id:"todo-note",label:"Todo Note"},{id:"findjob",label:"FindJob"},{id:"pipeline",label:"Pipeline"},{id:"met-nonlinear",label:"MET Nonlinear"},{id:"claudeqq",label:"ClaudeQQ"},{id:"codex-queue",label:"Codex Queue"},{id:"project-manager",label:"Project Manager"}]},{id:"config",label:"系统配置",code:"CFG",tabs:[{id:"topology",label:"连接拓扑"},{id:"auth",label:"认证策略"},{id:"security",label:"安全边界"}]}],kl=Object.fromEntries(h4.map((f)=>[f.id,f.tabs[0]?.id??""]));function MN(f){let u=String(f||"").trim();if(!u)return"";try{return decodeURIComponent(u)}catch{return u}}function b4(f){let u=String(f||"/"),[_]=u.split(/[?#]/u,1);if(_==="/")return"/";let l=`/${_.split("/").map(MN).filter(Boolean).join("/")}`;return l.endsWith("/")?l:`${l}/`}function PN(f){let u=2166136261;for(let _ of f)u^=_.charCodeAt(0),u=Math.imul(u,16777619);return Math.abs(u>>>0).toString(36)}function wj(f){return String(f||"").normalize("NFKD").replace(/[\u0300-\u036f]/gu,"").toLowerCase().replace(/[^a-z0-9]+/gu,"-").replace(/^-+|-+$/gu,"")}function Qz(f){return String(f||"").trim().toLowerCase().replace(/[\s/\\?#%]+/gu,"-").replace(/-+/gu,"-").replace(/^-+|-+$/gu,"")}function Az(f){let u=wj(f.routeSegment||"")||Qz(f.routeSegment||"");if(u)return u;let _=wj(f.id||"");if(_)return _;let y=wj(f.label||"")||Qz(f.label||"");if(y)return y;return`route-${PN(JSON.stringify(f))}`}function Tj(f,u){return`${f}:${u}`}function Uz(f){let u=f.map((F)=>{let A=Az(F);return{...F,routeSegment:A,tabs:F.tabs.map((U)=>({...U,routeSegment:Az(U)}))}}),_={},y={},l={},$=u.map((F)=>{let A=F.tabs[0]?.id??"";l[F.id]=A;let U=F.tabs.map((K)=>{let E=`/${F.routeSegment}/${K.routeSegment}/`,H=[E],O={moduleId:F.id,tabId:K.id};for(let z of H)_[b4(z)]=O;return y[Tj(F.id,K.id)]=E,{...K,canonicalPath:E,aliases:H}}),G=`/${F.routeSegment}/`,W={moduleId:F.id,tabId:A};return _[b4(G)]=W,{...F,routeSegment:F.routeSegment,canonicalPath:G,tabs:U}}),j=$[0],J={moduleId:j?.id||"",tabId:j?.tabs[0]?.id||""};return _["/"]=J,{modules:$,moduleById:Object.fromEntries($.map((F)=>[F.id,F])),defaultActiveTabs:l,routeMap:_,canonicalPathByTarget:y,fallbackTarget:J}}function rj(f,u){return f.routeMap[b4(u)]||f.fallbackTarget}function b$(f,u,_){return f.canonicalPathByTarget[Tj(u,_)]||f.canonicalPathByTarget[Tj(f.fallbackTarget.moduleId,f.fallbackTarget.tabId)]||"/"}function Wz(f,u){let _=f.routeMap[b4(u)];if(!_)return null;return b$(f,_.moduleId,_.tabId)}var fy=Sf(c0(),1);var o=Sf(Zz(),1),a=Sf(c0(),1);function B0(f){if(typeof f==="string"||typeof f==="number")return""+f;let u="";if(Array.isArray(f)){for(let _=0,y;_{}};function Ez(){for(var f=0,u=arguments.length,_={},y;f=0)y=_.slice(l+1),_=_.slice(0,l);if(_&&!u.hasOwnProperty(_))throw Error("unknown type: "+_);return{type:_,name:y}})}c4.prototype=Ez.prototype={constructor:c4,on:function(f,u){var _=this._,y=IN(f+"",_),l,$=-1,j=y.length;if(arguments.length<2){while(++$0)for(var _=Array(l),y=0,l,$;y=0&&(u=f.slice(0,_))!=="xmlns")f=f.slice(_+1);return Mj.hasOwnProperty(u)?{space:Mj[u],local:f}:f}function Pj(f){let u;while(u=f.sourceEvent)f=u;return f}function ju(f,u){if(f=Pj(f),u===void 0)u=f.currentTarget;if(u){var _=u.ownerSVGElement||u;if(_.createSVGPoint){var y=_.createSVGPoint();return y.x=f.clientX,y.y=f.clientY,y=y.matrixTransform(u.getScreenCTM().inverse()),[y.x,y.y]}if(u.getBoundingClientRect){var l=u.getBoundingClientRect();return[f.clientX-l.left-u.clientLeft,f.clientY-l.top-u.clientTop]}}return[f.pageX,f.pageY]}function pN(){}function c_(f){return f==null?pN:function(){return this.querySelector(f)}}function Sj(f){if(typeof f!=="function")f=c_(f);for(var u=this._groups,_=u.length,y=Array(_),l=0;l<_;++l)for(var $=u[l],j=$.length,J=y[l]=Array(j),F,A,U=0;U=Z)Z=q+1;while(!(L=O[Z])&&++Z=0;)if(j=y[l]){if($&&j.compareDocumentPosition($)^4)$.parentNode.insertBefore(j,$);$=j}return this}function gj(f){if(!f)f=uL;function u(G,W){return G&&W?f(G.__data__,W.__data__):!G-!W}for(var _=this._groups,y=_.length,l=Array(y),$=0;$u?1:f>=u?0:NaN}function nj(){var f=arguments[0];return arguments[0]=this,f.apply(null,arguments),this}function tj(){return Array.from(this)}function sj(){for(var f=this._groups,u=0,_=f.length;u<_;++u)for(var y=f[u],l=0,$=y.length;l<$;++l){var j=y[l];if(j)return j}return null}function oj(){let f=0;for(let u of this)++f;return f}function aj(){return!this.node()}function dj(f){for(var u=this._groups,_=0,y=u.length;_1?this.each((u==null?FL:typeof u==="function"?AL:QL)(f,u,_==null?"":_)):p_(this.node(),f)}function p_(f,u){return f.style.getPropertyValue(u)||c$(f).getComputedStyle(f,null).getPropertyValue(u)}function UL(f){return function(){delete this[f]}}function WL(f,u){return function(){this[f]=u}}function GL(f,u){return function(){var _=u.apply(this,arguments);if(_==null)delete this[f];else this[f]=_}}function uJ(f,u){return arguments.length>1?this.each((u==null?UL:typeof u==="function"?GL:WL)(f,u)):this.node()[f]}function Hz(f){return f.trim().split(/^|\s+/)}function _J(f){return f.classList||new Vz(f)}function Vz(f){this._node=f,this._names=Hz(f.getAttribute("class")||"")}Vz.prototype={add:function(f){var u=this._names.indexOf(f);if(u<0)this._names.push(f),this._node.setAttribute("class",this._names.join(" "))},remove:function(f){var u=this._names.indexOf(f);if(u>=0)this._names.splice(u,1),this._node.setAttribute("class",this._names.join(" "))},contains:function(f){return this._names.indexOf(f)>=0}};function Oz(f,u){var _=_J(f),y=-1,l=u.length;while(++y=0)_=u.slice(y+1),u=u.slice(0,y);return{type:u,name:_}})}function SL(f){return function(){var u=this.__on;if(!u)return;for(var _=0,y=-1,l=u.length,$;_()=>f;function i$(f,{sourceEvent:u,subject:_,target:y,identifier:l,active:$,x:j,y:J,dx:F,dy:A,dispatch:U}){Object.defineProperties(this,{type:{value:f,enumerable:!0,configurable:!0},sourceEvent:{value:u,enumerable:!0,configurable:!0},subject:{value:_,enumerable:!0,configurable:!0},target:{value:y,enumerable:!0,configurable:!0},identifier:{value:l,enumerable:!0,configurable:!0},active:{value:$,enumerable:!0,configurable:!0},x:{value:j,enumerable:!0,configurable:!0},y:{value:J,enumerable:!0,configurable:!0},dx:{value:F,enumerable:!0,configurable:!0},dy:{value:A,enumerable:!0,configurable:!0},_:{value:U}})}i$.prototype.on=function(){var f=this._.on.apply(this._,arguments);return f===this._?this:f};function iL(f){return!f.ctrlKey&&!f.button}function gL(){return this.parentNode}function nL(f,u){return u==null?{x:f.x,y:f.y}:u}function tL(){return navigator.maxTouchPoints||"ontouchstart"in this}function g$(){var f=iL,u=gL,_=nL,y=tL,l={},$=Py("start","drag","end"),j=0,J,F,A,U,G=0;function W(V){V.on("mousedown.drag",K).filter(y).on("touchstart.drag",O).on("touchmove.drag",z,Yz).on("touchend.drag touchcancel.drag",q).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function K(V,L){if(U||!f.call(this,V,L))return;var r=Z(this,u.call(this,V,L),V,L,"mouse");if(!r)return;R0(V.view).on("mousemove.drag",E,Sy).on("mouseup.drag",H,Sy),gl(V.view),m4(V),A=!1,J=V.clientX,F=V.clientY,r("start",V)}function E(V){if(u_(V),!A){var L=V.clientX-J,r=V.clientY-F;A=L*L+r*r>G}l.mouse("drag",V)}function H(V){R0(V.view).on("mousemove.drag mouseup.drag",null),k$(V.view,A),u_(V),l.mouse("end",V)}function O(V,L){if(!f.call(this,V,L))return;var r=V.changedTouches,N=u.call(this,V,L),D=r.length,x,c;for(x=0;x>8&15|u>>4&240,u>>4&15|u&240,(u&15)<<4|u&15,1):_===8?i4(u>>24&255,u>>16&255,u>>8&255,(u&255)/255):_===4?i4(u>>12&15|u>>8&240,u>>8&15|u>>4&240,u>>4&15|u&240,((u&15)<<4|u&15)/255):null):(u=oL.exec(f))?new Du(u[1],u[2],u[3],1):(u=aL.exec(f))?new Du(u[1]*255/100,u[2]*255/100,u[3]*255/100,1):(u=dL.exec(f))?i4(u[1],u[2],u[3],u[4]):(u=eL.exec(f))?i4(u[1]*255/100,u[2]*255/100,u[3]*255/100,u[4]):(u=fY.exec(f))?Pz(u[1],u[2]/100,u[3]/100,1):(u=uY.exec(f))?Pz(u[1],u[2]/100,u[3]/100,u[4]):Bz.hasOwnProperty(f)?Tz(Bz[f]):f==="transparent"?new Du(NaN,NaN,NaN,0):null}function Tz(f){return new Du(f>>16&255,f>>8&255,f&255,1)}function i4(f,u,_,y){if(y<=0)f=u=_=NaN;return new Du(f,u,_,y)}function lY(f){if(!(f instanceof o$))f=E1(f);if(!f)return new Du;return f=f.rgb(),new Du(f.r,f.g,f.b,f.opacity)}function tl(f,u,_,y){return arguments.length===1?lY(f):new Du(f,u,_,y==null?1:y)}function Du(f,u,_,y){this.r=+f,this.g=+u,this.b=+_,this.opacity=+y}n$(Du,tl,qJ(o$,{brighter(f){return f=f==null?n4:Math.pow(n4,f),new Du(this.r*f,this.g*f,this.b*f,this.opacity)},darker(f){return f=f==null?t$:Math.pow(t$,f),new Du(this.r*f,this.g*f,this.b*f,this.opacity)},rgb(){return this},clamp(){return new Du(Ry(this.r),Ry(this.g),Ry(this.b),t4(this.opacity))},displayable(){return-0.5<=this.r&&this.r<255.5&&(-0.5<=this.g&&this.g<255.5)&&(-0.5<=this.b&&this.b<255.5)&&(0<=this.opacity&&this.opacity<=1)},hex:rz,formatHex:rz,formatHex8:$Y,formatRgb:Mz,toString:Mz}));function rz(){return`#${Cy(this.r)}${Cy(this.g)}${Cy(this.b)}`}function $Y(){return`#${Cy(this.r)}${Cy(this.g)}${Cy(this.b)}${Cy((isNaN(this.opacity)?1:this.opacity)*255)}`}function Mz(){let f=t4(this.opacity);return`${f===1?"rgb(":"rgba("}${Ry(this.r)}, ${Ry(this.g)}, ${Ry(this.b)}${f===1?")":`, ${f})`}`}function t4(f){return isNaN(f)?1:Math.max(0,Math.min(1,f))}function Ry(f){return Math.max(0,Math.min(255,Math.round(f)||0))}function Cy(f){return f=Ry(f),(f<16?"0":"")+f.toString(16)}function Pz(f,u,_,y){if(y<=0)f=u=_=NaN;else if(_<=0||_>=1)f=u=NaN;else if(u<=0)f=NaN;return new q1(f,u,_,y)}function Cz(f){if(f instanceof q1)return new q1(f.h,f.s,f.l,f.opacity);if(!(f instanceof o$))f=E1(f);if(!f)return new q1;if(f instanceof q1)return f;f=f.rgb();var u=f.r/255,_=f.g/255,y=f.b/255,l=Math.min(u,_,y),$=Math.max(u,_,y),j=NaN,J=$-l,F=($+l)/2;if(J){if(u===$)j=(_-y)/J+(_0&&F<1?0:j;return new q1(j,J,F,f.opacity)}function Rz(f,u,_,y){return arguments.length===1?Cz(f):new q1(f,u,_,y==null?1:y)}function q1(f,u,_,y){this.h=+f,this.s=+u,this.l=+_,this.opacity=+y}n$(q1,Rz,qJ(o$,{brighter(f){return f=f==null?n4:Math.pow(n4,f),new q1(this.h,this.s,this.l*f,this.opacity)},darker(f){return f=f==null?t$:Math.pow(t$,f),new q1(this.h,this.s,this.l*f,this.opacity)},rgb(){var f=this.h%360+(this.h<0)*360,u=isNaN(f)||isNaN(this.s)?0:this.s,_=this.l,y=_+(_<0.5?_:1-_)*u,l=2*_-y;return new Du(EJ(f>=240?f-240:f+120,l,y),EJ(f,l,y),EJ(f<120?f+240:f-120,l,y),this.opacity)},clamp(){return new q1(Sz(this.h),g4(this.s),g4(this.l),t4(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&(0<=this.l&&this.l<=1)&&(0<=this.opacity&&this.opacity<=1)},formatHsl(){let f=t4(this.opacity);return`${f===1?"hsl(":"hsla("}${Sz(this.h)}, ${g4(this.s)*100}%, ${g4(this.l)*100}%${f===1?")":`, ${f})`}`}}));function Sz(f){return f=(f||0)%360,f<0?f+360:f}function g4(f){return Math.max(0,Math.min(1,f||0))}function EJ(f,u,_){return(f<60?u+(_-u)*f/60:f<180?_:f<240?u+(_-u)*(240-f)/60:u)*255}function HJ(f,u,_,y,l){var $=f*f,j=$*f;return((1-3*f+3*$-j)*u+(4-6*$+3*j)*_+(1+3*f+3*$-3*j)*y+j*l)/6}function VJ(f){var u=f.length-1;return function(_){var y=_<=0?_=0:_>=1?(_=1,u-1):Math.floor(_*u),l=f[y],$=f[y+1],j=y>0?f[y-1]:2*l-$,J=y()=>f;function JY(f,u){return function(_){return f+_*u}}function FY(f,u,_){return f=Math.pow(f,_),u=Math.pow(u,_)-f,_=1/_,function(y){return Math.pow(f+y*u,_)}}function xz(f){return(f=+f)===1?o4:function(u,_){return _-u?FY(u,_,f):a$(isNaN(u)?_:u)}}function o4(f,u){var _=u-f;return _?JY(f,_):a$(isNaN(f)?u:f)}var xy=function f(u){var _=xz(u);function y(l,$){var j=_((l=tl(l)).r,($=tl($)).r),J=_(l.g,$.g),F=_(l.b,$.b),A=o4(l.opacity,$.opacity);return function(U){return l.r=j(U),l.g=J(U),l.b=F(U),l.opacity=A(U),l+""}}return y.gamma=f,y}(1);function vz(f){return function(u){var _=u.length,y=Array(_),l=Array(_),$=Array(_),j,J;for(j=0;j<_;++j)J=tl(u[j]),y[j]=J.r||0,l[j]=J.g||0,$[j]=J.b||0;return y=f(y),l=f(l),$=f($),J.opacity=1,function(F){return J.r=y(F),J.g=l(F),J.b=$(F),J+""}}}var QY=vz(VJ),AY=vz(OJ);function XJ(f,u){if(!u)u=[];var _=f?Math.min(u.length,f.length):0,y=u.slice(),l;return function($){for(l=0;l<_;++l)y[l]=f[l]*(1-$)+u[l]*$;return y}}function bz(f){return ArrayBuffer.isView(f)&&!(f instanceof DataView)}function hz(f,u){var _=u?u.length:0,y=f?Math.min(_,f.length):0,l=Array(y),$=Array(_),j;for(j=0;j_)if($=u.slice(_,$),J[j])J[j]+=$;else J[++j]=$;if((y=y[0])===(l=l[0]))if(J[j])J[j]+=l;else J[++j]=l;else J[++j]=null,F.push({i:j,x:Ju(y,l)});_=YJ.lastIndex}if(_180)U+=360;else if(U-A>180)A+=360;W.push({i:G.push(l(G)+"rotate(",null,y)-2,x:Ju(A,U)})}else if(U)G.push(l(G)+"rotate("+U+y)}function J(A,U,G,W){if(A!==U)W.push({i:G.push(l(G)+"skewX(",null,y)-2,x:Ju(A,U)});else if(U)G.push(l(G)+"skewX("+U+y)}function F(A,U,G,W,K,E){if(A!==G||U!==W){var H=K.push(l(K)+"scale(",null,",",null,")");E.push({i:H-4,x:Ju(A,G)},{i:H-2,x:Ju(U,W)})}else if(G!==1||W!==1)K.push(l(K)+"scale("+G+","+W+")")}return function(A,U){var G=[],W=[];return A=f(A),U=f(U),$(A.translateX,A.translateY,U.translateX,U.translateY,G,W),j(A.rotate,U.rotate,G,W),J(A.skewX,U.skewX,G,W),F(A.scaleX,A.scaleY,U.scaleX,U.scaleY,G,W),A=U=null,function(K){var E=-1,H=W.length,O;while(++E=0)f._call.call(void 0,u);f=f._next}--ol}function tz(){by=(u5=u6.now())+_5,ol=e$=0;try{az()}finally{ol=0,rY(),by=0}}function TY(){var f=u6.now(),u=f-u5;if(u>sz)_5-=u,u5=f}function rY(){var f,u=f5,_,y=1/0;while(u)if(u._call){if(y>u._time)y=u._time;f=u,u=u._next}else _=u._next,u._next=null,u=f?f._next=_:f5=_;f6=f,TJ(y)}function TJ(f){if(ol)return;if(e$)e$=clearTimeout(e$);var u=f-by;if(u>24){if(f<1/0)e$=setTimeout(tz,f-u6.now()-_5);if(d$)d$=clearInterval(d$)}else{if(!d$)u5=u6.now(),d$=setInterval(TY,sz);ol=1,oz(tz)}}function l6(f,u,_){var y=new _6;return u=u==null?0:+u,y.restart((l)=>{y.stop(),f(l+u)},u,_),y}var PY=Py("start","end","cancel","interrupt"),SY=[],fK=0,dz=1,$5=2,l5=3,ez=4,j5=5,$6=6;function __(f,u,_,y,l,$){var j=f.__transition;if(!j)f.__transition={};else if(_ in j)return;CY(f,_,{name:u,index:y,group:l,on:PY,tween:SY,time:$.time,delay:$.delay,duration:$.duration,ease:$.ease,timer:null,state:fK})}function j6(f,u){var _=x0(f,u);if(_.state>fK)throw Error("too late; already scheduled");return _}function s0(f,u){var _=x0(f,u);if(_.state>l5)throw Error("too late; already running");return _}function x0(f,u){var _=f.__transition;if(!_||!(_=_[u]))throw Error("transition not found");return _}function CY(f,u,_){var y=f.__transition,l;y[u]=_,_.timer=y5($,0,_.time);function $(A){if(_.state=dz,_.timer.restart(j,_.delay,_.time),_.delay<=A)j(A-_.delay)}function j(A){var U,G,W,K;if(_.state!==dz)return F();for(U in y){if(K=y[U],K.name!==_.name)continue;if(K.state===l5)return l6(j);if(K.state===ez)K.state=$6,K.timer.stop(),K.on.call("interrupt",f,f.__data__,K.index,K.group),delete y[U];else if(+U$5&&y.state=0)u=u.slice(0,_);return!u||u==="start"})}function fB(f,u,_){var y,l,$=eY(u)?j6:s0;return function(){var j=$(this,f),J=j.on;if(J!==y)(l=(y=J).copy()).on(u,_);j.on=l}}function IJ(f,u){var _=this._id;return arguments.length<2?x0(this.node(),_).on.on(f):this.each(fB(_,f,u))}function uB(f){return function(){var u=this.parentNode;for(var _ in this.__transition)if(+_!==f)return;if(u)u.removeChild(this)}}function cJ(){return this.on("end.remove",uB(this._id))}function pJ(f){var u=this._name,_=this._id;if(typeof f!=="function")f=c_(f);for(var y=this._groups,l=y.length,$=Array(l),j=0;j()=>f;function dJ(f,{sourceEvent:u,target:_,transform:y,dispatch:l}){Object.defineProperties(this,{type:{value:f,enumerable:!0,configurable:!0},sourceEvent:{value:u,enumerable:!0,configurable:!0},target:{value:_,enumerable:!0,configurable:!0},transform:{value:y,enumerable:!0,configurable:!0},_:{value:l}})}function H1(f,u,_){this.k=f,this.x=u,this.y=_}H1.prototype={constructor:H1,scale:function(f){return f===1?this:new H1(this.k*f,this.x,this.y)},translate:function(f,u){return f===0&u===0?this:new H1(this.k,this.x+this.k*f,this.y+this.k*u)},apply:function(f){return[f[0]*this.k+this.x,f[1]*this.k+this.y]},applyX:function(f){return f*this.k+this.x},applyY:function(f){return f*this.k+this.y},invert:function(f){return[(f[0]-this.x)/this.k,(f[1]-this.y)/this.k]},invertX:function(f){return(f-this.x)/this.k},invertY:function(f){return(f-this.y)/this.k},rescaleX:function(f){return f.copy().domain(f.range().map(this.invertX,this).map(f.invert,f))},rescaleY:function(f){return f.copy().domain(f.range().map(this.invertY,this).map(f.invert,f))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var hy=new H1(1,0,0);Q6.prototype=H1.prototype;function Q6(f){while(!f.__zoom)if(!(f=f.parentNode))return hy;return f.__zoom}function q5(f){f.stopImmediatePropagation()}function Iy(f){f.preventDefault(),f.stopImmediatePropagation()}function qB(f){return(!f.ctrlKey||f.type==="wheel")&&!f.button}function EB(){var f=this;if(f instanceof SVGElement){if(f=f.ownerSVGElement||f,f.hasAttribute("viewBox"))return f=f.viewBox.baseVal,[[f.x,f.y],[f.x+f.width,f.y+f.height]];return[[0,0],[f.width.baseVal.value,f.height.baseVal.value]]}return[[0,0],[f.clientWidth,f.clientHeight]]}function yK(){return this.__zoom||hy}function HB(f){return-f.deltaY*(f.deltaMode===1?0.05:f.deltaMode?1:0.002)*(f.ctrlKey?10:1)}function VB(){return navigator.maxTouchPoints||"ontouchstart"in this}function OB(f,u,_){var y=f.invertX(u[0][0])-_[0][0],l=f.invertX(u[1][0])-_[1][0],$=f.invertY(u[0][1])-_[0][1],j=f.invertY(u[1][1])-_[1][1];return f.translate(l>y?(y+l)/2:Math.min(0,y)||Math.max(0,l),j>$?($+j)/2:Math.min(0,$)||Math.max(0,j))}function A6(){var f=qB,u=EB,_=OB,y=HB,l=VB,$=[0,1/0],j=[[-1/0,-1/0],[1/0,1/0]],J=250,F=vy,A=Py("start","zoom","end"),U,G,W,K=500,E=150,H=0,O=10;function z(B){B.property("__zoom",yK).on("wheel.zoom",D,{passive:!1}).on("mousedown.zoom",x).on("dblclick.zoom",c).filter(l).on("touchstart.zoom",v).on("touchmove.zoom",C).on("touchend.zoom touchcancel.zoom",S).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}z.transform=function(B,P,M,w){var Y=B.selection?B.selection():B;if(Y.property("__zoom",yK),B!==Y)L(B,P,M,w);else Y.interrupt().each(function(){r(this,arguments).event(w).start().zoom(null,typeof P==="function"?P.apply(this,arguments):P).end()})},z.scaleBy=function(B,P,M,w){z.scaleTo(B,function(){var Y=this.__zoom.k,R=typeof P==="function"?P.apply(this,arguments):P;return Y*R},M,w)},z.scaleTo=function(B,P,M,w){z.transform(B,function(){var Y=u.apply(this,arguments),R=this.__zoom,k=M==null?V(Y):typeof M==="function"?M.apply(this,arguments):M,p=R.invert(k),n=typeof P==="function"?P.apply(this,arguments):P;return _(Z(q(R,n),k,p),Y,j)},M,w)},z.translateBy=function(B,P,M,w){z.transform(B,function(){return _(this.__zoom.translate(typeof P==="function"?P.apply(this,arguments):P,typeof M==="function"?M.apply(this,arguments):M),u.apply(this,arguments),j)},null,w)},z.translateTo=function(B,P,M,w,Y){z.transform(B,function(){var R=u.apply(this,arguments),k=this.__zoom,p=w==null?V(R):typeof w==="function"?w.apply(this,arguments):w;return _(hy.translate(p[0],p[1]).scale(k.k).translate(typeof P==="function"?-P.apply(this,arguments):-P,typeof M==="function"?-M.apply(this,arguments):-M),R,j)},w,Y)};function q(B,P){return P=Math.max($[0],Math.min($[1],P)),P===B.k?B:new H1(P,B.x,B.y)}function Z(B,P,M){var w=P[0]-M[0]*B.k,Y=P[1]-M[1]*B.k;return w===B.x&&Y===B.y?B:new H1(B.k,w,Y)}function V(B){return[(+B[0][0]+ +B[1][0])/2,(+B[0][1]+ +B[1][1])/2]}function L(B,P,M,w){B.on("start.zoom",function(){r(this,arguments).event(w).start()}).on("interrupt.zoom end.zoom",function(){r(this,arguments).event(w).end()}).tween("zoom",function(){var Y=this,R=arguments,k=r(Y,R).event(w),p=u.apply(Y,R),n=M==null?V(p):typeof M==="function"?M.apply(Y,R):M,_f=Math.max(p[1][0]-p[0][0],p[1][1]-p[0][1]),s=Y.__zoom,ff=typeof P==="function"?P.apply(Y,R):P,Kf=F(s.invert(n).concat(_f/s.k),ff.invert(n).concat(_f/ff.k));return function(Gf){if(Gf===1)Gf=ff;else{var jf=Kf(Gf),Wf=_f/jf[2];Gf=new H1(Wf,n[0]-jf[0]*Wf,n[1]-jf[1]*Wf)}k.zoom(null,Gf)}})}function r(B,P,M){return!M&&B.__zooming||new N(B,P)}function N(B,P){this.that=B,this.args=P,this.active=0,this.sourceEvent=null,this.extent=u.apply(B,P),this.taps=0}N.prototype={event:function(B){if(B)this.sourceEvent=B;return this},start:function(){if(++this.active===1)this.that.__zooming=this,this.emit("start");return this},zoom:function(B,P){if(this.mouse&&B!=="mouse")this.mouse[1]=P.invert(this.mouse[0]);if(this.touch0&&B!=="touch")this.touch0[1]=P.invert(this.touch0[0]);if(this.touch1&&B!=="touch")this.touch1[1]=P.invert(this.touch1[0]);return this.that.__zoom=P,this.emit("zoom"),this},end:function(){if(--this.active===0)delete this.that.__zooming,this.emit("end");return this},emit:function(B){var P=R0(this.that).datum();A.call(B,this.that,new dJ(B,{sourceEvent:this.sourceEvent,target:z,type:B,transform:this.that.__zoom,dispatch:A}),P)}};function D(B,...P){if(!f.apply(this,arguments))return;var M=r(this,P).event(B),w=this.__zoom,Y=Math.max($[0],Math.min($[1],w.k*Math.pow(2,y.apply(this,arguments)))),R=ju(B);if(M.wheel){if(M.mouse[0][0]!==R[0]||M.mouse[0][1]!==R[1])M.mouse[1]=w.invert(M.mouse[0]=R);clearTimeout(M.wheel)}else if(w.k===Y)return;else M.mouse=[R,w.invert(R)],k_(this),M.start();Iy(B),M.wheel=setTimeout(k,E),M.zoom("mouse",_(Z(q(w,Y),M.mouse[0],M.mouse[1]),M.extent,j));function k(){M.wheel=null,M.end()}}function x(B,...P){if(W||!f.apply(this,arguments))return;var M=B.currentTarget,w=r(this,P,!0).event(B),Y=R0(B.view).on("mousemove.zoom",n,!0).on("mouseup.zoom",_f,!0),R=ju(B,M),k=B.clientX,p=B.clientY;gl(B.view),q5(B),w.mouse=[R,this.__zoom.invert(R)],k_(this),w.start();function n(s){if(Iy(s),!w.moved){var ff=s.clientX-k,Kf=s.clientY-p;w.moved=ff*ff+Kf*Kf>H}w.event(s).zoom("mouse",_(Z(w.that.__zoom,w.mouse[0]=ju(s,M),w.mouse[1]),w.extent,j))}function _f(s){Y.on("mousemove.zoom mouseup.zoom",null),k$(s.view,w.moved),Iy(s),w.event(s).end()}}function c(B,...P){if(!f.apply(this,arguments))return;var M=this.__zoom,w=ju(B.changedTouches?B.changedTouches[0]:B,this),Y=M.invert(w),R=M.k*(B.shiftKey?0.5:2),k=_(Z(q(M,R),w,Y),u.apply(this,P),j);if(Iy(B),J>0)R0(this).transition().duration(J).call(L,k,w,B);else R0(this).call(z.transform,k,w,B)}function v(B,...P){if(!f.apply(this,arguments))return;var M=B.touches,w=M.length,Y=r(this,P,B.changedTouches.length===w).event(B),R,k,p,n;q5(B);for(k=0;k"[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001",error002:()=>"It looks like you've created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them.",error003:(f)=>`Node type "${f}" not found. Using fallback type "default".`,error004:()=>"The React Flow parent container needs a width and a height to render the graph.",error005:()=>"Only child nodes can use a parent extent.",error006:()=>"Can't create edge. An edge needs a source and a target.",error007:(f)=>`The old edge with id=${f} does not exist.`,error009:(f)=>`Marker type "${f}" doesn't exist.`,error008:(f,{id:u,sourceHandle:_,targetHandle:y})=>`Couldn't create edge for ${f} handle id: "${f==="source"?_:y}", edge id: ${u}.`,error010:()=>"Handle: No node id found. Make sure to only use a Handle inside a custom Node.",error011:(f)=>`Edge type "${f}" not found. Using fallback type "default".`,error012:(f)=>`Node with id "${f}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,error013:(f="react")=>`It seems that you haven't loaded the styles. Please import '@xyflow/${f}/dist/style.css' or base.css to make sure everything is working properly.`,error014:()=>"useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.",error015:()=>"It seems that you are trying to drag a node that is not initialized. Please use onNodesChange as explained in the docs."},_3=[[Number.NEGATIVE_INFINITY,Number.NEGATIVE_INFINITY],[Number.POSITIVE_INFINITY,Number.POSITIVE_INFINITY]],yF=["Enter"," ","Escape"],lF={"node.a11yDescription.default":"Press enter or space to select a node. Press delete to remove it and escape to cancel.","node.a11yDescription.keyboardDisabled":"Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.","node.a11yDescription.ariaLiveMessage":({direction:f,x:u,y:_})=>`Moved selected node ${f}. New position, x: ${u}, y: ${_}`,"edge.a11yDescription.default":"Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.","controls.ariaLabel":"Control Panel","controls.zoomIn.ariaLabel":"Zoom In","controls.zoomOut.ariaLabel":"Zoom Out","controls.fitView.ariaLabel":"Fit View","controls.interactive.ariaLabel":"Toggle Interactivity","minimap.ariaLabel":"Mini Map","handle.ariaLabel":"Handle"},g_;(function(f){f.Strict="strict",f.Loose="loose"})(g_||(g_={}));var l_;(function(f){f.Free="free",f.Vertical="vertical",f.Horizontal="horizontal"})(l_||(l_={}));var cy;(function(f){f.Partial="partial",f.Full="full"})(cy||(cy={}));var $F={inProgress:!1,isValid:null,from:null,fromHandle:null,fromPosition:null,fromNode:null,to:null,toHandle:null,toPosition:null,toNode:null,pointer:null},C1;(function(f){f.Bezier="default",f.Straight="straight",f.Step="step",f.SmoothStep="smoothstep",f.SimpleBezier="simplebezier"})(C1||(C1={}));var n_;(function(f){f.Arrow="arrow",f.ArrowClosed="arrowclosed"})(n_||(n_={}));var Uf;(function(f){f.Left="left",f.Top="top",f.Right="right",f.Bottom="bottom"})(Uf||(Uf={}));var lK={[Uf.Left]:Uf.Right,[Uf.Right]:Uf.Left,[Uf.Top]:Uf.Bottom,[Uf.Bottom]:Uf.Top};function jF(f){return f===null?null:f?"valid":"invalid"}var JF=(f)=>("id"in f)&&("source"in f)&&("target"in f),qK=(f)=>("id"in f)&&("position"in f)&&!("source"in f)&&!("target"in f),FF=(f)=>("id"in f)&&("internals"in f)&&!("source"in f)&&!("target"in f);var G6=(f,u=[0,0])=>{let{width:_,height:y}=R1(f),l=f.origin??u,$=_*l[0],j=y*l[1];return{x:f.position.x-$,y:f.position.y-j}},QF=(f,u={nodeOrigin:[0,0]})=>{if(f.length===0)return{x:0,y:0,width:0,height:0};let _=f.reduce((y,l)=>{let $=typeof l==="string",j=!u.nodeLookup&&!$?l:void 0;if(u.nodeLookup)j=$?u.nodeLookup.get(l):!FF(l)?u.nodeLookup.get(l.id):l;let J=j?V5(j,u.nodeOrigin):{x:0,y:0,x2:0,y2:0};return X5(y,J)},{x:1/0,y:1/0,x2:-1/0,y2:-1/0});return N5(_)},y3=(f,u={})=>{let _={x:1/0,y:1/0,x2:-1/0,y2:-1/0},y=!1;return f.forEach((l)=>{if(u.filter===void 0||u.filter(l))_=X5(_,V5(l)),y=!0}),y?N5(_):{x:0,y:0,width:0,height:0}},O5=(f,u,[_,y,l]=[0,0,1],$=!1,j=!1)=>{let J={...j3(u,[_,y,l]),width:u.width/l,height:u.height/l},F=[];for(let A of f.values()){let{measured:U,selectable:G=!0,hidden:W=!1}=A;if(j&&!G||W)continue;let K=U.width??A.width??A.initialWidth??null,E=U.height??A.height??A.initialHeight??null,H=l3(J,ky(A)),O=(K??0)*(E??0),z=$&&H>0;if(!A.internals.handleBounds||z||H>=O||A.dragging)F.push(A)}return F},EK=(f,u)=>{let _=new Set;return f.forEach((y)=>{_.add(y.id)}),u.filter((y)=>_.has(y.source)||_.has(y.target))};function XB(f,u){let _=new Map,y=u?.nodes?new Set(u.nodes.map((l)=>l.id)):null;return f.forEach((l)=>{if(l.measured.width&&l.measured.height&&(u?.includeHiddenNodes||!l.hidden)&&(!y||y.has(l.id)))_.set(l.id,l)}),_}async function HK({nodes:f,width:u,height:_,panZoom:y,minZoom:l,maxZoom:$},j){if(f.size===0)return Promise.resolve(!0);let J=XB(f,j),F=y3(J),A=z6(F,u,_,j?.minZoom??l,j?.maxZoom??$,j?.padding??0.1);return await y.setViewport(A,{duration:j?.duration,ease:j?.ease,interpolate:j?.interpolate}),Promise.resolve(!0)}function AF({nodeId:f,nextPosition:u,nodeLookup:_,nodeOrigin:y=[0,0],nodeExtent:l,onError:$}){let j=_.get(f),J=j.parentId?_.get(j.parentId):void 0,{x:F,y:A}=J?J.internals.positionAbsolute:{x:0,y:0},U=j.origin??y,G=j.extent||l;if(j.extent==="parent"&&!j.expandParent)if(!J)$?.("005",Iu.error005());else{let K=J.measured.width,E=J.measured.height;if(K&&E)G=[[F,A],[F+K,A+E]]}else if(J&&u3(j.extent))G=[[j.extent[0][0]+F,j.extent[0][1]+A],[j.extent[1][0]+F,j.extent[1][1]+A]];let W=u3(G)?py(u,G,j.measured):u;if(j.measured.width===void 0||j.measured.height===void 0)$?.("015",Iu.error015());return{position:{x:W.x-F+(j.measured.width??0)*U[0],y:W.y-A+(j.measured.height??0)*U[1]},positionAbsolute:W}}async function VK({nodesToRemove:f=[],edgesToRemove:u=[],nodes:_,edges:y,onBeforeDelete:l}){let $=new Set(f.map((W)=>W.id)),j=[];for(let W of _){if(W.deletable===!1)continue;let K=$.has(W.id),E=!K&&W.parentId&&j.find((H)=>H.id===W.parentId);if(K||E)j.push(W)}let J=new Set(u.map((W)=>W.id)),F=y.filter((W)=>W.deletable!==!1),U=EK(j,F);for(let W of F)if(J.has(W.id)&&!U.find((E)=>E.id===W.id))U.push(W);if(!l)return{edges:U,nodes:j};let G=await l({nodes:j,edges:U});if(typeof G==="boolean")return G?{edges:U,nodes:j}:{edges:[],nodes:[]};return G}var f3=(f,u=0,_=1)=>Math.min(Math.max(f,u),_),py=(f={x:0,y:0},u,_)=>({x:f3(f.x,u[0][0],u[1][0]-(_?.width??0)),y:f3(f.y,u[0][1],u[1][1]-(_?.height??0))});function OK(f,u,_){let{width:y,height:l}=R1(_),{x:$,y:j}=_.internals.positionAbsolute;return py(f,[[$,j],[$+y,j+l]],u)}var $K=(f,u,_)=>{if(f_)return-f3(Math.abs(f-_),1,u)/u;return 0},XK=(f,u,_=15,y=40)=>{let l=$K(f.x,y,u.width-y)*_,$=$K(f.y,y,u.height-y)*_;return[l,$]},X5=(f,u)=>({x:Math.min(f.x,u.x),y:Math.min(f.y,u.y),x2:Math.max(f.x2,u.x2),y2:Math.max(f.y2,u.y2)}),_F=({x:f,y:u,width:_,height:y})=>({x:f,y:u,x2:f+_,y2:u+y}),N5=({x:f,y:u,x2:_,y2:y})=>({x:f,y:u,width:_-f,height:y-u}),ky=(f,u=[0,0])=>{let{x:_,y}=FF(f)?f.internals.positionAbsolute:G6(f,u);return{x:_,y,width:f.measured?.width??f.width??f.initialWidth??0,height:f.measured?.height??f.height??f.initialHeight??0}},V5=(f,u=[0,0])=>{let{x:_,y}=FF(f)?f.internals.positionAbsolute:G6(f,u);return{x:_,y,x2:_+(f.measured?.width??f.width??f.initialWidth??0),y2:y+(f.measured?.height??f.height??f.initialHeight??0)}},UF=(f,u)=>N5(X5(_F(f),_F(u))),l3=(f,u)=>{let _=Math.max(0,Math.min(f.x+f.width,u.x+u.width)-Math.max(f.x,u.x)),y=Math.max(0,Math.min(f.y+f.height,u.y+u.height)-Math.max(f.y,u.y));return Math.ceil(_*y)},WF=(f)=>u1(f.width)&&u1(f.height)&&u1(f.x)&&u1(f.y),u1=(f)=>!isNaN(f)&&isFinite(f),GF=(f,u)=>{},$3=(f,u=[1,1])=>{return{x:u[0]*Math.round(f.x/u[0]),y:u[1]*Math.round(f.y/u[1])}},j3=({x:f,y:u},[_,y,l],$=!1,j=[1,1])=>{let J={x:(f-_)/l,y:(u-y)/l};return $?$3(J,j):J},W6=({x:f,y:u},[_,y,l])=>{return{x:f*l+_,y:u*l+y}};function dl(f,u){if(typeof f==="number")return Math.floor((u-u/(1+f))*0.5);if(typeof f==="string"&&f.endsWith("px")){let _=parseFloat(f);if(!Number.isNaN(_))return Math.floor(_)}if(typeof f==="string"&&f.endsWith("%")){let _=parseFloat(f);if(!Number.isNaN(_))return Math.floor(u*_*0.01)}return console.error(`[React Flow] The padding value "${f}" is invalid. Please provide a number or a string with a valid unit (px or %).`),0}function NB(f,u,_){if(typeof f==="string"||typeof f==="number"){let y=dl(f,_),l=dl(f,u);return{top:y,right:l,bottom:y,left:l,x:l*2,y:y*2}}if(typeof f==="object"){let y=dl(f.top??f.y??0,_),l=dl(f.bottom??f.y??0,_),$=dl(f.left??f.x??0,u),j=dl(f.right??f.x??0,u);return{top:y,right:j,bottom:l,left:$,x:$+j,y:y+l}}return{top:0,right:0,bottom:0,left:0,x:0,y:0}}function LB(f,u,_,y,l,$){let{x:j,y:J}=W6(f,[u,_,y]),{x:F,y:A}=W6({x:f.x+f.width,y:f.y+f.height},[u,_,y]),U=l-F,G=$-A;return{left:Math.floor(j),top:Math.floor(J),right:Math.floor(U),bottom:Math.floor(G)}}var z6=(f,u,_,y,l,$)=>{let j=NB($,u,_),J=(u-j.x)/f.width,F=(_-j.y)/f.height,A=Math.min(J,F),U=f3(A,y,l),G=f.x+f.width/2,W=f.y+f.height/2,K=u/2-G*U,E=_/2-W*U,H=LB(f,K,E,U,u,_),O={left:Math.min(H.left-j.left,0),top:Math.min(H.top-j.top,0),right:Math.min(H.right-j.right,0),bottom:Math.min(H.bottom-j.bottom,0)};return{x:K-O.left+O.right,y:E-O.top+O.bottom,zoom:U}},J3=()=>typeof navigator<"u"&&navigator?.userAgent?.indexOf("Mac")>=0;function u3(f){return f!==void 0&&f!==null&&f!=="parent"}function R1(f){return{width:f.measured?.width??f.width??f.initialWidth??0,height:f.measured?.height??f.height??f.initialHeight??0}}function zF(f){return(f.measured?.width??f.width??f.initialWidth)!==void 0&&(f.measured?.height??f.height??f.initialHeight)!==void 0}function KF(f,u={width:0,height:0},_,y,l){let $={...f},j=y.get(_);if(j){let J=j.origin||l;$.x+=j.internals.positionAbsolute.x-(u.width??0)*J[0],$.y+=j.internals.positionAbsolute.y-(u.height??0)*J[1]}return $}function ZF(f,u){if(f.size!==u.size)return!1;for(let _ of f)if(!u.has(_))return!1;return!0}function NK(){let f,u;return{promise:new Promise((y,l)=>{f=y,u=l}),resolve:f,reject:u}}function LK(f){return{...lF,...f||{}}}function U6(f,{snapGrid:u=[0,0],snapToGrid:_=!1,transform:y,containerBounds:l}){let{x:$,y:j}=_1(f),J=j3({x:$-(l?.left??0),y:j-(l?.top??0)},y),{x:F,y:A}=_?$3(J,u):J;return{xSnapped:F,ySnapped:A,...J}}var L5=(f)=>({width:f.offsetWidth,height:f.offsetHeight}),qF=(f)=>f?.getRootNode?.()||window?.document,YB=["INPUT","SELECT","TEXTAREA"];function EF(f){let u=f.composedPath?.()?.[0]||f.target;if(u?.nodeType!==1)return!1;return YB.includes(u.nodeName)||u.hasAttribute("contenteditable")||!!u.closest(".nokey")}var HF=(f)=>("clientX"in f),_1=(f,u)=>{let _=HF(f),y=_?f.clientX:f.touches?.[0].clientX,l=_?f.clientY:f.touches?.[0].clientY;return{x:y-(u?.left??0),y:l-(u?.top??0)}},jK=(f,u,_,y,l)=>{let $=u.querySelectorAll(`.${f}`);if(!$||!$.length)return null;return Array.from($).map((j)=>{let J=j.getBoundingClientRect();return{id:j.getAttribute("data-handleid"),type:f,nodeId:l,position:j.getAttribute("data-handlepos"),x:(J.left-_.left)/y,y:(J.top-_.top)/y,...L5(j)}})};function Y5({sourceX:f,sourceY:u,targetX:_,targetY:y,sourceControlX:l,sourceControlY:$,targetControlX:j,targetControlY:J}){let F=f*0.125+l*0.375+j*0.375+_*0.125,A=u*0.125+$*0.375+J*0.375+y*0.125,U=Math.abs(F-f),G=Math.abs(A-u);return[F,A,U,G]}function E5(f,u){if(f>=0)return 0.5*f;return u*25*Math.sqrt(-f)}function JK({pos:f,x1:u,y1:_,x2:y,y2:l,c:$}){switch(f){case Uf.Left:return[u-E5(u-y,$),_];case Uf.Right:return[u+E5(y-u,$),_];case Uf.Top:return[u,_-E5(_-l,$)];case Uf.Bottom:return[u,_+E5(l-_,$)]}}function B5({sourceX:f,sourceY:u,sourcePosition:_=Uf.Bottom,targetX:y,targetY:l,targetPosition:$=Uf.Top,curvature:j=0.25}){let[J,F]=JK({pos:_,x1:f,y1:u,x2:y,y2:l,c:j}),[A,U]=JK({pos:$,x1:y,y1:l,x2:f,y2:u,c:j}),[G,W,K,E]=Y5({sourceX:f,sourceY:u,targetX:y,targetY:l,sourceControlX:J,sourceControlY:F,targetControlX:A,targetControlY:U});return[`M${f},${u} C${J},${F} ${A},${U} ${y},${l}`,G,W,K,E]}function VF({sourceX:f,sourceY:u,targetX:_,targetY:y}){let l=Math.abs(_-f)/2,$=_0}var BB=({source:f,sourceHandle:u,target:_,targetHandle:y})=>`xy-edge__${f}${u||""}-${_}${y||""}`,DB=(f,u)=>{return u.some((_)=>_.source===f.source&&_.target===f.target&&(_.sourceHandle===f.sourceHandle||!_.sourceHandle&&!f.sourceHandle)&&(_.targetHandle===f.targetHandle||!_.targetHandle&&!f.targetHandle))},OF=(f,u,_={})=>{if(!f.source||!f.target)return GF("006",Iu.error006()),u;let y=_.getEdgeId||BB,l;if(JF(f))l={...f};else l={...f,id:y(f)};if(DB(l,u))return u;if(l.sourceHandle===null)delete l.sourceHandle;if(l.targetHandle===null)delete l.targetHandle;return u.concat(l)};function D5({sourceX:f,sourceY:u,targetX:_,targetY:y}){let[l,$,j,J]=VF({sourceX:f,sourceY:u,targetX:_,targetY:y});return[`M ${f},${u}L ${_},${y}`,l,$,j,J]}var FK={[Uf.Left]:{x:-1,y:0},[Uf.Right]:{x:1,y:0},[Uf.Top]:{x:0,y:-1},[Uf.Bottom]:{x:0,y:1}},wB=({source:f,sourcePosition:u=Uf.Bottom,target:_})=>{if(u===Uf.Left||u===Uf.Right)return f.x<_.x?{x:1,y:0}:{x:-1,y:0};return f.y<_.y?{x:0,y:1}:{x:0,y:-1}},QK=(f,u)=>Math.sqrt(Math.pow(u.x-f.x,2)+Math.pow(u.y-f.y,2));function TB({source:f,sourcePosition:u=Uf.Bottom,target:_,targetPosition:y=Uf.Top,center:l,offset:$,stepPosition:j}){let J=FK[u],F=FK[y],A={x:f.x+J.x*$,y:f.y+J.y*$},U={x:_.x+F.x*$,y:_.y+F.y*$},G=wB({source:A,sourcePosition:u,target:U}),W=G.x!==0?"x":"y",K=G[W],E=[],H,O,z={x:0,y:0},q={x:0,y:0},[,,Z,V]=VF({sourceX:f.x,sourceY:f.y,targetX:_.x,targetY:_.y});if(J[W]*F[W]===-1){if(W==="x")H=l.x??A.x+(U.x-A.x)*j,O=l.y??(A.y+U.y)/2;else H=l.x??(A.x+U.x)/2,O=l.y??A.y+(U.y-A.y)*j;let D=[{x:H,y:A.y},{x:H,y:U.y}],x=[{x:A.x,y:O},{x:U.x,y:O}];if(J[W]===K)E=W==="x"?D:x;else E=W==="x"?x:D}else{let D=[{x:A.x,y:U.y}],x=[{x:U.x,y:A.y}];if(W==="x")E=J.x===K?x:D;else E=J.y===K?D:x;if(u===y){let B=Math.abs(f[W]-_[W]);if(B<=$){let P=Math.min($-1,$-B);if(J[W]===K)z[W]=(A[W]>f[W]?-1:1)*P;else q[W]=(U[W]>_[W]?-1:1)*P}}if(u!==y){let B=W==="x"?"y":"x",P=J[W]===F[B],M=A[B]>U[B],w=A[B]=S)H=(c.x+v.x)/2,O=E[0].y;else H=E[0].x,O=(c.y+v.y)/2}let L={x:A.x+z.x,y:A.y+z.y},r={x:U.x+q.x,y:U.y+q.y};return[[f,...L.x!==E[0].x||L.y!==E[0].y?[L]:[],...E,...r.x!==E[E.length-1].x||r.y!==E[E.length-1].y?[r]:[],_],H,O,Z,V]}function rB(f,u,_,y){let l=Math.min(QK(f,u)/2,QK(u,_)/2,y),{x:$,y:j}=u;if(f.x===$&&$===_.x||f.y===j&&j===_.y)return`L${$} ${j}`;if(f.y===j){let A=f.x<_.x?-1:1,U=f.y<_.y?1:-1;return`L ${$+l*A},${j}Q ${$},${j} ${$},${j+l*U}`}let J=f.x<_.x?1:-1,F=f.y<_.y?-1:1;return`L ${$},${j+l*F}Q ${$},${j} ${$+l*J},${j}`}function K6({sourceX:f,sourceY:u,sourcePosition:_=Uf.Bottom,targetX:y,targetY:l,targetPosition:$=Uf.Top,borderRadius:j=5,centerX:J,centerY:F,offset:A=20,stepPosition:U=0.5}){let[G,W,K,E,H]=TB({source:{x:f,y:u},sourcePosition:_,target:{x:y,y:l},targetPosition:$,center:{x:J,y:F},offset:A,stepPosition:U}),O=`M${G[0].x} ${G[0].y}`;for(let z=1;z_.id===u))||null}function w5(f,u){if(!f)return"";if(typeof f==="string")return f;return`${u?`${u}__`:""}${Object.keys(f).sort().map((y)=>`${y}=${f[y]}`).join("&")}`}function wK(f,{id:u,defaultColor:_,defaultMarkerStart:y,defaultMarkerEnd:l}){let $=new Set;return f.reduce((j,J)=>{return[J.markerStart||y,J.markerEnd||l].forEach((F)=>{if(F&&typeof F==="object"){let A=w5(F,u);if(!$.has(A))j.push({id:A,color:F.color||_,...F}),$.add(A)}}),j},[]).sort((j,J)=>j.id.localeCompare(J.id))}var TK=1000,MB=10,XF={nodeOrigin:[0,0],nodeExtent:_3,elevateNodesOnSelect:!0,zIndexMode:"basic",defaults:{}},PB={...XF,checkEquality:!0};function NF(f,u){let _={...f};for(let y in u)if(u[y]!==void 0)_[y]=u[y];return _}function rK(f,u,_){let y=NF(XF,_);for(let l of f.values())if(l.parentId)YF(l,f,u,y);else{let $=G6(l,y.nodeOrigin),j=u3(l.extent)?l.extent:y.nodeExtent,J=py($,j,R1(l));l.internals.positionAbsolute=J}}function SB(f,u){if(!f.handles)return!f.measured?void 0:u?.internals.handleBounds;let _=[],y=[];for(let l of f.handles){let $={id:l.id,width:l.width??1,height:l.height??1,nodeId:f.id,x:l.x,y:l.y,position:l.position,type:l.type};if(l.type==="source")_.push($);else if(l.type==="target")y.push($)}return{source:_,target:y}}function LF(f){return f==="manual"}function T5(f,u,_,y={}){let l=NF(PB,y),$={i:0},j=new Map(u),J=l?.elevateNodesOnSelect&&!LF(l.zIndexMode)?TK:0,F=f.length>0,A=!1;u.clear(),_.clear();for(let U of f){let G=j.get(U.id);if(l.checkEquality&&U===G?.internals.userNode)u.set(U.id,G);else{let W=G6(U,l.nodeOrigin),K=u3(U.extent)?U.extent:l.nodeExtent,E=py(W,K,R1(U));G={...l.defaults,...U,measured:{width:U.measured?.width,height:U.measured?.height},internals:{positionAbsolute:E,handleBounds:SB(U,G),z:MK(U,J,l.zIndexMode),userNode:U}},u.set(U.id,G)}if((G.measured===void 0||G.measured.width===void 0||G.measured.height===void 0)&&!G.hidden)F=!1;if(U.parentId)YF(G,u,_,y,$);A||=U.selected??!1}return{nodesInitialized:F,hasSelectedNodes:A}}function CB(f,u){if(!f.parentId)return;let _=u.get(f.parentId);if(_)_.set(f.id,f);else u.set(f.parentId,new Map([[f.id,f]]))}function YF(f,u,_,y,l){let{elevateNodesOnSelect:$,nodeOrigin:j,nodeExtent:J,zIndexMode:F}=NF(XF,y),A=f.parentId,U=u.get(A);if(!U){console.warn(`Parent node ${A} not found. Please make sure that parent nodes are in front of their child nodes in the nodes array.`);return}if(CB(f,_),l&&!U.parentId&&U.internals.rootParentIndex===void 0&&F==="auto")U.internals.rootParentIndex=++l.i,U.internals.z=U.internals.z+l.i*MB;if(l&&U.internals.rootParentIndex!==void 0)l.i=U.internals.rootParentIndex;let G=$&&!LF(F)?TK:0,{x:W,y:K,z:E}=RB(f,U,j,J,G,F),{positionAbsolute:H}=f.internals,O=W!==H.x||K!==H.y;if(O||E!==f.internals.z)u.set(f.id,{...f,internals:{...f.internals,positionAbsolute:O?{x:W,y:K}:H,z:E}})}function MK(f,u,_){let y=u1(f.zIndex)?f.zIndex:0;if(LF(_))return y;return y+(f.selected?u:0)}function RB(f,u,_,y,l,$){let{x:j,y:J}=u.internals.positionAbsolute,F=R1(f),A=G6(f,_),U=u3(f.extent)?py(A,f.extent,F):A,G=py({x:j+U.x,y:J+U.y},y,F);if(f.extent==="parent")G=OK(G,F,u);let W=MK(f,l,$),K=u.internals.z??0;return{x:G.x,y:G.y,z:K>=W?K+1:W}}function r5(f,u,_,y=[0,0]){let l=[],$=new Map;for(let j of f){let J=u.get(j.parentId);if(!J)continue;let F=$.get(j.parentId)?.expandedRect??ky(J),A=UF(F,j.rect);$.set(j.parentId,{expandedRect:A,parent:J})}if($.size>0)$.forEach(({expandedRect:j,parent:J},F)=>{let A=J.internals.positionAbsolute,U=R1(J),G=J.origin??y,W=j.x0||K>0||O||z)l.push({id:F,type:"position",position:{x:J.position.x-W+O,y:J.position.y-K+z}}),_.get(F)?.forEach((q)=>{if(!f.some((Z)=>Z.id===q.id))l.push({id:q.id,type:"position",position:{x:q.position.x+W,y:q.position.y+K}})});if(U.width0){let K=r5(W,u,_,l);A.push(...K)}return{changes:A,updatedInternals:F}}async function SK({delta:f,panZoom:u,transform:_,translateExtent:y,width:l,height:$}){if(!u||!f.x&&!f.y)return Promise.resolve(!1);let j=await u.setViewportConstrained({x:_[0]+f.x,y:_[1]+f.y,zoom:_[2]},[[0,0],[l,$]],y),J=!!j&&(j.x!==_[0]||j.y!==_[1]||j.k!==_[2]);return Promise.resolve(J)}function GK(f,u,_,y,l,$){let j=l,J=y.get(j)||new Map;y.set(j,J.set(_,u)),j=`${l}-${f}`;let F=y.get(j)||new Map;if(y.set(j,F.set(_,u)),$){j=`${l}-${f}-${$}`;let A=y.get(j)||new Map;y.set(j,A.set(_,u))}}function BF(f,u,_){f.clear(),u.clear();for(let y of _){let{source:l,target:$,sourceHandle:j=null,targetHandle:J=null}=y,F={edgeId:y.id,source:l,target:$,sourceHandle:j,targetHandle:J},A=`${l}-${j}--${$}-${J}`,U=`${$}-${J}--${l}-${j}`;GK("source",F,U,f,l,j),GK("target",F,A,f,$,J),u.set(y.id,y)}}function CK(f,u){if(!f.parentId)return!1;let _=u.get(f.parentId);if(!_)return!1;if(_.selected)return!0;return CK(_,u)}function zK(f,u,_){let y=f;do{if(y?.matches?.(u))return!0;if(y===_)return!1;y=y?.parentElement}while(y);return!1}function xB(f,u,_,y){let l=new Map;for(let[$,j]of f)if((j.selected||j.id===y)&&(!j.parentId||!CK(j,f))&&(j.draggable||u&&typeof j.draggable>"u")){let J=f.get($);if(J)l.set($,{id:$,position:J.position||{x:0,y:0},distance:{x:_.x-J.internals.positionAbsolute.x,y:_.y-J.internals.positionAbsolute.y},extent:J.extent,parentId:J.parentId,origin:J.origin,expandParent:J.expandParent,internals:{positionAbsolute:J.internals.positionAbsolute||{x:0,y:0}},measured:{width:J.measured.width??0,height:J.measured.height??0}})}return l}function eJ({nodeId:f,dragItems:u,nodeLookup:_,dragging:y=!0}){let l=[];for(let[j,J]of u){let F=_.get(j)?.internals.userNode;if(F)l.push({...F,position:J.position,dragging:y})}if(!f)return[l[0],l];let $=_.get(f)?.internals.userNode;return[!$?l[0]:{...$,position:u.get(f)?.position||$.position,dragging:y},l]}function vB({dragItems:f,snapGrid:u,x:_,y}){let l=f.values().next().value;if(!l)return null;let $={x:_-l.distance.x,y:y-l.distance.y},j=$3($,u);return{x:j.x-$.x,y:j.y-$.y}}function RK({onNodeMouseDown:f,getStoreItems:u,onDragStart:_,onDrag:y,onDragStop:l}){let $={x:null,y:null},j=0,J=new Map,F=!1,A={x:0,y:0},U=null,G=!1,W=null,K=!1,E=!1,H=null;function O({noDragClassName:q,handleSelector:Z,domNode:V,isSelectable:L,nodeId:r,nodeClickDistance:N=0}){W=R0(V);function D({x:C,y:S}){let{nodeLookup:B,nodeExtent:P,snapGrid:M,snapToGrid:w,nodeOrigin:Y,onNodeDrag:R,onSelectionDrag:k,onError:p,updateNodePositions:n}=u();$={x:C,y:S};let _f=!1,s=J.size>1,ff=s&&P?_F(y3(J)):null,Kf=s&&w?vB({dragItems:J,snapGrid:M,x:C,y:S}):null;for(let[Gf,jf]of J){if(!B.has(Gf))continue;let Wf={x:C-jf.distance.x,y:S-jf.distance.y};if(w)Wf=Kf?{x:Math.round(Wf.x+Kf.x),y:Math.round(Wf.y+Kf.y)}:$3(Wf,M);let Of=null;if(s&&P&&!jf.extent&&ff){let{positionAbsolute:i}=jf.internals,I=i.x-ff.x+P[0][0],lf=i.x+jf.measured.width-ff.x2+P[1][0],$f=i.y-ff.y+P[0][1],Af=i.y+jf.measured.height-ff.y2+P[1][1];Of=[[I,$f],[lf,Af]]}let{position:Zf,positionAbsolute:h}=AF({nodeId:Gf,nextPosition:Wf,nodeLookup:B,nodeExtent:Of?Of:P,nodeOrigin:Y,onError:p});_f=_f||jf.position.x!==Zf.x||jf.position.y!==Zf.y,jf.position=Zf,jf.internals.positionAbsolute=h}if(E=E||_f,!_f)return;if(n(J,!0),H&&(y||R||!r&&k)){let[Gf,jf]=eJ({nodeId:r,dragItems:J,nodeLookup:B});if(y?.(H,J,Gf,jf),R?.(H,Gf,jf),!r)k?.(H,jf)}}async function x(){if(!U)return;let{transform:C,panBy:S,autoPanSpeed:B,autoPanOnNodeDrag:P}=u();if(!P){F=!1,cancelAnimationFrame(j);return}let[M,w]=XK(A,U,B);if(M!==0||w!==0){if($.x=($.x??0)-M/C[2],$.y=($.y??0)-w/C[2],await S({x:M,y:w}))D($)}j=requestAnimationFrame(x)}function c(C){let{nodeLookup:S,multiSelectionActive:B,nodesDraggable:P,transform:M,snapGrid:w,snapToGrid:Y,selectNodesOnDrag:R,onNodeDragStart:k,onSelectionDragStart:p,unselectNodesAndEdges:n}=u();if(G=!0,(!R||!L)&&!B&&r){if(!S.get(r)?.selected)n()}if(L&&R&&r)f?.(r);let _f=U6(C.sourceEvent,{transform:M,snapGrid:w,snapToGrid:Y,containerBounds:U});if($=_f,J=xB(S,P,_f,r),J.size>0&&(_||k||!r&&p)){let[s,ff]=eJ({nodeId:r,dragItems:J,nodeLookup:S});if(_?.(C.sourceEvent,J,s,ff),k?.(C.sourceEvent,s,ff),!r)p?.(C.sourceEvent,ff)}}let v=g$().clickDistance(N).on("start",(C)=>{let{domNode:S,nodeDragThreshold:B,transform:P,snapGrid:M,snapToGrid:w}=u();if(U=S?.getBoundingClientRect()||null,K=!1,E=!1,H=C.sourceEvent,B===0)c(C);$=U6(C.sourceEvent,{transform:P,snapGrid:M,snapToGrid:w,containerBounds:U}),A=_1(C.sourceEvent,U)}).on("drag",(C)=>{let{autoPanOnNodeDrag:S,transform:B,snapGrid:P,snapToGrid:M,nodeDragThreshold:w,nodeLookup:Y}=u(),R=U6(C.sourceEvent,{transform:B,snapGrid:P,snapToGrid:M,containerBounds:U});if(H=C.sourceEvent,C.sourceEvent.type==="touchmove"&&C.sourceEvent.touches.length>1||r&&!Y.has(r))K=!0;if(K)return;if(!F&&S&&G)F=!0,x();if(!G){let k=_1(C.sourceEvent,U),p=k.x-A.x,n=k.y-A.y;if(Math.sqrt(p*p+n*n)>w)c(C)}if(($.x!==R.xSnapped||$.y!==R.ySnapped)&&J&&G)A=_1(C.sourceEvent,U),D(R)}).on("end",(C)=>{if(!G||K)return;if(F=!1,G=!1,cancelAnimationFrame(j),J.size>0){let{nodeLookup:S,updateNodePositions:B,onNodeDragStop:P,onSelectionDragStop:M}=u();if(E)B(J,!1),E=!1;if(l||P||!r&&M){let[w,Y]=eJ({nodeId:r,dragItems:J,nodeLookup:S,dragging:!1});if(l?.(C.sourceEvent,J,w,Y),P?.(C.sourceEvent,w,Y),!r)M?.(C.sourceEvent,Y)}}}).filter((C)=>{let S=C.target;return!C.button&&(!q||!zK(S,`.${q}`,V))&&(!Z||zK(S,Z,V))});W.call(v)}function z(){W?.on(".drag",null)}return{update:O,destroy:z}}function bB(f,u,_){let y=[],l={x:f.x-_,y:f.y-_,width:_*2,height:_*2};for(let $ of u.values())if(l3(l,ky($))>0)y.push($);return y}var hB=250;function IB(f,u,_,y){let l=[],$=1/0,j=bB(f,_,u+hB);for(let J of j){let F=[...J.internals.handleBounds?.source??[],...J.internals.handleBounds?.target??[]];for(let A of F){if(y.nodeId===A.nodeId&&y.type===A.type&&y.id===A.id)continue;let{x:U,y:G}=t_(J,A,A.position,!0),W=Math.sqrt(Math.pow(U-f.x,2)+Math.pow(G-f.y,2));if(W>u)continue;if(W<$)l=[{...A,x:U,y:G}],$=W;else if(W===$)l.push({...A,x:U,y:G})}}if(!l.length)return null;if(l.length>1){let J=y.type==="source"?"target":"source";return l.find((F)=>F.type===J)??l[0]}return l[0]}function xK(f,u,_,y,l,$=!1){let j=y.get(f);if(!j)return null;let J=l==="strict"?j.internals.handleBounds?.[u]:[...j.internals.handleBounds?.source??[],...j.internals.handleBounds?.target??[]],F=(_?J?.find((A)=>A.id===_):J?.[0])??null;return F&&$?{...F,...t_(j,F,F.position,!0)}:F}function vK(f,u){if(f)return f;else if(u?.classList.contains("target"))return"target";else if(u?.classList.contains("source"))return"source";return null}function cB(f,u){let _=null;if(u)_=!0;else if(f&&!u)_=!1;return _}var bK=()=>!0;function pB(f,{connectionMode:u,connectionRadius:_,handleId:y,nodeId:l,edgeUpdaterType:$,isTarget:j,domNode:J,nodeLookup:F,lib:A,autoPanOnConnect:U,flowId:G,panBy:W,cancelConnection:K,onConnectStart:E,onConnect:H,onConnectEnd:O,isValidConnection:z=bK,onReconnectEnd:q,updateConnection:Z,getTransform:V,getFromHandle:L,autoPanSpeed:r,dragThreshold:N=1,handleDomNode:D}){let x=qF(f.target),c=0,v,{x:C,y:S}=_1(f),B=vK($,D),P=J?.getBoundingClientRect(),M=!1;if(!P||!B)return;let w=xK(l,B,y,F,u);if(!w)return;let Y=_1(f,P),R=!1,k=null,p=!1,n=null;function _f(){if(!U||!P)return;let[Zf,h]=XK(Y,P,r);W({x:Zf,y:h}),c=requestAnimationFrame(_f)}let s={...w,nodeId:l,type:B,position:w.position},ff=F.get(l),Gf={inProgress:!0,isValid:null,from:t_(ff,s,Uf.Left,!0),fromHandle:s,fromPosition:s.position,fromNode:ff,to:Y,toHandle:null,toPosition:lK[s.position],toNode:null,pointer:Y};function jf(){M=!0,Z(Gf),E?.(f,{nodeId:l,handleId:y,handleType:B})}if(N===0)jf();function Wf(Zf){if(!M){let{x:Af,y:Yf}=_1(Zf),xf=Af-C,of=Yf-S;if(!(xf*xf+of*of>N*N))return;jf()}if(!L()||!s){Of(Zf);return}let h=V();if(Y=_1(Zf,P),v=IB(j3(Y,h,!1,[1,1]),_,F,s),!R)_f(),R=!0;let i=hK(Zf,{handle:v,connectionMode:u,fromNodeId:l,fromHandleId:y,fromType:j?"target":"source",isValidConnection:z,doc:x,lib:A,flowId:G,nodeLookup:F});n=i.handleDomNode,k=i.connection,p=cB(!!v,i.isValid);let I=F.get(l),lf=I?t_(I,s,Uf.Left,!0):Gf.from,$f={...Gf,from:lf,isValid:p,to:i.toHandle&&p?W6({x:i.toHandle.x,y:i.toHandle.y},h):Y,toHandle:i.toHandle,toPosition:p&&i.toHandle?i.toHandle.position:lK[s.position],toNode:i.toHandle?F.get(i.toHandle.nodeId):null,pointer:Y};Z($f),Gf=$f}function Of(Zf){if("touches"in Zf&&Zf.touches.length>0)return;if(M){if((v||n)&&k&&p)H?.(k);let{inProgress:h,...i}=Gf,I={...i,toPosition:Gf.toHandle?Gf.toPosition:null};if(O?.(Zf,I),$)q?.(Zf,I)}K(),cancelAnimationFrame(c),R=!1,p=!1,k=null,n=null,x.removeEventListener("mousemove",Wf),x.removeEventListener("mouseup",Of),x.removeEventListener("touchmove",Wf),x.removeEventListener("touchend",Of)}x.addEventListener("mousemove",Wf),x.addEventListener("mouseup",Of),x.addEventListener("touchmove",Wf),x.addEventListener("touchend",Of)}function hK(f,{handle:u,connectionMode:_,fromNodeId:y,fromHandleId:l,fromType:$,doc:j,lib:J,flowId:F,isValidConnection:A=bK,nodeLookup:U}){let G=$==="target",W=u?j.querySelector(`.${J}-flow__handle[data-id="${F}-${u?.nodeId}-${u?.id}-${u?.type}"]`):null,{x:K,y:E}=_1(f),H=j.elementFromPoint(K,E),O=H?.classList.contains(`${J}-flow__handle`)?H:W,z={handleDomNode:O,isValid:!1,connection:null,toHandle:null};if(O){let q=vK(void 0,O),Z=O.getAttribute("data-nodeid"),V=O.getAttribute("data-handleid"),L=O.classList.contains("connectable"),r=O.classList.contains("connectableend");if(!Z||!q)return z;let N={source:G?Z:y,sourceHandle:G?V:l,target:G?y:Z,targetHandle:G?l:V};z.connection=N;let x=L&&r&&(_===g_.Strict?G&&q==="source"||!G&&q==="target":Z!==y||V!==l);z.isValid=x&&A(N),z.toHandle=xK(Z,q,V,U,_,!0)}return z}var M5={onPointerDown:pB,isValid:hK};function IK({domNode:f,panZoom:u,getTransform:_,getViewScale:y}){let l=R0(f);function $({translateExtent:J,width:F,height:A,zoomStep:U=1,pannable:G=!0,zoomable:W=!0,inversePan:K=!1}){let E=(Z)=>{if(Z.sourceEvent.type!=="wheel"||!u)return;let V=_(),L=Z.sourceEvent.ctrlKey&&J3()?10:1,r=-Z.sourceEvent.deltaY*(Z.sourceEvent.deltaMode===1?0.05:Z.sourceEvent.deltaMode?1:0.002)*U,N=V[2]*Math.pow(2,r*L);u.scaleTo(N)},H=[0,0],O=(Z)=>{if(Z.sourceEvent.type==="mousedown"||Z.sourceEvent.type==="touchstart")H=[Z.sourceEvent.clientX??Z.sourceEvent.touches[0].clientX,Z.sourceEvent.clientY??Z.sourceEvent.touches[0].clientY]},z=(Z)=>{let V=_();if(Z.sourceEvent.type!=="mousemove"&&Z.sourceEvent.type!=="touchmove"||!u)return;let L=[Z.sourceEvent.clientX??Z.sourceEvent.touches[0].clientX,Z.sourceEvent.clientY??Z.sourceEvent.touches[0].clientY],r=[L[0]-H[0],L[1]-H[1]];H=L;let N=y()*Math.max(V[2],Math.log(V[2]))*(K?-1:1),D={x:V[0]-r[0]*N,y:V[1]-r[1]*N},x=[[0,0],[F,A]];u.setViewportConstrained({x:D.x,y:D.y,zoom:V[2]},x,J)},q=A6().on("start",O).on("zoom",G?z:null).on("zoom.wheel",W?E:null);l.call(q,{})}function j(){l.on("zoom",null)}return{update:$,destroy:j,pointer:ju}}var P5=(f)=>({x:f.x,y:f.y,zoom:f.k}),fF=({x:f,y:u,zoom:_})=>hy.translate(f,u).scale(_),el=(f,u)=>f.target.closest(`.${u}`),cK=(f,u)=>u===2&&Array.isArray(f)&&f.includes(2),kB=(f)=>((f*=2)<=1?f*f*f:(f-=2)*f*f+2)/2,uF=(f,u=0,_=kB,y=()=>{})=>{let l=typeof u==="number"&&u>0;if(!l)y();return l?f.transition().duration(u).ease(_).on("end",y):f},pK=(f)=>{let u=f.ctrlKey&&J3()?10:1;return-f.deltaY*(f.deltaMode===1?0.05:f.deltaMode?1:0.002)*u};function mB({zoomPanValues:f,noWheelClassName:u,d3Selection:_,d3Zoom:y,panOnScrollMode:l,panOnScrollSpeed:$,zoomOnPinch:j,onPanZoomStart:J,onPanZoom:F,onPanZoomEnd:A}){return(U)=>{if(el(U,u)){if(U.ctrlKey)U.preventDefault();return!1}U.preventDefault(),U.stopImmediatePropagation();let G=_.property("__zoom").k||1;if(U.ctrlKey&&j){let O=ju(U),z=pK(U),q=G*Math.pow(2,z);y.scaleTo(_,q,O,U);return}let W=U.deltaMode===1?20:1,K=l===l_.Vertical?0:U.deltaX*W,E=l===l_.Horizontal?0:U.deltaY*W;if(!J3()&&U.shiftKey&&l!==l_.Vertical)K=U.deltaY*W,E=0;y.translateBy(_,-(K/G)*$,-(E/G)*$,{internal:!0});let H=P5(_.property("__zoom"));if(clearTimeout(f.panScrollTimeout),!f.isPanScrolling)f.isPanScrolling=!0,J?.(U,H);else F?.(U,H),f.panScrollTimeout=setTimeout(()=>{A?.(U,H),f.isPanScrolling=!1},150)}}function iB({noWheelClassName:f,preventScrolling:u,d3ZoomHandler:_}){return function(y,l){let $=y.type==="wheel",j=!u&&$&&!y.ctrlKey,J=el(y,f);if(y.ctrlKey&&$&&J)y.preventDefault();if(j||J)return null;y.preventDefault(),_.call(this,y,l)}}function gB({zoomPanValues:f,onDraggingChange:u,onPanZoomStart:_}){return(y)=>{if(y.sourceEvent?.internal)return;let l=P5(y.transform);if(f.mouseButton=y.sourceEvent?.button||0,f.isZoomingOrPanning=!0,f.prevViewport=l,y.sourceEvent?.type==="mousedown")u(!0);if(_)_?.(y.sourceEvent,l)}}function nB({zoomPanValues:f,panOnDrag:u,onPaneContextMenu:_,onTransformChange:y,onPanZoom:l}){return($)=>{if(f.usedRightMouseButton=!!(_&&cK(u,f.mouseButton??0)),!$.sourceEvent?.sync)y([$.transform.x,$.transform.y,$.transform.k]);if(l&&!$.sourceEvent?.internal)l?.($.sourceEvent,P5($.transform))}}function tB({zoomPanValues:f,panOnDrag:u,panOnScroll:_,onDraggingChange:y,onPanZoomEnd:l,onPaneContextMenu:$}){return(j)=>{if(j.sourceEvent?.internal)return;if(f.isZoomingOrPanning=!1,$&&cK(u,f.mouseButton??0)&&!f.usedRightMouseButton&&j.sourceEvent)$(j.sourceEvent);if(f.usedRightMouseButton=!1,y(!1),l){let J=P5(j.transform);f.prevViewport=J,clearTimeout(f.timerId),f.timerId=setTimeout(()=>{l?.(j.sourceEvent,J)},_?150:0)}}}function sB({zoomActivationKeyPressed:f,zoomOnScroll:u,zoomOnPinch:_,panOnDrag:y,panOnScroll:l,zoomOnDoubleClick:$,userSelectionActive:j,noWheelClassName:J,noPanClassName:F,lib:A,connectionInProgress:U}){return(G)=>{let W=f||u,K=_&&G.ctrlKey,E=G.type==="wheel";if(G.button===1&&G.type==="mousedown"&&(el(G,`${A}-flow__node`)||el(G,`${A}-flow__edge`)))return!0;if(!y&&!W&&!l&&!$&&!_)return!1;if(j)return!1;if(U&&!E)return!1;if(el(G,J)&&E)return!1;if(el(G,F)&&(!E||l&&E&&!f))return!1;if(!_&&G.ctrlKey&&E)return!1;if(!_&&G.type==="touchstart"&&G.touches?.length>1)return G.preventDefault(),!1;if(!W&&!l&&!K&&E)return!1;if(!y&&(G.type==="mousedown"||G.type==="touchstart"))return!1;if(Array.isArray(y)&&!y.includes(G.button)&&G.type==="mousedown")return!1;let H=Array.isArray(y)&&y.includes(G.button)||!G.button||G.button<=1;return(!G.ctrlKey||E)&&H}}function kK({domNode:f,minZoom:u,maxZoom:_,translateExtent:y,viewport:l,onPanZoom:$,onPanZoomStart:j,onPanZoomEnd:J,onDraggingChange:F}){let A={isZoomingOrPanning:!1,usedRightMouseButton:!1,prevViewport:{x:0,y:0,zoom:0},mouseButton:0,timerId:void 0,panScrollTimeout:void 0,isPanScrolling:!1},U=f.getBoundingClientRect(),G=A6().scaleExtent([u,_]).translateExtent(y),W=R0(f).call(G);q({x:l.x,y:l.y,zoom:f3(l.zoom,u,_)},[[0,0],[U.width,U.height]],y);let K=W.on("wheel.zoom"),E=W.on("dblclick.zoom");G.wheelDelta(pK);function H(v,C){if(W)return new Promise((S)=>{G?.interpolate(C?.interpolate==="linear"?S1:vy).transform(uF(W,C?.duration,C?.ease,()=>S(!0)),v)});return Promise.resolve(!1)}function O({noWheelClassName:v,noPanClassName:C,onPaneContextMenu:S,userSelectionActive:B,panOnScroll:P,panOnDrag:M,panOnScrollMode:w,panOnScrollSpeed:Y,preventScrolling:R,zoomOnPinch:k,zoomOnScroll:p,zoomOnDoubleClick:n,zoomActivationKeyPressed:_f,lib:s,onTransformChange:ff,connectionInProgress:Kf,paneClickDistance:Gf,selectionOnDrag:jf}){if(B&&!A.isZoomingOrPanning)z();let Wf=P&&!_f&&!B;G.clickDistance(jf?1/0:!u1(Gf)||Gf<0?0:Gf);let Of=Wf?mB({zoomPanValues:A,noWheelClassName:v,d3Selection:W,d3Zoom:G,panOnScrollMode:w,panOnScrollSpeed:Y,zoomOnPinch:k,onPanZoomStart:j,onPanZoom:$,onPanZoomEnd:J}):iB({noWheelClassName:v,preventScrolling:R,d3ZoomHandler:K});if(W.on("wheel.zoom",Of,{passive:!1}),!B){let h=gB({zoomPanValues:A,onDraggingChange:F,onPanZoomStart:j});G.on("start",h);let i=nB({zoomPanValues:A,panOnDrag:M,onPaneContextMenu:!!S,onPanZoom:$,onTransformChange:ff});G.on("zoom",i);let I=tB({zoomPanValues:A,panOnDrag:M,panOnScroll:P,onPaneContextMenu:S,onPanZoomEnd:J,onDraggingChange:F});G.on("end",I)}let Zf=sB({zoomActivationKeyPressed:_f,panOnDrag:M,zoomOnScroll:p,panOnScroll:P,zoomOnDoubleClick:n,zoomOnPinch:k,userSelectionActive:B,noPanClassName:C,noWheelClassName:v,lib:s,connectionInProgress:Kf});if(G.filter(Zf),n)W.on("dblclick.zoom",E);else W.on("dblclick.zoom",null)}function z(){G.on("zoom",null)}async function q(v,C,S){let B=fF(v),P=G?.constrain()(B,C,S);if(P)await H(P);return new Promise((M)=>M(P))}async function Z(v,C){let S=fF(v);return await H(S,C),new Promise((B)=>B(S))}function V(v){if(W){let C=fF(v),S=W.property("__zoom");if(S.k!==v.zoom||S.x!==v.x||S.y!==v.y)G?.transform(W,C,null,{sync:!0})}}function L(){let v=W?Q6(W.node()):{x:0,y:0,k:1};return{x:v.x,y:v.y,zoom:v.k}}function r(v,C){if(W)return new Promise((S)=>{G?.interpolate(C?.interpolate==="linear"?S1:vy).scaleTo(uF(W,C?.duration,C?.ease,()=>S(!0)),v)});return Promise.resolve(!1)}function N(v,C){if(W)return new Promise((S)=>{G?.interpolate(C?.interpolate==="linear"?S1:vy).scaleBy(uF(W,C?.duration,C?.ease,()=>S(!0)),v)});return Promise.resolve(!1)}function D(v){G?.scaleExtent(v)}function x(v){G?.translateExtent(v)}function c(v){let C=!u1(v)||v<0?0:v;G?.clickDistance(C)}return{update:O,destroy:z,setViewport:Z,setViewportConstrained:q,getViewport:L,scaleTo:r,scaleBy:N,setScaleExtent:D,setTranslateExtent:x,syncViewport:V,setClickDistance:c}}var s_;(function(f){f.Line="line",f.Handle="handle"})(s_||(s_={}));function oB({width:f,prevWidth:u,height:_,prevHeight:y,affectsX:l,affectsY:$}){let j=f-u,J=_-y,F=[j>0?1:j<0?-1:0,J>0?1:J<0?-1:0];if(j&&l)F[0]=F[0]*-1;if(J&&$)F[1]=F[1]*-1;return F}function KK(f){let u=f.includes("right")||f.includes("left"),_=f.includes("bottom")||f.includes("top"),y=f.includes("left"),l=f.includes("top");return{isHorizontal:u,isVertical:_,affectsX:y,affectsY:l}}function m_(f,u){return Math.max(0,u-f)}function i_(f,u){return Math.max(0,f-u)}function H5(f,u,_){return Math.max(0,u-f,f-_)}function ZK(f,u){return f?!u:u}function aB(f,u,_,y,l,$,j,J){let{affectsX:F,affectsY:A}=u,{isHorizontal:U,isVertical:G}=u,W=U&&G,{xSnapped:K,ySnapped:E}=_,{minWidth:H,maxWidth:O,minHeight:z,maxHeight:q}=y,{x:Z,y:V,width:L,height:r,aspectRatio:N}=f,D=Math.floor(U?K-f.pointerX:0),x=Math.floor(G?E-f.pointerY:0),c=L+(F?-D:D),v=r+(A?-x:x),C=-$[0]*L,S=-$[1]*r,B=H5(c,H,O),P=H5(v,z,q);if(j){let Y=0,R=0;if(F&&D<0)Y=m_(Z+D+C,j[0][0]);else if(!F&&D>0)Y=i_(Z+c+C,j[1][0]);if(A&&x<0)R=m_(V+x+S,j[0][1]);else if(!A&&x>0)R=i_(V+v+S,j[1][1]);B=Math.max(B,Y),P=Math.max(P,R)}if(J){let Y=0,R=0;if(F&&D>0)Y=i_(Z+D,J[0][0]);else if(!F&&D<0)Y=m_(Z+c,J[1][0]);if(A&&x>0)R=i_(V+x,J[0][1]);else if(!A&&x<0)R=m_(V+v,J[1][1]);B=Math.max(B,Y),P=Math.max(P,R)}if(l){if(U){let Y=H5(c/N,z,q)*N;if(B=Math.max(B,Y),j){let R=0;if(!F&&!A||F&&!A&&W)R=i_(V+S+c/N,j[1][1])*N;else R=m_(V+S+(F?D:-D)/N,j[0][1])*N;B=Math.max(B,R)}if(J){let R=0;if(!F&&!A||F&&!A&&W)R=m_(V+c/N,J[1][1])*N;else R=i_(V+(F?D:-D)/N,J[0][1])*N;B=Math.max(B,R)}}if(G){let Y=H5(v*N,H,O)/N;if(P=Math.max(P,Y),j){let R=0;if(!F&&!A||A&&!F&&W)R=i_(Z+v*N+C,j[1][0])/N;else R=m_(Z+(A?x:-x)*N+C,j[0][0])/N;P=Math.max(P,R)}if(J){let R=0;if(!F&&!A||A&&!F&&W)R=m_(Z+v*N,J[1][0])/N;else R=i_(Z+(A?x:-x)*N,J[0][0])/N;P=Math.max(P,R)}}}if(x=x+(x<0?P:-P),D=D+(D<0?B:-B),l)if(W)if(c>v*N)x=(ZK(F,A)?-D:D)/N;else D=(ZK(F,A)?-x:x)*N;else if(U)x=D/N,A=F;else D=x*N,F=A;let M=F?Z+D:Z,w=A?V+x:V;return{width:L+(F?-D:D),height:r+(A?-x:x),x:$[0]*D*(!F?1:-1)+M,y:$[1]*x*(!A?1:-1)+w}}var mK={width:0,height:0,x:0,y:0},dB={...mK,pointerX:0,pointerY:0,aspectRatio:1};function eB(f){return[[0,0],[f.measured.width,f.measured.height]]}function fD(f,u,_){let y=u.position.x+f.position.x,l=u.position.y+f.position.y,$=f.measured.width??0,j=f.measured.height??0,J=_[0]*$,F=_[1]*j;return[[y-J,l-F],[y+$-J,l+j-F]]}function iK({domNode:f,nodeId:u,getStoreItems:_,onChange:y,onEnd:l}){let $=R0(f),j={controlDirection:KK("bottom-right"),boundaries:{minWidth:0,minHeight:0,maxWidth:Number.MAX_VALUE,maxHeight:Number.MAX_VALUE},resizeDirection:void 0,keepAspectRatio:!1};function J({controlPosition:A,boundaries:U,keepAspectRatio:G,resizeDirection:W,onResizeStart:K,onResize:E,onResizeEnd:H,shouldResize:O}){let z={...mK},q={...dB};j={boundaries:U,resizeDirection:W,keepAspectRatio:G,controlDirection:KK(A)};let Z=void 0,V=null,L=[],r=void 0,N=void 0,D=void 0,x=!1,c=g$().on("start",(v)=>{let{nodeLookup:C,transform:S,snapGrid:B,snapToGrid:P,nodeOrigin:M,paneDomNode:w}=_();if(Z=C.get(u),!Z)return;V=w?.getBoundingClientRect()??null;let{xSnapped:Y,ySnapped:R}=U6(v.sourceEvent,{transform:S,snapGrid:B,snapToGrid:P,containerBounds:V});if(z={width:Z.measured.width??0,height:Z.measured.height??0,x:Z.position.x??0,y:Z.position.y??0},q={...z,pointerX:Y,pointerY:R,aspectRatio:z.width/z.height},r=void 0,Z.parentId&&(Z.extent==="parent"||Z.expandParent))r=C.get(Z.parentId),N=r&&Z.extent==="parent"?eB(r):void 0;L=[],D=void 0;for(let[k,p]of C)if(p.parentId===u){if(L.push({id:k,position:{...p.position},extent:p.extent}),p.extent==="parent"||p.expandParent){let n=fD(p,Z,p.origin??M);if(D)D=[[Math.min(n[0][0],D[0][0]),Math.min(n[0][1],D[0][1])],[Math.max(n[1][0],D[1][0]),Math.max(n[1][1],D[1][1])]];else D=n}}K?.(v,{...z})}).on("drag",(v)=>{let{transform:C,snapGrid:S,snapToGrid:B,nodeOrigin:P}=_(),M=U6(v.sourceEvent,{transform:C,snapGrid:S,snapToGrid:B,containerBounds:V}),w=[];if(!Z)return;let{x:Y,y:R,width:k,height:p}=z,n={},_f=Z.origin??P,{width:s,height:ff,x:Kf,y:Gf}=aB(q,j.controlDirection,M,j.boundaries,j.keepAspectRatio,_f,N,D),jf=s!==k,Wf=ff!==p,Of=Kf!==Y&&jf,Zf=Gf!==R&&Wf;if(!Of&&!Zf&&!jf&&!Wf)return;if(Of||Zf||_f[0]===1||_f[1]===1){if(n.x=Of?Kf:z.x,n.y=Zf?Gf:z.y,z.x=n.x,z.y=n.y,L.length>0){let lf=Kf-Y,$f=Gf-R;for(let Af of L)Af.position={x:Af.position.x-lf+_f[0]*(s-k),y:Af.position.y-$f+_f[1]*(ff-p)},w.push(Af)}}if(jf||Wf)n.width=jf&&(!j.resizeDirection||j.resizeDirection==="horizontal")?s:z.width,n.height=Wf&&(!j.resizeDirection||j.resizeDirection==="vertical")?ff:z.height,z.width=n.width,z.height=n.height;if(r&&Z.expandParent){let lf=_f[0]*(n.width??0);if(n.x&&n.x{if(!x)return;H?.(v,{...z}),l?.({...z}),x=!1});$.call(c)}function F(){$.on(".drag",null)}return{update:J,destroy:F}}var yZ=Sf(c0(),1),lZ=Sf(eK(),1);var fZ=(f)=>{let u,_=new Set,y=(U,G)=>{let W=typeof U==="function"?U(u):U;if(!Object.is(W,u)){let K=u;u=(G!=null?G:typeof W!=="object"||W===null)?W:Object.assign({},u,W),_.forEach((E)=>E(u,K))}},l=()=>u,F={setState:y,getState:l,getInitialState:()=>A,subscribe:(U)=>{return _.add(U),()=>_.delete(U)},destroy:()=>{_.clear()}},A=u=f(y,l,F);return F},uZ=(f)=>f?fZ(f):fZ;var{useDebugValue:ED}=yZ.default,{useSyncExternalStoreWithSelector:HD}=lZ.default,VD=(f)=>f;function wF(f,u=VD,_){let y=HD(f.subscribe,f.getState,f.getServerState||f.getInitialState,u,_);return ED(y),y}var _Z=(f,u)=>{let _=uZ(f),y=(l,$=u)=>wF(_,l,$);return Object.assign(y,_),y},$Z=(f,u)=>f?_Z(f,u):_Z;function W0(f,u){if(Object.is(f,u))return!0;if(typeof f!=="object"||f===null||typeof u!=="object"||u===null)return!1;if(f instanceof Map&&u instanceof Map){if(f.size!==u.size)return!1;for(let[y,l]of f)if(!Object.is(l,u.get(y)))return!1;return!0}if(f instanceof Set&&u instanceof Set){if(f.size!==u.size)return!1;for(let y of f)if(!u.has(y))return!1;return!0}let _=Object.keys(f);if(_.length!==Object.keys(u).length)return!1;for(let y of _)if(!Object.prototype.hasOwnProperty.call(u,y)||!Object.is(f[y],u[y]))return!1;return!0}var OD=Sf(t7(),1),v5=a.createContext(null),XD=v5.Provider,TZ=Iu.error001();function kf(f,u){let _=a.useContext(v5);if(_===null)throw Error(TZ);return wF(_,f,u)}function z0(){let f=a.useContext(v5);if(f===null)throw Error(TZ);return a.useMemo(()=>({getState:f.getState,setState:f.setState,subscribe:f.subscribe}),[f])}var jZ={display:"none"},ND={position:"absolute",width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0px, 0px, 0px, 0px)",clipPath:"inset(100%)"},rZ="react-flow__node-desc",MZ="react-flow__edge-desc",LD="react-flow__aria-live",YD=(f)=>f.ariaLiveMessage,BD=(f)=>f.ariaLabelConfig;function DD({rfId:f}){let u=kf(YD);return o.jsx("div",{id:`${LD}-${f}`,"aria-live":"assertive","aria-atomic":"true",style:ND,children:u})}function wD({rfId:f,disableKeyboardA11y:u}){let _=kf(BD);return o.jsxs(o.Fragment,{children:[o.jsx("div",{id:`${rZ}-${f}`,style:jZ,children:u?_["node.a11yDescription.default"]:_["node.a11yDescription.keyboardDisabled"]}),o.jsx("div",{id:`${MZ}-${f}`,style:jZ,children:_["edge.a11yDescription.default"]}),!u&&o.jsx(DD,{rfId:f})]})}var b5=a.forwardRef(({position:f="top-left",children:u,className:_,style:y,...l},$)=>{let j=`${f}`.split("-");return o.jsx("div",{className:B0(["react-flow__panel",_,...j]),style:y,ref:$,...l,children:u})});b5.displayName="Panel";function TD({proOptions:f,position:u="bottom-right"}){if(f?.hideAttribution)return null;return o.jsx(b5,{position:u,className:"react-flow__attribution","data-message":"Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev",children:o.jsx("a",{href:"https://reactflow.dev",target:"_blank",rel:"noopener noreferrer","aria-label":"React Flow attribution",children:"React Flow"})})}var rD=(f)=>{let u=[],_=[];for(let[,y]of f.nodeLookup)if(y.selected)u.push(y.internals.userNode);for(let[,y]of f.edgeLookup)if(y.selected)_.push(y);return{selectedNodes:u,selectedEdges:_}},C5=(f)=>f.id;function MD(f,u){return W0(f.selectedNodes.map(C5),u.selectedNodes.map(C5))&&W0(f.selectedEdges.map(C5),u.selectedEdges.map(C5))}function PD({onSelectionChange:f}){let u=z0(),{selectedNodes:_,selectedEdges:y}=kf(rD,MD);return a.useEffect(()=>{let l={nodes:_,edges:y};f?.(l),u.getState().onSelectionChangeHandlers.forEach(($)=>$(l))},[_,y,f]),null}var SD=(f)=>!!f.onSelectionChangeHandlers;function CD({onSelectionChange:f}){let u=kf(SD);if(f||u)return o.jsx(PD,{onSelectionChange:f});return null}var MF=typeof window<"u"?a.useLayoutEffect:a.useEffect,PZ=[0,0],RD={x:0,y:0,zoom:1},xD=["nodes","edges","defaultNodes","defaultEdges","onConnect","onConnectStart","onConnectEnd","onClickConnectStart","onClickConnectEnd","nodesDraggable","autoPanOnNodeFocus","nodesConnectable","nodesFocusable","edgesFocusable","edgesReconnectable","elevateNodesOnSelect","elevateEdgesOnSelect","minZoom","maxZoom","nodeExtent","onNodesChange","onEdgesChange","elementsSelectable","connectionMode","snapGrid","snapToGrid","translateExtent","connectOnClick","defaultEdgeOptions","fitView","fitViewOptions","onNodesDelete","onEdgesDelete","onDelete","onNodeDrag","onNodeDragStart","onNodeDragStop","onSelectionDrag","onSelectionDragStart","onSelectionDragStop","onMoveStart","onMove","onMoveEnd","noPanClassName","nodeOrigin","autoPanOnConnect","autoPanOnNodeDrag","onError","connectionRadius","isValidConnection","selectNodesOnDrag","nodeDragThreshold","connectionDragThreshold","onBeforeDelete","debug","autoPanSpeed","ariaLabelConfig","zIndexMode"],JZ=[...xD,"rfId"],vD=(f)=>({setNodes:f.setNodes,setEdges:f.setEdges,setMinZoom:f.setMinZoom,setMaxZoom:f.setMaxZoom,setTranslateExtent:f.setTranslateExtent,setNodeExtent:f.setNodeExtent,reset:f.reset,setDefaultNodesAndEdges:f.setDefaultNodesAndEdges}),FZ={translateExtent:_3,nodeOrigin:PZ,minZoom:0.5,maxZoom:2,elementsSelectable:!0,noPanClassName:"nopan",rfId:"1"};function bD(f){let{setNodes:u,setEdges:_,setMinZoom:y,setMaxZoom:l,setTranslateExtent:$,setNodeExtent:j,reset:J,setDefaultNodesAndEdges:F}=kf(vD,W0),A=z0();MF(()=>{return F(f.defaultNodes,f.defaultEdges),()=>{U.current=FZ,J()}},[]);let U=a.useRef(FZ);return MF(()=>{for(let G of JZ){let W=f[G],K=U.current[G];if(W===K)continue;if(typeof f[G]>"u")continue;if(G==="nodes")u(W);else if(G==="edges")_(W);else if(G==="minZoom")y(W);else if(G==="maxZoom")l(W);else if(G==="translateExtent")$(W);else if(G==="nodeExtent")j(W);else if(G==="ariaLabelConfig")A.setState({ariaLabelConfig:LK(W)});else if(G==="fitView")A.setState({fitViewQueued:W});else if(G==="fitViewOptions")A.setState({fitViewOptions:W});else A.setState({[G]:W})}U.current=f},JZ.map((G)=>f[G])),null}function QZ(){if(typeof window>"u"||!window.matchMedia)return null;return window.matchMedia("(prefers-color-scheme: dark)")}function hD(f){let[u,_]=a.useState(f==="system"?null:f);return a.useEffect(()=>{if(f!=="system"){_(f);return}let y=QZ(),l=()=>_(y?.matches?"dark":"light");return l(),y?.addEventListener("change",l),()=>{y?.removeEventListener("change",l)}},[f]),u!==null?u:QZ()?.matches?"dark":"light"}var AZ=typeof document<"u"?document:null;function Z6(f=null,u={target:AZ,actInsideInputWithModifier:!0}){let[_,y]=a.useState(!1),l=a.useRef(!1),$=a.useRef(new Set([])),[j,J]=a.useMemo(()=>{if(f!==null){let A=(Array.isArray(f)?f:[f]).filter((G)=>typeof G==="string").map((G)=>G.replace("+",` +本次任务:`,y=u.indexOf(l);if(y===-1)return f;return u.slice(y+l.length).trimStart()}function rr(f){return f.length>0?f.split(/\r\n|\r|\n/u).length:0}function ZG(f){let u=String(f?.displayPrompt||"");if(u.length>0)return u;let l=String(f?.prompt||"");return tL(kL(l).userPrompt)}function tl(f){return f?._traceSummary&&typeof f._traceSummary==="object"&&!Array.isArray(f._traceSummary)?f._traceSummary:null}function W8(f){return f?._promptDetails&&typeof f._promptDetails==="object"&&!Array.isArray(f._promptDetails)?f._promptDetails:{}}function Gj(f){let u=tl(f)?.prompt;return u&&typeof u==="object"&&!Array.isArray(u)?u:{}}function Aj(f){let u=tl(f)?.execution;return u&&typeof u==="object"&&!Array.isArray(u)?u:{}}function U8(f){let u=Gj(f),l=String(u.basePrompt||"");return l.length>0?l:ZG(f)}function Fj(f){let u=tl(f);return String(u?.finalResponse||f?.finalResponse||"").trimEnd()}function Jj(f){let l=tl(f)?.lastJudge||f?.lastJudge;return l&&typeof l==="object"&&!Array.isArray(l)?l:null}function s0(f){return f&&typeof f==="object"&&!Array.isArray(f)?f:null}function sL(f){let u=tl(f)?.attempts;if(Array.isArray(u)&&u.length>0)return u;let l=NG(f);if(l.length>0)return l.map(($,j)=>({...$,index:Number($?.index||j+1),execution:j===l.length-1?Aj(f):s0($?.execution)||{},finalResponse:String($?.finalResponse||$?.finalResponsePreview||(j===l.length-1?Fj(f):"")),judge:s0($?.judge)||(j===l.length-1?Jj(f):null)}));let y=Aj(f),r=Fj(f),_=Jj(f);if(Object.keys(y).length===0&&r.length===0&&_===null)return[];return[{index:Number(f?.currentAttempt||1),mode:f?.currentMode||"initial",startedAt:f?.startedAt,finishedAt:f?.finishedAt,terminalStatus:f?.status,execution:y,finalResponse:r,finalResponseChars:r.length,judge:_}]}function oL(f,u){return s0(u?.execution)||Aj(f)}function aL(f,u,l,y){let r=tl(f),_=Number(r?.currentAttempt||f?.currentAttempt||0),$=Number(l),j=Number.isFinite($)&&$>0&&$===_,A=WG(f?.updatedAt,r?.updatedAt);if(j&&!u?.finishedAt&&A.length>0)return A;return String(u?.updatedAt||u?.finishedAt||y.effectiveEndAt||(j?A:"")||A||f?.finishedAt||f?.startedAt||"")}function dL(f,u){let l=String(u?.finalResponse||u?.finalResponsePreview||"");if(Object.prototype.hasOwnProperty.call(u||{},"finalResponse")||Object.prototype.hasOwnProperty.call(u||{},"finalResponsePreview"))return l.trimEnd();return l.length>0?l.trimEnd():Fj(f)}function EG(f,u){if(Object.prototype.hasOwnProperty.call(u||{},"judge"))return s0(u?.judge);return Jj(f)}function eL(f,u,l){if(!UB(f))return!1;if(Kj(u,l))return!1;if(u?.finishedAt)return!1;if(["succeeded","failed","canceled"].includes(String(u?.terminalStatus||"")))return!1;let y=tl(f),r=Number(y?.currentAttempt||f?.currentAttempt||0),_=Number(l);if(Number.isFinite(_)&&_>0&&Number.isFinite(r)&&r>0)return _===r;return!0}function HG(f){return`feedback:${String(f||"latest")}`}function fB(f,u,l){let y=String(u?.feedbackPrompt||"").trimEnd(),r=String(u?.feedbackPromptPreview||y||"").trimEnd(),_=Number(u?.feedbackPromptChars||y.length||r.length||0),$=Number(u?.feedbackPromptLines||rr(y||r));if(y.length>0||r.length>0||_>0)return{text:y,preview:r,chars:_,lines:$,source:u?.feedbackPromptSource||"judge-feedback",forAttempt:u?.feedbackPromptForAttempt||Number(l||0)+1,truncated:Boolean(u?.feedbackPromptTruncated)};let j=EG(f,u),A=String(j?.continuePrompt||"").trimEnd();if(j?.decision==="retry"&&A.length>0)return{text:"",preview:A,chars:A.length,lines:rr(A),source:"judge-continue-prompt",forAttempt:Number(l||0)+1,truncated:!1};return null}function uB(f){let u=Gj(f);return Boolean(u.hasReferenceInjection||Number(u.referencePromptChars||0)>0||f?.referenceInjection||f?.referenceInjectionSummary)}function lB(f,u=null){if(u!==null&&u!==void 0){let y=(s0(f?._traceStepsByAttempt)||{})[String(u)];return Array.isArray(y)?y:[]}return Array.isArray(f?._traceSteps)?f._traceSteps:[]}function Z_(f){return(Array.isArray(f?.summaryLines)?f.summaryLines:[]).map((u)=>String(u||""))}function z8(f){let u=String(f?.status||"").trim();if(u.length>0)return u;let l=Z_(f).join(` +`);return/^(item\/[A-Za-z]+(?:\/[A-Za-z]+)?):/u.exec(l)?.[1]||""}function rG(f){return/^item\/(?:started|completed): file changes status=/u.test(String(f||"").trim())}function yB(f){let u=Z_(f);for(let y=u.length-1;y>=0;y-=1){let r=/file changes status=([A-Za-z0-9_-]+)/u.exec(u[y]||"")?.[1];if(r)return r}let l=z8(f);if(l==="item/fileChange/outputDelta")return"updated";if(l==="item/started")return"started";if(l==="item/completed")return"completed";return l.replace(/^item\//u,"")||String(f?.status||"changed")}function rB(f){if(String(f?.kind||"")!=="edited")return!1;let u=String(f?.title||""),l=String(f?.status||""),y=Z_(f).join(` +`);if(u==="Edited files")return!0;if(/^item\/fileChange\//u.test(l))return!0;if((l==="item/started"||l==="item/completed")&&/file changes status=/u.test(y))return!0;if(/^Success\. Updated the following files:/mu.test(y))return!0;if(/^diff --git /mu.test(y))return!0;return/^([AMDRCU?]{1,2})\s+\S+/mu.test(y)}function _B(f){if(f.length<=1)return f[0];let u=f.find((_)=>z8(_)==="item/fileChange/outputDelta")||f.find((_)=>Z_(_).some(($)=>!rG($)))||f.at(-1)||f[0],l=f.flatMap((_)=>Array.isArray(_?.rawSeqs)?_.rawSeqs:[_?.seq]).filter((_)=>_!==void 0),y=f.flatMap(Z_).filter((_)=>_.trim().length>0&&!rG(_)),r=f[f.length-1]||u;return{...u,at:u?.at||r?.at,title:String(u?.title||"Edited files"),status:yB(r),summaryLines:y.length>0?y:Z_(u),rawSeqs:l}}function $B(f){let u=Array.isArray(f)?f:[],l=[],y=[],r=()=>{if(y.length>0)l.push(_B(y));y=[]};for(let _ of u){if(rB(_)){if(z8(_)==="item/started"&&y.length>0)r();if(y.push(_),z8(_)==="item/completed")r();continue}r(),l.push(_)}return r(),l}function jB(f,u){if(u.length===0)return f;let l=u.reduce((y,r)=>{let _=String(r?.kind||"");if(_==="explored")y.readCount+=1;else if(_==="edited")y.editCount+=1;else if(_==="ran")y.runCount+=1;return y},{readCount:0,editCount:0,runCount:0});return{...f,...l,toolCallCount:l.readCount+l.editCount+l.runCount,stepCount:u.length}}function OG(f,u=null){if(u!==null&&u!==void 0){let l=s0(f?._traceStepsLoadedByAttempt)||{};return Boolean(l[String(u)])}return Boolean(f?._traceStepsLoaded)}function Uj(f){return f?._traceStepDetails&&typeof f._traceStepDetails==="object"&&!Array.isArray(f._traceStepDetails)?f._traceStepDetails:{}}function AB(f,u){let l=Number(f?.index);return Number.isFinite(l)?l:u+1}function Kj(f,u){return Boolean(f?.synthetic)||Number(u)<=0}function K8(f){let u=Number(f);return Number.isFinite(u)?String(u):void 0}function FB(f){let u=f?.timing&&typeof f.timing==="object"?f.timing:{},l=String(f?.status||"");if(["queued"].includes(l))return`等待 ${Ml(u.queueWaitMs??u.totalElapsedMs)}`;if(["running","judging","retry_wait"].includes(l))return`耗时 ${Ml(u.durationMs??u.totalElapsedMs)}`;return`耗时 ${Ml(u.durationMs??u.totalElapsedMs)}`}function G8(f){return String(f?.queueId||"default")}function JB(f){return{system:"SYS",user:"YOU",assistant:"GPT",reasoning:"THINK",command:"CMD",diff:"DIFF",tool:"TOOL",error:"ERR"}[f]||f.toUpperCase()}function _G(f){return["running","judging","retry_wait"].includes(String(f?.status||""))}function UB(f){return String(f?.status||"")==="running"}function nl(f){return["succeeded","failed","canceled"].includes(String(f?.status||""))}function qG(f){if(f?.promptEditable===!0)return!0;if(f?.promptEditable===!1)return!1;return String(f?.status||"")==="queued"&&!f?.startedAt&&Number(f?.currentAttempt||0)===0&&!f?.codexThreadId&&!f?.nextMode}function E1(f){if(!nl(f))return!1;if(f?.terminalUnread===!0)return!0;if(f?.terminalUnread===!1)return!1;return!f?.readAt}function cu(f){let u=Number(f||0);return Number.isFinite(u)?u:0}function QB(f){return cu(f.queued)+cu(f.retry_wait)}function WB(f){return cu(f.running)+cu(f.judging)}function zB(f,u){return s0(f?.statistics)||s0(u?.statistics)||{}}function GB(f){return Array.isArray(f?.daily)?f.daily:[]}function KB(f){return s0(f?.totals)||{}}function Nj(f,u){let l=Number(f?.[u]??0);return Number.isFinite(l)&&l>0?l:0}function e7(f,u){return f.reduce((l,y)=>Math.max(l,Nj(y,u)),0)}var yr=700,$G=220,_y=30,N_=24,K3=184,Qj=K3-N_;function VG(f,u){if(u<=1)return yr/2;return _y+f*(yr-_y*2)/(u-1)}function LG(f,u){let l=u>0?u:1;return K3-Math.min(1,f/l)*Qj}function fj(f,u,l){let y=f.length>0?f:[{[u]:0}],r=y.length>1?y:[y[0],y[0]];return r.map((_,$)=>`${VG($,r.length).toFixed(2)},${LG(Nj(_,u),l).toFixed(2)}`).join(" ")}function Z1(f){let u=String(f||"");return/^\d{4}-\d{2}-\d{2}$/u.test(u)?u.slice(5):u||"--"}function F8(f){if(!f)return"";return`${String(f.seriesKey||"")}:${String(f.row?.date||f.index||"")}`}function NB(f,u,l,y){let r=Nj(f,y.key);return{...y,row:f,index:u,value:r,valueLabel:y.format(r),x:VG(u,l),y:LG(r,y.max),seriesKey:y.key}}function jG(f){if(E1(f))return 0;return{running:1,judging:2,retry_wait:3,queued:4,succeeded:8,failed:8,canceled:8}[String(f?.status||"")]??9}function W3(f){if(!f)return!1;if(f?._traceSummaryLoaded===!0)return!1;return f?.summaryOnly===!0||f?._metaLoaded!==!0}function ZB(f){return Boolean(f?._metaLoaded)||f?.summaryOnly===!1}function EB(f,u,l){let y=String(f?.[l]||""),r=String(u?.[l]||"");return y.length>r.length?y:r}function Wj(f,u,l){let y=Array.isArray(f?.[l])?f[l]:[],r=Array.isArray(u?.[l])?u[l]:[];if(r.length===0&&y.length>0)return y;return y.length>r.length?y:r}function HB(f,u){let l=u?.summaryOnly===!0&&ZB(f),y={...f,...u};if(!l)return y;for(let r of["prompt","basePrompt","displayPrompt","finalResponse"])y[r]=EB(f,u,r);for(let r of["promptHistory","attempts","output","events"])y[r]=Wj(f,u,r);if(f?.referenceInjection?.items&&!u?.referenceInjection?.items)y.referenceInjection=f.referenceInjection;if(f?.referenceInjectionSummary&&!u?.referenceInjectionSummary)y.referenceInjectionSummary=f.referenceInjectionSummary;y.summaryOnly=f?.summaryOnly===!1?!1:u.summaryOnly,y._metaLoaded=f?._metaLoaded,y._detailLoaded=f?._detailLoaded,y._transcriptComplete=f?._transcriptComplete,y._transcriptPreview=Object.prototype.hasOwnProperty.call(u,"_transcriptPreview")?u._transcriptPreview:f?._transcriptPreview;for(let r of["_traceSummary","_traceSummaryLoaded","_traceSteps","_traceStepsLoaded","_traceStepsByAttempt","_traceStepsLoadedByAttempt","_traceStepDetails","_promptDetails"])if(!Object.prototype.hasOwnProperty.call(u,r)&&Object.prototype.hasOwnProperty.call(f||{},r))y[r]=f[r];return y}function OB(f){let u=f?.selected,l=u?.task&&typeof u.task==="object"?u.task:null;if(l!==null){let r=Boolean(u?.preview);return{...l,transcript:Array.isArray(u?.transcript)?u.transcript:[],_detailLoaded:Array.isArray(u?.transcript)&&u.transcript.length>0,_transcriptComplete:Boolean(!r&&!u?.hasMore&&nl(l)),_transcriptPreview:r,_summaryLoaded:!0}}let y=y0(f)[0];return y?{...y,_summaryLoaded:!0}:null}function uj(f,u){let l=new Map;for(let y of[...Array.isArray(f)?f:[],...Array.isArray(u)?u:[]]){let r=`${Number(y?.seq??0)}:${String(y?.kind||"message")}`,_=l.get(r);if(!_){l.set(r,y);continue}let $={..._,...y};for(let[j,A]of[["bodyPreview","bodyOmittedLines"],["commandPreview","commandOmittedLines"]]){let F=String(_?.[j]||""),U=String(y?.[j]||"");if(F.length>U.length)$[j]=_[j],$[A]=_[A]}l.set(r,$)}return Array.from(l.values()).sort((y,r)=>Number(y?.seq??0)-Number(r?.seq??0))}function J8(f){return(Array.isArray(f)?f:[]).reduce((u,l)=>Math.max(u,Number(l?.seq??0)),0)}function AG(f,u=8){let l=Array.from(new Set((Array.isArray(f)?f:[]).map((r)=>Number(r?.seq??0)).filter((r)=>Number.isFinite(r)&&r>0))).sort((r,_)=>r-_);if(l.length===0)return 0;let y=l[Math.max(0,l.length-u)]??0;return Math.max(0,y-0.001)}function qB(f,u){let l=Array.isArray(f?.codeModels)?f.codeModels:Array.isArray(f?.codexModels)?f.codexModels:[],y=["gpt-5.5","gpt-5.4-mini","gpt-5.4","minimax-m2.7"];return Array.from(new Set([...l,...y,u].map((r)=>String(r||"").trim()).filter(Boolean)))}function VB(f,u){let y=(Array.isArray(f?.executionProviders)?f.executionProviders:[]).map(($)=>({id:String($?.id||"").trim(),label:String($?.label||$?.id||"").trim(),defaultWorkdir:String($?.defaultWorkdir||"").trim(),kind:String($?.kind||"").trim()})).filter(($)=>$.id.length>0),r=String(f?.mainProviderId||f?.defaultProviderId||"main-server").trim()||"main-server",_=new Map;for(let $ of[...y,{id:r,label:`${r} (master)`,defaultWorkdir:String(f?.defaultWorkdir||"/root/unidesk"),kind:"local"},u?{id:u,label:u,defaultWorkdir:Q8(f,u),kind:""}:null].filter(Boolean))if(!_.has($.id))_.set($.id,$);return Array.from(_.values())}function Q8(f,u){let l=String(u||"").trim(),y=f?.defaultWorkdirByProvider&&typeof f.defaultWorkdirByProvider==="object"?f.defaultWorkdirByProvider:{};if(typeof y[l]==="string"&&String(y[l]).trim().length>0)return String(y[l]).trim();let r=Array.isArray(f?.executionProviders)?f.executionProviders.find(($)=>String($?.id||"")===l):null;if(typeof r?.defaultWorkdir==="string"&&r.defaultWorkdir.trim().length>0)return r.defaultWorkdir.trim();let _=String(f?.mainProviderId||f?.defaultProviderId||"main-server");return l===_?String(f?.defaultWorkdir||"/root/unidesk"):String(f?.remoteDefaultWorkdir||"/home/ubuntu")}function LB({task:f,selected:u,onSelect:l,onCopy:y,onReference:r,onMarkRead:_,copied:$,markingRead:j}){let A=f?.lastJudge||{},F=String(f?.id||""),U=E1(f),Q=WG(f?.updatedAt,tl(f)?.updatedAt),W=`最近更新: ${QG(Q)}`;return L("article",{role:"button",tabIndex:0,className:`codex-task-card ${u?"selected":""} ${U?"unread-terminal":""}`,onClick:l,onKeyDown:(G)=>{if(G.key==="Enter"||G.key===" ")G.preventDefault(),l()},"data-unread-terminal":U?"true":"false","data-testid":`codex-task-${f?.id||"unknown"}`},U?L("span",{className:"codex-unread-badge",title:"待读","aria-label":"待读","data-testid":`codex-unread-task-${F||"unknown"}`}):null,L("div",{className:"codex-task-card-head"},L("div",{className:"codex-task-status-line"},L(_r,{status:f?.status},f?.status||"unknown")),L("span",{className:"mono-text"},`${f?.currentAttempt||0}/${f?.maxAttempts||0}`)),L("div",{className:"codex-task-id-row"},L("code",{title:F},F||"unknown"),L("div",{className:"codex-task-id-actions"},L("button",{type:"button",className:"codex-copy-id-btn",onClick:(G)=>{G.stopPropagation(),r(F)},"data-testid":`codex-reference-task-${F||"unknown"}`},"引用"),L("button",{type:"button",className:"codex-copy-id-btn",onClick:(G)=>{G.stopPropagation(),y(F)},"data-testid":`codex-copy-task-id-${F||"unknown"}`},$?"已复制":"复制ID"),U?L("button",{type:"button",className:"codex-copy-id-btn codex-mark-read-btn",disabled:Boolean(j),onClick:(G)=>{G.stopPropagation(),_(F)},"data-testid":`codex-mark-task-read-${F||"unknown"}`},j?"标记中":"标为已读"):null)),L("strong",null,rj(ZG(f),120)||"空任务"),L("div",{className:"codex-task-meta"},L("span",null,`queue=${G8(f)}`),L("span",null,`provider=${f?.providerId||"main-server"}`),L("span",null,f?.model||"--"),L("span",null,FB(f))),L("div",{className:"codex-task-meta codex-task-update-meta"},L("span",{className:"codex-task-recent-update",title:Q?`更新时间: ${Kf(Q)}`:W,"data-testid":`codex-task-recent-update-${F||"unknown"}`},W),L("span",null,Kf(Q||f?.updatedAt))),qG(f)?L("div",{className:"codex-judge-line","data-testid":`codex-task-prompt-editable-${F||"unknown"}`},"queued prompt 可编辑"):null,A?.decision?L("div",{className:"codex-judge-line"},`judge=${A.decision} ${Math.round(Number(A.confidence||0)*100)}%`):null)}function lj({title:f,tasks:u,selectedId:l,onSelect:y,onCopy:r,onReference:_,onMarkRead:$,copiedTaskId:j,markingReadTaskId:A,emptyText:F}){let U=Array.isArray(u)?u:[];return L("section",{className:"codex-task-section"},L("div",{className:"codex-task-section-head"},L("span",null,f),L("code",null,String(U.length))),U.length===0?L("p",{className:"codex-task-section-empty"},F):L("div",{className:"codex-task-section-list"},U.map((Q)=>L(LB,{key:Q.id,task:Q,selected:l===Q.id,onSelect:()=>y(Q.id),onCopy:r,onReference:_,onMarkRead:$,copied:j===Q.id,markingRead:A===Q.id}))))}function BB(){return L("span",{className:"codex-stats-icon","aria-hidden":"true"},L("svg",{viewBox:"0 0 36 24",focusable:"false"},L("path",{className:"grid",d:"M3 20.5H33M3 12.5H33M3 4.5H33"}),L("polyline",{className:"line tasks",points:"3,18 9,14 15,15 21,8 27,10 33,4"}),L("polyline",{className:"line retry",points:"3,20 9,17 15,18 21,13 27,14 33,9"})))}function XB({stats:f,queueName:u,onRaw:l}){let y=GB(f),r=KB(f),_=y.at(-1)||{},$=e7(y,"executedTasks"),j=e7(y,"retryAttempts"),A=e7(y,"avgDurationMs"),F=y.length>0,U=s0(f?.range)||{},[Q,W]=If(null),[G,K]=If(null),E=[];if($>0)E.push(`tasks ${$}`);if(j>0)E.push(`retry ${j}`);if(A>0)E.push(`avg ${Ml(A)}`);let O=[{key:"executedTasks",className:"tasks",label:"执行任务",max:$,format:(X)=>`${cu(X)} tasks`},{key:"retryAttempts",className:"retry",label:"重试次数",max:j,format:(X)=>`${cu(X)} retries`},{key:"avgDurationMs",className:"duration",label:"平均耗时",max:A,format:(X)=>Ml(X)}],z=Q||G,Z=F8(z),N=String(z?.row?.date||""),H=z?{left:`${Math.max(8,Math.min(92,Number(z.x)/yr*100))}%`,top:`${Math.max(14,Math.min(86,Number(z.y)/$G*100))}%`}:void 0;ry(()=>{W(null),K(null)},[u,U.startDate,U.endDate,y.length]);let Y=(X)=>{W(X)},w=(X)=>{let i=F8(X);K((m)=>F8(m)===i?null:X),W(X)},V=O.flatMap((X)=>y.map((i,m)=>{let M=NB(i,m,y.length,X),c=F8(M),C=Z===c,T=String(i?.date||`day-${m}`),R=`${Z1(T)} ${X.label}: ${M.valueLabel}`;return L("g",{key:`${X.key}-${T}`,className:`stat-point-group ${X.className} ${C?"active":""}`,role:"button",tabIndex:0,"aria-label":R,"data-testid":`codex-stats-point-${X.className}-${T}`,onMouseEnter:()=>Y(M),onFocus:()=>Y(M),onClick:()=>w(M),onKeyDown:(P)=>{if(P.key==="Enter"||P.key===" ")P.preventDefault(),w(M)}},L("circle",{className:"stat-hit-point",cx:M.x,cy:M.y,r:13}),L("circle",{className:`stat-point ${X.className} ${C?"active":""}`,cx:M.x,cy:M.y,r:C?5.6:4.2}))}));return L(K_,{title:"统计曲线",eyebrow:`Daily task stats / ${u}`,className:"codex-stats-panel",summary:L("span",null,`${Z1(U.startDate)} -> ${Z1(U.endDate)} · ${f?.timezone||"Asia/Shanghai"}`),actions:s0(f)?L(zG,{title:"Code Queue Stats",data:f,onOpen:l,testId:"raw-codex-stats"}):null},L("div",{className:"codex-stats-hero","data-testid":"codex-stats-panel"},L(BB),L("div",null,L("strong",null,`${cu(r.executedTasks)} tasks / ${cu(r.retryAttempts)} retries`),L("span",null,`平均完成耗时 ${Ml(r.avgDurationMs??void 0)} · 终态 ${cu(r.completedTasks)} 个`))),F?L("div",{className:"codex-stats-chart","data-testid":"codex-stats-chart",onMouseLeave:()=>W(null)},L("svg",{viewBox:`0 0 ${yr} ${$G}`,preserveAspectRatio:"none",role:"img","aria-label":"Code Queue daily task statistics"},L("line",{className:"axis",x1:_y,x2:yr-_y,y1:K3,y2:K3}),L("line",{className:"grid",x1:_y,x2:yr-_y,y1:N_+Qj/2,y2:N_+Qj/2}),L("line",{className:"grid",x1:_y,x2:yr-_y,y1:N_,y2:N_}),L("polyline",{className:"stat-line tasks",points:fj(y,"executedTasks",$)}),L("polyline",{className:"stat-line retry",points:fj(y,"retryAttempts",j)}),L("polyline",{className:"stat-line duration",points:fj(y,"avgDurationMs",A)}),z?L("g",{className:"stat-cursor-layer","data-testid":"codex-stats-active-point"},L("line",{className:"stat-cursor",x1:z.x,x2:z.x,y1:N_,y2:K3}),L("circle",{className:`stat-point-active ${z.className}`,cx:z.x,cy:z.y,r:8})):null,L("g",{className:"stat-point-layer"},V)),z?L("div",{className:"codex-stats-tooltip active",style:H,"data-testid":"codex-stats-tooltip"},L("b",null,Z1(z.row?.date)),L("span",null,`${z.label} · ${z.valueLabel}`),L("code",null,`${cu(z.row?.executedTasks)} exec / ${cu(z.row?.retryAttempts)} retry / ${Ml(z.row?.avgDurationMs??void 0)}`)):null,L("div",{className:"codex-stats-legend"},L("span",{className:"tasks"},"执行任务"),L("span",{className:"retry"},"重试次数"),L("span",{className:"duration"},"平均耗时")),L("div",{className:"codex-stats-scale"},L("span",null,Z1(y[0]?.date)),L("span",null,E.join(" · ")||"暂无峰值"),L("span",null,Z1(y.at(-1)?.date))),L("div",{className:`codex-stats-focus ${z?"active":""}`,"data-testid":"codex-stats-focus"},z?L(N3.default.Fragment,null,L("div",null,L("strong",null,Z1(z.row?.date)),L("span",null,`${z.label} · ${z.valueLabel}`)),L("div",{className:"codex-stats-focus-metrics"},L("code",null,`${cu(z.row?.executedTasks)} exec`),L("code",null,`${cu(z.row?.retryAttempts)} retry`),L("code",null,Ml(z.row?.avgDurationMs??void 0)))):L("span",null,"将鼠标悬停到曲线数据点查看明细,点击数据点可固定。"))):L($r,{title:"暂无统计",text:"任务开始执行后会生成按天汇总的曲线。"}),L("div",{className:"codex-stats-summary-grid"},L("article",null,L("span",null,"今日执行"),L("strong",null,String(cu(_.executedTasks))),L("code",null,Z1(_.date))),L("article",null,L("span",null,"今日重试"),L("strong",null,String(cu(_.retryAttempts))),L("code",null,`累计 ${cu(r.retryAttempts)}`)),L("article",null,L("span",null,"平均耗时"),L("strong",null,Ml(r.avgDurationMs??void 0)),L("code",null,`${cu(r.durationSamples)} samples`))),L("div",{className:"codex-stats-daily-list","data-testid":"codex-stats-daily-list"},y.slice(-7).map((X)=>L("div",{key:String(X?.date||""),className:`codex-stats-daily-row ${N===String(X?.date||"")?"active":""}`,"data-testid":`codex-stats-day-${String(X?.date||"unknown")}`},L("span",null,Z1(X?.date)),L("b",null,`${cu(X?.executedTasks)} exec`),L("b",null,`${cu(X?.retryAttempts)} retry`),L("code",null,Ml(X?.avgDurationMs??void 0))))))}function YB({task:f,queueRows:u,busy:l,onMove:y}){let r=String(f?.id||""),_=G8(f),[$,j]=If(_);ry(()=>{j(_)},[r,_]);let A=!r||l||["running","judging","retry_wait"].includes(String(f?.status||""));return L("div",{className:"codex-task-move-control","data-testid":"codex-task-queue-move-control"},L("label",null,"任务 queue",L("select",{value:$,disabled:!r||l,onChange:(F)=>j(String(F.target.value||_)),"data-testid":"codex-task-queue-move-select"},u.map((F)=>L("option",{key:String(F?.id||""),value:String(F?.id||"")},jj(F))))),L("button",{type:"button",className:"ghost-btn",disabled:A||$===_,onClick:()=>y($),title:A?"运行中 / judging / retry_wait 的任务不能移动;请先打断或等当前 turn 结束":"移动已创建任务到另一个 queue","data-testid":"codex-task-queue-move-button"},"移动"))}function FG(f,u=4){let l=(Array.isArray(f)?f:[]).map((r)=>String(r||"").trim()).filter(Boolean);if(l.length===0)return"--";let y=l.slice(0,u).join(" / ");return l.length>u?`${y} +${l.length-u}`:y}function wB({task:f,loading:u,onLoadPromptPart:l,testId:y="codex-initial-prompt-full",textTestId:r="codex-initial-prompt-full-text",baseTextTestId:_="codex-initial-prompt-base"}){let $=Gj(f),j=W8(f),A=U8(f).trimEnd(),F=String(j.full?.text||""),U=uB(f),Q=Number($.promptChars||f?.promptChars||F.length),W=Number($.basePromptLines||rr(A)),G=Number($.promptLines||rr(F));return L("section",{className:"codex-progressive-card codex-progressive-prompt","data-testid":"codex-progressive-prompt"},L("div",{className:"codex-progressive-card-head"},L("span",{className:"codex-output-channel"},"Prompt"),L("strong",null,"Submitted prompt / 原始用户 prompt"),L("code",null,`${W||rr(A)} lines / ${A.length} chars`)),L("pre",{className:"codex-prompt-full","data-testid":_},A||"空 prompt"),U?L("details",{className:"codex-reference-injection codex-progressive-full-prompt","data-testid":y,onToggle:(K)=>{if(K.currentTarget?.open&&!F)l?.("full")}},L("summary",null,L("span",null,"引用注入已折叠,点击按需拉取最终进入 Code agent 的完整 prompt"),L("code",null,F?`${G||rr(F)} lines / ${F.length} chars`:`${Number.isFinite(Q)&&Q>0?Q:"--"} chars`)),L("pre",{className:"codex-prompt-full codex-prompt-final-full","data-testid":r},F||(u?"正在按需拉取完整 prompt...":"展开后将只请求 full prompt,不拉取完整 transcript。"))):null)}function BG({task:f,attempt:u,attemptIndex:l,loading:y,onLoadSteps:r,onLoadStep:_,testId:$="codex-execution-summary"}){let j=$B(lB(f,l)),A=jB(oL(f,u),j),F=Uj(f),U=OG(f,l),Q=Number(A.toolCallCount||0),W=Array.isArray(A.editedFiles)?A.editedFiles:[],G=Array.isArray(A.commands)?A.commands:[],E=Kj(u,l)?` · ${String(u?.label||"recovered thread execution")}`:l?` #${l}`:"",O=aL(f,u,l,A),z=`最近更新: ${QG(O)}`,Z=eL(f,u,l);return L("details",{className:`codex-progressive-card codex-execution-summary ${Z?"running":""}`,"data-testid":$,"data-attempt-index":K8(l),"data-running":Z?"true":"false",onToggle:(N)=>{if(N.currentTarget?.open&&!U)r?.(l)}},L("summary",null,L("div",{className:"codex-progressive-card-head"},L("span",{className:"codex-output-channel"},"Summary"),L("strong",null,`执行过程摘要${E}`),Z?L("span",{className:"codex-summary-running-pill","data-testid":`${$}-running`},"执行中"):null,L("code",{title:O?`最近更新: ${Kf(O)}`:z},`${Ml(A.durationMs??A.totalElapsedMs)} / ${Q} tools / ${z}`)),L("div",{className:"codex-execution-digest"},L("span",null,`read ${Number(A.readCount||0)}`),L("span",null,`edit ${Number(A.editCount||0)}`),L("span",null,`run ${Number(A.runCount||0)}`),L("span",null,`${Number(A.stepCount||j.length||0)} steps`))),L("div",{className:"codex-execution-digest expanded"},L("span",null,`修改文件:${FG(W,6)}`),L("span",null,`执行命令:${FG(G,4)}`)),j.length===0?L("div",{className:"codex-output-empty"},y?"正在按需拉取步骤 summary...":"展开后将只请求执行步骤 summary,不拉取单步骤全量。"):L("div",{className:"codex-trace-step-list"},j.map((N)=>{let H=String(N?.seq??""),Y=F[H],w=Array.isArray(N?.summaryLines)?N.summaryLines.slice(0,4):[];return L("details",{key:H||`${N?.title}-${N?.at}`,className:`codex-trace-step ${String(N?.kind||"message")}`,"data-testid":`codex-trace-step-${H||"unknown"}`,onToggle:(V)=>{if(V.currentTarget?.open&&!Y)_?.(N?.seq)}},L("summary",null,L("span",{className:"codex-output-channel"},DB(N?.kind)),L("strong",null,String(N?.title||"Trace step")),N?.status?L("code",null,String(N.status)):null,L("time",null,Kf(N?.at))),L("div",{className:"codex-trace-step-summary"},w.length>0?w.map((V,X)=>L("pre",{key:`${H}-${X}`},String(V||""))):L("span",null,"无 summary")),Y?.line?L(j8,{items:[Y.line],autoScroll:!1,loading:!1,hasDetail:!0,emptyText:"无步骤详情",testId:`codex-trace-step-detail-${H||"unknown"}`,className:"codex-transcript codex-step-detail-transcript",collapseTools:!1}):L("div",{className:"codex-output-empty"},y?"正在按需拉取这个步骤的全量数据...":"展开后将只请求这个单步骤的全量数据。"))})))}function DB(f){let u=String(f||"");if(u==="ran")return"Ran";if(u==="explored")return"Explored";if(u==="edited")return"Edited";if(u==="error")return"Error";if(u==="system")return"System";return"Message"}function XG({task:f,attempt:u,attemptIndex:l,testId:y="codex-final-response"}){let r=dL(f,u);if(r.length===0)return null;let _=Number(u?.finalResponseChars||r.length),$=l?` #${l}`:"";return L("section",{className:"codex-progressive-card codex-final-response","data-testid":y,"data-attempt-index":K8(l)},L("div",{className:"codex-progressive-card-head"},L("span",{className:"codex-output-channel"},"Final"),L("strong",null,`最终 response${$}`),L("code",null,`${Number.isFinite(_)?_:r.length} chars`)),L(Yz,{markdown:r,className:"codex-transcript-body codex-markdown",testId:`${y}-markdown`}))}function YG({task:f,attempt:u,attemptIndex:l,testId:y="codex-progressive-judge"}){let r=EG(f,u);if(!r?.decision)return null;let _=l?` #${l}`:"";return L("section",{className:"codex-progressive-card codex-progressive-judge","data-testid":y,"data-attempt-index":K8(l)},L("div",{className:"codex-progressive-card-head"},L("span",{className:"codex-output-channel"},"Judge"),L("strong",null,`完成判定${_}`),L("code",null,`${r.decision} ${Math.round(Number(r.confidence||0)*100)}%`)),L("div",{className:"codex-judge-card","data-testid":`${y}-card`},L(_r,{status:r.decision},r.decision),L("strong",null,`${Math.round(Number(r.confidence||0)*100)}% confidence`),L("p",{"data-testid":`${y}-reason`},r.reason||"--"),r.continuePrompt?L("pre",{"data-testid":`${y}-continue-prompt`},String(r.continuePrompt||"")):null))}function TB({task:f,attempt:u,attemptIndex:l,loading:y,onLoadPromptPart:r,testId:_="codex-judge-feedback-prompt"}){let $=fB(f,u,l);if($===null)return null;let j=HG(l),F=W8(f)[j],U=String(F?.text||"").trimEnd(),Q=String($.preview||$.text||"").trimEnd(),W=U||String($.text||"").trimEnd(),G=Number(F?.chars||$.chars||W.length||Q.length),K=Number(F?.lines||$.lines||rr(W||Q)),E=F?.forAttempt||$.forAttempt||Number(l||0)+1;return L("details",{className:"codex-progressive-card codex-judge-feedback-prompt","data-testid":_,"data-attempt-index":K8(l),onToggle:(O)=>{if(O.currentTarget?.open&&!U)r?.("feedback",l)}},L("summary",null,L("div",{className:"codex-progressive-card-head"},L("span",{className:"codex-output-channel"},"Prompt"),L("strong",null,`judge feedback prompt #${l} -> #${E}`),L("code",null,`${K||"--"} lines / ${Number.isFinite(G)?G:Q.length} chars`)),L("p",{className:"codex-feedback-preview","data-testid":`${_}-preview`},Q||"展开后按需拉取 judge feedback prompt。")),L("pre",{className:"codex-prompt-full codex-feedback-full","data-testid":`${_}-text`},W||(y?"正在按需拉取 judge feedback prompt...":"展开后将只请求这一次 judge feedback prompt。")))}function nB({task:f,attempt:u,position:l,loading:y,onLoadPromptPart:r,onLoadSteps:_,onLoadStep:$}){let j=AB(u,l),A=l===0,F=Kj(u,j),U=F?String(u?.label||"Recovered thread execution"):`Attempt ${j}`;return L("section",{className:"codex-attempt-cycle","data-testid":`codex-attempt-cycle-${j}`},L("div",{className:"codex-attempt-cycle-head"},L("span",{className:"codex-output-channel"},U),L("strong",null,String(u?.mode||(j<=1?"initial":"retry"))),u?.terminalStatus?L(_r,{status:u.terminalStatus},u.terminalStatus):null,L("code",null,`${Kf(u?.startedAt)} -> ${Kf(u?.finishedAt)}`)),L(BG,{task:f,attempt:u,attemptIndex:j,loading:y,onLoadSteps:_,onLoadStep:$,testId:A?"codex-execution-summary":`codex-execution-summary-attempt-${j}`}),F?null:L(XG,{task:f,attempt:u,attemptIndex:j,testId:A?"codex-final-response":`codex-final-response-attempt-${j}`}),F?null:L(YG,{task:f,attempt:u,attemptIndex:j,testId:A?"codex-progressive-judge":`codex-progressive-judge-attempt-${j}`}),F?null:L(TB,{task:f,attempt:u,attemptIndex:j,loading:y,onLoadPromptPart:r,testId:A?"codex-judge-feedback-prompt":`codex-judge-feedback-prompt-attempt-${j}`}))}function MB({task:f,loading:u,onLoadPromptPart:l,onLoadSteps:y,onLoadStep:r}){if(!f)return L($r,{title:"未选择任务",text:"从左侧队列选择任务,或提交新 Codex 任务。"});let _=sL(f);return L("div",{className:"codex-transcript codex-progressive-trace","data-testid":"codex-output"},u&&!tl(f)?L("div",{className:"codex-output-empty"},"正在加载 Trace Summary..."):null,L(wB,{task:f,loading:u,onLoadPromptPart:l}),_.length>0?_.map(($,j)=>L(nB,{key:`${$?.index||j+1}-${$?.startedAt||j}`,task:f,attempt:$,position:j,loading:u,onLoadPromptPart:l,onLoadSteps:y,onLoadStep:r})):[L(BG,{key:"execution",task:f,loading:u,onLoadSteps:y,onLoadStep:r}),L(XG,{key:"final",task:f}),L(YG,{key:"judge",task:f})])}function SB({task:f}){let u=pL(f);if(!f||u.length===0)return L($r,{title:"暂无原始消息",text:"原始 Codex app-server 消息会保留在任务 JSON 中。"});return L("details",{className:"codex-raw-output"},L("summary",null,`原始 messages (${u.length})`),L("div",null,u.map((l)=>L("article",{key:`${l.seq}-${l.channel}`,className:`codex-output-line ${l.channel||"system"}`},L("div",{className:"codex-output-meta"},L("span",{className:"codex-output-channel"},JB(String(l.channel||"system"))),L("span",null,Kf(l.at)),l.method?L("code",null,l.method):null),L("pre",null,String(l.text||""))))))}function PB({task:f}){let u=NG(f).slice().reverse();if(u.length===0)return L($r,{title:"尚无 attempt",text:"任务开始运行后,这里会记录 Codex 终态、传输中断和 stderr tail。"});return L("div",{className:"table-wrap codex-attempt-table"},L("table",null,L("thead",null,L("tr",null,L("th",null,"#"),L("th",null,"模式"),L("th",null,"终态"),L("th",null,"传输"),L("th",null,"退出"),L("th",null,"完成时间"))),L("tbody",null,u.map((l)=>L("tr",{key:`${l.index}-${l.startedAt}`},L("td",null,l.index),L("td",null,l.mode),L("td",null,L(_r,{status:l.terminalStatus||"unknown"},l.terminalStatus||"unknown")),L("td",null,l.transportClosedBeforeTerminal?L(_r,{status:"failed"},"closed-before-terminal"):L(_r,{status:"succeeded"},"normal")),L("td",null,`code=${l.appServerExitCode??"--"} signal=${l.appServerSignal??"--"}`),L("td",null,Kf(l.finishedAt)))))))}function wG({microservices:f,onRaw:u,apiBaseUrl:l="/api",initialTasksData:y=null,standalone:r=!1}){let _=f.find((h)=>h.id==="code-queue")||null,$=OB(y),j=String($?.id||""),A=new Map;if($!==null&&j.length>0)A.set(j,{task:$,maxSeq:J8(Array.isArray($.transcript)?$.transcript:[]),complete:Boolean($._transcriptComplete),completeUpdatedAt:$._transcriptComplete?String($.updatedAt||""):""});let F=typeof performance>"u"?0:performance.now(),U=vu(j),Q=vu(0),W=vu(0),G=vu(0),K=vu(!1),E=vu(!1),O=vu(!1),z=vu(null),Z=vu(new Map),N=vu(new Map),H=vu(new Map),Y=vu(new Map),w=vu(new Set),V=vu(!1),X=vu(Boolean(y)),i=vu(new Map),m=vu(new Set),M=vu(A),c=vu(y),[C,T]=If(null),[R,P]=If(y),[n,B]=If(j),[D,I]=If($),[p,k]=If(!1),[_f,S]=If(""),[e,$f]=If(null),[Qf,Af]=If(!1),[zf,Hf]=If(!1),[Zf,b]=If(""),[t,a]=If(""),[Nf,o]=If("default"),[uf,qf]=If($y),[xf,tf]=If("main-server"),[df,lu]=If("gpt-5.5"),[Ou,mu]=If("/root/unidesk"),[R0,ou]=If(99),[_0,x0]=If(1),[au,Jf]=If(!1),[Sf,$0]=If(!1),[nf,pu]=If(""),[du,Iu]=If(""),[iu,ll]=If(""),[v0,a_]=If(!0),[wy,Dy]=If(()=>typeof window>"u"?!0:window.matchMedia(ML).matches),[d,Bf]=If(!1),[Mf,Pf]=If(""),[ju,gf]=If(""),[Ju,eu]=If(""),[El,N6]=If(""),[d_,Z6]=If(!1),[B0,xl]=If(y?{phase:"complete",taskId:j,queueMs:0,detailMs:0,totalMs:F,chunks:$?1:0,transcriptRows:Array.isArray($?.transcript)?$.transcript.length:0,partial:Boolean(y?.selected?.hasMore||W3($)),completedAt:new Date}:null),[E6,H6]=If(y?new Date:null),[e_,O6]=If(!1),Hl=Dr(y0(R)),f$=Hl.filter(E1),Pu=R?.queue||C?.body?.queue||C?.queue||{},d2=zB(R,Pu),q6=yy(R),r1=ez(Pu,Nf),Xr=G3(r1,uf),Ty=Number((M0(uf)?Pu?.total:Xr?.total)??q6.total??Hl.length),u$=z3(Pu),e2=M0(uf)?u$:[String(G3(r1,uf)?.activeTaskId||"")].filter(Boolean),Yr=fG(Pu,r1,uf,Hl),OJ=M0(uf)?d7(Pu):d7(Xr||{}),V6=d7(Pu),f5=QB(V6),u5=Math.max(WB(V6),u$.length),l5=cu((M0(uf)?Pu?.unreadTerminal:Xr?.unreadTerminal)??f$.length),w1=R?f$.length:l5,ny=M0(uf)?"All queues":$j(Xr||{id:uf,name:uf}),My=_j(_f),X0=My.length>0,L6=X0?Dr(y0(e)):[],l$=yy(e),yl=X0?L6:Hl,B6=yl.filter(E1),y5=yl.filter((h)=>!nl(h)),r5=yl.filter((h)=>nl(h)&&!E1(h)),y$=X0?l$:q6,D1=X0?Number(l$.total??L6.length):Ty,r$=y$.hasMore===!0&&String(y$.nextBeforeId||"").length>0,_$=X0?zf:e_,_5=_?DL(_):{},$$=_?TL(_):{},j$=oz(()=>IL(Zf),[Zf]),Ol=oz(()=>{let h=yG(_0);return j$.flatMap((g)=>Array.from({length:h},()=>gL(g,t)))},[j$,_0,t]),T1=Ol.length,qJ=T1>1&&!au,EH=Sf||d||T1===0||qJ,VJ=qB(Pu,df),LJ=VB(Pu,xf),HH=Q8(Pu,xf),$5=D?.id&&D?.activeTurnId&&String(D?.status)==="running",OH=D?.id&&!["succeeded","failed","canceled"].includes(String(D?.status||"")),qH=D?.id&&["succeeded","failed","canceled"].includes(String(D?.status||"")),wr=D?.id&&qG(D);function _1(h){let g=typeof h==="function"?h(c.current):h;return c.current=g,P(g),g}function VH(h,g,rf=!0){let Ff=Array.from(new Set(h.map((Of)=>String(Of||"")).filter(Boolean)));for(let Of of Ff)if(i.current.set(Of,g),rf)m.current.add(Of);return Ff}function BJ(h){for(let g of h.map((rf)=>String(rf||"")).filter(Boolean))i.current.delete(g),m.current.delete(g)}function j5(h){let g=String(h?.id||""),rf=g?i.current.get(g):void 0;if(!rf)return h;if(String(h?.status||"").length>0&&!nl(h))return i.current.delete(g),m.current.delete(g),h;return{...h,readAt:h?.readAt||rf,terminalUnread:!1}}function A5(h){let g=String(h?.id||"");return g.length>0&&m.current.has(g)&&nl(h)}function Dr(h,g=!0){let rf=[];for(let Ff of Array.isArray(h)?h:[]){let Of=j5(Ff);if(g&&A5(Of))continue;rf.push(Of)}return rf}function LH(h,g=!0){if(!h||!Array.isArray(h?.tasks))return h;let rf=Dr(y0(h),g),Ff=yy(h);return{...h,tasks:rf,pagination:h.pagination?{...Ff,returned:rf.length}:h.pagination}}function BH(h){let g=String(h||Pu?.mainProviderId||"main-server").trim()||"main-server";tf(g),mu(Q8(Pu,g))}function X6(h,g,rf=null,Ff=null){let Of=new Set(VH(h,g));if(Of.size===0&&Ff===null&&rf===null)return;_1((Ef)=>{if(!Ef)return Ef;let Cf=y0(Ef).flatMap((bf)=>{let kf=String(bf?.id||"");if(!Of.has(kf)){let yu=j5(bf);return A5(yu)?[]:[yu]}let hf=Ff&&String(Ff?.id||"")===kf?Ff:{},ef={...bf,...hf,readAt:g,terminalUnread:!1};return A5(ef)?[]:[ef]});return{...Ef,queue:rf||Ef.queue,tasks:Of.size>0?G_([Cf],Yr):Cf}});for(let Ef of Of){let Cf=M.current.get(Ef);if(Cf?.task){let bf=Ff&&String(Ff?.id||"")===Ef?Ff:{},kf={...Cf.task,...bf,readAt:g,terminalUnread:!1};if(M.current.set(Ef,{...Cf,task:kf}),U.current===Ef)I(kf)}}}ry(()=>{Jf(!1)},[Zf,_0,t]),ry(()=>{let h=_j(_f);W.current+=1;let g=W.current;if(!_||h.length===0){$f(null),Af(!1),Hf(!1),O.current=!1;return}Af(!0),$f(null);let rf=window.setTimeout(()=>{(async()=>{try{let Ff=await uG(l,{},uf,h);if(g!==W.current)return;$f(LH(Ff))}catch(Ff){if(g===W.current)$f(null),Pf(Wl(Ff,"搜索 Codex tasks 失败"))}finally{if(g===W.current)Af(!1)}})()},240);return()=>window.clearTimeout(rf)},[_?.id,l,uf,_f]),ry(()=>{Iu(D?U8(D):""),ll(Array.isArray(D?.referenceTaskIds)?D.referenceTaskIds.join(" "):"")},[n]);function n1(h,g,rf){let Ff=M.current.get(h)||{},Of=Ff.task||{},Ef=Array.isArray(Of.transcript)?Of.transcript:[],Cf=HB(Of,g),bf=Object.prototype.hasOwnProperty.call(g,"transcript")?uj(Ef,Array.isArray(g.transcript)?g.transcript:[]):Ef,kf={...Of,...Cf,transcript:bf,output:Array.isArray(Cf.output)?Wj(Of,Cf,"output"):Array.isArray(Of.output)?Of.output:[],events:Array.isArray(Cf.events)?Wj(Of,Cf,"events"):Array.isArray(Of.events)?Of.events:[]},hf=j5(kf),ef=String(hf?.updatedAt||""),yu=Boolean(g._transcriptComplete)&&nl(hf),K0=Boolean(Ff.complete)&&nl(hf)&&String(Ff.completeUpdatedAt||"")===ef,ql=yu||K0,Gu={...Ff,task:hf,maxSeq:J8(bf),complete:ql,completeUpdatedAt:ql?ef:""};if(M.current.set(h,Gu),rf===G.current&&U.current===h)I(hf);return Gu}async function Y6(h,g=!1,rf,Ff){if(!_||!h)return;let Ef=M.current.get(h)?.task,Cf=String(Ef?._traceSummaryUpdatedAt||""),bf=String(Ef?.updatedAt||"");if(!g&&Ef?._traceSummaryLoaded===!0&&Cf===bf)return;let kf=h,hf=Z.current.get(kf);if(hf)return hf;let ef=G.current,yu=performance.now();if(U.current===h)k(!0);let K0=(async()=>{try{let ql=await RL(l,h);if(ef!==G.current||U.current!==h)return;let Gu=ql?.summary||{};n1(h,{id:h,status:Gu.status,updatedAt:Gu.updatedAt,startedAt:Gu.startedAt,finishedAt:Gu.finishedAt,currentAttempt:Gu.currentAttempt,maxAttempts:Gu.maxAttempts,finalResponse:Gu.finalResponse,lastJudge:Gu.lastJudge,lastError:Gu.lastError,attempts:Array.isArray(Gu.attempts)?Gu.attempts:[],timing:Gu.timing,_traceSummary:Gu,_traceSummaryLoaded:!0,_traceSummaryUpdatedAt:String(Gu.updatedAt||""),_detailLoaded:!0},ef),xl({phase:"complete",taskId:h,queueMs:Ff??0,detailMs:performance.now()-yu,totalMs:rf===void 0?performance.now()-yu:performance.now()-rf,chunks:1,transcriptRows:Number(Gu?.execution?.stepCount||0),partial:!1,completedAt:new Date})}finally{if(Z.current.delete(kf),ef===G.current&&U.current===h)k(!1)}})();Z.current.set(kf,K0),await K0}async function XH(h,g=null){let rf=U.current;if(!_||!rf||!h)return;let Ff=M.current.get(rf)?.task,Of=W8(Ff),Ef=h==="feedback"||h==="judge-feedback"?HG(g):h;if(Of[Ef]?.text)return;let Cf=`${rf}:${Ef}`,bf=N.current.get(Cf);if(bf)return bf;let kf=G.current;if(U.current===rf)k(!0);let hf=(async()=>{try{let ef=await xL(l,rf,h,g);if(kf!==G.current||U.current!==rf)return;let yu=M.current.get(rf)?.task,K0=W8(yu);n1(rf,{...h==="full"?{prompt:String(ef?.text||""),promptChars:Number(ef?.chars||0)}:{},_promptDetails:{...K0,[Ef]:ef}},kf)}finally{if(N.current.delete(Cf),kf===G.current&&U.current===rf)k(!1)}})();N.current.set(Cf,hf),await hf}async function YH(h=null){let g=U.current;if(!_||!g)return;let rf=M.current.get(g)?.task,Ff=h===null||h===void 0||String(h).length===0?"":String(h);if(OG(rf,Ff||null))return;let Of=`${g}:${Ff||"all"}`,Ef=H.current.get(Of);if(Ef)return Ef;let Cf=G.current;if(U.current===g)k(!0);let bf=(async()=>{try{let kf=await vL(l,g,0,500,Ff||null);if(Cf!==G.current||U.current!==g)return;let hf=Array.isArray(kf?.steps)?kf.steps:[];if(Ff){let ef=M.current.get(g)?.task,yu=s0(ef?._traceStepsByAttempt)||{},K0=s0(ef?._traceStepsLoadedByAttempt)||{};n1(g,{_traceStepsByAttempt:{...yu,[Ff]:hf},_traceStepsLoadedByAttempt:{...K0,[Ff]:!0}},Cf)}else n1(g,{_traceSteps:hf,_traceStepsLoaded:!0,_traceStepsHasMore:Boolean(kf?.hasMore),_traceStepsNextAfterSeq:kf?.nextAfterSeq},Cf)}finally{if(H.current.delete(Of),Cf===G.current&&U.current===g)k(!1)}})();H.current.set(Of,bf),await bf}async function wH(h){let g=U.current,rf=String(h??"");if(!_||!g||rf.length===0)return;let Ff=M.current.get(g)?.task;if(Uj(Ff)[rf]?.line)return;let Ef=`${g}:${rf}`,Cf=Y.current.get(Ef);if(Cf)return Cf;let bf=G.current;if(U.current===g)k(!0);let kf=(async()=>{try{let hf=await bL(l,g,h);if(bf!==G.current||U.current!==g)return;let ef=M.current.get(g)?.task,yu=Uj(ef);n1(g,{_traceStepDetails:{...yu,[rf]:hf}},bf)}finally{if(Y.current.delete(Ef),bf===G.current&&U.current===g)k(!1)}})();Y.current.set(Ef,kf),await kf}async function mP(h,g,rf){if(!_||!h)return;let Ff=performance.now(),Of=G.current,Ef=M.current.get(h);if(Ef?.task){if(I(Ef.task),k(W3(Ef.task)||!Ef.complete),!W3(Ef.task)&&Ef.complete&&nl(Ef.task)&&String(Ef.completeUpdatedAt||"")===String(Ef.task?.updatedAt||"")){xl({phase:"complete",taskId:h,queueMs:rf??0,detailMs:0,totalMs:g===void 0?0:performance.now()-g,chunks:0,transcriptRows:Array.isArray(Ef.task.transcript)?Ef.task.transcript.length:0,completedAt:new Date});return}}else k(!0);let Cf=z.current;if(Cf?.taskId===h&&Cf.token===Of)return Cf.promise;let bf=(async()=>{try{let kf=await Du(Tu(l,`/api/tasks/${encodeURIComponent(h)}?meta=1`));if(Of!==G.current||U.current!==h)return;let hf=M.current.get(h),ef=Array.isArray(hf?.task?.transcript)?hf.task.transcript:[],yu=kf?.task||{},K0=Boolean(hf?.complete)&&String(hf?.completeUpdatedAt||"")===String(yu?.updatedAt||"");n1(h,{...yu,summaryOnly:!1,_metaLoaded:!0,transcript:ef,_detailLoaded:ef.length>0,_transcriptComplete:K0},Of);let ql=W3(hf?.task)||Boolean(hf?.task?._transcriptPreview),Gu=ql?0:ef.length>0?AG(ef):0,M1=!ql&&hf?.complete&&nl(yu)&&String(hf?.completeUpdatedAt||"")===String(yu?.updatedAt||"")?J8(ef):Gu,Tr=!0,w6=0,D6=ef.length;while(Tr){let _l=await Du(Tu(l,`/api/tasks/${encodeURIComponent(h)}/transcript?afterSeq=${encodeURIComponent(String(M1))}&limit=${XL}&fullText=1`));if(Of!==G.current||U.current!==h)return;let vl=M.current.get(h),Sy=Array.isArray(vl?.task?.transcript)?vl.task.transcript:[],Py=uj(Sy,Array.isArray(_l?.transcript)?_l.transcript:[]);w6+=1,D6=Py.length;let gu=Boolean(!_l?.hasMore);if(n1(h,{status:_l?.status||yu.status,updatedAt:_l?.updatedAt||yu.updatedAt,transcript:Py,_detailLoaded:gu||Py.length>0,_transcriptComplete:gu,_transcriptPreview:ql&&!gu},Of),Tr=Boolean(_l?.hasMore),M1=Number(_l?.nextAfterSeq??J8(Py)),!Tr)break;await new Promise((MJ)=>window.setTimeout(MJ,0))}xl({phase:"complete",taskId:h,queueMs:rf??0,detailMs:performance.now()-Ff,totalMs:g===void 0?performance.now()-Ff:performance.now()-g,chunks:w6,transcriptRows:D6,completedAt:new Date})}finally{if(z.current?.taskId===h&&z.current?.token===Of)z.current=null;if(Of===G.current&&U.current===h)k(!1)}})();z.current={taskId:h,token:Of,promise:bf},await bf}async function b0(h=U.current,g=!0,rf=uf){if(!_)return;if(!g&&V.current)return;let Ff=performance.now();if(g)V.current=!0;if(g)xl({phase:"loading",taskId:String(h||U.current||""),startedAt:new Date});let Of=Q.current+1;Q.current=Of;let Ef=String(h||U.current||""),Cf=Ef?M.current.get(Ef):null,bf=Array.isArray(Cf?.task?.transcript)?Cf.task.transcript:[],kf=AG(bf),hf=C||{},ef=null;try{ef=await iL(l,Ef,kf,rf)}catch{ef=await uG(l,hf,rf)}if(Of!==Q.current){if(g)V.current=!1;return}let yu=performance.now()-Ff;T(hf);let K0=ef?.queue||{},ql=String(K0?.activeTaskId||z3(K0)[0]||""),Gu=ef;_1((N0)=>{let A$=y0(ef),Cy=y0(N0),F$=Cy.length>0?G_([Cy,A$],ql):G_([A$],ql),n6=Dr(F$),kH=yy(ef),M6=yy(N0),tH=Cy.length>A$.length&&(M6.hasMore===!1||String(M6.nextBeforeId||"").length>0),sH={...kH,...tH?{hasMore:M6.hasMore,nextBeforeId:M6.nextBeforeId}:{},returned:n6.length};return Gu={...ef,tasks:n6,pagination:sH},Gu});let M1=y0(Gu),Tr=ez(K0,Nf),w6=fG(K0,Tr,rf,M1),D6=cL(Tr,rf,M1),_l=Ef||U.current,vl=Gu?.selected||null,Sy=vl?.task||null,Py=Array.isArray(vl?.transcript)?vl.transcript:null,gu=_l&&(M1.some((N0)=>N0.id===_l)||String(Sy?.id||"")===_l)?_l:w6||D6||M1[0]?.id||"";if(U.current!==gu)G.current+=1;U.current=gu,B(gu);let T6=M1.find((N0)=>N0.id===gu);if(T6){let N0=M.current.get(gu);if(N0?.task)M.current.set(gu,{...N0,task:{...T6,...N0.task,status:T6.status,updatedAt:T6.updatedAt}})}if(Sy?.id===gu&&Py!==null){let N0=M.current.get(gu),A$=Array.isArray(N0?.task?.transcript)?N0.task.transcript:[],Cy=uj(A$,Py),F$=Boolean(vl?.preview);if(n1(gu,{...Sy,_summaryLoaded:!0,transcript:Cy,_detailLoaded:!vl?.hasMore||Cy.length>0,_transcriptComplete:!F$&&!vl?.hasMore&&nl(Sy),_transcriptPreview:F$},G.current),k(!1),g)xl({phase:"complete",taskId:gu,queueMs:yu,detailMs:Math.max(0,performance.now()-Ff-yu),totalMs:performance.now()-Ff,chunks:1,transcriptRows:Cy.length,partial:Boolean(F$||vl?.hasMore||W3(Sy)),completedAt:new Date});if(H6(new Date),g)V.current=!1;Y6(gu,!1,g?Ff:void 0,g?yu:void 0).catch((n6)=>Pf(Wl(n6,"加载 Codex Trace Summary 失败")));return}if(g)xl({phase:"session",taskId:gu,queueMs:yu,totalMs:yu,startedAt:new Date(Date.now()-yu)});if(gu)Y6(gu,!0,g?Ff:void 0,g?yu:void 0).catch((N0)=>Pf(Wl(N0,"加载 Codex Trace Summary 失败")));else if(G.current+=1,I(null),k(!1),g)xl({phase:"complete",taskId:"",queueMs:yu,detailMs:0,totalMs:performance.now()-Ff,chunks:0,transcriptRows:0,completedAt:new Date});if(H6(new Date),g)V.current=!1}async function XJ(){if(X0){if(!_||zf||O.current)return;let g=String(l$.nextBeforeId||"");if(!g)return;O.current=!0,Hf(!0),Pf("");try{let rf=await lG(l,uf,g,JG,My),Ff=y0(rf),Of=rf?.queue||Pu||{},Ef=String(Of?.activeTaskId||z3(Of)[0]||Yr||"");$f((Cf)=>{let bf=Dr(G_([y0(Cf),Ff],Ef)),kf=yy(rf);return{...Cf||{},queue:Of,tasks:bf,pagination:{...kf,returned:bf.length}}})}catch(rf){Pf(Wl(rf,"加载更多搜索结果失败"))}finally{O.current=!1,Hf(!1)}return}if(!_||e_||E.current)return;let h=String(yy(R).nextBeforeId||"");if(!h)return;E.current=!0,O6(!0),Pf("");try{let g=await lG(l,uf,h),rf=y0(g),Ff=g?.queue||Pu||{},Of=String(Ff?.activeTaskId||z3(Ff)[0]||Yr||"");_1((Ef)=>{let Cf=Dr(G_([y0(Ef),rf],Of)),bf=yy(g);return{...Ef||{},queue:Ff,statistics:g?.statistics||Ef?.statistics,tasks:Cf,pagination:{...bf,returned:Cf.length}}})}catch(g){Pf(Wl(g,"加载更早 Codex tasks 失败"))}finally{E.current=!1,O6(!1)}}function DH(h){let g=h.currentTarget;if(!g||_$||!r$)return;if(g.scrollHeight-g.scrollTop-g.clientHeight<120)XJ()}async function rl(h,g){Bf(!0),Pf("");try{await h()}catch(rf){Pf(Wl(rf,g))}finally{Bf(!1)}}async function F5(h){if(!h)return;try{let g=!1;try{if(navigator.clipboard?.writeText)await navigator.clipboard.writeText(h),g=!0}catch{g=!1}if(!g){let rf=document.createElement("textarea");rf.value=h,rf.style.position="fixed",rf.style.opacity="0",document.body.appendChild(rf),rf.select(),g=document.execCommand("copy"),document.body.removeChild(rf)}if(!g)throw Error("browser clipboard rejected the copy request");eu(h),gf(`已复制任务 ID:${h}`),window.setTimeout(()=>eu((rf)=>rf===h?"":rf),1600)}catch(g){Pf(`复制任务 ID 失败:${Wl(g)}`)}}function J5(h){if(!h)return;a(h),gf(`已引用任务 ID:${h};提交时后端会读取并注入该任务上下文`)}async function U5(h){if(!_||!h)return;let g=new Date().toISOString();Q.current+=1,X6([h],g,null,{id:h,readAt:g,terminalUnread:!1}),N6(h);let rf=!1;if(await rl(async()=>{let Ff=await hL(l,h),Of=Ff?.task||{id:h,readAt:new Date().toISOString(),terminalUnread:!1},Ef=String(Of?.readAt||new Date().toISOString());X6([h],Ef,Ff?.queue||null,Of),rf=!0,gf(`已将任务 ${h} 标为已读`)},"标记 Codex task 已读失败"),!rf)BJ([h]),b0(U.current,!1).catch((Ff)=>Pf(Wl(Ff,"刷新 Codex tasks 失败")));N6((Ff)=>Ff===h?"":Ff)}async function TH(){if(!_||d_)return;Z6(!0);let h=new Date().toISOString(),g=Array.from(new Set([...y0(c.current).filter(E1).map((Ff)=>String(Ff?.id||"")).filter(Boolean),...Array.from(M.current.entries()).filter(([,Ff])=>E1(Ff?.task)).map(([Ff])=>Ff)]));if(Q.current+=1,g.length>0)X6(g,h);let rf=!1;if(await rl(async()=>{let Ff=await mL(l),Of=String(Ff?.readAt||new Date().toISOString()),Ef=y0(c.current).filter(E1).map((hf)=>String(hf?.id||"")).filter(Boolean),Cf=Array.from(M.current.entries()).filter(([,hf])=>E1(hf?.task)).map(([hf])=>hf),bf=Array.from(new Set([...g,...Ef,...Cf]));X6(bf,Of,Ff?.queue||null);let kf=Number(Ff?.count||bf.length);rf=!0,gf(`已将 ${kf} 个已结束未读任务标为已读`)},"全部标为已读失败"),!rf&&g.length>0)BJ(g),b0(U.current,!1).catch((Ff)=>Pf(Wl(Ff,"刷新 Codex tasks 失败")));Z6(!1)}function nH(h){let g=h||$y;if(qf(g),!M0(g))o(g);if(_1(null),!(M0(g)?U.current:""))U.current="",G.current+=1,B(""),I(null),k(!0)}async function MH(){let h=typeof window>"u"?"":window.prompt("输入新的 Codex queue ID(字母/数字/._-,最长 64)","new-lane"),g=String(h||"").trim();if(!g)return;await rl(async()=>{let rf=await Du(Tu(l,"/api/queues"),{method:"POST",body:{queueId:g}}),Ff=String(rf?.queue?.id||g);o(Ff),qf(Ff),_1(null),U.current="",G.current+=1,B(""),I(null),gf(`已创建并切换到 queue:${Ff}`),await b0("",!0,Ff)},"创建 Codex queue 失败")}async function SH(){let h=String(Nf||"default").trim()||"default",g=G3(r1,h)||{id:h,name:h},rf=typeof window>"u"?null:window.prompt(`输入 queue 显示名称(ID 不变:${h};留空恢复为 ID)`,KG(g));if(rf===null)return;await rl(async()=>{let Ff=await Du(Tu(l,`/api/queues/${encodeURIComponent(h)}`),{method:"PATCH",body:{name:String(rf)}}),Of=Ff?.queue||{id:h,name:String(rf||h)};if(Ff?.summary)_1((Ef)=>Ef?{...Ef,queue:Ff.summary}:Ef);gf(`已更新 queue 名称:${$j(Of)}`),await b0(U.current,!0,uf)},"修改 Codex queue 名称失败")}async function PH(h){if(h.preventDefault(),K.current){gf("任务正在提交中,请等待当前请求完成,已阻止重复提交。");return}if(Ol.length>1&&!au){Pf(`检测到将创建 ${Ol.length} 个任务;请先勾选“确认批量入队”,避免误传多个任务。`);return}K.current=!0,$0(!0),gf("正在提交 Code Queue 任务,请等待后端确认,输入已临时锁定。"),await rl(async()=>{if(Ol.length===0)throw Error("prompt 不能为空");let g=lr(t),rf=Nf.trim()||"default",Ff=[...Ol],Of=(hf)=>({prompt:hf,queueId:rf,providerId:xf,model:df,cwd:Ou,maxAttempts:Number(R0),...g.length>0?{referenceTaskIds:g}:{}}),Ef=Ff.length===1?Of(Ff[0]):{tasks:Ff.map(Of)},Cf=await Du(Tu(l,Ff.length===1?"/api/tasks":"/api/tasks/batch"),{method:"POST",body:Ef}),bf=Cf?.tasks?.[0]?.id||"",kf=Array.isArray(Cf?.tasks)?Cf.tasks.map((hf)=>String(hf?.id||"")).filter(Boolean):[];if(gf(`已创建 ${kf.length||Ff.length} 个任务${kf.length>0?`:${kf.join(" / ")}`:""}`),b(""),a(""),Jf(!1),U.current=bf,uf!==rf)_1(null);qf(rf),o(rf),await b0(bf,!0,rf)},"Codex 任务入队失败"),K.current=!1,$0(!1)}async function CH(h){if(h.preventDefault(),!D?.id)return;await rl(async()=>{await Du(Tu(l,`/api/tasks/${encodeURIComponent(D.id)}/steer`),{method:"POST",body:{prompt:nf}}),pu(""),await b0(D.id)},"追加 prompt 失败")}async function cH(h){h.preventDefault();let g=String(D?.id||"");if(!g||!wr)return;await rl(async()=>{let rf=lr(iu),Ff=await Du(Tu(l,`/api/tasks/${encodeURIComponent(g)}/edit`),{method:"POST",body:{prompt:du,referenceTaskIds:rf}}),Of={...Ff?.task||D||{},_traceSummary:null,_traceSummaryLoaded:!1,_traceSummaryUpdatedAt:"",_promptDetails:{},_traceSteps:[],_traceStepsLoaded:!1,_traceStepsByAttempt:{},_traceStepsLoadedByAttempt:{},_traceStepDetails:{}};M.current.set(g,{...M.current.get(g)||{},task:Of,complete:!1,completeUpdatedAt:""}),U.current=g,I(Of),B(g),Iu(U8(Of)),ll(Array.isArray(Of?.referenceTaskIds)?Of.referenceTaskIds.join(" "):""),_1((Ef)=>{if(!Ef)return Ef;let Cf=y0(Ef).map((bf)=>String(bf?.id||"")===g?{...bf,...Of}:bf);return{...Ef,queue:Ff?.queue||Ef.queue,tasks:G_([Cf],Yr)}}),gf(Ff?.changed===!1?`任务 ${g} 的 prompt 未变化`:`已更新 queued 任务 ${g} 的用户 prompt`),await b0(g,!0,uf)},"编辑 queued 任务 prompt 失败")}async function iH(){if(!D?.id)return;await rl(async()=>{await Du(Tu(l,`/api/tasks/${encodeURIComponent(D.id)}/interrupt`),{method:"POST",body:{}}),await b0(D.id)},"打断 Codex session 失败")}async function RH(){if(!D?.id)return;await rl(async()=>{await Du(Tu(l,`/api/tasks/${encodeURIComponent(D.id)}/retry`),{method:"POST",body:{}}),await b0(D.id)},"重新入队失败")}async function xH(h){let g=String(D?.id||""),rf=String(h||"").trim();if(!g||!rf)return;let Ff=G8(D);if(rf===Ff){gf(`任务 ${g} 已在 queue=${rf}`);return}await rl(async()=>{let Ef=(await Du(Tu(l,`/api/tasks/${encodeURIComponent(g)}/move`),{method:"POST",body:{queueId:rf}}))?.task||{...D,queueId:rf};if(M.current.set(g,{...M.current.get(g)||{},task:Ef}),U.current=g,I(Ef),B(g),o(rf),!M0(uf))_1(null),qf(rf);gf(`已将任务 ${g} 从 ${Ff} 移动到 ${rf}`),await b0(g,!0,M0(uf)?$y:rf)},"移动任务 queue 失败")}async function vH(){let h=U.current;if(!h)return;let g=performance.now();await rl(async()=>{xl({phase:"session",taskId:h,queueMs:0,totalMs:0,partial:!0,startedAt:new Date}),await Y6(h,!0,g,0)},"刷新 Trace Summary 失败")}function bH(h){U.current=h,G.current+=1,B(h);let g=M.current.get(h);if(g?.task)I(g.task),k(!1);else{k(!0);let rf=Hl.find((Ff)=>Ff.id===h);if(rf)I(rf);else I(null)}b0(h).catch((rf)=>Pf(Wl(rf,"切换 Codex session 失败")))}function Q5(h){if(bH(h),SL())Dy(!1)}ry(()=>{if(X.current){X.current=!1;return}rl(()=>b0(U.current),"Code Queue 加载失败")},[_?.id,uf]),ry(()=>{if(!_)return;let h=()=>{if(!az())return;b0(U.current,!1).catch((Ff)=>Pf(Wl(Ff,"Code Queue 轮询失败")))},g=window.setInterval(()=>{h()},1500),rf=()=>{if(az())h()};return document.addEventListener("visibilitychange",rf),()=>{window.clearInterval(g),document.removeEventListener("visibilitychange",rf)}},[_?.id,uf]),ry(()=>{if(!_||!D||p)return;let h=String(D.id||"");if(!h)return;let g=String(D.updatedAt||""),rf=String(D._traceSummaryUpdatedAt||"");if(D._traceSummaryLoaded===!0&&rf===g)return;let Ff=`${h}:${g||"unknown"}`;if(w.current.has(Ff))return;w.current.add(Ff),Y6(h,!0).catch((Of)=>Pf(Wl(Of,"自动加载 Trace Summary 失败")))},[_?.id,D?.id,D?.updatedAt,D?._traceSummaryUpdatedAt,D?._traceSummaryLoaded,p]);let hH=yl.length===0?L($r,{title:X0?Qf?"搜索中":"没有匹配任务":"队列为空",text:X0?Qf?`正在搜索包含“${My}”的 task...`:`未找到包含“${My}”的 task;可换个关键词或切换 queue。`:"提交一个任务后,Codex 会串行执行并保存输出。"}):[B6.length>0?L(lj,{key:"unread",title:"已结束未读",tasks:B6,selectedId:n,emptyText:"暂无已结束未读任务。",onSelect:Q5,onCopy:F5,onReference:J5,onMarkRead:U5,copiedTaskId:Ju,markingReadTaskId:El}):null,L(lj,{key:"active",title:"运行 / 排队",tasks:y5,selectedId:n,emptyText:"当前没有运行或排队任务。",onSelect:Q5,onCopy:F5,onReference:J5,onMarkRead:U5,copiedTaskId:Ju,markingReadTaskId:El}),L(lj,{key:"history",title:"历史 session",tasks:r5,selectedId:n,emptyText:"最近没有完成、失败或取消的 session。",onSelect:Q5,onCopy:F5,onReference:J5,onMarkRead:U5,copiedTaskId:Ju,markingReadTaskId:El}),L("div",{key:"pagination",className:"codex-task-pagination","data-testid":"codex-task-pagination"},L("span",null,X0?`搜索“${My}” · 已显示 ${yl.length} / ${Number.isFinite(D1)?D1:yl.length}`:`已加载 ${yl.length} / ${Number.isFinite(D1)?D1:yl.length}`),r$?L("button",{type:"button",className:"ghost-btn",disabled:_$,onClick:()=>void XJ(),"data-testid":"codex-load-more-tasks-button"},_$?"加载中":X0?"加载更多结果":"加载更早任务"):L("code",null,X0?"已到结果末尾":"已到队列末尾"))],YJ=(h,g=!1)=>L("label",{className:`code-queue-switcher ${g?"compact":""}`},L("span",null,g?"Queue":"查看 queue"),L("select",{value:uf,onChange:(rf)=>nH(String(rf.target.value||$y)),"data-testid":h},L("option",{value:$y},`All queues · ${Number.isFinite(Ty)?Ty:Hl.length} tasks · ${u$.length} running`),r1.map((rf)=>L("option",{key:String(rf?.id||""),value:String(rf?.id||"")},jj(rf))))),mH=L("div",{className:"codex-task-search","data-testid":"codex-task-search"},L("label",{htmlFor:"codex-task-search-input"},"搜索 task"),L("div",{className:"codex-task-search-row"},L("input",{id:"codex-task-search-input",type:"search",value:_f,placeholder:"关键词 / task ID / prompt",autoComplete:"off",onChange:(h)=>S(String(h.target.value||"")),"data-testid":"codex-task-search-input"}),_f?L("button",{type:"button",className:"ghost-btn",onClick:()=>S(""),"data-testid":"codex-task-search-clear"},"清除"):null),L("small",{"data-testid":"codex-task-search-summary"},X0?Qf?"搜索中...":`匹配 ${yl.length}/${Number.isFinite(D1)?D1:yl.length}`:"支持 task ID、prompt、状态、provider、模型和最近输出关键词")),pH=L("div",{className:"codex-trace-status","data-testid":"codex-trace-status-summary"},L("span",{className:"codex-trace-status-chip queued"},L("b",null,"排队"),String(f5)),L("span",{className:"codex-trace-status-chip running"},L("b",null,"运行"),String(u5)),L("span",{className:`codex-trace-status-chip unread ${w1>0?"warn":""}`},L("b",null,"结束未读"),String(w1)),L("span",{className:"codex-trace-status-chip service"},L("b",null,"服务"),`${_5.providerStatus||"unknown"} · ${_?.providerId||"main-server"} · ${$$.public?"公网暴露":"仅 UniDesk frontend 代理访问"}`),L("span",{className:"codex-trace-status-chip"},L("b",null,"执行节点"),LJ.map((h)=>h.id).join(" / ")),L("span",{className:"codex-trace-status-chip"},L("b",null,"模型"),VJ.join(" / ")),L("span",{className:"codex-trace-status-chip"},L("b",null,"加载"),B0?.phase==="complete"?wL(B0?.totalMs):String(B0?.phase||"idle")),L("span",{className:"codex-trace-status-chip"},L("b",null,"刷新"),E6?Uu(E6):"--")),IH=L(K_,{title:D?`Trace ${String(D.id).slice(0,22)}`:"Trace 输出",eyebrow:D?`${D.status} / view=${ny} / task queue=${G8(D)} / provider=${D.providerId||"main-server"} / ${D.model} / agent loop trace`:`Agent loop trace / view=${ny}`,summary:pH,loading:p||e_||Qf||zf||B0?.phase==="loading",actions:L("div",{className:"panel-actions"},YJ("code-queue-filter-select"),L("button",{type:"button",className:"ghost-btn codex-mark-all-read-btn",disabled:w1===0||d||d_,onClick:()=>void TH(),"data-testid":"codex-mark-all-read-button"},d_?"标记中":`全部标已读${w1>0?` (${w1})`:""}`),D?L("button",{type:"button",className:"ghost-btn",disabled:p||d,onClick:()=>void vH(),"data-testid":"codex-load-full-trace-button"},p?"加载中":tl(D)?"刷新 Summary":"加载 Summary"):null,L("button",{type:"button",className:"codex-session-title-toggle",onClick:()=>Dy((h)=>!h),"data-testid":"code-queue-sidebar-toggle"},wy?"收起队列":"展开队列"),L("label",{className:"inline-check"},L("input",{type:"checkbox",checked:v0,onChange:(h)=>a_(Boolean(h.target.checked))}),"自动滚动"),L("button",{type:"button",className:"ghost-btn",disabled:!OH||d,onClick:()=>void iH(),"data-testid":"codex-interrupt-button"},"打断"),L("button",{type:"button",className:"ghost-btn",disabled:!qH||d,onClick:()=>void RH()},"重试"),D?L(zG,{title:"Codex Task",data:D,onOpen:u,testId:"raw-codex-task"}):null),className:"codex-output-panel"},L("div",{className:`codex-session-shell ${wy?"":"queue-collapsed"}`},wy?L("aside",{className:"codex-session-sidebar","data-testid":"codex-session-sidebar"},L("div",{className:"codex-session-sidebar-head"},L("div",null,L("span",null,M0(uf)?"All queues":"Queue lane"),L("strong",null,`${ny} · ${Hl.length}/${Number.isFinite(Ty)?Ty:Hl.length} sessions · 未读 ${w1}`)),L("button",{type:"button",className:"ghost-btn",onClick:()=>Dy(!1)},"收起")),YJ("code-queue-filter-sidebar",!0),mH,L("div",{className:"codex-task-list codex-task-list-session",onScroll:DH,"data-testid":"codex-task-list-scroll"},hH)):null,L("div",{className:"codex-session-main"},L("div",{className:"codex-output-stack"},L(MB,{task:D,loading:p,onLoadPromptPart:XH,onLoadSteps:YH,onLoadStep:wH}),L(SB,{task:D})))));if(!_)return L($r,{title:"Code Queue 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=code-queue"});let wJ=Number(B0?.totalMs),DJ=Number(B0?.queueMs),TJ=Number(B0?.detailMs),nJ=Number(B0?.transcriptRows),gH=B0?.phase==="complete"?"complete":String(B0?.phase||"idle");return L("div",{className:`code-queue-page ${r?"codex-standalone-page":""}`,"data-testid":"code-queue-page","data-load-state":gH,"data-load-total-ms":Number.isFinite(wJ)?String(Math.round(wJ*10)/10):"","data-load-queue-ms":Number.isFinite(DJ)?String(Math.round(DJ*10)/10):"","data-load-detail-ms":Number.isFinite(TJ)?String(Math.round(TJ*10)/10):"","data-load-transcript-rows":Number.isFinite(nJ)?String(nJ):"","data-load-task-id":String(B0?.taskId||n||""),"data-load-partial":B0?.partial?"true":"false"},L(Au,{error:Mf,wide:!0}),ju?L("div",{className:"form-success wide","data-testid":"codex-create-success"},ju):null,L("div",{className:"codex-session-stage codex-session-stage-top"},IH),L("div",{className:"code-queue-layout"},L("div",{className:"codex-left-rail"},L(K_,{title:"提交任务",eyebrow:Sf?"Submitting...":Ol.length>1?`${Ol.length} tasks`:"Single or Batch",className:"codex-compose-panel",loading:Sf},L("form",{className:`codex-task-form ${Sf?"is-submitting":""}`,onSubmit:PH,"data-testid":"code-queue-task-form","aria-busy":Sf?"true":"false"},L("label",null,"Prompt / 多任务用单独一行 --- 分隔",L("textarea",{value:Zf,rows:8,disabled:Sf,onChange:(h)=>b(h.target.value),placeholder:"写入 Codex 任务;多个任务之间用 --- 分隔。"})),L("label",{className:"codex-reference-field"},"引用任务 ID(可选)",L("input",{value:t,disabled:Sf,onChange:(h)=>a(h.target.value),placeholder:"codex_...;支持空格/逗号分隔多个 ID","data-testid":"codex-reference-task-id"}),lr(t).length>0?L("code",null,`后端将解析并注入:${lr(t).join(" / ")}`):null),L("div",{className:"codex-form-grid"},L("label",{className:"codex-submit-queue-field"},"Queue",L("div",{className:"codex-submit-queue-row"},L("select",{value:Nf,disabled:Sf,onChange:(h)=>o(String(h.target.value||"default")),"data-testid":"code-queue-id-select"},r1.map((h)=>L("option",{key:String(h?.id||""),value:String(h?.id||"")},jj(h)))),L("button",{type:"button",className:"ghost-btn codex-rename-queue-btn",onClick:()=>void SH(),disabled:d||Sf||!Nf,title:"修改当前 queue 的显示名称,ID 不变","data-testid":"codex-rename-queue-button"},"改名"),L("button",{type:"button",className:"ghost-btn codex-create-queue-btn",onClick:()=>void MH(),disabled:d||Sf,"data-testid":"codex-create-queue-button"},"创建 queue"))),L("label",null,"模型",L("select",{value:df,disabled:Sf,onChange:(h)=>lu(h.target.value),"data-testid":"codex-model-select"},VJ.map((h)=>L("option",{key:h,value:h},h)))),L("label",null,"执行 Provider",L("select",{value:xf,disabled:Sf,onChange:(h)=>BH(String(h.target.value||"main-server")),"data-testid":"codex-provider-select"},LJ.map((h)=>L("option",{key:h.id,value:h.id},`${h.label||h.id} · ${h.defaultWorkdir||Q8(Pu,h.id)}`)))),L("label",null,"工作目录",L("input",{value:Ou,disabled:Sf,onChange:(h)=>mu(h.target.value),placeholder:HH||Pu?.defaultWorkdir||"/root/unidesk","data-testid":"codex-cwd-input"})),L("label",null,"最大尝试",L("input",{type:"number",min:1,max:99,value:R0,disabled:Sf,onChange:(h)=>ou(Number(h.target.value)),"data-testid":"codex-max-attempts-input"})),L("label",null,"入队份数",L("input",{type:"number",min:1,max:50,value:_0,disabled:Sf,onChange:(h)=>x0(Number(h.target.value)),"data-testid":"codex-repeat-count-input"}))),T1>1?L("label",{className:`codex-batch-confirm ${au?"confirmed":""}`,"data-testid":"codex-batch-confirm-row"},L("input",{type:"checkbox",checked:au,disabled:Sf,onChange:(h)=>Jf(Boolean(h.target.checked)),"data-testid":"codex-batch-confirm-checkbox"}),L("span",null,`确认批量入队 ${T1} 个任务(prompt 分段 ${j$.length} × 入队份数 ${yG(_0)})`)):null,Sf?L("div",{className:"codex-submit-wait","data-testid":"codex-submit-wait"},"正在提交到后端,已锁定输入以防重复提交..."):null,L("div",{className:"codex-form-actions"},L("button",{type:"button",className:"ghost-btn",disabled:d||Sf||Zf.length===0&&t.length===0,onClick:()=>{b(""),a(""),Jf(!1),gf("已清空任务输入栏")},"data-testid":"codex-clear-input-button"},"清空输入"),L("button",{type:"submit",className:"primary-btn",disabled:EH,"data-testid":"codex-enqueue-button"},Sf?"提交中,请等待...":qJ?`请确认批量入队 ${T1} 个任务`:Ol.length>1?`批量入队 ${Ol.length} 个任务`:"入队并运行"))))),L("div",{className:"codex-main-stage"},L("div",{className:"codex-detail-grid"},L(K_,{title:"运行控制",eyebrow:wr?"Queued prompt editable":$5?"Active turn steer":"Steer when running",loading:d},L("div",{className:"codex-run-control-stack"},L(YB,{task:D,queueRows:r1,busy:d,onMove:xH}),D?.id?L("form",{className:"codex-steer-form codex-edit-prompt-form",onSubmit:cH,"data-testid":"codex-edit-prompt-form"},L("label",null,"编辑 queued 用户 prompt",L("textarea",{value:du,rows:5,onChange:(h)=>Iu(h.target.value),placeholder:"仅 QUEUED 且尚未开始运行的任务可在这里修改原始用户 prompt。",disabled:!wr||d,"data-testid":"codex-edit-prompt-textarea"})),L("label",{className:"codex-reference-field"},"引用任务 ID(可选,留空会清除引用)",L("input",{value:iu,disabled:!wr||d,onChange:(h)=>ll(h.target.value),placeholder:"codex_...;支持空格/逗号分隔多个 ID","data-testid":"codex-edit-reference-task-id"}),lr(iu).length>0?L("code",null,`将保留/注入:${lr(iu).join(" / ")}`):null),L("div",{className:"codex-form-actions"},L("button",{type:"button",className:"ghost-btn",disabled:!D?.id||d,onClick:()=>{Iu(D?U8(D):""),ll(Array.isArray(D?.referenceTaskIds)?D.referenceTaskIds.join(" "):"")},"data-testid":"codex-edit-prompt-reset"},"恢复当前值"),L("button",{type:"submit",className:"primary-btn",disabled:!wr||d||du.trim().length===0,title:wr?"保存后会重写尚未运行任务的用户 prompt":"只有 QUEUED 且尚未开始的任务可编辑 prompt","data-testid":"codex-edit-prompt-submit"},"保存 queued prompt"))):null,L("form",{className:"codex-steer-form",onSubmit:CH},L("label",null,"追加 prompt",L("textarea",{value:nf,rows:4,onChange:(h)=>pu(h.target.value),placeholder:"给正在运行的 Codex session 推入新的指令或纠偏。",disabled:!$5})),L("button",{type:"submit",className:"primary-btn",disabled:!$5||d||nf.trim().length===0,"data-testid":"codex-steer-button"},"推入运行中 session")))),L(K_,{title:"完成判定",eyebrow:D?.lastJudge?D.lastJudge.source:"judge",loading:p},D?.lastJudge?L("div",{className:"codex-judge-card","data-testid":"codex-task-judge-card"},L(_r,{status:D.lastJudge.decision},D.lastJudge.decision),L("strong",null,`${Math.round(Number(D.lastJudge.confidence||0)*100)}% confidence`),L("p",{"data-testid":"codex-task-judge-reason"},rj(D.lastJudge.reason||"--",180)),D.lastJudge.continuePrompt?L("code",{"data-testid":"codex-task-judge-continue-prompt"},rj(D.lastJudge.continuePrompt,160)):null):L($r,{title:"尚未判定",text:"Codex turn 结束后会由 MiniMax M2.7 或 fallback judge 判定 complete/retry/fail;retry 会在已有 thread 追加继续执行 prompt。"}))),L(XB,{stats:d2,queueName:ny,onRaw:u}),L(K_,{title:"Attempts",eyebrow:"terminal vs interruption",loading:p},L(PB,{task:D})))))}var Z3=cf(Yu(),1);var Xf=Z3.default.createElement,{useEffect:Zj}=Z3.default,Ej=Z3.default.useState,CB=Z3.default.useRef,Oj=` +:root { + --surfacePrimary: #ffffff; + --surfaceSecondary: #f8fafc; + --background: #f8fafc; + --divider: #e5e7eb; + --borderPrimary: #dbe3ee; + --textPrimary: #17212f; + --textSecondary: #64748b; + --iconSecondary: #ffffff; + --blue: #2283e8; +} +* { + box-sizing: border-box !important; +} +html, body { + font-size: 13px !important; +} +body { + margin: 0 !important; + background: var(--surfacePrimary) !important; + color: var(--textPrimary) !important; + font-family: Arial, sans-serif !important; +} +button, +input { + font: inherit !important; +} +header { + height: 42px !important; + min-height: 42px !important; + padding: 4px 8px !important; +} +header title { + font-size: 14px !important; + padding: 0 8px !important; +} +header img { + height: 28px !important; +} +header .action { + min-width: 30px !important; + height: 30px !important; + padding: 4px 6px !important; +} +header .search { + max-width: 360px !important; + height: 32px !important; +} +nav { + top: 42px !important; + width: 12rem !important; +} +nav .action { + min-height: 34px !important; + padding: 5px 8px !important; + font-size: 13px !important; +} +nav .action i { + margin-right: 6px !important; +} +nav > div { + padding: 3px 0 !important; +} +main { + width: calc(100% - 12rem) !important; + margin-left: 12rem !important; +} +main > div { + padding-top: 42px !important; +} +.material-icons { + width: 22px !important; + min-width: 22px !important; + max-width: 22px !important; + height: 22px !important; + font-family: Arial, sans-serif !important; + font-size: 0 !important; + line-height: 22px !important; + overflow: hidden !important; + text-align: center !important; + text-transform: none !important; +} +.material-icons::before { + content: "." !important; + display: inline-block !important; + font-family: Arial, sans-serif !important; + font-size: 11px !important; + font-weight: 700 !important; + line-height: 22px !important; +} +header .material-icons::before, +nav .material-icons::before { + content: "op" !important; + font-size: 10px !important; +} +#listing { + min-height: 0 !important; +} +#listing h2 { + margin: 4px 8px !important; + font-size: 11px !important; +} +#listing > div { + display: block !important; +} +#listing.mosaic, +#listing.list { + width: 100% !important; + margin: 0 !important; + padding-top: 4px !important; +} +#listing.mosaic .header, +#listing.list .header { + display: flex !important; + min-height: 26px !important; + background: var(--background) !important; +} +#listing.mosaic .item, +#listing.list .item { + width: calc(100% - 8px) !important; + min-height: 30px !important; + margin: 0 4px !important; + padding: 3px 8px !important; + border-radius: 0 !important; + border-bottom: 1px solid var(--divider) !important; + box-shadow: none !important; +} +#listing.mosaic .item div:first-of-type, +#listing.list .item div:first-of-type { + width: 30px !important; + min-width: 30px !important; + height: 24px !important; +} +#listing .item i { + margin-right: 4px !important; +} +#listing .item i::before { + content: "file" !important; + color: inherit !important; + font-size: 10px !important; +} +#listing .item[data-dir="true"] i::before { + content: "dir" !important; +} +#listing.mosaic .item div:last-of-type, +#listing.list .item div:last-of-type { + width: calc(100% - 34px) !important; + display: grid !important; + grid-template-columns: minmax(180px, 1fr) 90px 140px !important; + gap: 8px !important; + align-items: center !important; +} +#listing .item .name { + min-width: 0 !important; + font-size: 13px !important; + font-weight: 600 !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} +#listing .item .size, +#listing .item .modified { + min-width: 0 !important; + color: var(--textSecondary) !important; + font-size: 11px !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} +#multiple-selection { + height: 36px !important; + min-height: 36px !important; + padding: 4px 8px !important; +} +@media (max-width: 780px) { + nav { + width: 9rem !important; + } + main { + width: calc(100% - 9rem) !important; + margin-left: 9rem !important; + } + #listing.mosaic .item div:last-of-type, + #listing.list .item div:last-of-type { + grid-template-columns: minmax(120px, 1fr) 80px !important; + } + #listing .item .modified { + display: none !important; + } +} +`;function Hj({title:f,eyebrow:u,actions:l,children:y,className:r,loading:_}){return Xf("section",{className:`panel ${r||""}`},Xf("div",{className:"panel-head"},Xf("div",null,u?Xf("p",{className:"panel-eyebrow"},u):null,Xf(_u,{title:f,loading:_})),l?Xf("div",{className:"panel-actions"},l):null),Xf("div",{className:"panel-body"},y))}function cB({title:f,data:u,onOpen:l,testId:y}){return Xf("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:()=>l(f,u)},"查看原始JSON")}function iB({title:f,text:u}){return Xf("div",{className:"empty-state"},Xf("strong",null,f),Xf("span",null,u))}function DG(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function TG(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function nG(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function RB(f){return f.filter((l)=>l?.id==="filebrowser"||String(l?.id||"").startsWith("filebrowser-")).sort((l,y)=>{let r=(_)=>_.providerId==="D518"?0:_.providerId==="D601"?1:_.id==="filebrowser"?2:3;return r(l)-r(y)||String(l.id).localeCompare(String(y.id))})}function xB(f){if(f?.providerId==="D518")return"D518";return f?.providerId||f?.name||f?.id||"Unknown"}function vB(f,u,l="/"){let y=l.startsWith("/")?l:`/${l}`;return`${f}/microservices/${encodeURIComponent(u)}/proxy${y}`}function bB(f,u){return`${f}/microservices/${encodeURIComponent(u)}/health`}async function hB(f,u=16000){let l=new AbortController,y=setTimeout(()=>l.abort(),u);try{return await Df(f,{signal:l.signal,failureFields:[!1]})}finally{clearTimeout(y)}}function MG(f){if(f?.providerId==="main-server")return"host / -> /srv";if(f?.providerId==="D601"||f?.providerId==="D518")return"WSL / + /mnt/c -> /srv";return"provider / -> /srv"}function N8(f){return f?.status==="OK"||f?.ok===!0}function mB({service:f,active:u,health:l,onSelect:y,onRaw:r}){let _=DG(f),$=TG(f),j=nG(f),A=_.container||{},F=N8(l?.body);return Xf("button",{type:"button",className:`filebrowser-target-card ${u?"active":""}`,"data-testid":`filebrowser-target-card-${f.id}`,onClick:y},Xf("span",{className:`status-badge ${F?"ok":_.providerStatus==="online"?"running":"warn"}`},F?"Health OK":_.providerStatus||"unknown"),Xf("strong",null,f.name||f.id),Xf("span",null,MG(f)),Xf("code",null,`${$.nodeBindHost||"--"}:${$.nodePort||"--"}`),Xf("small",null,A.name?`${A.name} / ${A.state||"--"}`:`${j.composeService||"--"}`),Xf("span",{className:"filebrowser-card-raw",onClick:(U)=>{U.stopPropagation(),r(`${f.name} service`,f)}},"JSON"))}function SG(f){try{return f?.contentDocument||f?.contentWindow?.document||null}catch{return null}}function qj(f){let u=SG(f);if(u===null||u.head===null)return!1;let l=u.getElementById("unidesk-filebrowser-compact-style");if(l===null)l=u.createElement("style"),l.id="unidesk-filebrowser-compact-style",u.head.appendChild(l);if(l.textContent!==Oj)l.textContent=Oj;return!0}function pB(f,u){let l=URL.createObjectURL(f),y=document.createElement("a");y.href=l,y.download=u,document.body.appendChild(y),y.click(),y.remove(),setTimeout(()=>URL.revokeObjectURL(l),2000)}function IB(f,u){let l=SG(f);if(l===null||l.documentElement===null)throw Error("无法访问 File Browser iframe 文档");qj(f);let y=Math.max(640,Math.ceil(f.clientWidth||l.documentElement.clientWidth||1280)),r=Math.max(480,Math.ceil(f.clientHeight||l.documentElement.clientHeight||720)),_=l.documentElement.cloneNode(!0);_.querySelectorAll("script, style, link[rel='stylesheet'], link[rel='preload'], link[rel='icon']").forEach((U)=>U.remove()),_.querySelectorAll("img").forEach((U)=>{U.removeAttribute("src"),U.removeAttribute("srcset")});let $=_.querySelector("head");if($===null)$=l.createElement("head"),_.insertBefore($,_.firstChild);let j=l.createElement("style");j.textContent=`${Oj} +html,body{width:${y}px!important;min-height:${r}px!important;overflow:hidden!important;}`,$.appendChild(j);let A=new XMLSerializer().serializeToString(_),F=`${A}`;pB(new Blob([F],{type:"image/svg+xml;charset=utf-8"}),u.replace(/\.png$/i,".svg"))}function PG({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let y=RB(Array.isArray(f)?f:[]),r=new URLSearchParams(window.location.search).get("target")||"",_=r==="filebrowser-d518"?"filebrowser":r,$=y.some((w)=>w.id===_)?_:y[0]?.id||"",[j,A]=Ej($),[F,U]=Ej({loading:!1,refreshedAt:null,health:{},error:""}),[Q,W]=Ej({exporting:!1,message:"",error:""}),G=CB(null),K=y.find((w)=>w.id===j)||y[0]||null,E=DG(K),O=TG(K),z=nG(K),Z=K?F.health[K.id]:null,N=K?vB(l,K.id,"/"):"about:blank";Zj(()=>{if(y.length===0)return;if(!j||!y.some((w)=>w.id===j))A(y[0].id)},[y.map((w)=>w.id).join(",")]),Zj(()=>{let w=0,V=setInterval(()=>{if(w+=1,qj(G.current)||w>=24)clearInterval(V)},500);return()=>clearInterval(V)},[N]),Zj(()=>{if(y.length===0)return;let w=!1;async function V(){U((m)=>({...m,loading:!0,error:""}));let i=await Promise.all(y.map(async(m)=>{try{let M=await hB(bB(l,m.id));return[m.id,{ok:!0,body:M}]}catch(M){return[m.id,{ok:!1,error:wf(M,"File Browser health failed")}]}}));if(w)return;U({loading:!1,refreshedAt:new Date().toISOString(),health:Object.fromEntries(i),error:""})}V();let X=setInterval(V,30000);return()=>{w=!0,clearInterval(X)}},[y.map((w)=>`${w.id}:${w.runtime?.providerStatus||""}`).join(","),l]);function H(w){A(w);let V=new URL(window.location.href);V.searchParams.set("target",w),window.history.replaceState({},"",`${V.pathname}${V.search}`)}async function Y(){if(Q.exporting)return;W({exporting:!0,message:"",error:""});try{let w=new Date().toISOString().replace(/[-:.TZ]/g,"").slice(0,14);await IB(G.current,`unidesk-filebrowser-${K?.id||"target"}-${w}.png`),W({exporting:!1,message:"截图已导出",error:""})}catch(w){W({exporting:!1,message:"",error:wf(w,"截图导出失败")})}}if(y.length===0)return Xf(iB,{title:"File Browser 未登记",text:"请在 config.json 的 microservices 中登记 id=filebrowser 或 filebrowser-* 用户服务"});return Xf("div",{className:"filebrowser-page","data-testid":"filebrowser-page"},F.error?Xf(Au,{error:F.error,wide:!0}):null,Xf(Hj,{title:"文件管理器",eyebrow:"File Browser / Host Files",loading:F.loading,actions:Xf("div",{className:"panel-actions"},K?Xf("button",{type:"button",className:"ghost-btn",onClick:Y,disabled:Q.exporting,"data-testid":"filebrowser-export-screenshot"},Q.exporting?"导出中...":"导出截图"):null,K?Xf("a",{className:"ghost-btn",href:N,target:"_blank",rel:"noreferrer"},"新窗口打开"):null,K?Xf(cB,{title:"File Browser 当前目标",data:{service:K,health:Z},onOpen:u,testId:"raw-filebrowser-active"}):null)},Xf("div",{className:"filebrowser-hero"},Xf("div",null,Xf("span",{className:`status-badge ${N8(Z?.body)?"ok":"warn"}`},N8(Z?.body)?"Health OK":"Health Pending"),Xf("h3",null,K?.name||"File Browser"),Xf("p",{className:"muted paragraph"},K?.description||"通过 UniDesk 登录态代理访问,不开放 File Browser 公网端口。"),Q.error?Xf("p",{className:"filebrowser-shot-error"},Q.error):null,Q.message?Xf("p",{className:"filebrowser-shot-ok"},Q.message):null),Xf("div",{className:"microservice-ref-card"},Xf("span",null,"Provider"),Xf("strong",null,K?.providerId||"--"),Xf("code",null,E.providerName||K?.providerId||"--")),Xf("div",{className:"microservice-ref-card"},Xf("span",null,"Private Backend"),Xf("strong",null,`${O.nodeBindHost||"--"}:${O.nodePort||"--"}`),Xf("code",null,O.nodeBaseUrl||"--")),Xf("div",{className:"microservice-ref-card"},Xf("span",null,"Image"),Xf("strong",null,z.dockerfile||"filebrowser/filebrowser:v2.63.3"),Xf("code",null,z.commitId||"--")),Xf("div",{className:"microservice-ref-card"},Xf("span",null,"Mount"),Xf("strong",null,MG(K)),Xf("code",null,K?.providerId==="main-server"?"/root, /var, /home":"/home, /mnt/c, /mnt/d")))),Xf(Hj,{title:"浏览目标",eyebrow:`${y.length} host targets`,loading:F.loading},Xf("div",{className:"filebrowser-target-grid"},y.map((w)=>Xf(mB,{key:w.id,service:w,active:w.id===K?.id,health:F.health[w.id],onSelect:()=>H(w.id),onRaw:u})))),Xf(Hj,{title:`${xB(K)} 文件视图`,eyebrow:Z?.body?`Health ${N8(Z.body)?"OK":"UNKNOWN"} / ${F.refreshedAt?Uu(F.refreshedAt):"--"}`:"Embedded WebUI",className:"filebrowser-frame-panel"},Xf("div",{className:"filebrowser-frame-shell"},Xf("div",{className:"filebrowser-frame-toolbar"},Xf("span",null,"BaseURL"),Xf("code",null,`/api/microservices/${K?.id||"filebrowser"}/proxy`),Xf("span",null,"Root"),Xf("code",null,"/srv"),Xf("span",{className:"filebrowser-compact-note"},"Compact layout injected")),Xf("iframe",{ref:G,key:N,title:`${K?.name||"File Browser"} WebUI`,src:N,className:"filebrowser-frame","data-testid":"filebrowser-frame",onLoad:(w)=>qj(w.currentTarget),sandbox:"allow-downloads allow-forms allow-modals allow-same-origin allow-scripts"}))))}var O8=cf(Yu(),1);var Uf=O8.default.createElement,{useEffect:gB}=O8.default,kB=O8.default.useState;function Z8({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return Uf("span",{className:`status-badge ${l}`},u||f||"unknown")}function jy({label:f,value:u,hint:l,tone:y}){return Uf("article",{className:`metric-card ${y||""}`},Uf("div",{className:"metric-label"},f),Uf("div",{className:"metric-value"},u),Uf("div",{className:"metric-hint"},l))}function E8({title:f,eyebrow:u,actions:l,children:y,className:r,loading:_}){return Uf("section",{className:`panel ${r||""}`},Uf("div",{className:"panel-head"},Uf("div",null,u?Uf("p",{className:"panel-eyebrow"},u):null,Uf(_u,{title:f,loading:_})),l?Uf("div",{className:"panel-actions"},l):null),Uf("div",{className:"panel-body"},y))}function H8({title:f,data:u,onOpen:l,testId:y}){return Uf("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:()=>l(f,u)},"查看原始JSON")}function Vj({title:f,text:u}){return Uf("div",{className:"empty-state"},Uf("strong",null,f),Uf("span",null,u))}function tB(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function sB(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function oB(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function jr(f,u){let l=f&&typeof f==="object"?f[u]:void 0;return Number.isFinite(Number(l))?String(l):"--"}function aB(f){return(Array.isArray(f?.jobs)?f.jobs:[]).slice(0,40)}function dB(f){return(Array.isArray(f?.drafts)?f.drafts:[]).slice(0,12)}function CG({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let y=f.find((K)=>K.id==="findjob")||null,[r,_]=kB({loading:!1,error:"",health:null,summary:null,jobs:null,drafts:null,refreshedAt:null});async function $(){if(!y)return;_((K)=>({...K,loading:!0,error:""}));try{let[K,E,O,z]=await Promise.all([Df(`${l}/microservices/findjob/health`),Df(`${l}/microservices/findjob/proxy/api/summary`),Df(`${l}/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:40`),Df(`${l}/microservices/findjob/proxy/api/drafts`)]);_({loading:!1,error:"",health:K,summary:E,jobs:O,drafts:z,refreshedAt:new Date})}catch(K){_((E)=>({...E,loading:!1,error:wf(K,"FindJob 加载失败")}))}}if(gB(()=>{$()},[y?.id,y?.runtime?.providerStatus]),!y)return Uf(Vj,{title:"FindJob 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=findjob"});let j=tB(y),A=oB(y),F=sB(y),U=r.summary||{},Q=aB(r.jobs),W=dB(r.drafts),G=r.jobs?._unidesk?.arrayLimits?.jobs;return Uf("div",{className:"findjob-page","data-testid":"findjob-page"},Uf(E8,{title:"FindJob 工作台",eyebrow:"D601 用户服务",loading:r.loading,actions:Uf("div",{className:"panel-actions"},Uf("button",{type:"button",className:"ghost-btn",onClick:$,disabled:r.loading,"data-testid":"findjob-refresh-button"},r.loading?"刷新中":"刷新"),Uf(H8,{title:"FindJob 用户服务",data:y,onOpen:u,testId:"raw-findjob-service"}))},Uf("div",{className:"findjob-hero"},Uf("div",null,Uf("div",{className:"node-version-line"},Uf(Z8,{status:j.providerStatus==="online"?"online":"warn"},j.providerStatus||"unknown"),Uf("span",null,y.providerId),Uf("span",null,F.public?"公网暴露":"仅 UniDesk frontend 代理访问")),Uf("p",{className:"muted paragraph"},y.description)),Uf("div",{className:"microservice-ref-card"},Uf("span",null,"Repo"),Uf("strong",null,A.url||"--"),Uf("code",null,A.commitId||"--")),Uf("div",{className:"microservice-ref-card"},Uf("span",null,"D601 Docker"),Uf("strong",null,`${F.nodeBindHost||"--"}:${F.nodePort||"--"}`),Uf("code",null,`${A.composeFile||"--"} / ${A.composeService||"--"}`))),Uf(Au,{error:r.error,wide:!0})),Uf("div",{className:"findjob-grid"},Uf(E8,{title:"岗位指标",eyebrow:r.refreshedAt?`Updated ${Uu(r.refreshedAt)}`:"Summary",loading:r.loading},Uf("div",{className:"metric-grid"},Uf(jy,{label:"岗位总量",value:jr(U,"totalJobs"),hint:"tracked jobs",tone:"ok"}),Uf(jy,{label:"原始岗位",value:jr(U,"rawJobs"),hint:"raw queue"}),Uf(jy,{label:"已验证",value:jr(U,"verifiedJobs"),hint:"verified set"}),Uf(jy,{label:"优先处理",value:jr(U,"prioritizedJobs"),hint:"prioritized"}),Uf(jy,{label:"过期",value:jr(U,"staleJobs"),hint:"stale jobs",tone:"warn"}),Uf(jy,{label:"无效",value:jr(U,"invalidJobs"),hint:"invalid jobs",tone:"warn"}),Uf(jy,{label:"上海",value:jr(U,"shanghaiJobs"),hint:"city filter"}),Uf(jy,{label:"Health",value:r.health?.ok?"OK":"--",hint:"D601 /api/health"})),Uf("div",{className:"panel-actions inline-actions"},Uf(H8,{title:"FindJob Summary",data:U,onOpen:u,testId:"raw-findjob-summary"}))),Uf(E8,{title:"近期岗位",eyebrow:G?`${G.returnedLength}/${G.originalLength} Preview`:`${Q.length} Preview`,loading:r.loading},Q.length===0?Uf(Vj,{title:"暂无岗位预览",text:"等待 D601 findjob backend 返回 /api/jobs"}):Uf("div",{className:"table-wrap findjob-job-table"},Uf("table",null,Uf("thead",null,Uf("tr",null,Uf("th",null,"优先级"),Uf("th",null,"状态"),Uf("th",null,"单位"),Uf("th",null,"职位"),Uf("th",null,"城市"),Uf("th",null,"阶段"),Uf("th",null,"截止"),Uf("th",null,"证据"))),Uf("tbody",null,Q.map((K)=>Uf("tr",{key:K.id},Uf("td",null,Uf(Z8,{status:String(K.priority||"").toLowerCase()||"unknown"},K.priority||"--")),Uf("td",null,Uf(Z8,{status:String(K.status||"").toLowerCase()||"unknown"},K.status||"--")),Uf("td",null,K.organization_name||"--",Uf("code",null,K.id||"--")),Uf("td",null,K.display_title||K.title||"--"),Uf("td",null,K.display_city||K.city||"--"),Uf("td",null,K.workflow_stage||"--"),Uf("td",null,K.deadline||"--"),Uf("td",null,K.evidence_url?Uf("a",{href:K.evidence_url,target:"_blank",rel:"noreferrer"},"打开"):Uf("span",{className:"muted"},"无"))))))),Uf("div",{className:"panel-actions inline-actions"},Uf(H8,{title:"FindJob Jobs Preview",data:r.jobs,onOpen:u,testId:"raw-findjob-jobs"}))),Uf(E8,{title:"草稿与报告",eyebrow:`${W.length} Drafts`,loading:r.loading},W.length===0?Uf(Vj,{title:"暂无草稿",text:"D601 findjob backend 未返回 drafts"}):Uf("div",{className:"draft-list"},W.map((K)=>Uf("article",{key:K.id,className:"draft-card"},Uf("div",{className:"node-card-head"},Uf("strong",null,K.id),Uf(Z8,{status:K.status},K.status||"--")),Uf("div",{className:"docker-meta compact"},Uf("span",null,K.workflow_stage||"--"),Uf("span",null,`jobs ${K.counts?.jobs??0}`),Uf("span",null,`reports ${K.counts?.reports??0}`)),Uf("span",null,K.latestReportPath||"暂无报告"),Uf("code",null,Kf(K.updated_at||K.updatedAt))))),Uf("div",{className:"panel-actions inline-actions"},Uf(H8,{title:"FindJob Drafts",data:r.drafts,onOpen:u,testId:"raw-findjob-drafts"})))))}var q3=cf(Yu(),1);var x=q3.default.createElement,{useEffect:eB}=q3.default,Lj=q3.default.useState;function E3(f){let u=Number(f);return Number.isFinite(u)?`${Math.max(0,Math.min(100,u)).toFixed(1)}%`:"--"}function Xj(f){if(f===null||f===void 0||f==="")return"--";let u=Number(f);if(!Number.isFinite(u))return"--";if(u<60)return`${Math.max(0,Math.round(u))}s`;if(u<3600)return`${Math.floor(u/60)}m ${Math.round(u%60)}s`;return`${Math.floor(u/3600)}h ${Math.floor(u%3600/60)}m`}function Yj(f,u=2){let l=Number(f);if(!Number.isFinite(l))return f===!1?"false":f===!0?"true":"--";let y=Math.abs(l);if(Number.isInteger(l)||y>=1000)return l.toLocaleString("zh-CN",{maximumFractionDigits:0});if(y>=1)return l.toLocaleString("zh-CN",{maximumFractionDigits:u});return l.toLocaleString("zh-CN",{maximumFractionDigits:Math.max(u,6)})}function O3(f){if(f===null||f===void 0||f==="")return"--";if(typeof f==="boolean")return f?"true":"false";if(typeof f==="number")return Yj(f,4);if(Array.isArray(f))return f.map((u)=>O3(u)).join(" x ");if(typeof f==="object")return"已上报";return String(f)}function q8(f){let u=Number(f);if(!Number.isFinite(u)||u<=0)return"--";let l=u>=100?0:u>=10?1:2;return`${u.toLocaleString("zh-CN",{maximumFractionDigits:l})} epoch/h`}function V8(f){return f.replace(/[^a-zA-Z0-9_-]/g,"-")}function S0(f){return f&&typeof f==="object"&&!Array.isArray(f)?f:{}}function H3({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return x("span",{className:`status-badge ${l}`},u||f||"unknown")}function Ay({label:f,value:u,hint:l,tone:y}){return x("article",{className:`metric-card ${y||""}`},x("div",{className:"metric-label"},f),x("div",{className:"metric-value"},u),x("div",{className:"metric-hint"},l))}function Bj({title:f,eyebrow:u,actions:l,children:y,className:r,loading:_}){return x("section",{className:`panel ${r||""}`},x("div",{className:"panel-head"},x("div",null,u?x("p",{className:"panel-eyebrow"},u):null,x(_u,{title:f,loading:_})),l?x("div",{className:"panel-actions"},l):null),x("div",{className:"panel-body"},y))}function E_({title:f,data:u,onOpen:l,testId:y}){return x("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:(r)=>{r?.stopPropagation?.(),l(f,u)}},"查看原始JSON")}function H1({title:f,text:u}){return x("div",{className:"empty-state"},x("strong",null,f),x("span",null,u))}function fX(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function uX(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function lX(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function yX(f){return f?.counts&&typeof f.counts==="object"&&!Array.isArray(f.counts)?f.counts:{}}function rX(f){return Array.isArray(f?.jobs)?f.jobs.slice(0,240):[]}function _X(f){return Array.isArray(f?.projects)?f.projects.slice(0,1000):[]}function L8(f){return Array.isArray(f?.projects)?f.projects:[]}function $X(f,u){if(Array.isArray(u?.gpu))return u.gpu;if(Array.isArray(f?.gpu))return f.gpu;return[]}function o0(f,u){return`${f}/microservices/met-nonlinear/proxy${u}`}function cG(f){return f.startedAt&&f.finishedAt?Xj((Date.parse(f.finishedAt)-Date.parse(f.startedAt))/1000):"--"}function jX(f){let u=f.progress||{};if(u.etaSeconds!==null&&u.etaSeconds!==void 0&&u.etaSeconds!==""){let $=Number(u.etaSeconds);if(Number.isFinite($))return Math.max(0,$)}let l=Number(u.currentEpoch),y=Number(u.epochTarget??f.epochTarget),r=Date.parse(f.startedAt||"");if(!Number.isFinite(l)||l<=0||!Number.isFinite(y)||y<=l||!Number.isFinite(r))return null;let _=Math.max(0,(Date.now()-r)/1000);if(_<=0)return null;return Math.max(0,_/l*(y-l))}function iG(f){let u=f.progress||{},l=Number(u.epochPerHour);if(Number.isFinite(l)&&l>0)return l;let y=Date.parse(f.startedAt||""),r=["succeeded","failed","canceled"].includes(f.status)?Date.parse(f.finishedAt||""):Date.now();if(!Number.isFinite(y)||!Number.isFinite(r)||r<=y)return null;let _=Number(u.currentEpoch??f.epochTarget);if(!Number.isFinite(_)||_<=0)return null;return _/((r-y)/3600000)}function RG(f){if(f==="staged")return"待启动";if(f==="queued")return"排队中";if(f==="running")return"训练中";if(f==="succeeded")return"已完成";if(f==="failed")return"失败";if(f==="canceled")return"已取消";return f||"unknown"}function xG(f,u,l){return{name:f,path:u,depth:l,count:0,children:[],project:null}}function AX(f){let u=xG("","",-1);for(let y of f){let _=String(y?.projectPath||"").replace(/\\/g,"/").split("/").filter(Boolean);if(_.length===0)continue;let $=u,j=[];for(let[A,F]of _.entries()){j.push(F);let U=j.join("/"),Q=$.children.find((W)=>W.path===U);if(!Q)Q=xG(F,U,A),$.children.push(Q);if(A===_.length-1)Q.project=y;$=Q}}let l=(y)=>{let r=y.children.reduce((_,$)=>_+l($),0);return y.count=(y.project?1:0)+r,y.children.sort((_,$)=>{if(Boolean(_.project)!==Boolean($.project))return _.project?1:-1;return _.name.localeCompare($.name,"zh-CN",{numeric:!0,sensitivity:"base"})}),y.count};return l(u),u}function FX(f){let u=S0(f.data);return S0(u.project).projectPath?S0(u.project):u}function JX(f){return S0(S0(f.data).job)}function vG({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let y=f.find((b)=>b.id==="met-nonlinear")||null,[r,_]=Lj({loading:!1,actionBusy:!1,error:"",health:null,summary:null,queue:null,projects:null,history:null,images:null,refreshedAt:null}),[$,j]=Lj({loading:!1,error:"",kind:"",key:"",title:"",data:null}),[A,F]=Lj(()=>({activeTab:"projects",selectedProjects:{},expandedProjectDirs:{},sourceProject:"",forkCount:1,forkEpochs:200,forkPrefix:`ui_fork_${Date.now()}`,maxConcurrency:3,targetGpuName:"2080 Ti",actionMessage:""}));function U(b){F((t)=>({...t,...b}))}async function Q(b=A.activeTab){if(!y)return;_((t)=>({...t,loading:!0,error:""}));try{let t=[["health",Df(`${l}/microservices/met-nonlinear/health`)],["summary",Df(o0(l,"/api/summary"))]];if(b==="projects")t.push(["projectsRoot",Df(o0(l,"/api/projects?root=projects&limit=500"))]),t.push(["exProjectsRoot",Df(o0(l,"/api/projects?root=ex_projects&limit=500"))]);if(b==="current"||b==="completed"||b==="failed")t.push(["queue",Df(o0(l,"/api/queue"))]);if(b==="completed"||b==="failed")t.push(["history",Df(o0(l,"/api/history"))]);if(b==="gpu")t.push(["images",Df(o0(l,"/api/images"))]);let a=Object.fromEntries(await Promise.all(t.map(async([o,uf])=>[o,await uf]))),Nf={loading:!1,actionBusy:!1,error:"",health:a.health,summary:a.summary,refreshedAt:new Date};if(a.projectsRoot||a.exProjectsRoot){let{projectsRoot:o,exProjectsRoot:uf}=a;Nf.projects={ok:o?.ok!==!1&&uf?.ok!==!1,roots:[{root:"projects",count:L8(o).length},{root:"ex_projects",count:L8(uf).length}],projects:[...L8(o),...L8(uf)]}}if(a.queue)Nf.queue=a.queue;if(a.history)Nf.history=a.history;if(a.images)Nf.images=a.images;_((o)=>({...o,...Nf}))}catch(t){_((a)=>({...a,loading:!1,actionBusy:!1,error:wf(t,"MET Nonlinear 加载失败")}))}}async function W(b,t){_((a)=>({...a,actionBusy:!0,error:""})),U({actionMessage:`${b}...`});try{let a=await t();U({actionMessage:a||`${b}完成`}),await Q()}catch(a){_((Nf)=>({...Nf,actionBusy:!1,error:wf(a,`${b}失败`)}))}}async function G(){await W("保存并发设置",async()=>{await Df(o0(l,"/api/queue/settings"),{method:"PUT",body:JSON.stringify({maxConcurrency:Number(A.maxConcurrency),targetGpuName:A.targetGpuName})})})}function K(){return Object.entries(A.selectedProjects).filter(([,b])=>b).map(([b])=>b)}async function E(){let b=K();if(b.length===0)throw Error("请先选择至少一个 project");await W("加入待启动队列",async()=>{await Df(o0(l,"/api/queue"),{method:"POST",body:JSON.stringify({projectPaths:b,maxConcurrency:Number(A.maxConcurrency),targetGpuName:A.targetGpuName,start:!1})}),U({activeTab:"current",selectedProjects:{}})})}async function O(){let b=A.sourceProject||C[0]?.projectPath;if(!b)throw Error("请先选择源 project");await W("Fork Project",async()=>{let t=await Df(o0(l,"/api/projects/fork"),{method:"POST",body:JSON.stringify({sourceProject:b,count:Number(A.forkCount),epochs:Number(A.forkEpochs),prefix:A.forkPrefix})}),a=Array.isArray(t.projectPaths)?t.projectPaths:[],Nf=a.reduce((o,uf)=>{return o[uf]=!0,o},{...A.selectedProjects});return U({selectedProjects:Nf}),`已 fork ${a.length} 个 project,并已自动勾选;请确认后点击加入待启动队列。`})}async function z(){await W("启动队列",async()=>{await Df(o0(l,"/api/queue/start"),{method:"POST",body:JSON.stringify({maxConcurrency:Number(A.maxConcurrency),targetGpuName:A.targetGpuName})}),U({activeTab:"current"})})}async function Z(b){await W("取消任务",async()=>{await Df(o0(l,`/api/jobs/${encodeURIComponent(b.id)}/cancel`),{method:"POST",body:JSON.stringify({})})})}async function N(b){let t=String(b?.projectPath||"");if(!t)return;j({loading:!0,error:"",kind:"project",key:t,title:t,data:null});try{let a=await Df(o0(l,`/api/projects/config?path=${encodeURIComponent(t)}`));j({loading:!1,error:"",kind:"project",key:t,title:t,data:a})}catch(a){j({loading:!1,error:wf(a,"Project 详情加载失败"),kind:"project",key:t,title:t,data:null})}}async function H(b){let t=String(b?.id||"");if(!t)return;j({loading:!0,error:"",kind:"job",key:t,title:b.projectPath||t,data:null});try{let a=await Df(o0(l,`/api/jobs/${encodeURIComponent(t)}`));j({loading:!1,error:"",kind:"job",key:t,title:a?.job?.projectPath||b.projectPath||t,data:a})}catch(a){j({loading:!1,error:wf(a,"Job 详情加载失败"),kind:"job",key:t,title:b.projectPath||t,data:null})}}if(eB(()=>{Q(A.activeTab)},[y?.id,y?.runtime?.providerStatus,A.activeTab]),!y)return x(H1,{title:"MET Nonlinear 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=met-nonlinear"});let Y=fX(y),w=lX(y),V=uX(y),X=yX(r.queue?.queue||r.summary?.queue),i=$X(r.health,r.queue),m=r.health?.targetGpu||r.summary?.targetGpu||i.find((b)=>String(b.name||"").includes("2080")),M=r.images?.mlImage||r.health?.image||{},c=rX(r.queue),C=_X(r.projects),T=AX(C),R=A.sourceProject||C[0]?.projectPath||"",P=c.filter((b)=>["staged","queued","running"].includes(b.status)),n=c.filter((b)=>b.status==="succeeded"),B=c.filter((b)=>["failed","canceled"].includes(b.status)),D=Array.isArray(r.history?.jobs)?r.history.jobs.slice(0,120):[],I=[{id:"projects",label:"项目库",count:C.length},{id:"current",label:"当前队列",count:P.length||Number(X.staged||0)+Number(X.queued||0)+Number(X.running||0)},{id:"completed",label:"已完成",count:n.length||Number(X.succeeded||0)},{id:"failed",label:"失败诊断",count:B.length||Number(X.failed||0)+Number(X.canceled||0)},{id:"gpu",label:"GPU/镜像",count:i.length}];function p(b,t){if(b.length===0)return x(H1,{title:t==="current"?"当前队列为空":"暂无记录",text:t==="current"?"从项目库选择或 fork project 后先加入待启动队列,再启动队列。":"终态任务会显示耗时、exit code 和失败诊断。"});return x("div",{className:"table-wrap met-job-table"},x("table",null,x("thead",null,x("tr",null,x("th",null,"状态"),x("th",null,"Project"),x("th",null,"Epoch"),x("th",null,"速度"),x("th",null,"ETA/耗时"),x("th",null,"GPU"),x("th",null,"Exit"),x("th",null,"更新时间"),x("th",null,"操作"))),x("tbody",null,b.map((a)=>{let Nf=a.progress||{},o=["staged","queued","running"].includes(a.status),uf=$.kind==="job"&&$.key===a.id;return x("tr",{key:a.id,className:`met-click-row ${uf?"active":""}`,onClick:()=>H(a),"data-testid":`met-job-row-${V8(a.id)}`},x("td",null,x(H3,{status:a.status},RG(a.status))),x("td",null,x("button",{type:"button",className:"met-inline-link",onClick:(qf)=>{qf.stopPropagation(),H(a)}},a.projectPath),x("code",null,a.id)),x("td",null,x("span",null,`${Nf.currentEpoch??"--"} / ${Nf.epochTarget??a.epochTarget??"--"}`),x("div",{className:"met-progress"},x("span",{style:{width:E3(Nf.progressPercent)}}))),x("td",null,x("strong",null,q8(iG(a)))),x("td",null,a.status==="succeeded"||a.status==="failed"||a.status==="canceled"?cG(a):a.status==="running"?`ETA ${Xj(jX(a))}`:"--"),x("td",null,a.gpuName||"--"),x("td",null,a.exitCode??"--"),x("td",null,Kf(a.updatedAt)),x("td",null,o?x("button",{type:"button",className:"ghost-btn mini",onClick:(qf)=>{qf.stopPropagation(),Z(a)},disabled:r.actionBusy},"取消"):null,x(E_,{title:`MET Job ${a.id}`,data:a,onOpen:u,testId:`raw-met-job-${a.id}`})))}))))}function k(){return x("div",{className:"met-queue-summary","data-testid":"met-current-summary"},x(H3,{status:"staged"},`待启动 ${X.staged??0}`),x(H3,{status:"queued"},`排队中 ${X.queued??0}`),x(H3,{status:"running"},`训练中 ${X.running??0}`),x("span",null,`最大并发 ${r.summary?.queue?.maxConcurrency??r.queue?.queue?.maxConcurrency??A.maxConcurrency}`),x("span",null,`目标 GPU ${r.summary?.queue?.targetGpuName??r.queue?.queue?.targetGpuName??A.targetGpuName}`))}function _f(b,t){let a=A.expandedProjectDirs[b];return a===void 0?t<2:Boolean(a)}function S(b,t){let a=_f(b,t);U({expandedProjectDirs:{...A.expandedProjectDirs,[b]:!a}})}function e(b){let t=8+Math.max(0,b.depth)*16;if(Boolean(b.project)){let o=b.project,uf=Boolean(A.selectedProjects[o.projectPath]),qf=$.kind==="project"&&$.key===o.projectPath;return x("div",{key:b.path,className:`met-tree-row project ${uf?"selected":""} ${qf?"active":""}`,style:{paddingLeft:t},onClick:()=>N(o),"data-testid":`met-project-node-${V8(o.projectPath)}`},x("div",{className:"met-tree-name"},x("input",{type:"checkbox",checked:uf,onClick:(xf)=>xf.stopPropagation(),onChange:(xf)=>U({selectedProjects:{...A.selectedProjects,[o.projectPath]:xf.target.checked}}),"data-testid":`met-project-checkbox-${V8(o.projectPath)}`}),x("button",{type:"button",className:"met-inline-link project-path",onClick:(xf)=>{xf.stopPropagation(),N(o)}},b.name)),x("span",null,o.useModel||"--"),x("span",null,o.epochTrain??"--"),x("span",null,E3(o.progress?.progressPercent)),x("span",null,q8(o.progress?.epochPerHour)))}let Nf=_f(b.path,b.depth);return x(q3.default.Fragment,{key:b.path},x("div",{className:"met-tree-row folder",style:{paddingLeft:t},"data-testid":`met-project-folder-${V8(b.path)}`},x("button",{type:"button",className:"met-tree-toggle",onClick:()=>S(b.path,b.depth),"aria-label":Nf?`折叠 ${b.path}`:`展开 ${b.path}`},Nf?"-":"+"),x("strong",null,b.name),x("span",{className:"met-tree-count"},`${b.count} projects`)),Nf?b.children.map((o)=>e(o)):null)}function $f(b){return x("div",{className:"met-detail-kv"},b.map((t)=>x("div",{key:t.label,className:"met-detail-kv-item"},x("span",null,t.label),x("strong",null,O3(t.value)),t.hint?x("small",null,t.hint):null)))}function Qf(b,t){return x("div",{className:"met-detail-section"},x("h3",null,b),$f(t))}function Af(b){if(!Array.isArray(b)||b.length===0)return x(H1,{title:"模型层未上报",text:"等待 data/model_info.json 或 compute_analysis.json 生成。"});return x("div",{className:"table-wrap met-layer-table"},x("table",null,x("thead",null,x("tr",null,x("th",null,"Layer"),x("th",null,"Type"),x("th",null,"Params"),x("th",null,"Trainable"),x("th",null,"Compute"))),x("tbody",null,b.slice(0,18).map((t,a)=>x("tr",{key:`${t.name||"layer"}-${a}`},x("td",null,t.name||`#${a+1}`),x("td",null,t.type||"--"),x("td",null,Yj(t.num_params)),x("td",null,t.trainable===void 0?"--":String(Boolean(t.trainable))),x("td",null,Yj(t.compute?.total??t.estimated_cost?.weighted_units?.total)))))))}function zf(b){let t=Array.isArray(b)?b:[];if(t.length===0)return x(H1,{title:"data/ 暂无文件",text:"训练或评估完成后会生成 training_state、metrics、model_info 等文件。"});return x("div",{className:"met-file-chip-grid"},t.slice(0,48).map((a)=>x("span",{key:a},a)),t.length>48?x("span",null,`+${t.length-48}`):null)}function Hf(b){let t=String(b||"").replace(/\x1b\[[0-9;]*[A-Za-z]/g,"").split(/\r?\n/).map((a)=>a.trim()).filter(Boolean).slice(-12);if(t.length===0)return x(H1,{title:"暂无日志尾部",text:"该任务未上报 logTail 或日志已轮转。"});return x("div",{className:"met-log-lines"},t.map((a,Nf)=>x("div",{key:`${Nf}-${a.slice(0,16)}`},a)))}function Zf(){if($.loading)return x("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},x("div",{className:"panel-head compact"},x("div",null,x("p",{className:"panel-eyebrow"},"Detail Loading"),x(_u,{title:"详情加载中",loading:!0}))),x(H1,{title:"详情加载中",text:$.title||"正在读取 D601 data/ 和 config.json"}));if($.error)return x("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},x(Au,{error:$.error,wide:!0}));if(!$.data)return x("section",{className:"met-detail-panel muted","data-testid":"met-detail-panel"},x(H1,{title:"选择一个项目或任务查看详情",text:"项目库、当前队列、已完成和失败诊断中的行都可以点击;默认只展示结构化字段,原始 JSON 需显式点击按钮。"}));let b=FX($),t=JX($),a=S0(b.config),Nf=S0(b.progress||t.progress),o=S0(b.data),uf=S0(b.metrics||o.metrics||Nf.trainingInfo?.evaluation_metrics),qf=S0(o.trainingInfo||Nf.trainingInfo),xf=S0(o.trainingState),tf=S0(b.model||o.model),df=Array.isArray(tf.modelSummary)&&tf.modelSummary.length>0?tf.modelSummary:tf.computeLayers,lu=S0(qf.evaluation_metrics),Ou=$.kind==="job"?"训练任务详情":"Project 详情";return x("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},x("div",{className:"panel-head compact"},x("div",null,x("p",{className:"panel-eyebrow"},$.kind==="job"?"Job + Project Detail":"Project Library Detail"),x(_u,{title:Ou}),x("code",null,b.projectPath||t.projectPath||$.title)),x("div",{className:"panel-actions"},x(E_,{title:`MET ${Ou}`,data:$.data,onOpen:u,testId:"raw-met-detail"}))),$.kind==="job"?Qf("任务状态",[{label:"Job ID",value:t.id},{label:"状态",value:RG(t.status)},{label:"GPU",value:t.gpuName},{label:"Exit Code",value:t.exitCode},{label:"耗时",value:cG(t)},{label:"训练速度",value:q8(iG({...t,progress:Nf}))}]):null,Qf("config.json",[{label:"use_model",value:a.use_model},{label:"epoch_train",value:a.epoch_train},{label:"step_per_epoch",value:a.step_per_epoch},{label:"learning_rate",value:a.learning_rate},{label:"using_gpu",value:a.using_gpu},{label:"use_points",value:a.use_points},{label:"sample_rate",value:a.sample_rate},{label:"time_clipped_s",value:a.time_clipped_s},{label:"H_UNITS",value:a.H_UNITS},{label:"INNER_KAN_UNITS",value:a.INNER_KAN_UNITS},{label:"INNER_KAN_LAYERS",value:a.INNER_KAN_LAYERS},{label:"GRID_SIZE",value:a.GRID_SIZE},{label:"SPLINE_ORDER",value:a.SPLINE_ORDER},{label:"USE_FAST_MODEL",value:a.USE_FAST_MODEL},{label:"IIR_TRAINABLE",value:a.IIR_TRAINABLE}]),Qf("data/ 训练状态",[{label:"Epoch",value:`${Nf.currentEpoch??xf.current_epoch??xf.completed_epoch??"--"} / ${Nf.epochTarget??a.epoch_train??"--"}`},{label:"Progress",value:E3(Nf.progressPercent)},{label:"Last Loss",value:Nf.lastLoss??xf.loss},{label:"Last Val Loss",value:Nf.lastValLoss??xf.val_loss},{label:"Min Loss",value:qf.min_loss??xf.min_loss},{label:"Min Val Loss",value:qf.min_val_loss??xf.min_val_loss},{label:"Log Lines",value:Nf.logLineCount},{label:"ETA",value:Xj(Nf.etaSeconds??xf.remaining_time)},{label:"训练速度",value:q8(Nf.epochPerHour??xf.smoothed_speed)},{label:"Training Alive",value:xf.training_alive}]),Qf("模型参数",[{label:"Model Type",value:tf.modelType??a.use_model},{label:"Total Params",value:tf.totalParams,hint:tf.totalParams===null||tf.totalParams===void 0?"未上报":"data/model_info.json"},{label:"Trainable",value:tf.trainableParams},{label:"Non-trainable",value:tf.nonTrainableParams},{label:"Compute Cost",value:tf.computeCost},{label:"Estimate Status",value:tf.estimateStatus},{label:"Unsupported Layers",value:tf.unsupportedLayerCount}]),Qf("指标",[{label:"train_loss",value:uf.train_loss??lu.train_loss},{label:"val_loss",value:uf.val_loss??lu.val_loss},{label:"train_mae",value:uf.train_mae??lu.train_mae},{label:"val_mae",value:uf.val_mae??lu.val_mae},{label:"train_afmae",value:uf.train_afmae??lu.train_afmae},{label:"val_afmae",value:uf.val_afmae??lu.val_afmae},{label:"freq_drift_hz",value:uf.freq_drift_hz},{label:"sens_drift_percent",value:uf.sens_drift_percent},{label:"linearity_percent",value:uf.linearity_percent},{label:"weights_source",value:uf.weights_source??lu.weights_source},{label:"lr min/mean/max",value:`${O3(qf.learning_rate_min)} / ${O3(qf.learning_rate_mean)} / ${O3(qf.learning_rate_max)}`}]),x("div",{className:"met-detail-section"},x("h3",null,"模型层"),Af(df)),x("div",{className:"met-detail-section"},x("h3",null,"data/ 文件"),zf(o.files)),$.kind==="job"?x("div",{className:"met-detail-section"},x("h3",null,"日志尾部"),Hf(S0($.data).logTail)):null)}return x("div",{className:"met-page","data-testid":"met-nonlinear-page"},x(Bj,{title:"MET Nonlinear 训练编排",eyebrow:"D601 GPU 用户服务",loading:r.loading||r.actionBusy,actions:x("div",{className:"panel-actions"},x("button",{type:"button",className:"ghost-btn",onClick:Q,disabled:r.loading,"data-testid":"met-refresh-button"},r.loading?"刷新中":"刷新"),x(E_,{title:"MET Nonlinear 用户服务",data:y,onOpen:u,testId:"raw-met-service"}))},x("div",{className:"findjob-hero"},x("div",null,x("div",{className:"node-version-line"},x(H3,{status:Y.providerStatus==="online"?"online":"warn"},Y.providerStatus||"unknown"),x("span",null,y.providerId),x("span",null,V.public?"公网暴露":"仅 UniDesk frontend 代理访问")),x("p",{className:"muted paragraph"},y.description)),x("div",{className:"microservice-ref-card"},x("span",null,"Repo"),x("strong",null,w.url||"--"),x("code",null,w.commitId||"--")),x("div",{className:"microservice-ref-card"},x("span",null,"D601 Docker"),x("strong",null,`${V.nodeBindHost||"--"}:${V.nodePort||"--"}`),x("code",null,`${w.composeFile||"--"} / ${w.containerName||"--"}`))),x(Au,{error:r.error,wide:!0}),A.actionMessage?x("div",{className:"met-action-log","data-testid":"met-action-message"},A.actionMessage):null),x("div",{className:"met-grid"},x(Bj,{title:"核心状态",eyebrow:r.refreshedAt?`Updated ${Uu(r.refreshedAt)}`:"Queue + GPU",loading:r.loading},x("div",{className:"metric-grid"},x(Ay,{label:"Staged",value:X.staged??0,hint:"加入队列未开始",tone:Number(X.staged||0)>0?"warn":""}),x(Ay,{label:"Queued",value:X.queued??0,hint:"排队等待调度",tone:Number(X.queued||0)>0?"warn":""}),x(Ay,{label:"Running",value:X.running??0,hint:`max ${r.summary?.queue?.maxConcurrency??r.queue?.queue?.maxConcurrency??"--"}`,tone:Number(X.running||0)>0?"ok":""}),x(Ay,{label:"Succeeded",value:X.succeeded??0,hint:"已完成"}),x(Ay,{label:"Failed",value:X.failed??0,hint:"需要诊断",tone:Number(X.failed||0)>0?"warn":""}),x(Ay,{label:"2080Ti Free",value:m?E3(Number(m.freeRatio)*100):"--",hint:m?`${m.memoryFreeMiB}/${m.memoryTotalMiB} MiB`:"等待 GPU 上报"}),x(Ay,{label:"ML Image",value:M.present?"READY":"MISSING",hint:M.image||"met-nonlinear-ml:tf26",tone:M.present?"ok":"warn"}),x(Ay,{label:"Health",value:r.health?.ok?"OK":"--",hint:"D601 /health"}))),x(Bj,{title:"队列控制",eyebrow:"Downloader-like staging",loading:r.actionBusy},x("div",{className:"met-control-strip"},x("label",null,"最大并发",x("input",{type:"number",min:1,max:16,value:A.maxConcurrency,"data-testid":"met-max-concurrency-input",onChange:(b)=>U({maxConcurrency:b.target.value})})),x("label",null,"目标 GPU",x("input",{value:A.targetGpuName,"data-testid":"met-target-gpu-input",onChange:(b)=>U({targetGpuName:b.target.value})})),x("button",{type:"button",className:"ghost-btn",onClick:G,disabled:r.actionBusy,"data-testid":"met-save-settings-button"},"保存设置"),x("button",{type:"button",className:"primary-btn",onClick:z,disabled:r.actionBusy||Number(X.staged||0)===0,"data-testid":"met-start-queue-button"},"启动队列")),x("p",{className:"muted paragraph"},"Project 先进入待启动队列,不会立即训练;点击启动队列后才切换为排队中,并由 D601 scheduler 按最大并发和 2080Ti 显存策略调度。")),x("section",{className:"panel met-workspace"},x("div",{className:"met-tabs",role:"tablist"},I.map((b)=>x("button",{key:b.id,type:"button",className:A.activeTab===b.id?"active":"",onClick:()=>U({activeTab:b.id}),"data-testid":`met-tab-${b.id}`},`${b.label} ${b.count}`))),x("div",{className:"panel-body"},A.activeTab==="projects"?x("div",{className:"met-form-grid","data-testid":"met-projects-pane"},x("div",{className:"met-fork-card"},x("h3",null,"Fork Project"),x("label",null,"源 Project",x("select",{value:R,"data-testid":"met-source-project-select",onChange:(b)=>U({sourceProject:b.target.value})},C.map((b)=>x("option",{key:b.projectPath,value:b.projectPath},`${b.projectPath} · ${b.useModel||"model?"}`)))),x("label",null,"Fork 数量",x("input",{type:"number",min:1,max:100,value:A.forkCount,"data-testid":"met-fork-count-input",onChange:(b)=>U({forkCount:b.target.value})})),x("label",null,"训练轮数",x("input",{type:"number",min:1,max:1e5,value:A.forkEpochs,"data-testid":"met-fork-epochs-input",onChange:(b)=>U({forkEpochs:b.target.value})})),x("label",null,"目标前缀",x("input",{value:A.forkPrefix,"data-testid":"met-fork-prefix-input",onChange:(b)=>U({forkPrefix:b.target.value})})),x("button",{type:"button",className:"primary-btn",onClick:O,disabled:r.actionBusy||!R,"data-testid":"met-fork-button"},"Fork Project"),x("p",{className:"muted paragraph"},"Fork 只创建新 Project 并自动勾选,不会直接训练;需要在右侧确认后加入待启动队列。")),x("div",{className:"met-project-list"},x("div",{className:"panel-head compact"},x("div",null,x("p",{className:"panel-eyebrow"},`Existing Projects · ${(r.projects?.roots||[]).map((b)=>`${b.root} ${b.count}`).join(" / ")}`),x(_u,{title:"选择已有 Project",loading:r.loading||r.actionBusy})),x("button",{type:"button",className:"ghost-btn",onClick:E,disabled:r.actionBusy||K().length===0,"data-testid":"met-stage-selected-button"},`加入待启动队列 (${K().length})`)),C.length===0?x(H1,{title:"暂无 project",text:"等待 D601 返回 /api/projects"}):x("div",{className:"met-project-table","data-testid":"met-project-tree"},x("div",{className:"met-tree-header"},x("span",null,"文件树 Project"),x("span",null,"Model"),x("span",null,"Epochs"),x("span",null,"Progress"),x("span",null,"速度")),T.children.map((b)=>e(b)))),Zf()):null,A.activeTab==="current"?x("div",{"data-testid":"met-current-pane"},k(),p(P,"current"),Zf(),x("div",{className:"panel-actions inline-actions"},x(E_,{title:"MET Queue",data:r.queue,onOpen:u,testId:"raw-met-queue"}))):null,A.activeTab==="completed"?x("div",{"data-testid":"met-completed-pane"},p(n.length>0?n:D.filter((b)=>b.status==="succeeded"),"completed"),Zf()):null,A.activeTab==="failed"?x("div",{"data-testid":"met-failed-pane"},p(B.length>0?B:D.filter((b)=>["failed","canceled"].includes(b.status)),"failed"),Zf(),x("div",{className:"panel-actions inline-actions"},x(E_,{title:"MET History",data:r.history,onOpen:u,testId:"raw-met-history"}))):null,A.activeTab==="gpu"?x("div",{className:"met-gpu-pane","data-testid":"met-gpu-pane"},i.length===0?x(H1,{title:"暂无 GPU 上报",text:"等待 D601 met-nonlinear-ts 或 ML image 提供 nvidia-smi 数据"}):x("div",{className:"table-wrap"},x("table",null,x("thead",null,x("tr",null,x("th",null,"Index"),x("th",null,"Name"),x("th",null,"Free"),x("th",null,"Policy"))),x("tbody",null,i.map((b)=>x("tr",{key:b.index},x("td",null,b.index),x("td",null,b.name),x("td",null,`${b.memoryFreeMiB} / ${b.memoryTotalMiB} MiB`,x("div",{className:"met-progress"},x("span",{style:{width:E3(Number(b.freeRatio)*100)}}))),x("td",null,String(b.name||"").includes("2080")?"target 2080Ti, <20% 限制并发":"non-target")))))),x("div",{className:"panel-actions inline-actions"},x(E_,{title:"MET Images",data:r.images,onOpen:u,testId:"raw-met-images"}))):null))))}var X8=[{id:"ops",label:"运行总览",code:"OPS",tabs:[{id:"status",label:"态势总览"},{id:"performance",label:"性能面板"},{id:"events",label:"事件摘要"},{id:"logs",label:"服务日志"}]},{id:"nodes",label:"资源节点",code:"NODE",tabs:[{id:"list",label:"节点清单"},{id:"monitor",label:"资源监控"},{id:"docker",label:"Docker 状态"},{id:"gateway",label:"网关版本"},{id:"labels",label:"资源标签"},{id:"heartbeats",label:"心跳状态"}]},{id:"tasks",label:"任务调度",code:"TASK",tabs:[{id:"dispatch",label:"下发任务"},{id:"pending",label:"待处理任务"},{id:"history",label:"任务历史"},{id:"results",label:"执行结果"}]},{id:"apps",label:"用户服务",code:"APP",routeSegment:"app",tabs:[{id:"catalog",label:"服务目录"},{id:"todo-note",label:"Todo Note"},{id:"findjob",label:"FindJob"},{id:"pipeline",label:"Pipeline"},{id:"met-nonlinear",label:"MET Nonlinear"},{id:"claudeqq",label:"ClaudeQQ"},{id:"baidu-netdisk",label:"Baidu Netdisk"},{id:"filebrowser",label:"File Browser"},{id:"code-queue",label:"Code Queue"},{id:"project-manager",label:"Project Manager"}]},{id:"config",label:"系统配置",code:"CFG",tabs:[{id:"topology",label:"连接拓扑"},{id:"auth",label:"认证策略"},{id:"security",label:"安全边界"}]}],H_=Object.fromEntries(X8.map((f)=>[f.id,f.tabs[0]?.id??""]));function UX(f){let u=String(f||"").trim();if(!u)return"";try{return decodeURIComponent(u)}catch{return u}}function B8(f){let u=String(f||"/"),[l]=u.split(/[?#]/u,1);if(l==="/")return"/";let r=`/${l.split("/").map(UX).filter(Boolean).join("/")}`;return r.endsWith("/")?r:`${r}/`}function QX(f){let u=2166136261;for(let l of f)u^=l.charCodeAt(0),u=Math.imul(u,16777619);return Math.abs(u>>>0).toString(36)}function wj(f){return String(f||"").normalize("NFKD").replace(/[\u0300-\u036f]/gu,"").toLowerCase().replace(/[^a-z0-9]+/gu,"-").replace(/^-+|-+$/gu,"")}function bG(f){return String(f||"").trim().toLowerCase().replace(/[\s/\\?#%]+/gu,"-").replace(/-+/gu,"-").replace(/^-+|-+$/gu,"")}function hG(f){let u=wj(f.routeSegment||"")||bG(f.routeSegment||"");if(u)return u;let l=wj(f.id||"");if(l)return l;let y=wj(f.label||"")||bG(f.label||"");if(y)return y;return`route-${QX(JSON.stringify(f))}`}function Dj(f,u){return`${f}:${u}`}function mG(f){let u=f.map((A)=>{let F=hG(A);return{...A,routeSegment:F,tabs:A.tabs.map((U)=>({...U,routeSegment:hG(U)}))}}),l={},y={},r={},_=u.map((A)=>{let F=A.tabs[0]?.id??"";r[A.id]=F;let U=A.tabs.map((G)=>{let K=`/${A.routeSegment}/${G.routeSegment}/`,E=[K],O={moduleId:A.id,tabId:G.id};for(let z of E)l[B8(z)]=O;return y[Dj(A.id,G.id)]=K,{...G,canonicalPath:K,aliases:E}}),Q=`/${A.routeSegment}/`,W={moduleId:A.id,tabId:F};return l[B8(Q)]=W,{...A,routeSegment:A.routeSegment,canonicalPath:Q,tabs:U}}),$=_[0],j={moduleId:$?.id||"",tabId:$?.tabs[0]?.id||""};return l["/"]=j,{modules:_,moduleById:Object.fromEntries(_.map((A)=>[A.id,A])),defaultActiveTabs:r,routeMap:l,canonicalPathByTarget:y,fallbackTarget:j}}function Tj(f,u){return f.routeMap[B8(u)]||f.fallbackTarget}function V3(f,u,l){return f.canonicalPathByTarget[Dj(u,l)]||f.canonicalPathByTarget[Dj(f.fallbackTarget.moduleId,f.fallbackTarget.tabId)]||"/"}function pG(f,u){let l=f.routeMap[B8(u)];if(!l)return null;return V3(f,l.moduleId,l.tabId)}var qy=cf(Yu(),1);var ff=cf(tG(),1),lf=cf(Yu(),1);function nu(f){if(typeof f==="string"||typeof f==="number")return""+f;let u="";if(Array.isArray(f)){for(let l=0,y;l{}};function oG(){for(var f=0,u=arguments.length,l={},y;f=0)y=l.slice(r+1),l=l.slice(0,r);if(l&&!u.hasOwnProperty(l))throw Error("unknown type: "+l);return{type:l,name:y}})}w8.prototype=oG.prototype={constructor:w8,on:function(f,u){var l=this._,y=HX(f+"",l),r,_=-1,$=y.length;if(arguments.length<2){while(++_<$)if((r=(f=y[_]).type)&&(r=OX(l[r],f.name)))return r;return}if(u!=null&&typeof u!=="function")throw Error("invalid callback: "+u);while(++_<$)if(r=(f=y[_]).type)l[r]=sG(l[r],f.name,u);else if(u==null)for(r in l)l[r]=sG(l[r],f.name,null);return this},copy:function(){var f={},u=this._;for(var l in u)f[l]=u[l].slice();return new w8(f)},call:function(f,u){if((r=arguments.length-2)>0)for(var l=Array(r),y=0,r,_;y=0&&(u=f.slice(0,l))!=="xmlns")f=f.slice(l+1);return nj.hasOwnProperty(u)?{space:nj[u],local:f}:f}function Mj(f){let u;while(u=f.sourceEvent)f=u;return f}function W0(f,u){if(f=Mj(f),u===void 0)u=f.currentTarget;if(u){var l=u.ownerSVGElement||u;if(l.createSVGPoint){var y=l.createSVGPoint();return y.x=f.clientX,y.y=f.clientY,y=y.matrixTransform(u.getScreenCTM().inverse()),[y.x,y.y]}if(u.getBoundingClientRect){var r=u.getBoundingClientRect();return[f.clientX-r.left-u.clientLeft,f.clientY-r.top-u.clientTop]}}return[f.pageX,f.pageY]}function qX(){}function Fy(f){return f==null?qX:function(){return this.querySelector(f)}}function Sj(f){if(typeof f!=="function")f=Fy(f);for(var u=this._groups,l=u.length,y=Array(l),r=0;r=N)N=Z+1;while(!(Y=O[N])&&++N=0;)if($=y[r]){if(_&&$.compareDocumentPosition(_)^4)_.parentNode.insertBefore($,_);_=$}return this}function gj(f){if(!f)f=CX;function u(Q,W){return Q&&W?f(Q.__data__,W.__data__):!Q-!W}for(var l=this._groups,y=l.length,r=Array(y),_=0;_u?1:f>=u?0:NaN}function kj(){var f=arguments[0];return arguments[0]=this,f.apply(null,arguments),this}function tj(){return Array.from(this)}function sj(){for(var f=this._groups,u=0,l=f.length;u1?this.each((u==null?hX:typeof u==="function"?pX:mX)(f,u,l==null?"":l)):Jy(this.node(),f)}function Jy(f,u){return f.style.getPropertyValue(u)||X3(f).getComputedStyle(f,null).getPropertyValue(u)}function IX(f){return function(){delete this[f]}}function gX(f,u){return function(){this[f]=u}}function kX(f,u){return function(){var l=u.apply(this,arguments);if(l==null)delete this[f];else this[f]=l}}function uA(f,u){return arguments.length>1?this.each((u==null?IX:typeof u==="function"?kX:gX)(f,u)):this.node()[f]}function aG(f){return f.trim().split(/^|\s+/)}function lA(f){return f.classList||new dG(f)}function dG(f){this._node=f,this._names=aG(f.getAttribute("class")||"")}dG.prototype={add:function(f){var u=this._names.indexOf(f);if(u<0)this._names.push(f),this._node.setAttribute("class",this._names.join(" "))},remove:function(f){var u=this._names.indexOf(f);if(u>=0)this._names.splice(u,1),this._node.setAttribute("class",this._names.join(" "))},contains:function(f){return this._names.indexOf(f)>=0}};function eG(f,u){var l=lA(f),y=-1,r=u.length;while(++y=0)l=u.slice(y+1),u=u.slice(0,y);return{type:u,name:l}})}function WY(f){return function(){var u=this.__on;if(!u)return;for(var l=0,y=-1,r=u.length,_;l()=>f;function T3(f,{sourceEvent:u,subject:l,target:y,identifier:r,active:_,x:$,y:j,dx:A,dy:F,dispatch:U}){Object.defineProperties(this,{type:{value:f,enumerable:!0,configurable:!0},sourceEvent:{value:u,enumerable:!0,configurable:!0},subject:{value:l,enumerable:!0,configurable:!0},target:{value:y,enumerable:!0,configurable:!0},identifier:{value:r,enumerable:!0,configurable:!0},active:{value:_,enumerable:!0,configurable:!0},x:{value:$,enumerable:!0,configurable:!0},y:{value:j,enumerable:!0,configurable:!0},dx:{value:A,enumerable:!0,configurable:!0},dy:{value:F,enumerable:!0,configurable:!0},_:{value:U}})}T3.prototype.on=function(){var f=this._.on.apply(this._,arguments);return f===this._?this:f};function BY(f){return!f.ctrlKey&&!f.button}function XY(){return this.parentNode}function YY(f,u){return u==null?{x:f.x,y:f.y}:u}function wY(){return navigator.maxTouchPoints||"ontouchstart"in this}function n3(){var f=BY,u=XY,l=YY,y=wY,r={},_=Ar("start","drag","end"),$=0,j,A,F,U,Q=0;function W(H){H.on("mousedown.drag",G).filter(y).on("touchstart.drag",O).on("touchmove.drag",z,yK).on("touchend.drag touchcancel.drag",Z).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function G(H,Y){if(U||!f.call(this,H,Y))return;var w=N(this,u.call(this,H,Y),H,Y,"mouse");if(!w)return;bu(H.view).on("mousemove.drag",K,Fr).on("mouseup.drag",E,Fr),V_(H.view),n8(H),F=!1,j=H.clientX,A=H.clientY,w("start",H)}function K(H){if(q1(H),!F){var Y=H.clientX-j,w=H.clientY-A;F=Y*Y+w*w>Q}r.mouse("drag",H)}function E(H){bu(H.view).on("mousemove.drag mouseup.drag",null),w3(H.view,F),q1(H),r.mouse("end",H)}function O(H,Y){if(!f.call(this,H,Y))return;var w=H.changedTouches,V=u.call(this,H,Y),X=w.length,i,m;for(i=0;i>8&15|u>>4&240,u>>4&15|u&240,(u&15)<<4|u&15,1):l===8?M8(u>>24&255,u>>16&255,u>>8&255,(u&255)/255):l===4?M8(u>>12&15|u>>8&240,u>>8&15|u>>4&240,u>>4&15|u&240,((u&15)<<4|u&15)/255):null):(u=TY.exec(f))?new P0(u[1],u[2],u[3],1):(u=nY.exec(f))?new P0(u[1]*255/100,u[2]*255/100,u[3]*255/100,1):(u=MY.exec(f))?M8(u[1],u[2],u[3],u[4]):(u=SY.exec(f))?M8(u[1]*255/100,u[2]*255/100,u[3]*255/100,u[4]):(u=PY.exec(f))?JK(u[1],u[2]/100,u[3]/100,1):(u=CY.exec(f))?JK(u[1],u[2]/100,u[3]/100,u[4]):rK.hasOwnProperty(f)?jK(rK[f]):f==="transparent"?new P0(NaN,NaN,NaN,0):null}function jK(f){return new P0(f>>16&255,f>>8&255,f&255,1)}function M8(f,u,l,y){if(y<=0)f=u=l=NaN;return new P0(f,u,l,y)}function RY(f){if(!(f instanceof C3))f=Pl(f);if(!f)return new P0;return f=f.rgb(),new P0(f.r,f.g,f.b,f.opacity)}function B_(f,u,l,y){return arguments.length===1?RY(f):new P0(f,u,l,y==null?1:y)}function P0(f,u,l,y){this.r=+f,this.g=+u,this.b=+l,this.opacity=+y}M3(P0,B_,NA(C3,{brighter(f){return f=f==null?P8:Math.pow(P8,f),new P0(this.r*f,this.g*f,this.b*f,this.opacity)},darker(f){return f=f==null?S3:Math.pow(S3,f),new P0(this.r*f,this.g*f,this.b*f,this.opacity)},rgb(){return this},clamp(){return new P0(Ur(this.r),Ur(this.g),Ur(this.b),C8(this.opacity))},displayable(){return-0.5<=this.r&&this.r<255.5&&(-0.5<=this.g&&this.g<255.5)&&(-0.5<=this.b&&this.b<255.5)&&(0<=this.opacity&&this.opacity<=1)},hex:AK,formatHex:AK,formatHex8:xY,formatRgb:FK,toString:FK}));function AK(){return`#${Jr(this.r)}${Jr(this.g)}${Jr(this.b)}`}function xY(){return`#${Jr(this.r)}${Jr(this.g)}${Jr(this.b)}${Jr((isNaN(this.opacity)?1:this.opacity)*255)}`}function FK(){let f=C8(this.opacity);return`${f===1?"rgb(":"rgba("}${Ur(this.r)}, ${Ur(this.g)}, ${Ur(this.b)}${f===1?")":`, ${f})`}`}function C8(f){return isNaN(f)?1:Math.max(0,Math.min(1,f))}function Ur(f){return Math.max(0,Math.min(255,Math.round(f)||0))}function Jr(f){return f=Ur(f),(f<16?"0":"")+f.toString(16)}function JK(f,u,l,y){if(y<=0)f=u=l=NaN;else if(l<=0||l>=1)f=u=NaN;else if(u<=0)f=NaN;return new Sl(f,u,l,y)}function QK(f){if(f instanceof Sl)return new Sl(f.h,f.s,f.l,f.opacity);if(!(f instanceof C3))f=Pl(f);if(!f)return new Sl;if(f instanceof Sl)return f;f=f.rgb();var u=f.r/255,l=f.g/255,y=f.b/255,r=Math.min(u,l,y),_=Math.max(u,l,y),$=NaN,j=_-r,A=(_+r)/2;if(j){if(u===_)$=(l-y)/j+(l0&&A<1?0:$;return new Sl($,j,A,f.opacity)}function WK(f,u,l,y){return arguments.length===1?QK(f):new Sl(f,u,l,y==null?1:y)}function Sl(f,u,l,y){this.h=+f,this.s=+u,this.l=+l,this.opacity=+y}M3(Sl,WK,NA(C3,{brighter(f){return f=f==null?P8:Math.pow(P8,f),new Sl(this.h,this.s,this.l*f,this.opacity)},darker(f){return f=f==null?S3:Math.pow(S3,f),new Sl(this.h,this.s,this.l*f,this.opacity)},rgb(){var f=this.h%360+(this.h<0)*360,u=isNaN(f)||isNaN(this.s)?0:this.s,l=this.l,y=l+(l<0.5?l:1-l)*u,r=2*l-y;return new P0(ZA(f>=240?f-240:f+120,r,y),ZA(f,r,y),ZA(f<120?f+240:f-120,r,y),this.opacity)},clamp(){return new Sl(UK(this.h),S8(this.s),S8(this.l),C8(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&(0<=this.l&&this.l<=1)&&(0<=this.opacity&&this.opacity<=1)},formatHsl(){let f=C8(this.opacity);return`${f===1?"hsl(":"hsla("}${UK(this.h)}, ${S8(this.s)*100}%, ${S8(this.l)*100}%${f===1?")":`, ${f})`}`}}));function UK(f){return f=(f||0)%360,f<0?f+360:f}function S8(f){return Math.max(0,Math.min(1,f||0))}function ZA(f,u,l){return(f<60?u+(l-u)*f/60:f<180?l:f<240?u+(l-u)*(240-f)/60:u)*255}function EA(f,u,l,y,r){var _=f*f,$=_*f;return((1-3*f+3*_-$)*u+(4-6*_+3*$)*l+(1+3*f+3*_-3*$)*y+$*r)/6}function HA(f){var u=f.length-1;return function(l){var y=l<=0?l=0:l>=1?(l=1,u-1):Math.floor(l*u),r=f[y],_=f[y+1],$=y>0?f[y-1]:2*r-_,j=y()=>f;function bY(f,u){return function(l){return f+l*u}}function hY(f,u,l){return f=Math.pow(f,l),u=Math.pow(u,l)-f,l=1/l,function(y){return Math.pow(f+y*u,l)}}function zK(f){return(f=+f)===1?i8:function(u,l){return l-u?hY(u,l,f):c3(isNaN(u)?l:u)}}function i8(f,u){var l=u-f;return l?bY(f,l):c3(isNaN(f)?u:f)}var Qr=function f(u){var l=zK(u);function y(r,_){var $=l((r=B_(r)).r,(_=B_(_)).r),j=l(r.g,_.g),A=l(r.b,_.b),F=i8(r.opacity,_.opacity);return function(U){return r.r=$(U),r.g=j(U),r.b=A(U),r.opacity=F(U),r+""}}return y.gamma=f,y}(1);function GK(f){return function(u){var l=u.length,y=Array(l),r=Array(l),_=Array(l),$,j;for($=0;$l)if(_=u.slice(l,_),j[$])j[$]+=_;else j[++$]=_;if((y=y[0])===(r=r[0]))if(j[$])j[$]+=r;else j[++$]=r;else j[++$]=null,A.push({i:$,x:z0(y,r)});l=BA.lastIndex}if(l180)U+=360;else if(U-F>180)F+=360;W.push({i:Q.push(r(Q)+"rotate(",null,y)-2,x:z0(F,U)})}else if(U)Q.push(r(Q)+"rotate("+U+y)}function j(F,U,Q,W){if(F!==U)W.push({i:Q.push(r(Q)+"skewX(",null,y)-2,x:z0(F,U)});else if(U)Q.push(r(Q)+"skewX("+U+y)}function A(F,U,Q,W,G,K){if(F!==Q||U!==W){var E=G.push(r(G)+"scale(",null,",",null,")");K.push({i:E-4,x:z0(F,Q)},{i:E-2,x:z0(U,W)})}else if(Q!==1||W!==1)G.push(r(G)+"scale("+Q+","+W+")")}return function(F,U){var Q=[],W=[];return F=f(F),U=f(U),_(F.translateX,F.translateY,U.translateX,U.translateY,Q,W),$(F.rotate,U.rotate,Q,W),j(F.skewX,U.skewX,Q,W),A(F.scaleX,F.scaleY,U.scaleX,U.scaleY,Q,W),F=U=null,function(G){var K=-1,E=W.length,O;while(++K=0)f._call.call(void 0,u);f=f._next}--Y_}function XK(){zr=(h8=v3.now())+m8,Y_=R3=0;try{DK()}finally{Y_=0,Jw(),zr=0}}function Fw(){var f=v3.now(),u=f-h8;if(u>YK)m8-=u,h8=f}function Jw(){var f,u=b8,l,y=1/0;while(u)if(u._call){if(y>u._time)y=u._time;f=u,u=u._next}else l=u._next,u._next=null,u=f?f._next=l:b8=l;x3=f,DA(y)}function DA(f){if(Y_)return;if(R3)R3=clearTimeout(R3);var u=f-zr;if(u>24){if(f<1/0)R3=setTimeout(XK,f-v3.now()-m8);if(i3)i3=clearInterval(i3)}else{if(!i3)h8=v3.now(),i3=setInterval(Fw,YK);Y_=1,wK(XK)}}function m3(f,u,l){var y=new b3;return u=u==null?0:+u,y.restart((r)=>{y.stop(),f(r+u)},u,l),y}var Qw=Ar("start","end","cancel","interrupt"),Ww=[],MK=0,TK=1,g8=2,I8=3,nK=4,k8=5,p3=6;function V1(f,u,l,y,r,_){var $=f.__transition;if(!$)f.__transition={};else if(l in $)return;zw(f,l,{name:u,index:y,group:r,on:Qw,tween:Ww,time:_.time,delay:_.delay,duration:_.duration,ease:_.ease,timer:null,state:MK})}function I3(f,u){var l=hu(f,u);if(l.state>MK)throw Error("too late; already scheduled");return l}function r0(f,u){var l=hu(f,u);if(l.state>I8)throw Error("too late; already running");return l}function hu(f,u){var l=f.__transition;if(!l||!(l=l[u]))throw Error("transition not found");return l}function zw(f,u,l){var y=f.__transition,r;y[u]=l,l.timer=p8(_,0,l.time);function _(F){if(l.state=TK,l.timer.restart($,l.delay,l.time),l.delay<=F)$(F-l.delay)}function $(F){var U,Q,W,G;if(l.state!==TK)return A();for(U in y){if(G=y[U],G.name!==l.name)continue;if(G.state===I8)return m3($);if(G.state===nK)G.state=p3,G.timer.stop(),G.on.call("interrupt",f,f.__data__,G.index,G.group),delete y[U];else if(+Ug8&&y.state=0)u=u.slice(0,l);return!u||u==="start"})}function Pw(f,u,l){var y,r,_=Sw(u)?I3:r0;return function(){var $=_(this,f),j=$.on;if(j!==y)(r=(y=j).copy()).on(u,l);$.on=r}}function vA(f,u){var l=this._id;return arguments.length<2?hu(this.node(),l).on.on(f):this.each(Pw(l,f,u))}function Cw(f){return function(){var u=this.parentNode;for(var l in this.__transition)if(+l!==f)return;if(u)u.removeChild(this)}}function bA(){return this.on("end.remove",Cw(this._id))}function hA(f){var u=this._name,l=this._id;if(typeof f!=="function")f=Fy(f);for(var y=this._groups,r=y.length,_=Array(r),$=0;$()=>f;function dA(f,{sourceEvent:u,target:l,transform:y,dispatch:r}){Object.defineProperties(this,{type:{value:f,enumerable:!0,configurable:!0},sourceEvent:{value:u,enumerable:!0,configurable:!0},target:{value:l,enumerable:!0,configurable:!0},transform:{value:y,enumerable:!0,configurable:!0},_:{value:r}})}function Cl(f,u,l){this.k=f,this.x=u,this.y=l}Cl.prototype={constructor:Cl,scale:function(f){return f===1?this:new Cl(this.k*f,this.x,this.y)},translate:function(f,u){return f===0&u===0?this:new Cl(this.k,this.x+this.k*f,this.y+this.k*u)},apply:function(f){return[f[0]*this.k+this.x,f[1]*this.k+this.y]},applyX:function(f){return f*this.k+this.x},applyY:function(f){return f*this.k+this.y},invert:function(f){return[(f[0]-this.x)/this.k,(f[1]-this.y)/this.k]},invertX:function(f){return(f-this.x)/this.k},invertY:function(f){return(f-this.y)/this.k},rescaleX:function(f){return f.copy().domain(f.range().map(this.invertX,this).map(f.invert,f))},rescaleY:function(f){return f.copy().domain(f.range().map(this.invertY,this).map(f.invert,f))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Gr=new Cl(1,0,0);t3.prototype=Cl.prototype;function t3(f){while(!f.__zoom)if(!(f=f.parentNode))return Gr;return f.__zoom}function r2(f){f.stopImmediatePropagation()}function Kr(f){f.preventDefault(),f.stopImmediatePropagation()}function aw(f){return(!f.ctrlKey||f.type==="wheel")&&!f.button}function dw(){var f=this;if(f instanceof SVGElement){if(f=f.ownerSVGElement||f,f.hasAttribute("viewBox"))return f=f.viewBox.baseVal,[[f.x,f.y],[f.x+f.width,f.y+f.height]];return[[0,0],[f.width.baseVal.value,f.height.baseVal.value]]}return[[0,0],[f.clientWidth,f.clientHeight]]}function CK(){return this.__zoom||Gr}function ew(f){return-f.deltaY*(f.deltaMode===1?0.05:f.deltaMode?1:0.002)*(f.ctrlKey?10:1)}function fD(){return navigator.maxTouchPoints||"ontouchstart"in this}function uD(f,u,l){var y=f.invertX(u[0][0])-l[0][0],r=f.invertX(u[1][0])-l[1][0],_=f.invertY(u[0][1])-l[0][1],$=f.invertY(u[1][1])-l[1][1];return f.translate(r>y?(y+r)/2:Math.min(0,y)||Math.max(0,r),$>_?(_+$)/2:Math.min(0,_)||Math.max(0,$))}function s3(){var f=aw,u=dw,l=uD,y=ew,r=fD,_=[0,1/0],$=[[-1/0,-1/0],[1/0,1/0]],j=250,A=Wr,F=Ar("start","zoom","end"),U,Q,W,G=500,K=150,E=0,O=10;function z(T){T.property("__zoom",CK).on("wheel.zoom",X,{passive:!1}).on("mousedown.zoom",i).on("dblclick.zoom",m).filter(r).on("touchstart.zoom",M).on("touchmove.zoom",c).on("touchend.zoom touchcancel.zoom",C).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}z.transform=function(T,R,P,n){var B=T.selection?T.selection():T;if(B.property("__zoom",CK),T!==B)Y(T,R,P,n);else B.interrupt().each(function(){w(this,arguments).event(n).start().zoom(null,typeof R==="function"?R.apply(this,arguments):R).end()})},z.scaleBy=function(T,R,P,n){z.scaleTo(T,function(){var B=this.__zoom.k,D=typeof R==="function"?R.apply(this,arguments):R;return B*D},P,n)},z.scaleTo=function(T,R,P,n){z.transform(T,function(){var B=u.apply(this,arguments),D=this.__zoom,I=P==null?H(B):typeof P==="function"?P.apply(this,arguments):P,p=D.invert(I),k=typeof R==="function"?R.apply(this,arguments):R;return l(N(Z(D,k),I,p),B,$)},P,n)},z.translateBy=function(T,R,P,n){z.transform(T,function(){return l(this.__zoom.translate(typeof R==="function"?R.apply(this,arguments):R,typeof P==="function"?P.apply(this,arguments):P),u.apply(this,arguments),$)},null,n)},z.translateTo=function(T,R,P,n,B){z.transform(T,function(){var D=u.apply(this,arguments),I=this.__zoom,p=n==null?H(D):typeof n==="function"?n.apply(this,arguments):n;return l(Gr.translate(p[0],p[1]).scale(I.k).translate(typeof R==="function"?-R.apply(this,arguments):-R,typeof P==="function"?-P.apply(this,arguments):-P),D,$)},n,B)};function Z(T,R){return R=Math.max(_[0],Math.min(_[1],R)),R===T.k?T:new Cl(R,T.x,T.y)}function N(T,R,P){var n=R[0]-P[0]*T.k,B=R[1]-P[1]*T.k;return n===T.x&&B===T.y?T:new Cl(T.k,n,B)}function H(T){return[(+T[0][0]+ +T[1][0])/2,(+T[0][1]+ +T[1][1])/2]}function Y(T,R,P,n){T.on("start.zoom",function(){w(this,arguments).event(n).start()}).on("interrupt.zoom end.zoom",function(){w(this,arguments).event(n).end()}).tween("zoom",function(){var B=this,D=arguments,I=w(B,D).event(n),p=u.apply(B,D),k=P==null?H(p):typeof P==="function"?P.apply(B,D):P,_f=Math.max(p[1][0]-p[0][0],p[1][1]-p[0][1]),S=B.__zoom,e=typeof R==="function"?R.apply(B,D):R,$f=A(S.invert(k).concat(_f/S.k),e.invert(k).concat(_f/e.k));return function(Qf){if(Qf===1)Qf=e;else{var Af=$f(Qf),zf=_f/Af[2];Qf=new Cl(zf,k[0]-Af[0]*zf,k[1]-Af[1]*zf)}I.zoom(null,Qf)}})}function w(T,R,P){return!P&&T.__zooming||new V(T,R)}function V(T,R){this.that=T,this.args=R,this.active=0,this.sourceEvent=null,this.extent=u.apply(T,R),this.taps=0}V.prototype={event:function(T){if(T)this.sourceEvent=T;return this},start:function(){if(++this.active===1)this.that.__zooming=this,this.emit("start");return this},zoom:function(T,R){if(this.mouse&&T!=="mouse")this.mouse[1]=R.invert(this.mouse[0]);if(this.touch0&&T!=="touch")this.touch0[1]=R.invert(this.touch0[0]);if(this.touch1&&T!=="touch")this.touch1[1]=R.invert(this.touch1[0]);return this.that.__zoom=R,this.emit("zoom"),this},end:function(){if(--this.active===0)delete this.that.__zooming,this.emit("end");return this},emit:function(T){var R=bu(this.that).datum();F.call(T,this.that,new dA(T,{sourceEvent:this.sourceEvent,target:z,type:T,transform:this.that.__zoom,dispatch:F}),R)}};function X(T,...R){if(!f.apply(this,arguments))return;var P=w(this,R).event(T),n=this.__zoom,B=Math.max(_[0],Math.min(_[1],n.k*Math.pow(2,y.apply(this,arguments)))),D=W0(T);if(P.wheel){if(P.mouse[0][0]!==D[0]||P.mouse[0][1]!==D[1])P.mouse[1]=n.invert(P.mouse[0]=D);clearTimeout(P.wheel)}else if(n.k===B)return;else P.mouse=[D,n.invert(D)],Uy(this),P.start();Kr(T),P.wheel=setTimeout(I,K),P.zoom("mouse",l(N(Z(n,B),P.mouse[0],P.mouse[1]),P.extent,$));function I(){P.wheel=null,P.end()}}function i(T,...R){if(W||!f.apply(this,arguments))return;var P=T.currentTarget,n=w(this,R,!0).event(T),B=bu(T.view).on("mousemove.zoom",k,!0).on("mouseup.zoom",_f,!0),D=W0(T,P),I=T.clientX,p=T.clientY;V_(T.view),r2(T),n.mouse=[D,this.__zoom.invert(D)],Uy(this),n.start();function k(S){if(Kr(S),!n.moved){var e=S.clientX-I,$f=S.clientY-p;n.moved=e*e+$f*$f>E}n.event(S).zoom("mouse",l(N(n.that.__zoom,n.mouse[0]=W0(S,P),n.mouse[1]),n.extent,$))}function _f(S){B.on("mousemove.zoom mouseup.zoom",null),w3(S.view,n.moved),Kr(S),n.event(S).end()}}function m(T,...R){if(!f.apply(this,arguments))return;var P=this.__zoom,n=W0(T.changedTouches?T.changedTouches[0]:T,this),B=P.invert(n),D=P.k*(T.shiftKey?0.5:2),I=l(N(Z(P,D),n,B),u.apply(this,R),$);if(Kr(T),j>0)bu(this).transition().duration(j).call(Y,I,n,T);else bu(this).call(z.transform,I,n,T)}function M(T,...R){if(!f.apply(this,arguments))return;var P=T.touches,n=P.length,B=w(this,R,T.changedTouches.length===n).event(T),D,I,p,k;r2(T);for(I=0;I"[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001",error002:()=>"It looks like you've created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them.",error003:(f)=>`Node type "${f}" not found. Using fallback type "default".`,error004:()=>"The React Flow parent container needs a width and a height to render the graph.",error005:()=>"Only child nodes can use a parent extent.",error006:()=>"Can't create edge. An edge needs a source and a target.",error007:(f)=>`The old edge with id=${f} does not exist.`,error009:(f)=>`Marker type "${f}" doesn't exist.`,error008:(f,{id:u,sourceHandle:l,targetHandle:y})=>`Couldn't create edge for ${f} handle id: "${f==="source"?l:y}", edge id: ${u}.`,error010:()=>"Handle: No node id found. Make sure to only use a Handle inside a custom Node.",error011:(f)=>`Edge type "${f}" not found. Using fallback type "default".`,error012:(f)=>`Node with id "${f}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,error013:(f="react")=>`It seems that you haven't loaded the styles. Please import '@xyflow/${f}/dist/style.css' or base.css to make sure everything is working properly.`,error014:()=>"useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.",error015:()=>"It seems that you are trying to drag a node that is not initialized. Please use onNodesChange as explained in the docs."},S_=[[Number.NEGATIVE_INFINITY,Number.NEGATIVE_INFINITY],[Number.POSITIVE_INFINITY,Number.POSITIVE_INFINITY]],yF=["Enter"," ","Escape"],rF={"node.a11yDescription.default":"Press enter or space to select a node. Press delete to remove it and escape to cancel.","node.a11yDescription.keyboardDisabled":"Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.","node.a11yDescription.ariaLiveMessage":({direction:f,x:u,y:l})=>`Moved selected node ${f}. New position, x: ${u}, y: ${l}`,"edge.a11yDescription.default":"Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.","controls.ariaLabel":"Control Panel","controls.zoomIn.ariaLabel":"Zoom In","controls.zoomOut.ariaLabel":"Zoom Out","controls.fitView.ariaLabel":"Fit View","controls.interactive.ariaLabel":"Toggle Interactivity","minimap.ariaLabel":"Mini Map","handle.ariaLabel":"Handle"},zy;(function(f){f.Strict="strict",f.Loose="loose"})(zy||(zy={}));var B1;(function(f){f.Free="free",f.Vertical="vertical",f.Horizontal="horizontal"})(B1||(B1={}));var Nr;(function(f){f.Partial="partial",f.Full="full"})(Nr||(Nr={}));var _F={inProgress:!1,isValid:null,from:null,fromHandle:null,fromPosition:null,fromNode:null,to:null,toHandle:null,toPosition:null,toNode:null,pointer:null},dl;(function(f){f.Bezier="default",f.Straight="straight",f.Step="step",f.SmoothStep="smoothstep",f.SimpleBezier="simplebezier"})(dl||(dl={}));var Gy;(function(f){f.Arrow="arrow",f.ArrowClosed="arrowclosed"})(Gy||(Gy={}));var Gf;(function(f){f.Left="left",f.Top="top",f.Right="right",f.Bottom="bottom"})(Gf||(Gf={}));var cK={[Gf.Left]:Gf.Right,[Gf.Right]:Gf.Left,[Gf.Top]:Gf.Bottom,[Gf.Bottom]:Gf.Top};function $F(f){return f===null?null:f?"valid":"invalid"}var jF=(f)=>("id"in f)&&("source"in f)&&("target"in f),sK=(f)=>("id"in f)&&("position"in f)&&!("source"in f)&&!("target"in f),AF=(f)=>("id"in f)&&("internals"in f)&&!("source"in f)&&!("target"in f);var d3=(f,u=[0,0])=>{let{width:l,height:y}=el(f),r=f.origin??u,_=l*r[0],$=y*r[1];return{x:f.position.x-_,y:f.position.y-$}},FF=(f,u={nodeOrigin:[0,0]})=>{if(f.length===0)return{x:0,y:0,width:0,height:0};let l=f.reduce((y,r)=>{let _=typeof r==="string",$=!u.nodeLookup&&!_?r:void 0;if(u.nodeLookup)$=_?u.nodeLookup.get(r):!AF(r)?u.nodeLookup.get(r.id):r;let j=$?j2($,u.nodeOrigin):{x:0,y:0,x2:0,y2:0};return F2(y,j)},{x:1/0,y:1/0,x2:-1/0,y2:-1/0});return J2(l)},P_=(f,u={})=>{let l={x:1/0,y:1/0,x2:-1/0,y2:-1/0},y=!1;return f.forEach((r)=>{if(u.filter===void 0||u.filter(r))l=F2(l,j2(r)),y=!0}),y?J2(l):{x:0,y:0,width:0,height:0}},A2=(f,u,[l,y,r]=[0,0,1],_=!1,$=!1)=>{let j={...i_(u,[l,y,r]),width:u.width/r,height:u.height/r},A=[];for(let F of f.values()){let{measured:U,selectable:Q=!0,hidden:W=!1}=F;if($&&!Q||W)continue;let G=U.width??F.width??F.initialWidth??null,K=U.height??F.height??F.initialHeight??null,E=C_(j,Er(F)),O=(G??0)*(K??0),z=_&&E>0;if(!F.internals.handleBounds||z||E>=O||F.dragging)A.push(F)}return A},oK=(f,u)=>{let l=new Set;return f.forEach((y)=>{l.add(y.id)}),u.filter((y)=>l.has(y.source)||l.has(y.target))};function lD(f,u){let l=new Map,y=u?.nodes?new Set(u.nodes.map((r)=>r.id)):null;return f.forEach((r)=>{if(r.measured.width&&r.measured.height&&(u?.includeHiddenNodes||!r.hidden)&&(!y||y.has(r.id)))l.set(r.id,r)}),l}async function aK({nodes:f,width:u,height:l,panZoom:y,minZoom:r,maxZoom:_},$){if(f.size===0)return Promise.resolve(!0);let j=lD(f,$),A=P_(j),F=e3(A,u,l,$?.minZoom??r,$?.maxZoom??_,$?.padding??0.1);return await y.setViewport(F,{duration:$?.duration,ease:$?.ease,interpolate:$?.interpolate}),Promise.resolve(!0)}function JF({nodeId:f,nextPosition:u,nodeLookup:l,nodeOrigin:y=[0,0],nodeExtent:r,onError:_}){let $=l.get(f),j=$.parentId?l.get($.parentId):void 0,{x:A,y:F}=j?j.internals.positionAbsolute:{x:0,y:0},U=$.origin??y,Q=$.extent||r;if($.extent==="parent"&&!$.expandParent)if(!j)_?.("005",a0.error005());else{let G=j.measured.width,K=j.measured.height;if(G&&K)Q=[[A,F],[A+G,F+K]]}else if(j&&M_($.extent))Q=[[$.extent[0][0]+A,$.extent[0][1]+F],[$.extent[1][0]+A,$.extent[1][1]+F]];let W=M_(Q)?Zr(u,Q,$.measured):u;if($.measured.width===void 0||$.measured.height===void 0)_?.("015",a0.error015());return{position:{x:W.x-A+($.measured.width??0)*U[0],y:W.y-F+($.measured.height??0)*U[1]},positionAbsolute:W}}async function dK({nodesToRemove:f=[],edgesToRemove:u=[],nodes:l,edges:y,onBeforeDelete:r}){let _=new Set(f.map((W)=>W.id)),$=[];for(let W of l){if(W.deletable===!1)continue;let G=_.has(W.id),K=!G&&W.parentId&&$.find((E)=>E.id===W.parentId);if(G||K)$.push(W)}let j=new Set(u.map((W)=>W.id)),A=y.filter((W)=>W.deletable!==!1),U=oK($,A);for(let W of A)if(j.has(W.id)&&!U.find((K)=>K.id===W.id))U.push(W);if(!r)return{edges:U,nodes:$};let Q=await r({nodes:$,edges:U});if(typeof Q==="boolean")return Q?{edges:U,nodes:$}:{edges:[],nodes:[]};return Q}var n_=(f,u=0,l=1)=>Math.min(Math.max(f,u),l),Zr=(f={x:0,y:0},u,l)=>({x:n_(f.x,u[0][0],u[1][0]-(l?.width??0)),y:n_(f.y,u[0][1],u[1][1]-(l?.height??0))});function eK(f,u,l){let{width:y,height:r}=el(l),{x:_,y:$}=l.internals.positionAbsolute;return Zr(f,[[_,$],[_+y,$+r]],u)}var iK=(f,u,l)=>{if(fl)return-n_(Math.abs(f-l),1,u)/u;return 0},fN=(f,u,l=15,y=40)=>{let r=iK(f.x,y,u.width-y)*l,_=iK(f.y,y,u.height-y)*l;return[r,_]},F2=(f,u)=>({x:Math.min(f.x,u.x),y:Math.min(f.y,u.y),x2:Math.max(f.x2,u.x2),y2:Math.max(f.y2,u.y2)}),lF=({x:f,y:u,width:l,height:y})=>({x:f,y:u,x2:f+l,y2:u+y}),J2=({x:f,y:u,x2:l,y2:y})=>({x:f,y:u,width:l-f,height:y-u}),Er=(f,u=[0,0])=>{let{x:l,y}=AF(f)?f.internals.positionAbsolute:d3(f,u);return{x:l,y,width:f.measured?.width??f.width??f.initialWidth??0,height:f.measured?.height??f.height??f.initialHeight??0}},j2=(f,u=[0,0])=>{let{x:l,y}=AF(f)?f.internals.positionAbsolute:d3(f,u);return{x:l,y,x2:l+(f.measured?.width??f.width??f.initialWidth??0),y2:y+(f.measured?.height??f.height??f.initialHeight??0)}},UF=(f,u)=>J2(F2(lF(f),lF(u))),C_=(f,u)=>{let l=Math.max(0,Math.min(f.x+f.width,u.x+u.width)-Math.max(f.x,u.x)),y=Math.max(0,Math.min(f.y+f.height,u.y+u.height)-Math.max(f.y,u.y));return Math.ceil(l*y)},QF=(f)=>zl(f.width)&&zl(f.height)&&zl(f.x)&&zl(f.y),zl=(f)=>!isNaN(f)&&isFinite(f),WF=(f,u)=>{},c_=(f,u=[1,1])=>{return{x:u[0]*Math.round(f.x/u[0]),y:u[1]*Math.round(f.y/u[1])}},i_=({x:f,y:u},[l,y,r],_=!1,$=[1,1])=>{let j={x:(f-l)/r,y:(u-y)/r};return _?c_(j,$):j},a3=({x:f,y:u},[l,y,r])=>{return{x:f*r+l,y:u*r+y}};function D_(f,u){if(typeof f==="number")return Math.floor((u-u/(1+f))*0.5);if(typeof f==="string"&&f.endsWith("px")){let l=parseFloat(f);if(!Number.isNaN(l))return Math.floor(l)}if(typeof f==="string"&&f.endsWith("%")){let l=parseFloat(f);if(!Number.isNaN(l))return Math.floor(u*l*0.01)}return console.error(`[React Flow] The padding value "${f}" is invalid. Please provide a number or a string with a valid unit (px or %).`),0}function yD(f,u,l){if(typeof f==="string"||typeof f==="number"){let y=D_(f,l),r=D_(f,u);return{top:y,right:r,bottom:y,left:r,x:r*2,y:y*2}}if(typeof f==="object"){let y=D_(f.top??f.y??0,l),r=D_(f.bottom??f.y??0,l),_=D_(f.left??f.x??0,u),$=D_(f.right??f.x??0,u);return{top:y,right:$,bottom:r,left:_,x:_+$,y:y+r}}return{top:0,right:0,bottom:0,left:0,x:0,y:0}}function rD(f,u,l,y,r,_){let{x:$,y:j}=a3(f,[u,l,y]),{x:A,y:F}=a3({x:f.x+f.width,y:f.y+f.height},[u,l,y]),U=r-A,Q=_-F;return{left:Math.floor($),top:Math.floor(j),right:Math.floor(U),bottom:Math.floor(Q)}}var e3=(f,u,l,y,r,_)=>{let $=yD(_,u,l),j=(u-$.x)/f.width,A=(l-$.y)/f.height,F=Math.min(j,A),U=n_(F,y,r),Q=f.x+f.width/2,W=f.y+f.height/2,G=u/2-Q*U,K=l/2-W*U,E=rD(f,G,K,U,u,l),O={left:Math.min(E.left-$.left,0),top:Math.min(E.top-$.top,0),right:Math.min(E.right-$.right,0),bottom:Math.min(E.bottom-$.bottom,0)};return{x:G-O.left+O.right,y:K-O.top+O.bottom,zoom:U}},R_=()=>typeof navigator<"u"&&navigator?.userAgent?.indexOf("Mac")>=0;function M_(f){return f!==void 0&&f!==null&&f!=="parent"}function el(f){return{width:f.measured?.width??f.width??f.initialWidth??0,height:f.measured?.height??f.height??f.initialHeight??0}}function zF(f){return(f.measured?.width??f.width??f.initialWidth)!==void 0&&(f.measured?.height??f.height??f.initialHeight)!==void 0}function GF(f,u={width:0,height:0},l,y,r){let _={...f},$=y.get(l);if($){let j=$.origin||r;_.x+=$.internals.positionAbsolute.x-(u.width??0)*j[0],_.y+=$.internals.positionAbsolute.y-(u.height??0)*j[1]}return _}function KF(f,u){if(f.size!==u.size)return!1;for(let l of f)if(!u.has(l))return!1;return!0}function uN(){let f,u;return{promise:new Promise((y,r)=>{f=y,u=r}),resolve:f,reject:u}}function lN(f){return{...rF,...f||{}}}function o3(f,{snapGrid:u=[0,0],snapToGrid:l=!1,transform:y,containerBounds:r}){let{x:_,y:$}=Gl(f),j=i_({x:_-(r?.left??0),y:$-(r?.top??0)},y),{x:A,y:F}=l?c_(j,u):j;return{xSnapped:A,ySnapped:F,...j}}var U2=(f)=>({width:f.offsetWidth,height:f.offsetHeight}),NF=(f)=>f?.getRootNode?.()||window?.document,_D=["INPUT","SELECT","TEXTAREA"];function ZF(f){let u=f.composedPath?.()?.[0]||f.target;if(u?.nodeType!==1)return!1;return _D.includes(u.nodeName)||u.hasAttribute("contenteditable")||!!u.closest(".nokey")}var EF=(f)=>("clientX"in f),Gl=(f,u)=>{let l=EF(f),y=l?f.clientX:f.touches?.[0].clientX,r=l?f.clientY:f.touches?.[0].clientY;return{x:y-(u?.left??0),y:r-(u?.top??0)}},RK=(f,u,l,y,r)=>{let _=u.querySelectorAll(`.${f}`);if(!_||!_.length)return null;return Array.from(_).map(($)=>{let j=$.getBoundingClientRect();return{id:$.getAttribute("data-handleid"),type:f,nodeId:r,position:$.getAttribute("data-handlepos"),x:(j.left-l.left)/y,y:(j.top-l.top)/y,...U2($)}})};function Q2({sourceX:f,sourceY:u,targetX:l,targetY:y,sourceControlX:r,sourceControlY:_,targetControlX:$,targetControlY:j}){let A=f*0.125+r*0.375+$*0.375+l*0.125,F=u*0.125+_*0.375+j*0.375+y*0.125,U=Math.abs(A-f),Q=Math.abs(F-u);return[A,F,U,Q]}function _2(f,u){if(f>=0)return 0.5*f;return u*25*Math.sqrt(-f)}function xK({pos:f,x1:u,y1:l,x2:y,y2:r,c:_}){switch(f){case Gf.Left:return[u-_2(u-y,_),l];case Gf.Right:return[u+_2(y-u,_),l];case Gf.Top:return[u,l-_2(l-r,_)];case Gf.Bottom:return[u,l+_2(r-l,_)]}}function W2({sourceX:f,sourceY:u,sourcePosition:l=Gf.Bottom,targetX:y,targetY:r,targetPosition:_=Gf.Top,curvature:$=0.25}){let[j,A]=xK({pos:l,x1:f,y1:u,x2:y,y2:r,c:$}),[F,U]=xK({pos:_,x1:y,y1:r,x2:f,y2:u,c:$}),[Q,W,G,K]=Q2({sourceX:f,sourceY:u,targetX:y,targetY:r,sourceControlX:j,sourceControlY:A,targetControlX:F,targetControlY:U});return[`M${f},${u} C${j},${A} ${F},${U} ${y},${r}`,Q,W,G,K]}function HF({sourceX:f,sourceY:u,targetX:l,targetY:y}){let r=Math.abs(l-f)/2,_=l0}var $D=({source:f,sourceHandle:u,target:l,targetHandle:y})=>`xy-edge__${f}${u||""}-${l}${y||""}`,jD=(f,u)=>{return u.some((l)=>l.source===f.source&&l.target===f.target&&(l.sourceHandle===f.sourceHandle||!l.sourceHandle&&!f.sourceHandle)&&(l.targetHandle===f.targetHandle||!l.targetHandle&&!f.targetHandle))},OF=(f,u,l={})=>{if(!f.source||!f.target)return WF("006",a0.error006()),u;let y=l.getEdgeId||$D,r;if(jF(f))r={...f};else r={...f,id:y(f)};if(jD(r,u))return u;if(r.sourceHandle===null)delete r.sourceHandle;if(r.targetHandle===null)delete r.targetHandle;return u.concat(r)};function z2({sourceX:f,sourceY:u,targetX:l,targetY:y}){let[r,_,$,j]=HF({sourceX:f,sourceY:u,targetX:l,targetY:y});return[`M ${f},${u}L ${l},${y}`,r,_,$,j]}var vK={[Gf.Left]:{x:-1,y:0},[Gf.Right]:{x:1,y:0},[Gf.Top]:{x:0,y:-1},[Gf.Bottom]:{x:0,y:1}},AD=({source:f,sourcePosition:u=Gf.Bottom,target:l})=>{if(u===Gf.Left||u===Gf.Right)return f.xMath.sqrt(Math.pow(u.x-f.x,2)+Math.pow(u.y-f.y,2));function FD({source:f,sourcePosition:u=Gf.Bottom,target:l,targetPosition:y=Gf.Top,center:r,offset:_,stepPosition:$}){let j=vK[u],A=vK[y],F={x:f.x+j.x*_,y:f.y+j.y*_},U={x:l.x+A.x*_,y:l.y+A.y*_},Q=AD({source:F,sourcePosition:u,target:U}),W=Q.x!==0?"x":"y",G=Q[W],K=[],E,O,z={x:0,y:0},Z={x:0,y:0},[,,N,H]=HF({sourceX:f.x,sourceY:f.y,targetX:l.x,targetY:l.y});if(j[W]*A[W]===-1){if(W==="x")E=r.x??F.x+(U.x-F.x)*$,O=r.y??(F.y+U.y)/2;else E=r.x??(F.x+U.x)/2,O=r.y??F.y+(U.y-F.y)*$;let X=[{x:E,y:F.y},{x:E,y:U.y}],i=[{x:F.x,y:O},{x:U.x,y:O}];if(j[W]===G)K=W==="x"?X:i;else K=W==="x"?i:X}else{let X=[{x:F.x,y:U.y}],i=[{x:U.x,y:F.y}];if(W==="x")K=j.x===G?i:X;else K=j.y===G?X:i;if(u===y){let T=Math.abs(f[W]-l[W]);if(T<=_){let R=Math.min(_-1,_-T);if(j[W]===G)z[W]=(F[W]>f[W]?-1:1)*R;else Z[W]=(U[W]>l[W]?-1:1)*R}}if(u!==y){let T=W==="x"?"y":"x",R=j[W]===A[T],P=F[T]>U[T],n=F[T]=C)E=(m.x+M.x)/2,O=K[0].y;else E=K[0].x,O=(m.y+M.y)/2}let Y={x:F.x+z.x,y:F.y+z.y},w={x:U.x+Z.x,y:U.y+Z.y};return[[f,...Y.x!==K[0].x||Y.y!==K[0].y?[Y]:[],...K,...w.x!==K[K.length-1].x||w.y!==K[K.length-1].y?[w]:[],l],E,O,N,H]}function JD(f,u,l,y){let r=Math.min(bK(f,u)/2,bK(u,l)/2,y),{x:_,y:$}=u;if(f.x===_&&_===l.x||f.y===$&&$===l.y)return`L${_} ${$}`;if(f.y===$){let F=f.xl.id===u))||null}function G2(f,u){if(!f)return"";if(typeof f==="string")return f;return`${u?`${u}__`:""}${Object.keys(f).sort().map((y)=>`${y}=${f[y]}`).join("&")}`}function $N(f,{id:u,defaultColor:l,defaultMarkerStart:y,defaultMarkerEnd:r}){let _=new Set;return f.reduce(($,j)=>{return[j.markerStart||y,j.markerEnd||r].forEach((A)=>{if(A&&typeof A==="object"){let F=G2(A,u);if(!_.has(F))$.push({id:F,color:A.color||l,...A}),_.add(F)}}),$},[]).sort(($,j)=>$.id.localeCompare(j.id))}var jN=1000,UD=10,qF={nodeOrigin:[0,0],nodeExtent:S_,elevateNodesOnSelect:!0,zIndexMode:"basic",defaults:{}},QD={...qF,checkEquality:!0};function VF(f,u){let l={...f};for(let y in u)if(u[y]!==void 0)l[y]=u[y];return l}function AN(f,u,l){let y=VF(qF,l);for(let r of f.values())if(r.parentId)BF(r,f,u,y);else{let _=d3(r,y.nodeOrigin),$=M_(r.extent)?r.extent:y.nodeExtent,j=Zr(_,$,el(r));r.internals.positionAbsolute=j}}function WD(f,u){if(!f.handles)return!f.measured?void 0:u?.internals.handleBounds;let l=[],y=[];for(let r of f.handles){let _={id:r.id,width:r.width??1,height:r.height??1,nodeId:f.id,x:r.x,y:r.y,position:r.position,type:r.type};if(r.type==="source")l.push(_);else if(r.type==="target")y.push(_)}return{source:l,target:y}}function LF(f){return f==="manual"}function K2(f,u,l,y={}){let r=VF(QD,y),_={i:0},$=new Map(u),j=r?.elevateNodesOnSelect&&!LF(r.zIndexMode)?jN:0,A=f.length>0,F=!1;u.clear(),l.clear();for(let U of f){let Q=$.get(U.id);if(r.checkEquality&&U===Q?.internals.userNode)u.set(U.id,Q);else{let W=d3(U,r.nodeOrigin),G=M_(U.extent)?U.extent:r.nodeExtent,K=Zr(W,G,el(U));Q={...r.defaults,...U,measured:{width:U.measured?.width,height:U.measured?.height},internals:{positionAbsolute:K,handleBounds:WD(U,Q),z:FN(U,j,r.zIndexMode),userNode:U}},u.set(U.id,Q)}if((Q.measured===void 0||Q.measured.width===void 0||Q.measured.height===void 0)&&!Q.hidden)A=!1;if(U.parentId)BF(Q,u,l,y,_);F||=U.selected??!1}return{nodesInitialized:A,hasSelectedNodes:F}}function zD(f,u){if(!f.parentId)return;let l=u.get(f.parentId);if(l)l.set(f.id,f);else u.set(f.parentId,new Map([[f.id,f]]))}function BF(f,u,l,y,r){let{elevateNodesOnSelect:_,nodeOrigin:$,nodeExtent:j,zIndexMode:A}=VF(qF,y),F=f.parentId,U=u.get(F);if(!U){console.warn(`Parent node ${F} not found. Please make sure that parent nodes are in front of their child nodes in the nodes array.`);return}if(zD(f,l),r&&!U.parentId&&U.internals.rootParentIndex===void 0&&A==="auto")U.internals.rootParentIndex=++r.i,U.internals.z=U.internals.z+r.i*UD;if(r&&U.internals.rootParentIndex!==void 0)r.i=U.internals.rootParentIndex;let Q=_&&!LF(A)?jN:0,{x:W,y:G,z:K}=GD(f,U,$,j,Q,A),{positionAbsolute:E}=f.internals,O=W!==E.x||G!==E.y;if(O||K!==f.internals.z)u.set(f.id,{...f,internals:{...f.internals,positionAbsolute:O?{x:W,y:G}:E,z:K}})}function FN(f,u,l){let y=zl(f.zIndex)?f.zIndex:0;if(LF(l))return y;return y+(f.selected?u:0)}function GD(f,u,l,y,r,_){let{x:$,y:j}=u.internals.positionAbsolute,A=el(f),F=d3(f,l),U=M_(f.extent)?Zr(F,f.extent,A):F,Q=Zr({x:$+U.x,y:j+U.y},y,A);if(f.extent==="parent")Q=eK(Q,A,u);let W=FN(f,r,_),G=u.internals.z??0;return{x:Q.x,y:Q.y,z:G>=W?G+1:W}}function N2(f,u,l,y=[0,0]){let r=[],_=new Map;for(let $ of f){let j=u.get($.parentId);if(!j)continue;let A=_.get($.parentId)?.expandedRect??Er(j),F=UF(A,$.rect);_.set($.parentId,{expandedRect:F,parent:j})}if(_.size>0)_.forEach(({expandedRect:$,parent:j},A)=>{let F=j.internals.positionAbsolute,U=el(j),Q=j.origin??y,W=$.x0||G>0||O||z)r.push({id:A,type:"position",position:{x:j.position.x-W+O,y:j.position.y-G+z}}),l.get(A)?.forEach((Z)=>{if(!f.some((N)=>N.id===Z.id))r.push({id:Z.id,type:"position",position:{x:Z.position.x+W,y:Z.position.y+G}})});if(U.width<$.width||U.height<$.height||W||G)r.push({id:A,type:"dimensions",setAttributes:!0,dimensions:{width:K+(W?Q[0]*W-O:0),height:E+(G?Q[1]*G-z:0)}})});return r}function JN(f,u,l,y,r,_,$){let j=y?.querySelector(".xyflow__viewport"),A=!1;if(!j)return{changes:[],updatedInternals:A};let F=[],U=window.getComputedStyle(j),{m22:Q}=new window.DOMMatrixReadOnly(U.transform),W=[];for(let G of f.values()){let K=u.get(G.id);if(!K)continue;if(K.hidden){u.set(K.id,{...K,internals:{...K.internals,handleBounds:void 0}}),A=!0;continue}let E=U2(G.nodeElement),O=K.measured.width!==E.width||K.measured.height!==E.height;if(!!(E.width&&E.height&&(O||!K.internals.handleBounds||G.force))){let Z=G.nodeElement.getBoundingClientRect(),N=M_(K.extent)?K.extent:_,{positionAbsolute:H}=K.internals;if(K.parentId&&K.extent==="parent")H=eK(H,E,u.get(K.parentId));else if(N)H=Zr(H,N,E);let Y={...K,measured:E,internals:{...K.internals,positionAbsolute:H,handleBounds:{source:RK("source",G.nodeElement,Z,Q,K.id),target:RK("target",G.nodeElement,Z,Q,K.id)}}};if(u.set(K.id,Y),K.parentId)BF(Y,u,l,{nodeOrigin:r,zIndexMode:$});if(A=!0,O){if(F.push({id:K.id,type:"dimensions",dimensions:E}),K.expandParent&&K.parentId)W.push({id:K.id,parentId:K.parentId,rect:Er(Y,r)})}}}if(W.length>0){let G=N2(W,u,l,r);F.push(...G)}return{changes:F,updatedInternals:A}}async function UN({delta:f,panZoom:u,transform:l,translateExtent:y,width:r,height:_}){if(!u||!f.x&&!f.y)return Promise.resolve(!1);let $=await u.setViewportConstrained({x:l[0]+f.x,y:l[1]+f.y,zoom:l[2]},[[0,0],[r,_]],y),j=!!$&&($.x!==l[0]||$.y!==l[1]||$.k!==l[2]);return Promise.resolve(j)}function IK(f,u,l,y,r,_){let $=r,j=y.get($)||new Map;y.set($,j.set(l,u)),$=`${r}-${f}`;let A=y.get($)||new Map;if(y.set($,A.set(l,u)),_){$=`${r}-${f}-${_}`;let F=y.get($)||new Map;y.set($,F.set(l,u))}}function XF(f,u,l){f.clear(),u.clear();for(let y of l){let{source:r,target:_,sourceHandle:$=null,targetHandle:j=null}=y,A={edgeId:y.id,source:r,target:_,sourceHandle:$,targetHandle:j},F=`${r}-${$}--${_}-${j}`,U=`${_}-${j}--${r}-${$}`;IK("source",A,U,f,r,$),IK("target",A,F,f,_,j),u.set(y.id,y)}}function QN(f,u){if(!f.parentId)return!1;let l=u.get(f.parentId);if(!l)return!1;if(l.selected)return!0;return QN(l,u)}function gK(f,u,l){let y=f;do{if(y?.matches?.(u))return!0;if(y===l)return!1;y=y?.parentElement}while(y);return!1}function KD(f,u,l,y){let r=new Map;for(let[_,$]of f)if(($.selected||$.id===y)&&(!$.parentId||!QN($,f))&&($.draggable||u&&typeof $.draggable>"u")){let j=f.get(_);if(j)r.set(_,{id:_,position:j.position||{x:0,y:0},distance:{x:l.x-j.internals.positionAbsolute.x,y:l.y-j.internals.positionAbsolute.y},extent:j.extent,parentId:j.parentId,origin:j.origin,expandParent:j.expandParent,internals:{positionAbsolute:j.internals.positionAbsolute||{x:0,y:0}},measured:{width:j.measured.width??0,height:j.measured.height??0}})}return r}function eA({nodeId:f,dragItems:u,nodeLookup:l,dragging:y=!0}){let r=[];for(let[$,j]of u){let A=l.get($)?.internals.userNode;if(A)r.push({...A,position:j.position,dragging:y})}if(!f)return[r[0],r];let _=l.get(f)?.internals.userNode;return[!_?r[0]:{..._,position:u.get(f)?.position||_.position,dragging:y},r]}function ND({dragItems:f,snapGrid:u,x:l,y}){let r=f.values().next().value;if(!r)return null;let _={x:l-r.distance.x,y:y-r.distance.y},$=c_(_,u);return{x:$.x-_.x,y:$.y-_.y}}function WN({onNodeMouseDown:f,getStoreItems:u,onDragStart:l,onDrag:y,onDragStop:r}){let _={x:null,y:null},$=0,j=new Map,A=!1,F={x:0,y:0},U=null,Q=!1,W=null,G=!1,K=!1,E=null;function O({noDragClassName:Z,handleSelector:N,domNode:H,isSelectable:Y,nodeId:w,nodeClickDistance:V=0}){W=bu(H);function X({x:c,y:C}){let{nodeLookup:T,nodeExtent:R,snapGrid:P,snapToGrid:n,nodeOrigin:B,onNodeDrag:D,onSelectionDrag:I,onError:p,updateNodePositions:k}=u();_={x:c,y:C};let _f=!1,S=j.size>1,e=S&&R?lF(P_(j)):null,$f=S&&n?ND({dragItems:j,snapGrid:P,x:c,y:C}):null;for(let[Qf,Af]of j){if(!T.has(Qf))continue;let zf={x:c-Af.distance.x,y:C-Af.distance.y};if(n)zf=$f?{x:Math.round(zf.x+$f.x),y:Math.round(zf.y+$f.y)}:c_(zf,P);let Hf=null;if(S&&R&&!Af.extent&&e){let{positionAbsolute:t}=Af.internals,a=t.x-e.x+R[0][0],Nf=t.x+Af.measured.width-e.x2+R[1][0],o=t.y-e.y+R[0][1],uf=t.y+Af.measured.height-e.y2+R[1][1];Hf=[[a,o],[Nf,uf]]}let{position:Zf,positionAbsolute:b}=JF({nodeId:Qf,nextPosition:zf,nodeLookup:T,nodeExtent:Hf?Hf:R,nodeOrigin:B,onError:p});_f=_f||Af.position.x!==Zf.x||Af.position.y!==Zf.y,Af.position=Zf,Af.internals.positionAbsolute=b}if(K=K||_f,!_f)return;if(k(j,!0),E&&(y||D||!w&&I)){let[Qf,Af]=eA({nodeId:w,dragItems:j,nodeLookup:T});if(y?.(E,j,Qf,Af),D?.(E,Qf,Af),!w)I?.(E,Af)}}async function i(){if(!U)return;let{transform:c,panBy:C,autoPanSpeed:T,autoPanOnNodeDrag:R}=u();if(!R){A=!1,cancelAnimationFrame($);return}let[P,n]=fN(F,U,T);if(P!==0||n!==0){if(_.x=(_.x??0)-P/c[2],_.y=(_.y??0)-n/c[2],await C({x:P,y:n}))X(_)}$=requestAnimationFrame(i)}function m(c){let{nodeLookup:C,multiSelectionActive:T,nodesDraggable:R,transform:P,snapGrid:n,snapToGrid:B,selectNodesOnDrag:D,onNodeDragStart:I,onSelectionDragStart:p,unselectNodesAndEdges:k}=u();if(Q=!0,(!D||!Y)&&!T&&w){if(!C.get(w)?.selected)k()}if(Y&&D&&w)f?.(w);let _f=o3(c.sourceEvent,{transform:P,snapGrid:n,snapToGrid:B,containerBounds:U});if(_=_f,j=KD(C,R,_f,w),j.size>0&&(l||I||!w&&p)){let[S,e]=eA({nodeId:w,dragItems:j,nodeLookup:C});if(l?.(c.sourceEvent,j,S,e),I?.(c.sourceEvent,S,e),!w)p?.(c.sourceEvent,e)}}let M=n3().clickDistance(V).on("start",(c)=>{let{domNode:C,nodeDragThreshold:T,transform:R,snapGrid:P,snapToGrid:n}=u();if(U=C?.getBoundingClientRect()||null,G=!1,K=!1,E=c.sourceEvent,T===0)m(c);_=o3(c.sourceEvent,{transform:R,snapGrid:P,snapToGrid:n,containerBounds:U}),F=Gl(c.sourceEvent,U)}).on("drag",(c)=>{let{autoPanOnNodeDrag:C,transform:T,snapGrid:R,snapToGrid:P,nodeDragThreshold:n,nodeLookup:B}=u(),D=o3(c.sourceEvent,{transform:T,snapGrid:R,snapToGrid:P,containerBounds:U});if(E=c.sourceEvent,c.sourceEvent.type==="touchmove"&&c.sourceEvent.touches.length>1||w&&!B.has(w))G=!0;if(G)return;if(!A&&C&&Q)A=!0,i();if(!Q){let I=Gl(c.sourceEvent,U),p=I.x-F.x,k=I.y-F.y;if(Math.sqrt(p*p+k*k)>n)m(c)}if((_.x!==D.xSnapped||_.y!==D.ySnapped)&&j&&Q)F=Gl(c.sourceEvent,U),X(D)}).on("end",(c)=>{if(!Q||G)return;if(A=!1,Q=!1,cancelAnimationFrame($),j.size>0){let{nodeLookup:C,updateNodePositions:T,onNodeDragStop:R,onSelectionDragStop:P}=u();if(K)T(j,!1),K=!1;if(r||R||!w&&P){let[n,B]=eA({nodeId:w,dragItems:j,nodeLookup:C,dragging:!1});if(r?.(c.sourceEvent,j,n,B),R?.(c.sourceEvent,n,B),!w)P?.(c.sourceEvent,B)}}}).filter((c)=>{let C=c.target;return!c.button&&(!Z||!gK(C,`.${Z}`,H))&&(!N||gK(C,N,H))});W.call(M)}function z(){W?.on(".drag",null)}return{update:O,destroy:z}}function ZD(f,u,l){let y=[],r={x:f.x-l,y:f.y-l,width:l*2,height:l*2};for(let _ of u.values())if(C_(r,Er(_))>0)y.push(_);return y}var ED=250;function HD(f,u,l,y){let r=[],_=1/0,$=ZD(f,l,u+ED);for(let j of $){let A=[...j.internals.handleBounds?.source??[],...j.internals.handleBounds?.target??[]];for(let F of A){if(y.nodeId===F.nodeId&&y.type===F.type&&y.id===F.id)continue;let{x:U,y:Q}=Ky(j,F,F.position,!0),W=Math.sqrt(Math.pow(U-f.x,2)+Math.pow(Q-f.y,2));if(W>u)continue;if(W<_)r=[{...F,x:U,y:Q}],_=W;else if(W===_)r.push({...F,x:U,y:Q})}}if(!r.length)return null;if(r.length>1){let j=y.type==="source"?"target":"source";return r.find((A)=>A.type===j)??r[0]}return r[0]}function zN(f,u,l,y,r,_=!1){let $=y.get(f);if(!$)return null;let j=r==="strict"?$.internals.handleBounds?.[u]:[...$.internals.handleBounds?.source??[],...$.internals.handleBounds?.target??[]],A=(l?j?.find((F)=>F.id===l):j?.[0])??null;return A&&_?{...A,...Ky($,A,A.position,!0)}:A}function GN(f,u){if(f)return f;else if(u?.classList.contains("target"))return"target";else if(u?.classList.contains("source"))return"source";return null}function OD(f,u){let l=null;if(u)l=!0;else if(f&&!u)l=!1;return l}var KN=()=>!0;function qD(f,{connectionMode:u,connectionRadius:l,handleId:y,nodeId:r,edgeUpdaterType:_,isTarget:$,domNode:j,nodeLookup:A,lib:F,autoPanOnConnect:U,flowId:Q,panBy:W,cancelConnection:G,onConnectStart:K,onConnect:E,onConnectEnd:O,isValidConnection:z=KN,onReconnectEnd:Z,updateConnection:N,getTransform:H,getFromHandle:Y,autoPanSpeed:w,dragThreshold:V=1,handleDomNode:X}){let i=NF(f.target),m=0,M,{x:c,y:C}=Gl(f),T=GN(_,X),R=j?.getBoundingClientRect(),P=!1;if(!R||!T)return;let n=zN(r,T,y,A,u);if(!n)return;let B=Gl(f,R),D=!1,I=null,p=!1,k=null;function _f(){if(!U||!R)return;let[Zf,b]=fN(B,R,w);W({x:Zf,y:b}),m=requestAnimationFrame(_f)}let S={...n,nodeId:r,type:T,position:n.position},e=A.get(r),Qf={inProgress:!0,isValid:null,from:Ky(e,S,Gf.Left,!0),fromHandle:S,fromPosition:S.position,fromNode:e,to:B,toHandle:null,toPosition:cK[S.position],toNode:null,pointer:B};function Af(){P=!0,N(Qf),K?.(f,{nodeId:r,handleId:y,handleType:T})}if(V===0)Af();function zf(Zf){if(!P){let{x:uf,y:qf}=Gl(Zf),xf=uf-c,tf=qf-C;if(!(xf*xf+tf*tf>V*V))return;Af()}if(!Y()||!S){Hf(Zf);return}let b=H();if(B=Gl(Zf,R),M=HD(i_(B,b,!1,[1,1]),l,A,S),!D)_f(),D=!0;let t=NN(Zf,{handle:M,connectionMode:u,fromNodeId:r,fromHandleId:y,fromType:$?"target":"source",isValidConnection:z,doc:i,lib:F,flowId:Q,nodeLookup:A});k=t.handleDomNode,I=t.connection,p=OD(!!M,t.isValid);let a=A.get(r),Nf=a?Ky(a,S,Gf.Left,!0):Qf.from,o={...Qf,from:Nf,isValid:p,to:t.toHandle&&p?a3({x:t.toHandle.x,y:t.toHandle.y},b):B,toHandle:t.toHandle,toPosition:p&&t.toHandle?t.toHandle.position:cK[S.position],toNode:t.toHandle?A.get(t.toHandle.nodeId):null,pointer:B};N(o),Qf=o}function Hf(Zf){if("touches"in Zf&&Zf.touches.length>0)return;if(P){if((M||k)&&I&&p)E?.(I);let{inProgress:b,...t}=Qf,a={...t,toPosition:Qf.toHandle?Qf.toPosition:null};if(O?.(Zf,a),_)Z?.(Zf,a)}G(),cancelAnimationFrame(m),D=!1,p=!1,I=null,k=null,i.removeEventListener("mousemove",zf),i.removeEventListener("mouseup",Hf),i.removeEventListener("touchmove",zf),i.removeEventListener("touchend",Hf)}i.addEventListener("mousemove",zf),i.addEventListener("mouseup",Hf),i.addEventListener("touchmove",zf),i.addEventListener("touchend",Hf)}function NN(f,{handle:u,connectionMode:l,fromNodeId:y,fromHandleId:r,fromType:_,doc:$,lib:j,flowId:A,isValidConnection:F=KN,nodeLookup:U}){let Q=_==="target",W=u?$.querySelector(`.${j}-flow__handle[data-id="${A}-${u?.nodeId}-${u?.id}-${u?.type}"]`):null,{x:G,y:K}=Gl(f),E=$.elementFromPoint(G,K),O=E?.classList.contains(`${j}-flow__handle`)?E:W,z={handleDomNode:O,isValid:!1,connection:null,toHandle:null};if(O){let Z=GN(void 0,O),N=O.getAttribute("data-nodeid"),H=O.getAttribute("data-handleid"),Y=O.classList.contains("connectable"),w=O.classList.contains("connectableend");if(!N||!Z)return z;let V={source:Q?N:y,sourceHandle:Q?H:r,target:Q?y:N,targetHandle:Q?r:H};z.connection=V;let i=Y&&w&&(l===zy.Strict?Q&&Z==="source"||!Q&&Z==="target":N!==y||H!==r);z.isValid=i&&F(V),z.toHandle=zN(N,Z,H,U,l,!0)}return z}var Z2={onPointerDown:qD,isValid:NN};function ZN({domNode:f,panZoom:u,getTransform:l,getViewScale:y}){let r=bu(f);function _({translateExtent:j,width:A,height:F,zoomStep:U=1,pannable:Q=!0,zoomable:W=!0,inversePan:G=!1}){let K=(N)=>{if(N.sourceEvent.type!=="wheel"||!u)return;let H=l(),Y=N.sourceEvent.ctrlKey&&R_()?10:1,w=-N.sourceEvent.deltaY*(N.sourceEvent.deltaMode===1?0.05:N.sourceEvent.deltaMode?1:0.002)*U,V=H[2]*Math.pow(2,w*Y);u.scaleTo(V)},E=[0,0],O=(N)=>{if(N.sourceEvent.type==="mousedown"||N.sourceEvent.type==="touchstart")E=[N.sourceEvent.clientX??N.sourceEvent.touches[0].clientX,N.sourceEvent.clientY??N.sourceEvent.touches[0].clientY]},z=(N)=>{let H=l();if(N.sourceEvent.type!=="mousemove"&&N.sourceEvent.type!=="touchmove"||!u)return;let Y=[N.sourceEvent.clientX??N.sourceEvent.touches[0].clientX,N.sourceEvent.clientY??N.sourceEvent.touches[0].clientY],w=[Y[0]-E[0],Y[1]-E[1]];E=Y;let V=y()*Math.max(H[2],Math.log(H[2]))*(G?-1:1),X={x:H[0]-w[0]*V,y:H[1]-w[1]*V},i=[[0,0],[A,F]];u.setViewportConstrained({x:X.x,y:X.y,zoom:H[2]},i,j)},Z=s3().on("start",O).on("zoom",Q?z:null).on("zoom.wheel",W?K:null);r.call(Z,{})}function $(){r.on("zoom",null)}return{update:_,destroy:$,pointer:W0}}var E2=(f)=>({x:f.x,y:f.y,zoom:f.k}),fF=({x:f,y:u,zoom:l})=>Gr.translate(f,u).scale(l),T_=(f,u)=>f.target.closest(`.${u}`),EN=(f,u)=>u===2&&Array.isArray(f)&&f.includes(2),VD=(f)=>((f*=2)<=1?f*f*f:(f-=2)*f*f+2)/2,uF=(f,u=0,l=VD,y=()=>{})=>{let r=typeof u==="number"&&u>0;if(!r)y();return r?f.transition().duration(u).ease(l).on("end",y):f},HN=(f)=>{let u=f.ctrlKey&&R_()?10:1;return-f.deltaY*(f.deltaMode===1?0.05:f.deltaMode?1:0.002)*u};function LD({zoomPanValues:f,noWheelClassName:u,d3Selection:l,d3Zoom:y,panOnScrollMode:r,panOnScrollSpeed:_,zoomOnPinch:$,onPanZoomStart:j,onPanZoom:A,onPanZoomEnd:F}){return(U)=>{if(T_(U,u)){if(U.ctrlKey)U.preventDefault();return!1}U.preventDefault(),U.stopImmediatePropagation();let Q=l.property("__zoom").k||1;if(U.ctrlKey&&$){let O=W0(U),z=HN(U),Z=Q*Math.pow(2,z);y.scaleTo(l,Z,O,U);return}let W=U.deltaMode===1?20:1,G=r===B1.Vertical?0:U.deltaX*W,K=r===B1.Horizontal?0:U.deltaY*W;if(!R_()&&U.shiftKey&&r!==B1.Vertical)G=U.deltaY*W,K=0;y.translateBy(l,-(G/Q)*_,-(K/Q)*_,{internal:!0});let E=E2(l.property("__zoom"));if(clearTimeout(f.panScrollTimeout),!f.isPanScrolling)f.isPanScrolling=!0,j?.(U,E);else A?.(U,E),f.panScrollTimeout=setTimeout(()=>{F?.(U,E),f.isPanScrolling=!1},150)}}function BD({noWheelClassName:f,preventScrolling:u,d3ZoomHandler:l}){return function(y,r){let _=y.type==="wheel",$=!u&&_&&!y.ctrlKey,j=T_(y,f);if(y.ctrlKey&&_&&j)y.preventDefault();if($||j)return null;y.preventDefault(),l.call(this,y,r)}}function XD({zoomPanValues:f,onDraggingChange:u,onPanZoomStart:l}){return(y)=>{if(y.sourceEvent?.internal)return;let r=E2(y.transform);if(f.mouseButton=y.sourceEvent?.button||0,f.isZoomingOrPanning=!0,f.prevViewport=r,y.sourceEvent?.type==="mousedown")u(!0);if(l)l?.(y.sourceEvent,r)}}function YD({zoomPanValues:f,panOnDrag:u,onPaneContextMenu:l,onTransformChange:y,onPanZoom:r}){return(_)=>{if(f.usedRightMouseButton=!!(l&&EN(u,f.mouseButton??0)),!_.sourceEvent?.sync)y([_.transform.x,_.transform.y,_.transform.k]);if(r&&!_.sourceEvent?.internal)r?.(_.sourceEvent,E2(_.transform))}}function wD({zoomPanValues:f,panOnDrag:u,panOnScroll:l,onDraggingChange:y,onPanZoomEnd:r,onPaneContextMenu:_}){return($)=>{if($.sourceEvent?.internal)return;if(f.isZoomingOrPanning=!1,_&&EN(u,f.mouseButton??0)&&!f.usedRightMouseButton&&$.sourceEvent)_($.sourceEvent);if(f.usedRightMouseButton=!1,y(!1),r){let j=E2($.transform);f.prevViewport=j,clearTimeout(f.timerId),f.timerId=setTimeout(()=>{r?.($.sourceEvent,j)},l?150:0)}}}function DD({zoomActivationKeyPressed:f,zoomOnScroll:u,zoomOnPinch:l,panOnDrag:y,panOnScroll:r,zoomOnDoubleClick:_,userSelectionActive:$,noWheelClassName:j,noPanClassName:A,lib:F,connectionInProgress:U}){return(Q)=>{let W=f||u,G=l&&Q.ctrlKey,K=Q.type==="wheel";if(Q.button===1&&Q.type==="mousedown"&&(T_(Q,`${F}-flow__node`)||T_(Q,`${F}-flow__edge`)))return!0;if(!y&&!W&&!r&&!_&&!l)return!1;if($)return!1;if(U&&!K)return!1;if(T_(Q,j)&&K)return!1;if(T_(Q,A)&&(!K||r&&K&&!f))return!1;if(!l&&Q.ctrlKey&&K)return!1;if(!l&&Q.type==="touchstart"&&Q.touches?.length>1)return Q.preventDefault(),!1;if(!W&&!r&&!G&&K)return!1;if(!y&&(Q.type==="mousedown"||Q.type==="touchstart"))return!1;if(Array.isArray(y)&&!y.includes(Q.button)&&Q.type==="mousedown")return!1;let E=Array.isArray(y)&&y.includes(Q.button)||!Q.button||Q.button<=1;return(!Q.ctrlKey||K)&&E}}function ON({domNode:f,minZoom:u,maxZoom:l,translateExtent:y,viewport:r,onPanZoom:_,onPanZoomStart:$,onPanZoomEnd:j,onDraggingChange:A}){let F={isZoomingOrPanning:!1,usedRightMouseButton:!1,prevViewport:{x:0,y:0,zoom:0},mouseButton:0,timerId:void 0,panScrollTimeout:void 0,isPanScrolling:!1},U=f.getBoundingClientRect(),Q=s3().scaleExtent([u,l]).translateExtent(y),W=bu(f).call(Q);Z({x:r.x,y:r.y,zoom:n_(r.zoom,u,l)},[[0,0],[U.width,U.height]],y);let G=W.on("wheel.zoom"),K=W.on("dblclick.zoom");Q.wheelDelta(HN);function E(M,c){if(W)return new Promise((C)=>{Q?.interpolate(c?.interpolate==="linear"?al:Wr).transform(uF(W,c?.duration,c?.ease,()=>C(!0)),M)});return Promise.resolve(!1)}function O({noWheelClassName:M,noPanClassName:c,onPaneContextMenu:C,userSelectionActive:T,panOnScroll:R,panOnDrag:P,panOnScrollMode:n,panOnScrollSpeed:B,preventScrolling:D,zoomOnPinch:I,zoomOnScroll:p,zoomOnDoubleClick:k,zoomActivationKeyPressed:_f,lib:S,onTransformChange:e,connectionInProgress:$f,paneClickDistance:Qf,selectionOnDrag:Af}){if(T&&!F.isZoomingOrPanning)z();let zf=R&&!_f&&!T;Q.clickDistance(Af?1/0:!zl(Qf)||Qf<0?0:Qf);let Hf=zf?LD({zoomPanValues:F,noWheelClassName:M,d3Selection:W,d3Zoom:Q,panOnScrollMode:n,panOnScrollSpeed:B,zoomOnPinch:I,onPanZoomStart:$,onPanZoom:_,onPanZoomEnd:j}):BD({noWheelClassName:M,preventScrolling:D,d3ZoomHandler:G});if(W.on("wheel.zoom",Hf,{passive:!1}),!T){let b=XD({zoomPanValues:F,onDraggingChange:A,onPanZoomStart:$});Q.on("start",b);let t=YD({zoomPanValues:F,panOnDrag:P,onPaneContextMenu:!!C,onPanZoom:_,onTransformChange:e});Q.on("zoom",t);let a=wD({zoomPanValues:F,panOnDrag:P,panOnScroll:R,onPaneContextMenu:C,onPanZoomEnd:j,onDraggingChange:A});Q.on("end",a)}let Zf=DD({zoomActivationKeyPressed:_f,panOnDrag:P,zoomOnScroll:p,panOnScroll:R,zoomOnDoubleClick:k,zoomOnPinch:I,userSelectionActive:T,noPanClassName:c,noWheelClassName:M,lib:S,connectionInProgress:$f});if(Q.filter(Zf),k)W.on("dblclick.zoom",K);else W.on("dblclick.zoom",null)}function z(){Q.on("zoom",null)}async function Z(M,c,C){let T=fF(M),R=Q?.constrain()(T,c,C);if(R)await E(R);return new Promise((P)=>P(R))}async function N(M,c){let C=fF(M);return await E(C,c),new Promise((T)=>T(C))}function H(M){if(W){let c=fF(M),C=W.property("__zoom");if(C.k!==M.zoom||C.x!==M.x||C.y!==M.y)Q?.transform(W,c,null,{sync:!0})}}function Y(){let M=W?t3(W.node()):{x:0,y:0,k:1};return{x:M.x,y:M.y,zoom:M.k}}function w(M,c){if(W)return new Promise((C)=>{Q?.interpolate(c?.interpolate==="linear"?al:Wr).scaleTo(uF(W,c?.duration,c?.ease,()=>C(!0)),M)});return Promise.resolve(!1)}function V(M,c){if(W)return new Promise((C)=>{Q?.interpolate(c?.interpolate==="linear"?al:Wr).scaleBy(uF(W,c?.duration,c?.ease,()=>C(!0)),M)});return Promise.resolve(!1)}function X(M){Q?.scaleExtent(M)}function i(M){Q?.translateExtent(M)}function m(M){let c=!zl(M)||M<0?0:M;Q?.clickDistance(c)}return{update:O,destroy:z,setViewport:N,setViewportConstrained:Z,getViewport:Y,scaleTo:w,scaleBy:V,setScaleExtent:X,setTranslateExtent:i,syncViewport:H,setClickDistance:m}}var Ny;(function(f){f.Line="line",f.Handle="handle"})(Ny||(Ny={}));function TD({width:f,prevWidth:u,height:l,prevHeight:y,affectsX:r,affectsY:_}){let $=f-u,j=l-y,A=[$>0?1:$<0?-1:0,j>0?1:j<0?-1:0];if($&&r)A[0]=A[0]*-1;if(j&&_)A[1]=A[1]*-1;return A}function kK(f){let u=f.includes("right")||f.includes("left"),l=f.includes("bottom")||f.includes("top"),y=f.includes("left"),r=f.includes("top");return{isHorizontal:u,isVertical:l,affectsX:y,affectsY:r}}function Qy(f,u){return Math.max(0,u-f)}function Wy(f,u){return Math.max(0,f-u)}function $2(f,u,l){return Math.max(0,u-f,f-l)}function tK(f,u){return f?!u:u}function nD(f,u,l,y,r,_,$,j){let{affectsX:A,affectsY:F}=u,{isHorizontal:U,isVertical:Q}=u,W=U&&Q,{xSnapped:G,ySnapped:K}=l,{minWidth:E,maxWidth:O,minHeight:z,maxHeight:Z}=y,{x:N,y:H,width:Y,height:w,aspectRatio:V}=f,X=Math.floor(U?G-f.pointerX:0),i=Math.floor(Q?K-f.pointerY:0),m=Y+(A?-X:X),M=w+(F?-i:i),c=-_[0]*Y,C=-_[1]*w,T=$2(m,E,O),R=$2(M,z,Z);if($){let B=0,D=0;if(A&&X<0)B=Qy(N+X+c,$[0][0]);else if(!A&&X>0)B=Wy(N+m+c,$[1][0]);if(F&&i<0)D=Qy(H+i+C,$[0][1]);else if(!F&&i>0)D=Wy(H+M+C,$[1][1]);T=Math.max(T,B),R=Math.max(R,D)}if(j){let B=0,D=0;if(A&&X>0)B=Wy(N+X,j[0][0]);else if(!A&&X<0)B=Qy(N+m,j[1][0]);if(F&&i>0)D=Wy(H+i,j[0][1]);else if(!F&&i<0)D=Qy(H+M,j[1][1]);T=Math.max(T,B),R=Math.max(R,D)}if(r){if(U){let B=$2(m/V,z,Z)*V;if(T=Math.max(T,B),$){let D=0;if(!A&&!F||A&&!F&&W)D=Wy(H+C+m/V,$[1][1])*V;else D=Qy(H+C+(A?X:-X)/V,$[0][1])*V;T=Math.max(T,D)}if(j){let D=0;if(!A&&!F||A&&!F&&W)D=Qy(H+m/V,j[1][1])*V;else D=Wy(H+(A?X:-X)/V,j[0][1])*V;T=Math.max(T,D)}}if(Q){let B=$2(M*V,E,O)/V;if(R=Math.max(R,B),$){let D=0;if(!A&&!F||F&&!A&&W)D=Wy(N+M*V+c,$[1][0])/V;else D=Qy(N+(F?i:-i)*V+c,$[0][0])/V;R=Math.max(R,D)}if(j){let D=0;if(!A&&!F||F&&!A&&W)D=Qy(N+M*V,j[1][0])/V;else D=Wy(N+(F?i:-i)*V,j[0][0])/V;R=Math.max(R,D)}}}if(i=i+(i<0?R:-R),X=X+(X<0?T:-T),r)if(W)if(m>M*V)i=(tK(A,F)?-X:X)/V;else X=(tK(A,F)?-i:i)*V;else if(U)i=X/V,F=A;else X=i*V,A=F;let P=A?N+X:N,n=F?H+i:H;return{width:Y+(A?-X:X),height:w+(F?-i:i),x:_[0]*X*(!A?1:-1)+P,y:_[1]*i*(!F?1:-1)+n}}var qN={width:0,height:0,x:0,y:0},MD={...qN,pointerX:0,pointerY:0,aspectRatio:1};function SD(f){return[[0,0],[f.measured.width,f.measured.height]]}function PD(f,u,l){let y=u.position.x+f.position.x,r=u.position.y+f.position.y,_=f.measured.width??0,$=f.measured.height??0,j=l[0]*_,A=l[1]*$;return[[y-j,r-A],[y+_-j,r+$-A]]}function VN({domNode:f,nodeId:u,getStoreItems:l,onChange:y,onEnd:r}){let _=bu(f),$={controlDirection:kK("bottom-right"),boundaries:{minWidth:0,minHeight:0,maxWidth:Number.MAX_VALUE,maxHeight:Number.MAX_VALUE},resizeDirection:void 0,keepAspectRatio:!1};function j({controlPosition:F,boundaries:U,keepAspectRatio:Q,resizeDirection:W,onResizeStart:G,onResize:K,onResizeEnd:E,shouldResize:O}){let z={...qN},Z={...MD};$={boundaries:U,resizeDirection:W,keepAspectRatio:Q,controlDirection:kK(F)};let N=void 0,H=null,Y=[],w=void 0,V=void 0,X=void 0,i=!1,m=n3().on("start",(M)=>{let{nodeLookup:c,transform:C,snapGrid:T,snapToGrid:R,nodeOrigin:P,paneDomNode:n}=l();if(N=c.get(u),!N)return;H=n?.getBoundingClientRect()??null;let{xSnapped:B,ySnapped:D}=o3(M.sourceEvent,{transform:C,snapGrid:T,snapToGrid:R,containerBounds:H});if(z={width:N.measured.width??0,height:N.measured.height??0,x:N.position.x??0,y:N.position.y??0},Z={...z,pointerX:B,pointerY:D,aspectRatio:z.width/z.height},w=void 0,N.parentId&&(N.extent==="parent"||N.expandParent))w=c.get(N.parentId),V=w&&N.extent==="parent"?SD(w):void 0;Y=[],X=void 0;for(let[I,p]of c)if(p.parentId===u){if(Y.push({id:I,position:{...p.position},extent:p.extent}),p.extent==="parent"||p.expandParent){let k=PD(p,N,p.origin??P);if(X)X=[[Math.min(k[0][0],X[0][0]),Math.min(k[0][1],X[0][1])],[Math.max(k[1][0],X[1][0]),Math.max(k[1][1],X[1][1])]];else X=k}}G?.(M,{...z})}).on("drag",(M)=>{let{transform:c,snapGrid:C,snapToGrid:T,nodeOrigin:R}=l(),P=o3(M.sourceEvent,{transform:c,snapGrid:C,snapToGrid:T,containerBounds:H}),n=[];if(!N)return;let{x:B,y:D,width:I,height:p}=z,k={},_f=N.origin??R,{width:S,height:e,x:$f,y:Qf}=nD(Z,$.controlDirection,P,$.boundaries,$.keepAspectRatio,_f,V,X),Af=S!==I,zf=e!==p,Hf=$f!==B&&Af,Zf=Qf!==D&&zf;if(!Hf&&!Zf&&!Af&&!zf)return;if(Hf||Zf||_f[0]===1||_f[1]===1){if(k.x=Hf?$f:z.x,k.y=Zf?Qf:z.y,z.x=k.x,z.y=k.y,Y.length>0){let Nf=$f-B,o=Qf-D;for(let uf of Y)uf.position={x:uf.position.x-Nf+_f[0]*(S-I),y:uf.position.y-o+_f[1]*(e-p)},n.push(uf)}}if(Af||zf)k.width=Af&&(!$.resizeDirection||$.resizeDirection==="horizontal")?S:z.width,k.height=zf&&(!$.resizeDirection||$.resizeDirection==="vertical")?e:z.height,z.width=k.width,z.height=k.height;if(w&&N.expandParent){let Nf=_f[0]*(k.width??0);if(k.x&&k.x{if(!i)return;E?.(M,{...z}),r?.({...z}),i=!1});_.call(m)}function A(){_.on(".drag",null)}return{update:j,destroy:A}}var CN=cf(Yu(),1),cN=cf(nN(),1);var MN=(f)=>{let u,l=new Set,y=(U,Q)=>{let W=typeof U==="function"?U(u):U;if(!Object.is(W,u)){let G=u;u=(Q!=null?Q:typeof W!=="object"||W===null)?W:Object.assign({},u,W),l.forEach((K)=>K(u,G))}},r=()=>u,A={setState:y,getState:r,getInitialState:()=>F,subscribe:(U)=>{return l.add(U),()=>l.delete(U)},destroy:()=>{l.clear()}},F=u=f(y,r,A);return A},SN=(f)=>f?MN(f):MN;var{useDebugValue:dD}=CN.default,{useSyncExternalStoreWithSelector:eD}=cN.default,fT=(f)=>f;function wF(f,u=fT,l){let y=eD(f.subscribe,f.getState,f.getServerState||f.getInitialState,u,l);return dD(y),y}var PN=(f,u)=>{let l=SN(f),y=(r,_=u)=>wF(l,r,_);return Object.assign(y,l),y},iN=(f,u)=>f?PN(f,u):PN;function Zu(f,u){if(Object.is(f,u))return!0;if(typeof f!=="object"||f===null||typeof u!=="object"||u===null)return!1;if(f instanceof Map&&u instanceof Map){if(f.size!==u.size)return!1;for(let[y,r]of f)if(!Object.is(r,u.get(y)))return!1;return!0}if(f instanceof Set&&u instanceof Set){if(f.size!==u.size)return!1;for(let y of f)if(!u.has(y))return!1;return!0}let l=Object.keys(f);if(l.length!==Object.keys(u).length)return!1;for(let y of l)if(!Object.prototype.hasOwnProperty.call(u,y)||!Object.is(f[y],u[y]))return!1;return!0}var uT=cf(C7(),1),L2=lf.createContext(null),lT=L2.Provider,jZ=a0.error001();function of(f,u){let l=lf.useContext(L2);if(l===null)throw Error(jZ);return wF(l,f,u)}function Hu(){let f=lf.useContext(L2);if(f===null)throw Error(jZ);return lf.useMemo(()=>({getState:f.getState,setState:f.setState,subscribe:f.subscribe}),[f])}var RN={display:"none"},yT={position:"absolute",width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0px, 0px, 0px, 0px)",clipPath:"inset(100%)"},AZ="react-flow__node-desc",FZ="react-flow__edge-desc",rT="react-flow__aria-live",_T=(f)=>f.ariaLiveMessage,$T=(f)=>f.ariaLabelConfig;function jT({rfId:f}){let u=of(_T);return ff.jsx("div",{id:`${rT}-${f}`,"aria-live":"assertive","aria-atomic":"true",style:yT,children:u})}function AT({rfId:f,disableKeyboardA11y:u}){let l=of($T);return ff.jsxs(ff.Fragment,{children:[ff.jsx("div",{id:`${AZ}-${f}`,style:RN,children:u?l["node.a11yDescription.default"]:l["node.a11yDescription.keyboardDisabled"]}),ff.jsx("div",{id:`${FZ}-${f}`,style:RN,children:l["edge.a11yDescription.default"]}),!u&&ff.jsx(jT,{rfId:f})]})}var B2=lf.forwardRef(({position:f="top-left",children:u,className:l,style:y,...r},_)=>{let $=`${f}`.split("-");return ff.jsx("div",{className:nu(["react-flow__panel",l,...$]),style:y,ref:_,...r,children:u})});B2.displayName="Panel";function FT({proOptions:f,position:u="bottom-right"}){if(f?.hideAttribution)return null;return ff.jsx(B2,{position:u,className:"react-flow__attribution","data-message":"Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev",children:ff.jsx("a",{href:"https://reactflow.dev",target:"_blank",rel:"noopener noreferrer","aria-label":"React Flow attribution",children:"React Flow"})})}var JT=(f)=>{let u=[],l=[];for(let[,y]of f.nodeLookup)if(y.selected)u.push(y.internals.userNode);for(let[,y]of f.edgeLookup)if(y.selected)l.push(y);return{selectedNodes:u,selectedEdges:l}},O2=(f)=>f.id;function UT(f,u){return Zu(f.selectedNodes.map(O2),u.selectedNodes.map(O2))&&Zu(f.selectedEdges.map(O2),u.selectedEdges.map(O2))}function QT({onSelectionChange:f}){let u=Hu(),{selectedNodes:l,selectedEdges:y}=of(JT,UT);return lf.useEffect(()=>{let r={nodes:l,edges:y};f?.(r),u.getState().onSelectionChangeHandlers.forEach((_)=>_(r))},[l,y,f]),null}var WT=(f)=>!!f.onSelectionChangeHandlers;function zT({onSelectionChange:f}){let u=of(WT);if(f||u)return ff.jsx(QT,{onSelectionChange:f});return null}var nF=typeof window<"u"?lf.useLayoutEffect:lf.useEffect,JZ=[0,0],GT={x:0,y:0,zoom:1},KT=["nodes","edges","defaultNodes","defaultEdges","onConnect","onConnectStart","onConnectEnd","onClickConnectStart","onClickConnectEnd","nodesDraggable","autoPanOnNodeFocus","nodesConnectable","nodesFocusable","edgesFocusable","edgesReconnectable","elevateNodesOnSelect","elevateEdgesOnSelect","minZoom","maxZoom","nodeExtent","onNodesChange","onEdgesChange","elementsSelectable","connectionMode","snapGrid","snapToGrid","translateExtent","connectOnClick","defaultEdgeOptions","fitView","fitViewOptions","onNodesDelete","onEdgesDelete","onDelete","onNodeDrag","onNodeDragStart","onNodeDragStop","onSelectionDrag","onSelectionDragStart","onSelectionDragStop","onMoveStart","onMove","onMoveEnd","noPanClassName","nodeOrigin","autoPanOnConnect","autoPanOnNodeDrag","onError","connectionRadius","isValidConnection","selectNodesOnDrag","nodeDragThreshold","connectionDragThreshold","onBeforeDelete","debug","autoPanSpeed","ariaLabelConfig","zIndexMode"],xN=[...KT,"rfId"],NT=(f)=>({setNodes:f.setNodes,setEdges:f.setEdges,setMinZoom:f.setMinZoom,setMaxZoom:f.setMaxZoom,setTranslateExtent:f.setTranslateExtent,setNodeExtent:f.setNodeExtent,reset:f.reset,setDefaultNodesAndEdges:f.setDefaultNodesAndEdges}),vN={translateExtent:S_,nodeOrigin:JZ,minZoom:0.5,maxZoom:2,elementsSelectable:!0,noPanClassName:"nopan",rfId:"1"};function ZT(f){let{setNodes:u,setEdges:l,setMinZoom:y,setMaxZoom:r,setTranslateExtent:_,setNodeExtent:$,reset:j,setDefaultNodesAndEdges:A}=of(NT,Zu),F=Hu();nF(()=>{return A(f.defaultNodes,f.defaultEdges),()=>{U.current=vN,j()}},[]);let U=lf.useRef(vN);return nF(()=>{for(let Q of xN){let W=f[Q],G=U.current[Q];if(W===G)continue;if(typeof f[Q]>"u")continue;if(Q==="nodes")u(W);else if(Q==="edges")l(W);else if(Q==="minZoom")y(W);else if(Q==="maxZoom")r(W);else if(Q==="translateExtent")_(W);else if(Q==="nodeExtent")$(W);else if(Q==="ariaLabelConfig")F.setState({ariaLabelConfig:lN(W)});else if(Q==="fitView")F.setState({fitViewQueued:W});else if(Q==="fitViewOptions")F.setState({fitViewOptions:W});else F.setState({[Q]:W})}U.current=f},xN.map((Q)=>f[Q])),null}function bN(){if(typeof window>"u"||!window.matchMedia)return null;return window.matchMedia("(prefers-color-scheme: dark)")}function ET(f){let[u,l]=lf.useState(f==="system"?null:f);return lf.useEffect(()=>{if(f!=="system"){l(f);return}let y=bN(),r=()=>l(y?.matches?"dark":"light");return r(),y?.addEventListener("change",r),()=>{y?.removeEventListener("change",r)}},[f]),u!==null?u:bN()?.matches?"dark":"light"}var hN=typeof document<"u"?document:null;function u6(f=null,u={target:hN,actInsideInputWithModifier:!0}){let[l,y]=lf.useState(!1),r=lf.useRef(!1),_=lf.useRef(new Set([])),[$,j]=lf.useMemo(()=>{if(f!==null){let F=(Array.isArray(f)?f:[f]).filter((Q)=>typeof Q==="string").map((Q)=>Q.replace("+",` `).replace(` `,` +`).split(` -`)),U=A.reduce((G,W)=>G.concat(...W),[]);return[A,U]}return[[],[]]},[f]);return a.useEffect(()=>{let F=u?.target??AZ,A=u?.actInsideInputWithModifier??!0;if(f!==null){let U=(K)=>{if(l.current=K.ctrlKey||K.metaKey||K.shiftKey||K.altKey,(!l.current||l.current&&!A)&&EF(K))return!1;let H=WZ(K.code,J);if($.current.add(K[H]),UZ(j,$.current,!1)){let O=K.composedPath?.()?.[0]||K.target,z=O?.nodeName==="BUTTON"||O?.nodeName==="A";if(u.preventDefault!==!1&&(l.current||!z))K.preventDefault();y(!0)}},G=(K)=>{let E=WZ(K.code,J);if(UZ(j,$.current,!0))y(!1),$.current.clear();else $.current.delete(K[E]);if(K.key==="Meta")$.current.clear();l.current=!1},W=()=>{$.current.clear(),y(!1)};return F?.addEventListener("keydown",U),F?.addEventListener("keyup",G),window.addEventListener("blur",W),window.addEventListener("contextmenu",W),()=>{F?.removeEventListener("keydown",U),F?.removeEventListener("keyup",G),window.removeEventListener("blur",W),window.removeEventListener("contextmenu",W)}}},[f,y]),_}function UZ(f,u,_){return f.filter((y)=>_||y.length===u.size).some((y)=>y.every((l)=>u.has(l)))}function WZ(f,u){return u.includes(f)?"code":"key"}var ID=()=>{let f=z0();return a.useMemo(()=>{return{zoomIn:(u)=>{let{panZoom:_}=f.getState();return _?_.scaleBy(1.2,u):Promise.resolve(!1)},zoomOut:(u)=>{let{panZoom:_}=f.getState();return _?_.scaleBy(0.8333333333333334,u):Promise.resolve(!1)},zoomTo:(u,_)=>{let{panZoom:y}=f.getState();return y?y.scaleTo(u,_):Promise.resolve(!1)},getZoom:()=>f.getState().transform[2],setViewport:async(u,_)=>{let{transform:[y,l,$],panZoom:j}=f.getState();if(!j)return Promise.resolve(!1);return await j.setViewport({x:u.x??y,y:u.y??l,zoom:u.zoom??$},_),Promise.resolve(!0)},getViewport:()=>{let[u,_,y]=f.getState().transform;return{x:u,y:_,zoom:y}},setCenter:async(u,_,y)=>{return f.getState().setCenter(u,_,y)},fitBounds:async(u,_)=>{let{width:y,height:l,minZoom:$,maxZoom:j,panZoom:J}=f.getState(),F=z6(u,y,l,$,j,_?.padding??0.1);if(!J)return Promise.resolve(!1);return await J.setViewport(F,{duration:_?.duration,ease:_?.ease,interpolate:_?.interpolate}),Promise.resolve(!0)},screenToFlowPosition:(u,_={})=>{let{transform:y,snapGrid:l,snapToGrid:$,domNode:j}=f.getState();if(!j)return u;let{x:J,y:F}=j.getBoundingClientRect(),A={x:u.x-J,y:u.y-F},U=_.snapGrid??l,G=_.snapToGrid??$;return j3(A,y,G,U)},flowToScreenPosition:(u)=>{let{transform:_,domNode:y}=f.getState();if(!y)return u;let{x:l,y:$}=y.getBoundingClientRect(),j=W6(u,_);return{x:j.x+l,y:j.y+$}}}},[])};function SZ(f,u){let _=[],y=new Map,l=[];for(let $ of f)if($.type==="add"){l.push($);continue}else if($.type==="remove"||$.type==="replace")y.set($.id,[$]);else{let j=y.get($.id);if(j)j.push($);else y.set($.id,[$])}for(let $ of u){let j=y.get($.id);if(!j){_.push($);continue}if(j[0].type==="remove")continue;if(j[0].type==="replace"){_.push({...j[0].item});continue}let J={...$};for(let F of j)cD(F,J);_.push(J)}if(l.length)l.forEach(($)=>{if($.index!==void 0)_.splice($.index,0,{...$.item});else _.push({...$.item})});return _}function cD(f,u){switch(f.type){case"select":{u.selected=f.selected;break}case"position":{if(typeof f.position<"u")u.position=f.position;if(typeof f.dragging<"u")u.dragging=f.dragging;break}case"dimensions":{if(typeof f.dimensions<"u"){if(u.measured={...f.dimensions},f.setAttributes){if(f.setAttributes===!0||f.setAttributes==="width")u.width=f.dimensions.width;if(f.setAttributes===!0||f.setAttributes==="height")u.height=f.dimensions.height}}if(typeof f.resizing==="boolean")u.resizing=f.resizing;break}}}function pD(f,u){return SZ(f,u)}function kD(f,u){return SZ(f,u)}function my(f,u){return{id:f,type:"select",selected:u}}function Q3(f,u=new Set,_=!1){let y=[];for(let[l,$]of f){let j=u.has(l);if(!($.selected===void 0&&!j)&&$.selected!==j){if(_)$.selected=j;y.push(my($.id,j))}}return y}function GZ({items:f=[],lookup:u}){let _=[],y=new Map(f.map((l)=>[l.id,l]));for(let[l,$]of f.entries()){let j=u.get($.id),J=j?.internals?.userNode??j;if(J!==void 0&&J!==$)_.push({id:$.id,item:$,type:"replace"});if(J===void 0)_.push({item:$,type:"add",index:l})}for(let[l]of u)if(y.get(l)===void 0)_.push({id:l,type:"remove"});return _}function zZ(f){return{id:f.id,type:"remove"}}var KZ=(f)=>qK(f),mD=(f)=>JF(f);function CZ(f){return a.forwardRef(f)}function ZZ(f){let[u,_]=a.useState(BigInt(0)),[y]=a.useState(()=>iD(()=>_((l)=>l+BigInt(1))));return MF(()=>{let l=y.get();if(l.length)f(l),y.reset()},[u]),y}function iD(f){let u=[];return{get:()=>u,reset:()=>{u=[]},push:(_)=>{u.push(_),f()}}}var RZ=a.createContext(null);function gD({children:f}){let u=z0(),_=a.useCallback((J)=>{let{nodes:F=[],setNodes:A,hasDefaultNodes:U,onNodesChange:G,nodeLookup:W,fitViewQueued:K,onNodesChangeMiddlewareMap:E}=u.getState(),H=F;for(let z of J)H=typeof z==="function"?z(H):z;let O=GZ({items:H,lookup:W});for(let z of E.values())O=z(O);if(U)A(H);if(O.length>0)G?.(O);else if(K)window.requestAnimationFrame(()=>{let{fitViewQueued:z,nodes:q,setNodes:Z}=u.getState();if(z)Z(q)})},[]),y=ZZ(_),l=a.useCallback((J)=>{let{edges:F=[],setEdges:A,hasDefaultEdges:U,onEdgesChange:G,edgeLookup:W}=u.getState(),K=F;for(let E of J)K=typeof E==="function"?E(K):E;if(U)A(K);else if(G)G(GZ({items:K,lookup:W}))},[]),$=ZZ(l),j=a.useMemo(()=>({nodeQueue:y,edgeQueue:$}),[]);return o.jsx(RZ.Provider,{value:j,children:f})}function nD(){let f=a.useContext(RZ);if(!f)throw Error("useBatchContext must be used within a BatchProvider");return f}var tD=(f)=>!!f.panZoom;function SF(){let f=ID(),u=z0(),_=nD(),y=kf(tD),l=a.useMemo(()=>{let $=(G)=>u.getState().nodeLookup.get(G),j=(G)=>{_.nodeQueue.push(G)},J=(G)=>{_.edgeQueue.push(G)},F=(G)=>{let{nodeLookup:W,nodeOrigin:K}=u.getState(),E=KZ(G)?G:W.get(G.id),H=E.parentId?KF(E.position,E.measured,E.parentId,W,K):E.position,O={...E,position:H,width:E.measured?.width??E.width,height:E.measured?.height??E.height};return ky(O)},A=(G,W,K={replace:!1})=>{j((E)=>E.map((H)=>{if(H.id===G){let O=typeof W==="function"?W(H):W;return K.replace&&KZ(O)?O:{...H,...O}}return H}))},U=(G,W,K={replace:!1})=>{J((E)=>E.map((H)=>{if(H.id===G){let O=typeof W==="function"?W(H):W;return K.replace&&mD(O)?O:{...H,...O}}return H}))};return{getNodes:()=>u.getState().nodes.map((G)=>({...G})),getNode:(G)=>$(G)?.internals.userNode,getInternalNode:$,getEdges:()=>{let{edges:G=[]}=u.getState();return G.map((W)=>({...W}))},getEdge:(G)=>u.getState().edgeLookup.get(G),setNodes:j,setEdges:J,addNodes:(G)=>{let W=Array.isArray(G)?G:[G];_.nodeQueue.push((K)=>[...K,...W])},addEdges:(G)=>{let W=Array.isArray(G)?G:[G];_.edgeQueue.push((K)=>[...K,...W])},toObject:()=>{let{nodes:G=[],edges:W=[],transform:K}=u.getState(),[E,H,O]=K;return{nodes:G.map((z)=>({...z})),edges:W.map((z)=>({...z})),viewport:{x:E,y:H,zoom:O}}},deleteElements:async({nodes:G=[],edges:W=[]})=>{let{nodes:K,edges:E,onNodesDelete:H,onEdgesDelete:O,triggerNodeChanges:z,triggerEdgeChanges:q,onDelete:Z,onBeforeDelete:V}=u.getState(),{nodes:L,edges:r}=await VK({nodesToRemove:G,edgesToRemove:W,nodes:K,edges:E,onBeforeDelete:V}),N=r.length>0,D=L.length>0;if(N){let x=r.map(zZ);O?.(r),q(x)}if(D){let x=L.map(zZ);H?.(L),z(x)}if(D||N)Z?.({nodes:L,edges:r});return{deletedNodes:L,deletedEdges:r}},getIntersectingNodes:(G,W=!0,K)=>{let E=WF(G),H=E?G:F(G),O=K!==void 0;if(!H)return[];return(K||u.getState().nodes).filter((z)=>{let q=u.getState().nodeLookup.get(z.id);if(q&&!E&&(z.id===G.id||!q.internals.positionAbsolute))return!1;let Z=ky(O?z:q),V=l3(Z,H);return W&&V>0||V>=Z.width*Z.height||V>=H.width*H.height})},isNodeIntersecting:(G,W,K=!0)=>{let H=WF(G)?G:F(G);if(!H)return!1;let O=l3(H,W);return K&&O>0||O>=W.width*W.height||O>=H.width*H.height},updateNode:A,updateNodeData:(G,W,K={replace:!1})=>{A(G,(E)=>{let H=typeof W==="function"?W(E):W;return K.replace?{...E,data:H}:{...E,data:{...E.data,...H}}},K)},updateEdge:U,updateEdgeData:(G,W,K={replace:!1})=>{U(G,(E)=>{let H=typeof W==="function"?W(E):W;return K.replace?{...E,data:H}:{...E,data:{...E.data,...H}}},K)},getNodesBounds:(G)=>{let{nodeLookup:W,nodeOrigin:K}=u.getState();return QF(G,{nodeLookup:W,nodeOrigin:K})},getHandleConnections:({type:G,id:W,nodeId:K})=>Array.from(u.getState().connectionLookup.get(`${K}-${G}${W?`-${W}`:""}`)?.values()??[]),getNodeConnections:({type:G,handleId:W,nodeId:K})=>Array.from(u.getState().connectionLookup.get(`${K}${G?W?`-${G}-${W}`:`-${G}`:""}`)?.values()??[]),fitView:async(G)=>{let W=u.getState().fitViewResolver??NK();return u.setState({fitViewQueued:!0,fitViewOptions:G,fitViewResolver:W}),_.nodeQueue.push((K)=>[...K]),W.promise}}},[]);return a.useMemo(()=>{return{...l,...f,viewportInitialized:y}},[y])}var qZ=(f)=>f.selected,sD=typeof window<"u"?window:void 0;function oD({deleteKeyCode:f,multiSelectionKeyCode:u}){let _=z0(),{deleteElements:y}=SF(),l=Z6(f,{actInsideInputWithModifier:!1}),$=Z6(u,{target:sD});a.useEffect(()=>{if(l){let{edges:j,nodes:J}=_.getState();y({nodes:J.filter(qZ),edges:j.filter(qZ)}),_.setState({nodesSelectionActive:!1})}},[l]),a.useEffect(()=>{_.setState({multiSelectionActive:$})},[$])}function aD(f){let u=z0();a.useEffect(()=>{let _=()=>{if(!f.current||!(f.current.checkVisibility?.()??!0))return!1;let y=L5(f.current);if(y.height===0||y.width===0)u.getState().onError?.("004",Iu.error004());u.setState({width:y.width||500,height:y.height||500})};if(f.current){_(),window.addEventListener("resize",_);let y=new ResizeObserver(()=>_());return y.observe(f.current),()=>{if(window.removeEventListener("resize",_),y&&f.current)y.unobserve(f.current)}}},[])}var h5={position:"absolute",width:"100%",height:"100%",top:0,left:0},dD=(f)=>({userSelectionActive:f.userSelectionActive,lib:f.lib,connectionInProgress:f.connection.inProgress});function eD({onPaneContextMenu:f,zoomOnScroll:u=!0,zoomOnPinch:_=!0,panOnScroll:y=!1,panOnScrollSpeed:l=0.5,panOnScrollMode:$=l_.Free,zoomOnDoubleClick:j=!0,panOnDrag:J=!0,defaultViewport:F,translateExtent:A,minZoom:U,maxZoom:G,zoomActivationKeyCode:W,preventScrolling:K=!0,children:E,noWheelClassName:H,noPanClassName:O,onViewportChange:z,isControlledViewport:q,paneClickDistance:Z,selectionOnDrag:V}){let L=z0(),r=a.useRef(null),{userSelectionActive:N,lib:D,connectionInProgress:x}=kf(dD,W0),c=Z6(W),v=a.useRef();aD(r);let C=a.useCallback((S)=>{if(z?.({x:S[0],y:S[1],zoom:S[2]}),!q)L.setState({transform:S})},[z,q]);return a.useEffect(()=>{if(r.current){v.current=kK({domNode:r.current,minZoom:U,maxZoom:G,translateExtent:A,viewport:F,onDraggingChange:(M)=>L.setState((w)=>w.paneDragging===M?w:{paneDragging:M}),onPanZoomStart:(M,w)=>{let{onViewportChangeStart:Y,onMoveStart:R}=L.getState();R?.(M,w),Y?.(w)},onPanZoom:(M,w)=>{let{onViewportChange:Y,onMove:R}=L.getState();R?.(M,w),Y?.(w)},onPanZoomEnd:(M,w)=>{let{onViewportChangeEnd:Y,onMoveEnd:R}=L.getState();R?.(M,w),Y?.(w)}});let{x:S,y:B,zoom:P}=v.current.getViewport();return L.setState({panZoom:v.current,transform:[S,B,P],domNode:r.current.closest(".react-flow")}),()=>{v.current?.destroy()}}},[]),a.useEffect(()=>{v.current?.update({onPaneContextMenu:f,zoomOnScroll:u,zoomOnPinch:_,panOnScroll:y,panOnScrollSpeed:l,panOnScrollMode:$,zoomOnDoubleClick:j,panOnDrag:J,zoomActivationKeyPressed:c,preventScrolling:K,noPanClassName:O,userSelectionActive:N,noWheelClassName:H,lib:D,onTransformChange:C,connectionInProgress:x,selectionOnDrag:V,paneClickDistance:Z})},[f,u,_,y,l,$,j,J,c,K,O,N,H,D,C,x,V,Z]),o.jsx("div",{className:"react-flow__renderer",ref:r,style:h5,children:E})}var fw=(f)=>({userSelectionActive:f.userSelectionActive,userSelectionRect:f.userSelectionRect});function uw(){let{userSelectionActive:f,userSelectionRect:u}=kf(fw,W0);if(!(f&&u))return null;return o.jsx("div",{className:"react-flow__selection react-flow__container",style:{width:u.width,height:u.height,transform:`translate(${u.x}px, ${u.y}px)`}})}var TF=(f,u)=>{return(_)=>{if(_.target!==u.current)return;f?.(_)}},_w=(f)=>({userSelectionActive:f.userSelectionActive,elementsSelectable:f.elementsSelectable,connectionInProgress:f.connection.inProgress,dragging:f.paneDragging});function yw({isSelecting:f,selectionKeyPressed:u,selectionMode:_=cy.Full,panOnDrag:y,paneClickDistance:l,selectionOnDrag:$,onSelectionStart:j,onSelectionEnd:J,onPaneClick:F,onPaneContextMenu:A,onPaneScroll:U,onPaneMouseEnter:G,onPaneMouseMove:W,onPaneMouseLeave:K,children:E}){let H=z0(),{userSelectionActive:O,elementsSelectable:z,dragging:q,connectionInProgress:Z}=kf(_w,W0),V=z&&(f||O),L=a.useRef(null),r=a.useRef(),N=a.useRef(new Set),D=a.useRef(new Set),x=a.useRef(!1),c=(Y)=>{if(x.current||Z){x.current=!1;return}F?.(Y),H.getState().resetSelectedElements(),H.setState({nodesSelectionActive:!1})},v=(Y)=>{if(Array.isArray(y)&&y?.includes(2)){Y.preventDefault();return}A?.(Y)},C=U?(Y)=>U(Y):void 0,S=(Y)=>{if(x.current)Y.stopPropagation(),x.current=!1},B=(Y)=>{let{domNode:R}=H.getState();if(r.current=R?.getBoundingClientRect(),!r.current)return;let k=Y.target===L.current;if(!k&&!!Y.target.closest(".nokey")||!f||!($&&k||u)||Y.button!==0||!Y.isPrimary)return;Y.target?.setPointerCapture?.(Y.pointerId),x.current=!1;let{x:_f,y:s}=_1(Y.nativeEvent,r.current);if(H.setState({userSelectionRect:{width:0,height:0,startX:_f,startY:s,x:_f,y:s}}),!k)Y.stopPropagation(),Y.preventDefault()},P=(Y)=>{let{userSelectionRect:R,transform:k,nodeLookup:p,edgeLookup:n,connectionLookup:_f,triggerNodeChanges:s,triggerEdgeChanges:ff,defaultEdgeOptions:Kf,resetSelectedElements:Gf}=H.getState();if(!r.current||!R)return;let{x:jf,y:Wf}=_1(Y.nativeEvent,r.current),{startX:Of,startY:Zf}=R;if(!x.current){let $f=u?0:l;if(Math.hypot(jf-Of,Wf-Zf)<=$f)return;Gf(),j?.(Y)}x.current=!0;let h={startX:Of,startY:Zf,x:jf$f.id)),D.current=new Set;let lf=Kf?.selectable??!0;for(let $f of N.current){let Af=_f.get($f);if(!Af)continue;for(let{edgeId:Yf}of Af.values()){let xf=n.get(Yf);if(xf&&(xf.selectable??lf))D.current.add(Yf)}}if(!ZF(i,N.current)){let $f=Q3(p,N.current,!0);s($f)}if(!ZF(I,D.current)){let $f=Q3(n,D.current);ff($f)}H.setState({userSelectionRect:h,userSelectionActive:!0,nodesSelectionActive:!1})},M=(Y)=>{if(Y.button!==0)return;if(Y.target?.releasePointerCapture?.(Y.pointerId),!O&&Y.target===L.current&&H.getState().userSelectionRect)c?.(Y);if(H.setState({userSelectionActive:!1,userSelectionRect:null}),x.current)J?.(Y),H.setState({nodesSelectionActive:N.current.size>0})},w=y===!0||Array.isArray(y)&&y.includes(0);return o.jsxs("div",{className:B0(["react-flow__pane",{draggable:w,dragging:q,selection:f}]),onClick:V?void 0:TF(c,L),onContextMenu:TF(v,L),onWheel:TF(C,L),onPointerEnter:V?void 0:G,onPointerMove:V?P:W,onPointerUp:V?M:void 0,onPointerDownCapture:V?B:void 0,onClickCapture:V?S:void 0,onPointerLeave:K,ref:L,style:h5,children:[E,o.jsx(uw,{})]})}function PF({id:f,store:u,unselect:_=!1,nodeRef:y}){let{addSelectedNodes:l,unselectNodesAndEdges:$,multiSelectionActive:j,nodeLookup:J,onError:F}=u.getState(),A=J.get(f);if(!A){F?.("012",Iu.error012(f));return}if(u.setState({nodesSelectionActive:!1}),!A.selected)l([f]);else if(_||A.selected&&j)$({nodes:[A],edges:[]}),requestAnimationFrame(()=>y?.current?.blur())}function xZ({nodeRef:f,disabled:u=!1,noDragClassName:_,handleSelector:y,nodeId:l,isSelectable:$,nodeClickDistance:j}){let J=z0(),[F,A]=a.useState(!1),U=a.useRef();return a.useEffect(()=>{U.current=RK({getStoreItems:()=>J.getState(),onNodeMouseDown:(G)=>{PF({id:G,store:J,nodeRef:f})},onDragStart:()=>{A(!0)},onDragStop:()=>{A(!1)}})},[]),a.useEffect(()=>{if(u||!f.current||!U.current)return;return U.current.update({noDragClassName:_,handleSelector:y,domNode:f.current,isSelectable:$,nodeId:l,nodeClickDistance:j}),()=>{U.current?.destroy()}},[_,y,u,$,f,l,j]),F}var lw=(f)=>(u)=>u.selected&&(u.draggable||f&&typeof u.draggable>"u");function vZ(){let f=z0();return a.useCallback((_)=>{let{nodeExtent:y,snapToGrid:l,snapGrid:$,nodesDraggable:j,onError:J,updateNodePositions:F,nodeLookup:A,nodeOrigin:U}=f.getState(),G=new Map,W=lw(j),K=l?$[0]:5,E=l?$[1]:5,H=_.direction.x*K*_.factor,O=_.direction.y*E*_.factor;for(let[,z]of A){if(!W(z))continue;let q={x:z.internals.positionAbsolute.x+H,y:z.internals.positionAbsolute.y+O};if(l)q=$3(q,$);let{position:Z,positionAbsolute:V}=AF({nodeId:z.id,nextPosition:q,nodeLookup:A,nodeExtent:y,nodeOrigin:U,onError:J});z.position=Z,z.internals.positionAbsolute=V,G.set(z.id,z)}F(G)},[])}var CF=a.createContext(null),$w=CF.Provider;CF.Consumer;var bZ=()=>{return a.useContext(CF)},jw=(f)=>({connectOnClick:f.connectOnClick,noPanClassName:f.noPanClassName,rfId:f.rfId}),Jw=(f,u,_)=>(y)=>{let{connectionClickStartHandle:l,connectionMode:$,connection:j}=y,{fromHandle:J,toHandle:F,isValid:A}=j,U=F?.nodeId===f&&F?.id===u&&F?.type===_;return{connectingFrom:J?.nodeId===f&&J?.id===u&&J?.type===_,connectingTo:U,clickConnecting:l?.nodeId===f&&l?.id===u&&l?.type===_,isPossibleEndHandle:$===g_.Strict?J?.type!==_:f!==J?.nodeId||u!==J?.id,connectionInProcess:!!J,clickConnectionInProcess:!!l,valid:U&&A}};function Fw({type:f="source",position:u=Uf.Top,isValidConnection:_,isConnectable:y=!0,isConnectableStart:l=!0,isConnectableEnd:$=!0,id:j,onConnect:J,children:F,className:A,onMouseDown:U,onTouchStart:G,...W},K){let E=j||null,H=f==="target",O=z0(),z=bZ(),{connectOnClick:q,noPanClassName:Z,rfId:V}=kf(jw,W0),{connectingFrom:L,connectingTo:r,clickConnecting:N,isPossibleEndHandle:D,connectionInProcess:x,clickConnectionInProcess:c,valid:v}=kf(Jw(z,E,f),W0);if(!z)O.getState().onError?.("010",Iu.error010());let C=(P)=>{let{defaultEdgeOptions:M,onConnect:w,hasDefaultEdges:Y}=O.getState(),R={...M,...P};if(Y){let{edges:k,setEdges:p}=O.getState();p(OF(R,k))}w?.(R),J?.(R)},S=(P)=>{if(!z)return;let M=HF(P.nativeEvent);if(l&&(M&&P.button===0||!M)){let w=O.getState();M5.onPointerDown(P.nativeEvent,{handleDomNode:P.currentTarget,autoPanOnConnect:w.autoPanOnConnect,connectionMode:w.connectionMode,connectionRadius:w.connectionRadius,domNode:w.domNode,nodeLookup:w.nodeLookup,lib:w.lib,isTarget:H,handleId:E,nodeId:z,flowId:w.rfId,panBy:w.panBy,cancelConnection:w.cancelConnection,onConnectStart:w.onConnectStart,onConnectEnd:(...Y)=>O.getState().onConnectEnd?.(...Y),updateConnection:w.updateConnection,onConnect:C,isValidConnection:_||((...Y)=>O.getState().isValidConnection?.(...Y)??!0),getTransform:()=>O.getState().transform,getFromHandle:()=>O.getState().connection.fromHandle,autoPanSpeed:w.autoPanSpeed,dragThreshold:w.connectionDragThreshold})}if(M)U?.(P);else G?.(P)},B=(P)=>{let{onClickConnectStart:M,onClickConnectEnd:w,connectionClickStartHandle:Y,connectionMode:R,isValidConnection:k,lib:p,rfId:n,nodeLookup:_f,connection:s}=O.getState();if(!z||!Y&&!l)return;if(!Y){M?.(P.nativeEvent,{nodeId:z,handleId:E,handleType:f}),O.setState({connectionClickStartHandle:{nodeId:z,type:f,id:E}});return}let ff=qF(P.target),Kf=_||k,{connection:Gf,isValid:jf}=M5.isValid(P.nativeEvent,{handle:{nodeId:z,id:E,type:f},connectionMode:R,fromNodeId:Y.nodeId,fromHandleId:Y.id||null,fromType:Y.type,isValidConnection:Kf,flowId:n,doc:ff,lib:p,nodeLookup:_f});if(jf&&Gf)C(Gf);let Wf=structuredClone(s);delete Wf.inProgress,Wf.toPosition=Wf.toHandle?Wf.toHandle.position:null,w?.(P,Wf),O.setState({connectionClickStartHandle:null})};return o.jsx("div",{"data-handleid":E,"data-nodeid":z,"data-handlepos":u,"data-id":`${V}-${z}-${E}-${f}`,className:B0(["react-flow__handle",`react-flow__handle-${u}`,"nodrag",Z,A,{source:!H,target:H,connectable:y,connectablestart:l,connectableend:$,clickconnecting:N,connectingfrom:L,connectingto:r,valid:v,connectionindicator:y&&(!x||D)&&(x||c?$:l)}]),onMouseDown:S,onTouchStart:S,onClick:q?B:void 0,ref:K,...W,children:F})}var iy=a.memo(CZ(Fw));function Qw({data:f,isConnectable:u,sourcePosition:_=Uf.Bottom}){return o.jsxs(o.Fragment,{children:[f?.label,o.jsx(iy,{type:"source",position:_,isConnectable:u})]})}function Aw({data:f,isConnectable:u,targetPosition:_=Uf.Top,sourcePosition:y=Uf.Bottom}){return o.jsxs(o.Fragment,{children:[o.jsx(iy,{type:"target",position:_,isConnectable:u}),f?.label,o.jsx(iy,{type:"source",position:y,isConnectable:u})]})}function Uw(){return null}function Ww({data:f,isConnectable:u,targetPosition:_=Uf.Top}){return o.jsxs(o.Fragment,{children:[o.jsx(iy,{type:"target",position:_,isConnectable:u}),f?.label]})}var x5={ArrowUp:{x:0,y:-1},ArrowDown:{x:0,y:1},ArrowLeft:{x:-1,y:0},ArrowRight:{x:1,y:0}},EZ={input:Qw,default:Aw,output:Ww,group:Uw};function Gw(f){if(f.internals.handleBounds===void 0)return{width:f.width??f.initialWidth??f.style?.width,height:f.height??f.initialHeight??f.style?.height};return{width:f.width??f.style?.width,height:f.height??f.style?.height}}var zw=(f)=>{let{width:u,height:_,x:y,y:l}=y3(f.nodeLookup,{filter:($)=>!!$.selected});return{width:u1(u)?u:null,height:u1(_)?_:null,userSelectionActive:f.userSelectionActive,transformString:`translate(${f.transform[0]}px,${f.transform[1]}px) scale(${f.transform[2]}) translate(${y}px,${l}px)`}};function Kw({onSelectionContextMenu:f,noPanClassName:u,disableKeyboardA11y:_}){let y=z0(),{width:l,height:$,transformString:j,userSelectionActive:J}=kf(zw,W0),F=vZ(),A=a.useRef(null);a.useEffect(()=>{if(!_)A.current?.focus({preventScroll:!0})},[_]);let U=!J&&l!==null&&$!==null;if(xZ({nodeRef:A,disabled:!U}),!U)return null;let G=f?(K)=>{let E=y.getState().nodes.filter((H)=>H.selected);f(K,E)}:void 0,W=(K)=>{if(Object.prototype.hasOwnProperty.call(x5,K.key))K.preventDefault(),F({direction:x5[K.key],factor:K.shiftKey?4:1})};return o.jsx("div",{className:B0(["react-flow__nodesselection","react-flow__container",u]),style:{transform:j},children:o.jsx("div",{ref:A,className:"react-flow__nodesselection-rect",onContextMenu:G,tabIndex:_?void 0:-1,onKeyDown:_?void 0:W,style:{width:l,height:$}})})}var HZ=typeof window<"u"?window:void 0,Zw=(f)=>{return{nodesSelectionActive:f.nodesSelectionActive,userSelectionActive:f.userSelectionActive}};function hZ({children:f,onPaneClick:u,onPaneMouseEnter:_,onPaneMouseMove:y,onPaneMouseLeave:l,onPaneContextMenu:$,onPaneScroll:j,paneClickDistance:J,deleteKeyCode:F,selectionKeyCode:A,selectionOnDrag:U,selectionMode:G,onSelectionStart:W,onSelectionEnd:K,multiSelectionKeyCode:E,panActivationKeyCode:H,zoomActivationKeyCode:O,elementsSelectable:z,zoomOnScroll:q,zoomOnPinch:Z,panOnScroll:V,panOnScrollSpeed:L,panOnScrollMode:r,zoomOnDoubleClick:N,panOnDrag:D,defaultViewport:x,translateExtent:c,minZoom:v,maxZoom:C,preventScrolling:S,onSelectionContextMenu:B,noWheelClassName:P,noPanClassName:M,disableKeyboardA11y:w,onViewportChange:Y,isControlledViewport:R}){let{nodesSelectionActive:k,userSelectionActive:p}=kf(Zw,W0),n=Z6(A,{target:HZ}),_f=Z6(H,{target:HZ}),s=_f||D,ff=_f||V,Kf=U&&s!==!0,Gf=n||p||Kf;return oD({deleteKeyCode:F,multiSelectionKeyCode:E}),o.jsx(eD,{onPaneContextMenu:$,elementsSelectable:z,zoomOnScroll:q,zoomOnPinch:Z,panOnScroll:ff,panOnScrollSpeed:L,panOnScrollMode:r,zoomOnDoubleClick:N,panOnDrag:!n&&s,defaultViewport:x,translateExtent:c,minZoom:v,maxZoom:C,zoomActivationKeyCode:O,preventScrolling:S,noWheelClassName:P,noPanClassName:M,onViewportChange:Y,isControlledViewport:R,paneClickDistance:J,selectionOnDrag:Kf,children:o.jsxs(yw,{onSelectionStart:W,onSelectionEnd:K,onPaneClick:u,onPaneMouseEnter:_,onPaneMouseMove:y,onPaneMouseLeave:l,onPaneContextMenu:$,onPaneScroll:j,panOnDrag:s,isSelecting:!!Gf,selectionMode:G,selectionKeyPressed:n,paneClickDistance:J,selectionOnDrag:Kf,children:[f,k&&o.jsx(Kw,{onSelectionContextMenu:B,noPanClassName:M,disableKeyboardA11y:w})]})})}hZ.displayName="FlowRenderer";var qw=a.memo(hZ),Ew=(f)=>(u)=>{return f?O5(u.nodeLookup,{x:0,y:0,width:u.width,height:u.height},u.transform,!0).map((_)=>_.id):Array.from(u.nodeLookup.keys())};function Hw(f){return kf(a.useCallback(Ew(f),[f]),W0)}var Vw=(f)=>f.updateNodeInternals;function Ow(){let f=kf(Vw),[u]=a.useState(()=>{if(typeof ResizeObserver>"u")return null;return new ResizeObserver((_)=>{let y=new Map;_.forEach((l)=>{let $=l.target.getAttribute("data-id");y.set($,{id:$,nodeElement:l.target,force:!0})}),f(y)})});return a.useEffect(()=>{return()=>{u?.disconnect()}},[u]),u}function Xw({node:f,nodeType:u,hasDimensions:_,resizeObserver:y}){let l=z0(),$=a.useRef(null),j=a.useRef(null),J=a.useRef(f.sourcePosition),F=a.useRef(f.targetPosition),A=a.useRef(u),U=_&&!!f.internals.handleBounds;return a.useEffect(()=>{if($.current&&!f.hidden&&(!U||j.current!==$.current)){if(j.current)y?.unobserve(j.current);y?.observe($.current),j.current=$.current}},[U,f.hidden]),a.useEffect(()=>{return()=>{if(j.current)y?.unobserve(j.current),j.current=null}},[]),a.useEffect(()=>{if($.current){let G=A.current!==u,W=J.current!==f.sourcePosition,K=F.current!==f.targetPosition;if(G||W||K)A.current=u,J.current=f.sourcePosition,F.current=f.targetPosition,l.getState().updateNodeInternals(new Map([[f.id,{id:f.id,nodeElement:$.current,force:!0}]]))}},[f.id,u,f.sourcePosition,f.targetPosition]),$}function Nw({id:f,onClick:u,onMouseEnter:_,onMouseMove:y,onMouseLeave:l,onContextMenu:$,onDoubleClick:j,nodesDraggable:J,elementsSelectable:F,nodesConnectable:A,nodesFocusable:U,resizeObserver:G,noDragClassName:W,noPanClassName:K,disableKeyboardA11y:E,rfId:H,nodeTypes:O,nodeClickDistance:z,onError:q}){let{node:Z,internals:V,isParent:L}=kf((jf)=>{let Wf=jf.nodeLookup.get(f),Of=jf.parentLookup.has(f);return{node:Wf,internals:Wf.internals,isParent:Of}},W0),r=Z.type||"default",N=O?.[r]||EZ[r];if(N===void 0)q?.("003",Iu.error003(r)),r="default",N=O?.default||EZ.default;let D=!!(Z.draggable||J&&typeof Z.draggable>"u"),x=!!(Z.selectable||F&&typeof Z.selectable>"u"),c=!!(Z.connectable||A&&typeof Z.connectable>"u"),v=!!(Z.focusable||U&&typeof Z.focusable>"u"),C=z0(),S=zF(Z),B=Xw({node:Z,nodeType:r,hasDimensions:S,resizeObserver:G}),P=xZ({nodeRef:B,disabled:Z.hidden||!D,noDragClassName:W,handleSelector:Z.dragHandle,nodeId:f,isSelectable:x,nodeClickDistance:z}),M=vZ();if(Z.hidden)return null;let w=R1(Z),Y=Gw(Z),R=x||D||u||_||y||l,k=_?(jf)=>_(jf,{...V.userNode}):void 0,p=y?(jf)=>y(jf,{...V.userNode}):void 0,n=l?(jf)=>l(jf,{...V.userNode}):void 0,_f=$?(jf)=>$(jf,{...V.userNode}):void 0,s=j?(jf)=>j(jf,{...V.userNode}):void 0,ff=(jf)=>{let{selectNodesOnDrag:Wf,nodeDragThreshold:Of}=C.getState();if(x&&(!Wf||!D||Of>0))PF({id:f,store:C,nodeRef:B});if(u)u(jf,{...V.userNode})},Kf=(jf)=>{if(EF(jf.nativeEvent)||E)return;if(yF.includes(jf.key)&&x){let Wf=jf.key==="Escape";PF({id:f,store:C,unselect:Wf,nodeRef:B})}else if(D&&Z.selected&&Object.prototype.hasOwnProperty.call(x5,jf.key)){jf.preventDefault();let{ariaLabelConfig:Wf}=C.getState();C.setState({ariaLiveMessage:Wf["node.a11yDescription.ariaLiveMessage"]({direction:jf.key.replace("Arrow","").toLowerCase(),x:~~V.positionAbsolute.x,y:~~V.positionAbsolute.y})}),M({direction:x5[jf.key],factor:jf.shiftKey?4:1})}},Gf=()=>{if(E||!B.current?.matches(":focus-visible"))return;let{transform:jf,width:Wf,height:Of,autoPanOnNodeFocus:Zf,setCenter:h}=C.getState();if(!Zf)return;if(!(O5(new Map([[f,Z]]),{x:0,y:0,width:Wf,height:Of},jf,!0).length>0))h(Z.position.x+w.width/2,Z.position.y+w.height/2,{zoom:jf[2]})};return o.jsx("div",{className:B0(["react-flow__node",`react-flow__node-${r}`,{[K]:D},Z.className,{selected:Z.selected,selectable:x,parent:L,draggable:D,dragging:P}]),ref:B,style:{zIndex:V.z,transform:`translate(${V.positionAbsolute.x}px,${V.positionAbsolute.y}px)`,pointerEvents:R?"all":"none",visibility:S?"visible":"hidden",...Z.style,...Y},"data-id":f,"data-testid":`rf__node-${f}`,onMouseEnter:k,onMouseMove:p,onMouseLeave:n,onContextMenu:_f,onClick:ff,onDoubleClick:s,onKeyDown:v?Kf:void 0,tabIndex:v?0:void 0,onFocus:v?Gf:void 0,role:Z.ariaRole??(v?"group":void 0),"aria-roledescription":"node","aria-describedby":E?void 0:`${rZ}-${H}`,"aria-label":Z.ariaLabel,...Z.domAttributes,children:o.jsx($w,{value:f,children:o.jsx(N,{id:f,data:Z.data,type:r,positionAbsoluteX:V.positionAbsolute.x,positionAbsoluteY:V.positionAbsolute.y,selected:Z.selected??!1,selectable:x,draggable:D,deletable:Z.deletable??!0,isConnectable:c,sourcePosition:Z.sourcePosition,targetPosition:Z.targetPosition,dragging:P,dragHandle:Z.dragHandle,zIndex:V.z,parentId:Z.parentId,...w})})})}var Lw=a.memo(Nw),Yw=(f)=>({nodesDraggable:f.nodesDraggable,nodesConnectable:f.nodesConnectable,nodesFocusable:f.nodesFocusable,elementsSelectable:f.elementsSelectable,onError:f.onError});function IZ(f){let{nodesDraggable:u,nodesConnectable:_,nodesFocusable:y,elementsSelectable:l,onError:$}=kf(Yw,W0),j=Hw(f.onlyRenderVisibleElements),J=Ow();return o.jsx("div",{className:"react-flow__nodes",style:h5,children:j.map((F)=>{return o.jsx(Lw,{id:F,nodeTypes:f.nodeTypes,nodeExtent:f.nodeExtent,onClick:f.onNodeClick,onMouseEnter:f.onNodeMouseEnter,onMouseMove:f.onNodeMouseMove,onMouseLeave:f.onNodeMouseLeave,onContextMenu:f.onNodeContextMenu,onDoubleClick:f.onNodeDoubleClick,noDragClassName:f.noDragClassName,noPanClassName:f.noPanClassName,rfId:f.rfId,disableKeyboardA11y:f.disableKeyboardA11y,resizeObserver:J,nodesDraggable:u,nodesConnectable:_,nodesFocusable:y,elementsSelectable:l,nodeClickDistance:f.nodeClickDistance,onError:$},F)})})}IZ.displayName="NodeRenderer";var Bw=a.memo(IZ);function Dw(f){return kf(a.useCallback((_)=>{if(!f)return _.edges.map((l)=>l.id);let y=[];if(_.width&&_.height)for(let l of _.edges){let $=_.nodeLookup.get(l.source),j=_.nodeLookup.get(l.target);if($&&j&&BK({sourceNode:$,targetNode:j,width:_.width,height:_.height,transform:_.transform}))y.push(l.id)}return y},[f]),W0)}var ww=({color:f="none",strokeWidth:u=1})=>{let _={strokeWidth:u,...f&&{stroke:f}};return o.jsx("polyline",{className:"arrow",style:_,strokeLinecap:"round",fill:"none",strokeLinejoin:"round",points:"-5,-4 0,0 -5,4"})},Tw=({color:f="none",strokeWidth:u=1})=>{let _={strokeWidth:u,...f&&{stroke:f,fill:f}};return o.jsx("polyline",{className:"arrowclosed",style:_,strokeLinecap:"round",strokeLinejoin:"round",points:"-5,-4 0,0 -5,4 -5,-4"})},VZ={[n_.Arrow]:ww,[n_.ArrowClosed]:Tw};function rw(f){let u=z0();return a.useMemo(()=>{if(!Object.prototype.hasOwnProperty.call(VZ,f))return u.getState().onError?.("009",Iu.error009(f)),null;return VZ[f]},[f])}var Mw=({id:f,type:u,color:_,width:y=12.5,height:l=12.5,markerUnits:$="strokeWidth",strokeWidth:j,orient:J="auto-start-reverse"})=>{let F=rw(u);if(!F)return null;return o.jsx("marker",{className:"react-flow__arrowhead",id:f,markerWidth:`${y}`,markerHeight:`${l}`,viewBox:"-10 -10 20 20",markerUnits:$,orient:J,refX:"0",refY:"0",children:o.jsx(F,{color:_,strokeWidth:j})})},cZ=({defaultColor:f,rfId:u})=>{let _=kf(($)=>$.edges),y=kf(($)=>$.defaultEdgeOptions),l=a.useMemo(()=>{return wK(_,{id:u,defaultColor:f,defaultMarkerStart:y?.markerStart,defaultMarkerEnd:y?.markerEnd})},[_,y,u,f]);if(!l.length)return null;return o.jsx("svg",{className:"react-flow__marker","aria-hidden":"true",children:o.jsx("defs",{children:l.map(($)=>o.jsx(Mw,{id:$.id,type:$.type,color:$.color,width:$.width,height:$.height,markerUnits:$.markerUnits,strokeWidth:$.strokeWidth,orient:$.orient},$.id))})})};cZ.displayName="MarkerDefinitions";var Pw=a.memo(cZ);function pZ({x:f,y:u,label:_,labelStyle:y,labelShowBg:l=!0,labelBgStyle:$,labelBgPadding:j=[2,4],labelBgBorderRadius:J=2,children:F,className:A,...U}){let[G,W]=a.useState({x:1,y:0,width:0,height:0}),K=B0(["react-flow__edge-textwrapper",A]),E=a.useRef(null);if(a.useEffect(()=>{if(E.current){let H=E.current.getBBox();W({x:H.x,y:H.y,width:H.width,height:H.height})}},[_]),!_)return null;return o.jsxs("g",{transform:`translate(${f-G.width/2} ${u-G.height/2})`,className:K,visibility:G.width?"visible":"hidden",...U,children:[l&&o.jsx("rect",{width:G.width+2*j[0],x:-j[0],y:-j[1],height:G.height+2*j[1],className:"react-flow__edge-textbg",style:$,rx:J,ry:J}),o.jsx("text",{className:"react-flow__edge-text",y:G.height/2,dy:"0.3em",ref:E,style:y,children:_}),F]})}pZ.displayName="EdgeText";var Sw=a.memo(pZ);function A3({path:f,labelX:u,labelY:_,label:y,labelStyle:l,labelShowBg:$,labelBgStyle:j,labelBgPadding:J,labelBgBorderRadius:F,interactionWidth:A=20,...U}){return o.jsxs(o.Fragment,{children:[o.jsx("path",{...U,d:f,fill:"none",className:B0(["react-flow__edge-path",U.className])}),A?o.jsx("path",{d:f,fill:"none",strokeOpacity:0,strokeWidth:A,className:"react-flow__edge-interaction"}):null,y&&u1(u)&&u1(_)?o.jsx(Sw,{x:u,y:_,label:y,labelStyle:l,labelShowBg:$,labelBgStyle:j,labelBgPadding:J,labelBgBorderRadius:F}):null]})}function OZ({pos:f,x1:u,y1:_,x2:y,y2:l}){if(f===Uf.Left||f===Uf.Right)return[0.5*(u+y),_];return[u,0.5*(_+l)]}function kZ({sourceX:f,sourceY:u,sourcePosition:_=Uf.Bottom,targetX:y,targetY:l,targetPosition:$=Uf.Top}){let[j,J]=OZ({pos:_,x1:f,y1:u,x2:y,y2:l}),[F,A]=OZ({pos:$,x1:y,y1:l,x2:f,y2:u}),[U,G,W,K]=Y5({sourceX:f,sourceY:u,targetX:y,targetY:l,sourceControlX:j,sourceControlY:J,targetControlX:F,targetControlY:A});return[`M${f},${u} C${j},${J} ${F},${A} ${y},${l}`,U,G,W,K]}function mZ(f){return a.memo(({id:u,sourceX:_,sourceY:y,targetX:l,targetY:$,sourcePosition:j,targetPosition:J,label:F,labelStyle:A,labelShowBg:U,labelBgStyle:G,labelBgPadding:W,labelBgBorderRadius:K,style:E,markerEnd:H,markerStart:O,interactionWidth:z})=>{let[q,Z,V]=kZ({sourceX:_,sourceY:y,sourcePosition:j,targetX:l,targetY:$,targetPosition:J}),L=f.isInternal?void 0:u;return o.jsx(A3,{id:L,path:q,labelX:Z,labelY:V,label:F,labelStyle:A,labelShowBg:U,labelBgStyle:G,labelBgPadding:W,labelBgBorderRadius:K,style:E,markerEnd:H,markerStart:O,interactionWidth:z})})}var Cw=mZ({isInternal:!1}),iZ=mZ({isInternal:!0});Cw.displayName="SimpleBezierEdge";iZ.displayName="SimpleBezierEdgeInternal";function gZ(f){return a.memo(({id:u,sourceX:_,sourceY:y,targetX:l,targetY:$,label:j,labelStyle:J,labelShowBg:F,labelBgStyle:A,labelBgPadding:U,labelBgBorderRadius:G,style:W,sourcePosition:K=Uf.Bottom,targetPosition:E=Uf.Top,markerEnd:H,markerStart:O,pathOptions:z,interactionWidth:q})=>{let[Z,V,L]=K6({sourceX:_,sourceY:y,sourcePosition:K,targetX:l,targetY:$,targetPosition:E,borderRadius:z?.borderRadius,offset:z?.offset,stepPosition:z?.stepPosition}),r=f.isInternal?void 0:u;return o.jsx(A3,{id:r,path:Z,labelX:V,labelY:L,label:j,labelStyle:J,labelShowBg:F,labelBgStyle:A,labelBgPadding:U,labelBgBorderRadius:G,style:W,markerEnd:H,markerStart:O,interactionWidth:q})})}var nZ=gZ({isInternal:!1}),tZ=gZ({isInternal:!0});nZ.displayName="SmoothStepEdge";tZ.displayName="SmoothStepEdgeInternal";function sZ(f){return a.memo(({id:u,..._})=>{let y=f.isInternal?void 0:u;return o.jsx(nZ,{..._,id:y,pathOptions:a.useMemo(()=>({borderRadius:0,offset:_.pathOptions?.offset}),[_.pathOptions?.offset])})})}var Rw=sZ({isInternal:!1}),oZ=sZ({isInternal:!0});Rw.displayName="StepEdge";oZ.displayName="StepEdgeInternal";function aZ(f){return a.memo(({id:u,sourceX:_,sourceY:y,targetX:l,targetY:$,label:j,labelStyle:J,labelShowBg:F,labelBgStyle:A,labelBgPadding:U,labelBgBorderRadius:G,style:W,markerEnd:K,markerStart:E,interactionWidth:H})=>{let[O,z,q]=D5({sourceX:_,sourceY:y,targetX:l,targetY:$}),Z=f.isInternal?void 0:u;return o.jsx(A3,{id:Z,path:O,labelX:z,labelY:q,label:j,labelStyle:J,labelShowBg:F,labelBgStyle:A,labelBgPadding:U,labelBgBorderRadius:G,style:W,markerEnd:K,markerStart:E,interactionWidth:H})})}var xw=aZ({isInternal:!1}),dZ=aZ({isInternal:!0});xw.displayName="StraightEdge";dZ.displayName="StraightEdgeInternal";function eZ(f){return a.memo(({id:u,sourceX:_,sourceY:y,targetX:l,targetY:$,sourcePosition:j=Uf.Bottom,targetPosition:J=Uf.Top,label:F,labelStyle:A,labelShowBg:U,labelBgStyle:G,labelBgPadding:W,labelBgBorderRadius:K,style:E,markerEnd:H,markerStart:O,pathOptions:z,interactionWidth:q})=>{let[Z,V,L]=B5({sourceX:_,sourceY:y,sourcePosition:j,targetX:l,targetY:$,targetPosition:J,curvature:z?.curvature}),r=f.isInternal?void 0:u;return o.jsx(A3,{id:r,path:Z,labelX:V,labelY:L,label:F,labelStyle:A,labelShowBg:U,labelBgStyle:G,labelBgPadding:W,labelBgBorderRadius:K,style:E,markerEnd:H,markerStart:O,interactionWidth:q})})}var vw=eZ({isInternal:!1}),fq=eZ({isInternal:!0});vw.displayName="BezierEdge";fq.displayName="BezierEdgeInternal";var XZ={default:fq,straight:dZ,step:oZ,smoothstep:tZ,simplebezier:iZ},NZ={sourceX:null,sourceY:null,targetX:null,targetY:null,sourcePosition:null,targetPosition:null},bw=(f,u,_)=>{if(_===Uf.Left)return f-u;if(_===Uf.Right)return f+u;return f},hw=(f,u,_)=>{if(_===Uf.Top)return f-u;if(_===Uf.Bottom)return f+u;return f},LZ="react-flow__edgeupdater";function YZ({position:f,centerX:u,centerY:_,radius:y=10,onMouseDown:l,onMouseEnter:$,onMouseOut:j,type:J}){return o.jsx("circle",{onMouseDown:l,onMouseEnter:$,onMouseOut:j,className:B0([LZ,`${LZ}-${J}`]),cx:bw(u,y,f),cy:hw(_,y,f),r:y,stroke:"transparent",fill:"transparent"})}function Iw({isReconnectable:f,reconnectRadius:u,edge:_,sourceX:y,sourceY:l,targetX:$,targetY:j,sourcePosition:J,targetPosition:F,onReconnect:A,onReconnectStart:U,onReconnectEnd:G,setReconnecting:W,setUpdateHover:K}){let E=z0(),H=(V,L)=>{if(V.button!==0)return;let{autoPanOnConnect:r,domNode:N,connectionMode:D,connectionRadius:x,lib:c,onConnectStart:v,cancelConnection:C,nodeLookup:S,rfId:B,panBy:P,updateConnection:M}=E.getState(),w=L.type==="target",Y=(p,n)=>{W(!1),G?.(p,_,L.type,n)},R=(p)=>A?.(_,p),k=(p,n)=>{W(!0),U?.(V,_,L.type),v?.(p,n)};M5.onPointerDown(V.nativeEvent,{autoPanOnConnect:r,connectionMode:D,connectionRadius:x,domNode:N,handleId:L.id,nodeId:L.nodeId,nodeLookup:S,isTarget:w,edgeUpdaterType:L.type,lib:c,flowId:B,cancelConnection:C,panBy:P,isValidConnection:(...p)=>E.getState().isValidConnection?.(...p)??!0,onConnect:R,onConnectStart:k,onConnectEnd:(...p)=>E.getState().onConnectEnd?.(...p),onReconnectEnd:Y,updateConnection:M,getTransform:()=>E.getState().transform,getFromHandle:()=>E.getState().connection.fromHandle,dragThreshold:E.getState().connectionDragThreshold,handleDomNode:V.currentTarget})},O=(V)=>H(V,{nodeId:_.target,id:_.targetHandle??null,type:"target"}),z=(V)=>H(V,{nodeId:_.source,id:_.sourceHandle??null,type:"source"}),q=()=>K(!0),Z=()=>K(!1);return o.jsxs(o.Fragment,{children:[(f===!0||f==="source")&&o.jsx(YZ,{position:J,centerX:y,centerY:l,radius:u,onMouseDown:O,onMouseEnter:q,onMouseOut:Z,type:"source"}),(f===!0||f==="target")&&o.jsx(YZ,{position:F,centerX:$,centerY:j,radius:u,onMouseDown:z,onMouseEnter:q,onMouseOut:Z,type:"target"})]})}function cw({id:f,edgesFocusable:u,edgesReconnectable:_,elementsSelectable:y,onClick:l,onDoubleClick:$,onContextMenu:j,onMouseEnter:J,onMouseMove:F,onMouseLeave:A,reconnectRadius:U,onReconnect:G,onReconnectStart:W,onReconnectEnd:K,rfId:E,edgeTypes:H,noPanClassName:O,onError:z,disableKeyboardA11y:q}){let Z=kf((h)=>h.edgeLookup.get(f)),V=kf((h)=>h.defaultEdgeOptions);Z=V?{...V,...Z}:Z;let L=Z.type||"default",r=H?.[L]||XZ[L];if(r===void 0)z?.("011",Iu.error011(L)),L="default",r=H?.default||XZ.default;let N=!!(Z.focusable||u&&typeof Z.focusable>"u"),D=typeof G<"u"&&(Z.reconnectable||_&&typeof Z.reconnectable>"u"),x=!!(Z.selectable||y&&typeof Z.selectable>"u"),c=a.useRef(null),[v,C]=a.useState(!1),[S,B]=a.useState(!1),P=z0(),{zIndex:M,sourceX:w,sourceY:Y,targetX:R,targetY:k,sourcePosition:p,targetPosition:n}=kf(a.useCallback((h)=>{let i=h.nodeLookup.get(Z.source),I=h.nodeLookup.get(Z.target);if(!i||!I)return{zIndex:Z.zIndex,...NZ};let lf=DK({id:f,sourceNode:i,targetNode:I,sourceHandle:Z.sourceHandle||null,targetHandle:Z.targetHandle||null,connectionMode:h.connectionMode,onError:z});return{zIndex:YK({selected:Z.selected,zIndex:Z.zIndex,sourceNode:i,targetNode:I,elevateOnSelect:h.elevateEdgesOnSelect,zIndexMode:h.zIndexMode}),...lf||NZ}},[Z.source,Z.target,Z.sourceHandle,Z.targetHandle,Z.selected,Z.zIndex]),W0),_f=a.useMemo(()=>Z.markerStart?`url('#${w5(Z.markerStart,E)}')`:void 0,[Z.markerStart,E]),s=a.useMemo(()=>Z.markerEnd?`url('#${w5(Z.markerEnd,E)}')`:void 0,[Z.markerEnd,E]);if(Z.hidden||w===null||Y===null||R===null||k===null)return null;let ff=(h)=>{let{addSelectedEdges:i,unselectNodesAndEdges:I,multiSelectionActive:lf}=P.getState();if(x)if(P.setState({nodesSelectionActive:!1}),Z.selected&&lf)I({nodes:[],edges:[Z]}),c.current?.blur();else i([f]);if(l)l(h,Z)},Kf=$?(h)=>{$(h,{...Z})}:void 0,Gf=j?(h)=>{j(h,{...Z})}:void 0,jf=J?(h)=>{J(h,{...Z})}:void 0,Wf=F?(h)=>{F(h,{...Z})}:void 0,Of=A?(h)=>{A(h,{...Z})}:void 0,Zf=(h)=>{if(!q&&yF.includes(h.key)&&x){let{unselectNodesAndEdges:i,addSelectedEdges:I}=P.getState();if(h.key==="Escape")c.current?.blur(),i({edges:[Z]});else I([f])}};return o.jsx("svg",{style:{zIndex:M},children:o.jsxs("g",{className:B0(["react-flow__edge",`react-flow__edge-${L}`,Z.className,O,{selected:Z.selected,animated:Z.animated,inactive:!x&&!l,updating:v,selectable:x}]),onClick:ff,onDoubleClick:Kf,onContextMenu:Gf,onMouseEnter:jf,onMouseMove:Wf,onMouseLeave:Of,onKeyDown:N?Zf:void 0,tabIndex:N?0:void 0,role:Z.ariaRole??(N?"group":"img"),"aria-roledescription":"edge","data-id":f,"data-testid":`rf__edge-${f}`,"aria-label":Z.ariaLabel===null?void 0:Z.ariaLabel||`Edge from ${Z.source} to ${Z.target}`,"aria-describedby":N?`${MZ}-${E}`:void 0,ref:c,...Z.domAttributes,children:[!S&&o.jsx(r,{id:f,source:Z.source,target:Z.target,type:Z.type,selected:Z.selected,animated:Z.animated,selectable:x,deletable:Z.deletable??!0,label:Z.label,labelStyle:Z.labelStyle,labelShowBg:Z.labelShowBg,labelBgStyle:Z.labelBgStyle,labelBgPadding:Z.labelBgPadding,labelBgBorderRadius:Z.labelBgBorderRadius,sourceX:w,sourceY:Y,targetX:R,targetY:k,sourcePosition:p,targetPosition:n,data:Z.data,style:Z.style,sourceHandleId:Z.sourceHandle,targetHandleId:Z.targetHandle,markerStart:_f,markerEnd:s,pathOptions:"pathOptions"in Z?Z.pathOptions:void 0,interactionWidth:Z.interactionWidth}),D&&o.jsx(Iw,{edge:Z,isReconnectable:D,reconnectRadius:U,onReconnect:G,onReconnectStart:W,onReconnectEnd:K,sourceX:w,sourceY:Y,targetX:R,targetY:k,sourcePosition:p,targetPosition:n,setUpdateHover:C,setReconnecting:B})]})})}var pw=a.memo(cw),kw=(f)=>({edgesFocusable:f.edgesFocusable,edgesReconnectable:f.edgesReconnectable,elementsSelectable:f.elementsSelectable,connectionMode:f.connectionMode,onError:f.onError});function uq({defaultMarkerColor:f,onlyRenderVisibleElements:u,rfId:_,edgeTypes:y,noPanClassName:l,onReconnect:$,onEdgeContextMenu:j,onEdgeMouseEnter:J,onEdgeMouseMove:F,onEdgeMouseLeave:A,onEdgeClick:U,reconnectRadius:G,onEdgeDoubleClick:W,onReconnectStart:K,onReconnectEnd:E,disableKeyboardA11y:H}){let{edgesFocusable:O,edgesReconnectable:z,elementsSelectable:q,onError:Z}=kf(kw,W0),V=Dw(u);return o.jsxs("div",{className:"react-flow__edges",children:[o.jsx(Pw,{defaultColor:f,rfId:_}),V.map((L)=>{return o.jsx(pw,{id:L,edgesFocusable:O,edgesReconnectable:z,elementsSelectable:q,noPanClassName:l,onReconnect:$,onContextMenu:j,onMouseEnter:J,onMouseMove:F,onMouseLeave:A,onClick:U,reconnectRadius:G,onDoubleClick:W,onReconnectStart:K,onReconnectEnd:E,rfId:_,onError:Z,edgeTypes:y,disableKeyboardA11y:H},L)})]})}uq.displayName="EdgeRenderer";var mw=a.memo(uq),iw=(f)=>`translate(${f.transform[0]}px,${f.transform[1]}px) scale(${f.transform[2]})`;function gw({children:f}){let u=kf(iw);return o.jsx("div",{className:"react-flow__viewport xyflow__viewport react-flow__container",style:{transform:u},children:f})}function nw(f){let u=SF(),_=a.useRef(!1);a.useEffect(()=>{if(!_.current&&u.viewportInitialized&&f)setTimeout(()=>f(u),1),_.current=!0},[f,u.viewportInitialized])}var tw=(f)=>f.panZoom?.syncViewport;function sw(f){let u=kf(tw),_=z0();return a.useEffect(()=>{if(f)u?.(f),_.setState({transform:[f.x,f.y,f.zoom]})},[f,u]),null}function BZ(f){return f.connection.inProgress?{...f.connection,to:j3(f.connection.to,f.transform)}:{...f.connection}}function ow(f){if(f)return(_)=>{let y=BZ(_);return f(y)};return BZ}function aw(f){let u=ow(f);return kf(u,W0)}var dw=(f)=>({nodesConnectable:f.nodesConnectable,isValid:f.connection.isValid,inProgress:f.connection.inProgress,width:f.width,height:f.height});function ew({containerStyle:f,style:u,type:_,component:y}){let{nodesConnectable:l,width:$,height:j,isValid:J,inProgress:F}=kf(dw,W0);if(!($&&l&&F))return null;return o.jsx("svg",{style:f,width:$,height:j,className:"react-flow__connectionline react-flow__container",children:o.jsx("g",{className:B0(["react-flow__connection",jF(J)]),children:o.jsx(_q,{style:u,type:_,CustomComponent:y,isValid:J})})})}var _q=({style:f,type:u=C1.Bezier,CustomComponent:_,isValid:y})=>{let{inProgress:l,from:$,fromNode:j,fromHandle:J,fromPosition:F,to:A,toNode:U,toHandle:G,toPosition:W,pointer:K}=aw();if(!l)return;if(_)return o.jsx(_,{connectionLineType:u,connectionLineStyle:f,fromNode:j,fromHandle:J,fromX:$.x,fromY:$.y,toX:A.x,toY:A.y,fromPosition:F,toPosition:W,connectionStatus:jF(y),toNode:U,toHandle:G,pointer:K});let E="",H={sourceX:$.x,sourceY:$.y,sourcePosition:F,targetX:A.x,targetY:A.y,targetPosition:W};switch(u){case C1.Bezier:[E]=B5(H);break;case C1.SimpleBezier:[E]=kZ(H);break;case C1.Step:[E]=K6({...H,borderRadius:0});break;case C1.SmoothStep:[E]=K6(H);break;default:[E]=D5(H)}return o.jsx("path",{d:E,fill:"none",className:"react-flow__connection-path",style:f})};_q.displayName="ConnectionLine";var fT={};function DZ(f=fT){let u=a.useRef(f),_=z0();a.useEffect(()=>{},[f])}function uT(){let f=z0(),u=a.useRef(!1);a.useEffect(()=>{},[])}function yq({nodeTypes:f,edgeTypes:u,onInit:_,onNodeClick:y,onEdgeClick:l,onNodeDoubleClick:$,onEdgeDoubleClick:j,onNodeMouseEnter:J,onNodeMouseMove:F,onNodeMouseLeave:A,onNodeContextMenu:U,onSelectionContextMenu:G,onSelectionStart:W,onSelectionEnd:K,connectionLineType:E,connectionLineStyle:H,connectionLineComponent:O,connectionLineContainerStyle:z,selectionKeyCode:q,selectionOnDrag:Z,selectionMode:V,multiSelectionKeyCode:L,panActivationKeyCode:r,zoomActivationKeyCode:N,deleteKeyCode:D,onlyRenderVisibleElements:x,elementsSelectable:c,defaultViewport:v,translateExtent:C,minZoom:S,maxZoom:B,preventScrolling:P,defaultMarkerColor:M,zoomOnScroll:w,zoomOnPinch:Y,panOnScroll:R,panOnScrollSpeed:k,panOnScrollMode:p,zoomOnDoubleClick:n,panOnDrag:_f,onPaneClick:s,onPaneMouseEnter:ff,onPaneMouseMove:Kf,onPaneMouseLeave:Gf,onPaneScroll:jf,onPaneContextMenu:Wf,paneClickDistance:Of,nodeClickDistance:Zf,onEdgeContextMenu:h,onEdgeMouseEnter:i,onEdgeMouseMove:I,onEdgeMouseLeave:lf,reconnectRadius:$f,onReconnect:Af,onReconnectStart:Yf,onReconnectEnd:xf,noDragClassName:of,noWheelClassName:F0,noPanClassName:y0,disableKeyboardA11y:T0,nodeExtent:Qu,rfId:X0,viewport:v0,onViewportChange:iu}){return DZ(f),DZ(u),uT(),nw(_),sw(v0),o.jsx(qw,{onPaneClick:s,onPaneMouseEnter:ff,onPaneMouseMove:Kf,onPaneMouseLeave:Gf,onPaneContextMenu:Wf,onPaneScroll:jf,paneClickDistance:Of,deleteKeyCode:D,selectionKeyCode:q,selectionOnDrag:Z,selectionMode:V,onSelectionStart:W,onSelectionEnd:K,multiSelectionKeyCode:L,panActivationKeyCode:r,zoomActivationKeyCode:N,elementsSelectable:c,zoomOnScroll:w,zoomOnPinch:Y,zoomOnDoubleClick:n,panOnScroll:R,panOnScrollSpeed:k,panOnScrollMode:p,panOnDrag:_f,defaultViewport:v,translateExtent:C,minZoom:S,maxZoom:B,onSelectionContextMenu:G,preventScrolling:P,noDragClassName:of,noWheelClassName:F0,noPanClassName:y0,disableKeyboardA11y:T0,onViewportChange:iu,isControlledViewport:!!v0,children:o.jsxs(gw,{children:[o.jsx(mw,{edgeTypes:u,onEdgeClick:l,onEdgeDoubleClick:j,onReconnect:Af,onReconnectStart:Yf,onReconnectEnd:xf,onlyRenderVisibleElements:x,onEdgeContextMenu:h,onEdgeMouseEnter:i,onEdgeMouseMove:I,onEdgeMouseLeave:lf,reconnectRadius:$f,defaultMarkerColor:M,noPanClassName:y0,disableKeyboardA11y:T0,rfId:X0}),o.jsx(ew,{style:H,type:E,component:O,containerStyle:z}),o.jsx("div",{className:"react-flow__edgelabel-renderer"}),o.jsx(Bw,{nodeTypes:f,onNodeClick:y,onNodeDoubleClick:$,onNodeMouseEnter:J,onNodeMouseMove:F,onNodeMouseLeave:A,onNodeContextMenu:U,nodeClickDistance:Zf,onlyRenderVisibleElements:x,noPanClassName:y0,noDragClassName:of,disableKeyboardA11y:T0,nodeExtent:Qu,rfId:X0}),o.jsx("div",{className:"react-flow__viewport-portal"})]})})}yq.displayName="GraphView";var _T=a.memo(yq),wZ=({nodes:f,edges:u,defaultNodes:_,defaultEdges:y,width:l,height:$,fitView:j,fitViewOptions:J,minZoom:F=0.5,maxZoom:A=2,nodeOrigin:U,nodeExtent:G,zIndexMode:W="basic"}={})=>{let K=new Map,E=new Map,H=new Map,O=new Map,z=y??u??[],q=_??f??[],Z=U??[0,0],V=G??_3;BF(H,O,z);let{nodesInitialized:L}=T5(q,K,E,{nodeOrigin:Z,nodeExtent:V,zIndexMode:W}),r=[0,0,1];if(j&&l&&$){let N=y3(K,{filter:(v)=>!!((v.width||v.initialWidth)&&(v.height||v.initialHeight))}),{x:D,y:x,zoom:c}=z6(N,l,$,F,A,J?.padding??0.1);r=[D,x,c]}return{rfId:"1",width:l??0,height:$??0,transform:r,nodes:q,nodesInitialized:L,nodeLookup:K,parentLookup:E,edges:z,edgeLookup:O,connectionLookup:H,onNodesChange:null,onEdgesChange:null,hasDefaultNodes:_!==void 0,hasDefaultEdges:y!==void 0,panZoom:null,minZoom:F,maxZoom:A,translateExtent:_3,nodeExtent:V,nodesSelectionActive:!1,userSelectionActive:!1,userSelectionRect:null,connectionMode:g_.Strict,domNode:null,paneDragging:!1,noPanClassName:"nopan",nodeOrigin:Z,nodeDragThreshold:1,connectionDragThreshold:1,snapGrid:[15,15],snapToGrid:!1,nodesDraggable:!0,nodesConnectable:!0,nodesFocusable:!0,edgesFocusable:!0,edgesReconnectable:!0,elementsSelectable:!0,elevateNodesOnSelect:!0,elevateEdgesOnSelect:!0,selectNodesOnDrag:!0,multiSelectionActive:!1,fitViewQueued:j??!1,fitViewOptions:J,fitViewResolver:null,connection:{...$F},connectionClickStartHandle:null,connectOnClick:!0,ariaLiveMessage:"",autoPanOnConnect:!0,autoPanOnNodeDrag:!0,autoPanOnNodeFocus:!0,autoPanSpeed:15,connectionRadius:20,onError:GF,isValidConnection:void 0,onSelectionChangeHandlers:[],lib:"react",debug:!1,ariaLabelConfig:lF,zIndexMode:W,onNodesChangeMiddlewareMap:new Map,onEdgesChangeMiddlewareMap:new Map}},yT=({nodes:f,edges:u,defaultNodes:_,defaultEdges:y,width:l,height:$,fitView:j,fitViewOptions:J,minZoom:F,maxZoom:A,nodeOrigin:U,nodeExtent:G,zIndexMode:W})=>$Z((K,E)=>{async function H(){let{nodeLookup:O,panZoom:z,fitViewOptions:q,fitViewResolver:Z,width:V,height:L,minZoom:r,maxZoom:N}=E();if(!z)return;await HK({nodes:O,width:V,height:L,panZoom:z,minZoom:r,maxZoom:N},q),Z?.resolve(!0),K({fitViewResolver:null})}return{...wZ({nodes:f,edges:u,width:l,height:$,fitView:j,fitViewOptions:J,minZoom:F,maxZoom:A,nodeOrigin:U,nodeExtent:G,defaultNodes:_,defaultEdges:y,zIndexMode:W}),setNodes:(O)=>{let{nodeLookup:z,parentLookup:q,nodeOrigin:Z,elevateNodesOnSelect:V,fitViewQueued:L,zIndexMode:r,nodesSelectionActive:N}=E(),{nodesInitialized:D,hasSelectedNodes:x}=T5(O,z,q,{nodeOrigin:Z,nodeExtent:G,elevateNodesOnSelect:V,checkEquality:!0,zIndexMode:r}),c=N&&x;if(L&&D)H(),K({nodes:O,nodesInitialized:D,fitViewQueued:!1,fitViewOptions:void 0,nodesSelectionActive:c});else K({nodes:O,nodesInitialized:D,nodesSelectionActive:c})},setEdges:(O)=>{let{connectionLookup:z,edgeLookup:q}=E();BF(z,q,O),K({edges:O})},setDefaultNodesAndEdges:(O,z)=>{if(O){let{setNodes:q}=E();q(O),K({hasDefaultNodes:!0})}if(z){let{setEdges:q}=E();q(z),K({hasDefaultEdges:!0})}},updateNodeInternals:(O)=>{let{triggerNodeChanges:z,nodeLookup:q,parentLookup:Z,domNode:V,nodeOrigin:L,nodeExtent:r,debug:N,fitViewQueued:D,zIndexMode:x}=E(),{changes:c,updatedInternals:v}=PK(O,q,Z,V,L,r,x);if(!v)return;if(rK(q,Z,{nodeOrigin:L,nodeExtent:r,zIndexMode:x}),D)H(),K({fitViewQueued:!1,fitViewOptions:void 0});else K({});if(c?.length>0){if(N)console.log("React Flow: trigger node changes",c);z?.(c)}},updateNodePositions:(O,z=!1)=>{let q=[],Z=[],{nodeLookup:V,triggerNodeChanges:L,connection:r,updateConnection:N,onNodesChangeMiddlewareMap:D}=E();for(let[x,c]of O){let v=V.get(x),C=!!(v?.expandParent&&v?.parentId&&c?.position),S={id:x,type:"position",position:C?{x:Math.max(0,c.position.x),y:Math.max(0,c.position.y)}:c.position,dragging:z};if(v&&r.inProgress&&r.fromNode.id===v.id){let B=t_(v,r.fromHandle,Uf.Left,!0);N({...r,from:B})}if(C&&v.parentId)q.push({id:x,parentId:v.parentId,rect:{...c.internals.positionAbsolute,width:c.measured.width??0,height:c.measured.height??0}});Z.push(S)}if(q.length>0){let{parentLookup:x,nodeOrigin:c}=E(),v=r5(q,V,x,c);Z.push(...v)}for(let x of D.values())Z=x(Z);L(Z)},triggerNodeChanges:(O)=>{let{onNodesChange:z,setNodes:q,nodes:Z,hasDefaultNodes:V,debug:L}=E();if(O?.length){if(V){let r=pD(O,Z);q(r)}if(L)console.log("React Flow: trigger node changes",O);z?.(O)}},triggerEdgeChanges:(O)=>{let{onEdgesChange:z,setEdges:q,edges:Z,hasDefaultEdges:V,debug:L}=E();if(O?.length){if(V){let r=kD(O,Z);q(r)}if(L)console.log("React Flow: trigger edge changes",O);z?.(O)}},addSelectedNodes:(O)=>{let{multiSelectionActive:z,edgeLookup:q,nodeLookup:Z,triggerNodeChanges:V,triggerEdgeChanges:L}=E();if(z){let r=O.map((N)=>my(N,!0));V(r);return}V(Q3(Z,new Set([...O]),!0)),L(Q3(q))},addSelectedEdges:(O)=>{let{multiSelectionActive:z,edgeLookup:q,nodeLookup:Z,triggerNodeChanges:V,triggerEdgeChanges:L}=E();if(z){let r=O.map((N)=>my(N,!0));L(r);return}L(Q3(q,new Set([...O]))),V(Q3(Z,new Set,!0))},unselectNodesAndEdges:({nodes:O,edges:z}={})=>{let{edges:q,nodes:Z,nodeLookup:V,triggerNodeChanges:L,triggerEdgeChanges:r}=E(),N=O?O:Z,D=z?z:q,x=[];for(let v of N){if(!v.selected)continue;let C=V.get(v.id);if(C)C.selected=!1;x.push(my(v.id,!1))}let c=[];for(let v of D){if(!v.selected)continue;c.push(my(v.id,!1))}L(x),r(c)},setMinZoom:(O)=>{let{panZoom:z,maxZoom:q}=E();z?.setScaleExtent([O,q]),K({minZoom:O})},setMaxZoom:(O)=>{let{panZoom:z,minZoom:q}=E();z?.setScaleExtent([q,O]),K({maxZoom:O})},setTranslateExtent:(O)=>{E().panZoom?.setTranslateExtent(O),K({translateExtent:O})},resetSelectedElements:()=>{let{edges:O,nodes:z,triggerNodeChanges:q,triggerEdgeChanges:Z,elementsSelectable:V}=E();if(!V)return;let L=z.reduce((N,D)=>D.selected?[...N,my(D.id,!1)]:N,[]),r=O.reduce((N,D)=>D.selected?[...N,my(D.id,!1)]:N,[]);q(L),Z(r)},setNodeExtent:(O)=>{let{nodes:z,nodeLookup:q,parentLookup:Z,nodeOrigin:V,elevateNodesOnSelect:L,nodeExtent:r,zIndexMode:N}=E();if(O[0][0]===r[0][0]&&O[0][1]===r[0][1]&&O[1][0]===r[1][0]&&O[1][1]===r[1][1])return;T5(z,q,Z,{nodeOrigin:V,nodeExtent:O,elevateNodesOnSelect:L,checkEquality:!1,zIndexMode:N}),K({nodeExtent:O})},panBy:(O)=>{let{transform:z,width:q,height:Z,panZoom:V,translateExtent:L}=E();return SK({delta:O,panZoom:V,transform:z,translateExtent:L,width:q,height:Z})},setCenter:async(O,z,q)=>{let{width:Z,height:V,maxZoom:L,panZoom:r}=E();if(!r)return Promise.resolve(!1);let N=typeof q?.zoom<"u"?q.zoom:L;return await r.setViewport({x:Z/2-O*N,y:V/2-z*N,zoom:N},{duration:q?.duration,ease:q?.ease,interpolate:q?.interpolate}),Promise.resolve(!0)},cancelConnection:()=>{K({connection:{...$F}})},updateConnection:(O)=>{K({connection:O})},reset:()=>K({...wZ()})}},Object.is);function lT({initialNodes:f,initialEdges:u,defaultNodes:_,defaultEdges:y,initialWidth:l,initialHeight:$,initialMinZoom:j,initialMaxZoom:J,initialFitViewOptions:F,fitView:A,nodeOrigin:U,nodeExtent:G,zIndexMode:W,children:K}){let[E]=a.useState(()=>yT({nodes:f,edges:u,defaultNodes:_,defaultEdges:y,width:l,height:$,fitView:A,minZoom:j,maxZoom:J,fitViewOptions:F,nodeOrigin:U,nodeExtent:G,zIndexMode:W}));return o.jsx(XD,{value:E,children:o.jsx(gD,{children:K})})}function $T({children:f,nodes:u,edges:_,defaultNodes:y,defaultEdges:l,width:$,height:j,fitView:J,fitViewOptions:F,minZoom:A,maxZoom:U,nodeOrigin:G,nodeExtent:W,zIndexMode:K}){if(a.useContext(v5))return o.jsx(o.Fragment,{children:f});return o.jsx(lT,{initialNodes:u,initialEdges:_,defaultNodes:y,defaultEdges:l,initialWidth:$,initialHeight:j,fitView:J,initialFitViewOptions:F,initialMinZoom:A,initialMaxZoom:U,nodeOrigin:G,nodeExtent:W,zIndexMode:K,children:f})}var jT={width:"100%",height:"100%",overflow:"hidden",position:"relative",zIndex:0};function JT({nodes:f,edges:u,defaultNodes:_,defaultEdges:y,className:l,nodeTypes:$,edgeTypes:j,onNodeClick:J,onEdgeClick:F,onInit:A,onMove:U,onMoveStart:G,onMoveEnd:W,onConnect:K,onConnectStart:E,onConnectEnd:H,onClickConnectStart:O,onClickConnectEnd:z,onNodeMouseEnter:q,onNodeMouseMove:Z,onNodeMouseLeave:V,onNodeContextMenu:L,onNodeDoubleClick:r,onNodeDragStart:N,onNodeDrag:D,onNodeDragStop:x,onNodesDelete:c,onEdgesDelete:v,onDelete:C,onSelectionChange:S,onSelectionDragStart:B,onSelectionDrag:P,onSelectionDragStop:M,onSelectionContextMenu:w,onSelectionStart:Y,onSelectionEnd:R,onBeforeDelete:k,connectionMode:p,connectionLineType:n=C1.Bezier,connectionLineStyle:_f,connectionLineComponent:s,connectionLineContainerStyle:ff,deleteKeyCode:Kf="Backspace",selectionKeyCode:Gf="Shift",selectionOnDrag:jf=!1,selectionMode:Wf=cy.Full,panActivationKeyCode:Of="Space",multiSelectionKeyCode:Zf=J3()?"Meta":"Control",zoomActivationKeyCode:h=J3()?"Meta":"Control",snapToGrid:i,snapGrid:I,onlyRenderVisibleElements:lf=!1,selectNodesOnDrag:$f,nodesDraggable:Af,autoPanOnNodeFocus:Yf,nodesConnectable:xf,nodesFocusable:of,nodeOrigin:F0=PZ,edgesFocusable:y0,edgesReconnectable:T0,elementsSelectable:Qu=!0,defaultViewport:X0=RD,minZoom:v0=0.5,maxZoom:iu=2,translateExtent:K0=_3,preventScrolling:Au=!0,nodeExtent:uf,defaultMarkerColor:vf="#b1b1b7",zoomOnScroll:o0=!0,zoomOnPinch:Bf=!0,panOnScroll:b0=!1,panOnScrollSpeed:i0=0.5,panOnScrollMode:a0=l_.Free,zoomOnDoubleClick:nf=!0,panOnDrag:d0=!0,onPaneClick:Hu,onPaneMouseEnter:oy,onPaneMouseMove:J_,onPaneMouseLeave:ay,onPaneScroll:t,onPaneContextMenu:Hf,paneClickDistance:Df=1,nodeClickDistance:If=0,children:Rf,onReconnect:Q0,onReconnectStart:af,onReconnectEnd:h0,onEdgeContextMenu:e0,onEdgeDoubleClick:S6,onEdgeMouseEnter:dy,onEdgeMouseMove:ey,onEdgeMouseLeave:$y,reconnectRadius:F_=10,onNodesChange:C6,onEdgesChange:W2,noDragClassName:G2="nodrag",noWheelClassName:Q_="nowheel",noPanClassName:jy="nopan",fitView:V3,fitViewOptions:R6,connectOnClick:O3,attributionPosition:X3,proOptions:j1,defaultEdgeOptions:Jy,elevateNodesOnSelect:x6=!0,elevateEdgesOnSelect:z2=!1,disableKeyboardA11y:N3=!1,autoPanOnConnect:L3,autoPanOnNodeDrag:K2,autoPanSpeed:Z2,connectionRadius:I1,isValidConnection:v6,onError:N1,style:fl,id:b6,nodeDragThreshold:q2,connectionDragThreshold:E2,viewport:EQ,onViewportChange:gu,width:h6,height:H2,colorMode:Mu="light",debug:Y3,onScroll:ul,ariaLabelConfig:B3,zIndexMode:I6="basic",...V2},O2){let D3=b6||"1",X2=hD(Mu),N2=a.useCallback((c6)=>{c6.currentTarget.scrollTo({top:0,left:0,behavior:"instant"}),ul?.(c6)},[ul]);return o.jsx("div",{"data-testid":"rf__wrapper",...V2,onScroll:N2,style:{...fl,...jT},ref:O2,className:B0(["react-flow",l,X2]),id:b6,role:"application",children:o.jsxs($T,{nodes:f,edges:u,width:h6,height:H2,fitView:V3,fitViewOptions:R6,minZoom:v0,maxZoom:iu,nodeOrigin:F0,nodeExtent:uf,zIndexMode:I6,children:[o.jsx(bD,{nodes:f,edges:u,defaultNodes:_,defaultEdges:y,onConnect:K,onConnectStart:E,onConnectEnd:H,onClickConnectStart:O,onClickConnectEnd:z,nodesDraggable:Af,autoPanOnNodeFocus:Yf,nodesConnectable:xf,nodesFocusable:of,edgesFocusable:y0,edgesReconnectable:T0,elementsSelectable:Qu,elevateNodesOnSelect:x6,elevateEdgesOnSelect:z2,minZoom:v0,maxZoom:iu,nodeExtent:uf,onNodesChange:C6,onEdgesChange:W2,snapToGrid:i,snapGrid:I,connectionMode:p,translateExtent:K0,connectOnClick:O3,defaultEdgeOptions:Jy,fitView:V3,fitViewOptions:R6,onNodesDelete:c,onEdgesDelete:v,onDelete:C,onNodeDragStart:N,onNodeDrag:D,onNodeDragStop:x,onSelectionDrag:P,onSelectionDragStart:B,onSelectionDragStop:M,onMove:U,onMoveStart:G,onMoveEnd:W,noPanClassName:jy,nodeOrigin:F0,rfId:D3,autoPanOnConnect:L3,autoPanOnNodeDrag:K2,autoPanSpeed:Z2,onError:N1,connectionRadius:I1,isValidConnection:v6,selectNodesOnDrag:$f,nodeDragThreshold:q2,connectionDragThreshold:E2,onBeforeDelete:k,debug:Y3,ariaLabelConfig:B3,zIndexMode:I6}),o.jsx(_T,{onInit:A,onNodeClick:J,onEdgeClick:F,onNodeMouseEnter:q,onNodeMouseMove:Z,onNodeMouseLeave:V,onNodeContextMenu:L,onNodeDoubleClick:r,nodeTypes:$,edgeTypes:j,connectionLineType:n,connectionLineStyle:_f,connectionLineComponent:s,connectionLineContainerStyle:ff,selectionKeyCode:Gf,selectionOnDrag:jf,selectionMode:Wf,deleteKeyCode:Kf,multiSelectionKeyCode:Zf,panActivationKeyCode:Of,zoomActivationKeyCode:h,onlyRenderVisibleElements:lf,defaultViewport:X0,translateExtent:K0,minZoom:v0,maxZoom:iu,preventScrolling:Au,zoomOnScroll:o0,zoomOnPinch:Bf,zoomOnDoubleClick:nf,panOnScroll:b0,panOnScrollSpeed:i0,panOnScrollMode:a0,panOnDrag:d0,onPaneClick:Hu,onPaneMouseEnter:oy,onPaneMouseMove:J_,onPaneMouseLeave:ay,onPaneScroll:t,onPaneContextMenu:Hf,paneClickDistance:Df,nodeClickDistance:If,onSelectionContextMenu:w,onSelectionStart:Y,onSelectionEnd:R,onReconnect:Q0,onReconnectStart:af,onReconnectEnd:h0,onEdgeContextMenu:e0,onEdgeDoubleClick:S6,onEdgeMouseEnter:dy,onEdgeMouseMove:ey,onEdgeMouseLeave:$y,reconnectRadius:F_,defaultMarkerColor:vf,noDragClassName:G2,noWheelClassName:Q_,noPanClassName:jy,rfId:D3,disableKeyboardA11y:N3,nodeExtent:uf,viewport:EQ,onViewportChange:gu}),o.jsx(CD,{onSelectionChange:S}),Rf,o.jsx(TD,{proOptions:j1,position:X3}),o.jsx(wD,{rfId:D3,disableKeyboardA11y:N3})]})})}var lq=CZ(JT);var yh=Iu.error014();function FT({dimensions:f,lineWidth:u,variant:_,className:y}){return o.jsx("path",{strokeWidth:u,d:`M${f[0]/2} 0 V${f[1]} M0 ${f[1]/2} H${f[0]}`,className:B0(["react-flow__background-pattern",_,y])})}function QT({radius:f,className:u}){return o.jsx("circle",{cx:f,cy:f,r:f,className:B0(["react-flow__background-pattern","dots",u])})}var o_;(function(f){f.Lines="lines",f.Dots="dots",f.Cross="cross"})(o_||(o_={}));var AT={[o_.Dots]:1,[o_.Lines]:1,[o_.Cross]:6},UT=(f)=>({transform:f.transform,patternId:`pattern-${f.rfId}`});function $q({id:f,variant:u=o_.Dots,gap:_=20,size:y,lineWidth:l=1,offset:$=0,color:j,bgColor:J,style:F,className:A,patternClassName:U}){let G=a.useRef(null),{transform:W,patternId:K}=kf(UT,W0),E=y||AT[u],H=u===o_.Dots,O=u===o_.Cross,z=Array.isArray(_)?_:[_,_],q=[z[0]*W[2]||1,z[1]*W[2]||1],Z=E*W[2],V=Array.isArray($)?$:[$,$],L=O?[Z,Z]:q,r=[V[0]*W[2]||1+L[0]/2,V[1]*W[2]||1+L[1]/2],N=`${K}${f?f:""}`;return o.jsxs("svg",{className:B0(["react-flow__background",A]),style:{...F,...h5,"--xy-background-color-props":J,"--xy-background-pattern-color-props":j},ref:G,"data-testid":"rf__background",children:[o.jsx("pattern",{id:N,x:W[0]%q[0],y:W[1]%q[1],width:q[0],height:q[1],patternUnits:"userSpaceOnUse",patternTransform:`translate(-${r[0]},-${r[1]})`,children:H?o.jsx(QT,{radius:Z/2,className:U}):o.jsx(FT,{dimensions:L,lineWidth:l,variant:u,className:U})}),o.jsx("rect",{x:"0",y:"0",width:"100%",height:"100%",fill:`url(#${N})`})]})}$q.displayName="Background";var jq=a.memo($q);function WT(){return o.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32",children:o.jsx("path",{d:"M32 18.133H18.133V32h-4.266V18.133H0v-4.266h13.867V0h4.266v13.867H32z"})})}function GT(){return o.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 5",children:o.jsx("path",{d:"M0 0h32v4.2H0z"})})}function zT(){return o.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 30",children:o.jsx("path",{d:"M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z"})})}function KT(){return o.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 25 32",children:o.jsx("path",{d:"M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z"})})}function ZT(){return o.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 25 32",children:o.jsx("path",{d:"M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z"})})}function R5({children:f,className:u,..._}){return o.jsx("button",{type:"button",className:B0(["react-flow__controls-button",u]),..._,children:f})}var qT=(f)=>({isInteractive:f.nodesDraggable||f.nodesConnectable||f.elementsSelectable,minZoomReached:f.transform[2]<=f.minZoom,maxZoomReached:f.transform[2]>=f.maxZoom,ariaLabelConfig:f.ariaLabelConfig});function Jq({style:f,showZoom:u=!0,showFitView:_=!0,showInteractive:y=!0,fitViewOptions:l,onZoomIn:$,onZoomOut:j,onFitView:J,onInteractiveChange:F,className:A,children:U,position:G="bottom-left",orientation:W="vertical","aria-label":K}){let E=z0(),{isInteractive:H,minZoomReached:O,maxZoomReached:z,ariaLabelConfig:q}=kf(qT,W0),{zoomIn:Z,zoomOut:V,fitView:L}=SF(),r=()=>{Z(),$?.()},N=()=>{V(),j?.()},D=()=>{L(l),J?.()},x=()=>{E.setState({nodesDraggable:!H,nodesConnectable:!H,elementsSelectable:!H}),F?.(!H)};return o.jsxs(b5,{className:B0(["react-flow__controls",W==="horizontal"?"horizontal":"vertical",A]),position:G,style:f,"data-testid":"rf__controls","aria-label":K??q["controls.ariaLabel"],children:[u&&o.jsxs(o.Fragment,{children:[o.jsx(R5,{onClick:r,className:"react-flow__controls-zoomin",title:q["controls.zoomIn.ariaLabel"],"aria-label":q["controls.zoomIn.ariaLabel"],disabled:z,children:o.jsx(WT,{})}),o.jsx(R5,{onClick:N,className:"react-flow__controls-zoomout",title:q["controls.zoomOut.ariaLabel"],"aria-label":q["controls.zoomOut.ariaLabel"],disabled:O,children:o.jsx(GT,{})})]}),_&&o.jsx(R5,{className:"react-flow__controls-fitview",onClick:D,title:q["controls.fitView.ariaLabel"],"aria-label":q["controls.fitView.ariaLabel"],children:o.jsx(zT,{})}),y&&o.jsx(R5,{className:"react-flow__controls-interactive",onClick:x,title:q["controls.interactive.ariaLabel"],"aria-label":q["controls.interactive.ariaLabel"],children:H?o.jsx(ZT,{}):o.jsx(KT,{})}),U]})}Jq.displayName="Controls";var Fq=a.memo(Jq);function ET({id:f,x:u,y:_,width:y,height:l,style:$,color:j,strokeColor:J,strokeWidth:F,className:A,borderRadius:U,shapeRendering:G,selected:W,onClick:K}){let{background:E,backgroundColor:H}=$||{},O=j||E||H;return o.jsx("rect",{className:B0(["react-flow__minimap-node",{selected:W},A]),x:u,y:_,rx:U,ry:U,width:y,height:l,style:{fill:O,stroke:J,strokeWidth:F},shapeRendering:G,onClick:K?(z)=>K(z,f):void 0})}var HT=a.memo(ET),VT=(f)=>f.nodes.map((u)=>u.id),rF=(f)=>f instanceof Function?f:()=>f;function OT({nodeStrokeColor:f,nodeColor:u,nodeClassName:_="",nodeBorderRadius:y=5,nodeStrokeWidth:l,nodeComponent:$=HT,onClick:j}){let J=kf(VT,W0),F=rF(u),A=rF(f),U=rF(_),G=typeof window>"u"||!!window.chrome?"crispEdges":"geometricPrecision";return o.jsx(o.Fragment,{children:J.map((W)=>o.jsx(NT,{id:W,nodeColorFunc:F,nodeStrokeColorFunc:A,nodeClassNameFunc:U,nodeBorderRadius:y,nodeStrokeWidth:l,NodeComponent:$,onClick:j,shapeRendering:G},W))})}function XT({id:f,nodeColorFunc:u,nodeStrokeColorFunc:_,nodeClassNameFunc:y,nodeBorderRadius:l,nodeStrokeWidth:$,shapeRendering:j,NodeComponent:J,onClick:F}){let{node:A,x:U,y:G,width:W,height:K}=kf((E)=>{let H=E.nodeLookup.get(f);if(!H)return{node:void 0,x:0,y:0,width:0,height:0};let O=H.internals.userNode,{x:z,y:q}=H.internals.positionAbsolute,{width:Z,height:V}=R1(O);return{node:O,x:z,y:q,width:Z,height:V}},W0);if(!A||A.hidden||!zF(A))return null;return o.jsx(J,{x:U,y:G,width:W,height:K,style:A.style,selected:!!A.selected,className:y(A),color:u(A),borderRadius:l,strokeColor:_(A),strokeWidth:$,shapeRendering:j,onClick:F,id:A.id})}var NT=a.memo(XT),LT=a.memo(OT),YT=200,BT=150,DT=(f)=>!f.hidden,wT=(f)=>{let u={x:-f.transform[0]/f.transform[2],y:-f.transform[1]/f.transform[2],width:f.width/f.transform[2],height:f.height/f.transform[2]};return{viewBB:u,boundingRect:f.nodeLookup.size>0?UF(y3(f.nodeLookup,{filter:DT}),u):u,rfId:f.rfId,panZoom:f.panZoom,translateExtent:f.translateExtent,flowWidth:f.width,flowHeight:f.height,ariaLabelConfig:f.ariaLabelConfig}},TT="react-flow__minimap-desc";function Qq({style:f,className:u,nodeStrokeColor:_,nodeColor:y,nodeClassName:l="",nodeBorderRadius:$=5,nodeStrokeWidth:j,nodeComponent:J,bgColor:F,maskColor:A,maskStrokeColor:U,maskStrokeWidth:G,position:W="bottom-right",onClick:K,onNodeClick:E,pannable:H=!1,zoomable:O=!1,ariaLabel:z,inversePan:q,zoomStep:Z=1,offsetScale:V=5}){let L=z0(),r=a.useRef(null),{boundingRect:N,viewBB:D,rfId:x,panZoom:c,translateExtent:v,flowWidth:C,flowHeight:S,ariaLabelConfig:B}=kf(wT,W0),P=f?.width??YT,M=f?.height??BT,w=N.width/P,Y=N.height/M,R=Math.max(w,Y),k=R*P,p=R*M,n=V*R,_f=N.x-(k-N.width)/2-n,s=N.y-(p-N.height)/2-n,ff=k+n*2,Kf=p+n*2,Gf=`${TT}-${x}`,jf=a.useRef(0),Wf=a.useRef();jf.current=R,a.useEffect(()=>{if(r.current&&c)return Wf.current=IK({domNode:r.current,panZoom:c,getTransform:()=>L.getState().transform,getViewScale:()=>jf.current}),()=>{Wf.current?.destroy()}},[c]),a.useEffect(()=>{Wf.current?.update({translateExtent:v,width:C,height:S,inversePan:q,pannable:H,zoomStep:Z,zoomable:O})},[H,O,q,Z,v,C,S]);let Of=K?(i)=>{let[I,lf]=Wf.current?.pointer(i)||[0,0];K(i,{x:I,y:lf})}:void 0,Zf=E?a.useCallback((i,I)=>{let lf=L.getState().nodeLookup.get(I).internals.userNode;E(i,lf)},[]):void 0,h=z??B["minimap.ariaLabel"];return o.jsx(b5,{position:W,style:{...f,"--xy-minimap-background-color-props":typeof F==="string"?F:void 0,"--xy-minimap-mask-background-color-props":typeof A==="string"?A:void 0,"--xy-minimap-mask-stroke-color-props":typeof U==="string"?U:void 0,"--xy-minimap-mask-stroke-width-props":typeof G==="number"?G*R:void 0,"--xy-minimap-node-background-color-props":typeof y==="string"?y:void 0,"--xy-minimap-node-stroke-color-props":typeof _==="string"?_:void 0,"--xy-minimap-node-stroke-width-props":typeof j==="number"?j:void 0},className:B0(["react-flow__minimap",u]),"data-testid":"rf__minimap",children:o.jsxs("svg",{width:P,height:M,viewBox:`${_f} ${s} ${ff} ${Kf}`,className:"react-flow__minimap-svg",role:"img","aria-labelledby":Gf,ref:r,onClick:Of,children:[h&&o.jsx("title",{id:Gf,children:h}),o.jsx(LT,{onClick:Zf,nodeColor:y,nodeStrokeColor:_,nodeBorderRadius:$,nodeClassName:l,nodeStrokeWidth:j,nodeComponent:J}),o.jsx("path",{className:"react-flow__minimap-mask",d:`M${_f-n},${s-n}h${ff+n*2}v${Kf+n*2}h${-ff-n*2}z - M${D.x},${D.y}h${D.width}v${D.height}h${-D.width}z`,fillRule:"evenodd",pointerEvents:"none"})]})})}Qq.displayName="MiniMap";var lh=a.memo(Qq),rT=(f)=>(u)=>f?`${Math.max(1/u.transform[2],1)}`:void 0,MT={[s_.Line]:"right",[s_.Handle]:"bottom-right"};function PT({nodeId:f,position:u,variant:_=s_.Handle,className:y,style:l=void 0,children:$,color:j,minWidth:J=10,minHeight:F=10,maxWidth:A=Number.MAX_VALUE,maxHeight:U=Number.MAX_VALUE,keepAspectRatio:G=!1,resizeDirection:W,autoScale:K=!0,shouldResize:E,onResizeStart:H,onResize:O,onResizeEnd:z}){let q=bZ(),Z=typeof f==="string"?f:q,V=z0(),L=a.useRef(null),r=_===s_.Handle,N=kf(a.useCallback(rT(r&&K),[r,K]),W0),D=a.useRef(null),x=u??MT[_];a.useEffect(()=>{if(!L.current||!Z)return;if(!D.current)D.current=iK({domNode:L.current,nodeId:Z,getStoreItems:()=>{let{nodeLookup:v,transform:C,snapGrid:S,snapToGrid:B,nodeOrigin:P,domNode:M}=V.getState();return{nodeLookup:v,transform:C,snapGrid:S,snapToGrid:B,nodeOrigin:P,paneDomNode:M}},onChange:(v,C)=>{let{triggerNodeChanges:S,nodeLookup:B,parentLookup:P,nodeOrigin:M}=V.getState(),w=[],Y={x:v.x,y:v.y},R=B.get(Z);if(R&&R.expandParent&&R.parentId){let k=R.origin??M,p=v.width??R.measured.width??0,n=v.height??R.measured.height??0,_f={id:R.id,parentId:R.parentId,rect:{width:p,height:n,...KF({x:v.x??R.position.x,y:v.y??R.position.y},{width:p,height:n},R.parentId,B,k)}},s=r5([_f],B,P,M);w.push(...s),Y.x=v.x?Math.max(k[0]*p,v.x):void 0,Y.y=v.y?Math.max(k[1]*n,v.y):void 0}if(Y.x!==void 0&&Y.y!==void 0){let k={id:Z,type:"position",position:{...Y}};w.push(k)}if(v.width!==void 0&&v.height!==void 0){let p={id:Z,type:"dimensions",resizing:!0,setAttributes:!W?!0:W==="horizontal"?"width":"height",dimensions:{width:v.width,height:v.height}};w.push(p)}for(let k of C){let p={...k,type:"position"};w.push(p)}S(w)},onEnd:({width:v,height:C})=>{let S={id:Z,type:"dimensions",resizing:!1,dimensions:{width:v,height:C}};V.getState().triggerNodeChanges([S])}});return D.current.update({controlPosition:x,boundaries:{minWidth:J,minHeight:F,maxWidth:A,maxHeight:U},keepAspectRatio:G,resizeDirection:W,onResizeStart:H,onResize:O,onResizeEnd:z,shouldResize:E}),()=>{D.current?.destroy()}},[x,J,F,A,U,G,H,O,z,E]);let c=x.split("-");return o.jsx("div",{className:B0(["react-flow__resize-control","nodrag",...c,_,y]),ref:L,style:{...l,scale:N,...j&&{[r?"backgroundColor":"borderColor"]:j}},children:$})}var $h=a.memo(PT);var X=fy.default.createElement,{useEffect:b1}=fy.default,wu=fy.default.useState,e_=fy.default.useRef,N6=[{id:"in-left",side:"left",position:Uf.Left,style:{top:"50%"}},{id:"in-top-left",side:"top",slot:"left",slotIndex:-1,position:Uf.Top,style:{left:"28%"}},{id:"in-top-mid",side:"top",slot:"mid",slotIndex:0,position:Uf.Top,style:{left:"50%"}},{id:"in-top-right",side:"top",slot:"right",slotIndex:1,position:Uf.Top,style:{left:"72%"}},{id:"in-bottom-left",side:"bottom",slot:"left",slotIndex:-1,position:Uf.Bottom,style:{left:"28%"}},{id:"in-bottom-mid",side:"bottom",slot:"mid",slotIndex:0,position:Uf.Bottom,style:{left:"50%"}},{id:"in-bottom-right",side:"bottom",slot:"right",slotIndex:1,position:Uf.Bottom,style:{left:"72%"}}],H6=[{id:"out-right",position:Uf.Right,style:{top:"50%"}}],Aq=["#4eb7a8","#d7a13a","#69aee8","#e0835f","#b7d86b","#d98bd2","#5fc6bf"],U3=236,W3=88,Uq=15000,ST=10,RF=96,x1=72,xF=64,Wq=12;function I5(){return typeof document>"u"||document.visibilityState!=="hidden"}function Gq(f,u){let _=Number.parseFloat(String(f||""));return Number.isFinite(_)?_/100:u}function CT(f,u,_){let y=String(f.side||"");if(y!=="top"&&y!=="bottom")return 0;let l=Number(f.slotIndex||0),$=y==="top"?"in-top-mid":"in-bottom-mid",j=u.get(f.id)||0,J=u.get($)||0;if(l===0)return J===0?-26:28+j*74;let F=_===0?Math.abs(l)*2:Math.sign(_)===Math.sign(l)?-3:3;if(J>0&&j===0)return-14+F;return 8+j*74+F}function c5(f){let u=f.filter(($,j)=>{let J=f[j-1];return!J||Math.abs(J.x-$.x)>0.5||Math.abs(J.y-$.y)>0.5});if(u.length<2)return"";let _=`M ${u[0].x},${u[0].y}`,y=u[0];for(let $=1;$0.5||Math.abs(W.y-y.y)>0.5)_+=` L ${W.x},${W.y}`;_+=` Q ${J.x},${J.y} ${K.x},${K.y}`,y=K}let l=u[u.length-1];return`${_} L ${l.x},${l.y}`}function Cq(f,u,_,y,l,$,j=""){let J=_>=f,F=Math.max(1,Math.abs(_-f)),A=Math.abs(y-u),U=Math.max(34,Math.min(118,F*0.26)),G=Math.min(280,Math.abs($));if(J&&l===Uf.Left&&G<4&&A<28&&F<420)return`M ${f},${u} C ${f+U},${u} ${_-U},${y} ${_},${y}`;if(J&&l===Uf.Left&&(j==="direct-forward-left"||F<=260&&A<=210)){let z=Math.max(42,Math.min(140,F*0.48)),q=Math.max(-28,Math.min(28,$*0.18));return`M ${f},${u} C ${f+z},${u+q} ${_-z},${y} ${_},${y}`}if(J){let z=f+U;if(l===Uf.Top||l===Uf.Bottom){let V=l===Uf.Top?-1:1,L=y+V*(54+G*0.42);return c5([{x:f,y:u},{x:z,y:u},{x:z+Math.min(120,F*0.18),y:L},{x:_,y:L},{x:_,y:y+V*34},{x:_,y}])}let q=_-U,Z=(u+y)/2+$;return c5([{x:f,y:u},{x:z,y:u},{x:z+Math.min(110,F*0.16),y:Z},{x:q-Math.min(90,F*0.12),y:Z},{x:q,y},{x:_,y}])}let E=l===Uf.Bottom?1:l===Uf.Top?-1:$>=0?1:-1,H=Math.max(f,_)+92+Math.min(180,G*0.52),O=E<0?Math.min(u,y)-84-G*0.62:Math.max(u,y)+84+G*0.62;if(l===Uf.Top||l===Uf.Bottom)return c5([{x:f,y:u},{x:f+U,y:u},{x:H,y:O},{x:_,y:O},{x:_,y:y+E*38},{x:_,y}]);return c5([{x:f,y:u},{x:f+U,y:u},{x:H,y:O},{x:_-U,y:O},{x:_-U,y},{x:_,y}])}function RT({data:f}){return X("div",{className:"pipeline-flow-node-body"},N6.map((u)=>X(iy,{key:u.id,id:u.id,type:"target",position:u.position,isConnectable:!1,className:`pipeline-flow-handle input ${u.side} slot-${u.slot||"mid"}`,style:u.style})),H6.map((u)=>X(iy,{key:u.id,id:u.id,type:"source",position:u.position,isConnectable:!1,className:"pipeline-flow-handle output right",style:u.style})),f?.label)}function xT({id:f,sourceX:u,sourceY:_,targetX:y,targetY:l,targetPosition:$,markerEnd:j,markerStart:J,style:F,data:A}){let U=Number(A?.laneOffset||0),G=Cq(u,_,y,l,$,U,String(A?.routeMode||""));return X(A3,{id:f,path:G,markerEnd:j,markerStart:J,style:F,interactionWidth:28})}var vT={pipelineCurve:xT},bT={pipelineNode:RT};function i5(f){if(!f)return"--";let u=new Date(f);if(Number.isNaN(u.getTime()))return"--";return L0(u)}function l1(f){let u=Number(f);if(!Number.isFinite(u)||u<0)return"--";let _=Math.round(u/1000);if(_<60)return`${_}s`;if(_<3600)return`${Math.floor(_/60)}m ${_%60}s`;return`${Math.floor(_/3600)}h ${Math.floor(_%3600/60)}m`}function vF(f){let u=Number(f);if(!Number.isFinite(u))return"--";return u.toLocaleString("zh-CN")}function zq(f){let u=Number(f);if(!Number.isFinite(u))return"--";return`${Math.round(Math.max(0,Math.min(1,u))*100)}%`}function Xf(f){return typeof f==="object"&&f!==null&&!Array.isArray(f)}function Vf(f){return Array.isArray(f)?f:[]}function Cf(f){if(!f)return null;let u=new Date(f);return Number.isNaN(u.getTime())?null:u.getTime()}function L6(f){return Number.isFinite(Number(f))?new Date(Number(f)).toISOString():""}function D6(...f){for(let u of f){let _=Cf(u);if(_!==null)return new Date(_).toISOString()}return""}function aF(...f){let u=f.map(Cf).filter((_)=>_!==null);return u.length>0?new Date(Math.max(...u)).toISOString():""}function dF(f){return["succeeded","failed","skipped","cancelled","canceled","completed"].includes(String(f||"").toLowerCase())}function Rq(f){let u=bq(f).toLowerCase();return["running","active","in-progress","in_progress"].includes(u)}function Kq(f,u="status"){return f.reduce((_,y)=>{let l=String(y?.[u]||"unknown").toLowerCase();return _[l]=(_[l]||0)+1,_},{})}function xq(f){if(!f||typeof f!=="string")return null;try{let u=JSON.parse(f);return Xf(u)?u:null}catch{return null}}function bF(f){let u=f.map(xq).filter(($)=>Boolean($)),_=u.flatMap(($)=>[$.timestamp,$.createdAt,$.updatedAt]).filter(Boolean),y=aF(..._),l=Array.from(new Set(u.map(($)=>String($.event||$.action||$.type||"")).filter(Boolean))).slice(0,3);return{total:f.length,parsed:u.length,lastAt:y,eventKinds:l}}function g5(f){if(f===null||f===void 0)return"--";if(typeof f==="boolean")return f?"是":"否";if(typeof f==="number")return String(f);if(typeof f==="string")return f.length>80?`${f.slice(0,77)}...`:f;if(Array.isArray(f))return`${f.length} 项`;if(typeof f==="object")return`${Object.keys(f).length} 字段`;return String(f)}function vq(f,u=280){if(f===null||f===void 0)return"";let y=(typeof f==="string"?f:String(f)).replace(/\r\n/gu,` -`).trim();return y.length>u?`${y.slice(0,Math.max(0,u-1))}...`:y}function bq(f){if(typeof f==="string")return f;if(Xf(f))return String(f.status||f.state||f.phase||"unknown");return"unknown"}function hT(f){return f.filter((u)=>u&&u.value!==void 0&&u.value!==null&&String(u.value)!=="")}function mF({items:f}){let u=hT(Vf(f));return X("div",{className:"pipeline-kv-grid"},u.map((_)=>X("span",{key:_.label},X("b",null,_.label),X("span",null,_.value))))}function eF({items:f}){let u=Vf(f).map((_)=>String(_||"")).filter(Boolean);if(u.length===0)return null;return X("div",{className:"pipeline-chip-row"},u.map((_,y)=>X("span",{key:`${y}-${_}`},_)))}function iF(f,u){let _=String(u?.procedureRunId||""),y=Vf(f?.procedureRuns);return y.find((l)=>String($1(l))===_)||y.at(-1)||null}function IT(f,u){let _=String(u||"");if(!_)return null;return Vf(f?.procedureRuns).find((y)=>$1(y)===_)||null}function hF(f){return Vf(f?.attempts).length}function Zq(f){return Vf(f?.attempts).reduce((u,_)=>u+e5(_).length,0)}function e5(f){return Vf(f?.opencodeMessages?.steps).filter(Xf)}function hq(f){let u=String(f?.status||"").toLowerCase();if(["error","failed","failure"].includes(u))return"failed";if(["completed","succeeded","success"].includes(u))return"succeeded";if(["running","started","in_progress"].includes(u))return"running";return"unknown"}function cT(f,u){let _=gF(f.map(($)=>$?.agent)).slice(0,3),y=gF(f.map(($)=>$?.model)).slice(0,3),l=u.length<=2?u.map(($)=>`session ${$}`):[`sessions ${u.length}`,...u.slice(0,2).map(($)=>`session ${$}`)];return[..._.map(($)=>`agent ${$}`),...y.map(($)=>`model ${$}`),...l]}function V6(f,u=0){return String(f?.messageId||f?.index||"")||`step-${u}`}function pT({steps:f,sessionIds:u,sessionFacts:_,matchedStepKey:y}){let l=Vf(f),$=l.findIndex((O,z)=>V6(O,z)===y),j=$>=0?l[$]:null,J=l.flatMap((O)=>[Cf(O?.createdAt),Cf(O?.completedAt)]).filter((O)=>O!==null),F=J.length>0?Math.min(...J):null,A=J.length>0?Math.max(...J):null,U=F!==null&&A!==null?Math.max(0,A-F):null,G=l.reduce((O,z)=>O+Vf(z?.parts).filter((q)=>String(q?.type||"").toLowerCase()==="tool").length,0),W=l.reduce((O,z)=>O+Vf(z?.parts).filter((q)=>["text","reasoning"].includes(String(q?.type||"").toLowerCase())).length,0),K=l.reduce((O,z)=>O+Vf(z?.parts).filter((q)=>String(q?.type||"").toLowerCase()==="tool"&&hq(q)==="failed").length,0),E=[`${l.length} steps`,`${u.length} sessions`,`${W} messages`,`${G} tools`,U!==null?`duration ${l1(U)}`:"",K>0?`${K} failed tools`:""].filter(Boolean),H=j?[`Step ${j?.index??$+1}`,String(j?.role||"role --"),j?.model?`model ${j.model}`:"",j?.finish?`finish ${j.finish}`:"",j?.durationMs!==void 0&&j?.durationMs!==null?`duration ${l1(j.durationMs)}`:""].filter(Boolean):[];return X("section",{className:"pipeline-trace-timeline","data-testid":"pipeline-step-timeline"},X("div",{className:"pipeline-trace-head"},X("div",null,X("b",null,"OpenCode Trace"),X("span",null,"Trace 使用 Codex Queue 统一样式展示完整 agent loop;Pipeline 旧 step/message/tool 卡片样式已废弃。")),X("div",{className:"pipeline-trace-session-head","data-testid":"pipeline-step-timeline-session"},X("span",null,E.join(" / ")||"Trace"),_.length>0?X(eF,{items:_}):null)),j?X("div",{className:"pipeline-trace-focus","data-testid":"pipeline-trace-matched-step"},X("span",{className:"codex-output-channel"},"Matched"),X("strong",null,`Gantt selection -> ${H.join(" / ")}`),X("time",null,`${i5(j?.createdAt)} -> ${i5(j?.completedAt)}`)):null,X(E4,{port:rG,input:l,className:"codex-transcript pipeline-trace",testId:"pipeline-opencode-step-trace",emptyText:"暂无 OpenCode Trace 输出",keepRecentToolCalls:3}))}function O6(f){return Vf(f).flatMap((u)=>{if(Xf(u))return[u];let _=xq(u);return _?[_]:[]})}function V1(f){return String(f?.event||f?.action||f?.requestedAction||f?.type||"").toLowerCase()}function gy(f){return D6(f?.timestamp,f?.createdAt,f?.updatedAt,f?.startedAt,f?.finishedAt)}function kT(f){return Cf(gy(f))}function f2(f){return String(f?.attempt||f?.id||"")}function gF(f){let u=new Set,_=[];for(let y of f){let l=String(y||"");if(!l||u.has(l))continue;u.add(l),_.push(l)}return _}function qq(f){switch(String(f||"").toLowerCase()){case"monitor":return"monitor";case"webui":return"webui";case"cli":return"cli";case"system":return"runner";default:return String(f||"--")}}function ny(f){return String(f?.requestedAction||f?.action||"").toLowerCase()}function X6(f){switch(ny(f)){case"guide":return"引导";case"modify":return"修改";case"approve":return"审核通过";case"restart":return"重启";case"redo":return"重做";default:return String(f?.requestedAction||f?.action||"控制")}}function Eq(f){switch(V1(f)){case"initial-prompt-delivered":return"初始 prompt";case"append-prompt-delivered":return"追加 prompt";case"append-prompt-queued":return"追加 prompt 已排队";case"monitor-prompt-delivered":return"Monitor prompt";case"node-long-running-observation":return"长任务观察";case"node-finished":return"节点完成";case"oa-policy-downstream-evaluated":return"OA 下游策略";case"control-command-queued":return`${X6(f)} 已发起`;case"control-command-applied":return`${X6(f)} 已生效`;case"control-command-ignored":return`${X6(f)} 已忽略`;default:return String(f?.event||f?.action||f?.requestedAction||"event")}}function Hq(f){return vq(f?.promptPreview||f?.reasonPreview||f?.prompt||f?.reason||"",240)}function mT(f){let u=String(f?.prompt||""),_=String(f?.reason||f?.restartReason||""),y=u?"":String(f?.promptPreview||""),l=_?"":String(f?.reasonPreview||"");return[u||y?{label:u?"prompt":"prompt preview",value:u||y}:null,_||l?{label:_?"reason":"reason preview",value:_||l}:null,Vf(f?.resetNodeIds).length>0?{label:"reset nodes",value:Vf(f.resetNodeIds).join(", ")}:null,Vf(f?.runningResetNodeIds).length>0?{label:"interrupted running nodes",value:Vf(f.runningResetNodeIds).join(", ")}:null,Vf(f?.interruptedProcedureRunIds).length>0?{label:"interrupted procedures",value:Vf(f.interruptedProcedureRunIds).join(", ")}:null,f?.interruptedProcedureRunId?{label:"interrupted procedure",value:String(f.interruptedProcedureRunId)}:null].filter(Boolean)}function IF(f){let u=e5(f),_=u.map((F)=>Cf(F?.createdAt)).filter((F)=>F!==null),y=u.map((F)=>Cf(F?.completedAt)??Cf(F?.createdAt)).filter((F)=>F!==null),l=O6(f?.controlEventRecords).map((F)=>kT(F)).filter((F)=>F!==null),$=Vf(f?.assistantOutputs).map((F)=>Cf(F?.updatedAt)).filter((F)=>F!==null),j=_[0]??l[0]??$[0]??null,J=y.at(-1)??l.at(-1)??$.at(-1)??j;return{startMs:j,endMs:J}}function iT(f,u,_,y,l=""){let $=Vf(f?.procedureRuns).filter((J)=>u2(J,u)===_);if($.length===0)return null;if(l){let J=$.find((F)=>$1(F)===l);if(J)return J}if(y===null)return $.at(-1)||null;let j=$.find((J)=>{let F=Cf(p5(J,f)),A=Cf(k5(J,f))??F;return F!==null&&A!==null&&y>=F-1000&&y<=A+1000});if(j)return j;return $.slice().sort((J,F)=>{let A=Cf(p5(J,f))??y,U=Cf(k5(J,f))??A,G=Cf(p5(F,f))??y,W=Cf(k5(F,f))??G,K=Math.min(Math.abs(A-y),Math.abs(U-y)),E=Math.min(Math.abs(G-y),Math.abs(W-y));return K-E})[0]||null}function Iq(f,u){let _=Vf(f?.attempts).filter(Xf);if(_.length===0)return null;let y=String(u?.attempt||"");if(y){let j=_.find((J)=>f2(J)===y);if(j)return j}let l=Number.isFinite(Number(u?.ms))?Number(u.ms):null;if(l===null)return _.at(-1)||null;let $=_.find((j)=>{let J=IF(j);return Number.isFinite(J.startMs)&&Number.isFinite(J.endMs)&&l>=Number(J.startMs)-1000&&l<=Number(J.endMs)+1000});if($)return $;return _.slice().sort((j,J)=>{let F=IF(j),A=IF(J),U=Math.min(Math.abs(Number(F.startMs??l)-l),Math.abs(Number(F.endMs??l)-l)),G=Math.min(Math.abs(Number(A.startMs??l)-l),Math.abs(Number(A.endMs??l)-l));return U-G})[0]||_.at(-1)||null}function cq(f,u){let _=e5(f);if(_.length===0)return{step:null,stepIndex:-1,stepKey:""};if(u===null){let $=_[0];return{step:$,stepIndex:0,stepKey:V6($,0)}}for(let $=0;$<_.length;$+=1){let j=_[$],J=Cf(j?.createdAt)??Cf(j?.completedAt),F=Cf(j?.completedAt)??J;if(J!==null&&F!==null&&u>=J-1000&&u<=F+1000)return{step:j,stepIndex:$,stepKey:V6(j,$)}}let y=_.findIndex(($)=>{let j=Cf($?.createdAt)??Cf($?.completedAt);return j!==null&&j>=u});if(y>=0){let $=_[y];return{step:$,stepIndex:y,stepKey:V6($,y)}}let l=Math.max(0,_.length-1);return{step:_[l],stepIndex:l,stepKey:V6(_[l],l)}}function gT(f,u){let _=String(u?.runId||f?.runId||"");if(String(u?.mode||"")==="interval"){let A=u?.interval||{},U=iF(f,A)||A.raw||{};return{mode:"interval",runId:_,interval:A,marker:null,nodeId:String(A?.nodeId||u2(U,_)||""),procedure:U,attempt:null,matchedStep:null,matchedStepIndex:-1,matchedStepKey:""}}let y=Xf(u?.marker)?u.marker:{},l=Number.isFinite(Number(y?.ms))?Number(y.ms):null,$=String(y?.nodeId||""),j=$?iT(f,_,$,l,String(y?.procedureRunId||"")):null,J=j?Iq(j,y):null,F=J?cq(J,l):{step:null,stepIndex:-1,stepKey:""};return{mode:"event",runId:_,interval:null,marker:y,nodeId:$,procedure:j,attempt:J,matchedStep:F.step,matchedStepIndex:F.stepIndex,matchedStepKey:F.stepKey}}function nT({procedure:f,matchedStepKey:u="",matchedAttemptId:_=""}){let y=Vf(f?.attempts);if(y.length===0)return X(pu,{title:"暂无 attempt 详情",text:"当前 procedure 还没有可展示的 attempt / OpenCode Trace;若刚点击甘特线,请等待 node 详情抓取完成。"});return y.map((l,$)=>{let j=l?.opencodeMessages||{},J=e5(l),F=Vf(j.sessionIds).map((W)=>String(W)).filter(Boolean),A=cT(J,F),U=f2(l)||`attempt-${$+1}`,G=J.reduce((W,K)=>W+Vf(K?.parts).filter((E)=>String(E?.type||"").toLowerCase()==="tool"&&hq(E)==="failed").length,0);return X("article",{key:U,className:`pipeline-attempt-card ${_===U?"matched":""}`},X("div",{className:"pipeline-attempt-head"},X("div",null,X("strong",null,U),X("span",null,j.source||"opencode")),X("div",{className:"pipeline-attempt-badges"},X("span",null,`${J.length} steps`),X("span",null,`${j.toolCallCount??"--"} tools`),G>0?X("span",{className:"danger"},`${G} failed`):null)),X(mF,{items:[{label:"messages",value:j.messageCount??"--"},{label:"steps",value:j.stepCount??J.length},{label:"tools",value:j.toolCallCount??"--"},{label:"updated",value:zf(j.updatedAt)},{label:"sessions",value:F.join(", ")||"--"}]}),J.length===0?X("p",{className:"muted paragraph"},"当前 attempt 尚未返回 OpenCode Trace;请确认 D601 pipeline-control 已重建并重新抓取。"):X(pT,{steps:J,sessionIds:F,sessionFacts:A,matchedStepKey:u}))})}function cF(f,u){return`${f}::${u}`}function n5(f,u,_){if(!Xf(f))return null;return String(f.runId||"")===u&&String(f.nodeId||"")===_?f:null}function tT(f,u){let _=Xf(f)?f:{};if(!Xf(u))return _;let y=Vf(u.attempts),l=Vf(_.attempts);return{..._,...u,attempts:y.length>0?y:l}}function sT(f,u,_,y){if(!n5(u,_,y))return f;let l=Vf(u.procedureRuns),$=Xf(f)?f:{};return{...$,...u,controlCommands:Vf(u.controlCommands).length>0?u.controlCommands:$.controlCommands,controlEvents:Vf(u.controlEvents).length>0?u.controlEvents:$.controlEvents,procedureRuns:l.length>0?l:$.procedureRuns}}function oT({selection:f,runDetails:u,nodeDetails:_,nodeDetailsState:y,onRaw:l,onCollapse:$}){if(!f?.mode)return X("aside",{className:"pipeline-gantt-detail-panel empty","data-testid":"pipeline-gantt-detail-panel"},X("div",{className:"pipeline-gantt-detail-head"},X("div",null,X("span",{className:"panel-eyebrow"},"Gantt Detail"),X("h3",null,"未选择元素")),X("button",{type:"button",className:"ghost-btn mini",onClick:$,"data-testid":"pipeline-gantt-sidebar-collapse"},"收起")),X(pu,{title:"选择一条执行线或一个控制点",text:"点击甘特图中的 node 执行线、prompt 点或控制点,在这里查看结构化过程和 OpenCode step。"}));let j=String(f?.runId||""),J=String(f?.interval?.nodeId||f?.marker?.nodeId||""),F=u?.runId===j?u.details:null,A=n5(_,j,J),U=String(y?.runId||"")===j&&String(y?.nodeId||"")===J,G=sT(F,A,j,J),W=(String(u?.runId||"")!==j||Boolean(u?.loading))&&!G,K=String(u?.runId||"")===j?String(u?.error||""):"",E=U?String(y?.error||""):"",H=G?gT(G,f):null,O=H?.interval||f?.interval||null,z=H?.marker||f?.marker||null,q=String(O?.procedureRunId||z?.procedureRunId||""),Z=A?IT(A,q)||iF(A,O||{procedureRunId:q}):null,V=H?.procedure||(G?iF(G,O||{procedureRunId:q}):null)||O?.raw||{};if(Z&&(hF(V)===0||Zq(Z)>=Zq(V)))V=tT(V,Z);let L=H?.attempt||null,r=String(H?.matchedStepKey||"");if(!L&&z&&hF(V)>0)L=Iq(V,z),r=String(cq(L,Number.isFinite(Number(z?.ms))?Number(z.ms):null).stepKey||"");let N=f2(L),D=hF(V)>0,x=U&&Boolean(y?.loading)&&!D,c=Boolean(W||x),v=[D?"":K,E].filter(Boolean).join(" / "),C=U&&y?.fetchedAt?y.fetchedAt:u?.fetchedAt,S=bq(V?.status||O?.status||z?.status||z?.event),B=f?.mode==="event"?z?.label||Eq(z?.raw||z)||"event":H?.nodeId||O?.nodeId||"node",P=z?mT(z?.raw||z):[],M=z?[V1(z?.raw||z)?`event ${V1(z?.raw||z)}`:"",z?.promptEvent?`prompt ${z.promptEvent}`:"",z?.action?`action ${z.action}`:"",z?.sourceKind?`source ${qq(z.sourceKind)}`:"",z?.sourceNodeId?`from ${z.sourceNodeId}`:"",z?.targetNodeId?`to ${z.targetNodeId}`:"",z?.snapReason?`draw ${z.snapReason}`:""].filter(Boolean):[];return X("aside",{className:"pipeline-gantt-detail-panel","data-testid":"pipeline-gantt-detail-panel"},X("div",{className:"pipeline-gantt-detail-head"},X("div",null,X("span",{className:"panel-eyebrow"},f?.mode==="event"?"Gantt Event Detail":"Gantt Line Detail"),X("h3",null,B)),X("div",{className:"pipeline-gantt-detail-head-actions"},X(uy,{status:S},S),X("button",{type:"button",className:"ghost-btn mini",onClick:$,"data-testid":"pipeline-gantt-sidebar-collapse"},"收起"))),z?X("article",{className:"pipeline-event-card"},X("div",{className:"pipeline-event-card-head"},X("strong",null,z?.label||Eq(z?.raw||z)),X(eF,{items:M})),X(mF,{items:[{label:"event time",value:zf(z?.timestampIso||z?.timestamp||"--")},z?.snapped?{label:"drawn time",value:zf(z?.renderedTimestampIso||z?.ms)}:null,{label:"node",value:z?.nodeId||"--"},{label:"procedure",value:z?.procedureRunId||$1(V)||"--"},{label:"attempt",value:z?.attempt||N||"--"},{label:"source kind",value:z?.sourceKind?qq(z.sourceKind):"--"},{label:"source node",value:z?.sourceNodeId||"--"},{label:"target node",value:z?.targetNodeId||"--"},{label:"command",value:z?.commandId||z?.eventId||"--"},z?.snapReason?{label:"placement",value:z.snapReason}:null]}),P.length>0?X("div",{className:"pipeline-event-blocks"},P.map((w,Y)=>X("section",{key:`${w.label}-${Y}`,className:"pipeline-event-text-block"},X("b",null,w.label),X("p",null,w.value)))):null,Hq(z?.raw||z)?X("p",{className:"pipeline-text-preview"},Hq(z?.raw||z)):null):null,X(mF,{items:[{label:"epoch",value:j||O?.runId||"--"},{label:"node",value:H?.nodeId||O?.nodeId||z?.nodeId||"--"},{label:"procedure",value:O?.procedureRunId||z?.procedureRunId||$1(V)||"--"},{label:"started",value:zf(O?.startedAt||V?.startedAt)},{label:"finished",value:zf(O?.finishedAt||V?.finishedAt)},{label:"duration",value:l1(O?.durationMs||V?.durationMs)},{label:"fetched",value:C?L0(C):"--"},H?.matchedStep?{label:"matched step",value:`Step ${H.matchedStep.index??H.matchedStepIndex+1}`}:null]}),c?X("div",{className:"form-success"},x?"正在抓取该 node 的 attempt / Trace...":"正在抓取 epoch 执行过程..."):null,X(H0,{error:v}),X("div",{className:"pipeline-gantt-detail-actions"},X(O1,{title:`Procedure ${O?.procedureRunId||z?.procedureRunId||H?.nodeId||"node"}`,data:V,onOpen:l,testId:"raw-pipeline-gantt-procedure"}),z?X(O1,{title:`Pipeline event ${z?.id||z?.commandId||z?.eventId||H?.nodeId||"event"}`,data:z?.raw||z,onOpen:l,testId:"raw-pipeline-gantt-event"}):null,G?X(O1,{title:`Pipeline run ${j||"--"}`,data:G,onOpen:l,testId:"raw-pipeline-gantt-node-details"}):null),!c&&!$1(V)&&!z?X(pu,{title:"暂无过程详情",text:"当前选择还没有可匹配的 procedure 运行记录。"}):null,!c&&$1(V)?X(nT,{procedure:V,matchedStepKey:r,matchedAttemptId:N}):null)}function aT({value:f}){let _=String(f||"--").split(/([_-])/u);return X(fy.default.Fragment,null,_.map((y,l)=>y==="-"||y==="_"?X(fy.default.Fragment,{key:l},y,X("wbr",null)):X(fy.default.Fragment,{key:l},y)))}async function a_(f,u={}){return wf(f,{invalidJsonPrefix:"Pipeline 返回了无效 JSON",...u})}function uy({status:f,children:u}){let _=String(f||"unknown").toLowerCase();return X("span",{className:`status-badge ${_}`},u||f||"unknown")}function Eu({label:f,value:u,hint:_,tone:y}){return X("article",{className:`metric-card ${y||""}`},X("div",{className:"metric-label"},f),X("div",{className:"metric-value"},u),X("div",{className:"metric-hint"},_))}function v1({title:f,eyebrow:u,actions:_,children:y,className:l}){return X("section",{className:`panel ${l||""}`},X("div",{className:"panel-head"},X("div",null,u?X("p",{className:"panel-eyebrow"},u):null,X("h2",null,f)),_?X("div",{className:"panel-actions"},_):null),X("div",{className:"panel-body"},y))}function O1({title:f,data:u,onOpen:_,testId:y}){return X("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:()=>_(f,u)},"查看原始JSON")}function y1({title:f,subtitle:u,facts:_,data:y,onRaw:l,testId:$}){let j=Vf(_).map((J)=>String(J||"")).filter(Boolean);return X("article",{className:"pipeline-evidence-row"},X("div",{className:"pipeline-evidence-main"},X("strong",null,f),u?X("span",null,u):null),X("div",{className:"pipeline-evidence-facts"},j.map((J,F)=>X("span",{key:`${F}-${J.slice(0,16)}`},J))),y!==void 0?X(O1,{title:f,data:y,onOpen:l,testId:$}):null)}function pu({title:f,text:u}){return X("div",{className:"empty-state"},X("strong",null,f),X("span",null,u))}function dT(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function eT(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function fr(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function ur(f){return{components:Array.isArray(f?.registry?.components)?f.registry.components:[],pipelines:Array.isArray(f?.pipelines)?f.pipelines:[],runs:Array.isArray(f?.runs)?f.runs:[]}}function Vq(f,u,_){let y=f?._unidesk?.arrayLimits?.[u],l=Number(y?.originalLength);return Number.isFinite(l)?l:_}function pq(f){if(!f||typeof f!=="object"||Array.isArray(f))return"--";return`${f.componentClass||"--"}/${f.id||"--"}`}function t5(f){if(!f||typeof f!=="object"||Array.isArray(f))return"";let u=String(f.componentClass||"").trim(),_=String(f.id||"").trim();return u&&_?`${u}/${_}`:""}function fQ(f){return f?.config&&typeof f.config==="object"&&!Array.isArray(f.config)?f.config:{}}function kq(f){let u=fQ(f),_=Array.isArray(u.nodes)?u.nodes:Array.isArray(f?.nodes)?f.nodes:[],y=new Map;for(let j of _){let J=String(j?.id||j?.nodeId||"");if(J)y.set(J,{...j,id:J})}let l=uQ(f),$=(j)=>{if(j&&!y.has(j))y.set(j,{id:j})};for(let j of _Q(f))Y6(j).forEach($);for(let j of l)$(String(j?.from||j?.source||"")),$(String(j?.to||j?.target||""));return Array.from(y.values())}function uQ(f){let u=fQ(f);return Array.isArray(u.edges)?u.edges:Array.isArray(f?.edges)?f.edges:[]}function _Q(f){let u=fQ(f);return Array.isArray(u.topologicalBatches)?u.topologicalBatches:Array.isArray(f?.topologicalBatches)?f.topologicalBatches:[]}function _r(f){let u=new Map;for(let _ of f){let y=t5(_);if(y)u.set(y,_);let l=Array.isArray(_?.refs)?_.refs:[];for(let $ of l){let j=t5($);if(j)u.set(j,_)}}return u}function Oq(f,u){let _=u.get(t5(f?.componentRef));if(_)return _;let y=t5({componentClass:f?.kind,id:f?.id});return y?u.get(y)||null:null}function Xq(f,u){let _=mq(f,u);return String(_?.status||"pending")}function mq(f,u){return(Array.isArray(f?.nodes)?f.nodes:[]).find((y)=>y?.nodeId===u||y?.id===u)||null}function yr(f){return f.reduce((u,_)=>{let y=String(_?.status||"unknown").toLowerCase();return u[y]=(u[y]||0)+1,u},{})}function lr(f){if(Array.isArray(f?.scorers))return f.scorers.filter(Xf);if(Array.isArray(f?.summary?.scorers))return f.summary.scorers.filter(Xf);if(Array.isArray(f?.artifact?.summary?.scorers))return f.artifact.summary.scorers.filter(Xf);return[]}function $r(f){if(Xf(f?.run))return f.run;if(Xf(f?.runSummary))return f.runSummary;return null}function jr(f,u){if(!Xf(f)&&!Xf(u))return null;if(!Xf(f))return u;if(!Xf(u))return f;return{...f,...u,request:Xf(f.request)||Xf(u.request)?{...Xf(f.request)?f.request:{},...Xf(u.request)?u.request:{}}:u.request??f.request,artifact:Xf(f.artifact)||Xf(u.artifact)?{...Xf(f.artifact)?f.artifact:{},...Xf(u.artifact)?u.artifact:{}}:u.artifact??f.artifact,summary:Xf(f.summary)||Xf(u.summary)?{...Xf(f.summary)?f.summary:{},...Xf(u.summary)?u.summary:{}}:u.summary??f.summary}}function s5(f){let u=lr(f),_=u.find((U)=>Xf(U?.score))||u[0]||null,y=Xf(_?.score)?_.score:{},l=Number(y.passed),$=Number(y.total),j=Number(y.ratio),J=Number.isFinite(j)?j:Number.isFinite(l)&&Number.isFinite($)&&$>0?l/$:null,F=J===null?null:Math.round(Math.max(0,Math.min(100,J<=1?J*100:J))),A=String(y.text||(Number.isFinite(l)&&Number.isFinite($)?`${l}/${$}`:""));return{scorer:_,scorers:u,score:y,passed:Number.isFinite(l)?l:null,total:Number.isFinite($)?$:null,percent:F,text:A}}function nF(f){let u=s5(f);return u.text||(u.scorers.length>0?String(u.scorer?.status||"pending"):"--")}function yQ(f){let u=s5(f);if(u.total>0&&u.passed===u.total)return"succeeded";if(u.total>0&&u.passed>0)return"running";if(u.scorers.length>0)return"failed";return"pending"}function Jr(f){return Array.isArray(f?.items)?f.items.filter(Xf):[]}function Fr({run:f}){let u=nF(f);return X("span",{className:`pipeline-score-badge ${yQ(f)}`},`score ${u}`)}function Qr({run:f,onRaw:u}){let y=s5(f).scorers;if(!f)return X(pu,{title:"暂无评分",text:"选择一个 epoch 后会显示 scorer 结果。"});if(y.length===0)return X("div",{className:"pipeline-score-empty"},X("strong",null,"评分器等待中"),X("span",null,"DAG 完成后,Pipeline control backend 会把 scorer summary 追加到 run artifact,并通过 UniDesk 显示。"));return X("div",{className:"pipeline-score-board","data-testid":"pipeline-score-board"},y.map((l,$)=>{let j=s5({scorers:[l]}),J=Jr(l),F=j.percent??0;return X("article",{key:`${l.scorerId||l.component||$}`,className:`pipeline-score-card ${yQ({scorers:[l]})}`},X("div",{className:"pipeline-score-head"},X("div",null,X("span",null,l.scorerId||l.component||"scorer"),X("strong",null,j.text||l.status||"--")),X(uy,{status:l.status||"unknown"},l.status||"unknown")),X("div",{className:"pipeline-score-meter","aria-label":`score ${F}%`},X("span",{style:{width:`${F}%`}})),X("div",{className:"pipeline-score-facts"},X("span",null,`${F}%`),X("span",null,l.component||"--"),X("span",null,l.applicationCheckoutRef||"--")),J.length>0?X("div",{className:"pipeline-score-items"},J.map((A)=>X("span",{key:`${A.id||A.filter}`,className:`pipeline-score-item ${String(A.status||"").toLowerCase()}`,title:`${A.filter||"--"} / ran=${A.ran??"?"}`},X("b",null,A.id||"--"),X("small",null,A.status||"--")))):X("p",{className:"muted paragraph"},"当前 scorer 尚未返回 item 级结果。"),l.error?X("p",{className:"pipeline-score-error"},vq(l.error,360)):null,X("div",{className:"panel-actions inline-actions"},X(O1,{title:`Scorer ${l.scorerId||$}`,data:l,onOpen:u,testId:"raw-pipeline-score"})))}))}function Ar(f){let u=f.reduce((_,y)=>{let l=String(y?.componentClass||"unknown");return _[l]=(_[l]||0)+1,_},{});return Object.entries(u).map(([_,y])=>({name:_,count:Number(y)})).sort((_,y)=>y.count-_.count||_.name.localeCompare(y.name))}function Y6(f){if(Array.isArray(f))return f.map((u)=>typeof u==="string"?u:String(u?.id||u?.nodeId||"")).filter(Boolean);if(Array.isArray(f?.nodes))return Y6(f.nodes);if(Array.isArray(f?.nodeIds))return Y6(f.nodeIds);return[]}function Ur(f){return Xf(f?.instanceInputs?.monitor)?f.instanceInputs.monitor:{}}function iq(f,u){if(String(f?.kind||"").toLowerCase()!=="procedure")return!1;let _=Ur(f);if(f?.instanceInputs?.monitorMode===!0||_.enabled===!0)return!0;let y=pq(f?.componentRef);return String(u?.id||u?.config?.id||y||"").toLowerCase().includes("monitor")}function Wr(f){return f.filter((u)=>iq(u)).map((u)=>String(u?.id||"")).filter(Boolean)}function Gr(f,u){if(u.length===0)return f;let _=new Set(u),y=u.filter((l)=>f.includes(l));if(y.length===0)return f;return[...y,...f.filter((l)=>!_.has(l))]}function zr(f,u){if(u.length===0)return f;let _=new Set(u),y=u.filter(($)=>f.some((j)=>j.includes($)));if(y.length===0)return f;let l=f.map(($)=>$.filter((j)=>!_.has(j))).filter(($)=>$.length>0);return[y,...l]}function Kr(f,u,_){let l=_Q(f).map(Y6).filter((W)=>W.length>0);if(l.length>0)return l;let $=u.map((W)=>String(W?.id||"")).filter(Boolean),j=new Set($),J=new Map($.map((W)=>[W,0])),F=new Map($.map((W)=>[W,[]]));for(let W of _){let K=String(W?.from||W?.source||""),E=String(W?.to||W?.target||"");if(!j.has(K)||!j.has(E))continue;F.get(K)?.push(E),J.set(E,(J.get(E)||0)+1)}let A=new Map,U=$.filter((W)=>(J.get(W)||0)===0);for(let W of U)A.set(W,0);while(U.length>0){let W=U.shift(),K=(A.get(W)||0)+1;for(let E of F.get(W)||[])if(J.set(E,Math.max(0,(J.get(E)||0)-1)),A.set(E,Math.max(A.get(E)||0,K)),(J.get(E)||0)===0)U.push(E)}$.forEach((W)=>{if(!A.has(W))A.set(W,0)});let G=Math.max(0,...Array.from(A.values()));return Array.from({length:G+1},(W,K)=>$.filter((E)=>A.get(E)===K)).filter((W)=>W.length>0)}function Zr(f,u,_){let l=_Q(f).map(Y6).filter((J)=>J.length>0),$=l.length>0?l.flatMap((J)=>J):(()=>{let J=u.map((H)=>String(H?.id||"")).filter(Boolean),F=new Set(J),A=_.filter((H)=>String(H?.edgeType||"").toLowerCase()!=="rework"),U=new Map(J.map((H)=>[H,0])),G=new Map(J.map((H)=>[H,[]]));for(let H of A){let O=String(H?.from||H?.source||""),z=String(H?.to||H?.target||"");if(!F.has(O)||!F.has(z))continue;G.get(O)?.push(z),U.set(z,(U.get(z)||0)+1)}let W=new Map,K=J.filter((H)=>(U.get(H)||0)===0);for(let H of K)W.set(H,0);while(K.length>0){let H=K.shift(),O=(W.get(H)||0)+1;for(let z of G.get(H)||[])if(U.set(z,Math.max(0,(U.get(z)||0)-1)),W.set(z,Math.max(W.get(z)||0,O)),(U.get(z)||0)===0)K.push(z)}J.forEach((H)=>{if(!W.has(H))W.set(H,0)});let E=Math.max(0,...Array.from(W.values()));return Array.from({length:E+1},(H,O)=>J.filter((z)=>W.get(z)===O)).flatMap((H)=>H)})(),j=new Set($);for(let J of u){let F=String(J?.id||"");if(!F||j.has(F))continue;$.push(F),j.add(F)}return Gr($,Wr(u))}function q6(f){return`${f.source}->${f.target}-${f.index}`}function Nq(f,u,_){let y=kq(f),l=uQ(f),$=_r(_),j=new Map(y.map((S)=>[String(S?.id||""),S])),J=y.filter((S)=>iq(S,Oq(S,$))).map((S)=>String(S?.id||"")).filter(Boolean),F=zr(Kr(f,y,l),J),A=[],U=new Map,G=330,W=122;F.forEach((S,B)=>{let P=S.length*122;S.forEach((M,w)=>{let Y=j.get(M)||{id:M},R=Oq(Y,$),k=Xq(u,M).toLowerCase(),p=String(Y.kind||R?.componentClass||"node").toLowerCase(),n=pq(Y.componentRef||R),_f=String(R?.config?.version||R?.version||""),s=String(R?.config?.description||R?.description||""),ff=w*122-Math.floor(P/2);U.set(M,{column:B,row:w,y:ff}),A.push({id:M,type:"pipelineNode",position:{x:B*330,y:ff},data:{exportLabel:{id:M,kind:p,componentRef:n,componentVersion:_f,componentDescription:s,status:k},label:X("div",{className:"flow-node-label"},X("strong",null,M),X("span",null,p),X("code",{title:s||n},_f?`${n}@${_f}`:n),X(uy,{status:k},k))},className:`pipeline-flow-node ${p} ${k}`})})});let K=l.flatMap((S,B)=>{let P=String(S?.from||S?.source||""),M=String(S?.to||S?.target||"");if(!j.has(P)||!j.has(M))return[];return[{source:P,target:M,index:B,condition:S?.condition,edgeType:S?.edgeType}]}),E=K.reduce((S,B)=>S.set(B.source,(S.get(B.source)||0)+1),new Map),H=K.reduce((S,B)=>S.set(B.target,(S.get(B.target)||0)+1),new Map),O=K.reduce((S,B)=>{let P=`${B.source}->${B.target}`;return S.set(P,(S.get(P)||0)+1)},new Map),z=new Map,q=new Map,Z=new Map,V=new Map,L=new Map,r=new Map,N=K.reduce((S,B)=>{let P=U.get(B.source),M=U.get(B.target),w=(M?.column||0)-(P?.column||0);if(w<=0||String(B.edgeType||"").toLowerCase()==="rework"||w!==1)return S;let R=`${B.source}->column:${M?.column??""}`,k=S.get(R)||[];return k.push(B),S.set(R,k),S},new Map);for(let S of N.values()){if(S.length<2)continue;S.slice().sort((B,P)=>{let M=U.get(B.target),w=U.get(P.target);return(M?.y||0)-(w?.y||0)||B.index-P.index}).forEach((B,P,M)=>{r.set(q6(B),{slot:P-(M.length-1)/2,count:M.length})})}[...K].sort((S,B)=>{let P=U.get(S.source),M=U.get(S.target),w=U.get(B.source),Y=U.get(B.target),R=Math.abs((M?.column||0)-(P?.column||0))*330+Math.abs((M?.y||0)-(P?.y||0)),k=Math.abs((Y?.column||0)-(w?.column||0))*330+Math.abs((Y?.y||0)-(w?.y||0));return R-k||S.index-B.index}).forEach((S)=>{let B=U.get(S.source)||{column:0,row:0,y:0},P=U.get(S.target)||{column:0,row:0,y:0},M=P.column-B.column,w=Math.max(0,M),Y=M<=0||String(S.edgeType||"").toLowerCase()==="rework",R=B.y-P.y,k=H.get(S.target)||1,p=r.has(q6(S)),n=!Y&&w<=1&&(p||k===1),_f=L.get(S.target)||new Map;L.set(S.target,_f);let s=N6.slice().sort((ff,Kf)=>{let Gf=(Zf)=>{let h=String(Zf.side),i=0;if(Y){if(h==="left")i+=86;if(h==="top")i+=P.y<=0?-22:12;if(h==="bottom")i+=P.y>=0?-22:12;if(Math.abs(P.y)<12&&h!=="left")i+=S.index%2===0?h==="top"?-6:6:h==="bottom"?-6:6;return i}if(n){if(h==="left")i-=p?72:44;if(h!=="left")i+=p?72:44;return i+Math.abs(R)*0.02}if(h==="left")i+=w<=1?0:24;if(h==="top")i+=R<-36?-18:42;if(h==="bottom")i+=R>36?-18:42;if(w<=1&&Math.abs(R)<=82&&h!=="left")i+=38;if(w>1&&h!=="left")i-=10;return i},jf=B.y-P.y,Wf=jf!==0?jf:S.index%2===0?-1:1,Of=(Zf)=>{let h=_f.get(Zf.id)||0;return Gf(Zf)+h*64+CT(Zf,_f,Wf)};return Of(ff)-Of(Kf)||String(ff.id).localeCompare(String(Kf.id))})[0];_f.set(s.id,(_f.get(s.id)||0)+1),V.set(q6(S),s)});let x=K.map((S)=>{let B=Xq(u,S.target).toLowerCase(),P=`${S.source}->${S.target}`,M=z.get(S.source)||0,w=q.get(S.target)||0,Y=Z.get(P)||0;z.set(S.source,M+1),q.set(S.target,w+1),Z.set(P,Y+1);let R=M-((E.get(S.source)||1)-1)/2,k=w-((H.get(S.target)||1)-1)/2,p=Y-((O.get(P)||1)-1)/2,n=U.get(S.source),_f=U.get(S.target),s=(_f?.column||0)-(n?.column||0),ff=Math.max(1,Math.abs(s)),Kf=s<=0||String(S.edgeType||"").toLowerCase()==="rework",Gf=Math.abs((_f?.y||0)-(n?.y||0)),jf=r.get(q6(S)),Wf=!Kf&&s===1&&(H.get(S.target)||0)>1,Of=jf?jf.slot:p*2+R+k*0.45,Zf=Of===0?S.index%2===0?-1:1:Math.sign(Of),h=V.get(q6(S))||N6[1],i=h.side==="top"?-1:h.side==="bottom"?1:Zf,I=Kf||ff>1||Gf>96||Math.abs(Of)>0.2||h.side!=="left",lf=Kf?118+ff*18:22+ff*16,$f=h.side==="left"?0:28,Af=I?Math.max(-280,Math.min(280,i*Math.min(180,lf+$f+Gf*0.22)+Of*28)):0,Yf=Math.max(0,Math.min(H6.length-1,Math.round(R+(H6.length-1)/2))),xf=H6[Yf]||H6[1],of=B==="succeeded"?"var(--accent-2)":B==="running"?"var(--accent)":B==="failed"?"var(--danger)":"rgba(129, 147, 159, 0.78)",F0=n?.column||0,y0=_f?.column||0,T0=Af===0?0:Math.sign(Af),Qu=Kf?`feedback:${F0}->${y0}:${T0}`:jf?`fanout:${F0}->${y0}:${S.source}`:Wf?`fanin:${F0}->${y0}:${S.target}`:h.side!=="left"||ff>1?`corridor:${F0}->${y0}:${h.side}:${T0}:${Math.round(Math.abs(Af)/56)}`:"";return{id:`${S.source}->${S.target}-${S.index}`,source:S.source,target:S.target,sourceHandle:xf.id,targetHandle:h.id,type:"pipelineCurve",zIndex:12,animated:B==="running",data:{baseEdgeColor:of,laneOffset:Af,routeMode:jf&&h.side==="left"?"direct-forward-left":"",targetSide:h.side,isFeedback:Kf,overlapGroup:Qu},targetStatus:B}}),c=x.reduce((S,B)=>{let P=String(B.data?.overlapGroup||"");return P?S.set(P,(S.get(P)||0)+1):S},new Map),v=new Map,C=x.map((S)=>{let B=String(S.targetStatus||"pending"),P={...S};delete P.targetStatus;let M=String(S.data?.overlapGroup||""),w=M?c.get(M)||0:0,Y=w>1?v.get(M)||0:-1;if(w>1)v.set(M,Y+1);let R=Y>=0?Aq[Y%Aq.length]:String(S.data.baseEdgeColor),k={stroke:R};if(S.data.isFeedback)k.strokeDasharray="9 7";return{...P,data:{...S.data,edgeColor:R,overlapSlot:Y,overlapCount:w},style:k,markerEnd:{type:n_.ArrowClosed,color:R},className:`pipeline-flow-edge ${B} ${S.data.isFeedback?"feedback":""} ${Y>=0?"overlap-colored":""}`}});return{nodes:A,edges:C}}function Tu(f){return String(f??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}function Lq(f){let u=String(f||"");if(u.includes("--accent-2"))return"#4eb7a8";if(u.includes("--accent"))return"#d7a13a";if(u.includes("--danger"))return"#cf6a54";return u.startsWith("#")?u:"#81939f"}function o5(f){return`arrow-${f.replace(/[^a-zA-Z0-9_-]+/g,"")}`}function gq(f,u="pipeline"){return String(f||u).replace(/[^a-zA-Z0-9_-]+/g,"-").replace(/^-|-$/g,"")||u}function Yq(f,u){let _=f.position.x,y=f.position.y,l=N6.find(($)=>$.id===u);if(l?.side==="top")return{x:_+U3*Gq(l.style?.left,0.5),y,position:Uf.Top};if(l?.side==="bottom")return{x:_+U3*Gq(l.style?.left,0.5),y:y+W3,position:Uf.Bottom};return{x:_,y:y+W3/2,position:Uf.Left}}function qr(f){return{x:f.position.x+U3,y:f.position.y+W3/2}}function Er(f,u){let _=Math.min(...f.nodes.map((H)=>H.position.x),0)-220,y=Math.min(...f.nodes.map((H)=>H.position.y),0)-220,l=Math.max(...f.nodes.map((H)=>H.position.x+U3),1)+220,$=Math.max(...f.nodes.map((H)=>H.position.y+W3),1)+220,j=Math.ceil(l-_),J=Math.ceil($-y),F=new Map(f.nodes.map((H)=>[H.id,H])),A=f.edges.map((H)=>Lq(H.data?.edgeColor||H.style?.stroke)),G=Array.from(new Set(["#4eb7a8","#d7a13a","#cf6a54","#81939f",...A])).map((H)=>``).join(""),W=f.edges.flatMap((H)=>{let O=F.get(H.source),z=F.get(H.target);if(!O||!z)return[];let q=qr(O),Z=Yq(z,String(H.targetHandle||"in-left")),V=Cq(q.x,q.y,Z.x,Z.y,Z.position,Number(H.data?.laneOffset||0),String(H.data?.routeMode||"")),L=Lq(H.data?.edgeColor||H.style?.stroke),r=H.data?.isFeedback?' stroke-dasharray="9 7"':"";return``}).join(` -`),K=f.nodes.map((H)=>{let O=H.data?.exportLabel||{},z=String(O.status||"pending").toLowerCase(),q=z==="succeeded"?"#4eb7a8":z==="running"?"#d7a13a":z==="failed"?"#cf6a54":"#81939f",Z=H.position.x,V=H.position.y,L=N6.map((r)=>{let N=Yq(H,r.id);if(r.side==="top"||r.side==="bottom")return``;return``}).join(` +`)),U=F.reduce((Q,W)=>Q.concat(...W),[]);return[F,U]}return[[],[]]},[f]);return lf.useEffect(()=>{let A=u?.target??hN,F=u?.actInsideInputWithModifier??!0;if(f!==null){let U=(G)=>{if(r.current=G.ctrlKey||G.metaKey||G.shiftKey||G.altKey,(!r.current||r.current&&!F)&&ZF(G))return!1;let E=pN(G.code,j);if(_.current.add(G[E]),mN($,_.current,!1)){let O=G.composedPath?.()?.[0]||G.target,z=O?.nodeName==="BUTTON"||O?.nodeName==="A";if(u.preventDefault!==!1&&(r.current||!z))G.preventDefault();y(!0)}},Q=(G)=>{let K=pN(G.code,j);if(mN($,_.current,!0))y(!1),_.current.clear();else _.current.delete(G[K]);if(G.key==="Meta")_.current.clear();r.current=!1},W=()=>{_.current.clear(),y(!1)};return A?.addEventListener("keydown",U),A?.addEventListener("keyup",Q),window.addEventListener("blur",W),window.addEventListener("contextmenu",W),()=>{A?.removeEventListener("keydown",U),A?.removeEventListener("keyup",Q),window.removeEventListener("blur",W),window.removeEventListener("contextmenu",W)}}},[f,y]),l}function mN(f,u,l){return f.filter((y)=>l||y.length===u.size).some((y)=>y.every((r)=>u.has(r)))}function pN(f,u){return u.includes(f)?"code":"key"}var HT=()=>{let f=Hu();return lf.useMemo(()=>{return{zoomIn:(u)=>{let{panZoom:l}=f.getState();return l?l.scaleBy(1.2,u):Promise.resolve(!1)},zoomOut:(u)=>{let{panZoom:l}=f.getState();return l?l.scaleBy(0.8333333333333334,u):Promise.resolve(!1)},zoomTo:(u,l)=>{let{panZoom:y}=f.getState();return y?y.scaleTo(u,l):Promise.resolve(!1)},getZoom:()=>f.getState().transform[2],setViewport:async(u,l)=>{let{transform:[y,r,_],panZoom:$}=f.getState();if(!$)return Promise.resolve(!1);return await $.setViewport({x:u.x??y,y:u.y??r,zoom:u.zoom??_},l),Promise.resolve(!0)},getViewport:()=>{let[u,l,y]=f.getState().transform;return{x:u,y:l,zoom:y}},setCenter:async(u,l,y)=>{return f.getState().setCenter(u,l,y)},fitBounds:async(u,l)=>{let{width:y,height:r,minZoom:_,maxZoom:$,panZoom:j}=f.getState(),A=e3(u,y,r,_,$,l?.padding??0.1);if(!j)return Promise.resolve(!1);return await j.setViewport(A,{duration:l?.duration,ease:l?.ease,interpolate:l?.interpolate}),Promise.resolve(!0)},screenToFlowPosition:(u,l={})=>{let{transform:y,snapGrid:r,snapToGrid:_,domNode:$}=f.getState();if(!$)return u;let{x:j,y:A}=$.getBoundingClientRect(),F={x:u.x-j,y:u.y-A},U=l.snapGrid??r,Q=l.snapToGrid??_;return i_(F,y,Q,U)},flowToScreenPosition:(u)=>{let{transform:l,domNode:y}=f.getState();if(!y)return u;let{x:r,y:_}=y.getBoundingClientRect(),$=a3(u,l);return{x:$.x+r,y:$.y+_}}}},[])};function UZ(f,u){let l=[],y=new Map,r=[];for(let _ of f)if(_.type==="add"){r.push(_);continue}else if(_.type==="remove"||_.type==="replace")y.set(_.id,[_]);else{let $=y.get(_.id);if($)$.push(_);else y.set(_.id,[_])}for(let _ of u){let $=y.get(_.id);if(!$){l.push(_);continue}if($[0].type==="remove")continue;if($[0].type==="replace"){l.push({...$[0].item});continue}let j={..._};for(let A of $)OT(A,j);l.push(j)}if(r.length)r.forEach((_)=>{if(_.index!==void 0)l.splice(_.index,0,{..._.item});else l.push({..._.item})});return l}function OT(f,u){switch(f.type){case"select":{u.selected=f.selected;break}case"position":{if(typeof f.position<"u")u.position=f.position;if(typeof f.dragging<"u")u.dragging=f.dragging;break}case"dimensions":{if(typeof f.dimensions<"u"){if(u.measured={...f.dimensions},f.setAttributes){if(f.setAttributes===!0||f.setAttributes==="width")u.width=f.dimensions.width;if(f.setAttributes===!0||f.setAttributes==="height")u.height=f.dimensions.height}}if(typeof f.resizing==="boolean")u.resizing=f.resizing;break}}}function qT(f,u){return UZ(f,u)}function VT(f,u){return UZ(f,u)}function Hr(f,u){return{id:f,type:"select",selected:u}}function v_(f,u=new Set,l=!1){let y=[];for(let[r,_]of f){let $=u.has(r);if(!(_.selected===void 0&&!$)&&_.selected!==$){if(l)_.selected=$;y.push(Hr(_.id,$))}}return y}function IN({items:f=[],lookup:u}){let l=[],y=new Map(f.map((r)=>[r.id,r]));for(let[r,_]of f.entries()){let $=u.get(_.id),j=$?.internals?.userNode??$;if(j!==void 0&&j!==_)l.push({id:_.id,item:_,type:"replace"});if(j===void 0)l.push({item:_,type:"add",index:r})}for(let[r]of u)if(y.get(r)===void 0)l.push({id:r,type:"remove"});return l}function gN(f){return{id:f.id,type:"remove"}}var kN=(f)=>sK(f),LT=(f)=>jF(f);function QZ(f){return lf.forwardRef(f)}function tN(f){let[u,l]=lf.useState(BigInt(0)),[y]=lf.useState(()=>BT(()=>l((r)=>r+BigInt(1))));return nF(()=>{let r=y.get();if(r.length)f(r),y.reset()},[u]),y}function BT(f){let u=[];return{get:()=>u,reset:()=>{u=[]},push:(l)=>{u.push(l),f()}}}var WZ=lf.createContext(null);function XT({children:f}){let u=Hu(),l=lf.useCallback((j)=>{let{nodes:A=[],setNodes:F,hasDefaultNodes:U,onNodesChange:Q,nodeLookup:W,fitViewQueued:G,onNodesChangeMiddlewareMap:K}=u.getState(),E=A;for(let z of j)E=typeof z==="function"?z(E):z;let O=IN({items:E,lookup:W});for(let z of K.values())O=z(O);if(U)F(E);if(O.length>0)Q?.(O);else if(G)window.requestAnimationFrame(()=>{let{fitViewQueued:z,nodes:Z,setNodes:N}=u.getState();if(z)N(Z)})},[]),y=tN(l),r=lf.useCallback((j)=>{let{edges:A=[],setEdges:F,hasDefaultEdges:U,onEdgesChange:Q,edgeLookup:W}=u.getState(),G=A;for(let K of j)G=typeof K==="function"?K(G):K;if(U)F(G);else if(Q)Q(IN({items:G,lookup:W}))},[]),_=tN(r),$=lf.useMemo(()=>({nodeQueue:y,edgeQueue:_}),[]);return ff.jsx(WZ.Provider,{value:$,children:f})}function YT(){let f=lf.useContext(WZ);if(!f)throw Error("useBatchContext must be used within a BatchProvider");return f}var wT=(f)=>!!f.panZoom;function SF(){let f=HT(),u=Hu(),l=YT(),y=of(wT),r=lf.useMemo(()=>{let _=(Q)=>u.getState().nodeLookup.get(Q),$=(Q)=>{l.nodeQueue.push(Q)},j=(Q)=>{l.edgeQueue.push(Q)},A=(Q)=>{let{nodeLookup:W,nodeOrigin:G}=u.getState(),K=kN(Q)?Q:W.get(Q.id),E=K.parentId?GF(K.position,K.measured,K.parentId,W,G):K.position,O={...K,position:E,width:K.measured?.width??K.width,height:K.measured?.height??K.height};return Er(O)},F=(Q,W,G={replace:!1})=>{$((K)=>K.map((E)=>{if(E.id===Q){let O=typeof W==="function"?W(E):W;return G.replace&&kN(O)?O:{...E,...O}}return E}))},U=(Q,W,G={replace:!1})=>{j((K)=>K.map((E)=>{if(E.id===Q){let O=typeof W==="function"?W(E):W;return G.replace&<(O)?O:{...E,...O}}return E}))};return{getNodes:()=>u.getState().nodes.map((Q)=>({...Q})),getNode:(Q)=>_(Q)?.internals.userNode,getInternalNode:_,getEdges:()=>{let{edges:Q=[]}=u.getState();return Q.map((W)=>({...W}))},getEdge:(Q)=>u.getState().edgeLookup.get(Q),setNodes:$,setEdges:j,addNodes:(Q)=>{let W=Array.isArray(Q)?Q:[Q];l.nodeQueue.push((G)=>[...G,...W])},addEdges:(Q)=>{let W=Array.isArray(Q)?Q:[Q];l.edgeQueue.push((G)=>[...G,...W])},toObject:()=>{let{nodes:Q=[],edges:W=[],transform:G}=u.getState(),[K,E,O]=G;return{nodes:Q.map((z)=>({...z})),edges:W.map((z)=>({...z})),viewport:{x:K,y:E,zoom:O}}},deleteElements:async({nodes:Q=[],edges:W=[]})=>{let{nodes:G,edges:K,onNodesDelete:E,onEdgesDelete:O,triggerNodeChanges:z,triggerEdgeChanges:Z,onDelete:N,onBeforeDelete:H}=u.getState(),{nodes:Y,edges:w}=await dK({nodesToRemove:Q,edgesToRemove:W,nodes:G,edges:K,onBeforeDelete:H}),V=w.length>0,X=Y.length>0;if(V){let i=w.map(gN);O?.(w),Z(i)}if(X){let i=Y.map(gN);E?.(Y),z(i)}if(X||V)N?.({nodes:Y,edges:w});return{deletedNodes:Y,deletedEdges:w}},getIntersectingNodes:(Q,W=!0,G)=>{let K=QF(Q),E=K?Q:A(Q),O=G!==void 0;if(!E)return[];return(G||u.getState().nodes).filter((z)=>{let Z=u.getState().nodeLookup.get(z.id);if(Z&&!K&&(z.id===Q.id||!Z.internals.positionAbsolute))return!1;let N=Er(O?z:Z),H=C_(N,E);return W&&H>0||H>=N.width*N.height||H>=E.width*E.height})},isNodeIntersecting:(Q,W,G=!0)=>{let E=QF(Q)?Q:A(Q);if(!E)return!1;let O=C_(E,W);return G&&O>0||O>=W.width*W.height||O>=E.width*E.height},updateNode:F,updateNodeData:(Q,W,G={replace:!1})=>{F(Q,(K)=>{let E=typeof W==="function"?W(K):W;return G.replace?{...K,data:E}:{...K,data:{...K.data,...E}}},G)},updateEdge:U,updateEdgeData:(Q,W,G={replace:!1})=>{U(Q,(K)=>{let E=typeof W==="function"?W(K):W;return G.replace?{...K,data:E}:{...K,data:{...K.data,...E}}},G)},getNodesBounds:(Q)=>{let{nodeLookup:W,nodeOrigin:G}=u.getState();return FF(Q,{nodeLookup:W,nodeOrigin:G})},getHandleConnections:({type:Q,id:W,nodeId:G})=>Array.from(u.getState().connectionLookup.get(`${G}-${Q}${W?`-${W}`:""}`)?.values()??[]),getNodeConnections:({type:Q,handleId:W,nodeId:G})=>Array.from(u.getState().connectionLookup.get(`${G}${Q?W?`-${Q}-${W}`:`-${Q}`:""}`)?.values()??[]),fitView:async(Q)=>{let W=u.getState().fitViewResolver??uN();return u.setState({fitViewQueued:!0,fitViewOptions:Q,fitViewResolver:W}),l.nodeQueue.push((G)=>[...G]),W.promise}}},[]);return lf.useMemo(()=>{return{...r,...f,viewportInitialized:y}},[y])}var sN=(f)=>f.selected,DT=typeof window<"u"?window:void 0;function TT({deleteKeyCode:f,multiSelectionKeyCode:u}){let l=Hu(),{deleteElements:y}=SF(),r=u6(f,{actInsideInputWithModifier:!1}),_=u6(u,{target:DT});lf.useEffect(()=>{if(r){let{edges:$,nodes:j}=l.getState();y({nodes:j.filter(sN),edges:$.filter(sN)}),l.setState({nodesSelectionActive:!1})}},[r]),lf.useEffect(()=>{l.setState({multiSelectionActive:_})},[_])}function nT(f){let u=Hu();lf.useEffect(()=>{let l=()=>{if(!f.current||!(f.current.checkVisibility?.()??!0))return!1;let y=U2(f.current);if(y.height===0||y.width===0)u.getState().onError?.("004",a0.error004());u.setState({width:y.width||500,height:y.height||500})};if(f.current){l(),window.addEventListener("resize",l);let y=new ResizeObserver(()=>l());return y.observe(f.current),()=>{if(window.removeEventListener("resize",l),y&&f.current)y.unobserve(f.current)}}},[])}var X2={position:"absolute",width:"100%",height:"100%",top:0,left:0},MT=(f)=>({userSelectionActive:f.userSelectionActive,lib:f.lib,connectionInProgress:f.connection.inProgress});function ST({onPaneContextMenu:f,zoomOnScroll:u=!0,zoomOnPinch:l=!0,panOnScroll:y=!1,panOnScrollSpeed:r=0.5,panOnScrollMode:_=B1.Free,zoomOnDoubleClick:$=!0,panOnDrag:j=!0,defaultViewport:A,translateExtent:F,minZoom:U,maxZoom:Q,zoomActivationKeyCode:W,preventScrolling:G=!0,children:K,noWheelClassName:E,noPanClassName:O,onViewportChange:z,isControlledViewport:Z,paneClickDistance:N,selectionOnDrag:H}){let Y=Hu(),w=lf.useRef(null),{userSelectionActive:V,lib:X,connectionInProgress:i}=of(MT,Zu),m=u6(W),M=lf.useRef();nT(w);let c=lf.useCallback((C)=>{if(z?.({x:C[0],y:C[1],zoom:C[2]}),!Z)Y.setState({transform:C})},[z,Z]);return lf.useEffect(()=>{if(w.current){M.current=ON({domNode:w.current,minZoom:U,maxZoom:Q,translateExtent:F,viewport:A,onDraggingChange:(P)=>Y.setState((n)=>n.paneDragging===P?n:{paneDragging:P}),onPanZoomStart:(P,n)=>{let{onViewportChangeStart:B,onMoveStart:D}=Y.getState();D?.(P,n),B?.(n)},onPanZoom:(P,n)=>{let{onViewportChange:B,onMove:D}=Y.getState();D?.(P,n),B?.(n)},onPanZoomEnd:(P,n)=>{let{onViewportChangeEnd:B,onMoveEnd:D}=Y.getState();D?.(P,n),B?.(n)}});let{x:C,y:T,zoom:R}=M.current.getViewport();return Y.setState({panZoom:M.current,transform:[C,T,R],domNode:w.current.closest(".react-flow")}),()=>{M.current?.destroy()}}},[]),lf.useEffect(()=>{M.current?.update({onPaneContextMenu:f,zoomOnScroll:u,zoomOnPinch:l,panOnScroll:y,panOnScrollSpeed:r,panOnScrollMode:_,zoomOnDoubleClick:$,panOnDrag:j,zoomActivationKeyPressed:m,preventScrolling:G,noPanClassName:O,userSelectionActive:V,noWheelClassName:E,lib:X,onTransformChange:c,connectionInProgress:i,selectionOnDrag:H,paneClickDistance:N})},[f,u,l,y,r,_,$,j,m,G,O,V,E,X,c,i,H,N]),ff.jsx("div",{className:"react-flow__renderer",ref:w,style:X2,children:K})}var PT=(f)=>({userSelectionActive:f.userSelectionActive,userSelectionRect:f.userSelectionRect});function CT(){let{userSelectionActive:f,userSelectionRect:u}=of(PT,Zu);if(!(f&&u))return null;return ff.jsx("div",{className:"react-flow__selection react-flow__container",style:{width:u.width,height:u.height,transform:`translate(${u.x}px, ${u.y}px)`}})}var DF=(f,u)=>{return(l)=>{if(l.target!==u.current)return;f?.(l)}},cT=(f)=>({userSelectionActive:f.userSelectionActive,elementsSelectable:f.elementsSelectable,connectionInProgress:f.connection.inProgress,dragging:f.paneDragging});function iT({isSelecting:f,selectionKeyPressed:u,selectionMode:l=Nr.Full,panOnDrag:y,paneClickDistance:r,selectionOnDrag:_,onSelectionStart:$,onSelectionEnd:j,onPaneClick:A,onPaneContextMenu:F,onPaneScroll:U,onPaneMouseEnter:Q,onPaneMouseMove:W,onPaneMouseLeave:G,children:K}){let E=Hu(),{userSelectionActive:O,elementsSelectable:z,dragging:Z,connectionInProgress:N}=of(cT,Zu),H=z&&(f||O),Y=lf.useRef(null),w=lf.useRef(),V=lf.useRef(new Set),X=lf.useRef(new Set),i=lf.useRef(!1),m=(B)=>{if(i.current||N){i.current=!1;return}A?.(B),E.getState().resetSelectedElements(),E.setState({nodesSelectionActive:!1})},M=(B)=>{if(Array.isArray(y)&&y?.includes(2)){B.preventDefault();return}F?.(B)},c=U?(B)=>U(B):void 0,C=(B)=>{if(i.current)B.stopPropagation(),i.current=!1},T=(B)=>{let{domNode:D}=E.getState();if(w.current=D?.getBoundingClientRect(),!w.current)return;let I=B.target===Y.current;if(!I&&!!B.target.closest(".nokey")||!f||!(_&&I||u)||B.button!==0||!B.isPrimary)return;B.target?.setPointerCapture?.(B.pointerId),i.current=!1;let{x:_f,y:S}=Gl(B.nativeEvent,w.current);if(E.setState({userSelectionRect:{width:0,height:0,startX:_f,startY:S,x:_f,y:S}}),!I)B.stopPropagation(),B.preventDefault()},R=(B)=>{let{userSelectionRect:D,transform:I,nodeLookup:p,edgeLookup:k,connectionLookup:_f,triggerNodeChanges:S,triggerEdgeChanges:e,defaultEdgeOptions:$f,resetSelectedElements:Qf}=E.getState();if(!w.current||!D)return;let{x:Af,y:zf}=Gl(B.nativeEvent,w.current),{startX:Hf,startY:Zf}=D;if(!i.current){let o=u?0:r;if(Math.hypot(Af-Hf,zf-Zf)<=o)return;Qf(),$?.(B)}i.current=!0;let b={startX:Hf,startY:Zf,x:Afo.id)),X.current=new Set;let Nf=$f?.selectable??!0;for(let o of V.current){let uf=_f.get(o);if(!uf)continue;for(let{edgeId:qf}of uf.values()){let xf=k.get(qf);if(xf&&(xf.selectable??Nf))X.current.add(qf)}}if(!KF(t,V.current)){let o=v_(p,V.current,!0);S(o)}if(!KF(a,X.current)){let o=v_(k,X.current);e(o)}E.setState({userSelectionRect:b,userSelectionActive:!0,nodesSelectionActive:!1})},P=(B)=>{if(B.button!==0)return;if(B.target?.releasePointerCapture?.(B.pointerId),!O&&B.target===Y.current&&E.getState().userSelectionRect)m?.(B);if(E.setState({userSelectionActive:!1,userSelectionRect:null}),i.current)j?.(B),E.setState({nodesSelectionActive:V.current.size>0})},n=y===!0||Array.isArray(y)&&y.includes(0);return ff.jsxs("div",{className:nu(["react-flow__pane",{draggable:n,dragging:Z,selection:f}]),onClick:H?void 0:DF(m,Y),onContextMenu:DF(M,Y),onWheel:DF(c,Y),onPointerEnter:H?void 0:Q,onPointerMove:H?R:W,onPointerUp:H?P:void 0,onPointerDownCapture:H?T:void 0,onClickCapture:H?C:void 0,onPointerLeave:G,ref:Y,style:X2,children:[K,ff.jsx(CT,{})]})}function MF({id:f,store:u,unselect:l=!1,nodeRef:y}){let{addSelectedNodes:r,unselectNodesAndEdges:_,multiSelectionActive:$,nodeLookup:j,onError:A}=u.getState(),F=j.get(f);if(!F){A?.("012",a0.error012(f));return}if(u.setState({nodesSelectionActive:!1}),!F.selected)r([f]);else if(l||F.selected&&$)_({nodes:[F],edges:[]}),requestAnimationFrame(()=>y?.current?.blur())}function zZ({nodeRef:f,disabled:u=!1,noDragClassName:l,handleSelector:y,nodeId:r,isSelectable:_,nodeClickDistance:$}){let j=Hu(),[A,F]=lf.useState(!1),U=lf.useRef();return lf.useEffect(()=>{U.current=WN({getStoreItems:()=>j.getState(),onNodeMouseDown:(Q)=>{MF({id:Q,store:j,nodeRef:f})},onDragStart:()=>{F(!0)},onDragStop:()=>{F(!1)}})},[]),lf.useEffect(()=>{if(u||!f.current||!U.current)return;return U.current.update({noDragClassName:l,handleSelector:y,domNode:f.current,isSelectable:_,nodeId:r,nodeClickDistance:$}),()=>{U.current?.destroy()}},[l,y,u,_,f,r,$]),A}var RT=(f)=>(u)=>u.selected&&(u.draggable||f&&typeof u.draggable>"u");function GZ(){let f=Hu();return lf.useCallback((l)=>{let{nodeExtent:y,snapToGrid:r,snapGrid:_,nodesDraggable:$,onError:j,updateNodePositions:A,nodeLookup:F,nodeOrigin:U}=f.getState(),Q=new Map,W=RT($),G=r?_[0]:5,K=r?_[1]:5,E=l.direction.x*G*l.factor,O=l.direction.y*K*l.factor;for(let[,z]of F){if(!W(z))continue;let Z={x:z.internals.positionAbsolute.x+E,y:z.internals.positionAbsolute.y+O};if(r)Z=c_(Z,_);let{position:N,positionAbsolute:H}=JF({nodeId:z.id,nextPosition:Z,nodeLookup:F,nodeExtent:y,nodeOrigin:U,onError:j});z.position=N,z.internals.positionAbsolute=H,Q.set(z.id,z)}A(Q)},[])}var PF=lf.createContext(null),xT=PF.Provider;PF.Consumer;var KZ=()=>{return lf.useContext(PF)},vT=(f)=>({connectOnClick:f.connectOnClick,noPanClassName:f.noPanClassName,rfId:f.rfId}),bT=(f,u,l)=>(y)=>{let{connectionClickStartHandle:r,connectionMode:_,connection:$}=y,{fromHandle:j,toHandle:A,isValid:F}=$,U=A?.nodeId===f&&A?.id===u&&A?.type===l;return{connectingFrom:j?.nodeId===f&&j?.id===u&&j?.type===l,connectingTo:U,clickConnecting:r?.nodeId===f&&r?.id===u&&r?.type===l,isPossibleEndHandle:_===zy.Strict?j?.type!==l:f!==j?.nodeId||u!==j?.id,connectionInProcess:!!j,clickConnectionInProcess:!!r,valid:U&&F}};function hT({type:f="source",position:u=Gf.Top,isValidConnection:l,isConnectable:y=!0,isConnectableStart:r=!0,isConnectableEnd:_=!0,id:$,onConnect:j,children:A,className:F,onMouseDown:U,onTouchStart:Q,...W},G){let K=$||null,E=f==="target",O=Hu(),z=KZ(),{connectOnClick:Z,noPanClassName:N,rfId:H}=of(vT,Zu),{connectingFrom:Y,connectingTo:w,clickConnecting:V,isPossibleEndHandle:X,connectionInProcess:i,clickConnectionInProcess:m,valid:M}=of(bT(z,K,f),Zu);if(!z)O.getState().onError?.("010",a0.error010());let c=(R)=>{let{defaultEdgeOptions:P,onConnect:n,hasDefaultEdges:B}=O.getState(),D={...P,...R};if(B){let{edges:I,setEdges:p}=O.getState();p(OF(D,I))}n?.(D),j?.(D)},C=(R)=>{if(!z)return;let P=EF(R.nativeEvent);if(r&&(P&&R.button===0||!P)){let n=O.getState();Z2.onPointerDown(R.nativeEvent,{handleDomNode:R.currentTarget,autoPanOnConnect:n.autoPanOnConnect,connectionMode:n.connectionMode,connectionRadius:n.connectionRadius,domNode:n.domNode,nodeLookup:n.nodeLookup,lib:n.lib,isTarget:E,handleId:K,nodeId:z,flowId:n.rfId,panBy:n.panBy,cancelConnection:n.cancelConnection,onConnectStart:n.onConnectStart,onConnectEnd:(...B)=>O.getState().onConnectEnd?.(...B),updateConnection:n.updateConnection,onConnect:c,isValidConnection:l||((...B)=>O.getState().isValidConnection?.(...B)??!0),getTransform:()=>O.getState().transform,getFromHandle:()=>O.getState().connection.fromHandle,autoPanSpeed:n.autoPanSpeed,dragThreshold:n.connectionDragThreshold})}if(P)U?.(R);else Q?.(R)},T=(R)=>{let{onClickConnectStart:P,onClickConnectEnd:n,connectionClickStartHandle:B,connectionMode:D,isValidConnection:I,lib:p,rfId:k,nodeLookup:_f,connection:S}=O.getState();if(!z||!B&&!r)return;if(!B){P?.(R.nativeEvent,{nodeId:z,handleId:K,handleType:f}),O.setState({connectionClickStartHandle:{nodeId:z,type:f,id:K}});return}let e=NF(R.target),$f=l||I,{connection:Qf,isValid:Af}=Z2.isValid(R.nativeEvent,{handle:{nodeId:z,id:K,type:f},connectionMode:D,fromNodeId:B.nodeId,fromHandleId:B.id||null,fromType:B.type,isValidConnection:$f,flowId:k,doc:e,lib:p,nodeLookup:_f});if(Af&&Qf)c(Qf);let zf=structuredClone(S);delete zf.inProgress,zf.toPosition=zf.toHandle?zf.toHandle.position:null,n?.(R,zf),O.setState({connectionClickStartHandle:null})};return ff.jsx("div",{"data-handleid":K,"data-nodeid":z,"data-handlepos":u,"data-id":`${H}-${z}-${K}-${f}`,className:nu(["react-flow__handle",`react-flow__handle-${u}`,"nodrag",N,F,{source:!E,target:E,connectable:y,connectablestart:r,connectableend:_,clickconnecting:V,connectingfrom:Y,connectingto:w,valid:M,connectionindicator:y&&(!i||X)&&(i||m?_:r)}]),onMouseDown:C,onTouchStart:C,onClick:Z?T:void 0,ref:G,...W,children:A})}var Or=lf.memo(QZ(hT));function mT({data:f,isConnectable:u,sourcePosition:l=Gf.Bottom}){return ff.jsxs(ff.Fragment,{children:[f?.label,ff.jsx(Or,{type:"source",position:l,isConnectable:u})]})}function pT({data:f,isConnectable:u,targetPosition:l=Gf.Top,sourcePosition:y=Gf.Bottom}){return ff.jsxs(ff.Fragment,{children:[ff.jsx(Or,{type:"target",position:l,isConnectable:u}),f?.label,ff.jsx(Or,{type:"source",position:y,isConnectable:u})]})}function IT(){return null}function gT({data:f,isConnectable:u,targetPosition:l=Gf.Top}){return ff.jsxs(ff.Fragment,{children:[ff.jsx(Or,{type:"target",position:l,isConnectable:u}),f?.label]})}var V2={ArrowUp:{x:0,y:-1},ArrowDown:{x:0,y:1},ArrowLeft:{x:-1,y:0},ArrowRight:{x:1,y:0}},oN={input:mT,default:pT,output:gT,group:IT};function kT(f){if(f.internals.handleBounds===void 0)return{width:f.width??f.initialWidth??f.style?.width,height:f.height??f.initialHeight??f.style?.height};return{width:f.width??f.style?.width,height:f.height??f.style?.height}}var tT=(f)=>{let{width:u,height:l,x:y,y:r}=P_(f.nodeLookup,{filter:(_)=>!!_.selected});return{width:zl(u)?u:null,height:zl(l)?l:null,userSelectionActive:f.userSelectionActive,transformString:`translate(${f.transform[0]}px,${f.transform[1]}px) scale(${f.transform[2]}) translate(${y}px,${r}px)`}};function sT({onSelectionContextMenu:f,noPanClassName:u,disableKeyboardA11y:l}){let y=Hu(),{width:r,height:_,transformString:$,userSelectionActive:j}=of(tT,Zu),A=GZ(),F=lf.useRef(null);lf.useEffect(()=>{if(!l)F.current?.focus({preventScroll:!0})},[l]);let U=!j&&r!==null&&_!==null;if(zZ({nodeRef:F,disabled:!U}),!U)return null;let Q=f?(G)=>{let K=y.getState().nodes.filter((E)=>E.selected);f(G,K)}:void 0,W=(G)=>{if(Object.prototype.hasOwnProperty.call(V2,G.key))G.preventDefault(),A({direction:V2[G.key],factor:G.shiftKey?4:1})};return ff.jsx("div",{className:nu(["react-flow__nodesselection","react-flow__container",u]),style:{transform:$},children:ff.jsx("div",{ref:F,className:"react-flow__nodesselection-rect",onContextMenu:Q,tabIndex:l?void 0:-1,onKeyDown:l?void 0:W,style:{width:r,height:_}})})}var aN=typeof window<"u"?window:void 0,oT=(f)=>{return{nodesSelectionActive:f.nodesSelectionActive,userSelectionActive:f.userSelectionActive}};function NZ({children:f,onPaneClick:u,onPaneMouseEnter:l,onPaneMouseMove:y,onPaneMouseLeave:r,onPaneContextMenu:_,onPaneScroll:$,paneClickDistance:j,deleteKeyCode:A,selectionKeyCode:F,selectionOnDrag:U,selectionMode:Q,onSelectionStart:W,onSelectionEnd:G,multiSelectionKeyCode:K,panActivationKeyCode:E,zoomActivationKeyCode:O,elementsSelectable:z,zoomOnScroll:Z,zoomOnPinch:N,panOnScroll:H,panOnScrollSpeed:Y,panOnScrollMode:w,zoomOnDoubleClick:V,panOnDrag:X,defaultViewport:i,translateExtent:m,minZoom:M,maxZoom:c,preventScrolling:C,onSelectionContextMenu:T,noWheelClassName:R,noPanClassName:P,disableKeyboardA11y:n,onViewportChange:B,isControlledViewport:D}){let{nodesSelectionActive:I,userSelectionActive:p}=of(oT,Zu),k=u6(F,{target:aN}),_f=u6(E,{target:aN}),S=_f||X,e=_f||H,$f=U&&S!==!0,Qf=k||p||$f;return TT({deleteKeyCode:A,multiSelectionKeyCode:K}),ff.jsx(ST,{onPaneContextMenu:_,elementsSelectable:z,zoomOnScroll:Z,zoomOnPinch:N,panOnScroll:e,panOnScrollSpeed:Y,panOnScrollMode:w,zoomOnDoubleClick:V,panOnDrag:!k&&S,defaultViewport:i,translateExtent:m,minZoom:M,maxZoom:c,zoomActivationKeyCode:O,preventScrolling:C,noWheelClassName:R,noPanClassName:P,onViewportChange:B,isControlledViewport:D,paneClickDistance:j,selectionOnDrag:$f,children:ff.jsxs(iT,{onSelectionStart:W,onSelectionEnd:G,onPaneClick:u,onPaneMouseEnter:l,onPaneMouseMove:y,onPaneMouseLeave:r,onPaneContextMenu:_,onPaneScroll:$,panOnDrag:S,isSelecting:!!Qf,selectionMode:Q,selectionKeyPressed:k,paneClickDistance:j,selectionOnDrag:$f,children:[f,I&&ff.jsx(sT,{onSelectionContextMenu:T,noPanClassName:P,disableKeyboardA11y:n})]})})}NZ.displayName="FlowRenderer";var aT=lf.memo(NZ),dT=(f)=>(u)=>{return f?A2(u.nodeLookup,{x:0,y:0,width:u.width,height:u.height},u.transform,!0).map((l)=>l.id):Array.from(u.nodeLookup.keys())};function eT(f){return of(lf.useCallback(dT(f),[f]),Zu)}var fn=(f)=>f.updateNodeInternals;function un(){let f=of(fn),[u]=lf.useState(()=>{if(typeof ResizeObserver>"u")return null;return new ResizeObserver((l)=>{let y=new Map;l.forEach((r)=>{let _=r.target.getAttribute("data-id");y.set(_,{id:_,nodeElement:r.target,force:!0})}),f(y)})});return lf.useEffect(()=>{return()=>{u?.disconnect()}},[u]),u}function ln({node:f,nodeType:u,hasDimensions:l,resizeObserver:y}){let r=Hu(),_=lf.useRef(null),$=lf.useRef(null),j=lf.useRef(f.sourcePosition),A=lf.useRef(f.targetPosition),F=lf.useRef(u),U=l&&!!f.internals.handleBounds;return lf.useEffect(()=>{if(_.current&&!f.hidden&&(!U||$.current!==_.current)){if($.current)y?.unobserve($.current);y?.observe(_.current),$.current=_.current}},[U,f.hidden]),lf.useEffect(()=>{return()=>{if($.current)y?.unobserve($.current),$.current=null}},[]),lf.useEffect(()=>{if(_.current){let Q=F.current!==u,W=j.current!==f.sourcePosition,G=A.current!==f.targetPosition;if(Q||W||G)F.current=u,j.current=f.sourcePosition,A.current=f.targetPosition,r.getState().updateNodeInternals(new Map([[f.id,{id:f.id,nodeElement:_.current,force:!0}]]))}},[f.id,u,f.sourcePosition,f.targetPosition]),_}function yn({id:f,onClick:u,onMouseEnter:l,onMouseMove:y,onMouseLeave:r,onContextMenu:_,onDoubleClick:$,nodesDraggable:j,elementsSelectable:A,nodesConnectable:F,nodesFocusable:U,resizeObserver:Q,noDragClassName:W,noPanClassName:G,disableKeyboardA11y:K,rfId:E,nodeTypes:O,nodeClickDistance:z,onError:Z}){let{node:N,internals:H,isParent:Y}=of((Af)=>{let zf=Af.nodeLookup.get(f),Hf=Af.parentLookup.has(f);return{node:zf,internals:zf.internals,isParent:Hf}},Zu),w=N.type||"default",V=O?.[w]||oN[w];if(V===void 0)Z?.("003",a0.error003(w)),w="default",V=O?.default||oN.default;let X=!!(N.draggable||j&&typeof N.draggable>"u"),i=!!(N.selectable||A&&typeof N.selectable>"u"),m=!!(N.connectable||F&&typeof N.connectable>"u"),M=!!(N.focusable||U&&typeof N.focusable>"u"),c=Hu(),C=zF(N),T=ln({node:N,nodeType:w,hasDimensions:C,resizeObserver:Q}),R=zZ({nodeRef:T,disabled:N.hidden||!X,noDragClassName:W,handleSelector:N.dragHandle,nodeId:f,isSelectable:i,nodeClickDistance:z}),P=GZ();if(N.hidden)return null;let n=el(N),B=kT(N),D=i||X||u||l||y||r,I=l?(Af)=>l(Af,{...H.userNode}):void 0,p=y?(Af)=>y(Af,{...H.userNode}):void 0,k=r?(Af)=>r(Af,{...H.userNode}):void 0,_f=_?(Af)=>_(Af,{...H.userNode}):void 0,S=$?(Af)=>$(Af,{...H.userNode}):void 0,e=(Af)=>{let{selectNodesOnDrag:zf,nodeDragThreshold:Hf}=c.getState();if(i&&(!zf||!X||Hf>0))MF({id:f,store:c,nodeRef:T});if(u)u(Af,{...H.userNode})},$f=(Af)=>{if(ZF(Af.nativeEvent)||K)return;if(yF.includes(Af.key)&&i){let zf=Af.key==="Escape";MF({id:f,store:c,unselect:zf,nodeRef:T})}else if(X&&N.selected&&Object.prototype.hasOwnProperty.call(V2,Af.key)){Af.preventDefault();let{ariaLabelConfig:zf}=c.getState();c.setState({ariaLiveMessage:zf["node.a11yDescription.ariaLiveMessage"]({direction:Af.key.replace("Arrow","").toLowerCase(),x:~~H.positionAbsolute.x,y:~~H.positionAbsolute.y})}),P({direction:V2[Af.key],factor:Af.shiftKey?4:1})}},Qf=()=>{if(K||!T.current?.matches(":focus-visible"))return;let{transform:Af,width:zf,height:Hf,autoPanOnNodeFocus:Zf,setCenter:b}=c.getState();if(!Zf)return;if(!(A2(new Map([[f,N]]),{x:0,y:0,width:zf,height:Hf},Af,!0).length>0))b(N.position.x+n.width/2,N.position.y+n.height/2,{zoom:Af[2]})};return ff.jsx("div",{className:nu(["react-flow__node",`react-flow__node-${w}`,{[G]:X},N.className,{selected:N.selected,selectable:i,parent:Y,draggable:X,dragging:R}]),ref:T,style:{zIndex:H.z,transform:`translate(${H.positionAbsolute.x}px,${H.positionAbsolute.y}px)`,pointerEvents:D?"all":"none",visibility:C?"visible":"hidden",...N.style,...B},"data-id":f,"data-testid":`rf__node-${f}`,onMouseEnter:I,onMouseMove:p,onMouseLeave:k,onContextMenu:_f,onClick:e,onDoubleClick:S,onKeyDown:M?$f:void 0,tabIndex:M?0:void 0,onFocus:M?Qf:void 0,role:N.ariaRole??(M?"group":void 0),"aria-roledescription":"node","aria-describedby":K?void 0:`${AZ}-${E}`,"aria-label":N.ariaLabel,...N.domAttributes,children:ff.jsx(xT,{value:f,children:ff.jsx(V,{id:f,data:N.data,type:w,positionAbsoluteX:H.positionAbsolute.x,positionAbsoluteY:H.positionAbsolute.y,selected:N.selected??!1,selectable:i,draggable:X,deletable:N.deletable??!0,isConnectable:m,sourcePosition:N.sourcePosition,targetPosition:N.targetPosition,dragging:R,dragHandle:N.dragHandle,zIndex:H.z,parentId:N.parentId,...n})})})}var rn=lf.memo(yn),_n=(f)=>({nodesDraggable:f.nodesDraggable,nodesConnectable:f.nodesConnectable,nodesFocusable:f.nodesFocusable,elementsSelectable:f.elementsSelectable,onError:f.onError});function ZZ(f){let{nodesDraggable:u,nodesConnectable:l,nodesFocusable:y,elementsSelectable:r,onError:_}=of(_n,Zu),$=eT(f.onlyRenderVisibleElements),j=un();return ff.jsx("div",{className:"react-flow__nodes",style:X2,children:$.map((A)=>{return ff.jsx(rn,{id:A,nodeTypes:f.nodeTypes,nodeExtent:f.nodeExtent,onClick:f.onNodeClick,onMouseEnter:f.onNodeMouseEnter,onMouseMove:f.onNodeMouseMove,onMouseLeave:f.onNodeMouseLeave,onContextMenu:f.onNodeContextMenu,onDoubleClick:f.onNodeDoubleClick,noDragClassName:f.noDragClassName,noPanClassName:f.noPanClassName,rfId:f.rfId,disableKeyboardA11y:f.disableKeyboardA11y,resizeObserver:j,nodesDraggable:u,nodesConnectable:l,nodesFocusable:y,elementsSelectable:r,nodeClickDistance:f.nodeClickDistance,onError:_},A)})})}ZZ.displayName="NodeRenderer";var $n=lf.memo(ZZ);function jn(f){return of(lf.useCallback((l)=>{if(!f)return l.edges.map((r)=>r.id);let y=[];if(l.width&&l.height)for(let r of l.edges){let _=l.nodeLookup.get(r.source),$=l.nodeLookup.get(r.target);if(_&&$&&rN({sourceNode:_,targetNode:$,width:l.width,height:l.height,transform:l.transform}))y.push(r.id)}return y},[f]),Zu)}var An=({color:f="none",strokeWidth:u=1})=>{let l={strokeWidth:u,...f&&{stroke:f}};return ff.jsx("polyline",{className:"arrow",style:l,strokeLinecap:"round",fill:"none",strokeLinejoin:"round",points:"-5,-4 0,0 -5,4"})},Fn=({color:f="none",strokeWidth:u=1})=>{let l={strokeWidth:u,...f&&{stroke:f,fill:f}};return ff.jsx("polyline",{className:"arrowclosed",style:l,strokeLinecap:"round",strokeLinejoin:"round",points:"-5,-4 0,0 -5,4 -5,-4"})},dN={[Gy.Arrow]:An,[Gy.ArrowClosed]:Fn};function Jn(f){let u=Hu();return lf.useMemo(()=>{if(!Object.prototype.hasOwnProperty.call(dN,f))return u.getState().onError?.("009",a0.error009(f)),null;return dN[f]},[f])}var Un=({id:f,type:u,color:l,width:y=12.5,height:r=12.5,markerUnits:_="strokeWidth",strokeWidth:$,orient:j="auto-start-reverse"})=>{let A=Jn(u);if(!A)return null;return ff.jsx("marker",{className:"react-flow__arrowhead",id:f,markerWidth:`${y}`,markerHeight:`${r}`,viewBox:"-10 -10 20 20",markerUnits:_,orient:j,refX:"0",refY:"0",children:ff.jsx(A,{color:l,strokeWidth:$})})},EZ=({defaultColor:f,rfId:u})=>{let l=of((_)=>_.edges),y=of((_)=>_.defaultEdgeOptions),r=lf.useMemo(()=>{return $N(l,{id:u,defaultColor:f,defaultMarkerStart:y?.markerStart,defaultMarkerEnd:y?.markerEnd})},[l,y,u,f]);if(!r.length)return null;return ff.jsx("svg",{className:"react-flow__marker","aria-hidden":"true",children:ff.jsx("defs",{children:r.map((_)=>ff.jsx(Un,{id:_.id,type:_.type,color:_.color,width:_.width,height:_.height,markerUnits:_.markerUnits,strokeWidth:_.strokeWidth,orient:_.orient},_.id))})})};EZ.displayName="MarkerDefinitions";var Qn=lf.memo(EZ);function HZ({x:f,y:u,label:l,labelStyle:y,labelShowBg:r=!0,labelBgStyle:_,labelBgPadding:$=[2,4],labelBgBorderRadius:j=2,children:A,className:F,...U}){let[Q,W]=lf.useState({x:1,y:0,width:0,height:0}),G=nu(["react-flow__edge-textwrapper",F]),K=lf.useRef(null);if(lf.useEffect(()=>{if(K.current){let E=K.current.getBBox();W({x:E.x,y:E.y,width:E.width,height:E.height})}},[l]),!l)return null;return ff.jsxs("g",{transform:`translate(${f-Q.width/2} ${u-Q.height/2})`,className:G,visibility:Q.width?"visible":"hidden",...U,children:[r&&ff.jsx("rect",{width:Q.width+2*$[0],x:-$[0],y:-$[1],height:Q.height+2*$[1],className:"react-flow__edge-textbg",style:_,rx:j,ry:j}),ff.jsx("text",{className:"react-flow__edge-text",y:Q.height/2,dy:"0.3em",ref:K,style:y,children:l}),A]})}HZ.displayName="EdgeText";var Wn=lf.memo(HZ);function b_({path:f,labelX:u,labelY:l,label:y,labelStyle:r,labelShowBg:_,labelBgStyle:$,labelBgPadding:j,labelBgBorderRadius:A,interactionWidth:F=20,...U}){return ff.jsxs(ff.Fragment,{children:[ff.jsx("path",{...U,d:f,fill:"none",className:nu(["react-flow__edge-path",U.className])}),F?ff.jsx("path",{d:f,fill:"none",strokeOpacity:0,strokeWidth:F,className:"react-flow__edge-interaction"}):null,y&&zl(u)&&zl(l)?ff.jsx(Wn,{x:u,y:l,label:y,labelStyle:r,labelShowBg:_,labelBgStyle:$,labelBgPadding:j,labelBgBorderRadius:A}):null]})}function eN({pos:f,x1:u,y1:l,x2:y,y2:r}){if(f===Gf.Left||f===Gf.Right)return[0.5*(u+y),l];return[u,0.5*(l+r)]}function OZ({sourceX:f,sourceY:u,sourcePosition:l=Gf.Bottom,targetX:y,targetY:r,targetPosition:_=Gf.Top}){let[$,j]=eN({pos:l,x1:f,y1:u,x2:y,y2:r}),[A,F]=eN({pos:_,x1:y,y1:r,x2:f,y2:u}),[U,Q,W,G]=Q2({sourceX:f,sourceY:u,targetX:y,targetY:r,sourceControlX:$,sourceControlY:j,targetControlX:A,targetControlY:F});return[`M${f},${u} C${$},${j} ${A},${F} ${y},${r}`,U,Q,W,G]}function qZ(f){return lf.memo(({id:u,sourceX:l,sourceY:y,targetX:r,targetY:_,sourcePosition:$,targetPosition:j,label:A,labelStyle:F,labelShowBg:U,labelBgStyle:Q,labelBgPadding:W,labelBgBorderRadius:G,style:K,markerEnd:E,markerStart:O,interactionWidth:z})=>{let[Z,N,H]=OZ({sourceX:l,sourceY:y,sourcePosition:$,targetX:r,targetY:_,targetPosition:j}),Y=f.isInternal?void 0:u;return ff.jsx(b_,{id:Y,path:Z,labelX:N,labelY:H,label:A,labelStyle:F,labelShowBg:U,labelBgStyle:Q,labelBgPadding:W,labelBgBorderRadius:G,style:K,markerEnd:E,markerStart:O,interactionWidth:z})})}var zn=qZ({isInternal:!1}),VZ=qZ({isInternal:!0});zn.displayName="SimpleBezierEdge";VZ.displayName="SimpleBezierEdgeInternal";function LZ(f){return lf.memo(({id:u,sourceX:l,sourceY:y,targetX:r,targetY:_,label:$,labelStyle:j,labelShowBg:A,labelBgStyle:F,labelBgPadding:U,labelBgBorderRadius:Q,style:W,sourcePosition:G=Gf.Bottom,targetPosition:K=Gf.Top,markerEnd:E,markerStart:O,pathOptions:z,interactionWidth:Z})=>{let[N,H,Y]=f6({sourceX:l,sourceY:y,sourcePosition:G,targetX:r,targetY:_,targetPosition:K,borderRadius:z?.borderRadius,offset:z?.offset,stepPosition:z?.stepPosition}),w=f.isInternal?void 0:u;return ff.jsx(b_,{id:w,path:N,labelX:H,labelY:Y,label:$,labelStyle:j,labelShowBg:A,labelBgStyle:F,labelBgPadding:U,labelBgBorderRadius:Q,style:W,markerEnd:E,markerStart:O,interactionWidth:Z})})}var BZ=LZ({isInternal:!1}),XZ=LZ({isInternal:!0});BZ.displayName="SmoothStepEdge";XZ.displayName="SmoothStepEdgeInternal";function YZ(f){return lf.memo(({id:u,...l})=>{let y=f.isInternal?void 0:u;return ff.jsx(BZ,{...l,id:y,pathOptions:lf.useMemo(()=>({borderRadius:0,offset:l.pathOptions?.offset}),[l.pathOptions?.offset])})})}var Gn=YZ({isInternal:!1}),wZ=YZ({isInternal:!0});Gn.displayName="StepEdge";wZ.displayName="StepEdgeInternal";function DZ(f){return lf.memo(({id:u,sourceX:l,sourceY:y,targetX:r,targetY:_,label:$,labelStyle:j,labelShowBg:A,labelBgStyle:F,labelBgPadding:U,labelBgBorderRadius:Q,style:W,markerEnd:G,markerStart:K,interactionWidth:E})=>{let[O,z,Z]=z2({sourceX:l,sourceY:y,targetX:r,targetY:_}),N=f.isInternal?void 0:u;return ff.jsx(b_,{id:N,path:O,labelX:z,labelY:Z,label:$,labelStyle:j,labelShowBg:A,labelBgStyle:F,labelBgPadding:U,labelBgBorderRadius:Q,style:W,markerEnd:G,markerStart:K,interactionWidth:E})})}var Kn=DZ({isInternal:!1}),TZ=DZ({isInternal:!0});Kn.displayName="StraightEdge";TZ.displayName="StraightEdgeInternal";function nZ(f){return lf.memo(({id:u,sourceX:l,sourceY:y,targetX:r,targetY:_,sourcePosition:$=Gf.Bottom,targetPosition:j=Gf.Top,label:A,labelStyle:F,labelShowBg:U,labelBgStyle:Q,labelBgPadding:W,labelBgBorderRadius:G,style:K,markerEnd:E,markerStart:O,pathOptions:z,interactionWidth:Z})=>{let[N,H,Y]=W2({sourceX:l,sourceY:y,sourcePosition:$,targetX:r,targetY:_,targetPosition:j,curvature:z?.curvature}),w=f.isInternal?void 0:u;return ff.jsx(b_,{id:w,path:N,labelX:H,labelY:Y,label:A,labelStyle:F,labelShowBg:U,labelBgStyle:Q,labelBgPadding:W,labelBgBorderRadius:G,style:K,markerEnd:E,markerStart:O,interactionWidth:Z})})}var Nn=nZ({isInternal:!1}),MZ=nZ({isInternal:!0});Nn.displayName="BezierEdge";MZ.displayName="BezierEdgeInternal";var fZ={default:MZ,straight:TZ,step:wZ,smoothstep:XZ,simplebezier:VZ},uZ={sourceX:null,sourceY:null,targetX:null,targetY:null,sourcePosition:null,targetPosition:null},Zn=(f,u,l)=>{if(l===Gf.Left)return f-u;if(l===Gf.Right)return f+u;return f},En=(f,u,l)=>{if(l===Gf.Top)return f-u;if(l===Gf.Bottom)return f+u;return f},lZ="react-flow__edgeupdater";function yZ({position:f,centerX:u,centerY:l,radius:y=10,onMouseDown:r,onMouseEnter:_,onMouseOut:$,type:j}){return ff.jsx("circle",{onMouseDown:r,onMouseEnter:_,onMouseOut:$,className:nu([lZ,`${lZ}-${j}`]),cx:Zn(u,y,f),cy:En(l,y,f),r:y,stroke:"transparent",fill:"transparent"})}function Hn({isReconnectable:f,reconnectRadius:u,edge:l,sourceX:y,sourceY:r,targetX:_,targetY:$,sourcePosition:j,targetPosition:A,onReconnect:F,onReconnectStart:U,onReconnectEnd:Q,setReconnecting:W,setUpdateHover:G}){let K=Hu(),E=(H,Y)=>{if(H.button!==0)return;let{autoPanOnConnect:w,domNode:V,connectionMode:X,connectionRadius:i,lib:m,onConnectStart:M,cancelConnection:c,nodeLookup:C,rfId:T,panBy:R,updateConnection:P}=K.getState(),n=Y.type==="target",B=(p,k)=>{W(!1),Q?.(p,l,Y.type,k)},D=(p)=>F?.(l,p),I=(p,k)=>{W(!0),U?.(H,l,Y.type),M?.(p,k)};Z2.onPointerDown(H.nativeEvent,{autoPanOnConnect:w,connectionMode:X,connectionRadius:i,domNode:V,handleId:Y.id,nodeId:Y.nodeId,nodeLookup:C,isTarget:n,edgeUpdaterType:Y.type,lib:m,flowId:T,cancelConnection:c,panBy:R,isValidConnection:(...p)=>K.getState().isValidConnection?.(...p)??!0,onConnect:D,onConnectStart:I,onConnectEnd:(...p)=>K.getState().onConnectEnd?.(...p),onReconnectEnd:B,updateConnection:P,getTransform:()=>K.getState().transform,getFromHandle:()=>K.getState().connection.fromHandle,dragThreshold:K.getState().connectionDragThreshold,handleDomNode:H.currentTarget})},O=(H)=>E(H,{nodeId:l.target,id:l.targetHandle??null,type:"target"}),z=(H)=>E(H,{nodeId:l.source,id:l.sourceHandle??null,type:"source"}),Z=()=>G(!0),N=()=>G(!1);return ff.jsxs(ff.Fragment,{children:[(f===!0||f==="source")&&ff.jsx(yZ,{position:j,centerX:y,centerY:r,radius:u,onMouseDown:O,onMouseEnter:Z,onMouseOut:N,type:"source"}),(f===!0||f==="target")&&ff.jsx(yZ,{position:A,centerX:_,centerY:$,radius:u,onMouseDown:z,onMouseEnter:Z,onMouseOut:N,type:"target"})]})}function On({id:f,edgesFocusable:u,edgesReconnectable:l,elementsSelectable:y,onClick:r,onDoubleClick:_,onContextMenu:$,onMouseEnter:j,onMouseMove:A,onMouseLeave:F,reconnectRadius:U,onReconnect:Q,onReconnectStart:W,onReconnectEnd:G,rfId:K,edgeTypes:E,noPanClassName:O,onError:z,disableKeyboardA11y:Z}){let N=of((b)=>b.edgeLookup.get(f)),H=of((b)=>b.defaultEdgeOptions);N=H?{...H,...N}:N;let Y=N.type||"default",w=E?.[Y]||fZ[Y];if(w===void 0)z?.("011",a0.error011(Y)),Y="default",w=E?.default||fZ.default;let V=!!(N.focusable||u&&typeof N.focusable>"u"),X=typeof Q<"u"&&(N.reconnectable||l&&typeof N.reconnectable>"u"),i=!!(N.selectable||y&&typeof N.selectable>"u"),m=lf.useRef(null),[M,c]=lf.useState(!1),[C,T]=lf.useState(!1),R=Hu(),{zIndex:P,sourceX:n,sourceY:B,targetX:D,targetY:I,sourcePosition:p,targetPosition:k}=of(lf.useCallback((b)=>{let t=b.nodeLookup.get(N.source),a=b.nodeLookup.get(N.target);if(!t||!a)return{zIndex:N.zIndex,...uZ};let Nf=_N({id:f,sourceNode:t,targetNode:a,sourceHandle:N.sourceHandle||null,targetHandle:N.targetHandle||null,connectionMode:b.connectionMode,onError:z});return{zIndex:yN({selected:N.selected,zIndex:N.zIndex,sourceNode:t,targetNode:a,elevateOnSelect:b.elevateEdgesOnSelect,zIndexMode:b.zIndexMode}),...Nf||uZ}},[N.source,N.target,N.sourceHandle,N.targetHandle,N.selected,N.zIndex]),Zu),_f=lf.useMemo(()=>N.markerStart?`url('#${G2(N.markerStart,K)}')`:void 0,[N.markerStart,K]),S=lf.useMemo(()=>N.markerEnd?`url('#${G2(N.markerEnd,K)}')`:void 0,[N.markerEnd,K]);if(N.hidden||n===null||B===null||D===null||I===null)return null;let e=(b)=>{let{addSelectedEdges:t,unselectNodesAndEdges:a,multiSelectionActive:Nf}=R.getState();if(i)if(R.setState({nodesSelectionActive:!1}),N.selected&&Nf)a({nodes:[],edges:[N]}),m.current?.blur();else t([f]);if(r)r(b,N)},$f=_?(b)=>{_(b,{...N})}:void 0,Qf=$?(b)=>{$(b,{...N})}:void 0,Af=j?(b)=>{j(b,{...N})}:void 0,zf=A?(b)=>{A(b,{...N})}:void 0,Hf=F?(b)=>{F(b,{...N})}:void 0,Zf=(b)=>{if(!Z&&yF.includes(b.key)&&i){let{unselectNodesAndEdges:t,addSelectedEdges:a}=R.getState();if(b.key==="Escape")m.current?.blur(),t({edges:[N]});else a([f])}};return ff.jsx("svg",{style:{zIndex:P},children:ff.jsxs("g",{className:nu(["react-flow__edge",`react-flow__edge-${Y}`,N.className,O,{selected:N.selected,animated:N.animated,inactive:!i&&!r,updating:M,selectable:i}]),onClick:e,onDoubleClick:$f,onContextMenu:Qf,onMouseEnter:Af,onMouseMove:zf,onMouseLeave:Hf,onKeyDown:V?Zf:void 0,tabIndex:V?0:void 0,role:N.ariaRole??(V?"group":"img"),"aria-roledescription":"edge","data-id":f,"data-testid":`rf__edge-${f}`,"aria-label":N.ariaLabel===null?void 0:N.ariaLabel||`Edge from ${N.source} to ${N.target}`,"aria-describedby":V?`${FZ}-${K}`:void 0,ref:m,...N.domAttributes,children:[!C&&ff.jsx(w,{id:f,source:N.source,target:N.target,type:N.type,selected:N.selected,animated:N.animated,selectable:i,deletable:N.deletable??!0,label:N.label,labelStyle:N.labelStyle,labelShowBg:N.labelShowBg,labelBgStyle:N.labelBgStyle,labelBgPadding:N.labelBgPadding,labelBgBorderRadius:N.labelBgBorderRadius,sourceX:n,sourceY:B,targetX:D,targetY:I,sourcePosition:p,targetPosition:k,data:N.data,style:N.style,sourceHandleId:N.sourceHandle,targetHandleId:N.targetHandle,markerStart:_f,markerEnd:S,pathOptions:"pathOptions"in N?N.pathOptions:void 0,interactionWidth:N.interactionWidth}),X&&ff.jsx(Hn,{edge:N,isReconnectable:X,reconnectRadius:U,onReconnect:Q,onReconnectStart:W,onReconnectEnd:G,sourceX:n,sourceY:B,targetX:D,targetY:I,sourcePosition:p,targetPosition:k,setUpdateHover:c,setReconnecting:T})]})})}var qn=lf.memo(On),Vn=(f)=>({edgesFocusable:f.edgesFocusable,edgesReconnectable:f.edgesReconnectable,elementsSelectable:f.elementsSelectable,connectionMode:f.connectionMode,onError:f.onError});function SZ({defaultMarkerColor:f,onlyRenderVisibleElements:u,rfId:l,edgeTypes:y,noPanClassName:r,onReconnect:_,onEdgeContextMenu:$,onEdgeMouseEnter:j,onEdgeMouseMove:A,onEdgeMouseLeave:F,onEdgeClick:U,reconnectRadius:Q,onEdgeDoubleClick:W,onReconnectStart:G,onReconnectEnd:K,disableKeyboardA11y:E}){let{edgesFocusable:O,edgesReconnectable:z,elementsSelectable:Z,onError:N}=of(Vn,Zu),H=jn(u);return ff.jsxs("div",{className:"react-flow__edges",children:[ff.jsx(Qn,{defaultColor:f,rfId:l}),H.map((Y)=>{return ff.jsx(qn,{id:Y,edgesFocusable:O,edgesReconnectable:z,elementsSelectable:Z,noPanClassName:r,onReconnect:_,onContextMenu:$,onMouseEnter:j,onMouseMove:A,onMouseLeave:F,onClick:U,reconnectRadius:Q,onDoubleClick:W,onReconnectStart:G,onReconnectEnd:K,rfId:l,onError:N,edgeTypes:y,disableKeyboardA11y:E},Y)})]})}SZ.displayName="EdgeRenderer";var Ln=lf.memo(SZ),Bn=(f)=>`translate(${f.transform[0]}px,${f.transform[1]}px) scale(${f.transform[2]})`;function Xn({children:f}){let u=of(Bn);return ff.jsx("div",{className:"react-flow__viewport xyflow__viewport react-flow__container",style:{transform:u},children:f})}function Yn(f){let u=SF(),l=lf.useRef(!1);lf.useEffect(()=>{if(!l.current&&u.viewportInitialized&&f)setTimeout(()=>f(u),1),l.current=!0},[f,u.viewportInitialized])}var wn=(f)=>f.panZoom?.syncViewport;function Dn(f){let u=of(wn),l=Hu();return lf.useEffect(()=>{if(f)u?.(f),l.setState({transform:[f.x,f.y,f.zoom]})},[f,u]),null}function rZ(f){return f.connection.inProgress?{...f.connection,to:i_(f.connection.to,f.transform)}:{...f.connection}}function Tn(f){if(f)return(l)=>{let y=rZ(l);return f(y)};return rZ}function nn(f){let u=Tn(f);return of(u,Zu)}var Mn=(f)=>({nodesConnectable:f.nodesConnectable,isValid:f.connection.isValid,inProgress:f.connection.inProgress,width:f.width,height:f.height});function Sn({containerStyle:f,style:u,type:l,component:y}){let{nodesConnectable:r,width:_,height:$,isValid:j,inProgress:A}=of(Mn,Zu);if(!(_&&r&&A))return null;return ff.jsx("svg",{style:f,width:_,height:$,className:"react-flow__connectionline react-flow__container",children:ff.jsx("g",{className:nu(["react-flow__connection",$F(j)]),children:ff.jsx(PZ,{style:u,type:l,CustomComponent:y,isValid:j})})})}var PZ=({style:f,type:u=dl.Bezier,CustomComponent:l,isValid:y})=>{let{inProgress:r,from:_,fromNode:$,fromHandle:j,fromPosition:A,to:F,toNode:U,toHandle:Q,toPosition:W,pointer:G}=nn();if(!r)return;if(l)return ff.jsx(l,{connectionLineType:u,connectionLineStyle:f,fromNode:$,fromHandle:j,fromX:_.x,fromY:_.y,toX:F.x,toY:F.y,fromPosition:A,toPosition:W,connectionStatus:$F(y),toNode:U,toHandle:Q,pointer:G});let K="",E={sourceX:_.x,sourceY:_.y,sourcePosition:A,targetX:F.x,targetY:F.y,targetPosition:W};switch(u){case dl.Bezier:[K]=W2(E);break;case dl.SimpleBezier:[K]=OZ(E);break;case dl.Step:[K]=f6({...E,borderRadius:0});break;case dl.SmoothStep:[K]=f6(E);break;default:[K]=z2(E)}return ff.jsx("path",{d:K,fill:"none",className:"react-flow__connection-path",style:f})};PZ.displayName="ConnectionLine";var Pn={};function _Z(f=Pn){let u=lf.useRef(f),l=Hu();lf.useEffect(()=>{},[f])}function Cn(){let f=Hu(),u=lf.useRef(!1);lf.useEffect(()=>{},[])}function CZ({nodeTypes:f,edgeTypes:u,onInit:l,onNodeClick:y,onEdgeClick:r,onNodeDoubleClick:_,onEdgeDoubleClick:$,onNodeMouseEnter:j,onNodeMouseMove:A,onNodeMouseLeave:F,onNodeContextMenu:U,onSelectionContextMenu:Q,onSelectionStart:W,onSelectionEnd:G,connectionLineType:K,connectionLineStyle:E,connectionLineComponent:O,connectionLineContainerStyle:z,selectionKeyCode:Z,selectionOnDrag:N,selectionMode:H,multiSelectionKeyCode:Y,panActivationKeyCode:w,zoomActivationKeyCode:V,deleteKeyCode:X,onlyRenderVisibleElements:i,elementsSelectable:m,defaultViewport:M,translateExtent:c,minZoom:C,maxZoom:T,preventScrolling:R,defaultMarkerColor:P,zoomOnScroll:n,zoomOnPinch:B,panOnScroll:D,panOnScrollSpeed:I,panOnScrollMode:p,zoomOnDoubleClick:k,panOnDrag:_f,onPaneClick:S,onPaneMouseEnter:e,onPaneMouseMove:$f,onPaneMouseLeave:Qf,onPaneScroll:Af,onPaneContextMenu:zf,paneClickDistance:Hf,nodeClickDistance:Zf,onEdgeContextMenu:b,onEdgeMouseEnter:t,onEdgeMouseMove:a,onEdgeMouseLeave:Nf,reconnectRadius:o,onReconnect:uf,onReconnectStart:qf,onReconnectEnd:xf,noDragClassName:tf,noWheelClassName:df,noPanClassName:lu,disableKeyboardA11y:Ou,nodeExtent:mu,rfId:R0,viewport:ou,onViewportChange:_0}){return _Z(f),_Z(u),Cn(),Yn(l),Dn(ou),ff.jsx(aT,{onPaneClick:S,onPaneMouseEnter:e,onPaneMouseMove:$f,onPaneMouseLeave:Qf,onPaneContextMenu:zf,onPaneScroll:Af,paneClickDistance:Hf,deleteKeyCode:X,selectionKeyCode:Z,selectionOnDrag:N,selectionMode:H,onSelectionStart:W,onSelectionEnd:G,multiSelectionKeyCode:Y,panActivationKeyCode:w,zoomActivationKeyCode:V,elementsSelectable:m,zoomOnScroll:n,zoomOnPinch:B,zoomOnDoubleClick:k,panOnScroll:D,panOnScrollSpeed:I,panOnScrollMode:p,panOnDrag:_f,defaultViewport:M,translateExtent:c,minZoom:C,maxZoom:T,onSelectionContextMenu:Q,preventScrolling:R,noDragClassName:tf,noWheelClassName:df,noPanClassName:lu,disableKeyboardA11y:Ou,onViewportChange:_0,isControlledViewport:!!ou,children:ff.jsxs(Xn,{children:[ff.jsx(Ln,{edgeTypes:u,onEdgeClick:r,onEdgeDoubleClick:$,onReconnect:uf,onReconnectStart:qf,onReconnectEnd:xf,onlyRenderVisibleElements:i,onEdgeContextMenu:b,onEdgeMouseEnter:t,onEdgeMouseMove:a,onEdgeMouseLeave:Nf,reconnectRadius:o,defaultMarkerColor:P,noPanClassName:lu,disableKeyboardA11y:Ou,rfId:R0}),ff.jsx(Sn,{style:E,type:K,component:O,containerStyle:z}),ff.jsx("div",{className:"react-flow__edgelabel-renderer"}),ff.jsx($n,{nodeTypes:f,onNodeClick:y,onNodeDoubleClick:_,onNodeMouseEnter:j,onNodeMouseMove:A,onNodeMouseLeave:F,onNodeContextMenu:U,nodeClickDistance:Zf,onlyRenderVisibleElements:i,noPanClassName:lu,noDragClassName:tf,disableKeyboardA11y:Ou,nodeExtent:mu,rfId:R0}),ff.jsx("div",{className:"react-flow__viewport-portal"})]})})}CZ.displayName="GraphView";var cn=lf.memo(CZ),$Z=({nodes:f,edges:u,defaultNodes:l,defaultEdges:y,width:r,height:_,fitView:$,fitViewOptions:j,minZoom:A=0.5,maxZoom:F=2,nodeOrigin:U,nodeExtent:Q,zIndexMode:W="basic"}={})=>{let G=new Map,K=new Map,E=new Map,O=new Map,z=y??u??[],Z=l??f??[],N=U??[0,0],H=Q??S_;XF(E,O,z);let{nodesInitialized:Y}=K2(Z,G,K,{nodeOrigin:N,nodeExtent:H,zIndexMode:W}),w=[0,0,1];if($&&r&&_){let V=P_(G,{filter:(M)=>!!((M.width||M.initialWidth)&&(M.height||M.initialHeight))}),{x:X,y:i,zoom:m}=e3(V,r,_,A,F,j?.padding??0.1);w=[X,i,m]}return{rfId:"1",width:r??0,height:_??0,transform:w,nodes:Z,nodesInitialized:Y,nodeLookup:G,parentLookup:K,edges:z,edgeLookup:O,connectionLookup:E,onNodesChange:null,onEdgesChange:null,hasDefaultNodes:l!==void 0,hasDefaultEdges:y!==void 0,panZoom:null,minZoom:A,maxZoom:F,translateExtent:S_,nodeExtent:H,nodesSelectionActive:!1,userSelectionActive:!1,userSelectionRect:null,connectionMode:zy.Strict,domNode:null,paneDragging:!1,noPanClassName:"nopan",nodeOrigin:N,nodeDragThreshold:1,connectionDragThreshold:1,snapGrid:[15,15],snapToGrid:!1,nodesDraggable:!0,nodesConnectable:!0,nodesFocusable:!0,edgesFocusable:!0,edgesReconnectable:!0,elementsSelectable:!0,elevateNodesOnSelect:!0,elevateEdgesOnSelect:!0,selectNodesOnDrag:!0,multiSelectionActive:!1,fitViewQueued:$??!1,fitViewOptions:j,fitViewResolver:null,connection:{..._F},connectionClickStartHandle:null,connectOnClick:!0,ariaLiveMessage:"",autoPanOnConnect:!0,autoPanOnNodeDrag:!0,autoPanOnNodeFocus:!0,autoPanSpeed:15,connectionRadius:20,onError:WF,isValidConnection:void 0,onSelectionChangeHandlers:[],lib:"react",debug:!1,ariaLabelConfig:rF,zIndexMode:W,onNodesChangeMiddlewareMap:new Map,onEdgesChangeMiddlewareMap:new Map}},Rn=({nodes:f,edges:u,defaultNodes:l,defaultEdges:y,width:r,height:_,fitView:$,fitViewOptions:j,minZoom:A,maxZoom:F,nodeOrigin:U,nodeExtent:Q,zIndexMode:W})=>iN((G,K)=>{async function E(){let{nodeLookup:O,panZoom:z,fitViewOptions:Z,fitViewResolver:N,width:H,height:Y,minZoom:w,maxZoom:V}=K();if(!z)return;await aK({nodes:O,width:H,height:Y,panZoom:z,minZoom:w,maxZoom:V},Z),N?.resolve(!0),G({fitViewResolver:null})}return{...$Z({nodes:f,edges:u,width:r,height:_,fitView:$,fitViewOptions:j,minZoom:A,maxZoom:F,nodeOrigin:U,nodeExtent:Q,defaultNodes:l,defaultEdges:y,zIndexMode:W}),setNodes:(O)=>{let{nodeLookup:z,parentLookup:Z,nodeOrigin:N,elevateNodesOnSelect:H,fitViewQueued:Y,zIndexMode:w,nodesSelectionActive:V}=K(),{nodesInitialized:X,hasSelectedNodes:i}=K2(O,z,Z,{nodeOrigin:N,nodeExtent:Q,elevateNodesOnSelect:H,checkEquality:!0,zIndexMode:w}),m=V&&i;if(Y&&X)E(),G({nodes:O,nodesInitialized:X,fitViewQueued:!1,fitViewOptions:void 0,nodesSelectionActive:m});else G({nodes:O,nodesInitialized:X,nodesSelectionActive:m})},setEdges:(O)=>{let{connectionLookup:z,edgeLookup:Z}=K();XF(z,Z,O),G({edges:O})},setDefaultNodesAndEdges:(O,z)=>{if(O){let{setNodes:Z}=K();Z(O),G({hasDefaultNodes:!0})}if(z){let{setEdges:Z}=K();Z(z),G({hasDefaultEdges:!0})}},updateNodeInternals:(O)=>{let{triggerNodeChanges:z,nodeLookup:Z,parentLookup:N,domNode:H,nodeOrigin:Y,nodeExtent:w,debug:V,fitViewQueued:X,zIndexMode:i}=K(),{changes:m,updatedInternals:M}=JN(O,Z,N,H,Y,w,i);if(!M)return;if(AN(Z,N,{nodeOrigin:Y,nodeExtent:w,zIndexMode:i}),X)E(),G({fitViewQueued:!1,fitViewOptions:void 0});else G({});if(m?.length>0){if(V)console.log("React Flow: trigger node changes",m);z?.(m)}},updateNodePositions:(O,z=!1)=>{let Z=[],N=[],{nodeLookup:H,triggerNodeChanges:Y,connection:w,updateConnection:V,onNodesChangeMiddlewareMap:X}=K();for(let[i,m]of O){let M=H.get(i),c=!!(M?.expandParent&&M?.parentId&&m?.position),C={id:i,type:"position",position:c?{x:Math.max(0,m.position.x),y:Math.max(0,m.position.y)}:m.position,dragging:z};if(M&&w.inProgress&&w.fromNode.id===M.id){let T=Ky(M,w.fromHandle,Gf.Left,!0);V({...w,from:T})}if(c&&M.parentId)Z.push({id:i,parentId:M.parentId,rect:{...m.internals.positionAbsolute,width:m.measured.width??0,height:m.measured.height??0}});N.push(C)}if(Z.length>0){let{parentLookup:i,nodeOrigin:m}=K(),M=N2(Z,H,i,m);N.push(...M)}for(let i of X.values())N=i(N);Y(N)},triggerNodeChanges:(O)=>{let{onNodesChange:z,setNodes:Z,nodes:N,hasDefaultNodes:H,debug:Y}=K();if(O?.length){if(H){let w=qT(O,N);Z(w)}if(Y)console.log("React Flow: trigger node changes",O);z?.(O)}},triggerEdgeChanges:(O)=>{let{onEdgesChange:z,setEdges:Z,edges:N,hasDefaultEdges:H,debug:Y}=K();if(O?.length){if(H){let w=VT(O,N);Z(w)}if(Y)console.log("React Flow: trigger edge changes",O);z?.(O)}},addSelectedNodes:(O)=>{let{multiSelectionActive:z,edgeLookup:Z,nodeLookup:N,triggerNodeChanges:H,triggerEdgeChanges:Y}=K();if(z){let w=O.map((V)=>Hr(V,!0));H(w);return}H(v_(N,new Set([...O]),!0)),Y(v_(Z))},addSelectedEdges:(O)=>{let{multiSelectionActive:z,edgeLookup:Z,nodeLookup:N,triggerNodeChanges:H,triggerEdgeChanges:Y}=K();if(z){let w=O.map((V)=>Hr(V,!0));Y(w);return}Y(v_(Z,new Set([...O]))),H(v_(N,new Set,!0))},unselectNodesAndEdges:({nodes:O,edges:z}={})=>{let{edges:Z,nodes:N,nodeLookup:H,triggerNodeChanges:Y,triggerEdgeChanges:w}=K(),V=O?O:N,X=z?z:Z,i=[];for(let M of V){if(!M.selected)continue;let c=H.get(M.id);if(c)c.selected=!1;i.push(Hr(M.id,!1))}let m=[];for(let M of X){if(!M.selected)continue;m.push(Hr(M.id,!1))}Y(i),w(m)},setMinZoom:(O)=>{let{panZoom:z,maxZoom:Z}=K();z?.setScaleExtent([O,Z]),G({minZoom:O})},setMaxZoom:(O)=>{let{panZoom:z,minZoom:Z}=K();z?.setScaleExtent([Z,O]),G({maxZoom:O})},setTranslateExtent:(O)=>{K().panZoom?.setTranslateExtent(O),G({translateExtent:O})},resetSelectedElements:()=>{let{edges:O,nodes:z,triggerNodeChanges:Z,triggerEdgeChanges:N,elementsSelectable:H}=K();if(!H)return;let Y=z.reduce((V,X)=>X.selected?[...V,Hr(X.id,!1)]:V,[]),w=O.reduce((V,X)=>X.selected?[...V,Hr(X.id,!1)]:V,[]);Z(Y),N(w)},setNodeExtent:(O)=>{let{nodes:z,nodeLookup:Z,parentLookup:N,nodeOrigin:H,elevateNodesOnSelect:Y,nodeExtent:w,zIndexMode:V}=K();if(O[0][0]===w[0][0]&&O[0][1]===w[0][1]&&O[1][0]===w[1][0]&&O[1][1]===w[1][1])return;K2(z,Z,N,{nodeOrigin:H,nodeExtent:O,elevateNodesOnSelect:Y,checkEquality:!1,zIndexMode:V}),G({nodeExtent:O})},panBy:(O)=>{let{transform:z,width:Z,height:N,panZoom:H,translateExtent:Y}=K();return UN({delta:O,panZoom:H,transform:z,translateExtent:Y,width:Z,height:N})},setCenter:async(O,z,Z)=>{let{width:N,height:H,maxZoom:Y,panZoom:w}=K();if(!w)return Promise.resolve(!1);let V=typeof Z?.zoom<"u"?Z.zoom:Y;return await w.setViewport({x:N/2-O*V,y:H/2-z*V,zoom:V},{duration:Z?.duration,ease:Z?.ease,interpolate:Z?.interpolate}),Promise.resolve(!0)},cancelConnection:()=>{G({connection:{..._F}})},updateConnection:(O)=>{G({connection:O})},reset:()=>G({...$Z()})}},Object.is);function xn({initialNodes:f,initialEdges:u,defaultNodes:l,defaultEdges:y,initialWidth:r,initialHeight:_,initialMinZoom:$,initialMaxZoom:j,initialFitViewOptions:A,fitView:F,nodeOrigin:U,nodeExtent:Q,zIndexMode:W,children:G}){let[K]=lf.useState(()=>Rn({nodes:f,edges:u,defaultNodes:l,defaultEdges:y,width:r,height:_,fitView:F,minZoom:$,maxZoom:j,fitViewOptions:A,nodeOrigin:U,nodeExtent:Q,zIndexMode:W}));return ff.jsx(lT,{value:K,children:ff.jsx(XT,{children:G})})}function vn({children:f,nodes:u,edges:l,defaultNodes:y,defaultEdges:r,width:_,height:$,fitView:j,fitViewOptions:A,minZoom:F,maxZoom:U,nodeOrigin:Q,nodeExtent:W,zIndexMode:G}){if(lf.useContext(L2))return ff.jsx(ff.Fragment,{children:f});return ff.jsx(xn,{initialNodes:u,initialEdges:l,defaultNodes:y,defaultEdges:r,initialWidth:_,initialHeight:$,fitView:j,initialFitViewOptions:A,initialMinZoom:F,initialMaxZoom:U,nodeOrigin:Q,nodeExtent:W,zIndexMode:G,children:f})}var bn={width:"100%",height:"100%",overflow:"hidden",position:"relative",zIndex:0};function hn({nodes:f,edges:u,defaultNodes:l,defaultEdges:y,className:r,nodeTypes:_,edgeTypes:$,onNodeClick:j,onEdgeClick:A,onInit:F,onMove:U,onMoveStart:Q,onMoveEnd:W,onConnect:G,onConnectStart:K,onConnectEnd:E,onClickConnectStart:O,onClickConnectEnd:z,onNodeMouseEnter:Z,onNodeMouseMove:N,onNodeMouseLeave:H,onNodeContextMenu:Y,onNodeDoubleClick:w,onNodeDragStart:V,onNodeDrag:X,onNodeDragStop:i,onNodesDelete:m,onEdgesDelete:M,onDelete:c,onSelectionChange:C,onSelectionDragStart:T,onSelectionDrag:R,onSelectionDragStop:P,onSelectionContextMenu:n,onSelectionStart:B,onSelectionEnd:D,onBeforeDelete:I,connectionMode:p,connectionLineType:k=dl.Bezier,connectionLineStyle:_f,connectionLineComponent:S,connectionLineContainerStyle:e,deleteKeyCode:$f="Backspace",selectionKeyCode:Qf="Shift",selectionOnDrag:Af=!1,selectionMode:zf=Nr.Full,panActivationKeyCode:Hf="Space",multiSelectionKeyCode:Zf=R_()?"Meta":"Control",zoomActivationKeyCode:b=R_()?"Meta":"Control",snapToGrid:t,snapGrid:a,onlyRenderVisibleElements:Nf=!1,selectNodesOnDrag:o,nodesDraggable:uf,autoPanOnNodeFocus:qf,nodesConnectable:xf,nodesFocusable:tf,nodeOrigin:df=JZ,edgesFocusable:lu,edgesReconnectable:Ou,elementsSelectable:mu=!0,defaultViewport:R0=GT,minZoom:ou=0.5,maxZoom:_0=2,translateExtent:x0=S_,preventScrolling:au=!0,nodeExtent:Jf,defaultMarkerColor:Sf="#b1b1b7",zoomOnScroll:$0=!0,zoomOnPinch:nf=!0,panOnScroll:pu=!1,panOnScrollSpeed:du=0.5,panOnScrollMode:Iu=B1.Free,zoomOnDoubleClick:iu=!0,panOnDrag:ll=!0,onPaneClick:v0,onPaneMouseEnter:a_,onPaneMouseMove:wy,onPaneMouseLeave:Dy,onPaneScroll:d,onPaneContextMenu:Bf,paneClickDistance:Mf=1,nodeClickDistance:Pf=0,children:ju,onReconnect:gf,onReconnectStart:Ju,onReconnectEnd:eu,onEdgeContextMenu:El,onEdgeDoubleClick:N6,onEdgeMouseEnter:d_,onEdgeMouseMove:Z6,onEdgeMouseLeave:B0,reconnectRadius:xl=10,onNodesChange:E6,onEdgesChange:H6,noDragClassName:e_="nodrag",noWheelClassName:O6="nowheel",noPanClassName:Hl="nopan",fitView:f$,fitViewOptions:Pu,connectOnClick:d2,attributionPosition:q6,proOptions:r1,defaultEdgeOptions:Xr,elevateNodesOnSelect:Ty=!0,elevateEdgesOnSelect:u$=!1,disableKeyboardA11y:e2=!1,autoPanOnConnect:Yr,autoPanOnNodeDrag:OJ,autoPanSpeed:V6,connectionRadius:f5,isValidConnection:u5,onError:l5,style:w1,id:ny,nodeDragThreshold:My,connectionDragThreshold:X0,viewport:L6,onViewportChange:l$,width:yl,height:B6,colorMode:y5="light",debug:r5,onScroll:y$,ariaLabelConfig:D1,zIndexMode:r$="basic",..._$},_5){let $$=ny||"1",j$=ET(y5),Ol=lf.useCallback((T1)=>{T1.currentTarget.scrollTo({top:0,left:0,behavior:"instant"}),y$?.(T1)},[y$]);return ff.jsx("div",{"data-testid":"rf__wrapper",..._$,onScroll:Ol,style:{...w1,...bn},ref:_5,className:nu(["react-flow",r,j$]),id:ny,role:"application",children:ff.jsxs(vn,{nodes:f,edges:u,width:yl,height:B6,fitView:f$,fitViewOptions:Pu,minZoom:ou,maxZoom:_0,nodeOrigin:df,nodeExtent:Jf,zIndexMode:r$,children:[ff.jsx(ZT,{nodes:f,edges:u,defaultNodes:l,defaultEdges:y,onConnect:G,onConnectStart:K,onConnectEnd:E,onClickConnectStart:O,onClickConnectEnd:z,nodesDraggable:uf,autoPanOnNodeFocus:qf,nodesConnectable:xf,nodesFocusable:tf,edgesFocusable:lu,edgesReconnectable:Ou,elementsSelectable:mu,elevateNodesOnSelect:Ty,elevateEdgesOnSelect:u$,minZoom:ou,maxZoom:_0,nodeExtent:Jf,onNodesChange:E6,onEdgesChange:H6,snapToGrid:t,snapGrid:a,connectionMode:p,translateExtent:x0,connectOnClick:d2,defaultEdgeOptions:Xr,fitView:f$,fitViewOptions:Pu,onNodesDelete:m,onEdgesDelete:M,onDelete:c,onNodeDragStart:V,onNodeDrag:X,onNodeDragStop:i,onSelectionDrag:R,onSelectionDragStart:T,onSelectionDragStop:P,onMove:U,onMoveStart:Q,onMoveEnd:W,noPanClassName:Hl,nodeOrigin:df,rfId:$$,autoPanOnConnect:Yr,autoPanOnNodeDrag:OJ,autoPanSpeed:V6,onError:l5,connectionRadius:f5,isValidConnection:u5,selectNodesOnDrag:o,nodeDragThreshold:My,connectionDragThreshold:X0,onBeforeDelete:I,debug:r5,ariaLabelConfig:D1,zIndexMode:r$}),ff.jsx(cn,{onInit:F,onNodeClick:j,onEdgeClick:A,onNodeMouseEnter:Z,onNodeMouseMove:N,onNodeMouseLeave:H,onNodeContextMenu:Y,onNodeDoubleClick:w,nodeTypes:_,edgeTypes:$,connectionLineType:k,connectionLineStyle:_f,connectionLineComponent:S,connectionLineContainerStyle:e,selectionKeyCode:Qf,selectionOnDrag:Af,selectionMode:zf,deleteKeyCode:$f,multiSelectionKeyCode:Zf,panActivationKeyCode:Hf,zoomActivationKeyCode:b,onlyRenderVisibleElements:Nf,defaultViewport:R0,translateExtent:x0,minZoom:ou,maxZoom:_0,preventScrolling:au,zoomOnScroll:$0,zoomOnPinch:nf,zoomOnDoubleClick:iu,panOnScroll:pu,panOnScrollSpeed:du,panOnScrollMode:Iu,panOnDrag:ll,onPaneClick:v0,onPaneMouseEnter:a_,onPaneMouseMove:wy,onPaneMouseLeave:Dy,onPaneScroll:d,onPaneContextMenu:Bf,paneClickDistance:Mf,nodeClickDistance:Pf,onSelectionContextMenu:n,onSelectionStart:B,onSelectionEnd:D,onReconnect:gf,onReconnectStart:Ju,onReconnectEnd:eu,onEdgeContextMenu:El,onEdgeDoubleClick:N6,onEdgeMouseEnter:d_,onEdgeMouseMove:Z6,onEdgeMouseLeave:B0,reconnectRadius:xl,defaultMarkerColor:Sf,noDragClassName:e_,noWheelClassName:O6,noPanClassName:Hl,rfId:$$,disableKeyboardA11y:e2,nodeExtent:Jf,viewport:L6,onViewportChange:l$}),ff.jsx(zT,{onSelectionChange:C}),ju,ff.jsx(FT,{proOptions:r1,position:q6}),ff.jsx(AT,{rfId:$$,disableKeyboardA11y:e2})]})})}var cZ=QZ(hn);var lh=a0.error014();function mn({dimensions:f,lineWidth:u,variant:l,className:y}){return ff.jsx("path",{strokeWidth:u,d:`M${f[0]/2} 0 V${f[1]} M0 ${f[1]/2} H${f[0]}`,className:nu(["react-flow__background-pattern",l,y])})}function pn({radius:f,className:u}){return ff.jsx("circle",{cx:f,cy:f,r:f,className:nu(["react-flow__background-pattern","dots",u])})}var Zy;(function(f){f.Lines="lines",f.Dots="dots",f.Cross="cross"})(Zy||(Zy={}));var In={[Zy.Dots]:1,[Zy.Lines]:1,[Zy.Cross]:6},gn=(f)=>({transform:f.transform,patternId:`pattern-${f.rfId}`});function iZ({id:f,variant:u=Zy.Dots,gap:l=20,size:y,lineWidth:r=1,offset:_=0,color:$,bgColor:j,style:A,className:F,patternClassName:U}){let Q=lf.useRef(null),{transform:W,patternId:G}=of(gn,Zu),K=y||In[u],E=u===Zy.Dots,O=u===Zy.Cross,z=Array.isArray(l)?l:[l,l],Z=[z[0]*W[2]||1,z[1]*W[2]||1],N=K*W[2],H=Array.isArray(_)?_:[_,_],Y=O?[N,N]:Z,w=[H[0]*W[2]||1+Y[0]/2,H[1]*W[2]||1+Y[1]/2],V=`${G}${f?f:""}`;return ff.jsxs("svg",{className:nu(["react-flow__background",F]),style:{...A,...X2,"--xy-background-color-props":j,"--xy-background-pattern-color-props":$},ref:Q,"data-testid":"rf__background",children:[ff.jsx("pattern",{id:V,x:W[0]%Z[0],y:W[1]%Z[1],width:Z[0],height:Z[1],patternUnits:"userSpaceOnUse",patternTransform:`translate(-${w[0]},-${w[1]})`,children:E?ff.jsx(pn,{radius:N/2,className:U}):ff.jsx(mn,{dimensions:Y,lineWidth:r,variant:u,className:U})}),ff.jsx("rect",{x:"0",y:"0",width:"100%",height:"100%",fill:`url(#${V})`})]})}iZ.displayName="Background";var RZ=lf.memo(iZ);function kn(){return ff.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32",children:ff.jsx("path",{d:"M32 18.133H18.133V32h-4.266V18.133H0v-4.266h13.867V0h4.266v13.867H32z"})})}function tn(){return ff.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 5",children:ff.jsx("path",{d:"M0 0h32v4.2H0z"})})}function sn(){return ff.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 30",children:ff.jsx("path",{d:"M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z"})})}function on(){return ff.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 25 32",children:ff.jsx("path",{d:"M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z"})})}function an(){return ff.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 25 32",children:ff.jsx("path",{d:"M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z"})})}function q2({children:f,className:u,...l}){return ff.jsx("button",{type:"button",className:nu(["react-flow__controls-button",u]),...l,children:f})}var dn=(f)=>({isInteractive:f.nodesDraggable||f.nodesConnectable||f.elementsSelectable,minZoomReached:f.transform[2]<=f.minZoom,maxZoomReached:f.transform[2]>=f.maxZoom,ariaLabelConfig:f.ariaLabelConfig});function xZ({style:f,showZoom:u=!0,showFitView:l=!0,showInteractive:y=!0,fitViewOptions:r,onZoomIn:_,onZoomOut:$,onFitView:j,onInteractiveChange:A,className:F,children:U,position:Q="bottom-left",orientation:W="vertical","aria-label":G}){let K=Hu(),{isInteractive:E,minZoomReached:O,maxZoomReached:z,ariaLabelConfig:Z}=of(dn,Zu),{zoomIn:N,zoomOut:H,fitView:Y}=SF(),w=()=>{N(),_?.()},V=()=>{H(),$?.()},X=()=>{Y(r),j?.()},i=()=>{K.setState({nodesDraggable:!E,nodesConnectable:!E,elementsSelectable:!E}),A?.(!E)};return ff.jsxs(B2,{className:nu(["react-flow__controls",W==="horizontal"?"horizontal":"vertical",F]),position:Q,style:f,"data-testid":"rf__controls","aria-label":G??Z["controls.ariaLabel"],children:[u&&ff.jsxs(ff.Fragment,{children:[ff.jsx(q2,{onClick:w,className:"react-flow__controls-zoomin",title:Z["controls.zoomIn.ariaLabel"],"aria-label":Z["controls.zoomIn.ariaLabel"],disabled:z,children:ff.jsx(kn,{})}),ff.jsx(q2,{onClick:V,className:"react-flow__controls-zoomout",title:Z["controls.zoomOut.ariaLabel"],"aria-label":Z["controls.zoomOut.ariaLabel"],disabled:O,children:ff.jsx(tn,{})})]}),l&&ff.jsx(q2,{className:"react-flow__controls-fitview",onClick:X,title:Z["controls.fitView.ariaLabel"],"aria-label":Z["controls.fitView.ariaLabel"],children:ff.jsx(sn,{})}),y&&ff.jsx(q2,{className:"react-flow__controls-interactive",onClick:i,title:Z["controls.interactive.ariaLabel"],"aria-label":Z["controls.interactive.ariaLabel"],children:E?ff.jsx(an,{}):ff.jsx(on,{})}),U]})}xZ.displayName="Controls";var vZ=lf.memo(xZ);function en({id:f,x:u,y:l,width:y,height:r,style:_,color:$,strokeColor:j,strokeWidth:A,className:F,borderRadius:U,shapeRendering:Q,selected:W,onClick:G}){let{background:K,backgroundColor:E}=_||{},O=$||K||E;return ff.jsx("rect",{className:nu(["react-flow__minimap-node",{selected:W},F]),x:u,y:l,rx:U,ry:U,width:y,height:r,style:{fill:O,stroke:j,strokeWidth:A},shapeRendering:Q,onClick:G?(z)=>G(z,f):void 0})}var fM=lf.memo(en),uM=(f)=>f.nodes.map((u)=>u.id),TF=(f)=>f instanceof Function?f:()=>f;function lM({nodeStrokeColor:f,nodeColor:u,nodeClassName:l="",nodeBorderRadius:y=5,nodeStrokeWidth:r,nodeComponent:_=fM,onClick:$}){let j=of(uM,Zu),A=TF(u),F=TF(f),U=TF(l),Q=typeof window>"u"||!!window.chrome?"crispEdges":"geometricPrecision";return ff.jsx(ff.Fragment,{children:j.map((W)=>ff.jsx(rM,{id:W,nodeColorFunc:A,nodeStrokeColorFunc:F,nodeClassNameFunc:U,nodeBorderRadius:y,nodeStrokeWidth:r,NodeComponent:_,onClick:$,shapeRendering:Q},W))})}function yM({id:f,nodeColorFunc:u,nodeStrokeColorFunc:l,nodeClassNameFunc:y,nodeBorderRadius:r,nodeStrokeWidth:_,shapeRendering:$,NodeComponent:j,onClick:A}){let{node:F,x:U,y:Q,width:W,height:G}=of((K)=>{let E=K.nodeLookup.get(f);if(!E)return{node:void 0,x:0,y:0,width:0,height:0};let O=E.internals.userNode,{x:z,y:Z}=E.internals.positionAbsolute,{width:N,height:H}=el(O);return{node:O,x:z,y:Z,width:N,height:H}},Zu);if(!F||F.hidden||!zF(F))return null;return ff.jsx(j,{x:U,y:Q,width:W,height:G,style:F.style,selected:!!F.selected,className:y(F),color:u(F),borderRadius:r,strokeColor:l(F),strokeWidth:_,shapeRendering:$,onClick:A,id:F.id})}var rM=lf.memo(yM),_M=lf.memo(lM),$M=200,jM=150,AM=(f)=>!f.hidden,FM=(f)=>{let u={x:-f.transform[0]/f.transform[2],y:-f.transform[1]/f.transform[2],width:f.width/f.transform[2],height:f.height/f.transform[2]};return{viewBB:u,boundingRect:f.nodeLookup.size>0?UF(P_(f.nodeLookup,{filter:AM}),u):u,rfId:f.rfId,panZoom:f.panZoom,translateExtent:f.translateExtent,flowWidth:f.width,flowHeight:f.height,ariaLabelConfig:f.ariaLabelConfig}},JM="react-flow__minimap-desc";function bZ({style:f,className:u,nodeStrokeColor:l,nodeColor:y,nodeClassName:r="",nodeBorderRadius:_=5,nodeStrokeWidth:$,nodeComponent:j,bgColor:A,maskColor:F,maskStrokeColor:U,maskStrokeWidth:Q,position:W="bottom-right",onClick:G,onNodeClick:K,pannable:E=!1,zoomable:O=!1,ariaLabel:z,inversePan:Z,zoomStep:N=1,offsetScale:H=5}){let Y=Hu(),w=lf.useRef(null),{boundingRect:V,viewBB:X,rfId:i,panZoom:m,translateExtent:M,flowWidth:c,flowHeight:C,ariaLabelConfig:T}=of(FM,Zu),R=f?.width??$M,P=f?.height??jM,n=V.width/R,B=V.height/P,D=Math.max(n,B),I=D*R,p=D*P,k=H*D,_f=V.x-(I-V.width)/2-k,S=V.y-(p-V.height)/2-k,e=I+k*2,$f=p+k*2,Qf=`${JM}-${i}`,Af=lf.useRef(0),zf=lf.useRef();Af.current=D,lf.useEffect(()=>{if(w.current&&m)return zf.current=ZN({domNode:w.current,panZoom:m,getTransform:()=>Y.getState().transform,getViewScale:()=>Af.current}),()=>{zf.current?.destroy()}},[m]),lf.useEffect(()=>{zf.current?.update({translateExtent:M,width:c,height:C,inversePan:Z,pannable:E,zoomStep:N,zoomable:O})},[E,O,Z,N,M,c,C]);let Hf=G?(t)=>{let[a,Nf]=zf.current?.pointer(t)||[0,0];G(t,{x:a,y:Nf})}:void 0,Zf=K?lf.useCallback((t,a)=>{let Nf=Y.getState().nodeLookup.get(a).internals.userNode;K(t,Nf)},[]):void 0,b=z??T["minimap.ariaLabel"];return ff.jsx(B2,{position:W,style:{...f,"--xy-minimap-background-color-props":typeof A==="string"?A:void 0,"--xy-minimap-mask-background-color-props":typeof F==="string"?F:void 0,"--xy-minimap-mask-stroke-color-props":typeof U==="string"?U:void 0,"--xy-minimap-mask-stroke-width-props":typeof Q==="number"?Q*D:void 0,"--xy-minimap-node-background-color-props":typeof y==="string"?y:void 0,"--xy-minimap-node-stroke-color-props":typeof l==="string"?l:void 0,"--xy-minimap-node-stroke-width-props":typeof $==="number"?$:void 0},className:nu(["react-flow__minimap",u]),"data-testid":"rf__minimap",children:ff.jsxs("svg",{width:R,height:P,viewBox:`${_f} ${S} ${e} ${$f}`,className:"react-flow__minimap-svg",role:"img","aria-labelledby":Qf,ref:w,onClick:Hf,children:[b&&ff.jsx("title",{id:Qf,children:b}),ff.jsx(_M,{onClick:Zf,nodeColor:y,nodeStrokeColor:l,nodeBorderRadius:_,nodeClassName:r,nodeStrokeWidth:$,nodeComponent:j}),ff.jsx("path",{className:"react-flow__minimap-mask",d:`M${_f-k},${S-k}h${e+k*2}v${$f+k*2}h${-e-k*2}z + M${X.x},${X.y}h${X.width}v${X.height}h${-X.width}z`,fillRule:"evenodd",pointerEvents:"none"})]})})}bZ.displayName="MiniMap";var yh=lf.memo(bZ),UM=(f)=>(u)=>f?`${Math.max(1/u.transform[2],1)}`:void 0,QM={[Ny.Line]:"right",[Ny.Handle]:"bottom-right"};function WM({nodeId:f,position:u,variant:l=Ny.Handle,className:y,style:r=void 0,children:_,color:$,minWidth:j=10,minHeight:A=10,maxWidth:F=Number.MAX_VALUE,maxHeight:U=Number.MAX_VALUE,keepAspectRatio:Q=!1,resizeDirection:W,autoScale:G=!0,shouldResize:K,onResizeStart:E,onResize:O,onResizeEnd:z}){let Z=KZ(),N=typeof f==="string"?f:Z,H=Hu(),Y=lf.useRef(null),w=l===Ny.Handle,V=of(lf.useCallback(UM(w&&G),[w,G]),Zu),X=lf.useRef(null),i=u??QM[l];lf.useEffect(()=>{if(!Y.current||!N)return;if(!X.current)X.current=VN({domNode:Y.current,nodeId:N,getStoreItems:()=>{let{nodeLookup:M,transform:c,snapGrid:C,snapToGrid:T,nodeOrigin:R,domNode:P}=H.getState();return{nodeLookup:M,transform:c,snapGrid:C,snapToGrid:T,nodeOrigin:R,paneDomNode:P}},onChange:(M,c)=>{let{triggerNodeChanges:C,nodeLookup:T,parentLookup:R,nodeOrigin:P}=H.getState(),n=[],B={x:M.x,y:M.y},D=T.get(N);if(D&&D.expandParent&&D.parentId){let I=D.origin??P,p=M.width??D.measured.width??0,k=M.height??D.measured.height??0,_f={id:D.id,parentId:D.parentId,rect:{width:p,height:k,...GF({x:M.x??D.position.x,y:M.y??D.position.y},{width:p,height:k},D.parentId,T,I)}},S=N2([_f],T,R,P);n.push(...S),B.x=M.x?Math.max(I[0]*p,M.x):void 0,B.y=M.y?Math.max(I[1]*k,M.y):void 0}if(B.x!==void 0&&B.y!==void 0){let I={id:N,type:"position",position:{...B}};n.push(I)}if(M.width!==void 0&&M.height!==void 0){let p={id:N,type:"dimensions",resizing:!0,setAttributes:!W?!0:W==="horizontal"?"width":"height",dimensions:{width:M.width,height:M.height}};n.push(p)}for(let I of c){let p={...I,type:"position"};n.push(p)}C(n)},onEnd:({width:M,height:c})=>{let C={id:N,type:"dimensions",resizing:!1,dimensions:{width:M,height:c}};H.getState().triggerNodeChanges([C])}});return X.current.update({controlPosition:i,boundaries:{minWidth:j,minHeight:A,maxWidth:F,maxHeight:U},keepAspectRatio:Q,resizeDirection:W,onResizeStart:E,onResize:O,onResizeEnd:z,shouldResize:K}),()=>{X.current?.destroy()}},[i,j,A,F,U,Q,E,O,z,K]);let m=i.split("-");return ff.jsx("div",{className:nu(["react-flow__resize-control","nodrag",...m,l,y]),ref:Y,style:{...r,scale:V,...$&&{[w?"backgroundColor":"borderColor"]:$}},children:_})}var rh=lf.memo(WM);var q=qy.default.createElement,{useEffect:l1}=qy.default,C0=qy.default.useState,Oy=qy.default.useRef,A6=[{id:"in-left",side:"left",position:Gf.Left,style:{top:"50%"}},{id:"in-top-left",side:"top",slot:"left",slotIndex:-1,position:Gf.Top,style:{left:"28%"}},{id:"in-top-mid",side:"top",slot:"mid",slotIndex:0,position:Gf.Top,style:{left:"50%"}},{id:"in-top-right",side:"top",slot:"right",slotIndex:1,position:Gf.Top,style:{left:"72%"}},{id:"in-bottom-left",side:"bottom",slot:"left",slotIndex:-1,position:Gf.Bottom,style:{left:"28%"}},{id:"in-bottom-mid",side:"bottom",slot:"mid",slotIndex:0,position:Gf.Bottom,style:{left:"50%"}},{id:"in-bottom-right",side:"bottom",slot:"right",slotIndex:1,position:Gf.Bottom,style:{left:"72%"}}],r6=[{id:"out-right",position:Gf.Right,style:{top:"50%"}}],hZ=["#4eb7a8","#d7a13a","#69aee8","#e0835f","#b7d86b","#d98bd2","#5fc6bf"],h_=236,m_=88,mZ=15000,zM=10,CF=96,f1=72,cF=64,pZ=12;function Y2(){return typeof document>"u"||document.visibilityState!=="hidden"}function IZ(f,u){let l=Number.parseFloat(String(f||""));return Number.isFinite(l)?l/100:u}function GM(f,u,l){let y=String(f.side||"");if(y!=="top"&&y!=="bottom")return 0;let r=Number(f.slotIndex||0),_=y==="top"?"in-top-mid":"in-bottom-mid",$=u.get(f.id)||0,j=u.get(_)||0;if(r===0)return j===0?-26:28+$*74;let A=l===0?Math.abs(r)*2:Math.sign(l)===Math.sign(r)?-3:3;if(j>0&&$===0)return-14+A;return 8+$*74+A}function w2(f){let u=f.filter((_,$)=>{let j=f[$-1];return!j||Math.abs(j.x-_.x)>0.5||Math.abs(j.y-_.y)>0.5});if(u.length<2)return"";let l=`M ${u[0].x},${u[0].y}`,y=u[0];for(let _=1;_0.5||Math.abs(W.y-y.y)>0.5)l+=` L ${W.x},${W.y}`;l+=` Q ${j.x},${j.y} ${G.x},${G.y}`,y=G}let r=u[u.length-1];return`${l} L ${r.x},${r.y}`}function QE(f,u,l,y,r,_,$=""){let j=l>=f,A=Math.max(1,Math.abs(l-f)),F=Math.abs(y-u),U=Math.max(34,Math.min(118,A*0.26)),Q=Math.min(280,Math.abs(_));if(j&&r===Gf.Left&&Q<4&&F<28&&A<420)return`M ${f},${u} C ${f+U},${u} ${l-U},${y} ${l},${y}`;if(j&&r===Gf.Left&&($==="direct-forward-left"||A<=260&&F<=210)){let z=Math.max(42,Math.min(140,A*0.48)),Z=Math.max(-28,Math.min(28,_*0.18));return`M ${f},${u} C ${f+z},${u+Z} ${l-z},${y} ${l},${y}`}if(j){let z=f+U;if(r===Gf.Top||r===Gf.Bottom){let H=r===Gf.Top?-1:1,Y=y+H*(54+Q*0.42);return w2([{x:f,y:u},{x:z,y:u},{x:z+Math.min(120,A*0.18),y:Y},{x:l,y:Y},{x:l,y:y+H*34},{x:l,y}])}let Z=l-U,N=(u+y)/2+_;return w2([{x:f,y:u},{x:z,y:u},{x:z+Math.min(110,A*0.16),y:N},{x:Z-Math.min(90,A*0.12),y:N},{x:Z,y},{x:l,y}])}let K=r===Gf.Bottom?1:r===Gf.Top?-1:_>=0?1:-1,E=Math.max(f,l)+92+Math.min(180,Q*0.52),O=K<0?Math.min(u,y)-84-Q*0.62:Math.max(u,y)+84+Q*0.62;if(r===Gf.Top||r===Gf.Bottom)return w2([{x:f,y:u},{x:f+U,y:u},{x:E,y:O},{x:l,y:O},{x:l,y:y+K*38},{x:l,y}]);return w2([{x:f,y:u},{x:f+U,y:u},{x:E,y:O},{x:l-U,y:O},{x:l-U,y},{x:l,y}])}function KM({data:f}){return q("div",{className:"pipeline-flow-node-body"},A6.map((u)=>q(Or,{key:u.id,id:u.id,type:"target",position:u.position,isConnectable:!1,className:`pipeline-flow-handle input ${u.side} slot-${u.slot||"mid"}`,style:u.style})),r6.map((u)=>q(Or,{key:u.id,id:u.id,type:"source",position:u.position,isConnectable:!1,className:"pipeline-flow-handle output right",style:u.style})),f?.label)}function NM({id:f,sourceX:u,sourceY:l,targetX:y,targetY:r,targetPosition:_,markerEnd:$,markerStart:j,style:A,data:F}){let U=Number(F?.laneOffset||0),Q=QE(u,l,y,r,_,U,String(F?.routeMode||""));return q(b_,{id:f,path:Q,markerEnd:$,markerStart:j,style:A,interactionWidth:28})}var ZM={pipelineCurve:NM},EM={pipelineNode:KM};function M2(f){if(!f)return"--";let u=new Date(f);if(Number.isNaN(u.getTime()))return"--";return Uu(u)}function Nl(f){let u=Number(f);if(!Number.isFinite(u)||u<0)return"--";let l=Math.round(u/1000);if(l<60)return`${l}s`;if(l<3600)return`${Math.floor(l/60)}m ${l%60}s`;return`${Math.floor(l/3600)}h ${Math.floor(l%3600/60)}m`}function iF(f){let u=Number(f);if(!Number.isFinite(u))return"--";return u.toLocaleString("zh-CN")}function gZ(f){let u=Number(f);if(!Number.isFinite(u))return"--";return`${Math.round(Math.max(0,Math.min(1,u))*100)}%`}function Yf(f){return typeof f==="object"&&f!==null&&!Array.isArray(f)}function Lf(f){return Array.isArray(f)?f:[]}function vf(f){if(!f)return null;let u=new Date(f);return Number.isNaN(u.getTime())?null:u.getTime()}function F6(f){return Number.isFinite(Number(f))?new Date(Number(f)).toISOString():""}function Q6(...f){for(let u of f){let l=vf(u);if(l!==null)return new Date(l).toISOString()}return""}function aF(...f){let u=f.map(vf).filter((l)=>l!==null);return u.length>0?new Date(Math.max(...u)).toISOString():""}function dF(f){return["succeeded","failed","skipped","cancelled","canceled","completed"].includes(String(f||"").toLowerCase())}function WE(f){let u=KE(f).toLowerCase();return["running","active","in-progress","in_progress"].includes(u)}function kZ(f,u="status"){return f.reduce((l,y)=>{let r=String(y?.[u]||"unknown").toLowerCase();return l[r]=(l[r]||0)+1,l},{})}function zE(f){if(!f||typeof f!=="string")return null;try{let u=JSON.parse(f);return Yf(u)?u:null}catch{return null}}function RF(f){let u=f.map(zE).filter((_)=>Boolean(_)),l=u.flatMap((_)=>[_.timestamp,_.createdAt,_.updatedAt]).filter(Boolean),y=aF(...l),r=Array.from(new Set(u.map((_)=>String(_.event||_.action||_.type||"")).filter(Boolean))).slice(0,3);return{total:f.length,parsed:u.length,lastAt:y,eventKinds:r}}function S2(f){if(f===null||f===void 0)return"--";if(typeof f==="boolean")return f?"是":"否";if(typeof f==="number")return String(f);if(typeof f==="string")return f.length>80?`${f.slice(0,77)}...`:f;if(Array.isArray(f))return`${f.length} 项`;if(typeof f==="object")return`${Object.keys(f).length} 字段`;return String(f)}function GE(f,u=280){if(f===null||f===void 0)return"";let y=(typeof f==="string"?f:String(f)).replace(/\r\n/gu,` +`).trim();return y.length>u?`${y.slice(0,Math.max(0,u-1))}...`:y}function KE(f){if(typeof f==="string")return f;if(Yf(f))return String(f.status||f.state||f.phase||"unknown");return"unknown"}function HM(f){return f.filter((u)=>u&&u.value!==void 0&&u.value!==null&&String(u.value)!=="")}function pF({items:f}){let u=HM(Lf(f));return q("div",{className:"pipeline-kv-grid"},u.map((l)=>q("span",{key:l.label},q("b",null,l.label),q("span",null,l.value))))}function eF({items:f}){let u=Lf(f).map((l)=>String(l||"")).filter(Boolean);if(u.length===0)return null;return q("div",{className:"pipeline-chip-row"},u.map((l,y)=>q("span",{key:`${y}-${l}`},l)))}function IF(f,u){let l=String(u?.procedureRunId||""),y=Lf(f?.procedureRuns);return y.find((r)=>String(Zl(r))===l)||y.at(-1)||null}function OM(f,u){let l=String(u||"");if(!l)return null;return Lf(f?.procedureRuns).find((y)=>Zl(y)===l)||null}function xF(f){return Lf(f?.attempts).length}function tZ(f){return Lf(f?.attempts).reduce((u,l)=>u+v2(l).length,0)}function v2(f){return Lf(f?.opencodeMessages?.steps).filter(Yf)}function NE(f){let u=String(f?.status||"").toLowerCase();if(["error","failed","failure"].includes(u))return"failed";if(["completed","succeeded","success"].includes(u))return"succeeded";if(["running","started","in_progress"].includes(u))return"running";return"unknown"}function qM(f,u){let l=gF(f.map((_)=>_?.agent)).slice(0,3),y=gF(f.map((_)=>_?.model)).slice(0,3),r=u.length<=2?u.map((_)=>`session ${_}`):[`sessions ${u.length}`,...u.slice(0,2).map((_)=>`session ${_}`)];return[...l.map((_)=>`agent ${_}`),...y.map((_)=>`model ${_}`),...r]}function _6(f,u=0){return String(f?.messageId||f?.index||"")||`step-${u}`}function VM({steps:f,sessionIds:u,sessionFacts:l,matchedStepKey:y}){let r=Lf(f),_=r.findIndex((O,z)=>_6(O,z)===y),$=_>=0?r[_]:null,j=r.flatMap((O)=>[vf(O?.createdAt),vf(O?.completedAt)]).filter((O)=>O!==null),A=j.length>0?Math.min(...j):null,F=j.length>0?Math.max(...j):null,U=A!==null&&F!==null?Math.max(0,F-A):null,Q=r.reduce((O,z)=>O+Lf(z?.parts).filter((Z)=>String(Z?.type||"").toLowerCase()==="tool").length,0),W=r.reduce((O,z)=>O+Lf(z?.parts).filter((Z)=>["text","reasoning"].includes(String(Z?.type||"").toLowerCase())).length,0),G=r.reduce((O,z)=>O+Lf(z?.parts).filter((Z)=>String(Z?.type||"").toLowerCase()==="tool"&&NE(Z)==="failed").length,0),K=[`${r.length} steps`,`${u.length} sessions`,`${W} messages`,`${Q} tools`,U!==null?`duration ${Nl(U)}`:"",G>0?`${G} failed tools`:""].filter(Boolean),E=$?[`Step ${$?.index??_+1}`,String($?.role||"role --"),$?.model?`model ${$.model}`:"",$?.finish?`finish ${$.finish}`:"",$?.durationMs!==void 0&&$?.durationMs!==null?`duration ${Nl($.durationMs)}`:""].filter(Boolean):[];return q("section",{className:"pipeline-trace-timeline","data-testid":"pipeline-step-timeline"},q("div",{className:"pipeline-trace-head"},q("div",null,q("b",null,"OpenCode Trace"),q("span",null,"Trace 使用 Code Queue 统一样式展示完整 agent loop;Pipeline 旧 step/message/tool 卡片样式已废弃。")),q("div",{className:"pipeline-trace-session-head","data-testid":"pipeline-step-timeline-session"},q("span",null,K.join(" / ")||"Trace"),l.length>0?q(eF,{items:l}):null)),$?q("div",{className:"pipeline-trace-focus","data-testid":"pipeline-trace-matched-step"},q("span",{className:"codex-output-channel"},"Matched"),q("strong",null,`Gantt selection -> ${E.join(" / ")}`),q("time",null,`${M2($?.createdAt)} -> ${M2($?.completedAt)}`)):null,q(j8,{port:tz,input:r,className:"codex-transcript pipeline-trace",testId:"pipeline-opencode-step-trace",emptyText:"暂无 OpenCode Trace 输出",keepRecentToolCalls:3}))}function $6(f){return Lf(f).flatMap((u)=>{if(Yf(u))return[u];let l=zE(u);return l?[l]:[]})}function cl(f){return String(f?.event||f?.action||f?.requestedAction||f?.type||"").toLowerCase()}function qr(f){return Q6(f?.timestamp,f?.createdAt,f?.updatedAt,f?.startedAt,f?.finishedAt)}function LM(f){return vf(qr(f))}function b2(f){return String(f?.attempt||f?.id||"")}function gF(f){let u=new Set,l=[];for(let y of f){let r=String(y||"");if(!r||u.has(r))continue;u.add(r),l.push(r)}return l}function sZ(f){switch(String(f||"").toLowerCase()){case"monitor":return"monitor";case"webui":return"webui";case"cli":return"cli";case"system":return"runner";default:return String(f||"--")}}function Vr(f){return String(f?.requestedAction||f?.action||"").toLowerCase()}function j6(f){switch(Vr(f)){case"guide":return"引导";case"modify":return"修改";case"approve":return"审核通过";case"restart":return"重启";case"redo":return"重做";default:return String(f?.requestedAction||f?.action||"控制")}}function oZ(f){switch(cl(f)){case"initial-prompt-delivered":return"初始 prompt";case"append-prompt-delivered":return"追加 prompt";case"append-prompt-queued":return"追加 prompt 已排队";case"monitor-prompt-delivered":return"Monitor prompt";case"node-long-running-observation":return"长任务观察";case"node-finished":return"节点完成";case"oa-policy-downstream-evaluated":return"OA 下游策略";case"control-command-queued":return`${j6(f)} 已发起`;case"control-command-applied":return`${j6(f)} 已生效`;case"control-command-ignored":return`${j6(f)} 已忽略`;default:return String(f?.event||f?.action||f?.requestedAction||"event")}}function aZ(f){return GE(f?.promptPreview||f?.reasonPreview||f?.prompt||f?.reason||"",240)}function BM(f){let u=String(f?.prompt||""),l=String(f?.reason||f?.restartReason||""),y=u?"":String(f?.promptPreview||""),r=l?"":String(f?.reasonPreview||"");return[u||y?{label:u?"prompt":"prompt preview",value:u||y}:null,l||r?{label:l?"reason":"reason preview",value:l||r}:null,Lf(f?.resetNodeIds).length>0?{label:"reset nodes",value:Lf(f.resetNodeIds).join(", ")}:null,Lf(f?.runningResetNodeIds).length>0?{label:"interrupted running nodes",value:Lf(f.runningResetNodeIds).join(", ")}:null,Lf(f?.interruptedProcedureRunIds).length>0?{label:"interrupted procedures",value:Lf(f.interruptedProcedureRunIds).join(", ")}:null,f?.interruptedProcedureRunId?{label:"interrupted procedure",value:String(f.interruptedProcedureRunId)}:null].filter(Boolean)}function vF(f){let u=v2(f),l=u.map((A)=>vf(A?.createdAt)).filter((A)=>A!==null),y=u.map((A)=>vf(A?.completedAt)??vf(A?.createdAt)).filter((A)=>A!==null),r=$6(f?.controlEventRecords).map((A)=>LM(A)).filter((A)=>A!==null),_=Lf(f?.assistantOutputs).map((A)=>vf(A?.updatedAt)).filter((A)=>A!==null),$=l[0]??r[0]??_[0]??null,j=y.at(-1)??r.at(-1)??_.at(-1)??$;return{startMs:$,endMs:j}}function XM(f,u,l,y,r=""){let _=Lf(f?.procedureRuns).filter((j)=>h2(j,u)===l);if(_.length===0)return null;if(r){let j=_.find((A)=>Zl(A)===r);if(j)return j}if(y===null)return _.at(-1)||null;let $=_.find((j)=>{let A=vf(D2(j,f)),F=vf(T2(j,f))??A;return A!==null&&F!==null&&y>=A-1000&&y<=F+1000});if($)return $;return _.slice().sort((j,A)=>{let F=vf(D2(j,f))??y,U=vf(T2(j,f))??F,Q=vf(D2(A,f))??y,W=vf(T2(A,f))??Q,G=Math.min(Math.abs(F-y),Math.abs(U-y)),K=Math.min(Math.abs(Q-y),Math.abs(W-y));return G-K})[0]||null}function ZE(f,u){let l=Lf(f?.attempts).filter(Yf);if(l.length===0)return null;let y=String(u?.attempt||"");if(y){let $=l.find((j)=>b2(j)===y);if($)return $}let r=Number.isFinite(Number(u?.ms))?Number(u.ms):null;if(r===null)return l.at(-1)||null;let _=l.find(($)=>{let j=vF($);return Number.isFinite(j.startMs)&&Number.isFinite(j.endMs)&&r>=Number(j.startMs)-1000&&r<=Number(j.endMs)+1000});if(_)return _;return l.slice().sort(($,j)=>{let A=vF($),F=vF(j),U=Math.min(Math.abs(Number(A.startMs??r)-r),Math.abs(Number(A.endMs??r)-r)),Q=Math.min(Math.abs(Number(F.startMs??r)-r),Math.abs(Number(F.endMs??r)-r));return U-Q})[0]||l.at(-1)||null}function EE(f,u){let l=v2(f);if(l.length===0)return{step:null,stepIndex:-1,stepKey:""};if(u===null){let _=l[0];return{step:_,stepIndex:0,stepKey:_6(_,0)}}for(let _=0;_=j-1000&&u<=A+1000)return{step:$,stepIndex:_,stepKey:_6($,_)}}let y=l.findIndex((_)=>{let $=vf(_?.createdAt)??vf(_?.completedAt);return $!==null&&$>=u});if(y>=0){let _=l[y];return{step:_,stepIndex:y,stepKey:_6(_,y)}}let r=Math.max(0,l.length-1);return{step:l[r],stepIndex:r,stepKey:_6(l[r],r)}}function YM(f,u){let l=String(u?.runId||f?.runId||"");if(String(u?.mode||"")==="interval"){let F=u?.interval||{},U=IF(f,F)||F.raw||{};return{mode:"interval",runId:l,interval:F,marker:null,nodeId:String(F?.nodeId||h2(U,l)||""),procedure:U,attempt:null,matchedStep:null,matchedStepIndex:-1,matchedStepKey:""}}let y=Yf(u?.marker)?u.marker:{},r=Number.isFinite(Number(y?.ms))?Number(y.ms):null,_=String(y?.nodeId||""),$=_?XM(f,l,_,r,String(y?.procedureRunId||"")):null,j=$?ZE($,y):null,A=j?EE(j,r):{step:null,stepIndex:-1,stepKey:""};return{mode:"event",runId:l,interval:null,marker:y,nodeId:_,procedure:$,attempt:j,matchedStep:A.step,matchedStepIndex:A.stepIndex,matchedStepKey:A.stepKey}}function wM({procedure:f,matchedStepKey:u="",matchedAttemptId:l=""}){let y=Lf(f?.attempts);if(y.length===0)return q(e0,{title:"暂无 attempt 详情",text:"当前 procedure 还没有可展示的 attempt / OpenCode Trace;若刚点击甘特线,请等待 node 详情抓取完成。"});return y.map((r,_)=>{let $=r?.opencodeMessages||{},j=v2(r),A=Lf($.sessionIds).map((W)=>String(W)).filter(Boolean),F=qM(j,A),U=b2(r)||`attempt-${_+1}`,Q=j.reduce((W,G)=>W+Lf(G?.parts).filter((K)=>String(K?.type||"").toLowerCase()==="tool"&&NE(K)==="failed").length,0);return q("article",{key:U,className:`pipeline-attempt-card ${l===U?"matched":""}`},q("div",{className:"pipeline-attempt-head"},q("div",null,q("strong",null,U),q("span",null,$.source||"opencode")),q("div",{className:"pipeline-attempt-badges"},q("span",null,`${j.length} steps`),q("span",null,`${$.toolCallCount??"--"} tools`),Q>0?q("span",{className:"danger"},`${Q} failed`):null)),q(pF,{items:[{label:"messages",value:$.messageCount??"--"},{label:"steps",value:$.stepCount??j.length},{label:"tools",value:$.toolCallCount??"--"},{label:"updated",value:Kf($.updatedAt)},{label:"sessions",value:A.join(", ")||"--"}]}),j.length===0?q("p",{className:"muted paragraph"},"当前 attempt 尚未返回 OpenCode Trace;请确认 D601 pipeline-control 已重建并重新抓取。"):q(VM,{steps:j,sessionIds:A,sessionFacts:F,matchedStepKey:u}))})}function bF(f,u){return`${f}::${u}`}function P2(f,u,l){if(!Yf(f))return null;return String(f.runId||"")===u&&String(f.nodeId||"")===l?f:null}function DM(f,u){let l=Yf(f)?f:{};if(!Yf(u))return l;let y=Lf(u.attempts),r=Lf(l.attempts);return{...l,...u,attempts:y.length>0?y:r}}function TM(f,u,l,y){if(!P2(u,l,y))return f;let r=Lf(u.procedureRuns),_=Yf(f)?f:{};return{..._,...u,controlCommands:Lf(u.controlCommands).length>0?u.controlCommands:_.controlCommands,controlEvents:Lf(u.controlEvents).length>0?u.controlEvents:_.controlEvents,procedureRuns:r.length>0?r:_.procedureRuns}}function nM({selection:f,runDetails:u,nodeDetails:l,nodeDetailsState:y,onRaw:r,onCollapse:_}){if(!f?.mode)return q("aside",{className:"pipeline-gantt-detail-panel empty","data-testid":"pipeline-gantt-detail-panel"},q("div",{className:"pipeline-gantt-detail-head"},q("div",null,q("span",{className:"panel-eyebrow"},"Gantt Detail"),q(_u,{title:"未选择元素",level:3})),q("button",{type:"button",className:"ghost-btn mini",onClick:_,"data-testid":"pipeline-gantt-sidebar-collapse"},"收起")),q(e0,{title:"选择一条执行线或一个控制点",text:"点击甘特图中的 node 执行线、prompt 点或控制点,在这里查看结构化过程和 OpenCode step。"}));let $=String(f?.runId||""),j=String(f?.interval?.nodeId||f?.marker?.nodeId||""),A=u?.runId===$?u.details:null,F=P2(l,$,j),U=String(y?.runId||"")===$&&String(y?.nodeId||"")===j,Q=TM(A,F,$,j),W=(String(u?.runId||"")!==$||Boolean(u?.loading))&&!Q,G=String(u?.runId||"")===$?String(u?.error||""):"",K=U?String(y?.error||""):"",E=Q?YM(Q,f):null,O=E?.interval||f?.interval||null,z=E?.marker||f?.marker||null,Z=String(O?.procedureRunId||z?.procedureRunId||""),N=F?OM(F,Z)||IF(F,O||{procedureRunId:Z}):null,H=E?.procedure||(Q?IF(Q,O||{procedureRunId:Z}):null)||O?.raw||{};if(N&&(xF(H)===0||tZ(N)>=tZ(H)))H=DM(H,N);let Y=E?.attempt||null,w=String(E?.matchedStepKey||"");if(!Y&&z&&xF(H)>0)Y=ZE(H,z),w=String(EE(Y,Number.isFinite(Number(z?.ms))?Number(z.ms):null).stepKey||"");let V=b2(Y),X=xF(H)>0,i=U&&Boolean(y?.loading)&&!X,m=Boolean(W||i),M=[X?"":G,K].filter(Boolean).join(" / "),c=U&&y?.fetchedAt?y.fetchedAt:u?.fetchedAt,C=KE(H?.status||O?.status||z?.status||z?.event),T=f?.mode==="event"?z?.label||oZ(z?.raw||z)||"event":E?.nodeId||O?.nodeId||"node",R=z?BM(z?.raw||z):[],P=z?[cl(z?.raw||z)?`event ${cl(z?.raw||z)}`:"",z?.promptEvent?`prompt ${z.promptEvent}`:"",z?.action?`action ${z.action}`:"",z?.sourceKind?`source ${sZ(z.sourceKind)}`:"",z?.sourceNodeId?`from ${z.sourceNodeId}`:"",z?.targetNodeId?`to ${z.targetNodeId}`:"",z?.snapReason?`draw ${z.snapReason}`:""].filter(Boolean):[];return q("aside",{className:"pipeline-gantt-detail-panel","data-testid":"pipeline-gantt-detail-panel"},q("div",{className:"pipeline-gantt-detail-head"},q("div",null,q("span",{className:"panel-eyebrow"},f?.mode==="event"?"Gantt Event Detail":"Gantt Line Detail"),q(_u,{title:T,level:3,loading:m})),q("div",{className:"pipeline-gantt-detail-head-actions"},q(Vy,{status:C},C),q("button",{type:"button",className:"ghost-btn mini",onClick:_,"data-testid":"pipeline-gantt-sidebar-collapse"},"收起"))),z?q("article",{className:"pipeline-event-card"},q("div",{className:"pipeline-event-card-head"},q("strong",null,z?.label||oZ(z?.raw||z)),q(eF,{items:P})),q(pF,{items:[{label:"event time",value:Kf(z?.timestampIso||z?.timestamp||"--")},z?.snapped?{label:"drawn time",value:Kf(z?.renderedTimestampIso||z?.ms)}:null,{label:"node",value:z?.nodeId||"--"},{label:"procedure",value:z?.procedureRunId||Zl(H)||"--"},{label:"attempt",value:z?.attempt||V||"--"},{label:"source kind",value:z?.sourceKind?sZ(z.sourceKind):"--"},{label:"source node",value:z?.sourceNodeId||"--"},{label:"target node",value:z?.targetNodeId||"--"},{label:"command",value:z?.commandId||z?.eventId||"--"},z?.snapReason?{label:"placement",value:z.snapReason}:null]}),R.length>0?q("div",{className:"pipeline-event-blocks"},R.map((n,B)=>q("section",{key:`${n.label}-${B}`,className:"pipeline-event-text-block"},q("b",null,n.label),q("p",null,n.value)))):null,aZ(z?.raw||z)?q("p",{className:"pipeline-text-preview"},aZ(z?.raw||z)):null):null,q(pF,{items:[{label:"epoch",value:$||O?.runId||"--"},{label:"node",value:E?.nodeId||O?.nodeId||z?.nodeId||"--"},{label:"procedure",value:O?.procedureRunId||z?.procedureRunId||Zl(H)||"--"},{label:"started",value:Kf(O?.startedAt||H?.startedAt)},{label:"finished",value:Kf(O?.finishedAt||H?.finishedAt)},{label:"duration",value:Nl(O?.durationMs||H?.durationMs)},{label:"fetched",value:c?Uu(c):"--"},E?.matchedStep?{label:"matched step",value:`Step ${E.matchedStep.index??E.matchedStepIndex+1}`}:null]}),m?q("div",{className:"form-success"},i?"正在抓取该 node 的 attempt / Trace...":"正在抓取 epoch 执行过程..."):null,q(Au,{error:M}),q("div",{className:"pipeline-gantt-detail-actions"},q(il,{title:`Procedure ${O?.procedureRunId||z?.procedureRunId||E?.nodeId||"node"}`,data:H,onOpen:r,testId:"raw-pipeline-gantt-procedure"}),z?q(il,{title:`Pipeline event ${z?.id||z?.commandId||z?.eventId||E?.nodeId||"event"}`,data:z?.raw||z,onOpen:r,testId:"raw-pipeline-gantt-event"}):null,Q?q(il,{title:`Pipeline run ${$||"--"}`,data:Q,onOpen:r,testId:"raw-pipeline-gantt-node-details"}):null),!m&&!Zl(H)&&!z?q(e0,{title:"暂无过程详情",text:"当前选择还没有可匹配的 procedure 运行记录。"}):null,!m&&Zl(H)?q(wM,{procedure:H,matchedStepKey:w,matchedAttemptId:V}):null)}function MM({value:f}){let l=String(f||"--").split(/([_-])/u);return q(qy.default.Fragment,null,l.map((y,r)=>y==="-"||y==="_"?q(qy.default.Fragment,{key:r},y,q("wbr",null)):q(qy.default.Fragment,{key:r},y)))}async function Ey(f,u={}){return Df(f,{invalidJsonPrefix:"Pipeline 返回了无效 JSON",...u})}function Vy({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return q("span",{className:`status-badge ${l}`},u||f||"unknown")}function L0({label:f,value:u,hint:l,tone:y}){return q("article",{className:`metric-card ${y||""}`},q("div",{className:"metric-label"},f),q("div",{className:"metric-value"},u),q("div",{className:"metric-hint"},l))}function u1({title:f,eyebrow:u,actions:l,children:y,className:r,loading:_}){return q("section",{className:`panel ${r||""}`},q("div",{className:"panel-head"},q("div",null,u?q("p",{className:"panel-eyebrow"},u):null,q(_u,{title:f,loading:_})),l?q("div",{className:"panel-actions"},l):null),q("div",{className:"panel-body"},y))}function il({title:f,data:u,onOpen:l,testId:y}){return q("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:()=>l(f,u)},"查看原始JSON")}function Kl({title:f,subtitle:u,facts:l,data:y,onRaw:r,testId:_}){let $=Lf(l).map((j)=>String(j||"")).filter(Boolean);return q("article",{className:"pipeline-evidence-row"},q("div",{className:"pipeline-evidence-main"},q("strong",null,f),u?q("span",null,u):null),q("div",{className:"pipeline-evidence-facts"},$.map((j,A)=>q("span",{key:`${A}-${j.slice(0,16)}`},j))),y!==void 0?q(il,{title:f,data:y,onOpen:r,testId:_}):null)}function e0({title:f,text:u}){return q("div",{className:"empty-state"},q("strong",null,f),q("span",null,u))}function SM(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function PM(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function CM(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function cM(f){return{components:Array.isArray(f?.registry?.components)?f.registry.components:[],pipelines:Array.isArray(f?.pipelines)?f.pipelines:[],runs:Array.isArray(f?.runs)?f.runs:[]}}function dZ(f,u,l){let y=f?._unidesk?.arrayLimits?.[u],r=Number(y?.originalLength);return Number.isFinite(r)?r:l}function HE(f){if(!f||typeof f!=="object"||Array.isArray(f))return"--";return`${f.componentClass||"--"}/${f.id||"--"}`}function C2(f){if(!f||typeof f!=="object"||Array.isArray(f))return"";let u=String(f.componentClass||"").trim(),l=String(f.id||"").trim();return u&&l?`${u}/${l}`:""}function fJ(f){return f?.config&&typeof f.config==="object"&&!Array.isArray(f.config)?f.config:{}}function OE(f){let u=fJ(f),l=Array.isArray(u.nodes)?u.nodes:Array.isArray(f?.nodes)?f.nodes:[],y=new Map;for(let $ of l){let j=String($?.id||$?.nodeId||"");if(j)y.set(j,{...$,id:j})}let r=uJ(f),_=($)=>{if($&&!y.has($))y.set($,{id:$})};for(let $ of lJ(f))J6($).forEach(_);for(let $ of r)_(String($?.from||$?.source||"")),_(String($?.to||$?.target||""));return Array.from(y.values())}function uJ(f){let u=fJ(f);return Array.isArray(u.edges)?u.edges:Array.isArray(f?.edges)?f.edges:[]}function lJ(f){let u=fJ(f);return Array.isArray(u.topologicalBatches)?u.topologicalBatches:Array.isArray(f?.topologicalBatches)?f.topologicalBatches:[]}function iM(f){let u=new Map;for(let l of f){let y=C2(l);if(y)u.set(y,l);let r=Array.isArray(l?.refs)?l.refs:[];for(let _ of r){let $=C2(_);if($)u.set($,l)}}return u}function eZ(f,u){let l=u.get(C2(f?.componentRef));if(l)return l;let y=C2({componentClass:f?.kind,id:f?.id});return y?u.get(y)||null:null}function fE(f,u){let l=qE(f,u);return String(l?.status||"pending")}function qE(f,u){return(Array.isArray(f?.nodes)?f.nodes:[]).find((y)=>y?.nodeId===u||y?.id===u)||null}function RM(f){return f.reduce((u,l)=>{let y=String(l?.status||"unknown").toLowerCase();return u[y]=(u[y]||0)+1,u},{})}function xM(f){if(Array.isArray(f?.scorers))return f.scorers.filter(Yf);if(Array.isArray(f?.summary?.scorers))return f.summary.scorers.filter(Yf);if(Array.isArray(f?.artifact?.summary?.scorers))return f.artifact.summary.scorers.filter(Yf);return[]}function vM(f){if(Yf(f?.run))return f.run;if(Yf(f?.runSummary))return f.runSummary;return null}function bM(f,u){if(!Yf(f)&&!Yf(u))return null;if(!Yf(f))return u;if(!Yf(u))return f;return{...f,...u,request:Yf(f.request)||Yf(u.request)?{...Yf(f.request)?f.request:{},...Yf(u.request)?u.request:{}}:u.request??f.request,artifact:Yf(f.artifact)||Yf(u.artifact)?{...Yf(f.artifact)?f.artifact:{},...Yf(u.artifact)?u.artifact:{}}:u.artifact??f.artifact,summary:Yf(f.summary)||Yf(u.summary)?{...Yf(f.summary)?f.summary:{},...Yf(u.summary)?u.summary:{}}:u.summary??f.summary}}function c2(f){let u=xM(f),l=u.find((U)=>Yf(U?.score))||u[0]||null,y=Yf(l?.score)?l.score:{},r=Number(y.passed),_=Number(y.total),$=Number(y.ratio),j=Number.isFinite($)?$:Number.isFinite(r)&&Number.isFinite(_)&&_>0?r/_:null,A=j===null?null:Math.round(Math.max(0,Math.min(100,j<=1?j*100:j))),F=String(y.text||(Number.isFinite(r)&&Number.isFinite(_)?`${r}/${_}`:""));return{scorer:l,scorers:u,score:y,passed:Number.isFinite(r)?r:null,total:Number.isFinite(_)?_:null,percent:A,text:F}}function kF(f){let u=c2(f);return u.text||(u.scorers.length>0?String(u.scorer?.status||"pending"):"--")}function yJ(f){let u=c2(f);if(u.total>0&&u.passed===u.total)return"succeeded";if(u.total>0&&u.passed>0)return"running";if(u.scorers.length>0)return"failed";return"pending"}function hM(f){return Array.isArray(f?.items)?f.items.filter(Yf):[]}function mM({run:f}){let u=kF(f);return q("span",{className:`pipeline-score-badge ${yJ(f)}`},`score ${u}`)}function pM({run:f,onRaw:u}){let y=c2(f).scorers;if(!f)return q(e0,{title:"暂无评分",text:"选择一个 epoch 后会显示 scorer 结果。"});if(y.length===0)return q("div",{className:"pipeline-score-empty"},q("strong",null,"评分器等待中"),q("span",null,"DAG 完成后,Pipeline control backend 会把 scorer summary 追加到 run artifact,并通过 UniDesk 显示。"));return q("div",{className:"pipeline-score-board","data-testid":"pipeline-score-board"},y.map((r,_)=>{let $=c2({scorers:[r]}),j=hM(r),A=$.percent??0;return q("article",{key:`${r.scorerId||r.component||_}`,className:`pipeline-score-card ${yJ({scorers:[r]})}`},q("div",{className:"pipeline-score-head"},q("div",null,q("span",null,r.scorerId||r.component||"scorer"),q("strong",null,$.text||r.status||"--")),q(Vy,{status:r.status||"unknown"},r.status||"unknown")),q("div",{className:"pipeline-score-meter","aria-label":`score ${A}%`},q("span",{style:{width:`${A}%`}})),q("div",{className:"pipeline-score-facts"},q("span",null,`${A}%`),q("span",null,r.component||"--"),q("span",null,r.applicationCheckoutRef||"--")),j.length>0?q("div",{className:"pipeline-score-items"},j.map((F)=>q("span",{key:`${F.id||F.filter}`,className:`pipeline-score-item ${String(F.status||"").toLowerCase()}`,title:`${F.filter||"--"} / ran=${F.ran??"?"}`},q("b",null,F.id||"--"),q("small",null,F.status||"--")))):q("p",{className:"muted paragraph"},"当前 scorer 尚未返回 item 级结果。"),r.error?q("p",{className:"pipeline-score-error"},GE(r.error,360)):null,q("div",{className:"panel-actions inline-actions"},q(il,{title:`Scorer ${r.scorerId||_}`,data:r,onOpen:u,testId:"raw-pipeline-score"})))}))}function IM(f){let u=f.reduce((l,y)=>{let r=String(y?.componentClass||"unknown");return l[r]=(l[r]||0)+1,l},{});return Object.entries(u).map(([l,y])=>({name:l,count:Number(y)})).sort((l,y)=>y.count-l.count||l.name.localeCompare(y.name))}function J6(f){if(Array.isArray(f))return f.map((u)=>typeof u==="string"?u:String(u?.id||u?.nodeId||"")).filter(Boolean);if(Array.isArray(f?.nodes))return J6(f.nodes);if(Array.isArray(f?.nodeIds))return J6(f.nodeIds);return[]}function gM(f){return Yf(f?.instanceInputs?.monitor)?f.instanceInputs.monitor:{}}function VE(f,u){if(String(f?.kind||"").toLowerCase()!=="procedure")return!1;let l=gM(f);if(f?.instanceInputs?.monitorMode===!0||l.enabled===!0)return!0;let y=HE(f?.componentRef);return String(u?.id||u?.config?.id||y||"").toLowerCase().includes("monitor")}function kM(f){return f.filter((u)=>VE(u)).map((u)=>String(u?.id||"")).filter(Boolean)}function tM(f,u){if(u.length===0)return f;let l=new Set(u),y=u.filter((r)=>f.includes(r));if(y.length===0)return f;return[...y,...f.filter((r)=>!l.has(r))]}function sM(f,u){if(u.length===0)return f;let l=new Set(u),y=u.filter((_)=>f.some(($)=>$.includes(_)));if(y.length===0)return f;let r=f.map((_)=>_.filter(($)=>!l.has($))).filter((_)=>_.length>0);return[y,...r]}function oM(f,u,l){let r=lJ(f).map(J6).filter((W)=>W.length>0);if(r.length>0)return r;let _=u.map((W)=>String(W?.id||"")).filter(Boolean),$=new Set(_),j=new Map(_.map((W)=>[W,0])),A=new Map(_.map((W)=>[W,[]]));for(let W of l){let G=String(W?.from||W?.source||""),K=String(W?.to||W?.target||"");if(!$.has(G)||!$.has(K))continue;A.get(G)?.push(K),j.set(K,(j.get(K)||0)+1)}let F=new Map,U=_.filter((W)=>(j.get(W)||0)===0);for(let W of U)F.set(W,0);while(U.length>0){let W=U.shift(),G=(F.get(W)||0)+1;for(let K of A.get(W)||[])if(j.set(K,Math.max(0,(j.get(K)||0)-1)),F.set(K,Math.max(F.get(K)||0,G)),(j.get(K)||0)===0)U.push(K)}_.forEach((W)=>{if(!F.has(W))F.set(W,0)});let Q=Math.max(0,...Array.from(F.values()));return Array.from({length:Q+1},(W,G)=>_.filter((K)=>F.get(K)===G)).filter((W)=>W.length>0)}function aM(f,u,l){let r=lJ(f).map(J6).filter((j)=>j.length>0),_=r.length>0?r.flatMap((j)=>j):(()=>{let j=u.map((E)=>String(E?.id||"")).filter(Boolean),A=new Set(j),F=l.filter((E)=>String(E?.edgeType||"").toLowerCase()!=="rework"),U=new Map(j.map((E)=>[E,0])),Q=new Map(j.map((E)=>[E,[]]));for(let E of F){let O=String(E?.from||E?.source||""),z=String(E?.to||E?.target||"");if(!A.has(O)||!A.has(z))continue;Q.get(O)?.push(z),U.set(z,(U.get(z)||0)+1)}let W=new Map,G=j.filter((E)=>(U.get(E)||0)===0);for(let E of G)W.set(E,0);while(G.length>0){let E=G.shift(),O=(W.get(E)||0)+1;for(let z of Q.get(E)||[])if(U.set(z,Math.max(0,(U.get(z)||0)-1)),W.set(z,Math.max(W.get(z)||0,O)),(U.get(z)||0)===0)G.push(z)}j.forEach((E)=>{if(!W.has(E))W.set(E,0)});let K=Math.max(0,...Array.from(W.values()));return Array.from({length:K+1},(E,O)=>j.filter((z)=>W.get(z)===O)).flatMap((E)=>E)})(),$=new Set(_);for(let j of u){let A=String(j?.id||"");if(!A||$.has(A))continue;_.push(A),$.add(A)}return tM(_,kM(u))}function l6(f){return`${f.source}->${f.target}-${f.index}`}function uE(f,u,l){let y=OE(f),r=uJ(f),_=iM(l),$=new Map(y.map((C)=>[String(C?.id||""),C])),j=y.filter((C)=>VE(C,eZ(C,_))).map((C)=>String(C?.id||"")).filter(Boolean),A=sM(oM(f,y,r),j),F=[],U=new Map,Q=330,W=122;A.forEach((C,T)=>{let R=C.length*122;C.forEach((P,n)=>{let B=$.get(P)||{id:P},D=eZ(B,_),I=fE(u,P).toLowerCase(),p=String(B.kind||D?.componentClass||"node").toLowerCase(),k=HE(B.componentRef||D),_f=String(D?.config?.version||D?.version||""),S=String(D?.config?.description||D?.description||""),e=n*122-Math.floor(R/2);U.set(P,{column:T,row:n,y:e}),F.push({id:P,type:"pipelineNode",position:{x:T*330,y:e},data:{exportLabel:{id:P,kind:p,componentRef:k,componentVersion:_f,componentDescription:S,status:I},label:q("div",{className:"flow-node-label"},q("strong",null,P),q("span",null,p),q("code",{title:S||k},_f?`${k}@${_f}`:k),q(Vy,{status:I},I))},className:`pipeline-flow-node ${p} ${I}`})})});let G=r.flatMap((C,T)=>{let R=String(C?.from||C?.source||""),P=String(C?.to||C?.target||"");if(!$.has(R)||!$.has(P))return[];return[{source:R,target:P,index:T,condition:C?.condition,edgeType:C?.edgeType}]}),K=G.reduce((C,T)=>C.set(T.source,(C.get(T.source)||0)+1),new Map),E=G.reduce((C,T)=>C.set(T.target,(C.get(T.target)||0)+1),new Map),O=G.reduce((C,T)=>{let R=`${T.source}->${T.target}`;return C.set(R,(C.get(R)||0)+1)},new Map),z=new Map,Z=new Map,N=new Map,H=new Map,Y=new Map,w=new Map,V=G.reduce((C,T)=>{let R=U.get(T.source),P=U.get(T.target),n=(P?.column||0)-(R?.column||0);if(n<=0||String(T.edgeType||"").toLowerCase()==="rework"||n!==1)return C;let D=`${T.source}->column:${P?.column??""}`,I=C.get(D)||[];return I.push(T),C.set(D,I),C},new Map);for(let C of V.values()){if(C.length<2)continue;C.slice().sort((T,R)=>{let P=U.get(T.target),n=U.get(R.target);return(P?.y||0)-(n?.y||0)||T.index-R.index}).forEach((T,R,P)=>{w.set(l6(T),{slot:R-(P.length-1)/2,count:P.length})})}[...G].sort((C,T)=>{let R=U.get(C.source),P=U.get(C.target),n=U.get(T.source),B=U.get(T.target),D=Math.abs((P?.column||0)-(R?.column||0))*330+Math.abs((P?.y||0)-(R?.y||0)),I=Math.abs((B?.column||0)-(n?.column||0))*330+Math.abs((B?.y||0)-(n?.y||0));return D-I||C.index-T.index}).forEach((C)=>{let T=U.get(C.source)||{column:0,row:0,y:0},R=U.get(C.target)||{column:0,row:0,y:0},P=R.column-T.column,n=Math.max(0,P),B=P<=0||String(C.edgeType||"").toLowerCase()==="rework",D=T.y-R.y,I=E.get(C.target)||1,p=w.has(l6(C)),k=!B&&n<=1&&(p||I===1),_f=Y.get(C.target)||new Map;Y.set(C.target,_f);let S=A6.slice().sort((e,$f)=>{let Qf=(Zf)=>{let b=String(Zf.side),t=0;if(B){if(b==="left")t+=86;if(b==="top")t+=R.y<=0?-22:12;if(b==="bottom")t+=R.y>=0?-22:12;if(Math.abs(R.y)<12&&b!=="left")t+=C.index%2===0?b==="top"?-6:6:b==="bottom"?-6:6;return t}if(k){if(b==="left")t-=p?72:44;if(b!=="left")t+=p?72:44;return t+Math.abs(D)*0.02}if(b==="left")t+=n<=1?0:24;if(b==="top")t+=D<-36?-18:42;if(b==="bottom")t+=D>36?-18:42;if(n<=1&&Math.abs(D)<=82&&b!=="left")t+=38;if(n>1&&b!=="left")t-=10;return t},Af=T.y-R.y,zf=Af!==0?Af:C.index%2===0?-1:1,Hf=(Zf)=>{let b=_f.get(Zf.id)||0;return Qf(Zf)+b*64+GM(Zf,_f,zf)};return Hf(e)-Hf($f)||String(e.id).localeCompare(String($f.id))})[0];_f.set(S.id,(_f.get(S.id)||0)+1),H.set(l6(C),S)});let i=G.map((C)=>{let T=fE(u,C.target).toLowerCase(),R=`${C.source}->${C.target}`,P=z.get(C.source)||0,n=Z.get(C.target)||0,B=N.get(R)||0;z.set(C.source,P+1),Z.set(C.target,n+1),N.set(R,B+1);let D=P-((K.get(C.source)||1)-1)/2,I=n-((E.get(C.target)||1)-1)/2,p=B-((O.get(R)||1)-1)/2,k=U.get(C.source),_f=U.get(C.target),S=(_f?.column||0)-(k?.column||0),e=Math.max(1,Math.abs(S)),$f=S<=0||String(C.edgeType||"").toLowerCase()==="rework",Qf=Math.abs((_f?.y||0)-(k?.y||0)),Af=w.get(l6(C)),zf=!$f&&S===1&&(E.get(C.target)||0)>1,Hf=Af?Af.slot:p*2+D+I*0.45,Zf=Hf===0?C.index%2===0?-1:1:Math.sign(Hf),b=H.get(l6(C))||A6[1],t=b.side==="top"?-1:b.side==="bottom"?1:Zf,a=$f||e>1||Qf>96||Math.abs(Hf)>0.2||b.side!=="left",Nf=$f?118+e*18:22+e*16,o=b.side==="left"?0:28,uf=a?Math.max(-280,Math.min(280,t*Math.min(180,Nf+o+Qf*0.22)+Hf*28)):0,qf=Math.max(0,Math.min(r6.length-1,Math.round(D+(r6.length-1)/2))),xf=r6[qf]||r6[1],tf=T==="succeeded"?"var(--accent-2)":T==="running"?"var(--accent)":T==="failed"?"var(--danger)":"rgba(129, 147, 159, 0.78)",df=k?.column||0,lu=_f?.column||0,Ou=uf===0?0:Math.sign(uf),mu=$f?`feedback:${df}->${lu}:${Ou}`:Af?`fanout:${df}->${lu}:${C.source}`:zf?`fanin:${df}->${lu}:${C.target}`:b.side!=="left"||e>1?`corridor:${df}->${lu}:${b.side}:${Ou}:${Math.round(Math.abs(uf)/56)}`:"";return{id:`${C.source}->${C.target}-${C.index}`,source:C.source,target:C.target,sourceHandle:xf.id,targetHandle:b.id,type:"pipelineCurve",zIndex:12,animated:T==="running",data:{baseEdgeColor:tf,laneOffset:uf,routeMode:Af&&b.side==="left"?"direct-forward-left":"",targetSide:b.side,isFeedback:$f,overlapGroup:mu},targetStatus:T}}),m=i.reduce((C,T)=>{let R=String(T.data?.overlapGroup||"");return R?C.set(R,(C.get(R)||0)+1):C},new Map),M=new Map,c=i.map((C)=>{let T=String(C.targetStatus||"pending"),R={...C};delete R.targetStatus;let P=String(C.data?.overlapGroup||""),n=P?m.get(P)||0:0,B=n>1?M.get(P)||0:-1;if(n>1)M.set(P,B+1);let D=B>=0?hZ[B%hZ.length]:String(C.data.baseEdgeColor),I={stroke:D};if(C.data.isFeedback)I.strokeDasharray="9 7";return{...R,data:{...C.data,edgeColor:D,overlapSlot:B,overlapCount:n},style:I,markerEnd:{type:Gy.ArrowClosed,color:D},className:`pipeline-flow-edge ${T} ${C.data.isFeedback?"feedback":""} ${B>=0?"overlap-colored":""}`}});return{nodes:F,edges:c}}function c0(f){return String(f??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}function lE(f){let u=String(f||"");if(u.includes("--accent-2"))return"#4eb7a8";if(u.includes("--accent"))return"#d7a13a";if(u.includes("--danger"))return"#cf6a54";return u.startsWith("#")?u:"#81939f"}function i2(f){return`arrow-${f.replace(/[^a-zA-Z0-9_-]+/g,"")}`}function LE(f,u="pipeline"){return String(f||u).replace(/[^a-zA-Z0-9_-]+/g,"-").replace(/^-|-$/g,"")||u}function yE(f,u){let l=f.position.x,y=f.position.y,r=A6.find((_)=>_.id===u);if(r?.side==="top")return{x:l+h_*IZ(r.style?.left,0.5),y,position:Gf.Top};if(r?.side==="bottom")return{x:l+h_*IZ(r.style?.left,0.5),y:y+m_,position:Gf.Bottom};return{x:l,y:y+m_/2,position:Gf.Left}}function dM(f){return{x:f.position.x+h_,y:f.position.y+m_/2}}function eM(f,u){let l=Math.min(...f.nodes.map((E)=>E.position.x),0)-220,y=Math.min(...f.nodes.map((E)=>E.position.y),0)-220,r=Math.max(...f.nodes.map((E)=>E.position.x+h_),1)+220,_=Math.max(...f.nodes.map((E)=>E.position.y+m_),1)+220,$=Math.ceil(r-l),j=Math.ceil(_-y),A=new Map(f.nodes.map((E)=>[E.id,E])),F=f.edges.map((E)=>lE(E.data?.edgeColor||E.style?.stroke)),Q=Array.from(new Set(["#4eb7a8","#d7a13a","#cf6a54","#81939f",...F])).map((E)=>``).join(""),W=f.edges.flatMap((E)=>{let O=A.get(E.source),z=A.get(E.target);if(!O||!z)return[];let Z=dM(O),N=yE(z,String(E.targetHandle||"in-left")),H=QE(Z.x,Z.y,N.x,N.y,N.position,Number(E.data?.laneOffset||0),String(E.data?.routeMode||"")),Y=lE(E.data?.edgeColor||E.style?.stroke),w=E.data?.isFeedback?' stroke-dasharray="9 7"':"";return``}).join(` +`),G=f.nodes.map((E)=>{let O=E.data?.exportLabel||{},z=String(O.status||"pending").toLowerCase(),Z=z==="succeeded"?"#4eb7a8":z==="running"?"#d7a13a":z==="failed"?"#cf6a54":"#81939f",N=E.position.x,H=E.position.y,Y=A6.map((w)=>{let V=yE(E,w.id);if(w.side==="top"||w.side==="bottom")return``;return``}).join(` `);return` - - ${L} - - ${Tu(O.id||H.id)} - ${Tu(O.kind||"node")} - ${Tu(O.componentRef||"--")} - ${Tu(z)} + + ${Y} + + ${c0(O.id||E.id)} + ${c0(O.kind||"node")} + ${c0(O.componentRef||"--")} + ${c0(z)} `}).join(` -`);return{svg:` - ${G} +`);return{svg:` + ${Q} - ${Tu(u)} - ${K}${W} - `,width:j,height:J}}function Hr(f){let u=String(f||"").toLowerCase();if(u==="succeeded"||u==="completed")return"#4eb7a8";if(u==="failed")return"#cf6a54";if(Rq(u))return"#69aee8";return"#d7a13a"}function Vr(f){let u=String(f?.kind||""),_=String(f?.tone||f?.status||"").toLowerCase();if(u==="prompt"&&_==="initial")return"#d7a13a";if(u==="prompt"&&_==="monitor")return"#69aee8";if(u==="prompt")return"#4eb7a8";if(_==="modify")return"#e0b95a";if(_==="approve"||_==="guide"||_==="monitor")return"#4eb7a8";if(_==="restart"||_==="redo")return"#d7a13a";if(_==="ignored")return"#81939f";if(_==="webui")return"#69aee8";if(_==="cli")return"#d7a13a";return"#a7bac5"}function Bq(f){let u=String(f?.sourceKind||"").toLowerCase(),_=String(f?.action||"").toLowerCase(),y=String(f?.status||"").toLowerCase();if(_==="observe"||y==="observation"||u==="monitor")return"#4eb7a8";if(u==="webui")return"#69aee8";if(u==="cli")return"#d7a13a";if(y.includes("ignored"))return"#81939f";return"#8aa0ad"}function Or(f,u,_){let y=Vr(f),l=String(f?.kind||"");if(l==="control-source")return``;if(l==="control-target"){let j=String(f?.tone||"").toLowerCase()==="approve"?"rgba(78,183,168,0.22)":"#081118";return``}return``}function Xr(f){let u=Vf(f.visibleNodeIds).map((Y)=>String(Y||"")).filter(Boolean),_=Vf(f.intervals).filter(Xf),y=Vf(f.markers).filter(Xf),l=Vf(f.arrows).filter(Xf),$=Vf(f.ticks).filter(Xf),j=Xf(f.bounds)?f.bounds:{},J=Xf(f.backendLayout)?f.backendLayout:null,F=Math.max(240,Math.round(Number(f.chartHeight||360))),A=Math.max(x1,108),U=128,G=24,W=58,K=56,E=128+Math.max(1,u.length)*A,H=Math.max(760,E+48),O=114+F+24,z=24,q=58,Z=114,V=(Y)=>152+Y*A,L=(Y)=>V(Y)+A/2,r=Vf(f.meta).map((Y)=>String(Y||"")).filter(Boolean).slice(0,4).join(" · "),N=new Map(y.map((Y)=>[String(Y.id||""),Y])),x=Array.from(new Set(["#4eb7a8","#69aee8","#d7a13a","#cf6a54","#8aa0ad",...l.map(Bq)])).map((Y)=>``).join(""),c=$.map((Y)=>{let R=114+fE(Y,j,F,J);return` - - ${Tu(zf(Y.ms))} - +${Tu(l1(Number(Y.offsetMs??Number(Y.ms)-Number(j.startMs))))} + ${c0(u)} + ${G}${W} + `,width:$,height:j}}function fS(f){let u=String(f||"").toLowerCase();if(u==="succeeded"||u==="completed")return"#4eb7a8";if(u==="failed")return"#cf6a54";if(WE(u))return"#69aee8";return"#d7a13a"}function uS(f){let u=String(f?.kind||""),l=String(f?.tone||f?.status||"").toLowerCase();if(u==="prompt"&&l==="initial")return"#d7a13a";if(u==="prompt"&&l==="monitor")return"#69aee8";if(u==="prompt")return"#4eb7a8";if(l==="modify")return"#e0b95a";if(l==="approve"||l==="guide"||l==="monitor")return"#4eb7a8";if(l==="restart"||l==="redo")return"#d7a13a";if(l==="ignored")return"#81939f";if(l==="webui")return"#69aee8";if(l==="cli")return"#d7a13a";return"#a7bac5"}function rE(f){let u=String(f?.sourceKind||"").toLowerCase(),l=String(f?.action||"").toLowerCase(),y=String(f?.status||"").toLowerCase();if(l==="observe"||y==="observation"||u==="monitor")return"#4eb7a8";if(u==="webui")return"#69aee8";if(u==="cli")return"#d7a13a";if(y.includes("ignored"))return"#81939f";return"#8aa0ad"}function lS(f,u,l){let y=uS(f),r=String(f?.kind||"");if(r==="control-source")return``;if(r==="control-target"){let $=String(f?.tone||"").toLowerCase()==="approve"?"rgba(78,183,168,0.22)":"#081118";return``}return``}function yS(f){let u=Lf(f.visibleNodeIds).map((B)=>String(B||"")).filter(Boolean),l=Lf(f.intervals).filter(Yf),y=Lf(f.markers).filter(Yf),r=Lf(f.arrows).filter(Yf),_=Lf(f.ticks).filter(Yf),$=Yf(f.bounds)?f.bounds:{},j=Yf(f.backendLayout)?f.backendLayout:null,A=Math.max(240,Math.round(Number(f.chartHeight||360))),F=Math.max(f1,108),U=128,Q=24,W=58,G=56,K=128+Math.max(1,u.length)*F,E=Math.max(760,K+48),O=114+A+24,z=24,Z=58,N=114,H=(B)=>152+B*F,Y=(B)=>H(B)+F/2,w=Lf(f.meta).map((B)=>String(B||"")).filter(Boolean).slice(0,4).join(" · "),V=new Map(y.map((B)=>[String(B.id||""),B])),i=Array.from(new Set(["#4eb7a8","#69aee8","#d7a13a","#cf6a54","#8aa0ad",...r.map(rE)])).map((B)=>``).join(""),m=_.map((B)=>{let D=114+ME(B,$,A,j);return` + + ${c0(Kf(B.ms))} + +${c0(Nl(Number(B.offsetMs??Number(B.ms)-Number($.startMs))))} `}).join(` -`),v=['','TIME',...u.map((Y,R)=>{let k=V(R),p=Y.length>18?`${Y.slice(0,16)}…`:Y;return` - - ${Tu(p)} - node ${R+1} +`),M=['','TIME',...u.map((B,D)=>{let I=H(D),p=B.length>18?`${B.slice(0,16)}…`:B;return` + + ${c0(p)} + node ${D+1} `})].join(` -`),C=u.map((Y,R)=>{return``}).join(` -`),S=_.map((Y)=>{let R=u.indexOf(String(Y.nodeId||""));if(R<0)return"";let k=114+d5(Y,j,F,J),p=Math.max(2,eq(Y,j,F,J)),n=Hr(Y.status),_f=L(R)-3.5,s=Y.live?``:"",ff=p>=28?`${Tu(String(Y.status||"working"))} - ${Tu(l1(Y.durationMs))}`:"";return` - - ${s} - ${ff} +`),c=u.map((B,D)=>{return``}).join(` +`),C=l.map((B)=>{let D=u.indexOf(String(B.nodeId||""));if(D<0)return"";let I=114+x2(B,$,A,j),p=Math.max(2,nE(B,$,A,j)),k=fS(B.status),_f=Y(D)-3.5,S=B.live?``:"",e=p>=28?`${c0(String(B.status||"working"))} + ${c0(Nl(B.durationMs))}`:"";return` + + ${S} + ${e} `}).join(` -`),B=y.map((Y)=>{let R=u.indexOf(String(Y.nodeId||""));if(R<0)return"";let k=114+cu(Y,j,F,J);return Or(Y,L(R),k)}).join(` -`),P=l.map((Y)=>{let R=N.get(String(Y.targetMarkerId||""));if(!R)return"";let k=N.get(String(Y.sourceMarkerId||"")),p=String(k?.nodeId||Y.sourceNodeId||""),n=String(R.nodeId||Y.targetNodeId||""),_f=u.indexOf(p),s=u.indexOf(n);if(_f<0||s<0)return"";let ff=L(_f)-24-128,Kf=L(s)-24-128,Gf=G3(J)?V0(Y.sourceY??Y.y1)??(k?cu(k,j,F,J):cu(R,j,F,J)):k?cu(k,j,F,J):cu(R,j,F,J),jf=G3(J)?V0(Y.targetY??Y.y2)??cu(R,j,F,J):cu(R,j,F,J),Wf=Bq(Y),Of=String(Y.action||"").toLowerCase()==="observe"?"3 4":"6 5",Zf=Tu(uE(ff,Gf,Kf,jf));return` - `}).join(` -`),M=u.length===0?'No visible Gantt nodes':"";return{svg:` - ${x} +`),T=y.map((B)=>{let D=u.indexOf(String(B.nodeId||""));if(D<0)return"";let I=114+d0(B,$,A,j);return lS(B,Y(D),I)}).join(` +`),R=r.map((B)=>{let D=V.get(String(B.targetMarkerId||""));if(!D)return"";let I=V.get(String(B.sourceMarkerId||"")),p=String(I?.nodeId||B.sourceNodeId||""),k=String(D.nodeId||B.targetNodeId||""),_f=u.indexOf(p),S=u.indexOf(k);if(_f<0||S<0)return"";let e=Y(_f)-24-128,$f=Y(S)-24-128,Qf=p_(j)?Bu(B.sourceY??B.y1)??(I?d0(I,$,A,j):d0(D,$,A,j)):I?d0(I,$,A,j):d0(D,$,A,j),Af=p_(j)?Bu(B.targetY??B.y2)??d0(D,$,A,j):d0(D,$,A,j),zf=rE(B),Hf=String(B.action||"").toLowerCase()==="observe"?"3 4":"6 5",Zf=c0(SE(e,Qf,$f,Af));return` + `}).join(` +`),P=u.length===0?'No visible Gantt nodes':"";return{svg:` + ${i} - - ${Tu(f.title||"Pipeline Epoch Gantt")} - ${Tu(r)} - ${v} - - ${C} - ${c} - ${S} - ${P} - ${B} + + ${c0(f.title||"Pipeline Epoch Gantt")} + ${c0(w)} ${M} - `,width:H,height:O}}function a5(f,u){let _=URL.createObjectURL(f),y=document.createElement("a");y.href=_,y.download=u,y.click(),setTimeout(()=>URL.revokeObjectURL(_),1000)}async function nq(f,u){let _=gq(u,"pipeline"),{svg:y,width:l,height:$}=Er(f,u),j=new Blob([y],{type:"image/svg+xml;charset=utf-8"}),J=URL.createObjectURL(j);try{let F=new Image;await new Promise((W,K)=>{F.onload=()=>W(),F.onerror=()=>K(Error("svg image load failed")),F.src=J});let A=document.createElement("canvas");A.width=l,A.height=$;let U=A.getContext("2d");if(!U)throw Error("canvas unavailable");U.drawImage(F,0,0);let G=await new Promise((W)=>A.toBlob(W,"image/png"));if(!G)throw Error("png export failed");a5(G,`${_}.png`)}catch{a5(j,`${_}.svg`)}finally{URL.revokeObjectURL(J)}}async function Nr(f){let u=gq(String(f?.title||"pipeline-gantt"),"pipeline-gantt"),{svg:_,width:y,height:l}=Xr(f),$=new Blob([_],{type:"image/svg+xml;charset=utf-8"}),j=URL.createObjectURL($);try{let J=new Image;await new Promise((G,W)=>{J.onload=()=>G(),J.onerror=()=>W(Error("gantt svg image load failed")),J.src=j});let F=document.createElement("canvas");F.width=y,F.height=l;let A=F.getContext("2d");if(!A)throw Error("canvas unavailable");A.drawImage(J,0,0);let U=await new Promise((G)=>F.toBlob(G,"image/png"));if(!U)throw Error("gantt png export failed");a5(U,`${u}.png`)}catch{a5($,`${u}.svg`)}finally{URL.revokeObjectURL(j)}}async function Lr(f){for(let u of f){if(u.flow.nodes.length===0)continue;await nq(u.flow,u.title),await new Promise((_)=>setTimeout(_,750))}}function Dq(f,u){return f.find((_)=>String(_?.pipelineId||"")===u)||null}function wq(f){return Cf(f?.startedAt)??Cf(f?.artifact?.startedAt)??Cf(f?.request?.createdAt)??Cf(f?.updatedAt)??0}function Yr(f,u){return f.filter((_)=>String(_?.pipelineId||"")===u).slice().sort((_,y)=>wq(_)-wq(y)||String(_?.runId||"").localeCompare(String(y?.runId||"")))}function tF(f,u){let _=String(u?.runId||""),y=f.findIndex((j)=>String(j?.runId||"")===_),l=y>=0?y+1:f.length,$=String(u?.status||"--");return`Epoch ${l} / ${_||"--"} / ${$}`}function $1(f){return String(f?.procedureRunId||f?.runId||"")}function u2(f,u){let _=String(f?.nodeId||f?.request?.nodeId||"");if(_)return _;let y=$1(f),l=`${u}__`;if(y.startsWith(l))return y.slice(l.length).replace(/__\d+$/u,"");return""}function p5(f,u){let _=Xf(f?.artifact)?f.artifact:{},y=Xf(f?.request)?f.request:{};return D6(f?.startedAt,_.startedAt,y.createdAt,y.startedAt,f?.createdAt,f?.updatedAt,u?.startedAt,u?.request?.createdAt)}function k5(f,u){let _=String(f?.status?.status||f?.artifact?.status||f?.status||"").toLowerCase(),y=Xf(f?.artifact)?f.artifact:{},l=dF(_);return D6(f?.finishedAt,y.finishedAt,f?.completedAt,l?f?.updatedAt:void 0,l?y.updatedAt:void 0,l?u?.updatedAt:void 0)}function tq(f,u,_=Date.now()){let y=String(f?.runId||""),l=new Set(u.map(($)=>String($?.id||"")).filter(Boolean));return Vf(f?.procedureRuns).flatMap(($)=>{let j=u2($,y);if(!j)return[];let J=String($?.status?.status||$?.artifact?.status||$?.status||"unknown").toLowerCase(),F=p5($,f),A=Cf(F);if(A===null)return[];let U=k5($,f),G=Cf(U)??(dF(J)?Cf($?.updatedAt)??A+1000:_),W=Math.max(A+1000,G);return[{nodeId:j,knownNode:l.has(j),procedureRunId:$1($),status:J,startMs:A,endMs:W,startedAt:L6(A),finishedAt:L6(W),durationMs:W-A,runId:y,raw:$}]}).sort(($,j)=>$.startMs-j.startMs||$.endMs-j.endMs||$.nodeId.localeCompare(j.nodeId))}function Br(f,u,_=[]){let y=u.map((U)=>Number(U.startMs)).filter(Number.isFinite),l=u.map((U)=>Number(U.endMs)).filter(Number.isFinite);for(let U of _){let G=V0(U?.eventMs??U?.ms);if(G!==null)y.push(G),l.push(G)}let $=Cf(f?.startedAt)??Cf(f?.artifact?.startedAt)??Cf(f?.request?.createdAt),j=Cf(f?.finishedAt)??Cf(f?.artifact?.finishedAt)??Cf(f?.updatedAt);if($!==null)y.push($);if(j!==null)l.push(j);let J=Date.now(),F=y.length>0?Math.min(...y):J-60000,A=Math.max(F+60000,l.length>0?Math.max(...l):J);return{startMs:F,endMs:A,durationMs:A-F}}var m5=12,sq=20,sF=100,Dr=!1;function _y(f){let u=Number(f);if(!Number.isFinite(u))return 0;return Math.max(0,Math.min(100,Math.round(u*100)/100))}function wr(f){let u=Math.max(m5,Number(f||m5)),_=Math.log(u/m5)/Math.log(sq);return _y(_*100)}var B6=wr(sF);function lQ(f){let u=_y(f)/100,_=m5*Math.pow(sq,u),y=u<0.24?"全局":u<0.64?"均衡":"细节";return{value:_y(u*100),pxPerMinute:_,label:y}}function pF(f){let u=Math.round(Number(f));return Math.abs(u-sF)<=1?sF:u}function Tr(f,u=B6){let _=Math.max(1,Number(f.durationMs||0)/60000),y=lQ(u);return Math.round(Math.max(360,Math.min(7200,_*Number(y.pxPerMinute||48))))}function rr(f,u=7){let _=Math.max(1,Number(f.endMs||0)-Number(f.startMs||0));return Array.from({length:u},(y,l)=>{let $=u===1?0:l/(u-1);return{ms:Number(f.startMs)+_*$,percent:$*100}})}function Mr(f,u){let _=Math.max(1,Number(u.endMs)-Number(u.startMs));return Math.max(0,Math.min(100,(f-Number(u.startMs))/_*100))}function V0(f){let u=Number(f);return Number.isFinite(u)?u:null}function $Q(f){return Rq(f?.status)&&!dF(f?.status)}function oq(f,u,_,y){let l=Math.max(1,_-u),$=Math.max(0,Math.min(1,(f-u)/l));return Number(($*y).toFixed(3))}function Tq(f,u){if(!u)return null;let _=V0(u?.startMs),y=V0(u?.endMs),l=V0(u?.chartHeight);if(_===null||y===null||l===null)return null;return oq(f,_,y,l)}function aq(f,u){let _=V0(f?.rawStartMs??f?.startMs)??V0(f?.startMs)??u,y=V0(f?.endMs)??_+1000;if(!$Q(f))return Math.max(_+1000,y);return Math.max(_+1000,y,u)}function Pr(f,u,_,y){let l=V0(f?.startMs)??y-60000,$=V0(f?.endMs)??y,j=_.reduce((E,H)=>Math.max(E,aq(H,y)),$),J=Math.max(l+60000,$,j),F=Math.max(1,J-l),A={startMs:l,endMs:J,durationMs:F},U=Tr(A,u),G=lQ(u),W=Math.max(5,Math.min(18,Math.round(U/150))),K=rr(A,W).map((E)=>{let H=Number(E.ms),O=oq(H,l,J,U);return{...E,y:O,timestamp:L6(H),offsetMs:H-l}});return{source:"frontend-y",startMs:l,endMs:J,durationMs:F,chartHeight:U,scale:_y(u),normalizedScale:Number((_y(u)/100).toFixed(3)),pxPerMinute:Number(Number(G.pxPerMinute||0).toFixed(3)),ticks:K}}function Sr(f,u,_){if(!$Q(f))return f;let y=V0(f?.rawStartMs??f?.startMs)??V0(f?.startMs)??_,l=aq(f,_),$=Tq(y,u),j=Tq(l,u),J=V0($??f?.y1??f?.startY)??0,F=V0(j??f?.y2??f?.endY)??J+10,A=Math.max(24,F-J);return{...f,live:!0,startMs:y,endMs:l,durationMs:Math.max(1000,l-y),finishedAt:L6(l),y1:J,y2:F,startY:J,endY:F,height:A}}function jQ(f,u,_){return Mr(f,u)/100*_}function G3(f){return Boolean(f&&String(f?.source||"")!=="frontend-y")}function dq(f,u,_,y,l){if(G3(y))for(let j of l){let J=V0(f?.[j]);if(J!==null)return J}let $=V0(f?.ms??f?.eventMs??f?.startMs);return jQ($??Number(u.startMs),u,_)}function d5(f,u,_,y){return dq(f,u,_,y,["y1","startY"])}function oF(f,u,_,y){if(G3(y)){let $=V0(f?.y2??f?.endY);if($!==null)return $}let l=V0(f?.endMs)??Number(u.endMs);return jQ(l,u,_)}function eq(f,u,_,y){if(G3(y)){let $=V0(f?.height);if($!==null)return Math.max(1,$)}let l=f?.live?24:10;return Math.max(l,oF(f,u,_,y)-d5(f,u,_,y))}function cu(f,u,_,y){return dq(f,u,_,y,["y","timeAxisY"])}function fE(f,u,_,y){if(G3(y)||String(y?.source||"")==="frontend-y"){let j=V0(f?.y);if(j!==null)return j}let l=V0(f?.percent);if(l!==null)return l/100*_;let $=V0(f?.ms)??Number(u.startMs);return jQ($,u,_)}function Cr(f){let u=String(f?.promptEvent||f?.raw?.promptEvent||f?.event||"").toLowerCase();if(!["node-long-running-observation","node-finished"].includes(u))return"";let _=String(f?.sourceNodeId||f?.raw?.sourceNodeId||f?.raw?.detail?.nodeId||""),y=String(f?.nodeId||f?.targetNodeId||"");return _&&_!==y?_:""}function Rr(f,u){let _=new Set(u.map((l)=>[String(l.sourceNodeId||""),String(l.targetNodeId||""),String(l.targetMarkerId||""),String(l.action||"")].join(":"))),y=[...u];for(let l of f){let $=Cr(l),j=String(l?.nodeId||""),J=String(l?.id||"");if(!$||!j||!J)continue;let F=[$,j,J,"observe"].join(":");if(_.has(F))continue;_.add(F),y.push({id:`observation-arrow:${J}:${$}:${j}`,commandId:String(l?.commandId||l?.eventId||J),sourceNodeId:$,targetNodeId:j,sourceMarkerId:"",targetMarkerId:J,sourceKind:"monitor",action:"observe",status:"observation"})}return{markers:f,arrows:y}}function rq(f,u=""){let _=V1(f)||u,y=String(f?.promptEvent||"");if(_==="initial-prompt-delivered")return"initial";if(y==="node-finished"||y==="node-long-running-observation"||y.startsWith("monitor-"))return"monitor";if(_==="monitor-prompt-delivered"||String(f?.sourceKind||"").toLowerCase()==="monitor"||u==="monitor-prompt-queued")return"monitor";return"append"}function xr(f){return Vf(f?.tags||f?.raw?.tags).map((u)=>String(u||"")).filter(Boolean)}function Mq(f,u=""){let _=V1(f)||u,y=String(f?.promptEvent||"");if(_==="initial-prompt-delivered")return"初始 prompt";if(y==="node-long-running-observation")return"长任务观察";if(y==="node-finished")return xr(f).includes("monitor.audit")?"节点完成 / OA 审核":"节点完成";if(y==="monitor-interval")return"旧版轮询";if(y==="monitor-start")return"Monitor start";if(y==="monitor-stop")return"Monitor stop";if(_==="monitor-prompt-delivered"||u==="monitor-prompt-queued")return"Monitor prompt";if(_==="append-prompt-queued")return"追加 prompt 已排队";return"追加 prompt"}function Pq(f){let u=V1(f);if(u==="control-command-applied")return 3;if(u==="control-command-ignored")return 2;if(u==="control-command-queued")return 1;return 0}function Sq(f,u){let _=String(f?.commandId||"");if(_)return`command:${_}`;return["fallback",gy(f)||D6(f?.createdAt,f?.timestamp)||`index-${u}`,String(f?.sourceKind||""),String(f?.sourceNodeId||""),String(f?.targetNodeId||""),ny(f)].join(":")}function vr(f){return gF([f?.targetNodeId,...Vf(f?.resetNodeIds)])}function br(f,u){let _=X6(f),y=V1(f),l=String(f?.targetNodeId||""),$=Boolean(l)&&u!==l;if(y==="control-command-applied")return $?`${_} 波及`:`${_} 生效`;if(y==="control-command-ignored")return`${_} 忽略`;if(y==="control-command-queued")return`${_} 已发起`;return $?`${_} 波及`:_}function hr(f){if(V1(f)==="control-command-ignored")return"ignored";let _=ny(f);if(_==="restart"||_==="redo")return"restart";if(_==="modify")return"modify";if(_==="approve")return"approve";if(_==="guide")return"guide";return"pending"}function Ir(f){let u=String(f?.sourceKind||"").toLowerCase();if(u==="monitor")return"monitor";if(u==="webui")return"webui";if(u==="cli")return"cli";return"system"}function cr(f,u,_,y){let l=f.filter((A)=>String(A.nodeId||"")===u).sort((A,U)=>Number(A.startMs)-Number(U.startMs)),$=l.find((A)=>_>=Number(A.startMs)-1000&&_<=Number(A.endMs)+1000);if($)return{ms:_,onInterval:!0,snapReason:"inside-interval",procedureRunId:String($.procedureRunId||"")};let j=ny(y),J=l.slice().reverse().find((A)=>Number(A.endMs)<=_+1000);if(J&&j==="approve")return{ms:Number(J.endMs),onInterval:!0,snapReason:"previous-interval-end",procedureRunId:String(J.procedureRunId||"")};let F=l.find((A)=>Number(A.startMs)>=_-1000);if(F&&["guide","modify","restart","redo"].includes(j))return{ms:Number(F.startMs),onInterval:!0,snapReason:"next-interval-start",procedureRunId:String(F.procedureRunId||"")};return{ms:_,onInterval:!1,snapReason:"event-time",procedureRunId:String(y?.procedureRunId||"")}}function uE(f,u,_,y){let l=Math.hypot(_-f,y-u),$=l>Wq?Wq:0,j=$>0?_-(_-f)/l*$:_,J=$>0?y-(y-u)/l*$:y,F=j-f,A=Math.max(16,Math.min(42,Math.abs(F)*0.45+12)),U=F===0?1:Math.sign(F);return`M ${f},${u} C ${f+U*A},${u} ${j-U*A},${J} ${j},${J}`}function pr(f,u){let _=String(f?.runId||u?.runId||""),y=tq({...Xf(u)?u:{},...Xf(f)?f:{},runId:_,procedureRuns:Vf(f?.procedureRuns).length>0?f.procedureRuns:u?.procedureRuns},[]),l=[],$=[],j=[],J=new Set,F=new Map,A=(W,K)=>{if(!W.nodeId||!Number.isFinite(Number(W.ms)))return;if(J.has(W.id))return;J.add(W.id),K.push(W)};for(let W of Vf(f?.procedureRuns)){let K=u2(W,_),E=$1(W);if(!K)continue;for(let H of Vf(W?.attempts)){let O=f2(H),z=new Set,q=new Set;for(let V of O6(H?.controlEventRecords)){let L=V1(V);if(!["initial-prompt-delivered","append-prompt-delivered","monitor-prompt-delivered"].includes(L))continue;let r=gy(V),N=Cf(r);if(N===null)continue;let D=String(V?.eventId||"");if(D)z.add(D);q.add(`${L}:${r}:${String(V?.sourceKind||"")}:${String(V?.promptPreview||"")}`),A({id:`prompt:${D||`${E}:${O}:${L}:${N}`}`,runId:_,nodeId:K,procedureRunId:E,attempt:O,kind:"prompt",tone:rq(V,L),status:"delivered",label:Mq(V,L),ms:N,timestampIso:r,sourceKind:String(V?.sourceKind||""),sourceNodeId:String(V?.sourceNodeId||""),targetNodeId:K,action:"",eventId:D,commandId:String(V?.commandId||""),raw:V},l)}let Z=[{records:O6(H?.controlPromptRecords),fallbackKind:"append-prompt-queued"},{records:O6(H?.monitorPromptRecords),fallbackKind:"monitor-prompt-queued"}];for(let V of Z)for(let L of V.records){let r=gy(L),N=Cf(r);if(N===null)continue;let D=String(L?.eventId||"");if(D&&z.has(D))continue;let c=`${V.fallbackKind==="monitor-prompt-queued"?"monitor-prompt-delivered":"append-prompt-delivered"}:${r}:${String(L?.sourceKind||"")}:${String(L?.promptPreview||"")}`;if(q.has(c))continue;A({id:`prompt-fallback:${D||`${E}:${O}:${V.fallbackKind}:${N}`}`,runId:_,nodeId:K,procedureRunId:E,attempt:O,kind:"prompt",tone:rq(L,V.fallbackKind),status:"queued",label:Mq(L,V.fallbackKind),ms:N,timestampIso:r,sourceKind:String(L?.sourceKind||""),sourceNodeId:String(L?.sourceNodeId||""),targetNodeId:K,action:"",eventId:D,commandId:String(L?.commandId||""),raw:L},l)}}}let U=new Map;O6(f?.controlEvents).forEach((W,K)=>{let E=Sq(W,K),H=U.get(E)||{key:E,events:[],commands:[]};H.events.push(W),U.set(E,H)}),Vf(f?.controlCommands).filter(Xf).forEach((W,K)=>{let E=Sq(W,K),H=U.get(E)||{key:E,events:[],commands:[]};H.commands.push(W),U.set(E,H)});for(let W of U.values()){let K=Vf(W.events).slice().sort((C,S)=>Pq(S)-Pq(C)),E=Vf(W.commands),H=Vf(W.events).find((C)=>V1(C)==="control-command-queued")||E[0]||null,O=K[0]||E[0]||H;if(!H&&!O)continue;let z=String(H?.sourceNodeId||O?.sourceNodeId||""),q=String(H?.sourceKind||O?.sourceKind||""),Z=gy(H)||gy(O)||D6(H?.createdAt,O?.createdAt),V=Cf(Z),L=String(O?.commandId||H?.commandId||W.key),r=(V1(O)||"control-command-queued").replace(/^control-command-/u,""),N="";if(z&&V!==null)N=`control-source:${L}:${z}`,F.set(L,N),A({id:N,runId:_,nodeId:z,procedureRunId:String(H?.procedureRunId||O?.procedureRunId||""),attempt:"",kind:"control-source",tone:Ir(H||O),status:r,label:`${X6(H||O)} 发起`,ms:V,timestampIso:Z,action:ny(H||O),sourceKind:q,sourceNodeId:z,targetNodeId:String(O?.targetNodeId||H?.targetNodeId||""),commandId:L,raw:H||O},$);let D=O||H,x=gy(D)||Z,c=Cf(x);if(c===null)continue;let v=vr(D);for(let C of v){let S=cr(y,C,c,D),B=`control-target:${L}:${C}`;if(A({id:B,runId:_,nodeId:C,procedureRunId:S.procedureRunId,attempt:"",kind:"control-target",tone:hr(D),status:r,label:br(D,C),ms:S.ms,eventMs:c,onInterval:S.onInterval,snapReason:S.snapReason,snapped:Number(S.ms)!==c,timestampIso:x,renderedTimestampIso:L6(Number(S.ms)),action:ny(D),sourceKind:q,sourceNodeId:z,targetNodeId:C,commandId:L,raw:D},$),N&&z&&z!==C)j.push({id:`control-arrow:${L}:${z}:${C}`,commandId:L,sourceNodeId:z,targetNodeId:C,sourceMarkerId:N,targetMarkerId:B,sourceKind:q,action:ny(D),status:r})}}let G=[...l,...$].sort((W,K)=>Number(W.ms)-Number(K.ms)||String(W.nodeId).localeCompare(String(K.nodeId))||String(W.id).localeCompare(String(K.id)));return{...Rr(G,j),sourceMarkerByCommand:F}}function kr({details:f,selectedNodeId:u,selectedNodeRuntime:_,control:y,onRaw:l}){if(!f)return X("span",{className:"muted"},"点击“抓取过程”读取 node 运行材料;主界面只显示结构化摘要,完整内容需点开原始 JSON。");let $=Vf(f.procedureRuns),j=$.at(-1)||{},J=Vf(j.attempts),F=J.at(-1)||{},A=Vf(j.workerLogTail),U=Vf(F.controlEventsTail),G=Vf(F.controlPromptsTail),W=Vf(F.monitorPromptsTail),K=bF(U),E=bF(G),H=bF(W),O=F.opencodeMessages||{};return X("div",{className:"pipeline-evidence-list compact"},X(y1,{title:"Node runtime",subtitle:u||"--",facts:[`status ${_?.status||"pending"}`,`attempts ${_?.attempts??J.length}`,`procedure ${_?.currentProcedureRunId||$1(j)||"--"}`,y.fetchedAt?`fetched ${L0(y.fetchedAt)}`:"not fetched"],data:f.node||f,onRaw:l,testId:"raw-pipeline-node-runtime"}),X(y1,{title:"Procedure runs",subtitle:`${$.length} groups`,facts:[`latest ${j.status?.status||j.status||"--"}`,`steps ${Vf(j.recentSteps).length}`,`duration ${l1(Cf(j.finishedAt)&&Cf(j.startedAt)?Number(Cf(j.finishedAt))-Number(Cf(j.startedAt)):j.durationMs)}`],data:$,onRaw:l,testId:"raw-pipeline-node-procedures"}),X(y1,{title:"OpenCode messages",subtitle:String(O.exists?"available":"not indexed"),facts:[`messages ${g5(O.messageCount)}`,`size ${g5(O.size)}`,`updated ${zf(O.updatedAt)}`],data:O,onRaw:l,testId:"raw-pipeline-node-messages"}),X(y1,{title:"Control prompts",subtitle:"manual / monitor append queues",facts:[`manual tail ${E.total}`,`monitor tail ${H.total}`,`last ${zf(aF(E.lastAt,H.lastAt))}`],data:{controlPromptsTail:G,monitorPromptsTail:W},onRaw:l,testId:"raw-pipeline-node-prompts"}),X(y1,{title:"Control events",subtitle:K.eventKinds.length>0?K.eventKinds.join(", "):"event tail",facts:[`tail ${K.total}`,`parsed ${K.parsed}`,`last ${zf(K.lastAt)}`],data:U,onRaw:l,testId:"raw-pipeline-node-events"}),X(y1,{title:"Worker log",subtitle:"tail is hidden on main canvas",facts:[`tail ${A.length} lines`,"raw only via button",`procedure ${$1(j)||"--"}`],data:A,onRaw:l,testId:"raw-pipeline-node-worker-log"}))}function mr({activeRun:f,onRaw:u}){if(!f)return X(pu,{title:"暂无运行材料",text:"没有 Pipeline epoch 时不会展示运行材料索引。"});let _=Vf(f.nodes),y=Vf(f.procedureRuns),l=Vf(f.submissions),$=Vf(f.workerLogTail),j=Kq(_),J=Kq(y),F=y.filter((U)=>String(U?.status||"").toLowerCase()==="failed"),A=aF(...y.flatMap((U)=>[U.updatedAt,U.finishedAt,U.startedAt]));return X("div",{className:"pipeline-evidence-list"},X(y1,{title:"Epoch overview",subtitle:f.runId||"--",facts:[`pipeline ${f.pipelineId||"--"}`,`status ${f.status||"--"}`,`started ${zf(f.startedAt)}`,`updated ${zf(f.updatedAt)}`],data:f,onRaw:u,testId:"raw-pipeline-run"}),X(y1,{title:"Node states",subtitle:`${_.length} nodes`,facts:[`running ${j.running||0}`,`succeeded ${j.succeeded||0}`,`failed ${j.failed||0}`,`pending ${j.pending||0}`],data:_,onRaw:u,testId:"raw-pipeline-run-nodes"}),X(y1,{title:"Procedure run index",subtitle:`${y.length} procedure records`,facts:[`succeeded ${J.succeeded||0}`,`failed ${J.failed||0}`,`latest ${zf(A)}`,`errors ${F.length}`],data:y,onRaw:u,testId:"raw-pipeline-run-procedures"}),X(y1,{title:"OA submissions",subtitle:`${l.length} submission files`,facts:[`records ${l.length}`,`task ${g5(f.task)}`,"raw grouped by run"],data:l,onRaw:u,testId:"raw-pipeline-run-submissions"}),X(y1,{title:"Worker log tail",subtitle:"hidden from main interface",facts:[`tail ${$.length} lines`,"display raw only after click",`updated ${zf(f.updatedAt)}`],data:$,onRaw:u,testId:"raw-pipeline-run-worker-log"}))}function ir({diagnostics:f,onRaw:u}){let _=Vf(f?.runs).filter(Xf),y=Vf(f?.forbiddenResiduals),l=Xf(f?.guarantees)?f.guarantees:{},$=f?.hasNeutralNodeFinishedEvidence===!0&&f?.hasNoAuditPolicyEvidence===!0&&f?.hasAuditPolicyEvidence===!0,j=f?.ok===!0&&$&&y.length===0,J=_[0]||null,F=[{label:"中性完成事实",ok:l.neutralNodeFinished===!0,hint:"node-finished 不携带流程策略"},{label:"Config 策略判定",ok:l.auditPolicyFromConfig===!0,hint:"OA backend 读取当前 epoch 配置"},{label:"控制命令来自 OA",ok:l.runnerConsumesControlCommandsFromOaEvents===!0,hint:"runner 只消费 OA control.command"},{label:"无独立审核事件",ok:l.noIndependentAuditRequestEvent===!0,hint:"审核由 node-finished + policy 派生"},{label:"无批次门禁",ok:l.noBatchFinishedControlGate===!0,hint:"下游启动由每个 node 完成驱动"}];return X("div",{className:"pipeline-oa-panel","data-testid":"pipeline-oa-event-flow-panel"},X("div",{className:"metric-grid compact"},X(Eu,{label:"OA Flow",value:j?"100%":"--",hint:String(f?.mode||"waiting diagnostics"),tone:j?"ok":"warn"}),X(Eu,{label:"禁止残留",value:y.length,hint:y.length===0?"source scan clean":"needs cleanup",tone:y.length===0?"ok":"warn"}),X(Eu,{label:"No-audit",value:f?.hasNoAuditPolicyEvidence?"OK":"--",hint:"OA 下游策略证据",tone:f?.hasNoAuditPolicyEvidence?"ok":"warn"}),X(Eu,{label:"Monitor 审核",value:f?.hasAuditPolicyEvidence?"OK":"--",hint:"OA 控制事件闭环",tone:f?.hasAuditPolicyEvidence?"ok":"warn"})),X("div",{className:"pipeline-oa-guarantees"},F.map((A)=>X("article",{key:A.label,className:`pipeline-oa-guarantee ${A.ok?"ok":"warn"}`},X(uy,{status:A.ok?"online":"warn"},A.ok?"OK":"MISS"),X("div",null,X("strong",null,A.label),X("span",null,A.hint))))),X("div",{className:"pipeline-evidence-list compact"},_.slice(0,6).map((A)=>X(y1,{key:A.runId,title:String(A.runId||"--"),subtitle:[Number(A.monitorAuditNodeFinishedCount||0)>0?"monitor audit":"",Number(A.noAuditPolicyCount||0)>0?"no-audit policy":""].filter(Boolean).join(" / ")||"event evidence",facts:[`events ${A.eventCount||0}`,`node-finished ${A.nodeFinishedCount||0}`,`policy-in-detail ${A.nodeFinishedWithPolicyCount||0}`,`queued ${A.controlQueuedCount||0}`,`applied ${A.controlAppliedCount||0}`],data:A,onRaw:u,testId:`raw-pipeline-oa-run-${String(A.runId||"run").replace(/[^a-zA-Z0-9_.-]+/g,"-")}`}))),J?X("p",{className:"muted paragraph"},`最新证据 ${J.runId}: ${J.nodeFinishedCount||0} 个 node-finished,${J.controlAppliedCount||0} 个控制结果。`):X(pu,{title:"暂无 OA 事件流证据",text:"等待 Pipeline backend 暴露 diagnostics。"}),f?X("div",{className:"panel-actions inline-actions"},X(O1,{title:"Pipeline OA Event Flow Diagnostics",data:f,onOpen:u,testId:"raw-pipeline-oa-event-flow"})):null)}function gr({quota:f,onRaw:u}){let _=Xf(f?.summary)?f.summary:{},y=Xf(f?.target)?f.target:{},l=Xf(f?.cache)?f.cache:{},$=f?.ok===!0,j=String(f?.modelId||_.modelName||y.modelName||"MiniMax-M2.7"),J=_.totalCount??y.currentIntervalTotalCount,F=_.usageCount??y.currentIntervalUsageCount,A=_.remainingCount??y.currentIntervalRemainingCount,U=_.remainingRatio??(Number.isFinite(Number(J))&&Number(J)>0&&Number.isFinite(Number(A))?Number(A)/Number(J):void 0),G=_.usageRatio??(Number.isFinite(Number(J))&&Number(J)>0&&Number.isFinite(Number(F))?Number(F)/Number(J):void 0),W=_.resetAt||y.endAt,K=_.remainsMs??y.remainsMs,E=Number(A),H=!$||Number.isFinite(E)&&E<=0?"warn":"ok",O=[$?`endpoint ${f?.endpoint||"--"}`:"quota unavailable",`fetched ${i5(f?.fetchedAt)}`,l.hit?`cache ${l1(l.ageMs)}`:"live quota"];return X("div",{className:"pipeline-minimax-quota-panel","data-testid":"pipeline-minimax-quota-panel"},X("div",{className:"metric-grid compact"},X(Eu,{label:"MiniMax",value:$?j:"--",hint:f?.modelComponent||f?.error||"model/minimax-m27",tone:H}),X(Eu,{label:"当前窗口",value:`${vF(F)}/${vF(J)}`,hint:`已用 ${zq(G)}`,tone:H}),X(Eu,{label:"剩余额度",value:vF(A),hint:`剩余 ${zq(U)}`,tone:H}),X(Eu,{label:"重置时间",value:i5(W),hint:K!==void 0?`约 ${l1(K)}`:zf(W),tone:H})),X(eF,{items:O}),$?X("p",{className:"muted paragraph"},`MiniMax 限额来自 D601 Pipeline 后端实时查询;当前模型匹配 ${_.modelName||y.modelName||j}。`):X(H0,{error:f?.error||"MiniMax 限额查询失败"}),f?X("div",{className:"panel-actions inline-actions"},X(O1,{title:"Pipeline MiniMax Quota",data:f,onOpen:u,testId:"raw-pipeline-minimax-quota"})):null)}function nr({epochs:f,activeRun:u,activePipeline:_,pipelineNodes:y,pipelineEdges:l,runDetails:$,nodeDetails:j,nodeDetailsState:J,ganttScale:F=B6,onGanttScaleChange:A,onRunChange:U,onIntervalSelect:G,onMarkerSelect:W,selection:K,detailOpen:E,onDetailOpenChange:H,onRaw:O}){let[z,q]=wu(Dr),[Z,V]=wu({startY:0,endY:0,startMs:0,endMs:0}),[L,r]=wu(Date.now()),N=e_(null),D=String(u?.runId||""),x=Boolean(E),c=(uf)=>{if(typeof H==="function")H(uf)},v=_y(F??B6),C=String($?.runId||"")===D?$?.details:null,S=C?{...Xf(u)?u:{},...Xf(C)?C:{},runId:D,procedureRuns:Vf(C?.procedureRuns).length>0?C.procedureRuns:u?.procedureRuns}:u,B=tq(S,y,L),P=C?pr(C,S):{markers:[],arrows:[]},M=Vf(P.markers),w=Br(S,B,M),Y=Pr(w,v,B,L),R=String(Y.source||"frontend-y"),k=B.map((uf)=>Sr(uf,Y,L)),p={startMs:Number(Y.startMs),endMs:Number(Y.endMs),durationMs:Math.max(1,Number(Y.durationMs??Number(Y.endMs)-Number(Y.startMs)))},n=lQ(v),_f={...n,pxPerMinute:Number(Y.pxPerMinute??n.pxPerMinute)},s=Math.round(Number(Y.chartHeight||360)),ff=B.some($Q);b1(()=>{if(!D||!ff)return;let uf=window.setInterval(()=>r(Date.now()),1000);return()=>window.clearInterval(uf)},[D,ff]);let Kf=Zr(_,y,Array.isArray(l)?l:[]),Gf=y.map((uf)=>String(uf?.id||"")).filter(Boolean),jf=k.map((uf)=>String(uf.nodeId||"")).filter(Boolean),Wf=M.map((uf)=>String(uf.nodeId||"")).filter(Boolean),Of=Array.from(new Set([...Kf,...Gf,...jf,...Wf])),Zf={startY:0,endY:s,startMs:Number(p.startMs),endMs:Number(p.endMs)},h=Number(Z?.endY||0)>0?Z:Zf,i=(uf)=>{return d5(uf,p,s,Y)<=Number(h.endY)&&oF(uf,p,s,Y)>=Number(h.startY)},I=(uf)=>{let vf=cu(uf,p,s,Y);return vf>=Number(h.startY)&&vf<=Number(h.endY)},lf=new Set(Of.filter((uf)=>k.some((vf)=>vf.nodeId===uf&&i(vf))||M.some((vf)=>vf.nodeId===uf&&I(vf)))),$f=z?Of.filter((uf)=>lf.has(uf)):Of,Af=`${RF}px ${$f.length>0?$f.map(()=>`${x1}px`).join(" "):"minmax(160px, 1fr)"}`,Yf=Vf(Y.ticks).filter(Xf),xf=String(K?.mode==="interval"?K?.interval?.procedureRunId||"":""),of=String(K?.mode==="event"?K?.marker?.id||"":""),F0=()=>{let uf=N.current;if(!uf){V(Zf);return}let vf=Math.max(0,uf.scrollTop-xF),o0=Math.max(120,uf.clientHeight-xF),Bf=Math.min(s,vf+o0),b0={startY:vf,endY:Bf,startMs:Number(p.startMs),endMs:Number(p.endMs)},i0=Math.max(0,Math.min(1,vf/s)),a0=Math.max(i0,Math.min(1,Bf/s)),nf=Math.max(1,Number(p.endMs)-Number(p.startMs));b0.startMs=Number(p.startMs)+nf*i0,b0.endMs=Number(p.startMs)+nf*a0,V(b0)};b1(()=>{let uf=N.current,vf=window.setTimeout(F0,0);return uf?.addEventListener("scroll",F0),window.addEventListener("resize",F0),()=>{window.clearTimeout(vf),uf?.removeEventListener("scroll",F0),window.removeEventListener("resize",F0)}},[D,p.startMs,p.endMs,s]);let y0=Math.max(0,Of.length-$f.length),T0=new Set(M.filter((uf)=>$f.includes(String(uf.nodeId||""))&&I(uf)).map((uf)=>String(uf.id))),Qu=new Map(M.map((uf)=>[String(uf.id),uf])),X0=Vf(P.arrows).filter((uf)=>{if(!T0.has(String(uf.targetMarkerId||"")))return!1;if(String(uf.action||"")==="observe")return $f.includes(String(uf.sourceNodeId||""));return T0.has(String(uf.sourceMarkerId||""))}),v0=RF+Math.max(1,$f.length)*x1,iu=(uf)=>{let vf=_y(uf.target.value);if(typeof A==="function")A(vf);window.setTimeout(F0,0)},K0=()=>Nr({title:`${_?.id||"pipeline"}-${D||"epoch"}-gantt`,meta:[`run ${D||"--"}`,`${zf(p.startMs)} -> ${zf(p.endMs)}`,`duration ${l1(p.durationMs)}`,`${_f.label} / ${pF(_f.pxPerMinute)} px/min`,`${$f.length}/${Of.length} nodes`,`${M.length} markers`],visibleNodeIds:$f,intervals:k,markers:M.filter((uf)=>$f.includes(String(uf.nodeId||""))),arrows:X0,ticks:Yf,bounds:p,chartHeight:s,backendLayout:Y}),Au=Xf(C?.gantt?.diagnostics)?C.gantt.diagnostics:null;return X(v1,{title:"Epoch 甘特图",eyebrow:`${_?.id||"pipeline"} / ${f.length} epochs`,className:"pipeline-wide-panel",actions:X("div",{className:"pipeline-gantt-actions"},X("select",{value:D,disabled:f.length===0,onChange:(uf)=>U(uf.target.value),"data-testid":"pipeline-epoch-select"},f.map((uf)=>X("option",{key:uf.runId,value:uf.runId},tF(f,uf)))),X("label",{className:"pipeline-gantt-toggle"},X("input",{type:"checkbox","data-testid":"pipeline-gantt-auto-hide-idle",checked:z,onChange:(uf)=>{q(Boolean(uf.target.checked)),window.setTimeout(F0,0)}}),X("span",null,"自动隐藏空闲列")),X("label",{className:"pipeline-gantt-scale"},X("span",null,X("b",null,"时间尺度"),X("em",{"data-testid":"pipeline-gantt-scale-label"},`${_f.label} · ${pF(_f.pxPerMinute)} px/min`)),X("input",{type:"range",min:0,max:100,step:0.01,value:v,onChange:iu,"aria-label":"调整甘特图时间尺度","data-testid":"pipeline-gantt-time-scale"}),X("small",null,X("span",null,"全局"),X("span",null,"细节"))),u?X("button",{type:"button",className:"ghost-btn",onClick:K0,disabled:$f.length===0,"data-testid":"pipeline-export-gantt"},"导出甘特图"):null,u?X(O1,{title:`Pipeline Epoch ${u.runId}`,data:u,onOpen:O,testId:"raw-pipeline-epoch-gantt"}):null)},!u?X(pu,{title:"暂无 Epoch",text:"当前 pipeline 还没有完整运行记录。"}):k.length===0?X(pu,{title:"暂无时间区间",text:"等待 D601 Pipeline backend 在 procedure summary 中返回 startedAt / finishedAt。"}):X("div",{className:"pipeline-gantt-wrap"},X("div",{className:`pipeline-gantt-detail-layout ${x?"detail-open":"detail-collapsed"}`,"data-testid":"pipeline-gantt-detail-layout","data-sidebar-open":x?"true":"false"},X("div",{className:"pipeline-gantt-main"},X("div",{className:"pipeline-gantt-main-head"},X("div",{className:"pipeline-gantt-meta"},X("span",null,`time ${zf(p.startMs)} -> ${zf(p.endMs)}`),X("span",null,`duration ${l1(p.durationMs)}`),X("span",null,`scale ${_f.label} / ${pF(_f.pxPerMinute)} px/min`),X("span",null,`layout ${R}`),Au?X("span",null,`align ${Au.timeAxisAlignmentOk===!1?"check":"ok"}`):null,X("span",null,`visible ${$f.length}/${Of.length} nodes`),C?X("span",null,`markers ${M.length}`):null,z&&y0>0?X("span",null,`hidden idle ${y0}`):null),!x?X("button",{type:"button",className:"pipeline-sidecar-tab right",disabled:!K?.mode,onClick:()=>c(!0),"data-testid":"pipeline-gantt-sidebar-toggle"},K?.mode?"展开详情":"点击甘特图元素展开详情"):null),X("div",{className:"pipeline-gantt-viewport",ref:N,"data-testid":"pipeline-epoch-gantt","data-pipeline-id":_?.id||"","data-run-id":D,"data-layout-source":R,"data-start-ms":String(p.startMs),"data-end-ms":String(p.endMs),"data-chart-height":String(s)},X("div",{className:"pipeline-gantt-board",style:{gridTemplateColumns:Af,minWidth:`${v0}px`}},X("div",{className:"pipeline-gantt-head time"},"Time"),$f.length===0?X("div",{className:"pipeline-gantt-head empty"},"当前时间窗无工作节点"):$f.map((uf)=>X("div",{key:`head-${uf}`,className:"pipeline-gantt-head node",title:uf,"data-testid":"pipeline-gantt-head-node","data-node-id":uf},X(aT,{value:uf}))),X("div",{className:"pipeline-gantt-time-axis",style:{height:`${s}px`}},Yf.map((uf)=>{let vf=fE(uf,p,s,Y);return X("div",{key:`tick-${uf.ms}-${vf}`,className:"pipeline-gantt-tick",style:{top:`${vf}px`},"data-testid":"pipeline-gantt-tick","data-ms":String(uf.ms),"data-y":String(vf)},X("b",null,zf(uf.ms)),X("span",null,`+${l1(Number(uf.offsetMs??Number(uf.ms)-Number(p.startMs)))}`))})),$f.length>0?X("svg",{className:"pipeline-gantt-arrow-layer",width:$f.length*x1,height:s,viewBox:`0 0 ${$f.length*x1} ${s}`,style:{left:`${RF}px`,top:`${xF}px`,width:`${$f.length*x1}px`,height:`${s}px`},"aria-hidden":"true"},X("defs",null,X("marker",{id:"pipeline-gantt-arrowhead",viewBox:"0 0 10 10",refX:9,refY:5,markerWidth:6,markerHeight:6,orient:"auto-start-reverse"},X("path",{d:"M 0 0 L 10 5 L 0 10 z",fill:"context-stroke"}))),X0.map((uf)=>{let vf=Qu.get(String(uf.targetMarkerId||""));if(!vf)return null;let o0=Qu.get(String(uf.sourceMarkerId||"")),Bf=String(o0?.nodeId||uf.sourceNodeId||""),b0=$f.indexOf(Bf),i0=$f.indexOf(String(vf.nodeId||""));if(b0<0||i0<0)return null;let a0=b0*x1+x1/2,nf=i0*x1+x1/2,d0=o0?cu(o0,p,s,Y):cu(vf,p,s,Y),Hu=cu(vf,p,s,Y);return X("path",{key:uf.id,className:`pipeline-gantt-arrow ${String(uf.sourceKind||"").toLowerCase()} ${String(uf.status||"").toLowerCase()} ${String(uf.action||"").toLowerCase()}`,d:uE(a0,d0,nf,Hu),markerEnd:"url(#pipeline-gantt-arrowhead)","data-testid":String(uf.action||"")==="observe"?"pipeline-gantt-observation-arrow":"pipeline-gantt-arrow","data-source-node-id":String(uf.sourceNodeId||""),"data-target-node-id":String(uf.targetNodeId||""),"data-target-marker-id":String(uf.targetMarkerId||""),"data-action":String(uf.action||""),"data-source-y":String(d0),"data-target-y":String(Hu)})})):null,$f.length===0?X("div",{className:"pipeline-gantt-empty-col",style:{height:`${s}px`}},"滚动到有活动的时间段后,相关 node 列会自动出现。"):$f.map((uf)=>{let vf=k.filter((Bf)=>Bf.nodeId===uf),o0=M.filter((Bf)=>String(Bf.nodeId||"")===uf);return X("div",{key:`col-${uf}`,className:"pipeline-gantt-node-col",style:{height:`${s}px`}},vf.map((Bf)=>{let b0=d5(Bf,p,s,Y),i0=oF(Bf,p,s,Y),a0=eq(Bf,p,s,Y),nf=String(Bf.procedureRunId||`${uf}-${Bf.startMs}`);return X("button",{key:nf,type:"button",className:`pipeline-gantt-bar ${Bf.status} ${Bf.live?"live":""} ${xf===nf?"selected":""}`,style:{top:`${b0}px`,height:`${a0}px`},title:`${uf} ${Bf.status} ${zf(Bf.startedAt||Bf.startMs)} -> ${zf(Bf.finishedAt||Bf.endMs)}`,onClick:()=>G(Bf),"data-testid":"pipeline-gantt-line","data-node-id":uf,"data-procedure-run-id":String(Bf.procedureRunId||""),"data-status":String(Bf.status||""),"data-live":Bf.live?"true":"false","data-start-ms":String(Bf.startMs||""),"data-end-ms":String(Bf.endMs||""),"data-y1":String(b0),"data-y2":String(i0),"data-natural-height":String(Math.max(0,i0-b0))},X("strong",null,Bf.status||"working"),X("span",null,l1(Bf.durationMs)))}),o0.map((Bf)=>X("button",{key:Bf.id,type:"button",className:`pipeline-gantt-marker ${Bf.kind} ${Bf.tone||""} ${Bf.status||""} ${of===String(Bf.id)?"selected":""}`,style:{top:`${cu(Bf,p,s,Y)}px`},title:`${Bf.label||"event"} / ${zf(Bf.timestampIso||Bf.timestamp||Bf.ms)}`,onClick:()=>W(Bf),"data-testid":Bf.kind==="prompt"?"pipeline-gantt-prompt-marker":"pipeline-gantt-control-marker","data-marker-id":String(Bf.id||""),"data-ms":String(Bf.ms??Bf.eventMs??""),"data-y":String(cu(Bf,p,s,Y))})))})))),x?X(oT,{selection:K,runDetails:$,nodeDetails:j,nodeDetailsState:J,onRaw:O,onCollapse:()=>c(!1)}):null)))}function $_(){return{loading:!1,actionLoading:"",error:"",message:"",details:null,fetchedAt:null,appendPrompt:"",guidePrompt:"",modifyPrompt:"",approveReason:"",redoReason:""}}function d_(){return{mode:"",runId:"",interval:null,marker:null}}function kF(){return{runId:"",loading:!1,error:"",details:null,fetchedAt:null}}function E6(f,u){return`${f}/microservices/pipeline/proxy${u}`}function tr({activeRun:f,pipelineRuns:u,selectedRunId:_,onRunChange:y,selectedNodeId:l,selectedNodeConfig:$,selectedNodeRuntime:j,control:J,onControlChange:F,onFetch:A,onAction:U,onRaw:G,onCollapse:W}){let K=String(f?.runId||""),E=String(j?.status||"pending"),H=!K||!l||J.loading||Boolean(J.actionLoading),O=(q)=>(Z)=>F({[q]:Z.target.value,error:"",message:""}),z=u.length>0?u:f?[f]:[];return X("aside",{className:"pipeline-node-control","data-testid":"pipeline-node-control"},X("div",{className:"pipeline-node-control-head"},X("div",null,X("p",{className:"panel-eyebrow"},"Manual Node Control"),X("h3",null,l||"点击控制图中的 node")),X("div",{className:"pipeline-node-control-head-actions"},l?X(uy,{status:E},E):X(uy,{status:"pending"},"idle"),X("button",{type:"button",className:"ghost-btn mini",onClick:W,"data-testid":"pipeline-node-sidebar-collapse"},"收起"))),X("div",{className:"pipeline-control-runbar"},X("label",null,X("span",null,"目标 run"),X("select",{value:K||_,disabled:z.length===0,onChange:(q)=>y(q.target.value),"data-testid":"pipeline-node-run-select"},z.map((q)=>X("option",{key:q.runId,value:q.runId},`${q.runId||"--"} / ${q.status||"--"}`)))),X("button",{type:"button",className:"ghost-btn",disabled:H,onClick:A,"data-testid":"pipeline-node-fetch"},J.loading?"抓取中":"抓取过程"),J.details?X(O1,{title:`Pipeline Node ${l}`,data:J.details,onOpen:G,testId:"raw-pipeline-node-control"}):null),X("div",{className:"pipeline-control-meta"},X("span",null,X("b",null,"kind"),String($?.kind||"--")),X("span",null,X("b",null,"procedure"),String(j?.currentProcedureRunId||"--")),X("span",null,X("b",null,"attempts"),String(j?.attempts??"--")),X("span",null,X("b",null,"updated"),zf(f?.updatedAt))),!l?X(pu,{title:"未选择 node",text:"点击 React Flow 控制图中的任意 node 后,可抓取执行过程、追加 prompt、下发引导、增量修改、审核通过或重做。"}):null,X(H0,{error:J.error,wide:!0}),J.message?X("div",{className:"form-success wide"},J.message):null,X("div",{className:"pipeline-control-actions"},X("label",null,X("span",null,"实时追加 prompt(仅 running node)"),X("textarea",{value:J.appendPrompt,onChange:O("appendPrompt"),placeholder:"让当前执行中的 agent 继续、补充检查或调整当前步骤...",rows:4,disabled:!l,"data-testid":"pipeline-node-append-input"}),X("button",{type:"button",className:"primary-btn compact",disabled:H||!String(J.appendPrompt||"").trim(),onClick:()=>U("append"),"data-testid":"pipeline-node-append-button"},J.actionLoading==="append"?"追加中":"追加到运行中 node")),X("label",null,X("span",null,"下次尝试引导 prompt"),X("textarea",{value:J.guidePrompt,onChange:O("guidePrompt"),placeholder:"给该 node 下一次 attempt 的执行提示;不会立即打断当前 session。",rows:4,disabled:!l,"data-testid":"pipeline-node-guide-input"}),X("button",{type:"button",className:"ghost-btn compact",disabled:H||!String(J.guidePrompt||"").trim(),onClick:()=>U("guide"),"data-testid":"pipeline-node-guide-button"},J.actionLoading==="guide"?"下发中":"下发 guide")),X("label",null,X("span",null,"完成后增量修改 prompt"),X("textarea",{value:J.modifyPrompt,onChange:O("modifyPrompt"),placeholder:"在该 node 已完成结果基础上追加修改要求;runner 会重跑目标 node,并保留同 node 既有 OA 输出作为上下文。",rows:4,disabled:!l,"data-testid":"pipeline-node-modify-input"}),X("button",{type:"button",className:"ghost-btn compact",disabled:H||!String(J.modifyPrompt||"").trim(),onClick:()=>U("modify"),"data-testid":"pipeline-node-modify-button"},J.actionLoading==="modify"?"排队中":"增量修改 node")),X("label",null,X("span",null,"Monitor 审核通过原因"),X("textarea",{value:J.approveReason,onChange:O("approveReason"),placeholder:"当流程配置开启 monitor 审核时,记录审核通过原因并释放后续 node。",rows:3,disabled:!l,"data-testid":"pipeline-node-approve-input"}),X("button",{type:"button",className:"primary-btn compact",disabled:H||!String(J.approveReason||"").trim(),onClick:()=>U("approve"),"data-testid":"pipeline-node-approve-button"},J.actionLoading==="approve"?"提交中":"审核通过")),X("label",null,X("span",null,"重做 / restart 原因"),X("textarea",{value:J.redoReason,onChange:O("redoReason"),placeholder:"说明为什么需要重做;runner 会重置目标 node 以及非 rework 下游 node。",rows:4,disabled:!l,"data-testid":"pipeline-node-redo-input"}),X("button",{type:"button",className:"danger-btn compact",disabled:H||!String(J.redoReason||"").trim(),onClick:()=>U("redo"),"data-testid":"pipeline-node-redo-button"},J.actionLoading==="redo"?"排队中":"重做 node"))),X("div",{className:"pipeline-control-evidence"},X("strong",null,"Node 过程索引"),X(kr,{details:J.details,selectedNodeId:l,selectedNodeRuntime:j,control:J,onRaw:G})))}function _E({microservices:f,onRaw:u,apiBaseUrl:_="/api"}){let y=f.find((t)=>t.id==="pipeline")||null,[l,$]=wu({loading:!1,error:"",health:null,snapshot:null,oaDiagnostics:null,minimaxQuota:null,refreshedAt:null}),[j,J]=wu(""),[F,A]=wu(""),[U,G]=wu(""),[W,K]=wu($_()),[E,H]=wu({}),[O,z]=wu(d_()),[q,Z]=wu(kF()),[V,L]=wu(B6),[r,N]=wu(!1),[D,x]=wu(!1),c=e_(0),v=e_(!1),C=e_(0),S=e_(""),B=e_({}),P=e_(""),M=e_("");async function w(t={}){let Hf=t.silent===!0;if(!y)return;if(v.current)return;v.current=!0;let Df=c.current+1;if(c.current=Df,!Hf)$((If)=>({...If,loading:!0,error:""}));try{let If=`__unideskArrayLimit=registry.components:80,runs:${ST}`,[Rf,Q0,af]=await Promise.all([a_(`${_}/microservices/pipeline/proxy/api/snapshot?${If}`,{cache:"no-store"}),a_(`${_}/microservices/pipeline/proxy/api/oa-event-flow/diagnostics`,{cache:"no-store"}).catch((e0)=>({ok:!1,error:Pf(e0,"OA event flow diagnostics failed")})),a_(`${_}/microservices/pipeline/proxy/api/model-quota/minimax`,{cache:"no-store"}).catch((e0)=>({ok:!1,error:Pf(e0,"MiniMax quota failed")}))]);if(Df!==c.current)return;let h0={ok:Rf?.ok!==!1,service:"pipeline-v2-control snapshot"};$({loading:!1,error:"",health:h0,snapshot:Rf,oaDiagnostics:Q0,minimaxQuota:af,refreshedAt:new Date})}catch(If){if(Df!==c.current)return;$((Rf)=>({...Rf,loading:!1,error:Pf(If,"Pipeline 加载失败")}))}finally{v.current=!1}}b1(()=>{if(w(),!y)return;let t=()=>{if(I5())w({silent:!0})},Hf=window.setInterval(()=>{t()},Uq),Df=()=>{if(I5())t()};return document.addEventListener("visibilitychange",Df),()=>{window.clearInterval(Hf),document.removeEventListener("visibilitychange",Df)}},[y?.id,y?.runtime?.providerStatus,_]);let Y=dT(y),R=fr(y),k=eT(y),p=l.snapshot||{},n=l.oaDiagnostics||null,_f=l.minimaxQuota||null,{components:s,pipelines:ff,runs:Kf}=ur(p),Gf=String(Kf[0]?.pipelineId||""),jf=(Gf?ff.find((t)=>String(t.id||"")===Gf):null)||ff[0]||{},Wf=ff.find((t)=>String(t.id||"")===j)||jf,Of=String(Wf.id||""),Zf=kq(Wf),h=uQ(Wf),i=Dq(Kf,Of),I=Yr(Kf,Of),lf=I.find((t)=>String(t?.runId||"")===F)||i,$f=String(q.runId||"")===String(lf?.runId||"")?$r(q.details):null,Af=jr(lf,$f),Yf=String(Af?.runId||""),xf=Zf.find((t)=>String(t?.id||"")===U)||null,of=U?mq(Af,U):null,F0=yr(Kf),y0=Ar(s),T0=Number(l.health?.components)||Vq(p,"registry.components",s.length),Qu=Vq(p,"runs",Kf.length),X0=Nq(Wf,Af,s),v0={nodes:X0.nodes.map((t)=>t.id===U?{...t,selected:!0,className:`${t.className||""} selected-control-node`}:t),edges:X0.edges},iu=ff.map((t)=>{let Hf=String(t.id||"pipeline"),Df=Dq(Kf,Hf);return{title:`${Hf}-${Df?.runId||"snapshot"}`,flow:Nq(t,Df,s)}}),K0=String(O?.runId||Yf||""),Au=String(O?.interval?.nodeId||O?.marker?.nodeId||""),uf=K0&&Au?E[cF(K0,Au)]||null:null,vf=n5(W.details,K0,Au),o0=n5(uf?.details,K0,Au)||vf,Bf=K0&&Au?{...Xf(uf)?uf:{},runId:K0,nodeId:Au,details:o0,loading:Boolean(uf?.loading)||!o0&&Boolean(W.loading)&&U===Au,error:String(uf?.error||""),fetchedAt:uf?.fetchedAt||(vf?W.fetchedAt:null)}:null,b0=I.map((t)=>String(t?.runId||"")).filter(Boolean).join("|"),i0=Zf.map((t)=>String(t?.id||"")).filter(Boolean).join("|");b1(()=>{P.current=U},[U]),b1(()=>{M.current=Yf},[Yf]),b1(()=>{if(!F||b0.split("|").includes(F))return;A("")},[F,b0]),b1(()=>{if(!U||i0.split("|").includes(U))return;G(""),K($_()),z(d_()),N(!1),x(!1)},[U,i0]),b1(()=>{if(!U)N(!1)},[U]),b1(()=>{if(!O.mode)x(!1)},[O.mode]);async function a0(t=Yf,Hf={}){if(!t){Z(kF());return}let Df=_y(Hf.scale??V??B6),If=`${t}:timeline`;if(S.current===If)return;S.current=If;let Rf=Hf.silent===!0,Q0=C.current+1;C.current=Q0,Z((af)=>({runId:t,scale:Df,loading:!Rf||String(af.runId||"")!==t||!af.details,error:"",details:Rf&&af.runId===t?af.details:af.runId===t?af.details:null,fetchedAt:af.runId===t?af.fetchedAt:null}));try{let[af,h0]=await Promise.all([a_(E6(_,`/api/node-control/runs/${encodeURIComponent(t)}?tail=160&view=timeline`),{cache:"no-store",strictJson:!0}),a_(E6(_,`/api/runs/${encodeURIComponent(t)}`),{cache:"no-store"}).catch((e0)=>({ok:!1,runSummaryError:Pf(e0,"抓取评分失败")}))]);if(Q0!==C.current)return;Z({runId:t,scale:Df,loading:!1,error:"",details:{...af,run:Xf(h0?.run)?h0.run:void 0,runSummaryError:h0?.runSummaryError},fetchedAt:new Date})}catch(af){if(Q0!==C.current)return;Z((h0)=>({runId:t,scale:Df,loading:!1,error:Pf(af,"抓取 epoch 执行过程失败"),details:h0.runId===t?h0.details:null,fetchedAt:h0.runId===t?h0.fetchedAt:null}))}finally{if(S.current===If)S.current=""}}function nf(t,Hf,Df){let If=cF(t,Hf);H((Rf)=>{let Q0={...Rf,[If]:{...Xf(Rf?.[If])?Rf[If]:{},runId:t,nodeId:Hf,...Df}},af=Object.keys(Q0);if(af.length>32)for(let h0 of af.slice(0,af.length-32))delete Q0[h0];return Q0})}async function d0(t,Hf){if(!t||!Hf)return;let Df=cF(t,Hf),If=Number(B.current?.[Df]||0)+1;B.current={...B.current,[Df]:If},nf(t,Hf,{loading:!0,error:""});try{let Rf=await a_(E6(_,`/api/node-control/runs/${encodeURIComponent(t)}/nodes/${encodeURIComponent(Hf)}?tail=160`),{cache:"no-store",strictJson:!0});if(Number(B.current?.[Df]||0)!==If)return;let Q0=new Date;if(nf(t,Hf,{loading:!1,details:Rf,fetchedAt:Q0,error:""}),P.current===Hf&&M.current===t)K((af)=>({...af,loading:!1,details:Rf,fetchedAt:Q0,error:""}))}catch(Rf){if(Number(B.current?.[Df]||0)!==If)return;nf(t,Hf,{loading:!1,error:Pf(Rf,"抓取 Gantt node 详情失败")})}}b1(()=>{if(!Yf){Z(kF());return}a0(Yf);let t=()=>{if(I5())a0(Yf,{silent:!0})},Hf=window.setInterval(()=>{t()},Uq),Df=()=>{if(I5())t()};return document.addEventListener("visibilitychange",Df),()=>{window.clearInterval(Hf),document.removeEventListener("visibilitychange",Df)}},[Yf,_]);async function Hu(t=Yf,Hf=U){if(!t||!Hf){K((Df)=>({...Df,error:"请先选择 run 和 node",message:""}));return}K((Df)=>({...Df,loading:!0,error:"",message:""}));try{let Df=await a_(E6(_,`/api/node-control/runs/${encodeURIComponent(t)}/nodes/${encodeURIComponent(Hf)}?tail=160`),{cache:"no-store",strictJson:!0}),If=new Date;K((Rf)=>({...Rf,loading:!1,details:Df,fetchedAt:If,error:""})),nf(t,Hf,{loading:!1,details:Df,fetchedAt:If,error:""})}catch(Df){K((If)=>({...If,loading:!1,error:Pf(Df,"抓取 node 执行过程失败")}))}}async function oy(t){let Hf=String(t?.runId||Yf||""),Df=String(t?.nodeId||"");if(z({mode:"interval",runId:Hf,interval:t,marker:null}),x(!0),!Hf||!Df)return;if(Hf!==Yf)A(Hf);G(Df),K($_()),a0(Hf,{silent:!0}),d0(Hf,Df)}async function J_(t){let Hf=String(t?.runId||Yf||""),Df=String(t?.nodeId||"");if(z({mode:"event",runId:Hf,interval:null,marker:t}),x(!0),!Hf)return;if(Hf!==Yf)A(Hf);if(a0(Hf,{silent:!0}),!Df)return;G(Df),K($_()),d0(Hf,Df)}async function ay(t){if(!Yf||!U){K((If)=>({...If,error:"请先选择 run 和 node",message:""}));return}let Hf=t==="append"?"prompts":t,Df=t==="append"?W.appendPrompt:t==="guide"?W.guidePrompt:t==="modify"?W.modifyPrompt:t==="approve"?W.approveReason:W.redoReason;if(!String(Df||"").trim()){K((If)=>({...If,error:"操作内容不能为空",message:""}));return}K((If)=>({...If,actionLoading:t,error:"",message:""}));try{let If=t==="redo"||t==="approve"?{reason:Df,source:"unidesk-frontend",sourceKind:"webui"}:{prompt:Df,source:"unidesk-frontend",sourceKind:"webui"},Rf=await a_(E6(_,`/api/node-control/runs/${encodeURIComponent(Yf)}/nodes/${encodeURIComponent(U)}/${Hf}`),{method:"POST",body:JSON.stringify(If)});if(K((Q0)=>({...Q0,actionLoading:"",details:Rf,fetchedAt:new Date,appendPrompt:t==="append"?"":Q0.appendPrompt,guidePrompt:t==="guide"?"":Q0.guidePrompt,modifyPrompt:t==="modify"?"":Q0.modifyPrompt,approveReason:t==="approve"?"":Q0.approveReason,redoReason:t==="redo"?"":Q0.redoReason,message:t==="append"?"已追加到运行中 node":t==="guide"?"已下发 guide,等待 runner 处理":t==="modify"?"已排队增量修改命令":t==="approve"?"已提交审核通过决策":"已排队重做命令"})),await Hu(Yf,U),await a0(Yf,{silent:!0}),t!=="append")await w()}catch(If){K((Rf)=>({...Rf,actionLoading:"",error:Pf(If,"node 控制操作失败")}))}}if(!y)return X(pu,{title:"Pipeline 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=pipeline"});return X("div",{className:"pipeline-page","data-testid":"pipeline-page"},X(v1,{title:"Pipeline v2 工作台",eyebrow:"D601 Snapshot 用户服务",actions:X("div",{className:"panel-actions"},X("button",{type:"button",className:"ghost-btn",onClick:w,disabled:l.loading,"data-testid":"pipeline-refresh-button"},l.loading?"刷新中":"刷新"),X(O1,{title:"Pipeline 用户服务",data:y,onOpen:u,testId:"raw-pipeline-service"}))},X("div",{className:"pipeline-hero"},X("div",null,X("div",{className:"node-version-line"},X(uy,{status:Y.providerStatus==="online"?"online":"warn"},Y.providerStatus||"unknown"),X("span",null,y.providerId),X("span",null,k.public?"公网暴露":"仅 UniDesk frontend 代理访问")),X("p",{className:"muted paragraph"},y.description)),X("div",{className:"microservice-ref-card"},X("span",null,"Repo"),X("strong",null,R.url||"--"),X("code",null,R.commitId||"--")),X("div",{className:"microservice-ref-card"},X("span",null,"D601 Docker"),X("strong",null,`${k.nodeBindHost||"--"}:${k.nodePort||"--"}`),X("code",null,`${R.composeFile||"--"} / ${R.composeService||"--"}`))),X(H0,{error:l.error,wide:!0})),X("div",{className:"pipeline-grid"},X(v1,{title:"控制图",eyebrow:`${Wf.id||"pipeline"} / run ${Af?.status||"--"}`,className:"pipeline-wide-panel",actions:X("div",{className:"pipeline-toolbar"},X("select",{value:Of,disabled:ff.length===0,onChange:(t)=>{J(t.target.value),A(""),G(""),K($_()),z(d_()),N(!1),x(!1)},"data-testid":"pipeline-select"},ff.map((t)=>X("option",{key:t.id,value:t.id},t.id||t.key))),X("select",{value:Yf,disabled:I.length===0,onChange:(t)=>{if(A(t.target.value),K($_()),z(d_()),N(!1),x(!1),U)Hu(t.target.value,U)},"data-testid":"pipeline-run-select"},I.map((t)=>X("option",{key:t.runId,value:t.runId},tF(I,t)))),X("button",{type:"button",className:"ghost-btn",disabled:v0.nodes.length===0,onClick:()=>nq(v0,`${Wf.id||"pipeline"}-${Af?.runId||"snapshot"}`),"data-testid":"pipeline-export-graph"},"导出渲染图"),X("button",{type:"button",className:"ghost-btn",disabled:iu.every((t)=>t.flow.nodes.length===0),onClick:()=>Lr(iu),"data-testid":"pipeline-export-all-graphs"},"批量导出"))},Zf.length===0?X(pu,{title:"暂无控制图",text:"等待 D601 pipeline backend 返回 config.nodes / config.edges"}):X("div",{className:`pipeline-control-shell ${r?"detail-open":"detail-collapsed"}`,"data-testid":"pipeline-control-shell","data-sidebar-open":r?"true":"false"},X("div",{className:"pipeline-flow-frame","data-testid":"pipeline-react-flow"},X(lq,{nodes:v0.nodes,edges:v0.edges,nodeTypes:bT,edgeTypes:vT,fitView:!0,fitViewOptions:{padding:0.18},nodesDraggable:!1,nodesConnectable:!1,elementsSelectable:!0,minZoom:0.25,maxZoom:1.4,proOptions:{hideAttribution:!0},onNodeClick:(t,Hf)=>{let Df=String(Hf.id);if(G(Df),K($_()),N(!0),Yf)Hu(Yf,Df)}},X(jq,{gap:22,size:1,color:"rgba(215, 161, 58, 0.24)"}),X(Fq,{showInteractive:!1})),!r?X("button",{type:"button",className:"pipeline-sidecar-tab right",disabled:!U,onClick:()=>N(!0),"data-testid":"pipeline-node-sidebar-toggle"},U?"展开 node 控制":"点击 node 展开控制"):null),r?X(tr,{activeRun:Af,pipelineRuns:I,selectedRunId:F,onRunChange:(t)=>{if(A(t),K($_()),z(d_()),U)Hu(t,U)},selectedNodeId:U,selectedNodeConfig:xf,selectedNodeRuntime:of,control:W,onControlChange:(t)=>K((Hf)=>({...Hf,...t})),onFetch:()=>Hu(),onAction:ay,onRaw:u,onCollapse:()=>N(!1)}):null),X("div",{className:"pipeline-flow-summary"},X("span",null,`${v0.nodes.length} nodes`),X("span",null,`${v0.edges.length} edges`),X("span",null,`${ff.length} pipelines`),X("span",null,`source config+components(${s.length})`),X("span",null,`run ${Af?.runId||"--"}`),X("span",null,`score ${nF(Af)}`),X("span",null,U?`selected ${U}`:"click node to control"))),X(nr,{epochs:I,activeRun:Af,activePipeline:Wf,pipelineNodes:Zf,pipelineEdges:h,selection:O,detailOpen:D,onDetailOpenChange:x,runDetails:q,nodeDetails:o0,nodeDetailsState:Bf,ganttScale:V,onGanttScaleChange:L,onIntervalSelect:oy,onMarkerSelect:J_,onRunChange:(t)=>{if(A(t),K($_()),z(d_()),x(!1),U)Hu(t,U)},onRaw:u}),X(v1,{title:"观测指标",eyebrow:l.refreshedAt?`Updated ${L0(l.refreshedAt)}`:"Snapshot"},X("div",{className:"metric-grid"},X(Eu,{label:"Health",value:l.health?.ok?"OK":"--",hint:l.health?.service||"D601 /health",tone:l.health?.ok?"ok":"warn"}),X(Eu,{label:"组件",value:T0,hint:"components registry",tone:p?.registry?.ok===!1?"warn":"ok"}),X(Eu,{label:"Pipeline",value:ff.length,hint:`${Zf.length} nodes / ${h.length} edges`}),X(Eu,{label:"运行记录",value:Qu,hint:`${F0.succeeded||0} succeeded / ${F0.running||0} running`}),X(Eu,{label:"OA 记录",value:Array.isArray(i?.submissions)?i.submissions.length:0,hint:i?.runId||"latest run"}),X(Eu,{label:"Procedure",value:Array.isArray(i?.procedureRuns)?i.procedureRuns.length:0,hint:i?.status||"no run"}),X(Eu,{label:"Score",value:nF(Af),hint:Af?.runId||"selected epoch",tone:yQ(Af)})),X("div",{className:"panel-actions inline-actions"},X(O1,{title:"Pipeline Snapshot",data:p,onOpen:u,testId:"raw-pipeline-snapshot"}))),X(v1,{title:"评分器",eyebrow:Af?.runId||"selected epoch"},X(Qr,{run:Af,onRaw:u})),X(v1,{title:"MiniMax 限额",eyebrow:"model/minimax-m27 quota"},X(gr,{quota:_f,onRaw:u})),X(v1,{title:"OA 事件流",eyebrow:"100% event-driven diagnostics",className:"pipeline-wide-panel"},X(ir,{diagnostics:n,onRaw:u})),X(v1,{title:"组件矩阵",eyebrow:`${y0.length} classes`},y0.length===0?X(pu,{title:"暂无组件",text:"等待 D601 pipeline backend 返回 registry.components"}):X("div",{className:"component-strata"},y0.map((t)=>X("article",{key:t.name,className:"component-stratum"},X("span",null,t.name),X("strong",null,t.count)))),X("div",{className:"pipeline-component-list"},s.slice(0,12).map((t)=>X("span",{key:t.key,className:"data-chip"},X("b",null,t.componentClass||"--"),X("span",null,t.id||t.key||"--"))))),X(v1,{title:"Epoch 列表",eyebrow:`${I.length}/${Qu} preview`},I.length===0?X(pu,{title:"暂无运行记录",text:"当前 pipeline 在 .state/pipeline-runs 中还没有 epoch。"}):X("div",{className:"pipeline-run-list"},I.map((t)=>{let Hf=String(t?.runId||"")===Yf?Af:t;return X("article",{key:t.runId,className:`pipeline-run-card ${String(t.runId||"")===Yf?"active":""}`,role:"button",tabIndex:0,onClick:()=>{A(String(t.runId||"")),z(d_())},onKeyDown:(Df)=>{if(Df.key==="Enter"||Df.key===" ")A(String(t.runId||"")),z(d_())}},X("div",{className:"node-card-head"},X("strong",null,tF(I,t)),X(uy,{status:t.status},t.status||"--")),X("div",{className:"docker-meta compact"},X("span",null,Hf?.pipelineId||"--"),X("span",null,`nodes ${Array.isArray(Hf?.nodes)?Hf.nodes.length:0}`),X("span",null,`oa ${Array.isArray(Hf?.submissions)?Hf.submissions.length:0}`),X("span",null,`procedures ${Array.isArray(Hf?.procedureRuns)?Hf.procedureRuns.length:0}`),X(Fr,{run:Hf})),X("p",{className:"muted paragraph"},g5(Hf?.task)),X("span",{className:"pipeline-run-time"},zf(Hf?.updatedAt)))}))),X(v1,{title:"运行材料索引",eyebrow:Af?.runId||"selected epoch",className:"pipeline-wide-panel"},X(mr,{activeRun:Af,onRaw:u}))))}var l2=Sf(c0(),1);var e=l2.default.createElement,{useEffect:sr}=l2.default,_2=l2.default.useState,JQ={id:"",sequenceNo:"",contractNo:"",name:"",currentStatus:"",pending:"",paymentStatus:"",notes:""};function or({status:f,children:u}){let _=String(f||"unknown").toLowerCase();return e("span",{className:`status-badge ${_}`},u||f||"unknown")}function y2({label:f,value:u,hint:_,tone:y}){return e("article",{className:`metric-card ${y||""}`},e("div",{className:"metric-label"},f),e("div",{className:"metric-value"},u),e("div",{className:"metric-hint"},_))}function FQ({title:f,eyebrow:u,actions:_,children:y,className:l}){return e("section",{className:`panel ${l||""}`},e("div",{className:"panel-head"},e("div",null,u?e("p",{className:"panel-eyebrow"},u):null,e("h2",null,f)),_?e("div",{className:"panel-actions"},_):null),e("div",{className:"panel-body"},y))}function yE({title:f,data:u,onOpen:_,testId:y}){return e("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:()=>_(f,u)},"查看原始JSON")}function lE({title:f,text:u}){return e("div",{className:"empty-state"},e("strong",null,f),e("span",null,u))}function ar(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function dr(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function er(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function z3(f,u){return`${f}/microservices/project-manager/proxy${u}`}function fM(f){return{id:String(f.id||""),sequenceNo:f.sequenceNo===null||f.sequenceNo===void 0?"":String(f.sequenceNo),contractNo:String(f.contractNo||""),name:String(f.name||""),currentStatus:String(f.currentStatus||""),pending:String(f.pending||""),paymentStatus:String(f.paymentStatus||""),notes:String(f.notes||"")}}function uM(f){return{sequenceNo:f.sequenceNo===""?null:Number(f.sequenceNo),contractNo:String(f.contractNo||"").trim(),name:String(f.name||"").trim(),currentStatus:String(f.currentStatus||"").trim(),pending:String(f.pending||"").trim(),paymentStatus:String(f.paymentStatus||"").trim(),paymentRatio:String(f.paymentStatus||"").trim(),notes:String(f.notes||"").trim()}}function QQ(f){return String(f||"item").replace(/[^A-Za-z0-9_-]+/g,"-")}function _M(f){let u=new Uint8Array(f),_="",y=32768;for(let l=0;le("tr",{key:l.id,className:u===l.id?"active-row":"","data-testid":`project-manager-row-${QQ(l.id)}`},e("td",null,l.sequenceNo??"--"),e("td",null,e("strong",null,l.contractNo||"--"),e("code",null,l.id||"--")),e("td",null,e("strong",null,l.name||"--"),e("span",{className:"muted block"},l.sourceFile||"--")),e("td",null,l.currentStatus||"--"),e("td",null,e("span",{className:"preline"},l.pending||"--")),e("td",null,e(or,{status:Number(l.paymentRatio||0)>=1?"online":"warn"},l.paymentStatus||"--")),e("td",null,l.notes||"--"),e("td",null,e("div",{className:"inline-actions"},e("button",{type:"button",className:"ghost-btn",onClick:()=>_(l),"data-testid":`project-manager-edit-${QQ(l.id)}`},"编辑"),e(yE,{title:`Project ${l.contractNo||l.id}`,data:l,onOpen:y,testId:`raw-project-${QQ(l.id)}`}))))))))}function $E({microservices:f,onRaw:u,apiBaseUrl:_="/api"}){let y=f.find((N)=>N.id==="project-manager")||null,[l,$]=_2({loading:!1,saving:!1,importing:!1,exporting:!1,error:"",notice:"",health:null,list:null,refreshedAt:null}),[j,J]=_2({...JQ}),[F,A]=_2(""),[U,G]=_2("all");async function W(N=F,D=U){if(!y)return;$((x)=>({...x,loading:!0,error:""}));try{let x=new URLSearchParams({pageSize:"200",status:D});if(N.trim())x.set("q",N.trim());let[c,v]=await Promise.all([wf(`${_}/microservices/project-manager/health`),wf(z3(_,`/api/projects?${x.toString()}`))]);$((C)=>({...C,loading:!1,health:c,list:v,refreshedAt:new Date,error:""}))}catch(x){$((c)=>({...c,loading:!1,error:Pf(x,"Project Manager 加载失败")}))}}sr(()=>{W()},[y?.id,y?.runtime?.providerStatus]);async function K(N){N.preventDefault(),$((D)=>({...D,saving:!0,error:"",notice:""}));try{let D=uM(j);if(j.id)await wf(z3(_,`/api/projects/${encodeURIComponent(j.id)}`),{method:"PUT",body:JSON.stringify(D)});else await wf(z3(_,"/api/projects"),{method:"POST",body:JSON.stringify(D)});$((x)=>({...x,saving:!1,notice:j.id?"项目已更新":"项目已创建"})),await W()}catch(D){$((x)=>({...x,saving:!1,error:Pf(D,"保存项目失败")}))}}async function E(){if(!j.id)return;if(!window.confirm(`删除项目 ${j.contractNo||j.name||j.id} ?`))return;$((N)=>({...N,saving:!0,error:"",notice:""}));try{await wf(z3(_,`/api/projects/${encodeURIComponent(j.id)}`),{method:"DELETE"}),J({...JQ}),$((N)=>({...N,saving:!1,notice:"项目已删除"})),await W()}catch(N){$((D)=>({...D,saving:!1,error:Pf(N,"删除项目失败")}))}}async function H(N){let D=N.target.files?.[0];if(!D)return;$((x)=>({...x,importing:!0,error:"",notice:""}));try{let x=_M(await D.arrayBuffer()),c=await wf(z3(_,"/api/import/excel"),{method:"POST",body:JSON.stringify({fileName:D.name,contentBase64:x,replace:!1})});$((v)=>({...v,importing:!1,notice:`Excel 已导入 ${c.imported||0} 条项目`})),N.target.value="",await W()}catch(x){$((c)=>({...c,importing:!1,error:Pf(x,"Excel 导入失败")}))}}async function O(){$((N)=>({...N,exporting:!0,error:""}));try{let N=await JG(z3(_,"/api/projects/export.xlsx")),D=URL.createObjectURL(N),x=document.createElement("a");x.href=D,x.download=`project-manager-${IQ()}.xlsx`,document.body.appendChild(x),x.click(),x.remove(),URL.revokeObjectURL(D),$((c)=>({...c,exporting:!1,notice:"Excel 已导出"}))}catch(N){$((D)=>({...D,exporting:!1,error:Pf(N,"Excel 导出失败")}))}}if(!y)return e(lE,{title:"Project Manager 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=project-manager"});let z=ar(y),q=er(y),Z=dr(y),V=Array.isArray(l.list?.projects)?l.list.projects:[],L=l.list?.summary||{},r=l.health||{};return e("div",{className:"project-manager-page","data-testid":"project-manager-page"},e(FQ,{title:"项目管理工作台",eyebrow:"Main Server PostgreSQL 用户服务",actions:e("div",{className:"panel-actions"},e("button",{type:"button",className:"ghost-btn",disabled:l.loading,onClick:()=>W(),"data-testid":"project-manager-refresh-button"},l.loading?"刷新中":"刷新"),e("button",{type:"button",className:"ghost-btn",disabled:l.exporting,onClick:O,"data-testid":"project-manager-export-button"},l.exporting?"导出中":"导出 Excel"),e(yE,{title:"Project Manager 用户服务",data:y,onOpen:u,testId:"raw-project-manager-service"}))},e("div",{className:"project-manager-hero"},e(y2,{label:"项目总数",value:L.total??V.length,hint:`PG 表 ${r.storage?.table||"project_manager_projects"}`,tone:"ok"}),e(y2,{label:"进行中",value:L.active??"--",hint:"当前状态未完全完成"}),e(y2,{label:"已完成",value:L.completed??"--",hint:"按 完成 关键字统计",tone:"ok"}),e(y2,{label:"未全款",value:L.unpaid??"--",hint:"付款比例 < 1",tone:Number(L.unpaid||0)>0?"warn":"ok"})),e(H0,{error:l.error}),l.notice?e("div",{className:"form-success"},l.notice):null),e("div",{className:"project-manager-hero"},e("div",{className:"microservice-ref-card"},e("span",null,"Repo"),e("strong",null,q.url||"--"),e("code",null,q.commitId||"--")),e("div",{className:"microservice-ref-card"},e("span",null,"Main Server Docker"),e("strong",null,`${Z.nodeBindHost||"--"}:${Z.nodePort||"--"}`),e("code",null,`${q.composeService||"--"} / ${q.containerName||"--"}`)),e("div",{className:"microservice-ref-card"},e("span",null,"Runtime"),e("strong",null,z.providerName||y.providerId),e("code",null,`Health ${r.ok?"OK":"--"} / ${l.refreshedAt?L0(l.refreshedAt):"--"}`)),e("div",{className:"microservice-ref-card"},e("span",null,"Import Source"),e("strong",null,"D601 WeChat Excel"),e("code",null,"合作项目列表_I_20260309.xlsx"))),e("div",{className:"project-manager-layout"},e(FQ,{title:"项目清单",eyebrow:"CRUD + Excel Export",actions:e("div",{className:"inline-actions project-manager-filters"},e("input",{value:F,onChange:(N)=>A(N.target.value),placeholder:"搜索合同号 / 项目名称 / 状态","data-testid":"project-manager-search"}),e("select",{value:U,onChange:(N)=>{G(N.target.value),W(F,N.target.value)},"data-testid":"project-manager-status-filter"},e("option",{value:"all"},"全部"),e("option",{value:"active"},"进行中"),e("option",{value:"completed"},"已完成"),e("option",{value:"unpaid"},"未全款")),e("button",{type:"button",className:"ghost-btn",onClick:()=>W(F,U)},"筛选"))},e(yM,{projects:V,activeId:j.id,onSelect:(N)=>J(fM(N)),onRaw:u})),e(FQ,{title:j.id?"编辑项目":"新建项目",eyebrow:"PostgreSQL Write Path"},e("form",{className:"stack-form project-manager-form",onSubmit:K,"data-testid":"project-manager-form"},j.id?e("label",null,"项目 ID",e("input",{value:j.id,disabled:!0})):null,e("label",null,"序号",e("input",{type:"number",value:j.sequenceNo,onChange:(N)=>J((D)=>({...D,sequenceNo:N.target.value}))})),e("label",null,"合同号",e("input",{value:j.contractNo,onChange:(N)=>J((D)=>({...D,contractNo:N.target.value})),required:!0})),e("label",null,"项目名称",e("input",{value:j.name,onChange:(N)=>J((D)=>({...D,name:N.target.value})),required:!0})),e("label",null,"当前状况",e("textarea",{value:j.currentStatus,onChange:(N)=>J((D)=>({...D,currentStatus:N.target.value}))})),e("label",null,"待完成",e("textarea",{value:j.pending,onChange:(N)=>J((D)=>({...D,pending:N.target.value}))})),e("label",null,"付款情况",e("input",{value:j.paymentStatus,onChange:(N)=>J((D)=>({...D,paymentStatus:N.target.value})),placeholder:"例如 1 / 0.5 / 50%"})),e("label",null,"其它",e("input",{value:j.notes,onChange:(N)=>J((D)=>({...D,notes:N.target.value}))})),e("div",{className:"inline-actions"},e("button",{type:"submit",className:"primary-btn",disabled:l.saving,"data-testid":"project-manager-save-button"},l.saving?"保存中":j.id?"保存修改":"创建项目"),e("button",{type:"button",className:"ghost-btn",onClick:()=>J({...JQ})},"清空"),j.id?e("button",{type:"button",className:"danger-btn",disabled:l.saving,onClick:E,"data-testid":"project-manager-delete-button"},"删除"):null)),e("div",{className:"project-manager-import"},e("p",{className:"muted paragraph"},"浏览器只访问 UniDesk frontend;后端通过同源用户服务代理写入主 PostgreSQL,不暴露 4233 公网端口。"),e("label",{className:"file-import"},l.importing?"导入中...":"导入 Excel",e("input",{type:"file",accept:".xlsx",onChange:H,disabled:l.importing,"data-testid":"project-manager-import-input"}))))))}var F2=Sf(c0(),1);var yf=F2.default.createElement,{useEffect:lM}=F2.default,ru=F2.default.useState;function $M({status:f,children:u}){let _=String(f||"unknown").toLowerCase();return yf("span",{className:`status-badge ${_}`},u||f||"unknown")}function $2({label:f,value:u,hint:_,tone:y}){return yf("article",{className:`metric-card ${y||""}`},yf("div",{className:"metric-label"},f),yf("div",{className:"metric-value"},u),yf("div",{className:"metric-hint"},_))}function AQ({title:f,eyebrow:u,actions:_,children:y,className:l}){return yf("section",{className:`panel ${l||""}`},yf("div",{className:"panel-head"},yf("div",null,u?yf("p",{className:"panel-eyebrow"},u):null,yf("h2",null,f)),_?yf("div",{className:"panel-actions"},_):null),yf("div",{className:"panel-body"},y))}function jE({title:f,data:u,onOpen:_,testId:y}){return yf("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:()=>_(f,u)},"查看原始JSON")}function j2({title:f,text:u}){return yf("div",{className:"empty-state"},yf("strong",null,f),yf("span",null,u))}function jM(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function JM(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function FM(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function FE(f){return String(f).replace(/[^a-zA-Z0-9_-]/g,"_")}function QM(f){if(!Number.isFinite(f))return"--";return`${f.toFixed(1)}%`}function K3(f,u){return`${f}/microservices/todo-note/proxy${u}`}function QE(f){return f.reduce((u,_)=>{let y=QE(Array.isArray(_.children)?_.children:[]),l=Boolean(_.completed);return{total:u.total+1+y.total,completed:u.completed+(l?1:0)+y.completed,active:u.active+(l?0:1)+y.active}},{total:0,completed:0,active:0})}function J2(f,u){let _=u==="all"||(u==="completed"?Boolean(f.completed):!f.completed),y=Array.isArray(f.children)?f.children:[];return _||y.some((l)=>J2(l,u))}function JE(f){return Array.isArray(f?.instances)?f.instances:[]}function AE({microservices:f,onRaw:u,apiBaseUrl:_="/api"}){let y=f.find((I)=>I.id==="todo-note")||null,[l,$]=ru(null),[j,J]=ru(null),[F,A]=ru(""),[U,G]=ru(null),[W,K]=ru("all"),[E,H]=ru(13),[O,z]=ru(""),[q,Z]=ru(""),[V,L]=ru(""),[r,N]=ru(""),[D,x]=ru(""),[c,v]=ru(!1),[C,S]=ru(""),[B,P]=ru(null),M=JE(j),w=QE(Array.isArray(U?.todos)?U.todos:[]),Y=y?jM(y):{},R=y?FM(y):{},k=y?JM(y):{};async function p(I=F){let[lf,$f]=await Promise.all([wf(`${_}/microservices/todo-note/health`),wf(K3(_,"/api/instances"))]);$(lf),J($f);let Af=JE($f),Yf=Af.some((xf)=>xf.id===I)?I:Af[0]?.id||"";return A(Yf),Yf}async function n(I=F){if(!I){G(null);return}let lf=await wf(K3(_,`/api/instances/${encodeURIComponent(I)}`));G(lf)}async function _f(I=F){if(!y)return;v(!0),S("");try{let lf=await p(I);await n(lf),P(new Date)}catch(lf){S(Pf(lf,"Todo Note 加载失败"))}finally{v(!1)}}async function s(I){if(!F)return;S("");try{let lf=await wf(K3(_,`/api/instances/${encodeURIComponent(F)}/actions`),{method:"POST",body:JSON.stringify({action:I})});G(lf),await p(F)}catch(lf){S(Pf(lf,"Todo 操作失败"))}}async function ff(I){I.preventDefault();let lf=O.trim();if(!lf)return;v(!0),S("");try{let $f=await wf(K3(_,"/api/instances"),{method:"POST",body:JSON.stringify({name:lf})});z(""),await _f($f.id)}catch($f){S(Pf($f,"创建清单失败"))}finally{v(!1)}}async function Kf(I){if(!window.confirm("确认删除这个 Todo Note 清单?"))return;v(!0),S("");try{await wf(K3(_,`/api/instances/${encodeURIComponent(I)}`),{method:"DELETE"}),await _f(F===I?"":F)}catch(lf){S(Pf(lf,"删除清单失败"))}finally{v(!1)}}async function Gf(I){I.preventDefault();let lf=q.trim();if(!lf)return;Z(""),await s({type:"addTodo",title:lf})}async function jf(I){if(!F)return;S("");try{let lf=await wf(K3(_,`/api/instances/${encodeURIComponent(F)}/${I}`),{method:"POST",body:JSON.stringify({})});G(lf),await p(F)}catch(lf){S(Pf(lf,`${I} 失败`))}}function Wf(I){L(I.id),N(String(I.title||""))}async function Of(I){let lf=r.trim();if(L(""),N(""),lf)await s({type:"updateTodoTitle",todoId:I,title:lf})}async function Zf(I){let lf=window.prompt("新增子任务标题");if(lf&&lf.trim())await s({type:"addTodo",title:lf.trim(),parentId:I})}async function h(I,lf){if(!D)return;let $f={type:"moveTodo",todoId:D,targetIndex:lf};if(I)$f.targetParentId=I;x(""),await s($f)}if(lM(()=>{_f()},[y?.id,y?.runtime?.providerStatus]),!y)return yf(j2,{title:"Todo Note 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=todo-note"});let i=M.find((I)=>I.id===F)||null;return yf("div",{className:"todo-note-page","data-testid":"todo-note-page"},yf(AQ,{title:"Todo Note 工作台",eyebrow:"Main Server 用户服务",actions:yf("div",{className:"panel-actions"},yf("button",{type:"button",className:"ghost-btn",disabled:c,onClick:()=>_f(F),"data-testid":"todo-note-refresh-button"},c?"刷新中":"刷新"),yf(jE,{title:"Todo Note 用户服务",data:y,onOpen:u,testId:"raw-todo-note-service"}))},yf("div",{className:"todo-note-hero"},yf("div",null,yf("div",{className:"node-version-line"},yf($M,{status:Y.providerStatus==="online"?"online":"warn"},Y.providerStatus||"unknown"),yf("span",null,y.providerId),yf("span",null,k.public?"公网暴露":"仅 UniDesk frontend 代理访问"),yf("span",null,l?.ok?"Health OK":"Health --")),yf("p",{className:"muted paragraph"},y.description)),yf("div",{className:"microservice-ref-card"},yf("span",null,"Repo"),yf("strong",null,R.url||"--"),yf("code",null,R.commitId||"--")),yf("div",{className:"microservice-ref-card"},yf("span",null,"Main Server Docker"),yf("strong",null,`${k.nodeBindHost||"--"}:${k.nodePort||"--"}`),yf("code",null,`${R.composeService||"--"} / ${R.containerName||"--"}`))),yf(H0,{error:C,wide:!0})),yf("div",{className:"todo-note-layout"},yf(AQ,{title:"清单",eyebrow:`${M.length} Instances`,className:"todo-list-panel"},yf("form",{className:"todo-create-list",onSubmit:ff},yf("input",{placeholder:"新清单名称",value:O,onChange:(I)=>z(I.target.value),"aria-label":"新清单名称"}),yf("button",{type:"submit",className:"ghost-btn",disabled:c||!O.trim()},"创建")),M.length===0?yf(j2,{title:"暂无清单",text:"迁移或创建清单后会出现在这里"}):yf("div",{className:"todo-instance-list"},M.map((I)=>yf("button",{key:I.id,type:"button",className:`todo-instance-row ${F===I.id?"active":""}`,onClick:()=>{A(I.id),n(I.id)},"data-testid":`todo-instance-${FE(I.id)}`},yf("strong",null,I.name),yf("span",null,`${I.completedCount??0}/${I.todoCount??0} 完成`),yf("code",null,I.id))))),yf("div",{className:"todo-main-stack"},yf(AQ,{title:i?.name||"待选择清单",eyebrow:B?`Updated ${L0(B)}`:"Todo Tree",actions:U?yf("div",{className:"panel-actions"},yf("button",{type:"button",className:"ghost-btn",onClick:()=>s({type:"renameInstance",name:window.prompt("清单新名称",U.name)||U.name})},"重命名"),yf("button",{type:"button",className:"ghost-btn danger",onClick:()=>Kf(F)},"删除清单"),yf(jE,{title:`Todo Instance ${F}`,data:U,onOpen:u,testId:"raw-todo-instance"})):null},!U?yf(j2,{title:"未选择清单",text:"左侧选择一个 Todo Note 清单"}):yf("div",{className:"todo-workbench",style:{"--todo-font-size":`${E}px`}},yf("div",{className:"todo-toolbar"},yf("form",{className:"todo-add-form",onSubmit:Gf},yf("input",{placeholder:"新增根任务",value:q,onChange:(I)=>Z(I.target.value),"aria-label":"新增根任务"}),yf("button",{type:"submit",className:"ghost-btn",disabled:!q.trim()},"新增")),yf("div",{className:"todo-filter-strip"},["all","active","completed"].map((I)=>yf("button",{key:I,type:"button",className:`todo-filter ${W===I?"active":""}`,onClick:()=>K(I)},I==="all"?"全部":I==="active"?"未完成":"已完成"))),yf("div",{className:"todo-toolbar-actions"},yf("button",{type:"button",className:"ghost-btn",onClick:()=>s({type:"setAllTodosExpanded",expanded:!0})},"全部展开"),yf("button",{type:"button",className:"ghost-btn",onClick:()=>s({type:"setAllTodosExpanded",expanded:!1})},"全部收起"),yf("button",{type:"button",className:"ghost-btn",onClick:()=>jf("undo")},"撤销"),yf("button",{type:"button",className:"ghost-btn",onClick:()=>jf("redo")},"重做"),yf("label",{className:"todo-font-control"},"字号",yf("input",{type:"range",min:11,max:18,value:E,onChange:(I)=>H(Number(I.target.value))})))),yf("div",{className:"todo-stats-grid"},yf($2,{label:"总任务",value:w.total,hint:`${M.length} lists`}),yf($2,{label:"已完成",value:w.completed,hint:`${QM(w.total?w.completed/w.total*100:0)}`,tone:"ok"}),yf($2,{label:"未完成",value:w.active,hint:W==="active"?"当前筛选":"active tasks",tone:w.active>0?"warn":"ok"}),yf($2,{label:"历史指针",value:U.historyPointer??0,hint:"undo / redo"})),yf("div",{className:"todo-root-drop",onDragOver:(I)=>I.preventDefault(),onDrop:(I)=>{I.preventDefault(),h(null,(U.todos||[]).length)}},"拖到这里可移为根任务末尾"),yf("div",{className:"todo-tree","data-testid":"todo-note-tree"},(U.todos||[]).filter((I)=>J2(I,W)).length===0?yf(j2,{title:"没有匹配任务",text:"调整筛选或新增任务"}):(U.todos||[]).filter((I)=>J2(I,W)).map((I,lf)=>yf(UE,{key:I.id,todo:I,depth:0,parentId:null,index:lf,siblingCount:U.todos.length,filter:W,editingId:V,editingTitle:r,setEditingTitle:N,beginEdit:Wf,saveEdit:Of,applyTodoAction:s,addChild:Zf,dragTodoId:D,setDragTodoId:x,dropTodo:h}))))))))}function UE(f){let{todo:u,depth:_,parentId:y,index:l,siblingCount:$,filter:j,editingId:J,editingTitle:F,setEditingTitle:A,beginEdit:U,saveEdit:G,applyTodoAction:W,addChild:K,dragTodoId:E,setDragTodoId:H,dropTodo:O}=f,z=Array.isArray(u.children)?u.children:[],q=z.filter((L)=>J2(L,j)),Z=J===u.id,V=y||null;return yf("div",{className:"todo-row-wrap"},yf("article",{className:`todo-row ${u.completed?"completed":""} ${E===u.id?"dragging":""}`,style:{"--todo-depth":_},draggable:!0,onDragStart:(L)=>{H(u.id),L.dataTransfer.effectAllowed="move"},onDragOver:(L)=>L.preventDefault(),onDrop:(L)=>{L.preventDefault(),O(u.id,z.length)},"data-testid":`todo-row-${FE(u.id)}`},yf("button",{type:"button",className:"todo-expand",disabled:z.length===0,onClick:()=>W({type:"toggleTodoExpanded",todoId:u.id})},z.length===0?"·":u.expanded?"▾":"▸"),yf("input",{type:"checkbox",checked:Boolean(u.completed),onChange:()=>W({type:"toggleTodoCompleted",todoId:u.id}),"aria-label":`完成 ${u.title}`}),yf("div",{className:"todo-title-cell",onDoubleClick:()=>U(u)},Z?yf("div",{className:"todo-edit-inline"},yf("input",{value:F,autoFocus:!0,onChange:(L)=>A(L.target.value),onKeyDown:(L)=>{if(L.key==="Enter")G(u.id);if(L.key==="Escape")U({id:"",title:""})}}),yf("button",{type:"button",className:"ghost-btn",onClick:()=>G(u.id)},"保存")):yf("strong",null,u.title||"Untitled"),yf("div",{className:"todo-meta-line"},yf("span",null,`子项 ${z.length}`),yf("span",null,`更新 ${zf(u.updatedAt)}`),u.reminderAt?yf("span",{className:"todo-reminder"},`提醒 ${zf(u.reminderAt)}`):yf("span",null,"无提醒"))),yf("input",{className:"todo-reminder-input",type:"datetime-local",value:P2(u.reminderAt),onChange:(L)=>W({type:"setTodoReminder",todoId:u.id,reminderAt:cQ(L.target.value)})}),yf("div",{className:"todo-row-actions"},yf("button",{type:"button",className:"ghost-btn",onClick:()=>U(u)},"编辑"),yf("button",{type:"button",className:"ghost-btn",onClick:()=>K(u.id)},"子项"),yf("button",{type:"button",className:"ghost-btn",disabled:l<=0,onClick:()=>W({type:"moveTodo",todoId:u.id,...V?{targetParentId:V}:{},targetIndex:l-1})},"上移"),yf("button",{type:"button",className:"ghost-btn",disabled:l>=$-1,onClick:()=>W({type:"moveTodo",todoId:u.id,...V?{targetParentId:V}:{},targetIndex:l+1})},"下移"),yf("button",{type:"button",className:"ghost-btn",disabled:!y,onClick:()=>W({type:"moveTodo",todoId:u.id,targetIndex:9999})},"提升"),yf("button",{type:"button",className:"ghost-btn danger",onClick:()=>W({type:"deleteTodo",todoId:u.id})},"删除"))),u.expanded&&q.length>0?yf("div",{className:"todo-children"},q.map((L,r)=>yf(UE,{key:L.id,todo:L,depth:_+1,parentId:u.id,index:r,siblingCount:z.length,filter:j,editingId:J,editingTitle:F,setEditingTitle:A,beginEdit:U,saveEdit:G,applyTodoAction:W,addChild:K,dragTodoId:E,setDragTodoId:H,dropTodo:O}))):null)}var WE=Sf(c0(),1),yy=WE.default.createElement;function GE({title:f,items:u,actions:_,className:y,testId:l}){let $=Array.isArray(u)?u:[];return yy("section",{className:`top-status-bar ${y||""}`,"data-testid":l},yy("div",{className:"top-status-main"},f?yy("strong",{className:"top-status-title"},f):null,yy("div",{className:"top-status-chips"},$.map((j,J)=>yy("span",{key:j?.key||`${j?.label||"status"}-${J}`,className:`top-status-chip ${j?.tone||""}`,"data-testid":j?.testId},j?.label?yy("b",null,j.label):null,yy("span",null,j?.value??"--"))))),_?yy("div",{className:"top-status-actions"},_):null)}function LE(f,u){let _=document.getElementById("root")?.getAttribute(f);if(!_)return u;try{let y=JSON.parse(_);return typeof y==="object"&&y!==null&&!Array.isArray(y)?y:u}catch{return u}}var sf=LE("data-config",{apiBaseUrl:"/api",authUsername:"admin"}),AM=LE("data-codex-overview",null),Q=r6.default.createElement,{useEffect:h1,useMemo:M6}=r6.default,hf=r6.default.useState,mu=Uz(h4),UM={id:"codex-queue",name:"Codex Queue",providerId:"main-server",description:"Codex Queue",repository:{containerName:"codex-queue-backend"},backend:{nodeBaseUrl:"http://codex-queue:4222",nodeBindHost:"codex-queue",nodePort:4222,public:!1},runtime:{providerStatus:"loading",providerName:"main-server"}};function zE(){return typeof document>"u"||document.visibilityState!=="hidden"}function WM(f,u){if(f==="ops"&&u==="status")return 5000;if(f==="nodes"&&u==="monitor")return 5000;if(f==="tasks"&&(u==="dispatch"||u==="pending"))return 5000;if(f==="nodes"||f==="ops")return 1e4;if(f==="apps")return 15000;if(f==="tasks")return 15000;return 30000}async function GM(f){if(!f?._summaryOnly||!f?.id)return f;return(await wf(`${sf.apiBaseUrl}/tasks/${encodeURIComponent(String(f.id))}`))?.task||f}function P6(f){return f?._summaryOnly?{...f,_loadRaw:()=>GM(f)}:f}function E3(f){if(!Number.isFinite(f))return"--";let u=Math.max(0,f);if(u===0)return"0s";if(u<0.01)return"<0.01s";if(u<0.1)return`${u.toFixed(2)}s`;if(u<1)return`${u.toFixed(1)}s`;if(u<10&&!Number.isInteger(u))return`${u.toFixed(1)}s`;if(u<60)return`${Math.round(u)}s`;let _=Math.floor(u);if(_<3600)return`${Math.floor(_/60)}m ${_%60}s`;return`${Math.floor(_/3600)}h ${Math.floor(_%3600/60)}m`}function ku(f){let u=Number(f);if(!Number.isFinite(u))return"--";if(u<1)return`${Math.max(0,u).toFixed(1)}ms`;if(u<10)return`${u.toFixed(1)}ms`;if(u<1000)return`${Math.round(u)}ms`;return E3(u/1000)}function Fu(f){let u=Number(f);if(!Number.isFinite(u)||u<=0)return"--";let _=["B","KB","MB","GB","TB"],y=u,l=0;while(y>=1024&&l<_.length-1)y/=1024,l+=1;return`${y.toFixed(l===0?0:1)} ${_[l]}`}function ly(f){let u=Number(f);return Number.isFinite(u)?`${Math.max(0,Math.min(100,u)).toFixed(1)}%`:"--"}function zM(f){let u=Number(f);return Number.isFinite(u)?`${Math.max(0,u).toFixed(1)}%`:"--"}function UQ(f){let u=Number(f);if(!Number.isFinite(u)||u<=0)return"0 B/s";return`${Fu(u)}/s`}function Mf(f,u=0){let _=Number(f);return Number.isFinite(_)?_:u}function H3(f){return["queued","dispatched","running"].includes(String(f?.status||"").toLowerCase())}function zQ(f){if(!f)return"--";let u=new Date(f);if(Number.isNaN(u.getTime()))return"--";return E3(Math.max(0,Math.floor((Date.now()-u.getTime())/1000)))}function Z3(f){if(!f)return null;let u=new Date(f);return Number.isNaN(u.getTime())?null:u.getTime()}function YE(f){let u=Z3(f?.createdAt);if(u===null)return null;let y=["succeeded","failed"].includes(String(f?.status||"").toLowerCase())?Z3(f?.updatedAt):Date.now();if(y===null)return null;return Math.max(0,(y-u)/1000)}function BE(f){if(String(f?.status||"").toLowerCase()!=="failed")return"";let u=f?.result;if(typeof u==="string")return u;if(u&&typeof u==="object"&&!Array.isArray(u)){let _=u;for(let y of["error","reason","message","stderr","detail"])if(typeof _[y]==="string"&&_[y].length>0)return _[y]}return"任务失败但 provider 未返回明确原因"}function ty(f){if(f===null||f===void 0)return"--";if(typeof f==="boolean")return f?"是":"否";if(typeof f==="number")return String(f);if(typeof f==="string")return f.length>80?`${f.slice(0,77)}...`:f;if(Array.isArray(f))return`${f.length} 项`;if(typeof f==="object")return`${Object.keys(f).length} 字段`;return String(f)}function KM(f,u){let _=f.replace(/[-_\s]/g,"").toLowerCase(),y=_==="ts"||_.endsWith("at")||_.endsWith("timestamp")||_.endsWith("heartbeat");if((typeof u==="string"||typeof u==="number")&&y){let l=zf(u);if(l!=="--")return l}if(f==="bodyText"&&typeof u==="string")return`${/^\s*[{[]/.test(u)?"JSON":"HTTP"} body ${u.length} chars`;return ty(u)}function DE(f){if(!f||typeof f!=="object"||Array.isArray(f))return[];return Object.entries(f)}function X1(f){return String(f).replace(/[^a-zA-Z0-9_-]/g,"_")}function KQ(f,u){return f&&typeof f==="object"&&!Array.isArray(f)?f[u]:void 0}function A2(f,u,_="未知"){let y=KQ(f?.labels,u);return typeof y==="string"&&y.length>0?y:_}function wE(f){return A2(f,"providerGatewayVersion")}function T6(f){return A2(f,"providerGatewayUpgradePolicy")}function KE(f){return A2(f,"providerGatewayStartedAt","")}function TE(f){let u=KQ(f?.labels,"unideskCapabilities");if(typeof u==="string")return u.split(",").map((_)=>_.trim()).filter(Boolean);return Array.isArray(u)?u.filter((_)=>typeof _==="string"):[]}function rE(f,u){return TE(f).includes(u)}function ZE(f,u){let _=KQ(f?.labels,u);return _===!0||_==="true"||_==="1"}function ZM(f){if(!rE(f,"host.ssh"))return{tone:"fail",label:"不可用",detail:"未声明 host.ssh"};if(!ZE(f,"hostSshConfigured"))return{tone:"warn",label:"未配置",detail:"缺少 SSH 环境变量"};if(!ZE(f,"hostSshKeyPresent"))return{tone:"warn",label:"缺 key",detail:"私钥未挂载"};return{tone:"ok",label:"可用",detail:A2(f,"hostSshTarget","host.ssh ready")}}function qM(f){if(!rE(f,"provider.upgrade"))return{tone:"fail",label:"不可用",detail:"未声明 provider.upgrade"};let u=T6(f);if(u!=="always-enabled")return{tone:"warn",label:"待确认",detail:`策略 ${u}`};return{tone:"ok",label:"可用",detail:"always-enabled"}}function ZQ(f){let u=typeof f==="string"&&f.length>0?f:"未知";if(u==="未知")return"版本未知";return u.startsWith("v")?u:`v${u}`}function ME(f){return f?.payload&&typeof f.payload==="object"&&!Array.isArray(f.payload)?f.payload:{}}function U2(f){return f?.result&&typeof f.result==="object"&&!Array.isArray(f.result)?f.result:{}}function Q2(f){let u=ME(f),_=U2(f);return(u.mode??_.mode)==="schedule"?"schedule":"plan"}function EM(f){let u=ME(f).source;return typeof u==="string"&&u.length>0?u:"unknown"}function HM(f){let u=U2(f),_=u.plan&&typeof u.plan==="object"&&!Array.isArray(u.plan)?u.plan:{},y=u.policy??_.policy;return typeof y==="string"&&y.length>0?y:"--"}function PE(f){let u=U2(f),_=u.plan&&typeof u.plan==="object"&&!Array.isArray(u.plan)?u.plan:{},y=u.targetProviderGatewayVersion??u.providerGatewayVersion??_.targetProviderGatewayVersion??_.providerGatewayVersion;return typeof y==="string"&&y.length>0?ZQ(y):"版本未知"}function SE(f){if(String(f?.status||"").toLowerCase()==="failed")return BE(f);if(H3(f))return"等待 provider 回传升级终态";let _=U2(f);if(typeof _.updaterContainerId==="string"&&_.updaterContainerId.length>0)return`updater ${_.updaterContainerId.slice(0,18)}`;if(typeof _.message==="string"&&_.message.length>0)return _.message;if(_.plan)return"升级计划已生成";return"无升级结果摘要"}function CE(f,u){return f.filter((_)=>_?.providerId===u&&_?.command==="provider.upgrade").sort((_,y)=>(Z3(y.updatedAt)??0)-(Z3(_.updatedAt)??0))}function VM(f){return f.find((u)=>Q2(u)==="schedule")||f[0]||null}function RE(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function qE(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function OM(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function O0({status:f,children:u}){let _=String(f||"unknown").toLowerCase();return Q("span",{className:`status-badge ${_}`},u||f||"unknown")}function _0({label:f,value:u,hint:_,tone:y,onClick:l,testId:$}){let j=typeof l==="function";return Q("article",{className:`metric-card ${y||""} ${j?"clickable":""}`,role:j?"button":void 0,tabIndex:j?0:void 0,"data-testid":$,onClick:l,onKeyDown:j?(J)=>{if(J.key==="Enter"||J.key===" ")J.preventDefault(),l()}:void 0},Q("div",{className:"metric-label"},f),Q("div",{className:"metric-value"},u),Q("div",{className:"metric-hint"},_))}function mf({title:f,eyebrow:u,actions:_,children:y,className:l}){return Q("section",{className:`panel ${l||""}`},Q("div",{className:"panel-head"},Q("div",null,u?Q("p",{className:"panel-eyebrow"},u):null,Q("h2",null,f)),_?Q("div",{className:"panel-actions"},_):null),Q("div",{className:"panel-body"},y))}function m0({title:f,data:u,onOpen:_,testId:y}){let[l,$]=hf(!1),j=u&&typeof u==="object"&&typeof u._loadRaw==="function"?u._loadRaw:null;async function J(){if(!j){_(f,u);return}$(!0);try{_(f,await j())}catch(F){_(f,{ok:!1,error:Pf(F,"读取原始 JSON 失败"),fallback:u})}finally{$(!1)}}return Q("button",{type:"button",className:"ghost-btn","data-testid":y,disabled:l,onClick:()=>void J()},l?"读取中":"查看原始JSON")}function XM({raw:f,onClose:u}){if(!f)return null;return Q("div",{className:"modal-backdrop",role:"presentation"},Q("section",{className:"raw-dialog",role:"dialog","aria-modal":"true","aria-label":f.title},Q("div",{className:"raw-dialog-head"},Q("h2",null,f.title),Q("button",{type:"button",className:"ghost-btn",onClick:u},"关闭")),Q("pre",{className:"raw-json","data-testid":"raw-json"},JSON.stringify(f.data,null,2))))}function xE({labels:f,limit:u=8}){let _=DE(f).slice(0,u);if(_.length===0)return Q("span",{className:"muted"},"无标签");return Q("div",{className:"chip-row"},_.map(([y,l])=>Q("span",{key:y,className:"data-chip"},Q("b",null,y),Q("span",null,ty(l)))))}function q3({node:f}){let u=wE(f);return Q("span",{className:`version-chip ${u==="未知"?"unknown":""}`,"data-testid":`gateway-version-${X1(f?.providerId||"unknown")}`},ZQ(u))}function EE({title:f,state:u,testId:_}){return Q("span",{className:`capability-badge ${u.tone}`,title:u.detail,"data-testid":_},Q("b",null,f),Q("strong",null,u.label),Q("small",null,u.detail))}function qQ({node:f}){let u=X1(f?.providerId||"unknown");return Q("div",{className:"node-availability-strip"},Q(EE,{title:"SSH 透传",state:ZM(f),testId:`ssh-availability-${u}`}),Q(EE,{title:"远程更新",state:qM(f),testId:`upgrade-availability-${u}`}))}function sy({data:f,empty:u="无数据"}){if(f===null||f===void 0)return Q("span",{className:"muted"},u);if(typeof f!=="object")return Q("span",{className:"summary-value"},ty(f));if(Array.isArray(f))return Q("span",{className:"summary-value"},`${f.length} 项列表`);let _=Object.entries(f).slice(0,5);if(_.length===0)return Q("span",{className:"muted"},u);return Q("div",{className:"summary-grid"},_.map(([y,l])=>Q("span",{key:y,className:"summary-item"},Q("b",null,y),Q("span",null,KM(y,l)))))}function J0({title:f,text:u}){return Q("div",{className:"empty-state"},Q("strong",null,f),Q("span",null,u))}function NM({onLogin:f}){let[u,_]=hf(sf.authUsername||"admin"),[y,l]=hf(""),[$,j]=hf(""),[J,F]=hf(!1);async function A(U){U.preventDefault(),F(!0),j("");try{let G=await wf("/login",{method:"POST",body:JSON.stringify({username:u,password:y})});f(G)}catch(G){j(Pf(G,"登录失败"))}finally{F(!1)}}return Q("main",{className:"login-screen","data-testid":"login-screen"},Q("section",{className:"login-card"},Q("div",{className:"login-brand"},Q("span",{className:"brand-mark"},"UD"),Q("div",null,Q("h1",null,"UniDesk"),Q("p",null,"Control Plane Login"))),Q("form",{className:"login-form",onSubmit:A},Q("label",null,"账号",Q("input",{name:"username",autoComplete:"username",value:u,onChange:(U)=>_(U.target.value)})),Q("label",null,"密码",Q("input",{name:"password",type:"password",autoComplete:"current-password",value:y,onChange:(U)=>l(U.target.value)})),Q(H0,{error:$}),Q("button",{type:"submit",disabled:J},J?"登录中":"登录")),Q("div",{className:"login-note"},"默认账号由 config.json 注入;公网入口只暴露前端登录面。")))}function LM({connection:f,lastRefresh:u,onRefresh:_,onLogout:y,session:l,clock:$,activeStatusItems:j=[]}){let J=[{key:"core",label:"核心",value:f.text,tone:f.ok?"ok":"fail",testId:"conn-text"},...Array.isArray(j)?j:[],{key:"refresh",label:"刷新",value:u?L0(u):"未刷新"},{key:"clock",label:s6,value:L0($)},{key:"user",label:"用户",value:l?.user?.username||"--",tone:"user"}];return Q("header",{className:"topbar"},Q("div",null,Q("p",{className:"eyebrow"},"Distributed Work Platform"),Q("h1",null,"UniDesk 控制平面")),Q(GE,{className:"global-top-status",title:"状态",items:J,actions:[Q("button",{key:"refresh",type:"button",className:"ghost-btn",onClick:_},"刷新"),Q("button",{key:"logout",type:"button",className:"ghost-btn danger",onClick:y},"退出")]}))}function YM({activeModule:f,activeTabs:u,onNavigate:_,collapsed:y,onToggle:l}){return Q("aside",{className:`rail ${y?"collapsed":""}`,"aria-label":"主模块"},Q("div",{className:"brand"},Q("span",{className:"brand-mark"},"UD"),Q("span",{className:"brand-text"},"UniDesk"),Q("button",{type:"button",className:"rail-toggle",onClick:l,"aria-label":y?"展开左侧边栏":"收起左侧边栏","data-testid":"rail-toggle"},y?"»":"«")),h4.map(($)=>Q("button",{key:$.id,type:"button",className:`module ${f===$.id?"active":""}`,onClick:()=>_($.id,u[$.id]||kl[$.id]||$.tabs[0]?.id||""),title:$.label,"data-route":b$(mu,$.id,u[$.id]||kl[$.id]||$.tabs[0]?.id||"")},Q("span",{className:"module-code"},$.code),Q("span",null,$.label))))}function BM({module:f,activeTab:u,onNavigate:_}){return Q("nav",{className:"tabs","aria-label":`${f.label} 子功能`},f.tabs.map((y)=>Q("button",{key:y.id,type:"button",className:`tab ${u===y.id?"active":""}`,onClick:()=>_(f.id,y.id),"data-route":b$(mu,f.id,y.id)},y.label)))}function DM({data:f,onRaw:u,onNavigate:_}){let y=f.overview||{},l=f.nodes.filter((A)=>A.status==="online"),$=f.pendingTasks||f.tasks.filter(H3),j=y.pendingTaskCount??$.length,J=f.tasks.slice(0,5),F=y.pgdata||{};return Q("div",{className:"page-grid overview-grid","data-testid":"overview-page"},Q(mf,{title:"核心指标",eyebrow:"Control"},Q("div",{className:"metric-grid"},Q(_0,{label:"数据库",value:y.dbReady?"READY":"WAIT",hint:"PostgreSQL internal network",tone:y.dbReady?"ok":"warn"}),Q(_0,{label:"PGDATA",value:Fu(F.databaseBytes),hint:`${F.volumeName||"unidesk_pgdata_10gb"} / ${F.databasePretty||"--"}`,tone:"ok",testId:"pgdata-usage-card"}),Q(_0,{label:"在线节点",value:y.onlineNodeCount??0,hint:`${y.nodeCount??0} registered`,tone:"ok"}),Q(_0,{label:"WebSocket",value:y.activeSocketCount??0,hint:"Provider ingress sockets"}),Q(_0,{label:"待处理任务",value:j,hint:j>0?"点击查看具体任务":`timeout ${E3(Math.floor((y.taskPendingTimeoutMs??0)/1000))}`,tone:j>0?"warn":"ok",onClick:()=>_("tasks","pending"),testId:"pending-task-card"}))),Q(mf,{title:"本机 Provider",eyebrow:"Self Connected"},l.length===0?Q(J0,{title:"暂无在线节点",text:"provider-gateway 未完成自接入"}):Q("div",{className:"node-card-list"},l.slice(0,4).map((A)=>Q(wM,{key:A.providerId,node:A,onRaw:u})))),Q(mf,{title:"待处理任务明细",eyebrow:`${j} Pending`,actions:Q("button",{type:"button",className:"ghost-btn",onClick:()=>_("tasks","pending"),"data-testid":"pending-task-detail-link"},"进入任务调度")},$.length===0?Q(J0,{title:"当前无待处理",text:"queued / dispatched / running 超时后会自动转为 failed,避免总览长期卡住"}):Q("div",{className:"compact-list"},$.slice(0,5).map((A)=>Q(XE,{key:A.id,task:A,onRaw:u})))),Q(mf,{title:"最近任务",eyebrow:"Dispatch"},J.length===0?Q(J0,{title:"暂无任务",text:"可以在任务调度模块发起 docker.ps 或 echo"}):Q("div",{className:"compact-list"},J.map((A)=>Q(XE,{key:A.id,task:A,onRaw:u})))))}function wM({node:f,onRaw:u}){return Q("article",{className:"node-card"},Q("div",{className:"node-card-head"},Q("div",null,Q("strong",null,f.name),Q("code",null,f.providerId)),Q(O0,{status:f.status})),Q("div",{className:"node-version-line"},Q(q3,{node:f}),Q("span",null,`升级策略 ${T6(f)}`)),Q(qQ,{node:f}),Q(xE,{labels:f.labels,limit:6}),Q("div",{className:"node-card-foot"},Q("span",null,`心跳 ${zf(f.lastHeartbeat)}`),Q(m0,{title:`Provider ${f.providerId}`,data:f,onOpen:u,testId:`raw-node-${X1(f.providerId)}`})))}function TM({events:f,onRaw:u}){return Q(mf,{title:"事件摘要",eyebrow:"Latest 100"},f.length===0?Q(J0,{title:"暂无事件",text:"Provider 注册、心跳超时和任务状态会写入事件流"}):Q("div",{className:"table-wrap"},Q("table",null,Q("thead",null,Q("tr",null,Q("th",null,"ID"),Q("th",null,"类型"),Q("th",null,"来源"),Q("th",null,"摘要"),Q("th",null,"时间"),Q("th",null,"操作"))),Q("tbody",null,f.map((_)=>Q("tr",{key:_.id},Q("td",null,Q("code",null,_.id)),Q("td",null,Q(O0,{status:_.type},_.type)),Q("td",null,Q("code",null,_.source)),Q("td",null,Q(sy,{data:_.payload})),Q("td",null,zf(_.createdAt)),Q("td",null,Q(m0,{title:`Event ${_.id}`,data:_,onOpen:u}))))))))}function rM({logs:f,onRaw:u}){return Q(mf,{title:"服务日志",eyebrow:"Core Recent"},f.length===0?Q(J0,{title:"暂无日志",text:"backend-core 内存日志会在请求和 provider 事件后出现"}):Q("div",{className:"log-list"},f.slice(-80).reverse().map((_,y)=>Q("article",{key:y,className:`log-row ${_.level||"info"}`},Q("span",null,zf(_.ts)),Q("b",null,_.level||"info"),Q("strong",null,_.message||"log"),Q(sy,{data:_.data,empty:"无附加字段"}),Q(m0,{title:`Log ${_.message||y}`,data:_,onOpen:u})))))}function MM({nodes:f,onRaw:u}){return Q(mf,{title:"节点清单",eyebrow:`${f.length} Providers`},f.length===0?Q(J0,{title:"暂无 Provider 节点",text:"确认 provider-gateway 已连接 provider ingress"}):Q("div",{className:"table-wrap"},Q("table",{className:"node-list-table"},Q("thead",null,Q("tr",null,Q("th",null,"状态"),Q("th",null,"Provider"),Q("th",null,"网关版本"),Q("th",null,"运维可用性"),Q("th",null,"资源标签"),Q("th",null,"连接时间"),Q("th",null,"最后心跳"),Q("th",null,"操作"))),Q("tbody",null,f.map((_)=>Q("tr",{key:_.providerId},Q("td",null,Q(O0,{status:_.status})),Q("td",null,Q("strong",null,_.name),Q("code",null,_.providerId)),Q("td",null,Q("div",{className:"gateway-cell"},Q(q3,{node:_}),Q("span",null,T6(_)))),Q("td",null,Q(qQ,{node:_})),Q("td",null,Q(xE,{labels:_.labels,limit:5})),Q("td",null,zf(_.connectedAt)),Q("td",null,zf(_.lastHeartbeat)),Q("td",null,Q(m0,{title:`Provider ${_.providerId}`,data:_,onOpen:u,testId:`raw-node-table-${X1(_.providerId)}`}))))))))}function PM({nodes:f}){let u=M6(()=>{let _=[];for(let y of f)for(let[l,$]of DE(y.labels))_.push({providerId:y.providerId,name:y.name,key:l,value:$});return _},[f]);return Q(mf,{title:"资源标签",eyebrow:"Structured Labels"},u.length===0?Q(J0,{title:"暂无标签",text:"provider-gateway 注册消息会同步资源标签"}):Q("div",{className:"label-matrix"},u.map((_)=>Q("article",{key:`${_.providerId}-${_.key}`,className:"label-card"},Q("span",null,_.key),Q("strong",null,ty(_.value)),Q("code",null,_.providerId)))))}function SM({nodes:f}){return Q(mf,{title:"心跳状态",eyebrow:"Provider Liveness"},f.length===0?Q(J0,{title:"无心跳",text:"等待 provider 注册和 heartbeat"}):Q("div",{className:"heartbeat-list"},f.map((u)=>Q("article",{key:u.providerId,className:"heartbeat-row"},Q("span",{className:`pulse ${u.status}`}),Q("div",null,Q("strong",null,u.name),Q("code",null,u.providerId)),Q("div",null,Q("span",null,"connected"),Q("b",null,zf(u.connectedAt))),Q("div",null,Q("span",null,"last heartbeat"),Q("b",null,zf(u.lastHeartbeat)))))))}function CM({nodes:f,systemStatuses:u,tasks:_,onRaw:y,refresh:l}){let[$,j]=hf(""),J=M6(()=>f.map((H)=>{let O=u.find((z)=>z.providerId===H.providerId);return{...H,systemCurrent:O?.current||null,systemHistory:O?.history||[],systemUpdatedAt:O?.updatedAt||null}}),[f,u]),F=J.find((H)=>H.providerId===$)||J[0]||null;if(h1(()=>{if(!$&&J[0])j(J[0].providerId)},[J.length,$]),!F)return Q(J0,{title:"暂无资源监控",text:"等待 provider 上报 CPU、内存和硬盘指标"});let A=F.systemCurrent,U=F.systemHistory||[],G=A?.cpu||{},W=A?.memory||{},K=A?.disk||{},E=U.length>0?U:A?[{at:A.collectedAt,cpuPercent:Mf(G.percent),memoryPercent:Mf(W.percent),diskPercent:Mf(K.percent)}]:[];return Q("div",{className:"monitor-page","data-testid":"node-monitor-page"},Q("div",{className:"docker-node-strip"},J.map((H)=>Q("button",{key:H.providerId,type:"button",className:`docker-node-tile ${F.providerId===H.providerId?"active":""}`,onClick:()=>j(H.providerId)},Q("span",{className:`pulse ${H.status}`}),Q("strong",null,H.name),Q("code",null,H.providerId),Q("span",null,H.systemCurrent?`CPU ${ly(H.systemCurrent.cpu?.percent)} / MEM ${ly(H.systemCurrent.memory?.percent)}`:"等待指标")))),Q("div",{className:"monitor-layout"},Q(mf,{title:"任务管理器视图",eyebrow:F.name,className:"monitor-main-panel",actions:A?Q(m0,{title:`System ${F.providerId}`,data:{current:A,history:U},onOpen:y}):null},!A?Q(J0,{title:"系统指标未上报",text:"provider-gateway 会周期性采集 /proc 与 df,并保存历史曲线"}):Q("div",null,Q("div",{className:"monitor-hero"},Q("div",null,Q("p",{className:"panel-eyebrow"},"Node Performance"),Q("h3",null,F.name),Q("div",{className:"docker-meta"},Q("span",null,`${G.cores||0} CPU cores`),Q("span",null,`load ${Mf(G.load1).toFixed(2)} / ${Mf(G.load5).toFixed(2)} / ${Mf(G.load15).toFixed(2)}`),Q("span",null,`memory actual ${Fu(W.usedBytes)} / ${Fu(W.totalBytes)}`),Q("span",null,`disk ${Fu(K.usedBytes)} / ${Fu(K.totalBytes)}`))),Q(O0,{status:A.ok?"online":"warn"},A.ok?"METRICS READY":"METRICS DEGRADED")),Q("div",{className:"monitor-chart-grid"},Q(WQ,{title:"CPU",metricKey:"cpuPercent",current:G.percent,points:E,detail:`${G.cores||0} cores / load ${Mf(G.load1).toFixed(2)}`,tone:"cpu",testId:"metric-chart-cpu"}),Q(WQ,{title:"Memory",metricKey:"memoryPercent",current:W.percent,points:E,detail:`${Fu(W.usedBytes)} actual / ${Fu(W.cacheBytes)} cache excluded`,tone:"memory",testId:"metric-chart-memory"}),Q(WQ,{title:"Disk",metricKey:"diskPercent",current:K.percent,points:E,detail:`${K.path||"/"} mounted ${K.mount||"--"}`,tone:"disk",testId:"metric-chart-disk"})),Q("div",{className:"monitor-summary-grid"},Q(_0,{label:"CPU 当前",value:ly(G.percent),hint:`history ${E.length} samples`,tone:"ok"}),Q(_0,{label:"实际内存",value:Fu(W.usedBytes),hint:`${ly(W.percent)} 不含缓存`}),Q(_0,{label:"硬盘已用",value:Fu(K.usedBytes),hint:ly(K.percent)}),Q(_0,{label:"更新时间",value:zf(F.systemUpdatedAt||A.collectedAt),hint:F.providerId})),Q(RM,{current:A,onRaw:y}))),Q("div",{className:"monitor-side-stack"},Q(kM,{provider:F,refresh:l,onRaw:y}),Q(mM,{provider:F,tasks:_,onRaw:y,limit:5}),Q(mf,{title:"采样说明",eyebrow:"Retention"},Q("div",{className:"monitor-note-list"},Q("article",null,Q("b",null,"CPU"),Q("span",null,"从 /proc/stat 计算相邻采样差值,首个采样用 load/cores 近似")),Q("article",null,Q("b",null,"Memory"),Q("span",null,"实际内存 = MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem,不把 page cache / buffer 计入占用")),Q("article",null,Q("b",null,"Disk"),Q("span",null,"使用 df -PB1 对配置路径采样,默认监控根文件系统")),Q("article",null,Q("b",null,"Process"),Q("span",null,"从 /proc/[pid] 采集进程 CPU、实际内存 RSS、线程数和磁盘 I/O 速率;表格默认按内存占用降序")))))))}function HE(f,u){if(u==="memory")return Mf(f.rssBytes);if(u==="cpu")return Mf(f.cpuPercent);if(u==="disk")return Mf(f.readBytesPerSecond)+Mf(f.writeBytesPerSecond);if(u==="pid")return Mf(f.pid);if(u==="threads")return Mf(f.threads);if(u==="runtime")return Mf(f.elapsedSeconds);if(u==="user")return String(f.user||"");return String(f.name||f.command||"")}function VE({value:f,label:u,tone:_}){let y=Math.max(1,Math.min(100,Mf(f)));return Q("div",{className:`process-meter ${_||""}`},Q("span",{style:{width:`${y}%`}}),Q("b",null,u))}function RM({current:f,onRaw:u}){let[_,y]=hf({key:"memory",direction:"desc"}),l=f?.processSummary&&typeof f.processSummary==="object"?f.processSummary:{},$=Array.isArray(f?.processes)?f.processes:[],j=M6(()=>{let F=_.direction==="asc"?1:-1;return[...$].sort((A,U)=>{let G=HE(A,_.key),W=HE(U,_.key);if(typeof G==="string"||typeof W==="string")return String(G).localeCompare(String(W),"zh-CN")*F;return(G-W)*F||Mf(A.pid)-Mf(U.pid)})},[$,_.key,_.direction]),J=(F,A)=>{let U=_.key===A,G=U?_.direction==="asc"?"ascending":"descending":"none";return Q("th",{"aria-sort":G},Q("button",{type:"button",className:`process-sort-button ${U?"active":""}`,"data-testid":`process-sort-${A}`,onClick:()=>y((W)=>({key:A,direction:W.key===A&&W.direction==="desc"?"asc":"desc"}))},F,Q("span",null,U?_.direction==="desc"?"↓":"↑":"↕")))};return Q("section",{className:"process-resource-panel","data-testid":"process-resource-panel"},Q("div",{className:"process-resource-head"},Q("div",null,Q("p",{className:"panel-eyebrow"},"Windows Resource Monitor Style"),Q("h3",null,"进程资源占用")),Q("div",{className:"process-resource-actions"},Q("span",{className:"data-chip"},"默认按内存排序"),Q("span",{className:"data-chip"},`${Mf(l.visible,j.length)} / ${Mf(l.total,j.length)} 进程`),Q(m0,{title:"Process Resource Snapshot",data:{processSummary:l,processes:$},onOpen:u,testId:"raw-process-resources"}))),j.length===0?Q(J0,{title:"暂无进程资源数据",text:"等待 provider-gateway 上报 /proc/[pid] 采样;旧版 provider 需要先升级到支持进程资源表的版本"}):Q("div",{className:"process-table-wrap"},Q("table",{className:"process-resource-table","data-testid":"process-resource-table"},Q("thead",null,Q("tr",null,J("进程","name"),J("PID","pid"),J("用户","user"),Q("th",null,"状态"),J("CPU","cpu"),J("内存","memory"),Q("th",null,"RSS"),J("磁盘 I/O","disk"),J("线程","threads"),J("运行时长","runtime"))),Q("tbody",null,j.map((F)=>{let A=Mf(F.readBytesPerSecond)+Mf(F.writeBytesPerSecond);return Q("tr",{key:`${F.pid}-${F.startedAt}`,"data-testid":`process-row-${X1(F.pid)}`,"data-memory-bytes":String(Mf(F.rssBytes)),"data-cpu-percent":String(Mf(F.cpuPercent)),"data-disk-bps":String(A),"data-pid":String(Mf(F.pid))},Q("td",null,Q("div",{className:"process-name-cell"},Q("strong",null,F.name||"--"),Q("span",{className:"process-command"},F.command||"--"))),Q("td",null,Q("code",null,F.pid||"--")),Q("td",null,F.user||`uid:${F.uid??"--"}`),Q("td",null,Q("span",{className:`process-state state-${X1(F.state||"unknown")}`},F.state||"?")),Q("td",null,Q(VE,{value:F.cpuPercent,label:zM(F.cpuPercent),tone:"cpu"})),Q("td",null,Q(VE,{value:F.memoryPercent,label:ly(F.memoryPercent),tone:"memory"})),Q("td",null,Fu(F.rssBytes)),Q("td",null,Q("div",{className:"process-io-cell"},Q("strong",null,UQ(A)),Q("span",null,`R ${UQ(F.readBytesPerSecond)} / W ${UQ(F.writeBytesPerSecond)}`))),Q("td",null,F.threads||0),Q("td",null,E3(Mf(F.elapsedSeconds))))})))))}function WQ({title:f,metricKey:u,current:_,points:y,detail:l,tone:$,testId:j}){let J=y.map((W)=>Math.max(0,Math.min(100,Mf(W[u])))),F=J.length>1?J:[J[0]||0,J[0]||0],A=F.length<=1?100:100/(F.length-1),U=F.map((W,K)=>`${(K*A).toFixed(2)},${(46-W*0.42).toFixed(2)}`).join(" "),G=`0,48 ${U} 100,48`;return Q("article",{className:`metric-chart ${$}`,"data-testid":j},Q("div",{className:"metric-chart-head"},Q("div",null,Q("span",null,f),Q("strong",null,ly(_))),Q("code",null,`${y.length} pts`)),Q("svg",{viewBox:"0 0 100 48",preserveAspectRatio:"none",role:"img","aria-label":`${f} usage curve`},Q("polygon",{points:G}),Q("polyline",{points:U}),Q("line",{x1:"0",x2:"100",y1:"24",y2:"24"})),Q("div",{className:"metric-chart-foot"},Q("span",null,"0%"),Q("span",null,l),Q("span",null,"100%")))}function j_(f){return Array.isArray(f)?f:[]}function xM(f){let u=j_(f?.core?.requests?.componentSummary);return[...j_(f?.frontend?.requests?.componentSummary),...u].sort((y,l)=>Mf(l.requestCount)-Mf(y.requestCount))}function vM(f){let u=j_(f?.core?.operations?.summary);return[...j_(f?.frontend?.operations?.summary),...u].sort((y,l)=>Mf(l.count)-Mf(y.count))}function bM(f){let u=j_(f?.core?.requests?.recentFailures).map((y)=>({source:"backend",...y}));return[...j_(f?.frontend?.requests?.recentFailures).map((y)=>({source:"frontend",...y})),...u].sort((y,l)=>(Z3(l.at)??0)-(Z3(y.at)??0)).slice(0,20)}function hM(f){let u=j_(f?.core?.operations?.recentSlowOperations);return[...j_(f?.frontend?.operations?.recentSlowOperations),...u].sort((y,l)=>Mf(l.durationMs)-Mf(y.durationMs)).slice(0,20)}function IM(f){let u=performance.memory,_=Number(u?.usedJSHeapSize);if(Number.isFinite(_)&&_>0)return _;let y=Number(f?.appBundleBytes);if(Number.isFinite(y)&&y>0)return y;return Mf(f?.process?.heapUsedBytes)}function cM({points:f}){let u=j_(f),_=u.map((W)=>Mf(W.mb)),y=Math.max(1,..._),l=Math.max(0,Math.min(..._,0)),$=Math.max(1,y-l),j=u.length>1?u:[...u,...u],J=j.length<=1?100:100/(j.length-1),F=j.map((W,K)=>{let E=Mf(W.mb);return`${(K*J).toFixed(2)},${(48-(E-l)/$*42).toFixed(2)}`}).join(" "),A=`0,50 ${F} 100,50`,U=u.at(-1),G=u[0];return Q("article",{className:"performance-memory-card","data-testid":"performance-memory-chart"},Q("div",{className:"performance-memory-head"},Q("strong",null,`Bwebui: ${U?`${Mf(U.mb).toFixed(1)}MB`:"--"}`),Q("span",null,u.length>0?`${u.length} samples`:"等待采样")),Q("svg",{viewBox:"0 0 100 50",preserveAspectRatio:"none",role:"img","aria-label":"Bwebui memory trend"},Q("polygon",{points:A}),Q("polyline",{points:F}),Q("line",{x1:"0",x2:"100",y1:"25",y2:"25"})),Q("div",{className:"performance-axis-row"},Q("span",null,G?L0(new Date(G.at)):"--"),Q("span",null,"时间"),Q("span",null,U?L0(new Date(U.at)):"--")),Q("div",{className:"performance-axis-row"},Q("span",null,`${l.toFixed(1)}`),Q("span",null,"(MB)"),Q("span",null,`${y.toFixed(1)}`)))}function pM({onRaw:f}){let[u,_]=hf({core:null,frontend:null}),[y,l]=hf([]),[$,j]=hf(""),[J,F]=hf(!1),[A,U]=hf(null),[G,W]=hf(!1);async function K(){F(!0),j("");try{let[C,S]=await Promise.all([wf(`${sf.apiBaseUrl}/performance`,{cache:"no-store"}),wf(`${sf.apiBaseUrl}/frontend-performance`,{cache:"no-store"})]);_({core:C,frontend:S});let B=IM(S);l((P)=>[...P,{at:new Date().toISOString(),mb:B/1048576}].slice(-80))}catch(C){j(Pf(C,"性能指标加载失败"))}finally{F(!1)}}h1(()=>{K();let C=setInterval(()=>void K(),5000);return()=>clearInterval(C)},[]);async function E(){W(!0),j(""),U(null);try{let C=await wf(`${sf.apiBaseUrl}/codex-queue-load-test`,{method:"POST",body:JSON.stringify({targetMs:1000,timeoutMs:90000,url:sf.frontendPublicUrl||window.location.origin})});U(C),K()}catch(C){j(Pf(C,"Codex Queue Playwright 测量失败"))}finally{W(!1)}}let H=xM(u),O=bM(u),z=vM(u),q=hM(u),Z=u.core?.process||{},V=u.frontend?.process||{},L=u.core?.database?.codexQueueStorage||{},r=Mf(L.total),N=A?.result||{},D=Mf(N.wallMs,NaN),x=Mf(N.networkIdleMs,NaN),c=N.withinTarget===!0,v=G?"running":A===null?"idle":A.measurementOk===!0?c?"passed":"slow":"failed";return Q("div",{className:"performance-page","data-testid":"performance-page"},Q("div",{className:"performance-hero"},Q("div",null,Q("p",{className:"panel-eyebrow"},"Unified Performance"),Q("h2",null,"性能面板"),Q("p",null,"按组件统计 HTTP 请求、失败率、P95 延迟,并汇总 backend/frontend 内部操作耗时。")),Q("div",{className:"inline-actions"},Q("button",{type:"button",className:"ghost-btn",onClick:()=>void E(),disabled:G,"data-testid":"codex-queue-load-test-button"},G?"测试中...":"测试 Codex Queue 加载"),Q("button",{type:"button",className:"ghost-btn",onClick:()=>void K(),disabled:J,"data-testid":"performance-refresh-button"},J?"刷新中":"刷新"),Q(m0,{title:"Performance Snapshot",data:u,onOpen:f,testId:"raw-performance"}))),Q(H0,{error:$}),Q("div",{className:"performance-top-grid"},Q(cM,{points:y}),Q("div",{className:"performance-metric-stack"},Q(_0,{label:"backend RSS",value:Fu(Z.rssBytes),hint:`heap ${Fu(Z.heapUsedBytes)}`}),Q(_0,{label:"frontend RSS",value:Fu(V.rssBytes),hint:`bundle ${Fu(u.frontend?.appBundleBytes)}`}),Q(_0,{label:"Codex PG 任务",value:r||"--",hint:L.ok?"unidesk_codex_queue_tasks":"等待表初始化",tone:L.ok?"ok":"warn"}),Q(_0,{label:"请求样本",value:Mf(u.core?.requests?.sampleCount)+Mf(u.frontend?.requests?.sampleCount),hint:"rolling window 3000"}))),Q(mf,{title:"Codex Queue 加载基准",eyebrow:"Playwright / target <1s",className:"codex-load-test-panel",actions:Q("div",{className:"panel-actions"},Q("button",{type:"button",className:"primary-btn",onClick:()=>void E(),disabled:G,"data-testid":"codex-queue-load-test-panel-button"},G?"正在运行 Playwright...":"手动触发测试"),A?Q(m0,{title:"Codex Queue Load Test",data:A,onOpen:f,testId:"raw-codex-queue-load-test"}):null)},Q("div",{className:"codex-load-test-grid","data-testid":"codex-queue-load-test-result"},Q(_0,{label:"总耗时",value:G?"运行中":Number.isFinite(D)?ku(D):"--",hint:A===null?"点击按钮启动远端 Playwright":`目标 ${ku(N.targetMs||1000)} / ${N.url||"Codex Queue"}`,tone:v==="passed"?"ok":v==="failed"||v==="slow"?"warn":""}),Q(_0,{label:"判定",value:G?"RUNNING":v==="passed"?"PASS <1s":v==="slow"?"SLOW":v==="failed"?"FAILED":"--",hint:A?.measurementOk===!1?String(A.error||N.error||"measurement failed").slice(0,120):"导航开始 -> DOMContentLoaded -> data-load-state=complete",tone:v==="passed"?"ok":v==="idle"||v==="running"?"":"fail"}),Q(_0,{label:"Network idle",value:Number.isFinite(x)?ku(x):"--",hint:`DOMContentLoaded ${ku(N.domContentLoadedMs)} / ${N.networkIdleReached===!1?"未在 5s 内空闲":"已空闲"}`,tone:Number.isFinite(x)&&x<=1000?"ok":"warn"}),Q(_0,{label:"组件耗时",value:Number.isFinite(Mf(N.componentLoadMs,NaN))?ku(N.componentLoadMs):"--",hint:`queue ${ku(N.queueMs)} / detail ${ku(N.detailMs)}`,tone:Mf(N.componentLoadMs)>1000?"warn":"ok"}),Q(_0,{label:"Trace 规模",value:Number.isFinite(Mf(N.transcriptRows,NaN))?String(N.transcriptRows):"--",hint:`${N.visibleTaskCount??0} visible tasks / ${N.partial?"preview":"complete"}`})),G?Q("div",{className:"performance-empty-line"},"正在通过 main-server Host SSH 启动 Playwright,完成后会显示 wall time、组件耗时和最慢 API。"):null,A&&Array.isArray(N.slowestApi)&&N.slowestApi.length>0?Q("div",{className:"table-wrap performance-table-wrap compact codex-load-api-table"},Q("table",{className:"performance-table"},Q("thead",null,Q("tr",null,["API","状态","耗时"].map((C)=>Q("th",{key:C},C)))),Q("tbody",null,N.slowestApi.slice(0,5).map((C,S)=>Q("tr",{key:`${C.url}-${S}`},Q("td",null,Q("code",null,C.url)),Q("td",null,C.status),Q("td",null,ku(C.durationMs))))))):null),Q("div",{className:"performance-grid"},Q(mf,{title:"组件汇总",eyebrow:"Requests"},H.length===0?Q(J0,{title:"暂无请求样本",text:"刷新几次或打开页面后会自动形成组件统计"}):Q("div",{className:"table-wrap performance-table-wrap"},Q("table",{className:"performance-table"},Q("thead",null,Q("tr",null,["组件","请求数","失败数","失败率","平均延迟","P95"].map((C)=>Q("th",{key:C},C)))),Q("tbody",null,H.map((C)=>Q("tr",{key:C.component},Q("td",null,Q("code",null,C.component)),Q("td",null,C.requestCount),Q("td",null,C.failureCount),Q("td",null,ly(Mf(C.failureRate)*100)),Q("td",null,ku(C.averageLatencyMs)),Q("td",null,ku(C.p95LatencyMs)))))))),Q(mf,{title:"最近失败请求",eyebrow:"Failures"},O.length===0?Q("div",{className:"performance-empty-line"},"最近没有失败请求"):Q("div",{className:"table-wrap performance-table-wrap compact"},Q("table",{className:"performance-table"},Q("thead",null,Q("tr",null,["时间","来源","组件","状态","路径"].map((C)=>Q("th",{key:C},C)))),Q("tbody",null,O.map((C,S)=>Q("tr",{key:`${C.at}-${S}`},Q("td",null,zf(C.at)),Q("td",null,C.source),Q("td",null,Q("code",null,C.component)),Q("td",null,Q(O0,{status:"failed"},C.status)),Q("td",null,Q("code",null,C.path)))))))),Q(mf,{title:"内部操作汇总",eyebrow:"Operations"},z.length===0?Q(J0,{title:"暂无内部操作样本",text:"API 查询和代理请求会自动记录内部操作耗时"}):Q("div",{className:"table-wrap performance-table-wrap"},Q("table",{className:"performance-table"},Q("thead",null,Q("tr",null,["服务","操作","次数","平均延迟","P95"].map((C)=>Q("th",{key:C},C)))),Q("tbody",null,z.map((C)=>Q("tr",{key:`${C.service}-${C.operation}`},Q("td",null,C.service),Q("td",null,Q("code",null,C.operation)),Q("td",null,C.count),Q("td",null,ku(C.averageLatencyMs)),Q("td",null,ku(C.p95LatencyMs)))))))),Q(mf,{title:"最近慢操作",eyebrow:"Slowest"},q.length===0?Q(J0,{title:"暂无慢操作",text:"后端会记录最近窗口内耗时最高的内部操作"}):Q("div",{className:"table-wrap performance-table-wrap"},Q("table",{className:"performance-table"},Q("thead",null,Q("tr",null,["时间","操作","耗时","结果","细节"].map((C)=>Q("th",{key:C},C)))),Q("tbody",null,q.map((C,S)=>Q("tr",{key:`${C.at}-${C.operation}-${S}`},Q("td",null,zf(C.at)),Q("td",null,Q("code",null,C.operation)),Q("td",null,ku(C.durationMs)),Q("td",null,C.ok?"成功":"失败"),Q("td",null,C.detail||"-")))))))))}function kM({provider:f,refresh:u,onRaw:_}){let[y,l]=hf(""),[$,j]=hf(null),[J,F]=hf("");async function A(U){l(U),F("");try{let G=await wf(`${sf.apiBaseUrl}/dispatch`,{method:"POST",body:JSON.stringify({providerId:f.providerId,command:"provider.upgrade",payload:{mode:U,source:"frontend-resource-monitor",requestedAt:new Date().toISOString()}})});j({mode:U,...G}),await u()}catch(G){F(Pf(G,"升级命令下发失败"))}finally{l("")}}return Q(mf,{title:"Provider Gateway 升级",eyebrow:"Remote Control"},Q("div",{className:"upgrade-control","data-testid":"provider-upgrade-control"},Q("p",null,"通过 UniDesk WebSocket 向当前计算节点下发 provider.upgrade;预检只生成升级计划,执行升级会调度节点本地 updater 容器。"),Q("div",{className:"upgrade-target-line"},Q("span",null,"指定 Provider"),Q("code",null,f.providerId),Q(q3,{node:f})),Q("div",{className:"upgrade-actions"},Q("button",{type:"button",className:"ghost-btn",disabled:Boolean(y),onClick:()=>A("plan"),"data-testid":"upgrade-plan-button"},y==="plan"?"预检中":"预检升级"),Q("button",{type:"button",className:"ghost-btn danger",disabled:Boolean(y),onClick:()=>A("schedule"),"data-testid":"upgrade-schedule-button"},y==="schedule"?"调度中":"执行升级")),Q(H0,{error:J}),$?Q("div",{className:"upgrade-result"},Q(O0,{status:$.status||"queued"},$.status||"queued"),Q("span",null,`${$.mode==="schedule"?"执行升级":"预检升级"} 已下发`),Q("span",null,`指定版本 ${ZQ(wE(f))}`),Q("code",null,$.taskId||"--"),Q(m0,{title:"Provider Upgrade Dispatch",data:$,onOpen:_})):Q("span",{className:"muted"},"升级任务结果会进入任务历史;执行升级可能导致 provider 短暂重连。")))}function vE({records:f,onRaw:u,compact:_=!1}){if(f.length===0)return Q(J0,{title:"暂无远程更新记录",text:"该节点还没有 provider.upgrade 任务;执行预检或升级后会在这里形成结构化记录"});return Q("div",{className:`upgrade-record-table-wrap table-wrap ${_?"compact":""}`},Q("table",{className:"upgrade-record-table"},Q("thead",null,Q("tr",null,Q("th",null,"状态"),Q("th",null,"模式"),Q("th",null,"任务"),Q("th",null,"来源"),Q("th",null,"耗时"),Q("th",null,"策略"),Q("th",null,"Gateway 版本"),Q("th",null,"结果记录"),Q("th",null,"更新时间"),Q("th",null,"操作"))),Q("tbody",null,f.map((y)=>Q("tr",{key:y.id,"data-testid":`gateway-upgrade-record-${X1(y.id)}`},Q("td",null,Q(O0,{status:y.status})),Q("td",null,Q("span",{className:`mode-chip ${Q2(y)}`},Q2(y)==="schedule"?"执行升级":"预检")),Q("td",null,Q("strong",null,"provider.upgrade"),Q("code",null,y.id)),Q("td",null,EM(y)),Q("td",null,Q(hE,{task:y})),Q("td",null,HM(y)),Q("td",null,Q("span",{className:"version-chip"},PE(y))),Q("td",null,Q("span",{className:`upgrade-outcome ${String(y.status||"").toLowerCase()}`},SE(y))),Q("td",null,zf(y.updatedAt)),Q("td",null,Q(m0,{title:`Provider Upgrade Task ${y.id}`,data:P6(y),onOpen:u})))))))}function mM({provider:f,tasks:u,onRaw:_,limit:y=5}){let l=CE(u,f.providerId).slice(0,y);return Q(mf,{title:"远程更新记录",eyebrow:f.providerId,actions:Q(q3,{node:f}),className:"provider-upgrade-records-panel"},Q("div",{"data-testid":`provider-upgrade-records-${X1(f.providerId)}`},Q(vE,{records:l,onRaw:_,compact:!0})))}function iM({nodes:f,tasks:u,onRaw:_}){let y=M6(()=>f.map(($)=>{let j=CE(u,$.providerId);return{node:$,records:j,latest:VM(j),capabilities:TE($)}}),[f,u]),l=y.reduce(($,j)=>$+j.records.length,0);return Q("div",{className:"gateway-page","data-testid":"gateway-version-page"},Q(mf,{title:"Provider Gateway 版本",eyebrow:`${f.length} Providers / ${l} 更新记录`},f.length===0?Q(J0,{title:"暂无 Provider 节点",text:"等待 provider-gateway 注册后显示版本号和升级记录"}):Q("div",{className:"table-wrap gateway-version-table-wrap"},Q("table",{className:"gateway-version-table"},Q("thead",null,Q("tr",null,Q("th",null,"状态"),Q("th",null,"Provider"),Q("th",null,"Gateway 版本"),Q("th",null,"升级策略"),Q("th",null,"运维可用性"),Q("th",null,"运行时间"),Q("th",null,"能力"),Q("th",null,"最近远程更新"),Q("th",null,"操作"))),Q("tbody",null,y.map(($)=>Q("tr",{key:$.node.providerId},Q("td",null,Q(O0,{status:$.node.status})),Q("td",null,Q("strong",null,$.node.name),Q("code",null,$.node.providerId)),Q("td",null,Q(q3,{node:$.node})),Q("td",null,T6($.node)),Q("td",null,Q(qQ,{node:$.node})),Q("td",null,KE($.node)?zf(KE($.node)):"待新版上报"),Q("td",null,Q("div",{className:"capability-row"},$.capabilities.length===0?Q("span",{className:"muted"},"未声明"):$.capabilities.slice(0,5).map((j)=>Q("span",{key:j,className:"data-chip"},j)))),Q("td",null,$.latest?Q("div",{className:"latest-upgrade-cell"},Q(O0,{status:$.latest.status}),Q("span",null,`${Q2($.latest)==="schedule"?"执行升级":"预检"} / ${zf($.latest.updatedAt)}`),Q("small",null,`Gateway ${PE($.latest)}`),Q("small",null,SE($.latest))):Q("span",{className:"muted"},"暂无记录")),Q("td",null,Q(m0,{title:`Provider ${$.node.providerId}`,data:$.node,onOpen:_})))))))),Q(mf,{title:"远程更新记录",eyebrow:"Structured provider.upgrade records"},f.length===0?Q(J0,{title:"暂无记录",text:"没有 provider 节点时不会生成远程更新记录"}):Q("div",{className:"gateway-record-grid"},y.map(($)=>Q("article",{key:$.node.providerId,className:"gateway-record-card","data-testid":`gateway-records-${X1($.node.providerId)}`},Q("div",{className:"gateway-record-head"},Q("div",null,Q("strong",null,$.node.name),Q("code",null,$.node.providerId)),Q(q3,{node:$.node})),Q("div",{className:"gateway-record-meta"},Q("span",null,`心跳 ${zf($.node.lastHeartbeat)}`),Q("span",null,`策略 ${T6($.node)}`),Q("span",null,`${$.records.length} 条记录`)),Q(vE,{records:$.records.slice(0,8),onRaw:_,compact:!0}))))))}function gM(f){if(f==="running")return"online";if(f==="paused"||f==="restarting")return"warn";if(f==="exited"||f==="dead")return"offline";return"internal"}function bE(f){return/^[a-f0-9]{48,64}$/i.test(f)}function w6(f){let u=String(f?.name||""),_=String(f?.labels||"");return u==="unidesk_pgdata_10gb"||_.includes("com.docker.compose.volume=unidesk_pgdata_10gb")||u.toLowerCase().includes("pgdata")}function OE(f){let u=String(f?.name||""),_=String(f?.labels||"");if(w6(f))return 0;if(_.includes("com.docker.compose.project=unidesk"))return 1;if(!bE(u))return 2;return 3}function nM(f){return[...f].sort((u,_)=>{let y=OE(u)-OE(_);if(y!==0)return y;return String(u.name||"").localeCompare(String(_.name||""))})}function tM({nodes:f,dockerStatuses:u,onRaw:_}){let[y,l]=hf(""),$=M6(()=>f.map((q)=>{let Z=u.find((V)=>V.providerId===q.providerId);return{...q,dockerStatus:Z?.dockerStatus||null,dockerUpdatedAt:Z?.updatedAt||null}}),[f,u]),j=$.find((q)=>q.providerId===y)||$[0]||null;if(h1(()=>{if(!y&&$[0])l($[0].providerId)},[$.length,y]),!j)return Q(J0,{title:"暂无 Docker 节点",text:"等待 provider 上报 Docker daemon 状态"});let J=j.dockerStatus,F=j.providerId==="main-server",A=J?.counts||{},U=J?.daemon||{},G=J?.containers||[],W=J?.images||[],K=nM(J?.volumes||[]),E=F?K.find(w6):null,H=J?.networks||[],O=G.filter((q)=>q.state==="running"),z=G.filter((q)=>q.state!=="running");return Q("div",{className:"docker-page","data-testid":"docker-status-page"},Q("div",{className:"docker-node-strip"},$.map((q)=>Q("button",{key:q.providerId,type:"button",className:`docker-node-tile ${j.providerId===q.providerId?"active":""}`,onClick:()=>l(q.providerId)},Q("span",{className:`pulse ${q.status}`}),Q("strong",null,q.name),Q("code",null,q.providerId),Q("span",null,q.dockerStatus?`Docker ${q.dockerStatus.ok?"ready":"degraded"}`:"等待上报")))),Q("div",{className:"docker-layout"},Q(mf,{title:"Docker Desktop 视图",eyebrow:j.name,className:"docker-main-panel",actions:J?Q(m0,{title:`Docker ${j.providerId}`,data:J,onOpen:_}):null},!J?Q(J0,{title:"Docker 状态未上报",text:"provider-gateway 会在连接后周期性采集 docker info / ps / images / volume / network"}):Q("div",null,Q("div",{className:"docker-hero"},Q("div",null,Q("p",{className:"panel-eyebrow"},"Daemon"),Q("h3",null,U.name||j.providerId),Q("div",{className:"docker-meta"},Q("span",null,U.serverVersion?`Engine ${U.serverVersion}`:"Engine --"),Q("span",null,U.operatingSystem||"OS --"),Q("span",null,U.architecture||"arch --"),Q("span",null,`${U.cpus||0} CPU / ${Fu(U.memoryBytes)}`))),Q(O0,{status:J.ok?"online":"warn"},J.ok?"Docker Ready":"Docker Degraded")),Q("div",{className:"docker-metrics"},Q(_0,{label:"Containers",value:A.containers??G.length,hint:`${A.running??O.length} running / ${A.stopped??z.length} stopped`,tone:"ok"}),Q(_0,{label:"Images",value:A.images??W.length,hint:`${A.daemonImages??A.images??W.length} daemon images`}),Q(_0,{label:"Volumes",value:A.volumes??K.length,hint:F?E?"database volume visible":"database volume missing":"node local volumes",tone:E?"ok":""}),Q(_0,{label:"Networks",value:A.networks??H.length,hint:U.driver?`driver ${U.driver}`:"docker networks"})),F?Q(sM,{volume:E,volumeCount:K.length}):null,Q("div",{className:"docker-section-head"},Q("h3",null,"Containers"),Q("span",null,`updated ${zf(j.dockerUpdatedAt||J.collectedAt)}`)),Q("div",{className:"docker-container-table table-wrap","data-testid":"docker-container-table"},Q("table",null,Q("thead",null,Q("tr",null,Q("th",null,"状态"),Q("th",null,"容器"),Q("th",null,"镜像"),Q("th",null,"端口"),Q("th",null,"运行时间"),Q("th",null,"大小"))),Q("tbody",null,G.length===0?Q("tr",null,Q("td",{colSpan:6},"暂无容器")):G.map((q)=>Q("tr",{key:`${q.id}-${q.name}`},Q("td",null,Q(O0,{status:gM(q.state)},q.state||"unknown")),Q("td",null,Q("strong",null,q.name||"--"),Q("code",null,q.id||"--")),Q("td",null,q.image||"--"),Q("td",null,q.ports||Q("span",{className:"muted"},"未发布")),Q("td",null,q.runningFor||q.status||"--"),Q("td",null,q.size||"--")))))))),Q("div",{className:"docker-side-stack"},Q(GQ,{title:"Images",items:W,render:(q)=>Q("article",{key:`${q.id}-${q.repository}`,className:"docker-side-row"},Q("strong",null,`${q.repository}:${q.tag}`),Q("span",null,q.size||"--"),Q("code",null,q.id||"--"))}),Q(GQ,{title:"Volumes",items:K,limit:K.length,render:(q)=>Q("article",{key:q.name,className:`docker-side-row volume-row ${F&&w6(q)?"database-volume":""}`,"data-testid":F&&w6(q)?"database-volume-row":void 0},Q("strong",null,q.name),Q("span",null,F&&w6(q)?"PostgreSQL":bE(String(q.name||""))?"anonymous":"named"),Q("code",null,q.mountpoint||q.driver||q.scope||"--"))}),Q(GQ,{title:"Networks",items:H,render:(q)=>Q("article",{key:q.id||q.name,className:"docker-side-row"},Q("strong",null,q.name),Q("span",null,q.driver||"--"),Q("code",null,q.id||"--"))}))))}function sM({volume:f,volumeCount:u}){return Q("section",{className:`docker-volume-focus ${f?"ready":"missing"}`,"data-testid":"database-volume-card"},Q("div",{className:"volume-focus-head"},Q("span",{className:"panel-eyebrow"},"Database Named Volume"),Q(O0,{status:f?"online":"warn"},f?"FOUND":"MISSING")),f?Q("div",{className:"volume-focus-body"},Q("strong",null,f.name),Q("span",null,"PostgreSQL data volume for unidesk-database"),Q("div",{className:"volume-route"},Q("code",null,f.mountpoint||"/var/lib/docker/volumes/unidesk_pgdata_10gb/_data"),Q("span",null,"->"),Q("code",null,"unidesk-database:/var/lib/postgresql/data")),Q("div",{className:"docker-meta compact"},Q("span",null,`driver ${f.driver||"--"}`),Q("span",null,`scope ${f.scope||"--"}`),Q("span",null,`${u} volumes reported`))):Q("div",{className:"volume-focus-body"},Q("strong",null,"unidesk_pgdata_10gb"),Q("span",null,"当前 Docker 快照没有发现数据库命名卷;请检查 provider-gateway 的 Docker volume 上报。")))}function GQ({title:f,items:u,render:_,limit:y}){let l=u.slice(0,y??12),$=Math.max(0,u.length-l.length);return Q(mf,{title:f,eyebrow:`${u.length} items`,className:"docker-side-panel"},u.length===0?Q(J0,{title:`暂无 ${f}`,text:"等待 Docker 状态采集"}):Q("div",{className:"docker-side-list"},l.map(_),$>0?Q("div",{className:"docker-side-more"},`+ ${$} more`):null))}function oM({microservices:f,onRaw:u,onNavigate:_}){let y=f.filter((l)=>qE(l).public===!1);return Q("div",{className:"microservice-page","data-testid":"microservice-catalog-page"},Q(mf,{title:"用户服务目录",eyebrow:"Provider Mounted User Services"},Q("div",{className:"metric-grid"},Q(_0,{label:"服务总数",value:f.length,hint:"config.json 用户服务登记"}),Q(_0,{label:"私有后端",value:y.length,hint:"不直接暴露公网",tone:"ok"}),Q(_0,{label:"D601 服务",value:f.filter((l)=>l.providerId==="D601").length,hint:"compute-node docker"}),Q(_0,{label:"集成前端",value:f.filter((l)=>l.frontend?.integrated).length,hint:"UniDesk React 页面"}))),Q(mf,{title:"服务映射",eyebrow:"Repo Reference + Runtime"},f.length===0?Q(J0,{title:"暂无用户服务",text:"在 config.json 的 microservices 中登记用户服务的 provider、仓库引用和后端映射"}):Q("div",{className:"table-wrap"},Q("table",{className:"microservice-table"},Q("thead",null,Q("tr",null,Q("th",null,"服务"),Q("th",null,"Provider"),Q("th",null,"代码引用"),Q("th",null,"Docker 引用"),Q("th",null,"后端映射"),Q("th",null,"开发入口"),Q("th",null,"运行态"),Q("th",null,"操作"))),Q("tbody",null,f.map((l)=>{let $=RE(l),j=OM(l),J=qE(l);return Q("tr",{key:l.id,"data-testid":`microservice-row-${X1(l.id)}`},Q("td",null,Q("strong",null,l.name),Q("code",null,l.id)),Q("td",null,Q("strong",null,$.providerName||l.providerId),Q("code",null,l.providerId)),Q("td",null,Q("span",null,j.url||"--"),Q("code",null,j.commitId||"--")),Q("td",null,Q("span",null,j.composeFile||"--"),Q("code",null,`${j.composeService||"--"} / ${j.containerName||"--"}`)),Q("td",null,Q(O0,{status:J.public?"warn":"online"},J.public?"public":"private"),Q("code",null,`${J.nodeBindHost||"--"}:${J.nodePort||"--"} -> ${J.proxyMode||"--"}`)),Q("td",null,Q("span",null,l.development?.sshPassthrough?"SSH 透传":"未配置"),Q("code",null,l.development?.worktreePath||"--")),Q("td",null,Q(O0,{status:$.providerStatus==="online"?"online":"warn"},$.providerStatus||"unknown"),Q(sy,{data:$.container,empty:"容器快照未上报"})),Q("td",null,Q("div",{className:"microservice-actions"},l.id==="findjob"?Q("button",{type:"button",className:"ghost-btn",onClick:()=>_("apps","findjob"),"data-testid":"open-findjob-button"},"打开"):null,l.id==="pipeline"?Q("button",{type:"button",className:"ghost-btn",onClick:()=>_("apps","pipeline"),"data-testid":"open-pipeline-button"},"打开"):null,l.id==="todo-note"?Q("button",{type:"button",className:"ghost-btn",onClick:()=>_("apps","todo-note"),"data-testid":"open-todo-note-button"},"打开"):null,l.id==="met-nonlinear"?Q("button",{type:"button",className:"ghost-btn",onClick:()=>_("apps","met-nonlinear"),"data-testid":"open-met-nonlinear-button"},"打开"):null,l.id==="claudeqq"?Q("button",{type:"button",className:"ghost-btn",onClick:()=>_("apps","claudeqq"),"data-testid":"open-claudeqq-button"},"打开"):null,l.id==="codex-queue"?Q("button",{type:"button",className:"ghost-btn",onClick:()=>_("apps","codex-queue"),"data-testid":"open-codex-queue-button"},"打开"):null,l.id==="project-manager"?Q("button",{type:"button",className:"ghost-btn",onClick:()=>_("apps","project-manager"),"data-testid":"open-project-manager-button"},"打开"):null,Q(m0,{title:`用户服务 ${l.id}`,data:l,onOpen:u}))))}))))))}function aM({nodes:f,onDispatched:u,onRaw:_}){let y=f.filter((v)=>v.status==="online"),[l,$]=hf(y[0]?.providerId||f[0]?.providerId||""),[j,J]=hf("docker.ps"),[F,A]=hf("frontend"),[U,G]=hf("operator-check"),[W,K]=hf("normal"),[E,H]=hf(!1),[O,z]=hf(""),[q,Z]=hf(!1),[V,L]=hf(null),[r,N]=hf("");h1(()=>{if(!l&&(y[0]?.providerId||f[0]?.providerId))$(y[0]?.providerId||f[0].providerId)},[f.length,y.length,l]);function D(){return{source:F,note:U,priority:W}}function x(){z(JSON.stringify(D(),null,2)),H(!0)}async function c(v){v.preventDefault(),Z(!0),N("");try{let C=E?JSON.parse(O||"{}"):D(),S=await wf(`${sf.apiBaseUrl}/dispatch`,{method:"POST",body:JSON.stringify({providerId:l,command:j,payload:C})});L(S),await u()}catch(C){N(Pf(C,"下发失败"))}finally{Z(!1)}}return Q("div",{className:"page-grid dispatch-grid"},Q(mf,{title:"下发任务",eyebrow:"Real WebSocket Dispatch"},Q("form",{className:"dispatch-form",onSubmit:c},Q("label",null,"Provider",Q("select",{value:l,onChange:(v)=>$(v.target.value)},f.map((v)=>Q("option",{key:v.providerId,value:v.providerId},`${v.name} / ${v.providerId}`)))),Q("label",null,"Command",Q("select",{value:j,onChange:(v)=>J(v.target.value)},Q("option",{value:"docker.ps"},"docker.ps"),Q("option",{value:"host.ssh"},"host.ssh"),Q("option",{value:"microservice.http"},"microservice.http"),Q("option",{value:"echo"},"echo"))),Q("label",null,"来源",Q("input",{value:F,onChange:(v)=>A(v.target.value)})),Q("label",null,"备注",Q("input",{value:U,onChange:(v)=>G(v.target.value)})),Q("label",null,"优先级",Q("select",{value:W,onChange:(v)=>K(v.target.value)},Q("option",{value:"normal"},"normal"),Q("option",{value:"low"},"low"),Q("option",{value:"urgent"},"urgent"))),Q("div",{className:"dispatch-actions"},Q("button",{type:"button",className:"ghost-btn",onClick:x},"查看原始JSON"),Q("button",{type:"submit",disabled:q||!l},q?"下发中":"下发任务")),E?Q("label",{className:"raw-editor-label"},"高级 Payload",Q("textarea",{className:"raw-editor",value:O,onChange:(v)=>z(v.target.value)})):null,Q(H0,{error:r,wide:!0}))),Q(mf,{title:"下发结果",eyebrow:"Response"},V?Q("div",{className:"result-card"},Q(O0,{status:V.status||"queued"},V.status||"queued"),Q("dl",null,Q("dt",null,"Task ID"),Q("dd",null,Q("code",null,V.taskId||"--")),Q("dt",null,"Provider 在线"),Q("dd",null,ty(V.providerOnline))),Q(m0,{title:"Dispatch Response",data:V,onOpen:_})):Q(J0,{title:"等待操作",text:"任务响应会以结构化结果卡展示"})))}function XE({task:f,onRaw:u}){return Q("article",{className:"compact-row"},Q(O0,{status:f.status}),Q("div",null,Q("strong",null,f.command),Q("code",null,f.id)),Q("span",null,H3(f)?`已等待 ${zQ(f.updatedAt)}`:`耗时 ${E3(YE(f)??0)}`),Q(m0,{title:`Task ${f.id}`,data:P6(f),onOpen:u}))}function hE({task:f}){let u=YE(f),_=H3(f);return Q("div",{className:"task-duration"},Q("strong",null,u===null?"--":E3(u)),Q("span",null,_?`已运行 / 创建 ${zf(f.createdAt)}`:`创建 ${zf(f.createdAt)}`))}function dM({task:f}){let u=String(f?.status||"").toLowerCase(),_=f?.result,y=_&&typeof _==="object"&&!Array.isArray(_)?_:{},$=["exitCode","code","signal","timeoutMs","previousStatus","mode"].filter((j)=>y[j]!==void 0&&y[j]!==null);if(u==="failed"){let j=BE(f);return Q("div",{className:"task-diagnostic failed"},Q("b",null,"失败原因"),Q("span",{className:"diagnostic-reason"},ty(j)),$.length>0?Q("div",{className:"diagnostic-meta"},$.map((J)=>Q("span",{key:J,className:"data-chip"},Q("b",null,J),Q("span",null,ty(y[J]))))):null)}if(H3(f))return Q("div",{className:"task-diagnostic warn"},Q("b",null,"等待终态"),Q("span",null,`最后更新 ${zQ(f.updatedAt)} 前`));return Q("div",{className:"task-diagnostic ok"},Q("b",null,"完成摘要"),Q(sy,{data:_,empty:"无执行输出"}))}function eM({tasks:f,onRaw:u}){let _=f.filter(H3);return Q("div",{"data-testid":"pending-task-page"},Q(mf,{title:"待处理任务",eyebrow:`${_.length} Pending`},_.length===0?Q(J0,{title:"当前无待处理任务",text:"queued / dispatched / running 会在超时后自动转为 failed;历史记录仍可在任务历史中查看"}):Q("div",{className:"table-wrap","data-testid":"pending-task-table"},Q("table",null,Q("thead",null,Q("tr",null,Q("th",null,"状态"),Q("th",null,"任务"),Q("th",null,"Provider"),Q("th",null,"已等待"),Q("th",null,"载荷摘要"),Q("th",null,"操作"))),Q("tbody",null,_.map((y)=>Q("tr",{key:y.id},Q("td",null,Q(O0,{status:y.status})),Q("td",null,Q("strong",null,y.command),Q("code",null,y.id)),Q("td",null,Q("code",null,y.providerId)),Q("td",null,zQ(y.updatedAt)),Q("td",null,Q(sy,{data:y.payload})),Q("td",null,Q(m0,{title:`Pending Task ${y.id}`,data:P6(y),onOpen:u})))))))))}function fP({tasks:f,onRaw:u}){return Q("div",{"data-testid":"task-history-page"},Q(mf,{title:"任务历史",eyebrow:`${f.length} Tasks`},f.length===0?Q(J0,{title:"暂无任务",text:"下发任务后会在这里看到生命周期"}):Q("div",{className:"table-wrap"},Q("table",{className:"task-history-table"},Q("thead",null,Q("tr",null,Q("th",null,"状态"),Q("th",null,"任务"),Q("th",null,"Provider"),Q("th",null,"任务耗时"),Q("th",null,"载荷摘要"),Q("th",null,"诊断信息"),Q("th",null,"更新时间"),Q("th",null,"操作"))),Q("tbody",null,f.map((_)=>Q("tr",{key:_.id,"data-testid":`task-row-${X1(_.id)}`},Q("td",null,Q(O0,{status:_.status})),Q("td",null,Q("strong",null,_.command),Q("code",null,_.id)),Q("td",null,Q("code",null,_.providerId)),Q("td",null,Q(hE,{task:_})),Q("td",null,Q(sy,{data:_.payload})),Q("td",null,Q(dM,{task:_})),Q("td",null,zf(_.updatedAt)),Q("td",null,Q(m0,{title:`Task ${_.id}`,data:P6(_),onOpen:u})))))))))}function uP({tasks:f,onRaw:u}){let _=f.filter((y)=>["succeeded","failed"].includes(y.status));return Q(mf,{title:"执行结果",eyebrow:"Finished Tasks"},_.length===0?Q(J0,{title:"暂无结果",text:"任务完成后展示 provider 返回的结构化摘要"}):Q("div",{className:"result-grid"},_.map((y)=>Q("article",{key:y.id,className:"result-card"},Q("div",{className:"node-card-head"},Q("strong",null,y.command),Q(O0,{status:y.status})),Q("code",null,y.id),Q(sy,{data:y.result,empty:"无执行输出"}),Q(m0,{title:`Task Result ${y.id}`,data:P6(y),onOpen:u})))))}function _P({data:f}){let u=f.overview||{};return Q("div",{className:"page-grid topology-grid"},Q(mf,{title:"公开入口",eyebrow:"Public"},Q("div",{className:"endpoint-list"},Q("article",null,Q("b",null,"Frontend"),Q("span",null,sf.frontendPublicUrl||window.location.origin),Q(O0,{status:"online"},"public")),Q("article",null,Q("b",null,"Provider Ingress"),Q("span",null,sf.providerIngressPublicUrl||"ws://public/ws/provider"),Q(O0,{status:"online"},"public")))),Q(mf,{title:"内部服务",eyebrow:"Docker Network Only"},Q("div",{className:"endpoint-list"},Q("article",null,Q("b",null,"backend-core API"),Q("span",null,"http://backend-core:8080"),Q(O0,{status:"internal"},"internal")),Q("article",null,Q("b",null,"database"),Q("span",null,"postgres://database:5432/unidesk"),Q(O0,{status:"internal"},"internal")))),Q(mf,{title:"运行态",eyebrow:"Runtime"},Q("div",{className:"metric-grid"},Q(_0,{label:"DB Ready",value:u.dbReady?"YES":"NO",hint:"internal health"}),Q(_0,{label:"Online Nodes",value:u.onlineNodeCount??0,hint:"provider-gateway self-link"}))))}function yP({session:f}){return Q(mf,{title:"认证策略",eyebrow:"Frontend Login"},Q("div",{className:"policy-grid"},Q("article",null,Q("span",null,"默认账号"),Q("strong",null,sf.authUsername||"admin")),Q("article",null,Q("span",null,"当前会话"),Q("strong",null,f?.user?.username||"--")),Q("article",null,Q("span",null,"Session TTL"),Q("strong",null,`${sf.sessionTtlSeconds||0}s`)),Q("article",null,Q("span",null,"API 访问"),Q("strong",null,"同源 Cookie 保护"))),Q("p",{className:"muted paragraph"},"浏览器只访问 frontend 同源接口;frontend 容器使用 Docker 内网代理 backend-core API。"))}function lP(){return Q(mf,{title:"安全边界",eyebrow:"Exposure Rule"},Q("div",{className:"security-board"},Q("article",{className:"allow"},Q("b",null,"允许公网"),Q("span",null,"frontend 登录入口"),Q("span",null,"provider ingress WebSocket/health")),Q("article",{className:"deny"},Q("b",null,"禁止公网"),Q("span",null,"backend-core REST API"),Q("span",null,"PostgreSQL database")),Q("article",null,Q("b",null,"数据库卷"),Q("span",null,"named volume unidesk_pgdata_10gb"),Q("span",null,"CLI stop/start 不删除数据卷"))))}function $P({activeModule:f,activeTab:u,data:_,session:y,refresh:l,onRaw:$,onNavigate:j}){if(f==="ops"&&u==="status")return Q(DM,{data:_,onRaw:$,onNavigate:j});if(f==="ops"&&u==="performance")return Q(pM,{onRaw:$});if(f==="ops"&&u==="events")return Q(TM,{events:_.events,onRaw:$});if(f==="ops"&&u==="logs")return Q(rM,{logs:_.logs,onRaw:$});if(f==="nodes"&&u==="list")return Q(MM,{nodes:_.nodes,onRaw:$});if(f==="nodes"&&u==="monitor")return Q(CM,{nodes:_.nodes,systemStatuses:_.systemStatuses,tasks:_.tasks,onRaw:$,refresh:l});if(f==="nodes"&&u==="docker")return Q(tM,{nodes:_.nodes,dockerStatuses:_.dockerStatuses,onRaw:$});if(f==="nodes"&&u==="gateway")return Q(iM,{nodes:_.nodes,tasks:_.tasks,onRaw:$});if(f==="nodes"&&u==="labels")return Q(PM,{nodes:_.nodes});if(f==="nodes"&&u==="heartbeats")return Q(SM,{nodes:_.nodes});if(f==="tasks"&&u==="dispatch")return Q(aM,{nodes:_.nodes,onDispatched:l,onRaw:$});if(f==="tasks"&&u==="pending")return Q(eM,{tasks:_.pendingTasks,onRaw:$});if(f==="tasks"&&u==="history")return Q(fP,{tasks:_.tasks,onRaw:$});if(f==="tasks"&&u==="results")return Q(uP,{tasks:_.tasks,onRaw:$});if(f==="apps"&&u==="catalog")return Q(oM,{microservices:_.microservices,onRaw:$,onNavigate:j});if(f==="apps"&&u==="todo-note")return Q(AE,{microservices:_.microservices,onRaw:$,apiBaseUrl:sf.apiBaseUrl});if(f==="apps"&&u==="findjob")return Q(yz,{microservices:_.microservices,onRaw:$,apiBaseUrl:sf.apiBaseUrl});if(f==="apps"&&u==="pipeline")return Q(_E,{microservices:_.microservices,onRaw:$,apiBaseUrl:sf.apiBaseUrl});if(f==="apps"&&u==="met-nonlinear")return Q(Fz,{microservices:_.microservices,onRaw:$,apiBaseUrl:sf.apiBaseUrl});if(f==="apps"&&u==="claudeqq")return Q(WG,{microservices:_.microservices,onRaw:$,apiBaseUrl:sf.apiBaseUrl});if(f==="apps"&&u==="codex-queue")return Q(_z,{microservices:_.microservices,onRaw:$,apiBaseUrl:sf.apiBaseUrl,initialTasksData:AM});if(f==="apps"&&u==="project-manager")return Q($E,{microservices:_.microservices,onRaw:$,apiBaseUrl:sf.apiBaseUrl});if(f==="config"&&u==="topology")return Q(_P,{data:_});if(f==="config"&&u==="auth")return Q(yP,{session:y});if(f==="config"&&u==="security")return Q(lP);return Q(J0,{title:"未找到页面",text:"请选择左侧主模块和顶部子功能标签"})}function jP({session:f,onLogout:u}){let _=rj(mu,window.location.pathname),[y,l]=hf(_.moduleId),[$,j]=hf({...kl,[_.moduleId]:_.tabId}),[J,F]=hf({overview:null,nodes:[],systemStatuses:[],dockerStatuses:[],microservices:[],events:[],tasks:[],pendingTasks:[],logs:[]}),[A,U]=hf({ok:!1,text:"连接中"}),[G,W]=hf(null),[K,E]=hf(new Date),[H,O]=hf(null),[z,q]=hf(!1),Z=r6.default.useRef(!1),V=mu.moduleById[y]||mu.modules[0],L=$[y]||kl[y]||V.tabs[0].id,r=Array.isArray(J.microservices)?J.microservices:[],N=r.length===0&&y==="apps"&&L==="codex-queue"?[UM]:r,D=N===r?J:{...J,microservices:N},x=y==="apps"?N.find((M)=>String(M?.id||"")===L):null,c=x?RE(x):{},v=V.tabs.find((M)=>M.id===L)?.label||L,C=x?[{key:"microservice",label:"用户服务",value:`${v} ${c.providerStatus==="online"?"在线":c.providerStatus||"未知"}`,tone:c.providerStatus==="online"?"ok":"warn",testId:"active-microservice-status"}]:[];async function S(){if(Z.current)return;Z.current=!0;try{let M=[],w=(n,_f)=>{M.push([n,wf(_f)])},Y=y==="ops"&&L==="status",R=Y||y==="config"&&L==="topology",k=Y||y==="nodes"||y==="tasks"&&L==="dispatch",p=y==="apps"&&L!=="codex-queue";if(R)w("overview",`${sf.apiBaseUrl}/overview`);if(k)w("nodes",`${sf.apiBaseUrl}/nodes`);if(y==="nodes"&&L==="monitor")w("systemStatuses",`${sf.apiBaseUrl}/nodes/system-status?limit=60`),w("tasks",`${sf.apiBaseUrl}/tasks?limit=120&summary=1`);else if(y==="nodes"&&L==="docker")w("dockerStatuses",`${sf.apiBaseUrl}/nodes/docker-status`);else if(y==="nodes"&&L==="gateway")w("tasks",`${sf.apiBaseUrl}/tasks?limit=300&summary=1`);else if(y==="tasks"&&L==="pending")w("pendingTasks",`${sf.apiBaseUrl}/tasks?status=pending&limit=100&summary=1`);else if(y==="tasks"&&(L==="history"||L==="results"))w("tasks",`${sf.apiBaseUrl}/tasks?limit=300&summary=1`);else if(Y)w("tasks",`${sf.apiBaseUrl}/tasks?limit=8&lite=1`),w("pendingTasks",`${sf.apiBaseUrl}/tasks?status=pending&limit=20&lite=1`);if(p)w("microservices",`${sf.apiBaseUrl}/microservices`);if(y==="ops"&&L==="events")w("events",`${sf.apiBaseUrl}/events?limit=100`);if(y==="ops"&&L==="logs")w("logs","/logs?limit=100");await Promise.all(M.map(async([n,_f])=>{let s=await _f,ff={};if(n==="overview")ff.overview=s;if(n==="nodes")ff.nodes=s.nodes||[];if(n==="systemStatuses")ff.systemStatuses=s.systemStatuses||[];if(n==="dockerStatuses")ff.dockerStatuses=s.dockerStatuses||[];if(n==="microservices")ff.microservices=s.microservices||[];if(n==="events")ff.events=s.events||[];if(n==="tasks")ff.tasks=s.tasks||[];if(n==="pendingTasks")ff.pendingTasks=s.tasks||[];if(n==="logs")ff.logs=s.logs||[];F((Kf)=>({...Kf,...ff}))})),U({ok:!0,text:"核心在线"}),W(new Date)}catch(M){if(U({ok:!1,text:Pf(M,"连接失败")}),M.status===401)u(!1)}finally{Z.current=!1}}h1(()=>{let M=()=>{if(!zE())return;S()};M();let w=setInterval(M,WM(y,L)),Y=()=>{if(zE())M()};return document.addEventListener("visibilitychange",Y),()=>{clearInterval(w),document.removeEventListener("visibilitychange",Y)}},[y,L]),h1(()=>{let M=setInterval(()=>E(new Date),1000);return()=>clearInterval(M)},[]),h1(()=>{let M=Wz(mu,window.location.pathname);if(M&&window.location.pathname!==M)window.history.replaceState(null,"",M)},[]),h1(()=>{let M=()=>{let w=rj(mu,window.location.pathname);l(w.moduleId),j((Y)=>({...Y,[w.moduleId]:w.tabId})),O(null)};return window.addEventListener("popstate",M),()=>window.removeEventListener("popstate",M)},[]),h1(()=>{window.scrollTo({top:0,left:0,behavior:"auto"})},[y,L]);function B(M,w,Y="push"){let R=mu.moduleById[M]?M:mu.fallbackTarget.moduleId,k=mu.moduleById[R]?.tabs.some((n)=>n.id===w)?w:kl[R]||mu.moduleById[R]?.tabs[0]?.id||mu.fallbackTarget.tabId;l(R),j((n)=>({...n,[R]:k}));let p=b$(mu,R,k);if(window.location.pathname!==p){let n=Y==="replace"?"replaceState":"pushState";window.history[n](null,"",p)}}function P(M,w){O({title:M,data:w})}return Q("div",{className:`shell ${z?"rail-collapsed":""}`,"data-testid":"app-shell"},Q(YM,{activeModule:y,activeTabs:$,onNavigate:B,collapsed:z,onToggle:()=>q((M)=>!M)}),Q("main",{className:"workspace"},Q(LM,{connection:A,lastRefresh:G,onRefresh:S,onLogout:()=>u(!0),session:f,clock:K,activeStatusItems:C}),Q(BM,{module:V,activeTab:L,onNavigate:B}),Q($P,{activeModule:y,activeTab:L,data:D,session:f,refresh:S,onRaw:P,onNavigate:B})),Q(XM,{raw:H,onClose:()=>O(null)}))}function JP(){let[f,u]=hf(!0),[_,y]=hf(null);async function l(){u(!0);try{let j=await wf("/api/session");y(j.authenticated?j:null)}catch{y(null)}finally{u(!1)}}async function $(j){if(j)try{await wf("/logout",{method:"POST"})}catch{}y(null)}if(h1(()=>{l()},[]),f)return Q("main",{className:"loading-screen"},Q("div",{className:"brand-mark"},"UD"),Q("span",null,"加载会话"));if(!_)return Q(NM,{onLogin:y});return Q(jP,{session:_,onLogout:$})}var IE=document.getElementById("root");if(IE===null)throw Error("root element not found");NE.createRoot(IE).render(Q(JP));})(); + + ${c} + ${m} + ${C} + ${R} + ${T} + ${P} + `,width:E,height:O}}function R2(f,u){let l=URL.createObjectURL(f),y=document.createElement("a");y.href=l,y.download=u,y.click(),setTimeout(()=>URL.revokeObjectURL(l),1000)}async function BE(f,u){let l=LE(u,"pipeline"),{svg:y,width:r,height:_}=eM(f,u),$=new Blob([y],{type:"image/svg+xml;charset=utf-8"}),j=URL.createObjectURL($);try{let A=new Image;await new Promise((W,G)=>{A.onload=()=>W(),A.onerror=()=>G(Error("svg image load failed")),A.src=j});let F=document.createElement("canvas");F.width=r,F.height=_;let U=F.getContext("2d");if(!U)throw Error("canvas unavailable");U.drawImage(A,0,0);let Q=await new Promise((W)=>F.toBlob(W,"image/png"));if(!Q)throw Error("png export failed");R2(Q,`${l}.png`)}catch{R2($,`${l}.svg`)}finally{URL.revokeObjectURL(j)}}async function rS(f){let u=LE(String(f?.title||"pipeline-gantt"),"pipeline-gantt"),{svg:l,width:y,height:r}=yS(f),_=new Blob([l],{type:"image/svg+xml;charset=utf-8"}),$=URL.createObjectURL(_);try{let j=new Image;await new Promise((Q,W)=>{j.onload=()=>Q(),j.onerror=()=>W(Error("gantt svg image load failed")),j.src=$});let A=document.createElement("canvas");A.width=y,A.height=r;let F=A.getContext("2d");if(!F)throw Error("canvas unavailable");F.drawImage(j,0,0);let U=await new Promise((Q)=>A.toBlob(Q,"image/png"));if(!U)throw Error("gantt png export failed");R2(U,`${u}.png`)}catch{R2(_,`${u}.svg`)}finally{URL.revokeObjectURL($)}}async function _S(f){for(let u of f){if(u.flow.nodes.length===0)continue;await BE(u.flow,u.title),await new Promise((l)=>setTimeout(l,750))}}function _E(f,u){return f.find((l)=>String(l?.pipelineId||"")===u)||null}function $E(f){return vf(f?.startedAt)??vf(f?.artifact?.startedAt)??vf(f?.request?.createdAt)??vf(f?.updatedAt)??0}function $S(f,u){return f.filter((l)=>String(l?.pipelineId||"")===u).slice().sort((l,y)=>$E(l)-$E(y)||String(l?.runId||"").localeCompare(String(y?.runId||"")))}function tF(f,u){let l=String(u?.runId||""),y=f.findIndex(($)=>String($?.runId||"")===l),r=y>=0?y+1:f.length,_=String(u?.status||"--");return`Epoch ${r} / ${l||"--"} / ${_}`}function Zl(f){return String(f?.procedureRunId||f?.runId||"")}function h2(f,u){let l=String(f?.nodeId||f?.request?.nodeId||"");if(l)return l;let y=Zl(f),r=`${u}__`;if(y.startsWith(r))return y.slice(r.length).replace(/__\d+$/u,"");return""}function D2(f,u){let l=Yf(f?.artifact)?f.artifact:{},y=Yf(f?.request)?f.request:{};return Q6(f?.startedAt,l.startedAt,y.createdAt,y.startedAt,f?.createdAt,f?.updatedAt,u?.startedAt,u?.request?.createdAt)}function T2(f,u){let l=String(f?.status?.status||f?.artifact?.status||f?.status||"").toLowerCase(),y=Yf(f?.artifact)?f.artifact:{},r=dF(l);return Q6(f?.finishedAt,y.finishedAt,f?.completedAt,r?f?.updatedAt:void 0,r?y.updatedAt:void 0,r?u?.updatedAt:void 0)}function XE(f,u,l=Date.now()){let y=String(f?.runId||""),r=new Set(u.map((_)=>String(_?.id||"")).filter(Boolean));return Lf(f?.procedureRuns).flatMap((_)=>{let $=h2(_,y);if(!$)return[];let j=String(_?.status?.status||_?.artifact?.status||_?.status||"unknown").toLowerCase(),A=D2(_,f),F=vf(A);if(F===null)return[];let U=T2(_,f),Q=vf(U)??(dF(j)?vf(_?.updatedAt)??F+1000:l),W=Math.max(F+1000,Q);return[{nodeId:$,knownNode:r.has($),procedureRunId:Zl(_),status:j,startMs:F,endMs:W,startedAt:F6(F),finishedAt:F6(W),durationMs:W-F,runId:y,raw:_}]}).sort((_,$)=>_.startMs-$.startMs||_.endMs-$.endMs||_.nodeId.localeCompare($.nodeId))}function jS(f,u,l=[]){let y=u.map((U)=>Number(U.startMs)).filter(Number.isFinite),r=u.map((U)=>Number(U.endMs)).filter(Number.isFinite);for(let U of l){let Q=Bu(U?.eventMs??U?.ms);if(Q!==null)y.push(Q),r.push(Q)}let _=vf(f?.startedAt)??vf(f?.artifact?.startedAt)??vf(f?.request?.createdAt),$=vf(f?.finishedAt)??vf(f?.artifact?.finishedAt)??vf(f?.updatedAt);if(_!==null)y.push(_);if($!==null)r.push($);let j=Date.now(),A=y.length>0?Math.min(...y):j-60000,F=Math.max(A+60000,r.length>0?Math.max(...r):j);return{startMs:A,endMs:F,durationMs:F-A}}var n2=12,YE=20,sF=100,AS=!1;function Ly(f){let u=Number(f);if(!Number.isFinite(u))return 0;return Math.max(0,Math.min(100,Math.round(u*100)/100))}function FS(f){let u=Math.max(n2,Number(f||n2)),l=Math.log(u/n2)/Math.log(YE);return Ly(l*100)}var U6=FS(sF);function rJ(f){let u=Ly(f)/100,l=n2*Math.pow(YE,u),y=u<0.24?"全局":u<0.64?"均衡":"细节";return{value:Ly(u*100),pxPerMinute:l,label:y}}function hF(f){let u=Math.round(Number(f));return Math.abs(u-sF)<=1?sF:u}function JS(f,u=U6){let l=Math.max(1,Number(f.durationMs||0)/60000),y=rJ(u);return Math.round(Math.max(360,Math.min(7200,l*Number(y.pxPerMinute||48))))}function US(f,u=7){let l=Math.max(1,Number(f.endMs||0)-Number(f.startMs||0));return Array.from({length:u},(y,r)=>{let _=u===1?0:r/(u-1);return{ms:Number(f.startMs)+l*_,percent:_*100}})}function QS(f,u){let l=Math.max(1,Number(u.endMs)-Number(u.startMs));return Math.max(0,Math.min(100,(f-Number(u.startMs))/l*100))}function Bu(f){let u=Number(f);return Number.isFinite(u)?u:null}function _J(f){return WE(f?.status)&&!dF(f?.status)}function wE(f,u,l,y){let r=Math.max(1,l-u),_=Math.max(0,Math.min(1,(f-u)/r));return Number((_*y).toFixed(3))}function jE(f,u){if(!u)return null;let l=Bu(u?.startMs),y=Bu(u?.endMs),r=Bu(u?.chartHeight);if(l===null||y===null||r===null)return null;return wE(f,l,y,r)}function DE(f,u){let l=Bu(f?.rawStartMs??f?.startMs)??Bu(f?.startMs)??u,y=Bu(f?.endMs)??l+1000;if(!_J(f))return Math.max(l+1000,y);return Math.max(l+1000,y,u)}function WS(f,u,l,y){let r=Bu(f?.startMs)??y-60000,_=Bu(f?.endMs)??y,$=l.reduce((K,E)=>Math.max(K,DE(E,y)),_),j=Math.max(r+60000,_,$),A=Math.max(1,j-r),F={startMs:r,endMs:j,durationMs:A},U=JS(F,u),Q=rJ(u),W=Math.max(5,Math.min(18,Math.round(U/150))),G=US(F,W).map((K)=>{let E=Number(K.ms),O=wE(E,r,j,U);return{...K,y:O,timestamp:F6(E),offsetMs:E-r}});return{source:"frontend-y",startMs:r,endMs:j,durationMs:A,chartHeight:U,scale:Ly(u),normalizedScale:Number((Ly(u)/100).toFixed(3)),pxPerMinute:Number(Number(Q.pxPerMinute||0).toFixed(3)),ticks:G}}function zS(f,u,l){if(!_J(f))return f;let y=Bu(f?.rawStartMs??f?.startMs)??Bu(f?.startMs)??l,r=DE(f,l),_=jE(y,u),$=jE(r,u),j=Bu(_??f?.y1??f?.startY)??0,A=Bu($??f?.y2??f?.endY)??j+10,F=Math.max(24,A-j);return{...f,live:!0,startMs:y,endMs:r,durationMs:Math.max(1000,r-y),finishedAt:F6(r),y1:j,y2:A,startY:j,endY:A,height:F}}function $J(f,u,l){return QS(f,u)/100*l}function p_(f){return Boolean(f&&String(f?.source||"")!=="frontend-y")}function TE(f,u,l,y,r){if(p_(y))for(let $ of r){let j=Bu(f?.[$]);if(j!==null)return j}let _=Bu(f?.ms??f?.eventMs??f?.startMs);return $J(_??Number(u.startMs),u,l)}function x2(f,u,l,y){return TE(f,u,l,y,["y1","startY"])}function oF(f,u,l,y){if(p_(y)){let _=Bu(f?.y2??f?.endY);if(_!==null)return _}let r=Bu(f?.endMs)??Number(u.endMs);return $J(r,u,l)}function nE(f,u,l,y){if(p_(y)){let _=Bu(f?.height);if(_!==null)return Math.max(1,_)}let r=f?.live?24:10;return Math.max(r,oF(f,u,l,y)-x2(f,u,l,y))}function d0(f,u,l,y){return TE(f,u,l,y,["y","timeAxisY"])}function ME(f,u,l,y){if(p_(y)||String(y?.source||"")==="frontend-y"){let $=Bu(f?.y);if($!==null)return $}let r=Bu(f?.percent);if(r!==null)return r/100*l;let _=Bu(f?.ms)??Number(u.startMs);return $J(_,u,l)}function GS(f){let u=String(f?.promptEvent||f?.raw?.promptEvent||f?.event||"").toLowerCase();if(!["node-long-running-observation","node-finished"].includes(u))return"";let l=String(f?.sourceNodeId||f?.raw?.sourceNodeId||f?.raw?.detail?.nodeId||""),y=String(f?.nodeId||f?.targetNodeId||"");return l&&l!==y?l:""}function KS(f,u){let l=new Set(u.map((r)=>[String(r.sourceNodeId||""),String(r.targetNodeId||""),String(r.targetMarkerId||""),String(r.action||"")].join(":"))),y=[...u];for(let r of f){let _=GS(r),$=String(r?.nodeId||""),j=String(r?.id||"");if(!_||!$||!j)continue;let A=[_,$,j,"observe"].join(":");if(l.has(A))continue;l.add(A),y.push({id:`observation-arrow:${j}:${_}:${$}`,commandId:String(r?.commandId||r?.eventId||j),sourceNodeId:_,targetNodeId:$,sourceMarkerId:"",targetMarkerId:j,sourceKind:"monitor",action:"observe",status:"observation"})}return{markers:f,arrows:y}}function AE(f,u=""){let l=cl(f)||u,y=String(f?.promptEvent||"");if(l==="initial-prompt-delivered")return"initial";if(y==="node-finished"||y==="node-long-running-observation"||y.startsWith("monitor-"))return"monitor";if(l==="monitor-prompt-delivered"||String(f?.sourceKind||"").toLowerCase()==="monitor"||u==="monitor-prompt-queued")return"monitor";return"append"}function NS(f){return Lf(f?.tags||f?.raw?.tags).map((u)=>String(u||"")).filter(Boolean)}function FE(f,u=""){let l=cl(f)||u,y=String(f?.promptEvent||"");if(l==="initial-prompt-delivered")return"初始 prompt";if(y==="node-long-running-observation")return"长任务观察";if(y==="node-finished")return NS(f).includes("monitor.audit")?"节点完成 / OA 审核":"节点完成";if(y==="monitor-interval")return"旧版轮询";if(y==="monitor-start")return"Monitor start";if(y==="monitor-stop")return"Monitor stop";if(l==="monitor-prompt-delivered"||u==="monitor-prompt-queued")return"Monitor prompt";if(l==="append-prompt-queued")return"追加 prompt 已排队";return"追加 prompt"}function JE(f){let u=cl(f);if(u==="control-command-applied")return 3;if(u==="control-command-ignored")return 2;if(u==="control-command-queued")return 1;return 0}function UE(f,u){let l=String(f?.commandId||"");if(l)return`command:${l}`;return["fallback",qr(f)||Q6(f?.createdAt,f?.timestamp)||`index-${u}`,String(f?.sourceKind||""),String(f?.sourceNodeId||""),String(f?.targetNodeId||""),Vr(f)].join(":")}function ZS(f){return gF([f?.targetNodeId,...Lf(f?.resetNodeIds)])}function ES(f,u){let l=j6(f),y=cl(f),r=String(f?.targetNodeId||""),_=Boolean(r)&&u!==r;if(y==="control-command-applied")return _?`${l} 波及`:`${l} 生效`;if(y==="control-command-ignored")return`${l} 忽略`;if(y==="control-command-queued")return`${l} 已发起`;return _?`${l} 波及`:l}function HS(f){if(cl(f)==="control-command-ignored")return"ignored";let l=Vr(f);if(l==="restart"||l==="redo")return"restart";if(l==="modify")return"modify";if(l==="approve")return"approve";if(l==="guide")return"guide";return"pending"}function OS(f){let u=String(f?.sourceKind||"").toLowerCase();if(u==="monitor")return"monitor";if(u==="webui")return"webui";if(u==="cli")return"cli";return"system"}function qS(f,u,l,y){let r=f.filter((F)=>String(F.nodeId||"")===u).sort((F,U)=>Number(F.startMs)-Number(U.startMs)),_=r.find((F)=>l>=Number(F.startMs)-1000&&l<=Number(F.endMs)+1000);if(_)return{ms:l,onInterval:!0,snapReason:"inside-interval",procedureRunId:String(_.procedureRunId||"")};let $=Vr(y),j=r.slice().reverse().find((F)=>Number(F.endMs)<=l+1000);if(j&&$==="approve")return{ms:Number(j.endMs),onInterval:!0,snapReason:"previous-interval-end",procedureRunId:String(j.procedureRunId||"")};let A=r.find((F)=>Number(F.startMs)>=l-1000);if(A&&["guide","modify","restart","redo"].includes($))return{ms:Number(A.startMs),onInterval:!0,snapReason:"next-interval-start",procedureRunId:String(A.procedureRunId||"")};return{ms:l,onInterval:!1,snapReason:"event-time",procedureRunId:String(y?.procedureRunId||"")}}function SE(f,u,l,y){let r=Math.hypot(l-f,y-u),_=r>pZ?pZ:0,$=_>0?l-(l-f)/r*_:l,j=_>0?y-(y-u)/r*_:y,A=$-f,F=Math.max(16,Math.min(42,Math.abs(A)*0.45+12)),U=A===0?1:Math.sign(A);return`M ${f},${u} C ${f+U*F},${u} ${$-U*F},${j} ${$},${j}`}function VS(f,u){let l=String(f?.runId||u?.runId||""),y=XE({...Yf(u)?u:{},...Yf(f)?f:{},runId:l,procedureRuns:Lf(f?.procedureRuns).length>0?f.procedureRuns:u?.procedureRuns},[]),r=[],_=[],$=[],j=new Set,A=new Map,F=(W,G)=>{if(!W.nodeId||!Number.isFinite(Number(W.ms)))return;if(j.has(W.id))return;j.add(W.id),G.push(W)};for(let W of Lf(f?.procedureRuns)){let G=h2(W,l),K=Zl(W);if(!G)continue;for(let E of Lf(W?.attempts)){let O=b2(E),z=new Set,Z=new Set;for(let H of $6(E?.controlEventRecords)){let Y=cl(H);if(!["initial-prompt-delivered","append-prompt-delivered","monitor-prompt-delivered"].includes(Y))continue;let w=qr(H),V=vf(w);if(V===null)continue;let X=String(H?.eventId||"");if(X)z.add(X);Z.add(`${Y}:${w}:${String(H?.sourceKind||"")}:${String(H?.promptPreview||"")}`),F({id:`prompt:${X||`${K}:${O}:${Y}:${V}`}`,runId:l,nodeId:G,procedureRunId:K,attempt:O,kind:"prompt",tone:AE(H,Y),status:"delivered",label:FE(H,Y),ms:V,timestampIso:w,sourceKind:String(H?.sourceKind||""),sourceNodeId:String(H?.sourceNodeId||""),targetNodeId:G,action:"",eventId:X,commandId:String(H?.commandId||""),raw:H},r)}let N=[{records:$6(E?.controlPromptRecords),fallbackKind:"append-prompt-queued"},{records:$6(E?.monitorPromptRecords),fallbackKind:"monitor-prompt-queued"}];for(let H of N)for(let Y of H.records){let w=qr(Y),V=vf(w);if(V===null)continue;let X=String(Y?.eventId||"");if(X&&z.has(X))continue;let m=`${H.fallbackKind==="monitor-prompt-queued"?"monitor-prompt-delivered":"append-prompt-delivered"}:${w}:${String(Y?.sourceKind||"")}:${String(Y?.promptPreview||"")}`;if(Z.has(m))continue;F({id:`prompt-fallback:${X||`${K}:${O}:${H.fallbackKind}:${V}`}`,runId:l,nodeId:G,procedureRunId:K,attempt:O,kind:"prompt",tone:AE(Y,H.fallbackKind),status:"queued",label:FE(Y,H.fallbackKind),ms:V,timestampIso:w,sourceKind:String(Y?.sourceKind||""),sourceNodeId:String(Y?.sourceNodeId||""),targetNodeId:G,action:"",eventId:X,commandId:String(Y?.commandId||""),raw:Y},r)}}}let U=new Map;$6(f?.controlEvents).forEach((W,G)=>{let K=UE(W,G),E=U.get(K)||{key:K,events:[],commands:[]};E.events.push(W),U.set(K,E)}),Lf(f?.controlCommands).filter(Yf).forEach((W,G)=>{let K=UE(W,G),E=U.get(K)||{key:K,events:[],commands:[]};E.commands.push(W),U.set(K,E)});for(let W of U.values()){let G=Lf(W.events).slice().sort((c,C)=>JE(C)-JE(c)),K=Lf(W.commands),E=Lf(W.events).find((c)=>cl(c)==="control-command-queued")||K[0]||null,O=G[0]||K[0]||E;if(!E&&!O)continue;let z=String(E?.sourceNodeId||O?.sourceNodeId||""),Z=String(E?.sourceKind||O?.sourceKind||""),N=qr(E)||qr(O)||Q6(E?.createdAt,O?.createdAt),H=vf(N),Y=String(O?.commandId||E?.commandId||W.key),w=(cl(O)||"control-command-queued").replace(/^control-command-/u,""),V="";if(z&&H!==null)V=`control-source:${Y}:${z}`,A.set(Y,V),F({id:V,runId:l,nodeId:z,procedureRunId:String(E?.procedureRunId||O?.procedureRunId||""),attempt:"",kind:"control-source",tone:OS(E||O),status:w,label:`${j6(E||O)} 发起`,ms:H,timestampIso:N,action:Vr(E||O),sourceKind:Z,sourceNodeId:z,targetNodeId:String(O?.targetNodeId||E?.targetNodeId||""),commandId:Y,raw:E||O},_);let X=O||E,i=qr(X)||N,m=vf(i);if(m===null)continue;let M=ZS(X);for(let c of M){let C=qS(y,c,m,X),T=`control-target:${Y}:${c}`;if(F({id:T,runId:l,nodeId:c,procedureRunId:C.procedureRunId,attempt:"",kind:"control-target",tone:HS(X),status:w,label:ES(X,c),ms:C.ms,eventMs:m,onInterval:C.onInterval,snapReason:C.snapReason,snapped:Number(C.ms)!==m,timestampIso:i,renderedTimestampIso:F6(Number(C.ms)),action:Vr(X),sourceKind:Z,sourceNodeId:z,targetNodeId:c,commandId:Y,raw:X},_),V&&z&&z!==c)$.push({id:`control-arrow:${Y}:${z}:${c}`,commandId:Y,sourceNodeId:z,targetNodeId:c,sourceMarkerId:V,targetMarkerId:T,sourceKind:Z,action:Vr(X),status:w})}}let Q=[...r,..._].sort((W,G)=>Number(W.ms)-Number(G.ms)||String(W.nodeId).localeCompare(String(G.nodeId))||String(W.id).localeCompare(String(G.id)));return{...KS(Q,$),sourceMarkerByCommand:A}}function LS({details:f,selectedNodeId:u,selectedNodeRuntime:l,control:y,onRaw:r}){if(!f)return q("span",{className:"muted"},"点击“抓取过程”读取 node 运行材料;主界面只显示结构化摘要,完整内容需点开原始 JSON。");let _=Lf(f.procedureRuns),$=_.at(-1)||{},j=Lf($.attempts),A=j.at(-1)||{},F=Lf($.workerLogTail),U=Lf(A.controlEventsTail),Q=Lf(A.controlPromptsTail),W=Lf(A.monitorPromptsTail),G=RF(U),K=RF(Q),E=RF(W),O=A.opencodeMessages||{};return q("div",{className:"pipeline-evidence-list compact"},q(Kl,{title:"Node runtime",subtitle:u||"--",facts:[`status ${l?.status||"pending"}`,`attempts ${l?.attempts??j.length}`,`procedure ${l?.currentProcedureRunId||Zl($)||"--"}`,y.fetchedAt?`fetched ${Uu(y.fetchedAt)}`:"not fetched"],data:f.node||f,onRaw:r,testId:"raw-pipeline-node-runtime"}),q(Kl,{title:"Procedure runs",subtitle:`${_.length} groups`,facts:[`latest ${$.status?.status||$.status||"--"}`,`steps ${Lf($.recentSteps).length}`,`duration ${Nl(vf($.finishedAt)&&vf($.startedAt)?Number(vf($.finishedAt))-Number(vf($.startedAt)):$.durationMs)}`],data:_,onRaw:r,testId:"raw-pipeline-node-procedures"}),q(Kl,{title:"OpenCode messages",subtitle:String(O.exists?"available":"not indexed"),facts:[`messages ${S2(O.messageCount)}`,`size ${S2(O.size)}`,`updated ${Kf(O.updatedAt)}`],data:O,onRaw:r,testId:"raw-pipeline-node-messages"}),q(Kl,{title:"Control prompts",subtitle:"manual / monitor append queues",facts:[`manual tail ${K.total}`,`monitor tail ${E.total}`,`last ${Kf(aF(K.lastAt,E.lastAt))}`],data:{controlPromptsTail:Q,monitorPromptsTail:W},onRaw:r,testId:"raw-pipeline-node-prompts"}),q(Kl,{title:"Control events",subtitle:G.eventKinds.length>0?G.eventKinds.join(", "):"event tail",facts:[`tail ${G.total}`,`parsed ${G.parsed}`,`last ${Kf(G.lastAt)}`],data:U,onRaw:r,testId:"raw-pipeline-node-events"}),q(Kl,{title:"Worker log",subtitle:"tail is hidden on main canvas",facts:[`tail ${F.length} lines`,"raw only via button",`procedure ${Zl($)||"--"}`],data:F,onRaw:r,testId:"raw-pipeline-node-worker-log"}))}function BS({activeRun:f,onRaw:u}){if(!f)return q(e0,{title:"暂无运行材料",text:"没有 Pipeline epoch 时不会展示运行材料索引。"});let l=Lf(f.nodes),y=Lf(f.procedureRuns),r=Lf(f.submissions),_=Lf(f.workerLogTail),$=kZ(l),j=kZ(y),A=y.filter((U)=>String(U?.status||"").toLowerCase()==="failed"),F=aF(...y.flatMap((U)=>[U.updatedAt,U.finishedAt,U.startedAt]));return q("div",{className:"pipeline-evidence-list"},q(Kl,{title:"Epoch overview",subtitle:f.runId||"--",facts:[`pipeline ${f.pipelineId||"--"}`,`status ${f.status||"--"}`,`started ${Kf(f.startedAt)}`,`updated ${Kf(f.updatedAt)}`],data:f,onRaw:u,testId:"raw-pipeline-run"}),q(Kl,{title:"Node states",subtitle:`${l.length} nodes`,facts:[`running ${$.running||0}`,`succeeded ${$.succeeded||0}`,`failed ${$.failed||0}`,`pending ${$.pending||0}`],data:l,onRaw:u,testId:"raw-pipeline-run-nodes"}),q(Kl,{title:"Procedure run index",subtitle:`${y.length} procedure records`,facts:[`succeeded ${j.succeeded||0}`,`failed ${j.failed||0}`,`latest ${Kf(F)}`,`errors ${A.length}`],data:y,onRaw:u,testId:"raw-pipeline-run-procedures"}),q(Kl,{title:"OA submissions",subtitle:`${r.length} submission files`,facts:[`records ${r.length}`,`task ${S2(f.task)}`,"raw grouped by run"],data:r,onRaw:u,testId:"raw-pipeline-run-submissions"}),q(Kl,{title:"Worker log tail",subtitle:"hidden from main interface",facts:[`tail ${_.length} lines`,"display raw only after click",`updated ${Kf(f.updatedAt)}`],data:_,onRaw:u,testId:"raw-pipeline-run-worker-log"}))}function XS({diagnostics:f,onRaw:u}){let l=Lf(f?.runs).filter(Yf),y=Lf(f?.forbiddenResiduals),r=Yf(f?.guarantees)?f.guarantees:{},_=f?.hasNeutralNodeFinishedEvidence===!0&&f?.hasNoAuditPolicyEvidence===!0&&f?.hasAuditPolicyEvidence===!0,$=f?.ok===!0&&_&&y.length===0,j=l[0]||null,A=[{label:"中性完成事实",ok:r.neutralNodeFinished===!0,hint:"node-finished 不携带流程策略"},{label:"Config 策略判定",ok:r.auditPolicyFromConfig===!0,hint:"OA backend 读取当前 epoch 配置"},{label:"控制命令来自 OA",ok:r.runnerConsumesControlCommandsFromOaEvents===!0,hint:"runner 只消费 OA control.command"},{label:"无独立审核事件",ok:r.noIndependentAuditRequestEvent===!0,hint:"审核由 node-finished + policy 派生"},{label:"无批次门禁",ok:r.noBatchFinishedControlGate===!0,hint:"下游启动由每个 node 完成驱动"}];return q("div",{className:"pipeline-oa-panel","data-testid":"pipeline-oa-event-flow-panel"},q("div",{className:"metric-grid compact"},q(L0,{label:"OA Flow",value:$?"100%":"--",hint:String(f?.mode||"waiting diagnostics"),tone:$?"ok":"warn"}),q(L0,{label:"禁止残留",value:y.length,hint:y.length===0?"source scan clean":"needs cleanup",tone:y.length===0?"ok":"warn"}),q(L0,{label:"No-audit",value:f?.hasNoAuditPolicyEvidence?"OK":"--",hint:"OA 下游策略证据",tone:f?.hasNoAuditPolicyEvidence?"ok":"warn"}),q(L0,{label:"Monitor 审核",value:f?.hasAuditPolicyEvidence?"OK":"--",hint:"OA 控制事件闭环",tone:f?.hasAuditPolicyEvidence?"ok":"warn"})),q("div",{className:"pipeline-oa-guarantees"},A.map((F)=>q("article",{key:F.label,className:`pipeline-oa-guarantee ${F.ok?"ok":"warn"}`},q(Vy,{status:F.ok?"online":"warn"},F.ok?"OK":"MISS"),q("div",null,q("strong",null,F.label),q("span",null,F.hint))))),q("div",{className:"pipeline-evidence-list compact"},l.slice(0,6).map((F)=>q(Kl,{key:F.runId,title:String(F.runId||"--"),subtitle:[Number(F.monitorAuditNodeFinishedCount||0)>0?"monitor audit":"",Number(F.noAuditPolicyCount||0)>0?"no-audit policy":""].filter(Boolean).join(" / ")||"event evidence",facts:[`events ${F.eventCount||0}`,`node-finished ${F.nodeFinishedCount||0}`,`policy-in-detail ${F.nodeFinishedWithPolicyCount||0}`,`queued ${F.controlQueuedCount||0}`,`applied ${F.controlAppliedCount||0}`],data:F,onRaw:u,testId:`raw-pipeline-oa-run-${String(F.runId||"run").replace(/[^a-zA-Z0-9_.-]+/g,"-")}`}))),j?q("p",{className:"muted paragraph"},`最新证据 ${j.runId}: ${j.nodeFinishedCount||0} 个 node-finished,${j.controlAppliedCount||0} 个控制结果。`):q(e0,{title:"暂无 OA 事件流证据",text:"等待 Pipeline backend 暴露 diagnostics。"}),f?q("div",{className:"panel-actions inline-actions"},q(il,{title:"Pipeline OA Event Flow Diagnostics",data:f,onOpen:u,testId:"raw-pipeline-oa-event-flow"})):null)}function YS({quota:f,onRaw:u}){let l=Yf(f?.summary)?f.summary:{},y=Yf(f?.target)?f.target:{},r=Yf(f?.cache)?f.cache:{},_=f?.ok===!0,$=String(f?.modelId||l.modelName||y.modelName||"MiniMax-M2.7"),j=l.totalCount??y.currentIntervalTotalCount,A=l.usageCount??y.currentIntervalUsageCount,F=l.remainingCount??y.currentIntervalRemainingCount,U=l.remainingRatio??(Number.isFinite(Number(j))&&Number(j)>0&&Number.isFinite(Number(F))?Number(F)/Number(j):void 0),Q=l.usageRatio??(Number.isFinite(Number(j))&&Number(j)>0&&Number.isFinite(Number(A))?Number(A)/Number(j):void 0),W=l.resetAt||y.endAt,G=l.remainsMs??y.remainsMs,K=Number(F),E=!_||Number.isFinite(K)&&K<=0?"warn":"ok",O=[_?`endpoint ${f?.endpoint||"--"}`:"quota unavailable",`fetched ${M2(f?.fetchedAt)}`,r.hit?`cache ${Nl(r.ageMs)}`:"live quota"];return q("div",{className:"pipeline-minimax-quota-panel","data-testid":"pipeline-minimax-quota-panel"},q("div",{className:"metric-grid compact"},q(L0,{label:"MiniMax",value:_?$:"--",hint:f?.modelComponent||f?.error||"model/minimax-m27",tone:E}),q(L0,{label:"当前窗口",value:`${iF(A)}/${iF(j)}`,hint:`已用 ${gZ(Q)}`,tone:E}),q(L0,{label:"剩余额度",value:iF(F),hint:`剩余 ${gZ(U)}`,tone:E}),q(L0,{label:"重置时间",value:M2(W),hint:G!==void 0?`约 ${Nl(G)}`:Kf(W),tone:E})),q(eF,{items:O}),_?q("p",{className:"muted paragraph"},`MiniMax 限额来自 D601 Pipeline 后端实时查询;当前模型匹配 ${l.modelName||y.modelName||$}。`):q(Au,{error:f?.error||"MiniMax 限额查询失败"}),f?q("div",{className:"panel-actions inline-actions"},q(il,{title:"Pipeline MiniMax Quota",data:f,onOpen:u,testId:"raw-pipeline-minimax-quota"})):null)}function wS({epochs:f,activeRun:u,activePipeline:l,pipelineNodes:y,pipelineEdges:r,runDetails:_,nodeDetails:$,nodeDetailsState:j,ganttScale:A=U6,onGanttScaleChange:F,onRunChange:U,onIntervalSelect:Q,onMarkerSelect:W,selection:G,detailOpen:K,onDetailOpenChange:E,onRaw:O}){let[z,Z]=C0(AS),[N,H]=C0({startY:0,endY:0,startMs:0,endMs:0}),[Y,w]=C0(Date.now()),V=Oy(null),X=String(u?.runId||""),i=Boolean(K),m=(Jf)=>{if(typeof E==="function")E(Jf)},M=Ly(A??U6),c=String(_?.runId||"")===X?_?.details:null,C=c?{...Yf(u)?u:{},...Yf(c)?c:{},runId:X,procedureRuns:Lf(c?.procedureRuns).length>0?c.procedureRuns:u?.procedureRuns}:u,T=XE(C,y,Y),R=c?VS(c,C):{markers:[],arrows:[]},P=Lf(R.markers),n=jS(C,T,P),B=WS(n,M,T,Y),D=String(B.source||"frontend-y"),I=T.map((Jf)=>zS(Jf,B,Y)),p={startMs:Number(B.startMs),endMs:Number(B.endMs),durationMs:Math.max(1,Number(B.durationMs??Number(B.endMs)-Number(B.startMs)))},k=rJ(M),_f={...k,pxPerMinute:Number(B.pxPerMinute??k.pxPerMinute)},S=Math.round(Number(B.chartHeight||360)),e=T.some(_J);l1(()=>{if(!X||!e)return;let Jf=window.setInterval(()=>w(Date.now()),1000);return()=>window.clearInterval(Jf)},[X,e]);let $f=aM(l,y,Array.isArray(r)?r:[]),Qf=y.map((Jf)=>String(Jf?.id||"")).filter(Boolean),Af=I.map((Jf)=>String(Jf.nodeId||"")).filter(Boolean),zf=P.map((Jf)=>String(Jf.nodeId||"")).filter(Boolean),Hf=Array.from(new Set([...$f,...Qf,...Af,...zf])),Zf={startY:0,endY:S,startMs:Number(p.startMs),endMs:Number(p.endMs)},b=Number(N?.endY||0)>0?N:Zf,t=(Jf)=>{return x2(Jf,p,S,B)<=Number(b.endY)&&oF(Jf,p,S,B)>=Number(b.startY)},a=(Jf)=>{let Sf=d0(Jf,p,S,B);return Sf>=Number(b.startY)&&Sf<=Number(b.endY)},Nf=new Set(Hf.filter((Jf)=>I.some((Sf)=>Sf.nodeId===Jf&&t(Sf))||P.some((Sf)=>Sf.nodeId===Jf&&a(Sf)))),o=z?Hf.filter((Jf)=>Nf.has(Jf)):Hf,uf=`${CF}px ${o.length>0?o.map(()=>`${f1}px`).join(" "):"minmax(160px, 1fr)"}`,qf=Lf(B.ticks).filter(Yf),xf=String(G?.mode==="interval"?G?.interval?.procedureRunId||"":""),tf=String(G?.mode==="event"?G?.marker?.id||"":""),df=()=>{let Jf=V.current;if(!Jf){H(Zf);return}let Sf=Math.max(0,Jf.scrollTop-cF),$0=Math.max(120,Jf.clientHeight-cF),nf=Math.min(S,Sf+$0),pu={startY:Sf,endY:nf,startMs:Number(p.startMs),endMs:Number(p.endMs)},du=Math.max(0,Math.min(1,Sf/S)),Iu=Math.max(du,Math.min(1,nf/S)),iu=Math.max(1,Number(p.endMs)-Number(p.startMs));pu.startMs=Number(p.startMs)+iu*du,pu.endMs=Number(p.startMs)+iu*Iu,H(pu)};l1(()=>{let Jf=V.current,Sf=window.setTimeout(df,0);return Jf?.addEventListener("scroll",df),window.addEventListener("resize",df),()=>{window.clearTimeout(Sf),Jf?.removeEventListener("scroll",df),window.removeEventListener("resize",df)}},[X,p.startMs,p.endMs,S]);let lu=Math.max(0,Hf.length-o.length),Ou=new Set(P.filter((Jf)=>o.includes(String(Jf.nodeId||""))&&a(Jf)).map((Jf)=>String(Jf.id))),mu=new Map(P.map((Jf)=>[String(Jf.id),Jf])),R0=Lf(R.arrows).filter((Jf)=>{if(!Ou.has(String(Jf.targetMarkerId||"")))return!1;if(String(Jf.action||"")==="observe")return o.includes(String(Jf.sourceNodeId||""));return Ou.has(String(Jf.sourceMarkerId||""))}),ou=CF+Math.max(1,o.length)*f1,_0=(Jf)=>{let Sf=Ly(Jf.target.value);if(typeof F==="function")F(Sf);window.setTimeout(df,0)},x0=()=>rS({title:`${l?.id||"pipeline"}-${X||"epoch"}-gantt`,meta:[`run ${X||"--"}`,`${Kf(p.startMs)} -> ${Kf(p.endMs)}`,`duration ${Nl(p.durationMs)}`,`${_f.label} / ${hF(_f.pxPerMinute)} px/min`,`${o.length}/${Hf.length} nodes`,`${P.length} markers`],visibleNodeIds:o,intervals:I,markers:P.filter((Jf)=>o.includes(String(Jf.nodeId||""))),arrows:R0,ticks:qf,bounds:p,chartHeight:S,backendLayout:B}),au=Yf(c?.gantt?.diagnostics)?c.gantt.diagnostics:null;return q(u1,{title:"Epoch 甘特图",eyebrow:`${l?.id||"pipeline"} / ${f.length} epochs`,className:"pipeline-wide-panel",loading:_?.loading,actions:q("div",{className:"pipeline-gantt-actions"},q("select",{value:X,disabled:f.length===0,onChange:(Jf)=>U(Jf.target.value),"data-testid":"pipeline-epoch-select"},f.map((Jf)=>q("option",{key:Jf.runId,value:Jf.runId},tF(f,Jf)))),q("label",{className:"pipeline-gantt-toggle"},q("input",{type:"checkbox","data-testid":"pipeline-gantt-auto-hide-idle",checked:z,onChange:(Jf)=>{Z(Boolean(Jf.target.checked)),window.setTimeout(df,0)}}),q("span",null,"自动隐藏空闲列")),q("label",{className:"pipeline-gantt-scale"},q("span",null,q("b",null,"时间尺度"),q("em",{"data-testid":"pipeline-gantt-scale-label"},`${_f.label} · ${hF(_f.pxPerMinute)} px/min`)),q("input",{type:"range",min:0,max:100,step:0.01,value:M,onChange:_0,"aria-label":"调整甘特图时间尺度","data-testid":"pipeline-gantt-time-scale"}),q("small",null,q("span",null,"全局"),q("span",null,"细节"))),u?q("button",{type:"button",className:"ghost-btn",onClick:x0,disabled:o.length===0,"data-testid":"pipeline-export-gantt"},"导出甘特图"):null,u?q(il,{title:`Pipeline Epoch ${u.runId}`,data:u,onOpen:O,testId:"raw-pipeline-epoch-gantt"}):null)},!u?q(e0,{title:"暂无 Epoch",text:"当前 pipeline 还没有完整运行记录。"}):I.length===0?q(e0,{title:"暂无时间区间",text:"等待 D601 Pipeline backend 在 procedure summary 中返回 startedAt / finishedAt。"}):q("div",{className:"pipeline-gantt-wrap"},q("div",{className:`pipeline-gantt-detail-layout ${i?"detail-open":"detail-collapsed"}`,"data-testid":"pipeline-gantt-detail-layout","data-sidebar-open":i?"true":"false"},q("div",{className:"pipeline-gantt-main"},q("div",{className:"pipeline-gantt-main-head"},q("div",{className:"pipeline-gantt-meta"},q("span",null,`time ${Kf(p.startMs)} -> ${Kf(p.endMs)}`),q("span",null,`duration ${Nl(p.durationMs)}`),q("span",null,`scale ${_f.label} / ${hF(_f.pxPerMinute)} px/min`),q("span",null,`layout ${D}`),au?q("span",null,`align ${au.timeAxisAlignmentOk===!1?"check":"ok"}`):null,q("span",null,`visible ${o.length}/${Hf.length} nodes`),c?q("span",null,`markers ${P.length}`):null,z&&lu>0?q("span",null,`hidden idle ${lu}`):null),!i?q("button",{type:"button",className:"pipeline-sidecar-tab right",disabled:!G?.mode,onClick:()=>m(!0),"data-testid":"pipeline-gantt-sidebar-toggle"},G?.mode?"展开详情":"点击甘特图元素展开详情"):null),q("div",{className:"pipeline-gantt-viewport",ref:V,"data-testid":"pipeline-epoch-gantt","data-pipeline-id":l?.id||"","data-run-id":X,"data-layout-source":D,"data-start-ms":String(p.startMs),"data-end-ms":String(p.endMs),"data-chart-height":String(S)},q("div",{className:"pipeline-gantt-board",style:{gridTemplateColumns:uf,minWidth:`${ou}px`}},q("div",{className:"pipeline-gantt-head time"},"Time"),o.length===0?q("div",{className:"pipeline-gantt-head empty"},"当前时间窗无工作节点"):o.map((Jf)=>q("div",{key:`head-${Jf}`,className:"pipeline-gantt-head node",title:Jf,"data-testid":"pipeline-gantt-head-node","data-node-id":Jf},q(MM,{value:Jf}))),q("div",{className:"pipeline-gantt-time-axis",style:{height:`${S}px`}},qf.map((Jf)=>{let Sf=ME(Jf,p,S,B);return q("div",{key:`tick-${Jf.ms}-${Sf}`,className:"pipeline-gantt-tick",style:{top:`${Sf}px`},"data-testid":"pipeline-gantt-tick","data-ms":String(Jf.ms),"data-y":String(Sf)},q("b",null,Kf(Jf.ms)),q("span",null,`+${Nl(Number(Jf.offsetMs??Number(Jf.ms)-Number(p.startMs)))}`))})),o.length>0?q("svg",{className:"pipeline-gantt-arrow-layer",width:o.length*f1,height:S,viewBox:`0 0 ${o.length*f1} ${S}`,style:{left:`${CF}px`,top:`${cF}px`,width:`${o.length*f1}px`,height:`${S}px`},"aria-hidden":"true"},q("defs",null,q("marker",{id:"pipeline-gantt-arrowhead",viewBox:"0 0 10 10",refX:9,refY:5,markerWidth:6,markerHeight:6,orient:"auto-start-reverse"},q("path",{d:"M 0 0 L 10 5 L 0 10 z",fill:"context-stroke"}))),R0.map((Jf)=>{let Sf=mu.get(String(Jf.targetMarkerId||""));if(!Sf)return null;let $0=mu.get(String(Jf.sourceMarkerId||"")),nf=String($0?.nodeId||Jf.sourceNodeId||""),pu=o.indexOf(nf),du=o.indexOf(String(Sf.nodeId||""));if(pu<0||du<0)return null;let Iu=pu*f1+f1/2,iu=du*f1+f1/2,ll=$0?d0($0,p,S,B):d0(Sf,p,S,B),v0=d0(Sf,p,S,B);return q("path",{key:Jf.id,className:`pipeline-gantt-arrow ${String(Jf.sourceKind||"").toLowerCase()} ${String(Jf.status||"").toLowerCase()} ${String(Jf.action||"").toLowerCase()}`,d:SE(Iu,ll,iu,v0),markerEnd:"url(#pipeline-gantt-arrowhead)","data-testid":String(Jf.action||"")==="observe"?"pipeline-gantt-observation-arrow":"pipeline-gantt-arrow","data-source-node-id":String(Jf.sourceNodeId||""),"data-target-node-id":String(Jf.targetNodeId||""),"data-target-marker-id":String(Jf.targetMarkerId||""),"data-action":String(Jf.action||""),"data-source-y":String(ll),"data-target-y":String(v0)})})):null,o.length===0?q("div",{className:"pipeline-gantt-empty-col",style:{height:`${S}px`}},"滚动到有活动的时间段后,相关 node 列会自动出现。"):o.map((Jf)=>{let Sf=I.filter((nf)=>nf.nodeId===Jf),$0=P.filter((nf)=>String(nf.nodeId||"")===Jf);return q("div",{key:`col-${Jf}`,className:"pipeline-gantt-node-col",style:{height:`${S}px`}},Sf.map((nf)=>{let pu=x2(nf,p,S,B),du=oF(nf,p,S,B),Iu=nE(nf,p,S,B),iu=String(nf.procedureRunId||`${Jf}-${nf.startMs}`);return q("button",{key:iu,type:"button",className:`pipeline-gantt-bar ${nf.status} ${nf.live?"live":""} ${xf===iu?"selected":""}`,style:{top:`${pu}px`,height:`${Iu}px`},title:`${Jf} ${nf.status} ${Kf(nf.startedAt||nf.startMs)} -> ${Kf(nf.finishedAt||nf.endMs)}`,onClick:()=>Q(nf),"data-testid":"pipeline-gantt-line","data-node-id":Jf,"data-procedure-run-id":String(nf.procedureRunId||""),"data-status":String(nf.status||""),"data-live":nf.live?"true":"false","data-start-ms":String(nf.startMs||""),"data-end-ms":String(nf.endMs||""),"data-y1":String(pu),"data-y2":String(du),"data-natural-height":String(Math.max(0,du-pu))},q("strong",null,nf.status||"working"),q("span",null,Nl(nf.durationMs)))}),$0.map((nf)=>q("button",{key:nf.id,type:"button",className:`pipeline-gantt-marker ${nf.kind} ${nf.tone||""} ${nf.status||""} ${tf===String(nf.id)?"selected":""}`,style:{top:`${d0(nf,p,S,B)}px`},title:`${nf.label||"event"} / ${Kf(nf.timestampIso||nf.timestamp||nf.ms)}`,onClick:()=>W(nf),"data-testid":nf.kind==="prompt"?"pipeline-gantt-prompt-marker":"pipeline-gantt-control-marker","data-marker-id":String(nf.id||""),"data-ms":String(nf.ms??nf.eventMs??""),"data-y":String(d0(nf,p,S,B))})))})))),i?q(nM,{selection:G,runDetails:_,nodeDetails:$,nodeDetailsState:j,onRaw:O,onCollapse:()=>m(!1)}):null)))}function X1(){return{loading:!1,actionLoading:"",error:"",message:"",details:null,fetchedAt:null,appendPrompt:"",guidePrompt:"",modifyPrompt:"",approveReason:"",redoReason:""}}function Hy(){return{mode:"",runId:"",interval:null,marker:null}}function mF(){return{runId:"",loading:!1,error:"",details:null,fetchedAt:null}}function y6(f,u){return`${f}/microservices/pipeline/proxy${u}`}function DS({activeRun:f,pipelineRuns:u,selectedRunId:l,onRunChange:y,selectedNodeId:r,selectedNodeConfig:_,selectedNodeRuntime:$,control:j,onControlChange:A,onFetch:F,onAction:U,onRaw:Q,onCollapse:W}){let G=String(f?.runId||""),K=String($?.status||"pending"),E=!G||!r||j.loading||Boolean(j.actionLoading),O=(Z)=>(N)=>A({[Z]:N.target.value,error:"",message:""}),z=u.length>0?u:f?[f]:[];return q("aside",{className:"pipeline-node-control","data-testid":"pipeline-node-control"},q("div",{className:"pipeline-node-control-head"},q("div",null,q("p",{className:"panel-eyebrow"},"Manual Node Control"),q(_u,{title:r||"点击控制图中的 node",level:3,loading:j.loading||Boolean(j.actionLoading)})),q("div",{className:"pipeline-node-control-head-actions"},r?q(Vy,{status:K},K):q(Vy,{status:"pending"},"idle"),q("button",{type:"button",className:"ghost-btn mini",onClick:W,"data-testid":"pipeline-node-sidebar-collapse"},"收起"))),q("div",{className:"pipeline-control-runbar"},q("label",null,q("span",null,"目标 run"),q("select",{value:G||l,disabled:z.length===0,onChange:(Z)=>y(Z.target.value),"data-testid":"pipeline-node-run-select"},z.map((Z)=>q("option",{key:Z.runId,value:Z.runId},`${Z.runId||"--"} / ${Z.status||"--"}`)))),q("button",{type:"button",className:"ghost-btn",disabled:E,onClick:F,"data-testid":"pipeline-node-fetch"},j.loading?"抓取中":"抓取过程"),j.details?q(il,{title:`Pipeline Node ${r}`,data:j.details,onOpen:Q,testId:"raw-pipeline-node-control"}):null),q("div",{className:"pipeline-control-meta"},q("span",null,q("b",null,"kind"),String(_?.kind||"--")),q("span",null,q("b",null,"procedure"),String($?.currentProcedureRunId||"--")),q("span",null,q("b",null,"attempts"),String($?.attempts??"--")),q("span",null,q("b",null,"updated"),Kf(f?.updatedAt))),!r?q(e0,{title:"未选择 node",text:"点击 React Flow 控制图中的任意 node 后,可抓取执行过程、追加 prompt、下发引导、增量修改、审核通过或重做。"}):null,q(Au,{error:j.error,wide:!0}),j.message?q("div",{className:"form-success wide"},j.message):null,q("div",{className:"pipeline-control-actions"},q("label",null,q("span",null,"实时追加 prompt(仅 running node)"),q("textarea",{value:j.appendPrompt,onChange:O("appendPrompt"),placeholder:"让当前执行中的 agent 继续、补充检查或调整当前步骤...",rows:4,disabled:!r,"data-testid":"pipeline-node-append-input"}),q("button",{type:"button",className:"primary-btn compact",disabled:E||!String(j.appendPrompt||"").trim(),onClick:()=>U("append"),"data-testid":"pipeline-node-append-button"},j.actionLoading==="append"?"追加中":"追加到运行中 node")),q("label",null,q("span",null,"下次尝试引导 prompt"),q("textarea",{value:j.guidePrompt,onChange:O("guidePrompt"),placeholder:"给该 node 下一次 attempt 的执行提示;不会立即打断当前 session。",rows:4,disabled:!r,"data-testid":"pipeline-node-guide-input"}),q("button",{type:"button",className:"ghost-btn compact",disabled:E||!String(j.guidePrompt||"").trim(),onClick:()=>U("guide"),"data-testid":"pipeline-node-guide-button"},j.actionLoading==="guide"?"下发中":"下发 guide")),q("label",null,q("span",null,"完成后增量修改 prompt"),q("textarea",{value:j.modifyPrompt,onChange:O("modifyPrompt"),placeholder:"在该 node 已完成结果基础上追加修改要求;runner 会重跑目标 node,并保留同 node 既有 OA 输出作为上下文。",rows:4,disabled:!r,"data-testid":"pipeline-node-modify-input"}),q("button",{type:"button",className:"ghost-btn compact",disabled:E||!String(j.modifyPrompt||"").trim(),onClick:()=>U("modify"),"data-testid":"pipeline-node-modify-button"},j.actionLoading==="modify"?"排队中":"增量修改 node")),q("label",null,q("span",null,"Monitor 审核通过原因"),q("textarea",{value:j.approveReason,onChange:O("approveReason"),placeholder:"当流程配置开启 monitor 审核时,记录审核通过原因并释放后续 node。",rows:3,disabled:!r,"data-testid":"pipeline-node-approve-input"}),q("button",{type:"button",className:"primary-btn compact",disabled:E||!String(j.approveReason||"").trim(),onClick:()=>U("approve"),"data-testid":"pipeline-node-approve-button"},j.actionLoading==="approve"?"提交中":"审核通过")),q("label",null,q("span",null,"重做 / restart 原因"),q("textarea",{value:j.redoReason,onChange:O("redoReason"),placeholder:"说明为什么需要重做;runner 会重置目标 node 以及非 rework 下游 node。",rows:4,disabled:!r,"data-testid":"pipeline-node-redo-input"}),q("button",{type:"button",className:"danger-btn compact",disabled:E||!String(j.redoReason||"").trim(),onClick:()=>U("redo"),"data-testid":"pipeline-node-redo-button"},j.actionLoading==="redo"?"排队中":"重做 node"))),q("div",{className:"pipeline-control-evidence"},q("strong",null,"Node 过程索引"),q(LS,{details:j.details,selectedNodeId:r,selectedNodeRuntime:$,control:j,onRaw:Q})))}function PE({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let y=f.find((d)=>d.id==="pipeline")||null,[r,_]=C0({loading:!1,error:"",health:null,snapshot:null,oaDiagnostics:null,minimaxQuota:null,refreshedAt:null}),[$,j]=C0(""),[A,F]=C0(""),[U,Q]=C0(""),[W,G]=C0(X1()),[K,E]=C0({}),[O,z]=C0(Hy()),[Z,N]=C0(mF()),[H,Y]=C0(U6),[w,V]=C0(!1),[X,i]=C0(!1),m=Oy(0),M=Oy(!1),c=Oy(0),C=Oy(""),T=Oy({}),R=Oy(""),P=Oy("");async function n(d={}){let Bf=d.silent===!0;if(!y)return;if(M.current)return;M.current=!0;let Mf=m.current+1;if(m.current=Mf,!Bf)_((Pf)=>({...Pf,loading:!0,error:""}));try{let Pf=`__unideskArrayLimit=registry.components:80,runs:${zM}`,[ju,gf,Ju]=await Promise.all([Ey(`${l}/microservices/pipeline/proxy/api/snapshot?${Pf}`,{cache:"no-store"}),Ey(`${l}/microservices/pipeline/proxy/api/oa-event-flow/diagnostics`,{cache:"no-store"}).catch((El)=>({ok:!1,error:wf(El,"OA event flow diagnostics failed")})),Ey(`${l}/microservices/pipeline/proxy/api/model-quota/minimax`,{cache:"no-store"}).catch((El)=>({ok:!1,error:wf(El,"MiniMax quota failed")}))]);if(Mf!==m.current)return;let eu={ok:ju?.ok!==!1,service:"pipeline-v2-control snapshot"};_({loading:!1,error:"",health:eu,snapshot:ju,oaDiagnostics:gf,minimaxQuota:Ju,refreshedAt:new Date})}catch(Pf){if(Mf!==m.current)return;_((ju)=>({...ju,loading:!1,error:wf(Pf,"Pipeline 加载失败")}))}finally{M.current=!1}}l1(()=>{if(n(),!y)return;let d=()=>{if(Y2())n({silent:!0})},Bf=window.setInterval(()=>{d()},mZ),Mf=()=>{if(Y2())d()};return document.addEventListener("visibilitychange",Mf),()=>{window.clearInterval(Bf),document.removeEventListener("visibilitychange",Mf)}},[y?.id,y?.runtime?.providerStatus,l]);let B=SM(y),D=CM(y),I=PM(y),p=r.snapshot||{},k=r.oaDiagnostics||null,_f=r.minimaxQuota||null,{components:S,pipelines:e,runs:$f}=cM(p),Qf=String($f[0]?.pipelineId||""),Af=(Qf?e.find((d)=>String(d.id||"")===Qf):null)||e[0]||{},zf=e.find((d)=>String(d.id||"")===$)||Af,Hf=String(zf.id||""),Zf=OE(zf),b=uJ(zf),t=_E($f,Hf),a=$S($f,Hf),Nf=a.find((d)=>String(d?.runId||"")===A)||t,o=String(Z.runId||"")===String(Nf?.runId||"")?vM(Z.details):null,uf=bM(Nf,o),qf=String(uf?.runId||""),xf=Zf.find((d)=>String(d?.id||"")===U)||null,tf=U?qE(uf,U):null,df=RM($f),lu=IM(S),Ou=Number(r.health?.components)||dZ(p,"registry.components",S.length),mu=dZ(p,"runs",$f.length),R0=uE(zf,uf,S),ou={nodes:R0.nodes.map((d)=>d.id===U?{...d,selected:!0,className:`${d.className||""} selected-control-node`}:d),edges:R0.edges},_0=e.map((d)=>{let Bf=String(d.id||"pipeline"),Mf=_E($f,Bf);return{title:`${Bf}-${Mf?.runId||"snapshot"}`,flow:uE(d,Mf,S)}}),x0=String(O?.runId||qf||""),au=String(O?.interval?.nodeId||O?.marker?.nodeId||""),Jf=x0&&au?K[bF(x0,au)]||null:null,Sf=P2(W.details,x0,au),$0=P2(Jf?.details,x0,au)||Sf,nf=x0&&au?{...Yf(Jf)?Jf:{},runId:x0,nodeId:au,details:$0,loading:Boolean(Jf?.loading)||!$0&&Boolean(W.loading)&&U===au,error:String(Jf?.error||""),fetchedAt:Jf?.fetchedAt||(Sf?W.fetchedAt:null)}:null,pu=a.map((d)=>String(d?.runId||"")).filter(Boolean).join("|"),du=Zf.map((d)=>String(d?.id||"")).filter(Boolean).join("|");l1(()=>{R.current=U},[U]),l1(()=>{P.current=qf},[qf]),l1(()=>{if(!A||pu.split("|").includes(A))return;F("")},[A,pu]),l1(()=>{if(!U||du.split("|").includes(U))return;Q(""),G(X1()),z(Hy()),V(!1),i(!1)},[U,du]),l1(()=>{if(!U)V(!1)},[U]),l1(()=>{if(!O.mode)i(!1)},[O.mode]);async function Iu(d=qf,Bf={}){if(!d){N(mF());return}let Mf=Ly(Bf.scale??H??U6),Pf=`${d}:timeline`;if(C.current===Pf)return;C.current=Pf;let ju=Bf.silent===!0,gf=c.current+1;c.current=gf,N((Ju)=>({runId:d,scale:Mf,loading:!ju||String(Ju.runId||"")!==d||!Ju.details,error:"",details:ju&&Ju.runId===d?Ju.details:Ju.runId===d?Ju.details:null,fetchedAt:Ju.runId===d?Ju.fetchedAt:null}));try{let[Ju,eu]=await Promise.all([Ey(y6(l,`/api/node-control/runs/${encodeURIComponent(d)}?tail=160&view=timeline`),{cache:"no-store",strictJson:!0}),Ey(y6(l,`/api/runs/${encodeURIComponent(d)}`),{cache:"no-store"}).catch((El)=>({ok:!1,runSummaryError:wf(El,"抓取评分失败")}))]);if(gf!==c.current)return;N({runId:d,scale:Mf,loading:!1,error:"",details:{...Ju,run:Yf(eu?.run)?eu.run:void 0,runSummaryError:eu?.runSummaryError},fetchedAt:new Date})}catch(Ju){if(gf!==c.current)return;N((eu)=>({runId:d,scale:Mf,loading:!1,error:wf(Ju,"抓取 epoch 执行过程失败"),details:eu.runId===d?eu.details:null,fetchedAt:eu.runId===d?eu.fetchedAt:null}))}finally{if(C.current===Pf)C.current=""}}function iu(d,Bf,Mf){let Pf=bF(d,Bf);E((ju)=>{let gf={...ju,[Pf]:{...Yf(ju?.[Pf])?ju[Pf]:{},runId:d,nodeId:Bf,...Mf}},Ju=Object.keys(gf);if(Ju.length>32)for(let eu of Ju.slice(0,Ju.length-32))delete gf[eu];return gf})}async function ll(d,Bf){if(!d||!Bf)return;let Mf=bF(d,Bf),Pf=Number(T.current?.[Mf]||0)+1;T.current={...T.current,[Mf]:Pf},iu(d,Bf,{loading:!0,error:""});try{let ju=await Ey(y6(l,`/api/node-control/runs/${encodeURIComponent(d)}/nodes/${encodeURIComponent(Bf)}?tail=160`),{cache:"no-store",strictJson:!0});if(Number(T.current?.[Mf]||0)!==Pf)return;let gf=new Date;if(iu(d,Bf,{loading:!1,details:ju,fetchedAt:gf,error:""}),R.current===Bf&&P.current===d)G((Ju)=>({...Ju,loading:!1,details:ju,fetchedAt:gf,error:""}))}catch(ju){if(Number(T.current?.[Mf]||0)!==Pf)return;iu(d,Bf,{loading:!1,error:wf(ju,"抓取 Gantt node 详情失败")})}}l1(()=>{if(!qf){N(mF());return}Iu(qf);let d=()=>{if(Y2())Iu(qf,{silent:!0})},Bf=window.setInterval(()=>{d()},mZ),Mf=()=>{if(Y2())d()};return document.addEventListener("visibilitychange",Mf),()=>{window.clearInterval(Bf),document.removeEventListener("visibilitychange",Mf)}},[qf,l]);async function v0(d=qf,Bf=U){if(!d||!Bf){G((Mf)=>({...Mf,error:"请先选择 run 和 node",message:""}));return}G((Mf)=>({...Mf,loading:!0,error:"",message:""}));try{let Mf=await Ey(y6(l,`/api/node-control/runs/${encodeURIComponent(d)}/nodes/${encodeURIComponent(Bf)}?tail=160`),{cache:"no-store",strictJson:!0}),Pf=new Date;G((ju)=>({...ju,loading:!1,details:Mf,fetchedAt:Pf,error:""})),iu(d,Bf,{loading:!1,details:Mf,fetchedAt:Pf,error:""})}catch(Mf){G((Pf)=>({...Pf,loading:!1,error:wf(Mf,"抓取 node 执行过程失败")}))}}async function a_(d){let Bf=String(d?.runId||qf||""),Mf=String(d?.nodeId||"");if(z({mode:"interval",runId:Bf,interval:d,marker:null}),i(!0),!Bf||!Mf)return;if(Bf!==qf)F(Bf);Q(Mf),G(X1()),Iu(Bf,{silent:!0}),ll(Bf,Mf)}async function wy(d){let Bf=String(d?.runId||qf||""),Mf=String(d?.nodeId||"");if(z({mode:"event",runId:Bf,interval:null,marker:d}),i(!0),!Bf)return;if(Bf!==qf)F(Bf);if(Iu(Bf,{silent:!0}),!Mf)return;Q(Mf),G(X1()),ll(Bf,Mf)}async function Dy(d){if(!qf||!U){G((Pf)=>({...Pf,error:"请先选择 run 和 node",message:""}));return}let Bf=d==="append"?"prompts":d,Mf=d==="append"?W.appendPrompt:d==="guide"?W.guidePrompt:d==="modify"?W.modifyPrompt:d==="approve"?W.approveReason:W.redoReason;if(!String(Mf||"").trim()){G((Pf)=>({...Pf,error:"操作内容不能为空",message:""}));return}G((Pf)=>({...Pf,actionLoading:d,error:"",message:""}));try{let Pf=d==="redo"||d==="approve"?{reason:Mf,source:"unidesk-frontend",sourceKind:"webui"}:{prompt:Mf,source:"unidesk-frontend",sourceKind:"webui"},ju=await Ey(y6(l,`/api/node-control/runs/${encodeURIComponent(qf)}/nodes/${encodeURIComponent(U)}/${Bf}`),{method:"POST",body:JSON.stringify(Pf)});if(G((gf)=>({...gf,actionLoading:"",details:ju,fetchedAt:new Date,appendPrompt:d==="append"?"":gf.appendPrompt,guidePrompt:d==="guide"?"":gf.guidePrompt,modifyPrompt:d==="modify"?"":gf.modifyPrompt,approveReason:d==="approve"?"":gf.approveReason,redoReason:d==="redo"?"":gf.redoReason,message:d==="append"?"已追加到运行中 node":d==="guide"?"已下发 guide,等待 runner 处理":d==="modify"?"已排队增量修改命令":d==="approve"?"已提交审核通过决策":"已排队重做命令"})),await v0(qf,U),await Iu(qf,{silent:!0}),d!=="append")await n()}catch(Pf){G((ju)=>({...ju,actionLoading:"",error:wf(Pf,"node 控制操作失败")}))}}if(!y)return q(e0,{title:"Pipeline 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=pipeline"});return q("div",{className:"pipeline-page","data-testid":"pipeline-page"},q(u1,{title:"Pipeline v2 工作台",eyebrow:"D601 Snapshot 用户服务",loading:r.loading,actions:q("div",{className:"panel-actions"},q("button",{type:"button",className:"ghost-btn",onClick:n,disabled:r.loading,"data-testid":"pipeline-refresh-button"},r.loading?"刷新中":"刷新"),q(il,{title:"Pipeline 用户服务",data:y,onOpen:u,testId:"raw-pipeline-service"}))},q("div",{className:"pipeline-hero"},q("div",null,q("div",{className:"node-version-line"},q(Vy,{status:B.providerStatus==="online"?"online":"warn"},B.providerStatus||"unknown"),q("span",null,y.providerId),q("span",null,I.public?"公网暴露":"仅 UniDesk frontend 代理访问")),q("p",{className:"muted paragraph"},y.description)),q("div",{className:"microservice-ref-card"},q("span",null,"Repo"),q("strong",null,D.url||"--"),q("code",null,D.commitId||"--")),q("div",{className:"microservice-ref-card"},q("span",null,"D601 Docker"),q("strong",null,`${I.nodeBindHost||"--"}:${I.nodePort||"--"}`),q("code",null,`${D.composeFile||"--"} / ${D.composeService||"--"}`))),q(Au,{error:r.error,wide:!0})),q("div",{className:"pipeline-grid"},q(u1,{title:"控制图",eyebrow:`${zf.id||"pipeline"} / run ${uf?.status||"--"}`,className:"pipeline-wide-panel",loading:r.loading,actions:q("div",{className:"pipeline-toolbar"},q("select",{value:Hf,disabled:e.length===0,onChange:(d)=>{j(d.target.value),F(""),Q(""),G(X1()),z(Hy()),V(!1),i(!1)},"data-testid":"pipeline-select"},e.map((d)=>q("option",{key:d.id,value:d.id},d.id||d.key))),q("select",{value:qf,disabled:a.length===0,onChange:(d)=>{if(F(d.target.value),G(X1()),z(Hy()),V(!1),i(!1),U)v0(d.target.value,U)},"data-testid":"pipeline-run-select"},a.map((d)=>q("option",{key:d.runId,value:d.runId},tF(a,d)))),q("button",{type:"button",className:"ghost-btn",disabled:ou.nodes.length===0,onClick:()=>BE(ou,`${zf.id||"pipeline"}-${uf?.runId||"snapshot"}`),"data-testid":"pipeline-export-graph"},"导出渲染图"),q("button",{type:"button",className:"ghost-btn",disabled:_0.every((d)=>d.flow.nodes.length===0),onClick:()=>_S(_0),"data-testid":"pipeline-export-all-graphs"},"批量导出"))},Zf.length===0?q(e0,{title:"暂无控制图",text:"等待 D601 pipeline backend 返回 config.nodes / config.edges"}):q("div",{className:`pipeline-control-shell ${w?"detail-open":"detail-collapsed"}`,"data-testid":"pipeline-control-shell","data-sidebar-open":w?"true":"false"},q("div",{className:"pipeline-flow-frame","data-testid":"pipeline-react-flow"},q(cZ,{nodes:ou.nodes,edges:ou.edges,nodeTypes:EM,edgeTypes:ZM,fitView:!0,fitViewOptions:{padding:0.18},nodesDraggable:!1,nodesConnectable:!1,elementsSelectable:!0,minZoom:0.25,maxZoom:1.4,proOptions:{hideAttribution:!0},onNodeClick:(d,Bf)=>{let Mf=String(Bf.id);if(Q(Mf),G(X1()),V(!0),qf)v0(qf,Mf)}},q(RZ,{gap:22,size:1,color:"rgba(215, 161, 58, 0.24)"}),q(vZ,{showInteractive:!1})),!w?q("button",{type:"button",className:"pipeline-sidecar-tab right",disabled:!U,onClick:()=>V(!0),"data-testid":"pipeline-node-sidebar-toggle"},U?"展开 node 控制":"点击 node 展开控制"):null),w?q(DS,{activeRun:uf,pipelineRuns:a,selectedRunId:A,onRunChange:(d)=>{if(F(d),G(X1()),z(Hy()),U)v0(d,U)},selectedNodeId:U,selectedNodeConfig:xf,selectedNodeRuntime:tf,control:W,onControlChange:(d)=>G((Bf)=>({...Bf,...d})),onFetch:()=>v0(),onAction:Dy,onRaw:u,onCollapse:()=>V(!1)}):null),q("div",{className:"pipeline-flow-summary"},q("span",null,`${ou.nodes.length} nodes`),q("span",null,`${ou.edges.length} edges`),q("span",null,`${e.length} pipelines`),q("span",null,`source config+components(${S.length})`),q("span",null,`run ${uf?.runId||"--"}`),q("span",null,`score ${kF(uf)}`),q("span",null,U?`selected ${U}`:"click node to control"))),q(wS,{epochs:a,activeRun:uf,activePipeline:zf,pipelineNodes:Zf,pipelineEdges:b,selection:O,detailOpen:X,onDetailOpenChange:i,runDetails:Z,nodeDetails:$0,nodeDetailsState:nf,ganttScale:H,onGanttScaleChange:Y,onIntervalSelect:a_,onMarkerSelect:wy,onRunChange:(d)=>{if(F(d),G(X1()),z(Hy()),i(!1),U)v0(d,U)},onRaw:u}),q(u1,{title:"观测指标",eyebrow:r.refreshedAt?`Updated ${Uu(r.refreshedAt)}`:"Snapshot",loading:r.loading},q("div",{className:"metric-grid"},q(L0,{label:"Health",value:r.health?.ok?"OK":"--",hint:r.health?.service||"D601 /health",tone:r.health?.ok?"ok":"warn"}),q(L0,{label:"组件",value:Ou,hint:"components registry",tone:p?.registry?.ok===!1?"warn":"ok"}),q(L0,{label:"Pipeline",value:e.length,hint:`${Zf.length} nodes / ${b.length} edges`}),q(L0,{label:"运行记录",value:mu,hint:`${df.succeeded||0} succeeded / ${df.running||0} running`}),q(L0,{label:"OA 记录",value:Array.isArray(t?.submissions)?t.submissions.length:0,hint:t?.runId||"latest run"}),q(L0,{label:"Procedure",value:Array.isArray(t?.procedureRuns)?t.procedureRuns.length:0,hint:t?.status||"no run"}),q(L0,{label:"Score",value:kF(uf),hint:uf?.runId||"selected epoch",tone:yJ(uf)})),q("div",{className:"panel-actions inline-actions"},q(il,{title:"Pipeline Snapshot",data:p,onOpen:u,testId:"raw-pipeline-snapshot"}))),q(u1,{title:"评分器",eyebrow:uf?.runId||"selected epoch",loading:r.loading},q(pM,{run:uf,onRaw:u})),q(u1,{title:"MiniMax 限额",eyebrow:"model/minimax-m27 quota",loading:r.loading},q(YS,{quota:_f,onRaw:u})),q(u1,{title:"OA 事件流",eyebrow:"100% event-driven diagnostics",className:"pipeline-wide-panel",loading:r.loading},q(XS,{diagnostics:k,onRaw:u})),q(u1,{title:"组件矩阵",eyebrow:`${lu.length} classes`,loading:r.loading},lu.length===0?q(e0,{title:"暂无组件",text:"等待 D601 pipeline backend 返回 registry.components"}):q("div",{className:"component-strata"},lu.map((d)=>q("article",{key:d.name,className:"component-stratum"},q("span",null,d.name),q("strong",null,d.count)))),q("div",{className:"pipeline-component-list"},S.slice(0,12).map((d)=>q("span",{key:d.key,className:"data-chip"},q("b",null,d.componentClass||"--"),q("span",null,d.id||d.key||"--"))))),q(u1,{title:"Epoch 列表",eyebrow:`${a.length}/${mu} preview`,loading:r.loading},a.length===0?q(e0,{title:"暂无运行记录",text:"当前 pipeline 在 .state/pipeline-runs 中还没有 epoch。"}):q("div",{className:"pipeline-run-list"},a.map((d)=>{let Bf=String(d?.runId||"")===qf?uf:d;return q("article",{key:d.runId,className:`pipeline-run-card ${String(d.runId||"")===qf?"active":""}`,role:"button",tabIndex:0,onClick:()=>{F(String(d.runId||"")),z(Hy())},onKeyDown:(Mf)=>{if(Mf.key==="Enter"||Mf.key===" ")F(String(d.runId||"")),z(Hy())}},q("div",{className:"node-card-head"},q("strong",null,tF(a,d)),q(Vy,{status:d.status},d.status||"--")),q("div",{className:"docker-meta compact"},q("span",null,Bf?.pipelineId||"--"),q("span",null,`nodes ${Array.isArray(Bf?.nodes)?Bf.nodes.length:0}`),q("span",null,`oa ${Array.isArray(Bf?.submissions)?Bf.submissions.length:0}`),q("span",null,`procedures ${Array.isArray(Bf?.procedureRuns)?Bf.procedureRuns.length:0}`),q(mM,{run:Bf})),q("p",{className:"muted paragraph"},S2(Bf?.task)),q("span",{className:"pipeline-run-time"},Kf(Bf?.updatedAt)))}))),q(u1,{title:"运行材料索引",eyebrow:uf?.runId||"selected epoch",className:"pipeline-wide-panel",loading:r.loading},q(BS,{activeRun:uf,onRaw:u}))))}var I2=cf(Yu(),1);var yf=I2.default.createElement,{useEffect:TS}=I2.default,m2=I2.default.useState,jJ={id:"",sequenceNo:"",contractNo:"",name:"",currentStatus:"",pending:"",paymentStatus:"",notes:""};function nS({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return yf("span",{className:`status-badge ${l}`},u||f||"unknown")}function p2({label:f,value:u,hint:l,tone:y}){return yf("article",{className:`metric-card ${y||""}`},yf("div",{className:"metric-label"},f),yf("div",{className:"metric-value"},u),yf("div",{className:"metric-hint"},l))}function AJ({title:f,eyebrow:u,actions:l,children:y,className:r,loading:_}){return yf("section",{className:`panel ${r||""}`},yf("div",{className:"panel-head"},yf("div",null,u?yf("p",{className:"panel-eyebrow"},u):null,yf(_u,{title:f,loading:_})),l?yf("div",{className:"panel-actions"},l):null),yf("div",{className:"panel-body"},y))}function CE({title:f,data:u,onOpen:l,testId:y}){return yf("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:()=>l(f,u)},"查看原始JSON")}function cE({title:f,text:u}){return yf("div",{className:"empty-state"},yf("strong",null,f),yf("span",null,u))}function MS(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function SS(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function PS(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function I_(f,u){return`${f}/microservices/project-manager/proxy${u}`}function CS(f){return{id:String(f.id||""),sequenceNo:f.sequenceNo===null||f.sequenceNo===void 0?"":String(f.sequenceNo),contractNo:String(f.contractNo||""),name:String(f.name||""),currentStatus:String(f.currentStatus||""),pending:String(f.pending||""),paymentStatus:String(f.paymentStatus||""),notes:String(f.notes||"")}}function cS(f){return{sequenceNo:f.sequenceNo===""?null:Number(f.sequenceNo),contractNo:String(f.contractNo||"").trim(),name:String(f.name||"").trim(),currentStatus:String(f.currentStatus||"").trim(),pending:String(f.pending||"").trim(),paymentStatus:String(f.paymentStatus||"").trim(),paymentRatio:String(f.paymentStatus||"").trim(),notes:String(f.notes||"").trim()}}function FJ(f){return String(f||"item").replace(/[^A-Za-z0-9_-]+/g,"-")}function iS(f){let u=new Uint8Array(f),l="",y=32768;for(let r=0;ryf("tr",{key:r.id,className:u===r.id?"active-row":"","data-testid":`project-manager-row-${FJ(r.id)}`},yf("td",null,r.sequenceNo??"--"),yf("td",null,yf("strong",null,r.contractNo||"--"),yf("code",null,r.id||"--")),yf("td",null,yf("strong",null,r.name||"--"),yf("span",{className:"muted block"},r.sourceFile||"--")),yf("td",null,r.currentStatus||"--"),yf("td",null,yf("span",{className:"preline"},r.pending||"--")),yf("td",null,yf(nS,{status:Number(r.paymentRatio||0)>=1?"online":"warn"},r.paymentStatus||"--")),yf("td",null,r.notes||"--"),yf("td",null,yf("div",{className:"inline-actions"},yf("button",{type:"button",className:"ghost-btn",onClick:()=>l(r),"data-testid":`project-manager-edit-${FJ(r.id)}`},"编辑"),yf(CE,{title:`Project ${r.contractNo||r.id}`,data:r,onOpen:y,testId:`raw-project-${FJ(r.id)}`}))))))))}function iE({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let y=f.find((V)=>V.id==="project-manager")||null,[r,_]=m2({loading:!1,saving:!1,importing:!1,exporting:!1,error:"",notice:"",health:null,list:null,refreshedAt:null}),[$,j]=m2({...jJ}),[A,F]=m2(""),[U,Q]=m2("all");async function W(V=A,X=U){if(!y)return;_((i)=>({...i,loading:!0,error:""}));try{let i=new URLSearchParams({pageSize:"200",status:X});if(V.trim())i.set("q",V.trim());let[m,M]=await Promise.all([Df(`${l}/microservices/project-manager/health`),Df(I_(l,`/api/projects?${i.toString()}`))]);_((c)=>({...c,loading:!1,health:m,list:M,refreshedAt:new Date,error:""}))}catch(i){_((m)=>({...m,loading:!1,error:wf(i,"Project Manager 加载失败")}))}}TS(()=>{W()},[y?.id,y?.runtime?.providerStatus]);async function G(V){V.preventDefault(),_((X)=>({...X,saving:!0,error:"",notice:""}));try{let X=cS($);if($.id)await Df(I_(l,`/api/projects/${encodeURIComponent($.id)}`),{method:"PUT",body:JSON.stringify(X)});else await Df(I_(l,"/api/projects"),{method:"POST",body:JSON.stringify(X)});_((i)=>({...i,saving:!1,notice:$.id?"项目已更新":"项目已创建"})),await W()}catch(X){_((i)=>({...i,saving:!1,error:wf(X,"保存项目失败")}))}}async function K(){if(!$.id)return;if(!window.confirm(`删除项目 ${$.contractNo||$.name||$.id} ?`))return;_((V)=>({...V,saving:!0,error:"",notice:""}));try{await Df(I_(l,`/api/projects/${encodeURIComponent($.id)}`),{method:"DELETE"}),j({...jJ}),_((V)=>({...V,saving:!1,notice:"项目已删除"})),await W()}catch(V){_((X)=>({...X,saving:!1,error:wf(V,"删除项目失败")}))}}async function E(V){let X=V.target.files?.[0];if(!X)return;_((i)=>({...i,importing:!0,error:"",notice:""}));try{let i=iS(await X.arrayBuffer()),m=await Df(I_(l,"/api/import/excel"),{method:"POST",body:JSON.stringify({fileName:X.name,contentBase64:i,replace:!1})});_((M)=>({...M,importing:!1,notice:`Excel 已导入 ${m.imported||0} 条项目`})),V.target.value="",await W()}catch(i){_((m)=>({...m,importing:!1,error:wf(i,"Excel 导入失败")}))}}async function O(){_((V)=>({...V,exporting:!0,error:""}));try{let V=await Kz(I_(l,"/api/projects/export.xlsx")),X=URL.createObjectURL(V),i=document.createElement("a");i.href=X,i.download=`project-manager-${tJ()}.xlsx`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(X),_((m)=>({...m,exporting:!1,notice:"Excel 已导出"}))}catch(V){_((X)=>({...X,exporting:!1,error:wf(V,"Excel 导出失败")}))}}if(!y)return yf(cE,{title:"Project Manager 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=project-manager"});let z=MS(y),Z=PS(y),N=SS(y),H=Array.isArray(r.list?.projects)?r.list.projects:[],Y=r.list?.summary||{},w=r.health||{};return yf("div",{className:"project-manager-page","data-testid":"project-manager-page"},yf(AJ,{title:"项目管理工作台",eyebrow:"Main Server PostgreSQL 用户服务",loading:r.loading||r.exporting,actions:yf("div",{className:"panel-actions"},yf("button",{type:"button",className:"ghost-btn",disabled:r.loading,onClick:()=>W(),"data-testid":"project-manager-refresh-button"},r.loading?"刷新中":"刷新"),yf("button",{type:"button",className:"ghost-btn",disabled:r.exporting,onClick:O,"data-testid":"project-manager-export-button"},r.exporting?"导出中":"导出 Excel"),yf(CE,{title:"Project Manager 用户服务",data:y,onOpen:u,testId:"raw-project-manager-service"}))},yf("div",{className:"project-manager-hero"},yf(p2,{label:"项目总数",value:Y.total??H.length,hint:`PG 表 ${w.storage?.table||"project_manager_projects"}`,tone:"ok"}),yf(p2,{label:"进行中",value:Y.active??"--",hint:"当前状态未完全完成"}),yf(p2,{label:"已完成",value:Y.completed??"--",hint:"按 完成 关键字统计",tone:"ok"}),yf(p2,{label:"未全款",value:Y.unpaid??"--",hint:"付款比例 < 1",tone:Number(Y.unpaid||0)>0?"warn":"ok"})),yf(Au,{error:r.error}),r.notice?yf("div",{className:"form-success"},r.notice):null),yf("div",{className:"project-manager-hero"},yf("div",{className:"microservice-ref-card"},yf("span",null,"Repo"),yf("strong",null,Z.url||"--"),yf("code",null,Z.commitId||"--")),yf("div",{className:"microservice-ref-card"},yf("span",null,"Main Server Docker"),yf("strong",null,`${N.nodeBindHost||"--"}:${N.nodePort||"--"}`),yf("code",null,`${Z.composeService||"--"} / ${Z.containerName||"--"}`)),yf("div",{className:"microservice-ref-card"},yf("span",null,"Runtime"),yf("strong",null,z.providerName||y.providerId),yf("code",null,`Health ${w.ok?"OK":"--"} / ${r.refreshedAt?Uu(r.refreshedAt):"--"}`)),yf("div",{className:"microservice-ref-card"},yf("span",null,"Import Source"),yf("strong",null,"D601 WeChat Excel"),yf("code",null,"合作项目列表_I_20260309.xlsx"))),yf("div",{className:"project-manager-layout"},yf(AJ,{title:"项目清单",eyebrow:"CRUD + Excel Export",loading:r.loading||r.importing||r.exporting,actions:yf("div",{className:"inline-actions project-manager-filters"},yf("input",{value:A,onChange:(V)=>F(V.target.value),placeholder:"搜索合同号 / 项目名称 / 状态","data-testid":"project-manager-search"}),yf("select",{value:U,onChange:(V)=>{Q(V.target.value),W(A,V.target.value)},"data-testid":"project-manager-status-filter"},yf("option",{value:"all"},"全部"),yf("option",{value:"active"},"进行中"),yf("option",{value:"completed"},"已完成"),yf("option",{value:"unpaid"},"未全款")),yf("button",{type:"button",className:"ghost-btn",onClick:()=>W(A,U)},"筛选"))},yf(RS,{projects:H,activeId:$.id,onSelect:(V)=>j(CS(V)),onRaw:u})),yf(AJ,{title:$.id?"编辑项目":"新建项目",eyebrow:"PostgreSQL Write Path",loading:r.saving||r.importing},yf("form",{className:"stack-form project-manager-form",onSubmit:G,"data-testid":"project-manager-form"},$.id?yf("label",null,"项目 ID",yf("input",{value:$.id,disabled:!0})):null,yf("label",null,"序号",yf("input",{type:"number",value:$.sequenceNo,onChange:(V)=>j((X)=>({...X,sequenceNo:V.target.value}))})),yf("label",null,"合同号",yf("input",{value:$.contractNo,onChange:(V)=>j((X)=>({...X,contractNo:V.target.value})),required:!0})),yf("label",null,"项目名称",yf("input",{value:$.name,onChange:(V)=>j((X)=>({...X,name:V.target.value})),required:!0})),yf("label",null,"当前状况",yf("textarea",{value:$.currentStatus,onChange:(V)=>j((X)=>({...X,currentStatus:V.target.value}))})),yf("label",null,"待完成",yf("textarea",{value:$.pending,onChange:(V)=>j((X)=>({...X,pending:V.target.value}))})),yf("label",null,"付款情况",yf("input",{value:$.paymentStatus,onChange:(V)=>j((X)=>({...X,paymentStatus:V.target.value})),placeholder:"例如 1 / 0.5 / 50%"})),yf("label",null,"其它",yf("input",{value:$.notes,onChange:(V)=>j((X)=>({...X,notes:V.target.value}))})),yf("div",{className:"inline-actions"},yf("button",{type:"submit",className:"primary-btn",disabled:r.saving,"data-testid":"project-manager-save-button"},r.saving?"保存中":$.id?"保存修改":"创建项目"),yf("button",{type:"button",className:"ghost-btn",onClick:()=>j({...jJ})},"清空"),$.id?yf("button",{type:"button",className:"danger-btn",disabled:r.saving,onClick:K,"data-testid":"project-manager-delete-button"},"删除"):null)),yf("div",{className:"project-manager-import"},yf("p",{className:"muted paragraph"},"浏览器只访问 UniDesk frontend;后端通过同源用户服务代理写入主 PostgreSQL,不暴露 4233 公网端口。"),yf("label",{className:"file-import"},r.importing?"导入中...":"导入 Excel",yf("input",{type:"file",accept:".xlsx",onChange:E,disabled:r.importing,"data-testid":"project-manager-import-input"}))))))}var t2=cf(Yu(),1);var jf=t2.default.createElement,{useEffect:xS}=t2.default,i0=t2.default.useState;function vS({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return jf("span",{className:`status-badge ${l}`},u||f||"unknown")}function g2({label:f,value:u,hint:l,tone:y}){return jf("article",{className:`metric-card ${y||""}`},jf("div",{className:"metric-label"},f),jf("div",{className:"metric-value"},u),jf("div",{className:"metric-hint"},l))}function JJ({title:f,eyebrow:u,actions:l,children:y,className:r,loading:_}){return jf("section",{className:`panel ${r||""}`},jf("div",{className:"panel-head"},jf("div",null,u?jf("p",{className:"panel-eyebrow"},u):null,jf(_u,{title:f,loading:_})),l?jf("div",{className:"panel-actions"},l):null),jf("div",{className:"panel-body"},y))}function RE({title:f,data:u,onOpen:l,testId:y}){return jf("button",{type:"button",className:"ghost-btn","data-testid":y,onClick:()=>l(f,u)},"查看原始JSON")}function k2({title:f,text:u}){return jf("div",{className:"empty-state"},jf("strong",null,f),jf("span",null,u))}function bS(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function hS(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function mS(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function vE(f){return String(f).replace(/[^a-zA-Z0-9_-]/g,"_")}function pS(f){if(!Number.isFinite(f))return"--";return`${f.toFixed(1)}%`}function g_(f,u){return`${f}/microservices/todo-note/proxy${u}`}function bE(f){return f.reduce((u,l)=>{let y=bE(Array.isArray(l.children)?l.children:[]),r=Boolean(l.completed);return{total:u.total+1+y.total,completed:u.completed+(r?1:0)+y.completed,active:u.active+(r?0:1)+y.active}},{total:0,completed:0,active:0})}function QJ(f,u){let l=u==="all"||(u==="completed"?Boolean(f.completed):!f.completed),y=Array.isArray(f.children)?f.children:[];return l||y.some((r)=>QJ(r,u))}function xE(f){return Array.isArray(f?.instances)?f.instances:[]}function UJ(f,u){for(let l of f){if(l?.id===u)return Array.isArray(l.children)?l.children:[];let y=UJ(Array.isArray(l?.children)?l.children:[],u);if(y.length>0)return y}return[]}function hE({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let y=f.find((o)=>o.id==="todo-note")||null,[r,_]=i0(null),[$,j]=i0(null),[A,F]=i0(""),[U,Q]=i0(null),[W,G]=i0("all"),[K,E]=i0(13),[O,z]=i0(""),[Z,N]=i0(""),[H,Y]=i0(""),[w,V]=i0(""),[X,i]=i0(""),[m,M]=i0(!1),[c,C]=i0(""),[T,R]=i0(null),P=xE($),n=bE(Array.isArray(U?.todos)?U.todos:[]),B=y?bS(y):{},D=y?mS(y):{},I=y?hS(y):{};async function p(o=A){let[uf,qf]=await Promise.all([Df(`${l}/microservices/todo-note/health`),Df(g_(l,"/api/instances"))]);_(uf),j(qf);let xf=xE(qf),tf=xf.some((df)=>df.id===o)?o:xf[0]?.id||"";return F(tf),tf}async function k(o=A){if(!o){Q(null);return}let uf=await Df(g_(l,`/api/instances/${encodeURIComponent(o)}`));Q(uf)}async function _f(o=A){if(!y)return;M(!0),C("");try{let uf=await p(o);await k(uf),R(new Date)}catch(uf){C(wf(uf,"Todo Note 加载失败"))}finally{M(!1)}}async function S(o){if(!A)return null;C("");try{let uf=await Df(g_(l,`/api/instances/${encodeURIComponent(A)}/actions`),{method:"POST",body:JSON.stringify({action:o})});return Q(uf),await p(A),uf}catch(uf){return C(wf(uf,"Todo 操作失败")),null}}async function e(o){o.preventDefault();let uf=O.trim();if(!uf)return;M(!0),C("");try{let qf=await Df(g_(l,"/api/instances"),{method:"POST",body:JSON.stringify({name:uf})});z(""),await _f(qf.id)}catch(qf){C(wf(qf,"创建清单失败"))}finally{M(!1)}}async function $f(o){if(!window.confirm("确认删除这个 Todo Note 清单?"))return;M(!0),C("");try{await Df(g_(l,`/api/instances/${encodeURIComponent(o)}`),{method:"DELETE"}),await _f(A===o?"":A)}catch(uf){C(wf(uf,"删除清单失败"))}finally{M(!1)}}async function Qf(o){o.preventDefault();let uf=Z.trim();if(!uf)return;N(""),await S({type:"addTodo",title:uf})}async function Af(o){if(!A)return;C("");try{let uf=await Df(g_(l,`/api/instances/${encodeURIComponent(A)}/${o}`),{method:"POST",body:JSON.stringify({})});Q(uf),await p(A)}catch(uf){C(wf(uf,`${o} 失败`))}}function zf(o){Y(o.id),V(String(o.title||""))}async function Hf(o){let uf=w.trim();if(Y(""),V(""),uf)await S({type:"updateTodoTitle",todoId:o,title:uf})}async function Zf(o){let qf=window.prompt("新增子任务标题")?.trim();if(!qf)return;let xf=UJ(Array.isArray(U?.todos)?U.todos:[],o),tf=new Set(xf.map((mu)=>mu.id)),df=await S({type:"addTodo",title:qf,parentId:o,targetIndex:0});if(!df)return;let lu=UJ(Array.isArray(df?.todos)?df.todos:[],o),Ou=lu.find((mu)=>!tf.has(mu.id));if(Ou&&lu[0]?.id!==Ou.id)await S({type:"moveTodo",todoId:Ou.id,targetParentId:o,targetIndex:0})}async function b(o,uf){if(!X)return;let qf={type:"moveTodo",todoId:X,targetIndex:uf};if(o)qf.targetParentId=o;i(""),await S(qf)}if(xS(()=>{_f()},[y?.id,y?.runtime?.providerStatus]),!y)return jf(k2,{title:"Todo Note 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=todo-note"});let t=P.find((o)=>o.id===A)||null,a=Array.isArray(U?.todos)?U.todos:[],Nf=a.map((o,uf)=>({todo:o,index:uf})).filter((o)=>QJ(o.todo,W));return jf("div",{className:"todo-note-page","data-testid":"todo-note-page"},jf(JJ,{title:"Todo Note 工作台",eyebrow:"Main Server 用户服务",loading:m,actions:jf("div",{className:"panel-actions"},jf("button",{type:"button",className:"ghost-btn",disabled:m,onClick:()=>_f(A),"data-testid":"todo-note-refresh-button"},m?"刷新中":"刷新"),jf(RE,{title:"Todo Note 用户服务",data:y,onOpen:u,testId:"raw-todo-note-service"}))},jf("div",{className:"todo-note-hero"},jf("div",null,jf("div",{className:"node-version-line"},jf(vS,{status:B.providerStatus==="online"?"online":"warn"},B.providerStatus||"unknown"),jf("span",null,y.providerId),jf("span",null,I.public?"公网暴露":"仅 UniDesk frontend 代理访问"),jf("span",null,r?.ok?"Health OK":"Health --")),jf("p",{className:"muted paragraph"},y.description)),jf("div",{className:"microservice-ref-card"},jf("span",null,"Repo"),jf("strong",null,D.url||"--"),jf("code",null,D.commitId||"--")),jf("div",{className:"microservice-ref-card"},jf("span",null,"Main Server Docker"),jf("strong",null,`${I.nodeBindHost||"--"}:${I.nodePort||"--"}`),jf("code",null,`${D.composeService||"--"} / ${D.containerName||"--"}`))),jf(Au,{error:c,wide:!0})),jf("div",{className:"todo-note-layout"},jf(JJ,{title:"清单",eyebrow:`${P.length} Instances`,className:"todo-list-panel",loading:m},jf("form",{className:"todo-create-list",onSubmit:e},jf("input",{placeholder:"新清单名称",value:O,onChange:(o)=>z(o.target.value),"aria-label":"新清单名称"}),jf("button",{type:"submit",className:"ghost-btn",disabled:m||!O.trim()},"创建")),P.length===0?jf(k2,{title:"暂无清单",text:"迁移或创建清单后会出现在这里"}):jf("div",{className:"todo-instance-list"},P.map((o)=>jf("button",{key:o.id,type:"button",className:`todo-instance-row ${A===o.id?"active":""}`,onClick:()=>{F(o.id),k(o.id)},"data-testid":`todo-instance-${vE(o.id)}`},jf("strong",null,o.name),jf("span",null,`${o.completedCount??0}/${o.todoCount??0} 完成`),jf("code",null,o.id))))),jf("div",{className:"todo-main-stack"},jf(JJ,{title:t?.name||"待选择清单",eyebrow:T?`Updated ${Uu(T)}`:"Todo Tree",loading:m,actions:U?jf("div",{className:"panel-actions"},jf("button",{type:"button",className:"ghost-btn",onClick:()=>S({type:"renameInstance",name:window.prompt("清单新名称",U.name)||U.name})},"重命名"),jf("button",{type:"button",className:"ghost-btn danger",onClick:()=>$f(A)},"删除清单"),jf(RE,{title:`Todo Instance ${A}`,data:U,onOpen:u,testId:"raw-todo-instance"})):null},!U?jf(k2,{title:"未选择清单",text:"左侧选择一个 Todo Note 清单"}):jf("div",{className:"todo-workbench",style:{"--todo-font-size":`${K}px`}},jf("div",{className:"todo-toolbar"},jf("form",{className:"todo-add-form",onSubmit:Qf},jf("input",{placeholder:"新增根任务",value:Z,onChange:(o)=>N(o.target.value),"aria-label":"新增根任务"}),jf("button",{type:"submit",className:"ghost-btn",disabled:!Z.trim()},"新增")),jf("div",{className:"todo-filter-strip"},["all","active","completed"].map((o)=>jf("button",{key:o,type:"button",className:`todo-filter ${W===o?"active":""}`,onClick:()=>G(o)},o==="all"?"全部":o==="active"?"未完成":"已完成"))),jf("div",{className:"todo-toolbar-actions"},jf("button",{type:"button",className:"ghost-btn",onClick:()=>S({type:"setAllTodosExpanded",expanded:!0})},"全部展开"),jf("button",{type:"button",className:"ghost-btn",onClick:()=>S({type:"setAllTodosExpanded",expanded:!1})},"全部收起"),jf("button",{type:"button",className:"ghost-btn",onClick:()=>Af("undo")},"撤销"),jf("button",{type:"button",className:"ghost-btn",onClick:()=>Af("redo")},"重做"),jf("label",{className:"todo-font-control"},"字号",jf("input",{type:"range",min:11,max:18,value:K,onChange:(o)=>E(Number(o.target.value))})))),jf("div",{className:"todo-stats-grid"},jf(g2,{label:"总任务",value:n.total,hint:`${P.length} lists`}),jf(g2,{label:"已完成",value:n.completed,hint:`${pS(n.total?n.completed/n.total*100:0)}`,tone:"ok"}),jf(g2,{label:"未完成",value:n.active,hint:W==="active"?"当前筛选":"active tasks",tone:n.active>0?"warn":"ok"}),jf(g2,{label:"历史指针",value:U.historyPointer??0,hint:"undo / redo"})),jf("div",{className:"todo-root-drop",onDragOver:(o)=>o.preventDefault(),onDrop:(o)=>{o.preventDefault(),b(null,a.length)}},"拖到这里可移为根任务末尾"),jf("div",{className:"todo-tree","data-testid":"todo-note-tree"},Nf.length===0?jf(k2,{title:"没有匹配任务",text:"调整筛选或新增任务"}):Nf.map(({todo:o,index:uf})=>jf(mE,{key:o.id,todo:o,depth:0,parentId:null,index:uf,siblingCount:a.length,filter:W,editingId:H,editingTitle:w,setEditingTitle:V,beginEdit:zf,saveEdit:Hf,applyTodoAction:S,addChild:Zf,dragTodoId:X,setDragTodoId:i,dropTodo:b}))))))))}function mE(f){let{todo:u,depth:l,parentId:y,index:r,siblingCount:_,filter:$,editingId:j,editingTitle:A,setEditingTitle:F,beginEdit:U,saveEdit:Q,applyTodoAction:W,addChild:G,dragTodoId:K,setDragTodoId:E,dropTodo:O}=f,z=Array.isArray(u.children)?u.children:[],Z=z.map((Y,w)=>({child:Y,childIndex:w})).filter((Y)=>QJ(Y.child,$)),N=j===u.id,H=y||null;return jf("div",{className:"todo-row-wrap"},jf("article",{className:`todo-row ${u.completed?"completed":""} ${K===u.id?"dragging":""}`,style:{"--todo-depth":l},draggable:!0,onDragStart:(Y)=>{E(u.id),Y.dataTransfer.effectAllowed="move"},onDragOver:(Y)=>Y.preventDefault(),onDrop:(Y)=>{Y.preventDefault(),O(u.id,z.length)},"data-testid":`todo-row-${vE(u.id)}`},jf("button",{type:"button",className:"todo-expand",disabled:z.length===0,onClick:()=>W({type:"toggleTodoExpanded",todoId:u.id})},z.length===0?"·":u.expanded?"▾":"▸"),jf("input",{type:"checkbox",checked:Boolean(u.completed),onChange:()=>W({type:"toggleTodoCompleted",todoId:u.id}),"aria-label":`完成 ${u.title}`}),jf("div",{className:"todo-title-cell",onDoubleClick:()=>U(u)},N?jf("div",{className:"todo-edit-inline"},jf("input",{value:A,autoFocus:!0,onChange:(Y)=>F(Y.target.value),onKeyDown:(Y)=>{if(Y.key==="Enter")Q(u.id);if(Y.key==="Escape")U({id:"",title:""})}}),jf("button",{type:"button",className:"ghost-btn",onClick:()=>Q(u.id)},"保存")):jf("strong",null,u.title||"Untitled"),jf("div",{className:"todo-meta-line"},jf("span",null,`子项 ${z.length}`),jf("span",null,`更新 ${Kf(u.updatedAt)}`),u.reminderAt?jf("span",{className:"todo-reminder"},`提醒 ${Kf(u.reminderAt)}`):jf("span",null,"无提醒"))),jf("input",{className:"todo-reminder-input",type:"datetime-local",value:E5(u.reminderAt),onChange:(Y)=>W({type:"setTodoReminder",todoId:u.id,reminderAt:sJ(Y.target.value)})}),jf("div",{className:"todo-row-actions"},jf("button",{type:"button",className:"ghost-btn",onClick:()=>U(u)},"编辑"),jf("button",{type:"button",className:"ghost-btn",onClick:()=>G(u.id)},"子项"),jf("button",{type:"button",className:"ghost-btn",disabled:r<=0,onClick:()=>W({type:"moveTodo",todoId:u.id,...H?{targetParentId:H}:{},targetIndex:r-1})},"上移"),jf("button",{type:"button",className:"ghost-btn",disabled:r<=0,onClick:()=>W({type:"moveTodo",todoId:u.id,...H?{targetParentId:H}:{},targetIndex:0})},"置顶"),jf("button",{type:"button",className:"ghost-btn",disabled:r>=_-1,onClick:()=>W({type:"moveTodo",todoId:u.id,...H?{targetParentId:H}:{},targetIndex:r+1})},"下移"),jf("button",{type:"button",className:"ghost-btn",disabled:!y,onClick:()=>W({type:"moveTodo",todoId:u.id,targetIndex:9999})},"提升"),jf("button",{type:"button",className:"ghost-btn danger",onClick:()=>W({type:"deleteTodo",todoId:u.id})},"删除"))),u.expanded&&Z.length>0?jf("div",{className:"todo-children"},Z.map(({child:Y,childIndex:w})=>jf(mE,{key:Y.id,todo:Y,depth:l+1,parentId:u.id,index:w,siblingCount:z.length,filter:$,editingId:j,editingTitle:A,setEditingTitle:F,beginEdit:U,saveEdit:Q,applyTodoAction:W,addChild:G,dragTodoId:K,setDragTodoId:E,dropTodo:O}))):null)}var pE=cf(Yu(),1),By=pE.default.createElement;function IE({title:f,items:u,actions:l,className:y,testId:r}){let _=Array.isArray(u)?u:[];return By("section",{className:`top-status-bar ${y||""}`,"data-testid":r},By("div",{className:"top-status-main"},f?By("strong",{className:"top-status-title"},f):null,By("div",{className:"top-status-chips"},_.map(($,j)=>By("span",{key:$?.key||`${$?.label||"status"}-${j}`,className:`top-status-chip ${$?.tone||""}`,"data-testid":$?.testId},$?.label?By("b",null,$.label):null,By("span",null,$?.value??"--"))))),l?By("div",{className:"top-status-actions"},l):null)}function lH(f,u){let l=document.getElementById("root")?.getAttribute(f);if(!l)return u;try{let y=JSON.parse(l);return typeof y==="object"&&y!==null&&!Array.isArray(y)?y:u}catch{return u}}var uu=lH("data-config",{apiBaseUrl:"/api",authUsername:"admin"}),IS=lH("data-codex-overview",null),J=Yy.default.createElement,{useEffect:y1,useMemo:G6}=Yy.default,mf=Yy.default.useState,KJ=Yy.default.createContext(!1),ul=mG(X8),gS={id:"code-queue",name:"Code Queue",providerId:"main-server",description:"Code Queue",repository:{containerName:"code-queue-backend"},backend:{nodeBaseUrl:"http://code-queue:4222",nodeBindHost:"code-queue",nodePort:4222,public:!1},runtime:{providerStatus:"loading",providerName:"main-server"}};function gE(){return typeof document>"u"||document.visibilityState!=="hidden"}function kS(f,u){if(f==="ops"&&u==="status")return 5000;if(f==="nodes"&&u==="monitor")return 5000;if(f==="tasks"&&(u==="dispatch"||u==="pending"))return 5000;if(f==="nodes"||f==="ops")return 1e4;if(f==="apps")return 15000;if(f==="tasks")return 15000;return 30000}async function tS(f){if(!f?._summaryOnly||!f?.id)return f;return(await Df(`${uu.apiBaseUrl}/tasks/${encodeURIComponent(String(f.id))}`))?.task||f}function K6(f){return f?._summaryOnly?{...f,_loadRaw:()=>tS(f)}:f}function s_(f){if(!Number.isFinite(f))return"--";let u=Math.max(0,f);if(u===0)return"0s";if(u<0.01)return"<0.01s";if(u<0.1)return`${u.toFixed(2)}s`;if(u<1)return`${u.toFixed(1)}s`;if(u<10&&!Number.isInteger(u))return`${u.toFixed(1)}s`;if(u<60)return`${Math.round(u)}s`;let l=Math.floor(u);if(l<3600)return`${Math.floor(l/60)}m ${l%60}s`;return`${Math.floor(l/3600)}h ${Math.floor(l%3600/60)}m`}function fl(f){let u=Number(f);if(!Number.isFinite(u))return"--";if(u<1)return`${Math.max(0,u).toFixed(1)}ms`;if(u<10)return`${u.toFixed(1)}ms`;if(u<1000)return`${Math.round(u)}ms`;return s_(u/1000)}function G0(f){let u=Number(f);if(!Number.isFinite(u)||u<=0)return"--";let l=["B","KB","MB","GB","TB"],y=u,r=0;while(y>=1024&&r0)return l[y]}return"任务失败但 provider 未返回明确原因"}function Lr(f){if(f===null||f===void 0)return"--";if(typeof f==="boolean")return f?"是":"否";if(typeof f==="number")return String(f);if(typeof f==="string")return f.length>80?`${f.slice(0,77)}...`:f;if(Array.isArray(f))return`${f.length} 项`;if(typeof f==="object")return`${Object.keys(f).length} 字段`;return String(f)}function oS(f,u){let l=f.replace(/[-_\s]/g,"").toLowerCase(),y=l==="ts"||l.endsWith("at")||l.endsWith("timestamp")||l.endsWith("heartbeat");if((typeof u==="string"||typeof u==="number")&&y){let r=Kf(u);if(r!=="--")return r}if(f==="bodyText"&&typeof u==="string")return`${/^\s*[{[]/.test(u)?"JSON":"HTTP"} body ${u.length} chars`;return Lr(u)}function _H(f){if(!f||typeof f!=="object"||Array.isArray(f))return[];return Object.entries(f)}function Rl(f){return String(f).replace(/[^a-zA-Z0-9_-]/g,"_")}function ZJ(f,u){return f&&typeof f==="object"&&!Array.isArray(f)?f[u]:void 0}function o2(f,u,l="未知"){let y=ZJ(f?.labels,u);return typeof y==="string"&&y.length>0?y:l}function $H(f){return o2(f,"providerGatewayVersion")}function z6(f){return o2(f,"providerGatewayUpgradePolicy")}function kE(f){return o2(f,"providerGatewayStartedAt","")}function jH(f){let u=ZJ(f?.labels,"unideskCapabilities");if(typeof u==="string")return u.split(",").map((l)=>l.trim()).filter(Boolean);return Array.isArray(u)?u.filter((l)=>typeof l==="string"):[]}function AH(f,u){return jH(f).includes(u)}function tE(f,u){let l=ZJ(f?.labels,u);return l===!0||l==="true"||l==="1"}function aS(f){if(!AH(f,"host.ssh"))return{tone:"fail",label:"不可用",detail:"未声明 host.ssh"};if(!tE(f,"hostSshConfigured"))return{tone:"warn",label:"未配置",detail:"缺少 SSH 环境变量"};if(!tE(f,"hostSshKeyPresent"))return{tone:"warn",label:"缺 key",detail:"私钥未挂载"};return{tone:"ok",label:"可用",detail:o2(f,"hostSshTarget","host.ssh ready")}}function dS(f){if(!AH(f,"provider.upgrade"))return{tone:"fail",label:"不可用",detail:"未声明 provider.upgrade"};let u=z6(f);if(u!=="always-enabled")return{tone:"warn",label:"待确认",detail:`策略 ${u}`};return{tone:"ok",label:"可用",detail:"always-enabled"}}function EJ(f){let u=typeof f==="string"&&f.length>0?f:"未知";if(u==="未知")return"版本未知";return u.startsWith("v")?u:`v${u}`}function FH(f){return f?.payload&&typeof f.payload==="object"&&!Array.isArray(f.payload)?f.payload:{}}function a2(f){return f?.result&&typeof f.result==="object"&&!Array.isArray(f.result)?f.result:{}}function s2(f){let u=FH(f),l=a2(f);return(u.mode??l.mode)==="schedule"?"schedule":"plan"}function eS(f){let u=FH(f).source;return typeof u==="string"&&u.length>0?u:"unknown"}function fP(f){let u=a2(f),l=u.plan&&typeof u.plan==="object"&&!Array.isArray(u.plan)?u.plan:{},y=u.policy??l.policy;return typeof y==="string"&&y.length>0?y:"--"}function JH(f){let u=a2(f),l=u.plan&&typeof u.plan==="object"&&!Array.isArray(u.plan)?u.plan:{},y=u.targetProviderGatewayVersion??u.providerGatewayVersion??l.targetProviderGatewayVersion??l.providerGatewayVersion;return typeof y==="string"&&y.length>0?EJ(y):"版本未知"}function UH(f){if(String(f?.status||"").toLowerCase()==="failed")return rH(f);if(o_(f))return"等待 provider 回传升级终态";let l=a2(f);if(typeof l.updaterContainerId==="string"&&l.updaterContainerId.length>0)return`updater ${l.updaterContainerId.slice(0,18)}`;if(typeof l.message==="string"&&l.message.length>0)return l.message;if(l.plan)return"升级计划已生成";return"无升级结果摘要"}function QH(f,u){return f.filter((l)=>l?.providerId===u&&l?.command==="provider.upgrade").sort((l,y)=>(k_(y.updatedAt)??0)-(k_(l.updatedAt)??0))}function uP(f){return f.find((u)=>s2(u)==="schedule")||f[0]||null}function WH(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function sE(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function lP(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function Xu({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return J("span",{className:`status-badge ${l}`},u||f||"unknown")}function $u({label:f,value:u,hint:l,tone:y,onClick:r,testId:_}){let $=typeof r==="function";return J("article",{className:`metric-card ${y||""} ${$?"clickable":""}`,role:$?"button":void 0,tabIndex:$?0:void 0,"data-testid":_,onClick:r,onKeyDown:$?(j)=>{if(j.key==="Enter"||j.key===" ")j.preventDefault(),r()}:void 0},J("div",{className:"metric-label"},f),J("div",{className:"metric-value"},u),J("div",{className:"metric-hint"},l))}function af({title:f,eyebrow:u,actions:l,children:y,className:r,loading:_}){let $=Yy.default.useContext(KJ),j=Boolean(_)||$;return J("section",{className:`panel ${r||""}`},J("div",{className:"panel-head"},J("div",null,u?J("p",{className:"panel-eyebrow"},u):null,J(_u,{title:f,loading:j})),l?J("div",{className:"panel-actions"},l):null),J("div",{className:"panel-body"},y))}function su({title:f,data:u,onOpen:l,testId:y}){let[r,_]=mf(!1),$=u&&typeof u==="object"&&typeof u._loadRaw==="function"?u._loadRaw:null;async function j(){if(!$){l(f,u);return}_(!0);try{l(f,await $())}catch(A){l(f,{ok:!1,error:wf(A,"读取原始 JSON 失败"),fallback:u})}finally{_(!1)}}return J("button",{type:"button",className:"ghost-btn","data-testid":y,disabled:r,onClick:()=>void j()},r?"读取中":"查看原始JSON")}function yP({raw:f,onClose:u}){if(!f)return null;return J("div",{className:"modal-backdrop",role:"presentation"},J("section",{className:"raw-dialog",role:"dialog","aria-modal":"true","aria-label":f.title},J("div",{className:"raw-dialog-head"},J("h2",null,f.title),J("button",{type:"button",className:"ghost-btn",onClick:u},"关闭")),J("pre",{className:"raw-json","data-testid":"raw-json"},JSON.stringify(f.data,null,2))))}function zH({labels:f,limit:u=8}){let l=_H(f).slice(0,u);if(l.length===0)return J("span",{className:"muted"},"无标签");return J("div",{className:"chip-row"},l.map(([y,r])=>J("span",{key:y,className:"data-chip"},J("b",null,y),J("span",null,Lr(r)))))}function t_({node:f}){let u=$H(f);return J("span",{className:`version-chip ${u==="未知"?"unknown":""}`,"data-testid":`gateway-version-${Rl(f?.providerId||"unknown")}`},EJ(u))}function oE({title:f,state:u,testId:l}){return J("span",{className:`capability-badge ${u.tone}`,title:u.detail,"data-testid":l},J("b",null,f),J("strong",null,u.label),J("small",null,u.detail))}function HJ({node:f}){let u=Rl(f?.providerId||"unknown");return J("div",{className:"node-availability-strip"},J(oE,{title:"SSH 透传",state:aS(f),testId:`ssh-availability-${u}`}),J(oE,{title:"远程更新",state:dS(f),testId:`upgrade-availability-${u}`}))}function Br({data:f,empty:u="无数据"}){if(f===null||f===void 0)return J("span",{className:"muted"},u);if(typeof f!=="object")return J("span",{className:"summary-value"},Lr(f));if(Array.isArray(f))return J("span",{className:"summary-value"},`${f.length} 项列表`);let l=Object.entries(f).slice(0,5);if(l.length===0)return J("span",{className:"muted"},u);return J("div",{className:"summary-grid"},l.map(([y,r])=>J("span",{key:y,className:"summary-item"},J("b",null,y),J("span",null,oS(y,r)))))}function zu({title:f,text:u}){return J("div",{className:"empty-state"},J("strong",null,f),J("span",null,u))}function rP({onLogin:f}){let[u,l]=mf(uu.authUsername||"admin"),[y,r]=mf(""),[_,$]=mf(""),[j,A]=mf(!1);async function F(U){U.preventDefault(),A(!0),$("");try{let Q=await Df("/login",{method:"POST",body:JSON.stringify({username:u,password:y})});f(Q)}catch(Q){$(wf(Q,"登录失败"))}finally{A(!1)}}return J("main",{className:"login-screen","data-testid":"login-screen"},J("section",{className:"login-card"},J("div",{className:"login-brand"},J("span",{className:"brand-mark"},"UD"),J("div",null,J("h1",null,"UniDesk"),J("p",null,"Control Plane Login"))),J("form",{className:"login-form",onSubmit:F},J("label",null,"账号",J("input",{name:"username",autoComplete:"username",value:u,onChange:(U)=>l(U.target.value)})),J("label",null,"密码",J("input",{name:"password",type:"password",autoComplete:"current-password",value:y,onChange:(U)=>r(U.target.value)})),J(Au,{error:_}),J("button",{type:"submit",disabled:j},j?"登录中":"登录")),J("div",{className:"login-note"},"默认账号由 config.json 注入;公网入口只暴露前端登录面。")))}function _P({connection:f,lastRefresh:u,onRefresh:l,onLogout:y,session:r,clock:_,activeStatusItems:$=[]}){let j=[{key:"core",label:"核心",value:f.text,tone:f.ok?"ok":"fail",testId:"conn-text"},...Array.isArray($)?$:[],{key:"refresh",label:"刷新",value:u?Uu(u):"未刷新"},{key:"clock",label:c6,value:Uu(_)},{key:"user",label:"用户",value:r?.user?.username||"--",tone:"user"}];return J("header",{className:"topbar"},J("div",null,J("p",{className:"eyebrow"},"Distributed Work Platform"),J("h1",null,"UniDesk 控制平面")),J(IE,{className:"global-top-status",title:"状态",items:j,actions:[J("button",{key:"refresh",type:"button",className:"ghost-btn",onClick:l},"刷新"),J("button",{key:"logout",type:"button",className:"ghost-btn danger",onClick:y},"退出")]}))}function $P({activeModule:f,activeTabs:u,onNavigate:l,collapsed:y,onToggle:r}){return J("aside",{className:`rail ${y?"collapsed":""}`,"aria-label":"主模块"},J("div",{className:"brand"},J("span",{className:"brand-mark"},"UD"),J("span",{className:"brand-text"},"UniDesk"),J("button",{type:"button",className:"rail-toggle",onClick:r,"aria-label":y?"展开左侧边栏":"收起左侧边栏","data-testid":"rail-toggle"},y?"»":"«")),X8.map((_)=>J("button",{key:_.id,type:"button",className:`module ${f===_.id?"active":""}`,onClick:()=>l(_.id,u[_.id]||H_[_.id]||_.tabs[0]?.id||""),title:_.label,"data-route":V3(ul,_.id,u[_.id]||H_[_.id]||_.tabs[0]?.id||"")},J("span",{className:"module-code"},_.code),J("span",null,_.label))))}function jP({module:f,activeTab:u,onNavigate:l}){return J("nav",{className:"tabs","aria-label":`${f.label} 子功能`},f.tabs.map((y)=>J("button",{key:y.id,type:"button",className:`tab ${u===y.id?"active":""}`,onClick:()=>l(f.id,y.id),"data-route":V3(ul,f.id,y.id)},y.label)))}function AP({data:f,onRaw:u,onNavigate:l}){let y=f.overview||{},r=f.nodes.filter((F)=>F.status==="online"),_=f.pendingTasks||f.tasks.filter(o_),$=y.pendingTaskCount??_.length,j=f.tasks.slice(0,5),A=y.pgdata||{};return J("div",{className:"page-grid overview-grid","data-testid":"overview-page"},J(af,{title:"核心指标",eyebrow:"Control"},J("div",{className:"metric-grid"},J($u,{label:"数据库",value:y.dbReady?"READY":"WAIT",hint:"PostgreSQL internal network",tone:y.dbReady?"ok":"warn"}),J($u,{label:"PGDATA",value:G0(A.databaseBytes),hint:`${A.volumeName||"unidesk_pgdata_10gb"} / ${A.databasePretty||"--"}`,tone:"ok",testId:"pgdata-usage-card"}),J($u,{label:"在线节点",value:y.onlineNodeCount??0,hint:`${y.nodeCount??0} registered`,tone:"ok"}),J($u,{label:"WebSocket",value:y.activeSocketCount??0,hint:"Provider ingress sockets"}),J($u,{label:"待处理任务",value:$,hint:$>0?"点击查看具体任务":`timeout ${s_(Math.floor((y.taskPendingTimeoutMs??0)/1000))}`,tone:$>0?"warn":"ok",onClick:()=>l("tasks","pending"),testId:"pending-task-card"}))),J(af,{title:"本机 Provider",eyebrow:"Self Connected"},r.length===0?J(zu,{title:"暂无在线节点",text:"provider-gateway 未完成自接入"}):J("div",{className:"node-card-list"},r.slice(0,4).map((F)=>J(FP,{key:F.providerId,node:F,onRaw:u})))),J(af,{title:"待处理任务明细",eyebrow:`${$} Pending`,actions:J("button",{type:"button",className:"ghost-btn",onClick:()=>l("tasks","pending"),"data-testid":"pending-task-detail-link"},"进入任务调度")},_.length===0?J(zu,{title:"当前无待处理",text:"queued / dispatched / running 超时后会自动转为 failed,避免总览长期卡住"}):J("div",{className:"compact-list"},_.slice(0,5).map((F)=>J(fH,{key:F.id,task:F,onRaw:u})))),J(af,{title:"最近任务",eyebrow:"Dispatch"},j.length===0?J(zu,{title:"暂无任务",text:"可以在任务调度模块发起 docker.ps 或 echo"}):J("div",{className:"compact-list"},j.map((F)=>J(fH,{key:F.id,task:F,onRaw:u})))))}function FP({node:f,onRaw:u}){return J("article",{className:"node-card"},J("div",{className:"node-card-head"},J("div",null,J("strong",null,f.name),J("code",null,f.providerId)),J(Xu,{status:f.status})),J("div",{className:"node-version-line"},J(t_,{node:f}),J("span",null,`升级策略 ${z6(f)}`)),J(HJ,{node:f}),J(zH,{labels:f.labels,limit:6}),J("div",{className:"node-card-foot"},J("span",null,`心跳 ${Kf(f.lastHeartbeat)}`),J(su,{title:`Provider ${f.providerId}`,data:f,onOpen:u,testId:`raw-node-${Rl(f.providerId)}`})))}function JP({events:f,onRaw:u}){return J(af,{title:"事件摘要",eyebrow:"Latest 100"},f.length===0?J(zu,{title:"暂无事件",text:"Provider 注册、心跳超时和任务状态会写入事件流"}):J("div",{className:"table-wrap"},J("table",null,J("thead",null,J("tr",null,J("th",null,"ID"),J("th",null,"类型"),J("th",null,"来源"),J("th",null,"摘要"),J("th",null,"时间"),J("th",null,"操作"))),J("tbody",null,f.map((l)=>J("tr",{key:l.id},J("td",null,J("code",null,l.id)),J("td",null,J(Xu,{status:l.type},l.type)),J("td",null,J("code",null,l.source)),J("td",null,J(Br,{data:l.payload})),J("td",null,Kf(l.createdAt)),J("td",null,J(su,{title:`Event ${l.id}`,data:l,onOpen:u}))))))))}function UP({logs:f,onRaw:u}){return J(af,{title:"服务日志",eyebrow:"Core Recent"},f.length===0?J(zu,{title:"暂无日志",text:"backend-core 内存日志会在请求和 provider 事件后出现"}):J("div",{className:"log-list"},f.slice(-80).reverse().map((l,y)=>J("article",{key:y,className:`log-row ${l.level||"info"}`},J("span",null,Kf(l.ts)),J("b",null,l.level||"info"),J("strong",null,l.message||"log"),J(Br,{data:l.data,empty:"无附加字段"}),J(su,{title:`Log ${l.message||y}`,data:l,onOpen:u})))))}function QP({nodes:f,onRaw:u}){return J(af,{title:"节点清单",eyebrow:`${f.length} Providers`},f.length===0?J(zu,{title:"暂无 Provider 节点",text:"确认 provider-gateway 已连接 provider ingress"}):J("div",{className:"table-wrap"},J("table",{className:"node-list-table"},J("thead",null,J("tr",null,J("th",null,"状态"),J("th",null,"Provider"),J("th",null,"网关版本"),J("th",null,"运维可用性"),J("th",null,"资源标签"),J("th",null,"连接时间"),J("th",null,"最后心跳"),J("th",null,"操作"))),J("tbody",null,f.map((l)=>J("tr",{key:l.providerId},J("td",null,J(Xu,{status:l.status})),J("td",null,J("strong",null,l.name),J("code",null,l.providerId)),J("td",null,J("div",{className:"gateway-cell"},J(t_,{node:l}),J("span",null,z6(l)))),J("td",null,J(HJ,{node:l})),J("td",null,J(zH,{labels:l.labels,limit:5})),J("td",null,Kf(l.connectedAt)),J("td",null,Kf(l.lastHeartbeat)),J("td",null,J(su,{title:`Provider ${l.providerId}`,data:l,onOpen:u,testId:`raw-node-table-${Rl(l.providerId)}`}))))))))}function WP({nodes:f}){let u=G6(()=>{let l=[];for(let y of f)for(let[r,_]of _H(y.labels))l.push({providerId:y.providerId,name:y.name,key:r,value:_});return l},[f]);return J(af,{title:"资源标签",eyebrow:"Structured Labels"},u.length===0?J(zu,{title:"暂无标签",text:"provider-gateway 注册消息会同步资源标签"}):J("div",{className:"label-matrix"},u.map((l)=>J("article",{key:`${l.providerId}-${l.key}`,className:"label-card"},J("span",null,l.key),J("strong",null,Lr(l.value)),J("code",null,l.providerId)))))}function zP({nodes:f}){return J(af,{title:"心跳状态",eyebrow:"Provider Liveness"},f.length===0?J(zu,{title:"无心跳",text:"等待 provider 注册和 heartbeat"}):J("div",{className:"heartbeat-list"},f.map((u)=>J("article",{key:u.providerId,className:"heartbeat-row"},J("span",{className:`pulse ${u.status}`}),J("div",null,J("strong",null,u.name),J("code",null,u.providerId)),J("div",null,J("span",null,"connected"),J("b",null,Kf(u.connectedAt))),J("div",null,J("span",null,"last heartbeat"),J("b",null,Kf(u.lastHeartbeat)))))))}function GP({nodes:f,systemStatuses:u,tasks:l,onRaw:y,refresh:r}){let[_,$]=mf(""),j=G6(()=>f.map((E)=>{let O=u.find((z)=>z.providerId===E.providerId);return{...E,systemCurrent:O?.current||null,systemHistory:O?.history||[],systemUpdatedAt:O?.updatedAt||null}}),[f,u]),A=j.find((E)=>E.providerId===_)||j[0]||null;if(y1(()=>{if(!_&&j[0])$(j[0].providerId)},[j.length,_]),!A)return J(zu,{title:"暂无资源监控",text:"等待 provider 上报 CPU、内存和硬盘指标"});let F=A.systemCurrent,U=A.systemHistory||[],Q=F?.cpu||{},W=F?.memory||{},G=F?.disk||{},K=U.length>0?U:F?[{at:F.collectedAt,cpuPercent:Rf(Q.percent),memoryPercent:Rf(W.percent),diskPercent:Rf(G.percent)}]:[];return J("div",{className:"monitor-page","data-testid":"node-monitor-page"},J("div",{className:"docker-node-strip"},j.map((E)=>J("button",{key:E.providerId,type:"button",className:`docker-node-tile ${A.providerId===E.providerId?"active":""}`,onClick:()=>$(E.providerId)},J("span",{className:`pulse ${E.status}`}),J("strong",null,E.name),J("code",null,E.providerId),J("span",null,E.systemCurrent?`CPU ${Xy(E.systemCurrent.cpu?.percent)} / MEM ${Xy(E.systemCurrent.memory?.percent)}`:"等待指标")))),J("div",{className:"monitor-layout"},J(af,{title:"任务管理器视图",eyebrow:A.name,className:"monitor-main-panel",actions:F?J(su,{title:`System ${A.providerId}`,data:{current:F,history:U},onOpen:y}):null},!F?J(zu,{title:"系统指标未上报",text:"provider-gateway 会周期性采集 /proc 与 df,并保存历史曲线"}):J("div",null,J("div",{className:"monitor-hero"},J("div",null,J("p",{className:"panel-eyebrow"},"Node Performance"),J("h3",null,A.name),J("div",{className:"docker-meta"},J("span",null,`${Q.cores||0} CPU cores`),J("span",null,`load ${Rf(Q.load1).toFixed(2)} / ${Rf(Q.load5).toFixed(2)} / ${Rf(Q.load15).toFixed(2)}`),J("span",null,`memory actual ${G0(W.usedBytes)} / ${G0(W.totalBytes)}`),J("span",null,`disk ${G0(G.usedBytes)} / ${G0(G.totalBytes)}`))),J(Xu,{status:F.ok?"online":"warn"},F.ok?"METRICS READY":"METRICS DEGRADED")),J("div",{className:"monitor-chart-grid"},J(zJ,{title:"CPU",metricKey:"cpuPercent",current:Q.percent,points:K,detail:`${Q.cores||0} cores / load ${Rf(Q.load1).toFixed(2)}`,tone:"cpu",testId:"metric-chart-cpu"}),J(zJ,{title:"Memory",metricKey:"memoryPercent",current:W.percent,points:K,detail:`${G0(W.usedBytes)} actual / ${G0(W.cacheBytes)} cache excluded`,tone:"memory",testId:"metric-chart-memory"}),J(zJ,{title:"Disk",metricKey:"diskPercent",current:G.percent,points:K,detail:`${G.path||"/"} mounted ${G.mount||"--"}`,tone:"disk",testId:"metric-chart-disk"})),J("div",{className:"monitor-summary-grid"},J($u,{label:"CPU 当前",value:Xy(Q.percent),hint:`history ${K.length} samples`,tone:"ok"}),J($u,{label:"实际内存",value:G0(W.usedBytes),hint:`${Xy(W.percent)} 不含缓存`}),J($u,{label:"硬盘已用",value:G0(G.usedBytes),hint:Xy(G.percent)}),J($u,{label:"更新时间",value:Kf(A.systemUpdatedAt||F.collectedAt),hint:A.providerId})),J(KP,{current:F,onRaw:y}))),J("div",{className:"monitor-side-stack"},J(LP,{provider:A,refresh:r,onRaw:y}),J(BP,{provider:A,tasks:l,onRaw:y,limit:5}),J(af,{title:"采样说明",eyebrow:"Retention"},J("div",{className:"monitor-note-list"},J("article",null,J("b",null,"CPU"),J("span",null,"从 /proc/stat 计算相邻采样差值,首个采样用 load/cores 近似")),J("article",null,J("b",null,"Memory"),J("span",null,"实际内存 = MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem,不把 page cache / buffer 计入占用")),J("article",null,J("b",null,"Disk"),J("span",null,"使用 df -PB1 对配置路径采样,默认监控根文件系统")),J("article",null,J("b",null,"Process"),J("span",null,"从 /proc/[pid] 采集进程 CPU、实际内存 RSS、线程数和磁盘 I/O 速率;表格默认按内存占用降序")))))))}function aE(f,u){if(u==="memory")return Rf(f.rssBytes);if(u==="cpu")return Rf(f.cpuPercent);if(u==="disk")return Rf(f.readBytesPerSecond)+Rf(f.writeBytesPerSecond);if(u==="pid")return Rf(f.pid);if(u==="threads")return Rf(f.threads);if(u==="runtime")return Rf(f.elapsedSeconds);if(u==="user")return String(f.user||"");return String(f.name||f.command||"")}function dE({value:f,label:u,tone:l}){let y=Math.max(1,Math.min(100,Rf(f)));return J("div",{className:`process-meter ${l||""}`},J("span",{style:{width:`${y}%`}}),J("b",null,u))}function KP({current:f,onRaw:u}){let[l,y]=mf({key:"memory",direction:"desc"}),r=Yy.default.useContext(KJ),_=f?.processSummary&&typeof f.processSummary==="object"?f.processSummary:{},$=Array.isArray(f?.processes)?f.processes:[],j=G6(()=>{let F=l.direction==="asc"?1:-1;return[...$].sort((U,Q)=>{let W=aE(U,l.key),G=aE(Q,l.key);if(typeof W==="string"||typeof G==="string")return String(W).localeCompare(String(G),"zh-CN")*F;return(W-G)*F||Rf(U.pid)-Rf(Q.pid)})},[$,l.key,l.direction]),A=(F,U)=>{let Q=l.key===U,W=Q?l.direction==="asc"?"ascending":"descending":"none";return J("th",{"aria-sort":W},J("button",{type:"button",className:`process-sort-button ${Q?"active":""}`,"data-testid":`process-sort-${U}`,onClick:()=>y((G)=>({key:U,direction:G.key===U&&G.direction==="desc"?"asc":"desc"}))},F,J("span",null,Q?l.direction==="desc"?"↓":"↑":"↕")))};return J("section",{className:"process-resource-panel","data-testid":"process-resource-panel"},J("div",{className:"process-resource-head"},J("div",null,J("p",{className:"panel-eyebrow"},"Windows Resource Monitor Style"),J(_u,{title:"进程资源占用",level:3,loading:r})),J("div",{className:"process-resource-actions"},J("span",{className:"data-chip"},"默认按内存排序"),J("span",{className:"data-chip"},`${Rf(_.visible,j.length)} / ${Rf(_.total,j.length)} 进程`),J(su,{title:"Process Resource Snapshot",data:{processSummary:_,processes:$},onOpen:u,testId:"raw-process-resources"}))),j.length===0?J(zu,{title:"暂无进程资源数据",text:"等待 provider-gateway 上报 /proc/[pid] 采样;旧版 provider 需要先升级到支持进程资源表的版本"}):J("div",{className:"process-table-wrap"},J("table",{className:"process-resource-table","data-testid":"process-resource-table"},J("thead",null,J("tr",null,A("进程","name"),A("PID","pid"),A("用户","user"),J("th",null,"状态"),A("CPU","cpu"),A("内存","memory"),J("th",null,"RSS"),A("磁盘 I/O","disk"),A("线程","threads"),A("运行时长","runtime"))),J("tbody",null,j.map((F)=>{let U=Rf(F.readBytesPerSecond)+Rf(F.writeBytesPerSecond);return J("tr",{key:`${F.pid}-${F.startedAt}`,"data-testid":`process-row-${Rl(F.pid)}`,"data-memory-bytes":String(Rf(F.rssBytes)),"data-cpu-percent":String(Rf(F.cpuPercent)),"data-disk-bps":String(U),"data-pid":String(Rf(F.pid))},J("td",null,J("div",{className:"process-name-cell"},J("strong",null,F.name||"--"),J("span",{className:"process-command"},F.command||"--"))),J("td",null,J("code",null,F.pid||"--")),J("td",null,F.user||`uid:${F.uid??"--"}`),J("td",null,J("span",{className:`process-state state-${Rl(F.state||"unknown")}`},F.state||"?")),J("td",null,J(dE,{value:F.cpuPercent,label:sS(F.cpuPercent),tone:"cpu"})),J("td",null,J(dE,{value:F.memoryPercent,label:Xy(F.memoryPercent),tone:"memory"})),J("td",null,G0(F.rssBytes)),J("td",null,J("div",{className:"process-io-cell"},J("strong",null,WJ(U)),J("span",null,`R ${WJ(F.readBytesPerSecond)} / W ${WJ(F.writeBytesPerSecond)}`))),J("td",null,F.threads||0),J("td",null,s_(Rf(F.elapsedSeconds))))})))))}function zJ({title:f,metricKey:u,current:l,points:y,detail:r,tone:_,testId:$}){let j=y.map((W)=>Math.max(0,Math.min(100,Rf(W[u])))),A=j.length>1?j:[j[0]||0,j[0]||0],F=A.length<=1?100:100/(A.length-1),U=A.map((W,G)=>`${(G*F).toFixed(2)},${(46-W*0.42).toFixed(2)}`).join(" "),Q=`0,48 ${U} 100,48`;return J("article",{className:`metric-chart ${_}`,"data-testid":$},J("div",{className:"metric-chart-head"},J("div",null,J("span",null,f),J("strong",null,Xy(l))),J("code",null,`${y.length} pts`)),J("svg",{viewBox:"0 0 100 48",preserveAspectRatio:"none",role:"img","aria-label":`${f} usage curve`},J("polygon",{points:Q}),J("polyline",{points:U}),J("line",{x1:"0",x2:"100",y1:"24",y2:"24"})),J("div",{className:"metric-chart-foot"},J("span",null,"0%"),J("span",null,r),J("span",null,"100%")))}function Y1(f){return Array.isArray(f)?f:[]}function NP(f){let u=Y1(f?.core?.requests?.componentSummary);return[...Y1(f?.frontend?.requests?.componentSummary),...u].sort((y,r)=>Rf(r.requestCount)-Rf(y.requestCount))}function ZP(f){let u=Y1(f?.core?.operations?.summary);return[...Y1(f?.frontend?.operations?.summary),...u].sort((y,r)=>Rf(r.count)-Rf(y.count))}function EP(f){let u=Y1(f?.core?.requests?.recentFailures).map((y)=>({source:"backend",...y}));return[...Y1(f?.frontend?.requests?.recentFailures).map((y)=>({source:"frontend",...y})),...u].sort((y,r)=>(k_(r.at)??0)-(k_(y.at)??0)).slice(0,20)}function HP(f){let u=Y1(f?.core?.operations?.recentSlowOperations);return[...Y1(f?.frontend?.operations?.recentSlowOperations),...u].sort((y,r)=>Rf(r.durationMs)-Rf(y.durationMs)).slice(0,20)}function OP(f){let u=performance.memory,l=Number(u?.usedJSHeapSize);if(Number.isFinite(l)&&l>0)return l;let y=Number(f?.appBundleBytes);if(Number.isFinite(y)&&y>0)return y;return Rf(f?.process?.heapUsedBytes)}function qP({points:f}){let u=Y1(f),l=u.map((W)=>Rf(W.mb)),y=Math.max(1,...l),r=Math.max(0,Math.min(...l,0)),_=Math.max(1,y-r),$=u.length>1?u:[...u,...u],j=$.length<=1?100:100/($.length-1),A=$.map((W,G)=>{let K=Rf(W.mb);return`${(G*j).toFixed(2)},${(48-(K-r)/_*42).toFixed(2)}`}).join(" "),F=`0,50 ${A} 100,50`,U=u.at(-1),Q=u[0];return J("article",{className:"performance-memory-card","data-testid":"performance-memory-chart"},J("div",{className:"performance-memory-head"},J("strong",null,`Bwebui: ${U?`${Rf(U.mb).toFixed(1)}MB`:"--"}`),J("span",null,u.length>0?`${u.length} samples`:"等待采样")),J("svg",{viewBox:"0 0 100 50",preserveAspectRatio:"none",role:"img","aria-label":"Bwebui memory trend"},J("polygon",{points:F}),J("polyline",{points:A}),J("line",{x1:"0",x2:"100",y1:"25",y2:"25"})),J("div",{className:"performance-axis-row"},J("span",null,Q?Uu(new Date(Q.at)):"--"),J("span",null,"时间"),J("span",null,U?Uu(new Date(U.at)):"--")),J("div",{className:"performance-axis-row"},J("span",null,`${r.toFixed(1)}`),J("span",null,"(MB)"),J("span",null,`${y.toFixed(1)}`)))}function VP({onRaw:f}){let[u,l]=mf({core:null,frontend:null}),[y,r]=mf([]),[_,$]=mf(""),[j,A]=mf(!1),[F,U]=mf(null),[Q,W]=mf(!1);async function G(){A(!0),$("");try{let[c,C]=await Promise.all([Df(`${uu.apiBaseUrl}/performance`,{cache:"no-store"}),Df(`${uu.apiBaseUrl}/frontend-performance`,{cache:"no-store"})]);l({core:c,frontend:C});let T=OP(C);r((R)=>[...R,{at:new Date().toISOString(),mb:T/1048576}].slice(-80))}catch(c){$(wf(c,"性能指标加载失败"))}finally{A(!1)}}y1(()=>{G();let c=setInterval(()=>void G(),5000);return()=>clearInterval(c)},[]);async function K(){W(!0),$(""),U(null);try{let c=await Df(`${uu.apiBaseUrl}/code-queue-load-test`,{method:"POST",body:JSON.stringify({targetMs:1000,timeoutMs:90000,url:uu.frontendPublicUrl||window.location.origin})});U(c),G()}catch(c){$(wf(c,"Code Queue Playwright 测量失败"))}finally{W(!1)}}let E=NP(u),O=EP(u),z=ZP(u),Z=HP(u),N=u.core?.process||{},H=u.frontend?.process||{},Y=u.core?.database?.codeQueueStorage||{},w=Rf(Y.total),V=F?.result||{},X=Rf(V.wallMs,NaN),i=Rf(V.networkIdleMs,NaN),m=V.withinTarget===!0,M=Q?"running":F===null?"idle":F.measurementOk===!0?m?"passed":"slow":"failed";return J("div",{className:"performance-page","data-testid":"performance-page"},J("div",{className:"performance-hero"},J("div",null,J("p",{className:"panel-eyebrow"},"Unified Performance"),J(_u,{title:"性能面板",loading:j||Q}),J("p",null,"按组件统计 HTTP 请求、失败率、P95 延迟,并汇总 backend/frontend 内部操作耗时。")),J("div",{className:"inline-actions"},J("button",{type:"button",className:"ghost-btn",onClick:()=>void K(),disabled:Q,"data-testid":"code-queue-load-test-button"},Q?"测试中...":"测试 Code Queue 加载"),J("button",{type:"button",className:"ghost-btn",onClick:()=>void G(),disabled:j,"data-testid":"performance-refresh-button"},j?"刷新中":"刷新"),J(su,{title:"Performance Snapshot",data:u,onOpen:f,testId:"raw-performance"}))),J(Au,{error:_}),J("div",{className:"performance-top-grid"},J(qP,{points:y}),J("div",{className:"performance-metric-stack"},J($u,{label:"backend RSS",value:G0(N.rssBytes),hint:`heap ${G0(N.heapUsedBytes)}`}),J($u,{label:"frontend RSS",value:G0(H.rssBytes),hint:`bundle ${G0(u.frontend?.appBundleBytes)}`}),J($u,{label:"Codex PG 任务",value:w||"--",hint:Y.ok?"unidesk_code_queue_tasks":"等待表初始化",tone:Y.ok?"ok":"warn"}),J($u,{label:"请求样本",value:Rf(u.core?.requests?.sampleCount)+Rf(u.frontend?.requests?.sampleCount),hint:"rolling window 3000"}))),J(af,{title:"Code Queue 加载基准",eyebrow:"Playwright / target <1s",className:"codex-load-test-panel",loading:Q,actions:J("div",{className:"panel-actions"},J("button",{type:"button",className:"primary-btn",onClick:()=>void K(),disabled:Q,"data-testid":"code-queue-load-test-panel-button"},Q?"正在运行 Playwright...":"手动触发测试"),F?J(su,{title:"Code Queue Load Test",data:F,onOpen:f,testId:"raw-code-queue-load-test"}):null)},J("div",{className:"codex-load-test-grid","data-testid":"code-queue-load-test-result"},J($u,{label:"总耗时",value:Q?"运行中":Number.isFinite(X)?fl(X):"--",hint:F===null?"点击按钮启动远端 Playwright":`目标 ${fl(V.targetMs||1000)} / ${V.url||"Code Queue"}`,tone:M==="passed"?"ok":M==="failed"||M==="slow"?"warn":""}),J($u,{label:"判定",value:Q?"RUNNING":M==="passed"?"PASS <1s":M==="slow"?"SLOW":M==="failed"?"FAILED":"--",hint:F?.measurementOk===!1?String(F.error||V.error||"measurement failed").slice(0,120):"导航开始 -> DOMContentLoaded -> data-load-state=complete",tone:M==="passed"?"ok":M==="idle"||M==="running"?"":"fail"}),J($u,{label:"Network idle",value:Number.isFinite(i)?fl(i):"--",hint:`DOMContentLoaded ${fl(V.domContentLoadedMs)} / ${V.networkIdleReached===!1?"未在 5s 内空闲":"已空闲"}`,tone:Number.isFinite(i)&&i<=1000?"ok":"warn"}),J($u,{label:"组件耗时",value:Number.isFinite(Rf(V.componentLoadMs,NaN))?fl(V.componentLoadMs):"--",hint:`queue ${fl(V.queueMs)} / detail ${fl(V.detailMs)}`,tone:Rf(V.componentLoadMs)>1000?"warn":"ok"}),J($u,{label:"Trace 规模",value:Number.isFinite(Rf(V.transcriptRows,NaN))?String(V.transcriptRows):"--",hint:`${V.visibleTaskCount??0} visible tasks / ${V.partial?"preview":"complete"}`})),Q?J("div",{className:"performance-empty-line"},"正在通过 main-server Host SSH 启动 Playwright,完成后会显示 wall time、组件耗时和最慢 API。"):null,F&&Array.isArray(V.slowestApi)&&V.slowestApi.length>0?J("div",{className:"table-wrap performance-table-wrap compact codex-load-api-table"},J("table",{className:"performance-table"},J("thead",null,J("tr",null,["API","状态","耗时"].map((c)=>J("th",{key:c},c)))),J("tbody",null,V.slowestApi.slice(0,5).map((c,C)=>J("tr",{key:`${c.url}-${C}`},J("td",null,J("code",null,c.url)),J("td",null,c.status),J("td",null,fl(c.durationMs))))))):null),J("div",{className:"performance-grid"},J(af,{title:"组件汇总",eyebrow:"Requests",loading:j},E.length===0?J(zu,{title:"暂无请求样本",text:"刷新几次或打开页面后会自动形成组件统计"}):J("div",{className:"table-wrap performance-table-wrap"},J("table",{className:"performance-table"},J("thead",null,J("tr",null,["组件","请求数","失败数","失败率","平均延迟","P95"].map((c)=>J("th",{key:c},c)))),J("tbody",null,E.map((c)=>J("tr",{key:c.component},J("td",null,J("code",null,c.component)),J("td",null,c.requestCount),J("td",null,c.failureCount),J("td",null,Xy(Rf(c.failureRate)*100)),J("td",null,fl(c.averageLatencyMs)),J("td",null,fl(c.p95LatencyMs)))))))),J(af,{title:"最近失败请求",eyebrow:"Failures",loading:j},O.length===0?J("div",{className:"performance-empty-line"},"最近没有失败请求"):J("div",{className:"table-wrap performance-table-wrap compact"},J("table",{className:"performance-table"},J("thead",null,J("tr",null,["时间","来源","组件","状态","路径"].map((c)=>J("th",{key:c},c)))),J("tbody",null,O.map((c,C)=>J("tr",{key:`${c.at}-${C}`},J("td",null,Kf(c.at)),J("td",null,c.source),J("td",null,J("code",null,c.component)),J("td",null,J(Xu,{status:"failed"},c.status)),J("td",null,J("code",null,c.path)))))))),J(af,{title:"内部操作汇总",eyebrow:"Operations",loading:j},z.length===0?J(zu,{title:"暂无内部操作样本",text:"API 查询和代理请求会自动记录内部操作耗时"}):J("div",{className:"table-wrap performance-table-wrap"},J("table",{className:"performance-table"},J("thead",null,J("tr",null,["服务","操作","次数","平均延迟","P95"].map((c)=>J("th",{key:c},c)))),J("tbody",null,z.map((c)=>J("tr",{key:`${c.service}-${c.operation}`},J("td",null,c.service),J("td",null,J("code",null,c.operation)),J("td",null,c.count),J("td",null,fl(c.averageLatencyMs)),J("td",null,fl(c.p95LatencyMs)))))))),J(af,{title:"最近慢操作",eyebrow:"Slowest",loading:j},Z.length===0?J(zu,{title:"暂无慢操作",text:"后端会记录最近窗口内耗时最高的内部操作"}):J("div",{className:"table-wrap performance-table-wrap"},J("table",{className:"performance-table"},J("thead",null,J("tr",null,["时间","操作","耗时","结果","细节"].map((c)=>J("th",{key:c},c)))),J("tbody",null,Z.map((c,C)=>J("tr",{key:`${c.at}-${c.operation}-${C}`},J("td",null,Kf(c.at)),J("td",null,J("code",null,c.operation)),J("td",null,fl(c.durationMs)),J("td",null,c.ok?"成功":"失败"),J("td",null,c.detail||"-")))))))))}function LP({provider:f,refresh:u,onRaw:l}){let[y,r]=mf(""),[_,$]=mf(null),[j,A]=mf("");async function F(U){r(U),A("");try{let Q=await Df(`${uu.apiBaseUrl}/dispatch`,{method:"POST",body:JSON.stringify({providerId:f.providerId,command:"provider.upgrade",payload:{mode:U,source:"frontend-resource-monitor",requestedAt:new Date().toISOString()}})});$({mode:U,...Q}),await u()}catch(Q){A(wf(Q,"升级命令下发失败"))}finally{r("")}}return J(af,{title:"Provider Gateway 升级",eyebrow:"Remote Control",loading:Boolean(y)},J("div",{className:"upgrade-control","data-testid":"provider-upgrade-control"},J("p",null,"通过 UniDesk WebSocket 向当前计算节点下发 provider.upgrade;预检只生成升级计划,执行升级会调度节点本地 updater 容器。"),J("div",{className:"upgrade-target-line"},J("span",null,"指定 Provider"),J("code",null,f.providerId),J(t_,{node:f})),J("div",{className:"upgrade-actions"},J("button",{type:"button",className:"ghost-btn",disabled:Boolean(y),onClick:()=>F("plan"),"data-testid":"upgrade-plan-button"},y==="plan"?"预检中":"预检升级"),J("button",{type:"button",className:"ghost-btn danger",disabled:Boolean(y),onClick:()=>F("schedule"),"data-testid":"upgrade-schedule-button"},y==="schedule"?"调度中":"执行升级")),J(Au,{error:j}),_?J("div",{className:"upgrade-result"},J(Xu,{status:_.status||"queued"},_.status||"queued"),J("span",null,`${_.mode==="schedule"?"执行升级":"预检升级"} 已下发`),J("span",null,`指定版本 ${EJ($H(f))}`),J("code",null,_.taskId||"--"),J(su,{title:"Provider Upgrade Dispatch",data:_,onOpen:l})):J("span",{className:"muted"},"升级任务结果会进入任务历史;执行升级可能导致 provider 短暂重连。")))}function GH({records:f,onRaw:u,compact:l=!1}){if(f.length===0)return J(zu,{title:"暂无远程更新记录",text:"该节点还没有 provider.upgrade 任务;执行预检或升级后会在这里形成结构化记录"});return J("div",{className:`upgrade-record-table-wrap table-wrap ${l?"compact":""}`},J("table",{className:"upgrade-record-table"},J("thead",null,J("tr",null,J("th",null,"状态"),J("th",null,"模式"),J("th",null,"任务"),J("th",null,"来源"),J("th",null,"耗时"),J("th",null,"策略"),J("th",null,"Gateway 版本"),J("th",null,"结果记录"),J("th",null,"更新时间"),J("th",null,"操作"))),J("tbody",null,f.map((y)=>J("tr",{key:y.id,"data-testid":`gateway-upgrade-record-${Rl(y.id)}`},J("td",null,J(Xu,{status:y.status})),J("td",null,J("span",{className:`mode-chip ${s2(y)}`},s2(y)==="schedule"?"执行升级":"预检")),J("td",null,J("strong",null,"provider.upgrade"),J("code",null,y.id)),J("td",null,eS(y)),J("td",null,J(NH,{task:y})),J("td",null,fP(y)),J("td",null,J("span",{className:"version-chip"},JH(y))),J("td",null,J("span",{className:`upgrade-outcome ${String(y.status||"").toLowerCase()}`},UH(y))),J("td",null,Kf(y.updatedAt)),J("td",null,J(su,{title:`Provider Upgrade Task ${y.id}`,data:K6(y),onOpen:u})))))))}function BP({provider:f,tasks:u,onRaw:l,limit:y=5}){let r=QH(u,f.providerId).slice(0,y);return J(af,{title:"远程更新记录",eyebrow:f.providerId,actions:J(t_,{node:f}),className:"provider-upgrade-records-panel"},J("div",{"data-testid":`provider-upgrade-records-${Rl(f.providerId)}`},J(GH,{records:r,onRaw:l,compact:!0})))}function XP({nodes:f,tasks:u,onRaw:l}){let y=G6(()=>f.map((_)=>{let $=QH(u,_.providerId);return{node:_,records:$,latest:uP($),capabilities:jH(_)}}),[f,u]),r=y.reduce((_,$)=>_+$.records.length,0);return J("div",{className:"gateway-page","data-testid":"gateway-version-page"},J(af,{title:"Provider Gateway 版本",eyebrow:`${f.length} Providers / ${r} 更新记录`},f.length===0?J(zu,{title:"暂无 Provider 节点",text:"等待 provider-gateway 注册后显示版本号和升级记录"}):J("div",{className:"table-wrap gateway-version-table-wrap"},J("table",{className:"gateway-version-table"},J("thead",null,J("tr",null,J("th",null,"状态"),J("th",null,"Provider"),J("th",null,"Gateway 版本"),J("th",null,"升级策略"),J("th",null,"运维可用性"),J("th",null,"运行时间"),J("th",null,"能力"),J("th",null,"最近远程更新"),J("th",null,"操作"))),J("tbody",null,y.map((_)=>J("tr",{key:_.node.providerId},J("td",null,J(Xu,{status:_.node.status})),J("td",null,J("strong",null,_.node.name),J("code",null,_.node.providerId)),J("td",null,J(t_,{node:_.node})),J("td",null,z6(_.node)),J("td",null,J(HJ,{node:_.node})),J("td",null,kE(_.node)?Kf(kE(_.node)):"待新版上报"),J("td",null,J("div",{className:"capability-row"},_.capabilities.length===0?J("span",{className:"muted"},"未声明"):_.capabilities.slice(0,5).map(($)=>J("span",{key:$,className:"data-chip"},$)))),J("td",null,_.latest?J("div",{className:"latest-upgrade-cell"},J(Xu,{status:_.latest.status}),J("span",null,`${s2(_.latest)==="schedule"?"执行升级":"预检"} / ${Kf(_.latest.updatedAt)}`),J("small",null,`Gateway ${JH(_.latest)}`),J("small",null,UH(_.latest))):J("span",{className:"muted"},"暂无记录")),J("td",null,J(su,{title:`Provider ${_.node.providerId}`,data:_.node,onOpen:l})))))))),J(af,{title:"远程更新记录",eyebrow:"Structured provider.upgrade records"},f.length===0?J(zu,{title:"暂无记录",text:"没有 provider 节点时不会生成远程更新记录"}):J("div",{className:"gateway-record-grid"},y.map((_)=>J("article",{key:_.node.providerId,className:"gateway-record-card","data-testid":`gateway-records-${Rl(_.node.providerId)}`},J("div",{className:"gateway-record-head"},J("div",null,J("strong",null,_.node.name),J("code",null,_.node.providerId)),J(t_,{node:_.node})),J("div",{className:"gateway-record-meta"},J("span",null,`心跳 ${Kf(_.node.lastHeartbeat)}`),J("span",null,`策略 ${z6(_.node)}`),J("span",null,`${_.records.length} 条记录`)),J(GH,{records:_.records.slice(0,8),onRaw:l,compact:!0}))))))}function YP(f){if(f==="running")return"online";if(f==="paused"||f==="restarting")return"warn";if(f==="exited"||f==="dead")return"offline";return"internal"}function KH(f){return/^[a-f0-9]{48,64}$/i.test(f)}function W6(f){let u=String(f?.name||""),l=String(f?.labels||"");return u==="unidesk_pgdata_10gb"||l.includes("com.docker.compose.volume=unidesk_pgdata_10gb")||u.toLowerCase().includes("pgdata")}function eE(f){let u=String(f?.name||""),l=String(f?.labels||"");if(W6(f))return 0;if(l.includes("com.docker.compose.project=unidesk"))return 1;if(!KH(u))return 2;return 3}function wP(f){return[...f].sort((u,l)=>{let y=eE(u)-eE(l);if(y!==0)return y;return String(u.name||"").localeCompare(String(l.name||""))})}function DP({nodes:f,dockerStatuses:u,onRaw:l}){let[y,r]=mf(""),_=G6(()=>f.map((Z)=>{let N=u.find((H)=>H.providerId===Z.providerId);return{...Z,dockerStatus:N?.dockerStatus||null,dockerUpdatedAt:N?.updatedAt||null}}),[f,u]),$=_.find((Z)=>Z.providerId===y)||_[0]||null;if(y1(()=>{if(!y&&_[0])r(_[0].providerId)},[_.length,y]),!$)return J(zu,{title:"暂无 Docker 节点",text:"等待 provider 上报 Docker daemon 状态"});let j=$.dockerStatus,A=$.providerId==="main-server",F=j?.counts||{},U=j?.daemon||{},Q=j?.containers||[],W=j?.images||[],G=wP(j?.volumes||[]),K=A?G.find(W6):null,E=j?.networks||[],O=Q.filter((Z)=>Z.state==="running"),z=Q.filter((Z)=>Z.state!=="running");return J("div",{className:"docker-page","data-testid":"docker-status-page"},J("div",{className:"docker-node-strip"},_.map((Z)=>J("button",{key:Z.providerId,type:"button",className:`docker-node-tile ${$.providerId===Z.providerId?"active":""}`,onClick:()=>r(Z.providerId)},J("span",{className:`pulse ${Z.status}`}),J("strong",null,Z.name),J("code",null,Z.providerId),J("span",null,Z.dockerStatus?`Docker ${Z.dockerStatus.ok?"ready":"degraded"}`:"等待上报")))),J("div",{className:"docker-layout"},J(af,{title:"Docker Desktop 视图",eyebrow:$.name,className:"docker-main-panel",actions:j?J(su,{title:`Docker ${$.providerId}`,data:j,onOpen:l}):null},!j?J(zu,{title:"Docker 状态未上报",text:"provider-gateway 会在连接后周期性采集 docker info / ps / images / volume / network"}):J("div",null,J("div",{className:"docker-hero"},J("div",null,J("p",{className:"panel-eyebrow"},"Daemon"),J("h3",null,U.name||$.providerId),J("div",{className:"docker-meta"},J("span",null,U.serverVersion?`Engine ${U.serverVersion}`:"Engine --"),J("span",null,U.operatingSystem||"OS --"),J("span",null,U.architecture||"arch --"),J("span",null,`${U.cpus||0} CPU / ${G0(U.memoryBytes)}`))),J(Xu,{status:j.ok?"online":"warn"},j.ok?"Docker Ready":"Docker Degraded")),J("div",{className:"docker-metrics"},J($u,{label:"Containers",value:F.containers??Q.length,hint:`${F.running??O.length} running / ${F.stopped??z.length} stopped`,tone:"ok"}),J($u,{label:"Images",value:F.images??W.length,hint:`${F.daemonImages??F.images??W.length} daemon images`}),J($u,{label:"Volumes",value:F.volumes??G.length,hint:A?K?"database volume visible":"database volume missing":"node local volumes",tone:K?"ok":""}),J($u,{label:"Networks",value:F.networks??E.length,hint:U.driver?`driver ${U.driver}`:"docker networks"})),A?J(TP,{volume:K,volumeCount:G.length}):null,J("div",{className:"docker-section-head"},J("h3",null,"Containers"),J("span",null,`updated ${Kf($.dockerUpdatedAt||j.collectedAt)}`)),J("div",{className:"docker-container-table table-wrap","data-testid":"docker-container-table"},J("table",null,J("thead",null,J("tr",null,J("th",null,"状态"),J("th",null,"容器"),J("th",null,"镜像"),J("th",null,"端口"),J("th",null,"运行时间"),J("th",null,"大小"))),J("tbody",null,Q.length===0?J("tr",null,J("td",{colSpan:6},"暂无容器")):Q.map((Z)=>J("tr",{key:`${Z.id}-${Z.name}`},J("td",null,J(Xu,{status:YP(Z.state)},Z.state||"unknown")),J("td",null,J("strong",null,Z.name||"--"),J("code",null,Z.id||"--")),J("td",null,Z.image||"--"),J("td",null,Z.ports||J("span",{className:"muted"},"未发布")),J("td",null,Z.runningFor||Z.status||"--"),J("td",null,Z.size||"--")))))))),J("div",{className:"docker-side-stack"},J(GJ,{title:"Images",items:W,render:(Z)=>J("article",{key:`${Z.id}-${Z.repository}`,className:"docker-side-row"},J("strong",null,`${Z.repository}:${Z.tag}`),J("span",null,Z.size||"--"),J("code",null,Z.id||"--"))}),J(GJ,{title:"Volumes",items:G,limit:G.length,render:(Z)=>J("article",{key:Z.name,className:`docker-side-row volume-row ${A&&W6(Z)?"database-volume":""}`,"data-testid":A&&W6(Z)?"database-volume-row":void 0},J("strong",null,Z.name),J("span",null,A&&W6(Z)?"PostgreSQL":KH(String(Z.name||""))?"anonymous":"named"),J("code",null,Z.mountpoint||Z.driver||Z.scope||"--"))}),J(GJ,{title:"Networks",items:E,render:(Z)=>J("article",{key:Z.id||Z.name,className:"docker-side-row"},J("strong",null,Z.name),J("span",null,Z.driver||"--"),J("code",null,Z.id||"--"))}))))}function TP({volume:f,volumeCount:u}){return J("section",{className:`docker-volume-focus ${f?"ready":"missing"}`,"data-testid":"database-volume-card"},J("div",{className:"volume-focus-head"},J("span",{className:"panel-eyebrow"},"Database Named Volume"),J(Xu,{status:f?"online":"warn"},f?"FOUND":"MISSING")),f?J("div",{className:"volume-focus-body"},J("strong",null,f.name),J("span",null,"PostgreSQL data volume for unidesk-database"),J("div",{className:"volume-route"},J("code",null,f.mountpoint||"/var/lib/docker/volumes/unidesk_pgdata_10gb/_data"),J("span",null,"->"),J("code",null,"unidesk-database:/var/lib/postgresql/data")),J("div",{className:"docker-meta compact"},J("span",null,`driver ${f.driver||"--"}`),J("span",null,`scope ${f.scope||"--"}`),J("span",null,`${u} volumes reported`))):J("div",{className:"volume-focus-body"},J("strong",null,"unidesk_pgdata_10gb"),J("span",null,"当前 Docker 快照没有发现数据库命名卷;请检查 provider-gateway 的 Docker volume 上报。")))}function GJ({title:f,items:u,render:l,limit:y}){let r=u.slice(0,y??12),_=Math.max(0,u.length-r.length);return J(af,{title:f,eyebrow:`${u.length} items`,className:"docker-side-panel"},u.length===0?J(zu,{title:`暂无 ${f}`,text:"等待 Docker 状态采集"}):J("div",{className:"docker-side-list"},r.map(l),_>0?J("div",{className:"docker-side-more"},`+ ${_} more`):null))}function nP({microservices:f,onRaw:u,onNavigate:l}){let y=f.filter((r)=>sE(r).public===!1);return J("div",{className:"microservice-page","data-testid":"microservice-catalog-page"},J(af,{title:"用户服务目录",eyebrow:"Provider Mounted User Services"},J("div",{className:"metric-grid"},J($u,{label:"服务总数",value:f.length,hint:"config.json 用户服务登记"}),J($u,{label:"私有后端",value:y.length,hint:"不直接暴露公网",tone:"ok"}),J($u,{label:"D601 服务",value:f.filter((r)=>r.providerId==="D601").length,hint:"compute-node docker"}),J($u,{label:"集成前端",value:f.filter((r)=>r.frontend?.integrated).length,hint:"UniDesk React 页面"}))),J(af,{title:"服务映射",eyebrow:"Repo Reference + Runtime"},f.length===0?J(zu,{title:"暂无用户服务",text:"在 config.json 的 microservices 中登记用户服务的 provider、仓库引用和后端映射"}):J("div",{className:"table-wrap"},J("table",{className:"microservice-table"},J("thead",null,J("tr",null,J("th",null,"服务"),J("th",null,"Provider"),J("th",null,"代码引用"),J("th",null,"Docker 引用"),J("th",null,"后端映射"),J("th",null,"开发入口"),J("th",null,"运行态"),J("th",null,"操作"))),J("tbody",null,f.map((r)=>{let _=WH(r),$=lP(r),j=sE(r);return J("tr",{key:r.id,"data-testid":`microservice-row-${Rl(r.id)}`},J("td",null,J("strong",null,r.name),J("code",null,r.id)),J("td",null,J("strong",null,_.providerName||r.providerId),J("code",null,r.providerId)),J("td",null,J("span",null,$.url||"--"),J("code",null,$.commitId||"--")),J("td",null,J("span",null,$.composeFile||"--"),J("code",null,`${$.composeService||"--"} / ${$.containerName||"--"}`)),J("td",null,J(Xu,{status:j.public?"warn":"online"},j.public?"public":"private"),J("code",null,`${j.nodeBindHost||"--"}:${j.nodePort||"--"} -> ${j.proxyMode||"--"}`)),J("td",null,J("span",null,r.development?.sshPassthrough?"SSH 透传":"未配置"),J("code",null,r.development?.worktreePath||"--")),J("td",null,J(Xu,{status:_.providerStatus==="online"?"online":"warn"},_.providerStatus||"unknown"),J(Br,{data:_.container,empty:"容器快照未上报"})),J("td",null,J("div",{className:"microservice-actions"},r.id==="findjob"?J("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","findjob"),"data-testid":"open-findjob-button"},"打开"):null,r.id==="pipeline"?J("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","pipeline"),"data-testid":"open-pipeline-button"},"打开"):null,r.id==="todo-note"?J("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","todo-note"),"data-testid":"open-todo-note-button"},"打开"):null,r.id==="met-nonlinear"?J("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","met-nonlinear"),"data-testid":"open-met-nonlinear-button"},"打开"):null,r.id==="claudeqq"?J("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","claudeqq"),"data-testid":"open-claudeqq-button"},"打开"):null,r.id==="baidu-netdisk"?J("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","baidu-netdisk"),"data-testid":"open-baidu-netdisk-button"},"打开"):null,r.id==="code-queue"?J("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","code-queue"),"data-testid":"open-code-queue-button"},"打开"):null,r.id==="project-manager"?J("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","project-manager"),"data-testid":"open-project-manager-button"},"打开"):null,J(su,{title:`用户服务 ${r.id}`,data:r,onOpen:u}))))}))))))}function MP({nodes:f,onDispatched:u,onRaw:l}){let y=f.filter((M)=>M.status==="online"),[r,_]=mf(y[0]?.providerId||f[0]?.providerId||""),[$,j]=mf("docker.ps"),[A,F]=mf("frontend"),[U,Q]=mf("operator-check"),[W,G]=mf("normal"),[K,E]=mf(!1),[O,z]=mf(""),[Z,N]=mf(!1),[H,Y]=mf(null),[w,V]=mf("");y1(()=>{if(!r&&(y[0]?.providerId||f[0]?.providerId))_(y[0]?.providerId||f[0].providerId)},[f.length,y.length,r]);function X(){return{source:A,note:U,priority:W}}function i(){z(JSON.stringify(X(),null,2)),E(!0)}async function m(M){M.preventDefault(),N(!0),V("");try{let c=K?JSON.parse(O||"{}"):X(),C=await Df(`${uu.apiBaseUrl}/dispatch`,{method:"POST",body:JSON.stringify({providerId:r,command:$,payload:c})});Y(C),await u()}catch(c){V(wf(c,"下发失败"))}finally{N(!1)}}return J("div",{className:"page-grid dispatch-grid"},J(af,{title:"下发任务",eyebrow:"Real WebSocket Dispatch"},J("form",{className:"dispatch-form",onSubmit:m},J("label",null,"Provider",J("select",{value:r,onChange:(M)=>_(M.target.value)},f.map((M)=>J("option",{key:M.providerId,value:M.providerId},`${M.name} / ${M.providerId}`)))),J("label",null,"Command",J("select",{value:$,onChange:(M)=>j(M.target.value)},J("option",{value:"docker.ps"},"docker.ps"),J("option",{value:"host.ssh"},"host.ssh"),J("option",{value:"microservice.http"},"microservice.http"),J("option",{value:"echo"},"echo"))),J("label",null,"来源",J("input",{value:A,onChange:(M)=>F(M.target.value)})),J("label",null,"备注",J("input",{value:U,onChange:(M)=>Q(M.target.value)})),J("label",null,"优先级",J("select",{value:W,onChange:(M)=>G(M.target.value)},J("option",{value:"normal"},"normal"),J("option",{value:"low"},"low"),J("option",{value:"urgent"},"urgent"))),J("div",{className:"dispatch-actions"},J("button",{type:"button",className:"ghost-btn",onClick:i},"查看原始JSON"),J("button",{type:"submit",disabled:Z||!r},Z?"下发中":"下发任务")),K?J("label",{className:"raw-editor-label"},"高级 Payload",J("textarea",{className:"raw-editor",value:O,onChange:(M)=>z(M.target.value)})):null,J(Au,{error:w,wide:!0}))),J(af,{title:"下发结果",eyebrow:"Response"},H?J("div",{className:"result-card"},J(Xu,{status:H.status||"queued"},H.status||"queued"),J("dl",null,J("dt",null,"Task ID"),J("dd",null,J("code",null,H.taskId||"--")),J("dt",null,"Provider 在线"),J("dd",null,Lr(H.providerOnline))),J(su,{title:"Dispatch Response",data:H,onOpen:l})):J(zu,{title:"等待操作",text:"任务响应会以结构化结果卡展示"})))}function fH({task:f,onRaw:u}){return J("article",{className:"compact-row"},J(Xu,{status:f.status}),J("div",null,J("strong",null,f.command),J("code",null,f.id)),J("span",null,o_(f)?`已等待 ${NJ(f.updatedAt)}`:`耗时 ${s_(yH(f)??0)}`),J(su,{title:`Task ${f.id}`,data:K6(f),onOpen:u}))}function NH({task:f}){let u=yH(f),l=o_(f);return J("div",{className:"task-duration"},J("strong",null,u===null?"--":s_(u)),J("span",null,l?`已运行 / 创建 ${Kf(f.createdAt)}`:`创建 ${Kf(f.createdAt)}`))}function SP({task:f}){let u=String(f?.status||"").toLowerCase(),l=f?.result,y=l&&typeof l==="object"&&!Array.isArray(l)?l:{},_=["exitCode","code","signal","timeoutMs","previousStatus","mode"].filter(($)=>y[$]!==void 0&&y[$]!==null);if(u==="failed"){let $=rH(f);return J("div",{className:"task-diagnostic failed"},J("b",null,"失败原因"),J("span",{className:"diagnostic-reason"},Lr($)),_.length>0?J("div",{className:"diagnostic-meta"},_.map((j)=>J("span",{key:j,className:"data-chip"},J("b",null,j),J("span",null,Lr(y[j]))))):null)}if(o_(f))return J("div",{className:"task-diagnostic warn"},J("b",null,"等待终态"),J("span",null,`最后更新 ${NJ(f.updatedAt)} 前`));return J("div",{className:"task-diagnostic ok"},J("b",null,"完成摘要"),J(Br,{data:l,empty:"无执行输出"}))}function PP({tasks:f,onRaw:u}){let l=f.filter(o_);return J("div",{"data-testid":"pending-task-page"},J(af,{title:"待处理任务",eyebrow:`${l.length} Pending`},l.length===0?J(zu,{title:"当前无待处理任务",text:"queued / dispatched / running 会在超时后自动转为 failed;历史记录仍可在任务历史中查看"}):J("div",{className:"table-wrap","data-testid":"pending-task-table"},J("table",null,J("thead",null,J("tr",null,J("th",null,"状态"),J("th",null,"任务"),J("th",null,"Provider"),J("th",null,"已等待"),J("th",null,"载荷摘要"),J("th",null,"操作"))),J("tbody",null,l.map((y)=>J("tr",{key:y.id},J("td",null,J(Xu,{status:y.status})),J("td",null,J("strong",null,y.command),J("code",null,y.id)),J("td",null,J("code",null,y.providerId)),J("td",null,NJ(y.updatedAt)),J("td",null,J(Br,{data:y.payload})),J("td",null,J(su,{title:`Pending Task ${y.id}`,data:K6(y),onOpen:u})))))))))}function CP({tasks:f,onRaw:u}){return J("div",{"data-testid":"task-history-page"},J(af,{title:"任务历史",eyebrow:`${f.length} Tasks`},f.length===0?J(zu,{title:"暂无任务",text:"下发任务后会在这里看到生命周期"}):J("div",{className:"table-wrap"},J("table",{className:"task-history-table"},J("thead",null,J("tr",null,J("th",null,"状态"),J("th",null,"任务"),J("th",null,"Provider"),J("th",null,"任务耗时"),J("th",null,"载荷摘要"),J("th",null,"诊断信息"),J("th",null,"更新时间"),J("th",null,"操作"))),J("tbody",null,f.map((l)=>J("tr",{key:l.id,"data-testid":`task-row-${Rl(l.id)}`},J("td",null,J(Xu,{status:l.status})),J("td",null,J("strong",null,l.command),J("code",null,l.id)),J("td",null,J("code",null,l.providerId)),J("td",null,J(NH,{task:l})),J("td",null,J(Br,{data:l.payload})),J("td",null,J(SP,{task:l})),J("td",null,Kf(l.updatedAt)),J("td",null,J(su,{title:`Task ${l.id}`,data:K6(l),onOpen:u})))))))))}function cP({tasks:f,onRaw:u}){let l=f.filter((y)=>["succeeded","failed"].includes(y.status));return J(af,{title:"执行结果",eyebrow:"Finished Tasks"},l.length===0?J(zu,{title:"暂无结果",text:"任务完成后展示 provider 返回的结构化摘要"}):J("div",{className:"result-grid"},l.map((y)=>J("article",{key:y.id,className:"result-card"},J("div",{className:"node-card-head"},J("strong",null,y.command),J(Xu,{status:y.status})),J("code",null,y.id),J(Br,{data:y.result,empty:"无执行输出"}),J(su,{title:`Task Result ${y.id}`,data:K6(y),onOpen:u})))))}function iP({data:f}){let u=f.overview||{};return J("div",{className:"page-grid topology-grid"},J(af,{title:"公开入口",eyebrow:"Public"},J("div",{className:"endpoint-list"},J("article",null,J("b",null,"Frontend"),J("span",null,uu.frontendPublicUrl||window.location.origin),J(Xu,{status:"online"},"public")),J("article",null,J("b",null,"Provider Ingress"),J("span",null,uu.providerIngressPublicUrl||"ws://public/ws/provider"),J(Xu,{status:"online"},"public")))),J(af,{title:"内部服务",eyebrow:"Docker Network Only"},J("div",{className:"endpoint-list"},J("article",null,J("b",null,"backend-core API"),J("span",null,"http://backend-core:8080"),J(Xu,{status:"internal"},"internal")),J("article",null,J("b",null,"database"),J("span",null,"postgres://database:5432/unidesk"),J(Xu,{status:"internal"},"internal")))),J(af,{title:"运行态",eyebrow:"Runtime"},J("div",{className:"metric-grid"},J($u,{label:"DB Ready",value:u.dbReady?"YES":"NO",hint:"internal health"}),J($u,{label:"Online Nodes",value:u.onlineNodeCount??0,hint:"provider-gateway self-link"}))))}function RP({session:f}){return J(af,{title:"认证策略",eyebrow:"Frontend Login"},J("div",{className:"policy-grid"},J("article",null,J("span",null,"默认账号"),J("strong",null,uu.authUsername||"admin")),J("article",null,J("span",null,"当前会话"),J("strong",null,f?.user?.username||"--")),J("article",null,J("span",null,"Session TTL"),J("strong",null,`${uu.sessionTtlSeconds||0}s`)),J("article",null,J("span",null,"API 访问"),J("strong",null,"同源 Cookie 保护"))),J("p",{className:"muted paragraph"},"浏览器只访问 frontend 同源接口;frontend 容器使用 Docker 内网代理 backend-core API。"))}function xP(){return J(af,{title:"安全边界",eyebrow:"Exposure Rule"},J("div",{className:"security-board"},J("article",{className:"allow"},J("b",null,"允许公网"),J("span",null,"frontend 登录入口"),J("span",null,"provider ingress WebSocket/health")),J("article",{className:"deny"},J("b",null,"禁止公网"),J("span",null,"backend-core REST API"),J("span",null,"PostgreSQL database")),J("article",null,J("b",null,"数据库卷"),J("span",null,"named volume unidesk_pgdata_10gb"),J("span",null,"CLI stop/start 不删除数据卷"))))}function vP({activeModule:f,activeTab:u,data:l,session:y,refresh:r,onRaw:_,onNavigate:$}){if(f==="ops"&&u==="status")return J(AP,{data:l,onRaw:_,onNavigate:$});if(f==="ops"&&u==="performance")return J(VP,{onRaw:_});if(f==="ops"&&u==="events")return J(JP,{events:l.events,onRaw:_});if(f==="ops"&&u==="logs")return J(UP,{logs:l.logs,onRaw:_});if(f==="nodes"&&u==="list")return J(QP,{nodes:l.nodes,onRaw:_});if(f==="nodes"&&u==="monitor")return J(GP,{nodes:l.nodes,systemStatuses:l.systemStatuses,tasks:l.tasks,onRaw:_,refresh:r});if(f==="nodes"&&u==="docker")return J(DP,{nodes:l.nodes,dockerStatuses:l.dockerStatuses,onRaw:_});if(f==="nodes"&&u==="gateway")return J(XP,{nodes:l.nodes,tasks:l.tasks,onRaw:_});if(f==="nodes"&&u==="labels")return J(WP,{nodes:l.nodes});if(f==="nodes"&&u==="heartbeats")return J(zP,{nodes:l.nodes});if(f==="tasks"&&u==="dispatch")return J(MP,{nodes:l.nodes,onDispatched:r,onRaw:_});if(f==="tasks"&&u==="pending")return J(PP,{tasks:l.pendingTasks,onRaw:_});if(f==="tasks"&&u==="history")return J(CP,{tasks:l.tasks,onRaw:_});if(f==="tasks"&&u==="results")return J(cP,{tasks:l.tasks,onRaw:_});if(f==="apps"&&u==="catalog")return J(nP,{microservices:l.microservices,onRaw:_,onNavigate:$});if(f==="apps"&&u==="todo-note")return J(hE,{microservices:l.microservices,onRaw:_,apiBaseUrl:uu.apiBaseUrl});if(f==="apps"&&u==="findjob")return J(CG,{microservices:l.microservices,onRaw:_,apiBaseUrl:uu.apiBaseUrl});if(f==="apps"&&u==="pipeline")return J(PE,{microservices:l.microservices,onRaw:_,apiBaseUrl:uu.apiBaseUrl});if(f==="apps"&&u==="met-nonlinear")return J(vG,{microservices:l.microservices,onRaw:_,apiBaseUrl:uu.apiBaseUrl});if(f==="apps"&&u==="claudeqq")return J(qz,{microservices:l.microservices,onRaw:_,apiBaseUrl:uu.apiBaseUrl});if(f==="apps"&&u==="baidu-netdisk")return J(Ez,{microservices:l.microservices,onRaw:_,apiBaseUrl:uu.apiBaseUrl});if(f==="apps"&&u==="filebrowser")return J(PG,{microservices:l.microservices,onRaw:_,apiBaseUrl:uu.apiBaseUrl});if(f==="apps"&&u==="code-queue")return J(wG,{microservices:l.microservices,onRaw:_,apiBaseUrl:uu.apiBaseUrl,initialTasksData:IS});if(f==="apps"&&u==="project-manager")return J(iE,{microservices:l.microservices,onRaw:_,apiBaseUrl:uu.apiBaseUrl});if(f==="config"&&u==="topology")return J(iP,{data:l});if(f==="config"&&u==="auth")return J(RP,{session:y});if(f==="config"&&u==="security")return J(xP);return J(zu,{title:"未找到页面",text:"请选择左侧主模块和顶部子功能标签"})}function bP({session:f,onLogout:u}){let l=Tj(ul,window.location.pathname),[y,r]=mf(l.moduleId),[_,$]=mf({...H_,[l.moduleId]:l.tabId}),[j,A]=mf({overview:null,nodes:[],systemStatuses:[],dockerStatuses:[],microservices:[],events:[],tasks:[],pendingTasks:[],logs:[]}),[F,U]=mf({ok:!1,text:"连接中"}),[Q,W]=mf(null),[G,K]=mf(new Date),[E,O]=mf(null),[z,Z]=mf(!1),[N,H]=mf(!1),Y=Yy.default.useRef(!1),w=ul.moduleById[y]||ul.modules[0],V=_[y]||H_[y]||w.tabs[0].id,X=Array.isArray(j.microservices)?j.microservices:[],i=X.length===0&&y==="apps"&&V==="code-queue"?[gS]:X,m=i===X?j:{...j,microservices:i},M=y==="apps"?i.find((B)=>String(B?.id||"")===V):null,c=M?WH(M):{},C=w.tabs.find((B)=>B.id===V)?.label||V,T=M?[{key:"microservice",label:"用户服务",value:`${C} ${c.providerStatus==="online"?"在线":c.providerStatus||"未知"}`,tone:c.providerStatus==="online"?"ok":"warn",testId:"active-microservice-status"}]:[];async function R(){if(Y.current)return;Y.current=!0,H(!0);try{let B=[],D=(S,e)=>{B.push([S,Df(e)])},I=y==="ops"&&V==="status",p=I||y==="config"&&V==="topology",k=I||y==="nodes"||y==="tasks"&&V==="dispatch",_f=y==="apps"&&V!=="code-queue";if(p)D("overview",`${uu.apiBaseUrl}/overview`);if(k)D("nodes",`${uu.apiBaseUrl}/nodes`);if(y==="nodes"&&V==="monitor")D("systemStatuses",`${uu.apiBaseUrl}/nodes/system-status?limit=60`),D("tasks",`${uu.apiBaseUrl}/tasks?limit=120&summary=1`);else if(y==="nodes"&&V==="docker")D("dockerStatuses",`${uu.apiBaseUrl}/nodes/docker-status`);else if(y==="nodes"&&V==="gateway")D("tasks",`${uu.apiBaseUrl}/tasks?limit=300&summary=1`);else if(y==="tasks"&&V==="pending")D("pendingTasks",`${uu.apiBaseUrl}/tasks?status=pending&limit=100&summary=1`);else if(y==="tasks"&&(V==="history"||V==="results"))D("tasks",`${uu.apiBaseUrl}/tasks?limit=300&summary=1`);else if(I)D("tasks",`${uu.apiBaseUrl}/tasks?limit=8&lite=1`),D("pendingTasks",`${uu.apiBaseUrl}/tasks?status=pending&limit=20&lite=1`);if(_f)D("microservices",`${uu.apiBaseUrl}/microservices`);if(y==="ops"&&V==="events")D("events",`${uu.apiBaseUrl}/events?limit=100`);if(y==="ops"&&V==="logs")D("logs","/logs?limit=100");await Promise.all(B.map(async([S,e])=>{let $f=await e,Qf={};if(S==="overview")Qf.overview=$f;if(S==="nodes")Qf.nodes=$f.nodes||[];if(S==="systemStatuses")Qf.systemStatuses=$f.systemStatuses||[];if(S==="dockerStatuses")Qf.dockerStatuses=$f.dockerStatuses||[];if(S==="microservices")Qf.microservices=$f.microservices||[];if(S==="events")Qf.events=$f.events||[];if(S==="tasks")Qf.tasks=$f.tasks||[];if(S==="pendingTasks")Qf.pendingTasks=$f.tasks||[];if(S==="logs")Qf.logs=$f.logs||[];A((Af)=>({...Af,...Qf}))})),U({ok:!0,text:"核心在线"}),W(new Date)}catch(B){if(U({ok:!1,text:wf(B,"连接失败")}),B.status===401)u(!1)}finally{Y.current=!1,H(!1)}}y1(()=>{let B=()=>{if(!gE())return;R()};B();let D=setInterval(B,kS(y,V)),I=()=>{if(gE())B()};return document.addEventListener("visibilitychange",I),()=>{clearInterval(D),document.removeEventListener("visibilitychange",I)}},[y,V]),y1(()=>{let B=setInterval(()=>K(new Date),1000);return()=>clearInterval(B)},[]),y1(()=>{let B=pG(ul,window.location.pathname);if(B&&window.location.pathname!==B)window.history.replaceState(null,"",B)},[]),y1(()=>{let B=()=>{let D=Tj(ul,window.location.pathname);r(D.moduleId),$((I)=>({...I,[D.moduleId]:D.tabId})),O(null)};return window.addEventListener("popstate",B),()=>window.removeEventListener("popstate",B)},[]),y1(()=>{window.scrollTo({top:0,left:0,behavior:"auto"})},[y,V]);function P(B,D,I="push"){let p=ul.moduleById[B]?B:ul.fallbackTarget.moduleId,k=ul.moduleById[p]?.tabs.some((S)=>S.id===D)?D:H_[p]||ul.moduleById[p]?.tabs[0]?.id||ul.fallbackTarget.tabId;r(p),$((S)=>({...S,[p]:k}));let _f=V3(ul,p,k);if(window.location.pathname!==_f){let S=I==="replace"?"replaceState":"pushState";window.history[S](null,"",_f)}}function n(B,D){O({title:B,data:D})}return J("div",{className:`shell ${z?"rail-collapsed":""}`,"data-testid":"app-shell"},J($P,{activeModule:y,activeTabs:_,onNavigate:P,collapsed:z,onToggle:()=>Z((B)=>!B)}),J("main",{className:"workspace"},J(_P,{connection:F,lastRefresh:Q,onRefresh:R,onLogout:()=>u(!0),session:f,clock:G,activeStatusItems:T}),J(jP,{module:w,activeTab:V,onNavigate:P}),J(KJ.Provider,{value:N},J(vP,{activeModule:y,activeTab:V,data:m,session:f,refresh:R,onRaw:n,onNavigate:P}))),J(yP,{raw:E,onClose:()=>O(null)}))}function hP(){let[f,u]=mf(!0),[l,y]=mf(null);async function r(){u(!0);try{let $=await Df("/api/session");y($.authenticated?$:null)}catch{y(null)}finally{u(!1)}}async function _($){if($)try{await Df("/logout",{method:"POST"})}catch{}y(null)}if(y1(()=>{r()},[]),f)return J("main",{className:"loading-screen"},J("div",{className:"brand-mark"},"UD"),J("span",null,"加载会话"));if(!l)return J(rP,{onLogin:y});return J(bP,{session:l,onLogout:_})}var ZH=document.getElementById("root");if(ZH===null)throw Error("root element not found");uH.createRoot(ZH).render(J(hP));})(); diff --git a/src/components/frontend/public/style.css b/src/components/frontend/public/style.css index d8b587bc..97979465 100644 --- a/src/components/frontend/public/style.css +++ b/src/components/frontend/public/style.css @@ -124,6 +124,8 @@ code, pre, textarea { font-family: "Cascadia Mono", "IBM Plex Mono", "Liberation color: var(--muted); background: transparent; text-align: left; + text-decoration: none; + cursor: pointer; } .module-code { @@ -165,6 +167,49 @@ h1, h2 { margin: 0; font-weight: 650; } h1 { font-size: 21px; letter-spacing: 0.07em; } h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; } +.loading-title { + display: inline-flex; + align-items: center; + gap: 7px; + max-width: 100%; + margin: 0; + min-width: 0; +} +.loading-title-text { + min-width: 0; + overflow-wrap: anywhere; +} +.loading-title.is-loading .loading-title-text { + color: var(--text); +} +.loading-spinner-indicator { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 14px; + height: 14px; + vertical-align: -3px; +} +.loading-spinner-ring { + display: block; + width: 12px; + height: 12px; + border-radius: 999px; + background: conic-gradient(from 0deg, rgba(105, 174, 232, 0.15), #69aee8, #4eb7a8, rgba(105, 174, 232, 0.15)); + box-shadow: 0 0 0 1px rgba(255,255,255,0.22), 0 0 8px rgba(105,174,232,0.55); + -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 0); + mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 0); + animation: loading-spinner-spin 0.82s linear infinite; +} +@keyframes loading-spinner-spin { + to { transform: rotate(360deg); } +} +@media (prefers-reduced-motion: reduce) { + .loading-spinner-ring { animation-duration: 1.8s; } +} + .status-strip { display: flex; align-items: center; @@ -265,8 +310,12 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; } } .tab { + display: inline-flex; + align-items: center; + justify-content: center; min-width: 112px; padding: 7px 12px; + text-decoration: none; } .tab.active, .tab:hover { @@ -377,9 +426,17 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; } .node-card-head { display: flex; justify-content: space-between; + flex-wrap: wrap; gap: 10px; + min-width: 0; margin-bottom: 8px; } +.node-card-head > * { + min-width: 0; +} +.node-card-head strong { + overflow-wrap: anywhere; +} .node-card code, .compact-row code, td code { display: block; margin-top: 2px; @@ -496,7 +553,7 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; } text-transform: uppercase; font-size: 11px; } -.status-badge.online, .status-badge.succeeded, .status-badge.public { color: var(--ok); border-color: rgba(113, 191, 120, 0.45); } +.status-badge.online, .status-badge.succeeded, .status-badge.public, .status-badge.ok { color: var(--ok); border-color: rgba(113, 191, 120, 0.45); } .status-badge.offline, .status-badge.failed, .status-badge.canceled { color: var(--danger); border-color: rgba(207, 106, 84, 0.45); } .status-badge.running, .status-badge.dispatched, .status-badge.accepted, .status-badge.internal, .status-badge.delivered, .status-badge.applied { color: var(--accent-2); border-color: rgba(78, 183, 168, 0.45); } .status-badge.queued, .status-badge.staged, .status-badge.warn { color: var(--warn); border-color: rgba(215, 161, 58, 0.45); } @@ -1335,7 +1392,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } .result-card dd { margin: 0; } .result-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } -.microservice-page, .findjob-page, .pipeline-page, .met-page, .codex-queue-page { +.microservice-page, .findjob-page, .pipeline-page, .met-page, .code-queue-page, .baidu-netdisk-page, .filebrowser-page { display: grid; gap: 10px; } @@ -1379,6 +1436,112 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } } .findjob-grid .panel:nth-child(3) { grid-column: 1 / -1; } .claudeqq-page .findjob-grid .panel:nth-child(n+3) { grid-column: 1 / -1; } +.filebrowser-hero { + display: grid; + grid-template-columns: minmax(240px, 1.1fr) repeat(4, minmax(150px, 0.72fr)); + gap: 6px; + align-items: stretch; +} +.filebrowser-hero h3 { + margin: 5px 0 0; + color: var(--text); + font-size: 18px; + letter-spacing: 0.04em; +} +.filebrowser-target-grid { + display: grid; + grid-template-columns: repeat(3, minmax(220px, 1fr)); + gap: 6px; +} +.filebrowser-target-card { + position: relative; + display: grid; + gap: 3px; + min-width: 0; + min-height: 84px; + padding: 8px; + border: 1px solid rgba(78, 183, 168, 0.18); + background: + radial-gradient(circle at 100% 0, rgba(78, 183, 168, 0.16), transparent 38%), + linear-gradient(135deg, rgba(215, 161, 58, 0.08), rgba(255,255,255,0.018) 42%, rgba(0,0,0,0.08)), + var(--panel-3); + color: var(--muted); + text-align: left; +} +.filebrowser-target-card:hover, +.filebrowser-target-card.active { + border-color: rgba(215, 161, 58, 0.58); + box-shadow: inset 0 0 0 1px rgba(215, 161, 58, 0.16), 0 12px 32px rgba(0,0,0,0.25); +} +.filebrowser-target-card strong { + min-width: 0; + overflow: hidden; + color: var(--text); + font-size: 14px; + text-overflow: ellipsis; + white-space: nowrap; +} +.filebrowser-target-card code, +.filebrowser-target-card small { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.filebrowser-card-raw { + position: absolute; + right: 8px; + bottom: 8px; + color: var(--accent); + font-size: 10px; + letter-spacing: 0.16em; + text-transform: uppercase; +} +.filebrowser-frame-panel { + min-height: min(88vh, 960px); +} +.filebrowser-frame-panel .panel-body { + padding: 0; +} +.filebrowser-frame-shell { + display: grid; + min-height: min(82vh, 860px); + background: #071018; +} +.filebrowser-frame-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + min-height: 28px; + padding: 4px 8px; + border-bottom: 1px solid rgba(78, 183, 168, 0.18); + background: + linear-gradient(90deg, rgba(78, 183, 168, 0.09), rgba(215, 161, 58, 0.06)), + rgba(0,0,0,0.22); + color: var(--muted); +} +.filebrowser-frame { + width: 100%; + height: min(82vh, 860px); + min-height: 680px; + border: 0; + background: #0b1117; +} +.filebrowser-compact-note { + margin-left: auto; + color: var(--accent); + font-size: 10px; + letter-spacing: 0.12em; + text-transform: uppercase; +} +.filebrowser-shot-error, +.filebrowser-shot-ok { + margin: 4px 0 0; + font-size: 12px; +} +.filebrowser-shot-error { color: var(--danger); } +.filebrowser-shot-ok { color: var(--ok); } .claudeqq-login-card { display: grid; grid-template-columns: 176px minmax(0, 1fr); @@ -1409,6 +1572,132 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } .claudeqq-login-copy .microservice-ref-card { padding: 7px; } +.baidu-netdisk-hero { + display: grid; + grid-template-columns: minmax(280px, 1.4fr) minmax(260px, 0.9fr) minmax(220px, 0.7fr); + gap: 8px; + align-items: stretch; +} +.baidu-netdisk-grid { + display: grid; + grid-template-columns: minmax(380px, 0.9fr) minmax(560px, 1.35fr); + gap: 10px; + align-items: start; +} +.baidu-files-panel, .baidu-transfers-panel, .baidu-wide-panel, .baidu-docs-panel { grid-column: 1 / -1; } +.baidu-doc-grid { + display: grid; + grid-template-columns: repeat(3, minmax(220px, 1fr)); + gap: 8px; +} +.doc-link-card { + display: grid; + gap: 5px; + min-height: 128px; + padding: 10px; + border: 1px solid var(--line-soft); + background: + linear-gradient(135deg, rgba(78, 183, 168, 0.08), transparent 54%), + var(--panel-3); + color: var(--text); + text-decoration: none; +} +.doc-link-card:hover { + border-color: var(--accent); + background: + linear-gradient(135deg, rgba(215, 161, 58, 0.12), rgba(78, 183, 168, 0.06) 54%, transparent), + var(--panel-3); +} +.doc-link-card > span { + color: var(--accent); + font-size: 10px; + letter-spacing: 0.16em; + text-transform: uppercase; +} +.doc-link-card strong { + font-size: 15px; + letter-spacing: 0.04em; +} +.doc-link-card p { + margin: 0; + color: var(--muted); + line-height: 1.42; +} +.doc-link-card code { + margin-top: auto; + color: #bcd2d7; + overflow-wrap: anywhere; +} +.baidu-login-card { + display: grid; + grid-template-columns: 176px minmax(0, 1fr); + gap: 12px; + align-items: center; +} +.baidu-qr-frame { + display: grid; + min-height: 176px; + place-items: center; + padding: 10px; + border: 1px solid rgba(48, 92, 112, 0.45); + background: + radial-gradient(circle at 25% 20%, rgba(78, 183, 168, 0.18), transparent 32%), + linear-gradient(135deg, rgba(236, 246, 240, 0.95), rgba(210, 229, 222, 0.86)); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.62); +} +.baidu-qr-frame img { + width: min(152px, 100%); + height: auto; + image-rendering: pixelated; +} +.baidu-account-card { + display: grid; + gap: 8px; +} +.baidu-account-card h3 { + margin: 0; + font-size: 20px; + letter-spacing: 0.04em; +} +.quota-bar, .baidu-progress { + position: relative; + min-height: 8px; + overflow: hidden; + border: 1px solid var(--line-soft); + background: var(--panel-3); +} +.quota-bar span, .baidu-progress span { + display: block; + height: 100%; + min-width: 2px; + background: linear-gradient(90deg, var(--accent-2), var(--accent)); +} +.baidu-progress { + min-width: 150px; + min-height: 18px; +} +.baidu-progress em { + position: absolute; + inset: 0; + display: grid; + place-items: center; + color: var(--text); + font-size: 10px; + font-style: normal; + text-shadow: 0 1px 2px rgba(0,0,0,0.7); +} +.baidu-pathbar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + margin-bottom: 8px; +} +.baidu-transfer-forms { + display: grid; + grid-template-columns: repeat(2, minmax(260px, 1fr)); + gap: 10px; + margin-bottom: 10px; +} .pipeline-grid { display: grid; grid-template-columns: minmax(360px, 0.9fr) minmax(520px, 1.25fr); @@ -1460,24 +1749,26 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } color: var(--accent-2); font-size: 12px; } -.codex-queue-hero { +.code-queue-hero { display: grid; grid-template-columns: minmax(300px, 1.35fr) minmax(260px, 0.8fr) minmax(220px, 0.65fr); gap: 8px; align-items: stretch; } -.codex-queue-metrics { +.code-queue-metrics { display: grid; grid-template-columns: repeat(6, minmax(130px, 1fr)); gap: 8px; } -.codex-queue-layout { +.code-queue-layout { display: grid; grid-template-columns: minmax(300px, 0.52fr) minmax(0, 1.58fr); gap: 10px; align-items: start; } .codex-session-stage { + display: grid; + gap: 10px; min-width: 0; width: 100%; } @@ -1499,13 +1790,14 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } } .codex-session-sidebar { display: grid; - grid-template-rows: auto auto minmax(0, 1fr); + grid-template-rows: auto auto auto minmax(0, 1fr); gap: 8px; align-self: start; align-content: start; align-items: start; justify-items: stretch; min-width: 0; + overflow: hidden; padding: 10px; border-right: 1px solid var(--line); background: @@ -1540,7 +1832,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } text-overflow: ellipsis; white-space: nowrap; } -.codex-queue-switcher { +.code-queue-switcher { display: grid; gap: 3px; min-width: min(260px, 48vw); @@ -1549,7 +1841,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } letter-spacing: 0.13em; text-transform: uppercase; } -.codex-queue-switcher select { +.code-queue-switcher select { min-width: 0; width: 100%; max-width: 100%; @@ -1562,15 +1854,60 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } #071014; box-sizing: border-box; } -.codex-queue-switcher.compact { +.code-queue-switcher.compact { min-width: 0; width: 100%; margin-top: 2px; } -.codex-queue-switcher.compact select { +.code-queue-switcher.compact select { min-height: 31px; font-size: 12px; } +.codex-task-search { + display: grid; + gap: 6px; + padding: 8px; + border: 1px solid rgba(78, 183, 168, 0.28); + background: + linear-gradient(135deg, rgba(78, 183, 168, 0.10), rgba(215, 161, 58, 0.05)), + rgba(255,255,255,0.025); + box-sizing: border-box; +} +.codex-task-search label { + color: var(--muted); + font-size: 10px; + letter-spacing: 0.13em; + text-transform: uppercase; +} +.codex-task-search-row { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} +.codex-task-search input { + min-width: 0; + width: 100%; + min-height: 32px; + padding: 6px 9px; + border: 1px solid rgba(78, 183, 168, 0.34); + color: var(--text); + background: #071014; + box-sizing: border-box; +} +.codex-task-search input:focus { + outline: 2px solid rgba(78, 183, 168, 0.24); + outline-offset: 1px; +} +.codex-task-search .ghost-btn { + flex: 0 0 auto; + padding: 6px 8px; +} +.codex-task-search small { + color: var(--muted); + font-size: 11px; + line-height: 1.35; +} .codex-output-panel .panel-summary { margin-top: 6px; } @@ -1615,6 +1952,9 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } linear-gradient(135deg, rgba(215, 161, 58, 0.13), rgba(78, 183, 168, 0.06)), rgba(0,0,0,0.18); } +.codex-trace-status-chip.service { + white-space: normal; +} .codex-mark-all-read-btn { border-color: rgba(78, 183, 168, 0.40); color: #bdece4; @@ -1630,11 +1970,295 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } display: grid; gap: 10px; } +.codex-main-stage > .codex-stats-panel { + width: 100%; +} .codex-run-control-stack { display: grid; gap: 10px; min-width: 0; } +.codex-stats-panel .panel-body { + display: grid; + gap: 9px; +} +.codex-stats-hero { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 10px; + align-items: center; + min-width: 0; + padding: 9px; + border: 1px solid rgba(78, 183, 168, 0.30); + background: + radial-gradient(circle at 0 0, rgba(78, 183, 168, 0.18), transparent 45%), + linear-gradient(135deg, rgba(215, 161, 58, 0.09), rgba(255,255,255,0.018)), + #071014; +} +.codex-stats-hero > div { + display: grid; + gap: 3px; + min-width: 0; +} +.codex-stats-hero strong { + color: var(--text); + font-size: 18px; + letter-spacing: 0.04em; +} +.codex-stats-hero span:not(.codex-stats-icon) { + color: var(--muted); + font-size: 12px; +} +.codex-stats-icon { + display: grid; + width: 48px; + height: 48px; + place-items: center; + border: 1px solid rgba(215, 161, 58, 0.42); + background: + linear-gradient(135deg, rgba(215, 161, 58, 0.22), rgba(78, 183, 168, 0.10)), + #11110b; + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.03), 0 12px 24px rgba(0,0,0,0.28); +} +.codex-stats-icon svg { + width: 36px; + height: 24px; +} +.codex-stats-icon .grid { + fill: none; + stroke: rgba(255,255,255,0.18); + stroke-width: 1; +} +.codex-stats-icon .line { + fill: none; + stroke-width: 2.2; + stroke-linecap: round; + stroke-linejoin: round; +} +.codex-stats-icon .line.tasks { stroke: var(--accent-2); } +.codex-stats-icon .line.retry { stroke: var(--accent); } +.codex-stats-chart { + position: relative; + display: grid; + gap: 7px; + min-width: 0; + padding: 10px; + border: 1px solid var(--line-soft); + background: + linear-gradient(rgba(78, 183, 168, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(78, 183, 168, 0.025) 1px, transparent 1px), + #071014; + background-size: 18px 18px, 18px 18px, auto; +} +.codex-stats-chart svg { + width: 100%; + height: clamp(180px, 24vw, 260px); + overflow: visible; +} +.codex-stats-chart .axis, +.codex-stats-chart .grid { + stroke: rgba(255,255,255,0.14); + stroke-width: 1; + vector-effect: non-scaling-stroke; +} +.codex-stats-chart .grid { + stroke-dasharray: 2 3; +} +.codex-stats-chart .stat-line { + fill: none; + stroke-width: 2.2; + vector-effect: non-scaling-stroke; + stroke-linecap: round; + stroke-linejoin: round; +} +.codex-stats-chart .stat-line.tasks { stroke: var(--accent-2); } +.codex-stats-chart .stat-line.retry { stroke: var(--accent); } +.codex-stats-chart .stat-line.duration { stroke: #9db7ff; } +.codex-stats-chart .stat-point-group { + outline: none; + cursor: pointer; +} +.codex-stats-chart .stat-hit-point { + fill: transparent; + stroke: transparent; + pointer-events: all; +} +.codex-stats-chart .stat-point { + fill: #071014; + stroke-width: 2.4; + vector-effect: non-scaling-stroke; + transition: r 150ms ease, fill 150ms ease, filter 150ms ease; +} +.codex-stats-chart .stat-point.tasks { stroke: var(--accent-2); } +.codex-stats-chart .stat-point.retry { stroke: var(--accent); } +.codex-stats-chart .stat-point.duration { stroke: #9db7ff; } +.codex-stats-chart .stat-point-group:hover .stat-point, +.codex-stats-chart .stat-point-group:focus .stat-point, +.codex-stats-chart .stat-point.active { + fill: currentColor; + filter: drop-shadow(0 0 7px rgba(78, 183, 168, 0.55)); +} +.codex-stats-chart .stat-point-active { + fill: rgba(255,255,255,0.12); + stroke-width: 1.4; + vector-effect: non-scaling-stroke; + pointer-events: none; +} +.codex-stats-chart .stat-point-active.tasks { stroke: var(--accent-2); } +.codex-stats-chart .stat-point-active.retry { stroke: var(--accent); } +.codex-stats-chart .stat-point-active.duration { stroke: #9db7ff; } +.codex-stats-chart .stat-cursor { + stroke: rgba(255,255,255,0.28); + stroke-dasharray: 4 4; + stroke-width: 1; + vector-effect: non-scaling-stroke; + pointer-events: none; +} +.codex-stats-tooltip { + position: absolute; + z-index: 3; + display: grid; + gap: 3px; + min-width: 190px; + max-width: min(280px, calc(100% - 20px)); + padding: 8px 10px; + border: 1px solid rgba(215, 161, 58, 0.46); + background: + radial-gradient(circle at 0 0, rgba(215, 161, 58, 0.18), transparent 46%), + linear-gradient(135deg, rgba(6, 10, 13, 0.98), rgba(12, 24, 28, 0.96)); + box-shadow: 0 16px 36px rgba(0,0,0,0.38), inset 0 0 0 1px rgba(255,255,255,0.04); + color: var(--text); + font-size: 12px; + line-height: 1.35; + pointer-events: none; + transform: translate(-50%, calc(-100% - 12px)); +} +.codex-stats-tooltip b { + color: var(--accent); + font-size: 13px; +} +.codex-stats-tooltip span { + color: var(--text); +} +.codex-stats-tooltip code { + color: var(--accent-2); + white-space: normal; +} +.codex-stats-focus { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 8px; + min-height: 36px; + padding: 7px 9px; + border: 1px dashed rgba(78, 183, 168, 0.25); + color: var(--muted); + background: rgba(255,255,255,0.018); + font-size: 12px; +} +.codex-stats-focus.active { + border-style: solid; + border-color: rgba(215, 161, 58, 0.38); + color: var(--text); + background: linear-gradient(135deg, rgba(215, 161, 58, 0.09), rgba(78, 183, 168, 0.05)); +} +.codex-stats-focus > div { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + min-width: 0; +} +.codex-stats-focus strong { + color: var(--accent); +} +.codex-stats-focus-metrics code { + color: var(--accent-2); +} +.codex-stats-legend, +.codex-stats-scale { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 6px; + color: var(--muted); + font-size: 11px; +} +.codex-stats-legend { + justify-content: flex-start; +} +.codex-stats-legend span { + display: inline-flex; + align-items: center; + gap: 5px; +} +.codex-stats-legend span::before { + content: ""; + width: 14px; + height: 2px; + background: var(--accent-2); +} +.codex-stats-legend .retry::before { background: var(--accent); } +.codex-stats-legend .duration::before { background: #9db7ff; } +.codex-stats-summary-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 7px; +} +.codex-stats-summary-grid article { + display: grid; + gap: 3px; + min-width: 0; + padding: 8px; + border: 1px solid rgba(78, 183, 168, 0.22); + background: rgba(255,255,255,0.024); +} +.codex-stats-summary-grid span { + color: var(--muted); + font-size: 10px; + letter-spacing: 0.12em; + text-transform: uppercase; +} +.codex-stats-summary-grid strong { + color: var(--text); + font-size: 18px; +} +.codex-stats-summary-grid code { + min-width: 0; + overflow: hidden; + color: var(--accent-2); + text-overflow: ellipsis; + white-space: nowrap; +} +.codex-stats-daily-list { + display: grid; + gap: 5px; +} +.codex-stats-daily-row { + display: grid; + grid-template-columns: 50px repeat(2, minmax(58px, 1fr)) minmax(72px, 1fr); + gap: 5px; + align-items: center; + padding: 5px 7px; + border: 1px solid rgba(255,255,255,0.06); + background: rgba(255,255,255,0.018); + color: var(--muted); + font-size: 11px; +} +.codex-stats-daily-row b { + color: var(--text); + font-weight: 650; +} +.codex-stats-daily-row code { + color: #9db7ff; + text-align: right; +} +.codex-stats-daily-row.active { + border-color: rgba(215, 161, 58, 0.34); + background: rgba(215, 161, 58, 0.07); +} .codex-task-move-control { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -1694,14 +2318,30 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } .codex-submit-queue-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: + "queue-select queue-select" + "rename rename" + ". create"; gap: 6px; min-width: 0; align-items: center; } +.codex-submit-queue-row select { + grid-area: queue-select; +} +.codex-rename-queue-btn, .codex-create-queue-btn { min-height: 32px; white-space: nowrap; } +.codex-rename-queue-btn { + grid-area: rename; + width: 100%; + min-height: 36px; +} +.codex-create-queue-btn { + grid-area: create; +} .codex-reference-field { display: grid; gap: 5px; @@ -1761,11 +2401,13 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } max-height: calc(100vh - 460px); min-height: 180px; overflow: auto; + overflow-x: hidden; } .codex-task-list-session { align-self: start; min-height: 0; - max-height: calc(100vh - 318px); + max-height: calc(100vh - 386px); + overflow-x: hidden; } .codex-task-pagination { display: flex; @@ -1832,6 +2474,10 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } display: grid; gap: 6px; width: 100%; + min-width: 0; + overflow: hidden; + word-break: break-word; + white-space: normal; padding: 9px; border: 1px solid var(--line-soft); color: var(--muted); @@ -1849,7 +2495,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } #0c181c; } .codex-task-card.unread-terminal { - padding-right: 23px; + padding-left: 23px; } .codex-task-card:focus-visible { outline: 2px solid var(--accent); @@ -1874,7 +2520,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } .codex-unread-badge { position: absolute; top: 7px; - right: 7px; + left: 7px; width: 9px; height: 9px; padding: 0; @@ -1934,6 +2580,20 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } color: var(--muted); font-size: 11px; } +.codex-task-update-meta { + flex-wrap: wrap; +} +.codex-task-recent-update { + color: #bdece4; + font-weight: 700; +} +.codex-task-step-count { + padding: 2px 6px; + border: 1px solid rgba(78, 183, 168, 0.32); + border-radius: 999px; + background: rgba(78, 183, 168, 0.08); + letter-spacing: 0.04em; +} .codex-output-panel .panel-body { padding: 0; } @@ -2126,16 +2786,23 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } .codex-attempt-cycle-head { display: flex; align-items: center; + flex-wrap: wrap; gap: 8px; min-width: 0; } .codex-attempt-cycle-head strong { + min-width: 0; + overflow-wrap: anywhere; color: var(--text); } .codex-attempt-cycle-head code { + flex: 1 1 280px; + min-width: 0; margin-left: auto; color: var(--muted); - white-space: nowrap; + overflow-wrap: anywhere; + text-align: right; + white-space: normal; } .codex-progressive-card { min-width: 0; @@ -2144,6 +2811,40 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } linear-gradient(135deg, rgba(78, 183, 168, 0.08), rgba(215, 161, 58, 0.035) 46%, transparent), rgba(6, 10, 13, 0.84); } +.codex-execution-summary.running { + border-color: rgba(89, 224, 193, 0.58); + background: + linear-gradient(115deg, rgba(89, 224, 193, 0.18), rgba(215, 161, 58, 0.09) 46%, rgba(89, 224, 193, 0.04)), + rgba(6, 10, 13, 0.9); + box-shadow: 0 0 0 1px rgba(89, 224, 193, 0.12), 0 0 22px rgba(89, 224, 193, 0.16); + animation: codexSummaryRunningPulse 1.8s ease-in-out infinite; +} +.codex-execution-summary.running > summary .codex-progressive-card-head { + background: + linear-gradient(90deg, rgba(89, 224, 193, 0.18), rgba(215, 161, 58, 0.08), transparent 76%); +} +.codex-summary-running-pill { + display: inline-flex; + align-items: center; + gap: 5px; + flex: 0 0 auto; + padding: 2px 7px; + border: 1px solid rgba(89, 224, 193, 0.46); + color: #c7fff4; + background: rgba(89, 224, 193, 0.12); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.08em; +} +.codex-summary-running-pill::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 999px; + background: #59e0c1; + box-shadow: 0 0 10px rgba(89, 224, 193, 0.9); + animation: codexSummaryRunningDot 0.9s ease-in-out infinite; +} .codex-progressive-card-head { display: flex; align-items: center; @@ -2163,6 +2864,16 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } color: var(--accent-2); white-space: nowrap; } +.codex-execution-summary > summary .codex-progressive-card-head { + flex-wrap: wrap; +} +.codex-execution-summary > summary .codex-progressive-card-head code { + flex: 1 1 260px; + min-width: 0; + overflow-wrap: anywhere; + text-align: right; + white-space: normal; +} .codex-judge-feedback-prompt { border-color: rgba(148, 190, 255, 0.26); background: @@ -2220,6 +2931,12 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } text-overflow: ellipsis; white-space: nowrap; } +.codex-execution-digest .codex-execution-error-pill { + border-color: rgba(207, 106, 84, 0.52); + color: var(--danger); + background: rgba(207, 106, 84, 0.08); + font-weight: 750; +} .codex-trace-step-list { display: grid; gap: 7px; @@ -2230,6 +2947,12 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.025); } +.codex-trace-step.error { + border-color: rgba(207, 106, 84, 0.42); + background: + linear-gradient(135deg, rgba(207, 106, 84, 0.10), rgba(215, 161, 58, 0.025) 58%, transparent), + rgba(255,255,255,0.022); +} .codex-trace-step > summary { display: flex; align-items: center; @@ -2238,6 +2961,9 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } padding: 7px 9px; cursor: pointer; } +.codex-trace-step.error > summary { + background: linear-gradient(90deg, rgba(207, 106, 84, 0.12), rgba(207, 106, 84, 0.045) 56%, transparent); +} .codex-trace-step > summary strong { min-width: 0; overflow: hidden; @@ -2254,6 +2980,14 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } color: var(--accent-2); white-space: nowrap; } +.codex-trace-step.error > summary .codex-output-channel { + color: var(--danger); + border-color: rgba(207, 106, 84, 0.52); + background: rgba(207, 106, 84, 0.08); +} +.codex-trace-step.error > summary code { + color: var(--danger); +} .codex-trace-step-summary { display: grid; gap: 2px; @@ -2268,8 +3002,8 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } font-size: 11px; line-height: 1.42; } -.codex-step-detail-transcript { - min-height: 0; +.codex-transcript.codex-step-detail-transcript { + min-height: auto; max-height: 520px; margin: 0 8px 8px; padding: 8px; @@ -2283,9 +3017,162 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } .codex-final-response .codex-transcript-body { max-height: 520px; overflow: auto; - white-space: pre-wrap; + white-space: normal; overflow-wrap: anywhere; } +.codex-markdown { + display: block; + padding: 8px 10px 10px; +} +.markdown-body { + color: #d9e8e7; + font-size: 12px; + line-height: 1.58; + overflow-wrap: anywhere; +} +.markdown-body > :first-child { + margin-top: 0; +} +.markdown-body > :last-child { + margin-bottom: 0; +} +.markdown-body p { + margin: 0 0 10px; +} +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin: 14px 0 7px; + color: var(--text); + font-weight: 750; + line-height: 1.25; + letter-spacing: 0.03em; + text-transform: none; +} +.markdown-body h3 { + font-size: 15px; +} +.markdown-body h4 { + font-size: 14px; +} +.markdown-body h5, +.markdown-body h6 { + font-size: 13px; +} +.markdown-body a { + color: #8fc7ee; + text-decoration: none; + border-bottom: 1px solid rgba(143, 199, 238, 0.38); +} +.markdown-body a:hover { + color: #b6daff; + border-bottom-color: rgba(182, 218, 255, 0.72); +} +.markdown-body code { + padding: 1px 4px; + border: 1px solid rgba(78, 183, 168, 0.20); + border-radius: 4px; + color: #b6da89; + background: rgba(78, 183, 168, 0.08); + font-size: 0.92em; +} +.markdown-body pre { + margin: 8px 0 10px; + padding: 9px 10px; + overflow: auto; + border: 1px solid rgba(78, 183, 168, 0.22); + border-radius: 6px; + background: + linear-gradient(135deg, rgba(78, 183, 168, 0.08), rgba(215, 161, 58, 0.035)), + rgba(2, 6, 8, 0.72); + white-space: pre; +} +.markdown-body pre code { + display: block; + padding: 0; + border: 0; + color: #d9e8e7; + background: transparent; + font-size: 11px; + line-height: 1.55; +} +.markdown-body ul, +.markdown-body ol { + margin: 0 0 10px; + padding-left: 22px; +} +.markdown-body li { + margin: 3px 0; + padding-left: 2px; +} +.markdown-body li::marker { + color: var(--accent); +} +.markdown-body .task-list-item { + display: flex; + align-items: flex-start; + gap: 7px; + margin-left: -20px; + list-style: none; +} +.markdown-body .task-list-item input { + width: 13px; + height: 13px; + margin: 3px 0 0; + accent-color: var(--accent-2); +} +.markdown-body blockquote { + margin: 8px 0 10px; + padding: 7px 10px; + border-left: 3px solid rgba(215, 161, 58, 0.70); + color: #c8d8dc; + background: rgba(215, 161, 58, 0.055); +} +.markdown-body hr { + height: 1px; + margin: 12px 0; + border: 0; + background: linear-gradient(90deg, rgba(215, 161, 58, 0.65), rgba(78, 183, 168, 0.32), transparent); +} +.markdown-body .markdown-table-wrap { + max-width: 100%; + margin: 8px 0 10px; + overflow: auto; +} +.markdown-body table { + width: 100%; + border-collapse: collapse; + min-width: 360px; +} +.markdown-body th, +.markdown-body td { + padding: 6px 8px; + border: 1px solid rgba(78, 183, 168, 0.18); + vertical-align: top; +} +.markdown-body th { + color: var(--text); + background: rgba(78, 183, 168, 0.10); + font-weight: 750; +} +.markdown-body td { + background: rgba(255,255,255,0.018); +} +@keyframes codexSummaryRunningPulse { + 0%, 100% { box-shadow: 0 0 0 1px rgba(89, 224, 193, 0.10), 0 0 18px rgba(89, 224, 193, 0.12); } + 50% { box-shadow: 0 0 0 1px rgba(89, 224, 193, 0.32), 0 0 30px rgba(89, 224, 193, 0.30); } +} +@keyframes codexSummaryRunningDot { + 0%, 100% { opacity: 0.45; transform: scale(0.82); } + 50% { opacity: 1; transform: scale(1.15); } +} +@media (prefers-reduced-motion: reduce) { + .codex-execution-summary.running, + .codex-summary-running-pill::before { + animation-duration: 3s; + } +} .codex-edit-observation { min-width: 0; margin-top: 2px; @@ -4028,10 +4915,17 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } .draft-card { display: grid; gap: 6px; + min-width: 0; padding: 9px; border: 1px solid var(--line-soft); background: var(--panel-3); } +.draft-card > span, +.draft-card > code { + min-width: 0; + overflow-wrap: anywhere; + white-space: normal; +} .endpoint-list article { grid-template-columns: 150px minmax(220px, 1fr) auto; } .policy-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } @@ -4240,11 +5134,11 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } } @media (max-width: 1120px) { - .metric-grid, .policy-grid, .security-board, .docker-metrics, .monitor-chart-grid, .monitor-summary-grid, .performance-metric-stack, .codex-load-test-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .metric-grid, .policy-grid, .security-board, .docker-metrics, .monitor-chart-grid, .monitor-summary-grid, .performance-metric-stack, .codex-load-test-grid, .baidu-doc-grid, .filebrowser-target-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .pipeline-oa-guarantees { grid-template-columns: repeat(2, minmax(0, 1fr)); } .dispatch-form { grid-template-columns: 1fr 1fr; } .dispatch-actions { align-items: center; } - .page-grid, .docker-layout, .monitor-layout, .performance-top-grid, .performance-grid, .findjob-grid, .findjob-hero, .pipeline-grid, .pipeline-hero, .met-grid, .met-form-grid, .codex-queue-layout, .codex-queue-hero, .codex-detail-grid, .project-manager-hero, .project-manager-layout { grid-template-columns: 1fr; } + .page-grid, .docker-layout, .monitor-layout, .performance-top-grid, .performance-grid, .findjob-grid, .findjob-hero, .pipeline-grid, .pipeline-hero, .met-grid, .met-form-grid, .code-queue-layout, .code-queue-hero, .codex-detail-grid, .project-manager-hero, .project-manager-layout, .baidu-netdisk-grid, .baidu-netdisk-hero, .baidu-transfer-forms, .filebrowser-hero { grid-template-columns: 1fr; } .codex-session-shell { grid-template-columns: minmax(260px, 0.42fr) minmax(0, 1fr); position: relative; } .codex-session-shell.queue-collapsed { grid-template-columns: minmax(0, 1fr); } .codex-session-sidebar { border-right: 1px solid var(--line); border-bottom: 0; } @@ -4255,7 +5149,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } .pipeline-gantt-detail-layout.detail-open, .pipeline-gantt-detail-layout.detail-collapsed { grid-template-columns: 1fr; } .pipeline-node-control { max-height: none; min-height: 0; } - .findjob-grid .panel:nth-child(3), .claudeqq-page .findjob-grid .panel:nth-child(n+3), .pipeline-grid .panel:nth-child(3), .pipeline-grid .panel:nth-child(5), .met-grid .panel:nth-child(3), .met-grid .panel:nth-child(5), .met-detail-panel { grid-column: 1; } + .findjob-grid .panel:nth-child(3), .claudeqq-page .findjob-grid .panel:nth-child(n+3), .pipeline-grid .panel:nth-child(3), .pipeline-grid .panel:nth-child(5), .met-grid .panel:nth-child(3), .met-grid .panel:nth-child(5), .met-detail-panel, .baidu-files-panel, .baidu-transfers-panel, .baidu-wide-panel, .baidu-docs-panel { grid-column: 1; } .gateway-record-grid { grid-template-columns: 1fr; } .overview-grid .panel:nth-child(n+3), .dispatch-grid .panel:first-child, .topology-grid .panel:nth-child(3) { grid-column: 1; } } @@ -4274,6 +5168,12 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } grid-template-columns: 1fr; } .performance-hero { flex-direction: column; } + .filebrowser-target-grid { grid-template-columns: 1fr; } + .filebrowser-frame, + .filebrowser-frame-shell { + min-height: 520px; + height: 70vh; + } .pipeline-wide-panel .panel-head { align-items: flex-start; flex-direction: column; @@ -4423,20 +5323,40 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } padding: 4px 9px; white-space: nowrap; } - .metric-grid, .policy-grid, .security-board, .dispatch-form, .docker-metrics, .monitor-chart-grid, .monitor-summary-grid, .gateway-record-grid, .met-detail-kv, .codex-queue-metrics, .codex-form-grid { grid-template-columns: 1fr; } - .compact-row, .heartbeat-row, .log-row, .endpoint-list article, .volume-route, .findjob-hero, .pipeline-hero, .codex-queue-hero, .claudeqq-login-card { grid-template-columns: 1fr; align-items: start; } + .metric-grid, .policy-grid, .security-board, .dispatch-form, .docker-metrics, .monitor-chart-grid, .monitor-summary-grid, .gateway-record-grid, .met-detail-kv, .code-queue-metrics, .codex-stats-summary-grid, .codex-form-grid, .baidu-doc-grid { grid-template-columns: 1fr; } + .compact-row, .heartbeat-row, .log-row, .endpoint-list article, .volume-route, .findjob-hero, .pipeline-hero, .code-queue-hero, .claudeqq-login-card, .baidu-login-card, .baidu-pathbar { grid-template-columns: 1fr; align-items: start; } .codex-output-line { grid-template-columns: 1fr; } .codex-transcript { min-height: 360px; } .codex-output-panel .panel-head { align-items: flex-start; flex-direction: column; } .codex-output-panel .panel-head > div:first-child, .codex-output-panel .panel-actions { width: 100%; } .codex-output-panel .panel-actions { justify-content: flex-start; } - .codex-queue-switcher { width: 100%; min-width: 0; } - .codex-submit-queue-row { grid-template-columns: 1fr; } - .codex-create-queue-btn { width: 100%; } + .code-queue-switcher { width: 100%; min-width: 0; } + .codex-submit-queue-row { + grid-template-columns: 1fr; + grid-template-areas: + "queue-select" + "rename" + "create"; + } + .codex-rename-queue-btn, .codex-create-queue-btn { width: 100%; } .codex-session-title-toggle { min-height: 40px; padding: 9px 15px; font-size: 14px; } + .codex-attempt-cycle-head { align-items: flex-start; } + .codex-attempt-cycle-head code { + flex-basis: 100%; + margin-left: 0; + text-align: left; + } + .codex-execution-summary > summary .codex-progressive-card-head { align-items: flex-start; } + .codex-execution-summary > summary .codex-progressive-card-head code { + flex-basis: 100%; + margin-left: 0; + text-align: left; + } .codex-task-move-control { grid-template-columns: 1fr; } .codex-task-move-control .ghost-btn { width: 100%; } + .codex-stats-daily-row { grid-template-columns: 1fr 1fr; } + .codex-stats-daily-row code { text-align: left; } .codex-session-shell { display: block; position: relative; @@ -4452,7 +5372,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } box-shadow: 26px 0 48px rgba(0, 0, 0, 0.44); } .codex-session-sidebar .codex-task-list-session { - max-height: calc(100vh - 312px); + max-height: calc(100vh - 382px); } .codex-session-main { min-width: 0; diff --git a/src/components/frontend/src/app.tsx b/src/components/frontend/src/app.tsx index f0ff44ba..69331400 100644 --- a/src/components/frontend/src/app.tsx +++ b/src/components/frontend/src/app.tsx @@ -1,8 +1,10 @@ import React from "react"; import { BEIJING_TIME_LABEL, fmtClock, fmtDate } from "./time"; import { createRoot } from "react-dom/client"; +import { BaiduNetdiskPage } from "./baidu-netdisk"; import { ClaudeQqPage } from "./claudeqq"; -import { CodexQueuePage } from "./codex-queue"; +import { CodeQueuePage } from "./code-queue"; +import { FileBrowserPage } from "./filebrowser"; import { FindJobPage } from "./findjob"; import { MetNonlinearPage } from "./met-nonlinear"; import { canonicalizeKnownRoute, createRouteRegistry, DEFAULT_ACTIVE_TABS, MODULES, pathForTarget, resolveRouteTarget } from "./navigation"; @@ -10,6 +12,7 @@ import { PipelinePage } from "./pipeline"; import { ProjectManagerPage } from "./project-manager"; import { TodoNotePage } from "./todo-note"; import { TopStatusBar } from "./top-status"; +import { LoadingTitle } from "./loading-indicator"; import { errorMessage, requestJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; @@ -27,20 +30,21 @@ function readRootJsonAttribute(name: string, fallback: any): any { } const cfg: AnyRecord = readRootJsonAttribute("data-config", { apiBaseUrl: "/api", authUsername: "admin" }); -const initialCodexQueueOverview = readRootJsonAttribute("data-codex-overview", null); +const initialCodeQueueOverview = readRootJsonAttribute("data-codex-overview", null); const h = React.createElement; const { useEffect, useMemo } = React; const useState: any = React.useState; +const LoadingContext = React.createContext(false); const ROUTE_REGISTRY = createRouteRegistry(MODULES); -const fastCodexQueueService = { - id: "codex-queue", - name: "Codex Queue", +const fastCodeQueueService = { + id: "code-queue", + name: "Code Queue", providerId: "main-server", - description: "Codex Queue", - repository: { containerName: "codex-queue-backend" }, + description: "Code Queue", + repository: { containerName: "code-queue-backend" }, backend: { - nodeBaseUrl: "http://codex-queue:4222", - nodeBindHost: "codex-queue", + nodeBaseUrl: "http://code-queue:4222", + nodeBindHost: "code-queue", nodePort: 4222, public: false, }, @@ -356,12 +360,14 @@ function MetricCard({ label, value, hint, tone, onClick, testId }: AnyRecord) { ); } -function Panel({ title, eyebrow, actions, children, className }: AnyRecord) { +function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) { + const contextLoading = React.useContext(LoadingContext); + const isLoading = Boolean(loading) || contextLoading; return h("section", { className: `panel ${className || ""}` }, h("div", { className: "panel-head" }, h("div", null, eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null, - h("h2", null, title), + h(LoadingTitle, { title, loading: isLoading }), ), actions ? h("div", { className: "panel-actions" }, actions) : null, ), @@ -518,6 +524,47 @@ function TopBar({ connection, lastRefresh, onRefresh, onLogout, session, clock, ); } +type NavigateHandler = (moduleId: string, tabId: string, historyMode?: "push" | "replace") => void; + +interface RouteAnchorProps { + moduleId: string; + tabId: string; + className: string; + active?: boolean; + title?: string; + testId?: string; + onNavigate: NavigateHandler; + children?: React.ReactNode; +} + +function shouldHandleSpaRouteClick(event: React.MouseEvent): boolean { + return !event.defaultPrevented + && event.button === 0 + && !event.metaKey + && !event.altKey + && !event.ctrlKey + && !event.shiftKey + && event.currentTarget.target !== "_blank"; +} + +function RouteAnchor({ moduleId, tabId, className, active = false, title, testId, onNavigate, children }: RouteAnchorProps) { + const route = pathForTarget(ROUTE_REGISTRY, moduleId, tabId); + return h("a", { + href: route, + role: "button", + className, + title, + "aria-current": active ? "page" : undefined, + "data-testid": testId, + "data-route": route, + onClick: (event: React.MouseEvent) => { + if (!shouldHandleSpaRouteClick(event)) return; + event.preventDefault(); + onNavigate(moduleId, tabId); + }, + }, children); +} + function Sidebar({ activeModule, activeTabs, onNavigate, collapsed, onToggle }: AnyRecord) { return h("aside", { className: `rail ${collapsed ? "collapsed" : ""}`, "aria-label": "主模块" }, h("div", { className: "brand" }, @@ -525,25 +572,30 @@ function Sidebar({ activeModule, activeTabs, onNavigate, collapsed, onToggle }: h("span", { className: "brand-text" }, "UniDesk"), h("button", { type: "button", className: "rail-toggle", onClick: onToggle, "aria-label": collapsed ? "展开左侧边栏" : "收起左侧边栏", "data-testid": "rail-toggle" }, collapsed ? "»" : "«"), ), - MODULES.map((module: any) => h("button", { - key: module.id, - type: "button", - className: `module ${activeModule === module.id ? "active" : ""}`, - onClick: () => onNavigate(module.id, activeTabs[module.id] || DEFAULT_ACTIVE_TABS[module.id] || module.tabs[0]?.id || ""), - title: module.label, - "data-route": pathForTarget(ROUTE_REGISTRY, module.id, activeTabs[module.id] || DEFAULT_ACTIVE_TABS[module.id] || module.tabs[0]?.id || ""), - }, h("span", { className: "module-code" }, module.code), h("span", null, module.label))), + MODULES.map((module: any) => { + const tabId = activeTabs[module.id] || DEFAULT_ACTIVE_TABS[module.id] || module.tabs[0]?.id || ""; + return h(RouteAnchor, { + key: module.id, + moduleId: module.id, + tabId, + className: `module ${activeModule === module.id ? "active" : ""}`, + active: activeModule === module.id, + title: module.label, + onNavigate, + }, h("span", { className: "module-code" }, module.code), h("span", null, module.label)); + }), ); } function TabBar({ module, activeTab, onNavigate }: AnyRecord) { return h("nav", { className: "tabs", "aria-label": `${module.label} 子功能` }, - module.tabs.map((tab: any) => h("button", { + module.tabs.map((tab: any) => h(RouteAnchor, { key: tab.id, - type: "button", + moduleId: module.id, + tabId: tab.id, className: `tab ${activeTab === tab.id ? "active" : ""}`, - onClick: () => onNavigate(module.id, tab.id), - "data-route": pathForTarget(ROUTE_REGISTRY, module.id, tab.id), + active: activeTab === tab.id, + onNavigate, }, tab.label)), ); } @@ -796,6 +848,7 @@ function ProcessMeter({ value, label, tone }: AnyRecord) { function ProcessResourceTable({ current, onRaw }: AnyRecord) { const [sort, setSort] = useState({ key: "memory", direction: "desc" }); + const contextLoading = React.useContext(LoadingContext); const processSummary = current?.processSummary && typeof current.processSummary === "object" ? current.processSummary : {}; const processes = Array.isArray(current?.processes) ? current.processes : []; const rows = useMemo(() => { @@ -828,7 +881,7 @@ function ProcessResourceTable({ current, onRaw }: AnyRecord) { h("div", { className: "process-resource-head" }, h("div", null, h("p", { className: "panel-eyebrow" }, "Windows Resource Monitor Style"), - h("h3", null, "进程资源占用"), + h(LoadingTitle, { title: "进程资源占用", level: 3, loading: contextLoading }), ), h("div", { className: "process-resource-actions" }, h("span", { className: "data-chip" }, "默认按内存排序"), @@ -1013,12 +1066,12 @@ function PerformancePage({ onRaw }: AnyRecord) { return () => clearInterval(timer); }, []); - async function runCodexQueueLoadTest(): Promise { + async function runCodeQueueLoadTest(): Promise { setCodexPerfLoading(true); setError(""); setCodexPerf(null); try { - const result = await requestJson(`${cfg.apiBaseUrl}/codex-queue-load-test`, { + const result = await requestJson(`${cfg.apiBaseUrl}/code-queue-load-test`, { method: "POST", body: JSON.stringify({ targetMs: 1000, @@ -1029,7 +1082,7 @@ function PerformancePage({ onRaw }: AnyRecord) { setCodexPerf(result); void load(); } catch (err) { - setError(errorMessage(err, "Codex Queue Playwright 测量失败")); + setError(errorMessage(err, "Code Queue Playwright 测量失败")); } finally { setCodexPerfLoading(false); } @@ -1041,7 +1094,7 @@ function PerformancePage({ onRaw }: AnyRecord) { const slowRows = slowOperationRows(snapshot); const backendProcess = snapshot.core?.process || {}; const frontendProcess = snapshot.frontend?.process || {}; - const codexStorage = snapshot.core?.database?.codexQueueStorage || {}; + const codexStorage = snapshot.core?.database?.codeQueueStorage || {}; const codexTotal = asNumber(codexStorage.total); const codexPerfResult = codexPerf?.result || {}; const codexPerfWallMs = asNumber(codexPerfResult.wallMs, NaN); @@ -1059,11 +1112,11 @@ function PerformancePage({ onRaw }: AnyRecord) { h("div", { className: "performance-hero" }, h("div", null, h("p", { className: "panel-eyebrow" }, "Unified Performance"), - h("h2", null, "性能面板"), + h(LoadingTitle, { title: "性能面板", loading: loading || codexPerfLoading }), h("p", null, "按组件统计 HTTP 请求、失败率、P95 延迟,并汇总 backend/frontend 内部操作耗时。"), ), h("div", { className: "inline-actions" }, - h("button", { type: "button", className: "ghost-btn", onClick: () => void runCodexQueueLoadTest(), disabled: codexPerfLoading, "data-testid": "codex-queue-load-test-button" }, codexPerfLoading ? "测试中..." : "测试 Codex Queue 加载"), + h("button", { type: "button", className: "ghost-btn", onClick: () => void runCodeQueueLoadTest(), disabled: codexPerfLoading, "data-testid": "code-queue-load-test-button" }, codexPerfLoading ? "测试中..." : "测试 Code Queue 加载"), h("button", { type: "button", className: "ghost-btn", onClick: () => void load(), disabled: loading, "data-testid": "performance-refresh-button" }, loading ? "刷新中" : "刷新"), h(RawButton, { title: "Performance Snapshot", data: snapshot, onOpen: onRaw, testId: "raw-performance" }), ), @@ -1074,24 +1127,25 @@ function PerformancePage({ onRaw }: AnyRecord) { h("div", { className: "performance-metric-stack" }, h(MetricCard, { label: "backend RSS", value: fmtBytes(backendProcess.rssBytes), hint: `heap ${fmtBytes(backendProcess.heapUsedBytes)}` }), h(MetricCard, { label: "frontend RSS", value: fmtBytes(frontendProcess.rssBytes), hint: `bundle ${fmtBytes(snapshot.frontend?.appBundleBytes)}` }), - h(MetricCard, { label: "Codex PG 任务", value: codexTotal || "--", hint: codexStorage.ok ? "unidesk_codex_queue_tasks" : "等待表初始化", tone: codexStorage.ok ? "ok" : "warn" }), + h(MetricCard, { label: "Codex PG 任务", value: codexTotal || "--", hint: codexStorage.ok ? "unidesk_code_queue_tasks" : "等待表初始化", tone: codexStorage.ok ? "ok" : "warn" }), h(MetricCard, { label: "请求样本", value: asNumber(snapshot.core?.requests?.sampleCount) + asNumber(snapshot.frontend?.requests?.sampleCount), hint: "rolling window 3000" }), ), ), h(Panel, { - title: "Codex Queue 加载基准", + title: "Code Queue 加载基准", eyebrow: "Playwright / target <1s", className: "codex-load-test-panel", + loading: codexPerfLoading, actions: h("div", { className: "panel-actions" }, - h("button", { type: "button", className: "primary-btn", onClick: () => void runCodexQueueLoadTest(), disabled: codexPerfLoading, "data-testid": "codex-queue-load-test-panel-button" }, codexPerfLoading ? "正在运行 Playwright..." : "手动触发测试"), - codexPerf ? h(RawButton, { title: "Codex Queue Load Test", data: codexPerf, onOpen: onRaw, testId: "raw-codex-queue-load-test" }) : null, + h("button", { type: "button", className: "primary-btn", onClick: () => void runCodeQueueLoadTest(), disabled: codexPerfLoading, "data-testid": "code-queue-load-test-panel-button" }, codexPerfLoading ? "正在运行 Playwright..." : "手动触发测试"), + codexPerf ? h(RawButton, { title: "Code Queue Load Test", data: codexPerf, onOpen: onRaw, testId: "raw-code-queue-load-test" }) : null, ), }, - h("div", { className: "codex-load-test-grid", "data-testid": "codex-queue-load-test-result" }, + h("div", { className: "codex-load-test-grid", "data-testid": "code-queue-load-test-result" }, h(MetricCard, { label: "总耗时", value: codexPerfLoading ? "运行中" : Number.isFinite(codexPerfWallMs) ? fmtMs(codexPerfWallMs) : "--", - hint: codexPerf === null ? "点击按钮启动远端 Playwright" : `目标 ${fmtMs(codexPerfResult.targetMs || 1000)} / ${codexPerfResult.url || "Codex Queue"}`, + hint: codexPerf === null ? "点击按钮启动远端 Playwright" : `目标 ${fmtMs(codexPerfResult.targetMs || 1000)} / ${codexPerfResult.url || "Code Queue"}`, tone: codexPerfStatus === "passed" ? "ok" : codexPerfStatus === "failed" || codexPerfStatus === "slow" ? "warn" : "", }), h(MetricCard, { @@ -1131,7 +1185,7 @@ function PerformancePage({ onRaw }: AnyRecord) { ) : null, ), h("div", { className: "performance-grid" }, - h(Panel, { title: "组件汇总", eyebrow: "Requests" }, + h(Panel, { title: "组件汇总", eyebrow: "Requests", loading }, components.length === 0 ? h(EmptyState, { title: "暂无请求样本", text: "刷新几次或打开页面后会自动形成组件统计" }) : h("div", { className: "table-wrap performance-table-wrap" }, h("table", { className: "performance-table" }, h("thead", null, h("tr", null, ["组件", "请求数", "失败数", "失败率", "平均延迟", "P95"].map((label) => h("th", { key: label }, label)))), @@ -1145,7 +1199,7 @@ function PerformancePage({ onRaw }: AnyRecord) { ))), )), ), - h(Panel, { title: "最近失败请求", eyebrow: "Failures" }, + h(Panel, { title: "最近失败请求", eyebrow: "Failures", loading }, failures.length === 0 ? h("div", { className: "performance-empty-line" }, "最近没有失败请求") : h("div", { className: "table-wrap performance-table-wrap compact" }, h("table", { className: "performance-table" }, h("thead", null, h("tr", null, ["时间", "来源", "组件", "状态", "路径"].map((label) => h("th", { key: label }, label)))), @@ -1158,7 +1212,7 @@ function PerformancePage({ onRaw }: AnyRecord) { ))), )), ), - h(Panel, { title: "内部操作汇总", eyebrow: "Operations" }, + h(Panel, { title: "内部操作汇总", eyebrow: "Operations", loading }, operations.length === 0 ? h(EmptyState, { title: "暂无内部操作样本", text: "API 查询和代理请求会自动记录内部操作耗时" }) : h("div", { className: "table-wrap performance-table-wrap" }, h("table", { className: "performance-table" }, h("thead", null, h("tr", null, ["服务", "操作", "次数", "平均延迟", "P95"].map((label) => h("th", { key: label }, label)))), @@ -1171,7 +1225,7 @@ function PerformancePage({ onRaw }: AnyRecord) { ))), )), ), - h(Panel, { title: "最近慢操作", eyebrow: "Slowest" }, + h(Panel, { title: "最近慢操作", eyebrow: "Slowest", loading }, slowRows.length === 0 ? h(EmptyState, { title: "暂无慢操作", text: "后端会记录最近窗口内耗时最高的内部操作" }) : h("div", { className: "table-wrap performance-table-wrap" }, h("table", { className: "performance-table" }, h("thead", null, h("tr", null, ["时间", "操作", "耗时", "结果", "细节"].map((label) => h("th", { key: label }, label)))), @@ -1214,7 +1268,7 @@ function UpgradeControl({ provider, refresh, onRaw }: AnyRecord) { } } - return h(Panel, { title: "Provider Gateway 升级", eyebrow: "Remote Control" }, + return h(Panel, { title: "Provider Gateway 升级", eyebrow: "Remote Control", loading: Boolean(busyMode) }, h("div", { className: "upgrade-control", "data-testid": "provider-upgrade-control" }, h("p", null, "通过 UniDesk WebSocket 向当前计算节点下发 provider.upgrade;预检只生成升级计划,执行升级会调度节点本地 updater 容器。"), h("div", { className: "upgrade-target-line" }, @@ -1458,13 +1512,15 @@ function DockerStatusPage({ nodes, dockerStatuses, onRaw }: AnyRecord) { h("span", null, `updated ${fmtDate(active.dockerUpdatedAt || status.collectedAt)}`), ), h("div", { className: "docker-container-table table-wrap", "data-testid": "docker-container-table" }, h("table", null, - h("thead", null, h("tr", null, h("th", null, "状态"), h("th", null, "容器"), h("th", null, "镜像"), h("th", null, "端口"), h("th", null, "运行时间"), h("th", null, "大小"))), - h("tbody", null, containers.length === 0 ? h("tr", null, h("td", { colSpan: 6 }, "暂无容器")) : containers.map((item: any) => h("tr", { key: `${item.id}-${item.name}` }, + h("thead", null, h("tr", null, h("th", null, "状态"), h("th", null, "容器"), h("th", null, "镜像"), h("th", null, "端口"), h("th", null, "运行时间"), h("th", null, "重启策略"), h("th", null, "PID"), h("th", null, "大小"))), + h("tbody", null, containers.length === 0 ? h("tr", null, h("td", { colSpan: 8 }, "暂无容器")) : containers.map((item: any) => h("tr", { key: `${item.id}-${item.name}` }, h("td", null, h(StatusBadge, { status: dockerStateTone(item.state) }, item.state || "unknown")), h("td", null, h("strong", null, item.name || "--"), h("code", null, item.id || "--")), h("td", null, item.image || "--"), h("td", null, item.ports || h("span", { className: "muted" }, "未发布")), h("td", null, item.runningFor || item.status || "--"), + h("td", null, item.restartPolicy ? h(StatusBadge, { status: item.restartPolicy === "always" ? "online" : "warn" }, item.restartPolicy) : "--"), + h("td", null, item.pidMode ? h("code", null, item.pidMode) : "--"), h("td", null, item.size || "--"), ))), )), @@ -1558,7 +1614,8 @@ function MicroserviceCatalogPage({ microservices, onRaw, onNavigate }: AnyRecord service.id === "todo-note" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "todo-note"), "data-testid": "open-todo-note-button" }, "打开") : null, service.id === "met-nonlinear" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "met-nonlinear"), "data-testid": "open-met-nonlinear-button" }, "打开") : null, service.id === "claudeqq" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "claudeqq"), "data-testid": "open-claudeqq-button" }, "打开") : null, - service.id === "codex-queue" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "codex-queue"), "data-testid": "open-codex-queue-button" }, "打开") : null, + service.id === "baidu-netdisk" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "baidu-netdisk"), "data-testid": "open-baidu-netdisk-button" }, "打开") : null, + service.id === "code-queue" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "code-queue"), "data-testid": "open-code-queue-button" }, "打开") : null, service.id === "project-manager" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "project-manager"), "data-testid": "open-project-manager-button" }, "打开") : null, h(RawButton, { title: `用户服务 ${service.id}`, data: service, onOpen: onRaw }), ), @@ -1822,7 +1879,9 @@ function WorkArea({ activeModule, activeTab, data, session, refresh, onRaw, onNa if (activeModule === "apps" && activeTab === "pipeline") return h(PipelinePage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl }); if (activeModule === "apps" && activeTab === "met-nonlinear") return h(MetNonlinearPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl }); if (activeModule === "apps" && activeTab === "claudeqq") return h(ClaudeQqPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl }); - if (activeModule === "apps" && activeTab === "codex-queue") return h(CodexQueuePage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl, initialTasksData: initialCodexQueueOverview }); + if (activeModule === "apps" && activeTab === "baidu-netdisk") return h(BaiduNetdiskPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl }); + if (activeModule === "apps" && activeTab === "filebrowser") return h(FileBrowserPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl }); + if (activeModule === "apps" && activeTab === "code-queue") return h(CodeQueuePage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl, initialTasksData: initialCodeQueueOverview }); if (activeModule === "apps" && activeTab === "project-manager") return h(ProjectManagerPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl }); if (activeModule === "config" && activeTab === "topology") return h(TopologyPage, { data }); if (activeModule === "config" && activeTab === "auth") return h(AuthPage, { session }); @@ -1840,13 +1899,14 @@ function Shell({ session, onLogout }: AnyRecord) { const [clock, setClock] = useState(new Date()); const [raw, setRaw] = useState(null); const [railCollapsed, setRailCollapsed] = useState(false); + const [refreshing, setRefreshing] = useState(false); const refreshInFlightRef = React.useRef(false); const module = ROUTE_REGISTRY.moduleById[activeModule] || ROUTE_REGISTRY.modules[0]; const activeTab = activeTabs[activeModule] || DEFAULT_ACTIVE_TABS[activeModule] || module.tabs[0].id; const microservices = Array.isArray(data.microservices) ? data.microservices : []; - const effectiveMicroservices = microservices.length === 0 && activeModule === "apps" && activeTab === "codex-queue" - ? [fastCodexQueueService] + const effectiveMicroservices = microservices.length === 0 && activeModule === "apps" && activeTab === "code-queue" + ? [fastCodeQueueService] : microservices; const effectiveData = effectiveMicroservices === microservices ? data : { ...data, microservices: effectiveMicroservices }; const activeService = activeModule === "apps" @@ -1865,6 +1925,7 @@ function Shell({ session, onLogout }: AnyRecord) { async function refresh(): Promise { if (refreshInFlightRef.current) return; refreshInFlightRef.current = true; + setRefreshing(true); try { const requests: Array<[string, Promise]> = []; const add = (key: string, path: string): void => { @@ -1873,7 +1934,7 @@ function Shell({ session, onLogout }: AnyRecord) { const isOverview = activeModule === "ops" && activeTab === "status"; const needsOverviewSummary = isOverview || (activeModule === "config" && activeTab === "topology"); const needsNodes = isOverview || activeModule === "nodes" || (activeModule === "tasks" && activeTab === "dispatch"); - const needsMicroservices = activeModule === "apps" && activeTab !== "codex-queue"; + const needsMicroservices = activeModule === "apps" && activeTab !== "code-queue"; if (needsOverviewSummary) add("overview", `${cfg.apiBaseUrl}/overview`); if (needsNodes) add("nodes", `${cfg.apiBaseUrl}/nodes`); if (activeModule === "nodes" && activeTab === "monitor") { @@ -1916,6 +1977,7 @@ function Shell({ session, onLogout }: AnyRecord) { if ((err as { status?: number }).status === 401) onLogout(false); } finally { refreshInFlightRef.current = false; + setRefreshing(false); } } @@ -1986,7 +2048,9 @@ function Shell({ session, onLogout }: AnyRecord) { h("main", { className: "workspace" }, h(TopBar, { connection, lastRefresh, onRefresh: refresh, onLogout: () => onLogout(true), session, clock, activeStatusItems }), h(TabBar, { module, activeTab, onNavigate: navigate }), - h(WorkArea, { activeModule, activeTab, data: effectiveData, session, refresh, onRaw: openRaw, onNavigate: navigate }), + h(LoadingContext.Provider, { value: refreshing }, + h(WorkArea, { activeModule, activeTab, data: effectiveData, session, refresh, onRaw: openRaw, onNavigate: navigate }), + ), ), h(RawDialog, { raw, onClose: () => setRaw(null) }), ); diff --git a/src/components/frontend/src/baidu-netdisk.tsx b/src/components/frontend/src/baidu-netdisk.tsx new file mode 100644 index 00000000..f4a8354d --- /dev/null +++ b/src/components/frontend/src/baidu-netdisk.tsx @@ -0,0 +1,552 @@ +import React from "react"; +import { fmtClock, fmtDate } from "./time"; +import { LoadingTitle } from "./loading-indicator"; +import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error"; +import { UniDeskErrorBanner } from "./unidesk-error-banner"; + +type AnyRecord = Record; + +const h = React.createElement; +const { useEffect } = React; +const useState: any = React.useState; + +function requestJson(path: string, options: AnyRecord = {}): Promise { + return requestUniDeskJson(path, { failureFields: ["ok", "success"], ...options }); +} + +function baiduApi(apiBaseUrl: string, path: string): string { + return `${apiBaseUrl}/microservices/baidu-netdisk/proxy${path}`; +} + +function numberText(value: any): string { + const number = Number(value); + return Number.isFinite(number) ? number.toLocaleString("zh-CN") : "--"; +} + +function fmtBytes(value: any): string { + const bytes = Number(value); + if (!Number.isFinite(bytes) || bytes <= 0) return "--"; + const units = ["B", "KB", "MB", "GB", "TB"]; + let current = bytes; + let index = 0; + while (current >= 1024 && index < units.length - 1) { + current /= 1024; + index += 1; + } + return `${current.toFixed(index === 0 ? 0 : 1)} ${units[index]}`; +} + +function StatusBadge({ status, children }: AnyRecord) { + const normalized = String(status || "unknown").toLowerCase(); + return h("span", { className: `status-badge ${normalized}` }, children || status || "unknown"); +} + +function MetricCard({ label, value, hint, tone }: AnyRecord) { + return h("article", { className: `metric-card ${tone || ""}` }, + h("div", { className: "metric-label" }, label), + h("div", { className: "metric-value" }, value), + h("div", { className: "metric-hint" }, hint), + ); +} + +function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) { + return h("section", { className: `panel ${className || ""}` }, + h("div", { className: "panel-head" }, + h("div", null, + eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null, + h(LoadingTitle, { title, loading }), + ), + actions ? h("div", { className: "panel-actions" }, actions) : null, + ), + h("div", { className: "panel-body" }, children), + ); +} + +function RawButton({ title, data, onOpen, testId }: AnyRecord) { + return h("button", { + type: "button", + className: "ghost-btn", + "data-testid": testId, + onClick: (event: any) => { + event?.stopPropagation?.(); + onOpen(title, data); + }, + }, "查看原始JSON"); +} + +function EmptyState({ title, text }: AnyRecord) { + return h("div", { className: "empty-state" }, h("strong", null, title), h("span", null, text)); +} + +function DocLinkCard({ title, text, href, badge, testId }: AnyRecord) { + return h("a", { + className: "doc-link-card", + href, + target: "_blank", + rel: "noreferrer", + "data-testid": testId, + }, + h("span", null, badge || "DOC"), + h("strong", null, title), + h("p", null, text), + h("code", null, href), + ); +} + +function microserviceRuntime(service: any): AnyRecord { + return service?.runtime && typeof service.runtime === "object" && !Array.isArray(service.runtime) ? service.runtime : {}; +} + +function microserviceBackend(service: any): AnyRecord { + return service?.backend && typeof service.backend === "object" && !Array.isArray(service.backend) ? service.backend : {}; +} + +function microserviceRepository(service: any): AnyRecord { + return service?.repository && typeof service.repository === "object" && !Array.isArray(service.repository) ? service.repository : {}; +} + +function filesFromPayload(payload: any): any[] { + return Array.isArray(payload?.files) ? payload.files : []; +} + +function jobsFromPayload(payload: any): any[] { + return Array.isArray(payload?.jobs) ? payload.jobs : []; +} + +function pathParent(path: string, root: string): string { + if (!path || path === root) return root; + const withoutSlash = path.replace(/\/+$/u, ""); + const parent = withoutSlash.slice(0, withoutSlash.lastIndexOf("/")) || root; + return parent.length < root.length ? root : parent; +} + +function pathJoin(base: string, name: string): string { + const safeName = String(name || "").replace(/^\/+|\/+$/gu, ""); + return `${base.replace(/\/+$/u, "")}/${safeName}`; +} + +function ProgressBar({ percent }: AnyRecord) { + const value = Math.max(0, Math.min(100, Number(percent) || 0)); + return h("div", { className: "baidu-progress" }, h("span", { style: { width: `${value}%` } }), h("em", null, `${value.toFixed(1)}%`)); +} + +export function BaiduNetdiskPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyRecord) { + const service = microservices.find((item: any) => item.id === "baidu-netdisk") || null; + const [state, setState] = useState({ loading: false, actionLoading: false, error: "", message: "", health: null, account: null, files: null, transfers: null, logs: null, selfTest: null, refreshedAt: null }); + const [currentDir, setCurrentDir] = useState("/apps/UniDeskBaiduNetdisk"); + const [deviceSession, setDeviceSession] = useState(null); + const [folderName, setFolderName] = useState(""); + const [uploadForm, setUploadForm] = useState({ localPath: "sample.txt", remotePath: "/apps/UniDeskBaiduNetdisk/sample.txt" }); + const [downloadForm, setDownloadForm] = useState({ fsId: "", localPath: "downloads/" }); + + const appRoot = state.health?.baidu?.appRoot || state.account?.rootPath || "/apps/UniDeskBaiduNetdisk"; + + async function loadFiles(dir = currentDir): Promise { + const targetDir = dir || appRoot; + const files = await requestJson(baiduApi(apiBaseUrl, `/api/files?dir=${encodeURIComponent(targetDir)}&limit=100`)); + setState((prev: any) => ({ ...prev, files })); + } + + async function loadTransfers(): Promise { + const transfers = await requestJson(baiduApi(apiBaseUrl, "/api/transfers?limit=80")); + setState((prev: any) => ({ ...prev, transfers })); + } + + async function load(): Promise { + if (!service) return; + setState((prev: any) => ({ ...prev, loading: true, error: "", message: "" })); + try { + const health = await requestJson(`${apiBaseUrl}/microservices/baidu-netdisk/health`); + const root = health?.baidu?.appRoot || appRoot; + let account: any = null; + let files: any = null; + if (health?.auth?.loggedIn) { + account = await requestJson(baiduApi(apiBaseUrl, "/api/account?refresh=1")); + const dir = currentDir && currentDir.startsWith(root) ? currentDir : root; + setCurrentDir(dir); + files = await requestJson(baiduApi(apiBaseUrl, `/api/files?dir=${encodeURIComponent(dir)}&limit=100`)); + } else { + setCurrentDir(root); + } + const transfers = await requestJson(baiduApi(apiBaseUrl, "/api/transfers?limit=80")); + const logs = await requestJson(baiduApi(apiBaseUrl, "/logs?limit=60")); + setState((prev: any) => ({ ...prev, loading: false, health, account: account?.account || null, files, transfers, logs, refreshedAt: new Date() })); + } catch (err) { + setState((prev: any) => ({ ...prev, loading: false, error: errorMessage(err, "百度网盘服务加载失败") })); + } + } + + async function startLogin(): Promise { + setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" })); + try { + const result = await requestJson(baiduApi(apiBaseUrl, "/api/auth/device/start"), { method: "POST", body: {} }); + setDeviceSession(result.session || null); + setState((prev: any) => ({ ...prev, actionLoading: false, message: "设备码已生成,请扫码授权" })); + } catch (err) { + setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "创建设备码失败") })); + } + } + + async function pollLogin(manual = false): Promise { + if (!deviceSession?.id) return; + if (manual) setState((prev: any) => ({ ...prev, actionLoading: true, error: "" })); + try { + const result = await requestJson(baiduApi(apiBaseUrl, `/api/auth/device/status?sessionId=${encodeURIComponent(deviceSession.id)}`)); + setDeviceSession(result.session || null); + if (result.session?.status === "succeeded") { + setState((prev: any) => ({ ...prev, actionLoading: false, message: "授权成功,正在刷新账号与文件列表" })); + await load(); + } else if (manual) { + setState((prev: any) => ({ ...prev, actionLoading: false })); + } + } catch (err) { + setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "轮询登录状态失败") })); + } + } + + async function logout(): Promise { + setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" })); + try { + await requestJson(baiduApi(apiBaseUrl, "/api/auth/logout"), { method: "POST", body: {} }); + setDeviceSession(null); + setState((prev: any) => ({ ...prev, actionLoading: false, account: null, files: null, message: "本地 token 已清除" })); + await load(); + } catch (err) { + setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "退出登录失败") })); + } + } + + async function createFolder(event: any): Promise { + event.preventDefault(); + const name = folderName.trim(); + if (!name) return; + setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" })); + try { + await requestJson(baiduApi(apiBaseUrl, "/api/folders"), { method: "POST", body: { path: pathJoin(currentDir, name) } }); + setFolderName(""); + setState((prev: any) => ({ ...prev, actionLoading: false, message: "文件夹已创建" })); + await loadFiles(currentDir); + } catch (err) { + setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "创建文件夹失败") })); + } + } + + async function deleteRemote(path: string): Promise { + if (!path) return; + setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" })); + try { + await requestJson(baiduApi(apiBaseUrl, "/api/files/manage"), { method: "POST", body: { opera: "delete", filelist: [{ path }], async: 1 } }); + setState((prev: any) => ({ ...prev, actionLoading: false, message: "删除任务已提交" })); + await loadFiles(currentDir); + } catch (err) { + setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "删除失败") })); + } + } + + async function submitUpload(event: any): Promise { + event.preventDefault(); + setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" })); + try { + await requestJson(baiduApi(apiBaseUrl, "/api/transfers/upload-from-path"), { method: "POST", body: uploadForm }); + setState((prev: any) => ({ ...prev, actionLoading: false, message: "上传任务已入队" })); + await loadTransfers(); + } catch (err) { + setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "上传任务创建失败") })); + } + } + + async function submitDownload(event: any): Promise { + event.preventDefault(); + setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" })); + try { + await requestJson(baiduApi(apiBaseUrl, "/api/transfers/download-to-path"), { method: "POST", body: downloadForm }); + setState((prev: any) => ({ ...prev, actionLoading: false, message: "下载任务已入队" })); + await loadTransfers(); + } catch (err) { + setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "下载任务创建失败") })); + } + } + + async function transferAction(id: string, action: "cancel" | "retry"): Promise { + setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "" })); + try { + await requestJson(baiduApi(apiBaseUrl, `/api/transfers/${encodeURIComponent(id)}/${action}`), { method: "POST", body: {} }); + setState((prev: any) => ({ ...prev, actionLoading: false, message: action === "cancel" ? "已请求取消任务" : "任务已重新入队" })); + await loadTransfers(); + } catch (err) { + setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "任务操作失败") })); + } + } + + async function runSelfTest(): Promise { + setState((prev: any) => ({ ...prev, actionLoading: true, error: "", message: "正在运行上传/下载自测..." })); + try { + const selfTest = await requestJson(baiduApi(apiBaseUrl, "/api/self-test"), { method: "POST", body: {} }); + setState((prev: any) => ({ ...prev, actionLoading: false, selfTest, message: `上传/下载自测通过:${selfTest.remotePath || ""}` })); + await loadFiles(currentDir); + await loadTransfers(); + } catch (err) { + setState((prev: any) => ({ ...prev, actionLoading: false, error: errorMessage(err, "上传/下载自测失败") })); + } + } + + useEffect(() => { + if (!service) return undefined; + load(); + return undefined; + }, [service?.id, service?.runtime?.providerStatus]); + + useEffect(() => { + if (!deviceSession?.id || deviceSession.status !== "pending") return undefined; + const timer = window.setInterval(() => void pollLogin(false), Math.max(5000, Number(deviceSession.pollIntervalSeconds || 5) * 1000)); + return () => window.clearInterval(timer); + }, [deviceSession?.id, deviceSession?.status, deviceSession?.pollIntervalSeconds]); + + useEffect(() => { + if (!service) return undefined; + const timer = window.setInterval(() => void loadTransfers(), 5000); + return () => window.clearInterval(timer); + }, [service?.id]); + + if (!service) return h(EmptyState, { title: "Baidu Netdisk 未登记", text: "请在 config.json 的 microservices 中登记用户服务 id=baidu-netdisk" }); + + const runtime = microserviceRuntime(service); + const repository = microserviceRepository(service); + const backend = microserviceBackend(service); + const health = state.health || {}; + const account = state.account || health.auth?.account || null; + const auth = health.auth || {}; + const files = filesFromPayload(state.files); + const jobs = jobsFromPayload(state.transfers); + const quota = account?.quota || {}; + const loggedIn = Boolean(auth.loggedIn || account); + const configured = Boolean(auth.configured); + + return h("div", { className: "baidu-netdisk-page", "data-testid": "baidu-netdisk-page" }, + h(Panel, { + title: "Baidu Netdisk 工作台", + eyebrow: "Containerized Storage Gateway", + loading: state.loading, + actions: h("div", { className: "panel-actions" }, + h("a", { className: "ghost-btn", href: "/docs/issue/baidu-netdisk-env-setup.md", target: "_blank", rel: "noreferrer", "data-testid": "baidu-netdisk-config-doc-link" }, "配置文档"), + h("button", { type: "button", className: "ghost-btn", onClick: load, disabled: state.loading, "data-testid": "baidu-netdisk-refresh" }, state.loading ? "刷新中" : "刷新"), + h(RawButton, { title: "Baidu Netdisk 用户服务", data: service, onOpen: onRaw, testId: "raw-baidu-netdisk-service" }), + ), + }, + h("div", { className: "baidu-netdisk-hero" }, + h("div", null, + h("div", { className: "node-version-line" }, + h(StatusBadge, { status: runtime.providerStatus === "online" ? "online" : "warn" }, runtime.providerStatus || "unknown"), + h("span", null, service.providerId), + h(StatusBadge, { status: backend.public ? "warn" : "private" }, backend.public ? "公网暴露" : "仅 UniDesk frontend 代理访问"), + ), + h("p", { className: "muted paragraph" }, service.description), + ), + h("div", { className: "microservice-ref-card" }, + h("span", null, "Repo"), + h("strong", null, repository.url || "--"), + h("code", null, repository.commitId || "--"), + ), + h("div", { className: "microservice-ref-card" }, + h("span", null, "Private Backend"), + h("strong", null, `${backend.nodeBindHost || "--"}:${backend.nodePort || "--"}`), + h("code", null, `${repository.composeFile || "--"} / ${repository.composeService || "--"}`), + ), + ), + h(UniDeskErrorBanner, { error: state.error, wide: true }), + state.message ? h("div", { className: "form-success wide" }, state.message) : null, + ), + h("div", { className: "metric-grid" }, + h(MetricCard, { label: "Health", value: health.ok ? "OK" : "--", hint: health.storage?.postgres || "postgres", tone: health.ok ? "ok" : "warn" }), + h(MetricCard, { label: "OAuth", value: configured ? "已配置" : "待配置", hint: configured ? "client + secret + token key" : "需要设置 UNIDESK_BAIDU_NETDISK_*", tone: configured ? "ok" : "warn" }), + h(MetricCard, { label: "Login", value: loggedIn ? "已登录" : "未登录", hint: account?.username || "Device Code QR", tone: loggedIn ? "ok" : "warn" }), + h(MetricCard, { label: "App Root", value: appRoot.split("/").pop() || "apps", hint: appRoot }), + h(MetricCard, { label: "Quota", value: fmtBytes(quota.used), hint: quota.total ? `${quota.usedPercent || 0}% / ${fmtBytes(quota.total)}` : "授权后刷新" }), + h(MetricCard, { label: "Transfers", value: numberText(jobs.length), hint: `running ${state.transfers?.counts?.running || 0} / failed ${state.transfers?.counts?.failed || 0}` }), + ), + h("div", { className: "baidu-netdisk-grid" }, + h(Panel, { + title: "配置与文档", + eyebrow: "Deployment References", + className: "baidu-docs-panel", + actions: h("div", { className: "panel-actions inline-actions" }, + h("a", { className: "ghost-btn", href: "/docs/issue/baidu-netdisk-env-setup.md", target: "_blank", rel: "noreferrer" }, "打开环境配置"), + h("a", { className: "ghost-btn", href: "/docs/issue/baidu-netdisk-user-service.md", target: "_blank", rel: "noreferrer" }, "打开服务方案"), + ), + }, + h("p", { className: "muted paragraph" }, configured + ? "OAuth 运行时变量已配置;如需轮换密钥、迁移部署或排查代理边界,可直接打开下面的项目内文档。" + : "首次使用请先按环境变量配置文档填入百度应用 client id / secret,然后重建 baidu-netdisk 服务并刷新本页。"), + h("div", { className: "baidu-doc-grid", "data-testid": "baidu-netdisk-doc-links" }, + h(DocLinkCard, { + title: "环境变量配置", + text: "填写 UNIDESK_BAIDU_NETDISK_CLIENT_ID、CLIENT_SECRET、TOKEN_KEY,并执行重建与健康检查。", + href: "/docs/issue/baidu-netdisk-env-setup.md", + badge: "SETUP", + testId: "baidu-netdisk-env-doc-card", + }), + h(DocLinkCard, { + title: "服务方案与 API", + text: "说明 OAuth Device Code、应用目录、staging 上传下载任务和后端 API 设计。", + href: "/docs/issue/baidu-netdisk-user-service.md", + badge: "DESIGN", + }), + h(DocLinkCard, { + title: "用户服务安全边界", + text: "查看 UniDesk microservice 私有代理、允许路径、frontendOnly 和密钥边界规则。", + href: "/docs/reference/microservices.md", + badge: "REF", + }), + h(DocLinkCard, { + title: "部署与重建流程", + text: "查看 server rebuild、Compose 编排、健康检查和交付验证的长期规则。", + href: "/docs/reference/deployment.md", + badge: "DEPLOY", + }), + h(DocLinkCard, { + title: "CLI 验证命令", + text: "查看 microservice health/proxy、server rebuild、job status 等命令入口。", + href: "/docs/reference/cli.md", + badge: "CLI", + }), + h(DocLinkCard, { + title: "百度设备码模式", + text: "打开百度官方 OAuth Device Code 文档,对照扫码登录和轮询参数。", + href: "https://pan.baidu.com/union/doc/fl1x114ti", + badge: "OFFICIAL", + }), + ), + ), + h(Panel, { + title: "设备码登录", + eyebrow: "OAuth Device Code", + className: "baidu-login-panel", + loading: state.actionLoading, + actions: h("div", { className: "panel-actions inline-actions" }, + h("button", { type: "button", className: "primary-btn", onClick: startLogin, disabled: state.actionLoading || !configured, "data-testid": "baidu-netdisk-start-login" }, "生成二维码"), + deviceSession?.id ? h("button", { type: "button", className: "ghost-btn", onClick: () => pollLogin(true), disabled: state.actionLoading }, "检查状态") : null, + loggedIn ? h("button", { type: "button", className: "ghost-btn", onClick: logout, disabled: state.actionLoading }, "清除本地登录") : null, + h(RawButton, { title: "Baidu Device Session", data: deviceSession || auth.latestSession, onOpen: onRaw, testId: "raw-baidu-device-session" }), + ), + }, + h("div", { className: "baidu-login-card", "data-testid": "baidu-netdisk-login-card" }, + h("div", { className: "baidu-qr-frame" }, + deviceSession?.qrcodeUrl + ? h("img", { src: deviceSession.qrcodeUrl, alt: "百度网盘设备码授权二维码", "data-testid": "baidu-netdisk-qrcode" }) + : h(EmptyState, { title: configured ? "等待二维码" : "OAuth 未配置", text: configured ? "点击生成二维码后使用百度网盘或百度 App 扫码" : "设置 client id、secret 和 token key 后重建服务" }), + ), + h("div", { className: "claudeqq-login-copy" }, + h("div", { className: "node-version-line" }, + h(StatusBadge, { status: loggedIn ? "online" : deviceSession?.status === "pending" ? "warn" : "unknown" }, loggedIn ? "已登录" : deviceSession?.status || "未开始"), + h("span", null, deviceSession?.secondsRemaining !== undefined ? `${deviceSession.secondsRemaining}s` : "--"), + h("span", null, "scope basic,netdisk"), + ), + h("p", { className: "muted paragraph" }, loggedIn + ? "access token / refresh token 已加密保存到 PostgreSQL;前端只看到脱敏登录态。" + : "后端使用百度 OAuth Device Code 轮询换取 token;二维码过期后重新生成即可。"), + h("div", { className: "microservice-ref-card" }, h("span", null, "User Code"), h("strong", null, deviceSession?.userCode || "--"), h("code", null, deviceSession?.verificationUrl || "https://openapi.baidu.com/device")), + h("div", { className: "microservice-ref-card" }, h("span", null, "Expires"), h("strong", null, deviceSession?.expiresAt ? fmtDate(deviceSession.expiresAt) : "--"), h("code", null, deviceSession?.error || "no token exposed")), + ), + ), + ), + h(Panel, { title: "账号与容量", eyebrow: state.refreshedAt ? `Updated ${fmtClock(state.refreshedAt)}` : "Account", loading: state.loading, + actions: h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "Baidu Account", data: account, onOpen: onRaw, testId: "raw-baidu-account" })), + }, + account ? h("div", { className: "baidu-account-card" }, + h("div", { className: "node-version-line" }, h(StatusBadge, { status: "online" }, "connected"), h("span", null, account.baiduUid || "--"), h("span", null, `VIP ${account.vipType ?? "--"}`)), + h("h3", null, account.username || "Baidu Netdisk"), + h("p", { className: "muted paragraph" }, `应用目录固定在 ${account.rootPath || appRoot};v1 上传/下载只读写容器 staging 目录,不把大文件字节流穿过 UniDesk proxy。`), + h("div", { className: "quota-bar" }, h("span", { style: { width: `${Math.max(0, Math.min(100, Number(quota.usedPercent || 0)))}%` } })), + h("div", { className: "microservice-ref-card" }, h("span", null, "Quota"), h("strong", null, `${fmtBytes(quota.used)} / ${fmtBytes(quota.total)}`), h("code", null, `${quota.usedPercent || 0}% used`)), + ) : h(EmptyState, { title: "尚未登录", text: "扫码授权后这里会显示账号、UID、会员状态和容量" }), + ), + h(Panel, { title: "文件浏览器", eyebrow: currentDir, className: "baidu-files-panel", loading: state.loading, + actions: h("div", { className: "panel-actions inline-actions" }, + h("button", { type: "button", className: "ghost-btn", onClick: () => { const parent = pathParent(currentDir, appRoot); setCurrentDir(parent); void loadFiles(parent); }, disabled: !loggedIn || currentDir === appRoot }, "上级"), + h("button", { type: "button", className: "ghost-btn", onClick: () => loadFiles(currentDir), disabled: !loggedIn }, "刷新文件"), + h(RawButton, { title: "Baidu Files", data: state.files, onOpen: onRaw, testId: "raw-baidu-files" }), + ), + }, + h("form", { className: "baidu-pathbar", onSubmit: (event: any) => { event.preventDefault(); void loadFiles(currentDir); } }, + h("input", { value: currentDir, onChange: (event: any) => setCurrentDir(event.target.value), disabled: !loggedIn }), + h("button", { type: "submit", className: "ghost-btn", disabled: !loggedIn }, "打开路径"), + ), + h("form", { className: "baidu-pathbar", onSubmit: createFolder }, + h("input", { value: folderName, onChange: (event: any) => setFolderName(event.target.value), placeholder: "新文件夹名称", disabled: !loggedIn }), + h("button", { type: "submit", className: "primary-btn", disabled: !loggedIn || !folderName.trim() }, "新建文件夹"), + ), + !loggedIn ? h(EmptyState, { title: "等待授权", text: "登录后通过 /api/files 读取应用目录文件列表" }) : files.length === 0 ? h(EmptyState, { title: "目录为空", text: "可以从 staging 目录上传文件或新建文件夹" }) : + h("div", { className: "table-wrap", "data-testid": "baidu-netdisk-file-table" }, h("table", null, + h("thead", null, h("tr", null, h("th", null, "名称"), h("th", null, "类型"), h("th", null, "大小"), h("th", null, "修改时间"), h("th", null, "fs_id"), h("th", null, "操作"))), + h("tbody", null, files.map((file: any) => h("tr", { key: file.fsId || file.path }, + h("td", null, h("strong", null, file.serverFilename || file.path), h("code", null, file.path || "--")), + h("td", null, h(StatusBadge, { status: file.isDir ? "queued" : "private" }, file.isDir ? "DIR" : "FILE")), + h("td", null, file.isDir ? "--" : fmtBytes(file.size)), + h("td", null, file.serverMtime ? fmtDate(file.serverMtime * 1000) : "--"), + h("td", null, h("code", null, file.fsId || "--")), + h("td", null, h("div", { className: "inline-actions" }, + file.isDir ? h("button", { type: "button", className: "ghost-btn", onClick: () => { setCurrentDir(file.path); void loadFiles(file.path); } }, "打开") : + h("button", { type: "button", className: "ghost-btn", onClick: () => setDownloadForm((prev: any) => ({ ...prev, fsId: file.fsId })) }, "填入下载"), + h("button", { type: "button", className: "ghost-btn", onClick: () => deleteRemote(file.path), disabled: state.actionLoading }, "删除"), + )), + ))), + )), + ), + h(Panel, { title: "传输任务", eyebrow: "staging path jobs", className: "baidu-transfers-panel", loading: state.actionLoading, + actions: h("div", { className: "panel-actions inline-actions" }, + h("button", { type: "button", className: "primary-btn", onClick: runSelfTest, disabled: !loggedIn || state.actionLoading, "data-testid": "baidu-netdisk-self-test" }, "运行自测"), + h("button", { type: "button", className: "ghost-btn", onClick: loadTransfers }, "刷新任务"), + h(RawButton, { title: "Baidu Transfers", data: state.transfers, onOpen: onRaw, testId: "raw-baidu-transfers" }), + ), + }, + h("div", { className: "baidu-transfer-forms" }, + h("form", { className: "stack-form", onSubmit: submitUpload, "data-testid": "baidu-upload-form" }, + h("label", null, "容器 staging 文件", h("input", { value: uploadForm.localPath, onChange: (event: any) => setUploadForm((prev: any) => ({ ...prev, localPath: event.target.value })), placeholder: "sample.txt" })), + h("label", null, "百度网盘目标路径", h("input", { value: uploadForm.remotePath, onChange: (event: any) => setUploadForm((prev: any) => ({ ...prev, remotePath: event.target.value })), placeholder: `${appRoot}/sample.txt` })), + h("button", { type: "submit", className: "primary-btn", disabled: !loggedIn || state.actionLoading }, "上传 staging 文件"), + ), + h("form", { className: "stack-form", onSubmit: submitDownload, "data-testid": "baidu-download-form" }, + h("label", null, "文件 fs_id", h("input", { value: downloadForm.fsId, onChange: (event: any) => setDownloadForm((prev: any) => ({ ...prev, fsId: event.target.value })), placeholder: "从文件表填入" })), + h("label", null, "保存到 staging 路径", h("input", { value: downloadForm.localPath, onChange: (event: any) => setDownloadForm((prev: any) => ({ ...prev, localPath: event.target.value })), placeholder: "downloads/" })), + h("button", { type: "submit", className: "primary-btn", disabled: !loggedIn || !downloadForm.fsId || state.actionLoading }, "下载到 staging"), + ), + ), + state.selfTest ? h("div", { className: "baidu-account-card", "data-testid": "baidu-netdisk-self-test-result" }, + h("div", { className: "node-version-line" }, + h(StatusBadge, { status: state.selfTest.ok ? "online" : "warn" }, state.selfTest.ok ? "self-test ok" : "self-test"), + h("span", null, fmtBytes(state.selfTest.sizeBytes)), + ), + h("h3", null, state.selfTest.remotePath || "Baidu self-test"), + h("div", { className: "microservice-ref-card" }, h("span", null, "fs_id"), h("strong", null, state.selfTest.fsId || "--"), h("code", null, state.selfTest.downloadedPath || "--")), + h("div", { className: "microservice-ref-card" }, h("span", null, "MD5"), h("strong", null, state.selfTest.downloadedMd5 || "--"), h("code", null, state.selfTest.expectedMd5 || "--")), + h(RawButton, { title: "Baidu Self Test", data: state.selfTest, onOpen: onRaw, testId: "raw-baidu-self-test" }), + ) : null, + jobs.length === 0 ? h(EmptyState, { title: "暂无传输任务", text: "上传/下载任务会在后端容器内执行,避免大文件穿过 UniDesk proxy" }) : + h("div", { className: "table-wrap", "data-testid": "baidu-transfer-table" }, h("table", null, + h("thead", null, h("tr", null, h("th", null, "状态"), h("th", null, "方向"), h("th", null, "路径"), h("th", null, "进度"), h("th", null, "时间"), h("th", null, "操作"))), + h("tbody", null, jobs.map((job: any) => h("tr", { key: job.id }, + h("td", null, h(StatusBadge, { status: job.status }, job.status)), + h("td", null, job.direction), + h("td", null, h("strong", null, job.remotePath || job.fsId || "--"), h("code", null, job.localPath || "--"), job.error ? h("span", { className: "form-error" }, job.error) : null), + h("td", null, h(ProgressBar, { percent: job.progressPercent }), h("span", { className: "muted" }, `${fmtBytes(job.bytesDone)} / ${fmtBytes(job.sizeBytes)}`)), + h("td", null, fmtDate(job.updatedAt)), + h("td", null, h("div", { className: "inline-actions" }, + ["queued", "running"].includes(job.status) ? h("button", { type: "button", className: "ghost-btn", onClick: () => transferAction(job.id, "cancel") }, "取消") : null, + ["failed", "canceled"].includes(job.status) ? h("button", { type: "button", className: "ghost-btn", onClick: () => transferAction(job.id, "retry") }, "重试") : null, + h(RawButton, { title: `Transfer ${job.id}`, data: job, onOpen: onRaw }), + )), + ))), + )), + ), + h(Panel, { title: "安全与日志", eyebrow: "redacted diagnostics", className: "baidu-wide-panel", loading: state.loading, + actions: h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "Baidu Health", data: health, onOpen: onRaw, testId: "raw-baidu-health" }), h(RawButton, { title: "Baidu Logs", data: state.logs, onOpen: onRaw, testId: "raw-baidu-logs" })), + }, + h("div", { className: "policy-grid" }, + h("article", null, h("b", null, "私有后端"), h("span", null, "4244 只在 Compose 网络 expose,浏览器经 UniDesk 同源代理访问")), + h("article", null, h("b", null, "Token 加密"), h("span", null, "access/refresh token 使用 BAIDU_NETDISK_TOKEN_KEY 加密后写入 PostgreSQL")), + h("article", null, h("b", null, "无浏览器大文件流"), h("span", null, "上传/下载以容器 staging 目录为边界,避免 proxy 文本通道传输大字节流")), + ), + ), + ), + ); +} diff --git a/src/components/frontend/src/claudeqq.tsx b/src/components/frontend/src/claudeqq.tsx index 43ca04b0..216a6783 100644 --- a/src/components/frontend/src/claudeqq.tsx +++ b/src/components/frontend/src/claudeqq.tsx @@ -1,5 +1,6 @@ import React from "react"; import { fmtClock, fmtDate } from "./time"; +import { LoadingTitle } from "./loading-indicator"; import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; @@ -33,12 +34,12 @@ function MetricCard({ label, value, hint, tone }: AnyRecord) { ); } -function Panel({ title, eyebrow, actions, children, className }: AnyRecord) { +function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) { return h("section", { className: `panel ${className || ""}` }, h("div", { className: "panel-head" }, h("div", null, eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null, - h("h2", null, title), + h(LoadingTitle, { title, loading }), ), actions ? h("div", { className: "panel-actions" }, actions) : null, ), @@ -246,6 +247,7 @@ export function ClaudeQqPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR h(Panel, { title: "ClaudeQQ 工作台", eyebrow: "D601 QQ Event Gateway", + loading: state.loading, actions: h("div", { className: "panel-actions" }, h("button", { type: "button", className: "ghost-btn", onClick: load, disabled: state.loading, "data-testid": "claudeqq-refresh-button" }, state.loading ? "刷新中" : "刷新"), h(RawButton, { title: "ClaudeQQ 用户服务", data: service, onOpen: onRaw, testId: "raw-claudeqq-service" }), @@ -287,6 +289,7 @@ export function ClaudeQqPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR title: "NapCat 容器登录", eyebrow: "QR Login", className: "claudeqq-login-panel", + loading: state.qrLoading, actions: h("div", { className: "panel-actions inline-actions" }, h("button", { type: "button", className: "ghost-btn", onClick: () => refreshNapcatQrcode(true), disabled: state.qrLoading, "data-testid": "claudeqq-napcat-refresh" }, state.qrLoading ? "刷新中" : "手动刷新二维码"), h(RawButton, { title: "NapCat Login", data: state.napcatLogin, onOpen: onRaw, testId: "raw-claudeqq-napcat-login" }), @@ -339,7 +342,7 @@ export function ClaudeQqPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR ), h("p", { className: "muted paragraph" }, `主 server 和其他用户服务可通过 UniDesk 同源代理调用 /api/push/text;当前人工推送测试默认使用 ${PRIMARY_PRIVATE_CHAT.label} ${PRIMARY_PRIVATE_CHAT.userId},不需要暴露 D601 后端端口。`), ), - h(Panel, { title: "QQ 事件订阅", eyebrow: "Webhook Subscription" }, + h(Panel, { title: "QQ 事件订阅", eyebrow: "Webhook Subscription", loading: state.loading }, h("form", { className: "stack-form", onSubmit: createSubscription, "data-testid": "claudeqq-subscription-form" }, h("label", null, "订阅名称", h("input", { value: subscriptionForm.name, onChange: (event: any) => setSubscriptionForm((prev: any) => ({ ...prev, name: event.target.value })) })), h("label", null, "回调 URL", h("input", { value: subscriptionForm.targetUrl, onChange: (event: any) => setSubscriptionForm((prev: any) => ({ ...prev, targetUrl: event.target.value })), placeholder: "http://host.docker.internal:18080/..." })), @@ -361,7 +364,7 @@ export function ClaudeQqPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR )), h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "ClaudeQQ Subscriptions", data: state.subscriptions, onOpen: onRaw, testId: "raw-claudeqq-subscriptions" })), ), - h(Panel, { title: "最近 QQ 事件", eyebrow: state.refreshedAt ? `Updated ${fmtClock(state.refreshedAt)}` : "Event Stream" }, + h(Panel, { title: "最近 QQ 事件", eyebrow: state.refreshedAt ? `Updated ${fmtClock(state.refreshedAt)}` : "Event Stream", loading: state.loading }, events.length === 0 ? h(EmptyState, { title: "暂无事件", text: "等待 NapCat WebSocket 上报 QQ 消息事件,或通过订阅 API 消费后续事件" }) : h("div", { className: "table-wrap", "data-testid": "claudeqq-event-list" }, h("table", null, h("thead", null, h("tr", null, h("th", null, "时间"), h("th", null, "类型"), h("th", null, "会话"), h("th", null, "消息"), h("th", null, "ID"))), @@ -375,7 +378,7 @@ export function ClaudeQqPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR )), h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "ClaudeQQ Events", data: state.events, onOpen: onRaw, testId: "raw-claudeqq-events" })), ), - h(Panel, { title: "已发送消息", eyebrow: `${sent.length} Sent` }, + h(Panel, { title: "已发送消息", eyebrow: `${sent.length} Sent`, loading: state.loading }, sent.length === 0 ? h(EmptyState, { title: "暂无发送记录", text: "发送日志来自 ClaudeQQ bot_workspace/messages/sent_messages.jsonl" }) : h("div", { className: "table-wrap" }, h("table", null, h("thead", null, h("tr", null, h("th", null, "时间"), h("th", null, "目标"), h("th", null, "消息"), h("th", null, "结果"))), diff --git a/src/components/frontend/src/codex-queue-entry.tsx b/src/components/frontend/src/code-queue-entry.tsx similarity index 79% rename from src/components/frontend/src/codex-queue-entry.tsx rename to src/components/frontend/src/code-queue-entry.tsx index 2ea80962..2acff0ee 100644 --- a/src/components/frontend/src/codex-queue-entry.tsx +++ b/src/components/frontend/src/code-queue-entry.tsx @@ -1,6 +1,6 @@ import React from "react"; import { createRoot } from "react-dom/client"; -import { CodexQueuePage } from "./codex-queue"; +import { CodeQueuePage } from "./code-queue"; type AnyRecord = Record; @@ -26,15 +26,15 @@ const clientConfig = readJsonAttr("data-config", { apiBaseUrl: "/api" }); const initialOverview = readJsonAttr("data-codex-overview", null); const apiBaseUrl = String(clientConfig.apiBaseUrl || "/api"); -const standaloneCodexQueueService = { - id: "codex-queue", - name: "Codex Queue", +const standaloneCodeQueueService = { + id: "code-queue", + name: "Code Queue", providerId: "main-server", - description: "Codex Queue 独立入口,使用 summary 首屏和按需全量加载保持信息完整。", - repository: { containerName: "codex-queue-backend" }, + description: "Code Queue 独立入口,使用 summary 首屏和按需全量加载保持信息完整。", + repository: { containerName: "code-queue-backend" }, backend: { - nodeBaseUrl: "http://codex-queue:4222", - nodeBindHost: "codex-queue", + nodeBaseUrl: "http://code-queue:4222", + nodeBindHost: "code-queue", nodePort: 4222, public: false, }, @@ -57,13 +57,13 @@ function RawDialog({ raw, onClose }: AnyRecord) { ); } -function StandaloneCodexQueueApp() { +function StandaloneCodeQueueApp() { const [raw, setRaw] = useState(null); - return h("div", { className: "shell codex-standalone-shell", "data-testid": "codex-queue-standalone" }, + return h("div", { className: "shell codex-standalone-shell", "data-testid": "code-queue-standalone" }, h("main", { className: "workspace codex-standalone-workspace" }, h("div", { className: "topbar codex-standalone-topbar" }, h("div", null, - h("strong", null, "Codex Queue"), + h("strong", null, "Code Queue"), h("span", { className: "muted" }, "Standalone progressive loader"), ), h("div", { className: "topbar-actions" }, @@ -71,8 +71,8 @@ function StandaloneCodexQueueApp() { h("a", { className: "ghost-btn", href: "/" }, "完整工作台"), ), ), - h(CodexQueuePage, { - microservices: [standaloneCodexQueueService], + h(CodeQueuePage, { + microservices: [standaloneCodeQueueService], apiBaseUrl, initialTasksData: initialOverview, standalone: true, @@ -83,4 +83,4 @@ function StandaloneCodexQueueApp() { ); } -createRoot(root).render(h(StandaloneCodexQueueApp)); +createRoot(root).render(h(StandaloneCodeQueueApp)); diff --git a/src/components/frontend/src/codex-queue.tsx b/src/components/frontend/src/code-queue.tsx similarity index 72% rename from src/components/frontend/src/codex-queue.tsx rename to src/components/frontend/src/code-queue.tsx index 8382c062..e982e8e0 100644 --- a/src/components/frontend/src/codex-queue.tsx +++ b/src/components/frontend/src/code-queue.tsx @@ -1,5 +1,7 @@ import React from "react"; import { fmtClock, fmtDate } from "./time"; +import { LoadingTitle } from "./loading-indicator"; +import { MarkdownBody } from "./markdown"; import { TraceView, codexTracePort } from "./trace"; import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; @@ -89,7 +91,7 @@ async function requestJson(path: string, options: AnyRecord = {}): Promise return requestUniDeskJson(path, { strictJson: true, retryInvalidJson: 1, - invalidJsonPrefix: "Codex Queue 返回了无效 JSON", + invalidJsonPrefix: "Code Queue 返回了无效 JSON", invalidJsonPreview: true, responsePreviewLength: queueErrorPreviewLength, ...options, @@ -101,20 +103,12 @@ function StatusBadge({ status, children }: AnyRecord) { return h("span", { className: `status-badge ${normalized}` }, children || status || "unknown"); } -function MetricCard({ label, value, hint, tone }: AnyRecord) { - return h("article", { className: `metric-card ${tone || ""}` }, - h("div", { className: "metric-label" }, label), - h("div", { className: "metric-value" }, value), - h("div", { className: "metric-hint" }, hint), - ); -} - -function Panel({ title, eyebrow, summary, actions, children, className }: AnyRecord) { +function Panel({ title, eyebrow, summary, actions, children, className, loading }: AnyRecord) { return h("section", { className: `panel ${className || ""}` }, h("div", { className: "panel-head" }, h("div", null, eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null, - h("h2", null, title), + h(LoadingTitle, { title, loading }), summary ? h("div", { className: "panel-summary" }, summary) : null, ), actions ? h("div", { className: "panel-actions" }, actions) : null, @@ -144,12 +138,8 @@ function microserviceBackend(service: any): AnyRecord { return service?.backend && typeof service.backend === "object" && !Array.isArray(service.backend) ? service.backend : {}; } -function microserviceRepository(service: any): AnyRecord { - return service?.repository && typeof service.repository === "object" && !Array.isArray(service.repository) ? service.repository : {}; -} - function codexApi(apiBaseUrl: string, path: string): string { - return `${apiBaseUrl}/codex-queue-direct${path}`; + return `${apiBaseUrl}/code-queue-direct${path}`; } function taskRows(data: any): any[] { @@ -215,6 +205,19 @@ function queueQuerySuffix(queueId: string): string { return isAllQueues(queueId) ? "" : `&queueId=${encodeURIComponent(queueId)}`; } +function normalizedTaskSearchQuery(value: any): string { + return String(value || "").trim().replace(/\s+/gu, " ").slice(0, 200); +} + +function taskSearchQuerySuffix(query: string): string { + const normalized = normalizedTaskSearchQuery(query); + return normalized.length === 0 ? "" : `&search=${encodeURIComponent(normalized)}`; +} + +function taskListQuerySuffix(queueId: string, searchQuery = ""): string { + return `${queueQuerySuffix(queueId)}${taskSearchQuerySuffix(searchQuery)}`; +} + function queueStatusCount(row: any, status: string): number { return Number(row?.counts?.[status] || 0); } @@ -223,10 +226,10 @@ function knownQueueRows(queue: any, currentQueueId = ""): any[] { const byId = new Map(); for (const row of Array.isArray(queue?.queues) ? queue.queues : []) { const id = String(row?.id || "").trim(); - if (id.length > 0) byId.set(id, row); + if (id.length > 0) byId.set(id, { ...row, name: String(row?.name || id).trim() || id }); } for (const id of [String(queue?.defaultQueueId || "default"), currentQueueId].map((value) => value.trim()).filter(Boolean)) { - if (!byId.has(id)) byId.set(id, { id, total: 0, counts: {}, activeTaskId: null, runnableTaskId: null, processing: false }); + if (!byId.has(id)) byId.set(id, { id, name: id, total: 0, counts: {}, activeTaskId: null, runnableTaskId: null, processing: false }); } const rows = Array.from(byId.values()); return rows.sort((left, right) => { @@ -237,12 +240,23 @@ function knownQueueRows(queue: any, currentQueueId = ""): any[] { }); } -function queueOptionLabel(row: any): string { +function queueName(row: any): string { const id = String(row?.id || "default"); + const name = String(row?.name || "").trim(); + return name.length > 0 ? name : id; +} + +function queueDisplayName(row: any): string { + const id = String(row?.id || "default"); + const name = queueName(row); + return name === id ? id : `${name} (${id})`; +} + +function queueOptionLabel(row: any): string { const running = queueStatusCount(row, "running") + queueStatusCount(row, "judging"); const waiting = queueStatusCount(row, "queued") + queueStatusCount(row, "retry_wait"); const total = Number(row?.total || 0); - const parts = [`${id}`, `${total} tasks`]; + const parts = [queueDisplayName(row), `${total} tasks`]; if (running > 0) parts.push(`${running} running`); if (waiting > 0) parts.push(`${waiting} queued`); return parts.join(" · "); @@ -270,8 +284,8 @@ function queueRunnableTaskId(queueRows: any[], queueId: string, rows: any[]): st return String(rows.find((task: any) => String(task?.status || "") === "queued" || String(task?.status || "") === "retry_wait")?.id || ""); } -async function loadTaskQueue(apiBaseUrl: string, healthResult: any, queueId = allQueuesId): Promise { - const suffix = queueQuerySuffix(queueId); +async function loadTaskQueue(apiBaseUrl: string, healthResult: any, queueId = allQueuesId, searchQuery = ""): Promise { + const suffix = taskListQuerySuffix(queueId, searchQuery); try { return await requestJson(codexApi(apiBaseUrl, `/api/tasks?limit=${codexInitialTaskLimit}&lite=1&devReady=0${suffix}`)); } catch { @@ -286,22 +300,22 @@ async function loadTaskQueue(apiBaseUrl: string, healthResult: any, queueId = al const historyResult = await requestJson(codexApi(apiBaseUrl, `/api/tasks?limit=${codexInitialTaskLimit}&lite=1&devReady=0${suffix}`)).catch(() => null); const queue = results.find((item) => item?.queue)?.queue || historyResult?.queue || healthResult?.queue || healthResult?.body?.queue || {}; const rows = mergeTaskRows([...results.map((item) => taskRows(item)), taskRows(historyResult)], String(queue?.activeTaskId || "")); - if (rows.length > 0) return { ok: true, queue, tasks: rows }; + if (rows.length > 0) return { ok: true, queue, statistics: historyResult?.statistics || results.find((item) => item?.statistics)?.statistics || null, tasks: rows }; return requestJson(codexApi(apiBaseUrl, `/api/tasks?limit=5&lite=1&devReady=0${suffix}`)); } } -async function loadTaskOverview(apiBaseUrl: string, preferId: string, afterSeq = 0, queueId = allQueuesId): Promise { +async function loadTaskOverview(apiBaseUrl: string, preferId: string, afterSeq = 0, queueId = allQueuesId, searchQuery = ""): Promise { return requestJson(codexApi( apiBaseUrl, - `/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=3&compact=1&afterSeq=${encodeURIComponent(String(Math.max(0, afterSeq)))}&preferId=${encodeURIComponent(preferId)}${queueQuerySuffix(queueId)}`, + `/api/tasks/overview?limit=${codexInitialTaskLimit}&transcriptLimit=3&compact=1&afterSeq=${encodeURIComponent(String(Math.max(0, afterSeq)))}&preferId=${encodeURIComponent(preferId)}${taskListQuerySuffix(queueId, searchQuery)}`, )); } -async function loadTaskPage(apiBaseUrl: string, queueId: string, beforeId: string, limit = codexMoreTaskLimit): Promise { +async function loadTaskPage(apiBaseUrl: string, queueId: string, beforeId: string, limit = codexMoreTaskLimit, searchQuery = ""): Promise { return requestJson(codexApi( apiBaseUrl, - `/api/tasks?limit=${encodeURIComponent(String(limit))}&lite=1&devReady=0&includeActive=0&beforeId=${encodeURIComponent(beforeId)}${queueQuerySuffix(queueId)}`, + `/api/tasks?limit=${encodeURIComponent(String(limit))}&lite=1&devReady=0&includeActive=0&beforeId=${encodeURIComponent(beforeId)}${taskListQuerySuffix(queueId, searchQuery)}`, )); } @@ -466,7 +480,7 @@ function withReferenceHint(text: string, taskId: string): string { const ids = parseReferenceTaskIds(taskId); if (ids.length === 0) return text; return [ - `引用 Codex Queue 任务 ${ids.join(" ")}。后端会在入队时只注入这些任务的 initial prompt 和 final response 全文;中间执行过程不注入,如需补充核查可运行:${ids.map((id) => `bun scripts/cli.ts codex task ${id}`).join(";")}`, + `引用 Code Queue 任务 ${ids.join(" ")}。后端会在入队时只注入这些任务的 initial prompt 和 final response 全文;中间执行过程不注入,如需补充核查可运行:${ids.map((id) => `bun scripts/cli.ts codex task ${id}`).join(";")}`, "", "本次任务:", text, @@ -475,7 +489,7 @@ function withReferenceHint(text: string, taskId: string): string { function splitResolvedReferencePrompt(prompt: string): { hasInjection: boolean; reference: string; userPrompt: string } { const marker = "\n# 本次任务\n"; - const title = "# Codex Queue 已解析引用上下文"; + const title = "# Code Queue 已解析引用上下文"; const trimmed = prompt.trimStart(); if (!trimmed.startsWith(title)) return { hasInjection: false, reference: "", userPrompt: prompt }; const offset = prompt.length - trimmed.length; @@ -490,7 +504,7 @@ function splitResolvedReferencePrompt(prompt: string): { hasInjection: boolean; function stripAutoReferenceHint(text: string): string { const trimmed = text.trimStart(); - if (!/^引用\s+Codex Queue\s+任务\s+codex_\d+_[A-Za-z0-9_-]+/u.test(trimmed)) return text; + if (!/^引用\s+Code Queue\s+任务\s+codex_\d+_[A-Za-z0-9_-]+/u.test(trimmed)) return text; const marker = "\n\n本次任务:"; const index = trimmed.indexOf(marker); if (index === -1) return text; @@ -628,6 +642,20 @@ function attemptJudge(task: any, attempt: any): AnyRecord | null { return taskLastJudge(task); } +function attemptExecutionIsRunning(task: any, attempt: any, attemptIndex: any): boolean { + if (!taskIsExecuting(task)) return false; + if (isSyntheticAttemptSegment(attempt, attemptIndex)) return false; + if (attempt?.finishedAt) return false; + if (["succeeded", "failed", "canceled"].includes(String(attempt?.terminalStatus || ""))) return false; + const summary = taskTraceSummary(task); + const currentAttempt = Number(summary?.currentAttempt || task?.currentAttempt || 0); + const numericAttempt = Number(attemptIndex); + if (Number.isFinite(numericAttempt) && numericAttempt > 0 && Number.isFinite(currentAttempt) && currentAttempt > 0) { + return numericAttempt === currentAttempt; + } + return true; +} + function feedbackPromptDetailKey(attemptIndex: any): string { return `feedback:${String(attemptIndex || "latest")}`; } @@ -682,6 +710,16 @@ function traceStepSummaryLines(step: any): string[] { return (Array.isArray(step?.summaryLines) ? step.summaryLines : []).map((line: any) => String(line || "")); } +function traceStepIsError(step: any): boolean { + const kind = String(step?.kind || "").trim().toLowerCase(); + const status = String(step?.status || "").trim().toLowerCase(); + return kind === "error" || status === "error"; +} + +function traceStepErrorCount(steps: any[]): number { + return (Array.isArray(steps) ? steps : []).reduce((count, step) => count + (traceStepIsError(step) ? 1 : 0), 0); +} + function traceStepFileChangeMethod(step: any): string { const status = String(step?.status || "").trim(); if (status.length > 0) return status; @@ -759,20 +797,38 @@ function coalesceFileChangeTraceSteps(steps: any[]): any[] { return merged; } -function executionSummaryWithDisplayedSteps(execution: AnyRecord, steps: any[]): AnyRecord { - if (steps.length === 0) return execution; - const counts = steps.reduce((memo: AnyRecord, step: any) => { +function toolStepCountFromExecution(execution: AnyRecord): number { + const value = Number(execution?.toolCallCount); + if (Number.isFinite(value) && value >= 0) return Math.floor(value); + const total = Number(execution?.readCount || 0) + Number(execution?.editCount || 0) + Number(execution?.runCount || 0); + return Number.isFinite(total) && total >= 0 ? Math.floor(total) : 0; +} + +function toolStepCountsFromTraceSteps(steps: any[]): AnyRecord { + const rows = Array.isArray(steps) ? steps : []; + const counts = rows.reduce((memo: AnyRecord, step: any) => { const kind = String(step?.kind || ""); if (kind === "explored") memo.readCount += 1; else if (kind === "edited") memo.editCount += 1; else if (kind === "ran") memo.runCount += 1; return memo; }, { readCount: 0, editCount: 0, runCount: 0 }); + counts.toolCallCount = counts.readCount + counts.editCount + counts.runCount; + return counts; +} + +function executionSummaryWithDisplayedSteps(execution: AnyRecord, steps: any[]): AnyRecord { + if (steps.length === 0) { + const stepCount = toolStepCountFromExecution(execution); + return { ...execution, stepCount, llmStepCount: stepCount }; + } + const counts = toolStepCountsFromTraceSteps(steps); return { ...execution, ...counts, - toolCallCount: counts.readCount + counts.editCount + counts.runCount, - stepCount: steps.length, + stepCount: counts.toolCallCount, + llmStepCount: counts.toolCallCount, + traceLineCount: steps.length, }; } @@ -832,10 +888,24 @@ function taskIsActive(task: any): boolean { return ["running", "judging", "retry_wait"].includes(String(task?.status || "")); } +function taskIsExecuting(task: any): boolean { + return String(task?.status || "") === "running"; +} + function taskIsTerminal(task: any): boolean { return ["succeeded", "failed", "canceled"].includes(String(task?.status || "")); } +function taskPromptEditable(task: any): boolean { + if (task?.promptEditable === true) return true; + if (task?.promptEditable === false) return false; + return String(task?.status || "") === "queued" + && !task?.startedAt + && Number(task?.currentAttempt || 0) === 0 + && !task?.codexThreadId + && !task?.nextMode; +} + function taskIsUnreadTerminal(task: any): boolean { if (!taskIsTerminal(task)) return false; if (task?.terminalUnread === true) return true; @@ -856,6 +926,76 @@ function queueRunningCount(counts: AnyRecord): number { return countNumber(counts.running) + countNumber(counts.judging); } +function taskStatistics(data: any, fallbackQueue: any): AnyRecord { + return objectRecord(data?.statistics) || objectRecord(fallbackQueue?.statistics) || {}; +} + +function taskStatisticsRows(stats: any): any[] { + return Array.isArray(stats?.daily) ? stats.daily : []; +} + +function taskStatisticsTotals(stats: any): AnyRecord { + return objectRecord(stats?.totals) || {}; +} + +function statMetric(row: any, key: string): number { + const value = Number(row?.[key] ?? 0); + return Number.isFinite(value) && value > 0 ? value : 0; +} + +function statMetricMax(rows: any[], key: string): number { + return rows.reduce((max, row) => Math.max(max, statMetric(row, key)), 0); +} + +const statChartWidth = 700; +const statChartHeight = 220; +const statChartPaddingX = 30; +const statChartTopY = 24; +const statChartBaselineY = 184; +const statChartValueHeight = statChartBaselineY - statChartTopY; + +function statPointX(index: number, total: number): number { + if (total <= 1) return statChartWidth / 2; + return statChartPaddingX + (index * (statChartWidth - statChartPaddingX * 2)) / (total - 1); +} + +function statPointY(value: number, max: number): number { + const denominator = max > 0 ? max : 1; + return statChartBaselineY - Math.min(1, value / denominator) * statChartValueHeight; +} + +function statLinePoints(rows: any[], key: string, max: number): string { + const source = rows.length > 0 ? rows : [{ [key]: 0 }]; + const chartRows = source.length > 1 ? source : [source[0], source[0]]; + return chartRows + .map((row, index) => `${statPointX(index, chartRows.length).toFixed(2)},${statPointY(statMetric(row, key), max).toFixed(2)}`) + .join(" "); +} + +function statDateLabel(value: any): string { + const text = String(value || ""); + return /^\d{4}-\d{2}-\d{2}$/u.test(text) ? text.slice(5) : text || "--"; +} + +function statPointId(point: any): string { + if (!point) return ""; + return `${String(point.seriesKey || "")}:${String(point.row?.date || point.index || "")}`; +} + +function statPointFor(row: any, index: number, total: number, series: AnyRecord): AnyRecord { + const value = statMetric(row, series.key); + return { + ...series, + row, + index, + value, + valueLabel: series.format(value), + x: statPointX(index, total), + y: statPointY(value, series.max), + seriesKey: series.key, + }; +} + function taskListRank(task: any): number { if (taskIsUnreadTerminal(task)) return 0; const statusRank: Record = { running: 1, judging: 2, retry_wait: 3, queued: 4, succeeded: 8, failed: 8, canceled: 8 }; @@ -976,21 +1116,63 @@ function transcriptResumeSeq(transcript: any[], overlapRows = 8): number { return Math.max(0, anchor - 0.001); } -function countValue(counts: AnyRecord, key: string): string { - const value = Number(counts[key] ?? 0); - return Number.isFinite(value) ? String(value) : "0"; +function codexModelOptions(queue: any, currentModel: string): string[] { + const configured = Array.isArray(queue?.codeModels) ? queue.codeModels : Array.isArray(queue?.codexModels) ? queue.codexModels : []; + const fallback = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4", "minimax-m2.7"]; + return Array.from(new Set([...configured, ...fallback, currentModel].map((item) => String(item || "").trim()).filter(Boolean))); } -function codexModelOptions(queue: any, currentModel: string): string[] { - const configured = Array.isArray(queue?.codexModels) ? queue.codexModels : []; - const fallback = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4"]; - return Array.from(new Set([...configured, ...fallback, currentModel].map((item) => String(item || "").trim()).filter(Boolean))); +function codexProviderOptions(queue: any, currentProviderId: string): any[] { + const configured = Array.isArray(queue?.executionProviders) ? queue.executionProviders : []; + const rows = configured + .map((item: any) => ({ + id: String(item?.id || "").trim(), + label: String(item?.label || item?.id || "").trim(), + defaultWorkdir: String(item?.defaultWorkdir || "").trim(), + kind: String(item?.kind || "").trim(), + })) + .filter((item: any) => item.id.length > 0); + const fallbackMain = String(queue?.mainProviderId || queue?.defaultProviderId || "main-server").trim() || "main-server"; + const byId = new Map(); + for (const item of [ + ...rows, + { id: fallbackMain, label: `${fallbackMain} (master)`, defaultWorkdir: String(queue?.defaultWorkdir || "/root/unidesk"), kind: "local" }, + currentProviderId ? { id: currentProviderId, label: currentProviderId, defaultWorkdir: providerDefaultWorkdir(queue, currentProviderId), kind: "" } : null, + ].filter(Boolean) as any[]) { + if (!byId.has(item.id)) byId.set(item.id, item); + } + return Array.from(byId.values()); +} + +function providerDefaultWorkdir(queue: any, providerId: string): string { + const id = String(providerId || "").trim(); + const map = queue?.defaultWorkdirByProvider && typeof queue.defaultWorkdirByProvider === "object" ? queue.defaultWorkdirByProvider : {}; + if (typeof map[id] === "string" && String(map[id]).trim().length > 0) return String(map[id]).trim(); + const option = Array.isArray(queue?.executionProviders) ? queue.executionProviders.find((item: any) => String(item?.id || "") === id) : null; + if (typeof option?.defaultWorkdir === "string" && option.defaultWorkdir.trim().length > 0) return option.defaultWorkdir.trim(); + const mainProvider = String(queue?.mainProviderId || queue?.defaultProviderId || "main-server"); + return id === mainProvider ? String(queue?.defaultWorkdir || "/root/unidesk") : String(queue?.remoteDefaultWorkdir || "/home/ubuntu"); +} + +function taskStepCount(task: any): number { + const attempts = taskProgressiveAttempts(task).filter((attempt: any) => !isSyntheticAttemptSegment(attempt, attempt?.index)); + if (attempts.length > 0) { + const attemptTotal = attempts.reduce((sum: number, attempt: any) => sum + toolStepCountFromExecution(attemptExecutionSummary(task, attempt)), 0); + if (attemptTotal > 0) return attemptTotal; + } + const executionTotal = toolStepCountFromExecution(taskExecutionSummary(task)); + if (executionTotal > 0) return executionTotal; + const apiTotal = Number(task?.stepCount ?? task?.llmStepCount ?? 0); + return Number.isFinite(apiTotal) && apiTotal >= 0 ? Math.floor(apiTotal) : 0; } function TaskCard({ task, selected, onSelect, onCopy, onReference, onMarkRead, copied, markingRead }: AnyRecord) { const judge = task?.lastJudge || {}; const taskId = String(task?.id || ""); const unread = taskIsUnreadTerminal(task); + const updatedAt = latestTimestampValue(task?.updatedAt, taskTraceSummary(task)?.updatedAt); + const recentUpdateLabel = `最近更新: ${fmtRelativeAge(updatedAt)}`; + const stepCount = taskStepCount(task); return h("article", { role: "button", tabIndex: 0, @@ -1048,12 +1230,16 @@ function TaskCard({ task, selected, onSelect, onCopy, onReference, onMarkRead, c h("strong", null, shortText(taskDisplayPrompt(task), 120) || "空任务"), h("div", { className: "codex-task-meta" }, h("span", null, `queue=${taskQueueLabel(task)}`), + h("span", null, `provider=${task?.providerId || "main-server"}`), h("span", null, task?.model || "--"), h("span", null, taskDurationLabel(task)), ), - h("div", { className: "codex-task-meta" }, - h("span", null, fmtDate(task?.updatedAt)), + h("div", { className: "codex-task-meta codex-task-update-meta" }, + h("span", { className: "codex-task-recent-update codex-task-step-count", title: "STEP 按 read/edit/run 工具动作统计", "data-testid": `codex-task-step-count-${taskId || "unknown"}` }, `STEP ${stepCount}`), + h("span", { className: "codex-task-recent-update", title: updatedAt ? `更新时间: ${fmtDate(updatedAt)}` : recentUpdateLabel, "data-testid": `codex-task-recent-update-${taskId || "unknown"}` }, recentUpdateLabel), + h("span", null, fmtDate(updatedAt || task?.updatedAt)), ), + taskPromptEditable(task) ? h("div", { className: "codex-judge-line", "data-testid": `codex-task-prompt-editable-${taskId || "unknown"}` }, "queued prompt 可编辑") : null, judge?.decision ? h("div", { className: "codex-judge-line" }, `judge=${judge.decision} ${Math.round(Number(judge.confidence || 0) * 100)}%`) : null, ); } @@ -1081,6 +1267,155 @@ function TaskListSection({ title, tasks, selectedId, onSelect, onCopy, onReferen ); } +function CodexStatsIcon() { + return h("span", { className: "codex-stats-icon", "aria-hidden": "true" }, + h("svg", { viewBox: "0 0 36 24", focusable: "false" }, + h("path", { className: "grid", d: "M3 20.5H33M3 12.5H33M3 4.5H33" }), + h("polyline", { className: "line tasks", points: "3,18 9,14 15,15 21,8 27,10 33,4" }), + h("polyline", { className: "line retry", points: "3,20 9,17 15,18 21,13 27,14 33,9" }), + ), + ); +} + +function CodexStatsPanel({ stats, queueName: activeQueueName, onRaw }: AnyRecord) { + const rows = taskStatisticsRows(stats); + const totals = taskStatisticsTotals(stats); + const latest = rows.at(-1) || {}; + const executedMax = statMetricMax(rows, "executedTasks"); + const retryMax = statMetricMax(rows, "retryAttempts"); + const durationMax = statMetricMax(rows, "avgDurationMs"); + const hasRows = rows.length > 0; + const statsRange = objectRecord(stats?.range) || {}; + const [hoveredStatPoint, setHoveredStatPoint] = useState(null); + const [pinnedStatPoint, setPinnedStatPoint] = useState(null); + const maxLabelParts: string[] = []; + if (executedMax > 0) maxLabelParts.push(`tasks ${executedMax}`); + if (retryMax > 0) maxLabelParts.push(`retry ${retryMax}`); + if (durationMax > 0) maxLabelParts.push(`avg ${fmtDuration(durationMax)}`); + const statSeries = [ + { key: "executedTasks", className: "tasks", label: "执行任务", max: executedMax, format: (value: any) => `${countNumber(value)} tasks` }, + { key: "retryAttempts", className: "retry", label: "重试次数", max: retryMax, format: (value: any) => `${countNumber(value)} retries` }, + { key: "avgDurationMs", className: "duration", label: "平均耗时", max: durationMax, format: (value: any) => fmtDuration(value) }, + ]; + const activeStatPoint = hoveredStatPoint || pinnedStatPoint; + const activeStatPointId = statPointId(activeStatPoint); + const activeDate = String(activeStatPoint?.row?.date || ""); + const activeTooltipStyle = activeStatPoint ? { + left: `${Math.max(8, Math.min(92, (Number(activeStatPoint.x) / statChartWidth) * 100))}%`, + top: `${Math.max(14, Math.min(86, (Number(activeStatPoint.y) / statChartHeight) * 100))}%`, + } : undefined; + useEffect(() => { + setHoveredStatPoint(null); + setPinnedStatPoint(null); + }, [activeQueueName, statsRange.startDate, statsRange.endDate, rows.length]); + const activateStatPoint = (point: AnyRecord) => { + setHoveredStatPoint(point); + }; + const togglePinnedStatPoint = (point: AnyRecord) => { + const pointId = statPointId(point); + setPinnedStatPoint((current: any) => statPointId(current) === pointId ? null : point); + setHoveredStatPoint(point); + }; + const statPointNodes = statSeries.flatMap((series) => rows.map((row: any, index: number) => { + const point = statPointFor(row, index, rows.length, series); + const pointId = statPointId(point); + const active = activeStatPointId === pointId; + const date = String(row?.date || `day-${index}`); + const ariaLabel = `${statDateLabel(date)} ${series.label}: ${point.valueLabel}`; + return h("g", { + key: `${series.key}-${date}`, + className: `stat-point-group ${series.className} ${active ? "active" : ""}`, + role: "button", + tabIndex: 0, + "aria-label": ariaLabel, + "data-testid": `codex-stats-point-${series.className}-${date}`, + onMouseEnter: () => activateStatPoint(point), + onFocus: () => activateStatPoint(point), + onClick: () => togglePinnedStatPoint(point), + onKeyDown: (event: any) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + togglePinnedStatPoint(point); + } + }, + }, + h("circle", { className: "stat-hit-point", cx: point.x, cy: point.y, r: 13 }), + h("circle", { className: `stat-point ${series.className} ${active ? "active" : ""}`, cx: point.x, cy: point.y, r: active ? 5.6 : 4.2 }), + ); + })); + return h(Panel, { + title: "统计曲线", + eyebrow: `Daily task stats / ${activeQueueName}`, + className: "codex-stats-panel", + summary: h("span", null, `${statDateLabel(statsRange.startDate)} -> ${statDateLabel(statsRange.endDate)} · ${stats?.timezone || "Asia/Shanghai"}`), + actions: objectRecord(stats) ? h(RawButton, { title: "Code Queue Stats", data: stats, onOpen: onRaw, testId: "raw-codex-stats" }) : null, + }, + h("div", { className: "codex-stats-hero", "data-testid": "codex-stats-panel" }, + h(CodexStatsIcon), + h("div", null, + h("strong", null, `${countNumber(totals.executedTasks)} tasks / ${countNumber(totals.retryAttempts)} retries`), + h("span", null, `平均完成耗时 ${fmtDuration(totals.avgDurationMs ?? undefined)} · 终态 ${countNumber(totals.completedTasks)} 个`), + ), + ), + hasRows ? h("div", { className: "codex-stats-chart", "data-testid": "codex-stats-chart", onMouseLeave: () => setHoveredStatPoint(null) }, + h("svg", { viewBox: `0 0 ${statChartWidth} ${statChartHeight}`, preserveAspectRatio: "none", role: "img", "aria-label": "Code Queue daily task statistics" }, + h("line", { className: "axis", x1: statChartPaddingX, x2: statChartWidth - statChartPaddingX, y1: statChartBaselineY, y2: statChartBaselineY }), + h("line", { className: "grid", x1: statChartPaddingX, x2: statChartWidth - statChartPaddingX, y1: statChartTopY + statChartValueHeight / 2, y2: statChartTopY + statChartValueHeight / 2 }), + h("line", { className: "grid", x1: statChartPaddingX, x2: statChartWidth - statChartPaddingX, y1: statChartTopY, y2: statChartTopY }), + h("polyline", { className: "stat-line tasks", points: statLinePoints(rows, "executedTasks", executedMax) }), + h("polyline", { className: "stat-line retry", points: statLinePoints(rows, "retryAttempts", retryMax) }), + h("polyline", { className: "stat-line duration", points: statLinePoints(rows, "avgDurationMs", durationMax) }), + activeStatPoint ? h("g", { className: "stat-cursor-layer", "data-testid": "codex-stats-active-point" }, + h("line", { className: "stat-cursor", x1: activeStatPoint.x, x2: activeStatPoint.x, y1: statChartTopY, y2: statChartBaselineY }), + h("circle", { className: `stat-point-active ${activeStatPoint.className}`, cx: activeStatPoint.x, cy: activeStatPoint.y, r: 8 }), + ) : null, + h("g", { className: "stat-point-layer" }, statPointNodes), + ), + activeStatPoint ? h("div", { className: "codex-stats-tooltip active", style: activeTooltipStyle, "data-testid": "codex-stats-tooltip" }, + h("b", null, statDateLabel(activeStatPoint.row?.date)), + h("span", null, `${activeStatPoint.label} · ${activeStatPoint.valueLabel}`), + h("code", null, `${countNumber(activeStatPoint.row?.executedTasks)} exec / ${countNumber(activeStatPoint.row?.retryAttempts)} retry / ${fmtDuration(activeStatPoint.row?.avgDurationMs ?? undefined)}`), + ) : null, + h("div", { className: "codex-stats-legend" }, + h("span", { className: "tasks" }, "执行任务"), + h("span", { className: "retry" }, "重试次数"), + h("span", { className: "duration" }, "平均耗时"), + ), + h("div", { className: "codex-stats-scale" }, + h("span", null, statDateLabel(rows[0]?.date)), + h("span", null, maxLabelParts.join(" · ") || "暂无峰值"), + h("span", null, statDateLabel(rows.at(-1)?.date)), + ), + h("div", { className: `codex-stats-focus ${activeStatPoint ? "active" : ""}`, "data-testid": "codex-stats-focus" }, + activeStatPoint ? h(React.Fragment, null, + h("div", null, + h("strong", null, statDateLabel(activeStatPoint.row?.date)), + h("span", null, `${activeStatPoint.label} · ${activeStatPoint.valueLabel}`), + ), + h("div", { className: "codex-stats-focus-metrics" }, + h("code", null, `${countNumber(activeStatPoint.row?.executedTasks)} exec`), + h("code", null, `${countNumber(activeStatPoint.row?.retryAttempts)} retry`), + h("code", null, fmtDuration(activeStatPoint.row?.avgDurationMs ?? undefined)), + ), + ) : h("span", null, "将鼠标悬停到曲线数据点查看明细,点击数据点可固定。"), + ), + ) : h(EmptyState, { title: "暂无统计", text: "任务开始执行后会生成按天汇总的曲线。" }), + h("div", { className: "codex-stats-summary-grid" }, + h("article", null, h("span", null, "今日执行"), h("strong", null, String(countNumber(latest.executedTasks))), h("code", null, statDateLabel(latest.date))), + h("article", null, h("span", null, "今日重试"), h("strong", null, String(countNumber(latest.retryAttempts))), h("code", null, `累计 ${countNumber(totals.retryAttempts)}`)), + h("article", null, h("span", null, "平均耗时"), h("strong", null, fmtDuration(totals.avgDurationMs ?? undefined)), h("code", null, `${countNumber(totals.durationSamples)} samples`)), + ), + h("div", { className: "codex-stats-daily-list", "data-testid": "codex-stats-daily-list" }, + rows.slice(-7).map((row: any) => h("div", { key: String(row?.date || ""), className: `codex-stats-daily-row ${activeDate === String(row?.date || "") ? "active" : ""}`, "data-testid": `codex-stats-day-${String(row?.date || "unknown")}` }, + h("span", null, statDateLabel(row?.date)), + h("b", null, `${countNumber(row?.executedTasks)} exec`), + h("b", null, `${countNumber(row?.retryAttempts)} retry`), + h("code", null, fmtDuration(row?.avgDurationMs ?? undefined)), + )), + ), + ); +} + function TaskQueueMoveControl({ task, queueRows, busy, onMove }: AnyRecord) { const taskId = String(task?.id || ""); const currentQueue = taskQueueLabel(task); @@ -1142,7 +1477,7 @@ function ProgressivePromptBlock({ task, loading, onLoadPromptPart, testId = "cod }, }, h("summary", null, - h("span", null, "引用注入已折叠,点击按需拉取最终进入 opencode 的完整 prompt"), + h("span", null, "引用注入已折叠,点击按需拉取最终进入 Code agent 的完整 prompt"), h("code", null, fullPrompt ? `${fullLines || promptLineCount(fullPrompt)} lines / ${fullPrompt.length} chars` : `${Number.isFinite(fullChars) && fullChars > 0 ? fullChars : "--"} chars`), @@ -1155,19 +1490,24 @@ function ProgressivePromptBlock({ task, loading, onLoadPromptPart, testId = "cod function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onLoadSteps, onLoadStep, testId = "codex-execution-summary" }: AnyRecord) { const steps = coalesceFileChangeTraceSteps(taskTraceSteps(task, attemptIndex)); const execution = executionSummaryWithDisplayedSteps(attemptExecutionSummary(task, attempt), steps); + const summary = taskTraceSummary(task); const stepDetails = taskTraceStepDetails(task); const stepsLoaded = taskTraceStepsLoaded(task, attemptIndex); + const errorCount = Number(attempt?.errorCount ?? summary?.errorCount ?? traceStepErrorCount(steps)); const toolCount = Number(execution.toolCallCount || 0); + const stepCount = Number(execution.stepCount ?? execution.llmStepCount ?? 0); const editedFiles = Array.isArray(execution.editedFiles) ? execution.editedFiles : []; const commands = Array.isArray(execution.commands) ? execution.commands : []; const synthetic = isSyntheticAttemptSegment(attempt, attemptIndex); const labelSuffix = synthetic ? ` · ${String(attempt?.label || "recovered thread execution")}` : attemptIndex ? ` #${attemptIndex}` : ""; const updatedAt = attemptExecutionUpdatedAt(task, attempt, attemptIndex, execution); const recentUpdateLabel = `最近更新: ${fmtRelativeAge(updatedAt)}`; + const running = attemptExecutionIsRunning(task, attempt, attemptIndex); return h("details", { - className: "codex-progressive-card codex-execution-summary", + className: `codex-progressive-card codex-execution-summary ${running ? "running" : ""}`, "data-testid": testId, "data-attempt-index": attemptDataIndex(attemptIndex), + "data-running": running ? "true" : "false", onToggle: (event: any) => { if (event.currentTarget?.open && !stepsLoaded) onLoadSteps?.(attemptIndex); }, @@ -1176,6 +1516,7 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL h("div", { className: "codex-progressive-card-head" }, h("span", { className: "codex-output-channel" }, "Summary"), h("strong", null, `执行过程摘要${labelSuffix}`), + running ? h("span", { className: "codex-summary-running-pill", "data-testid": `${testId}-running` }, "执行中") : null, h("code", { title: updatedAt ? `最近更新: ${fmtDate(updatedAt)}` : recentUpdateLabel }, `${fmtDuration(execution.durationMs ?? execution.totalElapsedMs)} / ${toolCount} tools / ${recentUpdateLabel}`), ), @@ -1183,7 +1524,8 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL h("span", null, `read ${Number(execution.readCount || 0)}`), h("span", null, `edit ${Number(execution.editCount || 0)}`), h("span", null, `run ${Number(execution.runCount || 0)}`), - h("span", null, `${Number(execution.stepCount || steps.length || 0)} steps`), + h("span", null, `STEP ${Number.isFinite(stepCount) ? Math.max(0, Math.floor(stepCount)) : 0}`), + errorCount > 0 ? h("span", { className: "codex-execution-error-pill", "data-testid": `${testId}-error-count` }, `Error ${errorCount}`) : null, ), ), h("div", { className: "codex-execution-digest expanded" }, @@ -1198,7 +1540,7 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL const summaryLines = Array.isArray(step?.summaryLines) ? step.summaryLines.slice(0, 4) : []; return h("details", { key: seq || `${step?.title}-${step?.at}`, - className: `codex-trace-step ${String(step?.kind || "message")}`, + className: `codex-trace-step ${String(step?.kind || "message")} ${traceStepIsError(step) ? "error" : ""}`, "data-testid": `codex-trace-step-${seq || "unknown"}`, onToggle: (event: any) => { if (event.currentTarget?.open && !detail) onLoadStep?.(step?.seq); @@ -1242,6 +1584,7 @@ function traceStepKindLabel(kind: any): string { function ProgressiveFinalResponse({ task, attempt, attemptIndex, testId = "codex-final-response" }: AnyRecord) { const text = attemptFinalResponseText(task, attempt); + if (text.length === 0) return null; const chars = Number(attempt?.finalResponseChars || text.length); const labelSuffix = attemptIndex ? ` #${attemptIndex}` : ""; return h("section", { className: "codex-progressive-card codex-final-response", "data-testid": testId, "data-attempt-index": attemptDataIndex(attemptIndex) }, @@ -1250,25 +1593,26 @@ function ProgressiveFinalResponse({ task, attempt, attemptIndex, testId = "codex h("strong", null, `最终 response${labelSuffix}`), h("code", null, `${Number.isFinite(chars) ? chars : text.length} chars`), ), - h("pre", { className: "codex-transcript-body" }, text || "暂无最终 response"), + h(MarkdownBody, { markdown: text, className: "codex-transcript-body codex-markdown", testId: `${testId}-markdown` }), ); } function ProgressiveJudge({ task, attempt, attemptIndex, testId = "codex-progressive-judge" }: AnyRecord) { const judge = attemptJudge(task, attempt); + if (!judge?.decision) return null; const labelSuffix = attemptIndex ? ` #${attemptIndex}` : ""; return h("section", { className: "codex-progressive-card codex-progressive-judge", "data-testid": testId, "data-attempt-index": attemptDataIndex(attemptIndex) }, h("div", { className: "codex-progressive-card-head" }, h("span", { className: "codex-output-channel" }, "Judge"), h("strong", null, `完成判定${labelSuffix}`), - judge?.decision ? h("code", null, `${judge.decision} ${Math.round(Number(judge.confidence || 0) * 100)}%`) : null, + h("code", null, `${judge.decision} ${Math.round(Number(judge.confidence || 0) * 100)}%`), ), - judge ? h("div", { className: "codex-judge-card", "data-testid": `${testId}-card` }, + h("div", { className: "codex-judge-card", "data-testid": `${testId}-card` }, h(StatusBadge, { status: judge.decision }, judge.decision), h("strong", null, `${Math.round(Number(judge.confidence || 0) * 100)}% confidence`), h("p", { "data-testid": `${testId}-reason` }, judge.reason || "--"), judge.continuePrompt ? h("pre", { "data-testid": `${testId}-continue-prompt` }, String(judge.continuePrompt || "")) : null, - ) : h("div", { className: "codex-output-empty" }, "尚未判定"), + ), ); } @@ -1409,8 +1753,8 @@ function AttemptTable({ task }: AnyRecord) { ); } -export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initialTasksData = null, standalone = false }: AnyRecord) { - const service = microservices.find((item: any) => item.id === "codex-queue") || null; +export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initialTasksData = null, standalone = false }: AnyRecord) { + const service = microservices.find((item: any) => item.id === "code-queue") || null; const initialSelectedTask = overviewSelectedTask(initialTasksData); const initialSelectedId = String(initialSelectedTask?.id || ""); const initialSessionCache = new Map(); @@ -1425,9 +1769,11 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const initialLoadMs = typeof performance === "undefined" ? 0 : performance.now(); const selectedIdRef = useRef(initialSelectedId); const queueLoadTokenRef = useRef(0); + const searchLoadTokenRef = useRef(0); const detailLoadTokenRef = useRef(0); const enqueueInFlightRef = useRef(false); const loadMoreInFlightRef = useRef(false); + const searchLoadMoreInFlightRef = useRef(false); const detailInFlightRef = useRef<{ taskId: string; token: number; promise: Promise } | null>(null); const traceSummaryInFlightRef = useRef>>(new Map()); const promptDetailInFlightRef = useRef>>(new Map()); @@ -1436,6 +1782,8 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const autoTraceLoadKeysRef = useRef>(new Set()); const trackedLoadInFlightRef = useRef(false); const initialLoadSkippedRef = useRef(Boolean(initialTasksData)); + const locallyReadTaskIdsRef = useRef>(new Map()); + const dismissedReadTaskIdsRef = useRef>(new Set()); const sessionCacheRef = useRef>(initialSessionCache); const tasksDataRef = useRef(initialTasksData); const [health, setHealth] = useState(null); @@ -1443,10 +1791,15 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const [selectedId, setSelectedId] = useState(initialSelectedId); const [selectedTask, setSelectedTask] = useState(initialSelectedTask); const [selectedDetailLoading, setSelectedDetailLoading] = useState(false); + const [taskSearchQuery, setTaskSearchQuery] = useState(""); + const [searchTasksData, setSearchTasksData] = useState(null); + const [searchLoading, setSearchLoading] = useState(false); + const [searchLoadingMoreTasks, setSearchLoadingMoreTasks] = useState(false); const [prompt, setPrompt] = useState(""); const [referenceTaskId, setReferenceTaskId] = useState(""); const [queueId, setQueueId] = useState("default"); const [selectedQueueId, setSelectedQueueId] = useState(allQueuesId); + const [providerId, setProviderId] = useState("main-server"); const [model, setModel] = useState("gpt-5.5"); const [cwd, setCwd] = useState("/root/unidesk"); const [maxAttempts, setMaxAttempts] = useState(99); @@ -1454,6 +1807,8 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const [batchConfirmed, setBatchConfirmed] = useState(false); const [submitting, setSubmitting] = useState(false); const [steerPrompt, setSteerPrompt] = useState(""); + const [editPrompt, setEditPrompt] = useState(""); + const [editReferenceTaskId, setEditReferenceTaskId] = useState(""); const [autoScroll, setAutoScroll] = useState(true); const [queueSidebarOpen, setQueueSidebarOpen] = useState(() => typeof window === "undefined" ? true : window.matchMedia(queueDesktopMediaQuery).matches); const [busy, setBusy] = useState(false); @@ -1476,16 +1831,14 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const [refreshedAt, setRefreshedAt] = useState(initialTasksData ? new Date() : null); const [loadingMoreTasks, setLoadingMoreTasks] = useState(false); - const tasks = taskRows(tasksData); - const unreadTerminalTasks = tasks.filter(taskIsUnreadTerminal); - const queuedTasks = tasks.filter((task: any) => !taskIsTerminal(task)); - const historyTasks = tasks.filter((task: any) => taskIsTerminal(task) && !taskIsUnreadTerminal(task)); + const tasks = applyLocalReadStateToRows(taskRows(tasksData)); + const loadedUnreadTerminalTasks = tasks.filter(taskIsUnreadTerminal); const queue = tasksData?.queue || health?.body?.queue || health?.queue || {}; + const statistics = taskStatistics(tasksData, queue); const pagination = taskPagination(tasksData); const queueRows = knownQueueRows(queue, queueId); const viewQueueRow = selectedQueueRow(queueRows, selectedQueueId); const totalTaskCount = Number((isAllQueues(selectedQueueId) ? queue?.total : viewQueueRow?.total) ?? pagination.total ?? tasks.length); - const hasMoreTasks = pagination.hasMore === true && String(pagination.nextBeforeId || "").length > 0; const globalActiveIds = activeTaskIds(queue); const activeIds = isAllQueues(selectedQueueId) ? globalActiveIds @@ -1495,10 +1848,22 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const globalCounts = queueCounts(queue); const overallQueuedCount = queueWaitingCount(globalCounts); const overallRunningCount = Math.max(queueRunningCount(globalCounts), globalActiveIds.length); - const overallUnreadTerminalCount = countNumber((isAllQueues(selectedQueueId) ? queue?.unreadTerminal : viewQueueRow?.unreadTerminal) ?? unreadTerminalTasks.length); - const selectedQueueName = isAllQueues(selectedQueueId) ? "All queues" : selectedQueueId; + const rawUnreadTerminalCount = countNumber((isAllQueues(selectedQueueId) ? queue?.unreadTerminal : viewQueueRow?.unreadTerminal) ?? loadedUnreadTerminalTasks.length); + const overallUnreadTerminalCount = tasksData ? loadedUnreadTerminalTasks.length : rawUnreadTerminalCount; + const selectedQueueName = isAllQueues(selectedQueueId) ? "All queues" : queueDisplayName(viewQueueRow || { id: selectedQueueId, name: selectedQueueId }); + const normalizedSearchQuery = normalizedTaskSearchQuery(taskSearchQuery); + const searchActive = normalizedSearchQuery.length > 0; + const searchTasks = searchActive ? applyLocalReadStateToRows(taskRows(searchTasksData)) : []; + const searchPagination = taskPagination(searchTasksData); + const sidebarTasks = searchActive ? searchTasks : tasks; + const unreadTerminalTasks = sidebarTasks.filter(taskIsUnreadTerminal); + const queuedTasks = sidebarTasks.filter((task: any) => !taskIsTerminal(task)); + const historyTasks = sidebarTasks.filter((task: any) => taskIsTerminal(task) && !taskIsUnreadTerminal(task)); + const sidebarPagination = searchActive ? searchPagination : pagination; + const sidebarTotalTaskCount = searchActive ? Number(searchPagination.total ?? searchTasks.length) : totalTaskCount; + const sidebarHasMoreTasks = sidebarPagination.hasMore === true && String(sidebarPagination.nextBeforeId || "").length > 0; + const sidebarLoadingMoreTasks = searchActive ? searchLoadingMoreTasks : loadingMoreTasks; const runtime = service ? microserviceRuntime(service) : {}; - const repository = service ? microserviceRepository(service) : {}; const backend = service ? microserviceBackend(service) : {}; const promptParts = useMemo(() => splitPromptTasks(prompt), [prompt]); const enqueueItems = useMemo(() => { @@ -1509,9 +1874,12 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const batchNeedsConfirmation = enqueueCount > 1 && !batchConfirmed; const submitDisabled = submitting || busy || enqueueCount === 0 || batchNeedsConfirmation; const codexModels = codexModelOptions(queue, model); + const providerOptions = codexProviderOptions(queue, providerId); + const currentProviderDefaultWorkdir = providerDefaultWorkdir(queue, providerId); const selectedCanSteer = selectedTask?.id && selectedTask?.activeTurnId && String(selectedTask?.status) === "running"; const selectedCanInterrupt = selectedTask?.id && !["succeeded", "failed", "canceled"].includes(String(selectedTask?.status || "")); const selectedCanRetry = selectedTask?.id && ["succeeded", "failed", "canceled"].includes(String(selectedTask?.status || "")); + const selectedCanEditPrompt = selectedTask?.id && taskPromptEditable(selectedTask); function setTasksData(nextOrUpdater: any): any { const next = typeof nextOrUpdater === "function" ? nextOrUpdater(tasksDataRef.current) : nextOrUpdater; @@ -1520,16 +1888,81 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init return next; } + function rememberLocalReadState(taskIds: string[], readAt: string, hideFromSidebar = true): string[] { + const ids = Array.from(new Set(taskIds.map((id) => String(id || "")).filter(Boolean))); + for (const id of ids) { + locallyReadTaskIdsRef.current.set(id, readAt); + if (hideFromSidebar) dismissedReadTaskIdsRef.current.add(id); + } + return ids; + } + + function forgetLocalReadState(taskIds: string[]): void { + for (const id of taskIds.map((value) => String(value || "")).filter(Boolean)) { + locallyReadTaskIdsRef.current.delete(id); + dismissedReadTaskIdsRef.current.delete(id); + } + } + + function taskWithLocalReadState(task: any): any { + const id = String(task?.id || ""); + const localReadAt = id ? locallyReadTaskIdsRef.current.get(id) : undefined; + if (!localReadAt) return task; + const status = String(task?.status || ""); + if (status.length > 0 && !taskIsTerminal(task)) { + locallyReadTaskIdsRef.current.delete(id); + dismissedReadTaskIdsRef.current.delete(id); + return task; + } + return { ...task, readAt: task?.readAt || localReadAt, terminalUnread: false }; + } + + function taskDismissedAfterRead(task: any): boolean { + const id = String(task?.id || ""); + return id.length > 0 && dismissedReadTaskIdsRef.current.has(id) && taskIsTerminal(task); + } + + function applyLocalReadStateToRows(rows: any[], hideDismissed = true): any[] { + const normalized: any[] = []; + for (const task of Array.isArray(rows) ? rows : []) { + const patchedTask = taskWithLocalReadState(task); + if (hideDismissed && taskDismissedAfterRead(patchedTask)) continue; + normalized.push(patchedTask); + } + return normalized; + } + + function applyLocalReadStateToResult(result: any, hideDismissed = true): any { + if (!result || !Array.isArray(result?.tasks)) return result; + const rows = applyLocalReadStateToRows(taskRows(result), hideDismissed); + const pagination = taskPagination(result); + return { + ...result, + tasks: rows, + pagination: result.pagination ? { ...pagination, returned: rows.length } : result.pagination, + }; + } + + function changeSubmitProvider(nextProviderId: string): void { + const next = String(nextProviderId || queue?.mainProviderId || "main-server").trim() || "main-server"; + setProviderId(next); + setCwd(providerDefaultWorkdir(queue, next)); + } + function patchLoadedReadState(taskIds: string[], readAt: string, queuePatch: any = null, taskPatch: any = null): void { - const ids = new Set(taskIds.map((id) => String(id || "")).filter(Boolean)); + const ids = new Set(rememberLocalReadState(taskIds, readAt)); if (ids.size === 0 && taskPatch === null && queuePatch === null) return; setTasksData((previous: any) => { if (!previous) return previous; - const rows = taskRows(previous).map((task: any) => { + const rows = taskRows(previous).flatMap((task: any) => { const id = String(task?.id || ""); - if (!ids.has(id)) return task; + if (!ids.has(id)) { + const patchedTask = taskWithLocalReadState(task); + return taskDismissedAfterRead(patchedTask) ? [] : [patchedTask]; + } const patch = taskPatch && String(taskPatch?.id || "") === id ? taskPatch : {}; - return { ...task, ...patch, readAt, terminalUnread: false }; + const updatedTask = { ...task, ...patch, readAt, terminalUnread: false }; + return taskDismissedAfterRead(updatedTask) ? [] : [updatedTask]; }); return { ...previous, queue: queuePatch || previous.queue, tasks: ids.size > 0 ? mergeTaskRowsPreferLatest([rows], activeTaskId) : rows }; }); @@ -1548,6 +1981,43 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init setBatchConfirmed(false); }, [prompt, repeatCount, referenceTaskId]); + useEffect(() => { + const query = normalizedTaskSearchQuery(taskSearchQuery); + searchLoadTokenRef.current += 1; + const token = searchLoadTokenRef.current; + if (!service || query.length === 0) { + setSearchTasksData(null); + setSearchLoading(false); + setSearchLoadingMoreTasks(false); + searchLoadMoreInFlightRef.current = false; + return undefined; + } + setSearchLoading(true); + setSearchTasksData(null); + const timer = window.setTimeout(() => { + void (async () => { + try { + const result = await loadTaskQueue(apiBaseUrl, {}, selectedQueueId, query); + if (token !== searchLoadTokenRef.current) return; + setSearchTasksData(applyLocalReadStateToResult(result)); + } catch (err) { + if (token === searchLoadTokenRef.current) { + setSearchTasksData(null); + setError(errorText(err, "搜索 Codex tasks 失败")); + } + } finally { + if (token === searchLoadTokenRef.current) setSearchLoading(false); + } + })(); + }, 240); + return () => window.clearTimeout(timer); + }, [service?.id, apiBaseUrl, selectedQueueId, taskSearchQuery]); + + useEffect(() => { + setEditPrompt(selectedTask ? taskBasePromptText(selectedTask) : ""); + setEditReferenceTaskId(Array.isArray(selectedTask?.referenceTaskIds) ? selectedTask.referenceTaskIds.join(" ") : ""); + }, [selectedId]); + function publishCachedTask(taskId: string, patch: AnyRecord, token: number): AnyRecord { const cached = sessionCacheRef.current.get(taskId) || {}; const existingTask = cached.task || {}; @@ -1563,21 +2033,22 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init output: Array.isArray(mergedPatch.output) ? preferRicherArray(existingTask, mergedPatch, "output") : (Array.isArray(existingTask.output) ? existingTask.output : []), events: Array.isArray(mergedPatch.events) ? preferRicherArray(existingTask, mergedPatch, "events") : (Array.isArray(existingTask.events) ? existingTask.events : []), }; - const taskUpdatedAt = String(task?.updatedAt || ""); - const patchComplete = Boolean(patch._transcriptComplete) && taskIsTerminal(task); + const taskWithReadState = taskWithLocalReadState(task); + const taskUpdatedAt = String(taskWithReadState?.updatedAt || ""); + const patchComplete = Boolean(patch._transcriptComplete) && taskIsTerminal(taskWithReadState); const cachedComplete = Boolean(cached.complete) - && taskIsTerminal(task) + && taskIsTerminal(taskWithReadState) && String(cached.completeUpdatedAt || "") === taskUpdatedAt; const complete = patchComplete || cachedComplete; const entry = { ...cached, - task, + task: taskWithReadState, maxSeq: transcriptMaxSeq(nextTranscript), complete, completeUpdatedAt: complete ? taskUpdatedAt : "", }; sessionCacheRef.current.set(taskId, entry); - if (token === detailLoadTokenRef.current && selectedIdRef.current === taskId) setSelectedTask(task); + if (token === detailLoadTokenRef.current && selectedIdRef.current === taskId) setSelectedTask(taskWithReadState); return entry; } @@ -1624,7 +2095,7 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init detailMs: performance.now() - startedAt, totalMs: loadStartedAt === undefined ? performance.now() - startedAt : performance.now() - loadStartedAt, chunks: 1, - transcriptRows: Number(summary?.execution?.stepCount || 0), + transcriptRows: Number(summary?.execution?.traceLineCount || summary?.execution?.stepCount || 0), partial: false, completedAt: new Date(), }); @@ -1863,15 +2334,16 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const mergedRows = previousRows.length > 0 ? mergeTaskRowsPreferLatest([previousRows, incomingRows], activeSortId) : mergeTaskRowsPreferLatest([incomingRows], activeSortId); + const visibleRows = applyLocalReadStateToRows(mergedRows); const incomingPagination = taskPagination(tasksResult); const previousPagination = taskPagination(previous); const preserveCursor = previousRows.length > incomingRows.length && (previousPagination.hasMore === false || String(previousPagination.nextBeforeId || "").length > 0); const pagination = { ...incomingPagination, ...(preserveCursor ? { hasMore: previousPagination.hasMore, nextBeforeId: previousPagination.nextBeforeId } : {}), - returned: mergedRows.length, + returned: visibleRows.length, }; - mergedTasksResult = { ...tasksResult, tasks: mergedRows, pagination }; + mergedTasksResult = { ...tasksResult, tasks: visibleRows, pagination }; return mergedTasksResult; }); const rows = taskRows(mergedTasksResult); @@ -1962,6 +2434,39 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init } async function loadMoreTasks(): Promise { + if (searchActive) { + if (!service || searchLoadingMoreTasks || searchLoadMoreInFlightRef.current) return; + const beforeId = String(searchPagination.nextBeforeId || ""); + if (!beforeId) return; + searchLoadMoreInFlightRef.current = true; + setSearchLoadingMoreTasks(true); + setError(""); + try { + const result = await loadTaskPage(apiBaseUrl, selectedQueueId, beforeId, codexMoreTaskLimit, normalizedSearchQuery); + const incomingRows = taskRows(result); + const resultQueue = result?.queue || queue || {}; + const activeSortId = String(resultQueue?.activeTaskId || activeTaskIds(resultQueue)[0] || activeTaskId || ""); + setSearchTasksData((previous: any) => { + const mergedRows = applyLocalReadStateToRows(mergeTaskRowsPreferLatest([taskRows(previous), incomingRows], activeSortId)); + const incomingPagination = taskPagination(result); + return { + ...(previous || {}), + queue: resultQueue, + tasks: mergedRows, + pagination: { + ...incomingPagination, + returned: mergedRows.length, + }, + }; + }); + } catch (err) { + setError(errorText(err, "加载更多搜索结果失败")); + } finally { + searchLoadMoreInFlightRef.current = false; + setSearchLoadingMoreTasks(false); + } + return; + } if (!service || loadingMoreTasks || loadMoreInFlightRef.current) return; const beforeId = String(taskPagination(tasksData).nextBeforeId || ""); if (!beforeId) return; @@ -1974,11 +2479,12 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const resultQueue = result?.queue || queue || {}; const activeSortId = String(resultQueue?.activeTaskId || activeTaskIds(resultQueue)[0] || activeTaskId || ""); setTasksData((previous: any) => { - const mergedRows = mergeTaskRowsPreferLatest([taskRows(previous), incomingRows], activeSortId); + const mergedRows = applyLocalReadStateToRows(mergeTaskRowsPreferLatest([taskRows(previous), incomingRows], activeSortId)); const incomingPagination = taskPagination(result); return { ...(previous || {}), queue: resultQueue, + statistics: result?.statistics || previous?.statistics, tasks: mergedRows, pagination: { ...incomingPagination, @@ -1996,7 +2502,7 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init function handleTaskListScroll(event: any): void { const element = event.currentTarget as HTMLElement; - if (!element || loadingMoreTasks || !hasMoreTasks) return; + if (!element || sidebarLoadingMoreTasks || !sidebarHasMoreTasks) return; const distanceToBottom = element.scrollHeight - element.scrollTop - element.clientHeight; if (distanceToBottom < 120) void loadMoreTasks(); } @@ -2052,20 +2558,42 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init async function markTaskRead(taskId: string): Promise { if (!service || !taskId) return; + const optimisticReadAt = new Date().toISOString(); + queueLoadTokenRef.current += 1; + patchLoadedReadState([taskId], optimisticReadAt, null, { id: taskId, readAt: optimisticReadAt, terminalUnread: false }); setMarkingReadTaskId(taskId); + let marked = false; await guarded(async () => { const result = await markTaskReadRequest(apiBaseUrl, taskId); const task = result?.task || { id: taskId, readAt: new Date().toISOString(), terminalUnread: false }; const readAt = String(task?.readAt || new Date().toISOString()); patchLoadedReadState([taskId], readAt, result?.queue || null, task); + marked = true; setNotice(`已将任务 ${taskId} 标为已读`); }, "标记 Codex task 已读失败"); + if (!marked) { + forgetLocalReadState([taskId]); + void load(selectedIdRef.current, false).catch((err) => setError(errorText(err, "刷新 Codex tasks 失败"))); + } setMarkingReadTaskId((value: string) => value === taskId ? "" : value); } async function markAllTerminalRead(): Promise { if (!service || markingAllRead) return; setMarkingAllRead(true); + const optimisticReadAt = new Date().toISOString(); + const optimisticReadIds = Array.from(new Set([ + ...taskRows(tasksDataRef.current) + .filter(taskIsUnreadTerminal) + .map((task: any) => String(task?.id || "")) + .filter(Boolean), + ...Array.from(sessionCacheRef.current.entries()) + .filter(([, value]) => taskIsUnreadTerminal(value?.task)) + .map(([id]) => id), + ])); + queueLoadTokenRef.current += 1; + if (optimisticReadIds.length > 0) patchLoadedReadState(optimisticReadIds, optimisticReadAt); + let marked = false; await guarded(async () => { const result = await markAllTerminalReadRequest(apiBaseUrl); const readAt = String(result?.readAt || new Date().toISOString()); @@ -2076,11 +2604,16 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const cachedUnreadIds = Array.from(sessionCacheRef.current.entries()) .filter(([, value]) => taskIsUnreadTerminal(value?.task)) .map(([id]) => id); - const readIds = Array.from(new Set([...loadedUnreadIds, ...cachedUnreadIds])); + const readIds = Array.from(new Set([...optimisticReadIds, ...loadedUnreadIds, ...cachedUnreadIds])); patchLoadedReadState(readIds, readAt, result?.queue || null); const markedCount = Number(result?.count || readIds.length); + marked = true; setNotice(`已将 ${markedCount} 个已结束未读任务标为已读`); }, "全部标为已读失败"); + if (!marked && optimisticReadIds.length > 0) { + forgetLocalReadState(optimisticReadIds); + void load(selectedIdRef.current, false).catch((err) => setError(errorText(err, "刷新 Codex tasks 失败"))); + } setMarkingAllRead(false); } @@ -2118,6 +2651,24 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init }, "创建 Codex queue 失败"); } + async function renameQueue(): Promise { + const targetQueueId = String(queueId || "default").trim() || "default"; + const row = selectedQueueRow(queueRows, targetQueueId) || { id: targetQueueId, name: targetQueueId }; + const proposed = typeof window === "undefined" + ? null + : window.prompt(`输入 queue 显示名称(ID 不变:${targetQueueId};留空恢复为 ID)`, queueName(row)); + if (proposed === null) return; + await guarded(async () => { + const result = await requestJson(codexApi(apiBaseUrl, `/api/queues/${encodeURIComponent(targetQueueId)}`), { method: "PATCH", body: { name: String(proposed) } }); + const updatedQueue = result?.queue || { id: targetQueueId, name: String(proposed || targetQueueId) }; + if (result?.summary) { + setTasksData((previous: any) => previous ? { ...previous, queue: result.summary } : previous); + } + setNotice(`已更新 queue 名称:${queueDisplayName(updatedQueue)}`); + await load(selectedIdRef.current, true, selectedQueueId); + }, "修改 Codex queue 名称失败"); + } + async function enqueue(event: any): Promise { event.preventDefault(); if (enqueueInFlightRef.current) { @@ -2130,7 +2681,7 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init } enqueueInFlightRef.current = true; setSubmitting(true); - setNotice("正在提交 Codex Queue 任务,请等待后端确认,输入已临时锁定。"); + setNotice("正在提交 Code Queue 任务,请等待后端确认,输入已临时锁定。"); await guarded(async () => { if (enqueueItems.length === 0) throw new Error("prompt 不能为空"); const referenceTaskIds = parseReferenceTaskIds(referenceTaskId); @@ -2139,6 +2690,7 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const taskPayload = (text: string) => ({ prompt: text, queueId: submitQueueId, + providerId, model, cwd, maxAttempts: Number(maxAttempts), @@ -2174,6 +2726,44 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init }, "追加 prompt 失败"); } + async function editSelectedQueuedPrompt(event: any): Promise { + event.preventDefault(); + const taskId = String(selectedTask?.id || ""); + if (!taskId || !selectedCanEditPrompt) return; + await guarded(async () => { + const referenceTaskIds = parseReferenceTaskIds(editReferenceTaskId); + const result = await requestJson(codexApi(apiBaseUrl, `/api/tasks/${encodeURIComponent(taskId)}/edit`), { + method: "POST", + body: { prompt: editPrompt, referenceTaskIds }, + }); + const updatedTask = { + ...(result?.task || selectedTask || {}), + _traceSummary: null, + _traceSummaryLoaded: false, + _traceSummaryUpdatedAt: "", + _promptDetails: {}, + _traceSteps: [], + _traceStepsLoaded: false, + _traceStepsByAttempt: {}, + _traceStepsLoadedByAttempt: {}, + _traceStepDetails: {}, + }; + sessionCacheRef.current.set(taskId, { ...(sessionCacheRef.current.get(taskId) || {}), task: updatedTask, complete: false, completeUpdatedAt: "" }); + selectedIdRef.current = taskId; + setSelectedTask(updatedTask); + setSelectedId(taskId); + setEditPrompt(taskBasePromptText(updatedTask)); + setEditReferenceTaskId(Array.isArray(updatedTask?.referenceTaskIds) ? updatedTask.referenceTaskIds.join(" ") : ""); + setTasksData((previous: any) => { + if (!previous) return previous; + const rows = taskRows(previous).map((task: any) => String(task?.id || "") === taskId ? { ...task, ...updatedTask } : task); + return { ...previous, queue: result?.queue || previous.queue, tasks: mergeTaskRowsPreferLatest([rows], activeTaskId) }; + }); + setNotice(result?.changed === false ? `任务 ${taskId} 的 prompt 未变化` : `已更新 queued 任务 ${taskId} 的用户 prompt`); + await load(taskId, true, selectedQueueId); + }, "编辑 queued 任务 prompt 失败"); + } + async function interrupt(): Promise { if (!selectedTask?.id) return; await guarded(async () => { @@ -2260,14 +2850,14 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init initialLoadSkippedRef.current = false; return; } - void guarded(() => load(selectedIdRef.current), "Codex Queue 加载失败"); + void guarded(() => load(selectedIdRef.current), "Code Queue 加载失败"); }, [service?.id, selectedQueueId]); useEffect(() => { if (!service) return undefined; const tick = (): void => { if (!isDocumentVisible()) return; - void load(selectedIdRef.current, false).catch((err) => setError(errorText(err, "Codex Queue 轮询失败"))); + void load(selectedIdRef.current, false).catch((err) => setError(errorText(err, "Code Queue 轮询失败"))); }; const timer = window.setInterval(() => { tick(); @@ -2295,7 +2885,10 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init void ensureTraceSummary(taskId, true).catch((err) => setError(errorText(err, "自动加载 Trace Summary 失败"))); }, [service?.id, selectedTask?.id, selectedTask?.updatedAt, selectedTask?._traceSummaryUpdatedAt, selectedTask?._traceSummaryLoaded, selectedDetailLoading]); - const taskListContent = tasks.length === 0 ? h(EmptyState, { title: "队列为空", text: "提交一个任务后,Codex 会串行执行并保存输出。" }) : [ + const taskListContent = sidebarTasks.length === 0 ? h(EmptyState, { + title: searchActive ? (searchLoading ? "搜索中" : "没有匹配任务") : "队列为空", + text: searchActive ? (searchLoading ? `正在搜索包含“${normalizedSearchQuery}”的 task...` : `未找到包含“${normalizedSearchQuery}”的 task;可换个关键词或切换 queue。`) : "提交一个任务后,Codex 会串行执行并保存输出。", + }) : [ unreadTerminalTasks.length > 0 ? h(TaskListSection, { key: "unread", title: "已结束未读", @@ -2336,18 +2929,20 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init markingReadTaskId, }), h("div", { key: "pagination", className: "codex-task-pagination", "data-testid": "codex-task-pagination" }, - h("span", null, `已加载 ${tasks.length} / ${Number.isFinite(totalTaskCount) ? totalTaskCount : tasks.length}`), - hasMoreTasks ? h("button", { + h("span", null, searchActive + ? `搜索“${normalizedSearchQuery}” · 已显示 ${sidebarTasks.length} / ${Number.isFinite(sidebarTotalTaskCount) ? sidebarTotalTaskCount : sidebarTasks.length}` + : `已加载 ${sidebarTasks.length} / ${Number.isFinite(sidebarTotalTaskCount) ? sidebarTotalTaskCount : sidebarTasks.length}`), + sidebarHasMoreTasks ? h("button", { type: "button", className: "ghost-btn", - disabled: loadingMoreTasks, + disabled: sidebarLoadingMoreTasks, onClick: () => void loadMoreTasks(), "data-testid": "codex-load-more-tasks-button", - }, loadingMoreTasks ? "加载中" : "加载更早任务") : h("code", null, "已到队列末尾"), + }, sidebarLoadingMoreTasks ? "加载中" : (searchActive ? "加载更多结果" : "加载更早任务")) : h("code", null, searchActive ? "已到结果末尾" : "已到队列末尾"), ), ]; - const queueSelector = (testId: string, compact = false) => h("label", { className: `codex-queue-switcher ${compact ? "compact" : ""}` }, + const queueSelector = (testId: string, compact = false) => h("label", { className: `code-queue-switcher ${compact ? "compact" : ""}` }, h("span", null, compact ? "Queue" : "查看 queue"), h("select", { value: selectedQueueId, onChange: (event: any) => changeSelectedQueue(String(event.target.value || allQueuesId)), "data-testid": testId }, h("option", { value: allQueuesId }, `All queues · ${Number.isFinite(totalTaskCount) ? totalTaskCount : tasks.length} tasks · ${globalActiveIds.length} running`), @@ -2355,18 +2950,48 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init ), ); + const taskSearchControl = h("div", { className: "codex-task-search", "data-testid": "codex-task-search" }, + h("label", { htmlFor: "codex-task-search-input" }, "搜索 task"), + h("div", { className: "codex-task-search-row" }, + h("input", { + id: "codex-task-search-input", + type: "search", + value: taskSearchQuery, + placeholder: "关键词 / task ID / prompt", + autoComplete: "off", + onChange: (event: any) => setTaskSearchQuery(String(event.target.value || "")), + "data-testid": "codex-task-search-input", + }), + taskSearchQuery ? h("button", { + type: "button", + className: "ghost-btn", + onClick: () => setTaskSearchQuery(""), + "data-testid": "codex-task-search-clear", + }, "清除") : null, + ), + h("small", { "data-testid": "codex-task-search-summary" }, searchActive + ? (searchLoading ? "搜索中..." : `匹配 ${sidebarTasks.length}/${Number.isFinite(sidebarTotalTaskCount) ? sidebarTotalTaskCount : sidebarTasks.length}`) + : "支持 task ID、prompt、状态、provider、模型和最近输出关键词"), + ); + const traceStatusSummary = h("div", { className: "codex-trace-status", "data-testid": "codex-trace-status-summary" }, h("span", { className: "codex-trace-status-chip queued" }, h("b", null, "排队"), String(overallQueuedCount)), h("span", { className: "codex-trace-status-chip running" }, h("b", null, "运行"), String(overallRunningCount)), h("span", { className: `codex-trace-status-chip unread ${overallUnreadTerminalCount > 0 ? "warn" : ""}` }, h("b", null, "结束未读"), String(overallUnreadTerminalCount)), + h("span", { className: "codex-trace-status-chip service" }, h("b", null, "服务"), `${runtime.providerStatus || "unknown"} · ${service?.providerId || "main-server"} · ${backend.public ? "公网暴露" : "仅 UniDesk frontend 代理访问"}`), + h("span", { className: "codex-trace-status-chip" }, h("b", null, "执行节点"), providerOptions.map((provider: any) => provider.id).join(" / ")), + h("span", { className: "codex-trace-status-chip" }, h("b", null, "模型"), codexModels.join(" / ")), + h("span", { className: "codex-trace-status-chip" }, h("b", null, "加载"), loadStats?.phase === "complete" ? fmtPreciseMs(loadStats?.totalMs) : String(loadStats?.phase || "idle")), + h("span", { className: "codex-trace-status-chip" }, h("b", null, "刷新"), refreshedAt ? fmtClock(refreshedAt) : "--"), ); const sessionPanel = h(Panel, { title: selectedTask ? `Trace ${String(selectedTask.id).slice(0, 22)}` : "Trace 输出", - eyebrow: selectedTask ? `${selectedTask.status} / view=${selectedQueueName} / task queue=${taskQueueLabel(selectedTask)} / ${selectedTask.model} / agent loop trace` : `Agent loop trace / view=${selectedQueueName}`, + eyebrow: selectedTask ? `${selectedTask.status} / view=${selectedQueueName} / task queue=${taskQueueLabel(selectedTask)} / provider=${selectedTask.providerId || "main-server"} / ${selectedTask.model} / agent loop trace` : `Agent loop trace / view=${selectedQueueName}`, summary: traceStatusSummary, + loading: selectedDetailLoading || loadingMoreTasks || searchLoading || searchLoadingMoreTasks || loadStats?.phase === "loading", actions: h("div", { className: "panel-actions" }, - queueSelector("codex-queue-filter-select"), + queueSelector("code-queue-filter-select"), h("button", { type: "button", className: "ghost-btn codex-mark-all-read-btn", @@ -2381,7 +3006,7 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init onClick: () => void loadFullTrace(), "data-testid": "codex-load-full-trace-button", }, selectedDetailLoading ? "加载中" : taskTraceSummary(selectedTask) ? "刷新 Summary" : "加载 Summary") : null, - h("button", { type: "button", className: "codex-session-title-toggle", onClick: () => setQueueSidebarOpen((value: boolean) => !value), "data-testid": "codex-queue-sidebar-toggle" }, queueSidebarOpen ? "收起队列" : "展开队列"), + h("button", { type: "button", className: "codex-session-title-toggle", onClick: () => setQueueSidebarOpen((value: boolean) => !value), "data-testid": "code-queue-sidebar-toggle" }, queueSidebarOpen ? "收起队列" : "展开队列"), h("label", { className: "inline-check" }, h("input", { type: "checkbox", checked: autoScroll, onChange: (event: any) => setAutoScroll(Boolean(event.target.checked)) }), "自动滚动"), h("button", { type: "button", className: "ghost-btn", disabled: !selectedCanInterrupt || busy, onClick: () => void interrupt(), "data-testid": "codex-interrupt-button" }, "打断"), h("button", { type: "button", className: "ghost-btn", disabled: !selectedCanRetry || busy, onClick: () => void retry() }, "重试"), @@ -2398,7 +3023,8 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init ), h("button", { type: "button", className: "ghost-btn", onClick: () => setQueueSidebarOpen(false) }, "收起"), ), - queueSelector("codex-queue-filter-sidebar", true), + queueSelector("code-queue-filter-sidebar", true), + taskSearchControl, h("div", { className: "codex-task-list codex-task-list-session", onScroll: handleTaskListScroll, "data-testid": "codex-task-list-scroll" }, taskListContent), ) : null, h("div", { className: "codex-session-main" }, @@ -2410,7 +3036,7 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init ), ); - if (!service) return h(EmptyState, { title: "Codex Queue 未登记", text: "请在 config.json 的 microservices 中登记用户服务 id=codex-queue" }); + if (!service) return h(EmptyState, { title: "Code Queue 未登记", text: "请在 config.json 的 microservices 中登记用户服务 id=code-queue" }); const loadTotalMs = Number(loadStats?.totalMs); const loadQueueMs = Number(loadStats?.queueMs); @@ -2419,8 +3045,8 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init const loadState = loadStats?.phase === "complete" ? "complete" : String(loadStats?.phase || "idle"); return h("div", { - className: `codex-queue-page ${standalone ? "codex-standalone-page" : ""}`, - "data-testid": "codex-queue-page", + className: `code-queue-page ${standalone ? "codex-standalone-page" : ""}`, + "data-testid": "code-queue-page", "data-load-state": loadState, "data-load-total-ms": Number.isFinite(loadTotalMs) ? String(Math.round(loadTotalMs * 10) / 10) : "", "data-load-queue-ms": Number.isFinite(loadQueueMs) ? String(Math.round(loadQueueMs * 10) / 10) : "", @@ -2431,11 +3057,13 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init }, h(UniDeskErrorBanner, { error, wide: true }), notice ? h("div", { className: "form-success wide", "data-testid": "codex-create-success" }, notice) : null, - h("div", { className: "codex-session-stage codex-session-stage-top" }, sessionPanel), - h("div", { className: "codex-queue-layout" }, + h("div", { className: "codex-session-stage codex-session-stage-top" }, + sessionPanel, + ), + h("div", { className: "code-queue-layout" }, h("div", { className: "codex-left-rail" }, - h(Panel, { title: "提交任务", eyebrow: submitting ? "Submitting..." : enqueueItems.length > 1 ? `${enqueueItems.length} tasks` : "Single or Batch", className: "codex-compose-panel" }, - h("form", { className: `codex-task-form ${submitting ? "is-submitting" : ""}`, onSubmit: enqueue, "data-testid": "codex-queue-task-form", "aria-busy": submitting ? "true" : "false" }, + h(Panel, { title: "提交任务", eyebrow: submitting ? "Submitting..." : enqueueItems.length > 1 ? `${enqueueItems.length} tasks` : "Single or Batch", className: "codex-compose-panel", loading: submitting }, + h("form", { className: `codex-task-form ${submitting ? "is-submitting" : ""}`, onSubmit: enqueue, "data-testid": "code-queue-task-form", "aria-busy": submitting ? "true" : "false" }, h("label", null, "Prompt / 多任务用单独一行 --- 分隔", h("textarea", { value: prompt, rows: 8, disabled: submitting, onChange: (event: any) => setPrompt(event.target.value), placeholder: "写入 Codex 任务;多个任务之间用 --- 分隔。" }), ), @@ -2446,9 +3074,10 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init h("div", { className: "codex-form-grid" }, h("label", { className: "codex-submit-queue-field" }, "Queue", h("div", { className: "codex-submit-queue-row" }, - h("select", { value: queueId, disabled: submitting, onChange: (event: any) => setQueueId(String(event.target.value || "default")), "data-testid": "codex-queue-id-select" }, + h("select", { value: queueId, disabled: submitting, onChange: (event: any) => setQueueId(String(event.target.value || "default")), "data-testid": "code-queue-id-select" }, queueRows.map((row: any) => h("option", { key: String(row?.id || ""), value: String(row?.id || "") }, queueOptionLabel(row))), ), + h("button", { type: "button", className: "ghost-btn codex-rename-queue-btn", onClick: () => void renameQueue(), disabled: busy || submitting || !queueId, title: "修改当前 queue 的显示名称,ID 不变", "data-testid": "codex-rename-queue-button" }, "改名"), h("button", { type: "button", className: "ghost-btn codex-create-queue-btn", onClick: () => void createQueue(), disabled: busy || submitting, "data-testid": "codex-create-queue-button" }, "创建 queue"), ), ), @@ -2457,7 +3086,12 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init codexModels.map((name) => h("option", { key: name, value: name }, name)), ), ), - h("label", null, "工作目录", h("input", { value: cwd, disabled: submitting, onChange: (event: any) => setCwd(event.target.value), placeholder: queue?.defaultWorkdir || "/root/unidesk" })), + h("label", null, "执行 Provider", + h("select", { value: providerId, disabled: submitting, onChange: (event: any) => changeSubmitProvider(String(event.target.value || "main-server")), "data-testid": "codex-provider-select" }, + providerOptions.map((provider: any) => h("option", { key: provider.id, value: provider.id }, `${provider.label || provider.id} · ${provider.defaultWorkdir || providerDefaultWorkdir(queue, provider.id)}`)), + ), + ), + h("label", null, "工作目录", h("input", { value: cwd, disabled: submitting, onChange: (event: any) => setCwd(event.target.value), placeholder: currentProviderDefaultWorkdir || queue?.defaultWorkdir || "/root/unidesk", "data-testid": "codex-cwd-input" })), h("label", null, "最大尝试", h("input", { type: "number", min: 1, max: 99, value: maxAttempts, disabled: submitting, onChange: (event: any) => setMaxAttempts(Number(event.target.value)), "data-testid": "codex-max-attempts-input" })), h("label", null, "入队份数", h("input", { type: "number", min: 1, max: 50, value: repeatCount, disabled: submitting, onChange: (event: any) => setRepeatCount(Number(event.target.value)), "data-testid": "codex-repeat-count-input" })), ), @@ -2492,9 +3126,50 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init ), h("div", { className: "codex-main-stage" }, h("div", { className: "codex-detail-grid" }, - h(Panel, { title: "运行控制", eyebrow: selectedCanSteer ? "Active turn steer" : "Steer when running" }, + h(Panel, { title: "运行控制", eyebrow: selectedCanEditPrompt ? "Queued prompt editable" : selectedCanSteer ? "Active turn steer" : "Steer when running", loading: busy }, h("div", { className: "codex-run-control-stack" }, h(TaskQueueMoveControl, { task: selectedTask, queueRows, busy, onMove: moveSelectedTaskQueue }), + selectedTask?.id ? h("form", { className: "codex-steer-form codex-edit-prompt-form", onSubmit: editSelectedQueuedPrompt, "data-testid": "codex-edit-prompt-form" }, + h("label", null, "编辑 queued 用户 prompt", + h("textarea", { + value: editPrompt, + rows: 5, + onChange: (event: any) => setEditPrompt(event.target.value), + placeholder: "仅 QUEUED 且尚未开始运行的任务可在这里修改原始用户 prompt。", + disabled: !selectedCanEditPrompt || busy, + "data-testid": "codex-edit-prompt-textarea", + }), + ), + h("label", { className: "codex-reference-field" }, "引用任务 ID(可选,留空会清除引用)", + h("input", { + value: editReferenceTaskId, + disabled: !selectedCanEditPrompt || busy, + onChange: (event: any) => setEditReferenceTaskId(event.target.value), + placeholder: "codex_...;支持空格/逗号分隔多个 ID", + "data-testid": "codex-edit-reference-task-id", + }), + parseReferenceTaskIds(editReferenceTaskId).length > 0 ? h("code", null, `将保留/注入:${parseReferenceTaskIds(editReferenceTaskId).join(" / ")}`) : null, + ), + h("div", { className: "codex-form-actions" }, + h("button", { + type: "button", + className: "ghost-btn", + disabled: !selectedTask?.id || busy, + onClick: () => { + setEditPrompt(selectedTask ? taskBasePromptText(selectedTask) : ""); + setEditReferenceTaskId(Array.isArray(selectedTask?.referenceTaskIds) ? selectedTask.referenceTaskIds.join(" ") : ""); + }, + "data-testid": "codex-edit-prompt-reset", + }, "恢复当前值"), + h("button", { + type: "submit", + className: "primary-btn", + disabled: !selectedCanEditPrompt || busy || editPrompt.trim().length === 0, + title: selectedCanEditPrompt ? "保存后会重写尚未运行任务的用户 prompt" : "只有 QUEUED 且尚未开始的任务可编辑 prompt", + "data-testid": "codex-edit-prompt-submit", + }, "保存 queued prompt"), + ), + ) : null, h("form", { className: "codex-steer-form", onSubmit: steer }, h("label", null, "追加 prompt", h("textarea", { value: steerPrompt, rows: 4, onChange: (event: any) => setSteerPrompt(event.target.value), placeholder: "给正在运行的 Codex session 推入新的指令或纠偏。", disabled: !selectedCanSteer }), @@ -2503,64 +3178,18 @@ export function CodexQueuePage({ microservices, onRaw, apiBaseUrl = "/api", init ), ), ), - h(Panel, { title: "完成判定", eyebrow: selectedTask?.lastJudge ? selectedTask.lastJudge.source : "judge" }, + h(Panel, { title: "完成判定", eyebrow: selectedTask?.lastJudge ? selectedTask.lastJudge.source : "judge", loading: selectedDetailLoading }, selectedTask?.lastJudge ? h("div", { className: "codex-judge-card", "data-testid": "codex-task-judge-card" }, h(StatusBadge, { status: selectedTask.lastJudge.decision }, selectedTask.lastJudge.decision), h("strong", null, `${Math.round(Number(selectedTask.lastJudge.confidence || 0) * 100)}% confidence`), - h("p", { "data-testid": "codex-task-judge-reason" }, selectedTask.lastJudge.reason || "--"), - selectedTask.lastJudge.continuePrompt ? h("code", { "data-testid": "codex-task-judge-continue-prompt" }, shortText(selectedTask.lastJudge.continuePrompt, 220)) : null, + h("p", { "data-testid": "codex-task-judge-reason" }, shortText(selectedTask.lastJudge.reason || "--", 180)), + selectedTask.lastJudge.continuePrompt ? h("code", { "data-testid": "codex-task-judge-continue-prompt" }, shortText(selectedTask.lastJudge.continuePrompt, 160)) : null, ) : h(EmptyState, { title: "尚未判定", text: "Codex turn 结束后会由 MiniMax M2.7 或 fallback judge 判定 complete/retry/fail;retry 会在已有 thread 追加继续执行 prompt。" }), ), ), - h(Panel, { title: "Attempts", eyebrow: "terminal vs interruption" }, h(AttemptTable, { task: selectedTask })), + h(CodexStatsPanel, { stats: statistics, queueName: selectedQueueName, onRaw }), + h(Panel, { title: "Attempts", eyebrow: "terminal vs interruption", loading: selectedDetailLoading }, h(AttemptTable, { task: selectedTask })), ), ), - h(Panel, { - title: "运行概要", - eyebrow: "用户服务", - actions: h("div", { className: "panel-actions" }, - h("button", { type: "button", className: "ghost-btn", onClick: () => void guarded(() => load(selectedId), "刷新失败"), disabled: busy, "data-testid": "codex-refresh-button" }, busy ? "同步中" : "刷新"), - h(RawButton, { title: "Codex Queue 用户服务", data: service, onOpen: onRaw, testId: "raw-codex-queue-service" }), - ), - }, - h("div", { className: "codex-queue-hero" }, - h("div", null, - h("div", { className: "node-version-line" }, - h(StatusBadge, { status: runtime.providerStatus === "online" ? "online" : "warn" }, runtime.providerStatus || "unknown"), - h("span", null, service.providerId), - h("span", null, backend.public ? "公网暴露" : "仅 UniDesk frontend 代理访问"), - h("span", null, queue?.judgeConfigured ? `MiniMax ${queue?.minimaxModel || "M2.7"}` : "Fallback judge"), - ), - h("p", { className: "muted paragraph" }, service.description), - ), - h("div", { className: "microservice-ref-card" }, - h("span", null, "Queue view"), - h("strong", null, selectedQueueName), - h("code", null, `${tasks.length}/${Number.isFinite(totalTaskCount) ? totalTaskCount : tasks.length} loaded / ${globalActiveIds.length} active lanes`), - h("code", null, `models: ${codexModels.join(" / ")}`), - ), - h("div", { className: "microservice-ref-card" }, - h("span", null, "Backend"), - h("strong", null, `${backend.nodeBindHost || "--"}:${backend.nodePort || "--"}`), - h("code", null, repository.containerName || "codex-queue-backend"), - ), - ), - ), - h("div", { className: "codex-queue-metrics" }, - h(MetricCard, { label: "Queues", value: String(queue?.queueCount ?? queueRows.length ?? 1), hint: `${Number(activeIds.length || 0)} active lanes`, tone: activeIds.length > 1 ? "warn" : "" }), - h(MetricCard, { label: "排队", value: countValue(counts, "queued"), hint: "waiting turns" }), - h(MetricCard, { label: "运行", value: countValue(counts, "running"), hint: activeIds.length > 1 ? `${activeIds.length} parallel` : activeTaskId ? `active ${String(activeTaskId).slice(0, 16)}` : "idle", tone: activeTaskId ? "warn" : "ok" }), - h(MetricCard, { label: "成功", value: countValue(counts, "succeeded"), hint: "completed tasks", tone: "ok" }), - h(MetricCard, { label: "异常/取消", value: String(Number(counts.failed || 0) + Number(counts.canceled || 0)), hint: "terminal non-success", tone: Number(counts.failed || 0) > 0 ? "fail" : "" }), - h(MetricCard, { - label: "加载耗时", - value: fmtPreciseMs(loadStats?.totalMs), - hint: loadStats?.phase === "complete" - ? `queue ${fmtPreciseMs(loadStats?.queueMs)} / session ${fmtPreciseMs(loadStats?.detailMs)} / ${loadStats?.chunks ?? 0} chunks${loadStats?.partial ? " / preview" : ""}` - : `${loadStats?.phase || "idle"}...`, - tone: Number(loadStats?.totalMs || 0) > 1000 ? "warn" : "ok", - }), - h(MetricCard, { label: "最近刷新", value: refreshedAt ? fmtClock(refreshedAt) : "--", hint: "1.5s polling" }), - ), ); } diff --git a/src/components/frontend/src/filebrowser.tsx b/src/components/frontend/src/filebrowser.tsx new file mode 100644 index 00000000..de7f846e --- /dev/null +++ b/src/components/frontend/src/filebrowser.tsx @@ -0,0 +1,505 @@ +import React from "react"; +import { fmtClock } from "./time"; +import { LoadingTitle } from "./loading-indicator"; +import { errorMessage, requestJson } from "./unidesk-error"; +import { UniDeskErrorBanner } from "./unidesk-error-banner"; + +type AnyRecord = Record; + +const h = React.createElement; +const { useEffect } = React; +const useState: any = React.useState; +const useRef: any = React.useRef; + +const filebrowserCompactCss = ` +:root { + --surfacePrimary: #ffffff; + --surfaceSecondary: #f8fafc; + --background: #f8fafc; + --divider: #e5e7eb; + --borderPrimary: #dbe3ee; + --textPrimary: #17212f; + --textSecondary: #64748b; + --iconSecondary: #ffffff; + --blue: #2283e8; +} +* { + box-sizing: border-box !important; +} +html, body { + font-size: 13px !important; +} +body { + margin: 0 !important; + background: var(--surfacePrimary) !important; + color: var(--textPrimary) !important; + font-family: Arial, sans-serif !important; +} +button, +input { + font: inherit !important; +} +header { + height: 42px !important; + min-height: 42px !important; + padding: 4px 8px !important; +} +header title { + font-size: 14px !important; + padding: 0 8px !important; +} +header img { + height: 28px !important; +} +header .action { + min-width: 30px !important; + height: 30px !important; + padding: 4px 6px !important; +} +header .search { + max-width: 360px !important; + height: 32px !important; +} +nav { + top: 42px !important; + width: 12rem !important; +} +nav .action { + min-height: 34px !important; + padding: 5px 8px !important; + font-size: 13px !important; +} +nav .action i { + margin-right: 6px !important; +} +nav > div { + padding: 3px 0 !important; +} +main { + width: calc(100% - 12rem) !important; + margin-left: 12rem !important; +} +main > div { + padding-top: 42px !important; +} +.material-icons { + width: 22px !important; + min-width: 22px !important; + max-width: 22px !important; + height: 22px !important; + font-family: Arial, sans-serif !important; + font-size: 0 !important; + line-height: 22px !important; + overflow: hidden !important; + text-align: center !important; + text-transform: none !important; +} +.material-icons::before { + content: "." !important; + display: inline-block !important; + font-family: Arial, sans-serif !important; + font-size: 11px !important; + font-weight: 700 !important; + line-height: 22px !important; +} +header .material-icons::before, +nav .material-icons::before { + content: "op" !important; + font-size: 10px !important; +} +#listing { + min-height: 0 !important; +} +#listing h2 { + margin: 4px 8px !important; + font-size: 11px !important; +} +#listing > div { + display: block !important; +} +#listing.mosaic, +#listing.list { + width: 100% !important; + margin: 0 !important; + padding-top: 4px !important; +} +#listing.mosaic .header, +#listing.list .header { + display: flex !important; + min-height: 26px !important; + background: var(--background) !important; +} +#listing.mosaic .item, +#listing.list .item { + width: calc(100% - 8px) !important; + min-height: 30px !important; + margin: 0 4px !important; + padding: 3px 8px !important; + border-radius: 0 !important; + border-bottom: 1px solid var(--divider) !important; + box-shadow: none !important; +} +#listing.mosaic .item div:first-of-type, +#listing.list .item div:first-of-type { + width: 30px !important; + min-width: 30px !important; + height: 24px !important; +} +#listing .item i { + margin-right: 4px !important; +} +#listing .item i::before { + content: "file" !important; + color: inherit !important; + font-size: 10px !important; +} +#listing .item[data-dir="true"] i::before { + content: "dir" !important; +} +#listing.mosaic .item div:last-of-type, +#listing.list .item div:last-of-type { + width: calc(100% - 34px) !important; + display: grid !important; + grid-template-columns: minmax(180px, 1fr) 90px 140px !important; + gap: 8px !important; + align-items: center !important; +} +#listing .item .name { + min-width: 0 !important; + font-size: 13px !important; + font-weight: 600 !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} +#listing .item .size, +#listing .item .modified { + min-width: 0 !important; + color: var(--textSecondary) !important; + font-size: 11px !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} +#multiple-selection { + height: 36px !important; + min-height: 36px !important; + padding: 4px 8px !important; +} +@media (max-width: 780px) { + nav { + width: 9rem !important; + } + main { + width: calc(100% - 9rem) !important; + margin-left: 9rem !important; + } + #listing.mosaic .item div:last-of-type, + #listing.list .item div:last-of-type { + grid-template-columns: minmax(120px, 1fr) 80px !important; + } + #listing .item .modified { + display: none !important; + } +} +`; + +function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) { + return h("section", { className: `panel ${className || ""}` }, + h("div", { className: "panel-head" }, + h("div", null, + eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null, + h(LoadingTitle, { title, loading }), + ), + actions ? h("div", { className: "panel-actions" }, actions) : null, + ), + h("div", { className: "panel-body" }, children), + ); +} + +function RawButton({ title, data, onOpen, testId }: AnyRecord) { + return h("button", { + type: "button", + className: "ghost-btn", + "data-testid": testId, + onClick: () => onOpen(title, data), + }, "查看原始JSON"); +} + +function EmptyState({ title, text }: AnyRecord) { + return h("div", { className: "empty-state" }, h("strong", null, title), h("span", null, text)); +} + +function microserviceRuntime(service: any): AnyRecord { + return service?.runtime && typeof service.runtime === "object" && !Array.isArray(service.runtime) ? service.runtime : {}; +} + +function microserviceBackend(service: any): AnyRecord { + return service?.backend && typeof service.backend === "object" && !Array.isArray(service.backend) ? service.backend : {}; +} + +function microserviceRepository(service: any): AnyRecord { + return service?.repository && typeof service.repository === "object" && !Array.isArray(service.repository) ? service.repository : {}; +} + +function filebrowserServices(microservices: any[]): any[] { + const services = microservices.filter((item) => item?.id === "filebrowser" || String(item?.id || "").startsWith("filebrowser-")); + return services.sort((a, b) => { + const rank = (item: any) => item.providerId === "D518" ? 0 : item.providerId === "D601" ? 1 : item.id === "filebrowser" ? 2 : 3; + return rank(a) - rank(b) || String(a.id).localeCompare(String(b.id)); + }); +} + +function serviceLabel(service: any): string { + if (service?.providerId === "D518") return "D518"; + return service?.providerId || service?.name || service?.id || "Unknown"; +} + +function browserProxyUrl(apiBaseUrl: string, serviceId: string, suffix = "/"): string { + const path = suffix.startsWith("/") ? suffix : `/${suffix}`; + return `${apiBaseUrl}/microservices/${encodeURIComponent(serviceId)}/proxy${path}`; +} + +function healthUrl(apiBaseUrl: string, serviceId: string): string { + return `${apiBaseUrl}/microservices/${encodeURIComponent(serviceId)}/health`; +} + +async function requestHealthWithTimeout(url: string, timeoutMs = 16000): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + return await requestJson(url, { signal: controller.signal, failureFields: [false] }); + } finally { + clearTimeout(timer); + } +} + +function targetMountHint(service: any): string { + if (service?.providerId === "main-server") return "host / -> /srv"; + if (service?.providerId === "D601" || service?.providerId === "D518") return "WSL / + /mnt/c -> /srv"; + return "provider / -> /srv"; +} + +function healthOk(value: any): boolean { + return value?.status === "OK" || value?.ok === true; +} + +function TargetCard({ service, active, health, onSelect, onRaw }: AnyRecord) { + const runtime = microserviceRuntime(service); + const backend = microserviceBackend(service); + const repository = microserviceRepository(service); + const container = runtime.container || {}; + const ok = healthOk(health?.body); + return h("button", { + type: "button", + className: `filebrowser-target-card ${active ? "active" : ""}`, + "data-testid": `filebrowser-target-card-${service.id}`, + onClick: onSelect, + }, + h("span", { className: `status-badge ${ok ? "ok" : runtime.providerStatus === "online" ? "running" : "warn"}` }, ok ? "Health OK" : runtime.providerStatus || "unknown"), + h("strong", null, service.name || service.id), + h("span", null, targetMountHint(service)), + h("code", null, `${backend.nodeBindHost || "--"}:${backend.nodePort || "--"}`), + h("small", null, container.name ? `${container.name} / ${container.state || "--"}` : `${repository.composeService || "--"}`), + h("span", { className: "filebrowser-card-raw", onClick: (event: any) => { event.stopPropagation(); onRaw(`${service.name} service`, service); } }, "JSON"), + ); +} + +function frameDocument(iframe: any): Document | null { + try { + return iframe?.contentDocument || iframe?.contentWindow?.document || null; + } catch { + return null; + } +} + +function injectFilebrowserCompactStyle(iframe: any): boolean { + const doc = frameDocument(iframe); + if (doc === null || doc.head === null) return false; + let style = doc.getElementById("unidesk-filebrowser-compact-style") as HTMLStyleElement | null; + if (style === null) { + style = doc.createElement("style"); + style.id = "unidesk-filebrowser-compact-style"; + doc.head.appendChild(style); + } + if (style.textContent !== filebrowserCompactCss) style.textContent = filebrowserCompactCss; + return true; +} + +function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 2000); +} + +function exportIframeScreenshot(iframe: any, filename: string): void { + const doc = frameDocument(iframe); + if (doc === null || doc.documentElement === null) throw new Error("无法访问 File Browser iframe 文档"); + injectFilebrowserCompactStyle(iframe); + const width = Math.max(640, Math.ceil(iframe.clientWidth || doc.documentElement.clientWidth || 1280)); + const height = Math.max(480, Math.ceil(iframe.clientHeight || doc.documentElement.clientHeight || 720)); + const clone = doc.documentElement.cloneNode(true) as HTMLElement; + clone.querySelectorAll("script, style, link[rel='stylesheet'], link[rel='preload'], link[rel='icon']").forEach((node) => node.remove()); + clone.querySelectorAll("img").forEach((image) => { + image.removeAttribute("src"); + image.removeAttribute("srcset"); + }); + let head = clone.querySelector("head"); + if (head === null) { + head = doc.createElement("head"); + clone.insertBefore(head, clone.firstChild); + } + const style = doc.createElement("style"); + style.textContent = `${filebrowserCompactCss}\nhtml,body{width:${width}px!important;min-height:${height}px!important;overflow:hidden!important;}`; + head.appendChild(style); + const xhtml = new XMLSerializer().serializeToString(clone); + const svg = `${xhtml}`; + downloadBlob(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }), filename.replace(/\.png$/i, ".svg")); +} + +export function FileBrowserPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyRecord) { + const services = filebrowserServices(Array.isArray(microservices) ? microservices : []); + const rawRequestedTarget = new URLSearchParams(window.location.search).get("target") || ""; + const requestedTarget = rawRequestedTarget === "filebrowser-d518" ? "filebrowser" : rawRequestedTarget; + const initialId = services.some((service) => service.id === requestedTarget) ? requestedTarget : services[0]?.id || ""; + const [activeId, setActiveId] = useState(initialId); + const [state, setState] = useState({ loading: false, refreshedAt: null as string | null, health: {} as AnyRecord, error: "" }); + const [screenshot, setScreenshot] = useState({ exporting: false, message: "", error: "" }); + const iframeRef = useRef(null); + const activeService = services.find((service) => service.id === activeId) || services[0] || null; + const activeRuntime = microserviceRuntime(activeService); + const activeBackend = microserviceBackend(activeService); + const activeRepository = microserviceRepository(activeService); + const activeHealth = activeService ? state.health[activeService.id] : null; + const frameSrc = activeService ? browserProxyUrl(apiBaseUrl, activeService.id, "/") : "about:blank"; + + useEffect(() => { + if (services.length === 0) return; + if (!activeId || !services.some((service) => service.id === activeId)) setActiveId(services[0].id); + }, [services.map((service) => service.id).join(",")]); + + useEffect(() => { + let attempts = 0; + const timer = setInterval(() => { + attempts += 1; + const applied = injectFilebrowserCompactStyle(iframeRef.current); + if (applied || attempts >= 24) clearInterval(timer); + }, 500); + return () => clearInterval(timer); + }, [frameSrc]); + + useEffect(() => { + if (services.length === 0) return; + let cancelled = false; + async function load() { + setState((previous: AnyRecord) => ({ ...previous, loading: true, error: "" })); + const entries = await Promise.all(services.map(async (service) => { + try { + const body = await requestHealthWithTimeout(healthUrl(apiBaseUrl, service.id)); + return [service.id, { ok: true, body }] as const; + } catch (error) { + return [service.id, { ok: false, error: errorMessage(error, "File Browser health failed") }] as const; + } + })); + if (cancelled) return; + setState({ loading: false, refreshedAt: new Date().toISOString(), health: Object.fromEntries(entries), error: "" }); + } + load(); + const timer = setInterval(load, 30000); + return () => { + cancelled = true; + clearInterval(timer); + }; + }, [services.map((service) => `${service.id}:${service.runtime?.providerStatus || ""}`).join(","), apiBaseUrl]); + + function selectTarget(id: string) { + setActiveId(id); + const url = new URL(window.location.href); + url.searchParams.set("target", id); + window.history.replaceState({}, "", `${url.pathname}${url.search}`); + } + + async function exportScreenshot() { + if (screenshot.exporting) return; + setScreenshot({ exporting: true, message: "", error: "" }); + try { + const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14); + await exportIframeScreenshot(iframeRef.current, `unidesk-filebrowser-${activeService?.id || "target"}-${stamp}.png`); + setScreenshot({ exporting: false, message: "截图已导出", error: "" }); + } catch (error) { + setScreenshot({ exporting: false, message: "", error: errorMessage(error, "截图导出失败") }); + } + } + + if (services.length === 0) return h(EmptyState, { title: "File Browser 未登记", text: "请在 config.json 的 microservices 中登记 id=filebrowser 或 filebrowser-* 用户服务" }); + + return h("div", { className: "filebrowser-page", "data-testid": "filebrowser-page" }, + state.error ? h(UniDeskErrorBanner, { error: state.error, wide: true }) : null, + h(Panel, { + title: "文件管理器", + eyebrow: "File Browser / Host Files", + loading: state.loading, + actions: h("div", { className: "panel-actions" }, + activeService ? h("button", { type: "button", className: "ghost-btn", onClick: exportScreenshot, disabled: screenshot.exporting, "data-testid": "filebrowser-export-screenshot" }, screenshot.exporting ? "导出中..." : "导出截图") : null, + activeService ? h("a", { className: "ghost-btn", href: frameSrc, target: "_blank", rel: "noreferrer" }, "新窗口打开") : null, + activeService ? h(RawButton, { title: "File Browser 当前目标", data: { service: activeService, health: activeHealth }, onOpen: onRaw, testId: "raw-filebrowser-active" }) : null, + ), + }, + h("div", { className: "filebrowser-hero" }, + h("div", null, + h("span", { className: `status-badge ${healthOk(activeHealth?.body) ? "ok" : "warn"}` }, healthOk(activeHealth?.body) ? "Health OK" : "Health Pending"), + h("h3", null, activeService?.name || "File Browser"), + h("p", { className: "muted paragraph" }, activeService?.description || "通过 UniDesk 登录态代理访问,不开放 File Browser 公网端口。"), + screenshot.error ? h("p", { className: "filebrowser-shot-error" }, screenshot.error) : null, + screenshot.message ? h("p", { className: "filebrowser-shot-ok" }, screenshot.message) : null, + ), + h("div", { className: "microservice-ref-card" }, h("span", null, "Provider"), h("strong", null, activeService?.providerId || "--"), h("code", null, activeRuntime.providerName || activeService?.providerId || "--")), + h("div", { className: "microservice-ref-card" }, h("span", null, "Private Backend"), h("strong", null, `${activeBackend.nodeBindHost || "--"}:${activeBackend.nodePort || "--"}`), h("code", null, activeBackend.nodeBaseUrl || "--")), + h("div", { className: "microservice-ref-card" }, h("span", null, "Image"), h("strong", null, activeRepository.dockerfile || "filebrowser/filebrowser:v2.63.3"), h("code", null, activeRepository.commitId || "--")), + h("div", { className: "microservice-ref-card" }, h("span", null, "Mount"), h("strong", null, targetMountHint(activeService)), h("code", null, activeService?.providerId === "main-server" ? "/root, /var, /home" : "/home, /mnt/c, /mnt/d")), + ), + ), + h(Panel, { title: "浏览目标", eyebrow: `${services.length} host targets`, loading: state.loading }, + h("div", { className: "filebrowser-target-grid" }, + services.map((service) => h(TargetCard, { + key: service.id, + service, + active: service.id === activeService?.id, + health: state.health[service.id], + onSelect: () => selectTarget(service.id), + onRaw, + })), + ), + ), + h(Panel, { + title: `${serviceLabel(activeService)} 文件视图`, + eyebrow: activeHealth?.body ? `Health ${healthOk(activeHealth.body) ? "OK" : "UNKNOWN"} / ${state.refreshedAt ? fmtClock(state.refreshedAt) : "--"}` : "Embedded WebUI", + className: "filebrowser-frame-panel", + }, + h("div", { className: "filebrowser-frame-shell" }, + h("div", { className: "filebrowser-frame-toolbar" }, + h("span", null, "BaseURL"), + h("code", null, `/api/microservices/${activeService?.id || "filebrowser"}/proxy`), + h("span", null, "Root"), + h("code", null, "/srv"), + h("span", { className: "filebrowser-compact-note" }, "Compact layout injected"), + ), + h("iframe", { + ref: iframeRef, + key: frameSrc, + title: `${activeService?.name || "File Browser"} WebUI`, + src: frameSrc, + className: "filebrowser-frame", + "data-testid": "filebrowser-frame", + onLoad: (event: any) => injectFilebrowserCompactStyle(event.currentTarget), + sandbox: "allow-downloads allow-forms allow-modals allow-same-origin allow-scripts", + }), + ), + ), + ); +} diff --git a/src/components/frontend/src/findjob.tsx b/src/components/frontend/src/findjob.tsx index 32ece78e..5facd2b8 100644 --- a/src/components/frontend/src/findjob.tsx +++ b/src/components/frontend/src/findjob.tsx @@ -1,5 +1,6 @@ import React from "react"; import { fmtClock, fmtDate } from "./time"; +import { LoadingTitle } from "./loading-indicator"; import { errorMessage, requestJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; @@ -23,12 +24,12 @@ function MetricCard({ label, value, hint, tone }: AnyRecord) { ); } -function Panel({ title, eyebrow, actions, children, className }: AnyRecord) { +function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) { return h("section", { className: `panel ${className || ""}` }, h("div", { className: "panel-head" }, h("div", null, eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null, - h("h2", null, title), + h(LoadingTitle, { title, loading }), ), actions ? h("div", { className: "panel-actions" }, actions) : null, ), @@ -113,6 +114,7 @@ export function FindJobPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyRe h(Panel, { title: "FindJob 工作台", eyebrow: "D601 用户服务", + loading: state.loading, actions: h("div", { className: "panel-actions" }, h("button", { type: "button", className: "ghost-btn", onClick: load, disabled: state.loading, "data-testid": "findjob-refresh-button" }, state.loading ? "刷新中" : "刷新"), h(RawButton, { title: "FindJob 用户服务", data: service, onOpen: onRaw, testId: "raw-findjob-service" }), @@ -141,7 +143,7 @@ export function FindJobPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyRe h(UniDeskErrorBanner, { error: state.error, wide: true }), ), h("div", { className: "findjob-grid" }, - h(Panel, { title: "岗位指标", eyebrow: state.refreshedAt ? `Updated ${fmtClock(state.refreshedAt)}` : "Summary" }, + h(Panel, { title: "岗位指标", eyebrow: state.refreshedAt ? `Updated ${fmtClock(state.refreshedAt)}` : "Summary", loading: state.loading }, h("div", { className: "metric-grid" }, h(MetricCard, { label: "岗位总量", value: findjobSummaryMetric(summary, "totalJobs"), hint: "tracked jobs", tone: "ok" }), h(MetricCard, { label: "原始岗位", value: findjobSummaryMetric(summary, "rawJobs"), hint: "raw queue" }), @@ -154,7 +156,7 @@ export function FindJobPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyRe ), h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "FindJob Summary", data: summary, onOpen: onRaw, testId: "raw-findjob-summary" })), ), - h(Panel, { title: "近期岗位", eyebrow: transform ? `${transform.returnedLength}/${transform.originalLength} Preview` : `${jobs.length} Preview` }, + h(Panel, { title: "近期岗位", eyebrow: transform ? `${transform.returnedLength}/${transform.originalLength} Preview` : `${jobs.length} Preview`, loading: state.loading }, jobs.length === 0 ? h(EmptyState, { title: "暂无岗位预览", text: "等待 D601 findjob backend 返回 /api/jobs" }) : h("div", { className: "table-wrap findjob-job-table" }, h("table", null, h("thead", null, h("tr", null, h("th", null, "优先级"), h("th", null, "状态"), h("th", null, "单位"), h("th", null, "职位"), h("th", null, "城市"), h("th", null, "阶段"), h("th", null, "截止"), h("th", null, "证据"))), @@ -171,7 +173,7 @@ export function FindJobPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyRe )), h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "FindJob Jobs Preview", data: state.jobs, onOpen: onRaw, testId: "raw-findjob-jobs" })), ), - h(Panel, { title: "草稿与报告", eyebrow: `${drafts.length} Drafts` }, + h(Panel, { title: "草稿与报告", eyebrow: `${drafts.length} Drafts`, loading: state.loading }, drafts.length === 0 ? h(EmptyState, { title: "暂无草稿", text: "D601 findjob backend 未返回 drafts" }) : h("div", { className: "draft-list" }, drafts.map((draft: any) => h("article", { key: draft.id, className: "draft-card" }, h("div", { className: "node-card-head" }, h("strong", null, draft.id), h(StatusBadge, { status: draft.status }, draft.status || "--")), diff --git a/src/components/frontend/src/index.ts b/src/components/frontend/src/index.ts index 5c882e9c..29a9601c 100644 --- a/src/components/frontend/src/index.ts +++ b/src/components/frontend/src/index.ts @@ -1,6 +1,7 @@ -import { appendFileSync, mkdirSync, readFileSync } from "node:fs"; +import { readFileSync } from "node:fs"; import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"; -import { dirname, join } from "node:path"; +import { join } from "node:path"; +import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../shared/src/rotating-jsonl"; interface RuntimeConfig { port: number; @@ -46,6 +47,7 @@ const config = readConfig(); const logger = createLogger("frontend", config.logFile); const publicDir = join(import.meta.dir, "..", "public"); const vendorDir = join(import.meta.dir, "..", "node_modules"); +const docsDir = join(import.meta.dir, "../../../..", "docs"); const appBundle = await buildFrontendApp("app.tsx"); const clientConfig = JSON.stringify({ frontendPublicUrl: config.frontendPublicUrl, @@ -56,85 +58,82 @@ const clientConfig = JSON.stringify({ }); const indexHtmlTemplate = readFileSync(join(publicDir, "index.html"), "utf8"); const indexHtmlRootMarker = '
'; -const codexQueueOverviewCache = new Map(); -const codexQueueOverviewRefreshes = new Map>(); -const codexQueueOverviewCacheTtlMs = 10_000; -const defaultCodexQueueOverviewPath = "/api/tasks/overview?limit=24&transcriptLimit=3&compact=1&afterSeq=0&preferId="; +const baiduNetdiskDocsFallbackLinks = [ + { href: "/docs/issue/baidu-netdisk-env-setup.md", section: "Baidu Netdisk hero action / 配置与文档 SETUP", label: "Baidu Netdisk 环境变量配置" }, + { href: "/docs/issue/baidu-netdisk-user-service.md", section: "配置与文档 DESIGN", label: "Baidu Netdisk 服务方案与 API" }, + { href: "/docs/reference/microservices.md", section: "配置与文档 REF", label: "UniDesk 用户服务安全边界" }, + { href: "/docs/reference/deployment.md", section: "配置与文档 DEPLOY", label: "UniDesk 部署与重建流程" }, + { href: "/docs/reference/cli.md", section: "配置与文档 CLI", label: "UniDesk CLI 验证命令" }, +]; +const codeQueueOverviewCache = new Map(); +const codeQueueOverviewRefreshes = new Map>(); +const codeQueueOverviewCacheTtlMs = 10_000; +const defaultCodeQueueOverviewPath = "/api/tasks/overview?limit=24&transcriptLimit=3&compact=1&afterSeq=0&preferId="; -function codexQueueOverviewCacheKey(pathWithQuery: string): string { +function codeQueueOverviewCacheKey(pathWithQuery: string): string { return pathWithQuery; } -function cachedCodexQueueOverview(pathWithQuery: string, maxAgeMs = codexQueueOverviewCacheTtlMs): { payload: JsonValue; text: string } | null { - const cached = codexQueueOverviewCache.get(codexQueueOverviewCacheKey(pathWithQuery)); +function cachedCodeQueueOverview(pathWithQuery: string, maxAgeMs = codeQueueOverviewCacheTtlMs): { payload: JsonValue; text: string } | null { + const cached = codeQueueOverviewCache.get(codeQueueOverviewCacheKey(pathWithQuery)); if (!cached || Date.now() - cached.at > maxAgeMs) return null; return { payload: cached.payload, text: cached.text }; } -async function refreshCodexQueueOverview(pathWithQuery: string, timeoutMs = 800): Promise { - const existing = codexQueueOverviewRefreshes.get(pathWithQuery); +async function refreshCodeQueueOverview(pathWithQuery: string, timeoutMs = 800): Promise { + const existing = codeQueueOverviewRefreshes.get(pathWithQuery); if (existing !== undefined) return existing; - const refresh = refreshCodexQueueOverviewUncached(pathWithQuery, timeoutMs) + const refresh = refreshCodeQueueOverviewUncached(pathWithQuery, timeoutMs) .finally(() => { - codexQueueOverviewRefreshes.delete(pathWithQuery); + codeQueueOverviewRefreshes.delete(pathWithQuery); }); - codexQueueOverviewRefreshes.set(pathWithQuery, refresh); + codeQueueOverviewRefreshes.set(pathWithQuery, refresh); return refresh; } -async function refreshCodexQueueOverviewUncached(pathWithQuery: string, timeoutMs = 800): Promise { +async function refreshCodeQueueOverviewUncached(pathWithQuery: string, timeoutMs = 800): Promise { const started = performance.now(); try { - const response = await fetch(`http://codex-queue:4222${pathWithQuery}`, { + const response = await fetch(`http://code-queue:4222${pathWithQuery}`, { signal: AbortSignal.timeout(timeoutMs), headers: { accept: "application/json" }, }); const text = await response.text(); const payload = text ? JSON.parse(text) as unknown : null; const ok = response.ok && typeof payload === "object" && payload !== null; - recordOperationPerformance("frontend", "codex_queue_initial_overview", performance.now() - started, ok, pathWithQuery); + recordOperationPerformance("frontend", "code_queue_initial_overview", performance.now() - started, ok, pathWithQuery); if (!ok) return null; - codexQueueOverviewCache.set(codexQueueOverviewCacheKey(pathWithQuery), { at: Date.now(), payload: payload as JsonValue, text }); + codeQueueOverviewCache.set(codeQueueOverviewCacheKey(pathWithQuery), { at: Date.now(), payload: payload as JsonValue, text }); return payload as JsonValue; } catch (error) { const message = error instanceof Error ? error.message : String(error); - recordOperationPerformance("frontend", "codex_queue_initial_overview", performance.now() - started, false, message); + recordOperationPerformance("frontend", "code_queue_initial_overview", performance.now() - started, false, message); return null; } } -async function readCodexQueueInitialOverview(req: Request): Promise { - if (sessionFromRequest(req) === null) return null; - const overviewPath = defaultCodexQueueOverviewPath; - const cached = cachedCodexQueueOverview(overviewPath); - if (cached !== null) return cached.payload; - const stale = codexQueueOverviewCache.get(codexQueueOverviewCacheKey(overviewPath)); - if (stale !== undefined) { - refreshCodexQueueOverview(overviewPath).catch(() => undefined); - return stale.payload; - } - return refreshCodexQueueOverview(overviewPath); -} - -function isCodexQueueRoute(pathname: string): boolean { - return pathname === "/app/codex-queue" || pathname.startsWith("/app/codex-queue/"); -} - function renderIndexHtml(extraRootAttributes = ""): string { + const docsFallback = `
`; return indexHtmlTemplate.replace( indexHtmlRootMarker, - `
`, + `
${docsFallback}
`, ); } async function spaShellHtml(req: Request, pathname: string): Promise { - if (!isCodexQueueRoute(pathname)) return renderIndexHtml(); - const initialOverview = await readCodexQueueInitialOverview(req); - const initialOverviewAttr = initialOverview === null ? "" : ` data-codex-overview="${escapeHtmlAttribute(JSON.stringify(initialOverview))}"`; - return renderIndexHtml(initialOverviewAttr); + void req; + void pathname; + // Do not inline Codex task overview JSON into the root element: task prompts + // can be very large and contain quote-heavy text, which makes malformed + // markup failures show raw JSON at the page tail. The React page fetches the + // same overview through the authenticated API after mounting. + return renderIndexHtml(); } -refreshCodexQueueOverview(defaultCodexQueueOverviewPath, 2_000).catch(() => undefined); +refreshCodeQueueOverview(defaultCodeQueueOverviewPath, 2_000).catch(() => undefined); const requestPerformanceSamples: RequestPerformanceSample[] = []; const operationPerformanceSamples: OperationPerformanceSample[] = []; const maxPerformanceSamples = 3000; @@ -191,14 +190,19 @@ function readConfig(): RuntimeConfig { } function createLogger(service: string, logFile: string) { - mkdirSync(dirname(logFile), { recursive: true }); + const writer = createHourlyJsonlWriter({ + baseLogFile: logFile, + service, + maxBytes: logRetentionBytesForService(service), + }); + writer.prune(); return (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue): void => { const entry = data === undefined ? { ts: new Date().toISOString(), service, level, message } : { ts: new Date().toISOString(), service, level, message, data }; const line = `${JSON.stringify(entry)}\n`; try { - appendFileSync(logFile, line, "utf8"); + writer.appendLine(line, new Date(entry.ts)); } catch (error) { console.error(JSON.stringify({ ts: new Date().toISOString(), service, level: "error", message: "log_write_failed", data: String(error) })); } @@ -211,6 +215,7 @@ function contentType(pathname: string): string { if (pathname.endsWith(".html")) return "text/html; charset=utf-8"; if (pathname.endsWith(".css")) return "text/css; charset=utf-8"; if (pathname.endsWith(".js")) return "text/javascript; charset=utf-8"; + if (pathname.endsWith(".md")) return "text/markdown; charset=utf-8"; if (pathname.endsWith(".svg")) return "image/svg+xml"; if (pathname.endsWith(".ico")) return "image/x-icon"; return "text/plain; charset=utf-8"; @@ -387,6 +392,197 @@ function escapeHtmlAttribute(value: string): string { .replace(/>/g, ">"); } +function escapeHtmlText(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">"); +} + +function docsSlug(value: string): string { + const slug = String(value || "") + .trim() + .toLowerCase() + .replace(/[`*_~[\]().::,,/\\?#%]+/gu, "") + .replace(/\s+/gu, "-") + .replace(/-+/gu, "-") + .replace(/^-+|-+$/gu, ""); + return slug || "section"; +} + +function isSafeDocLink(url: string): boolean { + return /^https?:\/\//u.test(url) || url.startsWith("/docs/"); +} + +function linkifiedMarkdownInline(value: string): string { + const pattern = /(`[^`]+`|\[[^\]]+\]\((?:https?:\/\/|\/docs\/)[^) \t]+\)|https?:\/\/[^\s<>)]+|\/docs\/[^\s<>)]+)/gu; + let cursor = 0; + let html = ""; + for (const match of value.matchAll(pattern)) { + const token = match[0]; + const index = match.index ?? 0; + html += escapeHtmlText(value.slice(cursor, index)); + cursor = index + token.length; + if (token.startsWith("`") && token.endsWith("`")) { + html += `${escapeHtmlText(token.slice(1, -1))}`; + continue; + } + const markdownLink = token.match(/^\[([^\]]+)\]\((.+)\)$/u); + if (markdownLink !== null) { + const [, label, href] = markdownLink; + html += isSafeDocLink(href) + ? `${escapeHtmlText(label)}` + : escapeHtmlText(token); + continue; + } + html += `${escapeHtmlText(token)}`; + } + html += escapeHtmlText(value.slice(cursor)); + return html; +} + +function renderMarkdownBody(markdown: string): string { + const lines = markdown.split(/\r?\n/u); + let html = ""; + let paragraph: string[] = []; + let listKind: "ul" | "ol" | null = null; + let inFence = false; + + function flushParagraph(): void { + if (paragraph.length === 0) return; + html += `

${linkifiedMarkdownInline(paragraph.join(" "))}

`; + paragraph = []; + } + + function closeList(): void { + if (listKind === null) return; + html += ``; + listKind = null; + } + + function openList(kind: "ul" | "ol"): void { + flushParagraph(); + if (listKind === kind) return; + closeList(); + listKind = kind; + html += `<${kind}>`; + } + + for (const line of lines) { + if (/^```/u.test(line.trim())) { + flushParagraph(); + closeList(); + html += inFence ? "" : "
";
+      inFence = !inFence;
+      continue;
+    }
+    if (inFence) {
+      html += `${escapeHtmlText(line)}\n`;
+      continue;
+    }
+    const heading = line.match(/^(#{1,6})\s+(.+)$/u);
+    if (heading !== null) {
+      flushParagraph();
+      closeList();
+      const level = Math.min(6, heading[1].length);
+      const title = heading[2].trim();
+      html += `${linkifiedMarkdownInline(title)}`;
+      continue;
+    }
+    const unordered = line.match(/^\s*-\s+(.+)$/u);
+    if (unordered !== null) {
+      openList("ul");
+      html += `
  • ${linkifiedMarkdownInline(unordered[1])}
  • `; + continue; + } + const ordered = line.match(/^\s*\d+\.\s+(.+)$/u); + if (ordered !== null) { + openList("ol"); + html += `
  • ${linkifiedMarkdownInline(ordered[1])}
  • `; + continue; + } + if (line.trim().length === 0) { + flushParagraph(); + closeList(); + continue; + } + paragraph.push(line.trim()); + } + flushParagraph(); + closeList(); + if (inFence) html += "
    "; + return html; +} + +function renderDocsHtml(relativePath: string, markdown: string): string { + const firstHeading = markdown.match(/^#\s+(.+)$/mu)?.[1]?.trim(); + const title = firstHeading || relativePath.split("/").pop() || "UniDesk Docs"; + const escapedTitle = escapeHtmlText(title); + return ` + + + + + ${escapedTitle} - UniDesk Docs + + + +
    + +

    ${escapeHtmlText(relativePath)}

    + ${renderMarkdownBody(markdown)} +
    + +`; +} + +function safeDocsRelativePath(pathname: string): string | null { + const raw = pathname === "/docs" ? "" : pathname.slice("/docs/".length); + let decoded = ""; + try { + decoded = decodeURIComponent(raw); + } catch { + return null; + } + if (!decoded || decoded.includes("\0") || decoded.includes("\\") || decoded.startsWith("/") || decoded.split("/").includes("..")) return null; + if (!decoded.endsWith(".md")) return null; + return decoded; +} + +async function docsResponse(req: Request, url: URL): Promise { + if (req.method !== "GET" && req.method !== "HEAD") return jsonResponse({ ok: false, error: "method not allowed" }, 405); + if (sessionFromRequest(req) === null) return textResponse(req, "authentication required", "text/plain; charset=utf-8"); + const relativePath = safeDocsRelativePath(url.pathname); + if (relativePath === null) return jsonResponse({ ok: false, error: "invalid docs path", path: url.pathname }, 400); + const file = Bun.file(join(docsDir, relativePath)); + if (!(await file.exists())) return jsonResponse({ ok: false, error: "doc not found", path: relativePath }, 404); + if (req.method === "HEAD") return new Response(null, { headers: { "content-type": "text/html; charset=utf-8" } }); + return textResponse(req, renderDocsHtml(relativePath, await file.text()), "text/html; charset=utf-8"); +} + function signPayload(payload: string): string { return createHmac("sha256", config.sessionSecret).update(payload).digest("base64url"); } @@ -491,25 +687,25 @@ function sessionResponse(req: Request): Response { return jsonResponse({ ok: true, authenticated: true, user: { username: session.username }, expiresAt: new Date(session.expiresAt).toISOString() }); } -async function proxyCodexQueueDirect(req: Request, url: URL): Promise { +async function proxyCodeQueueDirect(req: Request, url: URL): Promise { if (sessionFromRequest(req) === null) { return jsonResponse({ ok: false, error: "authentication required" }, 401); } - const prefix = "/api/codex-queue-direct"; + const prefix = "/api/code-queue-direct"; const suffix = url.pathname === prefix ? "/" : `/${url.pathname.slice(prefix.length).replace(/^\/+/u, "")}`; if (!(suffix === "/health" || suffix.startsWith("/api/"))) { - return jsonResponse({ ok: false, error: "codex queue direct path is not allowed", path: suffix }, 403); + return jsonResponse({ ok: false, error: "code-queue direct path is not allowed", path: suffix }, 403); } const overviewCacheKey = `${suffix}${url.search}`; const canUseOverviewCache = req.method === "GET" && suffix === "/api/tasks/overview"; if (canUseOverviewCache) { - const cached = cachedCodexQueueOverview(overviewCacheKey); + const cached = cachedCodeQueueOverview(overviewCacheKey); if (cached !== null) { - recordOperationPerformance("frontend", "codex_queue_direct_proxy_cache", 0, true, overviewCacheKey); + recordOperationPerformance("frontend", "code_queue_direct_proxy_cache", 0, true, overviewCacheKey); return new Response(cached.text, { headers: { "content-type": "application/json; charset=utf-8" } }); } } - const upstreamUrl = new URL(`${suffix}${url.search}`, "http://codex-queue:4222"); + const upstreamUrl = new URL(`${suffix}${url.search}`, "http://code-queue:4222"); const headers = new Headers(req.headers); headers.delete("host"); headers.delete("connection"); @@ -522,7 +718,7 @@ async function proxyCodexQueueDirect(req: Request, url: URL): Promise const started = performance.now(); try { const upstream = await fetch(upstreamUrl, init); - recordOperationPerformance("frontend", "codex_queue_direct_proxy", performance.now() - started, upstream.ok, `${req.method} ${suffix}`); + recordOperationPerformance("frontend", "code_queue_direct_proxy", performance.now() - started, upstream.ok, `${req.method} ${suffix}`); const responseHeaders = new Headers(); const upstreamContentType = upstream.headers.get("content-type"); if (upstreamContentType !== null) responseHeaders.set("content-type", upstreamContentType); @@ -536,7 +732,7 @@ async function proxyCodexQueueDirect(req: Request, url: URL): Promise parsedJson = jsonText ? JSON.parse(jsonText) as unknown : null; } catch (error) { const detail = error instanceof Error ? error.message : String(error); - logger("warn", "codex_queue_direct_proxy_invalid_json", { + logger("warn", "code_queue_direct_proxy_invalid_json", { path: suffix, upstreamUrl: upstreamUrl.toString(), status: upstream.status, @@ -547,7 +743,7 @@ async function proxyCodexQueueDirect(req: Request, url: URL): Promise return jsonResponse({ ok: false, error: { - message: "codex queue upstream returned invalid JSON", + message: "code-queue upstream returned invalid JSON", detail, status: upstream.status, bodyBytes: upstreamBody.byteLength, @@ -559,7 +755,7 @@ async function proxyCodexQueueDirect(req: Request, url: URL): Promise if (canUseOverviewCache && upstream.ok && isJsonResponse) { const text = new TextDecoder().decode(upstreamBody); if (typeof parsedJson === "object" && parsedJson !== null) { - codexQueueOverviewCache.set(codexQueueOverviewCacheKey(overviewCacheKey), { at: Date.now(), payload: parsedJson as JsonValue, text }); + codeQueueOverviewCache.set(codeQueueOverviewCacheKey(overviewCacheKey), { at: Date.now(), payload: parsedJson as JsonValue, text }); } return new Response(text, { status: upstream.status, headers: responseHeaders }); } @@ -567,9 +763,9 @@ async function proxyCodexQueueDirect(req: Request, url: URL): Promise return new Response(upstreamBody, { status: upstream.status, headers: responseHeaders }); } catch (error) { const message = error instanceof Error ? error.message : String(error); - recordOperationPerformance("frontend", "codex_queue_direct_proxy", performance.now() - started, false, message); - logger("warn", "codex_queue_direct_proxy_failed", { path: suffix, upstreamUrl: upstreamUrl.toString(), error: message }); - return jsonResponse({ ok: false, error: { message: "codex queue direct proxy failed", detail: message } }, 502); + recordOperationPerformance("frontend", "code_queue_direct_proxy", performance.now() - started, false, message); + logger("warn", "code_queue_direct_proxy_failed", { path: suffix, upstreamUrl: upstreamUrl.toString(), error: message }); + return jsonResponse({ ok: false, error: { message: "code-queue direct proxy failed", detail: message } }, 502); } } @@ -639,8 +835,9 @@ async function handleRequest(req: Request): Promise { if (url.pathname === "/logout" && req.method === "POST") return logout(); if (url.pathname === "/api/session") return sessionResponse(req); if (url.pathname === "/api/frontend-performance") return frontendPerformanceResponse(req); - if (url.pathname === "/api/codex-queue-direct" || url.pathname.startsWith("/api/codex-queue-direct/")) return proxyCodexQueueDirect(req, url); + if (url.pathname === "/api/code-queue-direct" || url.pathname.startsWith("/api/code-queue-direct/")) return proxyCodeQueueDirect(req, url); if (url.pathname.startsWith("/api/") || url.pathname === "/logs") return proxyApi(req, url); + if (url.pathname === "/docs" || url.pathname.startsWith("/docs/")) return docsResponse(req, url); if (url.pathname === "/" || url.pathname === "/index.html") { return textResponse(req, await spaShellHtml(req, url.pathname), "text/html; charset=utf-8"); } diff --git a/src/components/frontend/src/loading-indicator.tsx b/src/components/frontend/src/loading-indicator.tsx new file mode 100644 index 00000000..3619615e --- /dev/null +++ b/src/components/frontend/src/loading-indicator.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +type LoadingTitleProps = { + title?: React.ReactNode; + children?: React.ReactNode; + loading?: boolean; + level?: 2 | 3; + className?: string; + label?: string; +}; + +const h = React.createElement; + +export function LoadingIndicator({ active = true, label = "正在加载" }: { active?: boolean; label?: string }) { + if (!active) return null; + return h("span", { + className: "loading-spinner-indicator", + role: "status", + "aria-label": label, + title: label, + "data-testid": "loading-title-indicator", + }, + h("span", { className: "loading-spinner-ring", "aria-hidden": true }), + ); +} + +export function LoadingTitle({ title, children, loading, level = 2, className = "", label = "正在加载" }: LoadingTitleProps) { + const tag = level === 3 ? "h3" : "h2"; + return h(tag, { className: `loading-title ${loading ? "is-loading" : ""} ${className}`.trim() }, + h("span", { className: "loading-title-text" }, children ?? title), + h(LoadingIndicator, { active: Boolean(loading), label }), + ); +} diff --git a/src/components/frontend/src/markdown.tsx b/src/components/frontend/src/markdown.tsx new file mode 100644 index 00000000..d3c65245 --- /dev/null +++ b/src/components/frontend/src/markdown.tsx @@ -0,0 +1,338 @@ +import React from "react"; + +const h = React.createElement; + +type AnyRecord = Record; + +interface MarkdownBodyProps { + markdown: any; + className?: string; + testId?: string; +} + +interface FenceInfo { + marker: "`" | "~"; + length: number; + language: string; +} + +interface ListItemInfo { + ordered: boolean; + text: string; + start?: number; +} + +export function MarkdownBody({ markdown, className, testId }: MarkdownBodyProps) { + const text = String(markdown ?? "").trimEnd(); + const classes = ["markdown-body", className].filter(Boolean).join(" "); + return h("div", { className: classes, "data-testid": testId }, renderMarkdownBlocks(text, "md")); +} + +function renderMarkdownBlocks(markdown: string, keyPrefix: string): any[] { + const lines = normalizeMarkdown(markdown).split("\n"); + const nodes: any[] = []; + let index = 0; + + while (index < lines.length) { + const line = lines[index] ?? ""; + if (line.trim().length === 0) { + index += 1; + continue; + } + + const fence = matchFence(line); + if (fence !== null) { + const codeLines: string[] = []; + index += 1; + while (index < lines.length && !isClosingFence(lines[index] ?? "", fence)) { + codeLines.push(lines[index] ?? ""); + index += 1; + } + if (index < lines.length) index += 1; + nodes.push(renderCodeBlock(codeLines.join("\n"), fence.language, `${keyPrefix}-fence-${index}`)); + continue; + } + + if (isIndentedCodeLine(line)) { + const codeLines: string[] = []; + while (index < lines.length && (isIndentedCodeLine(lines[index] ?? "") || (lines[index] ?? "").trim().length === 0)) { + const current = lines[index] ?? ""; + codeLines.push(current.replace(/^(?: {4}|\t)/u, "")); + index += 1; + } + nodes.push(renderCodeBlock(codeLines.join("\n").trimEnd(), "", `${keyPrefix}-indent-${index}`)); + continue; + } + + const heading = line.match(/^(#{1,6})\s+(.+)$/u); + if (heading !== null) { + const sourceLevel = heading[1].length; + const tag = `h${Math.min(6, sourceLevel + 2)}`; + nodes.push(h(tag, { key: `${keyPrefix}-heading-${index}` }, renderInline(heading[2].trim(), `${keyPrefix}-heading-${index}`))); + index += 1; + continue; + } + + if (/^\s*(?:---+|\*\*\*+|___+)\s*$/u.test(line)) { + nodes.push(h("hr", { key: `${keyPrefix}-hr-${index}` })); + index += 1; + continue; + } + + if (/^\s*>\s?/u.test(line)) { + const quoteLines: string[] = []; + while (index < lines.length) { + const current = lines[index] ?? ""; + const quote = current.match(/^\s*>\s?(.*)$/u); + if (quote !== null) { + quoteLines.push(quote[1]); + index += 1; + continue; + } + if (current.trim().length === 0) { + quoteLines.push(""); + index += 1; + continue; + } + break; + } + nodes.push(h("blockquote", { key: `${keyPrefix}-quote-${index}` }, renderMarkdownBlocks(quoteLines.join("\n"), `${keyPrefix}-quote-${index}`))); + continue; + } + + if (isTableStart(lines, index)) { + const tableStart = index; + const header = splitTableRow(lines[index] ?? ""); + const separators = splitTableRow(lines[index + 1] ?? ""); + index += 2; + const rows: string[][] = []; + while (index < lines.length && (lines[index] ?? "").includes("|") && (lines[index] ?? "").trim().length > 0) { + rows.push(splitTableRow(lines[index] ?? "")); + index += 1; + } + nodes.push(renderTable(header, separators, rows, `${keyPrefix}-table-${tableStart}`)); + continue; + } + + const listItem = matchListItem(line); + if (listItem !== null) { + const listStart = index; + const ordered = listItem.ordered; + const start = listItem.start; + const items: string[] = []; + while (index < lines.length) { + const currentItem = matchListItem(lines[index] ?? ""); + if (currentItem === null || currentItem.ordered !== ordered) break; + const itemLines = [currentItem.text]; + index += 1; + while (index < lines.length) { + const continuation = lines[index] ?? ""; + if (continuation.trim().length === 0) break; + if (matchListItem(continuation) !== null) break; + if (!/^\s{2,}/u.test(continuation)) break; + itemLines.push(continuation.replace(/^\s{2,4}/u, "")); + index += 1; + } + items.push(itemLines.join("\n").trimEnd()); + } + const listProps: AnyRecord = { key: `${keyPrefix}-list-${listStart}` }; + if (ordered && start !== undefined && start !== 1) listProps.start = start; + nodes.push(h(ordered ? "ol" : "ul", listProps, items.map((item, itemIndex) => renderListItem(item, `${keyPrefix}-list-${listStart}-${itemIndex}`)))); + continue; + } + + const paragraphStart = index; + const paragraphLines: string[] = []; + while (index < lines.length && lines[index] !== undefined && lines[index].trim().length > 0 && !isBlockStart(lines, index)) { + paragraphLines.push(lines[index].trim()); + index += 1; + } + if (paragraphLines.length === 0) { + paragraphLines.push(line.trim()); + index += 1; + } + nodes.push(h("p", { key: `${keyPrefix}-p-${paragraphStart}` }, renderInline(paragraphLines.join("\n"), `${keyPrefix}-p-${paragraphStart}`))); + } + + return nodes; +} + +function normalizeMarkdown(markdown: string): string { + return String(markdown || "").replace(/\r\n/gu, "\n").replace(/\r/gu, "\n").trimEnd(); +} + +function matchFence(line: string): FenceInfo | null { + const match = line.match(/^\s*(```+|~~~+)\s*([A-Za-z0-9_-]+)?\s*$/u); + if (match === null) return null; + const fence = match[1]; + return { + marker: fence.startsWith("`") ? "`" : "~", + length: fence.length, + language: match[2] || "", + }; +} + +function isClosingFence(line: string, fence: FenceInfo): boolean { + const trimmed = line.trim(); + return trimmed.length >= fence.length && trimmed.split("").every((char) => char === fence.marker); +} + +function isIndentedCodeLine(line: string): boolean { + return /^(?: {4}|\t)/u.test(line); +} + +function renderCodeBlock(code: string, language: string, key: string): any { + const className = language.trim().length > 0 ? `language-${safeClassToken(language)}` : undefined; + return h("pre", { key, className: "markdown-code-block" }, h("code", { className }, code)); +} + +function isBlockStart(lines: string[], index: number): boolean { + const line = lines[index] ?? ""; + if (line.trim().length === 0) return true; + return matchFence(line) !== null + || isIndentedCodeLine(line) + || /^(#{1,6})\s+.+$/u.test(line) + || /^\s*(?:---+|\*\*\*+|___+)\s*$/u.test(line) + || /^\s*>\s?/u.test(line) + || isTableStart(lines, index) + || matchListItem(line) !== null; +} + +function matchListItem(line: string): ListItemInfo | null { + const unordered = line.match(/^\s{0,3}[-*+]\s+(.+)$/u); + if (unordered !== null) return { ordered: false, text: unordered[1] }; + const ordered = line.match(/^\s{0,3}(\d+)[.)]\s+(.+)$/u); + if (ordered !== null) return { ordered: true, start: Number(ordered[1]), text: ordered[2] }; + return null; +} + +function renderListItem(text: string, key: string): any { + const task = text.match(/^\[([ xX])\]\s+(.+)$/u); + if (task !== null) { + const checked = task[1].toLowerCase() === "x"; + return h("li", { key, className: "task-list-item" }, + h("input", { type: "checkbox", checked, readOnly: true, tabIndex: -1 }), + h("span", null, renderInline(task[2], `${key}-task`)), + ); + } + return h("li", { key }, renderInline(text, key)); +} + +function isTableStart(lines: string[], index: number): boolean { + const header = lines[index] ?? ""; + const separator = lines[index + 1] ?? ""; + if (!header.includes("|") || !separator.includes("|")) return false; + const headerCells = splitTableRow(header); + const separatorCells = splitTableRow(separator); + return headerCells.length > 1 + && separatorCells.length === headerCells.length + && separatorCells.every((cell) => /^:?-{3,}:?$/u.test(cell.trim())); +} + +function splitTableRow(line: string): string[] { + let source = line.trim(); + if (source.startsWith("|")) source = source.slice(1); + if (source.endsWith("|")) source = source.slice(0, -1); + return source.split("|").map((cell) => cell.trim()); +} + +function tableAlign(separator: string): "left" | "center" | "right" | undefined { + const value = separator.trim(); + if (value.startsWith(":") && value.endsWith(":")) return "center"; + if (value.endsWith(":")) return "right"; + if (value.startsWith(":")) return "left"; + return undefined; +} + +function renderTable(headers: string[], separators: string[], rows: string[][], key: string): any { + const aligns = separators.map(tableAlign); + return h("div", { key, className: "markdown-table-wrap" }, + h("table", null, + h("thead", null, + h("tr", null, headers.map((cell, index) => h("th", { key: `${key}-h-${index}`, style: aligns[index] ? { textAlign: aligns[index] } : undefined }, renderInline(cell, `${key}-h-${index}`)))), + ), + h("tbody", null, rows.map((row, rowIndex) => h("tr", { key: `${key}-r-${rowIndex}` }, + headers.map((_, cellIndex) => h("td", { + key: `${key}-r-${rowIndex}-${cellIndex}`, + style: aligns[cellIndex] ? { textAlign: aligns[cellIndex] } : undefined, + }, renderInline(row[cellIndex] || "", `${key}-r-${rowIndex}-${cellIndex}`))), + ))), + ), + ); +} + +function renderInline(text: string, keyPrefix: string): any[] { + const children: any[] = []; + const pattern = /`([^`\n]+)`|\[([^\]\n]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|(https?:\/\/[^\s<>)]+)|\*\*([^*\n]+)\*\*|__([^_\n]+)__|~~([^~\n]+)~~|\*([^*\n]+)\*|_([^_\n]+)_/gu; + let cursor = 0; + let tokenIndex = 0; + for (const match of text.matchAll(pattern)) { + const token = match[0]; + const offset = match.index ?? 0; + appendText(children, text.slice(cursor, offset), `${keyPrefix}-text-${tokenIndex}`); + cursor = offset + token.length; + const key = `${keyPrefix}-inline-${tokenIndex}`; + tokenIndex += 1; + + if (match[1] !== undefined) { + children.push(h("code", { key }, match[1])); + continue; + } + if (match[2] !== undefined && match[3] !== undefined) { + children.push(renderLink(match[2], match[3], key)); + continue; + } + if (match[4] !== undefined) { + children.push(renderLink(match[4], match[4], key)); + continue; + } + const strong = match[5] ?? match[6]; + if (strong !== undefined) { + children.push(h("strong", { key }, renderInline(strong, `${key}-strong`))); + continue; + } + if (match[7] !== undefined) { + children.push(h("del", { key }, renderInline(match[7], `${key}-del`))); + continue; + } + const emphasis = match[8] ?? match[9]; + if (emphasis !== undefined) { + children.push(h("em", { key }, renderInline(emphasis, `${key}-em`))); + } + } + appendText(children, text.slice(cursor), `${keyPrefix}-text-tail`); + return children; +} + +function appendText(children: any[], text: string, keyPrefix: string): void { + if (text.length === 0) return; + const parts = text.split("\n"); + parts.forEach((part, index) => { + if (index > 0) children.push(h("br", { key: `${keyPrefix}-br-${index}` })); + if (part.length > 0) children.push(part); + }); +} + +function renderLink(label: string, href: string, key: string): any { + const safeHref = safeMarkdownHref(href); + if (safeHref === null) return h("span", { key }, label); + const external = /^(?:https?:|mailto:)/iu.test(safeHref); + return h("a", { + key, + href: safeHref, + target: external ? "_blank" : undefined, + rel: external ? "noreferrer" : undefined, + }, renderInline(label, `${key}-label`)); +} + +function safeMarkdownHref(raw: string): string | null { + const href = String(raw || "").trim(); + if (/^(?:https?:|mailto:)/iu.test(href)) return href; + if (href.startsWith("/") && !href.startsWith("//")) return href; + if (href.startsWith("#")) return href; + return null; +} + +function safeClassToken(value: string): string { + return String(value || "").toLowerCase().replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/gu, "") || "text"; +} diff --git a/src/components/frontend/src/met-nonlinear.tsx b/src/components/frontend/src/met-nonlinear.tsx index 43d2389c..f3fe0186 100644 --- a/src/components/frontend/src/met-nonlinear.tsx +++ b/src/components/frontend/src/met-nonlinear.tsx @@ -1,5 +1,6 @@ import React from "react"; import { fmtClock, fmtDate } from "./time"; +import { LoadingTitle } from "./loading-indicator"; import { errorMessage, requestJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; @@ -70,12 +71,12 @@ function MetricCard({ label, value, hint, tone }: AnyRecord) { ); } -function Panel({ title, eyebrow, actions, children, className }: AnyRecord) { +function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) { return h("section", { className: `panel ${className || ""}` }, h("div", { className: "panel-head" }, h("div", null, eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null, - h("h2", null, title), + h(LoadingTitle, { title, loading }), ), actions ? h("div", { className: "panel-actions" }, actions) : null, ), @@ -556,7 +557,15 @@ export function MetNonlinearPage({ microservices, onRaw, apiBaseUrl = "/api" }: } function renderDetailPanel() { - if (detail.loading) return h("section", { className: "met-detail-panel", "data-testid": "met-detail-panel" }, h(EmptyState, { title: "详情加载中", text: detail.title || "正在读取 D601 data/ 和 config.json" })); + if (detail.loading) return h("section", { className: "met-detail-panel", "data-testid": "met-detail-panel" }, + h("div", { className: "panel-head compact" }, + h("div", null, + h("p", { className: "panel-eyebrow" }, "Detail Loading"), + h(LoadingTitle, { title: "详情加载中", loading: true }), + ), + ), + h(EmptyState, { title: "详情加载中", text: detail.title || "正在读取 D601 data/ 和 config.json" }), + ); if (detail.error) return h("section", { className: "met-detail-panel", "data-testid": "met-detail-panel" }, h(UniDeskErrorBanner, { error: detail.error, wide: true })); if (!detail.data) return h("section", { className: "met-detail-panel muted", "data-testid": "met-detail-panel" }, h(EmptyState, { title: "选择一个项目或任务查看详情", text: "项目库、当前队列、已完成和失败诊断中的行都可以点击;默认只展示结构化字段,原始 JSON 需显式点击按钮。" })); const payload = detailPayload(detail); @@ -575,7 +584,7 @@ export function MetNonlinearPage({ microservices, onRaw, apiBaseUrl = "/api" }: h("div", { className: "panel-head compact" }, h("div", null, h("p", { className: "panel-eyebrow" }, detail.kind === "job" ? "Job + Project Detail" : "Project Library Detail"), - h("h2", null, title), + h(LoadingTitle, { title }), h("code", null, payload.projectPath || job.projectPath || detail.title), ), h("div", { className: "panel-actions" }, h(RawButton, { title: `MET ${title}`, data: detail.data, onOpen: onRaw, testId: "raw-met-detail" })), @@ -649,6 +658,7 @@ export function MetNonlinearPage({ microservices, onRaw, apiBaseUrl = "/api" }: h(Panel, { title: "MET Nonlinear 训练编排", eyebrow: "D601 GPU 用户服务", + loading: state.loading || state.actionBusy, actions: h("div", { className: "panel-actions" }, h("button", { type: "button", className: "ghost-btn", onClick: load, disabled: state.loading, "data-testid": "met-refresh-button" }, state.loading ? "刷新中" : "刷新"), h(RawButton, { title: "MET Nonlinear 用户服务", data: service, onOpen: onRaw, testId: "raw-met-service" }), @@ -678,7 +688,7 @@ export function MetNonlinearPage({ microservices, onRaw, apiBaseUrl = "/api" }: ui.actionMessage ? h("div", { className: "met-action-log", "data-testid": "met-action-message" }, ui.actionMessage) : null, ), h("div", { className: "met-grid" }, - h(Panel, { title: "核心状态", eyebrow: state.refreshedAt ? `Updated ${fmtClock(state.refreshedAt)}` : "Queue + GPU" }, + h(Panel, { title: "核心状态", eyebrow: state.refreshedAt ? `Updated ${fmtClock(state.refreshedAt)}` : "Queue + GPU", loading: state.loading }, h("div", { className: "metric-grid" }, h(MetricCard, { label: "Staged", value: counts.staged ?? 0, hint: "加入队列未开始", tone: Number(counts.staged || 0) > 0 ? "warn" : "" }), h(MetricCard, { label: "Queued", value: counts.queued ?? 0, hint: "排队等待调度", tone: Number(counts.queued || 0) > 0 ? "warn" : "" }), @@ -690,7 +700,7 @@ export function MetNonlinearPage({ microservices, onRaw, apiBaseUrl = "/api" }: h(MetricCard, { label: "Health", value: state.health?.ok ? "OK" : "--", hint: "D601 /health" }), ), ), - h(Panel, { title: "队列控制", eyebrow: "Downloader-like staging" }, + h(Panel, { title: "队列控制", eyebrow: "Downloader-like staging", loading: state.actionBusy }, h("div", { className: "met-control-strip" }, h("label", null, "最大并发", h("input", { type: "number", min: 1, max: 16, value: ui.maxConcurrency, "data-testid": "met-max-concurrency-input", onChange: (event: any) => patchUi({ maxConcurrency: event.target.value }) })), h("label", null, "目标 GPU", h("input", { value: ui.targetGpuName, "data-testid": "met-target-gpu-input", onChange: (event: any) => patchUi({ targetGpuName: event.target.value }) })), @@ -719,7 +729,7 @@ export function MetNonlinearPage({ microservices, onRaw, apiBaseUrl = "/api" }: h("p", { className: "muted paragraph" }, "Fork 只创建新 Project 并自动勾选,不会直接训练;需要在右侧确认后加入待启动队列。"), ), h("div", { className: "met-project-list" }, - h("div", { className: "panel-head compact" }, h("div", null, h("p", { className: "panel-eyebrow" }, `Existing Projects · ${(state.projects?.roots || []).map((root: any) => `${root.root} ${root.count}`).join(" / ")}`), h("h2", null, "选择已有 Project")), h("button", { type: "button", className: "ghost-btn", onClick: stageSelectedProjects, disabled: state.actionBusy || selectedProjectPaths().length === 0, "data-testid": "met-stage-selected-button" }, `加入待启动队列 (${selectedProjectPaths().length})`)), + h("div", { className: "panel-head compact" }, h("div", null, h("p", { className: "panel-eyebrow" }, `Existing Projects · ${(state.projects?.roots || []).map((root: any) => `${root.root} ${root.count}`).join(" / ")}`), h(LoadingTitle, { title: "选择已有 Project", loading: state.loading || state.actionBusy })), h("button", { type: "button", className: "ghost-btn", onClick: stageSelectedProjects, disabled: state.actionBusy || selectedProjectPaths().length === 0, "data-testid": "met-stage-selected-button" }, `加入待启动队列 (${selectedProjectPaths().length})`)), projects.length === 0 ? h(EmptyState, { title: "暂无 project", text: "等待 D601 返回 /api/projects" }) : h("div", { className: "met-project-table", "data-testid": "met-project-tree" }, h("div", { className: "met-tree-header" }, h("span", null, "文件树 Project"), h("span", null, "Model"), h("span", null, "Epochs"), h("span", null, "Progress"), h("span", null, "速度")), diff --git a/src/components/frontend/src/navigation.ts b/src/components/frontend/src/navigation.ts index 91b85bc7..c70decdd 100644 --- a/src/components/frontend/src/navigation.ts +++ b/src/components/frontend/src/navigation.ts @@ -66,7 +66,9 @@ export const MODULES: UniDeskModuleDefinition[] = [ { id: "pipeline", label: "Pipeline" }, { id: "met-nonlinear", label: "MET Nonlinear" }, { id: "claudeqq", label: "ClaudeQQ" }, - { id: "codex-queue", label: "Codex Queue" }, + { id: "baidu-netdisk", label: "Baidu Netdisk" }, + { id: "filebrowser", label: "File Browser" }, + { id: "code-queue", label: "Code Queue" }, { id: "project-manager", label: "Project Manager" }, ] }, { id: "config", label: "系统配置", code: "CFG", tabs: [ diff --git a/src/components/frontend/src/pipeline.tsx b/src/components/frontend/src/pipeline.tsx index 6b74886b..ce9d9312 100644 --- a/src/components/frontend/src/pipeline.tsx +++ b/src/components/frontend/src/pipeline.tsx @@ -1,5 +1,6 @@ import React from "react"; import { fmtClock, fmtDate } from "./time"; +import { LoadingTitle } from "./loading-indicator"; import { Background, BaseEdge, Controls, Handle, MarkerType, Position, ReactFlow, type Edge, type Node } from "@xyflow/react"; import { TraceView, opencodeTracePort } from "./trace"; import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error"; @@ -576,7 +577,7 @@ function PipelineOpenCodeTrace({ steps, sessionIds, sessionFacts, matchedStepKey h("div", { className: "pipeline-trace-head" }, h("div", null, h("b", null, "OpenCode Trace"), - h("span", null, "Trace 使用 Codex Queue 统一样式展示完整 agent loop;Pipeline 旧 step/message/tool 卡片样式已废弃。"), + h("span", null, "Trace 使用 Code Queue 统一样式展示完整 agent loop;Pipeline 旧 step/message/tool 卡片样式已废弃。"), ), h("div", { className: "pipeline-trace-session-head", "data-testid": "pipeline-step-timeline-session" }, h("span", null, traceFacts.join(" / ") || "Trace"), @@ -897,7 +898,7 @@ function PipelineGanttDetailPanel({ selection, runDetails, nodeDetails, nodeDeta h("div", { className: "pipeline-gantt-detail-head" }, h("div", null, h("span", { className: "panel-eyebrow" }, "Gantt Detail"), - h("h3", null, "未选择元素"), + h(LoadingTitle, { title: "未选择元素", level: 3 }), ), h("button", { type: "button", className: "ghost-btn mini", onClick: onCollapse, "data-testid": "pipeline-gantt-sidebar-collapse" }, "收起"), ), @@ -952,7 +953,7 @@ function PipelineGanttDetailPanel({ selection, runDetails, nodeDetails, nodeDeta h("div", { className: "pipeline-gantt-detail-head" }, h("div", null, h("span", { className: "panel-eyebrow" }, selection?.mode === "event" ? "Gantt Event Detail" : "Gantt Line Detail"), - h("h3", null, detailTitle), + h(LoadingTitle, { title: detailTitle, level: 3, loading }), ), h("div", { className: "pipeline-gantt-detail-head-actions" }, h(StatusBadge, { status }, status), @@ -1034,12 +1035,12 @@ function MetricCard({ label, value, hint, tone }: AnyRecord) { ); } -function Panel({ title, eyebrow, actions, children, className }: AnyRecord) { +function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) { return h("section", { className: `panel ${className || ""}` }, h("div", { className: "panel-head" }, h("div", null, eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null, - h("h2", null, title), + h(LoadingTitle, { title, loading }), ), actions ? h("div", { className: "panel-actions" }, actions) : null, ), @@ -3046,6 +3047,7 @@ function PipelineEpochGantt({ epochs, activeRun, activePipeline, pipelineNodes, title: "Epoch 甘特图", eyebrow: `${activePipeline?.id || "pipeline"} / ${epochs.length} epochs`, className: "pipeline-wide-panel", + loading: runDetails?.loading, actions: h("div", { className: "pipeline-gantt-actions" }, h("select", { value: activeRunId, @@ -3308,7 +3310,7 @@ function PipelineNodeControlPanel({ activeRun, pipelineRuns, selectedRunId, onRu h("div", { className: "pipeline-node-control-head" }, h("div", null, h("p", { className: "panel-eyebrow" }, "Manual Node Control"), - h("h3", null, selectedNodeId || "点击控制图中的 node"), + h(LoadingTitle, { title: selectedNodeId || "点击控制图中的 node", level: 3, loading: control.loading || Boolean(control.actionLoading) }), ), h("div", { className: "pipeline-node-control-head-actions" }, selectedNodeId ? h(StatusBadge, { status }, status) : h(StatusBadge, { status: "pending" }, "idle"), @@ -3773,6 +3775,7 @@ export function PipelinePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR h(Panel, { title: "Pipeline v2 工作台", eyebrow: "D601 Snapshot 用户服务", + loading: state.loading, actions: h("div", { className: "panel-actions" }, h("button", { type: "button", className: "ghost-btn", onClick: load, disabled: state.loading, "data-testid": "pipeline-refresh-button" }, state.loading ? "刷新中" : "刷新"), h(RawButton, { title: "Pipeline 用户服务", data: service, onOpen: onRaw, testId: "raw-pipeline-service" }), @@ -3805,6 +3808,7 @@ export function PipelinePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR title: "控制图", eyebrow: `${activePipeline.id || "pipeline"} / run ${activeRun?.status || "--"}`, className: "pipeline-wide-panel", + loading: state.loading, actions: h("div", { className: "pipeline-toolbar" }, h("select", { value: activePipelineId, @@ -3944,7 +3948,7 @@ export function PipelinePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR }, onRaw, }), - h(Panel, { title: "观测指标", eyebrow: state.refreshedAt ? `Updated ${fmtClock(state.refreshedAt)}` : "Snapshot" }, + h(Panel, { title: "观测指标", eyebrow: state.refreshedAt ? `Updated ${fmtClock(state.refreshedAt)}` : "Snapshot", loading: state.loading }, h("div", { className: "metric-grid" }, h(MetricCard, { label: "Health", value: state.health?.ok ? "OK" : "--", hint: state.health?.service || "D601 /health", tone: state.health?.ok ? "ok" : "warn" }), h(MetricCard, { label: "组件", value: componentCount, hint: "components registry", tone: snapshot?.registry?.ok === false ? "warn" : "ok" }), @@ -3956,16 +3960,16 @@ export function PipelinePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR ), h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "Pipeline Snapshot", data: snapshot, onOpen: onRaw, testId: "raw-pipeline-snapshot" })), ), - h(Panel, { title: "评分器", eyebrow: activeRun?.runId || "selected epoch" }, + h(Panel, { title: "评分器", eyebrow: activeRun?.runId || "selected epoch", loading: state.loading }, h(PipelineScoreBoard, { run: activeRun, onRaw }), ), - h(Panel, { title: "MiniMax 限额", eyebrow: "model/minimax-m27 quota" }, + h(Panel, { title: "MiniMax 限额", eyebrow: "model/minimax-m27 quota", loading: state.loading }, h(PipelineMinimaxQuotaPanel, { quota: minimaxQuota, onRaw }), ), - h(Panel, { title: "OA 事件流", eyebrow: "100% event-driven diagnostics", className: "pipeline-wide-panel" }, + h(Panel, { title: "OA 事件流", eyebrow: "100% event-driven diagnostics", className: "pipeline-wide-panel", loading: state.loading }, h(PipelineOaEventFlowPanel, { diagnostics: oaDiagnostics, onRaw }), ), - h(Panel, { title: "组件矩阵", eyebrow: `${componentClasses.length} classes` }, + h(Panel, { title: "组件矩阵", eyebrow: `${componentClasses.length} classes`, loading: state.loading }, componentClasses.length === 0 ? h(EmptyState, { title: "暂无组件", text: "等待 D601 pipeline backend 返回 registry.components" }) : h("div", { className: "component-strata" }, componentClasses.map((item) => h("article", { key: item.name, className: "component-stratum" }, h("span", null, item.name), @@ -3975,7 +3979,7 @@ export function PipelinePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR components.slice(0, 12).map((component: any) => h("span", { key: component.key, className: "data-chip" }, h("b", null, component.componentClass || "--"), h("span", null, component.id || component.key || "--"))), ), ), - h(Panel, { title: "Epoch 列表", eyebrow: `${pipelineRuns.length}/${runCount} preview` }, + h(Panel, { title: "Epoch 列表", eyebrow: `${pipelineRuns.length}/${runCount} preview`, loading: state.loading }, pipelineRuns.length === 0 ? h(EmptyState, { title: "暂无运行记录", text: "当前 pipeline 在 .state/pipeline-runs 中还没有 epoch。" }) : h("div", { className: "pipeline-run-list" }, pipelineRuns.map((run: any) => { const cardRun = String(run?.runId || "") === activeRunId ? activeRun : run; @@ -4008,7 +4012,7 @@ export function PipelinePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR ); })), ), - h(Panel, { title: "运行材料索引", eyebrow: activeRun?.runId || "selected epoch", className: "pipeline-wide-panel" }, + h(Panel, { title: "运行材料索引", eyebrow: activeRun?.runId || "selected epoch", className: "pipeline-wide-panel", loading: state.loading }, h(PipelineRunMaterialIndex, { activeRun, onRaw }), ), ), diff --git a/src/components/frontend/src/project-manager.tsx b/src/components/frontend/src/project-manager.tsx index 55448822..acb2ad94 100644 --- a/src/components/frontend/src/project-manager.tsx +++ b/src/components/frontend/src/project-manager.tsx @@ -1,5 +1,6 @@ import React from "react"; import { beijingDateStamp, fmtClock, fmtDate } from "./time"; +import { LoadingTitle } from "./loading-indicator"; import { errorMessage, requestBlob, requestJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; @@ -34,12 +35,12 @@ function MetricCard({ label, value, hint, tone }: AnyRecord) { ); } -function Panel({ title, eyebrow, actions, children, className }: AnyRecord) { +function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) { return h("section", { className: `panel ${className || ""}` }, h("div", { className: "panel-head" }, h("div", null, eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null, - h("h2", null, title), + h(LoadingTitle, { title, loading }), ), actions ? h("div", { className: "panel-actions" }, actions) : null, ), @@ -245,6 +246,7 @@ export function ProjectManagerPage({ microservices, onRaw, apiBaseUrl = "/api" } h(Panel, { title: "项目管理工作台", eyebrow: "Main Server PostgreSQL 用户服务", + loading: state.loading || state.exporting, actions: h("div", { className: "panel-actions" }, h("button", { type: "button", className: "ghost-btn", disabled: state.loading, onClick: () => load(), "data-testid": "project-manager-refresh-button" }, state.loading ? "刷新中" : "刷新"), h("button", { type: "button", className: "ghost-btn", disabled: state.exporting, onClick: exportExcel, "data-testid": "project-manager-export-button" }, state.exporting ? "导出中" : "导出 Excel"), @@ -270,6 +272,7 @@ export function ProjectManagerPage({ microservices, onRaw, apiBaseUrl = "/api" } h(Panel, { title: "项目清单", eyebrow: "CRUD + Excel Export", + loading: state.loading || state.importing || state.exporting, actions: h("div", { className: "inline-actions project-manager-filters" }, h("input", { value: query, onChange: (event: any) => setQuery(event.target.value), placeholder: "搜索合同号 / 项目名称 / 状态", "data-testid": "project-manager-search" }), h("select", { value: status, onChange: (event: any) => { setStatus(event.target.value); load(query, event.target.value); }, "data-testid": "project-manager-status-filter" }, @@ -281,7 +284,7 @@ export function ProjectManagerPage({ microservices, onRaw, apiBaseUrl = "/api" } h("button", { type: "button", className: "ghost-btn", onClick: () => load(query, status) }, "筛选"), ), }, h(ProjectTable, { projects, activeId: form.id, onSelect: (project: AnyRecord) => setForm(projectToForm(project)), onRaw })), - h(Panel, { title: form.id ? "编辑项目" : "新建项目", eyebrow: "PostgreSQL Write Path" }, + h(Panel, { title: form.id ? "编辑项目" : "新建项目", eyebrow: "PostgreSQL Write Path", loading: state.saving || state.importing }, h("form", { className: "stack-form project-manager-form", onSubmit: submitProject, "data-testid": "project-manager-form" }, form.id ? h("label", null, "项目 ID", h("input", { value: form.id, disabled: true })) : null, h("label", null, "序号", h("input", { type: "number", value: form.sequenceNo, onChange: (event: any) => setForm((prev: any) => ({ ...prev, sequenceNo: event.target.value })) })), diff --git a/src/components/frontend/src/todo-note.tsx b/src/components/frontend/src/todo-note.tsx index 56ab7d19..13e54a59 100644 --- a/src/components/frontend/src/todo-note.tsx +++ b/src/components/frontend/src/todo-note.tsx @@ -1,5 +1,6 @@ import React from "react"; import { fmtClock, fmtDate, fmtDateTimeLocalInput, localDateTimeInputToBeijingIso } from "./time"; +import { LoadingTitle } from "./loading-indicator"; import { errorMessage, requestJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; @@ -23,12 +24,12 @@ function MetricCard({ label, value, hint, tone }: AnyRecord) { ); } -function Panel({ title, eyebrow, actions, children, className }: AnyRecord) { +function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) { return h("section", { className: `panel ${className || ""}` }, h("div", { className: "panel-head" }, h("div", null, eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null, - h("h2", null, title), + h(LoadingTitle, { title, loading }), ), actions ? h("div", { className: "panel-actions" }, actions) : null, ), @@ -96,6 +97,15 @@ function todoRegistryRows(registry: any): any[] { return Array.isArray(registry?.instances) ? registry.instances : []; } +function childTodosForParent(nodes: any[], parentId: string): any[] { + for (const node of nodes) { + if (node?.id === parentId) return Array.isArray(node.children) ? node.children : []; + const nested = childTodosForParent(Array.isArray(node?.children) ? node.children : [], parentId); + if (nested.length > 0) return nested; + } + return []; +} + export function TodoNotePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyRecord) { const service = microservices.find((item: any) => item.id === "todo-note") || null; const [health, setHealth] = useState(null); @@ -156,8 +166,8 @@ export function TodoNotePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR } } - async function applyTodoAction(action: AnyRecord): Promise { - if (!activeId) return; + async function applyTodoAction(action: AnyRecord): Promise { + if (!activeId) return null; setError(""); try { const updated = await requestJson(todoApi(apiBaseUrl, `/api/instances/${encodeURIComponent(activeId)}/actions`), { @@ -166,8 +176,10 @@ export function TodoNotePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR }); setInstance(updated); await loadRegistry(activeId); + return updated; } catch (err) { setError(errorMessage(err, "Todo 操作失败")); + return null; } } @@ -236,7 +248,19 @@ export function TodoNotePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR async function addChild(todoId: string): Promise { const title = window.prompt("新增子任务标题"); - if (title && title.trim()) await applyTodoAction({ type: "addTodo", title: title.trim(), parentId: todoId }); + const trimmed = title?.trim(); + if (!trimmed) return; + + const beforeChildren = childTodosForParent(Array.isArray(instance?.todos) ? instance.todos : [], todoId); + const beforeIds = new Set(beforeChildren.map((child: any) => child.id)); + const updated = await applyTodoAction({ type: "addTodo", title: trimmed, parentId: todoId, targetIndex: 0 }); + if (!updated) return; + + const afterChildren = childTodosForParent(Array.isArray(updated?.todos) ? updated.todos : [], todoId); + const newChild = afterChildren.find((child: any) => !beforeIds.has(child.id)); + if (newChild && afterChildren[0]?.id !== newChild.id) { + await applyTodoAction({ type: "moveTodo", todoId: newChild.id, targetParentId: todoId, targetIndex: 0 }); + } } async function dropTodo(targetParentId: string | null, targetIndex: number): Promise { @@ -254,10 +278,15 @@ export function TodoNotePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR if (!service) return h(EmptyState, { title: "Todo Note 未登记", text: "请在 config.json 的 microservices 中登记用户服务 id=todo-note" }); const activeSummary = rows.find((row: any) => row.id === activeId) || null; + const rootTodos = Array.isArray(instance?.todos) ? instance.todos : []; + const visibleRootTodos = rootTodos + .map((todo: any, index: number) => ({ todo, index })) + .filter((item: AnyRecord) => todoVisible(item.todo, filter)); return h("div", { className: "todo-note-page", "data-testid": "todo-note-page" }, h(Panel, { title: "Todo Note 工作台", eyebrow: "Main Server 用户服务", + loading, actions: h("div", { className: "panel-actions" }, h("button", { type: "button", className: "ghost-btn", disabled: loading, onClick: () => refresh(activeId), "data-testid": "todo-note-refresh-button" }, loading ? "刷新中" : "刷新"), h(RawButton, { title: "Todo Note 用户服务", data: service, onOpen: onRaw, testId: "raw-todo-note-service" }), @@ -279,7 +308,7 @@ export function TodoNotePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR h(UniDeskErrorBanner, { error, wide: true }), ), h("div", { className: "todo-note-layout" }, - h(Panel, { title: "清单", eyebrow: `${rows.length} Instances`, className: "todo-list-panel" }, + h(Panel, { title: "清单", eyebrow: `${rows.length} Instances`, className: "todo-list-panel", loading }, h("form", { className: "todo-create-list", onSubmit: createList }, h("input", { placeholder: "新清单名称", value: newListName, onChange: (event: any) => setNewListName(event.target.value), "aria-label": "新清单名称" }), h("button", { type: "submit", className: "ghost-btn", disabled: loading || !newListName.trim() }, "创建"), @@ -301,6 +330,7 @@ export function TodoNotePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR h(Panel, { title: activeSummary?.name || "待选择清单", eyebrow: refreshedAt ? `Updated ${fmtClock(refreshedAt)}` : "Todo Tree", + loading, actions: instance ? h("div", { className: "panel-actions" }, h("button", { type: "button", className: "ghost-btn", onClick: () => applyTodoAction({ type: "renameInstance", name: window.prompt("清单新名称", instance.name) || instance.name }) }, "重命名"), h("button", { type: "button", className: "ghost-btn danger", onClick: () => deleteList(activeId) }, "删除清单"), @@ -337,18 +367,18 @@ export function TodoNotePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR h("div", { className: "todo-root-drop", onDragOver: (event: any) => event.preventDefault(), - onDrop: (event: any) => { event.preventDefault(); dropTodo(null, (instance.todos || []).length); }, + onDrop: (event: any) => { event.preventDefault(); dropTodo(null, rootTodos.length); }, }, "拖到这里可移为根任务末尾"), h("div", { className: "todo-tree", "data-testid": "todo-note-tree" }, - (instance.todos || []).filter((todo: any) => todoVisible(todo, filter)).length === 0 + visibleRootTodos.length === 0 ? h(EmptyState, { title: "没有匹配任务", text: "调整筛选或新增任务" }) - : (instance.todos || []).filter((todo: any) => todoVisible(todo, filter)).map((todo: any, index: number) => h(TodoRow, { + : visibleRootTodos.map(({ todo, index }: AnyRecord) => h(TodoRow, { key: todo.id, todo, depth: 0, parentId: null, index, - siblingCount: instance.todos.length, + siblingCount: rootTodos.length, filter, editingId, editingTitle, @@ -372,7 +402,9 @@ export function TodoNotePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR function TodoRow(props: AnyRecord): ReactNode { const { todo, depth, parentId, index, siblingCount, filter, editingId, editingTitle, setEditingTitle, beginEdit, saveEdit, applyTodoAction, addChild, dragTodoId, setDragTodoId, dropTodo } = props; const children = Array.isArray(todo.children) ? todo.children : []; - const visibleChildren = children.filter((child: any) => todoVisible(child, filter)); + const visibleChildren = children + .map((child: any, childIndex: number) => ({ child, childIndex })) + .filter((item: AnyRecord) => todoVisible(item.child, filter)); const isEditing = editingId === todo.id; const targetParentId = parentId || null; return h("div", { className: "todo-row-wrap" }, @@ -411,12 +443,13 @@ function TodoRow(props: AnyRecord): ReactNode { h("button", { type: "button", className: "ghost-btn", onClick: () => beginEdit(todo) }, "编辑"), h("button", { type: "button", className: "ghost-btn", onClick: () => addChild(todo.id) }, "子项"), h("button", { type: "button", className: "ghost-btn", disabled: index <= 0, onClick: () => applyTodoAction({ type: "moveTodo", todoId: todo.id, ...(targetParentId ? { targetParentId } : {}), targetIndex: index - 1 }) }, "上移"), + h("button", { type: "button", className: "ghost-btn", disabled: index <= 0, onClick: () => applyTodoAction({ type: "moveTodo", todoId: todo.id, ...(targetParentId ? { targetParentId } : {}), targetIndex: 0 }) }, "置顶"), h("button", { type: "button", className: "ghost-btn", disabled: index >= siblingCount - 1, onClick: () => applyTodoAction({ type: "moveTodo", todoId: todo.id, ...(targetParentId ? { targetParentId } : {}), targetIndex: index + 1 }) }, "下移"), h("button", { type: "button", className: "ghost-btn", disabled: !parentId, onClick: () => applyTodoAction({ type: "moveTodo", todoId: todo.id, targetIndex: 9999 }) }, "提升"), h("button", { type: "button", className: "ghost-btn danger", onClick: () => applyTodoAction({ type: "deleteTodo", todoId: todo.id }) }, "删除"), ), ), - todo.expanded && visibleChildren.length > 0 ? h("div", { className: "todo-children" }, visibleChildren.map((child: any, childIndex: number) => h(TodoRow, { + todo.expanded && visibleChildren.length > 0 ? h("div", { className: "todo-children" }, visibleChildren.map(({ child, childIndex }: AnyRecord) => h(TodoRow, { key: child.id, todo: child, depth: depth + 1, diff --git a/src/components/frontend/src/trace.tsx b/src/components/frontend/src/trace.tsx index 4de1f5a5..9a965173 100644 --- a/src/components/frontend/src/trace.tsx +++ b/src/components/frontend/src/trace.tsx @@ -509,10 +509,13 @@ export function codexTranscriptToTrace(transcript: any[]): TraceItem[] { } function opencodePartDurationMs(part: any): number | undefined { + const start = finiteMs(part?.state?.time?.start) ?? finiteMs(part?.time?.start); + const end = finiteMs(part?.state?.time?.end) ?? finiteMs(part?.time?.end); return finiteMs(part?.durationMs) ?? finiteMs(part?.elapsedMs) ?? finiteMs(part?.timing?.durationMs) ?? finiteMs(part?.metadata?.durationMs) + ?? (start !== null && end !== null && end >= start ? end - start : null) ?? undefined; } @@ -526,6 +529,15 @@ function opencodePartRawSeq(part: any, fallback: any): any { function partFieldValue(part: any, keys: string[]): string { const normalized = new Set(keys.map((key) => key.toLowerCase())); + const records = [part?.state?.input, part?.input, part?.state?.metadata, part?.metadata, part] + .filter((item) => item && typeof item === "object" && !Array.isArray(item)); + for (const record of records) { + for (const [key, value] of Object.entries(record)) { + if (!normalized.has(key.toLowerCase())) continue; + if (typeof value === "string") return value; + if (value !== undefined && value !== null) return shortText(JSON.stringify(value), 900); + } + } for (const field of Array.isArray(part?.inputFields) ? part.inputFields : []) { const key = String(field?.key || "").toLowerCase(); if (normalized.has(key)) return String(field?.value || ""); @@ -535,18 +547,85 @@ function partFieldValue(part: any, keys: string[]): string { function opencodeToolKind(part: any): TraceItemKind { const tool = String(part?.tool || part?.title || "").toLowerCase(); - if (/read|grep|glob|list|ls|find|search|view|cat|sed|rg/u.test(tool)) return "explored"; - if (/edit|write|patch|apply|update|create|delete/u.test(tool)) return "edited"; const command = partFieldValue(part, ["command", "cmd"]); + const normalized = `${tool} ${command}`.toLowerCase(); + if (/\b(read|grep|glob|list|ls|find|search|view|cat|sed|rg)\b/u.test(normalized)) return "explored"; + if (/\b(edit|write|patch|apply|update|create|delete|apply_patch|git apply|sed -i)\b/u.test(normalized)) return "edited"; if (/\b(rg|grep|find|ls|cat|sed|tail|head|git status|git diff|ps)\b/u.test(command)) return "explored"; if (/\b(apply_patch|git apply|cat >|tee .*<<|sed -i|python3? .*write_text)\b/u.test(command)) return "edited"; return "ran"; } +function opencodePreviewValue(value: any, max = 1200): string { + if (typeof value === "string") return value; + if (value === undefined || value === null) return ""; + try { + return shortText(JSON.stringify(value), max); + } catch { + return shortText(String(value), max); + } +} + +function opencodeRawEventToTrace(event: any, fallbackSeq: number): TraceItem | null { + const part = event?.part && typeof event.part === "object" && !Array.isArray(event.part) ? event.part : {}; + const type = String(event?.type || event?.event || event?.name || part?.type || "").toLowerCase(); + const partType = String(part?.type || "").toLowerCase(); + const at = event?.at || event?.timestamp || part?.updatedAt || part?.createdAt; + const seq = Number.isFinite(Number(event?.seq)) ? Number(event.seq) : fallbackSeq; + if (type === "step_start" || type === "step-start" || partType === "step-start") { + return { seq, at, kind: "system", title: "OpenCode step started", status: "opencode/step-start", bodyPreview: `session=${event?.sessionID || part?.sessionID || "unknown"}`, rawSeqs: [part?.id || event?.sessionID || seq] }; + } + if (type === "step_finish" || type === "step-finish" || partType === "step-finish") { + return { seq, at, kind: "system", title: "OpenCode step finished", status: "opencode/step-finish", bodyPreview: `reason=${part?.reason || event?.reason || "finished"}`, rawSeqs: [part?.id || event?.sessionID || seq] }; + } + if (partType === "tool" || /tool|bash|command/iu.test(`${type} ${partType}`)) { + const state = part?.state && typeof part.state === "object" && !Array.isArray(part.state) ? part.state : {}; + const command = partFieldValue(part, ["command", "cmd"]) || partFieldValue(part, ["filePath", "filepath", "path"]) || String(part?.tool || part?.title || "tool"); + const output = opencodePreviewValue(state?.output ?? state?.result ?? part?.output ?? event?.output ?? state?.metadata?.output, 3000); + return { + seq, + at: opencodePartCompletedAt(part, at), + kind: opencodeToolKind(part), + title: String(state?.title || part?.title || state?.metadata?.description || part?.tool || "OpenCode tool"), + status: String(state?.status || part?.status || event?.status || ""), + commandPreview: command, + bodyPreview: output || opencodePreviewValue(event, 3000), + durationMs: opencodePartDurationMs(part), + rawSeqs: [part?.id || part?.callID || event?.sessionID || seq], + }; + } + const text = opencodePreviewValue(part?.text ?? part?.content ?? part?.delta ?? event?.text ?? event?.content ?? event?.delta, 3000).trim(); + if (text.length > 0) { + return { + seq, + at: opencodePartCompletedAt(part, at), + kind: partType === "reasoning" ? "message" : /error|failed/iu.test(`${type} ${partType}`) ? "error" : "message", + title: partType === "reasoning" ? "Reasoning" : /error|failed/iu.test(`${type} ${partType}`) ? "OpenCode error" : "Assistant message", + status: `opencode/${type || partType || "event"}`, + bodyPreview: text, + durationMs: opencodePartDurationMs(part), + rawSeqs: [part?.id || event?.sessionID || seq], + }; + } + return null; +} + export function opencodeStepsToTrace(steps: any[]): TraceItem[] { const rows: TraceItem[] = []; let seq = 1; for (const step of Array.isArray(steps) ? steps : []) { + if (step?.kind && step?.title) { + rows.push({ ...step, seq: Number.isFinite(Number(step?.seq)) ? Number(step.seq) : seq++ }); + continue; + } + if (step?.part || step?.sessionID || String(step?.type || "").startsWith("step_") || String(step?.type || "").includes("tool")) { + const item = opencodeRawEventToTrace(step, seq); + if (item !== null) { + rows.push(item); + seq = Math.max(seq + 1, Number(item.seq) + 1); + } + continue; + } const at = step?.createdAt || step?.updatedAt || step?.completedAt; const role = String(step?.role || "assistant").toLowerCase(); const parts = Array.isArray(step?.parts) ? step.parts : []; diff --git a/src/components/frontend/tsconfig.json b/src/components/frontend/tsconfig.json index 28db01ce..8c12c1ba 100644 --- a/src/components/frontend/tsconfig.json +++ b/src/components/frontend/tsconfig.json @@ -14,5 +14,6 @@ "outDir": "dist", "skipLibCheck": true }, - "include": ["src/**/*.ts", "src/**/*.tsx"] + "include": ["src/**/*.ts", "src/**/*.tsx"], + "references": [{ "path": "../shared" }] } diff --git a/src/components/microservices/baidu-netdisk/Dockerfile b/src/components/microservices/baidu-netdisk/Dockerfile new file mode 100644 index 00000000..d05feb5d --- /dev/null +++ b/src/components/microservices/baidu-netdisk/Dockerfile @@ -0,0 +1,11 @@ +FROM oven/bun:1-alpine + +WORKDIR /app/src/components/microservices/baidu-netdisk +COPY src/components/microservices/baidu-netdisk/package.json ./package.json +RUN bun install --production +COPY src/components/microservices/baidu-netdisk/tsconfig.json ./tsconfig.json +COPY src/components/shared /app/src/components/shared +COPY src/components/microservices/baidu-netdisk/src ./src + +EXPOSE 4244 +CMD ["bun", "run", "src/index.ts"] diff --git a/src/components/microservices/baidu-netdisk/bun.lock b/src/components/microservices/baidu-netdisk/bun.lock new file mode 100644 index 00000000..7659f97b --- /dev/null +++ b/src/components/microservices/baidu-netdisk/bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@unidesk/baidu-netdisk", + "dependencies": { + "postgres": "latest", + }, + }, + }, + "packages": { + "postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="], + } +} diff --git a/src/components/microservices/baidu-netdisk/package.json b/src/components/microservices/baidu-netdisk/package.json new file mode 100644 index 00000000..aa8f880c --- /dev/null +++ b/src/components/microservices/baidu-netdisk/package.json @@ -0,0 +1,12 @@ +{ + "name": "@unidesk/baidu-netdisk", + "private": true, + "type": "module", + "scripts": { + "start": "bun run src/index.ts", + "check": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "postgres": "latest" + } +} diff --git a/src/components/microservices/baidu-netdisk/src/index.ts b/src/components/microservices/baidu-netdisk/src/index.ts new file mode 100644 index 00000000..354e1dfb --- /dev/null +++ b/src/components/microservices/baidu-netdisk/src/index.ts @@ -0,0 +1,1385 @@ +import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto"; +import { createWriteStream, mkdirSync } from "node:fs"; +import { open, readFile, rename, stat, writeFile } from "node:fs/promises"; +import { basename, dirname, relative, resolve, sep } from "node:path"; +import pathPosix from "node:path/posix"; +import postgres from "postgres"; +import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../../shared/src/rotating-jsonl"; + +type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; +type JsonRecord = Record; + +type TransferDirection = "upload" | "download"; +type TransferStatus = "queued" | "running" | "succeeded" | "failed" | "canceled"; + +interface RuntimeConfig { + host: string; + port: number; + databaseUrl: string; + logFile: string; + clientId: string; + clientSecret: string; + tokenKey: string; + appRoot: string; + stagingDir: string; + partSizeBytes: number; + userAgent: string; +} + +interface AuthSessionRow { + id: string; + user_code: string; + verification_url: string; + qrcode_url: string; + expires_at: string | Date; + poll_interval_seconds: number; + status: string; + error: string | null; + last_poll_at: string | Date | null; + poll_count: number; + token_account_id: string | null; + created_at: string | Date; + updated_at: string | Date; +} + +interface TokenRow { + account_id: string; + access_token_ciphertext: string; + refresh_token_ciphertext: string; + expires_at: string | Date; + scope: string; + generation: number; +} + +interface TransferJobRow { + id: string; + account_id: string | null; + direction: TransferDirection; + status: TransferStatus; + local_path: string; + remote_path: string; + fs_id: string | null; + size_bytes: string | number | null; + bytes_done: string | number | null; + part_size: number | null; + block_list_json: JsonValue; + uploadid: string | null; + retry_count: number; + error: string | null; + result: JsonValue; + created_at: string | Date; + updated_at: string | Date; +} + +class HttpError extends Error { + readonly status: number; + readonly detail: JsonRecord; + + constructor(status: number, message: string, detail: JsonRecord = {}) { + super(message); + this.name = "HttpError"; + this.status = status; + this.detail = detail; + } +} + +class BaiduApiError extends Error { + readonly status: number; + readonly body: JsonValue; + + constructor(message: string, status: number, body: JsonValue) { + super(message); + this.name = "BaiduApiError"; + this.status = status; + this.body = body; + } +} + +const serviceStartedAt = new Date().toISOString(); +const recentLogs: JsonRecord[] = []; + +function normalizedAppRoot(value: string): string { + const root = pathPosix.normalize(`/${String(value || "").replace(/^\/+/, "")}`); + if (!root.startsWith("/apps/")) return "/apps/UniDeskBaiduNetdisk"; + return root.replace(/\/+$/u, "") || "/apps/UniDeskBaiduNetdisk"; +} + +function configFromEnv(): RuntimeConfig { + const databaseUrl = process.env.DATABASE_URL || ""; + if (!databaseUrl) throw new Error("DATABASE_URL is required"); + const partSizeBytes = Number(process.env.BAIDU_NETDISK_PART_SIZE_BYTES || 4 * 1024 * 1024); + return { + host: process.env.HOST || "0.0.0.0", + port: Number(process.env.PORT || 4244), + databaseUrl, + logFile: process.env.LOG_FILE || "", + clientId: process.env.BAIDU_NETDISK_CLIENT_ID || process.env.BAIDU_NETDISK_APP_KEY || "", + clientSecret: process.env.BAIDU_NETDISK_CLIENT_SECRET || process.env.BAIDU_NETDISK_SECRET_KEY || "", + tokenKey: process.env.BAIDU_NETDISK_TOKEN_KEY || "", + appRoot: normalizedAppRoot(process.env.BAIDU_NETDISK_APP_ROOT || "/apps/UniDeskBaiduNetdisk"), + stagingDir: resolve(process.env.BAIDU_NETDISK_STAGING_DIR || "/data/staging"), + partSizeBytes: Number.isFinite(partSizeBytes) && partSizeBytes > 0 ? partSizeBytes : 4 * 1024 * 1024, + userAgent: process.env.BAIDU_NETDISK_USER_AGENT || "pan.baidu.com", + }; +} + +const config = configFromEnv(); +const sql = postgres(config.databaseUrl, { max: 8, idle_timeout: 20, connect_timeout: 10 }); +const logWriter = config.logFile + ? createHourlyJsonlWriter({ + baseLogFile: config.logFile, + service: "baidu-netdisk", + maxBytes: logRetentionBytesForService("baidu-netdisk"), + }) + : null; +logWriter?.prune(); +let schemaReady = false; +let appRootEnsuredAt = 0; +let appRootEnsurePromise: Promise | null = null; + +function jsonResponse(body: JsonValue, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json; charset=utf-8" }, + }); +} + +function redactString(value: string): string { + if (value.length <= 10) return "[redacted]"; + return `${value.slice(0, 4)}...[redacted]...${value.slice(-4)}`; +} + +function redactJson(value: unknown): JsonValue { + if (value === null || value === undefined) return null; + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value; + if (Array.isArray(value)) return value.slice(0, 50).map((item) => redactJson(item)); + if (typeof value !== "object") return String(value); + const output: JsonRecord = {}; + for (const [key, item] of Object.entries(value as Record)) { + const normalized = key.toLowerCase(); + if (normalized.includes("token") || normalized.includes("secret") || normalized === "dlink" || normalized.includes("ciphertext")) { + output[key] = typeof item === "string" ? redactString(item) : "[redacted]"; + } else { + output[key] = redactJson(item); + } + } + return output; +} + +function errorToJson(error: unknown): JsonRecord { + if (error instanceof BaiduApiError) return { name: error.name, message: error.message, status: error.status, body: redactJson(error.body) }; + if (error instanceof HttpError) return { name: error.name, message: error.message, status: error.status, detail: error.detail }; + if (error instanceof Error) return { name: error.name, message: error.message }; + return { message: String(error) }; +} + +function errorResponse(error: unknown): Response { + const status = error instanceof HttpError ? error.status : 500; + log("request_error", { status, error: errorToJson(error) }); + if (error instanceof HttpError) return jsonResponse({ ok: false, error: error.message, ...error.detail }, status); + return jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, status); +} + +function log(event: string, detail: JsonRecord = {}): void { + const record: JsonRecord = { at: new Date().toISOString(), event, ...redactJson(detail) as JsonRecord }; + recentLogs.push(record); + if (recentLogs.length > 300) recentLogs.shift(); + if (logWriter !== null) { + try { + logWriter.appendJson(record, new Date(String(record.at))); + } catch { + // Logging must not break request handling. + } + } +} + +function iso(value: string | Date | null | undefined): string | null { + if (value === null || value === undefined) return null; + const date = value instanceof Date ? value : new Date(value); + return Number.isNaN(date.getTime()) ? String(value) : date.toISOString(); +} + +function asNumber(value: unknown, fallback = 0): number { + const number = Number(value); + return Number.isFinite(number) ? number : fallback; +} + +function asRecord(value: unknown): JsonRecord { + return typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonRecord : {}; +} + +async function readJsonBody(req: Request): Promise { + const text = await req.text(); + if (!text.trim()) return {}; + try { + const parsed = JSON.parse(text) as unknown; + if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed as JsonRecord; + throw new Error("JSON body must be an object"); + } catch (error) { + throw new HttpError(400, "invalid JSON body", { detail: error instanceof Error ? error.message : String(error) }); + } +} + +function requireBaiduConfigured(): void { + const missing = []; + if (!config.clientId) missing.push("BAIDU_NETDISK_CLIENT_ID"); + if (!config.clientSecret) missing.push("BAIDU_NETDISK_CLIENT_SECRET"); + if (!config.tokenKey) missing.push("BAIDU_NETDISK_TOKEN_KEY"); + if (missing.length > 0) { + throw new HttpError(400, "Baidu Netdisk OAuth is not configured", { missing: missing as JsonValue }); + } +} + +function encryptionKey(): Buffer { + if (!config.tokenKey) throw new HttpError(400, "BAIDU_NETDISK_TOKEN_KEY is required"); + return createHash("sha256").update(config.tokenKey).digest(); +} + +function encryptSecret(value: string): string { + const iv = randomBytes(12); + const cipher = createCipheriv("aes-256-gcm", encryptionKey(), iv); + const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]); + const tag = cipher.getAuthTag(); + return `v1:${iv.toString("base64url")}:${tag.toString("base64url")}:${ciphertext.toString("base64url")}`; +} + +function decryptSecret(value: string): string { + const [version, ivText, tagText, ciphertextText] = value.split(":"); + if (version !== "v1" || !ivText || !tagText || !ciphertextText) throw new Error("unsupported encrypted secret format"); + const decipher = createDecipheriv("aes-256-gcm", encryptionKey(), Buffer.from(ivText, "base64url")); + decipher.setAuthTag(Buffer.from(tagText, "base64url")); + return Buffer.concat([decipher.update(Buffer.from(ciphertextText, "base64url")), decipher.final()]).toString("utf8"); +} + +async function ensureSchema(): Promise { + await sql` + CREATE TABLE IF NOT EXISTS baidu_netdisk_accounts ( + id TEXT PRIMARY KEY, + baidu_uid TEXT NOT NULL DEFAULT '', + username TEXT NOT NULL DEFAULT '', + avatar_url TEXT NOT NULL DEFAULT '', + vip_type INTEGER, + root_path TEXT NOT NULL DEFAULT '', + quota_json JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `; + await sql` + CREATE TABLE IF NOT EXISTS baidu_netdisk_tokens ( + account_id TEXT PRIMARY KEY REFERENCES baidu_netdisk_accounts(id) ON DELETE CASCADE, + access_token_ciphertext TEXT NOT NULL, + refresh_token_ciphertext TEXT NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + scope TEXT NOT NULL DEFAULT '', + generation INTEGER NOT NULL DEFAULT 0, + last_refresh_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `; + await sql` + CREATE TABLE IF NOT EXISTS baidu_netdisk_auth_sessions ( + id TEXT PRIMARY KEY, + device_code_ciphertext TEXT NOT NULL, + user_code TEXT NOT NULL, + verification_url TEXT NOT NULL DEFAULT '', + qrcode_url TEXT NOT NULL DEFAULT '', + expires_at TIMESTAMPTZ NOT NULL, + poll_interval_seconds INTEGER NOT NULL DEFAULT 5, + status TEXT NOT NULL DEFAULT 'pending', + error TEXT, + poll_count INTEGER NOT NULL DEFAULT 0, + last_poll_at TIMESTAMPTZ, + token_account_id TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `; + await sql` + CREATE TABLE IF NOT EXISTS baidu_netdisk_transfer_jobs ( + id TEXT PRIMARY KEY, + account_id TEXT, + direction TEXT NOT NULL, + status TEXT NOT NULL, + local_path TEXT NOT NULL DEFAULT '', + remote_path TEXT NOT NULL DEFAULT '', + fs_id TEXT, + size_bytes BIGINT, + bytes_done BIGINT NOT NULL DEFAULT 0, + part_size INTEGER, + block_list_json JSONB NOT NULL DEFAULT '[]'::jsonb, + uploadid TEXT, + retry_count INTEGER NOT NULL DEFAULT 0, + error TEXT, + result JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `; + await sql` + CREATE TABLE IF NOT EXISTS baidu_netdisk_transfer_events ( + id BIGSERIAL PRIMARY KEY, + job_id TEXT NOT NULL REFERENCES baidu_netdisk_transfer_jobs(id) ON DELETE CASCADE, + level TEXT NOT NULL, + message TEXT NOT NULL, + data_json JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `; + await sql`CREATE INDEX IF NOT EXISTS idx_baidu_netdisk_auth_sessions_status ON baidu_netdisk_auth_sessions(status, updated_at DESC)`; + await sql`CREATE INDEX IF NOT EXISTS idx_baidu_netdisk_transfer_jobs_status ON baidu_netdisk_transfer_jobs(status, updated_at DESC)`; +} + +async function waitForSchema(): Promise { + let lastError: unknown = null; + for (let attempt = 1; attempt <= 30; attempt += 1) { + try { + await ensureSchema(); + schemaReady = true; + log("schema_ready", { attempt }); + return; + } catch (error) { + lastError = error; + log("schema_wait", { attempt, error: errorToJson(error) }); + await new Promise((resolveWait) => setTimeout(resolveWait, 1000)); + } + } + throw lastError instanceof Error ? lastError : new Error(String(lastError)); +} + +function buildUrl(base: string, params: Record): URL { + const url = new URL(base); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) url.searchParams.set(key, String(value)); + } + return url; +} + +function baiduHeaders(extra: HeadersInit = {}): Headers { + const headers = new Headers(extra); + headers.set("user-agent", config.userAgent); + if (!headers.has("accept")) headers.set("accept", "application/json"); + return headers; +} + +function baiduErrorMessage(body: unknown): string { + const record = asRecord(body); + for (const key of ["error_description", "error", "errmsg", "msg"]) { + if (typeof record[key] === "string" && String(record[key]).length > 0) return String(record[key]); + } + if (record.errno !== undefined && Number(record.errno) !== 0) return `Baidu API errno ${String(record.errno)}`; + return "Baidu API request failed"; +} + +async function fetchJsonRaw(url: URL, init: RequestInit = {}): Promise<{ ok: boolean; status: number; body: JsonRecord }> { + const response = await fetch(url, { ...init, headers: baiduHeaders(init.headers) }); + const text = await response.text(); + let body: unknown; + try { + body = text ? JSON.parse(text) as unknown : {}; + } catch { + body = { text }; + } + const record = asRecord(body); + return { ok: response.ok, status: response.status, body: record }; +} + +async function fetchJson(url: URL, init: RequestInit = {}, checkErrno = true): Promise { + const { ok, status, body: record } = await fetchJsonRaw(url, init); + if (!ok) throw new BaiduApiError(baiduErrorMessage(record), status, redactJson(record)); + if (checkErrno && record.errno !== undefined && Number(record.errno) !== 0) { + throw new BaiduApiError(baiduErrorMessage(record), status, redactJson(record)); + } + return record; +} + +async function oauthJson(url: URL): Promise { + return fetchJson(url, {}, false); +} + +async function oauthJsonAllowHttpError(url: URL): Promise { + return (await fetchJsonRaw(url)).body; +} + +function formBody(fields: Record): URLSearchParams { + const body = new URLSearchParams(); + for (const [key, value] of Object.entries(fields)) { + if (value === undefined) continue; + if (typeof value === "object") body.set(key, JSON.stringify(value)); + else body.set(key, String(value)); + } + return body; +} + +async function baiduFormJson(url: URL, fields: Record, checkErrno = true): Promise { + return fetchJson(url, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: formBody(fields), + }, checkErrno); +} + +function baiduErrno(body: JsonRecord): number | null { + if (body.errno === undefined || body.errno === null) return null; + const value = Number(body.errno); + return Number.isFinite(value) ? value : null; +} + +function baiduApiErrorFromBody(body: JsonRecord, status = 200): BaiduApiError { + return new BaiduApiError(baiduErrorMessage(body), status, redactJson(body)); +} + +function errorHasBaiduErrno(error: unknown, errno: number): boolean { + if (!(error instanceof BaiduApiError)) return false; + return baiduErrno(asRecord(error.body)) === errno; +} + +async function createRemoteFolder(accessToken: string, path: string): Promise { + const result = await baiduFormJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/file", { method: "create", access_token: accessToken }), { + path, + size: 0, + isdir: 1, + rtype: 0, + }, false); + const errno = baiduErrno(result); + if (errno !== null && errno !== 0 && errno !== -8) throw baiduApiErrorFromBody(result); + return result; +} + +async function ensureAppRoot(accessToken: string, force = false): Promise { + if (!force && appRootEnsuredAt > 0 && Date.now() - appRootEnsuredAt < 10 * 60 * 1000) return; + if (appRootEnsurePromise) return appRootEnsurePromise; + appRootEnsurePromise = (async () => { + const result = await createRemoteFolder(accessToken, config.appRoot); + appRootEnsuredAt = Date.now(); + const errno = baiduErrno(result); + log(errno === -8 ? "app_root_exists" : "app_root_ready", { path: config.appRoot, result }); + })(); + try { + await appRootEnsurePromise; + } finally { + appRootEnsurePromise = null; + } +} + +function remotePathInsideRoot(input: string | undefined, fallback = config.appRoot): string { + const raw = String(input || fallback).trim() || fallback; + const normalized = pathPosix.normalize(raw.startsWith("/") ? raw : pathPosix.join(config.appRoot, raw)); + const clean = normalized.replace(/\/+$/u, "") || config.appRoot; + if (clean !== config.appRoot && !clean.startsWith(`${config.appRoot}/`)) { + throw new HttpError(400, "remote path must stay inside app root", { appRoot: config.appRoot, path: clean }); + } + return clean; +} + +function remoteFilePath(input: string | undefined, localName = "upload.bin"): string { + const path = remotePathInsideRoot(input || pathPosix.join(config.appRoot, localName)); + if (path.endsWith("/")) return remotePathInsideRoot(pathPosix.join(path, localName)); + return path; +} + +function resolveStagingPath(input: string | undefined, fallbackName = "file"): string { + const raw = String(input || fallbackName).trim() || fallbackName; + const candidate = raw.startsWith("/") ? resolve(raw) : resolve(config.stagingDir, raw); + const rel = relative(config.stagingDir, candidate); + if (rel === "" || (!rel.startsWith("..") && !rel.includes(`..${sep}`) && !resolve(candidate).includes("\0"))) return candidate; + throw new HttpError(400, "local path must stay inside staging directory", { stagingDir: config.stagingDir }); +} + +function authSessionFromRow(row: AuthSessionRow): JsonRecord { + const expiresAt = iso(row.expires_at); + const secondsRemaining = expiresAt ? Math.max(0, Math.floor((new Date(expiresAt).getTime() - Date.now()) / 1000)) : 0; + return { + id: row.id, + userCode: row.user_code, + verificationUrl: row.verification_url, + qrcodeUrl: row.qrcode_url, + expiresAt, + secondsRemaining, + pollIntervalSeconds: row.poll_interval_seconds, + status: row.status, + error: row.error || "", + pollCount: row.poll_count, + lastPollAt: iso(row.last_poll_at), + tokenAccountId: row.token_account_id || "", + createdAt: iso(row.created_at), + updatedAt: iso(row.updated_at), + }; +} + +async function startDeviceAuth(): Promise { + requireBaiduConfigured(); + const url = buildUrl("https://openapi.baidu.com/oauth/2.0/device/code", { + response_type: "device_code", + client_id: config.clientId, + scope: "basic,netdisk", + }); + const body = await oauthJson(url); + const deviceCode = String(body.device_code || ""); + const userCode = String(body.user_code || ""); + if (!deviceCode || !userCode) throw new BaiduApiError("Baidu device code response is missing device_code/user_code", 502, redactJson(body)); + const expiresIn = Math.max(60, asNumber(body.expires_in, 1800)); + const interval = Math.max(5, asNumber(body.interval, 5)); + const sessionId = `bd_${randomUUID()}`; + const verificationUrl = String(body.verification_url || "https://openapi.baidu.com/device"); + const qrcodeUrl = String(body.qrcode_url || `https://openapi.baidu.com/device?display=mobile&code=${encodeURIComponent(userCode)}`); + const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); + await sql` + INSERT INTO baidu_netdisk_auth_sessions (id, device_code_ciphertext, user_code, verification_url, qrcode_url, expires_at, poll_interval_seconds, status) + VALUES (${sessionId}, ${encryptSecret(deviceCode)}, ${userCode}, ${verificationUrl}, ${qrcodeUrl}, ${expiresAt}, ${interval}, 'pending') + `; + log("device_auth_started", { sessionId, userCode, expiresAt, interval }); + const rows = await sql`SELECT * FROM baidu_netdisk_auth_sessions WHERE id = ${sessionId}`; + return { ok: true, session: authSessionFromRow(rows[0]) }; +} + +async function fetchBaiduAccount(accessToken: string): Promise<{ uinfo: JsonRecord; quota: JsonRecord }> { + const [uinfo, quota] = await Promise.all([ + fetchJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/nas", { method: "uinfo", access_token: accessToken, vip_version: "v2" })), + fetchJson(buildUrl("https://pan.baidu.com/api/quota", { access_token: accessToken, checkfree: 1, checkexpire: 1 })), + ]); + return { uinfo, quota }; +} + +async function persistTokenFromOAuth(tokenBody: JsonRecord): Promise { + const accessToken = String(tokenBody.access_token || ""); + const refreshToken = String(tokenBody.refresh_token || ""); + if (!accessToken || !refreshToken) throw new BaiduApiError("Baidu token response is missing tokens", 502, redactJson(tokenBody)); + const { uinfo, quota } = await fetchBaiduAccount(accessToken); + const uid = String(uinfo.uk || uinfo.baidu_uid || "default"); + const accountId = `baidu_${uid.replace(/[^A-Za-z0-9_-]/g, "_")}`; + const username = String(uinfo.netdisk_name || uinfo.baidu_name || ""); + const expiresIn = Math.max(60, asNumber(tokenBody.expires_in, 2592000)); + const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); + const scope = String(tokenBody.scope || ""); + await sql` + INSERT INTO baidu_netdisk_accounts (id, baidu_uid, username, avatar_url, vip_type, root_path, quota_json, updated_at) + VALUES (${accountId}, ${uid}, ${username}, ${String(uinfo.avatar_url || "")}, ${uinfo.vip_type === undefined ? null : Number(uinfo.vip_type)}, ${config.appRoot}, ${sql.json(quota)}, now()) + ON CONFLICT (id) DO UPDATE SET + baidu_uid = EXCLUDED.baidu_uid, + username = EXCLUDED.username, + avatar_url = EXCLUDED.avatar_url, + vip_type = EXCLUDED.vip_type, + root_path = EXCLUDED.root_path, + quota_json = EXCLUDED.quota_json, + updated_at = now() + `; + await sql` + INSERT INTO baidu_netdisk_tokens (account_id, access_token_ciphertext, refresh_token_ciphertext, expires_at, scope, generation, last_refresh_at, updated_at) + VALUES (${accountId}, ${encryptSecret(accessToken)}, ${encryptSecret(refreshToken)}, ${expiresAt}, ${scope}, 0, now(), now()) + ON CONFLICT (account_id) DO UPDATE SET + access_token_ciphertext = EXCLUDED.access_token_ciphertext, + refresh_token_ciphertext = EXCLUDED.refresh_token_ciphertext, + expires_at = EXCLUDED.expires_at, + scope = EXCLUDED.scope, + generation = baidu_netdisk_tokens.generation + 1, + last_refresh_at = now(), + updated_at = now() + `; + await ensureAppRoot(accessToken, true); + log("token_persisted", { accountId, uid, username, expiresAt, scope }); + return { accountId, uid, username, avatarUrl: String(uinfo.avatar_url || ""), vipType: Number(uinfo.vip_type ?? 0), rootPath: config.appRoot, quota, expiresAt, scope }; +} + +function oauthPendingError(error: string): boolean { + return ["authorization_pending", "authorization_declined", "slow_down", "expired_token", "access_denied"].includes(error); +} + +async function pollDeviceAuth(sessionId: string): Promise { + requireBaiduConfigured(); + const rows = await sql>`SELECT * FROM baidu_netdisk_auth_sessions WHERE id = ${sessionId} LIMIT 1`; + const row = rows[0]; + if (!row) throw new HttpError(404, "auth session not found", { sessionId }); + const expiresAtMs = new Date(row.expires_at).getTime(); + if (row.status === "succeeded" || row.status === "failed" || row.status === "rejected" || row.status === "expired") { + return { ok: true, session: authSessionFromRow(row) }; + } + if (Number.isFinite(expiresAtMs) && expiresAtMs <= Date.now()) { + await sql`UPDATE baidu_netdisk_auth_sessions SET status = 'expired', error = 'device code expired', updated_at = now() WHERE id = ${sessionId}`; + const latest = await sql`SELECT * FROM baidu_netdisk_auth_sessions WHERE id = ${sessionId}`; + return { ok: true, session: authSessionFromRow(latest[0]) }; + } + const lastPollMs = row.last_poll_at ? new Date(row.last_poll_at).getTime() : 0; + const minIntervalMs = Math.max(5, row.poll_interval_seconds) * 1000; + if (Number.isFinite(lastPollMs) && Date.now() - lastPollMs < minIntervalMs) { + return { ok: true, session: authSessionFromRow(row), nextPollAfterMs: Math.max(0, minIntervalMs - (Date.now() - lastPollMs)) }; + } + const tokenUrl = buildUrl("https://openapi.baidu.com/oauth/2.0/token", { + grant_type: "device_token", + code: decryptSecret(row.device_code_ciphertext), + client_id: config.clientId, + client_secret: config.clientSecret, + }); + const tokenBody = await oauthJsonAllowHttpError(tokenUrl); + const oauthError = typeof tokenBody.error === "string" ? tokenBody.error : ""; + if (oauthError) { + const description = String(tokenBody.error_description || oauthError); + let status = "pending"; + let pollInterval = row.poll_interval_seconds; + if (oauthError === "slow_down") pollInterval = Math.max(5, pollInterval + 5); + if (oauthError === "expired_token") status = "expired"; + if (oauthError === "authorization_declined" || oauthError === "access_denied") status = "rejected"; + if (!oauthPendingError(oauthError)) status = "failed"; + const sessionError = status === "pending" ? "" : description; + await sql` + UPDATE baidu_netdisk_auth_sessions + SET status = ${status}, error = ${sessionError}, poll_interval_seconds = ${pollInterval}, poll_count = poll_count + 1, last_poll_at = now(), updated_at = now() + WHERE id = ${sessionId} + `; + log("device_auth_poll", { sessionId, status, oauthError, pollInterval }); + const latest = await sql`SELECT * FROM baidu_netdisk_auth_sessions WHERE id = ${sessionId}`; + return { ok: true, session: authSessionFromRow(latest[0]) }; + } + const account = await persistTokenFromOAuth(tokenBody); + await sql` + UPDATE baidu_netdisk_auth_sessions + SET status = 'succeeded', error = NULL, token_account_id = ${String(account.accountId)}, poll_count = poll_count + 1, last_poll_at = now(), updated_at = now() + WHERE id = ${sessionId} + `; + const latest = await sql`SELECT * FROM baidu_netdisk_auth_sessions WHERE id = ${sessionId}`; + return { ok: true, session: authSessionFromRow(latest[0]), account }; +} + +async function activeTokenRow(): Promise { + const rows = await sql` + SELECT * FROM baidu_netdisk_tokens + ORDER BY updated_at DESC + LIMIT 1 + `; + return rows[0] ?? null; +} + +async function refreshToken(accountId: string): Promise { + requireBaiduConfigured(); + return sql.begin(async (tx) => { + const rows = await tx`SELECT * FROM baidu_netdisk_tokens WHERE account_id = ${accountId} FOR UPDATE`; + const row = rows[0]; + if (!row) throw new HttpError(401, "Baidu account is not logged in"); + const expiresAt = new Date(row.expires_at).getTime(); + if (Number.isFinite(expiresAt) && expiresAt - Date.now() > 5 * 60 * 1000) return decryptSecret(row.access_token_ciphertext); + const tokenUrl = buildUrl("https://openapi.baidu.com/oauth/2.0/token", { + grant_type: "refresh_token", + refresh_token: decryptSecret(row.refresh_token_ciphertext), + client_id: config.clientId, + client_secret: config.clientSecret, + }); + const tokenBody = await oauthJson(tokenUrl); + if (typeof tokenBody.error === "string" && tokenBody.error) throw new BaiduApiError(baiduErrorMessage(tokenBody), 502, redactJson(tokenBody)); + const accessToken = String(tokenBody.access_token || ""); + const refreshTokenValue = String(tokenBody.refresh_token || ""); + if (!accessToken || !refreshTokenValue) throw new BaiduApiError("Baidu refresh response is missing tokens", 502, redactJson(tokenBody)); + const expiresIn = Math.max(60, asNumber(tokenBody.expires_in, 2592000)); + const expiresAtText = new Date(Date.now() + expiresIn * 1000).toISOString(); + await tx` + UPDATE baidu_netdisk_tokens + SET access_token_ciphertext = ${encryptSecret(accessToken)}, + refresh_token_ciphertext = ${encryptSecret(refreshTokenValue)}, + expires_at = ${expiresAtText}, + scope = ${String(tokenBody.scope || row.scope || "")}, + generation = generation + 1, + last_refresh_at = now(), + updated_at = now() + WHERE account_id = ${accountId} + `; + log("token_refreshed", { accountId, expiresAt: expiresAtText }); + return accessToken; + }); +} + +async function getAccessToken(): Promise<{ accountId: string; accessToken: string }> { + const row = await activeTokenRow(); + if (!row) throw new HttpError(401, "Baidu Netdisk is not logged in"); + const expiresAt = new Date(row.expires_at).getTime(); + if (Number.isFinite(expiresAt) && expiresAt - Date.now() > 5 * 60 * 1000) { + return { accountId: row.account_id, accessToken: decryptSecret(row.access_token_ciphertext) }; + } + return { accountId: row.account_id, accessToken: await refreshToken(row.account_id) }; +} + +function accountView(account: JsonRecord, token: JsonRecord | null): JsonRecord { + const quota = asRecord(account.quota_json); + const total = asNumber(quota.total, 0); + const used = asNumber(quota.used, 0); + return { + id: String(account.id || ""), + baiduUid: String(account.baidu_uid || ""), + username: String(account.username || ""), + avatarUrl: String(account.avatar_url || ""), + vipType: account.vip_type === null || account.vip_type === undefined ? null : Number(account.vip_type), + rootPath: String(account.root_path || config.appRoot), + quota: { + ...quota, + total, + used, + freeBytes: Math.max(0, total - used), + usedPercent: total > 0 ? Math.round((used / total) * 1000) / 10 : 0, + }, + token: token ? { + expiresAt: iso(String(token.expires_at || "")), + scope: String(token.scope || ""), + generation: Number(token.generation || 0), + lastRefreshAt: iso(String(token.last_refresh_at || "")), + } : null, + updatedAt: iso(String(account.updated_at || "")), + }; +} + +async function accountSummary(refreshRemote: boolean): Promise { + const row = await activeTokenRow(); + if (!row) throw new HttpError(401, "Baidu Netdisk is not logged in"); + const { accountId, accessToken } = await getAccessToken(); + await ensureAppRoot(accessToken); + if (refreshRemote) { + const { uinfo, quota } = await fetchBaiduAccount(accessToken); + await sql` + UPDATE baidu_netdisk_accounts + SET baidu_uid = ${String(uinfo.uk || "")}, + username = ${String(uinfo.netdisk_name || uinfo.baidu_name || "")}, + avatar_url = ${String(uinfo.avatar_url || "")}, + vip_type = ${uinfo.vip_type === undefined ? null : Number(uinfo.vip_type)}, + root_path = ${config.appRoot}, + quota_json = ${sql.json(quota)}, + updated_at = now() + WHERE id = ${accountId} + `; + } + const rows = await sql` + SELECT a.*, t.expires_at, t.scope, t.generation, t.last_refresh_at + FROM baidu_netdisk_accounts a + LEFT JOIN baidu_netdisk_tokens t ON t.account_id = a.id + WHERE a.id = ${accountId} + LIMIT 1 + `; + const account = rows[0]; + if (!account) throw new HttpError(401, "Baidu account row is missing"); + return { ok: true, account: accountView(account, account) }; +} + +async function authSummary(): Promise { + const [accountRows, sessionRows] = await Promise.all([ + sql`SELECT a.*, t.expires_at, t.scope, t.generation, t.last_refresh_at FROM baidu_netdisk_accounts a LEFT JOIN baidu_netdisk_tokens t ON t.account_id = a.id ORDER BY a.updated_at DESC LIMIT 1`, + sql`SELECT * FROM baidu_netdisk_auth_sessions ORDER BY updated_at DESC LIMIT 1`, + ]); + return { + configured: Boolean(config.clientId && config.clientSecret && config.tokenKey), + clientIdConfigured: Boolean(config.clientId), + clientSecretConfigured: Boolean(config.clientSecret), + tokenKeyConfigured: Boolean(config.tokenKey), + loggedIn: accountRows.length > 0, + account: accountRows[0] ? accountView(accountRows[0], accountRows[0]) : null, + latestSession: sessionRows[0] ? authSessionFromRow(sessionRows[0]) : null, + }; +} + +async function health(): Promise { + return { + ok: schemaReady, + service: "baidu-netdisk", + startedAt: serviceStartedAt, + storage: { postgres: schemaReady ? "ready" : "starting", stagingDir: config.stagingDir }, + auth: await authSummary(), + baidu: { appRoot: config.appRoot, userAgent: config.userAgent, apiReachability: "not_checked" }, + queue: { worker: "in-process", transfers: await transferCounts() }, + }; +} + +async function forceRefreshAuth(): Promise { + const row = await activeTokenRow(); + if (!row) throw new HttpError(401, "Baidu Netdisk is not logged in"); + const accessToken = await refreshToken(row.account_id); + return { ok: true, accountId: row.account_id, accessToken: redactString(accessToken), account: (await accountSummary(true)).account }; +} + +async function logoutAuth(): Promise { + await sql`UPDATE baidu_netdisk_transfer_jobs SET status = 'canceled', error = 'logout requested', updated_at = now() WHERE status IN ('queued', 'running')`; + await sql`DELETE FROM baidu_netdisk_tokens`; + await sql`DELETE FROM baidu_netdisk_accounts`; + await sql`UPDATE baidu_netdisk_auth_sessions SET status = 'expired', error = 'logout requested', updated_at = now() WHERE status = 'pending'`; + log("logout_completed"); + return { ok: true }; +} + +function fileEntry(raw: JsonRecord): JsonRecord { + const rawPath = String(raw.path || ""); + return { + fsId: String(raw.fs_id || raw.fsid || ""), + path: rawPath, + serverFilename: String(raw.server_filename || raw.filename || raw.name || (rawPath ? pathPosix.basename(rawPath) : "")), + isDir: Number(raw.isdir || 0) === 1, + size: asNumber(raw.size, 0), + category: raw.category === undefined ? null : Number(raw.category), + serverMtime: raw.server_mtime === undefined ? null : Number(raw.server_mtime), + serverCtime: raw.server_ctime === undefined ? null : Number(raw.server_ctime), + localMtime: raw.local_mtime === undefined ? null : Number(raw.local_mtime), + md5: typeof raw.md5 === "string" ? raw.md5 : "", + thumbs: asRecord(raw.thumbs), + }; +} + +async function listFiles(url: URL): Promise { + const { accessToken } = await getAccessToken(); + const dir = remotePathInsideRoot(url.searchParams.get("dir") || config.appRoot); + const start = Math.max(0, asNumber(url.searchParams.get("start"), 0)); + const limit = Math.min(500, Math.max(1, asNumber(url.searchParams.get("limit"), 100))); + const order = url.searchParams.get("order") || "name"; + const desc = url.searchParams.get("desc") || "0"; + await ensureAppRoot(accessToken); + const requestList = () => fetchJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/file", { + method: "list", + access_token: accessToken, + dir, + start, + limit, + order, + desc, + web: 1, + folder: 0, + })); + let body: JsonRecord; + try { + body = await requestList(); + } catch (error) { + if (dir !== config.appRoot || !errorHasBaiduErrno(error, -9)) throw error; + await ensureAppRoot(accessToken, true); + body = await requestList(); + } + const list = Array.isArray(body.list) ? body.list.map((item) => fileEntry(asRecord(item))) : []; + return { ok: true, rootPath: config.appRoot, dir, start, limit, hasMore: Number(body.has_more || 0) === 1, files: list }; +} + +function parseFsIds(value: string | null): string[] { + if (!value) return []; + const trimmed = value.trim(); + if (!trimmed) return []; + if (trimmed.startsWith("[")) { + try { + const parsed = JSON.parse(trimmed) as unknown; + if (Array.isArray(parsed)) return parsed.map((item) => String(item)).filter(Boolean); + } catch { + return []; + } + } + return trimmed.split(",").map((item) => item.trim()).filter(Boolean); +} + +async function fileMetas(fsIds: string[], dlink: boolean): Promise { + if (fsIds.length === 0) throw new HttpError(400, "fsids is required"); + const { accessToken } = await getAccessToken(); + const body = await fetchJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/multimedia", { + method: "filemetas", + access_token: accessToken, + fsids: JSON.stringify(fsIds.map((id) => Number.isFinite(Number(id)) ? Number(id) : id)), + dlink: dlink ? 1 : 0, + })); + const list = Array.isArray(body.list) ? body.list.map((item) => { + const record = fileEntry(asRecord(item)); + if (dlink) record.dlinkRedacted = Boolean(asRecord(item).dlink); + return record; + }) : []; + return { ok: true, files: list, dlinkRedacted: dlink }; +} + +async function createFolder(body: JsonRecord): Promise { + const { accessToken } = await getAccessToken(); + const path = remotePathInsideRoot(String(body.path || "")); + await ensureAppRoot(accessToken); + const result = await createRemoteFolder(accessToken, path); + log("folder_created", { path, result }); + return { ok: true, folder: fileEntry(result), result: redactJson(result) }; +} + +function sanitizeFileManagerList(opera: string, raw: unknown): JsonValue[] { + if (!Array.isArray(raw) || raw.length === 0) throw new HttpError(400, "filelist must be a non-empty array"); + return raw.map((item) => { + const record = asRecord(item); + const output: JsonRecord = { path: remotePathInsideRoot(String(record.path || "")) }; + if (opera === "copy" || opera === "move") output.dest = remotePathInsideRoot(String(record.dest || config.appRoot)); + if (opera === "copy" || opera === "move" || opera === "rename") { + const newname = String(record.newname || "").trim(); + if (!newname || newname.includes("/") || newname.includes("\\")) throw new HttpError(400, "newname is required and must not contain slashes"); + output.newname = newname; + } + return output; + }); +} + +async function manageFiles(body: JsonRecord): Promise { + const opera = String(body.opera || "").trim(); + if (!["copy", "move", "rename", "delete"].includes(opera)) throw new HttpError(400, "opera must be copy, move, rename, or delete"); + const filelist = sanitizeFileManagerList(opera, body.filelist); + const { accessToken } = await getAccessToken(); + const result = await baiduFormJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/file", { method: "filemanager", access_token: accessToken, opera }), { + async: body.async === 0 ? 0 : 1, + ondup: typeof body.ondup === "string" ? body.ondup : undefined, + filelist, + }); + log("files_managed", { opera, count: filelist.length, result }); + return { ok: true, opera, result: redactJson(result) }; +} + +function transferFromRow(row: TransferJobRow): JsonRecord { + const sizeBytes = asNumber(row.size_bytes, 0); + const bytesDone = asNumber(row.bytes_done, 0); + return { + id: row.id, + accountId: row.account_id || "", + direction: row.direction, + status: row.status, + localPath: row.local_path, + remotePath: row.remote_path, + fsId: row.fs_id || "", + sizeBytes, + bytesDone, + progressPercent: sizeBytes > 0 ? Math.round((Math.min(bytesDone, sizeBytes) / sizeBytes) * 1000) / 10 : (row.status === "succeeded" ? 100 : 0), + partSize: row.part_size || config.partSizeBytes, + blockList: row.block_list_json, + uploadId: row.uploadid || "", + retryCount: row.retry_count, + error: row.error || "", + result: redactJson(row.result), + createdAt: iso(row.created_at), + updatedAt: iso(row.updated_at), + }; +} + +async function transferCounts(): Promise { + const rows = await sql`SELECT status, count(*)::int AS count FROM baidu_netdisk_transfer_jobs GROUP BY status`; + const counts: JsonRecord = {}; + for (const row of rows) counts[String(row.status)] = Number(row.count || 0); + return counts; +} + +async function recordTransferEvent(jobId: string, level: "info" | "warn" | "error", message: string, data: JsonRecord = {}): Promise { + await sql`INSERT INTO baidu_netdisk_transfer_events (job_id, level, message, data_json) VALUES (${jobId}, ${level}, ${message}, ${sql.json(redactJson(data))})`; + log("transfer_event", { jobId, level, message, data }); +} + +async function updateTransfer(jobId: string, patch: Partial<{ status: TransferStatus; accountId: string; localPath: string; remotePath: string; fsId: string; sizeBytes: number; bytesDone: number; partSize: number; blockList: JsonValue; uploadId: string; error: string; result: JsonValue }>): Promise { + const current = (await sql`SELECT * FROM baidu_netdisk_transfer_jobs WHERE id = ${jobId}`)[0]; + if (!current) return; + await sql` + UPDATE baidu_netdisk_transfer_jobs + SET status = ${patch.status ?? current.status}, + account_id = ${patch.accountId ?? current.account_id}, + local_path = ${patch.localPath ?? current.local_path}, + remote_path = ${patch.remotePath ?? current.remote_path}, + fs_id = ${patch.fsId ?? current.fs_id}, + size_bytes = ${patch.sizeBytes ?? current.size_bytes}, + bytes_done = ${patch.bytesDone ?? current.bytes_done}, + part_size = ${patch.partSize ?? current.part_size}, + block_list_json = ${sql.json(patch.blockList ?? current.block_list_json)}, + uploadid = ${patch.uploadId ?? current.uploadid}, + error = ${patch.error ?? current.error}, + result = ${sql.json(patch.result ?? current.result)}, + updated_at = now() + WHERE id = ${jobId} + `; +} + +async function transferIsCanceled(jobId: string): Promise { + const rows = await sql>`SELECT status FROM baidu_netdisk_transfer_jobs WHERE id = ${jobId}`; + return rows[0]?.status === "canceled"; +} + +async function assertNotCanceled(jobId: string): Promise { + if (await transferIsCanceled(jobId)) throw new HttpError(409, "transfer was canceled", { jobId }); +} + +async function createTransferJob(direction: TransferDirection, body: JsonRecord): Promise { + const { accountId } = await getAccessToken(); + const id = `bdt_${Date.now()}_${randomUUID().slice(0, 8)}`; + const localPath = direction === "upload" + ? resolveStagingPath(String(body.localPath || "")) + : resolveStagingPath(String(body.localPath || body.localDir || "downloads")); + const remotePath = direction === "upload" + ? remoteFilePath(String(body.remotePath || ""), basename(localPath)) + : remotePathInsideRoot(String(body.remotePath || config.appRoot)); + const fsId = body.fsId === undefined || body.fsId === null ? null : String(body.fsId); + await sql` + INSERT INTO baidu_netdisk_transfer_jobs (id, account_id, direction, status, local_path, remote_path, fs_id, part_size) + VALUES (${id}, ${accountId}, ${direction}, 'queued', ${localPath}, ${remotePath}, ${fsId}, ${config.partSizeBytes}) + `; + await recordTransferEvent(id, "info", "transfer queued", { direction, localPath, remotePath, fsId: fsId || "" }); + setTimeout(() => { + runTransferJob(id).catch((error) => log("transfer_worker_uncaught", { jobId: id, error: errorToJson(error) })); + }, 0); + const row = (await sql`SELECT * FROM baidu_netdisk_transfer_jobs WHERE id = ${id}`)[0]; + return { ok: true, job: transferFromRow(row) }; +} + +async function computeMd5Blocks(filePath: string, jobId: string): Promise<{ size: number; fullMd5: string; blocks: string[] }> { + const info = await stat(filePath); + if (!info.isFile()) throw new HttpError(400, "localPath must be a file inside staging directory", { localPath: filePath }); + const fullHash = createHash("md5"); + const blocks: string[] = []; + const handle = await open(filePath, "r"); + let offset = 0; + try { + while (offset < info.size) { + await assertNotCanceled(jobId); + const chunkSize = Math.min(config.partSizeBytes, info.size - offset); + const buffer = Buffer.allocUnsafe(chunkSize); + const { bytesRead } = await handle.read(buffer, 0, chunkSize, offset); + if (bytesRead <= 0) break; + const chunk = buffer.subarray(0, bytesRead); + fullHash.update(chunk); + blocks.push(createHash("md5").update(chunk).digest("hex")); + offset += bytesRead; + await updateTransfer(jobId, { sizeBytes: info.size, bytesDone: Math.min(offset, info.size), blockList: blocks }); + } + } finally { + await handle.close(); + } + return { size: info.size, fullMd5: fullHash.digest("hex"), blocks }; +} + +function uploadServerUrl(locateResult: JsonRecord): string { + const servers = Array.isArray(locateResult.servers) ? locateResult.servers : []; + const first = servers.find((item) => typeof item === "string" || (typeof item === "object" && item !== null)); + const raw = typeof first === "string" ? first : String(asRecord(first).server || asRecord(first).host || ""); + if (!raw) return "https://d.pcs.baidu.com"; + return raw.startsWith("http://") || raw.startsWith("https://") ? raw.replace(/\/+$/u, "") : `https://${raw.replace(/^\/+|\/+$/gu, "")}`; +} + +async function uploadPart(uploadBaseUrl: string, accessToken: string, filePath: string, remotePath: string, uploadId: string, partIndex: number): Promise { + const info = await stat(filePath); + const start = partIndex * config.partSizeBytes; + const chunkSize = Math.min(config.partSizeBytes, Math.max(0, info.size - start)); + const handle = await open(filePath, "r"); + try { + const buffer = Buffer.allocUnsafe(chunkSize); + const { bytesRead } = await handle.read(buffer, 0, chunkSize, start); + const chunk = buffer.subarray(0, bytesRead); + const form = new FormData(); + form.append("file", new Blob([chunk]), basename(remotePath)); + const url = buildUrl(`${uploadBaseUrl}/rest/2.0/pcs/superfile2`, { + method: "upload", + access_token: accessToken, + type: "tmpfile", + path: remotePath, + uploadid: uploadId, + partseq: partIndex, + }); + await fetchJson(url, { method: "POST", body: form }); + return bytesRead; + } finally { + await handle.close(); + } +} + +async function runUploadJob(job: TransferJobRow): Promise { + const { accountId, accessToken } = await getAccessToken(); + await updateTransfer(job.id, { accountId }); + await ensureAppRoot(accessToken); + await recordTransferEvent(job.id, "info", "computing md5 blocks"); + const hashes = await computeMd5Blocks(job.local_path, job.id); + await assertNotCanceled(job.id); + await recordTransferEvent(job.id, "info", "precreate upload", { size: hashes.size, blocks: hashes.blocks.length, fullMd5: hashes.fullMd5 }); + const precreate = await baiduFormJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/file", { method: "precreate", access_token: accessToken }), { + path: job.remote_path, + size: hashes.size, + isdir: 0, + autoinit: 1, + rtype: 1, + block_list: hashes.blocks, + }); + const uploadId = String(precreate.uploadid || ""); + await updateTransfer(job.id, { uploadId, blockList: hashes.blocks, bytesDone: 0, sizeBytes: hashes.size }); + const requiredParts = Array.isArray(precreate.block_list) + ? precreate.block_list.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0) + : hashes.blocks.map((_, index) => index); + if (hashes.size > 0 && requiredParts.length > 0) { + if (!uploadId) throw new BaiduApiError("precreate response is missing uploadid", 502, redactJson(precreate)); + const locate = await fetchJson(buildUrl("https://d.pcs.baidu.com/rest/2.0/pcs/file", { + method: "locateupload", + appid: 250528, + access_token: accessToken, + path: job.remote_path, + uploadid: uploadId, + upload_version: "2.0", + })); + const server = uploadServerUrl(locate); + let bytesUploaded = 0; + for (const partIndex of requiredParts) { + await assertNotCanceled(job.id); + const bytesRead = await uploadPart(server, accessToken, job.local_path, job.remote_path, uploadId, partIndex); + bytesUploaded += bytesRead; + await updateTransfer(job.id, { bytesDone: Math.min(hashes.size, bytesUploaded), uploadId }); + await recordTransferEvent(job.id, "info", "uploaded part", { partIndex, bytesRead }); + } + } + await assertNotCanceled(job.id); + const created = await baiduFormJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/file", { method: "create", access_token: accessToken }), { + path: job.remote_path, + size: hashes.size, + isdir: 0, + rtype: 1, + uploadid: uploadId, + block_list: hashes.blocks, + }); + return { remotePath: job.remote_path, sizeBytes: hashes.size, md5: hashes.fullMd5, baidu: redactJson(created) }; +} + +async function outputPathForDownload(job: TransferJobRow, filename: string): Promise { + const raw = job.local_path; + if (raw.endsWith("/") || raw.endsWith(sep)) { + const rel = relative(config.stagingDir, raw); + return resolveStagingPath(rel ? `${rel}/${filename}` : filename); + } + try { + const info = await stat(raw); + if (info.isDirectory()) { + const rel = relative(config.stagingDir, raw); + return resolveStagingPath(rel ? `${rel}/${filename}` : filename); + } + } catch { + // Missing path is treated as the final file path. + } + return raw; +} + +async function runDownloadJob(job: TransferJobRow): Promise { + if (!job.fs_id) throw new HttpError(400, "fsId is required for download jobs", { jobId: job.id }); + const { accountId, accessToken } = await getAccessToken(); + await updateTransfer(job.id, { accountId }); + const rawMeta = await fetchJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/multimedia", { + method: "filemetas", + access_token: accessToken, + fsids: JSON.stringify([Number.isFinite(Number(job.fs_id)) ? Number(job.fs_id) : job.fs_id]), + dlink: 1, + })); + const first = Array.isArray(rawMeta.list) ? asRecord(rawMeta.list[0]) : {}; + const dlink = String(first.dlink || ""); + if (!dlink) throw new BaiduApiError("Baidu file meta response is missing dlink", 502, redactJson(rawMeta)); + const filename = String(first.server_filename || first.filename || `${job.fs_id}.download`); + const outPath = await outputPathForDownload(job, filename); + mkdirSync(dirname(outPath), { recursive: true }); + const tempPath = `${outPath}.partial-${job.id}`; + const url = new URL(dlink); + url.searchParams.set("access_token", accessToken); + const response = await fetch(url, { headers: baiduHeaders(), redirect: "follow" }); + if (!response.ok || !response.body) throw new BaiduApiError(`download failed with HTTP ${response.status}`, response.status, { statusText: response.statusText }); + const total = asNumber(response.headers.get("content-length"), asNumber(first.size, 0)); + await updateTransfer(job.id, { localPath: outPath, sizeBytes: total, bytesDone: 0, remotePath: String(first.path || job.remote_path || "") }); + const writer = createWriteStream(tempPath); + const reader = response.body.getReader(); + let bytesDone = 0; + let lastUpdate = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + await assertNotCanceled(job.id); + if (value && value.byteLength > 0) { + bytesDone += value.byteLength; + if (!writer.write(Buffer.from(value))) await new Promise((resolveDrain) => writer.once("drain", resolveDrain)); + } + if (Date.now() - lastUpdate > 800) { + lastUpdate = Date.now(); + await updateTransfer(job.id, { bytesDone, sizeBytes: total }); + } + } + } catch (error) { + writer.destroy(); + throw error; + } + await new Promise((resolveEnd, rejectEnd) => { + writer.once("error", rejectEnd); + writer.end(resolveEnd); + }); + await rename(tempPath, outPath); + await updateTransfer(job.id, { bytesDone, sizeBytes: total, localPath: outPath }); + return { localPath: outPath, remotePath: String(first.path || job.remote_path || ""), fsId: job.fs_id, sizeBytes: bytesDone, filename }; +} + +async function runTransferJob(jobId: string): Promise { + const row = (await sql`SELECT * FROM baidu_netdisk_transfer_jobs WHERE id = ${jobId}`)[0]; + if (!row || row.status !== "queued") return; + await updateTransfer(jobId, { status: "running", error: "" }); + await recordTransferEvent(jobId, "info", "transfer started", { direction: row.direction }); + try { + const result = row.direction === "upload" ? await runUploadJob(row) : await runDownloadJob(row); + await updateTransfer(jobId, { status: "succeeded", result, bytesDone: asNumber(result.sizeBytes, asNumber(row.size_bytes, 0)) }); + await recordTransferEvent(jobId, "info", "transfer succeeded", result); + } catch (error) { + const canceled = await transferIsCanceled(jobId); + const message = error instanceof Error ? error.message : String(error); + await updateTransfer(jobId, { status: canceled ? "canceled" : "failed", error: message }); + await recordTransferEvent(jobId, canceled ? "warn" : "error", canceled ? "transfer canceled" : "transfer failed", { error: errorToJson(error) }); + } +} + +async function listTransfers(url: URL): Promise { + const limit = Math.min(200, Math.max(1, asNumber(url.searchParams.get("limit"), 80))); + const rows = await sql`SELECT * FROM baidu_netdisk_transfer_jobs ORDER BY updated_at DESC LIMIT ${limit}`; + return { ok: true, jobs: rows.map(transferFromRow), counts: await transferCounts() }; +} + +async function transferDetail(jobId: string): Promise { + const rows = await sql`SELECT * FROM baidu_netdisk_transfer_jobs WHERE id = ${jobId}`; + if (!rows[0]) throw new HttpError(404, "transfer job not found", { jobId }); + const events = await sql`SELECT id, level, message, data_json AS data, created_at FROM baidu_netdisk_transfer_events WHERE job_id = ${jobId} ORDER BY id DESC LIMIT 120`; + return { ok: true, job: transferFromRow(rows[0]), events: events.map((event) => ({ ...event, created_at: iso(String(event.created_at || "")) })) as JsonValue }; +} + +async function cancelTransfer(jobId: string): Promise { + const rows = await sql`UPDATE baidu_netdisk_transfer_jobs SET status = 'canceled', error = 'cancel requested', updated_at = now() WHERE id = ${jobId} AND status IN ('queued', 'running') RETURNING *`; + if (!rows[0]) return transferDetail(jobId); + await recordTransferEvent(jobId, "warn", "cancel requested"); + return { ok: true, job: transferFromRow(rows[0]) }; +} + +async function retryTransfer(jobId: string): Promise { + const rows = await sql` + UPDATE baidu_netdisk_transfer_jobs + SET status = 'queued', retry_count = retry_count + 1, error = NULL, bytes_done = 0, updated_at = now() + WHERE id = ${jobId} AND status IN ('failed', 'canceled') + RETURNING * + `; + if (!rows[0]) return transferDetail(jobId); + await recordTransferEvent(jobId, "info", "retry queued"); + setTimeout(() => { + runTransferJob(jobId).catch((error) => log("transfer_retry_uncaught", { jobId, error: errorToJson(error) })); + }, 0); + return { ok: true, job: transferFromRow(rows[0]) }; +} + +async function waitForTransferTerminal(jobId: string, timeoutMs = 90_000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const rows = await sql`SELECT * FROM baidu_netdisk_transfer_jobs WHERE id = ${jobId}`; + if (!rows[0]) throw new HttpError(404, "transfer job not found", { jobId }); + const view = transferFromRow(rows[0]); + if (["succeeded", "failed", "canceled"].includes(String(view.status))) return view; + await new Promise((resolveWait) => setTimeout(resolveWait, 800)); + } + const detail = await transferDetail(jobId); + throw new HttpError(504, "transfer self-test timed out", { jobId, detail }); +} + +async function runSelfTest(body: JsonRecord): Promise { + const timeoutMs = Math.min(300_000, Math.max(30_000, asNumber(body.timeoutMs, 90_000))); + const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14); + const filename = `unidesk-smoke-${stamp}-${randomUUID().slice(0, 8)}.txt`; + const uploadLocalRel = `self-test/${filename}`; + const downloadLocalRel = `self-test/downloaded-${filename}`; + const uploadLocalPath = resolveStagingPath(uploadLocalRel); + const remotePath = remoteFilePath(pathPosix.join(config.appRoot, filename), filename); + const content = `UniDesk Baidu Netdisk smoke test\ncreatedAt=${new Date().toISOString()}\nremotePath=${remotePath}\n`; + mkdirSync(dirname(uploadLocalPath), { recursive: true }); + await writeFile(uploadLocalPath, content, "utf8"); + const expectedMd5 = createHash("md5").update(content).digest("hex"); + + const uploadCreated = await createTransferJob("upload", { localPath: uploadLocalRel, remotePath }); + const uploadJobId = String(asRecord(uploadCreated.job).id || ""); + const uploadJob = await waitForTransferTerminal(uploadJobId, timeoutMs); + if (uploadJob.status !== "succeeded") throw new HttpError(502, "upload self-test failed", { job: uploadJob }); + + const listUrl = new URL("http://baidu-netdisk.local/api/files"); + listUrl.searchParams.set("dir", config.appRoot); + listUrl.searchParams.set("order", "time"); + listUrl.searchParams.set("desc", "1"); + listUrl.searchParams.set("limit", "100"); + const listed = await listFiles(listUrl); + const uploadedEntry = (Array.isArray(listed.files) ? listed.files : []) + .map((item) => asRecord(item)) + .find((item) => String(item.path || "") === remotePath); + const uploadResult = asRecord(uploadJob.result); + const baiduResult = asRecord(uploadResult.baidu); + const fsId = String(uploadedEntry?.fsId || baiduResult.fs_id || ""); + if (!fsId) throw new HttpError(502, "upload self-test could not find uploaded fs_id", { remotePath, listed }); + + const downloadCreated = await createTransferJob("download", { fsId, localPath: downloadLocalRel }); + const downloadJobId = String(asRecord(downloadCreated.job).id || ""); + const downloadJob = await waitForTransferTerminal(downloadJobId, timeoutMs); + if (downloadJob.status !== "succeeded") throw new HttpError(502, "download self-test failed", { job: downloadJob }); + const downloadedPath = String(downloadJob.localPath || resolveStagingPath(downloadLocalRel)); + const downloaded = await readFile(downloadedPath); + const downloadedMd5 = createHash("md5").update(downloaded).digest("hex"); + if (downloadedMd5 !== expectedMd5) { + throw new HttpError(502, "downloaded content md5 does not match uploaded fixture", { expectedMd5, downloadedMd5, downloadedPath }); + } + + const result = { + ok: true, + remotePath, + fsId, + sizeBytes: Buffer.byteLength(content), + expectedMd5, + downloadedMd5, + uploadJob, + downloadJob, + listed: Boolean(uploadedEntry), + downloadedPath, + stagingFixture: uploadLocalPath, + }; + log("self_test_succeeded", result); + return result; +} + +async function logsResponse(url: URL): Promise { + const limit = Math.min(300, Math.max(1, asNumber(url.searchParams.get("limit"), 120))); + return { ok: true, logs: recentLogs.slice(-limit).reverse() }; +} + +async function route(req: Request): Promise { + const url = new URL(req.url); + const path = url.pathname; + if (path === "/health") return jsonResponse(await health()); + if (path === "/logs") return jsonResponse(await logsResponse(url)); + if (path === "/api/auth/device/start" && req.method === "POST") return jsonResponse(await startDeviceAuth()); + if (path === "/api/auth/device/status" && req.method === "GET") return jsonResponse(await pollDeviceAuth(String(url.searchParams.get("sessionId") || ""))); + if (path === "/api/auth/status" && req.method === "GET") return jsonResponse({ ok: true, auth: await authSummary() }); + if (path === "/api/auth/refresh" && req.method === "POST") return jsonResponse(await forceRefreshAuth()); + if (path === "/api/auth/logout" && req.method === "POST") return jsonResponse(await logoutAuth()); + if (path === "/api/account" && req.method === "GET") return jsonResponse(await accountSummary(url.searchParams.get("refresh") !== "0")); + if (path === "/api/files" && req.method === "GET") return jsonResponse(await listFiles(url)); + if (path === "/api/files/meta" && req.method === "GET") return jsonResponse(await fileMetas(parseFsIds(url.searchParams.get("fsids")), url.searchParams.get("dlink") === "1")); + if (path === "/api/folders" && req.method === "POST") return jsonResponse(await createFolder(await readJsonBody(req))); + if (path === "/api/files/manage" && req.method === "POST") return jsonResponse(await manageFiles(await readJsonBody(req))); + if (path === "/api/transfers/upload-from-path" && req.method === "POST") return jsonResponse(await createTransferJob("upload", await readJsonBody(req))); + if (path === "/api/transfers/download-to-path" && req.method === "POST") return jsonResponse(await createTransferJob("download", await readJsonBody(req))); + if (path === "/api/self-test" && req.method === "POST") return jsonResponse(await runSelfTest(await readJsonBody(req))); + if (path === "/api/transfers" && req.method === "GET") return jsonResponse(await listTransfers(url)); + const transferMatch = path.match(/^\/api\/transfers\/([^/]+)(?:\/(cancel|retry))?$/u); + if (transferMatch && req.method === "GET" && !transferMatch[2]) return jsonResponse(await transferDetail(decodeURIComponent(transferMatch[1] || ""))); + if (transferMatch && req.method === "POST" && transferMatch[2] === "cancel") return jsonResponse(await cancelTransfer(decodeURIComponent(transferMatch[1] || ""))); + if (transferMatch && req.method === "POST" && transferMatch[2] === "retry") return jsonResponse(await retryTransfer(decodeURIComponent(transferMatch[1] || ""))); + return jsonResponse({ ok: false, error: "not found", path }, 404); +} + +async function resumeQueuedTransfers(): Promise { + await sql`UPDATE baidu_netdisk_transfer_jobs SET status = 'failed', error = 'service restarted before transfer completed', updated_at = now() WHERE status = 'running'`; + const rows = await sql`SELECT * FROM baidu_netdisk_transfer_jobs WHERE status = 'queued' ORDER BY created_at ASC LIMIT 20`; + for (const row of rows) { + setTimeout(() => { + runTransferJob(row.id).catch((error) => log("transfer_resume_uncaught", { jobId: row.id, error: errorToJson(error) })); + }, 0); + } +} + +await waitForSchema(); +mkdirSync(config.stagingDir, { recursive: true }); +await resumeQueuedTransfers(); + +const server = Bun.serve({ + hostname: config.host, + port: config.port, + idleTimeout: 120, + async fetch(req) { + try { + return await route(req); + } catch (error) { + return errorResponse(error); + } + }, +}); + +log("server_listening", { + url: `http://${config.host}:${server.port}`, + appRoot: config.appRoot, + stagingDir: config.stagingDir, + clientIdConfigured: Boolean(config.clientId), + tokenKeyConfigured: Boolean(config.tokenKey), +}); diff --git a/src/components/microservices/codex-queue/tsconfig.json b/src/components/microservices/baidu-netdisk/tsconfig.json similarity index 82% rename from src/components/microservices/codex-queue/tsconfig.json rename to src/components/microservices/baidu-netdisk/tsconfig.json index f62969e7..5d5f23f2 100644 --- a/src/components/microservices/codex-queue/tsconfig.json +++ b/src/components/microservices/baidu-netdisk/tsconfig.json @@ -13,5 +13,6 @@ "outDir": "dist", "skipLibCheck": true }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], + "references": [{ "path": "../../shared" }] } diff --git a/src/components/microservices/codex-queue/Dockerfile b/src/components/microservices/code-queue/Dockerfile similarity index 65% rename from src/components/microservices/codex-queue/Dockerfile rename to src/components/microservices/code-queue/Dockerfile index 103bc3b9..058eacd2 100644 --- a/src/components/microservices/codex-queue/Dockerfile +++ b/src/components/microservices/code-queue/Dockerfile @@ -15,6 +15,8 @@ RUN apt-get update \ gcc \ git \ gzip \ + iproute2 \ + iputils-ping \ jq \ make \ npm \ @@ -33,17 +35,18 @@ RUN apt-get update \ && mkdir -p /usr/local/lib/docker/cli-plugins /root/.docker/cli-plugins \ && ln -sf /usr/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose \ && ln -sf /usr/bin/docker-compose /root/.docker/cli-plugins/docker-compose \ - && npm install -g @openai/codex@0.128.0 playwright@1.59.1 \ + && npm install -g @openai/codex@0.128.0 opencode-ai@1.14.48 playwright@1.59.1 \ && playwright install --with-deps chromium \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -WORKDIR /app -COPY src/components/microservices/codex-queue/package.json ./package.json +WORKDIR /app/src/components/microservices/code-queue +COPY src/components/microservices/code-queue/package.json ./package.json RUN bun install --production -COPY src/components/microservices/codex-queue/tsconfig.json ./tsconfig.json -COPY src/components/microservices/codex-queue/src ./src +COPY src/components/shared /app/src/components/shared +COPY src/components/microservices/code-queue/tsconfig.json ./tsconfig.json +COPY src/components/microservices/code-queue/src ./src EXPOSE 4222 ENTRYPOINT ["tini", "--"] -CMD ["bun", "run", "src/index.ts"] +CMD ["bun", "--smol", "run", "src/index.ts"] diff --git a/src/components/microservices/codex-queue/package.json b/src/components/microservices/code-queue/package.json similarity index 85% rename from src/components/microservices/codex-queue/package.json rename to src/components/microservices/code-queue/package.json index e6258545..662f5bed 100644 --- a/src/components/microservices/codex-queue/package.json +++ b/src/components/microservices/code-queue/package.json @@ -1,5 +1,5 @@ { - "name": "@unidesk/codex-queue", + "name": "@unidesk/code-queue", "private": true, "type": "module", "scripts": { diff --git a/src/components/microservices/codex-queue/src/index.ts b/src/components/microservices/code-queue/src/index.ts similarity index 62% rename from src/components/microservices/codex-queue/src/index.ts rename to src/components/microservices/code-queue/src/index.ts index f1c08c69..56e590c1 100644 --- a/src/components/microservices/codex-queue/src/index.ts +++ b/src/components/microservices/code-queue/src/index.ts @@ -1,8 +1,10 @@ import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process"; import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, type Dirent } from "node:fs"; -import { dirname, resolve } from "node:path"; +import { resolve } from "node:path"; import * as readline from "node:readline"; +import { Database } from "bun:sqlite"; import postgres from "postgres"; +import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../../shared/src/rotating-jsonl"; type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; type TaskStatus = "queued" | "running" | "judging" | "retry_wait" | "succeeded" | "failed" | "canceled"; @@ -11,6 +13,7 @@ type JudgeDecision = "complete" | "retry" | "fail"; type OutputChannel = "system" | "user" | "assistant" | "reasoning" | "command" | "diff" | "tool" | "error"; type TerminalStatus = "completed" | "interrupted" | "failed" | null; type TranscriptKind = "ran" | "explored" | "edited" | "plan" | "message" | "system" | "error"; +type CodeAgentPortKind = "codex" | "opencode"; interface RuntimeConfig { host: string; @@ -19,8 +22,19 @@ interface RuntimeConfig { outputArchiveDir: string; logFile: string; defaultWorkdir: string; + mainProviderId: string; + remoteDefaultWorkdir: string; + executionProviderIds: string[]; + remoteCodexEnvKeys: string[]; codexHome: string; + opencodeXdgDir: string; sourceCodexConfig: string; + codexSqliteLogExportEnabled: boolean; + codexSqliteLogExportIntervalMs: number; + codexSqliteLogExportBatchSize: number; + codexSqliteLogMaxBytes: number; + memoryWatchdogThresholdBytes: number; + memoryWatchdogIntervalMs: number; defaultModel: string; codexModels: string[]; defaultReasoningEffort: string | null; @@ -28,11 +42,13 @@ interface RuntimeConfig { sandbox: "read-only" | "workspace-write" | "danger-full-access"; approvalPolicy: "untrusted" | "on-failure" | "on-request" | "never"; defaultMaxAttempts: number; + codeModels: string[]; minimaxApiKey: string; minimaxApiBase: string; minimaxModel: string; judgeTimeoutMs: number; judgeRepairAttempts: number; + judgeMaxTokens: number; turnNoActivityTimeoutMs: number; databaseUrl: string; databaseFlushIntervalMs: number; @@ -48,11 +64,17 @@ interface RuntimeConfig { notifyClaudeQqMaxOutboxItems: number; maxInMemoryOutputRecords: number; maxInMemoryEventRecords: number; + maxActiveQueues: number; + devContainerMasterHost: string; + devContainerDefaultProviderId: string; + devContainerImage: string; + devContainerWorkdir: string; } interface QueueTaskRequest { prompt: string; queueId?: string; + providerId?: string; cwd?: string; model?: string; reasoningEffort?: string; @@ -78,6 +100,7 @@ interface ArchivedLiveOutput extends LiveOutput { interface TranscriptLine { seq: number; at: string; + durationMs?: number; kind: TranscriptKind; title: string; status?: string; @@ -134,6 +157,7 @@ interface AttemptSummary { stderrTail: string; outputStartSeq?: number | null; outputEndSeq?: number | null; + errorCount?: number; } interface JudgeResult { @@ -171,6 +195,7 @@ interface ReferenceInjectionSummaryItem { taskId: string; viaTaskId: string | null; status: TaskStatus; + providerId: string; model: string; cwd: string; createdAt: string; @@ -209,6 +234,7 @@ interface QueueTask { basePrompt: string; referenceTaskIds: string[]; referenceInjection: ReferenceInjectionRecord | null; + providerId: string; cwd: string; model: string; reasoningEffort: string | null; @@ -280,6 +306,7 @@ interface ClaudeQqNotificationRow { interface QueueRecord { id: string; + name: string; createdAt: string; updatedAt: string; } @@ -324,6 +351,9 @@ interface JudgeProbeCase { finalResponse: string; expected: JudgeDecision; expectedContinuePromptIncludes?: string[]; + expectedContinuePromptExcludes?: string[]; + expectedContinuePromptMaxChars?: number; + expectedContinuePromptMaxLines?: number; terminalStatus: TerminalStatus; cancelRequested?: boolean; transportClosedBeforeTerminal?: boolean; @@ -333,14 +363,64 @@ interface JudgeProbeCase { events?: CodexEventSummary[]; } +interface DevContainerPlan { + providerId: string; + containerName: string; + image: string; + workdir: string; + containerWorkdir: string; + remoteCodexHome: string; + remoteOpencodeXdgDir: string; + masterHost: string; + tunId: number; + tunName: string; + serverIp: string; + clientIp: string; + natChain: string; + keyDir: string; + masterKeyPath: string; +} + +interface DevContainerCommandLog { + name: string; + providerId: string | null; + exitCode: number | null; + signal: NodeJS.Signals | null; + durationMs: number; + stdout: string; + stderr: string; +} + interface ActiveRun { taskId: string; queueId: string; - app: AppServerClient; - threadId: string; + app: CodeAgentClient; + port: CodeAgentPortKind; + threadId: string | null; turnId: string | null; } +interface ActiveRunSlotWaiter { + id: number; + taskId: string; + queueId: string; + enqueuedAt: string; +} + +interface CgroupMemoryUsage { + currentBytes: number; + inactiveFileBytes: number; + workingSetBytes: number; + swapCurrentBytes: number; + swapMaxBytes: number | null; +} + +interface CodeAgentClient { + stop(): void; + steer?(threadId: string, turnId: string, prompt: string): Promise; + interrupt?(threadId: string, turnId: string): Promise; +} + type SqlClient = postgres.Sql; type SqlExecutor = postgres.Sql | postgres.TransactionSql; @@ -354,12 +434,20 @@ const referenceInjectionMaxRounds: number | null = null; const retryBackoffBaseMs = 1000; const retryBackoffMaxMs = 10 * 60 * 1000; const queueIdPattern = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/u; +const queueNameMaxLength = 80; +const minimaxM27Model = "minimax-m2.7"; +const defaultCodeModels = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4", minimaxM27Model]; +const opencodeNpmPackage = "opencode-ai@1.14.48"; const config = readConfig(); -const logger = createLogger("codex-queue", config.logFile); +const logger = createLogger("code-queue", config.logFile); const state = emptyState(); let processing = false; const processingQueues = new Set(); const activeRuns = new Map(); +const activeRunSlotReservations = new Set(); +const activeRunSlotWaiters: ActiveRunSlotWaiter[] = []; +const devContainerEnsurePromises = new Map>(); +let nextActiveRunSlotWaiterId = 1; let devReadyCache: { checkedAtMs: number; value: JsonValue } | null = null; let persistTimer: ReturnType | null = null; let persistDirty = false; @@ -449,6 +537,40 @@ function withRequiredModel(models: string[], model: string): string[] { return models.includes(model) ? models : [model, ...models]; } +function uniqueModels(models: string[]): string[] { + return Array.from(new Set(models.map((item) => item.trim()).filter(Boolean))); +} + +function normalizeCodeModel(value: string): string { + const raw = String(value || "").trim(); + if (raw.length === 0) return raw; + const lower = raw.toLowerCase(); + const leaf = lower.includes("/") ? lower.split("/").at(-1) ?? lower : lower; + if (leaf === "minimax-m2.7" || leaf === "m2.7") return minimaxM27Model; + return raw; +} + +function codeAgentPortForModel(model: string): CodeAgentPortKind { + return normalizeCodeModel(model) === minimaxM27Model ? "opencode" : "codex"; +} + +function opencodeModels(): string[] { + return config.codeModels.filter((model) => codeAgentPortForModel(model) === "opencode"); +} + +function codeModelPorts(): Record { + return Object.fromEntries(config.codeModels.map((model) => [model, codeAgentPortForModel(model)])); +} + +function codeAgentPortInfo(kind: CodeAgentPortKind): Record { + return { + kind, + protocol: kind === "codex" ? "codex-app-server-jsonrpc-stdio" : "opencode-run-json-events", + sessionResume: kind === "codex" ? "thread/resume" : "opencode --session with persisted XDG storage and stale-session recovery", + tracePort: kind === "codex" ? "codex-transcript" : "opencode-json-event", + }; +} + function sandboxValue(raw: string): RuntimeConfig["sandbox"] { if (raw === "read-only" || raw === "workspace-write" || raw === "danger-full-access") return raw; return "danger-full-access"; @@ -460,50 +582,81 @@ function approvalValue(raw: string): RuntimeConfig["approvalPolicy"] { } function readConfig(): RuntimeConfig { - const defaultModel = envString("CODEX_QUEUE_DEFAULT_MODEL", "gpt-5.5"); - const dataDir = envString("CODEX_QUEUE_DATA_DIR", "/var/lib/unidesk/codex-queue"); - const notifyTargetTypeRaw = envString("CODEX_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE", "private").toLowerCase(); + const defaultModel = normalizeCodeModel(envString("CODE_QUEUE_DEFAULT_MODEL", "gpt-5.5")); + const codeModels = uniqueModels(withRequiredModel(envList("CODE_QUEUE_MODELS", defaultCodeModels).map(normalizeCodeModel), defaultModel)); + const dataDir = envString("CODE_QUEUE_DATA_DIR", "/var/lib/unidesk/code-queue"); + const notifyTargetTypeRaw = envString("CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE", "private").toLowerCase(); + const mainProviderId = envString("CODE_QUEUE_MAIN_PROVIDER_ID", "main-server"); + const devContainerDefaultProviderId = envString("CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID", "D601"); + const remoteDefaultWorkdir = envString("CODE_QUEUE_REMOTE_WORKDIR", "/home/ubuntu"); + const executionProviderIds = Array.from(new Set([ + mainProviderId, + ...envList("CODE_QUEUE_EXECUTION_PROVIDER_IDS", [devContainerDefaultProviderId]), + ].map((item) => item.trim()).filter(Boolean))); return { host: envString("HOST", "0.0.0.0"), port: envNumber("PORT", 4222), dataDir, - outputArchiveDir: envString("CODEX_QUEUE_OUTPUT_ARCHIVE_DIR", resolve(dataDir, "output-archive")), - logFile: envString("LOG_FILE", "/var/log/unidesk/codex-queue.jsonl"), - defaultWorkdir: envString("CODEX_QUEUE_WORKDIR", "/root/unidesk"), - codexHome: envString("CODEX_QUEUE_CODEX_HOME", "/var/lib/unidesk/codex-queue/codex-home"), - sourceCodexConfig: envString("CODEX_QUEUE_SOURCE_CODEX_CONFIG", "/root/.codex/config.toml"), + outputArchiveDir: envString("CODE_QUEUE_OUTPUT_ARCHIVE_DIR", resolve(dataDir, "output-archive")), + logFile: envString("LOG_FILE", "/var/log/unidesk/code-queue.jsonl"), + defaultWorkdir: envString("CODE_QUEUE_WORKDIR", "/root/unidesk"), + mainProviderId, + remoteDefaultWorkdir, + executionProviderIds, + remoteCodexEnvKeys: envList("CODE_QUEUE_REMOTE_CODEX_ENV_KEYS", ["OPENAI_API_KEY", "CRS_OAI_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE", "MINIMAX_API_KEY", "MINIMAX_API_BASE", "MINIMAX_MODEL"]), + codexHome: envString("CODE_QUEUE_CODEX_HOME", "/var/lib/unidesk/code-queue/codex-home"), + opencodeXdgDir: envString("CODE_QUEUE_OPENCODE_XDG_DIR", resolve(dataDir, "opencode-xdg")), + sourceCodexConfig: envString("CODE_QUEUE_SOURCE_CODEX_CONFIG", "/root/.codex/config.toml"), + codexSqliteLogExportEnabled: envBool("CODE_QUEUE_CODEX_SQLITE_LOG_EXPORT_ENABLED", true), + codexSqliteLogExportIntervalMs: Math.max(10_000, Math.min(10 * 60_000, envNumber("CODE_QUEUE_CODEX_SQLITE_LOG_EXPORT_INTERVAL_MS", 60_000))), + codexSqliteLogExportBatchSize: Math.max(100, Math.min(20_000, envNumber("CODE_QUEUE_CODEX_SQLITE_LOG_EXPORT_BATCH_SIZE", 500))), + codexSqliteLogMaxBytes: logRetentionBytesForService("codex-app-server", ["CODE_QUEUE_CODEX_SQLITE_LOG_MAX_BYTES"]), + memoryWatchdogThresholdBytes: Math.max(0, envNumber("CODE_QUEUE_MEMORY_WATCHDOG_THRESHOLD_BYTES", 0)), + memoryWatchdogIntervalMs: Math.max(1000, Math.min(60_000, envNumber("CODE_QUEUE_MEMORY_WATCHDOG_INTERVAL_MS", 2000))), defaultModel, - codexModels: withRequiredModel(envList("CODEX_QUEUE_MODELS", ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4"]), defaultModel), - defaultReasoningEffort: envNullableString("CODEX_QUEUE_REASONING_EFFORT"), - modelReasoningEfforts: envModelReasoningEfforts("CODEX_QUEUE_MODEL_REASONING_EFFORTS", { "gpt-5.5": "xhigh" }), - sandbox: sandboxValue(envString("CODEX_QUEUE_SANDBOX", "danger-full-access")), - approvalPolicy: approvalValue(envString("CODEX_QUEUE_APPROVAL_POLICY", "never")), - defaultMaxAttempts: Math.max(1, Math.min(maxTaskAttempts, envNumber("CODEX_QUEUE_MAX_ATTEMPTS", maxTaskAttempts))), + codexModels: codeModels, + defaultReasoningEffort: envNullableString("CODE_QUEUE_REASONING_EFFORT"), + modelReasoningEfforts: envModelReasoningEfforts("CODE_QUEUE_MODEL_REASONING_EFFORTS", { "gpt-5.5": "xhigh" }), + sandbox: sandboxValue(envString("CODE_QUEUE_SANDBOX", "danger-full-access")), + approvalPolicy: approvalValue(envString("CODE_QUEUE_APPROVAL_POLICY", "never")), + defaultMaxAttempts: Math.max(1, Math.min(maxTaskAttempts, envNumber("CODE_QUEUE_MAX_ATTEMPTS", maxTaskAttempts))), + codeModels, minimaxApiKey: envString("MINIMAX_API_KEY", ""), minimaxApiBase: envString("MINIMAX_API_BASE", "https://api.minimaxi.com/v1").replace(/\/+$/u, ""), minimaxModel: envString("MINIMAX_MODEL", "MiniMax-M2.7"), judgeTimeoutMs: envNumber("MINIMAX_JUDGE_TIMEOUT_MS", 60_000), judgeRepairAttempts: Math.max(0, Math.min(5, envNumber("MINIMAX_JUDGE_REPAIR_ATTEMPTS", 2))), + judgeMaxTokens: Math.max(800, Math.min(4000, envNumber("MINIMAX_JUDGE_MAX_TOKENS", 1800))), turnNoActivityTimeoutMs: Math.max(60_000, Math.min(30 * 60_000, envNumber("CODEX_TURN_NO_ACTIVITY_TIMEOUT_MS", 6 * 60_000))), databaseUrl: envRequiredString("DATABASE_URL"), - databaseFlushIntervalMs: Math.max(100, Math.min(10_000, envNumber("CODEX_QUEUE_DATABASE_FLUSH_INTERVAL_MS", 1000))), - notifyClaudeQqEnabled: envBool("CODEX_QUEUE_NOTIFY_CLAUDEQQ_ENABLED", false), - notifyClaudeQqBaseUrl: envString("CODEX_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL", "http://backend-core:8080/api/microservices/claudeqq/proxy").replace(/\/+$/u, ""), + databaseFlushIntervalMs: Math.max(100, Math.min(10_000, envNumber("CODE_QUEUE_DATABASE_FLUSH_INTERVAL_MS", 1000))), + notifyClaudeQqEnabled: envBool("CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED", false), + notifyClaudeQqBaseUrl: envString("CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL", "http://backend-core:8080/api/microservices/claudeqq/proxy").replace(/\/+$/u, ""), notifyClaudeQqTargetType: notifyTargetTypeRaw === "group" ? "group" : "private", - notifyClaudeQqUserId: envString("CODEX_QUEUE_NOTIFY_CLAUDEQQ_USER_ID", "645275593").trim(), - notifyClaudeQqGroupId: envString("CODEX_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID", "").trim(), - notifyClaudeQqMaxResponseChars: Math.max(500, Math.min(50_000, envNumber("CODEX_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS", 12_000))), - notifyClaudeQqTimeoutMs: Math.max(1000, Math.min(60_000, envNumber("CODEX_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS", 15_000))), - notifyClaudeQqSendAttempts: Math.max(1, Math.min(10, envNumber("CODEX_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS", 3))), - notifyClaudeQqRetryIntervalMs: Math.max(5_000, Math.min(10 * 60_000, envNumber("CODEX_QUEUE_NOTIFY_CLAUDEQQ_RETRY_INTERVAL_MS", 60_000))), - notifyClaudeQqMaxOutboxItems: Math.max(100, Math.min(10_000, envNumber("CODEX_QUEUE_NOTIFY_CLAUDEQQ_MAX_OUTBOX_ITEMS", 2000))), - maxInMemoryOutputRecords: envNonNegativeNumber("CODEX_QUEUE_IN_MEMORY_OUTPUT_RECORDS", 600), - maxInMemoryEventRecords: envNonNegativeNumber("CODEX_QUEUE_IN_MEMORY_EVENT_RECORDS", 400), + notifyClaudeQqUserId: envString("CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID", "645275593").trim(), + notifyClaudeQqGroupId: envString("CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID", "").trim(), + notifyClaudeQqMaxResponseChars: Math.max(500, Math.min(50_000, envNumber("CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS", 12_000))), + notifyClaudeQqTimeoutMs: Math.max(1000, Math.min(60_000, envNumber("CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS", 15_000))), + notifyClaudeQqSendAttempts: Math.max(1, Math.min(10, envNumber("CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS", 3))), + notifyClaudeQqRetryIntervalMs: Math.max(5_000, Math.min(10 * 60_000, envNumber("CODE_QUEUE_NOTIFY_CLAUDEQQ_RETRY_INTERVAL_MS", 60_000))), + notifyClaudeQqMaxOutboxItems: Math.max(100, Math.min(10_000, envNumber("CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_OUTBOX_ITEMS", 2000))), + maxInMemoryOutputRecords: envNonNegativeNumber("CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS", 10), + maxInMemoryEventRecords: envNonNegativeNumber("CODE_QUEUE_IN_MEMORY_EVENT_RECORDS", 10), + maxActiveQueues: Math.max(0, Math.min(32, envNumber("CODE_QUEUE_MAX_ACTIVE_QUEUES", 1))), + devContainerMasterHost: envString("CODE_QUEUE_DEV_CONTAINER_MASTER_HOST", "74.48.78.17"), + devContainerDefaultProviderId, + devContainerImage: envString("CODE_QUEUE_DEV_CONTAINER_IMAGE", ""), + devContainerWorkdir: envString("CODE_QUEUE_DEV_CONTAINER_WORKDIR", remoteDefaultWorkdir), }; } function createLogger(service: string, logFile: string) { - mkdirSync(dirname(logFile), { recursive: true }); + const writer = createHourlyJsonlWriter({ + baseLogFile: logFile, + service, + maxBytes: logRetentionBytesForService(service, ["CODE_QUEUE_SERVICE_LOG_MAX_BYTES"]), + }); + writer.prune(); return (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue): void => { const entry = data === undefined ? { ts: new Date().toISOString(), service, level, message } @@ -512,7 +665,7 @@ function createLogger(service: string, logFile: string) { while (recentLogs.length > 500) recentLogs.shift(); const line = `${JSON.stringify(entry)}\n`; try { - appendFileSync(logFile, line, "utf8"); + writer.appendLine(line, new Date(entry.ts)); } catch (error) { console.error(JSON.stringify({ ts: new Date().toISOString(), service, level: "error", message: "log_write_failed", error: String(error) })); } @@ -521,6 +674,238 @@ function createLogger(service: string, logFile: string) { }; } +interface CodexSqliteLogRow { + id: number; + ts: number; + ts_nanos: number; + level: string; + target: string; + feedback_log_body: string | null; + module_path: string | null; + file: string | null; + line: number | null; + thread_id: string | null; + process_uuid: string | null; + estimated_bytes: number | null; +} + +const codexSqliteLogExporter = { + running: false, + lastRunAt: null as string | null, + lastExportedRows: 0, + totalExportedRows: 0, + lastDeletedRows: 0, + lastVacuumAt: null as string | null, + lastError: null as string | null, +}; + +function codexLogDate(row: CodexSqliteLogRow): Date { + const seconds = Number(row.ts); + const nanos = Number(row.ts_nanos); + const millis = seconds * 1000 + Math.floor((Number.isFinite(nanos) ? nanos : 0) / 1_000_000); + return new Date(Number.isFinite(millis) ? millis : Date.now()); +} + +function codexLogJson(row: CodexSqliteLogRow, sqlitePath: string): Record { + return { + ts: codexLogDate(row).toISOString(), + service: "codex-app-server", + source: "codex-log-db", + sqlitePath, + id: row.id, + level: row.level, + target: row.target, + message: row.feedback_log_body ?? "", + modulePath: row.module_path, + file: row.file, + line: row.line, + threadId: row.thread_id, + processUuid: row.process_uuid, + estimatedBytes: row.estimated_bytes ?? 0, + }; +} + +function codexLogSqliteFiles(): string[] { + if (!existsSync(config.codexHome)) return []; + return readdirSync(config.codexHome, { withFileTypes: true }) + .filter((entry) => entry.isFile() && /^logs_\d+\.sqlite$/u.test(entry.name)) + .map((entry) => resolve(config.codexHome, entry.name)) + .sort(); +} + +function runSqliteMaintenance(db: Database, sqlitePath: string, forceVacuum: boolean): void { + try { + db.exec("PRAGMA wal_checkpoint(TRUNCATE)"); + } catch { + // Active Codex app-server writers may hold a lock; the next pass will retry. + } + if (!forceVacuum) return; + try { + db.exec("VACUUM"); + codexSqliteLogExporter.lastVacuumAt = new Date().toISOString(); + } catch (error) { + logger("warn", "codex_sqlite_log_vacuum_failed", { sqlitePath, error: errorToJson(error) }); + } +} + +function exportCodexSqliteLogFile(sqlitePath: string, writer: ReturnType): number { + const db = new Database(sqlitePath); + let exported = 0; + let deleted = 0; + try { + db.exec("PRAGMA busy_timeout = 2000"); + const rows = db.query("SELECT id, ts, ts_nanos, level, target, feedback_log_body, module_path, file, line, thread_id, process_uuid, estimated_bytes FROM logs ORDER BY id ASC LIMIT ?") + .all(config.codexSqliteLogExportBatchSize) as unknown as CodexSqliteLogRow[]; + for (const row of rows) { + writer.appendJson(codexLogJson(row, sqlitePath), codexLogDate(row)); + exported += 1; + } + const maxId = rows.at(-1)?.id; + if (typeof maxId === "number" && Number.isFinite(maxId)) { + const result = db.query("DELETE FROM logs WHERE id <= ?").run(maxId); + deleted = Number(result.changes ?? 0); + codexSqliteLogExporter.lastDeletedRows = deleted; + } + const size = statSync(sqlitePath).size; + runSqliteMaintenance(db, sqlitePath, size > config.codexSqliteLogMaxBytes); + return exported; + } finally { + db.close(); + } +} + +function exportCodexSqliteLogsOnce(): void { + if (!config.codexSqliteLogExportEnabled || codexSqliteLogExporter.running) return; + codexSqliteLogExporter.running = true; + codexSqliteLogExporter.lastRunAt = new Date().toISOString(); + codexSqliteLogExporter.lastExportedRows = 0; + try { + const writer = createHourlyJsonlWriter({ + baseLogFile: config.logFile, + service: "codex-app-server", + maxBytes: config.codexSqliteLogMaxBytes, + }); + let exported = 0; + for (const sqlitePath of codexLogSqliteFiles()) { + for (let batch = 0; batch < 12; batch += 1) { + const batchRows = exportCodexSqliteLogFile(sqlitePath, writer); + exported += batchRows; + if (batchRows < config.codexSqliteLogExportBatchSize) break; + } + } + writer.prune(); + codexSqliteLogExporter.lastExportedRows = exported; + codexSqliteLogExporter.totalExportedRows += exported; + codexSqliteLogExporter.lastError = null; + if (exported > 0) logger("info", "codex_sqlite_logs_exported", { rows: exported, totalRows: codexSqliteLogExporter.totalExportedRows }); + } catch (error) { + codexSqliteLogExporter.lastError = error instanceof Error ? error.message : String(error); + logger("warn", "codex_sqlite_log_export_failed", { error: errorToJson(error) }); + } finally { + codexSqliteLogExporter.running = false; + } +} + +function startCodexSqliteLogExporter(): void { + if (!config.codexSqliteLogExportEnabled) return; + setTimeout(exportCodexSqliteLogsOnce, 1000); + setInterval(exportCodexSqliteLogsOnce, config.codexSqliteLogExportIntervalMs); +} + +function readCgroupMemoryValue(path: string): number | null { + try { + const raw = readFileSync(path, "utf8").trim(); + if (raw === "max" || raw.length === 0) return null; + const value = Number(raw); + return Number.isFinite(value) && value > 0 ? value : null; + } catch { + return null; + } +} + +function readCgroupMemoryNonNegativeValue(path: string): number | null { + try { + const raw = readFileSync(path, "utf8").trim(); + if (raw === "max" || raw.length === 0) return null; + const value = Number(raw); + return Number.isFinite(value) && value >= 0 ? value : null; + } catch { + return null; + } +} + +function readCgroupMemoryStatValue(name: string): number { + try { + const raw = readFileSync("/sys/fs/cgroup/memory.stat", "utf8"); + for (const line of raw.split(/\n/u)) { + const [key, value] = line.trim().split(/\s+/u); + if (key !== name) continue; + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; + } + } catch { + // memory.stat is optional outside cgroup v2; fall back to raw memory.current. + } + return 0; +} + +function readCgroupMemoryUsage(): CgroupMemoryUsage | null { + const current = readCgroupMemoryValue("/sys/fs/cgroup/memory.current"); + if (current === null) return null; + const inactiveFile = readCgroupMemoryStatValue("inactive_file"); + const swapMax = readCgroupMemoryNonNegativeValue("/sys/fs/cgroup/memory.swap.max"); + return { + currentBytes: current, + inactiveFileBytes: inactiveFile, + workingSetBytes: Math.max(0, current - inactiveFile), + swapCurrentBytes: readCgroupMemoryNonNegativeValue("/sys/fs/cgroup/memory.swap.current") ?? 0, + swapMaxBytes: swapMax, + }; +} + +function memoryWatchdogThreshold(): number | null { + if (config.memoryWatchdogThresholdBytes > 0) return config.memoryWatchdogThresholdBytes; + const cgroupMax = readCgroupMemoryValue("/sys/fs/cgroup/memory.max"); + return cgroupMax === null ? null : Math.floor(cgroupMax * 0.98); +} + +function activeRunMemoryPressure(): (CgroupMemoryUsage & { thresholdBytes: number }) | null { + const threshold = memoryWatchdogThreshold(); + if (threshold === null || threshold <= 0) return null; + const usage = readCgroupMemoryUsage(); + const swapHasHeadroom = usage?.swapMaxBytes === null || (usage !== null && usage.swapMaxBytes > 0 && usage.swapCurrentBytes < Math.floor(usage.swapMaxBytes * 0.9)); + return usage !== null && usage.workingSetBytes >= threshold && !swapHasHeadroom ? { ...usage, thresholdBytes: threshold } : null; +} + +function startMemoryWatchdog(): void { + const threshold = memoryWatchdogThreshold(); + if (threshold === null || threshold <= 0) return; + const interval = setInterval(() => { + if (shutdownRequested || activeRuns.size === 0) return; + const usage = activeRunMemoryPressure(); + if (usage === null) return; + const runs = Array.from(activeRuns.values()); + logger("warn", "memory_watchdog_stopping_active_runs", { + currentBytes: usage.currentBytes, + inactiveFileBytes: usage.inactiveFileBytes, + workingSetBytes: usage.workingSetBytes, + swapCurrentBytes: usage.swapCurrentBytes, + swapMaxBytes: usage.swapMaxBytes, + thresholdBytes: threshold, + activeRunCount: runs.length, + taskIds: runs.map((run) => run.taskId), + }); + for (const run of runs) { + const task = findTask(run.taskId); + if (task !== null) { + appendOutput(task, "error", `Code Queue memory watchdog stopped the active ${run.port} run at ${Math.round(usage.workingSetBytes / 1024 / 1024)}MiB working set (${Math.round(usage.currentBytes / 1024 / 1024)}MiB cgroup current) to keep the memory-capped container alive.\n`, "memory/watchdog"); + } + run.app.stop(); + } + }, config.memoryWatchdogIntervalMs); + interval.unref?.(); +} + function nowIso(): string { return new Date().toISOString(); } @@ -547,9 +932,25 @@ function safeQueueId(value: unknown): string { } } +function normalizeQueueName(value: unknown, queueId: string): string { + const fallback = queueId.trim().length > 0 ? queueId : defaultQueueId; + const text = typeof value === "string" ? value.replace(/[\u0000-\u001F\u007F]+/gu, " ").replace(/\s+/gu, " ").trim() : ""; + if (text.length === 0) return fallback; + if (text.length > queueNameMaxLength) throw new Error(`queue name must be ${queueNameMaxLength} characters or fewer`); + return text; +} + +function safeQueueName(value: unknown, queueId: string): string { + try { + return normalizeQueueName(value, queueId); + } catch { + return queueId; + } +} + function emptyState(): PersistedState { const at = nowIso(); - return { version: 1, updatedAt: at, nextSeq: 1, queues: [{ id: defaultQueueId, createdAt: at, updatedAt: at }], tasks: [] }; + return { version: 1, updatedAt: at, nextSeq: 1, queues: [{ id: defaultQueueId, name: defaultQueueId, createdAt: at, updatedAt: at }], tasks: [] }; } function emptyClaudeQqNotificationOutbox(): ClaudeQqNotificationOutboxState { @@ -577,7 +978,7 @@ function notificationItemFromRow(row: ClaudeQqNotificationRow): ClaudeQqNotifica async function loadClaudeQqNotificationOutboxFromDatabase(client: SqlExecutor = sql): Promise { const rows = await client` SELECT id, kind, dedup_key, target, message, created_at, updated_at, attempts, next_attempt_at, last_error, sent_at - FROM unidesk_codex_queue_notifications + FROM unidesk_code_queue_notifications ORDER BY created_at ASC, id ASC `; claudeQqNotificationOutbox = { @@ -590,7 +991,7 @@ async function loadClaudeQqNotificationOutboxFromDatabase(client: SqlExecutor = async function upsertClaudeQqNotificationToDatabase(client: SqlExecutor, item: ClaudeQqNotificationItem): Promise { await client` - INSERT INTO unidesk_codex_queue_notifications ( + INSERT INTO unidesk_code_queue_notifications ( id, kind, dedup_key, @@ -635,7 +1036,7 @@ async function persistClaudeQqNotificationItem(item: ClaudeQqNotificationItem): const stillPresent = claudeQqNotificationOutbox.items.some((candidate) => candidate.id === item.id); await sql.begin(async (client) => { if (stillPresent) await upsertClaudeQqNotificationToDatabase(client, item); - for (const id of deletedIds) await client`DELETE FROM unidesk_codex_queue_notifications WHERE id = ${id}`; + for (const id of deletedIds) await client`DELETE FROM unidesk_code_queue_notifications WHERE id = ${id}`; }); if (stillPresent && !claudeQqNotificationOutbox.items.some((candidate) => candidate.id === item.id)) { claudeQqNotificationOutbox.items.push(item); @@ -649,16 +1050,20 @@ async function persistClaudeQqNotificationOutbox(): Promise { const deletedIds = pruneClaudeQqNotificationOutbox(); await sql.begin(async (client) => { for (const item of claudeQqNotificationOutbox.items) await upsertClaudeQqNotificationToDatabase(client, item); - for (const id of deletedIds) await client`DELETE FROM unidesk_codex_queue_notifications WHERE id = ${id}`; + for (const id of deletedIds) await client`DELETE FROM unidesk_code_queue_notifications WHERE id = ${id}`; }); } -function ensureQueue(queueId: string): QueueRecord { +function ensureQueue(queueId: string, queueName?: unknown): QueueRecord { const id = normalizeQueueId(queueId); const existing = state.queues.find((queue) => queue.id === id); - if (existing !== undefined) return existing; + if (existing !== undefined) { + existing.name = safeQueueName(existing.name, id); + if (queueName !== undefined) existing.name = normalizeQueueName(queueName, id); + return existing; + } const at = nowIso(); - const queue = { id, createdAt: at, updatedAt: at }; + const queue = { id, name: normalizeQueueName(queueName, id), createdAt: at, updatedAt: at }; state.queues.push(queue); state.queues.sort((left, right) => left.id.localeCompare(right.id)); markQueueDirty(id); @@ -735,9 +1140,14 @@ function normalizeTask(task: QueueTask): QueueTask { task.events ??= []; task.attempts ??= []; task.readAt = typeof task.readAt === "string" && task.readAt.length > 0 ? task.readAt : null; + if (task.status !== "succeeded" && task.status !== "failed" && task.status !== "canceled") { + task.finishedAt = null; + task.readAt = null; + } task.activeTurnId ??= null; + task.providerId = normalizeTaskProviderId(task.providerId); task.model ||= config.defaultModel; - task.cwd ||= config.defaultWorkdir; + task.cwd = resolveTaskCwd(task.providerId, task.cwd); task.reasoningEffort = resolveReasoningEffort(task.model, task.reasoningEffort); task.basePrompt ||= userPromptForDisplay(task.prompt); task.referenceTaskIds ??= referenceTaskIdsFromPrompt(task.prompt); @@ -749,7 +1159,7 @@ function normalizeTask(task: QueueTask): QueueTask { return task; } -function persistState(markAllDatabaseTasks = true): void { +function persistState(markAllDatabaseTasks = false): void { persistDirty = false; if (persistTimer !== null) { clearTimeout(persistTimer); @@ -795,6 +1205,11 @@ function markTaskDirty(taskId: string): void { scheduleDatabaseFlush(); } +function persistTaskState(task: QueueTask): void { + markTaskDirty(task.id); + persistState(false); +} + function markQueueDirty(queueId: string): void { dirtyDatabaseQueueIds.add(queueId); scheduleDatabaseFlush(); @@ -805,6 +1220,11 @@ function markAllDatabaseTasksDirty(): void { for (const queue of state.queues) dirtyDatabaseQueueIds.add(queue.id); } +function runGarbageCollection(): void { + const gc = (Bun as typeof Bun & { gc?: (force?: boolean) => void }).gc; + if (typeof gc === "function") gc(true); +} + function scheduleDatabaseFlush(delayMs = config.databaseFlushIntervalMs): void { if (!databaseReady || (dirtyDatabaseTaskIds.size === 0 && dirtyDatabaseQueueIds.size === 0) || shutdownRequested) return; if (databaseFlushTimer !== null) return; @@ -837,10 +1257,11 @@ function updateNextSeqFromTasks(): void { async function upsertTaskToDatabase(client: SqlExecutor, task: QueueTask): Promise { await client` - INSERT INTO unidesk_codex_queue_tasks ( + INSERT INTO unidesk_code_queue_tasks ( id, queue_id, status, + provider_id, model, cwd, prompt, @@ -868,6 +1289,7 @@ async function upsertTaskToDatabase(client: SqlExecutor, task: QueueTask): Promi ${task.id}, ${queueIdOf(task)}, ${task.status}, + ${task.providerId}, ${task.model}, ${task.cwd}, ${task.prompt}, @@ -895,6 +1317,7 @@ async function upsertTaskToDatabase(client: SqlExecutor, task: QueueTask): Promi ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, queue_id = EXCLUDED.queue_id, + provider_id = EXCLUDED.provider_id, model = EXCLUDED.model, cwd = EXCLUDED.cwd, prompt = EXCLUDED.prompt, @@ -923,20 +1346,201 @@ async function upsertTaskToDatabase(client: SqlExecutor, task: QueueTask): Promi async function upsertQueueToDatabase(client: SqlExecutor, queue: QueueRecord): Promise { await client` - INSERT INTO unidesk_codex_queue_queues ( + INSERT INTO unidesk_code_queue_queues ( id, + name, created_at, updated_at ) VALUES ( ${queue.id}, + ${safeQueueName(queue.name, queue.id)}, ${taskTimestamp(queue.createdAt) ?? nowIso()}, ${taskTimestamp(queue.updatedAt) ?? nowIso()} ) ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, updated_at = EXCLUDED.updated_at `; } +interface DatabaseTaskRow { + id: string; + updated_at: Date | string; + task_json: unknown; +} + +function normalizeDatabaseTaskRows(rows: DatabaseTaskRow[], source: string): QueueTask[] { + const tasks: QueueTask[] = []; + for (const row of rows) { + try { + tasks.push(normalizeTask(row.task_json as QueueTask)); + } catch (error) { + logger("warn", "database_task_row_ignored", { source, id: String(row.id), error: errorToJson(error) }); + } + } + return tasks.sort((left, right) => (timestampMs(left.createdAt) ?? 0) - (timestampMs(right.createdAt) ?? 0) || left.id.localeCompare(right.id)); +} + +async function loadPrunedDatabaseTaskRows(where: "all" | "hot"): Promise { + return await sql` + SELECT id, updated_at, task_json + FROM ( + SELECT + id, + updated_at, + jsonb_set( + jsonb_set( + task_json, + '{output}', + CASE + WHEN ${config.maxInMemoryOutputRecords} > 0 THEN COALESCE(( + SELECT jsonb_agg(value ORDER BY ord) + FROM ( + SELECT value, ord + FROM jsonb_array_elements( + CASE + WHEN jsonb_typeof(task_json->'output') = 'array' THEN task_json->'output' + ELSE '[]'::jsonb + END + ) WITH ORDINALITY AS output_items(value, ord) + ORDER BY ord DESC + LIMIT ${config.maxInMemoryOutputRecords} + ) AS kept_output + ), '[]'::jsonb) + ELSE CASE + WHEN jsonb_typeof(task_json->'output') = 'array' THEN task_json->'output' + ELSE '[]'::jsonb + END + END, + true + ), + '{events}', + CASE + WHEN ${config.maxInMemoryEventRecords} > 0 THEN COALESCE(( + SELECT jsonb_agg(value ORDER BY ord) + FROM ( + SELECT value, ord + FROM jsonb_array_elements( + CASE + WHEN jsonb_typeof(task_json->'events') = 'array' THEN task_json->'events' + ELSE '[]'::jsonb + END + ) WITH ORDINALITY AS event_items(value, ord) + ORDER BY ord DESC + LIMIT ${config.maxInMemoryEventRecords} + ) AS kept_events + ), '[]'::jsonb) + ELSE CASE + WHEN jsonb_typeof(task_json->'events') = 'array' THEN task_json->'events' + ELSE '[]'::jsonb + END + END, + true + ) AS task_json, + created_at, + status + FROM unidesk_code_queue_tasks + ) AS pruned_tasks + WHERE ${where === "all"} OR status IN ('queued', 'running', 'judging', 'retry_wait') + ORDER BY created_at ASC, id ASC + `; +} + +async function loadTasksFromDatabase(where: "all" | "hot" = "all"): Promise { + return normalizeDatabaseTaskRows(await loadPrunedDatabaseTaskRows(where), where); +} + +async function loadTaskFromDatabase(taskId: string): Promise { + const rows = await sql` + SELECT id, updated_at, task_json + FROM ( + SELECT + id, + updated_at, + jsonb_set( + jsonb_set( + task_json, + '{output}', + CASE + WHEN ${config.maxInMemoryOutputRecords} > 0 THEN COALESCE(( + SELECT jsonb_agg(value ORDER BY ord) + FROM ( + SELECT value, ord + FROM jsonb_array_elements( + CASE + WHEN jsonb_typeof(task_json->'output') = 'array' THEN task_json->'output' + ELSE '[]'::jsonb + END + ) WITH ORDINALITY AS output_items(value, ord) + ORDER BY ord DESC + LIMIT ${config.maxInMemoryOutputRecords} + ) AS kept_output + ), '[]'::jsonb) + ELSE CASE + WHEN jsonb_typeof(task_json->'output') = 'array' THEN task_json->'output' + ELSE '[]'::jsonb + END + END, + true + ), + '{events}', + CASE + WHEN ${config.maxInMemoryEventRecords} > 0 THEN COALESCE(( + SELECT jsonb_agg(value ORDER BY ord) + FROM ( + SELECT value, ord + FROM jsonb_array_elements( + CASE + WHEN jsonb_typeof(task_json->'events') = 'array' THEN task_json->'events' + ELSE '[]'::jsonb + END + ) WITH ORDINALITY AS event_items(value, ord) + ORDER BY ord DESC + LIMIT ${config.maxInMemoryEventRecords} + ) AS kept_events + ), '[]'::jsonb) + ELSE CASE + WHEN jsonb_typeof(task_json->'events') = 'array' THEN task_json->'events' + ELSE '[]'::jsonb + END + END, + true + ) AS task_json + FROM unidesk_code_queue_tasks + WHERE id = ${taskId} + ) AS pruned_tasks + LIMIT 1 + `; + return normalizeDatabaseTaskRows(rows, "single")[0] ?? null; +} + +function rememberHotTask(task: QueueTask): QueueTask { + const existing = findTask(task.id); + if (existing !== null) return existing; + state.tasks.push(task); + state.tasks.sort((left, right) => (timestampMs(left.createdAt) ?? 0) - (timestampMs(right.createdAt) ?? 0) || left.id.localeCompare(right.id)); + updateNextSeqFromTasks(); + return task; +} + +async function findTaskForRead(taskId: string): Promise { + return findTask(taskId) ?? await loadTaskFromDatabase(taskId); +} + +async function findTaskForMutation(taskId: string): Promise { + const task = findTask(taskId) ?? await loadTaskFromDatabase(taskId); + return task === null ? null : rememberHotTask(task); +} + +async function loadNextSeqFromDatabase(): Promise { + const rows = await sql>` + SELECT COALESCE(MAX(last_output_seq), 0) + 1 AS next_seq + FROM unidesk_code_queue_tasks + `; + const value = Number(rows[0]?.next_seq ?? 1); + return Number.isFinite(value) && value > 0 ? Math.floor(value) : 1; +} + async function flushDirtyTasksToDatabase(force = false): Promise { if (!databaseReady) return; if (databaseFlushInFlight && !force) { @@ -950,15 +1554,13 @@ async function flushDirtyTasksToDatabase(force = false): Promise { dirtyDatabaseQueueIds.clear(); databaseFlushInFlight = true; try { - const byId = new Map(state.tasks.map((task) => [task.id, task])); - const queuesById = new Map(state.queues.map((queue) => [queue.id, queue])); await sql.begin(async (client) => { for (const id of queueIds) { - const queue = queuesById.get(id); + const queue = state.queues.find((item) => item.id === id); if (queue !== undefined) await upsertQueueToDatabase(client, queue); } for (const id of ids) { - const task = byId.get(id); + const task = state.tasks.find((item) => item.id === id); if (task !== undefined) await upsertTaskToDatabase(client, task); } }); @@ -976,10 +1578,11 @@ async function flushDirtyTasksToDatabase(force = false): Promise { async function initDatabasePersistence(): Promise { logger("info", "database_persistence_init_start", { databaseUrl: redactDatabaseUrl(config.databaseUrl) }); await sql` - CREATE TABLE IF NOT EXISTS unidesk_codex_queue_tasks ( + CREATE TABLE IF NOT EXISTS unidesk_code_queue_tasks ( id TEXT PRIMARY KEY, queue_id TEXT NOT NULL DEFAULT 'default', status TEXT NOT NULL, + provider_id TEXT NOT NULL DEFAULT 'main-server', model TEXT NOT NULL, cwd TEXT NOT NULL, prompt TEXT NOT NULL, @@ -1006,14 +1609,15 @@ async function initDatabasePersistence(): Promise { ) `; await sql` - CREATE TABLE IF NOT EXISTS unidesk_codex_queue_queues ( + CREATE TABLE IF NOT EXISTS unidesk_code_queue_queues ( id TEXT PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ) `; await sql` - CREATE TABLE IF NOT EXISTS unidesk_codex_queue_notifications ( + CREATE TABLE IF NOT EXISTS unidesk_code_queue_notifications ( id TEXT PRIMARY KEY, kind TEXT NOT NULL, dedup_key TEXT NOT NULL, @@ -1027,56 +1631,53 @@ async function initDatabasePersistence(): Promise { sent_at TIMESTAMPTZ ) `; - await sql`ALTER TABLE unidesk_codex_queue_tasks ADD COLUMN IF NOT EXISTS queue_id TEXT NOT NULL DEFAULT 'default'`; - await sql`ALTER TABLE unidesk_codex_queue_tasks ADD COLUMN IF NOT EXISTS base_prompt TEXT NOT NULL DEFAULT ''`; - await sql`ALTER TABLE unidesk_codex_queue_tasks ADD COLUMN IF NOT EXISTS reference_task_ids JSONB NOT NULL DEFAULT '[]'::jsonb`; - await sql`ALTER TABLE unidesk_codex_queue_tasks ADD COLUMN IF NOT EXISTS reference_injection JSONB`; - await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_codex_queue_tasks_status_updated ON unidesk_codex_queue_tasks(status, updated_at DESC)`; - await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_codex_queue_tasks_queue_status_updated ON unidesk_codex_queue_tasks(queue_id, status, updated_at DESC)`; - await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_codex_queue_tasks_created ON unidesk_codex_queue_tasks(created_at DESC)`; - await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_codex_queue_tasks_model_updated ON unidesk_codex_queue_tasks(model, updated_at DESC)`; - await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_codex_queue_notifications_pending ON unidesk_codex_queue_notifications(sent_at, next_attempt_at)`; - await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_codex_queue_notifications_created ON unidesk_codex_queue_notifications(created_at DESC)`; + await sql`ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS queue_id TEXT NOT NULL DEFAULT 'default'`; + await sql`ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS provider_id TEXT NOT NULL DEFAULT 'main-server'`; + await sql`ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS base_prompt TEXT NOT NULL DEFAULT ''`; + await sql`ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS reference_task_ids JSONB NOT NULL DEFAULT '[]'::jsonb`; + await sql`ALTER TABLE unidesk_code_queue_tasks ADD COLUMN IF NOT EXISTS reference_injection JSONB`; + await sql`ALTER TABLE unidesk_code_queue_queues ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT ''`; + await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_code_queue_tasks_status_updated ON unidesk_code_queue_tasks(status, updated_at DESC)`; + await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_code_queue_tasks_queue_status_updated ON unidesk_code_queue_tasks(queue_id, status, updated_at DESC)`; + await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_code_queue_tasks_provider_updated ON unidesk_code_queue_tasks(provider_id, updated_at DESC)`; + await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_code_queue_tasks_created ON unidesk_code_queue_tasks(created_at DESC)`; + await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_code_queue_tasks_model_updated ON unidesk_code_queue_tasks(model, updated_at DESC)`; + await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_code_queue_notifications_pending ON unidesk_code_queue_notifications(sent_at, next_attempt_at)`; + await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_code_queue_notifications_created ON unidesk_code_queue_notifications(created_at DESC)`; - const rows = await sql>` - SELECT id, updated_at, task_json - FROM unidesk_codex_queue_tasks - ORDER BY created_at ASC, id ASC - `; - const queueRows = await sql>` - SELECT id, created_at, updated_at - FROM unidesk_codex_queue_queues + const countRows = await sql>`SELECT COUNT(*) AS count FROM unidesk_code_queue_tasks`; + const hotTasks = await loadTasksFromDatabase("hot"); + state.tasks.splice(0, state.tasks.length, ...hotTasks); + state.nextSeq = await loadNextSeqFromDatabase(); + state.updatedAt = nowIso(); + logger("info", "database_task_rows_loaded", { + databaseTaskCount: Number(countRows[0]?.count ?? hotTasks.length), + hotTaskCount: hotTasks.length, + inMemoryOutputRecords: config.maxInMemoryOutputRecords, + inMemoryEventRecords: config.maxInMemoryEventRecords, + }); + const queueRows = await sql>` + SELECT id, name, created_at, updated_at + FROM unidesk_code_queue_queues ORDER BY id ASC `; - const merged = new Map(); - for (const row of rows) { - try { - const task = normalizeTask(row.task_json as QueueTask); - merged.set(task.id, task); - } catch (error) { - logger("warn", "database_task_row_ignored", { id: String(row.id), error: errorToJson(error) }); - } - } - if (merged.size > 0) { - const tasks = Array.from(merged.values()).sort((left, right) => (timestampMs(left.createdAt) ?? 0) - (timestampMs(right.createdAt) ?? 0) || left.id.localeCompare(right.id)); - state.tasks.splice(0, state.tasks.length, ...tasks); - updateNextSeqFromTasks(); - state.updatedAt = nowIso(); - } + runGarbageCollection(); const queueMap = new Map(); for (const row of queueRows) { - queueMap.set(row.id, { - id: safeQueueId(row.id), + const id = safeQueueId(row.id); + queueMap.set(id, { + id, + name: safeQueueName(row.name, id), createdAt: taskTimestamp(String(row.created_at)) ?? nowIso(), updatedAt: taskTimestamp(String(row.updated_at)) ?? nowIso(), }); } - if (!queueMap.has(defaultQueueId)) queueMap.set(defaultQueueId, { id: defaultQueueId, createdAt: state.updatedAt, updatedAt: state.updatedAt }); + if (!queueMap.has(defaultQueueId)) queueMap.set(defaultQueueId, { id: defaultQueueId, name: defaultQueueId, createdAt: state.updatedAt, updatedAt: state.updatedAt }); for (const task of state.tasks) { const id = queueIdOf(task); const existing = queueMap.get(id); if (existing === undefined) { - queueMap.set(id, { id, createdAt: task.createdAt, updatedAt: task.updatedAt }); + queueMap.set(id, { id, name: id, createdAt: task.createdAt, updatedAt: task.updatedAt }); } else if ((timestampMs(task.updatedAt) ?? 0) > (timestampMs(existing.updatedAt) ?? 0)) { existing.updatedAt = task.updatedAt; } @@ -1084,11 +1685,13 @@ async function initDatabasePersistence(): Promise { state.queues.splice(0, state.queues.length, ...Array.from(queueMap.values()).sort((left, right) => left.id.localeCompare(right.id))); await loadClaudeQqNotificationOutboxFromDatabase(); databaseReady = true; - markAllDatabaseTasksDirty(); + for (const queue of state.queues) markQueueDirty(queue.id); await flushDirtyTasksToDatabase(true); + runGarbageCollection(); await persistClaudeQqNotificationOutbox(); logger("info", "database_persistence_init_complete", { - databaseTaskCount: rows.length, + databaseTaskCount: Number(countRows[0]?.count ?? hotTasks.length), + hotTaskCount: state.tasks.length, databaseQueueCount: queueRows.length, databaseNotificationCount: claudeQqNotificationOutbox.items.length, taskCount: state.tasks.length, @@ -1127,6 +1730,19 @@ function prepareCodexHome(): void { } } +function openCodeXdgEnv(root = config.opencodeXdgDir): Record { + return { + XDG_DATA_HOME: resolve(root, "data"), + XDG_CONFIG_HOME: resolve(root, "config"), + XDG_CACHE_HOME: resolve(root, "cache"), + XDG_STATE_HOME: resolve(root, "state"), + }; +} + +function prepareOpenCodeHome(): void { + for (const dir of Object.values(openCodeXdgEnv())) mkdirSync(dir, { recursive: true }); +} + function safePreview(value: string, max = 900): string { const compact = value.replace(/\s+/gu, " ").trim(); return compact.length > max ? `${compact.slice(0, max)}...` : compact; @@ -1544,6 +2160,165 @@ function taskTiming(task: QueueTask): JsonValue { }; } +const codexStatsTimeZone = "Asia/Shanghai"; +const codexStatsDateFormatter = new Intl.DateTimeFormat("en-CA", { + timeZone: codexStatsTimeZone, + year: "numeric", + month: "2-digit", + day: "2-digit", +}); + +interface DailyTaskStatsBucket { + date: string; + executedTasks: number; + completedTasks: number; + retryAttempts: number; + succeededTasks: number; + failedTasks: number; + canceledTasks: number; + totalDurationMs: number; + durationSamples: number; +} + +function codexStatsDateKey(value: string | Date | null | undefined): string | null { + const date = value instanceof Date ? value : typeof value === "string" && value.length > 0 ? new Date(value) : null; + if (date === null || Number.isNaN(date.getTime())) return null; + const parts = codexStatsDateFormatter.formatToParts(date).reduce>((memo, part) => { + if (part.type !== "literal") memo[part.type] = part.value; + return memo; + }, {}); + const year = parts.year; + const month = parts.month; + const day = parts.day; + return year !== undefined && month !== undefined && day !== undefined ? `${year}-${month}-${day}` : null; +} + +function shiftDateKey(dateKey: string, offsetDays: number): string { + const [year = "1970", month = "01", day = "01"] = dateKey.split("-"); + const shifted = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day) + offsetDays)); + return shifted.toISOString().slice(0, 10); +} + +function statsDaysFromUrl(url: URL): number { + const value = Number(url.searchParams.get("days") ?? 14); + return Number.isInteger(value) && value > 0 ? Math.min(90, value) : 14; +} + +function emptyDailyTaskStatsBucket(date: string): DailyTaskStatsBucket { + return { + date, + executedTasks: 0, + completedTasks: 0, + retryAttempts: 0, + succeededTasks: 0, + failedTasks: 0, + canceledTasks: 0, + totalDurationMs: 0, + durationSamples: 0, + }; +} + +function retryAttemptDates(task: QueueTask): Array { + const attempts = Array.isArray(task.attempts) ? task.attempts : []; + const retryAttempts = attempts.filter((attempt, index) => attempt.mode === "retry" || Number(attempt.index || 0) > 1 || index > 0); + const fallbackRetryCount = Math.max(0, Math.floor(Number(task.currentAttempt || 0)) - 1); + const fallbackAt = taskTimestamp(task.updatedAt) ?? taskTimestamp(task.startedAt) ?? taskTimestamp(task.createdAt); + return [ + ...retryAttempts.map((attempt) => taskTimestamp(attempt.startedAt) ?? taskTimestamp(attempt.finishedAt) ?? fallbackAt), + ...Array.from({ length: Math.max(0, fallbackRetryCount - retryAttempts.length) }, () => fallbackAt), + ]; +} + +function completedTaskDurationMs(task: QueueTask, finishedAt: string): number | null { + const startedAt = taskTimestamp(task.startedAt) ?? taskTimestamp(task.attempts[0]?.startedAt ?? null) ?? taskTimestamp(task.createdAt); + return durationMsBetween(startedAt, finishedAt); +} + +function taskStatisticsSummary(tasks: QueueTask[], days = 14): JsonValue { + const generatedAt = nowIso(); + const endDate = codexStatsDateKey(new Date()) ?? generatedAt.slice(0, 10); + const safeDays = Math.max(1, Math.min(90, Math.floor(days))); + const startDate = shiftDateKey(endDate, 1 - safeDays); + const buckets = new Map(); + for (let offset = 0; offset < safeDays; offset += 1) { + const date = shiftDateKey(startDate, offset); + buckets.set(date, emptyDailyTaskStatsBucket(date)); + } + + const bucketFor = (value: string | null): DailyTaskStatsBucket | null => { + const date = codexStatsDateKey(value); + return date === null ? null : buckets.get(date) ?? null; + }; + + for (const task of tasks) { + const executedBucket = bucketFor(taskTimestamp(task.startedAt) ?? taskTimestamp(task.attempts[0]?.startedAt ?? null)); + if (executedBucket !== null) executedBucket.executedTasks += 1; + + for (const retryAt of retryAttemptDates(task)) { + const retryBucket = bucketFor(retryAt); + if (retryBucket !== null) retryBucket.retryAttempts += 1; + } + + if (!terminalTask(task)) continue; + const finishedAt = taskTimestamp(task.finishedAt) ?? taskTimestamp(task.updatedAt); + const completedBucket = bucketFor(finishedAt); + if (finishedAt === null || completedBucket === null) continue; + completedBucket.completedTasks += 1; + if (task.status === "succeeded") completedBucket.succeededTasks += 1; + if (task.status === "failed") completedBucket.failedTasks += 1; + if (task.status === "canceled") completedBucket.canceledTasks += 1; + const durationMs = completedTaskDurationMs(task, finishedAt); + if (durationMs !== null) { + completedBucket.totalDurationMs += durationMs; + completedBucket.durationSamples += 1; + } + } + + const daily = Array.from(buckets.values()).map((bucket) => ({ + date: bucket.date, + executedTasks: bucket.executedTasks, + completedTasks: bucket.completedTasks, + retryAttempts: bucket.retryAttempts, + succeededTasks: bucket.succeededTasks, + failedTasks: bucket.failedTasks, + canceledTasks: bucket.canceledTasks, + avgDurationMs: bucket.durationSamples > 0 ? Math.round(bucket.totalDurationMs / bucket.durationSamples) : null, + totalDurationMs: bucket.totalDurationMs, + durationSamples: bucket.durationSamples, + })); + const totals = daily.reduce((memo, day) => { + memo.executedTasks += day.executedTasks; + memo.completedTasks += day.completedTasks; + memo.retryAttempts += day.retryAttempts; + memo.succeededTasks += day.succeededTasks; + memo.failedTasks += day.failedTasks; + memo.canceledTasks += day.canceledTasks; + memo.totalDurationMs += day.totalDurationMs; + memo.durationSamples += day.durationSamples; + return memo; + }, { + executedTasks: 0, + completedTasks: 0, + retryAttempts: 0, + succeededTasks: 0, + failedTasks: 0, + canceledTasks: 0, + totalDurationMs: 0, + durationSamples: 0, + }); + return { + generatedAt, + timezone: codexStatsTimeZone, + days: safeDays, + range: { startDate, endDate }, + totals: { + ...totals, + avgDurationMs: totals.durationSamples > 0 ? Math.round(totals.totalDurationMs / totals.durationSamples) : null, + }, + daily, + } as unknown as JsonValue; +} + function transcriptLine(kind: TranscriptKind, at: string, seq: number, title: string, rawSeqs: number[], body = "", command = "", status?: string, fullText = false): TranscriptLine { const bodyInfo = fullText ? completeTraceText(body) : kind === "message" ? fullMessageBody(body) : kind === "edited" ? editedOutputPreview(body) : outputPreview(body); const commandInfo = command.length > 0 ? fullText ? completeTraceText(displayCommand(command)) : commandPreview(command) : { text: "", omittedLines: 0 }; @@ -1562,7 +2337,7 @@ function transcriptLine(kind: TranscriptKind, at: string, seq: number, title: st } function commandPath(command: string): string | null { - const result = spawnSync("sh", ["-lc", `command -v ${command}`], { encoding: "utf8", timeout: 2_000 }); + const result = spawnSync("sh", ["-lc", `command -v ${shellQuote(command)}`], { encoding: "utf8", timeout: 2_000 }); if (result.status !== 0) return null; const stdout = typeof result.stdout === "string" ? result.stdout.trim() : ""; return stdout.length > 0 ? stdout.split(/\r?\n/u)[0] ?? null : null; @@ -1586,6 +2361,7 @@ function collectDevReady(): JsonValue { "npm", "npx", "codex", + "opencode", "git", "rg", "curl", @@ -1675,7 +2451,7 @@ function collectTaskIdsFromValue(value: unknown, ids: string[]): void { function referenceTaskIdsFromPrompt(prompt: string): string[] { const ids: string[] = []; const patterns = [ - /引用\s+Codex Queue\s+任务\s+(codex_\d+_[A-Za-z0-9_-]+)/giu, + /引用\s+Code Queue\s+任务\s+(codex_\d+_[A-Za-z0-9_-]+)/giu, /\bcodex\s+task\s+(codex_\d+_[A-Za-z0-9_-]+)/giu, /(?:引用|上下文|context|reference)[^\n]{0,160}\b(codex_\d+_[A-Za-z0-9_-]+)/giu, ]; @@ -1699,6 +2475,8 @@ function normalizeRequest(value: unknown): QueueTaskRequest { if (typeof record.prompt !== "string" || record.prompt.trim().length === 0) throw new Error("prompt is required"); const request: QueueTaskRequest = { prompt: record.prompt }; if (typeof record.queueId === "string" && record.queueId.trim().length > 0) request.queueId = normalizeQueueId(record.queueId); + const providerId = normalizeProviderId(record.providerId); + if (providerId !== null) request.providerId = providerId; if (typeof record.cwd === "string" && record.cwd.length > 0) request.cwd = record.cwd; if (typeof record.model === "string" && record.model.length > 0) request.model = record.model; if (typeof record.reasoningEffort === "string" && record.reasoningEffort.length > 0) request.reasoningEffort = record.reasoningEffort; @@ -1712,7 +2490,8 @@ function createTask(request: QueueTaskRequest): QueueTask { const at = nowIso(); const basePrompt = request.basePrompt ?? userPromptForDisplay(request.prompt); const referenceTaskIds = request.referenceTaskIds ?? []; - const model = request.model ?? config.defaultModel; + const providerId = normalizeTaskProviderId(request.providerId); + const model = normalizeCodeModel(request.model ?? config.defaultModel); const queueId = normalizeQueueId(request.queueId); ensureQueue(queueId); return { @@ -1723,7 +2502,8 @@ function createTask(request: QueueTaskRequest): QueueTask { basePrompt, referenceTaskIds, referenceInjection: request.referenceInjection ?? null, - cwd: resolve(request.cwd ?? config.defaultWorkdir), + providerId, + cwd: resolveTaskCwd(providerId, request.cwd), model, reasoningEffort: resolveReasoningEffort(model, request.reasoningEffort), maxAttempts: request.maxAttempts ?? config.defaultMaxAttempts, @@ -1909,6 +2689,91 @@ function commandKindLabel(kind: TranscriptKind): string { return "Ran"; } +function jsonPreview(value: unknown, max = 4000): string { + try { + return safePreview(JSON.stringify(value), max); + } catch { + return safePreview(String(value ?? ""), max); + } +} + +function recordDisplayField(record: Record | null, keys: string[], max = 3000): string { + if (record === null) return ""; + for (const key of keys) { + const value = record[key]; + if (typeof value === "string") return value; + if (value !== undefined && value !== null) return jsonPreview(value, max); + } + return ""; +} + +function openCodeRecordFromOutput(item: LiveOutput): Record | null { + if (!String(item.method || "").startsWith("opencode/")) return null; + try { + return extractRecord(JSON.parse(item.text) as unknown); + } catch { + return null; + } +} + +function openCodeToolInputCommand(tool: string, input: Record | null): string { + const command = recordStringField(input, ["command", "cmd"]); + if (command.length > 0) return command; + const path = recordStringField(input, ["filePath", "filepath", "path"]); + const pattern = recordStringField(input, ["pattern", "query"]); + const offset = recordNumberField(input, ["offset"]); + const limit = recordNumberField(input, ["limit"]); + const args: string[] = [tool]; + if (pattern.length > 0) args.push(pattern); + if (path.length > 0) args.push(path); + if (offset !== null) args.push(`offset=${offset}`); + if (limit !== null) args.push(`limit=${limit}`); + if (args.length > 1) return args.join(" "); + return input === null ? tool : `${tool} ${jsonPreview(input, 1200)}`; +} + +function openCodeToolKind(tool: string, command: string): TranscriptKind { + const normalized = `${tool} ${command}`.toLowerCase(); + if (/\b(read|grep|glob|list|ls|find|search|view|cat|sed|rg)\b/u.test(normalized)) return "explored"; + if (/\b(edit|write|patch|apply|update|create|delete|apply_patch|git apply|sed -i)\b/u.test(normalized)) return "edited"; + return commandKind(command); +} + +function openCodeToolTitle(tool: string, command: string, input: Record | null): string { + const title = recordStringField(input, ["title", "description"]); + if (title.length > 0) return safePreview(title, 180); + const path = recordStringField(input, ["filePath", "filepath", "path"]); + if (path.length > 0) return `${commandKindLabel(openCodeToolKind(tool, command))} ${path}`; + return shortCommandTitle(command || tool); +} + +function openCodeToolDurationMs(part: Record | null, state: Record | null): number | undefined { + const explicit = recordNumberField(part, ["durationMs", "elapsedMs"]) ?? recordNumberField(state, ["durationMs", "elapsedMs"]); + if (explicit !== null && explicit >= 0) return explicit; + const time = extractRecord(state?.time) ?? extractRecord(part?.time); + const start = recordNumberField(time, ["start", "startedAt"]); + const end = recordNumberField(time, ["end", "finishedAt", "completedAt"]); + return start !== null && end !== null && end >= start ? end - start : undefined; +} + +function openCodeToolTranscriptLine(item: LiveOutput, fullText: boolean): TranscriptLine | null { + const record = openCodeRecordFromOutput(item); + const part = extractRecord(record?.part); + if (record === null || part === null) return null; + const state = extractRecord(part.state); + const input = extractRecord(state?.input) ?? extractRecord(part.input); + const tool = recordStringField(part, ["tool", "title"]) || recordStringField(record, ["type", "event", "name"]) || "tool"; + const command = openCodeToolInputCommand(tool, input); + const output = recordDisplayField(state, ["output", "result"]) || recordDisplayField(part, ["output", "text", "content"]) || recordDisplayField(record, ["output", "text", "content"]); + const status = recordStringField(state, ["status"]) || recordStringField(part, ["status"]) || recordStringField(record, ["status"]); + const kind = openCodeToolKind(tool, command); + const body = output.length > 0 ? output : jsonPreview(record, 3000); + const line = transcriptLine(kind, item.at, item.seq, openCodeToolTitle(tool, command, input), [item.seq], body, command, status || item.method, fullText); + const durationMs = openCodeToolDurationMs(part, state); + if (durationMs !== undefined) line.durationMs = durationMs; + return line; +} + function promptLineCount(text: string): number { return text.length > 0 ? text.split(/\r\n|\r|\n/u).length : 0; } @@ -2071,11 +2936,13 @@ function buildTaskTranscript(task: QueueTask, limit = 180, rawOutputWindow = 0, entries.push(transcriptLine("message", item.at, item.seq, "Reasoning", [item.seq], item.text, "", item.method, fullText)); } else if (item.channel === "user") { entries.push(transcriptLine("message", item.at, item.seq, item.method === "enqueue" ? "Submitted prompt" : "User prompt", [item.seq], item.text, "", item.method, fullText)); + } else if (item.channel === "tool" && String(item.method || "").startsWith("opencode/")) { + entries.push(openCodeToolTranscriptLine(item, fullText) ?? transcriptLine("system", item.at, item.seq, "OpenCode tool", [item.seq], item.text, "", item.method, fullText)); } else { const title = item.method === "queue" && item.text.startsWith("attempt ") ? "Attempt started" : item.method === "startup" || item.method === "shutdown" - ? "Queue recovered" + ? "Recovered thread execution" : item.method === "judge" ? "Judge result" : "System"; @@ -2135,8 +3002,16 @@ function buildCompactTaskTranscript(task: QueueTask, limit = 12, rawOutputWindow entries.push(compactTaskTranscriptLine(item, "Error", "error")); } else if (item.channel === "assistant" || item.channel === "reasoning" || item.channel === "user") { entries.push(compactTaskTranscriptLine(item, item.channel === "assistant" ? "Assistant message" : item.channel === "reasoning" ? "Reasoning" : "User prompt", "message")); + } else if (item.channel === "tool" && String(item.method || "").startsWith("opencode/")) { + const line = openCodeToolTranscriptLine(item, false); + entries.push(line ?? compactTaskTranscriptLine(item, "OpenCode tool", "system")); } else { - entries.push(compactTaskTranscriptLine(item, item.method === "judge" ? "Judge result" : "System", "system")); + const title = item.method === "judge" + ? "Judge result" + : item.method === "startup" || item.method === "shutdown" + ? "Recovered thread execution" + : "System"; + entries.push(compactTaskTranscriptLine(item, title, "system")); } } return boundedTranscript(entries, limit); @@ -2202,12 +3077,18 @@ function attemptForResponse(attempt: AttemptSummary, full = false): JsonValue { function taskForResponse(task: QueueTask, full = false, includeRaw = full): JsonValue { const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt); + const stepCount = taskLlmStepCount(task); return { ...task, + agentPort: codeAgentPortForModel(task.model), + agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)), judgeFailRetryLimit, + stepCount, + llmStepCount: stepCount, prompt: full ? task.prompt : safePreview(task.prompt, 2000), basePrompt: full ? task.basePrompt : safePreview(task.basePrompt, 2000), displayPrompt: full ? displayPrompt : safePreview(displayPrompt, 2000), + promptEditable: queuedTaskPromptEditable(task), referenceTaskIds: task.referenceTaskIds, referenceInjection: task.referenceInjection, finalResponse: full ? task.finalResponse : safePreview(task.finalResponse, 5000), @@ -2223,10 +3104,15 @@ function fullTranscript(task: QueueTask): TranscriptLine[] { return cachedFullTranscript(task); } +function taskLlmStepCount(task: QueueTask): number { + return taskStepCountFromTranscript(task, cachedPreviewTranscript(task)); +} + function taskForMetaResponse(task: QueueTask): JsonValue { const fullOutput = taskFullOutput(task); const lastOutputSeq = fullOutput.at(-1)?.seq ?? 0; const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt); + const stepCount = taskLlmStepCount(task); return { id: task.id, queueId: queueIdOf(task), @@ -2237,12 +3123,18 @@ function taskForMetaResponse(task: QueueTask): JsonValue { promptChars: task.prompt.length, basePromptChars: task.basePrompt.length, displayPromptChars: displayPrompt.length, + promptEditable: queuedTaskPromptEditable(task), finalResponseChars: task.finalResponse.length, + stepCount, + llmStepCount: stepCount, summaryOnly: false, referenceTaskIds: task.referenceTaskIds, referenceInjection: task.referenceInjection, + providerId: task.providerId, cwd: task.cwd, model: task.model, + agentPort: codeAgentPortForModel(task.model), + agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)), reasoningEffort: task.reasoningEffort, maxAttempts: task.maxAttempts, status: task.status, @@ -2280,6 +3172,7 @@ function taskForMetaResponse(task: QueueTask): JsonValue { function taskForCompactMetaResponse(task: QueueTask): JsonValue { const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt); const lastOutputSeq = task.output.at(-1)?.seq ?? 0; + const stepCount = taskLlmStepCount(task); return { id: task.id, queueId: queueIdOf(task), @@ -2290,7 +3183,10 @@ function taskForCompactMetaResponse(task: QueueTask): JsonValue { promptChars: task.prompt.length, basePromptChars: task.basePrompt.length, displayPromptChars: displayPrompt.length, + promptEditable: queuedTaskPromptEditable(task), finalResponseChars: task.finalResponse.length, + stepCount, + llmStepCount: stepCount, summaryOnly: true, referenceTaskIds: task.referenceTaskIds, referenceInjection: task.referenceInjection === null ? null : { @@ -2300,8 +3196,11 @@ function taskForCompactMetaResponse(task: QueueTask): JsonValue { maxRounds: task.referenceInjection.maxRounds, truncated: task.referenceInjection.truncated, }, + providerId: task.providerId, cwd: task.cwd, model: task.model, + agentPort: codeAgentPortForModel(task.model), + agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)), reasoningEffort: task.reasoningEffort, maxAttempts: task.maxAttempts, status: task.status, @@ -2459,33 +3358,70 @@ function transcriptLineSummaryLines(line: TranscriptLine): string[] { return lines.slice(0, 4); } +function isToolActionLine(line: TranscriptLine): boolean { + return line.kind === "ran" || line.kind === "explored" || line.kind === "edited"; +} + +function toolStepCountsFromTranscript(lines: TranscriptLine[]): { toolCallCount: number; readCount: number; editCount: number; runCount: number } { + const counts = lines.reduce((memo, line) => { + if (line.kind === "explored") memo.readCount += 1; + else if (line.kind === "edited") memo.editCount += 1; + else if (line.kind === "ran") memo.runCount += 1; + return memo; + }, { readCount: 0, editCount: 0, runCount: 0 }); + return { + ...counts, + toolCallCount: counts.readCount + counts.editCount + counts.runCount, + }; +} + +function llmStepCountFromTranscript(lines: TranscriptLine[], _fallback = 0): number { + return toolStepCountsFromTranscript(lines).toolCallCount; +} + +function realAttemptWindows(task: QueueTask, transcript: TranscriptLine[]): TraceAttemptWindow[] { + return traceAttemptWindows(task, transcript).filter((window) => window.synthetic !== true && window.index > 0); +} + +function taskStepCountFromTranscript(task: QueueTask, transcript: TranscriptLine[]): number { + const windows = realAttemptWindows(task, transcript); + if (windows.length === 0) return llmStepCountFromTranscript(transcript); + return windows.reduce((sum, window) => sum + llmStepCountFromTranscript(executionLinesForAttempt(window.lines)), 0); +} + +function taskExecutionTranscript(task: QueueTask, transcript: TranscriptLine[]): TranscriptLine[] { + const windows = realAttemptWindows(task, transcript); + if (windows.length === 0) return transcript; + return sortTranscript(windows.flatMap((window) => executionLinesForAttempt(window.lines))); +} + function executionSummaryFromTranscript( task: QueueTask, transcript: TranscriptLine[], timing: Record, outputCount = taskFullOutput(task).length, retainedOutputCount = task.output.length, + fallbackStepCount = 0, ): JsonValue { - const toolLines = transcript.filter((line) => line.kind === "ran" || line.kind === "explored" || line.kind === "edited"); + const toolLines = transcript.filter(isToolActionLine); const editedFiles = uniqueStrings(toolLines.flatMap((line) => line.kind === "edited" ? parseEditedFilesFromText(`${line.title}\n${line.bodyPreview ?? ""}`) : []), 40); const commands = uniqueStrings(toolLines .map((line) => String(line.commandPreview || "").split(/\r?\n/u).find((row) => row.trim().length > 0)?.trim() ?? "") .filter(Boolean), 30); - const counts = toolLines.reduce>((memo, line) => { - memo[line.kind] = (memo[line.kind] ?? 0) + 1; - return memo; - }, {}); + const counts = toolStepCountsFromTranscript(transcript); return { durationMs: timing.durationMs ?? timing.totalElapsedMs ?? null, totalElapsedMs: timing.totalElapsedMs ?? null, queueWaitMs: timing.queueWaitMs ?? null, - toolCallCount: toolLines.length, - readCount: counts.explored ?? 0, - editCount: counts.edited ?? 0, - runCount: counts.ran ?? 0, + toolCallCount: counts.toolCallCount, + readCount: counts.readCount, + editCount: counts.editCount, + runCount: counts.runCount, editedFiles, commands, - stepCount: transcript.filter((line) => line.title !== "Submitted prompt").length, + stepCount: counts.toolCallCount, + llmStepCount: counts.toolCallCount, + traceLineCount: transcript.filter((line) => line.title !== "Submitted prompt").length, transcriptMaxSeq: transcript.at(-1)?.seq ?? 0, outputCount, retainedOutputCount, @@ -2493,7 +3429,7 @@ function executionSummaryFromTranscript( } function taskExecutionSummary(task: QueueTask, transcript = cachedPreviewTranscript(task)): JsonValue { - return executionSummaryFromTranscript(task, transcript, taskTiming(task) as Record); + return executionSummaryFromTranscript(task, taskExecutionTranscript(task, transcript), taskTiming(task) as Record, undefined, undefined, task.currentAttempt || task.attempts.length); } function parseAttemptIndex(text: string): number | null { @@ -2591,6 +3527,63 @@ interface TraceAttemptWindow { label?: string; } +function transcriptLineSeq(line: TranscriptLine | undefined): number | null { + const value = Number(line?.seq ?? NaN); + return Number.isFinite(value) ? value : null; +} + +function traceWindowFirstSeq(window: TraceAttemptWindow): number { + return transcriptLineSeq(window.lines[0]) ?? window.startSeq ?? Number.POSITIVE_INFINITY; +} + +function isRecoveredThreadLine(line: TranscriptLine): boolean { + const status = String(line.status || ""); + const text = `${line.title}\n${line.bodyPreview ?? ""}\n${line.commandPreview ?? ""}`; + return line.kind === "system" && ( + line.title === "Recovered thread execution" + || line.title === "Queue recovered" + || status === "startup" + || status === "shutdown" + || /\btask queued for retry\b|Service (?:re)?started while task was active|Service stopping while task was active/iu.test(text) + ); +} + +function mergeTraceWindowLines(left: TranscriptLine[], right: TranscriptLine[]): TranscriptLine[] { + const seen = new Set(); + const merged: TranscriptLine[] = []; + for (const line of [...left, ...right]) { + const key = `${line.seq}:${line.title}:${line.status ?? ""}`; + if (seen.has(key)) continue; + seen.add(key); + merged.push(line); + } + return sortTranscript(merged); +} + +function minNullableSeq(left: number | null, right: number | null): number | null { + if (left === null) return right; + if (right === null) return left; + return Math.min(left, right); +} + +function isSystemOnlyOrphanGroup(lines: TranscriptLine[]): boolean { + const executionLines = executionLinesForAttempt(lines); + return executionLines.length > 0 && executionLines.every((line) => line.kind === "system"); +} + +function mergeSystemGroupIntoNextAttempt(windows: TraceAttemptWindow[], lines: TranscriptLine[]): boolean { + if (!isSystemOnlyOrphanGroup(lines)) return false; + const groupEndSeq = transcriptLineSeq(lines.at(-1)); + if (groupEndSeq === null) return false; + const target = windows + .filter((window) => window.synthetic !== true && traceWindowFirstSeq(window) > groupEndSeq) + .sort((left, right) => traceWindowFirstSeq(left) - traceWindowFirstSeq(right))[0]; + if (target === undefined) return false; + target.lines = mergeTraceWindowLines(lines, target.lines); + target.startSeq = minNullableSeq(target.startSeq, transcriptLineSeq(lines[0])); + return true; +} + function traceAttemptWindows(task: QueueTask, transcript: TranscriptLine[]): TraceAttemptWindow[] { const starts = transcript .map((line, position) => ({ line, position, index: line.title === "Attempt started" ? traceAttemptIndexFromLine(line) : null })) @@ -2645,7 +3638,9 @@ function traceAttemptWindows(task: QueueTask, transcript: TranscriptLine[]): Tra let syntheticIndex = -1; for (const lines of orphanedGroups) { + if (mergeSystemGroupIntoNextAttempt(windows, lines)) continue; const hasSteer = lines.some((line) => line.title === "Steer prompt" || line.status === "turn/steer"); + const hasRecovery = lines.some(isRecoveredThreadLine); const startSeq = Number(lines[0]?.seq ?? NaN); const endSeq = Number(lines.at(-1)?.seq ?? NaN); windows.push({ @@ -2655,7 +3650,9 @@ function traceAttemptWindows(task: QueueTask, transcript: TranscriptLine[]): Tra endSeq: Number.isFinite(endSeq) ? endSeq : null, lines, synthetic: true, - label: hasSteer ? "Recovered thread execution with steer prompt" : "Recovered thread execution", + label: hasRecovery + ? hasSteer ? "Recovered thread execution with steer prompt" : "Recovered thread execution" + : hasSteer ? "Recovered thread execution with steer prompt" : "System events", }); syntheticIndex -= 1; } @@ -2678,6 +3675,7 @@ function taskTraceAttemptSummaries(task: QueueTask, transcript: TranscriptLine[] const finalResponse = synthetic ? "" : String(attempt?.finalResponse ?? attempt?.finalResponsePreview ?? (window.index === task.attempts.length ? task.finalResponse : "")); const finalResponseChars = Number(attempt?.finalResponseChars ?? finalResponse.length); const executionLines = executionLinesForAttempt(window.lines); + const errorCount = executionLines.filter((line) => line.kind === "error").length; const feedbackPrompt = synthetic ? null : attemptFeedbackPromptRecord(task, window.index, attempt, judge); const inputPrompt = promptSnapshot(String(attempt?.inputPrompt ?? ""), 1200); return { @@ -2714,13 +3712,21 @@ function taskTraceAttemptSummaries(task: QueueTask, transcript: TranscriptLine[] feedbackPromptSource: feedbackPrompt?.source ?? null, feedbackPromptForAttempt: feedbackPrompt?.forAttempt ?? null, feedbackPromptTruncated: feedbackPrompt?.truncated ?? false, - execution: executionSummaryFromTranscript(task, executionLines, attemptTimingSummary(attempt, executionLines.length > 0 ? executionLines : window.lines), window.lines.length, window.lines.length), + errorCount, + execution: executionSummaryFromTranscript( + task, + executionLines, + attemptTimingSummary(attempt, executionLines.length > 0 ? executionLines : window.lines), + window.lines.length, + window.lines.length, + synthetic || window.index <= 0 ? 0 : 1, + ), }; }) as unknown as JsonValue[]; } function resolvedReferencePromptParts(prompt: string): { reference: string; userPrompt: string } { - const withoutEnvironment = stripCodexQueueEnvironmentHint(prompt); + const withoutEnvironment = stripCodeQueueEnvironmentHint(prompt); const trimmed = withoutEnvironment.trimStart(); if (!trimmed.startsWith(resolvedReferenceContextTitle)) { return { reference: "", userPrompt: userPromptForDisplay(prompt) }; @@ -2760,11 +3766,20 @@ function taskTracePromptSummary(task: QueueTask): JsonValue { function taskTraceSummaryResponse(task: QueueTask): JsonValue { const transcript = cachedPreviewTranscript(task); const attempts = taskTraceAttemptSummaries(task, transcript); + const stepCount = taskStepCountFromTranscript(task, transcript); + const errorCount = attempts.reduce((sum: number, attempt: JsonValue) => { + if (typeof attempt !== "object" || attempt === null || Array.isArray(attempt)) return sum; + const value = Number((attempt as { errorCount?: unknown }).errorCount || 0); + return sum + (Number.isFinite(value) && value > 0 ? value : 0); + }, 0); return { id: task.id, queueId: queueIdOf(task), status: task.status, + providerId: task.providerId, model: task.model, + agentPort: codeAgentPortForModel(task.model), + agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)), cwd: task.cwd, reasoningEffort: task.reasoningEffort, createdAt: task.createdAt, @@ -2773,12 +3788,16 @@ function taskTraceSummaryResponse(task: QueueTask): JsonValue { updatedAt: task.updatedAt, currentAttempt: task.currentAttempt, maxAttempts: task.maxAttempts, + stepCount, + llmStepCount: stepCount, + promptEditable: queuedTaskPromptEditable(task), prompt: taskTracePromptSummary(task), execution: taskExecutionSummary(task, transcript), finalResponse: task.finalResponse, finalResponseChars: task.finalResponse.length, lastJudge: task.lastJudge, lastError: task.lastError, + errorCount, attempts, timing: taskTiming(task), } as unknown as JsonValue; @@ -2845,6 +3864,7 @@ function taskPromptDetailResponse(task: QueueTask, url: URL): Response { function taskTraceStepsResponse(task: QueueTask, url: URL): Response { const limit = parseLimit(url); const attemptIndex = parseSeqParam(url, "attempt", null); + const agentPort = codeAgentPortForModel(task.model); const previewTranscript = cachedPreviewTranscript(task); const attemptWindow = attemptIndex === null ? null : traceAttemptWindows(task, previewTranscript).find((window) => window.index === attemptIndex) ?? null; const sourceTranscript = attemptWindow === null ? previewTranscript : executionLinesForAttempt(attemptWindow.lines); @@ -2856,6 +3876,8 @@ function taskTraceStepsResponse(task: QueueTask, url: URL): Response { queueId: queueIdOf(task), status: task.status, updatedAt: task.updatedAt, + agentPort, + agentPortInfo: codeAgentPortInfo(agentPort), attempt: attemptIndex, steps: page.chunk.map((line) => ({ seq: line.seq, @@ -2863,6 +3885,7 @@ function taskTraceStepsResponse(task: QueueTask, url: URL): Response { kind: line.kind, title: line.title, status: line.status ?? null, + durationMs: line.durationMs ?? null, rawSeqs: line.rawSeqs, summaryLines: transcriptLineSummaryLines(line), hasDetail: true, @@ -2882,6 +3905,7 @@ function taskTraceStepsResponse(task: QueueTask, url: URL): Response { function taskTraceStepDetailResponse(task: QueueTask, url: URL): Response { const seq = Number(url.searchParams.get("seq")); if (!Number.isFinite(seq)) return jsonResponse({ ok: false, error: "seq is required" }, 400); + const agentPort = codeAgentPortForModel(task.model); const transcript = fullTranscript(task).filter((line) => line.title !== "Submitted prompt"); const line = transcript.find((item) => Number(item.seq) === seq || item.rawSeqs.includes(seq)); if (line === undefined) return jsonResponse({ ok: false, error: "trace step not found", seq }, 404); @@ -2891,6 +3915,8 @@ function taskTraceStepDetailResponse(task: QueueTask, url: URL): Response { queueId: queueIdOf(task), status: task.status, updatedAt: task.updatedAt, + agentPort, + agentPortInfo: codeAgentPortInfo(agentPort), seq, line, }); @@ -2903,7 +3929,10 @@ function taskSummaryResponse(task: QueueTask, url: URL): JsonValue { id: task.id, queueId: queueIdOf(task), status: task.status, + providerId: task.providerId, model: task.model, + agentPort: codeAgentPortForModel(task.model), + agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)), cwd: task.cwd, reasoningEffort: task.reasoningEffort, maxAttempts: task.maxAttempts, @@ -2921,6 +3950,7 @@ function taskSummaryResponse(task: QueueTask, url: URL): JsonValue { initialPrompt: task.prompt, basePrompt: task.basePrompt, prompt: task.prompt, + promptEditable: queuedTaskPromptEditable(task), referenceTaskIds: task.referenceTaskIds, referenceInjection: task.referenceInjection, lastAssistantMessage: lastAssistantMessage(task, transcript), @@ -2941,17 +3971,17 @@ function taskSummaryResponse(task: QueueTask, url: URL): JsonValue { } as unknown as JsonValue; } -const resolvedReferenceContextTitle = "# Codex Queue 已解析引用上下文"; +const resolvedReferenceContextTitle = "# Code Queue 已解析引用上下文"; const currentTaskPromptMarker = "\n# 本次任务\n"; -const codexQueueEnvironmentHintTitle = "# Codex Queue 运行环境提示"; -const codexQueueEnvironmentHint = [ - codexQueueEnvironmentHintTitle, - "如果当前 Codex Queue Docker 容器缺少完成任务所需的环境、系统包或语言依赖,可以先在容器内临时安装以推进当前任务;同时必须把该依赖补到 `src/components/microservices/codex-queue/Dockerfile`,让后续任务重建镜像后可直接使用。", +const codeQueueEnvironmentHintTitle = "# Code Queue 运行环境提示"; +const codeQueueEnvironmentHint = [ + codeQueueEnvironmentHintTitle, + "如果当前 Code Queue Docker 容器缺少完成任务所需的环境、系统包或语言依赖,可以先在容器内临时安装以推进当前任务;同时必须把该依赖补到 `src/components/microservices/code-queue/Dockerfile`,让后续任务重建镜像后可直接使用。", ].join("\n"); function stripAutoReferenceHint(prompt: string): string { const trimmed = prompt.trimStart(); - if (!/^引用\s+Codex Queue\s+任务\s+codex_\d+_[A-Za-z0-9_-]+/u.test(trimmed)) return prompt; + if (!/^引用\s+Code Queue\s+任务\s+codex_\d+_[A-Za-z0-9_-]+/u.test(trimmed)) return prompt; const marker = "\n\n本次任务:"; const index = trimmed.indexOf(marker); if (index === -1) return prompt; @@ -2967,9 +3997,9 @@ function stripResolvedReferenceContext(prompt: string): string { return prompt.slice(index + currentTaskPromptMarker.length).trimStart(); } -function stripCodexQueueEnvironmentHint(prompt: string): string { +function stripCodeQueueEnvironmentHint(prompt: string): string { const trimmed = prompt.trimStart(); - if (!trimmed.startsWith(codexQueueEnvironmentHintTitle)) return prompt; + if (!trimmed.startsWith(codeQueueEnvironmentHintTitle)) return prompt; const offset = prompt.length - trimmed.length; const index = prompt.indexOf(currentTaskPromptMarker, offset); if (index < offset) return prompt; @@ -2977,18 +4007,78 @@ function stripCodexQueueEnvironmentHint(prompt: string): string { } function userPromptForDisplay(prompt: string): string { - return stripAutoReferenceHint(stripResolvedReferenceContext(stripCodexQueueEnvironmentHint(prompt))); + return stripAutoReferenceHint(stripResolvedReferenceContext(stripCodeQueueEnvironmentHint(prompt))); } -function promptWithCodexQueueEnvironmentHint(prompt: string): string { - if (prompt.trimStart().startsWith(codexQueueEnvironmentHintTitle)) return prompt; - return [codexQueueEnvironmentHint, "", "# 本次任务", prompt.trim()].join("\n"); +function promptWithCodeQueueEnvironmentHint(prompt: string): string { + if (prompt.trimStart().startsWith(codeQueueEnvironmentHintTitle)) return prompt; + return [codeQueueEnvironmentHint, "", "# 本次任务", prompt.trim()].join("\n"); } -function injectCodexQueueEnvironmentHint(request: QueueTaskRequest): QueueTaskRequest { - if (request.prompt.trimStart().startsWith(codexQueueEnvironmentHintTitle)) return request; +function injectCodeQueueEnvironmentHint(request: QueueTaskRequest): QueueTaskRequest { + if (request.prompt.trimStart().startsWith(codeQueueEnvironmentHintTitle)) return request; const basePrompt = request.basePrompt ?? userPromptForDisplay(request.prompt); - return { ...request, prompt: promptWithCodexQueueEnvironmentHint(request.prompt), basePrompt }; + return { ...request, prompt: promptWithCodeQueueEnvironmentHint(request.prompt), basePrompt }; +} + +function taskReferencesEqual(left: string[], right: string[]): boolean { + if (left.length !== right.length) return false; + return left.every((value, index) => value === right[index]); +} + +function queuedTaskPromptEditable(task: QueueTask): boolean { + return task.status === "queued" + && task.startedAt === null + && task.currentAttempt === 0 + && task.codexThreadId === null + && task.nextMode === null + && task.nextPrompt === null + && task.attempts.length === 0; +} + +function normalizePromptEditRequest(task: QueueTask, value: unknown): QueueTaskRequest { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("request body must be an object"); + const record = value as Record; + if (typeof record.prompt !== "string" || record.prompt.trim().length === 0) throw new Error("prompt is required"); + const referenceTaskIds: string[] = []; + const hasExplicitReferenceIds = Object.prototype.hasOwnProperty.call(record, "referenceTaskId") + || Object.prototype.hasOwnProperty.call(record, "referenceTaskIds"); + if (hasExplicitReferenceIds) { + collectTaskIdsFromValue(record.referenceTaskId, referenceTaskIds); + collectTaskIdsFromValue(record.referenceTaskIds, referenceTaskIds); + } else { + for (const id of task.referenceTaskIds ?? []) addUniqueTaskId(referenceTaskIds, id); + } + for (const id of referenceTaskIdsFromPrompt(record.prompt)) addUniqueTaskId(referenceTaskIds, id); + if (referenceTaskIds.includes(task.id)) throw new Error("a task cannot reference itself while editing prompt"); + return { + prompt: record.prompt, + basePrompt: userPromptForDisplay(record.prompt), + referenceTaskIds, + queueId: queueIdOf(task), + providerId: task.providerId, + cwd: task.cwd, + model: task.model, + reasoningEffort: task.reasoningEffort ?? undefined, + maxAttempts: task.maxAttempts, + }; +} + +function buildQueuedPromptUpdate(task: QueueTask, body: unknown): QueueTaskRequest { + return injectCodeQueueEnvironmentHint(injectReferencedTaskContext(normalizePromptEditRequest(task, body))); +} + +function rewriteEnqueueOutput(task: QueueTask): void { + const text = `${task.prompt}\n`; + const output = taskFullOutput(task).find((item) => item.channel === "user" && item.method === "enqueue") ?? null; + if (output === null) return; + const nextOutput = { ...output, at: task.updatedAt, text }; + const inMemory = task.output.find((item) => item.seq === output.seq); + if (inMemory !== undefined) { + inMemory.at = nextOutput.at; + inMemory.text = nextOutput.text; + } + appendOutputArchive(task, nextOutput, "set", text); } function taskReferenceIds(task: QueueTask): string[] { @@ -3011,6 +4101,7 @@ function referenceSummaryItem(task: QueueTask, round: number, roundIndex: number taskId: task.id, viaTaskId, status: task.status, + providerId: task.providerId, model: task.model, cwd: task.cwd, createdAt: task.createdAt, @@ -3088,7 +4179,7 @@ function referencedTaskContext(task: QueueTask, summary: ReferenceInjectionSumma return [ `## Round ${summary.round}.${summary.roundIndex} referenced task ${task.id}`, `- via: ${summary.viaTaskId ?? "direct"}`, - `- status/model/cwd: ${task.status} / ${task.model} / ${task.cwd}`, + `- status/provider/model/cwd: ${task.status} / ${task.providerId} / ${task.model} / ${task.cwd}`, `- created/updated: ${task.createdAt} / ${task.updatedAt}`, `- cli: bun scripts/cli.ts codex task ${task.id}`, "", @@ -3109,7 +4200,7 @@ function injectReferencedTaskContext(request: QueueTaskRequest, finder: (id: str if (ids.length > 5) throw new Error(`referenceTaskIds supports at most 5 task ids, got ${ids.length}`); const referencedTasks = ids.map((id) => finder(id)); const missing = ids.filter((_id, index) => referencedTasks[index] === null); - if (missing.length > 0) throw new Error(`referenced Codex Queue task not found: ${missing.join(", ")}`); + if (missing.length > 0) throw new Error(`referenced Code Queue task not found: ${missing.join(", ")}`); const userPrompt = request.basePrompt ?? userPromptForDisplay(request.prompt); const graph = collectReferenceGraph(ids, referenceInjectionMaxRounds, finder); const injection: ReferenceInjectionRecord = { @@ -3132,7 +4223,7 @@ function injectReferencedTaskContext(request: QueueTaskRequest, finder: (id: str `injectedAt: ${injectedAt}`, `directReferences: ${ids.join(", ")}`, `referenceGraphItems: ${graph.items.length}${graph.truncated ? " (truncated)" : ""}`, - "说明:Codex Queue 后端只读取每个被引用任务的结构化 basePrompt(注入前 prompt)和 final/last response;不会把历史引用注入块继续套入。多轮引用按上游/最早上下文在前、直接引用在后的顺序注入;中间执行过程不注入,只保留 CLI 查询提示。", + "说明:Code Queue 后端只读取每个被引用任务的结构化 basePrompt(注入前 prompt)和 final/last response;不会把历史引用注入块继续套入。多轮引用按上游/最早上下文在前、直接引用在后的顺序注入;中间执行过程不注入,只保留 CLI 查询提示。", "", ...(groupedItems.flatMap((group) => [ referenceRoundSeparator(group.round, groupedItems.length, group.items), @@ -3203,6 +4294,7 @@ function pageBySeq(items: T[], url: URL, limit: numbe function transcriptChunkResponse(task: QueueTask, url: URL): Response { const limit = parseLimit(url); const fullText = truthyParam(url, "fullText") || truthyParam(url, "raw"); + const agentPort = codeAgentPortForModel(task.model); const transcript = fullText ? fullTranscript(task) : cachedPreviewTranscript(task); const page = pageBySeq(transcript, url, limit); return jsonResponse({ @@ -3211,6 +4303,8 @@ function transcriptChunkResponse(task: QueueTask, url: URL): Response { queueId: queueIdOf(task), status: task.status, updatedAt: task.updatedAt, + agentPort, + agentPortInfo: codeAgentPortInfo(agentPort), mode: page.mode, transcript: page.chunk, afterSeq: page.afterSeq, @@ -3266,6 +4360,7 @@ function outputChunkResponse(task: QueueTask, url: URL): Response { function taskForListResponse(task: QueueTask, lite = false): JsonValue { const timing = taskTiming(task); const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt); + const stepCount = taskLlmStepCount(task); if (lite) { return { id: task.id, @@ -3277,7 +4372,10 @@ function taskForListResponse(task: QueueTask, lite = false): JsonValue { promptChars: task.prompt.length, basePromptChars: task.basePrompt.length, displayPromptChars: displayPrompt.length, + promptEditable: queuedTaskPromptEditable(task), finalResponseChars: task.finalResponse.length, + stepCount, + llmStepCount: stepCount, summaryOnly: true, referenceTaskIds: task.referenceTaskIds, referenceInjectionSummary: task.referenceInjection === null ? null : { @@ -3287,8 +4385,11 @@ function taskForListResponse(task: QueueTask, lite = false): JsonValue { maxRounds: task.referenceInjection.maxRounds, truncated: task.referenceInjection.truncated, }, + providerId: task.providerId, cwd: task.cwd, model: task.model, + agentPort: codeAgentPortForModel(task.model), + agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)), reasoningEffort: task.reasoningEffort, maxAttempts: task.maxAttempts, status: task.status, @@ -3328,12 +4429,18 @@ function taskForListResponse(task: QueueTask, lite = false): JsonValue { promptChars: task.prompt.length, basePromptChars: task.basePrompt.length, displayPromptChars: displayPrompt.length, + promptEditable: queuedTaskPromptEditable(task), finalResponseChars: task.finalResponse.length, + stepCount, + llmStepCount: stepCount, summaryOnly: true, referenceTaskIds: task.referenceTaskIds, referenceInjection: task.referenceInjection, + providerId: task.providerId, cwd: task.cwd, model: task.model, + agentPort: codeAgentPortForModel(task.model), + agentPortInfo: codeAgentPortInfo(codeAgentPortForModel(task.model)), reasoningEffort: task.reasoningEffort, maxAttempts: task.maxAttempts, status: task.status, @@ -3360,8 +4467,9 @@ function taskForListResponse(task: QueueTask, lite = false): JsonValue { } as unknown as JsonValue; } -function perQueueSummaries(): JsonValue[] { +function perQueueSummaries(tasks: QueueTask[] = state.tasks): JsonValue[] { const summaries = new Map; unreadTerminal: number; @@ -3371,7 +4479,9 @@ function perQueueSummaries(): JsonValue[] { updatedAt: string | null; }>(); for (const queue of state.queues) { + queue.name = safeQueueName(queue.name, queue.id); summaries.set(queue.id, { + name: queue.name, total: 0, counts: {}, unreadTerminal: 0, @@ -3381,11 +4491,12 @@ function perQueueSummaries(): JsonValue[] { updatedAt: queue.updatedAt, }); } - for (const task of state.tasks) { + for (const task of tasks) { const queueId = queueIdOf(task); let summary = summaries.get(queueId); if (summary === undefined) { summary = { + name: queueId, total: 0, counts: {}, unreadTerminal: 0, @@ -3409,6 +4520,7 @@ function perQueueSummaries(): JsonValue[] { const rows = Array.from(summaries.entries()).sort(([left], [right]) => left.localeCompare(right)).map(([queueId, summary]) => { return { id: queueId, + name: summary.name, total: summary.total, counts: summary.counts, unreadTerminal: summary.unreadTerminal, @@ -3422,25 +4534,29 @@ function perQueueSummaries(): JsonValue[] { return rows; } -function queueSummary(includeDevReady = true): JsonValue { - const counts = state.tasks.reduce>((memo, task) => { +function queueSummary(includeDevReady = true, tasks: QueueTask[] = state.tasks): JsonValue { + const counts = tasks.reduce>((memo, task) => { memo[task.status] = (memo[task.status] ?? 0) + 1; return memo; }, {}); - const unreadTerminal = state.tasks.reduce((total, task) => total + (terminalTaskUnread(task) ? 1 : 0), 0); + const unreadTerminal = tasks.reduce((total, task) => total + (terminalTaskUnread(task) ? 1 : 0), 0); + const activeRunSlots = activeRunSlotQueueIds(); const activeTaskIdSet = new Set(Array.from(activeRuns.values()).map((run) => run.taskId)); - for (const task of state.tasks) { + for (const task of tasks) { if (task.status === "running" || task.status === "judging") activeTaskIdSet.add(task.id); } const activeTaskIds = Array.from(activeTaskIdSet).sort(); - const activeTaskId = activeTaskIds[0] ?? state.tasks.find((task) => task.status === "running" || task.status === "judging")?.id ?? null; - const queues = perQueueSummaries(); + const activeTaskId = activeTaskIds[0] ?? tasks.find((task) => task.status === "running" || task.status === "judging")?.id ?? null; + const queues = perQueueSummaries(tasks); const summary: Record = { - total: state.tasks.length, + total: tasks.length, defaultQueueId, queueCount: queues.length, queues, - activeQueueIds: Array.from(processingQueues).sort(), + activeQueueIds: activeRunSlots, + processingQueueIds: Array.from(processingQueues).sort(), + activeRunSlotCount: activeRunSlots.length, + activeRunSlotWaiters: activeRunSlotWaiterSummaries(), activeTaskIds, activeTaskId, processing, @@ -3450,11 +4566,25 @@ function queueSummary(includeDevReady = true): JsonValue { judgeFailRetryLimit, minimaxModel: config.minimaxModel, minimaxJudgeRepairAttempts: config.judgeRepairAttempts, + minimaxJudgeMaxTokens: config.judgeMaxTokens, defaultModel: config.defaultModel, + codeModels: config.codeModels, codexModels: config.codexModels, + opencodeModels: opencodeModels(), + modelPorts: codeModelPorts() as unknown as JsonValue, + agentPorts: { + codex: codeAgentPortInfo("codex"), + opencode: codeAgentPortInfo("opencode"), + } as unknown as JsonValue, defaultReasoningEffort: config.defaultReasoningEffort, modelReasoningEfforts: config.modelReasoningEfforts, + defaultProviderId: config.mainProviderId, + mainProviderId: config.mainProviderId, defaultWorkdir: config.defaultWorkdir, + remoteDefaultWorkdir: config.remoteDefaultWorkdir, + maxActiveQueues: config.maxActiveQueues, + executionProviders: executionProviderOptions(), + defaultWorkdirByProvider: Object.fromEntries((executionProviderOptions() as Array>).map((provider) => [String(provider.id), provider.defaultWorkdir ?? config.defaultWorkdir])) as JsonValue, notifications: { claudeqq: { enabled: config.notifyClaudeQqEnabled, @@ -3478,12 +4608,134 @@ function queueSummary(includeDevReady = true): JsonValue { outputArchiveDir: config.outputArchiveDir, inMemoryOutputRecords: config.maxInMemoryOutputRecords, inMemoryEventRecords: config.maxInMemoryEventRecords, + codexSqliteLogExport: { + enabled: config.codexSqliteLogExportEnabled, + intervalMs: config.codexSqliteLogExportIntervalMs, + batchSize: config.codexSqliteLogExportBatchSize, + maxBytes: config.codexSqliteLogMaxBytes, + running: codexSqliteLogExporter.running, + lastRunAt: codexSqliteLogExporter.lastRunAt, + lastExportedRows: codexSqliteLogExporter.lastExportedRows, + totalExportedRows: codexSqliteLogExporter.totalExportedRows, + lastDeletedRows: codexSqliteLogExporter.lastDeletedRows, + lastVacuumAt: codexSqliteLogExporter.lastVacuumAt, + lastError: codexSqliteLogExporter.lastError, + }, }, }; if (includeDevReady) summary.devReady = collectDevReady(); return summary; } +async function loadAllTasksForRead(): Promise { + if (!databaseReady) return state.tasks; + const tasks = await loadTasksFromDatabase("all"); + const byId = new Map(tasks.map((task) => [task.id, task])); + for (const active of state.tasks) { + byId.set(active.id, active); + } + runGarbageCollection(); + return Array.from(byId.values()).sort((left, right) => (timestampMs(left.createdAt) ?? 0) - (timestampMs(right.createdAt) ?? 0) || left.id.localeCompare(right.id)); +} + +async function queueSummaryForResponse(includeDevReady = true, tasks?: QueueTask[]): Promise { + return queueSummary(includeDevReady, tasks ?? await loadAllTasksForRead()); +} + +async function queueSummaryForHealth(includeDevReady = true): Promise { + const summary = queueSummary(includeDevReady, state.tasks) as Record; + if (!databaseReady) return summary; + const [totalRows, statusRows, queueStatusRows, unreadRows] = await Promise.all([ + sql>`SELECT COUNT(*) AS total FROM unidesk_code_queue_tasks`, + sql>` + SELECT status, COUNT(*) AS count + FROM unidesk_code_queue_tasks + GROUP BY status + `, + sql>` + SELECT queue_id, status, COUNT(*) AS count + FROM unidesk_code_queue_tasks + GROUP BY queue_id, status + `, + sql>` + SELECT queue_id, COUNT(*) AS count + FROM unidesk_code_queue_tasks + WHERE status IN ('succeeded', 'failed', 'canceled') + AND COALESCE(task_json->>'readAt', '') = '' + GROUP BY queue_id + `, + ]); + const counts: Record = {}; + for (const row of statusRows) counts[row.status] = Number(row.count); + const unreadByQueue = new Map(unreadRows.map((row) => [safeQueueId(row.queue_id), Number(row.count)])); + const summaries = new Map; + unreadTerminal: number; + activeTaskId: string | null; + runnableTaskId: string | null; + createdAt: string | null; + updatedAt: string | null; + }>(); + for (const queue of state.queues) { + summaries.set(queue.id, { + name: safeQueueName(queue.name, queue.id), + total: 0, + counts: {}, + unreadTerminal: unreadByQueue.get(queue.id) ?? 0, + activeTaskId: activeRuns.get(queue.id)?.taskId ?? null, + runnableTaskId: null, + createdAt: queue.createdAt, + updatedAt: queue.updatedAt, + }); + } + for (const row of queueStatusRows) { + const queueId = safeQueueId(row.queue_id); + let queue = summaries.get(queueId); + if (queue === undefined) { + queue = { + name: queueId, + total: 0, + counts: {}, + unreadTerminal: unreadByQueue.get(queueId) ?? 0, + activeTaskId: activeRuns.get(queueId)?.taskId ?? null, + runnableTaskId: null, + createdAt: null, + updatedAt: null, + }; + summaries.set(queueId, queue); + } + const count = Number(row.count); + queue.counts[row.status] = count; + queue.total += count; + } + for (const [queueId, queue] of summaries) { + const activeRun = activeRuns.get(queueId); + const head = queueHeadTask(queueId); + queue.activeTaskId = activeRun?.taskId ?? (head !== null && (head.status === "running" || head.status === "judging") ? head.id : null); + queue.runnableTaskId = head !== null && queueTaskIsRunnable(head) ? head.id : null; + } + const queues = Array.from(summaries.entries()).sort(([left], [right]) => left.localeCompare(right)).map(([id, queue]) => ({ + id, + name: queue.name, + total: queue.total, + counts: queue.counts, + unreadTerminal: queue.unreadTerminal, + activeTaskId: queue.activeTaskId, + runnableTaskId: queue.runnableTaskId, + processing: processingQueues.has(id), + createdAt: queue.createdAt, + updatedAt: queue.updatedAt, + })) as unknown as JsonValue[]; + summary.total = Number(totalRows[0]?.total ?? state.tasks.length); + summary.counts = counts as unknown as JsonValue; + summary.unreadTerminal = Array.from(unreadByQueue.values()).reduce((total, count) => total + count, 0); + summary.queues = queues; + summary.queueCount = queues.length; + return summary as JsonValue; +} + function terminalTask(task: QueueTask): boolean { return task.status === "succeeded" || task.status === "failed" || task.status === "canceled"; } @@ -3534,6 +4786,7 @@ function queueNotificationStats(): Record { queueCount: perQueueSummaries().length, processingQueueCount: processingQueues.size, activeRunCount: activeRuns.size, + activeRunSlotCount: activeRunSlotCount(), activeTaskIds, activeTaskElapsed: activeTask === null ? "-" : formatDurationMs(durationMsBetween(activeTask.startedAt ?? activeTask.createdAt, nowIso())), }; @@ -3561,7 +4814,7 @@ function notificationTargetLabel(): string { function truncateNotificationText(value: string, maxChars: number): string { if (value.length <= maxChars) return value; - return `${value.slice(0, maxChars)}\n\n...[Codex Queue notification truncated: ${value.length - maxChars} chars omitted; use CLI/WebUI for the full trace]`; + return `${value.slice(0, maxChars)}\n\n...[Code Queue notification truncated: ${value.length - maxChars} chars omitted; use CLI/WebUI for the full trace]`; } function taskFinalResponseForNotification(task: QueueTask): string { @@ -3781,10 +5034,12 @@ function taskTerminalNotificationMessage(task: QueueTask): string { const runElapsed = formatDurationMs(durationMsBetween(task.startedAt ?? task.createdAt, task.finishedAt ?? task.updatedAt)); const response = truncateNotificationText(taskFinalResponseForNotification(task), config.notifyClaudeQqMaxResponseChars); return [ - "Codex Queue 任务结束", + "Code Queue 任务结束", `task: ${task.id}`, `queue: ${queueIdOf(task)}`, `status: ${task.status}`, + `provider: ${task.providerId}`, + `cwd: ${task.cwd}`, `model: ${task.model}${task.reasoningEffort ? ` (${task.reasoningEffort})` : ""}`, `attempts: ${task.attempts.length}/${task.maxAttempts}`, `elapsed: total=${totalElapsed}, run=${runElapsed}`, @@ -3845,7 +5100,7 @@ async function maybeNotifyQueueIdle(triggerTaskId: string | null = null): Promis idleNotificationInFlight = true; try { const message = [ - "Codex Queue 已空闲", + "Code Queue 已空闲", "running=0, queued=0", `total tasks=${stats.total}, queues=${stats.queueCount}`, triggerTaskId === null ? "" : `last task=${triggerTaskId}`, @@ -3885,11 +5140,17 @@ class AppServerClient { constructor(private readonly task: QueueTask, private readonly onNotification: (message: Record) => void) { this.closedPromise = new Promise((resolveClosed) => { this.closeResolve = resolveClosed; }); - this.child = spawn("codex", ["app-server", "--listen", "stdio://"], { - cwd: task.cwd, - env: { ...process.env, CODEX_HOME: config.codexHome, CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "unidesk_codex_queue" }, - stdio: "pipe", - }); + this.child = providerIsMain(task.providerId) + ? spawn("codex", ["app-server", "--listen", "stdio://"], { + cwd: task.cwd, + env: { ...process.env, CODEX_HOME: config.codexHome, CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "unidesk_code_queue" }, + stdio: "pipe", + }) + : spawn("bun", ["scripts/cli.ts", "ssh", task.providerId, remoteAppServerCommand(task)], { + cwd: config.defaultWorkdir, + env: process.env, + stdio: "pipe", + }); this.child.stderr.on("data", (chunk: Buffer) => { this.stderrChunks.push(chunk); while (Buffer.concat(this.stderrChunks).length > 96_000) this.stderrChunks.shift(); @@ -3902,7 +5163,7 @@ class AppServerClient { async initialize(): Promise { await this.request("initialize", { - clientInfo: { name: "unidesk_codex_queue", title: "UniDesk Codex Queue", version: "0.1.0" }, + clientInfo: { name: "unidesk_code_queue", title: "UniDesk Code Queue", version: "0.1.0" }, capabilities: { experimentalApi: true }, }); this.notify("initialized", {}); @@ -3923,15 +5184,12 @@ class AppServerClient { appendOutput(this.task, "system", `thread resumed ${threadId}\n`, "thread/resume"); return threadId; } - if (this.task.currentMode === "retry") { - throw new Error("Retry requires an existing codexThreadId; refusing to start a new thread for retry."); - } const response = await this.request("thread/start", { model: this.task.model, cwd: this.task.cwd, approvalPolicy: config.approvalPolicy, sandbox: config.sandbox, - serviceName: "unidesk-codex-queue", + serviceName: "unidesk-code-queue", }); const threadId = extractString(extractRecord(response)?.thread, "id"); if (threadId === null) throw new Error("thread/start response did not include thread.id"); @@ -4149,11 +5407,401 @@ function handleNotification(task: QueueTask, message: Record, t } } +interface OpenCodeTextParts { + reasoning: string; + assistant: string; +} + +function splitOpenCodeAssistantText(text: string): OpenCodeTextParts { + const reasoning: string[] = []; + const withoutClosedThink = String(text || "").replace(/<(?:think|thinking)\b[^>]*>([\s\S]*?)<\/(?:think|thinking)>/giu, (_match, body) => { + const item = String(body || "").trim(); + if (item.length > 0) reasoning.push(item); + return ""; + }); + const closeThinkMatches = Array.from(withoutClosedThink.matchAll(/<\/(?:think|thinking)>/giu)); + const lastClose = closeThinkMatches.at(-1); + const assistant = lastClose?.index === undefined + ? withoutClosedThink.trim() + : withoutClosedThink.slice(lastClose.index + lastClose[0].length).trim(); + return { reasoning: reasoning.join("\n\n").trim(), assistant }; +} + +function openCodeModelId(model: string): string { + if (normalizeCodeModel(model) !== minimaxM27Model) throw new Error(`OpenCode port does not support model ${model}`); + const providerModel = config.minimaxModel.trim() || "MiniMax-M2.7"; + return `minimax/${providerModel}`; +} + +function openCodeConfigContent(): string { + const providerModel = config.minimaxModel.trim() || "MiniMax-M2.7"; + return JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + minimax: { + npm: "@ai-sdk/openai-compatible", + name: "MiniMax", + options: { + baseURL: config.minimaxApiBase, + apiKey: "{env:MINIMAX_API_KEY}", + }, + models: { + [providerModel]: { + name: minimaxM27Model, + limit: { context: 200000, output: 16384 }, + }, + }, + }, + }, + }); +} + +function openCodeEnv(): NodeJS.ProcessEnv { + const xdgEnv = openCodeXdgEnv(); + return { + ...process.env, + ...xdgEnv, + MINIMAX_API_KEY: config.minimaxApiKey, + MINIMAX_API_BASE: config.minimaxApiBase, + MINIMAX_MODEL: config.minimaxModel, + OPENCODE_CONFIG_CONTENT: openCodeConfigContent(), + }; +} + +function shellJoin(args: string[]): string { + return args.map(shellQuote).join(" "); +} + +function openCodeRunArgs(task: QueueTask, prompt: string): string[] { + const args = ["run", "--format", "json", "--model", openCodeModelId(task.model), "--dir", task.cwd, "--dangerously-skip-permissions"]; + if (task.codexThreadId !== null && task.codexThreadId.trim().length > 0) args.push("--session", task.codexThreadId); + args.push(prompt); + return args; +} + +function remoteOpenCodeRunCommand(task: QueueTask, prompt: string): string { + const plan = buildDevContainerPlan(task.providerId, { workdir: remoteHostWorkdirForTask(task) }); + const envExports = [ + ...Object.entries(openCodeXdgEnv(plan.remoteOpencodeXdgDir)).map(([key, value]) => `export ${key}=${shellQuote(value)}`), + `export MINIMAX_API_BASE=${shellQuote(config.minimaxApiBase)}`, + `export MINIMAX_MODEL=${shellQuote(config.minimaxModel)}`, + `export OPENCODE_CONFIG_CONTENT=${shellQuote(openCodeConfigContent())}`, + ].join("; "); + const inner = [ + "set -euo pipefail", + `mkdir -p ${shellQuote(task.cwd)}`, + `cd ${shellQuote(task.cwd)}`, + envExports, + `exec ${shellJoin(openCodeRunArgs(task, prompt))}`, + ].join("; "); + return `docker exec -i ${shellQuote(plan.containerName)} bash -lc ${shellQuote(inner)}`; +} + +function openCodeSessionIdFromRecord(record: Record, part: Record | null): string | null { + const top = recordStringField(record, ["sessionID", "sessionId", "session_id"]); + if (top.length > 0) return top; + const nested = recordStringField(part, ["sessionID", "sessionId", "session_id"]); + return nested.length > 0 ? nested : null; +} + +function openCodeEventSummary(record: Record): CodexEventSummary { + const part = extractRecord(record.part); + const type = recordStringField(record, ["type", "event", "name"]) || recordStringField(part, ["type"]) || "unknown"; + const text = recordStringField(part, ["text", "content", "delta", "message"]) || recordStringField(record, ["text", "content", "delta", "message"]); + const error = recordStringField(part, ["error", "message"]) || recordStringField(record, ["error", "message"]); + return { + at: nowIso(), + method: `opencode/${type}`, + itemType: recordStringField(part, ["type"]) || type, + status: recordStringField(part, ["status", "reason"]) || recordStringField(record, ["status", "reason"]) || undefined, + message: error.length > 0 ? safePreview(error, 600) : undefined, + textPreview: text.length > 0 ? safePreview(text, 800) : undefined, + }; +} + +class OpenCodeRunClient implements CodeAgentClient { + private child: ChildProcessWithoutNullStreams; + private stderrChunks: Buffer[] = []; + private closed = false; + private closeResolve!: (value: AppServerExit) => void; + private assistantChunks: string[] = []; + private sessionAnnounced = false; + readonly closedPromise: Promise; + readonly runId = `opencode_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`; + readonly events: CodexEventSummary[] = []; + sessionId: string | null = null; + finalResponse = ""; + stepFinished = false; + lastActivityAt = Date.now(); + + constructor(private readonly task: QueueTask, prompt: string) { + this.closedPromise = new Promise((resolveClosed) => { this.closeResolve = resolveClosed; }); + this.child = providerIsMain(task.providerId) + ? spawn("opencode", openCodeRunArgs(task, prompt), { + cwd: task.cwd, + env: openCodeEnv(), + stdio: "pipe", + }) + : spawn("bun", ["scripts/cli.ts", "ssh", task.providerId, remoteOpenCodeRunCommand(task, prompt)], { + cwd: config.defaultWorkdir, + env: process.env, + stdio: "pipe", + }); + // opencode waits for stdin EOF even when the prompt is supplied as argv. + // Close stdin immediately so non-interactive queue runs can start. + this.child.stdin.end(); + this.child.stderr.on("data", (chunk: Buffer) => { + this.stderrChunks.push(chunk); + while (Buffer.concat(this.stderrChunks).length > 96_000) this.stderrChunks.shift(); + }); + const rl = readline.createInterface({ input: this.child.stdout, crlfDelay: Infinity }); + void this.readLines(rl); + this.child.on("close", (code, signal) => this.handleClose(code, signal)); + this.child.on("error", (error) => this.handleClose(127, error.message)); + } + + stop(): void { + if (this.closed) return; + this.child.kill("SIGTERM"); + setTimeout(() => { + if (!this.closed) this.child.kill("SIGKILL"); + }, 1500).unref?.(); + } + + private async readLines(rl: readline.Interface): Promise { + try { + for await (const line of rl) { + const trimmed = String(line).trim(); + if (trimmed.length === 0) continue; + this.lastActivityAt = Date.now(); + this.handleLine(trimmed); + } + } catch (error) { + appendOutput(this.task, "error", `opencode stream error: ${error instanceof Error ? error.message : String(error)}\n`, "opencode/stream"); + } + } + + private handleLine(line: string): void { + let parsed: Record | null = null; + try { + const value = JSON.parse(line) as unknown; + parsed = extractRecord(value); + } catch { + this.appendAssistantText(line, "opencode/stdout", undefined); + return; + } + if (parsed === null) { + this.appendAssistantText(line, "opencode/stdout", undefined); + return; + } + const part = extractRecord(parsed.part); + const event = openCodeEventSummary(parsed); + this.events.push(event); + addEvent(this.task, event); + + const sessionId = openCodeSessionIdFromRecord(parsed, part); + if (sessionId !== null) this.setSessionId(sessionId); + + const type = recordStringField(parsed, ["type", "event", "name"]) || recordStringField(part, ["type"]) || "unknown"; + const partType = recordStringField(part, ["type"]); + const itemId = recordStringField(part, ["id"]) || undefined; + if (type === "step_finish" || type === "step-finish" || partType === "step-finish") { + this.stepFinished = true; + const reason = recordStringField(part, ["reason"]) || "finished"; + appendOutput(this.task, "system", `opencode run finished reason=${reason}\n`, "opencode/step-finish"); + return; + } + if (type === "step_start" || type === "step-start" || partType === "step-start") { + appendOutput(this.task, "system", `opencode run started session=${this.sessionId ?? sessionId ?? "unknown"}\n`, "opencode/step-start"); + return; + } + const text = recordStringField(part, ["text", "content", "delta"]) || recordStringField(parsed, ["text", "content", "delta"]); + if (text.length > 0 && (type === "text" || partType === "text" || partType === "reasoning")) { + if (partType === "reasoning") appendOutput(this.task, "reasoning", `${text.trimEnd()}\n`, "opencode/reasoning", itemId, true); + else this.appendAssistantText(text, "opencode/text", itemId); + return; + } + if (/tool|bash|command/iu.test(`${type} ${partType}`)) { + appendOutput(this.task, type.includes("command") || partType.includes("command") ? "command" : "tool", `${safePreview(JSON.stringify(parsed), 3000)}\n`, "opencode/tool", itemId); + return; + } + if (/error|failed/iu.test(`${type} ${partType}`)) { + appendOutput(this.task, "error", `${safePreview(JSON.stringify(parsed), 3000)}\n`, "opencode/error", itemId); + } + } + + private setSessionId(sessionId: string): void { + this.sessionId = sessionId; + if (this.task.codexThreadId === null) this.task.codexThreadId = sessionId; + const run = activeRuns.get(queueIdOf(this.task)); + if (run?.app === this) run.threadId = sessionId; + if (this.sessionAnnounced) return; + const resumed = this.task.currentMode === "retry" || this.task.attempts.length > 0; + appendOutput(this.task, "system", `opencode session ${resumed ? "resumed" : "started"} ${sessionId}\n`, resumed ? "opencode/session-resume" : "opencode/session-start"); + this.sessionAnnounced = true; + } + + private appendAssistantText(rawText: string, method: string, itemId: string | undefined): void { + const parts = splitOpenCodeAssistantText(rawText); + if (parts.reasoning.length > 0) appendOutput(this.task, "reasoning", `${parts.reasoning.trimEnd()}\n`, "opencode/reasoning", itemId, true); + const visible = parts.assistant.length > 0 ? parts.assistant : rawText.trim(); + if (visible.length === 0) return; + this.assistantChunks.push(visible); + this.finalResponse = this.assistantChunks.join("\n\n").trim(); + this.task.finalResponse = this.finalResponse; + appendOutput(this.task, "assistant", `${visible.trimEnd()}\n`, method, itemId, true); + } + + private handleClose(code: number | null, signal: string | null): void { + if (this.closed) return; + this.closed = true; + this.closeResolve({ code, signal, stderrTail: Buffer.concat(this.stderrChunks).toString("utf8").slice(-8000) }); + } +} + function terminalStatus(value: string): TerminalStatus { if (value === "completed" || value === "interrupted" || value === "failed") return value; return null; } +function stripAnsi(text: string): string { + return text.replace(/\u001b\[[0-9;]*m/gu, ""); +} + +function openCodeExitError(exit: AppServerExit): string { + const base = `OpenCode exited with code=${exit.code} signal=${exit.signal}`; + const stderr = stripAnsi(exit.stderrTail).trim(); + return stderr.length > 0 ? `${base}: ${safePreview(stderr, 800)}` : base; +} + +function openCodeSessionMissing(result: CodexRunResult): boolean { + return /Session not found/iu.test(stripAnsi(`${result.terminalError ?? ""}\n${result.appServerExit.stderrTail}`)); +} + +function openCodeFreshRecoveryPrompt(task: QueueTask, prompt: string, reason: string): string { + const previous = safePreview(task.finalResponse, 2000); + return [ + "OpenCode port recovery:上一个 OpenCode session 无法恢复;Code Queue 将为同一任务开启新的 OpenCode session,避免对缺失 session 无限重试。", + `恢复原因:${judgeReasonForPrompt(reason)}`, + "原始任务摘要/按需查询:", + compactRetryTaskContext(task), + previous.length > 0 ? `上一轮可见 assistant response 摘要:\n${previous}` : "", + "本轮 recovery continuation prompt:", + prompt, + ].filter((line) => line.length > 0).join("\n\n"); +} + +function codexFreshRecoveryPrompt(task: QueueTask, prompt: string, reason: string): string { + const previous = safePreview(task.finalResponse, 2000); + return [ + "Codex port recovery:上一个 Codex threadId 缺失;Code Queue 将为同一任务开启新的 Codex thread,避免对缺失 thread 无限重试。", + `恢复原因:${judgeReasonForPrompt(reason)}`, + "原始任务摘要/按需查询:", + compactRetryTaskContext(task), + previous.length > 0 ? `上一轮可见 assistant response 摘要:\n${previous}` : "", + "本轮 recovery continuation prompt:", + prompt, + ].filter((line) => line.length > 0).join("\n\n"); +} + +async function runOpenCodeTurn(task: QueueTask, prompt: string): Promise { + const attemptedSessionId = task.codexThreadId; + const first = await runOpenCodeTurnOnce(task, prompt); + if (attemptedSessionId === null || task.cancelRequested || shutdownRequested || !openCodeSessionMissing(first)) return first; + const reason = first.terminalError ?? first.appServerExit.stderrTail; + appendOutput(task, "system", `opencode session ${attemptedSessionId} was not found; clearing stale session and starting a fresh OpenCode session via the opencode port\n`, "opencode/session-recovery"); + logger("warn", "opencode_session_missing_recover_fresh", { taskId: task.id, sessionId: attemptedSessionId, reason: safePreview(stripAnsi(reason), 500) }); + task.codexThreadId = null; + task.activeTurnId = null; + persistTaskState(task); + return runOpenCodeTurnOnce(task, openCodeFreshRecoveryPrompt(task, prompt, reason)); +} + +async function runOpenCodeTurnOnce(task: QueueTask, prompt: string): Promise { + const queueId = queueIdOf(task); + if (config.minimaxApiKey.length === 0) { + const message = "MINIMAX_API_KEY is required for opencode model minimax-m2.7."; + appendOutput(task, "error", `${message}\n`, "opencode/config"); + return { + threadId: task.codexThreadId, + turnId: null, + finalResponse: task.finalResponse, + terminalStatus: "failed", + terminalError: message, + transportClosedBeforeTerminal: true, + appServerExit: { code: 1, signal: null, stderrTail: message }, + events: [], + }; + } + await ensureTaskExecutionContainer(task); + const app = new OpenCodeRunClient(task, prompt); + activeRuns.set(queueId, { taskId: task.id, queueId, app, port: "opencode", threadId: task.codexThreadId, turnId: app.runId }); + task.activeTurnId = null; + persistTaskState(task); + const activityWatchdog = setInterval(() => { + const idleMs = Date.now() - app.lastActivityAt; + if (idleMs < config.turnNoActivityTimeoutMs) return; + const message = `No OpenCode activity for ${Math.round(idleMs / 1000)}s; stopping opencode run so the existing session can retry.`; + appendOutput(task, "error", `${message}\n`, "turn/no-activity-watchdog"); + logger("warn", "opencode_no_activity_watchdog", { taskId: task.id, runId: app.runId, idleMs, timeoutMs: config.turnNoActivityTimeoutMs }); + app.stop(); + }, 15_000); + try { + const exit = await app.closedPromise; + clearInterval(activityWatchdog); + const finalResponse = app.finalResponse || task.finalResponse; + const exitOk = exit.code === 0; + const hasFinal = finalResponse.trim().length > 0; + const status: TerminalStatus = exitOk && hasFinal ? "completed" : "failed"; + const terminalError = status === "completed" + ? null + : exitOk + ? "OpenCode returned no final assistant response." + : openCodeExitError(exit); + const stderr = stripAnsi(exit.stderrTail).trim(); + appendOutput( + task, + status === "completed" ? "system" : "error", + `opencode completed status=${status} exit=${exit.code ?? "null"} signal=${exit.signal ?? "null"}${stderr.length > 0 ? ` stderr=${safePreview(stderr, 1200)}` : ""}\n`, + "opencode/complete", + ); + return { + threadId: app.sessionId ?? task.codexThreadId, + turnId: app.runId, + finalResponse, + terminalStatus: status, + terminalError, + transportClosedBeforeTerminal: !exitOk || !app.stepFinished, + appServerExit: exit, + events: app.events, + }; + } catch (error) { + clearInterval(activityWatchdog); + const message = error instanceof Error ? error.message : String(error); + appendOutput(task, "error", `${message}\n`, "opencode"); + app.stop(); + const exit = await app.closedPromise; + return { + threadId: app.sessionId ?? task.codexThreadId, + turnId: app.runId, + finalResponse: app.finalResponse || task.finalResponse, + terminalStatus: "failed", + terminalError: message, + transportClosedBeforeTerminal: true, + appServerExit: exit, + events: app.events, + }; + } finally { + clearInterval(activityWatchdog); + if (activeRuns.get(queueId)?.app === app) activeRuns.delete(queueId); + app.stop(); + } +} + +async function runCodeAgentTurn(task: QueueTask, prompt: string): Promise { + return codeAgentPortForModel(task.model) === "opencode" ? runOpenCodeTurn(task, prompt) : runCodexTurn(task, prompt); +} + async function runCodexTurn(task: QueueTask, prompt: string): Promise { const queueId = queueIdOf(task); const events: CodexEventSummary[] = []; @@ -4162,6 +5810,7 @@ async function runCodexTurn(task: QueueTask, prompt: string): Promise void; const terminalPromise = new Promise((resolveTerminal) => { terminalResolve = resolveTerminal; }); + await ensureTaskExecutionContainer(task); const app = new AppServerClient(task, (message) => { const method = typeof message.method === "string" ? message.method : "unknown"; if (method !== "thread/started" && method !== "turn/started") lastAppActivityAt = Date.now(); @@ -4178,12 +5827,12 @@ async function runCodexTurn(task: QueueTask, prompt: string): Promise { if (terminalSeen) return; const idleMs = Date.now() - lastAppActivityAt; @@ -4239,12 +5888,14 @@ function fallbackJudge(result: CodexRunResult, minimaxError?: string): JudgeResu if (result.terminalStatus === "interrupted") { return { decision: "fail", confidence: 0.8, reason: "Codex turn 被用户请求打断。", source: "fallback" }; } + if (result.finalResponse.trim().length === 0) { + return { decision: "retry", confidence: 0.78, reason: "Codex turn 没有返回最终 assistant response,不能视为任务已完成。", continuePrompt: retryInstruction, source: "fallback" }; + } if (minimaxError !== undefined) { return { decision: "retry", confidence: 0.65, reason: `MiniMax judge 失败(${safePreview(minimaxError, 240)});安全 fallback 将继续现有 session,而不是把 turn/completed 当作任务已完成。`, - continuePrompt: retryInstruction, source: "fallback", }; } @@ -4252,18 +5903,49 @@ function fallbackJudge(result: CodexRunResult, minimaxError?: string): JudgeResu } const retryInstruction = "这是同一个 Codex thread 的 continuation,不是新任务。请基于上文继续完成原始任务;只做最小必要状态核查,避免从头重新摸索、避免重复已经完成的修改。"; +const codeQueueRestartSafetyGuidance = "Code Queue 服务具备 restart-recovery,允许在任何时候重启、重建或替换 `code-queue-backend`;当当前任务修改 Code Queue 自身时,禁止等待当前 Code Queue task 退出或等待队列归零后再重启,因为这会等待自己退出形成自锁。正确做法是直接触发 `server rebuild code-queue` 或等价 no-deps force-recreate,并在恢复后用 live health/task 查询验证。"; function explicitUserInterrupt(task: QueueTask, result: CodexRunResult): boolean { return result.terminalStatus === "interrupted" && (task.cancelRequested || task.output.some((item) => item.method === "turn/interrupt" || item.text.includes("interrupt requested"))); } +function currentAttemptOutputForJudge(task: QueueTask): LiveOutput[] { + const latestAttempt = task.attempts.at(-1); + const startSeq = Number(latestAttempt?.outputStartSeq ?? NaN); + const endSeq = Number(latestAttempt?.outputEndSeq ?? NaN); + const output = taskFullOutput(task); + if (Number.isFinite(startSeq)) { + return output.filter((item) => item.seq >= startSeq && (!Number.isFinite(endSeq) || item.seq <= endSeq)); + } + return task.output.slice(-80); +} + +function judgeEvidenceText(task: QueueTask, result: CodexRunResult): string { + const currentOutput = currentAttemptOutputForJudge(task); + return [ + result.terminalError ?? "", + result.appServerExit.stderrTail, + ...currentOutput.slice(-120) + .filter((item) => item.channel === "error" || item.method === "turn/completed" || item.method === "app-server" || item.method?.includes("watchdog") === true) + .map((item) => item.text), + ...result.events.slice(-80).map((event) => [event.method, event.status, event.message, event.textPreview].filter(Boolean).join(" ")), + ].join("\n"); +} + +function hasServiceLimitOrTransportError(text: string): boolean { + return /(429|Too Many Requests|rate limit|quota exceeded|overloaded|exceeded retry limit|stream disconnected|no activity timeout|app-server closed|ECONNRESET|ETIMEDOUT)/iu.test(text); +} + function judgePrompt(task: QueueTask, result: CodexRunResult): string { const latestAttempt = task.attempts[task.attempts.length - 1] ?? null; const originalUserTask = task.basePrompt || userPromptForDisplay(task.prompt); const resolvedPromptForCodex = task.prompt === originalUserTask ? null : safePreview(task.prompt, 12000); + const currentAttemptOutput = currentAttemptOutputForJudge(task); + // Keep this record factual. Do not add local completion gates here; MiniMax + // is authoritative whenever it returns a valid judge JSON. return JSON.stringify({ - instruction: "请判定一个 Codex 编码任务是否真正完成、是否应通过向现有 Codex thread 追加 continuation prompt 继续重试,或是否应作为不可重试失败处理。只能返回 JSON,不要输出 Markdown fence、解释性正文或注释。所有自然语言字段必须使用中文,尤其是 reason 和 continuePrompt。重要:普通的 Codex turn/completed 状态只表示传输/session 终止事件,不等于用户任务已完成。决策前必须检查 transcript、最终回复、命令/文件变更事件、stderr 和原始任务。请严格判定:如果用户任务中的任一显式验收项缺少已完成证据,就选择 retry。最常见错误是把未完成或跑偏的工作标成 fail;未完成工作必须选择 retry,让同一个 session 继续。", + instruction: "请判定一个 Codex 编码任务是否真正完成、是否应通过向现有 Codex thread 追加 continuation prompt 继续重试,或是否应作为不可重试失败处理。只能返回 JSON,不要输出 Markdown fence、解释性正文或注释。所有自然语言字段必须使用中文,尤其是 reason 和 continuePrompt。重要:普通的 Codex turn/completed 状态只表示传输/session 终止事件,不等于用户任务已完成。决策前必须检查当前尝试的 transcript、最终回复、命令/文件变更事件、stderr 和原始任务。请严格判定:如果用户任务中的任一显式验收项缺少已完成证据,就选择 retry。最常见错误是把未完成或跑偏的工作标成 fail;未完成工作必须选择 retry,让同一个 session 继续。不要把更早 attempt 的限流/中断证据自动当作当前 attempt 的完成门禁;如果当前最新 attempt 已经提供完整完成证据,可以判定 complete。", schema: { decision: "complete|retry|fail", confidence: "0..1", reason: "中文短句", continuePrompt: "decision=retry 时必填,除非确实没有可用的继续提示;内容必须是中文,保持简洁,不要粘贴原始任务、引用上下文、transcript 或 JSON" }, originalTask: originalUserTask, resolvedPromptForCodex, @@ -4276,41 +5958,54 @@ function judgePrompt(task: QueueTask, result: CodexRunResult): string { appServerExitCode: result.appServerExit.code, appServerSignal: result.appServerExit.signal, stderrTail: safePreview(result.appServerExit.stderrTail, 2000), - finalResponse: safePreview(task.finalResponse, 6000), + finalResponse: safePreview(result.finalResponse, 6000), + finalResponseChars: result.finalResponse.length, + finalResponseMissing: result.finalResponse.trim().length === 0, latestAttempt, judgeFailCount: task.judgeFailCount, judgeFailRetryLimit, - recentOutput: task.output.slice(-80).map((item) => ({ channel: item.channel, text: safePreview(item.text, 500), method: item.method })), - recentEvents: task.events.slice(-60), + cancelRequested: task.cancelRequested, + currentAttemptOutput: currentAttemptOutput.slice(-80).map((item) => ({ channel: item.channel, text: safePreview(item.text, 500), method: item.method })), + currentAttemptEvents: result.events.slice(-60), }, policy: { complete: "仅当 transcript/最终回复证明当前任务确实完成,并且每个显式验收项都有证据时使用;队列 worker 随后会推进到下一个 queued 任务。", retry: "当当前任务未完成、Codex 只做了计划、跳过了请求的编辑/命令、只完成了部分工作、在 steer prompt 后跑偏到旁支任务、需要再跑一轮,或遇到临时网络/server/disconnected/transport/internal 错误时使用。只要存在 thread id,retry 必须恢复现有 thread 并追加 continuePrompt。", fail: "仅用于明确的用户打断/取消、缺少 agent 无法补充的凭据/权限/用户输入,或已证实的确定性不可重试外部阻塞。不要仅因为 agent 漏掉核心目标、产出错误/部分工作、或未运行必要验证就使用 fail;这些都应选择 retry。", + codeQueueSelfRestart: codeQueueRestartSafetyGuidance, }, retryContinuePromptRules: [ - "当 decision=retry 时,continuePrompt 必须是给同一个 Codex thread 的具体反馈。若已知道缺失工作,不要返回泛泛的“继续任务”。", - "continuePrompt 只写下一轮必须补齐的具体缺口和验收证据,通常不超过 1200 字;不要复制 originalTask、resolvedPromptForCodex、recentOutput、引用任务全文或完整 transcript。", + "当 decision=retry 时,continuePrompt 是给同一个 Codex thread 的反馈;只能基于 originalTask 中已存在的要求和 executionRecord 中已证实的缺口,不得新增 originalTask 没有要求的 API 形态、参数签名、实现细节、文件路径或命令细节。", + "continuePrompt 只写“未完成哪些原始需求,继续按照原始需求补齐剩余任务”和必要验收证据,通常不超过 1200 字;不要复制 originalTask、resolvedPromptForCodex、recentOutput、引用任务全文或完整 transcript。", + "如果 retry 原因是 429/Too Many Requests/exceeded retry limit、transport/app-server 中断、finalResponse 为空,或 executionRecord 还没有足够证据确认具体未完成细节,continuePrompt 必须保持高层反馈:`未完成xxx原始需求,继续按照原始需求完成剩余任务`;不要推测下一步代码写法。", + "如果缺口很多,必须在生成 continuePrompt 的源头去重、合并和分组;不要依赖接收方对已生成长文本做末端截断,因为截断会丢失验收信息。", "列出缺失的验收证据,并要求下一轮在最终回复中给出真实命令/API/UI 结果。", "如果 agent 在交付中途停下来询问用户如何处理一个它本不需要触碰的并发修改文件,continuePrompt 必须要求它忽略该并发文件并继续交付自己的变更范围,而不是让用户选择。", "对于未部署/未上线的服务或 WebUI 工作,continuePrompt 必须明确要求重建/重启所有受影响的 service/container/bundle,并在部署后验证运行中的真实行为。", - "如果受影响服务包含 Codex Queue 和 frontend,部署反馈应点名 `bun scripts/cli.ts server rebuild codex-queue`、`bun scripts/cli.ts server rebuild frontend`,并按场景要求 live API/WebUI 验证,例如 `/api/judge/probe`、`/trace-summary` 或已服务的 Codex Queue 页面。", + "如果受影响服务包含 Code Queue 和 frontend,部署反馈应点名 `bun scripts/cli.ts server rebuild code-queue`、`bun scripts/cli.ts server rebuild frontend`,并按场景要求 live API/WebUI 验证,例如 `/api/judge/probe`、`/trace-summary` 或已服务的 Code Queue 页面。", + "如果受影响服务包含 Code Queue 自身,continuePrompt 禁止要求等待当前 task 结束、等待 queue idle 或等待 `0 running` 后再重启;这会形成自锁。应明确 Code Queue 可以立即重启/重建,并依赖 restart-recovery 恢复本任务后继续验证。", ], - decisionSafetyOverrides: [ + hallucinationNoiseGuardrails: [ + "judge 只能判断完成/未完成和指出原始需求缺口,不能充当实现规划器;不要在 feedback 中发明“下一步必须这样改”的新要求。", + "如果 originalTask 只要求分析初始化/配置并在既有打印条件下新增滤波前/后打印,continuePrompt 不得额外要求把现有 API 改成无参数调用,也不得指定函数签名从 out-parameter 改成 return value。", + "若缺口是“原始任务尚未完成”,feedback 采用高层模板:未完成<原始需求摘要>,继续按照原始需求完成剩余任务,并在最终回复给出证据。", + ], + completionEvidenceGuidance: [ "如果 finalResponse 表示“我没有重建运行中的容器”“后续如需上线”“若要上线验证”“未上线”“not deployed”“not rebuilt”或等价含义,并且任务触碰了 runtime/UI/service 代码,则禁止 decision=complete。", "如果 transcript 只证明源码编辑、检查或构建,但没有针对 runtime/UI/service 变更的部署后 live API/browser 验证,即使最终回复说实现已完成,也要选择 retry。", "把 rebuild/deploy 描述为可选、建议、未来工作或下一步,本身就是用户可见/runtime 任务尚未完成的证据。", - ], - strictCompletionRules: [ - "如果 terminalStatus 是 failed/null/interrupted,transportClosedBeforeTerminal 为 true,或上一轮以 Codex/API 基础设施错误结束,则禁止 complete;除非这是应判为 fail 的明确用户打断。service/rate-limit/transport/internal 失败应选择 retry。", - "如果 stderr、terminalError、recentOutput 或 recentEvents 包含 429、Too Many Requests、rate limit、quota、overloaded、exceeded retry limit、stream disconnected、no activity timeout 或 app-server closed,即使错误前发生过代码编辑或重建,也要选择 retry。", + "判断 429、Too Many Requests、exceeded retry limit、stream disconnected 等限流/传输证据时,只把当前最新 attempt 的证据作为当前判定依据;更早 attempt 的失败不能自动否定后续正常完成的 attempt。", + "如果当前 attempt 的 terminalStatus 是 failed/null/interrupted,transportClosedBeforeTerminal 为 true,或当前 attempt 以 Codex/API 基础设施错误结束,则禁止 complete;除非这是应判为 fail 的明确用户打断。service/rate-limit/transport/internal 失败应选择 retry。", + "如果当前 attempt 的 stderr、terminalError、currentAttemptOutput 或 currentAttemptEvents 包含 429、Too Many Requests、rate limit、quota、overloaded、exceeded retry limit、stream disconnected、no activity timeout 或 app-server closed,即使错误前发生过代码编辑或重建,也要选择 retry。", + "如果 terminalStatus=failed 且 finalResponse 为空,必须选择 retry;即使中途已有容器 healthy、API 200、目录列表、文件改动或文档更新证据,也不能把没有最终回复的失败 turn 判为 complete。", "如果原始任务要求运行、通过、复现、比较、打分、benchmark、validate 或证明经验结果,complete 必须有 transcript 证据显示请求的命令/pipeline/scorer 已运行,并给出结果数字或状态。", "如果任务要求 A/B、ablation、with/without、before/after、skill/no-skill、baseline/optimized 或正/负样本比较,complete 必须为每个请求侧都提供证据;只实现一侧代码或测试属于未完成。", "如果最终回复说必需的 benchmark/pipeline 未运行、为节省 quota/time 跳过、应作为下一步运行,或仅根据代码检查预期会通过,即使单元测试通过,也要选择 retry。", "不要接受用 unit/type/component 测试替代明确请求的 benchmark 分数或 pipeline 运行的自我辩解。", "对于 frontend 或 WebUI 可见变更,源码编辑和 type check 不够。complete 需要证明已重建/重启或刷新已服务的 frontend bundle;当用户可见行为是验收目标时,还要有针对运行中 UniDesk frontend 的 browser/E2E/UI 验证。", "如果 frontend/UI 任务的最终回复说 public/full E2E 未运行,或 transcript 缺少 frontend rebuild/server rebuild 加 served-UI 验证,而请求变更可能在已部署 UI bundle 中不可见,则选择 retry。", - "对于 Codex Queue、backend-core、provider-gateway、frontend 或任何其他 UniDesk service/runtime 行为变更,源码编辑、TypeScript 检查和本地构建不够。complete 需要证明每个受影响的运行中 service/container/bundle 已重建或重启,并且部署后的 live API/UI 行为已验证。", + "对于 Code Queue、backend-core、provider-gateway、frontend 或任何其他 UniDesk service/runtime 行为变更,源码编辑、TypeScript 检查和本地构建不够。complete 需要证明每个受影响的运行中 service/container/bundle 已重建或重启,并且部署后的 live API/UI 行为已验证。", + "对于 Code Queue 自身的 runtime 行为变更,不得把“等待当前 Code Queue task 结束/等待自己退出后再重启”当作完成计划或阻塞理由;这是自锁。应选择 retry,反馈要求直接重启/重建 Code Queue 并在 restart-recovery 后验证 live health/task 证据。", "如果用户要求功能在 WebUI 可见或“上线/生效/提供展示”,不要把 rebuild/restart/deploy 当成建议。若没有运行中服务或已服务浏览器 UI 的证据,选择 retry,并要求部署加验证。", "如果最终回复说它停下来要求用户确认如何处理一个 unexpected/concurrently modified 文件,而该文件不在 agent 必要交付范围内,则选择 retry:任务尚未自主交付完成。", "对于提到 4/4、8/8、40/40 或 skill/no-skill 行为等分数的 PikaPython/PikaBench 任务,在 complete 前必须有实际 scorer 或 pipeline 运行证据,证明请求分数和比较结果。", @@ -4336,6 +6031,12 @@ function judgePrompt(task: QueueTask, result: CodexRunResult): string { requiredDecision: "retry", reason: "rate-limit 或 retry-limit 终止属于基础设施中断,不能算完成,必须继续现有 session。", }, + { + pattern: "原始任务要求基于 filebrowser 开发/部署文件管理器,支持 main server host、provider host 和 WSL Windows /mnt/c 文件浏览,例如 task codex_1778545138372_ec5e05。", + incompleteEvidence: "中途命令显示 main-server/D601/D518 filebrowser 容器健康、WebUI/API/Windows 路径一度可读,但最后出现 `exceeded retry limit, last status: 429 Too Many Requests`、turn completed status=failed,且 finalResponse 为空。", + requiredDecision: "retry", + reason: "当前 attempt 的限流失败和空 finalResponse 是未完成证据;不能用中途运行证据替代失败 turn 的最终交付。必须继续同一 thread 生成最终总结并补齐/复核验证。", + }, { pattern: "原始任务要求把 UniDesk sidebar/WebUI 标签从“微服务”改为“用户服务”,并更新长期文档。", incompleteEvidence: "执行代理编辑了 frontend 源码、文档和 E2E selector,并运行 `bun scripts/cli.ts check`,但没有重建/重启 frontend bundle,也没有验证运行中的 served UI;最终回复还说 full public E2E 未运行。用户后来观察到 UI 变更实际上未生效。", @@ -4343,18 +6044,25 @@ function judgePrompt(task: QueueTask, result: CodexRunResult): string { reason: "WebUI 可见变更只有在 deployed/served frontend 已重建或刷新并验证后才算完成;源码编辑加 type check 可能让 live UI 保持不变。", }, { - pattern: "原始任务要求移除 Codex Queue 前端的 `Prompt 全量` card,因为 TraceView 已可展开它,例如 task codex_1778483956252_c65680。", - incompleteEvidence: "执行代理编辑 frontend 并运行检查,但最终回复停下来要求用户选择如何处理 `src/components/microservices/codex-queue/src/index.ts`;它自己说没有修改该文件,并给出忽略该文件、只交付 frontend 变更的选项。", + pattern: "原始任务要求移除 Code Queue 前端的 `Prompt 全量` card,因为 TraceView 已可展开它,例如 task codex_1778483956252_c65680。", + incompleteEvidence: "执行代理编辑 frontend 并运行检查,但最终回复停下来要求用户选择如何处理 `src/components/microservices/code-queue/src/index.ts`;它自己说没有修改该文件,并给出忽略该文件、只交付 frontend 变更的选项。", requiredDecision: "retry", requiredContinuePrompt: "这是其他任务正在并发开发,忽略这个文件,只继续交付我已改的前端相关变更。", reason: "agent 尚未自主完成交付;即使安全下一步是忽略无关并发文件并完成自己的 frontend 范围,它仍暂停要求用户确认。", }, { - pattern: "原始任务要求 Codex Queue 在 trace 链路和 WebUI 中暴露 judge feedback prompt,例如 task codex_1778476426776_a2bf19。", - incompleteEvidence: "执行代理编辑 backend/frontend 源码并通过 type check,但没有 `server rebuild codex-queue`,没有 `server rebuild frontend` 或等价 served bundle 部署,也没有 live API/UI 验证证明 `/trace-summary` 或 WebUI 包含 judge feedback prompt。最终回复说“我没有重建运行中的容器”,或把 rebuild/deploy 当作建议/未来工作。", + pattern: "原始任务要求 Code Queue 在 trace 链路和 WebUI 中暴露 judge feedback prompt,例如 task codex_1778476426776_a2bf19。", + incompleteEvidence: "执行代理编辑 backend/frontend 源码并通过 type check,但没有 `server rebuild code-queue`,没有 `server rebuild frontend` 或等价 served bundle 部署,也没有 live API/UI 验证证明 `/trace-summary` 或 WebUI 包含 judge feedback prompt。最终回复说“我没有重建运行中的容器”,或把 rebuild/deploy 当作建议/未来工作。", requiredDecision: "retry", - requiredContinuePrompt: "请让同一个 Codex thread 部署已修改的 Codex Queue/frontend 服务,然后在声明完成前验证 live API/UI 证据。", - reason: "这是 service/UI 功能。只有运行中的 Codex Queue backend 和 served frontend 已更新并验证后才算完成;否则用户仍看不到该功能。", + requiredContinuePrompt: "请让同一个 Codex thread 部署已修改的 Code Queue/frontend 服务,然后在声明完成前验证 live API/UI 证据。", + reason: "这是 service/UI 功能。只有运行中的 Code Queue backend 和 served frontend 已更新并验证后才算完成;否则用户仍看不到该功能。", + }, + { + pattern: "原始任务要求修改或验证 Code Queue 自身,并且完成条件需要重启/重建 `code-queue-backend`。", + incompleteEvidence: "执行代理表示要等当前 Code Queue task 结束、等队列空闲、等 0 running、或等自己退出后再重启 Code Queue,因此没有执行重启/重建和恢复后 live 验证。", + requiredDecision: "retry", + requiredContinuePrompt: "不要等待当前 Code Queue task 退出;Code Queue 可随时重启/重建。请直接执行 `server rebuild code-queue` 或等价 no-deps force-recreate,依赖 restart-recovery 恢复本任务,然后验证 live health/task 证据。", + reason: "等待当前任务结束后再重启 Code Queue 会让任务等待自己退出,属于自锁;正确交付路径是先重启/重建再由 restart-recovery 继续。", }, ], }); @@ -4404,20 +6112,40 @@ function balancedJsonCandidates(text: string): string[] { function judgeJsonCandidates(text: string): Array<{ source: string; text: string }> { const normalized = text.replace(/^\uFEFF/u, "").trim(); - const candidates: Array<{ source: string; text: string }> = [{ source: "direct", text: normalized }]; - const fenced = /```[ \t]*(?:json|JSON|javascript|js)?[^\n\r]*[\r\n]+([\s\S]*?)```/gu; - for (const match of normalized.matchAll(fenced)) { - const body = typeof match[1] === "string" ? match[1].trim() : ""; - if (body.length > 0) candidates.push({ source: "fenced", text: body }); + const candidates: Array<{ source: string; text: string }> = []; + const pushText = (source: string, value: string): void => { + const trimmed = value.trim(); + if (trimmed.length > 0) candidates.push({ source, text: trimmed }); + }; + const pushDerivedCandidates = (sourcePrefix: string, value: string): void => { + const trimmed = value.trim(); + if (trimmed.length === 0) return; + pushText(sourcePrefix, trimmed); + const fenced = /```[ \t]*(?:json|JSON|javascript|js)?[^\n\r]*[\r\n]+([\s\S]*?)```/gu; + for (const match of trimmed.matchAll(fenced)) { + const body = typeof match[1] === "string" ? match[1].trim() : ""; + if (body.length > 0) pushText(`${sourcePrefix}_fenced`, body); + } + const strippedFence = trimmed + .replace(/^```[^\n\r]*[\r\n]?/u, "") + .replace(/```$/u, "") + .trim(); + if (strippedFence !== trimmed) pushText(`${sourcePrefix}_stripped_fence`, strippedFence); + const strippedLabel = trimmed.replace(/^(?:json|JSON)\s*[:\n\r]\s*/u, "").trim(); + if (strippedLabel !== trimmed) pushText(`${sourcePrefix}_stripped_label`, strippedLabel); + const jsonTag = trimmed.match(/]*>([\s\S]*?)<\/json>/iu); + if (typeof jsonTag?.[1] === "string") pushText(`${sourcePrefix}_json_tag`, jsonTag[1]); + for (const candidate of balancedJsonCandidates(trimmed)) pushText(`${sourcePrefix}_balanced_object`, candidate); + }; + pushDerivedCandidates("direct", normalized); + const withoutClosedThink = normalized.replace(/<(?:think|thinking)\b[^>]*>[\s\S]*?<\/(?:think|thinking)>/giu, "").trim(); + if (withoutClosedThink !== normalized) pushDerivedCandidates("stripped_think", withoutClosedThink); + const closeThinkMatches = Array.from(normalized.matchAll(/<\/(?:think|thinking)>/giu)); + const lastThinkClose = closeThinkMatches.at(-1); + if (lastThinkClose?.index !== undefined) { + const afterThink = normalized.slice(lastThinkClose.index + lastThinkClose[0].length).trim(); + if (afterThink.length > 0) pushDerivedCandidates("after_think", afterThink); } - const strippedFence = normalized - .replace(/^```[^\n\r]*[\r\n]?/u, "") - .replace(/```$/u, "") - .trim(); - if (strippedFence !== normalized && strippedFence.length > 0) candidates.push({ source: "stripped_fence", text: strippedFence }); - const strippedLabel = normalized.replace(/^(?:json|JSON)\s*[:\n\r]\s*/u, "").trim(); - if (strippedLabel !== normalized && strippedLabel.length > 0) candidates.push({ source: "stripped_label", text: strippedLabel }); - for (const candidate of balancedJsonCandidates(normalized)) candidates.push({ source: "balanced_object", text: candidate }); const seen = new Set(); return candidates.filter((candidate) => { const key = candidate.text; @@ -4451,6 +6179,28 @@ function validJudgeDecisionValue(value: unknown): boolean { return value === "complete" || value === "retry" || value === "fail" || value === "continue"; } +function parsedContinuePromptForJudge(parsed: Record, decision: JudgeDecision): string | undefined { + const prompt = typeof parsed.continuePrompt === "string" ? parsed.continuePrompt.trim() : ""; + if (prompt.length === 0 || decision === "complete") return undefined; + if (prompt.length > continuePromptSourceBudgetChars) { + throw new Error(`MiniMax judge continuePrompt exceeds source budget (${prompt.length}/${continuePromptSourceBudgetChars} chars); resynthesize compact feedback at the source instead of tail-truncating`); + } + return prompt; +} + +function judgeRepairInstruction(error: string): string { + if (/continuePrompt exceeds source budget/iu.test(error)) { + return [ + "你上一条 judge 回答的 continuePrompt 过长,不能作为 continuation prompt 使用。", + "请基于原始 judge 输入、executionRecord 和上一条回答重新合成紧凑反馈;这是源头重写,不是截取前 N 字。", + `continuePrompt 目标不超过 ${compactContinuationPromptTargetChars} 字,绝对不超过 ${continuePromptSourceBudgetChars} 字;保留所有不同的缺口,合并重复项,只列下一轮必须执行的行动和验收证据。`, + "不要粘贴 originalTask、resolvedPromptForCodex、recentOutput、引用任务全文、完整 transcript 或 JSON。", + "只能返回一个原始 JSON object,不要使用 Markdown fence、、思考过程、说明正文或注释;所有自然语言字段必须使用中文,尤其是 reason 和 continuePrompt。", + ].join("\n"); + } + return "你上一条 judge 回答在清理后仍无法解析为 JSON。只能返回一个原始 JSON object,不要使用 Markdown fence、、思考过程、说明正文或注释。所有自然语言字段必须使用中文,尤其是 reason 和 continuePrompt。"; +} + function judgeScopeText(task: QueueTask, result: CodexRunResult): string { return [ task.basePrompt, @@ -4462,7 +6212,7 @@ function judgeScopeText(task: QueueTask, result: CodexRunResult): string { } function needsRuntimeDeploymentEvidence(text: string): boolean { - return /(Codex Queue|codex-queue|frontend|WebUI|trace-summary|Trace|judge feedback|src\/components\/frontend|src\/components\/microservices\/codex-queue|backend-core|provider-gateway|server rebuild|served frontend|running service|前端|后端|容器|上线|生效)/iu.test(text); + return /(Code Queue|code-queue|frontend|WebUI|trace-summary|Trace|judge feedback|src\/components\/frontend|src\/components\/microservices\/code-queue|backend-core|provider-gateway|server rebuild|served frontend|running service|前端|后端|容器|上线|生效)/iu.test(text); } function lineAdmitsMissingDeployment(line: string): boolean { @@ -4501,20 +6251,45 @@ function deploymentFeedbackPrompt(task: QueueTask, reason: string): string { retryInstruction, "上一次 judge 判定为 retry:当前实现仍是未上线/未完成状态,不能只复述源码修改。", `judge 未完成原因:${judgeReasonForPrompt(reason)}`, - "请先确认受影响范围;凡是 Codex Queue、frontend、backend-core、provider-gateway 或其他运行服务/前端 bundle 的行为变化,都必须更新运行中的服务后再验收。", - "如果本轮改动影响 Codex Queue 和 frontend,请执行并记录真实结果:`bun scripts/cli.ts server rebuild codex-queue`、`bun scripts/cli.ts server rebuild frontend`。", - "部署后必须做 live verification:至少用运行中的 API 或公网 WebUI 证明目标行为已经生效,例如 Codex Queue `/api/judge/probe` 命中该样例、`/trace-summary` 返回 judge feedback prompt,或 served Codex Queue 页面展示对应 feedback prompt。", + "请先确认受影响范围;凡是 Code Queue、frontend、backend-core、provider-gateway 或其他运行服务/前端 bundle 的行为变化,都必须更新运行中的服务后再验收。", + "若受影响的是 Code Queue 自身,不要等待当前 Code Queue task 退出或等待队列空闲;可以立即重启/重建 `code-queue-backend`,由 restart-recovery 恢复本任务后继续验证。", + "如果本轮改动影响 Code Queue 和 frontend,请执行并记录真实结果:`bun scripts/cli.ts server rebuild code-queue`、`bun scripts/cli.ts server rebuild frontend`。", + "部署后必须做 live verification:至少用运行中的 API 或公网 WebUI 证明目标行为已经生效,例如 Code Queue `/api/judge/probe` 命中该样例、`/trace-summary` 返回 judge feedback prompt,或 served Code Queue 页面展示对应 feedback prompt。", "最终 response 必须列出实际执行的部署命令和 live verification 结果;如果不能上线或验证,请说明阻塞并保持 retry 语义。", "原始任务摘要/按需查询:", compactRetryTaskContext(task), ].join("\n\n"); } +function rateLimitFeedbackNeedsOriginalRequirementOnly(task: QueueTask, result: CodexRunResult): boolean { + if (!hasServiceLimitOrTransportError(judgeEvidenceText(task, result))) return false; + const finalResponseMissing = result.finalResponse.trim().length === 0; + const terminalNotCompleted = result.terminalStatus === "failed" || result.terminalStatus === null || result.transportClosedBeforeTerminal; + if (!finalResponseMissing && !terminalNotCompleted) return false; + const scopeText = [task.basePrompt, task.prompt, result.finalResponse, ...task.output.slice(-40).map((item) => item.text)].join("\n"); + return /period_sum\s*\/\s*mpu_read_num|滑动滤波前|滑动滤波后|71-Freq|71-FREQ|main\.c:282/iu.test(scopeText); +} + +function originalRequirementOnlyFeedbackPrompt(task: QueueTask, reason: string): string { + const originalTask = safePreview(task.basePrompt || userPromptForDisplay(task.prompt), 260).replace(/\s+/gu, " ").trim(); + void reason; + return `未完成原始需求:${originalTask || "原始任务尚未完成"}。继续按照原始需求完成剩余任务。`; +} + function applyFallbackSafetyOverrides(task: QueueTask, result: CodexRunResult, judge: JudgeResult): JudgeResult { // Non-LLM string/regex overrides are a last-resort fallback only. Never call // this on a successful MiniMax judge result; MiniMax is the authoritative judge. if (judge.source !== "fallback") return judge; const currentFinalText = result.finalResponse || ""; + if (judge.decision === "retry" && rateLimitFeedbackNeedsOriginalRequirementOnly(task, result)) { + const reason = judge.reason || "任务因限流/传输中断,原始需求尚未完成。"; + return { + ...judge, + reason, + continuePrompt: originalRequirementOnlyFeedbackPrompt(task, reason), + raw: { previous: judge.raw ?? null, _safetyOverride: "original_requirement_only_feedback" }, + }; + } if (asksToConfirmConcurrentFileInsteadOfDelivery(currentFinalText)) { const reason = "最终回复停下来询问用户如何处理并发修改/无关文件,而不是自主完成自己的变更交付范围。"; return { @@ -4541,10 +6316,9 @@ function applyFallbackSafetyOverrides(task: QueueTask, result: CodexRunResult, j } async function judgeTask(task: QueueTask, result: CodexRunResult): Promise { - if (explicitUserInterrupt(task, result)) return fallbackJudge(result); if (config.minimaxApiKey.length === 0) return applyFallbackSafetyOverrides(task, result, fallbackJudge(result)); const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [ - { role: "system", content: "你是严格的任务状态分类器。只能返回紧凑 JSON,不得使用 Markdown fence。所有自然语言字段必须使用中文,尤其是 reason 和 continuePrompt。" }, + { role: "system", content: "你是严格的任务状态分类器。只能返回一个紧凑原始 JSON object。禁止输出 Markdown fence、、思考过程、解释性正文或注释。所有自然语言字段必须使用中文,尤其是 reason 和 continuePrompt。" }, { role: "user", content: judgePrompt(task, result) }, ]; try { @@ -4558,15 +6332,20 @@ async function judgeTask(task: QueueTask, result: CodexRunResult): Promise 0 ? boundedContinuationPrompt(parsed.continuePrompt) : undefined, + continuePrompt, source: "minimax", raw: { ...(parsed as Record), _parseSource: parsedResult.source, _repairAttempt: repairAttempt }, }; + // No local hard-gate validation or safety override is applied to a + // successful MiniMax result. Fallback rules are only for MiniMax failure. + return judge; } catch (error) { lastParseError = error instanceof Error ? error.message : String(error); if (repairAttempt >= config.judgeRepairAttempts) throw new Error(lastParseError); @@ -4581,9 +6360,9 @@ async function judgeTask(task: QueueTask, result: CodexRunResult): Promise 执行过程 Summary 1 -> finalresponse1 -> judge1 -> 执行过程 Summary 2 -> finalresponse2 judge2 还不够,还要有 judge feedback prompt,例如 originprompt -> 执行过程 Summary 1 -> finalresponse1 -> judge1 -> judge feedback prompt -> 执行过程 Summary 2 -> finalresponse2 judge2", - finalResponse: "已把 Trace 顶级链路补成:origin prompt -> Summary 1 -> final response 1 -> judge 1 -> judge feedback prompt -> Summary 2 -> final response 2 -> judge 2。后端扩展 attempt 数据,trace-summary 现在返回 feedback prompt preview/chars/lines/source,/api/tasks/:id/prompt?part=feedback&attempt=N 支持按需拉取完整内容,前端显示 feedback prompt cards。验证已通过:bun run --cwd src/components/microservices/codex-queue check、bun run --cwd src/components/frontend check、bun scripts/cli.ts check、git diff --check。当前 git diff clean。我没有重建运行中的容器。若要上线验证:1. bun scripts/cli.ts server rebuild codex-queue 2. bun scripts/cli.ts server rebuild frontend。", + finalResponse: "已把 Trace 顶级链路补成:origin prompt -> Summary 1 -> final response 1 -> judge 1 -> judge feedback prompt -> Summary 2 -> final response 2 -> judge 2。后端扩展 attempt 数据,trace-summary 现在返回 feedback prompt preview/chars/lines/source,/api/tasks/:id/prompt?part=feedback&attempt=N 支持按需拉取完整内容,前端显示 feedback prompt cards。验证已通过:bun run --cwd src/components/microservices/code-queue check、bun run --cwd src/components/frontend check、bun scripts/cli.ts check、git diff --check。当前 git diff clean。我没有重建运行中的容器。若要上线验证:1. bun scripts/cli.ts server rebuild code-queue 2. bun scripts/cli.ts server rebuild frontend。", expected: "retry", terminalStatus: "completed", outputs: [ @@ -4810,12 +6688,12 @@ const defaultJudgeProbeCases: JudgeProbeCase[] = [ text: "originprompt -> 执行过程 Summary 1 -> finalresponse1 -> judge1 -> 执行过程 Summary 2 -> finalresponse2 judge2 还不够,还要有 judge feedback prompt,例如 originprompt -> 执行过程 Summary 1 -> finalresponse1 -> judge1 -> judge feedback prompt -> 执行过程 Summary 2 -> finalresponse2 judge2\n", method: "enqueue", }, - { channel: "diff", text: "item/completed: file changes status=completed; M src/components/microservices/codex-queue/src/index.ts M src/components/frontend/src/codex-queue.tsx", method: "item/completed" }, - { channel: "command", text: "item/completed: bun run --cwd src/components/microservices/codex-queue check status=completed; ok=true", method: "item/completed" }, + { channel: "diff", text: "item/completed: file changes status=completed; M src/components/microservices/code-queue/src/index.ts M src/components/frontend/src/code-queue.tsx", method: "item/completed" }, + { channel: "command", text: "item/completed: bun run --cwd src/components/microservices/code-queue check status=completed; ok=true", method: "item/completed" }, { channel: "command", text: "item/completed: bun run --cwd src/components/frontend check status=completed; ok=true", method: "item/completed" }, { channel: "assistant", - text: "已把 Trace 顶级链路补成 judge feedback prompt。验证已通过 type/check;我没有重建运行中的容器。若要上线验证可重建 codex-queue 和 frontend。", + text: "已把 Trace 顶级链路补成 judge feedback prompt。验证已通过 type/check;我没有重建运行中的容器。若要上线验证可重建 code-queue 和 frontend。", method: "item/agentMessage/delta", }, { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, @@ -4825,7 +6703,7 @@ const defaultJudgeProbeCases: JudgeProbeCase[] = [ { id: "beijing_time_deployed_after_prior_retry_feedback_should_complete", prompt: "unidesk 的所有 frontend 时间显示必须采用北京时间,要修改 unidesk 的前端,并在长期参考文档中要求这一点", - finalResponse: "已完成北京时间任务并已上线验证。源码新增统一北京时间工具并替换 frontend 时间显示入口;长期参考文档已加入 Frontend Time Zone Policy,要求所有 UniDesk frontend timestamps、clocks、Trace times、Gantt labels 和 datetime-local 都固定使用 Asia/Shanghai / UTC+8。已执行 `bun scripts/cli.ts server rebuild frontend`,job succeeded,`unidesk-frontend` healthy。已用 Playwright 打开公网 UniDesk UI,并在 America/Los_Angeles 浏览器时区下验证顶部北京时间时钟、任务历史、Codex Queue card、Codex Trace 和 Pipeline run 时间都显示北京时间。本轮不是只改源码未上线,运行中 served UI 已验证通过。", + finalResponse: "已完成北京时间任务并已上线验证。源码新增统一北京时间工具并替换 frontend 时间显示入口;长期参考文档已加入 Frontend Time Zone Policy,要求所有 UniDesk frontend timestamps、clocks、Trace times、Gantt labels 和 datetime-local 都固定使用 Asia/Shanghai / UTC+8。已执行 `bun scripts/cli.ts server rebuild frontend`,job succeeded,`unidesk-frontend` healthy。已用 Playwright 打开公网 UniDesk UI,并在 America/Los_Angeles 浏览器时区下验证顶部北京时间时钟、任务历史、Code Queue card、Codex Trace 和 Pipeline run 时间都显示北京时间。本轮不是只改源码未上线,运行中 served UI 已验证通过。", expected: "complete", terminalStatus: "completed", outputs: [ @@ -4913,6 +6791,7 @@ function taskForJudgeProbe(probe: JudgeProbeCase): QueueTask { basePrompt: probe.prompt, referenceTaskIds: [], referenceInjection: null, + providerId: config.mainProviderId, cwd: config.defaultWorkdir, model: config.defaultModel, reasoningEffort: resolveReasoningEffort(config.defaultModel, config.defaultReasoningEffort), @@ -4967,15 +6846,25 @@ async function runJudgeProbe(): Promise { task.attempts.push(attemptFromResult(task, "initial", startedAt, finishedAt, result)); const judge = await judgeTask(task, result); const expectedContinuePromptIncludes = probe.expectedContinuePromptIncludes ?? []; + const expectedContinuePromptExcludes = probe.expectedContinuePromptExcludes ?? []; const continuePrompt = judge.continuePrompt ?? ""; const continuePromptHit = expectedContinuePromptIncludes.every((text) => continuePrompt.includes(text)); + const continuePromptExclusionHit = expectedContinuePromptExcludes.every((text) => !continuePrompt.includes(text)); + const continuePromptMaxCharsHit = probe.expectedContinuePromptMaxChars === undefined || continuePrompt.length <= probe.expectedContinuePromptMaxChars; + const continuePromptMaxLinesHit = probe.expectedContinuePromptMaxLines === undefined || promptLineCount(continuePrompt) <= probe.expectedContinuePromptMaxLines; return { id: probe.id, expected: probe.expected, decision: judge.decision, - hit: judge.decision === probe.expected && continuePromptHit, + hit: judge.decision === probe.expected && continuePromptHit && continuePromptExclusionHit && continuePromptMaxCharsHit && continuePromptMaxLinesHit, continuePromptHit, + continuePromptExclusionHit, + continuePromptMaxCharsHit, + continuePromptMaxLinesHit, expectedContinuePromptIncludes, + expectedContinuePromptExcludes, + expectedContinuePromptMaxChars: probe.expectedContinuePromptMaxChars ?? null, + expectedContinuePromptMaxLines: probe.expectedContinuePromptMaxLines ?? null, confidence: judge.confidence, source: judge.source, reason: judge.reason, @@ -5024,20 +6913,15 @@ function attemptFromResult(task: QueueTask, mode: RunMode, startedAt: string, fi const retryTaskSummaryMaxChars = 1200; const judgeReasonPromptMaxChars = 1200; -const maxContinuationPromptChars = 4000; +const compactContinuationPromptTargetChars = 1200; +const continuePromptSourceBudgetChars = 4000; function judgeReasonForPrompt(reason: string): string { return safePreview(reason, judgeReasonPromptMaxChars) || "(empty)"; } -function boundedContinuationPrompt(prompt: string): string { - const trimmed = prompt.trim(); - if (trimmed.length <= maxContinuationPromptChars) return trimmed; - return [ - trimmed.slice(0, maxContinuationPromptChars), - "", - "[Codex Queue: judge continuePrompt 过长,已截断;请基于上述关键反馈和当前 thread 上文继续,不要粘贴长上下文。]", - ].join("\n"); +function continuationPromptForRetry(prompt: string): string { + return prompt.trim(); } function compactRetryTaskContext(task: QueueTask): string { @@ -5054,7 +6938,8 @@ function compactRetryTaskContext(task: QueueTask): string { function queueRecoveryRetryPrompt(task: QueueTask, reason: string): string { return [ retryInstruction, - "Codex Queue 服务在任务运行中重启/停止;这是自动恢复提示,不是新任务,也不需要重新粘贴原始任务或引用全文。", + "Code Queue 服务在任务运行中重启/停止;这是自动恢复提示,不是新任务,也不需要重新粘贴原始任务或引用全文。", + "如果本轮任务正是修改 Code Queue 自身,不要等待当前 task 退出;服务重启已经发生,继续完成恢复后的验证和剩余交付。", `恢复原因:${judgeReasonForPrompt(reason)}`, "请基于当前 thread 上文继续,只做最小必要状态核查,恢复未完成的等待、验证、部署或命令;最终 response 必须给出真实结果证据。", "原始任务摘要/按需查询:", @@ -5063,7 +6948,7 @@ function queueRecoveryRetryPrompt(task: QueueTask, reason: string): string { } function retryPrompt(task: QueueTask, judge: JudgeResult): string { - if (judge.continuePrompt !== undefined && judge.continuePrompt.trim().length > 0) return boundedContinuationPrompt(judge.continuePrompt); + if (judge.continuePrompt !== undefined && judge.continuePrompt.trim().length > 0) return continuationPromptForRetry(judge.continuePrompt); return [ retryInstruction, "上一次 judge 判定为 retry,下面是必须传递给本轮 continuation 的 judge feedback。请优先补齐这些缺口,不要只做泛泛的状态核查。", @@ -5093,13 +6978,13 @@ async function sleepForRetryBackoff(task: QueueTask, delayMs: number): Promise 0) { - parts.push("judge 建议的继续提示:", boundedContinuationPrompt(judge.continuePrompt)); + parts.push("judge 建议的继续提示:", continuationPromptForRetry(judge.continuePrompt)); } parts.push("原始任务摘要/按需查询:", compactRetryTaskContext(task)); return parts.join("\n\n"); @@ -5110,6 +6995,8 @@ function queueActiveTasksForRestartRetry(reason: string, method: string): number for (const task of state.tasks) { if (task.status !== "running" && task.status !== "judging") continue; task.status = "retry_wait"; + task.finishedAt = null; + task.readAt = null; task.activeTurnId = null; task.lastError = reason; task.nextMode = "retry"; @@ -5135,7 +7022,7 @@ function failTaskForFallbackRetryLimit(task: QueueTask, judge: JudgeResult | nul task.nextMode = null; task.lastError = safePreview(reason, 2000); appendOutput(task, "error", `${reason}\n`, "queue"); - persistState(); + persistTaskState(task); logger("warn", "task_failed_by_fallback_retry_limit", { taskId: task.id, fallbackRetryCount: count, @@ -5146,7 +7033,7 @@ function failTaskForFallbackRetryLimit(task: QueueTask, judge: JudgeResult | nul } async function runTask(task: QueueTask): Promise { - logger("info", "task_run_start", { taskId: task.id, queueId: queueIdOf(task), maxAttempts: task.maxAttempts, model: task.model, promptPreview: safePreview(task.prompt, 240) }); + logger("info", "task_processor_start", { taskId: task.id, queueId: queueIdOf(task), providerId: task.providerId, cwd: task.cwd, maxAttempts: task.maxAttempts, model: task.model, agentPort: codeAgentPortForModel(task.model), promptPreview: safePreview(task.prompt, 240) }); if (task.status === "retry_wait" && task.lastJudge?.source === "fallback" && task.lastJudge.decision === "retry" && fallbackJudgeRetryCount(task) >= fallbackJudgeRetryLimit) { failTaskForFallbackRetryLimit(task, task.lastJudge); return; @@ -5158,22 +7045,41 @@ async function runTask(task: QueueTask): Promise { while (task.attempts.length < task.maxAttempts && !task.cancelRequested && !shutdownRequested) { const mode = task.nextMode ?? (task.attempts.length === 0 ? "initial" : "retry"); const rawPrompt = task.nextPrompt ?? task.prompt; - const prompt = promptWithCodexQueueEnvironmentHint(rawPrompt); + const needsFreshRecoveryPrompt = mode === "retry" && task.codexThreadId === null && task.attempts.length > 0; + const recoveryPrompt = needsFreshRecoveryPrompt + ? codeAgentPortForModel(task.model) === "opencode" + ? openCodeFreshRecoveryPrompt(task, rawPrompt, "retry_wait task has no persisted OpenCode session id") + : codexFreshRecoveryPrompt(task, rawPrompt, "retry_wait task has no persisted Codex thread id") + : rawPrompt; + const prompt = promptWithCodeQueueEnvironmentHint(recoveryPrompt); + if (needsFreshRecoveryPrompt) { + appendOutput(task, "system", "retry has no persisted thread/session id; starting a fresh agent thread with compact recovery context\n", "thread/recovery"); + } + const releaseRunSlot = await acquireActiveRunSlot(task); + if (releaseRunSlot === null) break; const startedAt = nowIso(); task.currentAttempt = task.attempts.length + 1; task.currentMode = mode; task.status = "running"; task.readAt = null; + task.finishedAt = null; task.updatedAt = startedAt; - const attemptStartOutput = appendOutput(task, "system", `attempt ${task.currentAttempt}/${task.maxAttempts} queue=${queueIdOf(task)} mode=${mode} model=${task.model}\n`, "queue"); + logger("info", "task_run_start", { taskId: task.id, queueId: queueIdOf(task), attempt: task.currentAttempt, mode, providerId: task.providerId, cwd: task.cwd, maxAttempts: task.maxAttempts, model: task.model, agentPort: codeAgentPortForModel(task.model), freshRecovery: needsFreshRecoveryPrompt }); + const attemptStartOutput = appendOutput(task, "system", `attempt ${task.currentAttempt}/${task.maxAttempts} queue=${queueIdOf(task)} provider=${task.providerId} cwd=${task.cwd} mode=${mode} model=${task.model} port=${codeAgentPortForModel(task.model)}\n`, "queue"); - const result = await runCodexTurn(task, prompt); + let result: CodexRunResult; + try { + result = await runCodeAgentTurn(task, prompt); + } finally { + releaseRunSlot(); + } const finishedAt = nowIso(); task.finalResponse = result.finalResponse || task.finalResponse; - task.attempts.push(attemptFromResult(task, mode, startedAt, finishedAt, result, attemptStartOutput?.seq ?? null, taskFullOutput(task).at(-1)?.seq ?? null, rawPrompt)); + task.attempts.push(attemptFromResult(task, mode, startedAt, finishedAt, result, attemptStartOutput?.seq ?? null, taskFullOutput(task).at(-1)?.seq ?? null, recoveryPrompt)); task.status = "judging"; + task.activeTurnId = null; task.updatedAt = nowIso(); - persistState(); + persistTaskState(task); if (task.cancelRequested) break; const judge = await judgeTask(task, result); @@ -5194,7 +7100,7 @@ async function runTask(task: QueueTask): Promise { task.activeTurnId = null; task.nextPrompt = null; task.nextMode = null; - persistState(); + persistTaskState(task); logger("info", "task_succeeded", { taskId: task.id, attempts: task.attempts.length }); void notifyTaskTerminal(task); return; @@ -5203,6 +7109,8 @@ async function runTask(task: QueueTask): Promise { task.judgeFailCount += 1; if (!explicitUserInterrupt(task, result) && task.judgeFailCount < judgeFailRetryLimit) { task.status = "retry_wait"; + task.finishedAt = null; + task.readAt = null; const nextPrompt = judgeFailContinuationPrompt(task, judge, task.judgeFailCount); task.nextPrompt = nextPrompt; task.nextMode = "retry"; @@ -5210,7 +7118,7 @@ async function runTask(task: QueueTask): Promise { setAttemptFeedbackPrompt(latestAttempt, nextPrompt, "judge-fail-retry", task.attempts.length + 1); task.updatedAt = nowIso(); appendOutput(task, "system", `judge=fail treated as retry (${task.judgeFailCount}/${judgeFailRetryLimit}); appending continuation prompt to existing session\n`, "queue"); - persistState(); + persistTaskState(task); logger("warn", "task_judge_fail_treated_as_retry", { taskId: task.id, attempt: task.currentAttempt, @@ -5229,7 +7137,7 @@ async function runTask(task: QueueTask): Promise { task.activeTurnId = null; task.lastError = judge.reason; appendOutput(task, "system", `judge=fail reached terminal threshold (${task.judgeFailCount}/${judgeFailRetryLimit}); queue will continue to the next queued task\n`, "queue"); - persistState(); + persistTaskState(task); logger("warn", "task_failed_by_judge_queue_continues", { taskId: task.id, judgeFailCount: task.judgeFailCount, judgeFailRetryLimit, reason: safePreview(judge.reason, 500) }); void notifyTaskTerminal(task); return; @@ -5239,12 +7147,14 @@ async function runTask(task: QueueTask): Promise { return; } task.status = "retry_wait"; + task.finishedAt = null; + task.readAt = null; const nextPrompt = retryPrompt(task, judge); task.nextPrompt = nextPrompt; task.nextMode = "retry"; setAttemptFeedbackPrompt(latestAttempt, nextPrompt, judge.continuePrompt?.trim() ? "judge-continue-prompt" : "judge-retry-generated", task.attempts.length + 1); task.updatedAt = nowIso(); - persistState(); + persistTaskState(task); const delayMs = retryBackoffMs(task.attempts.length); appendOutput(task, "system", `retry backoff ${Math.round(delayMs / 1000)}s before appending continuation to existing session\n`, "queue"); await sleepForRetryBackoff(task, delayMs); @@ -5268,7 +7178,7 @@ async function runTask(task: QueueTask): Promise { task.lastError = `Max attempts reached (${task.maxAttempts}).`; } task.activeTurnId = null; - persistState(); + persistTaskState(task); logger(task.status === "canceled" ? "warn" : "error", "task_terminal", { taskId: task.id, status: task.status, attempts: task.attempts.length, error: task.lastError ?? "" }); void notifyTaskTerminal(task); } @@ -5277,6 +7187,92 @@ function updateProcessingFlag(): void { processing = processingQueues.size > 0; } +function activeRunSlotQueueIds(): string[] { + return Array.from(new Set([ + ...Array.from(activeRuns.keys()), + ...Array.from(activeRunSlotReservations), + ])).sort((left, right) => left.localeCompare(right)); +} + +function activeRunSlotCount(): number { + return activeRunSlotQueueIds().length; +} + +function enqueueActiveRunSlotWaiter(task: QueueTask): ActiveRunSlotWaiter { + const waiter = { + id: nextActiveRunSlotWaiterId, + taskId: task.id, + queueId: queueIdOf(task), + enqueuedAt: nowIso(), + }; + nextActiveRunSlotWaiterId += 1; + activeRunSlotWaiters.push(waiter); + return waiter; +} + +function removeActiveRunSlotWaiter(waiter: ActiveRunSlotWaiter): void { + const index = activeRunSlotWaiters.findIndex((item) => item.id === waiter.id); + if (index >= 0) activeRunSlotWaiters.splice(index, 1); +} + +function firstActiveRunSlotWaiter(): ActiveRunSlotWaiter | null { + return activeRunSlotWaiters[0] ?? null; +} + +function activeRunSlotWaiterSummaries(): JsonValue[] { + return activeRunSlotWaiters.map((waiter, index) => ({ + position: index + 1, + taskId: waiter.taskId, + queueId: waiter.queueId, + enqueuedAt: waiter.enqueuedAt, + })); +} + +async function acquireActiveRunSlot(task: QueueTask): Promise<(() => void) | null> { + const queueId = queueIdOf(task); + const waiter = enqueueActiveRunSlotWaiter(task); + let lastWaitLogAt = 0; + try { + while (!shutdownRequested && !task.cancelRequested && queueTaskIsRunnable(task)) { + const memoryPressure = activeRunMemoryPressure(); + const atHead = firstActiveRunSlotWaiter()?.id === waiter.id; + if (atHead && availableQueueStartSlots() > 0 && memoryPressure === null) { + removeActiveRunSlotWaiter(waiter); + activeRunSlotReservations.add(queueId); + let released = false; + return () => { + if (released) return; + released = true; + activeRunSlotReservations.delete(queueId); + if (!shutdownRequested) scheduleQueue(); + }; + } + if (Date.now() - lastWaitLogAt > 30_000) { + lastWaitLogAt = Date.now(); + const head = firstActiveRunSlotWaiter(); + logger(memoryPressure === null ? "info" : "warn", "active_run_slot_waiting", { + taskId: task.id, + queueId, + waitPosition: activeRunSlotWaiters.findIndex((item) => item.id === waiter.id) + 1, + headTaskId: head?.taskId ?? null, + headQueueId: head?.queueId ?? null, + currentBytes: memoryPressure?.currentBytes ?? null, + inactiveFileBytes: memoryPressure?.inactiveFileBytes ?? null, + workingSetBytes: memoryPressure?.workingSetBytes ?? null, + swapCurrentBytes: memoryPressure?.swapCurrentBytes ?? null, + swapMaxBytes: memoryPressure?.swapMaxBytes ?? null, + thresholdBytes: memoryPressure?.thresholdBytes ?? memoryWatchdogThreshold(), + activeRunSlotCount: activeRunSlotCount(), + }); + } + await Bun.sleep(memoryPressure === null ? 500 : 2000); + } + return null; + } finally { + removeActiveRunSlotWaiter(waiter); + } +} + function taskQueueOrderMs(task: QueueTask): number { return timestampMs(taskQueueEnteredAt(task)) ?? timestampMs(task.createdAt) ?? timestampMs(task.updatedAt) ?? 0; } @@ -5322,6 +7318,15 @@ function runnableQueueIds(): string[] { return queueIdsForTasks().filter((queueId) => nextRunnableTaskFrom(queueId) !== null); } +function availableQueueStartSlotsFor(activeSlotCount: number, maxActiveQueues = config.maxActiveQueues): number { + if (maxActiveQueues <= 0) return Number.POSITIVE_INFINITY; + return Math.max(0, maxActiveQueues - activeSlotCount); +} + +function availableQueueStartSlots(): number { + return availableQueueStartSlotsFor(activeRunSlotCount()); +} + function nextRunnableTask(queueId: string): QueueTask | null { return nextRunnableTaskFrom(queueId); } @@ -5345,7 +7350,7 @@ async function processQueue(queueId: string): Promise { task.activeTurnId = null; task.lastError = safePreview(message, 2000); task.updatedAt = nowIso(); - persistState(); + persistTaskState(task); logger("error", "task_failed_by_queue_exception", { taskId: task.id, error: safePreview(message, 1000) }); void notifyTaskTerminal(task); } @@ -5355,6 +7360,7 @@ async function processQueue(queueId: string): Promise { updateProcessingFlag(); persistState(); if (!shutdownRequested && nextRunnableTask(queueId) !== null) scheduleQueue(queueId); + if (!shutdownRequested) scheduleQueue(); void maybeNotifyQueueIdle().catch((error) => logger("warn", "claudeqq_idle_notify_schedule_failed", { error: errorToJson(error) })); } } @@ -5363,9 +7369,14 @@ function scheduleQueue(queueId?: string): void { if (!serviceReady || shutdownRequested) return; const ids = queueId === undefined ? runnableQueueIds() : [queueId]; for (const id of ids) { + if (processingQueues.has(id)) continue; void processQueue(id).catch((error) => { logger("error", "queue_loop_failed", { queueId: id, error: error instanceof Error ? error.stack ?? error.message : String(error) }); processingQueues.delete(id); + activeRunSlotReservations.delete(id); + for (let index = activeRunSlotWaiters.length - 1; index >= 0; index -= 1) { + if (activeRunSlotWaiters[index]?.queueId === id) activeRunSlotWaiters.splice(index, 1); + } updateProcessingFlag(); const run = activeRuns.get(id); if (run !== undefined) { @@ -5394,6 +7405,8 @@ function installShutdownHandlers(): void { const recovered = queueActiveTasksForRestartRetry("Service stopping while task was active", "shutdown"); for (const run of activeRuns.values()) run.app.stop(); activeRuns.clear(); + activeRunSlotReservations.clear(); + activeRunSlotWaiters.splice(0, activeRunSlotWaiters.length); processingQueues.clear(); updateProcessingFlag(); persistState(); @@ -5426,7 +7439,7 @@ function jsonResponse(body: unknown, status = 200): Response { headers: { "content-type": "application/json; charset=utf-8", "access-control-allow-origin": "*", - "access-control-allow-methods": "GET,HEAD,POST,DELETE,OPTIONS", + "access-control-allow-methods": "GET,HEAD,POST,PATCH,DELETE,OPTIONS", "access-control-allow-headers": "content-type", }, }); @@ -5438,7 +7451,7 @@ function compactJsonResponse(body: unknown, status = 200): Response { headers: { "content-type": "application/json; charset=utf-8", "access-control-allow-origin": "*", - "access-control-allow-methods": "GET,HEAD,POST,DELETE,OPTIONS", + "access-control-allow-methods": "GET,HEAD,POST,PATCH,DELETE,OPTIONS", "access-control-allow-headers": "content-type", }, }); @@ -5482,6 +7495,39 @@ function taskUpdatedSortValue(task: QueueTask): number { return Number.isFinite(time) ? time : 0; } +function taskSearchTerms(url: URL): string[] { + const query = String(url.searchParams.get("search") ?? url.searchParams.get("q") ?? "") + .trim() + .replace(/\s+/gu, " ") + .slice(0, 200) + .toLowerCase(); + return query.length === 0 ? [] : query.split(/\s+/u).filter(Boolean); +} + +function taskMatchesSearch(task: QueueTask, terms: string[]): boolean { + if (terms.length === 0) return true; + const judge = task.lastJudge; + const haystack = [ + task.id, + queueIdOf(task), + task.status, + task.providerId, + task.cwd, + task.model, + task.reasoningEffort ?? "", + task.basePrompt, + userPromptForDisplay(task.prompt), + task.finalResponse, + task.lastError ?? "", + judge?.decision ?? "", + judge?.reason ?? "", + ...task.referenceTaskIds, + ...task.promptHistory.map((item) => item.text), + ...task.output.slice(-50).map((item) => `${item.channel} ${item.method ?? ""} ${item.text}`), + ].join("\n").toLowerCase(); + return terms.every((term) => haystack.includes(term)); +} + function taskPageRows(filteredTasks: QueueTask[], url: URL, limit: number): { rows: QueueTask[]; total: number; @@ -5527,14 +7573,18 @@ function taskPageRows(filteredTasks: QueueTask[], url: URL, limit: number): { }; } -function tasksOverviewResponse(url: URL): Response { +async function tasksOverviewResponse(url: URL): Promise { const limit = parseLimit(url); const compact = truthyParam(url, "compact"); const queueFilter = url.searchParams.get("queueId"); - const filteredTasks = queueFilter === null ? state.tasks : state.tasks.filter((task) => queueIdOf(task) === safeQueueId(queueFilter)); + const searchTerms = taskSearchTerms(url); + const allTasks = await loadAllTasksForRead(); + const filteredTasks = allTasks + .filter((task) => queueFilter === null || queueIdOf(task) === safeQueueId(queueFilter)) + .filter((task) => taskMatchesSearch(task, searchTerms)); const page = taskPageRows(filteredTasks, url, limit); const rowsSource = page.rows; - const queue = queueSummary(false) as Record; + const queue = queueSummary(false, allTasks) as Record; const preferId = url.searchParams.get("preferId") ?? ""; const activeTaskId = typeof queue.activeTaskId === "string" ? queue.activeTaskId : ""; const selectedTask = filteredTasks.find((task) => task.id === preferId) @@ -5568,6 +7618,7 @@ function tasksOverviewResponse(url: URL): Response { return compactJsonResponse({ ok: true, queue, + statistics: taskStatisticsSummary(filteredTasks, statsDaysFromUrl(url)), tasks: rowsSource.map((task) => taskForListResponse(task, true)), selected, pagination: { @@ -5592,15 +7643,16 @@ async function createTasks(req: Request): Promise { const tasks = records.map((record) => { const normalized = normalizeRequest(record); if (normalized.queueId === undefined && batchQueueId !== undefined) normalized.queueId = batchQueueId; - return createTask(injectCodexQueueEnvironmentHint(injectReferencedTaskContext(normalized))); + return createTask(injectCodeQueueEnvironmentHint(injectReferencedTaskContext(normalized))); }); for (const task of tasks) appendOutput(task, "user", `${task.prompt}\n`, "enqueue"); state.tasks.push(...tasks); if (tasks.length > 0) armIdleNotification(); persistState(); - logger("info", "tasks_enqueued", { count: tasks.length, ids: tasks.map((task) => task.id), queueIds: Array.from(new Set(tasks.map(queueIdOf))) }); + logger("info", "tasks_enqueued", { count: tasks.length, ids: tasks.map((task) => task.id), queueIds: Array.from(new Set(tasks.map(queueIdOf))), providerIds: Array.from(new Set(tasks.map((task) => task.providerId))) }); scheduleQueue(); - return jsonResponse({ ok: true, tasks: tasks.map((task) => taskForResponse(task)), queue: queueSummary() }, 202); + await flushDirtyTasksToDatabase(true); + return jsonResponse({ ok: true, tasks: tasks.map((task) => taskForResponse(task)), queue: await queueSummaryForResponse() }, 202); } function testTask(id: string, prompt: string, finalResponse: string, referenceTaskIds: string[] = [], createdAt = nowIso()): QueueTask { @@ -5612,6 +7664,7 @@ function testTask(id: string, prompt: string, finalResponse: string, referenceTa basePrompt: prompt, referenceTaskIds, referenceInjection: null, + providerId: config.mainProviderId, cwd: config.defaultWorkdir, model: config.defaultModel, reasoningEffort: resolveReasoningEffort(config.defaultModel, config.defaultReasoningEffort), @@ -5648,7 +7701,7 @@ function runReferenceInjectionSelfTest(): JsonValue { const at = "2026-05-08T00:00:00.000Z"; const taskA = testTask("codex_1000_aaaaaa", "A base prompt", "A final", [], at); const injectedB = injectReferencedTaskContext({ - prompt: "引用 Codex Queue 任务 codex_1000_aaaaaa。\n\n本次任务:\nB user prompt", + prompt: "引用 Code Queue 任务 codex_1000_aaaaaa。\n\n本次任务:\nB user prompt", referenceTaskIds: [taskA.id], }, (id) => id === taskA.id ? taskA : null, "2026-05-08T00:01:00.000Z"); const taskB = testTask("codex_1001_bbbbbb", injectedB.prompt, "B final", injectedB.referenceTaskIds, "2026-05-08T00:02:00.000Z"); @@ -5659,14 +7712,14 @@ function runReferenceInjectionSelfTest(): JsonValue { referenceTaskIds: [taskB.id], }, (id) => id === taskA.id ? taskA : id === taskB.id ? taskB : null, "2026-05-08T00:03:00.000Z"); const promptC = injectedC.prompt; - const hintedC = injectCodexQueueEnvironmentHint(injectedC); + const hintedC = injectCodeQueueEnvironmentHint(injectedC); assertReferenceTest(injectedB.basePrompt === "B user prompt", "B basePrompt should strip frontend reference hint"); assertReferenceTest(taskB.referenceInjection?.items.length === 1, "B should have one reference item"); assertReferenceTest(injectedC.basePrompt === "C user prompt", "C basePrompt should be raw C prompt"); assertReferenceTest((injectedC.referenceInjection?.items.length ?? 0) === 2, "C should include B and A as two structured graph items"); - assertReferenceTest(hintedC.prompt.startsWith(codexQueueEnvironmentHintTitle), "C should include the Codex Queue environment hint"); + assertReferenceTest(hintedC.prompt.startsWith(codeQueueEnvironmentHintTitle), "C should include the Code Queue environment hint"); assertReferenceTest(userPromptForDisplay(hintedC.prompt) === "C user prompt", "C display prompt should strip environment and reference injection"); - assertReferenceTest(promptWithCodexQueueEnvironmentHint(hintedC.prompt) === hintedC.prompt, "environment hint injection should be idempotent"); + assertReferenceTest(promptWithCodeQueueEnvironmentHint(hintedC.prompt) === hintedC.prompt, "environment hint injection should be idempotent"); const indexA = promptC.indexOf("Round 1.1 referenced task codex_1000_aaaaaa"); const indexB = promptC.indexOf("Round 2.1 referenced task codex_1001_bbbbbb"); assertReferenceTest(indexA >= 0, "C should include upstream A as round 1"); @@ -5676,7 +7729,7 @@ function runReferenceInjectionSelfTest(): JsonValue { assertReferenceTest(promptC.indexOf("----- Reference Round 1/2 -----") < indexA, "round 1 separator should appear before A"); assertReferenceTest(promptC.indexOf("----- Reference Round 2/2 -----") < indexB, "round 2 separator should appear before B"); assertReferenceTest(promptC.includes("### Initial prompt\nB user prompt"), "C should inject B base prompt, not B injected prompt"); - assertReferenceTest(!promptC.includes("### Initial prompt\n# Codex Queue 已解析引用上下文"), "C should not nest prior injected prompt blocks"); + assertReferenceTest(!promptC.includes("### Initial prompt\n# Code Queue 已解析引用上下文"), "C should not nest prior injected prompt blocks"); assertReferenceTest(promptC.includes("injectedAt: 2026-05-08T00:03:00.000Z"), "C should include injection timestamp"); assertReferenceTest(promptC.includes("# 本次任务\nC user prompt"), "C should include current user prompt"); const deepTasks: QueueTask[] = []; @@ -5698,10 +7751,24 @@ function runReferenceInjectionSelfTest(): JsonValue { retryTask.referenceInjection = injectedDeep.referenceInjection ?? null; const retryAfterDeepReference = retryPrompt(retryTask, { decision: "retry", confidence: 1, reason: "Service restarted while task was active", source: "fallback" }); const recoveryAfterDeepReference = queueRecoveryRetryPrompt(retryTask, "Service restarted while task was active"); + const explicitLongContinuePrompt = [ + "请保留并完成以下所有验收点,不要截断:", + ...Array.from({ length: 80 }, (_, index) => `验收点 ${index + 1}: 基于当前 thread 上文补齐缺失证据,并在最终 response 中写出真实命令/API/UI 结果。`), + ].join("\n"); + const explicitRetryPrompt = retryPrompt(retryTask, { decision: "retry", confidence: 1, reason: "Long MiniMax feedback fixture", continuePrompt: explicitLongContinuePrompt, source: "minimax" }); + let longMiniMaxPromptRejectedAtSource = false; + try { + parsedContinuePromptForJudge({ continuePrompt: `${"x".repeat(continuePromptSourceBudgetChars + 1)}` }, "retry"); + } catch { + longMiniMaxPromptRejectedAtSource = true; + } assertReferenceTest(retryAfterDeepReference.length < 2600, "retry prompt should stay compact for referenced tasks"); assertReferenceTest(recoveryAfterDeepReference.length < 2200, "queue recovery prompt should stay compact for referenced tasks"); assertReferenceTest(!retryAfterDeepReference.includes("Reference Round"), "retry prompt should not re-inject reference rounds"); assertReferenceTest(!recoveryAfterDeepReference.includes("Reference Round"), "queue recovery prompt should not re-inject reference rounds"); + assertReferenceTest(explicitRetryPrompt === explicitLongContinuePrompt, "explicit continuePrompt should not be tail-truncated"); + assertReferenceTest(!explicitRetryPrompt.includes("已截断"), "explicit continuePrompt should not include truncation marker"); + assertReferenceTest(longMiniMaxPromptRejectedAtSource, "over-budget MiniMax continuePrompt should be rejected for source repair"); return { ok: true, cases: [ @@ -5714,6 +7781,8 @@ function runReferenceInjectionSelfTest(): JsonValue { { name: "deep_reference_graph_not_six_round_truncated", ok: true, itemCount: injectedDeep.referenceInjection?.itemCount ?? 0 }, { name: "retry_prompt_does_not_reinject_reference_graph", ok: true, chars: retryAfterDeepReference.length }, { name: "queue_recovery_prompt_is_compact", ok: true, chars: recoveryAfterDeepReference.length }, + { name: "explicit_continue_prompt_not_tail_truncated", ok: true, chars: explicitRetryPrompt.length }, + { name: "over_budget_minimax_continue_prompt_requires_source_repair", ok: true, budgetChars: continuePromptSourceBudgetChars }, ], promptPreview: safePreview(promptC, 1200), }; @@ -5745,11 +7814,41 @@ function runQueueOrderingSelfTest(): JsonValue { const queuedBehindRunning = queueOrderTestTask("codex_4101_queued", "queued", "2026-05-11T10:01:00.000Z", "2026-05-11T10:01:00.000Z"); const terminalAhead = queueOrderTestTask("codex_4200_done", "succeeded", "2026-05-11T11:00:00.000Z", "2026-05-11T11:00:00.000Z"); const queuedAfterTerminal = queueOrderTestTask("codex_4201_queued", "queued", "2026-05-11T11:01:00.000Z", "2026-05-11T11:01:00.000Z"); + const originalMaxActiveQueues = config.maxActiveQueues; assertReferenceTest(queueHeadTask("queue_order_test", blockedByRetry)?.id === activeRetry.id, "retry_wait head must keep blocking a moved older-created task"); assertReferenceTest(nextRunnableTaskFrom("queue_order_test", blockedByRetry)?.id === activeRetry.id, "next runnable should be the retry_wait head"); assertReferenceTest(nextRunnableTaskFrom("queue_order_test", [queuedBehindRunning, runningHead]) === null, "running head must block queued tasks behind it"); assertReferenceTest(nextRunnableTaskFrom("queue_order_test", [queuedAfterTerminal, terminalAhead])?.id === queuedAfterTerminal.id, "terminal head should not block later queued task"); + assertReferenceTest(availableQueueStartSlotsFor(0, 1) === 1, "empty active run slots should leave one slot available"); + assertReferenceTest(availableQueueStartSlotsFor(1, 1) === 0, "one active run slot should exhaust maxActiveQueues=1"); + try { + const marker = "__queue_order_idle_processing_self_test__"; + const beforeSlotCount = activeRunSlotCount(); + const hadProcessingMarker = processingQueues.has(marker); + const hadReservationMarker = activeRunSlotReservations.has(marker); + processingQueues.add(marker); + assertReferenceTest(activeRunSlotCount() === beforeSlotCount, "processing idle queue must not consume an active run slot"); + activeRunSlotReservations.add(marker); + assertReferenceTest(activeRunSlotCount() === beforeSlotCount + (hadReservationMarker ? 0 : 1), "reserved running queue must consume an active run slot"); + if (!hadProcessingMarker) processingQueues.delete(marker); + if (!hadReservationMarker) activeRunSlotReservations.delete(marker); + } finally { + config.maxActiveQueues = originalMaxActiveQueues; + updateProcessingFlag(); + } + { + const waiterCount = activeRunSlotWaiters.length; + const firstWaiter = enqueueActiveRunSlotWaiter(activeRetry); + const secondWaiter = enqueueActiveRunSlotWaiter(queuedAfterTerminal); + try { + assertReferenceTest(activeRunSlotWaiters[waiterCount]?.id === firstWaiter.id, "first active run slot waiter should keep FIFO position"); + assertReferenceTest(activeRunSlotWaiters[waiterCount + 1]?.id === secondWaiter.id, "second active run slot waiter should not jump ahead"); + } finally { + removeActiveRunSlotWaiter(firstWaiter); + removeActiveRunSlotWaiter(secondWaiter); + } + } return { ok: true, @@ -5757,22 +7856,683 @@ function runQueueOrderingSelfTest(): JsonValue { { name: "retry_wait_head_blocks_moved_older_created_task", ok: true, head: activeRetry.id, moved: movedOlderCreated.id }, { name: "running_head_blocks_later_queued_task", ok: true }, { name: "terminal_task_does_not_block_queue", ok: true }, + { name: "idle_processing_queue_does_not_consume_active_run_slot", ok: true }, + { name: "active_run_slot_waiters_are_fifo", ok: true }, ], }; } +function opencodeTraceOutput(seq: number, tool: string, input: Record, output: string, status = "completed"): LiveOutput { + return { + seq, + at: "2026-05-12T00:00:00.000Z", + channel: "tool", + method: "opencode/tool", + text: `${JSON.stringify({ + type: "tool_use", + sessionID: "ses_trace_self_test", + part: { + type: "tool", + tool, + id: `prt_trace_${seq}`, + state: { + status, + input, + output, + time: { start: 1000, end: 1337 }, + }, + }, + })}\n`, + itemId: `prt_trace_${seq}`, + }; +} + +function runTracePortSelfTest(): JsonValue { + const task = testTask("codex_5000_trace", "trace prompt", "", [], "2026-05-12T00:00:00.000Z"); + task.model = minimaxM27Model; + task.output = [ + { seq: 1, at: "2026-05-12T00:00:00.000Z", channel: "system", method: "queue", text: "attempt 1/1 queue=default provider=main-server cwd=/root/unidesk mode=initial model=minimax-m2.7 port=opencode\n" }, + opencodeTraceOutput(2, "grep", { command: "rg -n trace src/components/microservices/code-queue/src/index.ts" }, "src/components/microservices/code-queue/src/index.ts:1:trace"), + opencodeTraceOutput(3, "edit", { filePath: "src/components/frontend/src/trace.tsx" }, "M src/components/frontend/src/trace.tsx"), + opencodeTraceOutput(4, "bash", { command: "bunx tsc -p scripts/tsconfig.json --noEmit" }, "ok"), + ]; + const transcript = buildTaskTranscript(task, 20, 0); + const explored = transcript.find((line) => line.seq === 2); + const edited = transcript.find((line) => line.seq === 3); + const ran = transcript.find((line) => line.seq === 4); + if (explored === undefined || edited === undefined || ran === undefined) throw new Error("opencode trace self-test transcript lines missing"); + assertReferenceTest(explored.kind === "explored", "opencode grep tool should normalize to explored trace line"); + assertReferenceTest(edited.kind === "edited", "opencode edit tool should normalize to edited trace line"); + assertReferenceTest(ran.kind === "ran", "opencode bash command should normalize to ran trace line"); + assertReferenceTest(Number(explored.durationMs ?? 0) === 337, "opencode tool duration should be preserved"); + assertReferenceTest(transcriptLineSummaryLines(edited).some((line) => line.includes("trace.tsx")), "summary lines should expose edited path"); + return { + ok: true, + cases: [ + { name: "opencode_tool_to_explored", ok: true, title: explored?.title ?? null }, + { name: "opencode_tool_to_edited", ok: true, title: edited?.title ?? null }, + { name: "opencode_tool_to_ran", ok: true, title: ran?.title ?? null }, + { name: "duration_preserved", ok: true, durationMs: explored?.durationMs ?? null }, + ], + transcript: transcript.filter((line) => line.seq >= 2).map((line) => ({ + seq: line.seq, + kind: line.kind, + title: line.title, + status: line.status, + commandPreview: line.commandPreview, + summaryLines: transcriptLineSummaryLines(line), + durationMs: line.durationMs ?? null, + })), + } as unknown as JsonValue; +} + +function shellQuote(value: string): string { + return `'${value.replace(/'/gu, "'\\''")}'`; +} + +function safeDockerName(value: string): string { + return value.replace(/[^a-zA-Z0-9_.-]/gu, "-").replace(/^-+/u, "").slice(0, 80) || "node"; +} + +function normalizeProviderId(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return /^[A-Za-z0-9_.-]{1,64}$/u.test(trimmed) ? trimmed : null; +} + +function normalizeTaskProviderId(value: unknown): string { + return normalizeProviderId(value) ?? config.mainProviderId; +} + +function providerIsMain(providerId: string): boolean { + return normalizeTaskProviderId(providerId) === config.mainProviderId; +} + +function defaultWorkdirForProvider(providerId: string): string { + return providerIsMain(providerId) ? config.defaultWorkdir : config.remoteDefaultWorkdir; +} + +function resolveTaskCwd(providerId: string, value: unknown): string { + const raw = typeof value === "string" ? value.trim() : ""; + const base = defaultWorkdirForProvider(providerId); + if (raw.length === 0) return base; + return raw.startsWith("/") ? raw : resolve(base, raw); +} + +function remoteHostWorkdirForTask(task: QueueTask): string { + const base = defaultWorkdirForProvider(task.providerId); + return task.cwd === base || task.cwd.startsWith(`${base}/`) ? base : task.cwd; +} + +function executionProviderOptions(): JsonValue[] { + const ids = Array.from(new Set([ + config.mainProviderId, + ...config.executionProviderIds, + config.devContainerDefaultProviderId, + ].map(normalizeProviderId).filter((value): value is string => value !== null))); + return ids.map((providerId) => ({ + id: providerId, + label: providerIsMain(providerId) ? `${providerId} (master)` : providerId, + kind: providerIsMain(providerId) ? "local" : "remote-dev-container", + defaultWorkdir: defaultWorkdirForProvider(providerId), + containerName: providerIsMain(providerId) ? null : buildDevContainerPlan(providerId, {}).containerName, + })); +} + +function numericIdFromProvider(providerId: string): number { + const digits = providerId.match(/\d+/u)?.[0] ?? ""; + if (digits.length > 0) { + const parsed = Number(digits); + if (Number.isInteger(parsed) && parsed >= 0 && parsed <= 4095) return parsed; + } + let hash = 0; + for (const char of providerId) hash = (Math.imul(hash, 31) + char.charCodeAt(0)) >>> 0; + return 100 + (hash % 3900); +} + +function buildDevContainerPlan(providerId: string, body: Record): DevContainerPlan { + const safeProvider = safeDockerName(providerId); + const tunId = numericIdFromProvider(providerId); + const imageFromBody = typeof body.image === "string" && body.image.trim().length > 0 ? body.image.trim() : ""; + const workdirFromBody = typeof body.workdir === "string" && body.workdir.trim().length > 0 ? body.workdir.trim() : ""; + const masterFromBody = typeof body.masterHost === "string" && body.masterHost.trim().length > 0 ? body.masterHost.trim() : ""; + const containerFromBody = typeof body.containerName === "string" && body.containerName.trim().length > 0 ? body.containerName.trim() : ""; + const image = imageFromBody.length > 0 + ? imageFromBody + : config.devContainerImage.length > 0 + ? config.devContainerImage + : "unidesk-code-queue:latest"; + const workdir = workdirFromBody.length > 0 ? workdirFromBody : config.devContainerWorkdir; + const thirdOctet = providerId === "D601" ? 6 : Math.max(1, Math.min(250, Math.floor(tunId / 64))); + const baseOctet = providerId === "D601" ? 0 : (tunId % 64) * 4; + const serverLastOctet = providerId === "D601" ? 1 : baseOctet + 1; + const clientLastOctet = providerId === "D601" ? 2 : baseOctet + 2; + return { + providerId, + containerName: safeDockerName(containerFromBody.length > 0 ? containerFromBody : `unidesk-codex-dev-${safeProvider}`), + image, + workdir, + containerWorkdir: workdir, + remoteCodexHome: "/var/lib/unidesk/code-queue/codex-home", + remoteOpencodeXdgDir: "/var/lib/unidesk/code-queue/opencode-xdg", + masterHost: masterFromBody.length > 0 ? masterFromBody : config.devContainerMasterHost, + tunId, + tunName: `tun${tunId}`, + serverIp: `10.214.${thirdOctet}.${serverLastOctet}`, + clientIp: `10.214.${thirdOctet}.${clientLastOctet}`, + natChain: safeDockerName(`UNIDESK-CODEX-DEV-${safeProvider}`).toUpperCase().slice(0, 28), + keyDir: `/home/ubuntu/.unidesk/codex-dev-proxy/${safeProvider}`, + masterKeyPath: resolve(config.defaultWorkdir, ".state/code-queue/dev-proxy", safeProvider, "id_ed25519"), + }; +} + +function runCodeQueueSsh(providerId: string, script: string, timeoutMs: number, name: string): DevContainerCommandLog { + const started = Date.now(); + const result = spawnSync("bun", ["scripts/cli.ts", "ssh", providerId, "bash -s"], { + cwd: config.defaultWorkdir, + input: script, + encoding: "utf8", + timeout: timeoutMs, + maxBuffer: 8 * 1024 * 1024, + }); + const stdout = typeof result.stdout === "string" ? result.stdout : String(result.stdout ?? ""); + const stderr = typeof result.stderr === "string" ? result.stderr : String(result.stderr ?? ""); + const errorText = result.error === undefined ? "" : `${result.error.name}: ${result.error.message}`; + return { + name, + providerId, + exitCode: result.status, + signal: result.signal, + durationMs: Date.now() - started, + stdout, + stderr: errorText.length > 0 ? `${stderr}\n${errorText}`.trim() : stderr, + }; +} + +function throwIfCommandFailed(command: DevContainerCommandLog): void { + if (command.exitCode === 0) return; + throw new Error(`${command.name} failed on ${command.providerId ?? "local"} exit=${command.exitCode} stderr=${safePreview(command.stderr, 1000)}`); +} + +function masterKeySetupScript(plan: DevContainerPlan): string { + const marker = `unidesk-codex-dev-proxy-${plan.providerId}`; + return `set -euo pipefail +PROVIDER_ID=${shellQuote(plan.providerId)} +TUN_ID=${shellQuote(String(plan.tunId))} +KEY=${shellQuote(plan.masterKeyPath)} +BASE=$(dirname "$KEY") +MARK=${shellQuote(marker)} +mkdir -p "$BASE" /root/.ssh /etc/ssh/sshd_config.d +chmod 700 /root/.ssh +if [ ! -s "$KEY" ]; then + ssh-keygen -t ed25519 -N '' -C "$MARK" -f "$KEY" >/dev/null +fi +chmod 600 "$KEY" +printf 'PermitTunnel yes\\n' > /etc/ssh/sshd_config.d/90-unidesk-codex-dev-tunnel.conf +sshd -t +if command -v systemctl >/dev/null 2>&1; then systemctl reload ssh || systemctl reload sshd || true; fi +if command -v service >/dev/null 2>&1; then service ssh reload || service sshd reload || true; fi +touch /root/.ssh/authorized_keys +chmod 600 /root/.ssh/authorized_keys +tmp=$(mktemp) +grep -v "$MARK" /root/.ssh/authorized_keys > "$tmp" || true +cat "$tmp" > /root/.ssh/authorized_keys +rm -f "$tmp" +PUB=$(cat "$KEY.pub") +printf 'tunnel="%s",no-agent-forwarding,no-X11-forwarding,no-pty %s\\n' "$TUN_ID" "$PUB" >> /root/.ssh/authorized_keys +echo "master_key_ready provider=$PROVIDER_ID key=$KEY permitTunnel=$(sshd -T | awk '$1==\"permittunnel\"{print $2; exit}')"`; +} + +function masterKeyReadScript(plan: DevContainerPlan): string { + return `set -euo pipefail +base64 -w0 ${shellQuote(plan.masterKeyPath)}`; +} + +function remoteKeyInstallScript(plan: DevContainerPlan, privateKeyBase64: string): string { + return `set -euo pipefail +KEY_DIR=${shellQuote(plan.keyDir)} +MASTER=${shellQuote(plan.masterHost)} +umask 077 +mkdir -p "$KEY_DIR" +printf %s ${shellQuote(privateKeyBase64)} | base64 -d > "$KEY_DIR/id_ed25519" +chmod 600 "$KEY_DIR/id_ed25519" +ssh-keyscan -H "$MASTER" > "$KEY_DIR/known_hosts" 2>/dev/null +chmod 600 "$KEY_DIR/known_hosts" +echo "remote_key_ready keyDir=$KEY_DIR master=$MASTER"`; +} + +function base64Text(value: string): string { + return Buffer.from(value, "utf8").toString("base64"); +} + +function remoteCodexEnvFile(): string { + return config.remoteCodexEnvKeys + .map((key) => key.trim()) + .filter((key) => /^[A-Za-z_][A-Za-z0-9_]*$/u.test(key) && process.env[key] !== undefined) + .map((key) => `${key}=${String(process.env[key] ?? "").replace(/\r?\n/gu, "")}`) + .join("\n"); +} + +function remoteCodexConfigText(): string { + const homeConfig = resolve(config.codexHome, "config.toml"); + const path = existsSync(homeConfig) ? homeConfig : config.sourceCodexConfig; + if (!existsSync(path)) return ""; + return readFileSync(path, "utf8"); +} + +function remoteCodexConfigInstallScript(plan: DevContainerPlan): string { + const configBase64 = base64Text(remoteCodexConfigText()); + const envBase64 = base64Text(remoteCodexEnvFile()); + return `set -euo pipefail +KEY_DIR=${shellQuote(plan.keyDir)} +CODEX_HOME_DIR="$KEY_DIR/codex-home" +OPENCODE_XDG_DIR="$KEY_DIR/opencode-xdg" +mkdir -p "$CODEX_HOME_DIR" "$OPENCODE_XDG_DIR" +chmod 700 "$KEY_DIR" "$CODEX_HOME_DIR" "$OPENCODE_XDG_DIR" +printf %s ${shellQuote(configBase64)} | base64 -d > "$CODEX_HOME_DIR/config.toml" +printf %s ${shellQuote(envBase64)} | base64 -d > "$KEY_DIR/codex-env" +chmod 600 "$CODEX_HOME_DIR/config.toml" "$KEY_DIR/codex-env" +echo "remote_codex_config_ready keyDir=$KEY_DIR codexHome=$CODEX_HOME_DIR opencodeXdg=$OPENCODE_XDG_DIR envKeys=${config.remoteCodexEnvKeys.filter((key) => process.env[key] !== undefined).length}"`; +} + +function remoteContainerStartScript(plan: DevContainerPlan, forceRecreate: boolean): string { + return `set -euo pipefail +CONTAINER=${shellQuote(plan.containerName)} +REQUESTED_IMAGE=${shellQuote(plan.image)} +CODEX_IMAGE=unidesk-code-queue:latest +FALLBACK_IMAGE=${shellQuote(`unidesk_provider-gateway:${plan.providerId.toLowerCase()}`)} +KEY_DIR=${shellQuote(plan.keyDir)} +WORKDIR=${shellQuote(plan.workdir)} +CONTAINER_WORKDIR=${shellQuote(plan.containerWorkdir)} +IMAGE="$REQUESTED_IMAGE" +SSH_DIR="\${UNIDESK_HOST_ROOT_SSH_DIR:-$HOME/.ssh}" +SSH_MOUNT_ARGS=() +if [ -d "$SSH_DIR" ]; then SSH_MOUNT_ARGS=(-v "$SSH_DIR":/root/.ssh:ro); fi +if ! docker image inspect "$IMAGE" >/dev/null 2>&1 && docker image inspect "$CODEX_IMAGE" >/dev/null 2>&1; then + IMAGE="$CODEX_IMAGE" +fi +if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then + if docker image inspect "$FALLBACK_IMAGE" >/dev/null 2>&1; then + IMAGE="$FALLBACK_IMAGE" + else + echo "missing requested image $REQUESTED_IMAGE, codex image $CODEX_IMAGE and fallback $FALLBACK_IMAGE" >&2 + exit 1 + fi +fi +test -r "$KEY_DIR/id_ed25519" +test -r "$KEY_DIR/known_hosts" +test -r "$KEY_DIR/codex-env" +test -r /usr/bin/busybox +mkdir -p "$WORKDIR" "$KEY_DIR/opencode-xdg" +if [ ${forceRecreate ? "1" : "0"} -eq 0 ] && docker inspect "$CONTAINER" >/dev/null 2>&1; then + STATE=$(docker inspect "$CONTAINER" --format '{{.State.Status}}' 2>/dev/null || true) + LABEL_WORKDIR=$(docker inspect "$CONTAINER" --format '{{ index .Config.Labels "unidesk.workdir" }}' 2>/dev/null || true) + if [ "$STATE" = "running" ] && [ "$LABEL_WORKDIR" = "$WORKDIR" ]; then + docker exec "$CONTAINER" bash -lc 'echo reuse_ready; command -v bash; test -d /run/unidesk-dev-proxy; test -d /var/lib/unidesk/code-queue/codex-home; test -d /var/lib/unidesk/code-queue/opencode-xdg' >/dev/null + echo "remote_container_reused container=$CONTAINER image=$(docker inspect "$CONTAINER" --format '{{.Config.Image}}') workdir=$WORKDIR" + exit 0 + fi +fi +docker rm -f "$CONTAINER" >/dev/null 2>&1 || true +cid=$(docker run -d \\ + --name "$CONTAINER" \\ + --hostname ${shellQuote(`codex-dev-${plan.providerId}`)} \\ + --user root \\ + --cap-add NET_ADMIN \\ + --device /dev/net/tun \\ + --add-host host.docker.internal:host-gateway \\ + --label unidesk.role=codex-dev \\ + --label unidesk.provider=${shellQuote(plan.providerId)} \\ + --label "unidesk.workdir=$WORKDIR" \\ + --env-file "$KEY_DIR/codex-env" \\ + -e CODEX_HOME=${shellQuote(plan.remoteCodexHome)} \\ + -e CODEX_INTERNAL_ORIGINATOR_OVERRIDE=unidesk_code_queue \\ + -e XDG_DATA_HOME=${shellQuote(resolve(plan.remoteOpencodeXdgDir, "data"))} \\ + -e XDG_CONFIG_HOME=${shellQuote(resolve(plan.remoteOpencodeXdgDir, "config"))} \\ + -e XDG_CACHE_HOME=${shellQuote(resolve(plan.remoteOpencodeXdgDir, "cache"))} \\ + -e XDG_STATE_HOME=${shellQuote(resolve(plan.remoteOpencodeXdgDir, "state"))} \\ + -v /var/run/docker.sock:/var/run/docker.sock \\ + -v /usr/bin/busybox:/usr/local/bin/busybox:ro \\ + -v "$KEY_DIR":/run/unidesk-dev-proxy:ro \\ + -v "$KEY_DIR/codex-home":${shellQuote(plan.remoteCodexHome)} \\ + -v "$KEY_DIR/opencode-xdg":${shellQuote(plan.remoteOpencodeXdgDir)} \\ + "\${SSH_MOUNT_ARGS[@]}" \\ + -v "$WORKDIR":"$CONTAINER_WORKDIR" \\ + -v "$WORKDIR":/root/unidesk \\ + -w "$CONTAINER_WORKDIR" \\ + "$IMAGE" \\ + bash -lc 'mkdir -p /tmp/unidesk-tools; ln -sf /usr/local/bin/busybox /tmp/unidesk-tools/ip; ln -sf /usr/local/bin/busybox /tmp/unidesk-tools/ping; export PATH=/tmp/unidesk-tools:$PATH; echo ready; sleep infinity') +docker exec "$CONTAINER" bash -lc 'mkdir -p /tmp/unidesk-tools; ln -sf /usr/local/bin/busybox /tmp/unidesk-tools/ip; ln -sf /usr/local/bin/busybox /tmp/unidesk-tools/ping; export PATH=/tmp/unidesk-tools:$PATH; echo container=$(hostname); pwd; ip route show default; ls -ld /dev/net/tun /run/unidesk-dev-proxy/id_ed25519 /var/lib/unidesk/code-queue/codex-home /var/lib/unidesk/code-queue/opencode-xdg; command -v ssh; command -v ip; command -v ping' +echo "remote_container_ready container=$CONTAINER cid=$cid image=$IMAGE workdir=$WORKDIR"`; +} + +function masterProxyPrepareScript(plan: DevContainerPlan): string { + return `set -euo pipefail +TUN=${shellQuote(plan.tunName)} +CLIENT_IP=${shellQuote(plan.clientIp)} +CHAIN=${shellQuote(plan.natChain)} +EGRESS=$(ip route get 8.8.8.8 | awk '{for(i=1;i<=NF;i++){if($i=="dev"){print $(i+1); exit}}}') +if [ -z "$EGRESS" ]; then echo "cannot detect master egress interface" >&2; exit 1; fi +ip link show "$TUN" >/dev/null 2>&1 && ip link delete "$TUN" || true +sysctl -w net.ipv4.ip_forward=1 >/dev/null +iptables -t nat -N "$CHAIN" 2>/dev/null || true +iptables -t nat -F "$CHAIN" +iptables -t nat -A "$CHAIN" -s "$CLIENT_IP/32" -o "$EGRESS" -j MASQUERADE +iptables -t nat -C POSTROUTING -j "$CHAIN" 2>/dev/null || iptables -t nat -A POSTROUTING -j "$CHAIN" +iptables -C FORWARD -i "$TUN" -o "$EGRESS" -j ACCEPT 2>/dev/null || iptables -I FORWARD 1 -i "$TUN" -o "$EGRESS" -j ACCEPT +iptables -C FORWARD -i "$EGRESS" -o "$TUN" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || iptables -I FORWARD 1 -i "$EGRESS" -o "$TUN" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +echo "master_proxy_prepared tun=$TUN egress=$EGRESS client=$CLIENT_IP chain=$CHAIN" +iptables -t nat -L "$CHAIN" -v -n -x`; +} + +function containerTunnelStartScript(plan: DevContainerPlan): string { + return `set -euo pipefail +CONTAINER=${shellQuote(plan.containerName)} +docker exec -i "$CONTAINER" bash -s <<'INNER' +set -euo pipefail +mkdir -p /tmp/unidesk-tools +ln -sf /usr/local/bin/busybox /tmp/unidesk-tools/ip +ln -sf /usr/local/bin/busybox /tmp/unidesk-tools/ping +export PATH=/tmp/unidesk-tools:$PATH +MASTER=${plan.masterHost} +TUN_ID=${plan.tunId} +TUN=${plan.tunName} +CLIENT_IP=${plan.clientIp} +SERVER_IP=${plan.serverIp} +KNOWN_HOSTS=/tmp/unidesk-dev-proxy-known_hosts +cp /run/unidesk-dev-proxy/known_hosts "$KNOWN_HOSTS" +chmod 600 "$KNOWN_HOSTS" +DEFAULT_LINE=$(ip route show default | head -1) +ORIG_GW=$(printf '%s\\n' "$DEFAULT_LINE" | sed -n 's/^default via \\([^ ]*\\) dev \\([^ ]*\\).*/\\1/p') +ORIG_DEV=$(printf '%s\\n' "$DEFAULT_LINE" | sed -n 's/^default via \\([^ ]*\\) dev \\([^ ]*\\).*/\\2/p') +if [ -z "$ORIG_GW" ] || [ -z "$ORIG_DEV" ]; then echo "cannot parse default route: $DEFAULT_LINE" >&2; exit 1; fi +( ping -c 1 -W 3 google.com >/tmp/direct-ping.log 2>&1 && echo direct_ping=unexpected_ok ) || echo direct_ping=failed_expected +ip route replace $MASTER/32 via "$ORIG_GW" dev "$ORIG_DEV" +if command -v pkill >/dev/null 2>&1; then pkill -f "ssh .* -w $TUN_ID:$TUN_ID .*$MASTER" >/dev/null 2>&1 || true; fi +ssh -f -N -w $TUN_ID:$TUN_ID -o Tunnel=point-to-point -o ExitOnForwardFailure=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=2 -o StrictHostKeyChecking=yes -o UserKnownHostsFile="$KNOWN_HOSTS" -i /run/unidesk-dev-proxy/id_ed25519 root@$MASTER +for i in 1 2 3 4 5; do ip link show "$TUN" >/dev/null 2>&1 && break; sleep 1; done +ip addr replace $CLIENT_IP peer $SERVER_IP dev "$TUN" +ip link set "$TUN" up +ip route replace default via $SERVER_IP dev "$TUN" +printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\noptions timeout:2 attempts:2\\n' > /etc/resolv.conf +echo "container_tunnel_ready orig=$ORIG_GW/$ORIG_DEV route_to_master=$(ip route get $MASTER | head -1) default=$(ip route show default | head -1) dns=$(tr '\\n' ' ' /dev/null 2>&1 && break; sleep 1; done +ip addr replace $SERVER_IP peer $CLIENT_IP dev "$TUN" +ip link set "$TUN" up +echo "master_tunnel_ready $(ip addr show "$TUN" | tr '\\n' ' ')" +iptables -t nat -L "$CHAIN" -v -n -x`; +} + +function masterProxyEvidenceScript(plan: DevContainerPlan): string { + return `set -euo pipefail +CHAIN=${shellQuote(plan.natChain)} +TUN=${shellQuote(plan.tunName)} +iptables -t nat -L "$CHAIN" -v -n -x +ip -s link show "$TUN"`; +} + +function devContainerPingScript(plan: DevContainerPlan): string { + return `set -euo pipefail +CONTAINER=${shellQuote(plan.containerName)} +docker exec "$CONTAINER" bash -lc 'export PATH=/tmp/unidesk-tools:$PATH; echo route=$(ip route show default | head -1); echo resolv=$(tr "\\n" " " /dev/null 2>&1; then + missing="" + for c in git rg curl python3 pip3 jq rsync patch make gcc g++ tar gzip unzip; do command -v "$c" >/dev/null 2>&1 || missing="$missing $c"; done + if [ -n "$missing" ]; then + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y --no-install-recommends git ripgrep curl python3 python3-pip jq rsync patch make gcc g++ tar gzip unzip ca-certificates + rm -rf /var/lib/apt/lists/* + fi +fi +if ! command -v codex >/dev/null 2>&1; then + npm install -g @openai/codex@0.128.0 +fi +if ! command -v opencode >/dev/null 2>&1; then + npm install -g ${opencodeNpmPackage} +fi +mkdir -p "$WORKDIR" "$CODEX_HOME_DIR" "$OPENCODE_XDG_DIR/data" "$OPENCODE_XDG_DIR/config" "$OPENCODE_XDG_DIR/cache" "$OPENCODE_XDG_DIR/state" +echo "code_agent_runtime_ready codex=$(command -v codex) opencode=$(command -v opencode) cwd=$WORKDIR home=$CODEX_HOME_DIR opencodeXdg=$OPENCODE_XDG_DIR" +INNER`; +} + +function remoteAppServerCommand(task: QueueTask): string { + const plan = buildDevContainerPlan(task.providerId, { workdir: remoteHostWorkdirForTask(task) }); + const inner = [ + "set -euo pipefail", + `mkdir -p ${shellQuote(task.cwd)}`, + `cd ${shellQuote(task.cwd)}`, + `export CODEX_HOME=${shellQuote(plan.remoteCodexHome)}`, + "export CODEX_INTERNAL_ORIGINATOR_OVERRIDE=unidesk_code_queue", + "exec codex app-server --listen stdio://", + ].join("; "); + return `docker exec -i ${shellQuote(plan.containerName)} bash -lc ${shellQuote(inner)}`; +} + +async function startDevContainerPlan(plan: DevContainerPlan, options: { forceRecreate: boolean; verifyPing: boolean; prepareRuntime: boolean }): Promise<{ + ok: boolean; + providerId: string; + plan: DevContainerPlan; + commands: DevContainerCommandLog[]; + verification?: Record; +}> { + const commands: DevContainerCommandLog[] = []; + const run = (targetProviderId: string, script: string, timeoutMs: number, name: string): DevContainerCommandLog => { + const command = runCodeQueueSsh(targetProviderId, script, timeoutMs, name); + commands.push(command); + throwIfCommandFailed(command); + return command; + }; + run("main-server", masterKeySetupScript(plan), 45_000, "master-key-setup"); + const keyRead = run("main-server", masterKeyReadScript(plan), 15_000, "master-key-read"); + const keyBase64 = keyRead.stdout.trim(); + if (!/^[A-Za-z0-9+/=\r\n]+$/u.test(keyBase64) || keyBase64.length < 40) throw new Error("master key read returned invalid base64"); + keyRead.stdout = "[redacted private tunnel key]\n"; + run(plan.providerId, remoteKeyInstallScript(plan, keyBase64), 30_000, "remote-key-install"); + run(plan.providerId, remoteCodexConfigInstallScript(plan), 30_000, "remote-codex-config-install"); + run(plan.providerId, remoteContainerStartScript(plan, options.forceRecreate), 60_000, "remote-container-start"); + run("main-server", masterProxyPrepareScript(plan), 30_000, "master-proxy-prepare"); + run(plan.providerId, containerTunnelStartScript(plan), 45_000, "remote-tunnel-start"); + run("main-server", masterProxyFinishScript(plan), 30_000, "master-proxy-finish"); + if (options.prepareRuntime) run(plan.providerId, remoteCodexRuntimePrepareScript(plan), 180_000, "remote-codex-runtime-prepare"); + const verification: Record = {}; + if (options.verifyPing) { + const before = run("main-server", masterProxyEvidenceScript(plan), 15_000, "master-proxy-evidence-before-ping"); + const ping = run(plan.providerId, devContainerPingScript(plan), 20_000, "remote-container-ping-google"); + const after = run("main-server", masterProxyEvidenceScript(plan), 15_000, "master-proxy-evidence-after-ping"); + verification.pingGoogleOk = ping.exitCode === 0 && /0% packet loss|1 packets received|1 received/iu.test(ping.stdout); + verification.directPingEvidence = commands.find((command) => command.name === "remote-tunnel-start")?.stdout.includes("direct_ping=failed_expected") === true + ? `direct ${plan.providerId} container ping failed before tunnel` + : "direct ping did not fail before tunnel"; + verification.masterProxyEvidenceBefore = safePreview(before.stdout, 2000); + verification.pingGoogleLog = ping.stdout; + verification.masterProxyEvidenceAfter = safePreview(after.stdout, 2000); + } + return { ok: true, providerId: plan.providerId, plan, commands, verification }; +} + +async function ensureTaskExecutionContainer(task: QueueTask): Promise { + if (providerIsMain(task.providerId)) return; + const plan = buildDevContainerPlan(task.providerId, { workdir: remoteHostWorkdirForTask(task) }); + const existing = devContainerEnsurePromises.get(plan.providerId); + if (existing !== undefined) return existing; + const promise = (async () => { + appendOutput(task, "system", `ensuring provider=${plan.providerId} container=${plan.containerName} workdir=${task.cwd}\n`, "provider/container"); + const result = await startDevContainerPlan(plan, { forceRecreate: false, verifyPing: false, prepareRuntime: true }); + appendOutput(task, "system", `provider container ready provider=${plan.providerId} container=${plan.containerName} commands=${result.commands.length}\n`, "provider/container"); + logger("info", "task_provider_container_ready", { + taskId: task.id, + providerId: plan.providerId, + containerName: plan.containerName, + workdir: task.cwd, + hostWorkdir: plan.workdir, + }); + })(); + devContainerEnsurePromises.set(plan.providerId, promise); + try { + await promise; + } finally { + if (devContainerEnsurePromises.get(plan.providerId) === promise) devContainerEnsurePromises.delete(plan.providerId); + } +} + +async function startDevContainer(req: Request, providerFromPath: string | null): Promise { + const body = extractRecord(await readJson(req)) ?? {}; + const providerFromBody = normalizeProviderId(body.providerId); + const providerId = normalizeProviderId(providerFromPath ?? "") ?? providerFromBody ?? normalizeProviderId(config.devContainerDefaultProviderId) ?? "D601"; + const plan = buildDevContainerPlan(providerId, body); + try { + const result = await startDevContainerPlan(plan, { forceRecreate: true, verifyPing: true, prepareRuntime: false }); + logger("info", "dev_container_started", { + providerId, + containerName: plan.containerName, + masterHost: plan.masterHost, + tunName: plan.tunName, + natChain: plan.natChain, + pingPreview: safePreview(String(result.verification?.pingGoogleLog ?? ""), 600), + natAfterPreview: safePreview(String(result.verification?.masterProxyEvidenceAfter ?? ""), 600), + }); + return jsonResponse({ + ok: true, + providerId, + container: { + name: plan.containerName, + image: plan.image, + workdir: plan.workdir, + containerWorkdir: plan.containerWorkdir, + }, + masterProxy: { + mode: "ssh-tun-nat", + masterHost: plan.masterHost, + tunId: plan.tunId, + tunName: plan.tunName, + serverIp: plan.serverIp, + clientIp: plan.clientIp, + natChain: plan.natChain, + }, + verification: result.verification, + commands: result.commands, + }); + } catch (error) { + logger("error", "dev_container_start_failed", { providerId, containerName: plan.containerName, error: errorToJson(error) }); + return jsonResponse({ + ok: false, + error: error instanceof Error ? error.message : String(error), + providerId, + containerName: plan.containerName, + masterProxy: { + mode: "ssh-tun-nat", + masterHost: plan.masterHost, + tunName: plan.tunName, + clientIp: plan.clientIp, + natChain: plan.natChain, + }, + }, 500); + } +} + +async function devContainerStatus(providerFromPath: string | null): Promise { + const providerId = normalizeProviderId(providerFromPath ?? "") ?? normalizeProviderId(config.devContainerDefaultProviderId) ?? "D601"; + const plan = buildDevContainerPlan(providerId, {}); + const commands: DevContainerCommandLog[] = []; + const statusScript = `set -euo pipefail +CONTAINER=${shellQuote(plan.containerName)} +docker inspect "$CONTAINER" --format 'container={{.Name}} state={{.State.Status}} image={{.Config.Image}} workdir={{ index .Config.Labels "unidesk.workdir" }} started={{.State.StartedAt}}' 2>/dev/null || true +if docker inspect "$CONTAINER" >/dev/null 2>&1; then + docker exec "$CONTAINER" bash -lc 'export PATH=/tmp/unidesk-tools:$PATH; echo default=$(ip route show default | head -1); echo resolv=$(tr "\\n" " " /dev/null || true' || true +fi`; + commands.push(runCodeQueueSsh(providerId, statusScript, 15_000, "remote-container-status")); + commands.push(runCodeQueueSsh("main-server", masterProxyEvidenceScript(plan), 15_000, "master-proxy-status")); + return jsonResponse({ + ok: commands.every((command) => command.exitCode === 0), + providerId, + containerName: plan.containerName, + masterProxy: { + mode: "ssh-tun-nat", + masterHost: plan.masterHost, + tunName: plan.tunName, + clientIp: plan.clientIp, + natChain: plan.natChain, + }, + commands, + }); +} + async function steerTask(task: QueueTask, req: Request): Promise { const body = await readJson(req); const prompt = typeof (body as Record).prompt === "string" ? String((body as Record).prompt) : ""; if (prompt.trim().length === 0) return jsonResponse({ ok: false, error: "prompt is required" }, 400); const activeRun = activeRunForTask(task); - if (activeRun === null || activeRun.turnId === null) { + if (activeRun === null || activeRun.threadId === null || activeRun.turnId === null || typeof activeRun.app.steer !== "function") { return jsonResponse({ ok: false, error: "task does not have an active steerable turn", task: taskForResponse(task) }, 409); } const output = appendOutput(task, "user", `\n[steer] ${prompt}\n`, "turn/steer"); appendPromptHistory(task, output, "turn/steer", prompt); await activeRun.app.steer(activeRun.threadId, activeRun.turnId, prompt); - return jsonResponse({ ok: true, task: taskForResponse(task), queue: queueSummary() }); + await flushDirtyTasksToDatabase(true); + return jsonResponse({ ok: true, task: taskForResponse(task), queue: await queueSummaryForResponse() }); +} + +async function editQueuedTaskPrompt(task: QueueTask, req: Request): Promise { + if (!queuedTaskPromptEditable(task)) { + return jsonResponse({ + ok: false, + error: `task prompt can only be edited before first run while status=queued; current status=${task.status}`, + editable: false, + task: taskForResponse(task), + }, 409); + } + let update: QueueTaskRequest; + try { + update = buildQueuedPromptUpdate(task, await readJson(req)); + } catch (error) { + return jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400); + } + const nextBasePrompt = update.basePrompt ?? userPromptForDisplay(update.prompt); + const nextReferenceTaskIds = update.referenceTaskIds ?? []; + if (task.prompt === update.prompt && task.basePrompt === nextBasePrompt && taskReferencesEqual(task.referenceTaskIds, nextReferenceTaskIds)) { + return jsonResponse({ ok: true, changed: false, editable: true, task: taskForResponse(task), queue: await queueSummaryForResponse(false) }); + } + const previousPromptChars = task.prompt.length; + const previousBasePromptChars = task.basePrompt.length; + task.prompt = update.prompt; + task.basePrompt = nextBasePrompt; + task.referenceTaskIds = nextReferenceTaskIds; + task.referenceInjection = update.referenceInjection ?? null; + task.updatedAt = nowIso(); + rewriteEnqueueOutput(task); + appendOutput(task, "system", `queued prompt edited before first run; base ${previousBasePromptChars}->${task.basePrompt.length} chars; final ${previousPromptChars}->${task.prompt.length} chars\n`, "prompt/edit"); + persistState(); + logger("info", "queued_task_prompt_edited", { + taskId: task.id, + queueId: queueIdOf(task), + promptChars: task.prompt.length, + basePromptChars: task.basePrompt.length, + referenceTaskIds: task.referenceTaskIds, + }); + scheduleQueue(queueIdOf(task)); + await flushDirtyTasksToDatabase(true); + return jsonResponse({ ok: true, changed: true, editable: true, task: taskForResponse(task), queue: await queueSummaryForResponse() }); } async function interruptTask(task: QueueTask): Promise { @@ -5784,7 +8544,7 @@ async function interruptTask(task: QueueTask): Promise { appendOutput(task, "system", "interrupt requested\n", "turn/interrupt"); const activeRun = activeRunForTask(task); if (activeRun !== null) { - if (activeRun.turnId !== null) { + if (activeRun.threadId !== null && activeRun.turnId !== null && typeof activeRun.app.interrupt === "function") { await activeRun.app.interrupt(activeRun.threadId, activeRun.turnId).catch((error) => { appendOutput(task, "error", `interrupt request failed: ${error instanceof Error ? error.message : String(error)}\n`, "turn/interrupt"); }); @@ -5796,11 +8556,12 @@ async function interruptTask(task: QueueTask): Promise { task.status = "canceled"; task.finishedAt = nowIso(); } - persistState(); + persistTaskState(task); if (terminalTask(task)) { void notifyTaskTerminal(task).then(() => maybeNotifyQueueIdle(task.id)).catch((error) => logger("warn", "claudeqq_interrupt_notify_failed", { taskId: task.id, error: errorToJson(error) })); } - return jsonResponse({ ok: true, task: taskForResponse(task), queue: queueSummary() }); + await flushDirtyTasksToDatabase(true); + return jsonResponse({ ok: true, task: taskForResponse(task), queue: await queueSummaryForResponse() }); } async function manualRetry(task: QueueTask, req: Request): Promise { @@ -5825,10 +8586,11 @@ async function manualRetry(task: QueueTask, req: Request): Promise { armIdleNotification(); persistState(); scheduleQueue(queueIdOf(task)); - return jsonResponse({ ok: true, task: taskForResponse(task), queue: queueSummary() }, 202); + await flushDirtyTasksToDatabase(true); + return jsonResponse({ ok: true, task: taskForResponse(task), queue: await queueSummaryForResponse() }, 202); } -function markTaskRead(task: QueueTask): Response { +async function markTaskRead(task: QueueTask): Promise { if (!terminalTask(task)) { return jsonResponse({ ok: false, error: `task is not terminal: ${task.status}`, task: taskForResponse(task) }, 409); } @@ -5838,10 +8600,11 @@ function markTaskRead(task: QueueTask): Response { persistState(false); logger("info", "task_marked_read", { taskId: task.id, queueId: queueIdOf(task), status: task.status }); } - return jsonResponse({ ok: true, task: taskForResponse(task), queue: queueSummary(false) }); + await flushDirtyTasksToDatabase(true); + return jsonResponse({ ok: true, task: taskForResponse(task), queue: await queueSummaryForResponse(false) }); } -function markTerminalTasksRead(url: URL): Response { +async function markTerminalTasksRead(url: URL): Promise { const queueFilter = url.searchParams.get("queueId"); const queueId = queueFilter === null || queueFilter.length === 0 ? null : safeQueueId(queueFilter); const readAt = nowIso(); @@ -5857,7 +8620,8 @@ function markTerminalTasksRead(url: URL): Response { persistState(false); logger("info", "terminal_tasks_marked_read", { count, queueId }); } - return jsonResponse({ ok: true, count, readAt, queue: queueSummary(false) }); + await flushDirtyTasksToDatabase(true); + return jsonResponse({ ok: true, count, readAt, queue: await queueSummaryForResponse(false) }); } async function createQueue(req: Request): Promise { @@ -5865,12 +8629,34 @@ async function createQueue(req: Request): Promise { const record = typeof body === "object" && body !== null && !Array.isArray(body) ? body as Record : {}; const queueId = normalizeQueueId(record.queueId ?? record.id); const beforeCount = state.queues.length; - const queue = ensureQueue(queueId); + const queue = ensureQueue(queueId, record.name); queue.updatedAt = nowIso(); markQueueDirty(queue.id); persistState(false); - logger("info", "queue_created", { queueId, existed: beforeCount === state.queues.length }); - return jsonResponse({ ok: true, queue, queues: perQueueSummaries(), summary: queueSummary(false) }, beforeCount === state.queues.length ? 200 : 201); + logger("info", "queue_created", { queueId, name: queue.name, existed: beforeCount === state.queues.length }); + await flushDirtyTasksToDatabase(true); + const tasks = await loadAllTasksForRead(); + return jsonResponse({ ok: true, queue, queues: perQueueSummaries(tasks), summary: queueSummary(false, tasks) }, beforeCount === state.queues.length ? 200 : 201); +} + +async function updateQueue(queueIdValue: string, req: Request): Promise { + const queueId = normalizeQueueId(queueIdValue); + const queue = state.queues.find((item) => item.id === queueId); + if (queue === undefined) return jsonResponse({ ok: false, error: "queue not found" }, 404); + const body = await readJson(req); + const record = typeof body === "object" && body !== null && !Array.isArray(body) ? body as Record : {}; + if (!Object.prototype.hasOwnProperty.call(record, "name")) { + return jsonResponse({ ok: false, error: "name is required" }, 400); + } + const previousName = safeQueueName(queue.name, queue.id); + queue.name = normalizeQueueName(record.name, queue.id); + queue.updatedAt = nowIso(); + markQueueDirty(queue.id); + persistState(false); + logger("info", "queue_updated", { queueId, previousName, name: queue.name }); + await flushDirtyTasksToDatabase(true); + const tasks = await loadAllTasksForRead(); + return jsonResponse({ ok: true, queue, queues: perQueueSummaries(tasks), summary: queueSummary(false, tasks) }); } async function moveTaskToQueue(task: QueueTask, req: Request): Promise { @@ -5893,19 +8679,45 @@ async function moveTaskToQueue(task: QueueTask, req: Request): Promise persistState(); logger("info", "task_moved_queue", { taskId: task.id, previousQueueId, queueId, status: task.status }); if (task.status === "queued" || task.status === "retry_wait") scheduleQueue(queueId); - return jsonResponse({ ok: true, task: taskForResponse(task), queue: queueSummary() }, 202); + await flushDirtyTasksToDatabase(true); + return jsonResponse({ ok: true, task: taskForResponse(task), queue: await queueSummaryForResponse() }, 202); } async function route(req: Request): Promise { const url = new URL(req.url); if (req.method === "OPTIONS") return jsonResponse({ ok: true }); try { - if (url.pathname === "/" || url.pathname === "/health") return jsonResponse({ ok: true, service: "codex-queue", queue: queueSummary(), startedAt: serviceStartedAt }); + if (url.pathname === "/" || url.pathname === "/health") return jsonResponse({ ok: true, service: "code-queue", queue: await queueSummaryForHealth(), startedAt: serviceStartedAt }); if (url.pathname === "/logs") return jsonResponse({ ok: true, logs: recentLogs.slice(-parseLimit(url)) }); if (url.pathname === "/api/dev-ready" && req.method === "GET") return jsonResponse({ ok: true, devReady: collectDevReady() }); + if (url.pathname === "/api/dev-containers" && req.method === "GET") { + const plan = buildDevContainerPlan(normalizeProviderId(config.devContainerDefaultProviderId) ?? "D601", {}); + return jsonResponse({ + ok: true, + defaultProviderId: plan.providerId, + startEndpoint: `/api/dev-containers/${encodeURIComponent(plan.providerId)}/start`, + statusEndpoint: `/api/dev-containers/${encodeURIComponent(plan.providerId)}/status`, + masterProxyMode: "ssh-tun-nat", + defaultPlan: { + containerName: plan.containerName, + image: plan.image, + workdir: plan.workdir, + masterHost: plan.masterHost, + tunName: plan.tunName, + serverIp: plan.serverIp, + clientIp: plan.clientIp, + natChain: plan.natChain, + }, + }); + } + const devContainerStartMatch = url.pathname.match(/^\/api\/dev-containers(?:\/([^/]+))?\/start$/u); + if (devContainerStartMatch !== null && req.method === "POST") return startDevContainer(req, devContainerStartMatch[1] === undefined ? null : decodeURIComponent(devContainerStartMatch[1])); + const devContainerStatusMatch = url.pathname.match(/^\/api\/dev-containers(?:\/([^/]+))?\/status$/u); + if (devContainerStatusMatch !== null && req.method === "GET") return devContainerStatus(devContainerStatusMatch[1] === undefined ? null : decodeURIComponent(devContainerStatusMatch[1])); if (url.pathname === "/api/judge/probe" && (req.method === "GET" || req.method === "POST")) return runJudgeProbe(); if (url.pathname === "/api/queue-order/self-test" && (req.method === "GET" || req.method === "POST")) return jsonResponse(runQueueOrderingSelfTest()); if (url.pathname === "/api/reference-injection/self-test" && (req.method === "GET" || req.method === "POST")) return jsonResponse(runReferenceInjectionSelfTest()); + if (url.pathname === "/api/trace-port/self-test" && (req.method === "GET" || req.method === "POST")) return jsonResponse(runTracePortSelfTest()); if (url.pathname === "/api/notifications/claudeqq" && req.method === "GET") { await loadClaudeQqNotificationOutboxFromDatabase(); const limit = parseLimit(url); @@ -5937,24 +8749,40 @@ async function route(req: Request): Promise { const limit = Number.isInteger(rawLimit) && rawLimit > 0 ? Math.min(500, rawLimit) : 100; return jsonResponse(await backfillClaudeQqTaskNotifications(since, limit, dryRun)); } - if (url.pathname === "/api/queues" && req.method === "GET") return jsonResponse({ ok: true, queues: perQueueSummaries(), queue: queueSummary(false) }); + if (url.pathname === "/api/queues" && req.method === "GET") { + const tasks = await loadAllTasksForRead(); + return jsonResponse({ ok: true, queues: perQueueSummaries(tasks), queue: queueSummary(false, tasks) }); + } if (url.pathname === "/api/queues" && req.method === "POST") return createQueue(req); + const queueMatch = url.pathname.match(/^\/api\/queues\/([^/]+)$/u); + if (queueMatch !== null && (req.method === "PATCH" || req.method === "PUT" || req.method === "POST")) return updateQueue(decodeURIComponent(queueMatch[1] ?? ""), req); if (url.pathname === "/api/tasks/read-all" && req.method === "POST") return markTerminalTasksRead(url); + if (url.pathname === "/api/tasks/stats" && req.method === "GET") { + const queueId = url.searchParams.get("queueId"); + const allTasks = await loadAllTasksForRead(); + const statsTasks = queueId === null ? allTasks : allTasks.filter((task) => queueIdOf(task) === safeQueueId(queueId)); + return jsonResponse({ ok: true, statistics: taskStatisticsSummary(statsTasks, statsDaysFromUrl(url)), queue: queueSummary(false, allTasks) }); + } if (url.pathname === "/api/tasks/overview" && req.method === "GET") return tasksOverviewResponse(url); if (url.pathname === "/api/tasks" && req.method === "GET") { const status = url.searchParams.get("status"); const queueId = url.searchParams.get("queueId"); const lite = url.searchParams.get("lite") === "1"; const includeDevReady = url.searchParams.get("devReady") !== "0" && !lite; - const filteredTasks = state.tasks + const searchTerms = taskSearchTerms(url); + const allTasks = await loadAllTasksForRead(); + const queueFilteredTasks = queueId === null ? allTasks : allTasks.filter((task) => queueIdOf(task) === safeQueueId(queueId)); + const filteredTasks = allTasks .filter((task) => status === null || task.status === status) - .filter((task) => queueId === null || queueIdOf(task) === safeQueueId(queueId)); + .filter((task) => queueId === null || queueIdOf(task) === safeQueueId(queueId)) + .filter((task) => taskMatchesSearch(task, searchTerms)); const limit = parseLimit(url); const page = taskPageRows(filteredTasks, url, limit); const tasks = page.rows.map((task) => taskForListResponse(task, lite)); return jsonResponse({ ok: true, - queue: queueSummary(includeDevReady), + queue: queueSummary(includeDevReady, allTasks), + statistics: taskStatisticsSummary(queueFilteredTasks, statsDaysFromUrl(url)), tasks, pagination: { limit, @@ -5970,56 +8798,64 @@ async function route(req: Request): Promise { if ((url.pathname === "/api/tasks" || url.pathname === "/api/tasks/batch") && req.method === "POST") return createTasks(req); const outputMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/output$/u); if (outputMatch !== null && req.method === "GET") { - const task = findTask(decodeURIComponent(outputMatch[1] ?? "")); + const task = await findTaskForRead(decodeURIComponent(outputMatch[1] ?? "")); if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404); return outputChunkResponse(task, url); } const transcriptMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/transcript$/u); if (transcriptMatch !== null && req.method === "GET") { - const task = findTask(decodeURIComponent(transcriptMatch[1] ?? "")); + const task = await findTaskForRead(decodeURIComponent(transcriptMatch[1] ?? "")); if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404); return transcriptChunkResponse(task, url); } const promptMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/prompt$/u); if (promptMatch !== null && req.method === "GET") { - const task = findTask(decodeURIComponent(promptMatch[1] ?? "")); + const task = await findTaskForRead(decodeURIComponent(promptMatch[1] ?? "")); if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404); return taskPromptDetailResponse(task, url); } + if (promptMatch !== null && req.method === "PATCH") { + const task = await findTaskForMutation(decodeURIComponent(promptMatch[1] ?? "")); + if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404); + return editQueuedTaskPrompt(task, req); + } const traceSummaryMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/trace-summary$/u); if (traceSummaryMatch !== null && req.method === "GET") { - const task = findTask(decodeURIComponent(traceSummaryMatch[1] ?? "")); + const task = await findTaskForRead(decodeURIComponent(traceSummaryMatch[1] ?? "")); if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404); return jsonResponse({ ok: true, summary: taskTraceSummaryResponse(task) }); } const traceStepsMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/trace-steps$/u); if (traceStepsMatch !== null && req.method === "GET") { - const task = findTask(decodeURIComponent(traceStepsMatch[1] ?? "")); + const task = await findTaskForRead(decodeURIComponent(traceStepsMatch[1] ?? "")); if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404); return taskTraceStepsResponse(task, url); } const traceStepMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/trace-step$/u); if (traceStepMatch !== null && req.method === "GET") { - const task = findTask(decodeURIComponent(traceStepMatch[1] ?? "")); + const task = await findTaskForRead(decodeURIComponent(traceStepMatch[1] ?? "")); if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404); return taskTraceStepDetailResponse(task, url); } const summaryMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/summary$/u); if (summaryMatch !== null && req.method === "GET") { - const task = findTask(decodeURIComponent(summaryMatch[1] ?? "")); + const task = await findTaskForRead(decodeURIComponent(summaryMatch[1] ?? "")); if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404); return jsonResponse({ ok: true, summary: taskSummaryResponse(task, url) }); } - const match = url.pathname.match(/^\/api\/tasks\/([^/]+)(?:\/(retry|steer|interrupt|move|read))?$/u); + const match = url.pathname.match(/^\/api\/tasks\/([^/]+)(?:\/(retry|steer|interrupt|move|read|edit))?$/u); if (match !== null) { - const task = findTask(decodeURIComponent(match[1] ?? "")); - if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404); const action = match[2]; + const task = action === undefined && req.method === "GET" + ? await findTaskForRead(decodeURIComponent(match[1] ?? "")) + : await findTaskForMutation(decodeURIComponent(match[1] ?? "")); + if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404); if (action === "retry" && req.method === "POST") return manualRetry(task, req); if (action === "steer" && req.method === "POST") return steerTask(task, req); if (action === "interrupt" && req.method === "POST") return interruptTask(task); if (action === "move" && req.method === "POST") return moveTaskToQueue(task, req); if (action === "read" && req.method === "POST") return markTaskRead(task); + if (action === "edit" && (req.method === "POST" || req.method === "PATCH")) return editQueuedTaskPrompt(task, req); if (action !== undefined) return jsonResponse({ ok: false, error: "not found" }, 404); if (req.method === "GET") { if (url.searchParams.get("meta") === "1") return jsonResponse({ ok: true, task: taskForMetaResponse(task) }); @@ -6038,6 +8874,9 @@ async function route(req: Request): Promise { installShutdownHandlers(); prepareCodexHome(); +prepareOpenCodeHome(); +startCodexSqliteLogExporter(); +startMemoryWatchdog(); await initDatabasePersistenceWithRetry(); Bun.serve({ hostname: config.host, port: config.port, idleTimeout: 120, fetch: route }); logger("info", "service_started", { port: config.port, workdir: config.defaultWorkdir, defaultModel: config.defaultModel, judgeConfigured: config.minimaxApiKey.length > 0, storage: "postgres" }); diff --git a/src/components/microservices/code-queue/tsconfig.json b/src/components/microservices/code-queue/tsconfig.json new file mode 100644 index 00000000..5d5f23f2 --- /dev/null +++ b/src/components/microservices/code-queue/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "composite": true, + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["bun", "node"], + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "references": [{ "path": "../../shared" }] +} diff --git a/src/components/microservices/project-manager/Dockerfile b/src/components/microservices/project-manager/Dockerfile index 6a085c9d..2f55674a 100644 --- a/src/components/microservices/project-manager/Dockerfile +++ b/src/components/microservices/project-manager/Dockerfile @@ -1,9 +1,10 @@ FROM oven/bun:1-alpine -WORKDIR /app +WORKDIR /app/src/components/microservices/project-manager COPY src/components/microservices/project-manager/package.json ./package.json RUN bun install --production COPY src/components/microservices/project-manager/tsconfig.json ./tsconfig.json +COPY src/components/shared /app/src/components/shared COPY src/components/microservices/project-manager/src ./src EXPOSE 4233 diff --git a/src/components/microservices/project-manager/src/index.ts b/src/components/microservices/project-manager/src/index.ts index 60db10ef..f2a702c9 100644 --- a/src/components/microservices/project-manager/src/index.ts +++ b/src/components/microservices/project-manager/src/index.ts @@ -1,8 +1,7 @@ import { createHash, randomUUID } from "node:crypto"; -import { appendFileSync, mkdirSync } from "node:fs"; -import { dirname } from "node:path"; import postgres from "postgres"; import * as XLSX from "xlsx"; +import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../../shared/src/rotating-jsonl"; type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; type JsonRecord = Record; @@ -96,15 +95,22 @@ function configFromEnv(): RuntimeConfig { const config = configFromEnv(); const sql = postgres(config.databaseUrl, { max: 8, idle_timeout: 20, connect_timeout: 10 }); +const logWriter = config.logFile + ? createHourlyJsonlWriter({ + baseLogFile: config.logFile, + service: "project-manager", + maxBytes: logRetentionBytesForService("project-manager"), + }) + : null; +logWriter?.prune(); function log(event: string, detail: JsonRecord = {}): void { const record: JsonRecord = { at: new Date().toISOString(), event, ...detail }; recentLogs.push(record); if (recentLogs.length > 300) recentLogs.shift(); - if (config.logFile) { + if (logWriter !== null) { try { - mkdirSync(dirname(config.logFile), { recursive: true }); - appendFileSync(config.logFile, `${JSON.stringify(record)}\n`, "utf8"); + logWriter.appendJson(record, new Date(String(record.at))); } catch { // Logging must not break request handling. } diff --git a/src/components/microservices/project-manager/tsconfig.json b/src/components/microservices/project-manager/tsconfig.json index f62969e7..5d5f23f2 100644 --- a/src/components/microservices/project-manager/tsconfig.json +++ b/src/components/microservices/project-manager/tsconfig.json @@ -13,5 +13,6 @@ "outDir": "dist", "skipLibCheck": true }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], + "references": [{ "path": "../../shared" }] } diff --git a/src/components/provider-gateway/package.json b/src/components/provider-gateway/package.json index ec5c9b34..005dc017 100644 --- a/src/components/provider-gateway/package.json +++ b/src/components/provider-gateway/package.json @@ -1,6 +1,6 @@ { "name": "@unidesk/provider-gateway", - "version": "0.2.12", + "version": "0.2.17", "private": true, "type": "module", "scripts": { diff --git a/src/components/provider-gateway/src/index.ts b/src/components/provider-gateway/src/index.ts index 18999294..55266b71 100644 --- a/src/components/provider-gateway/src/index.ts +++ b/src/components/provider-gateway/src/index.ts @@ -1,5 +1,5 @@ -import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs"; -import { dirname } from "node:path"; +import { spawnSync } from "node:child_process"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; import { type CoreDispatchMessage, type CoreHostSshCloseMessage, @@ -19,6 +19,7 @@ import { type SystemStatusSnapshot, parseJsonObject, } from "../../shared/src/index"; +import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../shared/src/rotating-jsonl"; interface RuntimeConfig { serverUrl: string; @@ -65,6 +66,8 @@ let reconnectAttempt = 0; let stopping = false; let upgradeSleepUntil = 0; let upgradeSleepTimer: ReturnType | null = null; +let selfContainerGuard: ProviderGatewayContainerGuardState | null = null; +let selfContainerGuardLastCheckMs = 0; interface MicroserviceHttpCacheEntry { createdAt: number; @@ -83,11 +86,53 @@ interface HostSshStdin { end(): unknown; } +interface ProviderGatewayContainerGuardState { + checkedAt: string; + containerId: string; + containerName: string; + restartPolicy: string; + restartPolicyMaximumRetryCount: number; + restartPolicyOk: boolean; + pidMode: string; + pidModeOk: boolean; + remediatedRestartPolicy: boolean; + ok: boolean; + error: string; +} + +interface DockerContainerInspectDetails { + id: string; + name: string; + restartPolicy: string; + restartPolicyMaximumRetryCount: number; + pidMode: string; + composeProject: string; + composeService: string; + labels: Record; +} + const hostSshSessions = new Map(); const microserviceHttpCache = new Map(); const microserviceHttpInFlight = new Map>(); const gatewayMetadata = readGatewayMetadata(); +const defaultMasterServer = "http://74.48.78.17/"; +const defaultProviderToken = "unidesk-dev-token-change-me"; const microserviceHttpMaxBodyTextLength = 8 * 1024 * 1024; +const microserviceForwardRequestHeaders = [ + "accept", + "content-type", + "range", + "x-auth", + "x-requested-with", + "destination", + "overwrite", + "tus-resumable", + "upload-concat", + "upload-defer-length", + "upload-length", + "upload-metadata", + "upload-offset", +] as const; function readGatewayMetadataFile(path: string): { name: string; version: string } | null { try { @@ -112,16 +157,24 @@ function readTargetGatewayMetadata(workspace: string): { name: string; version: return readGatewayMetadataFile(`${root}/src/components/provider-gateway/package.json`) ?? gatewayMetadata; } -function requiredEnv(name: string): string { - const value = process.env[name]; - if (value === undefined || value.length === 0) { - throw new Error(`Missing required environment variable: ${name}`); +function readEnv(...names: string[]): string | null { + for (const name of names) { + const value = process.env[name]; + if (value !== undefined && value.length > 0) return value; + } + return null; +} + +function requiredEnv(name: string, ...aliases: string[]): string { + const value = readEnv(name, ...aliases); + if (value === null) { + throw new Error(`Missing required environment variable: ${[name, ...aliases].join(" or ")}`); } return value; } -function readNumberEnv(name: string): number { - const raw = requiredEnv(name); +function readNumberEnv(name: string, fallback: number, ...aliases: string[]): number { + const raw = readEnv(name, ...aliases) ?? String(fallback); const parsed = Number(raw); if (!Number.isFinite(parsed) || parsed <= 0) { throw new Error(`Environment variable ${name} must be a positive number, got ${raw}`); @@ -129,14 +182,12 @@ function readNumberEnv(name: string): number { return parsed; } -function readOptionalStringEnv(name: string): string | null { - const value = process.env[name]; - if (value === undefined || value.length === 0) return null; - return value; +function readOptionalStringEnv(name: string, ...aliases: string[]): string | null { + return readEnv(name, ...aliases); } -function readOptionalNumberEnv(name: string): number | null { - const raw = readOptionalStringEnv(name); +function readOptionalNumberEnv(name: string, ...aliases: string[]): number | null { + const raw = readOptionalStringEnv(name, ...aliases); if (raw === null) return null; const parsed = Number(raw); if (!Number.isFinite(parsed) || parsed <= 0) { @@ -145,44 +196,290 @@ function readOptionalNumberEnv(name: string): number | null { return parsed; } -function readConfig(): RuntimeConfig { +function providerServerUrlFromMaster(rawMaster: string): string { + const raw = rawMaster.trim() || defaultMasterServer; + const withScheme = /^[a-z][a-z0-9+.-]*:\/\//iu.test(raw) ? raw : `http://${raw}`; + const url = new URL(withScheme); + if (url.protocol === "ws:" || url.protocol === "wss:") { + if (url.pathname === "" || url.pathname === "/") url.pathname = "/ws/provider"; + return url.toString(); + } + const providerIngressPort = readEnv("PROVIDER_INGRESS_PORT", "UNIDESK_PROVIDER_INGRESS_PORT") + ?? (url.port && url.port !== "18081" ? url.port : "18082"); + const protocol = url.protocol === "https:" ? "wss:" : "ws:"; + const host = url.hostname.includes(":") ? `[${url.hostname}]` : url.hostname; + const providerUrl = new URL(`${protocol}//${host}:${providerIngressPort}/ws/provider`); + return providerUrl.toString(); +} + +function readProviderServerUrl(): string { + const explicit = readEnv("PROVIDER_SERVER_URL", "UNIDESK_PROVIDER_SERVER_URL"); + if (explicit !== null) return explicit; + return providerServerUrlFromMaster(readEnv("UNIDESK_MASTER_SERVER", "UNIDESK_MASTER_SERVER_URL", "MASTER_SERVER_URL", "MASTER_SERVER_IP") ?? defaultMasterServer); +} + +function readProviderId(): string { + return requiredEnv("PROVIDER_ID", "UNIDESK_PROVIDER_ID"); +} + +function safeProviderSlug(value: string): string { + return value.replace(/[^a-zA-Z0-9_.-]/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "provider"; +} + +function safeReadText(path: string): string { + try { + return readFileSync(path, "utf8"); + } catch { + return ""; + } +} + +function defaultProviderLabels(providerId: string): ProviderLabels { + const labels: ProviderLabels = { + host: providerId, + role: "compute-provider", + docker: true, + attachMode: "simple", + }; + const kernel = safeReadText("/proc/sys/kernel/osrelease").toLowerCase(); + if (kernel.includes("microsoft") || kernel.includes("wsl") || process.env.WSL_INTEROP !== undefined) { + labels.role = "wsl-provider"; + labels.wsl = true; + } + const osRelease = safeReadText("/etc/os-release"); + const distro = osRelease.match(/^PRETTY_NAME="?([^"\n]+)"?/m)?.[1] ?? osRelease.match(/^ID="?([^"\n]+)"?/m)?.[1] ?? ""; + if (distro.length > 0) labels.distro = distro; + return labels; +} + +function readProviderLabels(providerId: string): ProviderLabels { + const raw = readEnv("PROVIDER_LABELS_JSON", "UNIDESK_PROVIDER_LABELS_JSON"); + return raw === null ? defaultProviderLabels(providerId) : parseJsonObject(raw, "PROVIDER_LABELS_JSON"); +} + +function goTemplateString(value: string): string { + return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`; +} + +function currentContainerId(): string | null { + const hostname = (process.env.HOSTNAME ?? safeReadText("/etc/hostname")).trim(); + return hostname.length > 0 ? hostname : null; +} + +function unknownContainerGuardState(error: string): ProviderGatewayContainerGuardState { return { - serverUrl: requiredEnv("PROVIDER_SERVER_URL"), - token: requiredEnv("PROVIDER_TOKEN"), - providerId: requiredEnv("PROVIDER_ID"), - providerName: requiredEnv("PROVIDER_NAME"), - labels: parseJsonObject(requiredEnv("PROVIDER_LABELS_JSON"), "PROVIDER_LABELS_JSON"), - heartbeatIntervalMs: readNumberEnv("HEARTBEAT_INTERVAL_MS"), - reconnectBaseMs: readNumberEnv("RECONNECT_BASE_MS"), - reconnectMaxMs: readNumberEnv("RECONNECT_MAX_MS"), - dockerSocketPath: requiredEnv("DOCKER_SOCKET_PATH"), - monitorDiskPath: requiredEnv("MONITOR_DISK_PATH"), - upgradeHostProjectRoot: requiredEnv("PROVIDER_UPGRADE_HOST_PROJECT_ROOT"), - upgradeWorkspacePath: requiredEnv("PROVIDER_UPGRADE_WORKSPACE_PATH"), - upgradeComposeFile: requiredEnv("PROVIDER_UPGRADE_COMPOSE_FILE"), - upgradeEnvFile: requiredEnv("PROVIDER_UPGRADE_ENV_FILE"), - upgradeComposeProject: requiredEnv("PROVIDER_UPGRADE_COMPOSE_PROJECT"), - upgradeService: requiredEnv("PROVIDER_UPGRADE_SERVICE"), - upgradeRunnerImage: requiredEnv("PROVIDER_UPGRADE_RUNNER_IMAGE"), - hostSshHost: readOptionalStringEnv("HOST_SSH_HOST"), - hostSshPort: readOptionalNumberEnv("HOST_SSH_PORT"), - hostSshUser: readOptionalStringEnv("HOST_SSH_USER"), - hostSshKey: readOptionalStringEnv("HOST_SSH_KEY"), - hostRemoteCwd: readOptionalStringEnv("HOST_REMOTE_CWD"), - hostLoginShell: readOptionalStringEnv("HOST_LOGIN_SHELL"), - logFile: requiredEnv("LOG_FILE"), + checkedAt: new Date().toISOString(), + containerId: currentContainerId() ?? "", + containerName: "", + restartPolicy: "", + restartPolicyMaximumRetryCount: 0, + restartPolicyOk: false, + pidMode: "", + pidModeOk: false, + remediatedRestartPolicy: false, + ok: false, + error, + }; +} + +function stringRecordFromUnknown(value: unknown): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) return {}; + const output: Record = {}; + for (const [key, raw] of Object.entries(value)) { + if (typeof raw === "string") output[key] = raw; + } + return output; +} + +function dockerInspectDetails(value: unknown): DockerContainerInspectDetails | null { + if (typeof value !== "object" || value === null || Array.isArray(value)) return null; + const row = value as Record; + const hostConfig = typeof row.HostConfig === "object" && row.HostConfig !== null && !Array.isArray(row.HostConfig) + ? row.HostConfig as Record + : {}; + const configRow = typeof row.Config === "object" && row.Config !== null && !Array.isArray(row.Config) + ? row.Config as Record + : {}; + const restartPolicy = typeof hostConfig.RestartPolicy === "object" && hostConfig.RestartPolicy !== null && !Array.isArray(hostConfig.RestartPolicy) + ? hostConfig.RestartPolicy as Record + : {}; + const labels = stringRecordFromUnknown(configRow.Labels); + const rawName = typeof row.Name === "string" ? row.Name : ""; + const rawMaxRetry = restartPolicy.MaximumRetryCount; + return { + id: typeof row.Id === "string" ? row.Id : "", + name: rawName.replace(/^\/+/, ""), + restartPolicy: typeof restartPolicy.Name === "string" ? restartPolicy.Name : "", + restartPolicyMaximumRetryCount: typeof rawMaxRetry === "number" && Number.isFinite(rawMaxRetry) ? rawMaxRetry : 0, + pidMode: typeof hostConfig.PidMode === "string" ? hostConfig.PidMode : "", + composeProject: labels["com.docker.compose.project"] ?? "", + composeService: labels["com.docker.compose.service"] ?? "", + labels, + }; +} + +function inspectCurrentContainerDetails(containerId: string): DockerContainerInspectDetails | null { + const result = spawnSync("docker", ["inspect", "--format", "{{json .}}", containerId], { + encoding: "utf8", + timeout: 2500, + maxBuffer: 1024 * 1024, + }); + if (result.status !== 0) return null; + try { + return dockerInspectDetails(JSON.parse(result.stdout.trim()) as unknown); + } catch { + return null; + } +} + +function containerGuardStateFromDetails(details: DockerContainerInspectDetails, remediatedRestartPolicy: boolean, error = ""): ProviderGatewayContainerGuardState { + const restartPolicyOk = details.restartPolicy === "always"; + const pidModeOk = details.pidMode === "host"; + return { + checkedAt: new Date().toISOString(), + containerId: details.id.slice(0, 12), + containerName: details.name, + restartPolicy: details.restartPolicy, + restartPolicyMaximumRetryCount: details.restartPolicyMaximumRetryCount, + restartPolicyOk, + pidMode: details.pidMode, + pidModeOk, + remediatedRestartPolicy, + ok: restartPolicyOk && pidModeOk && error.length === 0, + error, + }; +} + +function refreshSelfContainerGuard(force = false): ProviderGatewayContainerGuardState { + const now = Date.now(); + if (!force && selfContainerGuard !== null && now - selfContainerGuardLastCheckMs < 60_000) return selfContainerGuard; + selfContainerGuardLastCheckMs = now; + const containerId = currentContainerId(); + if (containerId === null) { + selfContainerGuard = unknownContainerGuardState("unable to determine current container id from hostname"); + return selfContainerGuard; + } + if (!existsSync(config.dockerSocketPath)) { + selfContainerGuard = unknownContainerGuardState(`docker socket is not mounted at ${config.dockerSocketPath}`); + return selfContainerGuard; + } + + let details = inspectCurrentContainerDetails(containerId); + if (details === null) { + selfContainerGuard = unknownContainerGuardState("docker inspect of current provider-gateway container failed"); + return selfContainerGuard; + } + + let remediatedRestartPolicy = false; + let error = ""; + if (details.restartPolicy !== "always") { + const updateResult = spawnSync("docker", ["update", "--restart", "always", containerId], { + encoding: "utf8", + timeout: 2500, + maxBuffer: 64 * 1024, + }); + if (updateResult.status === 0) { + remediatedRestartPolicy = true; + details = inspectCurrentContainerDetails(containerId) ?? details; + } else { + error = `docker update --restart always failed: ${(updateResult.stderr || updateResult.stdout || "").slice(0, 500)}`; + } + } + + selfContainerGuard = containerGuardStateFromDetails(details, remediatedRestartPolicy, error); + if (remediatedRestartPolicy) { + logger("warn", "provider_gateway_restart_policy_remediated", selfContainerGuard as unknown as JsonValue); + } else if (!selfContainerGuard.restartPolicyOk) { + logger("error", "provider_gateway_restart_policy_guard_failed", selfContainerGuard as unknown as JsonValue); + } + if (!selfContainerGuard.pidModeOk) { + logger("error", "provider_gateway_pid_namespace_guard_failed", selfContainerGuard as unknown as JsonValue); + } + return selfContainerGuard; +} + +function discoverDockerMountSource(destination: string): string | null { + const containerId = currentContainerId(); + if (containerId === null) return null; + const template = `{{range .Mounts}}{{if eq .Destination ${goTemplateString(destination)}}{{println .Source}}{{end}}{{end}}`; + try { + const result = spawnSync("docker", ["inspect", "--format", template, containerId], { + encoding: "utf8", + timeout: 2000, + maxBuffer: 8192, + }); + if (result.status !== 0) return null; + const source = (result.stdout ?? "").split("\n").map((line) => line.trim()).find((line) => line.length > 0); + return source ?? null; + } catch { + return null; + } +} + +function inferHostSshUser(hostProjectRoot: string): string | null { + const homeMatch = hostProjectRoot.match(/^\/home\/([^/]+)/u); + if (homeMatch?.[1]) return homeMatch[1]; + if (hostProjectRoot === "/root" || hostProjectRoot.startsWith("/root/")) return "root"; + return null; +} + +function defaultHostRemoteCwd(user: string | null): string | null { + if (user === null) return null; + return user === "root" ? "/root" : `/home/${user}`; +} + +function readConfig(): RuntimeConfig { + const providerId = readProviderId(); + const workspacePath = readOptionalStringEnv("PROVIDER_UPGRADE_WORKSPACE_PATH") ?? "/workspace"; + const hostProjectRoot = readOptionalStringEnv("PROVIDER_UPGRADE_HOST_PROJECT_ROOT") + ?? discoverDockerMountSource(workspacePath) + ?? workspacePath; + const inferredSshUser = inferHostSshUser(hostProjectRoot); + const defaultHostSshKey = "/run/host-ssh/id_ed25519"; + const mountedHostSshKey = existsSync(defaultHostSshKey); + const hostSshUser = readOptionalStringEnv("HOST_SSH_USER") ?? (mountedHostSshKey ? inferredSshUser : null); + return { + serverUrl: readProviderServerUrl(), + token: readEnv("PROVIDER_TOKEN", "UNIDESK_PROVIDER_TOKEN") ?? defaultProviderToken, + providerId, + providerName: readEnv("PROVIDER_NAME", "UNIDESK_PROVIDER_NAME") ?? providerId, + labels: readProviderLabels(providerId), + heartbeatIntervalMs: readNumberEnv("HEARTBEAT_INTERVAL_MS", 15000, "UNIDESK_HEARTBEAT_INTERVAL_MS"), + reconnectBaseMs: readNumberEnv("RECONNECT_BASE_MS", 1000, "UNIDESK_RECONNECT_BASE_MS"), + reconnectMaxMs: readNumberEnv("RECONNECT_MAX_MS", 30000, "UNIDESK_RECONNECT_MAX_MS"), + dockerSocketPath: readOptionalStringEnv("DOCKER_SOCKET_PATH") ?? "/var/run/docker.sock", + monitorDiskPath: readOptionalStringEnv("MONITOR_DISK_PATH", "UNIDESK_MONITOR_DISK_PATH") ?? "/", + upgradeHostProjectRoot: hostProjectRoot, + upgradeWorkspacePath: workspacePath, + upgradeComposeFile: readOptionalStringEnv("PROVIDER_UPGRADE_COMPOSE_FILE") ?? `provider-${providerId}.yml`, + upgradeEnvFile: readOptionalStringEnv("PROVIDER_UPGRADE_ENV_FILE") ?? `.state/provider-${providerId}.env`, + upgradeComposeProject: readOptionalStringEnv("PROVIDER_UPGRADE_COMPOSE_PROJECT") ?? `unidesk-${safeProviderSlug(providerId)}`, + upgradeService: readOptionalStringEnv("PROVIDER_UPGRADE_SERVICE") ?? "provider-gateway", + upgradeRunnerImage: readOptionalStringEnv("PROVIDER_UPGRADE_RUNNER_IMAGE") ?? `unidesk_provider-gateway:${safeProviderSlug(providerId)}`, + hostSshHost: readOptionalStringEnv("HOST_SSH_HOST") ?? (mountedHostSshKey ? "host.docker.internal" : null), + hostSshPort: readOptionalNumberEnv("HOST_SSH_PORT") ?? (mountedHostSshKey ? 22 : null), + hostSshUser, + hostSshKey: readOptionalStringEnv("HOST_SSH_KEY") ?? (mountedHostSshKey ? defaultHostSshKey : null), + hostRemoteCwd: readOptionalStringEnv("HOST_REMOTE_CWD") ?? defaultHostRemoteCwd(hostSshUser), + hostLoginShell: readOptionalStringEnv("HOST_LOGIN_SHELL") ?? (hostSshUser === null ? null : "/bin/bash"), + logFile: readOptionalStringEnv("LOG_FILE") ?? `/var/log/unidesk/provider-gateway-${providerId}.jsonl`, }; } function createLogger(service: string, logFile: string) { - mkdirSync(dirname(logFile), { recursive: true }); + const writer = createHourlyJsonlWriter({ + baseLogFile: logFile, + service, + maxBytes: logRetentionBytesForService(service), + }); + writer.prune(); return (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue): void => { const entry = data === undefined ? { ts: new Date().toISOString(), service, level, message } : { ts: new Date().toISOString(), service, level, message, data }; const line = `${JSON.stringify(entry)}\n`; try { - appendFileSync(logFile, line, "utf8"); + writer.appendLine(line, new Date(entry.ts)); } catch (error) { console.error(JSON.stringify({ ts: new Date().toISOString(), service, level: "error", message: "log_write_failed", data: String(error) })); } @@ -199,6 +496,7 @@ function withToken(rawUrl: string, token: string): string { function currentLabels(): ProviderLabels { const hostSshConfigured = isHostSshConfigured(); + const containerGuard = refreshSelfContainerGuard(); return { ...config.labels, dockerSocketPresent: existsSync(config.dockerSocketPath), @@ -211,6 +509,17 @@ function currentLabels(): ProviderLabels { providerGatewayStartedAt: startedAt.toISOString(), providerGatewayUpgradePolicy: "always-enabled", providerGatewayMicroserviceHttpCache: true, + providerGatewayDockerRestartGuard: true, + providerGatewayContainerId: containerGuard.containerId, + providerGatewayContainerName: containerGuard.containerName, + providerGatewayRestartPolicy: containerGuard.restartPolicy, + providerGatewayRestartPolicyOk: containerGuard.restartPolicyOk, + providerGatewayRestartPolicyRemediated: containerGuard.remediatedRestartPolicy, + providerGatewayPidMode: containerGuard.pidMode, + providerGatewayPidModeOk: containerGuard.pidModeOk, + providerGatewayRuntimeGuardOk: containerGuard.ok, + providerGatewayRuntimeGuardError: containerGuard.error, + providerGatewayRuntimeGuardCheckedAt: containerGuard.checkedAt, gatewayUptimeSeconds: Math.floor((Date.now() - startedAt.getTime()) / 1000), }; } @@ -321,6 +630,51 @@ async function runDockerCommand(args: string[], timeoutMs = 6000): Promise<{ ok: return runProcessCommand("docker", args, timeoutMs); } +function containerInspectMapFromJson(stdout: string): Map { + const map = new Map(); + let parsed: unknown; + try { + parsed = JSON.parse(stdout) as unknown; + } catch { + return map; + } + const rows = Array.isArray(parsed) ? parsed : [parsed]; + for (const row of rows) { + const details = dockerInspectDetails(row); + if (details === null) continue; + if (details.id.length > 0) { + map.set(details.id, details); + map.set(details.id.slice(0, 12), details); + } + if (details.name.length > 0) map.set(details.name, details); + } + return map; +} + +async function inspectContainerDetails(containers: DockerContainerSummary[]): Promise<{ details: Map; errors: JsonValue[] }> { + const details = new Map(); + const errors: JsonValue[] = []; + const ids = containers.map((container) => container.id).filter((id) => id.length > 0); + if (ids.length === 0) return { details, errors }; + + const result = await runDockerCommand(["inspect", ...ids], 8000); + if (result.ok) { + return { details: containerInspectMapFromJson(result.stdout), errors }; + } + + errors.push({ source: "docker.inspect", exitCode: result.exitCode, stderr: result.stderr.slice(0, 500) }); + const gatewayContainers = containers.filter((container) => container.name.includes("provider-gateway")); + for (const container of gatewayContainers) { + const fallback = await runDockerCommand(["inspect", container.id], 3000); + if (!fallback.ok) { + errors.push({ source: "docker.inspect.provider-gateway", container: container.name, exitCode: fallback.exitCode, stderr: fallback.stderr.slice(0, 300) }); + continue; + } + for (const [key, value] of containerInspectMapFromJson(fallback.stdout)) details.set(key, value); + } + return { details, errors }; +} + function stringField(row: Record, key: string): string { const value = row[key]; return typeof value === "string" ? value : value === undefined || value === null ? "" : String(value); @@ -342,7 +696,7 @@ function parseJsonLines(stdout: string, limit: number): Array): DockerContainerSummary { +function toContainer(row: Record, inspect: DockerContainerInspectDetails | null = null): DockerContainerSummary { return { id: stringField(row, "ID"), name: stringField(row, "Names"), @@ -354,6 +708,12 @@ function toContainer(row: Record): DockerContainerSummary { runningFor: stringField(row, "RunningFor"), size: stringField(row, "Size"), networks: stringField(row, "Networks"), + restartPolicy: inspect?.restartPolicy ?? "", + restartPolicyMaximumRetryCount: inspect?.restartPolicyMaximumRetryCount ?? 0, + pidMode: inspect?.pidMode ?? "", + composeProject: inspect?.composeProject ?? "", + composeService: inspect?.composeService ?? "", + labels: inspect?.labels ?? {}, }; } @@ -764,7 +1124,16 @@ async function collectDockerStatus(): Promise { if (!result.ok) errors.push({ source, exitCode: result.exitCode, stderr: result.stderr.slice(0, 500) }); } - const containers = containersResult.ok ? parseJsonLines(containersResult.stdout, 80).map(toContainer) : []; + const baseContainers = containersResult.ok ? parseJsonLines(containersResult.stdout, 80).map((row) => toContainer(row)) : []; + const inspected = await inspectContainerDetails(baseContainers); + for (const error of inspected.errors) errors.push(error); + const containers = containersResult.ok + ? parseJsonLines(containersResult.stdout, 80).map((row) => { + const id = stringField(row, "ID"); + const name = stringField(row, "Names"); + return toContainer(row, inspected.details.get(id) ?? inspected.details.get(name) ?? null); + }) + : []; const images = imagesResult.ok ? parseJsonLines(imagesResult.stdout, 80).map(toImage) : []; const volumes = volumesResult.ok ? parseJsonLines(volumesResult.stdout, 60).map(toVolume) : []; const networks = networksResult.ok ? parseJsonLines(networksResult.stdout, 60).map(toNetwork) : []; @@ -1236,9 +1605,12 @@ function upgradePlan(taskId: string): Record { `attempt=0`, `validated=0`, `while [ "$attempt" -lt "${validationAttempts}" ]; do logs=$(docker logs ${shellQuote(candidateName)} 2>&1 || true); has_open=0; has_ack=0; has_ok=0; case "$logs" in *${shellQuote(validationNeedleOpen)}*) has_open=1;; esac; case "$logs" in *${shellQuote(validationNeedleAck)}*) has_ack=1;; esac; case "$logs" in *${shellQuote(validationNeedleOk)}*) has_ok=1;; esac; if [ "$has_open" = "1" ] && [ "$has_ack" = "1" ] && [ "$has_ok" = "1" ]; then validated=1; break; fi; candidate_running=$(docker inspect --format '{{.State.Running}}' ${shellQuote(candidateName)} 2>/dev/null || true); if [ "$candidate_running" != "true" ]; then break; fi; attempt=$((attempt + 1)); sleep 2; done`, - `if [ "$validated" != "1" ]; then echo "candidate validation failed; old gateway will leave upgrade sleep automatically" >&2; docker logs ${shellQuote(candidateName)} >&2 || true; docker rm -f ${shellQuote(candidateName)} >/dev/null 2>&1 || true; rm -f "$candidate_env_file"; exit 1; fi`, - `docker update --restart always ${shellQuote(candidateName)} >/dev/null`, - `if [ -n "$old_ids" ]; then docker rm -f $old_ids; fi`, + `if [ "$validated" != "1" ]; then echo "candidate validation failed; old gateway will leave upgrade sleep automatically" >&2; docker logs ${shellQuote(candidateName)} >&2 || true; docker rm -f ${shellQuote(candidateName)} >/dev/null 2>&1 || true; rm -f "$candidate_env_file"; exit 1; fi`, + `docker update --restart always ${shellQuote(candidateName)} >/dev/null`, + `final_restart=$(docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' ${shellQuote(candidateName)})`, + `final_pid_mode=$(docker inspect --format '{{.HostConfig.PidMode}}' ${shellQuote(candidateName)})`, + `if [ "$final_restart" != "always" ] || [ "$final_pid_mode" != "host" ]; then echo "candidate runtime guard failed: restart=$final_restart pid=$final_pid_mode" >&2; docker rm -f ${shellQuote(candidateName)} >/dev/null 2>&1 || true; rm -f "$candidate_env_file"; exit 1; fi`, + `if [ -n "$old_ids" ]; then docker rm -f $old_ids; fi`, `if [ -n "$old_name" ] && [ "$old_name" != ${shellQuote(candidateName)} ]; then docker rename ${shellQuote(candidateName)} "$old_name" || true; fi`, `rm -f "$candidate_env_file"`, `echo "candidate provider-gateway validated and promoted"`, @@ -1296,12 +1668,15 @@ function upgradePlan(taskId: string): Record { validationTimeoutMs, oldGatewayRestartPolicyBeforeSleep: "always", promoteOnlyAfterCandidateValidation: true, - candidateRestartPolicyAfterPromotion: "always", - candidateUsesOldContainerMounts: true, - candidateUsesOldContainerNetworks: true, - candidateUsesOldContainerExtraHosts: true, - candidateUsesOldContainerEnvironment: true, - candidateUsesHostPidNamespace: true, + candidateRestartPolicyAfterPromotion: "always", + candidateFinalRestartPolicyValidation: true, + candidateUsesOldContainerMounts: true, + candidateUsesOldContainerNetworks: true, + candidateUsesOldContainerExtraHosts: true, + candidateUsesOldContainerEnvironment: true, + candidateUsesHostPidNamespace: true, + startupSelfHealsRestartPolicy: true, + dockerStatusReportsRestartPolicyAndPidMode: true, removeScope: { projectLabel: config.upgradeComposeProject, serviceLabel: config.upgradeService, @@ -1367,7 +1742,7 @@ function assertAllowedMicroserviceBase(rawBaseUrl: string): URL { const baseUrl = new URL(rawBaseUrl); if (baseUrl.protocol !== "http:") throw new Error(`microservice backend only supports http URLs, got ${baseUrl.protocol}`); const host = baseUrl.hostname.toLowerCase(); - const allowedHosts = new Set(["127.0.0.1", "localhost", "host.docker.internal", "todo-note", "codex-queue"]); + const allowedHosts = new Set(["127.0.0.1", "localhost", "host.docker.internal", "todo-note", "code-queue"]); if (!allowedHosts.has(host)) throw new Error(`microservice backend host is not allowed: ${baseUrl.hostname}`); return baseUrl; } @@ -1494,6 +1869,15 @@ function invalidateMicroserviceHttpCache(serviceId: string, targetBaseUrl: strin } } +function headersFromMicroserviceRequest(requestHeaders: Record): Headers { + const headers = new Headers(); + for (const name of microserviceForwardRequestHeaders) { + const value = requestHeaders[name]; + if (typeof value === "string" && value.length > 0) headers.set(name, value); + } + return headers; +} + async function runMicroserviceHttp(payload: Record): Promise { const rawMethod = String(payload.method || "GET").toUpperCase(); const allowedMethods = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"]); @@ -1532,9 +1916,7 @@ async function runMicroserviceHttp(payload: Record): Promise< const requestHeaders = typeof payload.requestHeaders === "object" && payload.requestHeaders !== null && !Array.isArray(payload.requestHeaders) ? payload.requestHeaders as Record : {}; - const headers = new Headers(); - const contentType = typeof requestHeaders["content-type"] === "string" ? requestHeaders["content-type"] : ""; - if (contentType.length > 0) headers.set("content-type", contentType); + const headers = headersFromMicroserviceRequest(requestHeaders); const requestStartedAt = Date.now(); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); diff --git a/src/components/shared/src/index.ts b/src/components/shared/src/index.ts index a55752ef..1e040d10 100644 --- a/src/components/shared/src/index.ts +++ b/src/components/shared/src/index.ts @@ -68,6 +68,12 @@ export interface DockerContainerSummary { runningFor: string; size: string; networks: string; + restartPolicy?: string; + restartPolicyMaximumRetryCount?: number; + pidMode?: string; + composeProject?: string; + composeService?: string; + labels?: Record; } export interface DockerImageSummary { diff --git a/src/components/shared/src/rotating-jsonl.ts b/src/components/shared/src/rotating-jsonl.ts new file mode 100644 index 00000000..9b3493d1 --- /dev/null +++ b/src/components/shared/src/rotating-jsonl.ts @@ -0,0 +1,165 @@ +import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs"; +import { basename, dirname, join } from "node:path"; + +export const DEFAULT_LOG_RETENTION_BYTES = 1024 * 1024 * 1024; + +export interface HourlyJsonlWriter { + appendLine(line: string, at?: Date): void; + appendJson(value: unknown, at?: Date): void; + currentPath(at?: Date): string; + prune(at?: Date): void; +} + +interface HourlyJsonlWriterOptions { + baseLogFile: string; + service: string; + maxBytes?: number; +} + +interface FileEntry { + path: string; + size: number; + mtimeMs: number; +} + +function pad(value: number): string { + return String(value).padStart(2, "0"); +} + +function localParts(date: Date): { day: string; hour: string } { + return { + day: `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`, + hour: pad(date.getHours()), + }; +} + +function safeServiceName(service: string): string { + return service.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "service"; +} + +function parseBase(baseLogFile: string, service: string): { rootDir: string; prefix: string; suffix: string } { + const suffix = `_${service}.jsonl`; + const dir = dirname(baseLogFile); + const dirName = basename(dir); + const rootDir = /^\d{8}$/u.test(dirName) ? dirname(dir) : dir; + const fileName = basename(baseLogFile); + const stem = fileName.replace(/\.jsonl$/u, ""); + const ownPrefix = fileName.endsWith(suffix) ? fileName.slice(0, -suffix.length) : ""; + const startStampPrefix = stem.match(/^(\d{8}_\d{6})(?:_\d{8}_\d{2})?_/u)?.[1] ?? ""; + const genericPrefix = stem.includes("_") ? stem.slice(0, stem.lastIndexOf("_")) : stem; + const prefix = ownPrefix || startStampPrefix || genericPrefix; + return { rootDir, prefix: prefix || "unidesk", suffix }; +} + +function collectFiles(root: string, suffix: string): FileEntry[] { + if (!existsSync(root)) return []; + const result: FileEntry[] = []; + const scan = (dir: string): void => { + let entries; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + scan(path); + continue; + } + if (!entry.isFile() || !entry.name.endsWith(suffix)) continue; + try { + const stat = statSync(path); + result.push({ path, size: stat.size, mtimeMs: stat.mtimeMs }); + } catch { + // Ignore files that disappear while retention is scanning. + } + } + }; + scan(root); + return result; +} + +export function logRetentionBytesFromEnv(name: string, fallback = DEFAULT_LOG_RETENTION_BYTES): number { + const raw = process.env[name]?.trim(); + if (!raw) return fallback; + const match = raw.match(/^(\d+(?:\.\d+)?)\s*(b|k|kb|kib|m|mb|mib|g|gb|gib)?$/iu); + if (!match) return fallback; + const value = Number(match[1]); + const unit = (match[2] ?? "b").toLowerCase(); + const multiplier = + unit === "g" || unit === "gb" || unit === "gib" ? 1024 * 1024 * 1024 + : unit === "m" || unit === "mb" || unit === "mib" ? 1024 * 1024 + : unit === "k" || unit === "kb" || unit === "kib" ? 1024 + : 1; + const bytes = value * multiplier; + return Number.isFinite(bytes) && bytes > 0 ? Math.floor(bytes) : fallback; +} + +export function logRetentionEnvNameForService(service: string): string { + const normalized = safeServiceName(service).toUpperCase().replace(/[^A-Z0-9]+/g, "_"); + return `UNIDESK_${normalized}_LOG_MAX_BYTES`; +} + +export function logRetentionBytesForService(service: string, extraEnvNames: string[] = [], fallback = DEFAULT_LOG_RETENTION_BYTES): number { + const globalMaxBytes = logRetentionBytesFromEnv("UNIDESK_LOG_RETENTION_BYTES", fallback); + for (const name of [logRetentionEnvNameForService(service), ...extraEnvNames]) { + if (process.env[name]?.trim()) return logRetentionBytesFromEnv(name, globalMaxBytes); + } + return globalMaxBytes; +} + +export function createHourlyJsonlWriter(options: HourlyJsonlWriterOptions): HourlyJsonlWriter { + const service = safeServiceName(options.service); + const maxBytes = Math.max(1, Math.floor(options.maxBytes ?? DEFAULT_LOG_RETENTION_BYTES)); + const { rootDir, prefix, suffix } = parseBase(options.baseLogFile, service); + let lastPruneAt = 0; + + const currentPath = (at = new Date()): string => { + const parts = localParts(at); + return join(rootDir, parts.day, `${prefix}_${parts.day}_${parts.hour}${suffix}`); + }; + + const prune = (at = new Date()): void => { + const activePath = currentPath(at); + const files = collectFiles(rootDir, suffix).sort((left, right) => { + if (left.path === activePath) return 1; + if (right.path === activePath) return -1; + return left.path.localeCompare(right.path) || left.mtimeMs - right.mtimeMs; + }); + let total = files.reduce((sum, file) => sum + file.size, 0); + for (const file of files) { + if (total <= maxBytes) break; + if (file.path === activePath) continue; + try { + unlinkSync(file.path); + total -= file.size; + } catch { + // Best-effort retention must never break service logging. + } + } + }; + + const maybePrune = (at: Date): void => { + const now = Date.now(); + if (now - lastPruneAt < 60_000) return; + lastPruneAt = now; + prune(at); + }; + + const appendLine = (line: string, at = new Date()): void => { + const path = currentPath(at); + mkdirSync(dirname(path), { recursive: true }); + appendFileSync(path, line.endsWith("\n") ? line : `${line}\n`, "utf8"); + maybePrune(at); + }; + + return { + appendLine, + appendJson(value: unknown, at = new Date()): void { + appendLine(JSON.stringify(value), at); + }, + currentPath, + prune, + }; +} diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index 76588187..f79a5b48 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -4,6 +4,9 @@ { "path": "components/shared" }, { "path": "components/backend-core" }, { "path": "components/provider-gateway" }, - { "path": "components/frontend" } + { "path": "components/frontend" }, + { "path": "components/microservices/code-queue" }, + { "path": "components/microservices/project-manager" }, + { "path": "components/microservices/baidu-netdisk" } ] }