From a242e3e3eccd13f92fb4fd1e2a5c6a2ddea3c753 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 13 May 2026 08:43:43 +0000 Subject: [PATCH] feat: expand scheduling, notifications, and queue runtime - add scheduled task plumbing across backend core, CLI, and frontend surfaces - add frontend notification UI and keep service pages using the repaired shared stylesheet - refactor code queue runtime and update baidu netdisk/service integration docs --- config.json | 2 +- docker-compose.yml | 10 +- docs/issue/baidu-netdisk-env-setup.md | 10 +- docs/issue/baidu-netdisk-user-service.md | 12 +- docs/reference/deployment.md | 2 +- docs/reference/frontend.md | 4 +- docs/reference/microservices.md | 16 +- scripts/cli.ts | 8 + scripts/src/e2e.ts | 1 + scripts/src/schedules.ts | 110 + src/components/backend-core/Dockerfile | 1 + src/components/backend-core/src/index.ts | 825 ++ src/components/database/config/pg_hba.conf | 9 + src/components/frontend/public/app.js | 142 +- src/components/frontend/public/style.css | 227 +- src/components/frontend/src/app.tsx | 278 +- src/components/frontend/src/baidu-netdisk.tsx | 32 +- src/components/frontend/src/claudeqq.tsx | 14 +- src/components/frontend/src/code-queue.tsx | 190 +- src/components/frontend/src/index.ts | 16 +- src/components/frontend/src/markdown.tsx | 17 +- src/components/frontend/src/navigation.ts | 1 + .../frontend/src/notification-context.tsx | 74 + .../frontend/src/notification-popup.tsx | 81 + .../frontend/src/notification-styles.ts | 171 + src/components/frontend/src/pipeline.tsx | 6 +- .../frontend/src/project-manager.tsx | 14 +- src/components/frontend/src/trace.tsx | 163 +- .../microservices/baidu-netdisk/src/index.ts | 20 +- .../code-queue/src/code-agent/codex.ts | 392 + .../code-queue/src/code-agent/common.ts | 84 + .../code-queue/src/code-agent/opencode.ts | 400 + .../code-queue/src/dev-containers.ts | 193 + .../microservices/code-queue/src/index.ts | 6832 ++--------------- .../code-queue/src/judge-probes.ts | 366 + .../microservices/code-queue/src/judge.ts | 715 ++ .../code-queue/src/notifications.ts | 567 ++ .../microservices/code-queue/src/prompts.ts | 53 + .../code-queue/src/provider-runtime.ts | 455 ++ .../microservices/code-queue/src/queue-api.ts | 1133 +++ .../code-queue/src/references.ts | 187 + .../code-queue/src/self-tests.ts | 402 + .../code-queue/src/task-output.ts | 158 + .../microservices/code-queue/src/task-view.ts | 2111 +++++ .../microservices/code-queue/src/types.ts | 411 + 45 files changed, 10421 insertions(+), 6494 deletions(-) create mode 100644 scripts/src/schedules.ts create mode 100644 src/components/database/config/pg_hba.conf create mode 100644 src/components/frontend/src/notification-context.tsx create mode 100644 src/components/frontend/src/notification-popup.tsx create mode 100644 src/components/frontend/src/notification-styles.ts create mode 100644 src/components/microservices/code-queue/src/code-agent/codex.ts create mode 100644 src/components/microservices/code-queue/src/code-agent/common.ts create mode 100644 src/components/microservices/code-queue/src/code-agent/opencode.ts create mode 100644 src/components/microservices/code-queue/src/dev-containers.ts create mode 100644 src/components/microservices/code-queue/src/judge-probes.ts create mode 100644 src/components/microservices/code-queue/src/judge.ts create mode 100644 src/components/microservices/code-queue/src/notifications.ts create mode 100644 src/components/microservices/code-queue/src/prompts.ts create mode 100644 src/components/microservices/code-queue/src/provider-runtime.ts create mode 100644 src/components/microservices/code-queue/src/queue-api.ts create mode 100644 src/components/microservices/code-queue/src/references.ts create mode 100644 src/components/microservices/code-queue/src/self-tests.ts create mode 100644 src/components/microservices/code-queue/src/task-output.ts create mode 100644 src/components/microservices/code-queue/src/task-view.ts create mode 100644 src/components/microservices/code-queue/src/types.ts diff --git a/config.json b/config.json index c37e0ec7..2e6c5629 100644 --- a/config.json +++ b/config.json @@ -324,7 +324,7 @@ "id": "baidu-netdisk", "name": "Baidu Netdisk", "providerId": "main-server", - "description": "容器化百度网盘存储用户服务,提供 OAuth 设备码登录、应用目录浏览和 staging 目录上传下载任务。", + "description": "容器化百度网盘存储用户服务,提供 OAuth 设备码登录、根目录浏览和 staging 目录上传下载任务。", "repository": { "url": "https://github.com/pikasTech/unidesk", "commitId": "ae462ed9ef8057909fee9eabfadce5ed55e958a2", diff --git a/docker-compose.yml b/docker-compose.yml index d59bd895..4481ada2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,8 @@ services: - "-c" - "config_file=/etc/postgresql/postgresql.conf" - "-c" + - "hba_file=/etc/postgresql/pg_hba.conf" + - "-c" - "logging_collector=on" - "-c" - "log_directory=/var/log/unidesk/${UNIDESK_LOG_DAY}" @@ -27,6 +29,7 @@ services: - unidesk_pgdata_10gb:/var/lib/postgresql/data - ./src/components/database/init:/docker-entrypoint-initdb.d:ro - ./src/components/database/config/postgresql.conf:/etc/postgresql/postgresql.conf:ro + - ./src/components/database/config/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro - ${UNIDESK_LOG_DIR}:/var/log/unidesk healthcheck: test: ["CMD-SHELL", "pg_isready -U ${UNIDESK_DATABASE_USER} -d ${UNIDESK_DATABASE_NAME}"] @@ -55,11 +58,14 @@ services: TASK_PENDING_TIMEOUT_MS: "${UNIDESK_TASK_PENDING_TIMEOUT_MS:-600000}" DATABASE_VOLUME_NAME: "${UNIDESK_DATABASE_VOLUME}" DATABASE_VOLUME_SIZE: "${UNIDESK_DATABASE_VOLUME_SIZE}" + PGDATA_BACKUP_STAGING_DIR: "/data/baidu-netdisk-staging" + BAIDU_NETDISK_INTERNAL_URL: "http://baidu-netdisk:4244" MICROSERVICES_JSON: "${UNIDESK_MICROSERVICES_JSON:-[]}" 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 + - ./.state/baidu-netdisk/staging:/data/baidu-netdisk-staging healthcheck: test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 5s @@ -129,7 +135,7 @@ services: 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}" + CODE_QUEUE_MAX_ACTIVE_QUEUES: "${UNIDESK_CODE_QUEUE_MAX_ACTIVE_QUEUES:-0}" 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}" @@ -217,7 +223,7 @@ services: 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_APP_ROOT: "${UNIDESK_BAIDU_NETDISK_APP_ROOT:-/}" 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}" diff --git a/docs/issue/baidu-netdisk-env-setup.md b/docs/issue/baidu-netdisk-env-setup.md index 5faa8d88..b72def23 100644 --- a/docs/issue/baidu-netdisk-env-setup.md +++ b/docs/issue/baidu-netdisk-env-setup.md @@ -40,8 +40,8 @@ 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", + # Optional. "/" makes UniDesk work at the Baidu Netdisk root; use /apps/ to sandbox again. + "UNIDESK_BAIDU_NETDISK_APP_ROOT": "/", } path = Path(".state/docker-compose.env") @@ -68,7 +68,7 @@ PY 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' +export UNIDESK_BAIDU_NETDISK_APP_ROOT='/' bun scripts/cli.ts server rebuild baidu-netdisk ``` @@ -101,11 +101,11 @@ bun scripts/cli.ts microservice proxy baidu-netdisk /api/auth/device/start --met ```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/files?dir=/&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,适合授权完成后的端到端验收。 +`/api/files` 访问根目录时应返回 `ok=true` 和文件数组;`/api/self-test` 会生成小文本、上传到当前 `UNIDESK_BAIDU_NETDISK_APP_ROOT` 指向的工作目录、列表确认、下载并比较 MD5,适合授权完成后的端到端验收。如果你把 `UNIDESK_BAIDU_NETDISK_APP_ROOT` 改回 `/apps/`,后端会在首次访问时确保该应用目录存在。 ## Token Key 轮换 diff --git a/docs/issue/baidu-netdisk-user-service.md b/docs/issue/baidu-netdisk-user-service.md index ecf11e34..0d678f8d 100644 --- a/docs/issue/baidu-netdisk-user-service.md +++ b/docs/issue/baidu-netdisk-user-service.md @@ -52,9 +52,9 @@ Token handling requirements: - 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 +### Scope and Remote Root -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. +Use `scope=basic,netdisk`. Official docs for download still describe third-party app data under `/apps/` and visible to users as `/我的应用数据/`, but the file-list docs also define `dir` as an absolute path defaulting to `/`. On 2026-05-13, the current UniDesk Baidu application and authorized account were tested directly against the official APIs: listing `/`, uploading a tiny temporary file to `/unidesk-root-probe-*.txt`, obtaining its `dlink`, downloading it back, verifying MD5, and deleting it all succeeded with `errno=0`. Therefore UniDesk now defaults `UNIDESK_BAIDU_NETDISK_APP_ROOT` to `/` and treats it as the remote working root. Operators can still set `UNIDESK_BAIDU_NETDISK_APP_ROOT=/apps/` to re-enable an app-folder sandbox. ### Browse and Metadata @@ -127,7 +127,7 @@ Expose a pure JSON control API first: - `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?dir=/&start=0&limit=100`: directory listing under the configured remote working root. - `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`. @@ -205,8 +205,8 @@ If deployed on main server, use a Compose service name such as `http://baidu-net 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. +- Account cards: username, UID, quota used/total, VIP state, remote working root path. +- File browser: breadcrumb rooted at the configured working root, now `/` by default, 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. @@ -219,7 +219,7 @@ Focused checks after implementation: - `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`. +- 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 working 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. diff --git a/docs/reference/deployment.md b/docs/reference/deployment.md index d80fd419..0a213da3 100644 --- a/docs/reference/deployment.md +++ b/docs/reference/deployment.md @@ -49,7 +49,7 @@ frontend 的 Docker 上线顺序为:先运行必要的本地校验,例如 `b ## 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 证明未重新引入内存爆炸风险。 +主 server 内存预算按稀缺资源管理,不能把用户服务当作无限内存的 worker 节点使用。`code-queue-backend` 必须保持明确的 memory/swap 硬上限,默认 `CODE_QUEUE_MAX_ACTIVE_QUEUES=0` 以恢复 queue 间并行,仍保持 `CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS=10`、`CODE_QUEUE_IN_MEMORY_EVENT_RECORDS=10` 这类小热窗口;任务历史、队列统计、Trace/output 读取和 `/health` 摘要必须优先从 PostgreSQL 直读或聚合,不能为了性能便利在 Bun 进程内缓存全量历史。任何提高 Code Queue 热窗口、日志缓冲、Playwright/Codex 子进程常驻规模或容器上限的变更,或把 `CODE_QUEUE_MAX_ACTIVE_QUEUES` 显式改成正数,都必须在同一任务里说明内存预算来源,并通过 `docker inspect code-queue-backend`、`docker stats --no-stream code-queue-backend`、`microservice health code-queue` 和对应 E2E 证明未重新引入内存爆炸风险。 ## Database Volume diff --git a/docs/reference/frontend.md b/docs/reference/frontend.md index 47893996..ccf2019c 100644 --- a/docs/reference/frontend.md +++ b/docs/reference/frontend.md @@ -77,11 +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 按钮。 - - `Baidu Netdisk` 子标签必须把主 server `baidu-netdisk-backend` 后端渲染为 UniDesk React 控件,包括 OAuth 设备码二维码/用户码登录、账号容量、应用目录文件浏览、staging 目录上传/下载任务、上传/下载自测按钮与 MD5 结果、脱敏安全说明、日志摘要和显式原始 JSON 按钮;不得把 access token、refresh token、dlink 或 staging 文件字节流裸露到浏览器。 + - `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 语义:只有当前滚动位置在底部附近时才跟随新增输出;用户手动向上滚动后立即暂停自动滚动,异步刷新不得把视图拉回底部,直到用户再次滚动到最底部才恢复自动跟随。 + - `Code Queue` 的 queue/session 左侧边栏必须提供 task 关键词搜索,并采用顶部对齐和内容高度优先布局:搜索栏、列表、分组和 task card 都不得用居中、space-between、stretch 或隐式等高网格去拉满侧栏高度;item 少时允许下半部分留空,不能把单个 item 拉高来铺满;每个 task card 必须显示 `最近更新: ...前` 这类相对更新时间,便于判断运行中的 Trace 是否卡住;`queued` task card 的状态徽标必须显示排队原因,例如 `QUEUED(PREV TASK)`、`QUEUED(MEM LIMIT)`。提交任务时必须立即锁定 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。 diff --git a/docs/reference/microservices.md b/docs/reference/microservices.md index 5d085375..a9a87d32 100644 --- a/docs/reference/microservices.md +++ b/docs/reference/microservices.md @@ -72,15 +72,15 @@ Project Manager 在 UniDesk 语境中按纯后端服务管理:不得将 `4233` - 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 写入仓库文件。 +- 配置密钥:Compose 只透传 `UNIDESK_BAIDU_NETDISK_CLIENT_ID`、`UNIDESK_BAIDU_NETDISK_CLIENT_SECRET`、`UNIDESK_BAIDU_NETDISK_TOKEN_KEY` 与可选 `UNIDESK_BAIDU_NETDISK_APP_ROOT`;当前默认工作根目录为 `/`,如需收回到应用目录可显式设为 `/apps/`;不得把百度 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_...` 目录。 +- 文件边界:v1 只支持容器 staging 目录 `/data/staging` 与百度网盘配置工作根之间的后台上传/下载任务;staging 目录由主 server `.state/baidu-netdisk/staging` 挂载,`.state/` 只保存可重建文件缓存,不作为 token 或任务权威状态。当前授权账号已实测可对百度网盘根目录 `/` 执行列表、上传、获取 dlink、下载和删除临时探针,因此 `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、前端按钮和交付验收使用。 +- 自测:`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 按钮。 +- 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。 @@ -111,17 +111,17 @@ Baidu Netdisk 在 UniDesk 语境中按纯后端服务管理:不得暴露百度 - 用户输入持久化:任务初始 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 必须自动重新调度。 - 稳定性与重启恢复: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 队列反复震荡。 +- 调度与 active run slot:Code Queue 必须把“queue processor 正在等待/退避/轮询”和“实际占用 Codex/OpenCode 子进程运行槽”分开建模;`CODE_QUEUE_MAX_ACTIVE_QUEUES` 只限制真实 active run slot,不能把 retry backoff、等待内存下降或等待前序任务的 `processingQueues` 计入 active slot,否则设置全局 active slot 上限时,一个空等队列会把其他 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=0` 表示不按 queue 数量设置全局排队上限;如显式设置为正数,必须同时说明内存预算并补充内存压测验收。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`,可通过 `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 探针,不能留下堆积的无超时探针进程。 +- 状态与日志:`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` 仅作为显式配置的全局 active slot 上限,`0` 表示不按 queue 数量限流,内存不足时由 cgroup memory pressure 阻止新 run 并在任务响应中暴露 `QUEUED(MEM LIMIT)`。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` 打开。 +- 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 应使用 `入队份数` 一次性生成多条队列任务,而不是依赖快速连点按钮;左侧 queue/session 卡片的 `QUEUED` 状态必须显示原因,例如 `QUEUED(PREV TASK)`、`QUEUED(MEM LIMIT)`、`QUEUED(ACTIVE LIMIT)`;原始任务 JSON 只能通过显式 `查看原始JSON` 打开。 ## D601 User Services diff --git a/scripts/cli.ts b/scripts/cli.ts index d6212f52..c36ccc93 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -10,6 +10,7 @@ import { extractRemoteCliOptions, runRemoteCli } from "./src/remote"; import { runMicroserviceCommand } from "./src/microservices"; import { runCodeQueueCommand } from "./src/code-queue"; import { runProviderCommand } from "./src/provider-attach"; +import { runScheduleCommand } from "./src/schedules"; const remoteOptions = extractRemoteCliOptions(process.argv.slice(2)); const args = remoteOptions.args; @@ -41,6 +42,8 @@ function help(): unknown { { 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 [--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: "schedule list|get|runs|run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run supports --wait-ms N." }, + { command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." }, { 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." }, @@ -178,6 +181,11 @@ async function main(): Promise { return; } + if (top === "schedule") { + emitJson(commandName, await runScheduleCommand(config, args.slice(1))); + return; + } + if (top === "codex") { emitJson(commandName, await runCodeQueueCommand(config, args.slice(1))); return; diff --git a/scripts/src/e2e.ts b/scripts/src/e2e.ts index c88f8170..25654cc2 100644 --- a/scripts/src/e2e.ts +++ b/scripts/src/e2e.ts @@ -301,6 +301,7 @@ const LAYOUT_OVERFLOW_PAGE_TEST_IDS: Record = { "/nodes/gateway/": "gateway-version-page", "/tasks/pending/": "pending-task-page", "/tasks/history/": "task-history-page", + "/tasks/scheduled/": "scheduled-task-page", "/app/catalog/": "microservice-catalog-page", "/app/todo-note/": "todo-note-page", "/app/findjob/": "findjob-page", diff --git a/scripts/src/schedules.ts b/scripts/src/schedules.ts new file mode 100644 index 00000000..cceafc2d --- /dev/null +++ b/scripts/src/schedules.ts @@ -0,0 +1,110 @@ +import { type UniDeskConfig } from "./config"; +import { coreInternalFetch } from "./microservices"; + +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 numberOption(args: string[], name: string, defaultValue: number): number { + const raw = stringOption(args, name); + if (raw === undefined) return defaultValue; + const value = Number(raw); + if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`); + return value; +} + +function booleanOption(args: string[], name: string, defaultValue: boolean): boolean { + const raw = stringOption(args, name); + if (raw === undefined) return defaultValue; + if (["1", "true", "yes", "on"].includes(raw.toLowerCase())) return true; + if (["0", "false", "no", "off"].includes(raw.toLowerCase())) return false; + throw new Error(`${name} must be true or false`); +} + +function responseBody(response: unknown): Record { + if (typeof response !== "object" || response === null || Array.isArray(response)) return {}; + const body = (response as { body?: unknown }).body; + return typeof body === "object" && body !== null && !Array.isArray(body) ? body as Record : {}; +} + +function terminalRunStatus(status: unknown): boolean { + return ["succeeded", "failed", "skipped"].includes(String(status || "")); +} + +async function waitForScheduleRun(scheduleId: string, runId: string, timeoutMs: number): Promise { + const started = Date.now(); + let latest: unknown = null; + while (Date.now() - started < timeoutMs) { + latest = coreInternalFetch(`/api/schedules/${encodeURIComponent(scheduleId)}/runs?limit=20`); + const runs = responseBody(latest).runs; + if (Array.isArray(runs)) { + const run = runs.find((item) => typeof item === "object" && item !== null && (item as { id?: unknown }).id === runId); + if (run !== undefined && terminalRunStatus((run as { status?: unknown }).status)) return { ok: true, run }; + } + await Bun.sleep(2000); + } + return { ok: false, timeoutMs, latest }; +} + +function pgdataBackupScheduleBody(config: UniDeskConfig, args: string[]): Record { + const id = stringOption(args, "--id") ?? "unidesk-pgdata-baidu-daily"; + const timeOfDay = stringOption(args, "--time") ?? "03:30"; + const remoteBaseDir = stringOption(args, "--remote-base") ?? "/SERVER_DATA/UNIDESK_PG_DATA"; + const stagingSubdir = stringOption(args, "--staging-subdir") ?? "server-data/unidesk-pg-data"; + const enabled = args.includes("--disabled") ? false : booleanOption(args, "--enabled", true); + const timeoutMs = numberOption(args, "--timeout-ms", 60 * 60_000); + return { + id, + name: "PGDATA daily Baidu Netdisk backup", + description: "Daily PostgreSQL physical base backup uploaded to Baidu Netdisk /SERVER_DATA with monthly rotation.", + enabled, + concurrencyPolicy: "skip", + schedule: { type: "daily", timeOfDay, timezone: config.project.timezone || "Etc/UTC" }, + action: { + type: "pgdata_backup", + volumeName: config.database.volume, + remoteBaseDir, + stagingSubdir, + baiduBaseUrl: "http://baidu-netdisk:4244", + timeoutMs, + cleanupLocal: true, + }, + }; +} + +export async function runScheduleCommand(config: UniDeskConfig, args: string[]): Promise { + const [action = "list", idArg] = args; + if (action === "list") return coreInternalFetch("/api/schedules?limit=200"); + if (action === "get") { + if (!idArg) throw new Error("schedule get requires schedule id"); + return coreInternalFetch(`/api/schedules/${encodeURIComponent(idArg)}`); + } + if (action === "runs") { + const limit = numberOption(args, "--limit", 50); + return idArg + ? coreInternalFetch(`/api/schedules/${encodeURIComponent(idArg)}/runs?limit=${limit}`) + : coreInternalFetch(`/api/schedules/runs?limit=${limit}`); + } + if (action === "delete") { + if (!idArg) throw new Error("schedule delete requires schedule id"); + return coreInternalFetch(`/api/schedules/${encodeURIComponent(idArg)}`, { method: "DELETE" }); + } + if (action === "run") { + if (!idArg) throw new Error("schedule run requires schedule id"); + const response = coreInternalFetch(`/api/schedules/${encodeURIComponent(idArg)}/run`, { method: "POST", body: {} }); + const run = responseBody(response).run as { id?: unknown } | undefined; + const waitMs = numberOption(args, "--wait-ms", 0); + const wait = waitMs > 0 && typeof run?.id === "string" ? await waitForScheduleRun(idArg, run.id, waitMs) : null; + return { trigger: response, wait }; + } + if (action === "upsert-pgdata-backup" || action === "pgdata-backup") { + const body = pgdataBackupScheduleBody(config, args); + const id = String(body.id || "unidesk-pgdata-baidu-daily"); + return coreInternalFetch(`/api/schedules/${encodeURIComponent(id)}`, { method: "PUT", body }); + } + throw new Error("schedule command must be one of: list, get, runs, run, delete, upsert-pgdata-backup"); +} diff --git a/src/components/backend-core/Dockerfile b/src/components/backend-core/Dockerfile index 4d1cd683..21890620 100644 --- a/src/components/backend-core/Dockerfile +++ b/src/components/backend-core/Dockerfile @@ -1,4 +1,5 @@ FROM oven/bun:1-alpine +RUN apk add --no-cache postgresql16-client tar gzip WORKDIR /app/src/components/backend-core COPY src/components/backend-core/package.json ./package.json RUN bun install --production diff --git a/src/components/backend-core/src/index.ts b/src/components/backend-core/src/index.ts index 5e35e2f7..b0634c23 100644 --- a/src/components/backend-core/src/index.ts +++ b/src/components/backend-core/src/index.ts @@ -1,4 +1,8 @@ import type { Server, ServerWebSocket } from "bun"; +import { createHash } from "node:crypto"; +import { createReadStream } from "node:fs"; +import { mkdir, rm, stat } from "node:fs/promises"; +import { basename, dirname, relative, resolve, posix as pathPosix } from "node:path"; import postgres from "postgres"; import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../shared/src/rotating-jsonl"; import { @@ -33,6 +37,8 @@ interface RuntimeConfig { taskPendingTimeoutMs: number; databaseVolumeName: string; databaseVolumeSize: string; + pgdataBackupStagingDir: string; + baiduNetdiskInternalUrl: string; microservices: MicroserviceConfig[]; logFile: string; } @@ -112,6 +118,63 @@ interface RawTaskRow { updated_at: Date | string; } +interface ScheduledTaskRow { + id: string; + name: string; + description: string; + enabled: boolean; + schedule_json: JsonValue; + action_json: JsonValue; + concurrency_policy: string; + next_run_at: Date | string | null; + last_run_at: Date | string | null; + last_run_id: string | null; + created_at: Date | string; + updated_at: Date | string; +} + +interface ScheduledTaskRunRow { + id: string; + schedule_id: string; + trigger_type: string; + status: string; + task_id: string | null; + result: JsonValue | null; + error: string | null; + started_at: Date | string | null; + finished_at: Date | string | null; + duration_ms: number | string | null; + created_at: Date | string; + updated_at: Date | string; +} + +interface ScheduleSpec { + type: "daily" | "interval"; + timeOfDay?: string; + timezone?: string; + everySeconds?: number; +} + +interface DispatchScheduleAction { + type: "dispatch"; + providerId: string; + command: CoreDispatchMessage["command"]; + payload: Record; + timeoutMs?: number; +} + +interface PgdataBackupScheduleAction { + type: "pgdata_backup"; + volumeName: string; + remoteBaseDir: string; + stagingSubdir: string; + baiduBaseUrl: string; + timeoutMs: number; + cleanupLocal: boolean; +} + +type ScheduleAction = DispatchScheduleAction | PgdataBackupScheduleAction; + type TaskTerminalWaiter = (task: RawTaskRow | null) => void; interface MicroserviceProxyCacheEntry { @@ -141,6 +204,7 @@ const maxPerformanceSamples = 3000; const taskTerminalWaiters = new Map>(); const microserviceProxyCache = new Map(); const microserviceProxyRefreshes = new Map>(); +const activeScheduledRuns = new Set(); let lastTaskSweepAt = 0; let taskSweepInFlight: Promise | null = null; const microserviceProxyMaxBodyTextLength = 8 * 1024 * 1024; @@ -280,6 +344,8 @@ function readConfig(): RuntimeConfig { taskPendingTimeoutMs: readOptionalNumberEnv("TASK_PENDING_TIMEOUT_MS", 10 * 60 * 1000), databaseVolumeName: requiredEnv("DATABASE_VOLUME_NAME"), databaseVolumeSize: requiredEnv("DATABASE_VOLUME_SIZE"), + pgdataBackupStagingDir: process.env.PGDATA_BACKUP_STAGING_DIR || "/data/baidu-netdisk-staging", + baiduNetdiskInternalUrl: process.env.BAIDU_NETDISK_INTERNAL_URL || "http://baidu-netdisk:4244", microservices: readMicroservicesEnv(), logFile: requiredEnv("LOG_FILE"), }; @@ -342,6 +408,7 @@ function classifyRequestComponent(pathname: string): string { if (pathname.startsWith("/api/nodes/")) return "node_metrics_api"; if (pathname === "/api/nodes") return "node_inventory_api"; if (pathname.startsWith("/api/tasks")) return "scheduler_api"; + if (pathname.startsWith("/api/schedules")) return "scheduler_api"; if (pathname.startsWith("/api/events")) return "event_api"; if (pathname.startsWith("/api/performance")) return "performance_api"; if (pathname.startsWith("/api/")) return "core_api"; @@ -556,6 +623,38 @@ async function initDatabase(client: SqlClient): Promise { updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) `; + await client` + CREATE TABLE IF NOT EXISTS unidesk_scheduled_tasks ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + enabled BOOLEAN NOT NULL DEFAULT true, + schedule_json JSONB NOT NULL DEFAULT '{}'::jsonb, + action_json JSONB NOT NULL DEFAULT '{}'::jsonb, + concurrency_policy TEXT NOT NULL DEFAULT 'skip', + next_run_at TIMESTAMPTZ, + last_run_at TIMESTAMPTZ, + last_run_id TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `; + await client` + CREATE TABLE IF NOT EXISTS unidesk_scheduled_task_runs ( + id TEXT PRIMARY KEY, + schedule_id TEXT NOT NULL REFERENCES unidesk_scheduled_tasks(id) ON DELETE CASCADE, + trigger_type TEXT NOT NULL, + status TEXT NOT NULL, + task_id TEXT, + result JSONB, + error TEXT, + started_at TIMESTAMPTZ, + finished_at TIMESTAMPTZ, + duration_ms BIGINT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `; await client` CREATE TABLE IF NOT EXISTS unidesk_node_docker_status ( provider_id TEXT PRIMARY KEY, @@ -586,6 +685,9 @@ async function initDatabase(client: SqlClient): Promise { `; await client`CREATE INDEX IF NOT EXISTS idx_unidesk_tasks_updated_at ON unidesk_tasks(updated_at DESC)`; await client`CREATE INDEX IF NOT EXISTS idx_unidesk_tasks_status_updated_at ON unidesk_tasks(status, updated_at DESC)`; + await client`CREATE INDEX IF NOT EXISTS idx_unidesk_scheduled_tasks_next_run ON unidesk_scheduled_tasks(enabled, next_run_at)`; + await client`CREATE INDEX IF NOT EXISTS idx_unidesk_scheduled_task_runs_schedule_updated ON unidesk_scheduled_task_runs(schedule_id, updated_at DESC)`; + await client`CREATE INDEX IF NOT EXISTS idx_unidesk_scheduled_task_runs_status_updated ON unidesk_scheduled_task_runs(status, updated_at DESC)`; await client`CREATE INDEX IF NOT EXISTS idx_unidesk_node_system_status_updated_at ON unidesk_node_system_status(updated_at DESC)`; await client`CREATE INDEX IF NOT EXISTS idx_unidesk_node_metric_samples_provider_time ON unidesk_node_metric_samples(provider_id, collected_at DESC)`; dbReady = true; @@ -1501,6 +1603,720 @@ async function waitForTaskTerminal(taskId: string, timeoutMs: number): Promise { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function scheduleRunView(row: ScheduledTaskRunRow): JsonValue { + return { + id: row.id, + scheduleId: row.schedule_id, + trigger: row.trigger_type, + status: row.status, + taskId: row.task_id, + result: compactJson(row.result ?? null), + error: row.error ?? "", + startedAt: rowIso(row.started_at), + finishedAt: rowIso(row.finished_at), + durationMs: row.duration_ms === null ? null : Number(row.duration_ms), + createdAt: rowIso(row.created_at), + updatedAt: rowIso(row.updated_at), + }; +} + +function scheduledTaskView(row: ScheduledTaskRow, runs: ScheduledTaskRunRow[] = []): JsonValue { + return { + id: row.id, + name: row.name, + description: row.description, + enabled: row.enabled, + schedule: row.schedule_json, + action: row.action_json, + concurrencyPolicy: row.concurrency_policy, + nextRunAt: rowIso(row.next_run_at), + lastRunAt: rowIso(row.last_run_at), + lastRunId: row.last_run_id, + createdAt: rowIso(row.created_at), + updatedAt: rowIso(row.updated_at), + recentRuns: runs.map(scheduleRunView), + }; +} + +function normalizeScheduleId(value: unknown): string { + const raw = typeof value === "string" && value.trim().length > 0 + ? value.trim() + : `schedule_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`; + if (!/^[A-Za-z0-9_.:-]{1,120}$/u.test(raw)) { + throw new Error("schedule id must be 1-120 chars using letters, numbers, _, ., :, or -"); + } + return raw; +} + +function normalizeTimeOfDay(value: unknown): string { + const raw = typeof value === "string" && value.trim().length > 0 ? value.trim() : "03:00"; + const match = raw.match(/^([01]\d|2[0-3]):([0-5]\d)$/u); + if (match === null) throw new Error("daily schedule timeOfDay must use HH:MM in UTC"); + return `${match[1]}:${match[2]}`; +} + +function numberInRange(value: unknown, fallback: number, min: number, max: number): number { + const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : fallback; + if (!Number.isFinite(parsed)) return fallback; + return Math.max(min, Math.min(max, Math.floor(parsed))); +} + +function normalizeScheduleSpec(value: unknown): ScheduleSpec { + const record = isJsonRecord(value) ? value : {}; + if (record.type === "interval") { + return { + type: "interval", + everySeconds: numberInRange(record.everySeconds, 3600, 60, 366 * 24 * 3600), + }; + } + return { + type: "daily", + timeOfDay: normalizeTimeOfDay(record.timeOfDay), + timezone: typeof record.timezone === "string" && record.timezone.length > 0 ? record.timezone : "Etc/UTC", + }; +} + +function normalizeScheduleAction(value: unknown): ScheduleAction { + if (!isJsonRecord(value)) throw new Error("scheduled task action must be an object"); + if (value.type === "dispatch") { + const providerId = typeof value.providerId === "string" ? value.providerId.trim() : ""; + if (providerId.length === 0) throw new Error("dispatch action providerId is required"); + if (!isProviderDispatchCommand(value.command)) throw new Error("dispatch action command is invalid"); + const payload = isJsonRecord(value.payload) ? value.payload : {}; + const timeoutMs = value.timeoutMs === undefined ? undefined : numberInRange(value.timeoutMs, config.taskPendingTimeoutMs, 1_000, 24 * 3600_000); + return { type: "dispatch", providerId, command: value.command, payload, ...(timeoutMs === undefined ? {} : { timeoutMs }) }; + } + if (value.type === "pgdata_backup") { + return { + type: "pgdata_backup", + volumeName: typeof value.volumeName === "string" && value.volumeName.length > 0 ? value.volumeName : config.databaseVolumeName, + remoteBaseDir: normalizeRemoteDir(typeof value.remoteBaseDir === "string" ? value.remoteBaseDir : "/SERVER_DATA/UNIDESK_PG_DATA"), + stagingSubdir: normalizeRelativeStagingPath(typeof value.stagingSubdir === "string" ? value.stagingSubdir : "server-data/unidesk-pg-data").relativePath, + baiduBaseUrl: typeof value.baiduBaseUrl === "string" && value.baiduBaseUrl.length > 0 ? value.baiduBaseUrl : config.baiduNetdiskInternalUrl, + timeoutMs: numberInRange(value.timeoutMs, 60 * 60_000, 60_000, 24 * 3600_000), + cleanupLocal: value.cleanupLocal !== false, + }; + } + throw new Error("scheduled task action.type must be dispatch or pgdata_backup"); +} + +function computeNextRunAt(schedule: ScheduleSpec, after = new Date()): Date { + if (schedule.type === "interval") { + return new Date(after.getTime() + Math.max(60, schedule.everySeconds ?? 3600) * 1000); + } + const [hourText = "03", minuteText = "00"] = (schedule.timeOfDay ?? "03:00").split(":"); + const candidate = new Date(Date.UTC(after.getUTCFullYear(), after.getUTCMonth(), after.getUTCDate(), Number(hourText), Number(minuteText), 0, 0)); + if (candidate.getTime() <= after.getTime()) candidate.setUTCDate(candidate.getUTCDate() + 1); + return candidate; +} + +async function getScheduledTasks(limit: number): Promise { + const rows = await sql` + SELECT * + FROM unidesk_scheduled_tasks + ORDER BY enabled DESC, next_run_at ASC NULLS LAST, updated_at DESC + LIMIT ${limit} + `; + const runRows = await sql` + SELECT * + FROM unidesk_scheduled_task_runs + ORDER BY updated_at DESC + LIMIT ${Math.max(20, limit * 5)} + `; + const runsBySchedule = new Map(); + for (const run of runRows) { + const list = runsBySchedule.get(run.schedule_id) ?? []; + if (list.length < 5) list.push(run); + runsBySchedule.set(run.schedule_id, list); + } + return rows.map((row) => scheduledTaskView(row, runsBySchedule.get(row.id) ?? [])); +} + +async function getScheduledTaskRuns(scheduleId: string | null, limit: number): Promise { + const rows = scheduleId === null + ? await sql` + SELECT * + FROM unidesk_scheduled_task_runs + ORDER BY updated_at DESC + LIMIT ${limit} + ` + : await sql` + SELECT * + FROM unidesk_scheduled_task_runs + WHERE schedule_id = ${scheduleId} + ORDER BY updated_at DESC + LIMIT ${limit} + `; + return rows.map(scheduleRunView); +} + +async function getScheduledTaskRow(scheduleId: string): Promise { + const rows = await sql` + SELECT * + FROM unidesk_scheduled_tasks + WHERE id = ${scheduleId} + LIMIT 1 + `; + return rows[0] ?? null; +} + +async function upsertScheduledTask(req: Request, scheduleIdFromPath: string | null): Promise { + const body = await req.json().catch(() => ({})) as Record; + const id = normalizeScheduleId(scheduleIdFromPath ?? body.id); + const schedule = normalizeScheduleSpec(body.schedule); + const action = normalizeScheduleAction(body.action); + const name = typeof body.name === "string" && body.name.trim().length > 0 ? body.name.trim() : id; + const description = typeof body.description === "string" ? body.description : ""; + const enabled = typeof body.enabled === "boolean" ? body.enabled : true; + const concurrencyPolicy = body.concurrencyPolicy === "parallel" ? "parallel" : "skip"; + const existing = await getScheduledTaskRow(id); + const nextRunAt = existing?.next_run_at && JSON.stringify(existing.schedule_json) === JSON.stringify(schedule) + ? rowIso(existing.next_run_at) + : computeNextRunAt(schedule).toISOString(); + const scheduleJson = schedule as unknown as JsonValue; + const actionJson = action as unknown as JsonValue; + const rows = await sql` + INSERT INTO unidesk_scheduled_tasks (id, name, description, enabled, schedule_json, action_json, concurrency_policy, next_run_at, updated_at) + VALUES (${id}, ${name}, ${description}, ${enabled}, ${sql.json(scheduleJson)}, ${sql.json(actionJson)}, ${concurrencyPolicy}, ${nextRunAt}, now()) + ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + description = EXCLUDED.description, + enabled = EXCLUDED.enabled, + schedule_json = EXCLUDED.schedule_json, + action_json = EXCLUDED.action_json, + concurrency_policy = EXCLUDED.concurrency_policy, + next_run_at = EXCLUDED.next_run_at, + updated_at = now() + RETURNING * + `; + await recordEvent(existing === null ? "scheduled_task_created" : "scheduled_task_updated", "scheduler", { scheduleId: id, name, enabled, schedule: scheduleJson, action: compactJson(action) }); + return jsonResponse({ ok: true, schedule: scheduledTaskView(rows[0]) }, existing === null ? 201 : 200); +} + +async function patchScheduledTask(scheduleId: string, req: Request): Promise { + const current = await getScheduledTaskRow(scheduleId); + if (current === null) return jsonResponse({ ok: false, error: `scheduled task not found: ${scheduleId}` }, 404); + const body = await req.json().catch(() => ({})) as Record; + const schedule = body.schedule === undefined ? normalizeScheduleSpec(current.schedule_json) : normalizeScheduleSpec(body.schedule); + const action = body.action === undefined ? normalizeScheduleAction(current.action_json) : normalizeScheduleAction(body.action); + const name = typeof body.name === "string" && body.name.trim().length > 0 ? body.name.trim() : current.name; + const description = typeof body.description === "string" ? body.description : current.description; + const enabled = typeof body.enabled === "boolean" ? body.enabled : current.enabled; + const concurrencyPolicy = body.concurrencyPolicy === "parallel" || body.concurrencyPolicy === "skip" ? body.concurrencyPolicy : current.concurrency_policy; + const scheduleChanged = body.schedule !== undefined && JSON.stringify(schedule) !== JSON.stringify(current.schedule_json); + const nextRunAt = scheduleChanged || current.next_run_at === null ? computeNextRunAt(schedule).toISOString() : rowIso(current.next_run_at); + const scheduleJson = schedule as unknown as JsonValue; + const actionJson = action as unknown as JsonValue; + const rows = await sql` + UPDATE unidesk_scheduled_tasks + SET name = ${name}, + description = ${description}, + enabled = ${enabled}, + schedule_json = ${sql.json(scheduleJson)}, + action_json = ${sql.json(actionJson)}, + concurrency_policy = ${concurrencyPolicy}, + next_run_at = ${nextRunAt}, + updated_at = now() + WHERE id = ${scheduleId} + RETURNING * + `; + await recordEvent("scheduled_task_updated", "scheduler", { scheduleId, name, enabled, schedule: scheduleJson, action: compactJson(action) }); + return jsonResponse({ ok: true, schedule: scheduledTaskView(rows[0]) }); +} + +async function deleteScheduledTask(scheduleId: string): Promise { + const rows = await sql>` + DELETE FROM unidesk_scheduled_tasks + WHERE id = ${scheduleId} + RETURNING id + `; + if (rows.length === 0) return jsonResponse({ ok: false, error: `scheduled task not found: ${scheduleId}` }, 404); + await recordEvent("scheduled_task_deleted", "scheduler", { scheduleId }); + return jsonResponse({ ok: true, deleted: scheduleId }); +} + +function scheduledRawTaskJson(task: RawTaskRow | null): JsonValue { + if (task === null) return null; + return { + id: task.id, + providerId: task.provider_id, + command: task.command, + status: task.status, + payload: compactJson(task.payload), + result: compactJson(task.result ?? null), + updatedAt: rowIso(task.updated_at), + }; +} + +async function executeDispatchScheduleAction(action: DispatchScheduleAction): Promise<{ ok: boolean; taskId: string; result: JsonValue }> { + const { taskId, providerOnline } = await createAndSendTask(action.providerId, action.command, { + source: "scheduled-task", + ...action.payload, + }); + if (!providerOnline) { + return { ok: false, taskId, result: { providerOnline, taskId, error: `provider is offline: ${action.providerId}` } }; + } + const timeoutMs = action.timeoutMs ?? numberInRange(action.payload.timeoutMs, config.taskPendingTimeoutMs + 5000, 1000, 24 * 3600_000); + const task = await waitForTaskTerminal(taskId, timeoutMs); + const ok = task?.status === "succeeded"; + return { + ok, + taskId, + result: { + providerOnline, + taskId, + timeoutMs, + terminal: task === null ? false : isTerminalTaskStatus(task.status), + task: scheduledRawTaskJson(task), + }, + }; +} + +function normalizeRemoteDir(input: string): string { + const raw = input.trim() || "/"; + if (raw.includes("\0")) throw new Error("remote directory must not contain null bytes"); + const normalized = pathPosix.normalize(raw.startsWith("/") ? raw : `/${raw}`); + return normalized === "/" ? "/" : normalized.replace(/\/+$/u, ""); +} + +function normalizeRelativeStagingPath(input: string): { relativePath: string; absolutePath: string } { + const cleaned = input.replace(/\\/g, "/").replace(/^\/+/u, "").trim(); + const normalized = pathPosix.normalize(cleaned || "."); + if (normalized === "." || normalized.startsWith("../") || normalized === "..") throw new Error("staging path must be a relative child path"); + const absolutePath = resolve(config.pgdataBackupStagingDir, normalized); + const rel = relative(config.pgdataBackupStagingDir, absolutePath); + if (rel.startsWith("..") || rel.includes("\0") || resolve(absolutePath) === resolve(config.pgdataBackupStagingDir)) { + throw new Error("staging path must stay inside PGDATA_BACKUP_STAGING_DIR"); + } + return { relativePath: normalized, absolutePath }; +} + +function remoteFolderChain(remoteDir: string): string[] { + const parts = normalizeRemoteDir(remoteDir).split("/").filter(Boolean); + const folders: string[] = []; + let current = ""; + for (const part of parts) { + current = `${current}/${part}`; + folders.push(current); + } + return folders; +} + +function safeFilePart(value: string): string { + return value.replace(/[^A-Za-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80) || "data"; +} + +function utcBackupParts(date = new Date()): { date: string; time: string; month: string; stamp: string } { + const year = String(date.getUTCFullYear()).padStart(4, "0"); + const monthNumber = String(date.getUTCMonth() + 1).padStart(2, "0"); + const day = String(date.getUTCDate()).padStart(2, "0"); + const hour = String(date.getUTCHours()).padStart(2, "0"); + const minute = String(date.getUTCMinutes()).padStart(2, "0"); + const second = String(date.getUTCSeconds()).padStart(2, "0"); + return { date: `${year}${monthNumber}${day}`, time: `${hour}${minute}${second}`, month: `${year}${monthNumber}`, stamp: `${year}${monthNumber}${day}_${hour}${minute}${second}` }; +} + +function databaseConnectionParts(): { host: string; port: string; user: string; password: string } { + const url = new URL(config.databaseUrl); + return { + host: url.hostname || "database", + port: url.port || "5432", + user: decodeURIComponent(url.username || "postgres"), + password: decodeURIComponent(url.password || ""), + }; +} + +async function runLocalCommand( + command: string, + args: string[], + options: { cwd?: string; env?: Record; timeoutMs: number }, +): Promise<{ ok: boolean; stdout: string; stderr: string; exitCode: number | null; timedOut: boolean }> { + const proc = Bun.spawn([command, ...args], { + cwd: options.cwd, + env: { ...process.env, ...(options.env ?? {}) }, + stdout: "pipe", + stderr: "pipe", + }); + let timedOut = false; + const timer = setTimeout(() => { + timedOut = true; + proc.kill("SIGTERM"); + }, Math.max(1, options.timeoutMs)); + try { + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + return { ok: exitCode === 0 && !timedOut, stdout: truncateText(stdout, 4000), stderr: truncateText(stderr, 4000), exitCode, timedOut }; + } finally { + clearTimeout(timer); + } +} + +async function sha256File(filePath: string): Promise { + const hash = createHash("sha256"); + await new Promise((resolveHash, rejectHash) => { + const stream = createReadStream(filePath); + stream.on("data", (chunk) => hash.update(chunk)); + stream.on("error", rejectHash); + stream.on("end", resolveHash); + }); + return hash.digest("hex"); +} + +async function fetchJsonWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise> { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), Math.max(1, timeoutMs)); + try { + const response = await fetch(url, { ...init, signal: controller.signal }); + const text = await response.text(); + let parsed: unknown = {}; + try { + parsed = text.length > 0 ? JSON.parse(text) as unknown : {}; + } catch { + parsed = { text }; + } + if (!response.ok) throw new Error(`HTTP ${response.status}: ${truncateText(text, 1000)}`); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { value: parsed }; + return parsed as Record; + } finally { + clearTimeout(timer); + } +} + +async function baiduJson(baseUrl: string, path: string, init: RequestInit = {}, timeoutMs = 30_000): Promise> { + const url = new URL(path, baseUrl); + const headers = new Headers(init.headers); + if (init.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json"); + const body = await fetchJsonWithTimeout(url.toString(), { ...init, headers }, timeoutMs); + if (body.ok === false) throw new Error(`Baidu Netdisk API failed: ${JSON.stringify(compactJson(body))}`); + return body; +} + +function jobRecord(value: unknown): Record { + return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; +} + +async function waitForBaiduTransfer(baseUrl: string, jobId: string, timeoutMs: number): Promise> { + const deadline = Date.now() + timeoutMs; + let latest: Record = {}; + while (Date.now() < deadline) { + const detail = await baiduJson(baseUrl, `/api/transfers/${encodeURIComponent(jobId)}`, {}, 30_000); + latest = detail; + const job = jobRecord(detail.job); + const status = String(job.status || ""); + if (status === "succeeded") return detail; + if (status === "failed" || status === "canceled") throw new Error(`Baidu transfer ${status}: ${String(job.error || "no error detail")}`); + await Bun.sleep(2000); + } + throw new Error(`Baidu transfer timed out after ${timeoutMs}ms: ${JSON.stringify(compactJson(latest))}`); +} + +async function executePgdataBackupAction(action: PgdataBackupScheduleAction, runId: string): Promise<{ ok: boolean; result: JsonValue }> { + const startedAt = new Date(); + const parts = utcBackupParts(startedAt); + const filename = `${parts.stamp}_${safeFilePart(action.volumeName)}.pg_basebackup.tar.gz`; + const monthRelativeDir = pathPosix.join(action.stagingSubdir, parts.month); + const backupRelativePath = pathPosix.join(monthRelativeDir, filename); + const backupPath = normalizeRelativeStagingPath(backupRelativePath).absolutePath; + const tempRelativeDir = pathPosix.join(action.stagingSubdir, parts.month, `.tmp_${runId}`); + const tempDir = normalizeRelativeStagingPath(tempRelativeDir).absolutePath; + const remoteDir = normalizeRemoteDir(pathPosix.join(action.remoteBaseDir, parts.month)); + const remotePath = normalizeRemoteDir(pathPosix.join(remoteDir, filename)); + const db = databaseConnectionParts(); + await mkdir(dirname(backupPath), { recursive: true }); + await rm(tempDir, { recursive: true, force: true }); + await mkdir(tempDir, { recursive: true }); + let uploadDetail: Record | null = null; + let backupBytes = 0; + let backupSha256 = ""; + try { + const backupTimeoutMs = Math.max(60_000, Math.floor(action.timeoutMs * 0.55)); + const packageTimeoutMs = Math.max(60_000, Math.floor(action.timeoutMs * 0.15)); + const uploadTimeoutMs = Math.max(60_000, action.timeoutMs - backupTimeoutMs - packageTimeoutMs); + const basebackup = await runLocalCommand("pg_basebackup", [ + "-h", db.host, + "-p", db.port, + "-U", db.user, + "-D", tempDir, + "-Ft", + "-X", "stream", + "-z", + "--checkpoint=fast", + "--no-sync", + ], { env: { PGPASSWORD: db.password }, timeoutMs: backupTimeoutMs }); + if (!basebackup.ok) throw new Error(`pg_basebackup failed exit=${basebackup.exitCode} timedOut=${basebackup.timedOut}: ${basebackup.stderr || basebackup.stdout}`); + const packaged = await runLocalCommand("tar", ["-czf", backupPath, "-C", tempDir, "."], { timeoutMs: packageTimeoutMs }); + if (!packaged.ok) throw new Error(`tar packaging failed exit=${packaged.exitCode} timedOut=${packaged.timedOut}: ${packaged.stderr || packaged.stdout}`); + const info = await stat(backupPath); + backupBytes = info.size; + backupSha256 = await sha256File(backupPath); + for (const folder of remoteFolderChain(remoteDir)) { + await baiduJson(action.baiduBaseUrl, "/api/folders", { + method: "POST", + body: JSON.stringify({ path: folder }), + }, 60_000); + } + const created = await baiduJson(action.baiduBaseUrl, "/api/transfers/upload-from-path", { + method: "POST", + body: JSON.stringify({ localPath: backupRelativePath, remotePath }), + }, 60_000); + const jobId = String(jobRecord(created.job).id || ""); + if (jobId.length === 0) throw new Error(`Baidu upload did not return a job id: ${JSON.stringify(compactJson(created))}`); + uploadDetail = await waitForBaiduTransfer(action.baiduBaseUrl, jobId, uploadTimeoutMs); + const uploadedJob = jobRecord(uploadDetail.job); + return { + ok: true, + result: { + backupType: "pg_basebackup", + volumeName: action.volumeName, + backupBytes, + backupSha256, + localRelativePath: backupRelativePath, + remotePath, + remoteDir, + month: parts.month, + timestampPrefix: parts.stamp, + baiduTransferJobId: jobId, + baiduTransferStatus: String(uploadedJob.status || ""), + baiduFsId: String(uploadedJob.fsId || ""), + baiduResult: compactJson(uploadedJob.result ?? null), + }, + }; + } finally { + await rm(tempDir, { recursive: true, force: true }).catch(() => undefined); + if (action.cleanupLocal) await rm(backupPath, { force: true }).catch(() => undefined); + } +} + +async function executeScheduleAction(action: ScheduleAction, runId: string): Promise<{ ok: boolean; taskId: string | null; result: JsonValue }> { + if (action.type === "dispatch") { + const dispatched = await executeDispatchScheduleAction(action); + return { ok: dispatched.ok, taskId: dispatched.taskId, result: dispatched.result }; + } + const backup = await executePgdataBackupAction(action, runId); + return { ok: backup.ok, taskId: null, result: backup.result }; +} + +async function executeScheduledRun(runId: string): Promise { + if (activeScheduledRuns.has(runId)) return; + activeScheduledRuns.add(runId); + const started = Date.now(); + try { + const queuedRuns = await sql` + SELECT * + FROM unidesk_scheduled_task_runs + WHERE id = ${runId} + LIMIT 1 + `; + const queuedRun = queuedRuns[0]; + if (queuedRun === undefined) return; + const scheduleRow = await getScheduledTaskRow(queuedRun.schedule_id); + if (scheduleRow === null) return; + const runRows = await sql` + UPDATE unidesk_scheduled_task_runs + SET status = 'running', started_at = now(), updated_at = now() + WHERE id = ${runId} AND status = 'queued' + RETURNING * + `; + if (runRows.length === 0) return; + try { + const action = normalizeScheduleAction(scheduleRow.action_json); + const outcome = await executeScheduleAction(action, runId); + const durationMs = Date.now() - started; + const status = outcome.ok ? "succeeded" : "failed"; + const error = outcome.ok ? null : "scheduled action failed"; + await sql.begin(async (tx) => { + await tx` + UPDATE unidesk_scheduled_task_runs + SET status = ${status}, + task_id = ${outcome.taskId}, + result = ${tx.json(outcome.result)}, + error = ${error}, + finished_at = now(), + duration_ms = ${durationMs}, + updated_at = now() + WHERE id = ${runId} + `; + await tx` + UPDATE unidesk_scheduled_tasks + SET last_run_at = now(), last_run_id = ${runId}, updated_at = now() + WHERE id = ${scheduleRow.id} + `; + }); + await recordEvent(`scheduled_task_${status}`, "scheduler", { scheduleId: scheduleRow.id, runId, durationMs, result: compactJson(outcome.result) }); + } catch (error) { + const durationMs = Date.now() - started; + await sql.begin(async (tx) => { + await tx` + UPDATE unidesk_scheduled_task_runs + SET status = 'failed', + result = ${tx.json(errorToJson(error))}, + error = ${error instanceof Error ? error.message : String(error)}, + finished_at = now(), + duration_ms = ${durationMs}, + updated_at = now() + WHERE id = ${runId} + `; + await tx` + UPDATE unidesk_scheduled_tasks + SET last_run_at = now(), last_run_id = ${runId}, updated_at = now() + WHERE id = ${scheduleRow.id} + `; + }); + await recordEvent("scheduled_task_failed", "scheduler", { scheduleId: scheduleRow.id, runId, durationMs, error: errorToJson(error) }); + } + } finally { + activeScheduledRuns.delete(runId); + } +} + +async function triggerScheduledTask(scheduleId: string, triggerType: "manual" | "schedule"): Promise { + const runId = `schedrun_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`; + let createdRun: ScheduledTaskRunRow | null = null; + await sql.begin(async (tx) => { + const rows = await tx` + SELECT * + FROM unidesk_scheduled_tasks + WHERE id = ${scheduleId} + FOR UPDATE + `; + const scheduleRow = rows[0]; + if (scheduleRow === undefined) throw new Error(`scheduled task not found: ${scheduleId}`); + if (triggerType === "schedule" && !scheduleRow.enabled) return; + const runningRows = await tx>` + SELECT count(*)::int AS count + FROM unidesk_scheduled_task_runs + WHERE schedule_id = ${scheduleId} + AND status IN ('queued', 'running') + `; + const runningCount = Number(runningRows[0]?.count ?? 0); + const schedule = normalizeScheduleSpec(scheduleRow.schedule_json); + const shouldAdvanceNext = triggerType === "schedule"; + if (shouldAdvanceNext) { + await tx` + UPDATE unidesk_scheduled_tasks + SET next_run_at = ${computeNextRunAt(schedule).toISOString()}, updated_at = now() + WHERE id = ${scheduleId} + `; + } + if (scheduleRow.concurrency_policy !== "parallel" && runningCount > 0) { + const skippedRows = await tx` + INSERT INTO unidesk_scheduled_task_runs (id, schedule_id, trigger_type, status, result, error, finished_at, duration_ms) + VALUES (${runId}, ${scheduleId}, ${triggerType}, 'skipped', ${tx.json({ reason: "previous run still active", runningCount })}, 'previous run still active', now(), 0) + RETURNING * + `; + createdRun = skippedRows[0] ?? null; + return; + } + const runRows = await tx` + INSERT INTO unidesk_scheduled_task_runs (id, schedule_id, trigger_type, status) + VALUES (${runId}, ${scheduleId}, ${triggerType}, 'queued') + RETURNING * + `; + createdRun = runRows[0] ?? null; + }); + const run = createdRun as ScheduledTaskRunRow | null; + if (run === null) return null; + if (run.status === "queued") { + setTimeout(() => { + executeScheduledRun(runId).catch((error) => logger("error", "scheduled_run_uncaught", { runId, error: errorToJson(error) })); + }, 0); + } + await recordEvent("scheduled_task_triggered", "scheduler", { scheduleId, runId, triggerType, status: run.status }); + return scheduleRunView(run); +} + +async function scheduledTaskRoute(req: Request, url: URL): Promise { + const prefix = "/api/schedules"; + const rest = url.pathname === prefix ? "" : url.pathname.slice(prefix.length + 1); + const segments = rest.split("/").filter(Boolean).map((part) => decodeURIComponent(part)); + if (url.pathname === prefix && req.method === "GET") { + return jsonResponse({ ok: true, schedules: await getScheduledTasks(readLimit(url, 100)) }); + } + if (url.pathname === prefix && req.method === "POST") return upsertScheduledTask(req, null); + if (segments.length === 1 && segments[0] === "runs" && req.method === "GET") { + return jsonResponse({ ok: true, runs: await getScheduledTaskRuns(null, readLimit(url, 100)) }); + } + if (segments.length === 1 && req.method === "GET") { + const schedule = await getScheduledTaskRow(segments[0]); + if (schedule === null) return jsonResponse({ ok: false, error: `scheduled task not found: ${segments[0]}` }, 404); + const runs = await sql` + SELECT * + FROM unidesk_scheduled_task_runs + WHERE schedule_id = ${segments[0]} + ORDER BY updated_at DESC + LIMIT 20 + `; + return jsonResponse({ ok: true, schedule: scheduledTaskView(schedule, runs) }); + } + if (segments.length === 1 && req.method === "PUT") return upsertScheduledTask(req, segments[0]); + if (segments.length === 1 && req.method === "PATCH") return patchScheduledTask(segments[0], req); + if (segments.length === 1 && req.method === "DELETE") return deleteScheduledTask(segments[0]); + if (segments.length === 2 && segments[1] === "run" && req.method === "POST") { + const run = await triggerScheduledTask(segments[0], "manual"); + if (run === null) return jsonResponse({ ok: false, error: `scheduled task was not triggered: ${segments[0]}` }, 409); + return jsonResponse({ ok: true, run }); + } + if (segments.length === 2 && segments[1] === "runs" && req.method === "GET") { + return jsonResponse({ ok: true, runs: await getScheduledTaskRuns(segments[0], readLimit(url, 100)) }); + } + return jsonResponse({ ok: false, error: "scheduled task route not found", path: url.pathname }, 404); +} + +async function recoverScheduledRuns(): Promise { + const rows = await sql` + UPDATE unidesk_scheduled_task_runs + SET status = 'failed', + error = 'backend-core restarted before scheduled run completed', + result = jsonb_build_object('error', 'backend-core restarted before scheduled run completed'), + finished_at = now(), + updated_at = now() + WHERE status IN ('queued', 'running') + RETURNING * + `; + if (rows.length > 0) await recordEvent("scheduled_runs_recovered", "scheduler", { failedRunCount: rows.length }); + const schedules = await sql` + SELECT * + FROM unidesk_scheduled_tasks + WHERE next_run_at IS NULL + LIMIT 200 + `; + for (const schedule of schedules) { + const nextRunAt = computeNextRunAt(normalizeScheduleSpec(schedule.schedule_json)).toISOString(); + await sql`UPDATE unidesk_scheduled_tasks SET next_run_at = ${nextRunAt}, updated_at = now() WHERE id = ${schedule.id}`; + } +} + +async function runDueScheduledTasks(): Promise { + if (!dbReady) return; + const rows = await sql` + SELECT * + FROM unidesk_scheduled_tasks + WHERE enabled = true + AND (next_run_at IS NULL OR next_run_at <= now()) + ORDER BY next_run_at ASC NULLS FIRST + LIMIT 5 + `; + for (const row of rows) { + try { + await triggerScheduledTask(row.id, "schedule"); + } catch (error) { + logger("error", "scheduled_task_due_trigger_failed", { scheduleId: row.id, error: errorToJson(error) }); + } + } +} + function isMicroservicePathAllowed(service: MicroserviceConfig, path: string): boolean { return service.backend.allowedPathPrefixes.some((prefix) => path === prefix || path.startsWith(prefix)); } @@ -2228,6 +3044,9 @@ async function routeInner(req: Request, server: Server): Promise getTasks(readLimit(url, 100), url.searchParams.get("status") ?? "all", lite, summary)) }); } + if (url.pathname === "/api/schedules" || url.pathname.startsWith("/api/schedules/")) { + return withPerformanceOperation("scheduler", "schedules", url.pathname, () => scheduledTaskRoute(req, url)); + } if (url.pathname === "/api/microservices") return jsonResponse({ ok: true, microservices: await withPerformanceOperation("core", "microservices", url.pathname, () => getMicroservices()) }); if (url.pathname === "/api/performance") return jsonResponse(await getPerformance()); 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)); @@ -2281,6 +3100,8 @@ function readLimit(url: URL, defaultLimit: number): number { await initDatabaseWithRetry(); markStaleTasksFailed().catch((error) => logger("error", "task_timeout_sweep_failed", { error: errorToJson(error) })); +await recoverScheduledRuns(); +runDueScheduledTasks().catch((error) => logger("error", "scheduled_task_sweep_failed", { error: errorToJson(error) })); const apiServer = Bun.serve({ port: config.port, @@ -2365,6 +3186,10 @@ setInterval(() => { markStaleTasksFailed().catch((error) => logger("error", "task_timeout_sweep_failed", { error: errorToJson(error) })); }, Math.min(config.taskPendingTimeoutMs, 60_000)); +setInterval(() => { + runDueScheduledTasks().catch((error) => logger("error", "scheduled_task_sweep_failed", { error: errorToJson(error) })); +}, 30_000); + logger("info", "server_listening", { apiUrl: `http://0.0.0.0:${apiServer.port}`, providerIngressUrl: `ws://0.0.0.0:${providerServer.port}/ws/provider`, diff --git a/src/components/database/config/pg_hba.conf b/src/components/database/config/pg_hba.conf new file mode 100644 index 00000000..6a736f0d --- /dev/null +++ b/src/components/database/config/pg_hba.conf @@ -0,0 +1,9 @@ +# UniDesk PostgreSQL client and physical backup access. +local all all trust +host all all 127.0.0.1/32 trust +host all all ::1/128 trust +local replication all trust +host replication all 127.0.0.1/32 trust +host replication all ::1/128 trust +host replication all all scram-sha-256 +host all all all scram-sha-256 diff --git a/src/components/frontend/public/app.js b/src/components/frontend/public/app.js index cab60f53..8a5ff871 100644 --- a/src/components/frontend/public/app.js +++ b/src/components/frontend/public/app.js @@ -1,39 +1,39 @@ -(()=>{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,` +(()=>{var LO=Object.create;var{getPrototypeOf:XO,defineProperty:oJ,getOwnPropertyNames:BO}=Object;var YO=Object.prototype.hasOwnProperty;function wO(f){return this[f]}var DO,TO,cf=(f,u,l)=>{var _=f!=null&&typeof f==="object";if(_){var y=u?DO??=new WeakMap:TO??=new WeakMap,$=y.get(f);if($)return $}l=f!=null?LO(XO(f)):{};let r=u||!f||!f.__esModule?oJ(l,"default",{value:f,enumerable:!0}):l;for(let j of BO(f))if(!YO.call(r,j))oJ(r,j,{get:wO.bind(f,j),enumerable:!0});if(_)y.set(f,r);return r};var su=(f,u)=>()=>(u||f((u={exports:{}}).exports,u),u.exports);var ef=((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 AU=su((kf)=>{var N3=Symbol.for("react.element"),MO=Symbol.for("react.portal"),PO=Symbol.for("react.fragment"),nO=Symbol.for("react.strict_mode"),SO=Symbol.for("react.profiler"),CO=Symbol.for("react.provider"),iO=Symbol.for("react.context"),cO=Symbol.for("react.forward_ref"),RO=Symbol.for("react.suspense"),xO=Symbol.for("react.memo"),bO=Symbol.for("react.lazy"),aJ=Symbol.iterator;function vO(f){if(f===null||typeof f!=="object")return null;return f=aJ&&f[aJ]||f["@@iterator"],typeof f==="function"?f:null}var fU={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},uU=Object.assign,lU={};function by(f,u,l){this.props=f,this.context=u,this.refs=lU,this.updater=l||fU}by.prototype.isReactComponent={};by.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")};by.prototype.forceUpdate=function(f){this.updater.enqueueForceUpdate(this,f,"forceUpdate")};function _U(){}_U.prototype=by.prototype;function Mr(f,u,l){this.props=f,this.context=u,this.refs=lU,this.updater=l||fU}var Pr=Mr.prototype=new _U;Pr.constructor=Mr;uU(Pr,by.prototype);Pr.isPureReactComponent=!0;var dJ=Array.isArray,yU=Object.prototype.hasOwnProperty,nr={current:null},$U={key:!0,ref:!0,__self:!0,__source:!0};function rU(f,u,l){var _,y={},$=null,r=null;if(u!=null)for(_ in u.ref!==void 0&&(r=u.ref),u.key!==void 0&&($=""+u.key),u)yU.call(u,_)&&!$U.hasOwnProperty(_)&&(y[_]=u[_]);var j=arguments.length-2;if(j===1)y.children=l;else if(1{FU.exports=AU()});var EU=su((z0)=>{function Rr(f,u){var l=f.length;f.push(u);f:for(;0>>1,y=f[_];if(0>>1;_<$;){var r=2*(_+1)-1,j=f[r],A=r+1,J=f[A];if(0>k4(j,l))Ak4(J,j)?(f[_]=J,f[A]=l,_=A):(f[_]=j,f[r]=l,_=r);else if(Ak4(J,l))f[_]=J,f[A]=l,_=A;else break f}}return u}function k4(f,u){var l=f.sortIndex-u.sortIndex;return l!==0?l:f.id-u.id}if(typeof performance==="object"&&typeof performance.now==="function")xr=performance,z0.unstable_now=function(){return xr.now()};else t4=Date,br=t4.now(),z0.unstable_now=function(){return t4.now()-br};var xr,t4,br,tl=[],h1=[],sO=1,Wl=null,Au=3,d4=!1,m_=!1,E3=!1,zU=typeof setTimeout==="function"?setTimeout:null,GU=typeof clearTimeout==="function"?clearTimeout:null,WU=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function vr(f){for(var u=wl(h1);u!==null;){if(u.callback===null)a4(h1);else if(u.startTime<=f)a4(h1),u.sortIndex=u.expirationTime,Rr(tl,u);else break;u=wl(h1)}}function Ir(f){if(E3=!1,vr(f),!m_)if(wl(tl)!==null)m_=!0,mr(pr);else{var u=wl(h1);u!==null&&gr(Ir,u.startTime-f)}}function pr(f,u){m_=!1,E3&&(E3=!1,GU(H3),H3=-1),d4=!0;var l=Au;try{vr(u);for(Wl=wl(tl);Wl!==null&&(!(Wl.expirationTime>u)||f&&!ZU());){var _=Wl.callback;if(typeof _==="function"){Wl.callback=null,Au=Wl.priorityLevel;var y=_(Wl.expirationTime<=u);u=z0.unstable_now(),typeof y==="function"?Wl.callback=y:Wl===wl(tl)&&a4(tl),vr(u)}else a4(tl);Wl=wl(tl)}if(Wl!==null)var $=!0;else{var r=wl(h1);r!==null&&gr(Ir,r.startTime-u),$=!1}return $}finally{Wl=null,Au=l,d4=!1}}var e4=!1,s4=null,H3=-1,KU=5,NU=-1;function ZU(){return z0.unstable_now()-NUf||125_?(f.sortIndex=l,Rr(h1,f),wl(tl)===null&&f===wl(h1)&&(E3?(GU(H3),H3=-1):E3=!0,gr(Ir,l-_))):(f.sortIndex=y,Rr(tl,f),m_||d4||(m_=!0,mr(pr))),f};z0.unstable_shouldYield=ZU;z0.unstable_wrapCallback=function(f){var u=Au;return function(){var l=Au;Au=u;try{return f.apply(this,arguments)}finally{Au=l}}}});var OU=su((PC,HU)=>{HU.exports=EU()});var qz=su((ul)=>{var oO=O0(),eu=OU();function zf(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"),z9=Object.prototype.hasOwnProperty,aO=/^[: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]*$/,VU={},qU={};function dO(f){if(z9.call(qU,f))return!0;if(z9.call(VU,f))return!1;if(aO.test(f))return qU[f]=!0;return VU[f]=!0,!1}function eO(f,u,l,_){if(l!==null&&l.type===0)return!1;switch(typeof u){case"function":case"symbol":return!0;case"boolean":if(_)return!1;if(l!==null)return!l.acceptsBooleans;return f=f.toLowerCase().slice(0,5),f!=="data-"&&f!=="aria-";default:return!1}}function fV(f,u,l,_){if(u===null||typeof u>"u"||eO(f,u,l,_))return!0;if(_)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 qu(f,u,l,_,y,$,r){this.acceptsBooleans=u===2||u===3||u===4,this.attributeName=_,this.attributeNamespace=y,this.mustUseProperty=l,this.propertyName=f,this.type=u,this.sanitizeURL=$,this.removeEmptyString=r}var _u={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(f){_u[f]=new qu(f,0,!1,f,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(f){var u=f[0];_u[u]=new qu(u,1,!1,f[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(f){_u[f]=new qu(f,2,!1,f.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(f){_u[f]=new qu(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){_u[f]=new qu(f,3,!1,f.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(f){_u[f]=new qu(f,3,!0,f,null,!1,!1)});["capture","download"].forEach(function(f){_u[f]=new qu(f,4,!1,f,null,!1,!1)});["cols","rows","size","span"].forEach(function(f){_u[f]=new qu(f,6,!1,f,null,!1,!1)});["rowSpan","start"].forEach(function(f){_u[f]=new qu(f,5,!1,f.toLowerCase(),null,!1,!1)});var A7=/[\-:]([a-z])/g;function F7(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(A7,F7);_u[u]=new qu(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(A7,F7);_u[u]=new qu(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(A7,F7);_u[u]=new qu(u,1,!1,f,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(f){_u[f]=new qu(f,1,!1,f.toLowerCase(),null,!1,!1)});_u.xlinkHref=new qu("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(f){_u[f]=new qu(f,1,!1,f.toLowerCase(),null,!0,!0)});function J7(f,u,l,_){var y=_u.hasOwnProperty(u)?_u[u]:null;if(y!==null?y.type!==0:_||!(2j||y[r]!==$[j]){var A=` +`+y[r].replace(" at new "," at ");return f.displayName&&A.includes("")&&(A=A.replace("",f.displayName)),A}while(1<=r&&0<=j);break}}}finally{tr=!1,Error.prepareStackTrace=l}return(f=f?f.displayName||f.name:"")?w3(f):""}function uV(f){switch(f.tag){case 5:return w3(f.type);case 16:return w3("Lazy");case 13:return w3("Suspense");case 19:return w3("SuspenseList");case 0:case 2:case 15:return f=sr(f.type,!1),f;case 11:return f=sr(f.type.render,!1),f;case 1:return f=sr(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 py:return"Fragment";case Iy:return"Portal";case G9:return"Profiler";case U7:return"StrictMode";case K9:return"Suspense";case N9:return"SuspenseList"}if(typeof f==="object")switch(f.$$typeof){case wQ:return(f.displayName||"Context")+".Consumer";case YQ:return(f._context.displayName||"Context")+".Provider";case Q7:var u=f.render;return f=f.displayName,f||(f=u.displayName||u.name||"",f=f!==""?"ForwardRef("+f+")":"ForwardRef"),f;case W7:return u=f.displayName||null,u!==null?u:Z9(f.type)||"Memo";case p1:u=f._payload,f=f._init;try{return Z9(f(u))}catch(l){}}return null}function lV(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===U7?"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 y_(f){switch(typeof f){case"boolean":case"number":case"string":case"undefined":return f;case"object":return f;default:return""}}function TQ(f){var u=f.type;return(f=f.nodeName)&&f.toLowerCase()==="input"&&(u==="checkbox"||u==="radio")}function _V(f){var u=TQ(f)?"checked":"value",l=Object.getOwnPropertyDescriptor(f.constructor.prototype,u),_=""+f[u];if(!f.hasOwnProperty(u)&&typeof l<"u"&&typeof l.get==="function"&&typeof l.set==="function"){var{get:y,set:$}=l;return Object.defineProperty(f,u,{configurable:!0,get:function(){return y.call(this)},set:function(r){_=""+r,$.call(this,r)}}),Object.defineProperty(f,u,{enumerable:l.enumerable}),{getValue:function(){return _},setValue:function(r){_=""+r},stopTracking:function(){f._valueTracker=null,delete f[u]}}}}function u8(f){f._valueTracker||(f._valueTracker=_V(f))}function MQ(f){if(!f)return!1;var u=f._valueTracker;if(!u)return!0;var l=u.getValue(),_="";return f&&(_=TQ(f)?f.checked?"true":"false":f.value),f=_,f!==l?(u.setValue(f),!0):!1}function B8(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 E9(f,u){var l=u.checked;return B0({},u,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:l!=null?l:f._wrapperState.initialChecked})}function XU(f,u){var l=u.defaultValue==null?"":u.defaultValue,_=u.checked!=null?u.checked:u.defaultChecked;l=y_(u.value!=null?u.value:l),f._wrapperState={initialChecked:_,initialValue:l,controlled:u.type==="checkbox"||u.type==="radio"?u.checked!=null:u.value!=null}}function PQ(f,u){u=u.checked,u!=null&&J7(f,"checked",u,!1)}function H9(f,u){PQ(f,u);var l=y_(u.value),_=u.type;if(l!=null)if(_==="number"){if(l===0&&f.value===""||f.value!=l)f.value=""+l}else f.value!==""+l&&(f.value=""+l);else if(_==="submit"||_==="reset"){f.removeAttribute("value");return}u.hasOwnProperty("value")?O9(f,u.type,l):u.hasOwnProperty("defaultValue")&&O9(f,u.type,y_(u.defaultValue)),u.checked==null&&u.defaultChecked!=null&&(f.defaultChecked=!!u.defaultChecked)}function BU(f,u,l){if(u.hasOwnProperty("value")||u.hasOwnProperty("defaultValue")){var _=u.type;if(!(_!=="submit"&&_!=="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 O9(f,u,l){if(u!=="number"||B8(f.ownerDocument)!==f)l==null?f.defaultValue=""+f._wrapperState.initialValue:f.defaultValue!==""+l&&(f.defaultValue=""+l)}var D3=Array.isArray;function u$(f,u,l,_){if(f=f.options,u){u={};for(var y=0;y"+u.valueOf().toString()+"";for(u=l8.firstChild;f.firstChild;)f.removeChild(f.firstChild);for(;u.firstChild;)f.appendChild(u.firstChild)}});function g3(f,u){if(u){var l=f.firstChild;if(l&&l===f.lastChild&&l.nodeType===3){l.nodeValue=u;return}}f.textContent=u}var i3={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},yV=["Webkit","ms","Moz","O"];Object.keys(i3).forEach(function(f){yV.forEach(function(u){u=u+f.charAt(0).toUpperCase()+f.substring(1),i3[u]=i3[f]})});function iQ(f,u,l){return u==null||typeof u==="boolean"||u===""?"":l||typeof u!=="number"||u===0||i3.hasOwnProperty(f)&&i3[f]?(""+u).trim():u+"px"}function cQ(f,u){f=f.style;for(var l in u)if(u.hasOwnProperty(l)){var _=l.indexOf("--")===0,y=iQ(l,u[l],_);l==="float"&&(l="cssFloat"),_?f.setProperty(l,y):f[l]=y}}var $V=B0({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 L9(f,u){if(u){if($V[f]&&(u.children!=null||u.dangerouslySetInnerHTML!=null))throw Error(zf(137,f));if(u.dangerouslySetInnerHTML!=null){if(u.children!=null)throw Error(zf(60));if(typeof u.dangerouslySetInnerHTML!=="object"||!("__html"in u.dangerouslySetInnerHTML))throw Error(zf(61))}if(u.style!=null&&typeof u.style!=="object")throw Error(zf(62))}}function X9(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 B9=null;function z7(f){return f=f.target||f.srcElement||window,f.correspondingUseElement&&(f=f.correspondingUseElement),f.nodeType===3?f.parentNode:f}var Y9=null,l$=null,_$=null;function DU(f){if(f=J6(f)){if(typeof Y9!=="function")throw Error(zf(280));var u=f.stateNode;u&&(u=f2(u),Y9(f.stateNode,f.type,u))}}function RQ(f){l$?_$?_$.push(f):_$=[f]:l$=f}function xQ(){if(l$){var f=l$,u=_$;if(_$=l$=null,DU(f),u)for(f=0;f>>=0,f===0?32:31-(KV(f)/NV|0)|0}var _8=64,y8=4194304;function T3(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 T8(f,u){var l=f.pendingLanes;if(l===0)return 0;var _=0,y=f.suspendedLanes,$=f.pingedLanes,r=l&268435455;if(r!==0){var j=r&~y;j!==0?_=T3(j):($&=r,$!==0&&(_=T3($)))}else r=l&~y,r!==0?_=T3(r):$!==0&&(_=T3($));if(_===0)return 0;if(u!==0&&u!==_&&(u&y)===0&&(y=_&-_,$=u&-u,y>=$||y===16&&($&4194240)!==0))return u;if((_&4)!==0&&(_|=l&16),u=f.entangledLanes,u!==0)for(f=f.entanglements,u&=_;0l;l++)u.push(f);return u}function A6(f,u,l){f.pendingLanes|=u,u!==536870912&&(f.suspendedLanes=0,f.pingedLanes=0),f=f.eventTimes,u=31-nl(u),f[u]=l}function OV(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 _=f.eventTimes;for(f=f.expirationTimes;0=R3),RU=String.fromCharCode(32),xU=!1;function $W(f,u){switch(f){case"keyup":return sV.indexOf(u.keyCode)!==-1;case"keydown":return u.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function rW(f){return f=f.detail,typeof f==="object"&&"data"in f?f.data:null}var my=!1;function aV(f,u){switch(f){case"compositionend":return rW(u);case"keypress":if(u.which!==32)return null;return xU=!0,RU;case"textInput":return f=u.data,f===RU&&xU?null:f;default:return null}}function dV(f,u){if(my)return f==="compositionend"||!V7&&$W(f,u)?(f=_W(),K8=E7=t1=null,my=!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=_}f:{for(;l;){if(l.nextSibling){l=l.nextSibling;break f}l=l.parentNode}l=void 0}l=hU(l)}}function JW(f,u){return f&&u?f===u?!0:f&&f.nodeType===3?!1:u&&u.nodeType===3?JW(f,u.parentNode):("contains"in f)?f.contains(u):f.compareDocumentPosition?!!(f.compareDocumentPosition(u)&16):!1:!1}function UW(){for(var f=window,u=B8();u instanceof f.HTMLIFrameElement;){try{var l=typeof u.contentWindow.location.href==="string"}catch(_){l=!1}if(l)f=u.contentWindow;else break;u=B8(f.document)}return u}function q7(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 jq(f){var u=UW(),l=f.focusedElem,_=f.selectionRange;if(u!==l&&l&&l.ownerDocument&&JW(l.ownerDocument.documentElement,l)){if(_!==null&&q7(l)){if(u=_.start,f=_.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 y=l.textContent.length,$=Math.min(_.start,y);_=_.end===void 0?$:Math.min(_.end,y),!f.extend&&$>_&&(y=_,_=$,$=y),y=IU(l,$);var r=IU(l,_);y&&r&&(f.rangeCount!==1||f.anchorNode!==y.node||f.anchorOffset!==y.offset||f.focusNode!==r.node||f.focusOffset!==r.offset)&&(u=u.createRange(),u.setStart(y.node,y.offset),f.removeAllRanges(),$>_?(f.addRange(u),f.extend(r.node,r.offset)):(u.setEnd(r.node,r.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,gy=null,n9=null,b3=null,S9=!1;function pU(f,u,l){var _=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;S9||gy==null||gy!==B8(_)||(_=gy,("selectionStart"in _)&&q7(_)?_={start:_.selectionStart,end:_.selectionEnd}:(_=(_.ownerDocument&&_.ownerDocument.defaultView||window).getSelection(),_={anchorNode:_.anchorNode,anchorOffset:_.anchorOffset,focusNode:_.focusNode,focusOffset:_.focusOffset}),b3&&d3(b3,_)||(b3=_,_=n8(n9,"onSelect"),0<_.length&&(u=new H7("onSelect","select",null,u,l),f.push({event:u,listeners:_}),u.target=gy)))}function j8(f,u){var l={};return l[f.toLowerCase()]=u.toLowerCase(),l["Webkit"+f]="webkit"+u,l["Moz"+f]="moz"+u,l}var ky={animationend:j8("Animation","AnimationEnd"),animationiteration:j8("Animation","AnimationIteration"),animationstart:j8("Animation","AnimationStart"),transitionend:j8("Transition","TransitionEnd")},u9={},QW={};q1&&(QW=document.createElement("div").style,("AnimationEvent"in window)||(delete ky.animationend.animation,delete ky.animationiteration.animation,delete ky.animationstart.animation),("TransitionEvent"in window)||delete ky.transitionend.transition);function e8(f){if(u9[f])return u9[f];if(!ky[f])return f;var u=ky[f],l;for(l in u)if(u.hasOwnProperty(l)&&l in QW)return u9[f]=u[l];return f}var WW=e8("animationend"),zW=e8("animationiteration"),GW=e8("animationstart"),KW=e8("transitionend"),NW=new Map,mU="abort auxClick cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" ");function r_(f,u){NW.set(f,u),$y(u,[f])}for(n3=0;n3sy||(f.current=h9[sy],h9[sy]=null,sy--)}function G0(f,u){sy++,h9[sy]=f.current,f.current=u}var $_={},Qu=j_($_),Su=j_(!1),fy=$_;function A$(f,u){var l=f.type.contextTypes;if(!l)return $_;var _=f.stateNode;if(_&&_.__reactInternalMemoizedUnmaskedChildContext===u)return _.__reactInternalMemoizedMaskedChildContext;var y={},$;for($ in l)y[$]=u[$];return _&&(f=f.stateNode,f.__reactInternalMemoizedUnmaskedChildContext=u,f.__reactInternalMemoizedMaskedChildContext=y),y}function Cu(f){return f=f.childContextTypes,f!==null&&f!==void 0}function C8(){E0(Su),E0(Qu)}function aU(f,u,l){if(Qu.current!==$_)throw Error(zf(168));G0(Qu,u),G0(Su,l)}function HW(f,u,l){var _=f.stateNode;if(u=u.childContextTypes,typeof _.getChildContext!=="function")return l;_=_.getChildContext();for(var y in _)if(!(y in u))throw Error(zf(108,lV(f)||"Unknown",y));return B0({},l,_)}function i8(f){return f=(f=f.stateNode)&&f.__reactInternalMemoizedMergedChildContext||$_,fy=Qu.current,G0(Qu,f),G0(Su,Su.current),!0}function dU(f,u,l){var _=f.stateNode;if(!_)throw Error(zf(169));l?(f=HW(f,u,fy),_.__reactInternalMemoizedMergedChildContext=f,E0(Su),E0(Qu),G0(Qu,f)):E0(Su),G0(Su,l)}var E1=null,u2=!1,$9=!1;function OW(f){E1===null?E1=[f]:E1.push(f)}function Nq(f){u2=!0,OW(f)}function A_(){if(!$9&&E1!==null){$9=!0;var f=0,u=r0;try{var l=E1;for(r0=1;f>=r,y-=r,H1=1<<32-nl(u)+y|l<B?(P=w,w=null):P=w.sibling;var h=W(z,w,N[B],E);if(h===null){w===null&&(w=P);break}f&&w&&h.alternate===null&&u(z,w),Z=$(h,Z,B),Y===null?q=h:Y.sibling=h,Y=h,w=P}if(B===N.length)return l(z,w),V0&&k_(z,B),q;if(w===null){for(;BB?(P=w,w=null):P=w.sibling;var M=W(z,w,h.value,E);if(M===null){w===null&&(w=P);break}f&&w&&M.alternate===null&&u(z,w),Z=$(M,Z,B),Y===null?q=M:Y.sibling=M,Y=M,w=P}if(h.done)return l(z,w),V0&&k_(z,B),q;if(w===null){for(;!h.done;B++,h=N.next())h=Q(z,h.value,E),h!==null&&(Z=$(h,Z,B),Y===null?q=h:Y.sibling=h,Y=h);return V0&&k_(z,B),q}for(w=_(z,w);!h.done;B++,h=N.next())h=G(w,z,B,h.value,E),h!==null&&(f&&h.alternate!==null&&w.delete(h.key===null?B:h.key),Z=$(h,Z,B),Y===null?q=h:Y.sibling=h,Y=h);return f&&w.forEach(function(n){return u(z,n)}),V0&&k_(z,B),q}function O(z,Z,N,E){if(typeof N==="object"&&N!==null&&N.type===py&&N.key===null&&(N=N.props.children),typeof N==="object"&&N!==null){switch(N.$$typeof){case f8:f:{for(var q=N.key,Y=Z;Y!==null;){if(Y.key===q){if(q=N.type,q===py){if(Y.tag===7){l(z,Y.sibling),Z=y(Y,N.props.children),Z.return=z,z=Z;break f}}else if(Y.elementType===q||typeof q==="object"&&q!==null&&q.$$typeof===p1&&uQ(q)===Y.type){l(z,Y.sibling),Z=y(Y,N.props),Z.ref=X3(z,Y,N),Z.return=z,z=Z;break f}l(z,Y);break}else u(z,Y);Y=Y.sibling}N.type===py?(Z=e_(N.props.children,z.mode,E,N.key),Z.return=z,z=Z):(E=X8(N.type,N.key,N.props,null,z.mode,E),E.ref=X3(z,Z,N),E.return=z,z=E)}return r(z);case Iy:f:{for(Y=N.key;Z!==null;){if(Z.key===Y)if(Z.tag===4&&Z.stateNode.containerInfo===N.containerInfo&&Z.stateNode.implementation===N.implementation){l(z,Z.sibling),Z=y(Z,N.children||[]),Z.return=z,z=Z;break f}else{l(z,Z);break}else u(z,Z);Z=Z.sibling}Z=W9(N,z.mode,E),Z.return=z,z=Z}return r(z);case p1:return Y=N._init,O(z,Z,Y(N._payload),E)}if(D3(N))return K(z,Z,N,E);if(O3(N))return H(z,Z,N,E);U8(z,N)}return typeof N==="string"&&N!==""||typeof N==="number"?(N=""+N,Z!==null&&Z.tag===6?(l(z,Z.sibling),Z=y(Z,N),Z.return=z,z=Z):(l(z,Z),Z=Q9(N,z.mode,E),Z.return=z,z=Z),r(z)):l(z,Z)}return O}var J$=XW(!0),BW=XW(!1),x8=j_(null),b8=null,dy=null,Y7=null;function w7(){Y7=dy=b8=null}function D7(f){var u=x8.current;E0(x8),f._currentValue=u}function m9(f,u,l){for(;f!==null;){var _=f.alternate;if((f.childLanes&u)!==u?(f.childLanes|=u,_!==null&&(_.childLanes|=u)):_!==null&&(_.childLanes&u)!==u&&(_.childLanes|=u),f===l)break;f=f.return}}function $$(f,u){b8=f,Y7=dy=null,f=f.dependencies,f!==null&&f.firstContext!==null&&((f.lanes&u)!==0&&(nu=!0),f.firstContext=null)}function Zl(f){var u=f._currentValue;if(Y7!==f)if(f={context:f,memoizedValue:u,next:null},dy===null){if(b8===null)throw Error(zf(308));dy=f,b8.dependencies={lanes:0,firstContext:f}}else dy=dy.next=f;return u}var o_=null;function T7(f){o_===null?o_=[f]:o_.push(f)}function YW(f,u,l,_){var y=u.interleaved;return y===null?(l.next=l,T7(u)):(l.next=y.next,y.next=l),u.interleaved=l,X1(f,_)}function X1(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 m1=!1;function M7(f){f.updateQueue={baseState:f.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function wW(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 V1(f,u){return{eventTime:f,lane:u,tag:0,payload:null,callback:null,next:null}}function f_(f,u,l){var _=f.updateQueue;if(_===null)return null;if(_=_.shared,(l0&2)!==0){var y=_.pending;return y===null?u.next=u:(u.next=y.next,y.next=u),_.pending=u,X1(f,l)}return y=_.interleaved,y===null?(u.next=u,T7(_)):(u.next=y.next,y.next=u),_.interleaved=u,X1(f,l)}function E8(f,u,l){if(u=u.updateQueue,u!==null&&(u=u.shared,(l&4194240)!==0)){var _=u.lanes;_&=f.pendingLanes,l|=_,u.lanes=l,K7(f,l)}}function lQ(f,u){var{updateQueue:l,alternate:_}=f;if(_!==null&&(_=_.updateQueue,l===_)){var y=null,$=null;if(l=l.firstBaseUpdate,l!==null){do{var r={eventTime:l.eventTime,lane:l.lane,tag:l.tag,payload:l.payload,callback:l.callback,next:null};$===null?y=$=r:$=$.next=r,l=l.next}while(l!==null);$===null?y=$=u:$=$.next=u}else y=$=u;l={baseState:_.baseState,firstBaseUpdate:y,lastBaseUpdate:$,shared:_.shared,effects:_.effects},f.updateQueue=l;return}f=l.lastBaseUpdate,f===null?l.firstBaseUpdate=u:f.next=u,l.lastBaseUpdate=u}function v8(f,u,l,_){var y=f.updateQueue;m1=!1;var{firstBaseUpdate:$,lastBaseUpdate:r}=y,j=y.shared.pending;if(j!==null){y.shared.pending=null;var A=j,J=A.next;A.next=null,r===null?$=J:r.next=J,r=A;var U=f.alternate;U!==null&&(U=U.updateQueue,j=U.lastBaseUpdate,j!==r&&(j===null?U.firstBaseUpdate=J:j.next=J,U.lastBaseUpdate=A))}if($!==null){var Q=y.baseState;r=0,U=J=A=null,j=$;do{var{lane:W,eventTime:G}=j;if((_&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,H=j;switch(W=u,G=l,H.tag){case 1:if(K=H.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=H.payload,W=typeof K==="function"?K.call(G,Q,W):K,W===null||W===void 0)break f;Q=B0({},Q,W);break f;case 2:m1=!0}}j.callback!==null&&j.lane!==0&&(f.flags|=64,W=y.effects,W===null?y.effects=[j]:W.push(j))}else G={eventTime:G,lane:W,tag:j.tag,payload:j.payload,callback:j.callback,next:null},U===null?(J=U=G,A=Q):U=U.next=G,r|=W;if(j=j.next,j===null)if(j=y.shared.pending,j===null)break;else W=j,j=W.next,W.next=null,y.lastBaseUpdate=W,y.shared.pending=null}while(1);if(U===null&&(A=Q),y.baseState=A,y.firstBaseUpdate=J,y.lastBaseUpdate=U,u=y.shared.interleaved,u!==null){y=u;do r|=y.lane,y=y.next;while(y!==u)}else $===null&&(y.shared.lanes=0);_y|=r,f.lanes=r,f.memoizedState=Q}}function _Q(f,u,l){if(f=u.effects,u.effects=null,f!==null)for(u=0;ul?l:4,f(!0);var _=j9.transition;j9.transition={};try{f(!1),u()}finally{r0=l,j9.transition=_}}function mW(){return El().memoizedState}function Oq(f,u,l){var _=l_(f);if(l={lane:_,action:l,hasEagerState:!1,eagerState:null,next:null},gW(f))kW(u,l);else if(l=YW(f,u,l,_),l!==null){var y=Vu();Sl(l,f,_,y),tW(l,u,_)}}function Vq(f,u,l){var _=l_(f),y={lane:_,action:l,hasEagerState:!1,eagerState:null,next:null};if(gW(f))kW(u,y);else{var $=f.alternate;if(f.lanes===0&&($===null||$.lanes===0)&&($=u.lastRenderedReducer,$!==null))try{var r=u.lastRenderedState,j=$(r,l);if(y.hasEagerState=!0,y.eagerState=j,Cl(j,r)){var A=u.interleaved;A===null?(y.next=y,T7(u)):(y.next=A.next,A.next=y),u.interleaved=y;return}}catch(J){}finally{}l=YW(f,u,y,_),l!==null&&(y=Vu(),Sl(l,f,_,y),tW(l,u,_))}}function gW(f){var u=f.alternate;return f===X0||u!==null&&u===X0}function kW(f,u){v3=I8=!0;var l=f.pending;l===null?u.next=u:(u.next=l.next,l.next=u),f.pending=u}function tW(f,u,l){if((l&4194240)!==0){var _=u.lanes;_&=f.pendingLanes,l|=_,u.lanes=l,K7(f,l)}}var p8={readContext:Zl,useCallback:Fu,useContext:Fu,useEffect:Fu,useImperativeHandle:Fu,useInsertionEffect:Fu,useLayoutEffect:Fu,useMemo:Fu,useReducer:Fu,useRef:Fu,useState:Fu,useDebugValue:Fu,useDeferredValue:Fu,useTransition:Fu,useMutableSource:Fu,useSyncExternalStore:Fu,useId:Fu,unstable_isNewReconciler:!1},qq={readContext:Zl,useCallback:function(f,u){return ol().memoizedState=[f,u===void 0?null:u],f},useContext:Zl,useEffect:$Q,useImperativeHandle:function(f,u,l){return l=l!==null&&l!==void 0?l.concat([f]):null,O8(4194308,4,bW.bind(null,u,f),l)},useLayoutEffect:function(f,u){return O8(4194308,4,f,u)},useInsertionEffect:function(f,u){return O8(4,2,f,u)},useMemo:function(f,u){var l=ol();return u=u===void 0?null:u,f=f(),l.memoizedState=[f,u],f},useReducer:function(f,u,l){var _=ol();return u=l!==void 0?l(u):u,_.memoizedState=_.baseState=u,f={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:f,lastRenderedState:u},_.queue=f,f=f.dispatch=Oq.bind(null,X0,f),[_.memoizedState,f]},useRef:function(f){var u=ol();return f={current:f},u.memoizedState=f},useState:yQ,useDebugValue:x7,useDeferredValue:function(f){return ol().memoizedState=f},useTransition:function(){var f=yQ(!1),u=f[0];return f=Hq.bind(null,f[1]),ol().memoizedState=f,[u,f]},useMutableSource:function(){},useSyncExternalStore:function(f,u,l){var _=X0,y=ol();if(V0){if(l===void 0)throw Error(zf(407));l=l()}else{if(l=u(),o0===null)throw Error(zf(349));(ly&30)!==0||PW(_,u,l)}y.memoizedState=l;var $={value:l,getSnapshot:u};return y.queue=$,$Q(SW.bind(null,_,$,f),[f]),_.flags|=2048,r6(9,nW.bind(null,_,$,l,u),void 0,null),l},useId:function(){var f=ol(),u=o0.identifierPrefix;if(V0){var l=O1,_=H1;l=(_&~(1<<32-nl(_)-1)).toString(32)+l,u=":"+u+"R"+l,l=y6++,0",f=f.removeChild(f.firstChild)):typeof _.is==="string"?f=r.createElement(l,{is:_.is}):(f=r.createElement(l),l==="select"&&(r=f,_.multiple?r.multiple=!0:_.size&&(r.size=_.size))):f=r.createElementNS(f,l),f[al]=u,f[u6]=_,yz(f,u,!1,!1),u.stateNode=f;f:{switch(r=X9(l,_),l){case"dialog":Z0("cancel",f),Z0("close",f),y=_;break;case"iframe":case"object":case"embed":Z0("load",f),y=_;break;case"video":case"audio":for(y=0;yW$&&(u.flags|=128,_=!0,B3($,!1),u.lanes=4194304)}else{if(!_)if(f=h8(r),f!==null){if(u.flags|=128,_=!0,l=f.updateQueue,l!==null&&(u.updateQueue=l,u.flags|=4),B3($,!0),$.tail===null&&$.tailMode==="hidden"&&!r.alternate&&!V0)return Ju(u),null}else 2*S0()-$.renderingStartTime>W$&&l!==1073741824&&(u.flags|=128,_=!0,B3($,!1),u.lanes=4194304);$.isBackwards?(r.sibling=u.child,u.child=r):(l=$.last,l!==null?l.sibling=r:u.child=r,$.last=r)}if($.tail!==null)return u=$.tail,$.rendering=u,$.tail=u.sibling,$.renderingStartTime=S0(),u.sibling=null,l=L0.current,G0(L0,_?l&1|2:l&1),u;return Ju(u),null;case 22:case 23:return m7(),_=u.memoizedState!==null,f!==null&&f.memoizedState!==null!==_&&(u.flags|=8192),_&&(u.mode&1)!==0?(ou&1073741824)!==0&&(Ju(u),u.subtreeFlags&6&&(u.flags|=8192)):Ju(u),null;case 24:return null;case 25:return null}throw Error(zf(156,u.tag))}function Mq(f,u){switch(X7(u),u.tag){case 1:return Cu(u.type)&&C8(),f=u.flags,f&65536?(u.flags=f&-65537|128,u):null;case 3:return U$(),E0(Su),E0(Qu),S7(),f=u.flags,(f&65536)!==0&&(f&128)===0?(u.flags=f&-65537|128,u):null;case 5:return n7(u),null;case 13:if(E0(L0),f=u.memoizedState,f!==null&&f.dehydrated!==null){if(u.alternate===null)throw Error(zf(340));F$()}return f=u.flags,f&65536?(u.flags=f&-65537|128,u):null;case 19:return E0(L0),null;case 4:return U$(),null;case 10:return D7(u.type._context),null;case 22:case 23:return m7(),null;case 24:return null;default:return null}}var W8=!1,Uu=!1,Pq=typeof WeakSet==="function"?WeakSet:Set,Vf=null;function ey(f,u){var l=f.ref;if(l!==null)if(typeof l==="function")try{l(null)}catch(_){w0(f,u,_)}else l.current=null}function f7(f,u,l){try{l()}catch(_){w0(f,u,_)}}var KQ=!1;function nq(f,u){if(c9=M8,f=UW(),q7(f)){if("selectionStart"in f)var l={start:f.selectionStart,end:f.selectionEnd};else f:{l=(l=f.ownerDocument)&&l.defaultView||window;var _=l.getSelection&&l.getSelection();if(_&&_.rangeCount!==0){l=_.anchorNode;var{anchorOffset:y,focusNode:$}=_;_=_.focusOffset;try{l.nodeType,$.nodeType}catch(E){l=null;break f}var r=0,j=-1,A=-1,J=0,U=0,Q=f,W=null;u:for(;;){for(var G;;){if(Q!==l||y!==0&&Q.nodeType!==3||(j=r+y),Q!==$||_!==0&&Q.nodeType!==3||(A=r+_),Q.nodeType===3&&(r+=Q.nodeValue.length),(G=Q.firstChild)===null)break;W=Q,Q=G}for(;;){if(Q===f)break u;if(W===l&&++J===y&&(j=r),W===$&&++U===_&&(A=r),(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;R9={focusedElem:f,selectionRange:l},M8=!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:H,memoizedState:O}=K,z=u.stateNode,Z=z.getSnapshotBeforeUpdate(u.elementType===u.type?H:Tl(u.type,H),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(zf(163))}}catch(E){w0(u,u.return,E)}if(f=u.sibling,f!==null){f.return=u.return,Vf=f;break}Vf=u.return}return K=KQ,KQ=!1,K}function h3(f,u,l){var _=u.updateQueue;if(_=_!==null?_.lastEffect:null,_!==null){var y=_=_.next;do{if((y.tag&f)===f){var $=y.destroy;y.destroy=void 0,$!==void 0&&f7(u,l,$)}y=y.next}while(y!==_)}}function y2(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 _=l.create;l.destroy=_()}l=l.next}while(l!==u)}}function u7(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 jz(f){var u=f.alternate;u!==null&&(f.alternate=null,jz(u)),f.child=null,f.deletions=null,f.sibling=null,f.tag===5&&(u=f.stateNode,u!==null&&(delete u[al],delete u[u6],delete u[v9],delete u[Gq],delete u[Kq])),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 Az(f){return f.tag===5||f.tag===3||f.tag===4}function NQ(f){f:for(;;){for(;f.sibling===null;){if(f.return===null||Az(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 l7(f,u,l){var _=f.tag;if(_===5||_===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=S8));else if(_!==4&&(f=f.child,f!==null))for(l7(f,u,l),f=f.sibling;f!==null;)l7(f,u,l),f=f.sibling}function _7(f,u,l){var _=f.tag;if(_===5||_===6)f=f.stateNode,u?l.insertBefore(f,u):l.appendChild(f);else if(_!==4&&(f=f.child,f!==null))for(_7(f,u,l),f=f.sibling;f!==null;)_7(f,u,l),f=f.sibling}var uu=null,Ml=!1;function I1(f,u,l){for(l=l.child;l!==null;)Fz(f,u,l),l=l.sibling}function Fz(f,u,l){if(dl&&typeof dl.onCommitFiberUnmount==="function")try{dl.onCommitFiberUnmount(o8,l)}catch(j){}switch(l.tag){case 5:Uu||ey(l,u);case 6:var _=uu,y=Ml;uu=null,I1(f,u,l),uu=_,Ml=y,uu!==null&&(Ml?(f=uu,l=l.stateNode,f.nodeType===8?f.parentNode.removeChild(l):f.removeChild(l)):uu.removeChild(l.stateNode));break;case 18:uu!==null&&(Ml?(f=uu,l=l.stateNode,f.nodeType===8?y9(f.parentNode,l):f.nodeType===1&&y9(f,l),o3(f)):y9(uu,l.stateNode));break;case 4:_=uu,y=Ml,uu=l.stateNode.containerInfo,Ml=!0,I1(f,u,l),uu=_,Ml=y;break;case 0:case 11:case 14:case 15:if(!Uu&&(_=l.updateQueue,_!==null&&(_=_.lastEffect,_!==null))){y=_=_.next;do{var $=y,r=$.destroy;$=$.tag,r!==void 0&&(($&2)!==0?f7(l,u,r):($&4)!==0&&f7(l,u,r)),y=y.next}while(y!==_)}I1(f,u,l);break;case 1:if(!Uu&&(ey(l,u),_=l.stateNode,typeof _.componentWillUnmount==="function"))try{_.props=l.memoizedProps,_.state=l.memoizedState,_.componentWillUnmount()}catch(j){w0(l,u,j)}I1(f,u,l);break;case 21:I1(f,u,l);break;case 22:l.mode&1?(Uu=(_=Uu)||l.memoizedState!==null,I1(f,u,l),Uu=_):I1(f,u,l);break;default:I1(f,u,l)}}function ZQ(f){var u=f.updateQueue;if(u!==null){f.updateQueue=null;var l=f.stateNode;l===null&&(l=f.stateNode=new Pq),u.forEach(function(_){var y=hq.bind(null,f,_);l.has(_)||(l.add(_),_.then(y,y))})}}function Dl(f,u){var l=u.deletions;if(l!==null)for(var _=0;_y&&(y=r),_&=~$}if(_=y,_=S0()-_,_=(120>_?120:480>_?480:1080>_?1080:1920>_?1920:3000>_?3000:4320>_?4320:1960*Cq(_/1960))-_,10<_){f.timeoutHandle=b9(t_.bind(null,f,Pu,Z1),_);break}t_(f,Pu,Z1);break;case 5:t_(f,Pu,Z1);break;default:throw Error(zf(329))}}}return iu(f,S0()),f.callbackNode===l?Qz.bind(null,f):null}function r7(f,u){var l=I3;return f.current.memoizedState.isDehydrated&&(d_(f,u).flags|=256),f=t8(f,u),f!==2&&(u=Pu,Pu=l,u!==null&&j7(u)),f}function j7(f){Pu===null?Pu=f:Pu.push.apply(Pu,f)}function iq(f){for(var u=f;;){if(u.flags&16384){var l=u.updateQueue;if(l!==null&&(l=l.stores,l!==null))for(var _=0;_f?16:f,s1===null)var _=!1;else{if(f=s1,s1=null,k8=0,(l0&6)!==0)throw Error(zf(331));var y=l0;l0|=4;for(Vf=f.current;Vf!==null;){var $=Vf,r=$.child;if((Vf.flags&16)!==0){var j=$.deletions;if(j!==null){for(var A=0;AS0()-I7?d_(f,0):h7|=l),iu(f,u)}function Nz(f,u){u===0&&((f.mode&1)===0?u=1:(u=y8,y8<<=1,(y8&130023424)===0&&(y8=4194304)));var l=Vu();f=X1(f,u),f!==null&&(A6(f,u,l),iu(f,l))}function vq(f){var u=f.memoizedState,l=0;u!==null&&(l=u.retryLane),Nz(f,l)}function hq(f,u){var l=0;switch(f.tag){case 13:var{stateNode:_,memoizedState:y}=f;y!==null&&(l=y.retryLane);break;case 19:_=f.stateNode;break;default:throw Error(zf(314))}_!==null&&_.delete(u),Nz(f,l)}var Zz;Zz=function(f,u,l){if(f!==null)if(f.memoizedProps!==u.pendingProps||Su.current)nu=!0;else{if((f.lanes&l)===0&&(u.flags&128)===0)return nu=!1,Dq(f,u,l);nu=(f.flags&131072)!==0?!0:!1}else nu=!1,V0&&(u.flags&1048576)!==0&&VW(u,R8,u.index);switch(u.lanes=0,u.tag){case 2:var _=u.type;V8(f,u),f=u.pendingProps;var y=A$(u,Qu.current);$$(u,l),y=i7(null,u,_,f,y,l);var $=c7();return u.flags|=1,typeof y==="object"&&y!==null&&typeof y.render==="function"&&y.$$typeof===void 0?(u.tag=1,u.memoizedState=null,u.updateQueue=null,Cu(_)?($=!0,i8(u)):$=!1,u.memoizedState=y.state!==null&&y.state!==void 0?y.state:null,M7(u),y.updater=_2,u.stateNode=y,y._reactInternals=u,k9(u,_,f,l),u=o9(null,u,_,!0,$,l)):(u.tag=0,V0&&$&&L7(u),Ou(null,u,y,l),u=u.child),u;case 16:_=u.elementType;f:{switch(V8(f,u),f=u.pendingProps,y=_._init,_=y(_._payload),u.type=_,y=u.tag=pq(_),f=Tl(_,f),y){case 0:u=s9(null,u,_,f,l);break f;case 1:u=WQ(null,u,_,f,l);break f;case 11:u=UQ(null,u,_,f,l);break f;case 14:u=QQ(null,u,_,Tl(_.type,f),l);break f}throw Error(zf(306,_,""))}return u;case 0:return _=u.type,y=u.pendingProps,y=u.elementType===_?y:Tl(_,y),s9(f,u,_,y,l);case 1:return _=u.type,y=u.pendingProps,y=u.elementType===_?y:Tl(_,y),WQ(f,u,_,y,l);case 3:f:{if(uz(u),f===null)throw Error(zf(387));_=u.pendingProps,$=u.memoizedState,y=$.element,wW(f,u),v8(u,_,null,l);var r=u.memoizedState;if(_=r.element,$.isDehydrated)if($={element:_,isDehydrated:!1,cache:r.cache,pendingSuspenseBoundaries:r.pendingSuspenseBoundaries,transitions:r.transitions},u.updateQueue.baseState=$,u.memoizedState=$,u.flags&256){y=Q$(Error(zf(423)),u),u=zQ(f,u,_,l,y);break f}else if(_!==y){y=Q$(Error(zf(424)),u),u=zQ(f,u,_,l,y);break f}else for(au=e1(u.stateNode.containerInfo.firstChild),du=u,V0=!0,Pl=null,l=BW(u,null,_,l),u.child=l;l;)l.flags=l.flags&-3|4096,l=l.sibling;else{if(F$(),_===y){u=B1(f,u,l);break f}Ou(f,u,_,l)}u=u.child}return u;case 5:return DW(u),f===null&&p9(u),_=u.type,y=u.pendingProps,$=f!==null?f.memoizedProps:null,r=y.children,x9(_,y)?r=null:$!==null&&x9(_,$)&&(u.flags|=32),fz(f,u),Ou(f,u,r,l),u.child;case 6:return f===null&&p9(u),null;case 13:return lz(f,u,l);case 4:return P7(u,u.stateNode.containerInfo),_=u.pendingProps,f===null?u.child=J$(u,null,_,l):Ou(f,u,_,l),u.child;case 11:return _=u.type,y=u.pendingProps,y=u.elementType===_?y:Tl(_,y),UQ(f,u,_,y,l);case 7:return Ou(f,u,u.pendingProps,l),u.child;case 8:return Ou(f,u,u.pendingProps.children,l),u.child;case 12:return Ou(f,u,u.pendingProps.children,l),u.child;case 10:f:{if(_=u.type._context,y=u.pendingProps,$=u.memoizedProps,r=y.value,G0(x8,_._currentValue),_._currentValue=r,$!==null)if(Cl($.value,r)){if($.children===y.children&&!Su.current){u=B1(f,u,l);break f}}else for($=u.child,$!==null&&($.return=u);$!==null;){var j=$.dependencies;if(j!==null){r=$.child;for(var A=j.firstContext;A!==null;){if(A.context===_){if($.tag===1){A=V1(-1,l&-l),A.tag=2;var J=$.updateQueue;if(J!==null){J=J.shared;var U=J.pending;U===null?A.next=A:(A.next=U.next,U.next=A),J.pending=A}}$.lanes|=l,A=$.alternate,A!==null&&(A.lanes|=l),m9($.return,l,u),j.lanes|=l;break}A=A.next}}else if($.tag===10)r=$.type===u.type?null:$.child;else if($.tag===18){if(r=$.return,r===null)throw Error(zf(341));r.lanes|=l,j=r.alternate,j!==null&&(j.lanes|=l),m9(r,l,u),r=$.sibling}else r=$.child;if(r!==null)r.return=$;else for(r=$;r!==null;){if(r===u){r=null;break}if($=r.sibling,$!==null){$.return=r.return,r=$;break}r=r.return}$=r}Ou(f,u,y.children,l),u=u.child}return u;case 9:return y=u.type,_=u.pendingProps.children,$$(u,l),y=Zl(y),_=_(y),u.flags|=1,Ou(f,u,_,l),u.child;case 14:return _=u.type,y=Tl(_,u.pendingProps),y=Tl(_.type,y),QQ(f,u,_,y,l);case 15:return dW(f,u,u.type,u.pendingProps,l);case 17:return _=u.type,y=u.pendingProps,y=u.elementType===_?y:Tl(_,y),V8(f,u),u.tag=1,Cu(_)?(f=!0,i8(u)):f=!1,$$(u,l),sW(u,_,y),k9(u,_,y,l),o9(null,u,_,!0,f,l);case 19:return _z(f,u,l);case 22:return eW(f,u,l)}throw Error(zf(156,u.tag))};function Ez(f,u){return gQ(f,u)}function Iq(f,u,l,_){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=_,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Kl(f,u,l,_){return new Iq(f,u,l,_)}function k7(f){return f=f.prototype,!(!f||!f.isReactComponent)}function pq(f){if(typeof f==="function")return k7(f)?1:0;if(f!==void 0&&f!==null){if(f=f.$$typeof,f===Q7)return 11;if(f===W7)return 14}return 2}function __(f,u){var l=f.alternate;return l===null?(l=Kl(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 X8(f,u,l,_,y,$){var r=2;if(_=f,typeof f==="function")k7(f)&&(r=1);else if(typeof f==="string")r=5;else f:switch(f){case py:return e_(l.children,y,$,u);case U7:r=8,y|=8;break;case G9:return f=Kl(12,l,u,y|2),f.elementType=G9,f.lanes=$,f;case K9:return f=Kl(13,l,u,y),f.elementType=K9,f.lanes=$,f;case N9:return f=Kl(19,l,u,y),f.elementType=N9,f.lanes=$,f;case DQ:return r2(l,y,$,u);default:if(typeof f==="object"&&f!==null)switch(f.$$typeof){case YQ:r=10;break f;case wQ:r=9;break f;case Q7:r=11;break f;case W7:r=14;break f;case p1:r=16,_=null;break f}throw Error(zf(130,f==null?f:typeof f,""))}return u=Kl(r,l,u,y),u.elementType=f,u.type=_,u.lanes=$,u}function e_(f,u,l,_){return f=Kl(7,f,_,u),f.lanes=l,f}function r2(f,u,l,_){return f=Kl(22,f,_,u),f.elementType=DQ,f.lanes=l,f.stateNode={isHidden:!1},f}function Q9(f,u,l){return f=Kl(6,f,null,u),f.lanes=l,f}function W9(f,u,l){return u=Kl(4,f.children!==null?f.children:[],f.key,u),u.lanes=l,u.stateNode={containerInfo:f.containerInfo,pendingChildren:null,implementation:f.implementation},u}function mq(f,u,l,_,y){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=ar(0),this.expirationTimes=ar(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=ar(0),this.identifierPrefix=_,this.onRecoverableError=y,this.mutableSourceEagerHydrationData=null}function t7(f,u,l,_,y,$,r,j,A){return f=new mq(f,u,l,j,A),u===1?(u=1,$===!0&&(u|=8)):u=0,$=Kl(3,null,null,u),f.current=$,$.stateNode=f,$.memoizedState={element:_,isDehydrated:l,cache:null,transitions:null,pendingSuspenseBoundaries:null},M7($),f}function gq(f,u,l){var _=3{function Lz(){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=="function")return;try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Lz)}catch(f){console.error(f)}}Lz(),Xz.exports=qz()});var Yz=su((e7)=>{var Bz=d7();e7.createRoot=Bz.createRoot,e7.hydrateRoot=Bz.hydrateRoot;var aq});var KK=su((g2)=>{var pB=O0(),mB=Symbol.for("react.element"),gB=Symbol.for("react.fragment"),kB=Object.prototype.hasOwnProperty,tB=pB.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,sB={key:!0,ref:!0,__self:!0,__source:!0};function GK(f,u,l){var _,y={},$=null,r=null;l!==void 0&&($=""+l),u.key!==void 0&&($=""+u.key),u.ref!==void 0&&(r=u.ref);for(_ in u)kB.call(u,_)&&!sB.hasOwnProperty(_)&&(y[_]=u[_]);if(f&&f.defaultProps)for(_ in u=f.defaultProps,u)y[_]===void 0&&(y[_]=u[_]);return{$$typeof:mB,type:f,key:$,ref:r,props:y,_owner:tB.current}}g2.Fragment=gB;g2.jsx=GK;g2.jsxs=GK});var ZK=su((Di,NK)=>{NK.exports=KK()});var sN=su((tN)=>{var s$=O0();function QT(f,u){return f===u&&(f!==0||1/f===1/u)||f!==f&&u!==u}var WT=typeof Object.is==="function"?Object.is:QT,zT=s$.useState,GT=s$.useEffect,KT=s$.useLayoutEffect,NT=s$.useDebugValue;function ZT(f,u){var l=u(),_=zT({inst:{value:l,getSnapshot:u}}),y=_[0].inst,$=_[1];return KT(function(){y.value=l,y.getSnapshot=u,IF(y)&&$({inst:y})},[f,l,u]),GT(function(){return IF(y)&&$({inst:y}),f(function(){IF(y)&&$({inst:y})})},[f]),NT(l),l}function IF(f){var u=f.getSnapshot;f=f.value;try{var l=u();return!WT(f,l)}catch(_){return!0}}function ET(f,u){return u()}var HT=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?ET:ZT;tN.useSyncExternalStore=s$.useSyncExternalStore!==void 0?s$.useSyncExternalStore:HT});var aN=su((Vh,oN)=>{oN.exports=sN()});var eN=su((dN)=>{var x5=O0(),OT=aN();function VT(f,u){return f===u&&(f!==0||1/f===1/u)||f!==f&&u!==u}var qT=typeof Object.is==="function"?Object.is:VT,LT=OT.useSyncExternalStore,XT=x5.useRef,BT=x5.useEffect,YT=x5.useMemo,wT=x5.useDebugValue;dN.useSyncExternalStoreWithSelector=function(f,u,l,_,y){var $=XT(null);if($.current===null){var r={hasValue:!1,value:null};$.current=r}else r=$.current;$=YT(function(){function A(G){if(!J){if(J=!0,U=G,G=_(G),y!==void 0&&r.hasValue){var K=r.value;if(y(K,G))return Q=K}return Q=G}if(K=Q,qT(U,G))return K;var H=_(G);if(y!==void 0&&y(K,H))return U=G,K;return U=G,Q=H}var J=!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]);var j=LT(f,$[0],$[1]);return BT(function(){r.hasValue=!0,r.value=j},[j]),wT(j),j}});var uZ=su((Lh,fZ)=>{fZ.exports=eN()});var i_=cf(O0(),1);var g4="北京时间";var gO={timeZone:"Asia/Shanghai",hour12:!1},kO={timeZone:"Asia/Shanghai",hour12:!1},tO=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 Cr(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 JU(f){let u=Cr(f);if(!u)return null;return tO.formatToParts(u).reduce((l,_)=>{if(_.type!=="literal")l[_.type]=_.value;return l},{})}function Nf(f){let u=Cr(f);return u?u.toLocaleString("zh-CN",gO):"--"}function W0(f){let u=Cr(f);return u?u.toLocaleTimeString("zh-CN",kO):"--"}function ir(f){let u=JU(f);if(!u)return"";let l=u.hour==="24"?"00":u.hour;return`${u.year}-${u.month}-${u.day}T${l}:${u.minute}`}function UU(f=new Date){let u=JU(f);if(!u)return"";return`${u.year}-${u.month}-${u.day}`}function QU(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(_)-1,Number(y),Number($)-8,Number(r),Number(j)),J=new Date(A),U=ir(J);return Number.isNaN(J.getTime())||U!==`${l}-${_}-${y}T${$}:${r}`?null:J.toISOString()}var wH=cf(Yz(),1);var W2=cf(O0(),1);var wz=cf(O0(),1),Q6=wz.default.createElement;function dq({active:f=!0,label:u="正在加载"}){if(!f)return null;return Q6("span",{className:"loading-spinner-indicator",role:"status","aria-label":u,title:u,"data-testid":"loading-title-indicator"},Q6("span",{className:"loading-spinner-ring","aria-hidden":!0}))}function j0({title:f,children:u,loading:l,level:_=2,className:y="",label:$="正在加载"}){return Q6(_===3?"h3":"h2",{className:`loading-title ${l?"is-loading":""} ${y}`.trim()},Q6("span",{className:"loading-title-text"},u??f),Q6(dq,{active:Boolean(l),label:$}))}class K$ extends Error{unideskRequestError=!0;meta;constructor(f,u){super(f);this.name="UniDeskRequestError",this.meta=u}}function eq(f){return new Promise((u)=>setTimeout(u,f))}function G6(f,u="操作失败"){return f instanceof Error?f.message:String(f||u)}function U2(f,u=500){if(f===null||f===void 0)return"";let l=typeof f==="string"?f:JSON.stringify(f),_=String(l||"").replace(/\s+/gu," ").trim();return _.length>u?`${_.slice(0,u)}...`:_}function fL(f){try{let u=typeof location<"u"&&location.origin?location.origin:"http://localhost";return new URL(f,u).toString()}catch{return f}}function Dz(f){return String(f.method||"GET").toUpperCase()}function uL(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 Tz(f){let u=new Headers(f.headers||{}),l=uL(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 Mz(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 lL(f,u){if(!f||typeof f!=="object"||Array.isArray(f))return!1;return u.some((l)=>l!==!1&&f[l]===!1)}function W6(f,u,l,_,y={}){return{kind:f,method:l,url:fL(u),occurredAt:_.toISOString(),...y}}function z6(f,u){if(!f)return"请求失败";return`HTTP ${f}${u?` ${u}`:""}`}function Pz(f){try{return{body:f?JSON.parse(f):null,parseError:""}}catch(u){return{body:{text:f},parseError:G6(u,"parse failed")}}}async function Mf(f,u={},l=0){let{failureFields:_=["ok"],strictJson:y=!1,retryInvalidJson:$=0,retryDelayMs:r=120,invalidJsonPrefix:j="服务返回了无效 JSON",invalidJsonPreview:A=!1,responsePreviewLength:J=500,...U}=u,Q=Dz(U),W=new Date,G;try{G=await fetch(f,Tz(U))}catch(O){let z=G6(O,"网络请求失败");throw new K$(z,W6("network",f,Q,W,{upstreamMessage:z}))}let K=await G.text(),H=Pz(K);if(H.parseError){if(y&&Q==="GET"&&l<$)return await eq(r),Mf(f,u,l+1);if(y){let O=A?`;响应预览:${U2(K,180)}`:"";throw new K$(`${j}(${K.length} bytes):${H.parseError}${O}`,W6("parse",f,Q,W,{status:G.status,statusText:G.statusText,parseError:H.parseError,responsePreview:U2(K,J)}))}}if(!G.ok||lL(H.body,_)){let O=Mz(H.body),z=O||z6(G.status,G.statusText);throw new K$(z,W6("http",f,Q,W,{status:G.status,statusText:G.statusText,upstreamMessage:O,responsePreview:U2(H.parseError?K:H.body,J)}))}return H.body}async function nz(f,u={}){let l=Dz(u),_=new Date,y;try{y=await fetch(f,Tz(u))}catch(J){let U=G6(J,"网络请求失败");throw new K$(U,W6("network",f,l,_,{upstreamMessage:U}))}if(y.ok)return y.blob();let $=await y.text(),r=Pz($),j=Mz(r.body),A=j||z6(y.status,y.statusText);throw new K$(A,W6("http",f,l,_,{status:y.status,statusText:y.statusText,upstreamMessage:j,responsePreview:U2(r.parseError?$:r.body),parseError:r.parseError||void 0}))}function Sz(f){return Boolean(f&&typeof f==="object"&&f.unideskRequestError===!0&&f.meta)}function _L(f){if(!f)return"";let u=new Date(f);if(Number.isNaN(u.getTime()))return f;return`${Nf(u)} ${g4}`}function fj(f,u="操作失败"){if(Sz(f)){let y=f.meta.kind==="parse"?"响应解析失败":f.meta.kind==="network"?"网络请求失败":f.meta.status&&(f.meta.status<200||f.meta.status>=300)?z6(f.meta.status,f.meta.statusText):"应用请求失败",$=f.meta.status?z6(f.meta.status):"",r=(A)=>!A||A===y||A===$,j=!r(f.message)?f.message:r(f.meta.upstreamMessage)?"":f.meta.upstreamMessage||"";return{title:y,message:j,status:f.meta.status,statusText:f.meta.statusText,method:f.meta.method,url:f.meta.url,occurredAt:_L(f.meta.occurredAt),responsePreview:f.meta.responsePreview,parseError:f.meta.parseError,structured:!0}}let _=G6(f,u).split(/\r?\n/u);return{title:_[0]||u,message:_.slice(1).join(` +`),structured:_.length>1}}function yL(f,u="操作失败"){let l=fj(f,u),_=[l.title];if(l.message)_.push(`原因: ${l.message}`);if(l.method||l.url)_.push(`请求: ${[l.method,l.url].filter(Boolean).join(" ")}`);if(l.status)_.push(`状态: ${z6(l.status,l.statusText)}`);if(l.occurredAt)_.push(`时间: ${l.occurredAt}`);if(l.parseError)_.push(`解析错误: ${l.parseError}`);if(l.responsePreview&&l.responsePreview!==l.message)_.push(`响应预览: ${l.responsePreview}`);return _.filter(Boolean).join(` +`)}function Df(f,u="操作失败"){return Sz(f)?yL(f,u):G6(f,u)}var Cz=cf(O0(),1);var F_=Cz.default.createElement;function K6(f,u){return u?[F_("dt",{key:`${f}-label`},f),F_("dd",{key:f},u)]:null}function A0({error:f,wide:u=!1,fallback:l="操作失败",className:_=""}){if(!f)return null;let y=fj(f,l),$=[K6("请求",[y.method,y.url].filter(Boolean).join(" ")),K6("状态",y.status?`HTTP ${y.status}${y.statusText?` ${y.statusText}`:""}`:""),K6("时间",y.occurredAt),K6("解析错误",y.parseError),K6("响应预览",y.responsePreview)].filter(Boolean);return F_("div",{className:`form-error unidesk-error${u?" wide":""}${_?` ${_}`:""}`,role:"alert","data-testid":"unidesk-error"},F_("div",{className:"unidesk-error-title"},F_("strong",null,y.title),y.status?F_("span",{className:"unidesk-error-code"},`HTTP ${y.status}`):null),y.message?F_("pre",{className:"unidesk-error-message"},y.message):null,$.length>0?F_("dl",{className:"unidesk-error-details"},$):null)}var f1=cf(O0(),1),iz=f1.default.createContext(null);function cz({children:f}){let[u,l]=f1.default.useState([]),[_,y]=f1.default.useState(Date.now()),$=f1.default.useCallback((Q,W)=>{let K={id:`notif_${Date.now()}_${Math.random().toString(36).slice(2,8)}`,type:Q,message:W,timestamp:Date.now()};l((H)=>{let O=[...H,K];if(O.length>50)return O.slice(-50);return O})},[]),r=f1.default.useCallback((Q)=>{l((W)=>W.filter((G)=>G.id!==Q))},[]),j=f1.default.useCallback(()=>{l([]),y(Date.now())},[]),A=f1.default.useMemo(()=>{return u.filter((Q)=>Q.timestamp>_).length},[u,_]),J=A>0,U={notifications:u,addNotification:$,removeNotification:r,clearNotifications:j,unreadCount:A,hasUnread:J};return $L(iz.Provider,{value:U},f)}var $L=f1.default.createElement;function Lu(){let f=f1.default.useContext(iz);if(!f)throw Error("useNotification must be used within NotificationProvider");return f}var b=W2.default.createElement,{useEffect:Q2}=W2.default,N$=W2.default.useState;function Wu(f,u={}){return Mf(f,{failureFields:["ok","success"],...u})}function Xu(f,u){return`${f}/microservices/baidu-netdisk/proxy${u}`}function rL(f){let u=Number(f);return Number.isFinite(u)?u.toLocaleString("zh-CN"):"--"}function J_(f){let u=Number(f);if(!Number.isFinite(u)||u<=0)return"--";let l=["B","KB","MB","GB","TB"],_=u,y=0;while(_>=1024&&y{y?.stopPropagation?.(),l(f,u)}},"查看原始JSON")}function E$({title:f,text:u}){return b("div",{className:"empty-state"},b("strong",null,f),b("span",null,u))}function H$({title:f,text:u,href:l,badge:_,testId:y}){return b("a",{className:"doc-link-card",href:l,target:"_blank",rel:"noreferrer","data-testid":y},b("span",null,_||"DOC"),b("strong",null,f),b("p",null,u),b("code",null,l))}function jL(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function AL(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function FL(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function JL(f){return Array.isArray(f?.files)?f.files:[]}function UL(f){return Array.isArray(f?.jobs)?f.jobs:[]}function QL(f,u){if(!f||f===u)return u;let l=f.replace(/\/+$/u,""),_=l.slice(0,l.lastIndexOf("/"))||u;return _.lengthI.id==="baidu-netdisk")||null,[y,$]=N$({loading:!1,actionLoading:!1,error:"",message:"",health:null,account:null,files:null,transfers:null,logs:null,selfTest:null,refreshedAt:null}),[r,j]=N$("/"),[A,J]=N$(null),[U,Q]=N$(""),[W,G]=N$({localPath:"sample.txt",remotePath:"/sample.txt"}),[K,H]=N$({fsId:"",localPath:"downloads/"}),{addNotification:O}=Lu(),z=y.health?.baidu?.appRoot||y.account?.rootPath||"/";Q2(()=>{G((I)=>{let ff=new Set(["/sample.txt","/apps/UniDeskBaiduNetdisk/sample.txt"]);if(I.remotePath&&!ff.has(I.remotePath))return I;let yf=uj(z,"sample.txt");return I.remotePath===yf?I:{...I,remotePath:yf}})},[z]);async function Z(I=r){let yf=await Wu(Xu(l,`/api/files?dir=${encodeURIComponent(I||z)}&limit=100`));$((rf)=>({...rf,files:yf}))}async function N(){let I=await Wu(Xu(l,"/api/transfers?limit=80"));$((ff)=>({...ff,transfers:I}))}async function E(){if(!_)return;$((I)=>({...I,loading:!0,error:"",message:""}));try{let I=await Wu(`${l}/microservices/baidu-netdisk/health`),ff=I?.baidu?.appRoot||z,yf=null,rf=null;if(I?.auth?.loggedIn){yf=await Wu(Xu(l,"/api/account?refresh=1"));let Gf=r&&r.startsWith(ff)?r:ff;j(Gf),rf=await Wu(Xu(l,`/api/files?dir=${encodeURIComponent(Gf)}&limit=100`))}else j(ff);let Wf=await Wu(Xu(l,"/api/transfers?limit=80")),Ef=await Wu(Xu(l,"/logs?limit=60"));$((Gf)=>({...Gf,loading:!1,health:I,account:yf?.account||null,files:rf,transfers:Wf,logs:Ef,refreshedAt:new Date}))}catch(I){$((ff)=>({...ff,loading:!1,error:Df(I,"百度网盘服务加载失败")}))}}async function q(){$((I)=>({...I,actionLoading:!0,error:"",message:""}));try{let I=await Wu(Xu(l,"/api/auth/device/start"),{method:"POST",body:{}});J(I.session||null),$((ff)=>({...ff,actionLoading:!1,message:"设备码已生成,请扫码授权"}))}catch(I){$((ff)=>({...ff,actionLoading:!1,error:Df(I,"创建设备码失败")}))}}async function Y(I=!1){if(!A?.id)return;if(I)$((ff)=>({...ff,actionLoading:!0,error:""}));try{let ff=await Wu(Xu(l,`/api/auth/device/status?sessionId=${encodeURIComponent(A.id)}`));if(J(ff.session||null),ff.session?.status==="succeeded")$((yf)=>({...yf,actionLoading:!1,message:"授权成功,正在刷新账号与文件列表"})),await E();else if(I)$((yf)=>({...yf,actionLoading:!1}))}catch(ff){$((yf)=>({...yf,actionLoading:!1,error:Df(ff,"轮询登录状态失败")}))}}async function w(){$((I)=>({...I,actionLoading:!0,error:"",message:""}));try{await Wu(Xu(l,"/api/auth/logout"),{method:"POST",body:{}}),J(null),$((I)=>({...I,actionLoading:!1,account:null,files:null,message:"本地 token 已清除"})),await E()}catch(I){$((ff)=>({...ff,actionLoading:!1,error:Df(I,"退出登录失败")}))}}async function B(I){I.preventDefault();let ff=U.trim();if(!ff)return;$((yf)=>({...yf,actionLoading:!0,error:"",message:""}));try{await Wu(Xu(l,"/api/folders"),{method:"POST",body:{path:uj(r,ff)}}),Q(""),$((yf)=>({...yf,actionLoading:!1,message:"文件夹已创建"})),await Z(r)}catch(yf){$((rf)=>({...rf,actionLoading:!1,error:Df(yf,"创建文件夹失败")}))}}async function P(I){if(!I)return;$((ff)=>({...ff,actionLoading:!0,error:"",message:""}));try{await Wu(Xu(l,"/api/files/manage"),{method:"POST",body:{opera:"delete",filelist:[{path:I}],async:1}}),$((ff)=>({...ff,actionLoading:!1,message:"删除任务已提交"})),await Z(r)}catch(ff){$((yf)=>({...yf,actionLoading:!1,error:Df(ff,"删除失败")}))}}async function h(I){I.preventDefault(),$((ff)=>({...ff,actionLoading:!0,error:"",message:""}));try{await Wu(Xu(l,"/api/transfers/upload-from-path"),{method:"POST",body:W}),$((ff)=>({...ff,actionLoading:!1,message:"上传任务已入队"})),await N()}catch(ff){$((yf)=>({...yf,actionLoading:!1,error:Df(ff,"上传任务创建失败")}))}}async function M(I){I.preventDefault(),$((ff)=>({...ff,actionLoading:!0,error:"",message:""}));try{await Wu(Xu(l,"/api/transfers/download-to-path"),{method:"POST",body:K}),$((ff)=>({...ff,actionLoading:!1,message:"下载任务已入队"})),await N()}catch(ff){$((yf)=>({...yf,actionLoading:!1,error:Df(ff,"下载任务创建失败")}))}}async function n(I,ff){$((yf)=>({...yf,actionLoading:!0,error:"",message:""}));try{await Wu(Xu(l,`/api/transfers/${encodeURIComponent(I)}/${ff}`),{method:"POST",body:{}}),$((yf)=>({...yf,actionLoading:!1,message:ff==="cancel"?"已请求取消任务":"任务已重新入队"})),await N()}catch(yf){$((rf)=>({...rf,actionLoading:!1,error:Df(yf,"任务操作失败")}))}}async function S(){$((I)=>({...I,actionLoading:!0,error:"",message:"正在运行上传/下载自测..."}));try{let I=await Wu(Xu(l,"/api/self-test"),{method:"POST",body:{}});$((ff)=>({...ff,actionLoading:!1,selfTest:I,message:`上传/下载自测通过:${I.remotePath||""}`})),await Z(r),await N()}catch(I){$((ff)=>({...ff,actionLoading:!1,error:Df(I,"上传/下载自测失败")}))}}if(Q2(()=>{if(!_)return;E();return},[_?.id,_?.runtime?.providerStatus]),Q2(()=>{if(!A?.id||A.status!=="pending")return;let I=window.setInterval(()=>void Y(!1),Math.max(5000,Number(A.pollIntervalSeconds||5)*1000));return()=>window.clearInterval(I)},[A?.id,A?.status,A?.pollIntervalSeconds]),Q2(()=>{if(!_)return;let I=window.setInterval(()=>void N(),5000);return()=>window.clearInterval(I)},[_?.id]),!_)return b(E$,{title:"Baidu Netdisk 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=baidu-netdisk"});let T=jL(_),i=FL(_),C=AL(_),v=y.health||{},X=y.account||v.auth?.account||null,D=v.auth||{},p=JL(y.files),m=UL(y.transfers),s=X?.quota||{},d=Boolean(D.loggedIn||X),a=Boolean(D.configured);return b("div",{className:"baidu-netdisk-page","data-testid":"baidu-netdisk-page"},b(Ay,{title:"Baidu Netdisk 工作台",eyebrow:"Containerized Storage Gateway",loading:y.loading,actions:b("div",{className:"panel-actions"},b("a",{className:"ghost-btn",href:"/docs/issue/baidu-netdisk-env-setup.md",target:"_blank",rel:"noreferrer","data-testid":"baidu-netdisk-config-doc-link"},"配置文档"),b("button",{type:"button",className:"ghost-btn",onClick:E,disabled:y.loading,"data-testid":"baidu-netdisk-refresh"},y.loading?"刷新中":"刷新"),b(w1,{title:"Baidu Netdisk 用户服务",data:_,onOpen:u,testId:"raw-baidu-netdisk-service"}))},b("div",{className:"baidu-netdisk-hero"},b("div",null,b("div",{className:"node-version-line"},b(jy,{status:T.providerStatus==="online"?"online":"warn"},T.providerStatus||"unknown"),b("span",null,_.providerId),b(jy,{status:C.public?"warn":"private"},C.public?"公网暴露":"仅 UniDesk frontend 代理访问")),b("p",{className:"muted paragraph"},_.description)),b("div",{className:"microservice-ref-card"},b("span",null,"Repo"),b("strong",null,i.url||"--"),b("code",null,i.commitId||"--")),b("div",{className:"microservice-ref-card"},b("span",null,"Private Backend"),b("strong",null,`${C.nodeBindHost||"--"}:${C.nodePort||"--"}`),b("code",null,`${i.composeFile||"--"} / ${i.composeService||"--"}`))),b(A0,{error:y.error,wide:!0})),b("div",{className:"metric-grid"},b(Z$,{label:"Health",value:v.ok?"OK":"--",hint:v.storage?.postgres||"postgres",tone:v.ok?"ok":"warn"}),b(Z$,{label:"OAuth",value:a?"已配置":"待配置",hint:a?"client + secret + token key":"需要设置 UNIDESK_BAIDU_NETDISK_*",tone:a?"ok":"warn"}),b(Z$,{label:"Login",value:d?"已登录":"未登录",hint:X?.username||"Device Code QR",tone:d?"ok":"warn"}),b(Z$,{label:"Work Root",value:WL(z),hint:z}),b(Z$,{label:"Quota",value:J_(s.used),hint:s.total?`${s.usedPercent||0}% / ${J_(s.total)}`:"授权后刷新"}),b(Z$,{label:"Transfers",value:rL(m.length),hint:`running ${y.transfers?.counts?.running||0} / failed ${y.transfers?.counts?.failed||0}`})),b("div",{className:"baidu-netdisk-grid"},b(Ay,{title:"配置与文档",eyebrow:"Deployment References",className:"baidu-docs-panel",actions:b("div",{className:"panel-actions inline-actions"},b("a",{className:"ghost-btn",href:"/docs/issue/baidu-netdisk-env-setup.md",target:"_blank",rel:"noreferrer"},"打开环境配置"),b("a",{className:"ghost-btn",href:"/docs/issue/baidu-netdisk-user-service.md",target:"_blank",rel:"noreferrer"},"打开服务方案"))},b("p",{className:"muted paragraph"},a?"OAuth 运行时变量已配置;如需轮换密钥、迁移部署或排查代理边界,可直接打开下面的项目内文档。":"首次使用请先按环境变量配置文档填入百度应用 client id / secret,然后重建 baidu-netdisk 服务并刷新本页。"),b("div",{className:"baidu-doc-grid","data-testid":"baidu-netdisk-doc-links"},b(H$,{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"}),b(H$,{title:"服务方案与 API",text:"说明 OAuth Device Code、根目录工作区、staging 上传下载任务和后端 API 设计。",href:"/docs/issue/baidu-netdisk-user-service.md",badge:"DESIGN"}),b(H$,{title:"用户服务安全边界",text:"查看 UniDesk microservice 私有代理、允许路径、frontendOnly 和密钥边界规则。",href:"/docs/reference/microservices.md",badge:"REF"}),b(H$,{title:"部署与重建流程",text:"查看 server rebuild、Compose 编排、健康检查和交付验证的长期规则。",href:"/docs/reference/deployment.md",badge:"DEPLOY"}),b(H$,{title:"CLI 验证命令",text:"查看 microservice health/proxy、server rebuild、job status 等命令入口。",href:"/docs/reference/cli.md",badge:"CLI"}),b(H$,{title:"百度设备码模式",text:"打开百度官方 OAuth Device Code 文档,对照扫码登录和轮询参数。",href:"https://pan.baidu.com/union/doc/fl1x114ti",badge:"OFFICIAL"}))),b(Ay,{title:"设备码登录",eyebrow:"OAuth Device Code",className:"baidu-login-panel",loading:y.actionLoading,actions:b("div",{className:"panel-actions inline-actions"},b("button",{type:"button",className:"primary-btn",onClick:q,disabled:y.actionLoading||!a,"data-testid":"baidu-netdisk-start-login"},"生成二维码"),A?.id?b("button",{type:"button",className:"ghost-btn",onClick:()=>Y(!0),disabled:y.actionLoading},"检查状态"):null,d?b("button",{type:"button",className:"ghost-btn",onClick:w,disabled:y.actionLoading},"清除本地登录"):null,b(w1,{title:"Baidu Device Session",data:A||D.latestSession,onOpen:u,testId:"raw-baidu-device-session"}))},b("div",{className:"baidu-login-card","data-testid":"baidu-netdisk-login-card"},b("div",{className:"baidu-qr-frame"},A?.qrcodeUrl?b("img",{src:A.qrcodeUrl,alt:"百度网盘设备码授权二维码","data-testid":"baidu-netdisk-qrcode"}):b(E$,{title:a?"等待二维码":"OAuth 未配置",text:a?"点击生成二维码后使用百度网盘或百度 App 扫码":"设置 client id、secret 和 token key 后重建服务"})),b("div",{className:"claudeqq-login-copy"},b("div",{className:"node-version-line"},b(jy,{status:d?"online":A?.status==="pending"?"warn":"unknown"},d?"已登录":A?.status||"未开始"),b("span",null,A?.secondsRemaining!==void 0?`${A.secondsRemaining}s`:"--"),b("span",null,"scope basic,netdisk")),b("p",{className:"muted paragraph"},d?"access token / refresh token 已加密保存到 PostgreSQL;前端只看到脱敏登录态。":"后端使用百度 OAuth Device Code 轮询换取 token;二维码过期后重新生成即可。"),b("div",{className:"microservice-ref-card"},b("span",null,"User Code"),b("strong",null,A?.userCode||"--"),b("code",null,A?.verificationUrl||"https://openapi.baidu.com/device")),b("div",{className:"microservice-ref-card"},b("span",null,"Expires"),b("strong",null,A?.expiresAt?Nf(A.expiresAt):"--"),b("code",null,A?.error||"no token exposed"))))),b(Ay,{title:"账号与容量",eyebrow:y.refreshedAt?`Updated ${W0(y.refreshedAt)}`:"Account",loading:y.loading,actions:b("div",{className:"panel-actions inline-actions"},b(w1,{title:"Baidu Account",data:X,onOpen:u,testId:"raw-baidu-account"}))},X?b("div",{className:"baidu-account-card"},b("div",{className:"node-version-line"},b(jy,{status:"online"},"connected"),b("span",null,X.baiduUid||"--"),b("span",null,`VIP ${X.vipType??"--"}`)),b("h3",null,X.username||"Baidu Netdisk"),b("p",{className:"muted paragraph"},`工作目录固定在 ${X.rootPath||z};v1 上传/下载只读写容器 staging 目录,不把大文件字节流穿过 UniDesk proxy。`),b("div",{className:"quota-bar"},b("span",{style:{width:`${Math.max(0,Math.min(100,Number(s.usedPercent||0)))}%`}})),b("div",{className:"microservice-ref-card"},b("span",null,"Quota"),b("strong",null,`${J_(s.used)} / ${J_(s.total)}`),b("code",null,`${s.usedPercent||0}% used`))):b(E$,{title:"尚未登录",text:"扫码授权后这里会显示账号、UID、会员状态和容量"})),b(Ay,{title:"文件浏览器",eyebrow:r,className:"baidu-files-panel",loading:y.loading,actions:b("div",{className:"panel-actions inline-actions"},b("button",{type:"button",className:"ghost-btn",onClick:()=>{let I=QL(r,z);j(I),Z(I)},disabled:!d||r===z},"上级"),b("button",{type:"button",className:"ghost-btn",onClick:()=>Z(r),disabled:!d},"刷新文件"),b(w1,{title:"Baidu Files",data:y.files,onOpen:u,testId:"raw-baidu-files"}))},b("form",{className:"baidu-pathbar",onSubmit:(I)=>{I.preventDefault(),Z(r)}},b("input",{value:r,onChange:(I)=>j(I.target.value),disabled:!d}),b("button",{type:"submit",className:"ghost-btn",disabled:!d},"打开路径")),b("form",{className:"baidu-pathbar",onSubmit:B},b("input",{value:U,onChange:(I)=>Q(I.target.value),placeholder:"新文件夹名称",disabled:!d}),b("button",{type:"submit",className:"primary-btn",disabled:!d||!U.trim()},"新建文件夹")),!d?b(E$,{title:"等待授权",text:"登录后通过 /api/files 读取工作目录文件列表"}):p.length===0?b(E$,{title:"目录为空",text:"可以从 staging 目录上传文件或新建文件夹"}):b("div",{className:"table-wrap","data-testid":"baidu-netdisk-file-table"},b("table",null,b("thead",null,b("tr",null,b("th",null,"名称"),b("th",null,"类型"),b("th",null,"大小"),b("th",null,"修改时间"),b("th",null,"fs_id"),b("th",null,"操作"))),b("tbody",null,p.map((I)=>b("tr",{key:I.fsId||I.path},b("td",null,b("strong",null,I.serverFilename||I.path),b("code",null,I.path||"--")),b("td",null,b(jy,{status:I.isDir?"queued":"private"},I.isDir?"DIR":"FILE")),b("td",null,I.isDir?"--":J_(I.size)),b("td",null,I.serverMtime?Nf(I.serverMtime*1000):"--"),b("td",null,b("code",null,I.fsId||"--")),b("td",null,b("div",{className:"inline-actions"},I.isDir?b("button",{type:"button",className:"ghost-btn",onClick:()=>{j(I.path),Z(I.path)}},"打开"):b("button",{type:"button",className:"ghost-btn",onClick:()=>H((ff)=>({...ff,fsId:I.fsId}))},"填入下载"),b("button",{type:"button",className:"ghost-btn",onClick:()=>P(I.path),disabled:y.actionLoading},"删除"))))))))),b(Ay,{title:"传输任务",eyebrow:"staging path jobs",className:"baidu-transfers-panel",loading:y.actionLoading,actions:b("div",{className:"panel-actions inline-actions"},b("button",{type:"button",className:"primary-btn",onClick:S,disabled:!d||y.actionLoading,"data-testid":"baidu-netdisk-self-test"},"运行自测"),b("button",{type:"button",className:"ghost-btn",onClick:N},"刷新任务"),b(w1,{title:"Baidu Transfers",data:y.transfers,onOpen:u,testId:"raw-baidu-transfers"}))},b("div",{className:"baidu-transfer-forms"},b("form",{className:"stack-form",onSubmit:h,"data-testid":"baidu-upload-form"},b("label",null,"容器 staging 文件",b("input",{value:W.localPath,onChange:(I)=>G((ff)=>({...ff,localPath:I.target.value})),placeholder:"sample.txt"})),b("label",null,"百度网盘目标路径",b("input",{value:W.remotePath,onChange:(I)=>G((ff)=>({...ff,remotePath:I.target.value})),placeholder:uj(z,"sample.txt")})),b("button",{type:"submit",className:"primary-btn",disabled:!d||y.actionLoading},"上传 staging 文件")),b("form",{className:"stack-form",onSubmit:M,"data-testid":"baidu-download-form"},b("label",null,"文件 fs_id",b("input",{value:K.fsId,onChange:(I)=>H((ff)=>({...ff,fsId:I.target.value})),placeholder:"从文件表填入"})),b("label",null,"保存到 staging 路径",b("input",{value:K.localPath,onChange:(I)=>H((ff)=>({...ff,localPath:I.target.value})),placeholder:"downloads/"})),b("button",{type:"submit",className:"primary-btn",disabled:!d||!K.fsId||y.actionLoading},"下载到 staging"))),y.selfTest?b("div",{className:"baidu-account-card","data-testid":"baidu-netdisk-self-test-result"},b("div",{className:"node-version-line"},b(jy,{status:y.selfTest.ok?"online":"warn"},y.selfTest.ok?"self-test ok":"self-test"),b("span",null,J_(y.selfTest.sizeBytes))),b("h3",null,y.selfTest.remotePath||"Baidu self-test"),b("div",{className:"microservice-ref-card"},b("span",null,"fs_id"),b("strong",null,y.selfTest.fsId||"--"),b("code",null,y.selfTest.downloadedPath||"--")),b("div",{className:"microservice-ref-card"},b("span",null,"MD5"),b("strong",null,y.selfTest.downloadedMd5||"--"),b("code",null,y.selfTest.expectedMd5||"--")),b(w1,{title:"Baidu Self Test",data:y.selfTest,onOpen:u,testId:"raw-baidu-self-test"})):null,m.length===0?b(E$,{title:"暂无传输任务",text:"上传/下载任务会在后端容器内执行,避免大文件穿过 UniDesk proxy"}):b("div",{className:"table-wrap","data-testid":"baidu-transfer-table"},b("table",null,b("thead",null,b("tr",null,b("th",null,"状态"),b("th",null,"方向"),b("th",null,"路径"),b("th",null,"进度"),b("th",null,"时间"),b("th",null,"操作"))),b("tbody",null,m.map((I)=>b("tr",{key:I.id},b("td",null,b(jy,{status:I.status},I.status)),b("td",null,I.direction),b("td",null,b("strong",null,I.remotePath||I.fsId||"--"),b("code",null,I.localPath||"--"),I.error?b("span",{className:"form-error"},I.error):null),b("td",null,b(zL,{percent:I.progressPercent}),b("span",{className:"muted"},`${J_(I.bytesDone)} / ${J_(I.sizeBytes)}`)),b("td",null,Nf(I.updatedAt)),b("td",null,b("div",{className:"inline-actions"},["queued","running"].includes(I.status)?b("button",{type:"button",className:"ghost-btn",onClick:()=>n(I.id,"cancel")},"取消"):null,["failed","canceled"].includes(I.status)?b("button",{type:"button",className:"ghost-btn",onClick:()=>n(I.id,"retry")},"重试"):null,b(w1,{title:`Transfer ${I.id}`,data:I,onOpen:u}))))))))),b(Ay,{title:"安全与日志",eyebrow:"redacted diagnostics",className:"baidu-wide-panel",loading:y.loading,actions:b("div",{className:"panel-actions inline-actions"},b(w1,{title:"Baidu Health",data:v,onOpen:u,testId:"raw-baidu-health"}),b(w1,{title:"Baidu Logs",data:y.logs,onOpen:u,testId:"raw-baidu-logs"}))},b("div",{className:"policy-grid"},b("article",null,b("b",null,"私有后端"),b("span",null,"4244 只在 Compose 网络 expose,浏览器经 UniDesk 同源代理访问")),b("article",null,b("b",null,"Token 加密"),b("span",null,"access/refresh token 使用 BAIDU_NETDISK_TOKEN_KEY 加密后写入 PostgreSQL")),b("article",null,b("b",null,"无浏览器大文件流"),b("span",null,"上传/下载以容器 staging 目录为边界,避免 proxy 文本通道传输大字节流"))))))}var K2=cf(O0(),1);var t=K2.default.createElement,{useEffect:GL}=K2.default,z2=K2.default.useState,Fy={label:"主用户私聊账号",userId:645275593};function lj(f){let u=Number(f);return Number.isFinite(u)?u.toLocaleString("zh-CN"):"--"}async function D1(f,u={}){return Mf(f,{failureFields:["ok","success"],...u})}function G2({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return t("span",{className:`status-badge ${l}`},u||f||"unknown")}function O$({label:f,value:u,hint:l,tone:_}){return t("article",{className:`metric-card ${_||""}`},t("div",{className:"metric-label"},f),t("div",{className:"metric-value"},u),t("div",{className:"metric-hint"},l))}function V$({title:f,eyebrow:u,actions:l,children:_,className:y,loading:$}){return t("section",{className:`panel ${y||""}`},t("div",{className:"panel-head"},t("div",null,u?t("p",{className:"panel-eyebrow"},u):null,t(j0,{title:f,loading:$})),l?t("div",{className:"panel-actions"},l):null),t("div",{className:"panel-body"},_))}function N6({title:f,data:u,onOpen:l,testId:_}){return t("button",{type:"button",className:"ghost-btn","data-testid":_,onClick:(y)=>{y?.stopPropagation?.(),l(f,u)}},"查看原始JSON")}function Z6({title:f,text:u}){return t("div",{className:"empty-state"},t("strong",null,f),t("span",null,u))}function KL(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function NL(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function ZL(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function U_(f,u){return`${f}/microservices/claudeqq/proxy${u}`}function EL(f){return Array.isArray(f?.events)?f.events.slice(0,80):[]}function HL(f){return Array.isArray(f?.subscriptions)?f.subscriptions.slice(0,50):[]}function OL(f){return Array.isArray(f?.messages)?f.messages.slice(0,30):[]}function xz(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 bz(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 vz({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let _=f.find((X)=>X.id==="claudeqq")||null,[y,$]=z2({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}),[r,j]=z2({targetType:"private",targetId:String(Fy.userId),message:""}),[A,J]=z2({name:"unidesk-callback",targetUrl:"",eventTypes:"message",secret:""}),[U,Q]=z2(""),{addNotification:W}=Lu();async function G(){if(!_)return;$((X)=>({...X,loading:!0,error:""}));try{let[X,D,p,m,s]=await Promise.all([D1(`${l}/microservices/claudeqq/health`),D1(U_(l,"/api/server/status")),D1(U_(l,"/api/events/recent?limit=60")),D1(U_(l,"/api/events/subscriptions")),D1(U_(l,"/api/messages/sent?limit=20"))]);if($((d)=>({...d,loading:!1,error:"",health:X,status:D,events:p,subscriptions:m,sent:s,refreshedAt:new Date})),!y.qrcodeFetched)K(!1)}catch(X){$((D)=>({...D,loading:!1,error:Df(X,"ClaudeQQ 加载失败")}))}}async function K(X=!0){if(!_)return;$((D)=>({...D,qrLoading:!0,error:X?"":D.error}));try{let D=await D1(U_(l,"/api/napcat/login")),p=D?.napcat?.qrcode||D?.qrcode||null;$((m)=>({...m,qrLoading:!1,error:"",napcatLogin:D,napcatQrcode:p,qrcodeFetched:!0,qrcodeRefreshedAt:new Date}))}catch(D){$((p)=>({...p,qrLoading:!1,error:X||!p.napcatQrcode?Df(D,"NapCat 二维码加载失败"):p.error}))}}async function H(X){X.preventDefault(),Q("");let D=Number(r.targetId);if(!Number.isFinite(D)||D<=0||r.message.trim().length===0){$((p)=>({...p,error:"请填写 QQ 目标和消息内容"}));return}try{await D1(U_(l,"/api/push/text"),{method:"POST",body:JSON.stringify({userId:r.targetType==="private"?D:void 0,groupId:r.targetType==="group"?D:void 0,message:r.message})});let p="消息推送请求已提交";j((m)=>({...m,targetType:"private",targetId:String(Fy.userId),message:""})),Q(p),W("success",p),await G()}catch(p){$((m)=>({...m,error:Df(p,"发送失败")}))}}async function O(X){if(X.preventDefault(),Q(""),A.targetUrl.trim().length===0){$((D)=>({...D,error:"请填写订阅回调 URL"}));return}try{await D1(U_(l,"/api/events/subscriptions"),{method:"POST",body:JSON.stringify({name:A.name,targetUrl:A.targetUrl,eventTypes:A.eventTypes.split(",").map((p)=>p.trim()).filter(Boolean),secret:A.secret||void 0,enabled:!0})});let D="事件订阅已创建";Q(D),W("success",D),await G()}catch(D){$((p)=>({...p,error:Df(D,"订阅失败")}))}}async function z(X){if(!X)return;Q("");try{await D1(U_(l,`/api/events/subscriptions/${encodeURIComponent(X)}`),{method:"DELETE"});let D="事件订阅已删除";Q(D),W("success",D),await G()}catch(D){$((p)=>({...p,error:Df(D,"删除订阅失败")}))}}if(GL(()=>{if(!_)return;G();return},[_?.id,_?.runtime?.providerStatus]),!_)return t(Z6,{title:"ClaudeQQ 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=claudeqq"});let Z=KL(_),N=ZL(_),E=NL(_),q=y.health||{},Y=y.status||{},w=y.napcatLogin||{},B=q.napcat||Y.napcat||{},P={...w.napcat||{},...B,qrcode:y.napcatQrcode||{},webui:B.webui||w.napcat?.webui},h=w.login||{},M=y.napcatQrcode||{},n=EL(y.events),S=HL(y.subscriptions),T=OL(y.sent),i=Boolean(P.httpConnected||h.ready),C=String(P.loginState||h.state||(i?"logged_in":"unknown")),v=Boolean(M.available&&M.dataUrl);return t("div",{className:"claudeqq-page","data-testid":"claudeqq-page"},t(V$,{title:"ClaudeQQ 工作台",eyebrow:"D601 QQ Event Gateway",loading:y.loading,actions:t("div",{className:"panel-actions"},t("button",{type:"button",className:"ghost-btn",onClick:G,disabled:y.loading,"data-testid":"claudeqq-refresh-button"},y.loading?"刷新中":"刷新"),t(N6,{title:"ClaudeQQ 用户服务",data:_,onOpen:u,testId:"raw-claudeqq-service"}))},t("div",{className:"findjob-hero"},t("div",null,t("div",{className:"node-version-line"},t(G2,{status:Z.providerStatus==="online"?"online":"warn"},Z.providerStatus||"unknown"),t("span",null,_.providerId),t("span",null,E.public?"公网暴露":"仅 UniDesk frontend 代理访问")),t("p",{className:"muted paragraph"},_.description)),t("div",{className:"microservice-ref-card"},t("span",null,"Repo"),t("strong",null,N.url||"--"),t("code",null,N.commitId||"--")),t("div",{className:"microservice-ref-card"},t("span",null,"D601 Docker"),t("strong",null,`${E.nodeBindHost||"--"}:${E.nodePort||"--"}`),t("code",null,`${N.composeFile||"--"} / ${N.composeService||"--"}`))),t(A0,{error:y.error,wide:!0}),U?t("div",{className:"form-success wide"},U):null),t("div",{className:"metric-grid"},t(O$,{label:"Health",value:q.ok||q.status==="ok"?"OK":"--",hint:"D601 /health",tone:q.ok||q.status==="ok"?"ok":"warn"}),t(O$,{label:"NapCat HTTP",value:P.httpConnected||P.http?.connected?"OK":"离线",hint:`${P.httpHost||q.napcat?.httpHost||"--"}:${P.httpPort||q.napcat?.httpPort||"--"}`}),t(O$,{label:"NapCat WS",value:P.wsConnected||P.ws?.connected?"OK":"离线",hint:`${P.wsHost||q.napcat?.wsHost||"--"}:${P.wsPort||q.napcat?.wsPort||"--"}`}),t(O$,{label:"事件缓存",value:lj(y.events?.count??n.length),hint:"recent QQ events"}),t(O$,{label:"订阅",value:lj(y.subscriptions?.count??S.length),hint:"webhook subscribers"}),t(O$,{label:"已发送",value:lj(y.sent?.count??T.length),hint:"sent message log"})),t("div",{className:"findjob-grid"},t(V$,{title:"NapCat 容器登录",eyebrow:"QR Login",className:"claudeqq-login-panel",loading:y.qrLoading,actions:t("div",{className:"panel-actions inline-actions"},t("button",{type:"button",className:"ghost-btn",onClick:()=>K(!0),disabled:y.qrLoading,"data-testid":"claudeqq-napcat-refresh"},y.qrLoading?"刷新中":"手动刷新二维码"),t(N6,{title:"NapCat Login",data:y.napcatLogin,onOpen:u,testId:"raw-claudeqq-napcat-login"}))},t("div",{className:"claudeqq-login-card","data-testid":"claudeqq-napcat-login"},t("div",{className:"claudeqq-qr-frame"},v?t("img",{src:M.dataUrl,alt:"NapCat QQ 登录二维码","data-testid":"claudeqq-napcat-qrcode"}):t(Z6,{title:"等待二维码",text:"NapCat 容器启动后会把登录二维码写入 cache/qrcode.png"})),t("div",{className:"claudeqq-login-copy"},t("div",{className:"node-version-line"},t(G2,{status:i?"online":v?"warn":"unknown"},i?"已登录":v?"待扫码":"等待二维码"),t("span",null,C),t("span",null,"D601 containerized")),t("p",{className:"muted paragraph"},i?"NapCat 已登录,ClaudeQQ 可通过容器内 HTTP/WS 链路收发 QQ 消息。":"用手机 QQ 扫描二维码授权登录。二维码只在首次加载或手动刷新时更新,D601 的 NapCat 端口仍只绑定 127.0.0.1。"),t("div",{className:"microservice-ref-card"},t("span",null,"NapCat WebUI"),t("strong",null,P.webui?.url||"http://napcat:6099/webui"),t("code",null,"local-only / proxied QR login")),t("div",{className:"microservice-ref-card"},t("span",null,"QR Source"),t("strong",null,M.modifiedAt?Nf(M.modifiedAt):y.qrcodeRefreshedAt?Nf(y.qrcodeRefreshedAt):"--"),t("code",null,M.file||"/napcat/cache/qrcode.png"))))),t(V$,{title:"消息推送",eyebrow:"Push API"},t("div",{className:"microservice-ref-card"},t("span",null,Fy.label),t("strong",null,String(Fy.userId)),t("code",null,"private userId / 默认推送测试目标")),t("form",{className:"stack-form",onSubmit:H,"data-testid":"claudeqq-push-form"},t("label",null,"目标类型",t("select",{value:r.targetType,onChange:(X)=>j((D)=>({...D,targetType:X.target.value}))},t("option",{value:"private"},"私聊 userId"),t("option",{value:"group"},"群 groupId"))),t("label",null,"QQ ID",t("input",{value:r.targetId,onChange:(X)=>j((D)=>({...D,targetId:X.target.value})),placeholder:String(Fy.userId)})),t("label",null,"消息内容",t("textarea",{value:r.message,onChange:(X)=>j((D)=>({...D,message:X.target.value})),rows:4,placeholder:"通过 ClaudeQQ 推送一条 QQ 消息"})),t("button",{type:"submit",className:"primary-btn"},"发送 QQ 消息")),t("p",{className:"muted paragraph"},`主 server 和其他用户服务可通过 UniDesk 同源代理调用 /api/push/text;当前人工推送测试默认使用 ${Fy.label} ${Fy.userId},不需要暴露 D601 后端端口。`)),t(V$,{title:"QQ 事件订阅",eyebrow:"Webhook Subscription",loading:y.loading},t("form",{className:"stack-form",onSubmit:O,"data-testid":"claudeqq-subscription-form"},t("label",null,"订阅名称",t("input",{value:A.name,onChange:(X)=>J((D)=>({...D,name:X.target.value}))})),t("label",null,"回调 URL",t("input",{value:A.targetUrl,onChange:(X)=>J((D)=>({...D,targetUrl:X.target.value})),placeholder:"http://host.docker.internal:18080/..."})),t("label",null,"事件类型",t("input",{value:A.eventTypes,onChange:(X)=>J((D)=>({...D,eventTypes:X.target.value})),placeholder:"message,notice"})),t("label",null,"签名密钥",t("input",{value:A.secret,onChange:(X)=>J((D)=>({...D,secret:X.target.value})),placeholder:"可选,生成 x-claudeqq-signature"})),t("button",{type:"submit",className:"primary-btn"},"创建订阅")),S.length===0?t(Z6,{title:"暂无订阅",text:"可以为 main server 或其他用户服务注册 HTTP webhook"}):t("div",{className:"table-wrap","data-testid":"claudeqq-subscription-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,S.map((X)=>t("tr",{key:X.id},t("td",null,t("strong",null,X.name||X.id),t("code",null,X.id||"--")),t("td",null,t(G2,{status:X.enabled?"online":"warn"},X.enabled?"enabled":"disabled")),t("td",null,Array.isArray(X.eventTypes)?X.eventTypes.join(", "):"message"),t("td",null,X.targetUrl||"--"),t("td",null,X.lastDelivery?`${X.lastDelivery.ok?"OK":"FAIL"} ${Nf(X.lastDelivery.at)}`:"--"),t("td",null,t("button",{type:"button",className:"ghost-btn",onClick:()=>z(X.id)},"删除"))))))),t("div",{className:"panel-actions inline-actions"},t(N6,{title:"ClaudeQQ Subscriptions",data:y.subscriptions,onOpen:u,testId:"raw-claudeqq-subscriptions"}))),t(V$,{title:"最近 QQ 事件",eyebrow:y.refreshedAt?`Updated ${W0(y.refreshedAt)}`:"Event Stream",loading:y.loading},n.length===0?t(Z6,{title:"暂无事件",text:"等待 NapCat WebSocket 上报 QQ 消息事件,或通过订阅 API 消费后续事件"}):t("div",{className:"table-wrap","data-testid":"claudeqq-event-list"},t("table",null,t("thead",null,t("tr",null,t("th",null,"时间"),t("th",null,"类型"),t("th",null,"会话"),t("th",null,"消息"),t("th",null,"ID"))),t("tbody",null,n.map((X)=>t("tr",{key:X.id},t("td",null,Nf(X.receivedAt||X.timestamp)),t("td",null,t(G2,{status:X.postType||X.eventType},X.postType||X.eventType||"--")),t("td",null,bz(X)),t("td",null,xz(X)),t("td",null,t("code",null,X.messageId||X.id||"--"))))))),t("div",{className:"panel-actions inline-actions"},t(N6,{title:"ClaudeQQ Events",data:y.events,onOpen:u,testId:"raw-claudeqq-events"}))),t(V$,{title:"已发送消息",eyebrow:`${T.length} Sent`,loading:y.loading},T.length===0?t(Z6,{title:"暂无发送记录",text:"发送日志来自 ClaudeQQ bot_workspace/messages/sent_messages.jsonl"}):t("div",{className:"table-wrap"},t("table",null,t("thead",null,t("tr",null,t("th",null,"时间"),t("th",null,"目标"),t("th",null,"消息"),t("th",null,"结果"))),t("tbody",null,T.map((X,D)=>t("tr",{key:X.id||D},t("td",null,Nf(X.timestamp||X.sentAt||X.createdAt)),t("td",null,bz(X)),t("td",null,xz(X)),t("td",null,X.status||X.messageId||X.message_id||"--")))))),t("div",{className:"panel-actions inline-actions"},t(N6,{title:"ClaudeQQ Sent Messages",data:y.sent,onOpen:u,testId:"raw-claudeqq-sent"})))))}var Y6=cf(O0(),1);var pz=cf(O0(),1),U0=pz.default.createElement;function mz({markdown:f,className:u,testId:l}){let _=String(f??"").trimEnd(),y=["markdown-body",u].filter(Boolean).join(" ");return U0("div",{className:y,"data-testid":l},gz(_,"md"))}function gz(f,u){let l=VL(f).split(` +`),_=[],y=0;while(y\s?/u.test($)){let Q=[];while(y\s?(.*)$/u);if(G!==null){Q.push(G[1]),y+=1;continue}if(W.trim().length===0){Q.push(""),y+=1;continue}break}_.push(U0("blockquote",{key:`${u}-quote-${y}`},gz(Q.join(` +`),`${u}-quote-${y}`)));continue}if(tz(l,y)){let Q=y,W=E6(l[y]??""),G=E6(l[y+1]??"");y+=2;let K=[];while(y0)K.push(E6(l[y]??"")),y+=1;_.push(YL(W,G,K,`${u}-table-${Q}`));continue}let A=Z2($);if(A!==null){let Q=y,W=A.ordered,G=A.start,K=[];while(yXL(O,`${u}-list-${Q}-${z}`))));continue}let J=y,U=[];while(y0&&!LL(l,y))U.push(l[y].trim()),y+=1;if(U.length===0)U.push($.trim()),y+=1;_.push(U0("p",{key:`${u}-p-${J}`},u1(U.join(` +`),`${u}-p-${J}`)))}return _}function VL(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(` +`).trimEnd()}function kz(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 qL(f,u){let l=f.trim();return l.length>=u.length&&l.split("").every((_)=>_===u.marker)}function _j(f){return/^(?: {4}|\t)/u.test(f)}function hz(f,u,l){let _=u.trim().length>0?`language-${DL(u)}`:void 0;return U0("pre",{key:l,className:"markdown-code-block"},U0("code",{className:_},f))}function LL(f,u){let l=f[u]??"";if(l.trim().length===0)return!0;return kz(l)!==null||_j(l)||/^(#{1,6})\s+.+$/u.test(l)||/^\s*(?:---+|\*\*\*+|___+)\s*$/u.test(l)||/^\s*>\s?/u.test(l)||tz(f,u)||Z2(l)!==null}function Z2(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 XL(f,u){let l=f.match(/^\[([ xX])\]\s+(.+)$/u);if(l!==null){let _=l[1].toLowerCase()==="x";return U0("li",{key:u,className:"task-list-item"},U0("input",{type:"checkbox",checked:_,readOnly:!0,tabIndex:-1}),U0("span",null,u1(l[2],`${u}-task`)))}return U0("li",{key:u},u1(f,u))}function tz(f,u){let l=f[u]??"",_=f[u+1]??"";if(!l.includes("|")||!_.includes("|"))return!1;let y=E6(l),$=E6(_);return y.length>1&&$.length===y.length&&$.every((r)=>/^:?-{3,}:?$/u.test(r.trim()))}function E6(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 BL(f){let u=f.trim();if(u.startsWith(":")&&u.endsWith(":"))return"center";if(u.endsWith(":"))return"right";if(u.startsWith(":"))return"left";return}function YL(f,u,l,_){let y=u.map(BL);return U0("div",{key:_,className:"markdown-table-wrap"},U0("table",null,U0("thead",null,U0("tr",null,f.map(($,r)=>U0("th",{key:`${_}-h-${r}`,style:y[r]?{textAlign:y[r]}:void 0},u1($,`${_}-h-${r}`))))),U0("tbody",null,l.map(($,r)=>U0("tr",{key:`${_}-r-${r}`},f.map((j,A)=>U0("td",{key:`${_}-r-${r}-${A}`,style:y[A]?{textAlign:y[A]}:void 0},u1($[A]||"",`${_}-r-${r}-${A}`))))))))}function u1(f,u,l={}){let _=[],y=/`([^`\n]+)`|\[([^\]\n]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|(https?:\/\/[^\s<>)]+)|\*\*([^*\n]+)\*\*|__([^_\n]+)__|~~([^~\n]+)~~|\*([^*\n]+)\*|_([^_\n]+)_/gu,$=l.linkify!==!1,r=0,j=0;for(let A of f.matchAll(y)){let J=A[0],U=A.index??0;N2(_,f.slice(r,U),`${u}-text-${j}`),r=U+J.length;let Q=`${u}-inline-${j}`;if(j+=1,A[1]!==void 0){_.push(U0("code",{key:Q},A[1]));continue}if(A[2]!==void 0&&A[3]!==void 0){if(!$){N2(_,J,`${Q}-literal`);continue}_.push(Iz(A[2],A[3],Q));continue}if(A[4]!==void 0){if(!$){N2(_,J,`${Q}-literal`);continue}_.push(Iz(A[4],A[4],Q));continue}let W=A[5]??A[6];if(W!==void 0){_.push(U0("strong",{key:Q},u1(W,`${Q}-strong`)));continue}if(A[7]!==void 0){_.push(U0("del",{key:Q},u1(A[7],`${Q}-del`)));continue}let G=A[8]??A[9];if(G!==void 0)_.push(U0("em",{key:Q},u1(G,`${Q}-em`)))}return N2(_,f.slice(r),`${u}-text-tail`),_}function N2(f,u,l){if(u.length===0)return;u.split(` +`).forEach((y,$)=>{if($>0)f.push(U0("br",{key:`${l}-br-${$}`}));if(y.length>0)f.push(y)})}function Iz(f,u,l){let _=wL(u);if(_===null)return U0("span",{key:l},f);let y=/^(?:https?:|mailto:)/iu.test(_);return U0("a",{key:l,href:_,target:y?"_blank":void 0,rel:y?"noreferrer":void 0},u1(f,`${l}-label`,{linkify:!1}))}function wL(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 DL(f){return String(f||"").toLowerCase().replace(/[^a-z0-9_-]+/gu,"-").replace(/^-+|-+$/gu,"")||"text"}var Fj=cf(O0(),1);var Sf=Fj.default.createElement,{useEffect:TL,useRef:sz}=Fj.default;function ML(f,u){return UG(f.toTrace(u))}function PL(f){let u=Number(f);if(!Number.isFinite(u)||u<0)return"--";let l=Math.floor(u/1000),_=Math.floor(l/3600),y=Math.floor(l%3600/60),$=l%60;if(_>0)return`${_}h ${String(y).padStart(2,"0")}m`;if(y>0)return`${y}m ${String($).padStart(2,"0")}s`;return`${$}s`}function il(f){let u=Number(f);return Number.isFinite(u)&&u>=0?u:null}function L$(f,u=180){let l=String(f||"").replace(/\s+/gu," ").trim();return l.length>u?`${l.slice(0,u-1)}…`:l}function nL(f){if(!f)return 0;return f.split(/\r?\n/u).length}function rj(f){return{ran:"Ran",explored:"Explored",edited:"Edited",toolGroup:"Tool calls",plan:"Plan",message:"Message",system:"System",error:"Error"}[f]||"Message"}function jj(f){let u=Number(f||0);return Number.isFinite(u)&&u>0?`… +${Math.floor(u)} lines`:""}function SL(f){return(Array.isArray(f)?f:[]).reduce((u,l)=>Math.max(u,Number(l?.seq??0)),0)}function oz(f){return["explored","edited","ran"].includes(String(f?.kind||""))}function yG(f){let u={read:0,edit:0,run:0};for(let l of f){let _=String(l?.kind||"");if(_==="explored")u.read+=1;else if(_==="edited")u.edit+=1;else if(_==="ran")u.run+=1}return u}function $G(f){let u=yG(f);return`${u.read} read, ${u.edit} edit, ${u.run} run`}function rG(f){return f.replace(/^['"`([{<]+/u,"").replace(/['"`)\]}>.,;:]+$/u,"").replace(/:\d+(?::\d+)?$/u,"").trim()}function az(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)||[],_=[];for(let y of l){let $=rG(y);if($.length<2||$.includes("..."))continue;if(/^(http|https|status|method)$/iu.test($))continue;if(!_.includes($))_.push($)}return _}function yj(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 dz(f){let u="";for(let l of f){if(l.length===0)continue;if(u.length>0&&!u.endsWith(` `)&&!l.startsWith(` `))u+=` -`;u+=l}return u}function mz(f){let u=String(f||"").replace(/\r\n/gu,` +`;u+=l}return u}function jG(f){let u=String(f||"").replace(/\r\n/gu,` `).replace(/\r/gu,` `).trimEnd();return u.length>0?u.split(` -`):[]}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(` +`):[]}function Aj(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 CL(f){let u=String(f.bodyPreview||"");return/file changes status=([A-Za-z0-9_-]+)/u.exec(u)?.[1]}function iL(f){return/^item\/(?:started|completed): file changes status=/u.test(String(f||"").trim())}function AG(f){if(String(f.kind||"")!=="edited")return!1;let u=String(f.status||""),l=String(f.title||""),_=String(f.bodyPreview||""),y=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(_))return!0;if(/^Success\. Updated the following files:/mu.test(_))return!0;if(/^diff --git /mu.test(_))return!0;return/^([AMDRCU?]{1,2})\s+\S+/mu.test(_)||y.length>0&&H2(_).length>0}function q$(f){return rG(String(f||"").replace(/^[ab]\//u,"").trim())}function Jj(f){let u=/^([AMDRCU?]{1,2})\s+(.+)$/u.exec(f);if(!u)return null;let l=q$(u[2]||"");return l.length>0?{status:u[1]||"M",path:l}:null}function Uj(f){let u=/^\*\*\*\s+(Add|Update|Delete)\s+File:\s+(.+)$/u.exec(f);if(u){let _=u[1]==="Add"?"A":u[1]==="Delete"?"D":"M",y=q$(u[2]||"");return y.length>0?{status:_,path:y}:null}let l=/^\*\*\*\s+Move to:\s+(.+)$/u.exec(f);if(l){let _=q$(l[1]||"");return _.length>0?{status:"R",path:_}:null}return null}function H2(f){let u=[],l=(y,$)=>{let r=q$($);if(r.length===0||r==="/dev/null")return;let j=u.find((A)=>A.path===r);if(j){if(j.status==="M"&&y!=="M")j.status=y;return}u.push({status:y,path:r})},_="";for(let y of jG(f)){let $=Jj(y)||Uj(y);if($!==null){l($.status,$.path),_=$.path;continue}let r=/^diff --git a\/(.+?) b\/(.+)$/u.exec(y);if(r){let Q=r[2]||r[1]||"";l("M",Q),_=q$(Q);continue}let j=/^\+\+\+ b\/(.+)$/u.exec(y);if(j&&j[1]!=="/dev/null"){l("M",j[1]||""),_=q$(j[1]||"");continue}if(/^new file mode /u.exec(y)&&_)l("A",_);if(/^deleted file mode /u.exec(y)&&_)l("D",_);let U=/^rename to (.+)$/u.exec(y);if(U)l("R",U[1]||"")}return u}function cL(f){if(Jj(f)!==null||Uj(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 Qj(f){return jG(f).map((u)=>{let l=Jj(u)||Uj(u);if(l!==null)return{text:u,kind:"file",path:l.path,status:l.status};return{text:u,kind:cL(u)}})}function RL(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 ez(f,u){return`${u} ${f} line${f===1?"":"s"}`}function xL(f,u){let l=[];if(f>0)l.push(ez(f,"Added"));if(u>0)l.push(ez(u,"removed"));return l.join(", ")}function bL(f){for(let l=f.length-1;l>=0;l-=1){let _=String(f[l]?.status||"").trim();if(_.length>0)return _}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 vL(f){return`${f} file${f===1?"":"s"}`}function FG(f){let u=f.length>0?f:[],l=dz(u.map((W)=>String(W.bodyPreview||""))),y=dz(u.map((W)=>String(W.bodyPreview||"")).filter((W)=>W.trim().length>0&&!iL(W)))||l,$=H2(y||l),r=u.map((W)=>({method:Aj(W),status:CL(W),at:W.at})),j=Qj(y||l),A=RL(j),J=xL(A.added,A.removed),U=$.length>0?vL($.length):"",Q=J.length>0?`${J}${U?` in ${U}`:""}`:$.length>0?U:L$(y||l||"File changes",72);return{status:bL(r),summary:Q,files:$,stages:r,lines:j,addedLines:A.added,removedLines:A.removed,rawText:l}}function hL(f){let u=f[0],l=f[f.length-1]||u,_=FG(f);return{...u,seq:Number.isFinite(Number(l?.seq))?Number(l?.seq):Number(u?.seq??0),at:l?.at||u?.at,title:_.files.length>0?`Edited ${_.summary}`:"Edited files",status:_.status,commandPreview:"",commandOmittedLines:void 0,bodyPreview:_.rawText,bodyOmittedLines:f.reduce((y,$)=>y+Number($.bodyOmittedLines||0),0)||void 0,rawSeqs:f.flatMap((y)=>Array.isArray(y?.rawSeqs)?y.rawSeqs:[y?.seq]).filter((y)=>y!==void 0),editObservation:_}}function IL(f){let u=Array.isArray(f)?f:[],l=[],_=[],y=()=>{if(_.length===0)return;l.push(hL(_)),_=[]};for(let $ of u){if(AG($)){if(Aj($)==="item/started"&&_.length>0)y();if(_.push($),Aj($)==="item/completed")y();continue}y(),l.push($)}return y(),l}function JG(f){let u=[],l=[],_=[],y=(J,U)=>{for(let Q of U)if(!J.includes(Q))J.push(Q)};for(let J of f){let U=String(J?.kind||""),Q=[J?.commandPreview,J?.bodyPreview,J?.title].map((W)=>String(W||"")).join(` +`);if(U==="explored")y(u,az(Q));else if(U==="edited")y(l,az(Q));else if(U==="ran"){let W=String(J?.commandPreview||J?.title||"").trim();if(W.length>0&&!_.includes(W))_.push(L$(W,90))}}let $=f.map((J)=>Date.parse(String(J?.at||""))).filter((J)=>Number.isFinite(J)),r=$.length>=2?Math.max(0,Math.max(...$)-Math.min(...$)):0,j=f.reduce((J,U)=>J+(il(U?.durationMs)??il(U?.elapsedMs)??0),0),A=r>0?r:j;return{readFiles:u,editedFiles:l,runCommands:_,durationLabel:PL(A)}}function pL(f,u=3){let l=Array.isArray(f)?f:[],_=[],y=[],$=Math.max(0,u),r=new Set;for(let A=l.length-1;A>=0&&$>0;A-=1){let J=l[A];if(!oz(J))continue;r.add(J),$-=1}let j=()=>{if(y.length>=2){let A=yG(y);_.push({seq:Number(y[0]?.seq??0),at:y[0]?.at||y.at(-1)?.at,kind:"toolGroup",title:$G(y),status:`${y.length} calls`,items:y,counts:A,digest:JG(y),rawSeqs:y.flatMap((J)=>Array.isArray(J?.rawSeqs)?J.rawSeqs:[J?.seq]).filter((J)=>J!==void 0)})}else _.push(...y);y=[]};for(let A of l){if(oz(A)&&!r.has(A)){y.push(A);continue}j(),_.push(A)}return j(),_}function UG(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:il(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 H6(f){let u=il(f?.state?.time?.start)??il(f?.time?.start),l=il(f?.state?.time?.end)??il(f?.time?.end);return il(f?.durationMs)??il(f?.elapsedMs)??il(f?.timing?.durationMs)??il(f?.metadata?.durationMs)??(u!==null&&l!==null&&l>=u?l-u:null)??void 0}function O6(f,u){return f?.createdAt||f?.updatedAt||f?.completedAt||u||void 0}function $j(f,u){return f?.id||f?.messageId||u}function mL(f,u=1200){if(typeof f==="string")return f;if(f===void 0||f===null)return"";try{return L$(JSON.stringify(f),u)}catch{return L$(String(f),u)}}function QG(f,u,l){if(typeof f?.metadata?.diff==="string"&&f.metadata.diff.length>0)return f.metadata.diff;if(typeof f?.metadata?.filediff?.patch==="string"&&f.metadata.filediff.patch.length>0)return f.metadata.filediff.patch;if(typeof f?.output==="string"&&f.output.length>0)return f.output;if(typeof f?.result==="string"&&f.result.length>0)return f.result;if(typeof u?.output==="string"&&u.output.length>0)return u.output;if(typeof l?.output==="string"&&l.output.length>0)return l.output;if(typeof f?.metadata?.output==="string"&&f.metadata.output.length>0)return f.metadata.output;return""}function E2(f,u){if(!f||typeof f!=="object"||Array.isArray(f))return"";for(let l of u){let _=f[l];if(typeof _==="string"&&_.length>0)return _;if(_!==void 0&&_!==null&&typeof _!=="object")return String(_)}return""}function fG(f,u){if(!f||typeof f!=="object"||Array.isArray(f))return null;for(let l of u){let _=Number(f[l]);if(Number.isFinite(_))return _}return null}function WG(f,u){let l=u?.input&&typeof u.input==="object"&&!Array.isArray(u.input)?u.input:f?.input&&typeof f.input==="object"&&!Array.isArray(f.input)?f.input:{},_=E2(l,["command","cmd","script"]);if(_.length>0)return _;if(typeof f?.command==="string"&&f.command.length>0)return f.command;if(typeof u?.command==="string"&&u.command.length>0)return u.command;let y=String(f?.tool||f?.title||"tool"),$=E2(l,["filePath","filepath","path"])||E2(f,["filePath","filepath","path"]),r=E2(l,["pattern","query"]),j=fG(l,["offset"]),A=fG(l,["limit"]),J=[y];if(r.length>0)J.push(r);if($.length>0)J.push($);if(j!==null)J.push(`offset=${j}`);if(A!==null)J.push(`limit=${A}`);return J.length>1?J.join(" "):y}function gL(f,u){let l=f?.part&&typeof f.part==="object"&&!Array.isArray(f.part)?f.part:{},_=String(f?.type||f?.event||f?.name||l?.type||"").toLowerCase(),y=String(l?.type||"").toLowerCase(),$=f?.at||f?.timestamp||l?.updatedAt||l?.createdAt,r=Number.isFinite(Number(f?.seq))?Number(f.seq):u;if(_==="step_start"||_==="step-start"||y==="step-start")return null;if(_==="step_finish"||_==="step-finish"||y==="step-finish")return null;if(y==="tool"||/tool|bash|command/iu.test(`${_} ${y}`)){let A=l?.state&&typeof l.state==="object"&&!Array.isArray(l.state)?l.state:{},J=WG(l,A),U=QG(A,l,f),Q=zG(J,String(l?.tool||l?.title||"")),W=Q==="edited"?{status:String(A?.status||l?.status||f?.status||""),summary:L$(U||J,72),files:H2(U),stages:[],lines:Qj(U),addedLines:0,removedLines:0,rawText:U}:void 0;return{seq:r,at:O6(l,$),kind:Q,title:String(A?.title||l?.title||A?.metadata?.description||l?.tool||"OpenCode tool"),status:String(A?.status||l?.status||f?.status||""),commandPreview:J,bodyPreview:U,durationMs:H6(l),rawSeqs:[l?.id||l?.callID||f?.sessionID||r],editObservation:W}}let j=mL(l?.text??l?.content??l?.delta??f?.text??f?.content??f?.delta,3000).trim();if(j.length>0)return{seq:r,at:O6(l,$),kind:y==="reasoning"?"message":/error|failed/iu.test(`${_} ${y}`)?"error":"message",title:y==="reasoning"?"Reasoning":/error|failed/iu.test(`${_} ${y}`)?"OpenCode error":"Assistant message",status:`opencode/${_||y||"event"}`,bodyPreview:j,durationMs:H6(l),rawSeqs:[l?.id||f?.sessionID||r]};return null}function zG(f,u){let l=`${u} ${f}`.toLowerCase();if(/\b(read|grep|glob|list|ls|find|search|view|cat|sed|rg|head|tail|wc|file)\b/iu.test(l))return"explored";if(/\b(edit|write|patch|apply|update|create|delete|apply_patch|git apply|cat >|tee .*<<|sed -i|python3? .*write_text|mkdir|rm |touch )\b/iu.test(l))return"edited";return"ran"}function kL(f){let u=[],l=1;for(let _ of Array.isArray(f)?f:[]){if(_?.kind&&_?.title){let j=String(_?.status||"");if(j==="opencode/step-start"||j==="opencode/step-finish")continue;u.push({..._,seq:Number.isFinite(Number(_?.seq))?Number(_.seq):l++});continue}let y=_?.createdAt||_?.updatedAt||_?.completedAt,$=String(_?.role||"assistant").toLowerCase(),r=Array.isArray(_?.parts)?_.parts:[];if(r.length===0){if(_?.part&&(_?.sessionID||String(_?.type||"").startsWith("step_")||String(_?.type||"").includes("tool"))){let j=gL(_,l);if(j!==null)u.push(j),l=Math.max(l+1,Number(j.seq)+1)}else if(_?.textPreview)u.push({seq:l++,at:y,kind:"message",title:`${$||"assistant"} message`,status:$,bodyPreview:String(_.textPreview),rawSeqs:[_?.messageId||l]});continue}for(let j of r){let A=String(j?.type||"").toLowerCase();if(A==="step-start"||A==="step-finish")continue;if(A==="text"||A==="reasoning"){let U=String(j?.textPreview||_?.textPreview||"").trim();if(U.length===0)continue;u.push({seq:l++,at:O6(j,y),kind:"message",title:A==="reasoning"?"Reasoning":$==="user"?"User message":$==="system"?"System message":"Assistant message",status:A==="reasoning"?"reasoning":$,bodyPreview:U,durationMs:H6(j),rawSeqs:[$j(j,l)]});continue}if(A==="tool"){let U=j?.state&&typeof j.state==="object"&&!Array.isArray(j.state)?j.state:{},Q=WG(j,U),W=QG(U,j,{}),G=zG(Q,String(j?.tool||j?.title||"")),K=G==="edited"?{status:String(U?.status||j?.status||""),summary:L$(W||Q,72),files:H2(W),stages:[],lines:Qj(W),addedLines:0,removedLines:0,rawText:W}:void 0;u.push({seq:l++,at:O6(j,y),kind:G,title:String(j?.title||j?.tool||"tool"),status:String(j?.status||""),commandPreview:Q,bodyPreview:W,durationMs:H6(j),rawSeqs:[$j(j,l)],editObservation:K});continue}let J=String(j?.textPreview||j?.title||A||"").trim();if(J)u.push({seq:l++,at:O6(j,y),kind:"system",title:A||"part",bodyPreview:J,status:String(j?.status||""),durationMs:H6(j),rawSeqs:[$j(j,l)]})}}return u}var GG={source:"opencode",toTrace:kL};function tL(f){return String(f||"unknown").toLowerCase().replace(/[^a-z0-9_-]+/gu,"-")||"unknown"}function uG(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 sL(f){if(f==="item/fileChange/outputDelta")return"delta";return f.replace(/^item\//u,"")}function oL(f,u){if(f.kind==="file"){let y=String(f.status||"M");return Sf("div",{key:`${u}-${f.text}`,className:`codex-edit-diff-line file ${uG(y)}`},Sf("span",{className:`codex-edit-file-status ${uG(y)}`},y),Sf("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":"",_=f.kind==="add"||f.kind==="del"?f.text.slice(1):f.text;return Sf("div",{key:`${u}-${f.text}`,className:`codex-edit-diff-line ${f.kind}`},Sf("span",{className:"codex-edit-diff-sign"},l),Sf("code",null,_||" "))}function aL(f,u){let l=f.lines.length>0?f.lines:f.files.map((y)=>({text:`${y.status} ${y.path}`,kind:"file",path:y.path,status:y.status})),_=Number(f.addedLines||0)+Number(f.removedLines||0)>0;return Sf("div",{className:"codex-edit-observation","data-testid":"codex-edit-observation"},Sf("div",{className:"codex-edit-observation-head"},Sf("span",{className:"codex-edit-window-controls","aria-hidden":"true"},Sf("i",null),Sf("i",null),Sf("i",null)),Sf("strong",null,_?"git diff":"git diff --stat"),Sf("code",null,f.summary||"File changes")),f.stages.length>0?Sf("div",{className:"codex-edit-stage-strip"},f.stages.map((y,$)=>Sf("span",{key:`${y.method}-${$}`,className:`codex-edit-stage ${tL(y.status||y.method)}`},Sf("b",null,sL(y.method)),y.status?Sf("em",null,y.status):null))):null,l.length>0?Sf("div",{className:"codex-edit-diff",role:"list"},l.map(oL)):null,u?Sf("div",{className:"codex-edit-omitted"},`${u} (查看原始JSON获取完整记录)`):null)}function lG(f,u,l){let _=jj(l);return Sf("div",{className:`codex-transcript-stream ${f}`,"data-testid":`codex-trace-${f}`},Sf("span",{className:"codex-transcript-stream-label"},f),Sf("pre",{className:"codex-transcript-body"},u,_?` +${_} (查看原始JSON获取完整记录)`:""))}function KG(f,u=!1){let l=String(f.kind||"message"),_=["ran","explored","edited"].includes(l),y=jj(f.commandOmittedLines),$=jj(f.bodyOmittedLines),r=String(f.commandPreview||(_?f.title||"":"")),j=String(f.stdoutPreview||""),A=String(f.stderrPreview||""),J=j.length>0||A.length>0,U=Boolean(f.foldedReferencePrompt)&&String(f.fullPrompt||"").length>0,Q=l==="edited"&&(f.editObservation!==void 0||AG(f))?f.editObservation||FG([f]):null;return Sf("article",{key:`${f.seq}-${l}`,className:`codex-transcript-item ${l} ${u?"nested":""}`},Sf("div",{className:"codex-transcript-main"},Sf("div",{className:"codex-transcript-title"},Sf("span",{className:"codex-output-channel"},rj(l)),_&&Q===null?null:Sf("strong",null,Q!==null?"File changes":String(f.title||rj(l))),f.status?Sf("code",null,String(Q?.status||f.status)):null,Sf("time",null,Nf(f.at))),r&&Q===null?Sf("pre",{className:"codex-transcript-command"},r,y?` +${y}`:""):null,Q!==null?aL(Q,$):J?Sf("div",{className:"codex-transcript-streams"},j.length>0?lG("stdout",j,f.stdoutOmittedLines):null,A.length>0?lG("stderr",A,f.stderrOmittedLines):null):f.bodyPreview?Sf("pre",{className:"codex-transcript-body"},String(f.bodyPreview),$?` +${$} (查看原始JSON获取完整记录)`:""):null,U?Sf("details",{className:"codex-initial-prompt-full","data-testid":"codex-initial-prompt-full"},Sf("summary",null,Sf("span",null,"引用注入已折叠,点击查看最终传入 Codex 的完整 prompt"),Sf("code",null,`${f.fullPromptLines||nL(String(f.fullPrompt||""))} lines / ${f.fullPromptChars||String(f.fullPrompt||"").length} chars`)),Sf("pre",{className:"codex-transcript-body codex-transcript-full-prompt","data-testid":"codex-initial-prompt-full-text"},String(f.fullPrompt||""))):null))}function dL(f){let u=Array.isArray(f.items)?f.items:[],l=f.digest&&typeof f.digest==="object"?f.digest:JG(u);return Sf("article",{key:`${f.seq}-toolGroup`,className:"codex-transcript-item toolGroup"},Sf("div",{className:"codex-transcript-main"},Sf("details",{className:"codex-tool-group","data-testid":"codex-tool-group"},Sf("summary",null,Sf("div",{className:"codex-tool-group-head"},Sf("span",{className:"codex-output-channel"},rj("toolGroup")),Sf("strong",null,String(f.title||$G(u))),Sf("code",null,String(f.status||`${u.length} calls`)),Sf("time",null,Nf(f.at)))),Sf("div",{className:"codex-tool-group-digest"},Sf("span",null,`read: ${yj(Array.isArray(l.readFiles)?l.readFiles:[])}`),Sf("span",null,`edit: ${yj(Array.isArray(l.editedFiles)?l.editedFiles:[])}`),Sf("span",null,`run: ${yj(Array.isArray(l.runCommands)?l.runCommands:[],2)}`),Sf("span",null,`duration: ${l.durationLabel||"--"}`)),Sf("div",{className:"codex-tool-group-items"},u.map((_)=>KG(_,!0))))))}var eL=16;function _G(f){return f.scrollHeight-f.scrollTop-f.clientHeight<=eL}function O2({items:f,input:u,port:l,autoScroll:_=!1,loading:y=!1,hasDetail:$=!0,emptyText:r="等待 Trace 输出...",loadingText:j="正在加载完整 Trace...",testId:A="trace-output",className:J="codex-transcript",keepRecentToolCalls:U=3,collapseTools:Q=!0}){let W=sz(null),G=sz(!0),K=IL(l?ML(l,u):UG(f)),H=Q?pL(K,U):K,O=SL(K);TL(()=>{let N=W.current;if(!_||!N)return;if(!G.current&&!_G(N))return;N.scrollTop=N.scrollHeight,G.current=!0},[_,K.length,O]);let Z={className:J,ref:W,onScroll:(N)=>{let E=N.currentTarget;G.current=_G(E)},"data-testid":A};if(y&&!$)return Sf("div",Z,Sf("div",{className:"codex-output-empty"},j));return Sf("div",Z,H.length===0?Sf("div",{className:"codex-output-empty"},r):H.map((N)=>String(N.kind||"")==="toolGroup"?dL(N):KG(N)))}var L=Y6.default.createElement,{useEffect:W_,useMemo:NG,useRef:m0}=Y6.default,tf=Y6.default.useState,fX=120,Zj=24,MG=48,uX=1200;function ZG(){return typeof document>"u"||document.visibilityState!=="hidden"}function ll(f,u="操作失败"){return Df(f,u)}function Rl(f){let u=Number(f);if(!Number.isFinite(u)||u<0)return"--";let l=Math.floor(u/1000),_=Math.floor(l/3600),y=Math.floor(l%3600/60),$=l%60;if(_>0)return`${_}h ${String(y).padStart(2,"0")}m`;if(y>0)return`${y}m ${String($).padStart(2,"0")}s`;return`${$}s`}function D$(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 PG(f,u=Date.now()){let l=D$(f);if(l===null)return"--";let _=Math.max(0,Math.floor((u-l)/1000));if(_<1)return"刚刚";let y=Math.floor(_/86400),$=Math.floor(_%86400/3600),r=Math.floor(_%3600/60),j=_%60;if(y>0)return`${y}天${$>0?`${$}小时`:""}前`;if($>0)return`${$}小时${r>0?`${r}分钟`:""}前`;if(r>0)return`${r}分钟${j}秒前`;return`${j}秒前`}function Dj(...f){let u="",l=-1/0;for(let _ of f){let y=String(_||"");if(y.length===0)continue;let $=D$(_);if($!==null&&$>=l)u=y,l=$;else if(u.length===0)u=y}return u}function lX(f,u){let l=D$(f);if(l===null)return!1;let _=D$(u);return _===null||l>_+1}function _X(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 Ej(f,u=180){let l=String(f||"").replace(/\s+/gu," ").trim();return l.length>u?`${l.slice(0,u-1)}…`:l}async function D0(f,u={}){return Mf(f,{strictJson:!0,retryInvalidJson:1,invalidJsonPrefix:"Code Queue 返回了无效 JSON",invalidJsonPreview:!0,responsePreviewLength:uX,...u})}function Wy({status:f,children:u,title:l}){let _=String(f||"unknown").toLowerCase();return L("span",{className:`status-badge ${_}`,title:l},u||f||"unknown")}function B$({title:f,eyebrow:u,summary:l,actions:_,children:y,className:$,loading:r}){return L("section",{className:`panel ${$||""}`},L("div",{className:"panel-head"},L("div",null,u?L("p",{className:"panel-eyebrow"},u):null,L(j0,{title:f,loading:r}),l?L("div",{className:"panel-summary"},l):null),_?L("div",{className:"panel-actions"},_):null),L("div",{className:"panel-body"},y))}function nG({title:f,data:u,onOpen:l,testId:_}){return L("button",{type:"button",className:"ghost-btn","data-testid":_,onClick:()=>l(f,u)},"查看原始JSON")}function zy({title:f,text:u}){return L("div",{className:"empty-state"},L("strong",null,f),L("span",null,u))}function yX(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function $X(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function T0(f,u){return`${f}/code-queue-direct${u}`}function yu(f){return Array.isArray(f?.tasks)?f.tasks:[]}function Q_(f){return f?.pagination&&typeof f.pagination==="object"&&!Array.isArray(f.pagination)?f.pagination:{}}function EG(f){let u=Date.parse(String(f?.updatedAt||f?.createdAt||""));return Number.isFinite(u)?u:0}function SG(f,u=""){let l=new Map;for(let _ of f)for(let y of _){let $=String(y?.id||"");if($.length>0&&!l.has($))l.set($,y)}return Array.from(l.values()).sort((_,y)=>{let $=wG(_)-wG(y);if($!==0)return $;let r=String(_?.id||"")===u?0:1,j=String(y?.id||"")===u?0:1;if(r!==j)return r-j;return EG(y)-EG(_)})}function X$(f,u=""){let l=new Map;for(let _ of f)for(let y of _){let $=String(y?.id||"");if($.length===0)continue;l.set($,{...l.get($)||{},...y})}return SG([Array.from(l.values())],u)}function q6(f){return Array.isArray(f?.activeTaskIds)?f.activeTaskIds.map((u)=>String(u||"")).filter(Boolean):[String(f?.activeTaskId||"")].filter(Boolean)}var G_="__all__",rX="(max-width: 760px)",jX="(min-width: 761px)";function cu(f){return!f||f===G_}function AX(){return typeof window<"u"&&window.matchMedia(rX).matches}function FX(f){return cu(f)?"":`&queueId=${encodeURIComponent(f)}`}function Hj(f){return String(f||"").trim().replace(/\s+/gu," ").slice(0,200)}function JX(f){let u=Hj(f);return u.length===0?"":`&search=${encodeURIComponent(u)}`}function Tj(f,u=""){return`${FX(f)}${JX(u)}`}function V2(f,u){return Number(f?.counts?.[u]||0)}function HG(f,u=""){let l=new Map;for(let y of Array.isArray(f?.queues)?f.queues:[]){let $=String(y?.id||"").trim();if($.length>0)l.set($,{...y,name:String(y?.name||$).trim()||$})}for(let y of[String(f?.defaultQueueId||"default"),u].map(($)=>$.trim()).filter(Boolean))if(!l.has(y))l.set(y,{id:y,name:y,total:0,counts:{},activeTaskId:null,runnableTaskId:null,processing:!1});return Array.from(l.values()).sort((y,$)=>{let r=String(y?.id||"")===String(f?.defaultQueueId||"default")?0:1,j=String($?.id||"")===String(f?.defaultQueueId||"default")?0:1;if(r!==j)return r-j;return String(y?.id||"").localeCompare(String($?.id||""))})}function CG(f){let u=String(f?.id||"default"),l=String(f?.name||"").trim();return l.length>0?l:u}function Oj(f){let u=String(f?.id||"default"),l=CG(f);return l===u?u:`${l} (${u})`}function Vj(f){let u=V2(f,"running")+V2(f,"judging"),l=V2(f,"queued")+V2(f,"retry_wait"),_=Number(f?.total||0),y=[Oj(f),`${_} tasks`];if(u>0)y.push(`${u} running`);if(l>0)y.push(`${l} queued`);return y.join(" · ")}function L6(f,u){if(cu(u))return null;return f.find((l)=>String(l?.id||"")===u)||null}function OG(f,u,l,_){if(cu(l)){let $=q6(f);return String(f?.activeTaskId||$[0]||_.find((r)=>BG(r))?.id||"")}let y=L6(u,l);return String(y?.activeTaskId||_.find(($)=>BG($))?.id||"")}function UX(f,u,l){if(!cu(u)){let _=L6(f,u);return String(_?.runnableTaskId||l.find((y)=>String(y?.status||"")==="queued"||String(y?.status||"")==="retry_wait")?.id||"")}return String(l.find((_)=>String(_?.status||"")==="queued"||String(_?.status||"")==="retry_wait")?.id||"")}async function VG(f,u,l=G_,_=""){let y=Tj(l,_);try{return await D0(T0(f,`/api/tasks?limit=${Zj}&lite=1&devReady=0${y}`))}catch{let r=await Promise.all(["running","judging","retry_wait","queued"].map(async(U)=>{try{return await D0(T0(f,`/api/tasks?status=${encodeURIComponent(U)}&limit=80&lite=1&devReady=0${y}`))}catch{return null}})),j=await D0(T0(f,`/api/tasks?limit=${Zj}&lite=1&devReady=0${y}`)).catch(()=>null),A=r.find((U)=>U?.queue)?.queue||j?.queue||u?.queue||u?.body?.queue||{},J=SG([...r.map((U)=>yu(U)),yu(j)],String(A?.activeTaskId||""));if(J.length>0)return{ok:!0,queue:A,statistics:j?.statistics||r.find((U)=>U?.statistics)?.statistics||null,tasks:J};return D0(T0(f,`/api/tasks?limit=5&lite=1&devReady=0${y}`))}}async function QX(f,u,l=0,_=G_,y=""){return D0(T0(f,`/api/tasks/overview?limit=${Zj}&transcriptLimit=3&compact=1&afterSeq=${encodeURIComponent(String(Math.max(0,l)))}&preferId=${encodeURIComponent(u)}${Tj(_,y)}`))}async function qG(f,u,l,_=MG,y=""){return D0(T0(f,`/api/tasks?limit=${encodeURIComponent(String(_))}&lite=1&devReady=0&includeActive=0&beforeId=${encodeURIComponent(l)}${Tj(u,y)}`))}async function WX(f,u){return D0(T0(f,`/api/tasks/${encodeURIComponent(u)}/trace-summary`))}async function zX(f,u,l,_=null){let y=_===null||_===void 0||String(_).length===0?"":`&attempt=${encodeURIComponent(String(_))}`;return D0(T0(f,`/api/tasks/${encodeURIComponent(u)}/prompt?part=${encodeURIComponent(l)}${y}`))}async function GX(f,u,l=0,_=500,y=null){let $=y===null||y===void 0||String(y).length===0?"":`&attempt=${encodeURIComponent(String(y))}`;return D0(T0(f,`/api/tasks/${encodeURIComponent(u)}/trace-steps?afterSeq=${encodeURIComponent(String(l))}&limit=${encodeURIComponent(String(_))}${$}`))}async function KX(f,u,l){return D0(T0(f,`/api/tasks/${encodeURIComponent(u)}/trace-step?seq=${encodeURIComponent(String(l))}`))}async function NX(f,u){return D0(T0(f,`/api/tasks/${encodeURIComponent(u)}/read`),{method:"POST",body:{}})}async function ZX(f){return D0(T0(f,"/api/tasks/read-all"),{method:"POST",body:{}})}function EX(f){return Array.isArray(f?.output)?f.output:[]}function iG(f){return Array.isArray(f?.attempts)?f.attempts:[]}function Wj(f){return f?.counts&&typeof f.counts==="object"&&!Array.isArray(f.counts)?f.counts:{}}function HX(f){return f.split(/^\s*---+\s*$/gmu).map((u)=>u.trim()).filter(Boolean)}function LG(f){let u=Number(f);return Number.isFinite(u)?Math.max(1,Math.min(50,Math.floor(u))):1}function Jy(f){let u=[];for(let l of f.split(/[\s,,;;]+/u)){let _=l.trim();if(/^codex_\d+_[A-Za-z0-9_-]+$/u.test(_)&&!u.includes(_))u.push(_)}return u}function OX(f,u){let l=Jy(u);if(l.length===0)return f;return[`引用 Code Queue 任务 ${l.join(" ")}。后端会在入队时只注入这些任务的 initial prompt 和 final response 全文;中间执行过程不注入,如需补充核查可运行:${l.map((_)=>`bun scripts/cli.ts codex task ${_}`).join(";")}`,"","本次任务:",f].join(` +`)}function VX(f){let _=f.trimStart();if(!_.startsWith("# Code Queue 已解析引用上下文"))return{hasInjection:!1,reference:"",userPrompt:f};let y=f.length-_.length,$=f.lastIndexOf(` # 本次任务 -`);if(_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=` +本次任务:`,_=u.indexOf(l);if(_===-1)return f;return u.slice(_+l.length).trimStart()}function Qy(f){return f.length>0?f.split(/\r\n|\r|\n/u).length:0}function cG(f){let u=String(f?.displayPrompt||"");if(u.length>0)return u;let l=String(f?.prompt||"");return qX(VX(l).userPrompt)}function _l(f){return f?._traceSummary&&typeof f._traceSummary==="object"&&!Array.isArray(f._traceSummary)?f._traceSummary:null}function D2(f){return f?._promptDetails&&typeof f._promptDetails==="object"&&!Array.isArray(f._promptDetails)?f._promptDetails:{}}function Mj(f){let u=_l(f)?.prompt;return u&&typeof u==="object"&&!Array.isArray(u)?u:{}}function T2(f){let u=_l(f)?.execution;return u&&typeof u==="object"&&!Array.isArray(u)?u:{}}function M2(f){let u=Number(f?.stepCount??f?.llmStepCount??0);return Number.isFinite(u)&&u>=0?Math.floor(u):0}function qj(f){let u=Bu(f?.execution)||{},l=Number(f?.stepCount??f?.llmStepCount??u.stepCount??u.llmStepCount??u.toolCallCount??0);return Number.isFinite(l)&&l>=0?Math.floor(l):0}function q2(f){if(!f||f?._traceSummaryLoaded!==!0)return!1;let u=_l(f),l=String(f?._traceSummaryUpdatedAt||u?.updatedAt||""),_=String(f?.updatedAt||"");if(_.length>0){let $=D$(l),r=D$(_);if($!==null&&r!==null){if($+1=y}function B2(f){let u=Mj(f),l=String(u.basePrompt||"");return l.length>0?l:cG(f)}function Lj(f){let u=_l(f);return String(u?.finalResponse||f?.finalResponse||"").trimEnd()}function Xj(f){let l=_l(f)?.lastJudge||f?.lastJudge;return l&&typeof l==="object"&&!Array.isArray(l)?l:null}function Bu(f){return f&&typeof f==="object"&&!Array.isArray(f)?f:null}function Pj(f){let u=_l(f)?.attempts;if(Array.isArray(u)&&u.length>0)return u;let l=iG(f);if(l.length>0)return l.map((r,j)=>({...r,index:Number(r?.index||j+1),execution:j===l.length-1?T2(f):Bu(r?.execution)||{},finalResponse:String(r?.finalResponse||r?.finalResponsePreview||(j===l.length-1?Lj(f):"")),judge:Bu(r?.judge)||(j===l.length-1?Xj(f):null)}));let _=T2(f),y=Lj(f),$=Xj(f);if(Object.keys(_).length===0&&y.length===0&&$===null)return[];return[{index:Number(f?.currentAttempt||1),mode:f?.currentMode||"initial",startedAt:f?.startedAt,finishedAt:f?.finishedAt,terminalStatus:f?.status,execution:_,finalResponse:y,finalResponseChars:y.length,judge:$}]}function RG(f,u){return Bu(u?.execution)||T2(f)}function LX(f,u,l,_){let y=_l(f),$=Number(y?.currentAttempt||f?.currentAttempt||0),r=Number(l),j=Number.isFinite(r)&&r>0&&r===$,A=Dj(f?.updatedAt,y?.updatedAt);if(j&&!u?.finishedAt&&A.length>0)return A;return String(u?.updatedAt||u?.finishedAt||_.effectiveEndAt||(j?A:"")||A||f?.finishedAt||f?.startedAt||"")}function XX(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():Lj(f)}function xG(f,u){if(Object.prototype.hasOwnProperty.call(u||{},"judge"))return Bu(u?.judge);return Xj(f)}function BX(f,u,l){if(!hX(f))return!1;if(B6(u,l))return!1;if(u?.finishedAt)return!1;if(["succeeded","failed","canceled"].includes(String(u?.terminalStatus||"")))return!1;let _=_l(f),y=Number(_?.currentAttempt||f?.currentAttempt||0),$=Number(l);if(Number.isFinite($)&&$>0&&Number.isFinite(y)&&y>0)return $===y;return!0}function bG(f){return`feedback:${String(f||"latest")}`}function YX(f,u,l){let _=String(u?.feedbackPrompt||"").trimEnd(),y=String(u?.feedbackPromptPreview||_||"").trimEnd(),$=Number(u?.feedbackPromptChars||_.length||y.length||0),r=Number(u?.feedbackPromptLines||Qy(_||y));if(_.length>0||y.length>0||$>0)return{text:_,preview:y,chars:$,lines:r,source:u?.feedbackPromptSource||"judge-feedback",forAttempt:u?.feedbackPromptForAttempt||Number(l||0)+1,truncated:Boolean(u?.feedbackPromptTruncated)};let j=xG(f,u),A=String(j?.continuePrompt||"").trimEnd();if(j?.decision==="retry"&&A.length>0)return{text:"",preview:A,chars:A.length,lines:Qy(A),source:"judge-continue-prompt",forAttempt:Number(l||0)+1,truncated:!1};return null}function wX(f){let u=Mj(f);return Boolean(u.hasReferenceInjection||Number(u.referencePromptChars||0)>0||f?.referenceInjection||f?.referenceInjectionSummary)}function DX(f,u=null){if(u!==null&&u!==void 0){let _=(Bu(f?._traceStepsByAttempt)||{})[String(u)];return Array.isArray(_)?_:[]}return Array.isArray(f?._traceSteps)?f._traceSteps:[]}function w$(f){return(Array.isArray(f?.summaryLines)?f.summaryLines:[]).map((u)=>String(u||""))}function vG(f){let u=String(f?.kind||"").trim().toLowerCase(),l=String(f?.status||"").trim().toLowerCase();return u==="error"||l==="error"}function TX(f){return(Array.isArray(f)?f:[]).reduce((u,l)=>u+(vG(l)?1:0),0)}function P2(f){let u=String(f?.status||"").trim();if(u.length>0)return u;let l=w$(f).join(` +`);return/^(item\/[A-Za-z]+(?:\/[A-Za-z]+)?):/u.exec(l)?.[1]||""}function XG(f){return/^item\/(?:started|completed): file changes status=/u.test(String(f||"").trim())}function MX(f){let u=w$(f);for(let _=u.length-1;_>=0;_-=1){let y=/file changes status=([A-Za-z0-9_-]+)/u.exec(u[_]||"")?.[1];if(y)return y}let l=P2(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 PX(f){if(String(f?.kind||"")!=="edited")return!1;let u=String(f?.title||""),l=String(f?.status||""),_=w$(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(_))return!0;if(/^Success\. Updated the following files:/mu.test(_))return!0;if(/^diff --git /mu.test(_))return!0;return/^([AMDRCU?]{1,2})\s+\S+/mu.test(_)}function nX(f){if(f.length<=1)return f[0];let u=f.find(($)=>P2($)==="item/fileChange/outputDelta")||f.find(($)=>w$($).some((r)=>!XG(r)))||f.at(-1)||f[0],l=f.flatMap(($)=>Array.isArray($?.rawSeqs)?$.rawSeqs:[$?.seq]).filter(($)=>$!==void 0),_=f.flatMap(w$).filter(($)=>$.trim().length>0&&!XG($)),y=f[f.length-1]||u;return{...u,at:u?.at||y?.at,title:String(u?.title||"Edited files"),status:MX(y),summaryLines:_.length>0?_:w$(u),rawSeqs:l}}function SX(f){let u=Array.isArray(f)?f:[],l=[],_=[],y=()=>{if(_.length>0)l.push(nX(_));_=[]};for(let $ of u){if(PX($)){if(P2($)==="item/started"&&_.length>0)y();if(_.push($),P2($)==="item/completed")y();continue}y(),l.push($)}return y(),l}function Bj(f){let u=Number(f?.toolCallCount);if(Number.isFinite(u)&&u>=0)return Math.floor(u);let l=Number(f?.readCount||0)+Number(f?.editCount||0)+Number(f?.runCount||0);return Number.isFinite(l)&&l>=0?Math.floor(l):0}function CX(f){let l=(Array.isArray(f)?f:[]).reduce((_,y)=>{let $=String(y?.kind||"");if($==="explored")_.readCount+=1;else if($==="edited")_.editCount+=1;else if($==="ran")_.runCount+=1;return _},{readCount:0,editCount:0,runCount:0});return l.toolCallCount=l.readCount+l.editCount+l.runCount,l}function iX(f,u){if(u.length===0){let _=Bj(f);return{...f,stepCount:_,llmStepCount:_}}let l=CX(u);return{...f,...l,stepCount:l.toolCallCount,llmStepCount:l.toolCallCount,traceLineCount:u.length}}function hG(f,u=null){if(u!==null&&u!==void 0){let l=Bu(f?._traceStepsLoadedByAttempt)||{};return Boolean(l[String(u)])}return Boolean(f?._traceStepsLoaded)}function Yj(f){return f?._traceStepDetails&&typeof f._traceStepDetails==="object"&&!Array.isArray(f._traceStepDetails)?f._traceStepDetails:{}}function cX(f,u){let l=Number(f?.index);return Number.isFinite(l)?l:u+1}function B6(f,u){return Boolean(f?.synthetic)||Number(u)<=0}function S2(f){let u=Number(f);return Number.isFinite(u)?String(u):void 0}function RX(f){let u=f?.timing&&typeof f.timing==="object"?f.timing:{},l=String(f?.status||"");if(["queued"].includes(l))return`等待 ${Rl(u.queueWaitMs??u.totalElapsedMs)}`;if(["running","judging","retry_wait"].includes(l))return`耗时 ${Rl(u.durationMs??u.totalElapsedMs)}`;return`耗时 ${Rl(u.durationMs??u.totalElapsedMs)}`}function n2(f){return String(f?.queueId||"default")}function IG(f){return Bu(f?.queuedReason)}function pG(f){let u=String(f?.queuedReasonLabel||"").trim();if(u.length>0)return u.toUpperCase();let l=IG(f),_=String(l?.label||"").trim();return _.length>0?_.toUpperCase():""}function xX(f){let u=String(f?.status||"unknown");if(u!=="queued")return u;let l=pG(f);return l.length>0?`QUEUED(${l})`:"QUEUED"}function bX(f){if(String(f?.status||"")!=="queued")return;let u=IG(f),l=String(u?.message||"").trim(),_=pG(f);if(l.length>0&&_.length>0)return`${_}: ${l}`;if(l.length>0)return l;return _.length>0?_:void 0}function vX(f){return{system:"SYS",user:"YOU",assistant:"GPT",reasoning:"THINK",command:"CMD",diff:"DIFF",tool:"TOOL",error:"ERR"}[f]||f.toUpperCase()}function BG(f){return["running","judging","retry_wait"].includes(String(f?.status||""))}function hX(f){return String(f?.status||"")==="running"}function cl(f){return["succeeded","failed","canceled"].includes(String(f?.status||""))}function mG(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 M1(f){if(!cl(f))return!1;if(f?.terminalUnread===!0)return!0;if(f?.terminalUnread===!1)return!1;return!f?.readAt}function R0(f){let u=Number(f||0);return Number.isFinite(u)?u:0}function IX(f){return R0(f.queued)+R0(f.retry_wait)}function pX(f){return R0(f.running)+R0(f.judging)}function mX(f,u){return Bu(f?.statistics)||Bu(u?.statistics)||{}}function gX(f){return Array.isArray(f?.daily)?f.daily:[]}function kX(f){return Bu(f?.totals)||{}}function nj(f,u){let l=Number(f?.[u]??0);return Number.isFinite(l)&&l>0?l:0}function zj(f,u){return f.reduce((l,_)=>Math.max(l,nj(_,u)),0)}var Uy=700,YG=220,z_=30,Y$=24,X6=184,wj=X6-Y$;function gG(f,u){if(u<=1)return Uy/2;return z_+f*(Uy-z_*2)/(u-1)}function kG(f,u){let l=u>0?u:1;return X6-Math.min(1,f/l)*wj}function Gj(f,u,l){let _=f.length>0?f:[{[u]:0}],y=_.length>1?_:[_[0],_[0]];return y.map(($,r)=>`${gG(r,y.length).toFixed(2)},${kG(nj($,u),l).toFixed(2)}`).join(" ")}function T1(f){let u=String(f||"");return/^\d{4}-\d{2}-\d{2}$/u.test(u)?u.slice(5):u||"--"}function L2(f){if(!f)return"";return`${String(f.seriesKey||"")}:${String(f.row?.date||f.index||"")}`}function tX(f,u,l,_){let y=nj(f,_.key);return{..._,row:f,index:u,value:y,valueLabel:_.format(y),x:gG(u,l),y:kG(y,_.max),seriesKey:_.key}}function wG(f){if(M1(f))return 0;return{running:1,judging:2,retry_wait:3,queued:4,succeeded:8,failed:8,canceled:8}[String(f?.status||"")]??9}function V6(f){if(!f)return!1;if(f?._traceSummaryLoaded===!0)return!1;return f?.summaryOnly===!0||f?._metaLoaded!==!0}function sX(f){return Boolean(f?._metaLoaded)||f?.summaryOnly===!1}function tG(f,u,l){let _=String(f?.[l]||""),y=String(u?.[l]||"");return _.length>y.length?_:y}function Y2(f,u,l){let _=Array.isArray(f?.[l])?f[l]:[],y=Array.isArray(u?.[l])?u[l]:[];if(y.length===0&&_.length>0)return _;return _.length>y.length?_:y}function oX(f,u){let l=u?.summaryOnly===!0&&sX(f),_={...f,...u};if(!l)return _;for(let y of["prompt","basePrompt","displayPrompt","finalResponse"])_[y]=tG(f,u,y);for(let y of["promptHistory","attempts","output","events"])_[y]=Y2(f,u,y);if(f?.referenceInjection?.items&&!u?.referenceInjection?.items)_.referenceInjection=f.referenceInjection;if(f?.referenceInjectionSummary&&!u?.referenceInjectionSummary)_.referenceInjectionSummary=f.referenceInjectionSummary;_.summaryOnly=f?.summaryOnly===!1?!1:u.summaryOnly,_._metaLoaded=f?._metaLoaded,_._detailLoaded=f?._detailLoaded,_._transcriptComplete=f?._transcriptComplete,_._transcriptPreview=Object.prototype.hasOwnProperty.call(u,"_transcriptPreview")?u._transcriptPreview:f?._transcriptPreview;for(let y of["_traceSummary","_traceSummaryLoaded","_traceSteps","_traceStepsLoaded","_traceStepsByAttempt","_traceStepsLoadedByAttempt","_traceStepDetails","_promptDetails"])if(!Object.prototype.hasOwnProperty.call(u,y)&&Object.prototype.hasOwnProperty.call(f||{},y))_[y]=f[y];return _}function aX(f){let u=f?.selected,l=u?.task&&typeof u.task==="object"?u.task:null;if(l!==null){let y=Boolean(u?.preview);return{...l,transcript:Array.isArray(u?.transcript)?u.transcript:[],_detailLoaded:Array.isArray(u?.transcript)&&u.transcript.length>0,_transcriptComplete:Boolean(!y&&!u?.hasMore&&cl(l)),_transcriptPreview:y,_summaryLoaded:!0}}let _=yu(f)[0];return _?{..._,_summaryLoaded:!0}:null}function Kj(f,u){let l=new Map;for(let _ of[...Array.isArray(f)?f:[],...Array.isArray(u)?u:[]]){let y=`${Number(_?.seq??0)}:${String(_?.kind||"message")}`,$=l.get(y);if(!$){l.set(y,_);continue}let r={...$,..._};for(let[j,A]of[["bodyPreview","bodyOmittedLines"],["commandPreview","commandOmittedLines"]]){let J=String($?.[j]||""),U=String(_?.[j]||"");if(J.length>U.length)r[j]=$[j],r[A]=$[A]}l.set(y,r)}return Array.from(l.values()).sort((_,y)=>Number(_?.seq??0)-Number(y?.seq??0))}function X2(f){return(Array.isArray(f)?f:[]).reduce((u,l)=>Math.max(u,Number(l?.seq??0)),0)}function DG(f,u=8){let l=Array.from(new Set((Array.isArray(f)?f:[]).map((y)=>Number(y?.seq??0)).filter((y)=>Number.isFinite(y)&&y>0))).sort((y,$)=>y-$);if(l.length===0)return 0;let _=l[Math.max(0,l.length-u)]??0;return Math.max(0,_-0.001)}function dX(f,u){let l=Array.isArray(f?.codeModels)?f.codeModels:Array.isArray(f?.codexModels)?f.codexModels:[],_=["gpt-5.5","gpt-5.4-mini","gpt-5.4","minimax-m2.7"];return Array.from(new Set([...l,..._,u].map((y)=>String(y||"").trim()).filter(Boolean)))}function eX(f,u){let _=(Array.isArray(f?.executionProviders)?f.executionProviders:[]).map((r)=>({id:String(r?.id||"").trim(),label:String(r?.label||r?.id||"").trim(),defaultWorkdir:String(r?.defaultWorkdir||"").trim(),kind:String(r?.kind||"").trim()})).filter((r)=>r.id.length>0),y=String(f?.mainProviderId||f?.defaultProviderId||"main-server").trim()||"main-server",$=new Map;for(let r of[..._,{id:y,label:`${y} (master)`,defaultWorkdir:String(f?.defaultWorkdir||"/root/unidesk"),kind:"local"},u?{id:u,label:u,defaultWorkdir:w2(f,u),kind:""}:null].filter(Boolean))if(!$.has(r.id))$.set(r.id,r);return Array.from($.values())}function w2(f,u){let l=String(u||"").trim(),_=f?.defaultWorkdirByProvider&&typeof f.defaultWorkdirByProvider==="object"?f.defaultWorkdirByProvider:{};if(typeof _[l]==="string"&&String(_[l]).trim().length>0)return String(_[l]).trim();let y=Array.isArray(f?.executionProviders)?f.executionProviders.find((r)=>String(r?.id||"")===l):null;if(typeof y?.defaultWorkdir==="string"&&y.defaultWorkdir.trim().length>0)return y.defaultWorkdir.trim();let $=String(f?.mainProviderId||f?.defaultProviderId||"main-server");return l===$?String(f?.defaultWorkdir||"/root/unidesk"):String(f?.remoteDefaultWorkdir||"/home/ubuntu")}function fB(f){let u=Pj(f).filter((y)=>!B6(y,y?.index));if(u.length>0){let y=u.reduce(($,r)=>$+Bj(RG(f,r)),0);if(y>0)return y}let l=Bj(T2(f));if(l>0)return l;let _=Number(f?.stepCount??f?.llmStepCount??0);return Number.isFinite(_)&&_>=0?Math.floor(_):0}function uB({task:f,selected:u,onSelect:l,onCopy:_,onReference:y,onMarkRead:$,copied:r,markingRead:j}){let A=f?.lastJudge||{},J=String(f?.id||""),U=M1(f),Q=Dj(f?.updatedAt,_l(f)?.updatedAt),W=`最近更新: ${PG(Q)}`,G=fB(f);return L("article",{role:"button",tabIndex:0,className:`codex-task-card ${u?"selected":""} ${U?"unread-terminal":""}`,onClick:l,onKeyDown:(K)=>{if(K.key==="Enter"||K.key===" ")K.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-${J||"unknown"}`}):null,L("div",{className:"codex-task-card-head"},L("div",{className:"codex-task-status-line"},L(Wy,{status:f?.status,title:bX(f)},xX(f))),L("span",{className:"mono-text"},`${f?.currentAttempt||0}/${f?.maxAttempts||0}`)),L("div",{className:"codex-task-id-row"},L("code",{title:J},J||"unknown"),L("div",{className:"codex-task-id-actions"},L("button",{type:"button",className:"codex-copy-id-btn",onClick:(K)=>{K.stopPropagation(),y(J)},"data-testid":`codex-reference-task-${J||"unknown"}`},"引用"),L("button",{type:"button",className:"codex-copy-id-btn",onClick:(K)=>{K.stopPropagation(),_(J)},"data-testid":`codex-copy-task-id-${J||"unknown"}`},r?"已复制":"复制ID"),U?L("button",{type:"button",className:"codex-copy-id-btn codex-mark-read-btn",disabled:Boolean(j),onClick:(K)=>{K.stopPropagation(),$(J)},"data-testid":`codex-mark-task-read-${J||"unknown"}`},j?"标记中":"标为已读"):null)),L("strong",null,Ej(cG(f),120)||"空任务"),L("div",{className:"codex-task-meta"},L("span",null,`queue=${n2(f)}`),L("span",null,`provider=${f?.providerId||"main-server"}`),L("span",null,f?.model||"--"),L("span",null,RX(f))),L("div",{className:"codex-task-meta codex-task-update-meta"},L("span",{className:"codex-task-recent-update codex-task-step-count",title:"STEP 按 read/edit/run 工具动作统计","data-testid":`codex-task-step-count-${J||"unknown"}`},`STEP ${G}`),L("span",{className:"codex-task-recent-update",title:Q?`更新时间: ${Nf(Q)}`:W,"data-testid":`codex-task-recent-update-${J||"unknown"}`},W),L("span",null,Nf(Q||f?.updatedAt))),mG(f)?L("div",{className:"codex-judge-line","data-testid":`codex-task-prompt-editable-${J||"unknown"}`},"queued prompt 可编辑"):null,A?.decision?L("div",{className:"codex-judge-line"},`judge=${A.decision} ${Math.round(Number(A.confidence||0)*100)}%`):null)}function Nj({title:f,tasks:u,selectedId:l,onSelect:_,onCopy:y,onReference:$,onMarkRead:r,copiedTaskId:j,markingReadTaskId:A,emptyText:J}){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"},J):L("div",{className:"codex-task-section-list"},U.map((Q)=>L(uB,{key:Q.id,task:Q,selected:l===Q.id,onSelect:()=>_(Q.id),onCopy:y,onReference:$,onMarkRead:r,copied:j===Q.id,markingRead:A===Q.id}))))}function lB(){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 _B({stats:f,queueName:u,onRaw:l}){let _=gX(f),y=kX(f),$=_.at(-1)||{},r=zj(_,"executedTasks"),j=zj(_,"retryAttempts"),A=zj(_,"avgDurationMs"),J=_.length>0,U=Bu(f?.range)||{},[Q,W]=tf(null),[G,K]=tf(null),H=[];if(r>0)H.push(`tasks ${r}`);if(j>0)H.push(`retry ${j}`);if(A>0)H.push(`avg ${Rl(A)}`);let O=[{key:"executedTasks",className:"tasks",label:"执行任务",max:r,format:(B)=>`${R0(B)} tasks`},{key:"retryAttempts",className:"retry",label:"重试次数",max:j,format:(B)=>`${R0(B)} retries`},{key:"avgDurationMs",className:"duration",label:"平均耗时",max:A,format:(B)=>Rl(B)}],z=Q||G,Z=L2(z),N=String(z?.row?.date||""),E=z?{left:`${Math.max(8,Math.min(92,Number(z.x)/Uy*100))}%`,top:`${Math.max(14,Math.min(86,Number(z.y)/YG*100))}%`}:void 0;W_(()=>{W(null),K(null)},[u,U.startDate,U.endDate,_.length]);let q=(B)=>{W(B)},Y=(B)=>{let P=L2(B);K((h)=>L2(h)===P?null:B),W(B)},w=O.flatMap((B)=>_.map((P,h)=>{let M=tX(P,h,_.length,B),n=L2(M),S=Z===n,T=String(P?.date||`day-${h}`),i=`${T1(T)} ${B.label}: ${M.valueLabel}`;return L("g",{key:`${B.key}-${T}`,className:`stat-point-group ${B.className} ${S?"active":""}`,role:"button",tabIndex:0,"aria-label":i,"data-testid":`codex-stats-point-${B.className}-${T}`,onMouseEnter:()=>q(M),onFocus:()=>q(M),onClick:()=>Y(M),onKeyDown:(C)=>{if(C.key==="Enter"||C.key===" ")C.preventDefault(),Y(M)}},L("circle",{className:"stat-hit-point",cx:M.x,cy:M.y,r:13}),L("circle",{className:`stat-point ${B.className} ${S?"active":""}`,cx:M.x,cy:M.y,r:S?5.6:4.2}))}));return L(B$,{title:"统计曲线",eyebrow:`Daily task stats / ${u}`,className:"codex-stats-panel",summary:L("span",null,`${T1(U.startDate)} -> ${T1(U.endDate)} · ${f?.timezone||"Asia/Shanghai"}`),actions:Bu(f)?L(nG,{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(lB),L("div",null,L("strong",null,`${R0(y.executedTasks)} tasks / ${R0(y.retryAttempts)} retries`),L("span",null,`平均完成耗时 ${Rl(y.avgDurationMs??void 0)} · 终态 ${R0(y.completedTasks)} 个`))),J?L("div",{className:"codex-stats-chart","data-testid":"codex-stats-chart",onMouseLeave:()=>W(null)},L("svg",{viewBox:`0 0 ${Uy} ${YG}`,preserveAspectRatio:"none",role:"img","aria-label":"Code Queue daily task statistics"},L("line",{className:"axis",x1:z_,x2:Uy-z_,y1:X6,y2:X6}),L("line",{className:"grid",x1:z_,x2:Uy-z_,y1:Y$+wj/2,y2:Y$+wj/2}),L("line",{className:"grid",x1:z_,x2:Uy-z_,y1:Y$,y2:Y$}),L("polyline",{className:"stat-line tasks",points:Gj(_,"executedTasks",r)}),L("polyline",{className:"stat-line retry",points:Gj(_,"retryAttempts",j)}),L("polyline",{className:"stat-line duration",points:Gj(_,"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:Y$,y2:X6}),L("circle",{className:`stat-point-active ${z.className}`,cx:z.x,cy:z.y,r:8})):null,L("g",{className:"stat-point-layer"},w)),z?L("div",{className:"codex-stats-tooltip active",style:E,"data-testid":"codex-stats-tooltip"},L("b",null,T1(z.row?.date)),L("span",null,`${z.label} · ${z.valueLabel}`),L("code",null,`${R0(z.row?.executedTasks)} exec / ${R0(z.row?.retryAttempts)} retry / ${Rl(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,T1(_[0]?.date)),L("span",null,H.join(" · ")||"暂无峰值"),L("span",null,T1(_.at(-1)?.date))),L("div",{className:`codex-stats-focus ${z?"active":""}`,"data-testid":"codex-stats-focus"},z?L(Y6.default.Fragment,null,L("div",null,L("strong",null,T1(z.row?.date)),L("span",null,`${z.label} · ${z.valueLabel}`)),L("div",{className:"codex-stats-focus-metrics"},L("code",null,`${R0(z.row?.executedTasks)} exec`),L("code",null,`${R0(z.row?.retryAttempts)} retry`),L("code",null,Rl(z.row?.avgDurationMs??void 0)))):L("span",null,"将鼠标悬停到曲线数据点查看明细,点击数据点可固定。"))):L(zy,{title:"暂无统计",text:"任务开始执行后会生成按天汇总的曲线。"}),L("div",{className:"codex-stats-summary-grid"},L("article",null,L("span",null,"今日执行"),L("strong",null,String(R0($.executedTasks))),L("code",null,T1($.date))),L("article",null,L("span",null,"今日重试"),L("strong",null,String(R0($.retryAttempts))),L("code",null,`累计 ${R0(y.retryAttempts)}`)),L("article",null,L("span",null,"平均耗时"),L("strong",null,Rl(y.avgDurationMs??void 0)),L("code",null,`${R0(y.durationSamples)} samples`))),L("div",{className:"codex-stats-daily-list","data-testid":"codex-stats-daily-list"},_.slice(-7).map((B)=>L("div",{key:String(B?.date||""),className:`codex-stats-daily-row ${N===String(B?.date||"")?"active":""}`,"data-testid":`codex-stats-day-${String(B?.date||"unknown")}`},L("span",null,T1(B?.date)),L("b",null,`${R0(B?.executedTasks)} exec`),L("b",null,`${R0(B?.retryAttempts)} retry`),L("code",null,Rl(B?.avgDurationMs??void 0))))))}function yB({task:f,queueRows:u,busy:l,onMove:_}){let y=String(f?.id||""),$=n2(f),[r,j]=tf($);W_(()=>{j($)},[y,$]);let A=!y||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:r,disabled:!y||l,onChange:(J)=>j(String(J.target.value||$)),"data-testid":"codex-task-queue-move-select"},u.map((J)=>L("option",{key:String(J?.id||""),value:String(J?.id||"")},Vj(J))))),L("button",{type:"button",className:"ghost-btn",disabled:A||r===$,onClick:()=>_(r),title:A?"运行中 / judging / retry_wait 的任务不能移动;请先打断或等当前 turn 结束":"移动已创建任务到另一个 queue","data-testid":"codex-task-queue-move-button"},"移动"))}function TG(f,u=4){let l=(Array.isArray(f)?f:[]).map((y)=>String(y||"").trim()).filter(Boolean);if(l.length===0)return"--";let _=l.slice(0,u).join(" / ");return l.length>u?`${_} +${l.length-u}`:_}function $B({task:f,loading:u,onLoadPromptPart:l,testId:_="codex-initial-prompt-full",textTestId:y="codex-initial-prompt-full-text",baseTextTestId:$="codex-initial-prompt-base"}){let r=Mj(f),j=D2(f),A=B2(f).trimEnd(),J=String(j.full?.text||""),U=wX(f),Q=Number(r.promptChars||f?.promptChars||J.length),W=Number(r.basePromptLines||Qy(A)),G=Number(r.promptLines||Qy(J));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||Qy(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":_,onToggle:(K)=>{if(K.currentTarget?.open&&!J)l?.("full")}},L("summary",null,L("span",null,"引用注入已折叠,点击按需拉取最终进入 Code agent 的完整 prompt"),L("code",null,J?`${G||Qy(J)} lines / ${J.length} chars`:`${Number.isFinite(Q)&&Q>0?Q:"--"} chars`)),L("pre",{className:"codex-prompt-full codex-prompt-final-full","data-testid":y},J||(u?"正在按需拉取完整 prompt...":"展开后将只请求 full prompt,不拉取完整 transcript。"))):null)}function sG({task:f,attempt:u,attemptIndex:l,loading:_,onLoadSteps:y,onLoadStep:$,testId:r="codex-execution-summary"}){let j=SX(DX(f,l)),A=iX(RG(f,u),j),J=_l(f),U=Yj(f),Q=hG(f,l),W=Number(u?.errorCount??J?.errorCount??TX(j)),G=Number(A.toolCallCount||0),K=Number(A.stepCount??A.llmStepCount??0),H=Number.isFinite(K)&&K>=0?Math.floor(K):0,O=Array.isArray(A.editedFiles)?A.editedFiles:[],z=Array.isArray(A.commands)?A.commands:[],Z=B6(u,l),N=Z?` · ${String(u?.label||"recovered thread execution")}`:l?` #${l}`:"",E=LX(f,u,l,A),q=`最近更新: ${PG(E)}`,Y=BX(f,u,l),w=M2(f),B=Pj(f).filter((n)=>!B6(n,n?.index)).length,P=!Z&&j.length===0&&w>0&&B<=1,h=P?Math.max(H,w):H,M=P?Math.max(G,h):G;return L("details",{className:`codex-progressive-card codex-execution-summary ${Y?"running":""}`,"data-testid":r,"data-attempt-index":S2(l),"data-running":Y?"true":"false",onToggle:(n)=>{if(n.currentTarget?.open&&!Q)y?.(l)}},L("summary",null,L("div",{className:"codex-progressive-card-head"},L("span",{className:"codex-output-channel"},"Summary"),L("strong",null,`执行过程摘要${N}`),Y?L("span",{className:"codex-summary-running-pill","data-testid":`${r}-running`},"执行中"):null,L("code",{title:E?`最近更新: ${Nf(E)}`:q},`${Rl(A.durationMs??A.totalElapsedMs)} / ${M} tools / ${q}`)),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,`STEP ${Number.isFinite(h)?Math.max(0,Math.floor(h)):0}`),W>0?L("span",{className:"codex-execution-error-pill","data-testid":`${r}-error-count`},`Error ${W}`):null)),L("div",{className:"codex-execution-digest expanded"},L("span",null,`修改文件:${TG(O,6)}`),L("span",null,`执行命令:${TG(z,4)}`)),j.length===0?L("div",{className:"codex-output-empty"},_?"正在按需拉取步骤 summary...":"展开后将只请求执行步骤 summary,不拉取单步骤全量。"):L("div",{className:"codex-trace-step-list"},j.map((n)=>{let S=String(n?.seq??""),T=U[S],i=Array.isArray(n?.summaryLines)?n.summaryLines.slice(0,4):[];return L("details",{key:S||`${n?.title}-${n?.at}`,className:`codex-trace-step ${String(n?.kind||"message")} ${vG(n)?"error":""}`,"data-testid":`codex-trace-step-${S||"unknown"}`,onToggle:(C)=>{if(C.currentTarget?.open&&!T)$?.(n?.seq)}},L("summary",null,L("span",{className:"codex-output-channel"},rB(n?.kind)),L("strong",null,String(n?.title||"Trace step")),n?.status?L("code",null,String(n.status)):null,L("time",null,Nf(n?.at))),L("div",{className:"codex-trace-step-summary"},i.length>0?i.map((C,v)=>L("pre",{key:`${S}-${v}`},String(C||""))):L("span",null,"无 summary")),T?.line?L(O2,{items:[T.line],autoScroll:!1,loading:!1,hasDetail:!0,emptyText:"无步骤详情",testId:`codex-trace-step-detail-${S||"unknown"}`,className:"codex-transcript codex-step-detail-transcript",collapseTools:!1}):L("div",{className:"codex-output-empty"},_?"正在按需拉取这个步骤的全量数据...":"展开后将只请求这个单步骤的全量数据。"))})))}function rB(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 oG({task:f,attempt:u,attemptIndex:l,testId:_="codex-final-response"}){let y=XX(f,u);if(y.length===0)return null;let $=Number(u?.finalResponseChars||y.length),r=l?` #${l}`:"";return L("section",{className:"codex-progressive-card codex-final-response","data-testid":_,"data-attempt-index":S2(l)},L("div",{className:"codex-progressive-card-head"},L("span",{className:"codex-output-channel"},"Final"),L("strong",null,`最终 response${r}`),L("code",null,`${Number.isFinite($)?$:y.length} chars`)),L(mz,{markdown:y,className:"codex-transcript-body codex-markdown",testId:`${_}-markdown`}))}function aG({task:f,attempt:u,attemptIndex:l,testId:_="codex-progressive-judge"}){let y=xG(f,u);if(!y?.decision)return null;let $=l?` #${l}`:"";return L("section",{className:"codex-progressive-card codex-progressive-judge","data-testid":_,"data-attempt-index":S2(l)},L("div",{className:"codex-progressive-card-head"},L("span",{className:"codex-output-channel"},"Judge"),L("strong",null,`完成判定${$}`),L("code",null,`${y.decision} ${Math.round(Number(y.confidence||0)*100)}%`)),L("div",{className:"codex-judge-card","data-testid":`${_}-card`},L(Wy,{status:y.decision},y.decision),L("strong",null,`${Math.round(Number(y.confidence||0)*100)}% confidence`),L("p",{"data-testid":`${_}-reason`},y.reason||"--"),y.continuePrompt?L("pre",{"data-testid":`${_}-continue-prompt`},String(y.continuePrompt||"")):null))}function jB({task:f,attempt:u,attemptIndex:l,loading:_,onLoadPromptPart:y,testId:$="codex-judge-feedback-prompt"}){let r=YX(f,u,l);if(r===null)return null;let j=bG(l),J=D2(f)[j],U=String(J?.text||"").trimEnd(),Q=String(r.preview||r.text||"").trimEnd(),W=U||String(r.text||"").trimEnd(),G=Number(J?.chars||r.chars||W.length||Q.length),K=Number(J?.lines||r.lines||Qy(W||Q)),H=J?.forAttempt||r.forAttempt||Number(l||0)+1;return L("details",{className:"codex-progressive-card codex-judge-feedback-prompt","data-testid":$,"data-attempt-index":S2(l),onToggle:(O)=>{if(O.currentTarget?.open&&!U)y?.("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} -> #${H}`),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||(_?"正在按需拉取 judge feedback prompt...":"展开后将只请求这一次 judge feedback prompt。")))}function AB({task:f,attempt:u,position:l,loading:_,onLoadPromptPart:y,onLoadSteps:$,onLoadStep:r}){let j=cX(u,l),A=l===0,J=B6(u,j),U=J?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(Wy,{status:u.terminalStatus},u.terminalStatus):null,L("code",null,`${Nf(u?.startedAt)} -> ${Nf(u?.finishedAt)}`)),L(sG,{task:f,attempt:u,attemptIndex:j,loading:_,onLoadSteps:$,onLoadStep:r,testId:A?"codex-execution-summary":`codex-execution-summary-attempt-${j}`}),J?null:L(oG,{task:f,attempt:u,attemptIndex:j,testId:A?"codex-final-response":`codex-final-response-attempt-${j}`}),J?null:L(aG,{task:f,attempt:u,attemptIndex:j,testId:A?"codex-progressive-judge":`codex-progressive-judge-attempt-${j}`}),J?null:L(jB,{task:f,attempt:u,attemptIndex:j,loading:_,onLoadPromptPart:y,testId:A?"codex-judge-feedback-prompt":`codex-judge-feedback-prompt-attempt-${j}`}))}function FB({task:f,loading:u,onLoadPromptPart:l,onLoadSteps:_,onLoadStep:y}){if(!f)return L(zy,{title:"未选择任务",text:"从左侧队列选择任务,或提交新 Codex 任务。"});let $=Pj(f);return L("div",{className:"codex-transcript codex-progressive-trace","data-testid":"codex-output"},u&&!_l(f)?L("div",{className:"codex-output-empty"},"正在加载 Trace Summary..."):null,L($B,{task:f,loading:u,onLoadPromptPart:l}),$.length>0?$.map((r,j)=>L(AB,{key:`${r?.index||j+1}-${r?.startedAt||j}`,task:f,attempt:r,position:j,loading:u,onLoadPromptPart:l,onLoadSteps:_,onLoadStep:y})):[L(sG,{key:"execution",task:f,loading:u,onLoadSteps:_,onLoadStep:y}),L(oG,{key:"final",task:f}),L(aG,{key:"judge",task:f})])}function JB({task:f}){let u=EX(f);if(!f||u.length===0)return L(zy,{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"},vX(String(l.channel||"system"))),L("span",null,Nf(l.at)),l.method?L("code",null,l.method):null),L("pre",null,String(l.text||""))))))}function UB({task:f}){let u=iG(f).slice().reverse();if(u.length===0)return L(zy,{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(Wy,{status:l.terminalStatus||"unknown"},l.terminalStatus||"unknown")),L("td",null,l.transportClosedBeforeTerminal?L(Wy,{status:"failed"},"closed-before-terminal"):L(Wy,{status:"succeeded"},"normal")),L("td",null,`code=${l.appServerExitCode??"--"} signal=${l.appServerSignal??"--"}`),L("td",null,Nf(l.finishedAt)))))))}function dG({microservices:f,onRaw:u,apiBaseUrl:l="/api",initialTasksData:_=null,standalone:y=!1}){let $=f.find((x)=>x.id==="code-queue")||null,r=aX(_),j=String(r?.id||""),A=new Map;if(r!==null&&j.length>0)A.set(j,{task:r,maxSeq:X2(Array.isArray(r.transcript)?r.transcript:[]),complete:Boolean(r._transcriptComplete),completeUpdatedAt:r._transcriptComplete?String(r.updatedAt||""):""});let J=typeof performance>"u"?0:performance.now(),U=m0(j),Q=m0(0),W=m0(0),G=m0(0),K=m0(!1),H=m0(!1),O=m0(!1),z=m0(null),Z=m0(new Map),N=m0(new Map),E=m0(new Map),q=m0(new Map),Y=m0(new Set),w=m0(!1),B=m0(Boolean(_)),P=m0(new Map),h=m0(new Set),M=m0(A),n=m0(_),[S,T]=tf(null),[i,C]=tf(_),[v,X]=tf(j),[D,p]=tf(r),[m,s]=tf(!1),[d,a]=tf(""),[I,ff]=tf(null),[yf,rf]=tf(!1),[Wf,Ef]=tf(!1),[Gf,c]=tf(""),[o,e]=tf(""),[Kf,k]=tf("default"),[Af,Yf]=tf(G_),[Bf,df]=tf("main-server"),[_0,y0]=tf("gpt-5.5"),[N0,a0]=tf("/root/unidesk"),[pu,mu]=tf(99),[C0,Q1]=tf(1),[ru,Uf]=tf(!1),[nf,Nu]=tf(!1),[Cf,d0]=tf(""),[e0,Zu]=tf(""),[i0,Du]=tf(""),[W1,pl]=tf(!0),[R_,x_]=tf(()=>typeof window>"u"?!0:window.matchMedia(jX).matches),[v0,uf]=tf(!1),[wf,Hf]=tf(""),[gf,pf]=tf(""),{addNotification:Q0}=Lu(),[u0,fu]=tf(""),[Bl,w4]=tf(""),[$3,D4]=tf(!1),[Tu,ml]=tf(_?{phase:"complete",taskId:j,queueMs:0,detailMs:0,totalMs:J,chunks:r?1:0,transcriptRows:Array.isArray(r?.transcript)?r.transcript.length:0,partial:Boolean(_?.selected?.hasMore||V6(r)),completedAt:new Date}:null),[T4,M4]=tf(_?new Date:null),[r3,j3]=tf(!1),Yl=Ry(yu(i)),A3=Yl.filter(M1),h0=i?.queue||S?.body?.queue||S?.queue||{},Nr=mX(i,h0),P4=Q_(i),z1=HG(h0,Kf),ny=L6(z1,Af),b_=Number((cu(Af)?h0?.total:ny?.total)??P4.total??Yl.length),Sy=q6(h0),cJ=cu(Af)?Sy:[String(L6(z1,Af)?.activeTaskId||"")].filter(Boolean),Cy=OG(h0,z1,Af,Yl),RJ=cu(Af)?Wj(h0):Wj(ny||{}),n4=Wj(h0),Zr=IX(n4),Er=Math.max(pX(n4),Sy.length),Hr=R0((cu(Af)?h0?.unreadTerminal:ny?.unreadTerminal)??A3.length),G1=i?A3.length:Hr,iy=cu(Af)?"All queues":Oj(ny||{id:Af,name:Af}),v_=Hj(d),Mu=v_.length>0,S4=Mu?Ry(yu(I)):[],F3=Q_(I),Fl=Mu?S4:Yl,C4=Fl.filter(M1),Or=Fl.filter((x)=>!cl(x)),i4=Fl.filter((x)=>cl(x)&&!M1(x)),c4=Mu?F3:P4,K1=Mu?Number(F3.total??S4.length):b_,R4=c4.hasMore===!0&&String(c4.nextBeforeId||"").length>0,J3=Mu?Wf:r3,U3=$?yX($):{},Vr=$?$X($):{},Q3=NG(()=>HX(Gf),[Gf]),Jl=NG(()=>{let x=LG(C0);return Q3.flatMap((g)=>Array.from({length:x},()=>OX(g,o)))},[Q3,C0,o]),W3=Jl.length,xJ=W3>1&&!ru,kH=nf||v0||W3===0||xJ,bJ=dX(h0,_0),vJ=eX(h0,Bf),tH=w2(h0,Bf),qr=D?.id&&D?.activeTurnId&&String(D?.status)==="running",sH=D?.id&&!["succeeded","failed","canceled"].includes(String(D?.status||"")),oH=D?.id&&["succeeded","failed","canceled"].includes(String(D?.status||"")),cy=D?.id&&mG(D);function N1(x){let g=typeof x==="function"?x(n.current):x;return n.current=g,C(g),g}function aH(x,g,Ff=!0){let jf=Array.from(new Set(x.map((Of)=>String(Of||"")).filter(Boolean)));for(let Of of jf)if(P.current.set(Of,g),Ff)h.current.add(Of);return jf}function hJ(x){for(let g of x.map((Ff)=>String(Ff||"")).filter(Boolean))P.current.delete(g),h.current.delete(g)}function Lr(x){let g=String(x?.id||""),Ff=g?P.current.get(g):void 0;if(!Ff)return x;if(String(x?.status||"").length>0&&!cl(x))return P.current.delete(g),h.current.delete(g),x;return{...x,readAt:x?.readAt||Ff,terminalUnread:!1}}function Xr(x){let g=String(x?.id||"");return g.length>0&&h.current.has(g)&&cl(x)}function Ry(x,g=!0){let Ff=[];for(let jf of Array.isArray(x)?x:[]){let Of=Lr(jf);if(g&&Xr(Of))continue;Ff.push(Of)}return Ff}function dH(x,g=!0){if(!x||!Array.isArray(x?.tasks))return x;let Ff=Ry(yu(x),g),jf=Q_(x);return{...x,tasks:Ff,pagination:x.pagination?{...jf,returned:Ff.length}:x.pagination}}function eH(x){let g=String(x||h0?.mainProviderId||"main-server").trim()||"main-server";df(g),a0(w2(h0,g))}function x4(x,g,Ff=null,jf=null){let Of=new Set(aH(x,g));if(Of.size===0&&jf===null&&Ff===null)return;N1((qf)=>{if(!qf)return qf;let Lf=yu(qf).flatMap((hf)=>{let If=String(hf?.id||"");if(!Of.has(If)){let $0=Lr(hf);return Xr($0)?[]:[$0]}let of=jf&&String(jf?.id||"")===If?jf:{},xf={...hf,...of,readAt:g,terminalUnread:!1};return Xr(xf)?[]:[xf]});return{...qf,queue:Ff||qf.queue,tasks:Of.size>0?X$([Lf],Cy):Lf}});for(let qf of Of){let Lf=M.current.get(qf);if(Lf?.task){let hf=jf&&String(jf?.id||"")===qf?jf:{},If={...Lf.task,...hf,readAt:g,terminalUnread:!1};if(M.current.set(qf,{...Lf,task:If}),U.current===qf)p(If)}}}W_(()=>{Uf(!1)},[Gf,C0,o]),W_(()=>{let x=Hj(d);W.current+=1;let g=W.current;if(!$||x.length===0){ff(null),rf(!1),Ef(!1),O.current=!1;return}rf(!0),ff(null);let Ff=window.setTimeout(()=>{(async()=>{try{let jf=await VG(l,{},Af,x);if(g!==W.current)return;ff(dH(jf))}catch(jf){if(g===W.current)ff(null),Hf(ll(jf,"搜索 Codex tasks 失败"))}finally{if(g===W.current)rf(!1)}})()},240);return()=>window.clearTimeout(Ff)},[$?.id,l,Af,d]),W_(()=>{Zu(D?B2(D):""),Du(Array.isArray(D?.referenceTaskIds)?D.referenceTaskIds.join(" "):"")},[v]);function v1(x,g,Ff){let jf=M.current.get(x)||{},Of=jf.task||{},qf=Array.isArray(Of.transcript)?Of.transcript:[],Lf=oX(Of,g),hf=Object.prototype.hasOwnProperty.call(g,"transcript")?Kj(qf,Array.isArray(g.transcript)?g.transcript:[]):qf,If={...Of,...Lf,transcript:hf,output:Array.isArray(Lf.output)?Y2(Of,Lf,"output"):Array.isArray(Of.output)?Of.output:[],events:Array.isArray(Lf.events)?Y2(Of,Lf,"events"):Array.isArray(Of.events)?Of.events:[]},of=Lr(If),xf=String(of?.updatedAt||""),$0=Boolean(g._transcriptComplete)&&cl(of),ju=Boolean(jf.complete)&&cl(of)&&String(jf.completeUpdatedAt||"")===xf,af=$0||ju,n0={...jf,task:of,maxSeq:X2(hf),complete:af,completeUpdatedAt:af?xf:""};if(M.current.set(x,n0),Ff===G.current&&U.current===x)p(of);return n0}async function z3(x,g=!1,Ff,jf){if(!$||!x)return;let qf=M.current.get(x)?.task;if(!g&&q2(qf))return;let Lf=x,hf=Z.current.get(Lf);if(hf){if(g||!q2(qf))hf.refreshAfter=!0;return hf.promise}let If=G.current,of=performance.now();if(U.current===x)s(!0);let xf={promise:Promise.resolve(),refreshAfter:!1},$0=(async()=>{try{let ju=await WX(l,x);if(If!==G.current||U.current!==x)return;let af=ju?.summary||{},n0=M.current.get(x)?.task||{},ku=String(af.updatedAt||""),gl=String(n0?.updatedAt||""),tu=lX(gl,ku)||M2(n0)>qj(af);if(tu)xf.refreshAfter=!0;let xy=tu?Dj(gl,ku):ku||gl;v1(x,{id:x,status:tu?n0?.status||af.status:af.status,updatedAt:xy,startedAt:af.startedAt||n0?.startedAt,finishedAt:tu?n0?.finishedAt||af.finishedAt:af.finishedAt,currentAttempt:af.currentAttempt??n0?.currentAttempt,maxAttempts:af.maxAttempts??n0?.maxAttempts,finalResponse:tu?tG(n0,af,"finalResponse"):af.finalResponse,lastJudge:tu?n0?.lastJudge||af.lastJudge:af.lastJudge,lastError:tu?n0?.lastError||af.lastError:af.lastError,attempts:tu?Y2(n0,{attempts:Array.isArray(af.attempts)?af.attempts:[]},"attempts"):Array.isArray(af.attempts)?af.attempts:[],timing:af.timing,_traceSummary:af,_traceSummaryLoaded:!0,_traceSummaryUpdatedAt:ku,_detailLoaded:!0},If),ml({phase:"complete",taskId:x,queueMs:jf??0,detailMs:performance.now()-of,totalMs:Ff===void 0?performance.now()-of:performance.now()-Ff,chunks:1,transcriptRows:Number(af?.execution?.traceLineCount||af?.execution?.stepCount||0),partial:!1,completedAt:new Date})}finally{let ju=Boolean(xf.refreshAfter&&U.current===x&&!q2(M.current.get(x)?.task));if(Z.current.delete(Lf),If===G.current&&U.current===x)s(!1);if(ju)window.setTimeout(()=>{z3(x,!0).catch((af)=>Hf(ll(af,"自动刷新 Trace Summary 失败")))},0)}})();xf.promise=$0,Z.current.set(Lf,xf),await $0}async function fO(x,g=null){let Ff=U.current;if(!$||!Ff||!x)return;let jf=M.current.get(Ff)?.task,Of=D2(jf),qf=x==="feedback"||x==="judge-feedback"?bG(g):x;if(Of[qf]?.text)return;let Lf=`${Ff}:${qf}`,hf=N.current.get(Lf);if(hf)return hf;let If=G.current;if(U.current===Ff)s(!0);let of=(async()=>{try{let xf=await zX(l,Ff,x,g);if(If!==G.current||U.current!==Ff)return;let $0=M.current.get(Ff)?.task,ju=D2($0);v1(Ff,{...x==="full"?{prompt:String(xf?.text||""),promptChars:Number(xf?.chars||0)}:{},_promptDetails:{...ju,[qf]:xf}},If)}finally{if(N.current.delete(Lf),If===G.current&&U.current===Ff)s(!1)}})();N.current.set(Lf,of),await of}async function uO(x=null){let g=U.current;if(!$||!g)return;let Ff=M.current.get(g)?.task,jf=x===null||x===void 0||String(x).length===0?"":String(x);if(hG(Ff,jf||null))return;let Of=`${g}:${jf||"all"}`,qf=E.current.get(Of);if(qf)return qf;let Lf=G.current;if(U.current===g)s(!0);let hf=(async()=>{try{let If=await GX(l,g,0,500,jf||null);if(Lf!==G.current||U.current!==g)return;let of=Array.isArray(If?.steps)?If.steps:[];if(jf){let xf=M.current.get(g)?.task,$0=Bu(xf?._traceStepsByAttempt)||{},ju=Bu(xf?._traceStepsLoadedByAttempt)||{};v1(g,{_traceStepsByAttempt:{...$0,[jf]:of},_traceStepsLoadedByAttempt:{...ju,[jf]:!0}},Lf)}else v1(g,{_traceSteps:of,_traceStepsLoaded:!0,_traceStepsHasMore:Boolean(If?.hasMore),_traceStepsNextAfterSeq:If?.nextAfterSeq},Lf)}finally{if(E.current.delete(Of),Lf===G.current&&U.current===g)s(!1)}})();E.current.set(Of,hf),await hf}async function lO(x){let g=U.current,Ff=String(x??"");if(!$||!g||Ff.length===0)return;let jf=M.current.get(g)?.task;if(Yj(jf)[Ff]?.line)return;let qf=`${g}:${Ff}`,Lf=q.current.get(qf);if(Lf)return Lf;let hf=G.current;if(U.current===g)s(!0);let If=(async()=>{try{let of=await KX(l,g,x);if(hf!==G.current||U.current!==g)return;let xf=M.current.get(g)?.task,$0=Yj(xf);v1(g,{_traceStepDetails:{...$0,[Ff]:of}},hf)}finally{if(q.current.delete(qf),hf===G.current&&U.current===g)s(!1)}})();q.current.set(qf,If),await If}async function BC(x,g,Ff){if(!$||!x)return;let jf=performance.now(),Of=G.current,qf=M.current.get(x);if(qf?.task){if(p(qf.task),s(V6(qf.task)||!qf.complete),!V6(qf.task)&&qf.complete&&cl(qf.task)&&String(qf.completeUpdatedAt||"")===String(qf.task?.updatedAt||"")){ml({phase:"complete",taskId:x,queueMs:Ff??0,detailMs:0,totalMs:g===void 0?0:performance.now()-g,chunks:0,transcriptRows:Array.isArray(qf.task.transcript)?qf.task.transcript.length:0,completedAt:new Date});return}}else s(!0);let Lf=z.current;if(Lf?.taskId===x&&Lf.token===Of)return Lf.promise;let hf=(async()=>{try{let If=await D0(T0(l,`/api/tasks/${encodeURIComponent(x)}?meta=1`));if(Of!==G.current||U.current!==x)return;let of=M.current.get(x),xf=Array.isArray(of?.task?.transcript)?of.task.transcript:[],$0=If?.task||{},ju=Boolean(of?.complete)&&String(of?.completeUpdatedAt||"")===String($0?.updatedAt||"");v1(x,{...$0,summaryOnly:!1,_metaLoaded:!0,transcript:xf,_detailLoaded:xf.length>0,_transcriptComplete:ju},Of);let af=V6(of?.task)||Boolean(of?.task?._transcriptPreview),n0=af?0:xf.length>0?DG(xf):0,ku=!af&&of?.complete&&cl($0)&&String(of?.completeUpdatedAt||"")===String($0?.updatedAt||"")?X2(xf):n0,gl=!0,tu=0,xy=xf.length;while(gl){let Ql=await D0(T0(l,`/api/tasks/${encodeURIComponent(x)}/transcript?afterSeq=${encodeURIComponent(String(ku))}&limit=${fX}&fullText=1`));if(Of!==G.current||U.current!==x)return;let kl=M.current.get(x),h_=Array.isArray(kl?.task?.transcript)?kl.task.transcript:[],I_=Kj(h_,Array.isArray(Ql?.transcript)?Ql.transcript:[]);tu+=1,xy=I_.length;let t0=Boolean(!Ql?.hasMore);if(v1(x,{status:Ql?.status||$0.status,updatedAt:Ql?.updatedAt||$0.updatedAt,transcript:I_,_detailLoaded:t0||I_.length>0,_transcriptComplete:t0,_transcriptPreview:af&&!t0},Of),gl=Boolean(Ql?.hasMore),ku=Number(Ql?.nextAfterSeq??X2(I_)),!gl)break;await new Promise((sJ)=>window.setTimeout(sJ,0))}ml({phase:"complete",taskId:x,queueMs:Ff??0,detailMs:performance.now()-jf,totalMs:g===void 0?performance.now()-jf:performance.now()-g,chunks:tu,transcriptRows:xy,completedAt:new Date})}finally{if(z.current?.taskId===x&&z.current?.token===Of)z.current=null;if(Of===G.current&&U.current===x)s(!1)}})();z.current={taskId:x,token:Of,promise:hf},await hf}async function gu(x=U.current,g=!0,Ff=Af){if(!$)return;if(!g&&w.current)return;let jf=performance.now();if(g)w.current=!0;if(g)ml({phase:"loading",taskId:String(x||U.current||""),startedAt:new Date});let Of=Q.current+1;Q.current=Of;let qf=String(x||U.current||""),Lf=qf?M.current.get(qf):null,hf=Array.isArray(Lf?.task?.transcript)?Lf.task.transcript:[],If=DG(hf),of=S||{},xf=null;try{xf=await QX(l,qf,If,Ff)}catch{xf=await VG(l,of,Ff)}if(Of!==Q.current){if(g)w.current=!1;return}let $0=performance.now()-jf;T(of);let ju=xf?.queue||{},af=String(ju?.activeTaskId||q6(ju)[0]||""),n0=xf;N1((Eu)=>{let G3=yu(xf),p_=yu(Eu),K3=p_.length>0?X$([p_,G3],af):X$([G3],af),v4=Ry(K3),OO=Q_(xf),h4=Q_(Eu),VO=p_.length>G3.length&&(h4.hasMore===!1||String(h4.nextBeforeId||"").length>0),qO={...OO,...VO?{hasMore:h4.hasMore,nextBeforeId:h4.nextBeforeId}:{},returned:v4.length};return n0={...xf,tasks:v4,pagination:qO},n0});let ku=yu(n0),gl=HG(ju,Kf),tu=OG(ju,gl,Ff,ku),xy=UX(gl,Ff,ku),Ql=qf||U.current,kl=n0?.selected||null,h_=kl?.task||null,I_=Array.isArray(kl?.transcript)?kl.transcript:null,t0=Ql&&(ku.some((Eu)=>Eu.id===Ql)||String(h_?.id||"")===Ql)?Ql:tu||xy||ku[0]?.id||"";if(U.current!==t0)G.current+=1;U.current=t0,X(t0);let b4=ku.find((Eu)=>Eu.id===t0);if(b4){let Eu=M.current.get(t0);if(Eu?.task)M.current.set(t0,{...Eu,task:{...b4,...Eu.task,status:b4.status,updatedAt:b4.updatedAt}})}if(h_?.id===t0&&I_!==null){let Eu=M.current.get(t0),G3=Array.isArray(Eu?.task?.transcript)?Eu.task.transcript:[],p_=Kj(G3,I_),K3=Boolean(kl?.preview);if(v1(t0,{...h_,_summaryLoaded:!0,transcript:p_,_detailLoaded:!kl?.hasMore||p_.length>0,_transcriptComplete:!K3&&!kl?.hasMore&&cl(h_),_transcriptPreview:K3},G.current),s(!1),g)ml({phase:"complete",taskId:t0,queueMs:$0,detailMs:Math.max(0,performance.now()-jf-$0),totalMs:performance.now()-jf,chunks:1,transcriptRows:p_.length,partial:Boolean(K3||kl?.hasMore||V6(h_)),completedAt:new Date});if(M4(new Date),g)w.current=!1;z3(t0,!1,g?jf:void 0,g?$0:void 0).catch((v4)=>Hf(ll(v4,"加载 Codex Trace Summary 失败")));return}if(g)ml({phase:"session",taskId:t0,queueMs:$0,totalMs:$0,startedAt:new Date(Date.now()-$0)});if(t0)z3(t0,!0,g?jf:void 0,g?$0:void 0).catch((Eu)=>Hf(ll(Eu,"加载 Codex Trace Summary 失败")));else if(G.current+=1,p(null),s(!1),g)ml({phase:"complete",taskId:"",queueMs:$0,detailMs:0,totalMs:performance.now()-jf,chunks:0,transcriptRows:0,completedAt:new Date});if(M4(new Date),g)w.current=!1}async function IJ(){if(Mu){if(!$||Wf||O.current)return;let g=String(F3.nextBeforeId||"");if(!g)return;O.current=!0,Ef(!0),Hf("");try{let Ff=await qG(l,Af,g,MG,v_),jf=yu(Ff),Of=Ff?.queue||h0||{},qf=String(Of?.activeTaskId||q6(Of)[0]||Cy||"");ff((Lf)=>{let hf=Ry(X$([yu(Lf),jf],qf)),If=Q_(Ff);return{...Lf||{},queue:Of,tasks:hf,pagination:{...If,returned:hf.length}}})}catch(Ff){Hf(ll(Ff,"加载更多搜索结果失败"))}finally{O.current=!1,Ef(!1)}return}if(!$||r3||H.current)return;let x=String(Q_(i).nextBeforeId||"");if(!x)return;H.current=!0,j3(!0),Hf("");try{let g=await qG(l,Af,x),Ff=yu(g),jf=g?.queue||h0||{},Of=String(jf?.activeTaskId||q6(jf)[0]||Cy||"");N1((qf)=>{let Lf=Ry(X$([yu(qf),Ff],Of)),hf=Q_(g);return{...qf||{},queue:jf,statistics:g?.statistics||qf?.statistics,tasks:Lf,pagination:{...hf,returned:Lf.length}}})}catch(g){Hf(ll(g,"加载更早 Codex tasks 失败"))}finally{H.current=!1,j3(!1)}}function _O(x){let g=x.currentTarget;if(!g||J3||!R4)return;if(g.scrollHeight-g.scrollTop-g.clientHeight<120)IJ()}async function Ul(x,g){uf(!0),Hf("");try{await x()}catch(Ff){Hf(ll(Ff,g))}finally{uf(!1)}}async function Br(x){if(!x)return;try{let g=!1;try{if(navigator.clipboard?.writeText)await navigator.clipboard.writeText(x),g=!0}catch{g=!1}if(!g){let jf=document.createElement("textarea");jf.value=x,jf.style.position="fixed",jf.style.opacity="0",document.body.appendChild(jf),jf.select(),g=document.execCommand("copy"),document.body.removeChild(jf)}if(!g)throw Error("browser clipboard rejected the copy request");fu(x);let Ff=`已复制任务 ID:${x}`;pf(Ff),Q0("success",Ff),window.setTimeout(()=>fu((jf)=>jf===x?"":jf),1600)}catch(g){Hf(`复制任务 ID 失败:${ll(g)}`)}}function Yr(x){if(!x)return;e(x);let g=`已引用任务 ID:${x};提交时后端会读取并注入该任务上下文`;pf(g),Q0("success",g)}async function wr(x){if(!$||!x)return;let g=new Date().toISOString();Q.current+=1,x4([x],g,null,{id:x,readAt:g,terminalUnread:!1}),w4(x);let Ff=!1;if(await Ul(async()=>{let jf=await NX(l,x),Of=jf?.task||{id:x,readAt:new Date().toISOString(),terminalUnread:!1},qf=String(Of?.readAt||new Date().toISOString());x4([x],qf,jf?.queue||null,Of),Ff=!0;let Lf=`已将任务 ${x} 标为已读`;pf(Lf),Q0("success",Lf)},"标记 Codex task 已读失败"),!Ff)hJ([x]),gu(U.current,!1).catch((jf)=>Hf(ll(jf,"刷新 Codex tasks 失败")));w4((jf)=>jf===x?"":jf)}async function yO(){if(!$||$3)return;D4(!0);let x=new Date().toISOString(),g=Array.from(new Set([...yu(n.current).filter(M1).map((jf)=>String(jf?.id||"")).filter(Boolean),...Array.from(M.current.entries()).filter(([,jf])=>M1(jf?.task)).map(([jf])=>jf)]));if(Q.current+=1,g.length>0)x4(g,x);let Ff=!1;if(await Ul(async()=>{let jf=await ZX(l),Of=String(jf?.readAt||new Date().toISOString()),qf=yu(n.current).filter(M1).map((xf)=>String(xf?.id||"")).filter(Boolean),Lf=Array.from(M.current.entries()).filter(([,xf])=>M1(xf?.task)).map(([xf])=>xf),hf=Array.from(new Set([...g,...qf,...Lf]));x4(hf,Of,jf?.queue||null);let If=Number(jf?.count||hf.length);Ff=!0;let of=`已将 ${If} 个已结束未读任务标为已读`;pf(of),Q0("success",of)},"全部标为已读失败"),!Ff&&g.length>0)hJ(g),gu(U.current,!1).catch((jf)=>Hf(ll(jf,"刷新 Codex tasks 失败")));D4(!1)}function $O(x){let g=x||G_;if(Yf(g),!cu(g))k(g);if(N1(null),!(cu(g)?U.current:""))U.current="",G.current+=1,X(""),p(null),s(!0)}async function rO(){let x=typeof window>"u"?"":window.prompt("输入新的 Codex queue ID(字母/数字/._-,最长 64)","new-lane"),g=String(x||"").trim();if(!g)return;await Ul(async()=>{let Ff=await D0(T0(l,"/api/queues"),{method:"POST",body:{queueId:g}}),jf=String(Ff?.queue?.id||g);k(jf),Yf(jf),N1(null),U.current="",G.current+=1,X(""),p(null);let Of=`已创建并切换到 queue:${jf}`;pf(Of),Q0("success",Of),await gu("",!0,jf)},"创建 Codex queue 失败")}async function jO(){let x=String(Kf||"default").trim()||"default",g=L6(z1,x)||{id:x,name:x},Ff=typeof window>"u"?null:window.prompt(`输入 queue 显示名称(ID 不变:${x};留空恢复为 ID)`,CG(g));if(Ff===null)return;await Ul(async()=>{let jf=await D0(T0(l,`/api/queues/${encodeURIComponent(x)}`),{method:"PATCH",body:{name:String(Ff)}}),Of=jf?.queue||{id:x,name:String(Ff||x)};if(jf?.summary)N1((Lf)=>Lf?{...Lf,queue:jf.summary}:Lf);let qf=`已更新 queue 名称:${Oj(Of)}`;pf(qf),Q0("success",qf),await gu(U.current,!0,Af)},"修改 Codex queue 名称失败")}async function AO(x){if(x.preventDefault(),K.current){pf("任务正在提交中,请等待当前请求完成,已阻止重复提交。");return}if(Jl.length>1&&!ru){Hf(`检测到将创建 ${Jl.length} 个任务;请先勾选“确认批量入队”,避免误传多个任务。`);return}K.current=!0,Nu(!0),pf("正在提交 Code Queue 任务,请等待后端确认,输入已临时锁定。"),await Ul(async()=>{if(Jl.length===0)throw Error("prompt 不能为空");let g=Jy(o),Ff=Kf.trim()||"default",jf=[...Jl],Of=(xf)=>({prompt:xf,queueId:Ff,providerId:Bf,model:_0,cwd:N0,maxAttempts:Number(pu),...g.length>0?{referenceTaskIds:g}:{}}),qf=jf.length===1?Of(jf[0]):{tasks:jf.map(Of)},Lf=await D0(T0(l,jf.length===1?"/api/tasks":"/api/tasks/batch"),{method:"POST",body:qf}),hf=Lf?.tasks?.[0]?.id||"",If=Array.isArray(Lf?.tasks)?Lf.tasks.map((xf)=>String(xf?.id||"")).filter(Boolean):[],of=`已创建 ${If.length||jf.length} 个任务${If.length>0?`:${If.join(" / ")}`:""}`;if(pf(of),Q0("success",of),c(""),e(""),Uf(!1),U.current=hf,Af!==Ff)N1(null);k(Ff),await gu(hf,!0,Ff)},"Codex 任务入队失败"),K.current=!1,Nu(!1)}async function FO(x){if(x.preventDefault(),!D?.id)return;await Ul(async()=>{await D0(T0(l,`/api/tasks/${encodeURIComponent(D.id)}/steer`),{method:"POST",body:{prompt:Cf}}),d0(""),await gu(D.id)},"追加 prompt 失败")}async function JO(x){x.preventDefault();let g=String(D?.id||"");if(!g||!cy)return;await Ul(async()=>{let Ff=Jy(i0),jf=await D0(T0(l,`/api/tasks/${encodeURIComponent(g)}/edit`),{method:"POST",body:{prompt:e0,referenceTaskIds:Ff}}),Of={...jf?.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,p(Of),X(g),Zu(B2(Of)),Du(Array.isArray(Of?.referenceTaskIds)?Of.referenceTaskIds.join(" "):""),N1((Lf)=>{if(!Lf)return Lf;let hf=yu(Lf).map((If)=>String(If?.id||"")===g?{...If,...Of}:If);return{...Lf,queue:jf?.queue||Lf.queue,tasks:X$([hf],Cy)}});let qf=jf?.changed===!1?`任务 ${g} 的 prompt 未变化`:`已更新 queued 任务 ${g} 的用户 prompt`;pf(qf),Q0("success",qf),await gu(g,!0,Af)},"编辑 queued 任务 prompt 失败")}async function UO(){if(!D?.id)return;await Ul(async()=>{await D0(T0(l,`/api/tasks/${encodeURIComponent(D.id)}/interrupt`),{method:"POST",body:{}}),await gu(D.id)},"打断 Codex session 失败")}async function QO(){if(!D?.id)return;await Ul(async()=>{await D0(T0(l,`/api/tasks/${encodeURIComponent(D.id)}/retry`),{method:"POST",body:{}}),await gu(D.id)},"重新入队失败")}async function WO(x){let g=String(D?.id||""),Ff=String(x||"").trim();if(!g||!Ff)return;let jf=n2(D);if(Ff===jf){pf(`任务 ${g} 已在 queue=${Ff}`);return}await Ul(async()=>{let qf=(await D0(T0(l,`/api/tasks/${encodeURIComponent(g)}/move`),{method:"POST",body:{queueId:Ff}}))?.task||{...D,queueId:Ff};if(M.current.set(g,{...M.current.get(g)||{},task:qf}),U.current=g,p(qf),X(g),k(Ff),!cu(Af))N1(null),Yf(Ff);let Lf=`已将任务 ${g} 从 ${jf} 移动到 ${Ff}`;pf(Lf),Q0("success",Lf),await gu(g,!0,cu(Af)?G_:Ff)},"移动任务 queue 失败")}async function zO(){let x=U.current;if(!x)return;let g=performance.now();await Ul(async()=>{ml({phase:"session",taskId:x,queueMs:0,totalMs:0,partial:!0,startedAt:new Date}),await z3(x,!0,g,0)},"刷新 Trace Summary 失败")}function GO(x){U.current=x,G.current+=1,X(x);let g=M.current.get(x);if(g?.task)p(g.task),s(!1);else{s(!0);let Ff=Yl.find((jf)=>jf.id===x);if(Ff)p(Ff);else p(null)}gu(x).catch((Ff)=>Hf(ll(Ff,"切换 Codex session 失败")))}function Dr(x){if(GO(x),AX())x_(!1)}W_(()=>{if(B.current){B.current=!1;return}Ul(()=>gu(U.current),"Code Queue 加载失败")},[$?.id,Af]),W_(()=>{if(!$)return;let x=()=>{if(!ZG())return;gu(U.current,!1).catch((jf)=>Hf(ll(jf,"Code Queue 轮询失败")))},g=window.setInterval(()=>{x()},1500),Ff=()=>{if(ZG())x()};return document.addEventListener("visibilitychange",Ff),()=>{window.clearInterval(g),document.removeEventListener("visibilitychange",Ff)}},[$?.id,Af]),W_(()=>{if(!$||!D||m)return;let x=String(D.id||"");if(!x)return;let g=String(D.updatedAt||"");if(q2(D))return;let Ff=`${x}:${g||"unknown"}:${M2(D)}:${qj(_l(D))}`;if(Y.current.has(Ff))return;Y.current.add(Ff),z3(x,!0).catch((jf)=>Hf(ll(jf,"自动加载 Trace Summary 失败")))},[$?.id,D?.id,D?.updatedAt,D?.stepCount,D?.llmStepCount,D?._traceSummaryUpdatedAt,D?._traceSummaryLoaded,m]);let KO=Fl.length===0?L(zy,{title:Mu?yf?"搜索中":"没有匹配任务":"队列为空",text:Mu?yf?`正在搜索包含“${v_}”的 task...`:`未找到包含“${v_}”的 task;可换个关键词或切换 queue。`:"提交一个任务后,Codex 会串行执行并保存输出。"}):[C4.length>0?L(Nj,{key:"unread",title:"已结束未读",tasks:C4,selectedId:v,emptyText:"暂无已结束未读任务。",onSelect:Dr,onCopy:Br,onReference:Yr,onMarkRead:wr,copiedTaskId:u0,markingReadTaskId:Bl}):null,L(Nj,{key:"active",title:"运行 / 排队",tasks:Or,selectedId:v,emptyText:"当前没有运行或排队任务。",onSelect:Dr,onCopy:Br,onReference:Yr,onMarkRead:wr,copiedTaskId:u0,markingReadTaskId:Bl}),L(Nj,{key:"history",title:"历史 session",tasks:i4,selectedId:v,emptyText:"最近没有完成、失败或取消的 session。",onSelect:Dr,onCopy:Br,onReference:Yr,onMarkRead:wr,copiedTaskId:u0,markingReadTaskId:Bl}),L("div",{key:"pagination",className:"codex-task-pagination","data-testid":"codex-task-pagination"},L("span",null,Mu?`搜索“${v_}” · 已显示 ${Fl.length} / ${Number.isFinite(K1)?K1:Fl.length}`:`已加载 ${Fl.length} / ${Number.isFinite(K1)?K1:Fl.length}`),R4?L("button",{type:"button",className:"ghost-btn",disabled:J3,onClick:()=>void IJ(),"data-testid":"codex-load-more-tasks-button"},J3?"加载中":Mu?"加载更多结果":"加载更早任务"):L("code",null,Mu?"已到结果末尾":"已到队列末尾"))],pJ=(x,g=!1)=>L("label",{className:`code-queue-switcher ${g?"compact":""}`},L("span",null,g?"Queue":"查看 queue"),L("select",{value:Af,onChange:(Ff)=>$O(String(Ff.target.value||G_)),"data-testid":x},L("option",{value:G_},`All queues · ${Number.isFinite(b_)?b_:Yl.length} tasks · ${Sy.length} running`),z1.map((Ff)=>L("option",{key:String(Ff?.id||""),value:String(Ff?.id||"")},Vj(Ff))))),NO=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:d,placeholder:"关键词 / task ID / prompt",autoComplete:"off",onChange:(x)=>a(String(x.target.value||"")),"data-testid":"codex-task-search-input"}),d?L("button",{type:"button",className:"ghost-btn",onClick:()=>a(""),"data-testid":"codex-task-search-clear"},"清除"):null),L("small",{"data-testid":"codex-task-search-summary"},Mu?yf?"搜索中...":`匹配 ${Fl.length}/${Number.isFinite(K1)?K1:Fl.length}`:"支持 task ID、prompt、状态、provider、模型和最近输出关键词")),ZO=L("div",{className:"codex-trace-status","data-testid":"codex-trace-status-summary"},L("span",{className:"codex-trace-status-chip queued"},L("b",null,"排队"),String(Zr)),L("span",{className:"codex-trace-status-chip running"},L("b",null,"运行"),String(Er)),L("span",{className:`codex-trace-status-chip unread ${G1>0?"warn":""}`},L("b",null,"结束未读"),String(G1)),L("span",{className:"codex-trace-status-chip service"},L("b",null,"服务"),`${U3.providerStatus||"unknown"} · ${$?.providerId||"main-server"} · ${Vr.public?"公网暴露":"仅 UniDesk frontend 代理访问"}`),L("span",{className:"codex-trace-status-chip"},L("b",null,"执行节点"),vJ.map((x)=>x.id).join(" / ")),L("span",{className:"codex-trace-status-chip"},L("b",null,"模型"),bJ.join(" / ")),L("span",{className:"codex-trace-status-chip"},L("b",null,"加载"),Tu?.phase==="complete"?_X(Tu?.totalMs):String(Tu?.phase||"idle")),L("span",{className:"codex-trace-status-chip"},L("b",null,"刷新"),T4?W0(T4):"--")),EO=L(B$,{title:D?`Trace ${String(D.id).slice(0,22)}`:"Trace 输出",eyebrow:D?`${D.status} / view=${iy} / task queue=${n2(D)} / provider=${D.providerId||"main-server"} / ${D.model} / agent loop trace`:`Agent loop trace / view=${iy}`,summary:ZO,loading:m||r3||yf||Wf||Tu?.phase==="loading",actions:L("div",{className:"panel-actions"},pJ("code-queue-filter-select"),L("button",{type:"button",className:"ghost-btn codex-mark-all-read-btn",disabled:G1===0||v0||$3,onClick:()=>void yO(),"data-testid":"codex-mark-all-read-button"},$3?"标记中":`全部标已读${G1>0?` (${G1})`:""}`),D?L("button",{type:"button",className:"ghost-btn",disabled:m||v0,onClick:()=>void zO(),"data-testid":"codex-load-full-trace-button"},m?"加载中":_l(D)?"刷新 Summary":"加载 Summary"):null,L("button",{type:"button",className:"codex-session-title-toggle",onClick:()=>x_((x)=>!x),"data-testid":"code-queue-sidebar-toggle"},R_?"收起队列":"展开队列"),L("label",{className:"inline-check"},L("input",{type:"checkbox",checked:W1,onChange:(x)=>pl(Boolean(x.target.checked))}),"自动滚动"),L("button",{type:"button",className:"ghost-btn",disabled:!sH||v0,onClick:()=>void UO(),"data-testid":"codex-interrupt-button"},"打断"),L("button",{type:"button",className:"ghost-btn",disabled:!oH||v0,onClick:()=>void QO()},"重试"),D?L(nG,{title:"Codex Task",data:D,onOpen:u,testId:"raw-codex-task"}):null),className:"codex-output-panel"},L("div",{className:`codex-session-shell ${R_?"":"queue-collapsed"}`},R_?L("aside",{className:"codex-session-sidebar","data-testid":"codex-session-sidebar"},L("div",{className:"codex-session-sidebar-head"},L("div",null,L("span",null,cu(Af)?"All queues":"Queue lane"),L("strong",null,`${iy} · ${Yl.length}/${Number.isFinite(b_)?b_:Yl.length} sessions · 未读 ${G1}`)),L("button",{type:"button",className:"ghost-btn",onClick:()=>x_(!1)},"收起")),pJ("code-queue-filter-sidebar",!0),NO,L("div",{className:"codex-task-list codex-task-list-session",onScroll:_O,"data-testid":"codex-task-list-scroll"},KO)):null,L("div",{className:"codex-session-main"},L("div",{className:"codex-output-stack"},L(FB,{task:D,loading:m,onLoadPromptPart:fO,onLoadSteps:uO,onLoadStep:lO}),L(JB,{task:D})))));if(!$)return L(zy,{title:"Code Queue 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=code-queue"});let mJ=Number(Tu?.totalMs),gJ=Number(Tu?.queueMs),kJ=Number(Tu?.detailMs),tJ=Number(Tu?.transcriptRows),HO=Tu?.phase==="complete"?"complete":String(Tu?.phase||"idle");return L("div",{className:`code-queue-page ${y?"codex-standalone-page":""}`,"data-testid":"code-queue-page","data-load-state":HO,"data-load-total-ms":Number.isFinite(mJ)?String(Math.round(mJ*10)/10):"","data-load-queue-ms":Number.isFinite(gJ)?String(Math.round(gJ*10)/10):"","data-load-detail-ms":Number.isFinite(kJ)?String(Math.round(kJ*10)/10):"","data-load-transcript-rows":Number.isFinite(tJ)?String(tJ):"","data-load-task-id":String(Tu?.taskId||v||""),"data-load-partial":Tu?.partial?"true":"false"},L(A0,{error:wf,wide:!0}),gf?L("div",{className:"form-success wide","data-testid":"codex-create-success"},gf):null,L("div",{className:"codex-session-stage codex-session-stage-top"},EO),L("div",{className:"code-queue-layout"},L("div",{className:"codex-left-rail"},L(B$,{title:"提交任务",eyebrow:nf?"Submitting...":Jl.length>1?`${Jl.length} tasks`:"Single or Batch",className:"codex-compose-panel",loading:nf},L("form",{className:`codex-task-form ${nf?"is-submitting":""}`,onSubmit:AO,"data-testid":"code-queue-task-form","aria-busy":nf?"true":"false"},L("label",null,"Prompt / 多任务用单独一行 --- 分隔",L("textarea",{value:Gf,rows:8,disabled:nf,onChange:(x)=>c(x.target.value),placeholder:"写入 Codex 任务;多个任务之间用 --- 分隔。"})),L("label",{className:"codex-reference-field"},"引用任务 ID(可选)",L("input",{value:o,disabled:nf,onChange:(x)=>e(x.target.value),placeholder:"codex_...;支持空格/逗号分隔多个 ID","data-testid":"codex-reference-task-id"}),Jy(o).length>0?L("code",null,`后端将解析并注入:${Jy(o).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:Kf,disabled:nf,onChange:(x)=>k(String(x.target.value||"default")),"data-testid":"code-queue-id-select"},z1.map((x)=>L("option",{key:String(x?.id||""),value:String(x?.id||"")},Vj(x)))),L("button",{type:"button",className:"ghost-btn codex-rename-queue-btn",onClick:()=>void jO(),disabled:v0||nf||!Kf,title:"修改当前 queue 的显示名称,ID 不变","data-testid":"codex-rename-queue-button"},"改名"),L("button",{type:"button",className:"ghost-btn codex-create-queue-btn",onClick:()=>void rO(),disabled:v0||nf,"data-testid":"codex-create-queue-button"},"创建 queue"))),L("label",null,"模型",L("select",{value:_0,disabled:nf,onChange:(x)=>y0(x.target.value),"data-testid":"codex-model-select"},bJ.map((x)=>L("option",{key:x,value:x},x)))),L("label",null,"执行 Provider",L("select",{value:Bf,disabled:nf,onChange:(x)=>eH(String(x.target.value||"main-server")),"data-testid":"codex-provider-select"},vJ.map((x)=>L("option",{key:x.id,value:x.id},`${x.label||x.id} · ${x.defaultWorkdir||w2(h0,x.id)}`)))),L("label",null,"工作目录",L("input",{value:N0,disabled:nf,onChange:(x)=>a0(x.target.value),placeholder:tH||h0?.defaultWorkdir||"/root/unidesk","data-testid":"codex-cwd-input"})),L("label",null,"最大尝试",L("input",{type:"number",min:1,max:99,value:pu,disabled:nf,onChange:(x)=>mu(Number(x.target.value)),"data-testid":"codex-max-attempts-input"})),L("label",null,"入队份数",L("input",{type:"number",min:1,max:50,value:C0,disabled:nf,onChange:(x)=>Q1(Number(x.target.value)),"data-testid":"codex-repeat-count-input"}))),W3>1?L("label",{className:`codex-batch-confirm ${ru?"confirmed":""}`,"data-testid":"codex-batch-confirm-row"},L("input",{type:"checkbox",checked:ru,disabled:nf,onChange:(x)=>Uf(Boolean(x.target.checked)),"data-testid":"codex-batch-confirm-checkbox"}),L("span",null,`确认批量入队 ${W3} 个任务(prompt 分段 ${Q3.length} × 入队份数 ${LG(C0)})`)):null,nf?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:v0||nf||Gf.length===0&&o.length===0,onClick:()=>{c(""),e(""),Uf(!1);let x="已清空任务输入栏";pf(x),Q0("success",x)},"data-testid":"codex-clear-input-button"},"清空输入"),L("button",{type:"submit",className:"primary-btn",disabled:kH,"data-testid":"codex-enqueue-button"},nf?"提交中,请等待...":xJ?`请确认批量入队 ${W3} 个任务`:Jl.length>1?`批量入队 ${Jl.length} 个任务`:"入队并运行"))))),L("div",{className:"codex-main-stage"},L("div",{className:"codex-detail-grid"},L(B$,{title:"运行控制",eyebrow:cy?"Queued prompt editable":qr?"Active turn steer":"Steer when running",loading:v0},L("div",{className:"codex-run-control-stack"},L(yB,{task:D,queueRows:z1,busy:v0,onMove:WO}),D?.id?L("form",{className:"codex-steer-form codex-edit-prompt-form",onSubmit:JO,"data-testid":"codex-edit-prompt-form"},L("label",null,"编辑 queued 用户 prompt",L("textarea",{value:e0,rows:5,onChange:(x)=>Zu(x.target.value),placeholder:"仅 QUEUED 且尚未开始运行的任务可在这里修改原始用户 prompt。",disabled:!cy||v0,"data-testid":"codex-edit-prompt-textarea"})),L("label",{className:"codex-reference-field"},"引用任务 ID(可选,留空会清除引用)",L("input",{value:i0,disabled:!cy||v0,onChange:(x)=>Du(x.target.value),placeholder:"codex_...;支持空格/逗号分隔多个 ID","data-testid":"codex-edit-reference-task-id"}),Jy(i0).length>0?L("code",null,`将保留/注入:${Jy(i0).join(" / ")}`):null),L("div",{className:"codex-form-actions"},L("button",{type:"button",className:"ghost-btn",disabled:!D?.id||v0,onClick:()=>{Zu(D?B2(D):""),Du(Array.isArray(D?.referenceTaskIds)?D.referenceTaskIds.join(" "):"")},"data-testid":"codex-edit-prompt-reset"},"恢复当前值"),L("button",{type:"submit",className:"primary-btn",disabled:!cy||v0||e0.trim().length===0,title:cy?"保存后会重写尚未运行任务的用户 prompt":"只有 QUEUED 且尚未开始的任务可编辑 prompt","data-testid":"codex-edit-prompt-submit"},"保存 queued prompt"))):null,L("form",{className:"codex-steer-form",onSubmit:FO},L("label",null,"追加 prompt",L("textarea",{value:Cf,rows:4,onChange:(x)=>d0(x.target.value),placeholder:"给正在运行的 Codex session 推入新的指令或纠偏。",disabled:!qr})),L("button",{type:"submit",className:"primary-btn",disabled:!qr||v0||Cf.trim().length===0,"data-testid":"codex-steer-button"},"推入运行中 session")))),L(B$,{title:"完成判定",eyebrow:D?.lastJudge?D.lastJudge.source:"judge",loading:m},D?.lastJudge?L("div",{className:"codex-judge-card","data-testid":"codex-task-judge-card"},L(Wy,{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"},Ej(D.lastJudge.reason||"--",180)),D.lastJudge.continuePrompt?L("code",{"data-testid":"codex-task-judge-continue-prompt"},Ej(D.lastJudge.continuePrompt,160)):null):L(zy,{title:"尚未判定",text:"Codex turn 结束后会由 MiniMax M2.7 或 fallback judge 判定 complete/retry/fail;retry 会在已有 thread 追加继续执行 prompt。"}))),L(_B,{stats:Nr,queueName:iy,onRaw:u}),L(B$,{title:"Attempts",eyebrow:"terminal vs interruption",loading:m},L(UB,{task:D})))))}var w6=cf(O0(),1);var Tf=w6.default.createElement,{useEffect:Sj}=w6.default,Cj=w6.default.useState,QB=w6.default.useRef,cj=` :root { --surfacePrimary: #ffffff; --surfaceSecondary: #f8fafc; @@ -224,64 +224,64 @@ nav .material-icons::before { 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("+",` +`;function ij({title:f,eyebrow:u,actions:l,children:_,className:y,loading:$}){return Tf("section",{className:`panel ${y||""}`},Tf("div",{className:"panel-head"},Tf("div",null,u?Tf("p",{className:"panel-eyebrow"},u):null,Tf(j0,{title:f,loading:$})),l?Tf("div",{className:"panel-actions"},l):null),Tf("div",{className:"panel-body"},_))}function WB({title:f,data:u,onOpen:l,testId:_}){return Tf("button",{type:"button",className:"ghost-btn","data-testid":_,onClick:()=>l(f,u)},"查看原始JSON")}function zB({title:f,text:u}){return Tf("div",{className:"empty-state"},Tf("strong",null,f),Tf("span",null,u))}function eG(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function fK(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function uK(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function GB(f){return f.filter((l)=>l?.id==="filebrowser"||String(l?.id||"").startsWith("filebrowser-")).sort((l,_)=>{let y=($)=>$.providerId==="D518"?0:$.providerId==="D601"?1:$.id==="filebrowser"?2:3;return y(l)-y(_)||String(l.id).localeCompare(String(_.id))})}function KB(f){if(f?.providerId==="D518")return"D518";return f?.providerId||f?.name||f?.id||"Unknown"}function NB(f,u,l="/"){let _=l.startsWith("/")?l:`/${l}`;return`${f}/microservices/${encodeURIComponent(u)}/proxy${_}`}function ZB(f,u){return`${f}/microservices/${encodeURIComponent(u)}/health`}async function EB(f,u=16000){let l=new AbortController,_=setTimeout(()=>l.abort(),u);try{return await Mf(f,{signal:l.signal,failureFields:[!1]})}finally{clearTimeout(_)}}function lK(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 C2(f){return f?.status==="OK"||f?.ok===!0}function HB({service:f,active:u,health:l,onSelect:_,onRaw:y}){let $=eG(f),r=fK(f),j=uK(f),A=$.container||{},J=C2(l?.body);return Tf("button",{type:"button",className:`filebrowser-target-card ${u?"active":""}`,"data-testid":`filebrowser-target-card-${f.id}`,onClick:_},Tf("span",{className:`status-badge ${J?"ok":$.providerStatus==="online"?"running":"warn"}`},J?"Health OK":$.providerStatus||"unknown"),Tf("strong",null,f.name||f.id),Tf("span",null,lK(f)),Tf("code",null,`${r.nodeBindHost||"--"}:${r.nodePort||"--"}`),Tf("small",null,A.name?`${A.name} / ${A.state||"--"}`:`${j.composeService||"--"}`),Tf("span",{className:"filebrowser-card-raw",onClick:(U)=>{U.stopPropagation(),y(`${f.name} service`,f)}},"JSON"))}function _K(f){try{return f?.contentDocument||f?.contentWindow?.document||null}catch{return null}}function Rj(f){let u=_K(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!==cj)l.textContent=cj;return!0}function OB(f,u){let l=URL.createObjectURL(f),_=document.createElement("a");_.href=l,_.download=u,document.body.appendChild(_),_.click(),_.remove(),setTimeout(()=>URL.revokeObjectURL(l),2000)}function VB(f,u){let l=_K(f);if(l===null||l.documentElement===null)throw Error("无法访问 File Browser iframe 文档");Rj(f);let _=Math.max(640,Math.ceil(f.clientWidth||l.documentElement.clientWidth||1280)),y=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 r=$.querySelector("head");if(r===null)r=l.createElement("head"),$.insertBefore(r,$.firstChild);let j=l.createElement("style");j.textContent=`${cj} +html,body{width:${_}px!important;min-height:${y}px!important;overflow:hidden!important;}`,r.appendChild(j);let A=new XMLSerializer().serializeToString($),J=`${A}`;OB(new Blob([J],{type:"image/svg+xml;charset=utf-8"}),u.replace(/\.png$/i,".svg"))}function yK({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let _=GB(Array.isArray(f)?f:[]),y=new URLSearchParams(window.location.search).get("target")||"",$=y==="filebrowser-d518"?"filebrowser":y,r=_.some((Y)=>Y.id===$)?$:_[0]?.id||"",[j,A]=Cj(r),[J,U]=Cj({loading:!1,refreshedAt:null,health:{},error:""}),[Q,W]=Cj({exporting:!1,message:"",error:""}),G=QB(null),K=_.find((Y)=>Y.id===j)||_[0]||null,H=eG(K),O=fK(K),z=uK(K),Z=K?J.health[K.id]:null,N=K?NB(l,K.id,"/"):"about:blank";Sj(()=>{if(_.length===0)return;if(!j||!_.some((Y)=>Y.id===j))A(_[0].id)},[_.map((Y)=>Y.id).join(",")]),Sj(()=>{let Y=0,w=setInterval(()=>{if(Y+=1,Rj(G.current)||Y>=24)clearInterval(w)},500);return()=>clearInterval(w)},[N]),Sj(()=>{if(_.length===0)return;let Y=!1;async function w(){U((h)=>({...h,loading:!0,error:""}));let P=await Promise.all(_.map(async(h)=>{try{let M=await EB(ZB(l,h.id));return[h.id,{ok:!0,body:M}]}catch(M){return[h.id,{ok:!1,error:Df(M,"File Browser health failed")}]}}));if(Y)return;U({loading:!1,refreshedAt:new Date().toISOString(),health:Object.fromEntries(P),error:""})}w();let B=setInterval(w,30000);return()=>{Y=!0,clearInterval(B)}},[_.map((Y)=>`${Y.id}:${Y.runtime?.providerStatus||""}`).join(","),l]);function E(Y){A(Y);let w=new URL(window.location.href);w.searchParams.set("target",Y),window.history.replaceState({},"",`${w.pathname}${w.search}`)}async function q(){if(Q.exporting)return;W({exporting:!0,message:"",error:""});try{let Y=new Date().toISOString().replace(/[-:.TZ]/g,"").slice(0,14);await VB(G.current,`unidesk-filebrowser-${K?.id||"target"}-${Y}.png`),W({exporting:!1,message:"截图已导出",error:""})}catch(Y){W({exporting:!1,message:"",error:Df(Y,"截图导出失败")})}}if(_.length===0)return Tf(zB,{title:"File Browser 未登记",text:"请在 config.json 的 microservices 中登记 id=filebrowser 或 filebrowser-* 用户服务"});return Tf("div",{className:"filebrowser-page","data-testid":"filebrowser-page"},J.error?Tf(A0,{error:J.error,wide:!0}):null,Tf(ij,{title:"文件管理器",eyebrow:"File Browser / Host Files",loading:J.loading,actions:Tf("div",{className:"panel-actions"},K?Tf("button",{type:"button",className:"ghost-btn",onClick:q,disabled:Q.exporting,"data-testid":"filebrowser-export-screenshot"},Q.exporting?"导出中...":"导出截图"):null,K?Tf("a",{className:"ghost-btn",href:N,target:"_blank",rel:"noreferrer"},"新窗口打开"):null,K?Tf(WB,{title:"File Browser 当前目标",data:{service:K,health:Z},onOpen:u,testId:"raw-filebrowser-active"}):null)},Tf("div",{className:"filebrowser-hero"},Tf("div",null,Tf("span",{className:`status-badge ${C2(Z?.body)?"ok":"warn"}`},C2(Z?.body)?"Health OK":"Health Pending"),Tf("h3",null,K?.name||"File Browser"),Tf("p",{className:"muted paragraph"},K?.description||"通过 UniDesk 登录态代理访问,不开放 File Browser 公网端口。"),Q.error?Tf("p",{className:"filebrowser-shot-error"},Q.error):null,Q.message?Tf("p",{className:"filebrowser-shot-ok"},Q.message):null),Tf("div",{className:"microservice-ref-card"},Tf("span",null,"Provider"),Tf("strong",null,K?.providerId||"--"),Tf("code",null,H.providerName||K?.providerId||"--")),Tf("div",{className:"microservice-ref-card"},Tf("span",null,"Private Backend"),Tf("strong",null,`${O.nodeBindHost||"--"}:${O.nodePort||"--"}`),Tf("code",null,O.nodeBaseUrl||"--")),Tf("div",{className:"microservice-ref-card"},Tf("span",null,"Image"),Tf("strong",null,z.dockerfile||"filebrowser/filebrowser:v2.63.3"),Tf("code",null,z.commitId||"--")),Tf("div",{className:"microservice-ref-card"},Tf("span",null,"Mount"),Tf("strong",null,lK(K)),Tf("code",null,K?.providerId==="main-server"?"/root, /var, /home":"/home, /mnt/c, /mnt/d")))),Tf(ij,{title:"浏览目标",eyebrow:`${_.length} host targets`,loading:J.loading},Tf("div",{className:"filebrowser-target-grid"},_.map((Y)=>Tf(HB,{key:Y.id,service:Y,active:Y.id===K?.id,health:J.health[Y.id],onSelect:()=>E(Y.id),onRaw:u})))),Tf(ij,{title:`${KB(K)} 文件视图`,eyebrow:Z?.body?`Health ${C2(Z.body)?"OK":"UNKNOWN"} / ${J.refreshedAt?W0(J.refreshedAt):"--"}`:"Embedded WebUI",className:"filebrowser-frame-panel"},Tf("div",{className:"filebrowser-frame-shell"},Tf("div",{className:"filebrowser-frame-toolbar"},Tf("span",null,"BaseURL"),Tf("code",null,`/api/microservices/${K?.id||"filebrowser"}/proxy`),Tf("span",null,"Root"),Tf("code",null,"/srv"),Tf("span",{className:"filebrowser-compact-note"},"Compact layout injected")),Tf("iframe",{ref:G,key:N,title:`${K?.name||"File Browser"} WebUI`,src:N,className:"filebrowser-frame","data-testid":"filebrowser-frame",onLoad:(Y)=>Rj(Y.currentTarget),sandbox:"allow-downloads allow-forms allow-modals allow-same-origin allow-scripts"}))))}var x2=cf(O0(),1);var Qf=x2.default.createElement,{useEffect:qB}=x2.default,LB=x2.default.useState;function i2({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return Qf("span",{className:`status-badge ${l}`},u||f||"unknown")}function K_({label:f,value:u,hint:l,tone:_}){return Qf("article",{className:`metric-card ${_||""}`},Qf("div",{className:"metric-label"},f),Qf("div",{className:"metric-value"},u),Qf("div",{className:"metric-hint"},l))}function c2({title:f,eyebrow:u,actions:l,children:_,className:y,loading:$}){return Qf("section",{className:`panel ${y||""}`},Qf("div",{className:"panel-head"},Qf("div",null,u?Qf("p",{className:"panel-eyebrow"},u):null,Qf(j0,{title:f,loading:$})),l?Qf("div",{className:"panel-actions"},l):null),Qf("div",{className:"panel-body"},_))}function R2({title:f,data:u,onOpen:l,testId:_}){return Qf("button",{type:"button",className:"ghost-btn","data-testid":_,onClick:()=>l(f,u)},"查看原始JSON")}function xj({title:f,text:u}){return Qf("div",{className:"empty-state"},Qf("strong",null,f),Qf("span",null,u))}function XB(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function BB(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function YB(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function Gy(f,u){let l=f&&typeof f==="object"?f[u]:void 0;return Number.isFinite(Number(l))?String(l):"--"}function wB(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 $K({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let _=f.find((K)=>K.id==="findjob")||null,[y,$]=LB({loading:!1,error:"",health:null,summary:null,jobs:null,drafts:null,refreshedAt:null});async function r(){if(!_)return;$((K)=>({...K,loading:!0,error:""}));try{let[K,H,O,z]=await Promise.all([Mf(`${l}/microservices/findjob/health`),Mf(`${l}/microservices/findjob/proxy/api/summary`),Mf(`${l}/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:40`),Mf(`${l}/microservices/findjob/proxy/api/drafts`)]);$({loading:!1,error:"",health:K,summary:H,jobs:O,drafts:z,refreshedAt:new Date})}catch(K){$((H)=>({...H,loading:!1,error:Df(K,"FindJob 加载失败")}))}}if(qB(()=>{r()},[_?.id,_?.runtime?.providerStatus]),!_)return Qf(xj,{title:"FindJob 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=findjob"});let j=XB(_),A=YB(_),J=BB(_),U=y.summary||{},Q=wB(y.jobs),W=DB(y.drafts),G=y.jobs?._unidesk?.arrayLimits?.jobs;return Qf("div",{className:"findjob-page","data-testid":"findjob-page"},Qf(c2,{title:"FindJob 工作台",eyebrow:"D601 用户服务",loading:y.loading,actions:Qf("div",{className:"panel-actions"},Qf("button",{type:"button",className:"ghost-btn",onClick:r,disabled:y.loading,"data-testid":"findjob-refresh-button"},y.loading?"刷新中":"刷新"),Qf(R2,{title:"FindJob 用户服务",data:_,onOpen:u,testId:"raw-findjob-service"}))},Qf("div",{className:"findjob-hero"},Qf("div",null,Qf("div",{className:"node-version-line"},Qf(i2,{status:j.providerStatus==="online"?"online":"warn"},j.providerStatus||"unknown"),Qf("span",null,_.providerId),Qf("span",null,J.public?"公网暴露":"仅 UniDesk frontend 代理访问")),Qf("p",{className:"muted paragraph"},_.description)),Qf("div",{className:"microservice-ref-card"},Qf("span",null,"Repo"),Qf("strong",null,A.url||"--"),Qf("code",null,A.commitId||"--")),Qf("div",{className:"microservice-ref-card"},Qf("span",null,"D601 Docker"),Qf("strong",null,`${J.nodeBindHost||"--"}:${J.nodePort||"--"}`),Qf("code",null,`${A.composeFile||"--"} / ${A.composeService||"--"}`))),Qf(A0,{error:y.error,wide:!0})),Qf("div",{className:"findjob-grid"},Qf(c2,{title:"岗位指标",eyebrow:y.refreshedAt?`Updated ${W0(y.refreshedAt)}`:"Summary",loading:y.loading},Qf("div",{className:"metric-grid"},Qf(K_,{label:"岗位总量",value:Gy(U,"totalJobs"),hint:"tracked jobs",tone:"ok"}),Qf(K_,{label:"原始岗位",value:Gy(U,"rawJobs"),hint:"raw queue"}),Qf(K_,{label:"已验证",value:Gy(U,"verifiedJobs"),hint:"verified set"}),Qf(K_,{label:"优先处理",value:Gy(U,"prioritizedJobs"),hint:"prioritized"}),Qf(K_,{label:"过期",value:Gy(U,"staleJobs"),hint:"stale jobs",tone:"warn"}),Qf(K_,{label:"无效",value:Gy(U,"invalidJobs"),hint:"invalid jobs",tone:"warn"}),Qf(K_,{label:"上海",value:Gy(U,"shanghaiJobs"),hint:"city filter"}),Qf(K_,{label:"Health",value:y.health?.ok?"OK":"--",hint:"D601 /api/health"})),Qf("div",{className:"panel-actions inline-actions"},Qf(R2,{title:"FindJob Summary",data:U,onOpen:u,testId:"raw-findjob-summary"}))),Qf(c2,{title:"近期岗位",eyebrow:G?`${G.returnedLength}/${G.originalLength} Preview`:`${Q.length} Preview`,loading:y.loading},Q.length===0?Qf(xj,{title:"暂无岗位预览",text:"等待 D601 findjob backend 返回 /api/jobs"}):Qf("div",{className:"table-wrap findjob-job-table"},Qf("table",null,Qf("thead",null,Qf("tr",null,Qf("th",null,"优先级"),Qf("th",null,"状态"),Qf("th",null,"单位"),Qf("th",null,"职位"),Qf("th",null,"城市"),Qf("th",null,"阶段"),Qf("th",null,"截止"),Qf("th",null,"证据"))),Qf("tbody",null,Q.map((K)=>Qf("tr",{key:K.id},Qf("td",null,Qf(i2,{status:String(K.priority||"").toLowerCase()||"unknown"},K.priority||"--")),Qf("td",null,Qf(i2,{status:String(K.status||"").toLowerCase()||"unknown"},K.status||"--")),Qf("td",null,K.organization_name||"--",Qf("code",null,K.id||"--")),Qf("td",null,K.display_title||K.title||"--"),Qf("td",null,K.display_city||K.city||"--"),Qf("td",null,K.workflow_stage||"--"),Qf("td",null,K.deadline||"--"),Qf("td",null,K.evidence_url?Qf("a",{href:K.evidence_url,target:"_blank",rel:"noreferrer"},"打开"):Qf("span",{className:"muted"},"无"))))))),Qf("div",{className:"panel-actions inline-actions"},Qf(R2,{title:"FindJob Jobs Preview",data:y.jobs,onOpen:u,testId:"raw-findjob-jobs"}))),Qf(c2,{title:"草稿与报告",eyebrow:`${W.length} Drafts`,loading:y.loading},W.length===0?Qf(xj,{title:"暂无草稿",text:"D601 findjob backend 未返回 drafts"}):Qf("div",{className:"draft-list"},W.map((K)=>Qf("article",{key:K.id,className:"draft-card"},Qf("div",{className:"node-card-head"},Qf("strong",null,K.id),Qf(i2,{status:K.status},K.status||"--")),Qf("div",{className:"docker-meta compact"},Qf("span",null,K.workflow_stage||"--"),Qf("span",null,`jobs ${K.counts?.jobs??0}`),Qf("span",null,`reports ${K.counts?.reports??0}`)),Qf("span",null,K.latestReportPath||"暂无报告"),Qf("code",null,Nf(K.updated_at||K.updatedAt))))),Qf("div",{className:"panel-actions inline-actions"},Qf(R2,{title:"FindJob Drafts",data:y.drafts,onOpen:u,testId:"raw-findjob-drafts"})))))}var P6=cf(O0(),1);var R=P6.default.createElement,{useEffect:TB}=P6.default,bj=P6.default.useState;function D6(f){let u=Number(f);return Number.isFinite(u)?`${Math.max(0,Math.min(100,u)).toFixed(1)}%`:"--"}function hj(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 Ij(f,u=2){let l=Number(f);if(!Number.isFinite(l))return f===!1?"false":f===!0?"true":"--";let _=Math.abs(l);if(Number.isInteger(l)||_>=1000)return l.toLocaleString("zh-CN",{maximumFractionDigits:0});if(_>=1)return l.toLocaleString("zh-CN",{maximumFractionDigits:u});return l.toLocaleString("zh-CN",{maximumFractionDigits:Math.max(u,6)})}function M6(f){if(f===null||f===void 0||f==="")return"--";if(typeof f==="boolean")return f?"true":"false";if(typeof f==="number")return Ij(f,4);if(Array.isArray(f))return f.map((u)=>M6(u)).join(" x ");if(typeof f==="object")return"已上报";return String(f)}function b2(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 v2(f){return f.replace(/[^a-zA-Z0-9_-]/g,"-")}function Ru(f){return f&&typeof f==="object"&&!Array.isArray(f)?f:{}}function T6({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return R("span",{className:`status-badge ${l}`},u||f||"unknown")}function N_({label:f,value:u,hint:l,tone:_}){return R("article",{className:`metric-card ${_||""}`},R("div",{className:"metric-label"},f),R("div",{className:"metric-value"},u),R("div",{className:"metric-hint"},l))}function vj({title:f,eyebrow:u,actions:l,children:_,className:y,loading:$}){return R("section",{className:`panel ${y||""}`},R("div",{className:"panel-head"},R("div",null,u?R("p",{className:"panel-eyebrow"},u):null,R(j0,{title:f,loading:$})),l?R("div",{className:"panel-actions"},l):null),R("div",{className:"panel-body"},_))}function T$({title:f,data:u,onOpen:l,testId:_}){return R("button",{type:"button",className:"ghost-btn","data-testid":_,onClick:(y)=>{y?.stopPropagation?.(),l(f,u)}},"查看原始JSON")}function P1({title:f,text:u}){return R("div",{className:"empty-state"},R("strong",null,f),R("span",null,u))}function MB(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function PB(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function nB(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function SB(f){return f?.counts&&typeof f.counts==="object"&&!Array.isArray(f.counts)?f.counts:{}}function CB(f){return Array.isArray(f?.jobs)?f.jobs.slice(0,240):[]}function iB(f){return Array.isArray(f?.projects)?f.projects.slice(0,1000):[]}function h2(f){return Array.isArray(f?.projects)?f.projects:[]}function cB(f,u){if(Array.isArray(u?.gpu))return u.gpu;if(Array.isArray(f?.gpu))return f.gpu;return[]}function yl(f,u){return`${f}/microservices/met-nonlinear/proxy${u}`}function rK(f){return f.startedAt&&f.finishedAt?hj((Date.parse(f.finishedAt)-Date.parse(f.startedAt))/1000):"--"}function RB(f){let u=f.progress||{};if(u.etaSeconds!==null&&u.etaSeconds!==void 0&&u.etaSeconds!==""){let r=Number(u.etaSeconds);if(Number.isFinite(r))return Math.max(0,r)}let l=Number(u.currentEpoch),_=Number(u.epochTarget??f.epochTarget),y=Date.parse(f.startedAt||"");if(!Number.isFinite(l)||l<=0||!Number.isFinite(_)||_<=l||!Number.isFinite(y))return null;let $=Math.max(0,(Date.now()-y)/1000);if($<=0)return null;return Math.max(0,$/l*(_-l))}function jK(f){let u=f.progress||{},l=Number(u.epochPerHour);if(Number.isFinite(l)&&l>0)return l;let _=Date.parse(f.startedAt||""),y=["succeeded","failed","canceled"].includes(f.status)?Date.parse(f.finishedAt||""):Date.now();if(!Number.isFinite(_)||!Number.isFinite(y)||y<=_)return null;let $=Number(u.currentEpoch??f.epochTarget);if(!Number.isFinite($)||$<=0)return null;return $/((y-_)/3600000)}function AK(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 FK(f,u,l){return{name:f,path:u,depth:l,count:0,children:[],project:null}}function xB(f){let u=FK("","",-1);for(let _ of f){let $=String(_?.projectPath||"").replace(/\\/g,"/").split("/").filter(Boolean);if($.length===0)continue;let r=u,j=[];for(let[A,J]of $.entries()){j.push(J);let U=j.join("/"),Q=r.children.find((W)=>W.path===U);if(!Q)Q=FK(J,U,A),r.children.push(Q);if(A===$.length-1)Q.project=_;r=Q}}let l=(_)=>{let y=_.children.reduce(($,r)=>$+l(r),0);return _.count=(_.project?1:0)+y,_.children.sort(($,r)=>{if(Boolean($.project)!==Boolean(r.project))return $.project?1:-1;return $.name.localeCompare(r.name,"zh-CN",{numeric:!0,sensitivity:"base"})}),_.count};return l(u),u}function bB(f){let u=Ru(f.data);return Ru(u.project).projectPath?Ru(u.project):u}function vB(f){return Ru(Ru(f.data).job)}function JK({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let _=f.find((c)=>c.id==="met-nonlinear")||null,[y,$]=bj({loading:!1,actionBusy:!1,error:"",health:null,summary:null,queue:null,projects:null,history:null,images:null,refreshedAt:null}),[r,j]=bj({loading:!1,error:"",kind:"",key:"",title:"",data:null}),[A,J]=bj(()=>({activeTab:"projects",selectedProjects:{},expandedProjectDirs:{},sourceProject:"",forkCount:1,forkEpochs:200,forkPrefix:`ui_fork_${Date.now()}`,maxConcurrency:3,targetGpuName:"2080 Ti",actionMessage:""}));function U(c){J((o)=>({...o,...c}))}async function Q(c=A.activeTab){if(!_)return;$((o)=>({...o,loading:!0,error:""}));try{let o=[["health",Mf(`${l}/microservices/met-nonlinear/health`)],["summary",Mf(yl(l,"/api/summary"))]];if(c==="projects")o.push(["projectsRoot",Mf(yl(l,"/api/projects?root=projects&limit=500"))]),o.push(["exProjectsRoot",Mf(yl(l,"/api/projects?root=ex_projects&limit=500"))]);if(c==="current"||c==="completed"||c==="failed")o.push(["queue",Mf(yl(l,"/api/queue"))]);if(c==="completed"||c==="failed")o.push(["history",Mf(yl(l,"/api/history"))]);if(c==="gpu")o.push(["images",Mf(yl(l,"/api/images"))]);let e=Object.fromEntries(await Promise.all(o.map(async([k,Af])=>[k,await Af]))),Kf={loading:!1,actionBusy:!1,error:"",health:e.health,summary:e.summary,refreshedAt:new Date};if(e.projectsRoot||e.exProjectsRoot){let{projectsRoot:k,exProjectsRoot:Af}=e;Kf.projects={ok:k?.ok!==!1&&Af?.ok!==!1,roots:[{root:"projects",count:h2(k).length},{root:"ex_projects",count:h2(Af).length}],projects:[...h2(k),...h2(Af)]}}if(e.queue)Kf.queue=e.queue;if(e.history)Kf.history=e.history;if(e.images)Kf.images=e.images;$((k)=>({...k,...Kf}))}catch(o){$((e)=>({...e,loading:!1,actionBusy:!1,error:Df(o,"MET Nonlinear 加载失败")}))}}async function W(c,o){$((e)=>({...e,actionBusy:!0,error:""})),U({actionMessage:`${c}...`});try{let e=await o();U({actionMessage:e||`${c}完成`}),await Q()}catch(e){$((Kf)=>({...Kf,actionBusy:!1,error:Df(e,`${c}失败`)}))}}async function G(){await W("保存并发设置",async()=>{await Mf(yl(l,"/api/queue/settings"),{method:"PUT",body:JSON.stringify({maxConcurrency:Number(A.maxConcurrency),targetGpuName:A.targetGpuName})})})}function K(){return Object.entries(A.selectedProjects).filter(([,c])=>c).map(([c])=>c)}async function H(){let c=K();if(c.length===0)throw Error("请先选择至少一个 project");await W("加入待启动队列",async()=>{await Mf(yl(l,"/api/queue"),{method:"POST",body:JSON.stringify({projectPaths:c,maxConcurrency:Number(A.maxConcurrency),targetGpuName:A.targetGpuName,start:!1})}),U({activeTab:"current",selectedProjects:{}})})}async function O(){let c=A.sourceProject||S[0]?.projectPath;if(!c)throw Error("请先选择源 project");await W("Fork Project",async()=>{let o=await Mf(yl(l,"/api/projects/fork"),{method:"POST",body:JSON.stringify({sourceProject:c,count:Number(A.forkCount),epochs:Number(A.forkEpochs),prefix:A.forkPrefix})}),e=Array.isArray(o.projectPaths)?o.projectPaths:[],Kf=e.reduce((k,Af)=>{return k[Af]=!0,k},{...A.selectedProjects});return U({selectedProjects:Kf}),`已 fork ${e.length} 个 project,并已自动勾选;请确认后点击加入待启动队列。`})}async function z(){await W("启动队列",async()=>{await Mf(yl(l,"/api/queue/start"),{method:"POST",body:JSON.stringify({maxConcurrency:Number(A.maxConcurrency),targetGpuName:A.targetGpuName})}),U({activeTab:"current"})})}async function Z(c){await W("取消任务",async()=>{await Mf(yl(l,`/api/jobs/${encodeURIComponent(c.id)}/cancel`),{method:"POST",body:JSON.stringify({})})})}async function N(c){let o=String(c?.projectPath||"");if(!o)return;j({loading:!0,error:"",kind:"project",key:o,title:o,data:null});try{let e=await Mf(yl(l,`/api/projects/config?path=${encodeURIComponent(o)}`));j({loading:!1,error:"",kind:"project",key:o,title:o,data:e})}catch(e){j({loading:!1,error:Df(e,"Project 详情加载失败"),kind:"project",key:o,title:o,data:null})}}async function E(c){let o=String(c?.id||"");if(!o)return;j({loading:!0,error:"",kind:"job",key:o,title:c.projectPath||o,data:null});try{let e=await Mf(yl(l,`/api/jobs/${encodeURIComponent(o)}`));j({loading:!1,error:"",kind:"job",key:o,title:e?.job?.projectPath||c.projectPath||o,data:e})}catch(e){j({loading:!1,error:Df(e,"Job 详情加载失败"),kind:"job",key:o,title:c.projectPath||o,data:null})}}if(TB(()=>{Q(A.activeTab)},[_?.id,_?.runtime?.providerStatus,A.activeTab]),!_)return R(P1,{title:"MET Nonlinear 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=met-nonlinear"});let q=MB(_),Y=nB(_),w=PB(_),B=SB(y.queue?.queue||y.summary?.queue),P=cB(y.health,y.queue),h=y.health?.targetGpu||y.summary?.targetGpu||P.find((c)=>String(c.name||"").includes("2080")),M=y.images?.mlImage||y.health?.image||{},n=CB(y.queue),S=iB(y.projects),T=xB(S),i=A.sourceProject||S[0]?.projectPath||"",C=n.filter((c)=>["staged","queued","running"].includes(c.status)),v=n.filter((c)=>c.status==="succeeded"),X=n.filter((c)=>["failed","canceled"].includes(c.status)),D=Array.isArray(y.history?.jobs)?y.history.jobs.slice(0,120):[],p=[{id:"projects",label:"项目库",count:S.length},{id:"current",label:"当前队列",count:C.length||Number(B.staged||0)+Number(B.queued||0)+Number(B.running||0)},{id:"completed",label:"已完成",count:v.length||Number(B.succeeded||0)},{id:"failed",label:"失败诊断",count:X.length||Number(B.failed||0)+Number(B.canceled||0)},{id:"gpu",label:"GPU/镜像",count:P.length}];function m(c,o){if(c.length===0)return R(P1,{title:o==="current"?"当前队列为空":"暂无记录",text:o==="current"?"从项目库选择或 fork project 后先加入待启动队列,再启动队列。":"终态任务会显示耗时、exit code 和失败诊断。"});return R("div",{className:"table-wrap met-job-table"},R("table",null,R("thead",null,R("tr",null,R("th",null,"状态"),R("th",null,"Project"),R("th",null,"Epoch"),R("th",null,"速度"),R("th",null,"ETA/耗时"),R("th",null,"GPU"),R("th",null,"Exit"),R("th",null,"更新时间"),R("th",null,"操作"))),R("tbody",null,c.map((e)=>{let Kf=e.progress||{},k=["staged","queued","running"].includes(e.status),Af=r.kind==="job"&&r.key===e.id;return R("tr",{key:e.id,className:`met-click-row ${Af?"active":""}`,onClick:()=>E(e),"data-testid":`met-job-row-${v2(e.id)}`},R("td",null,R(T6,{status:e.status},AK(e.status))),R("td",null,R("button",{type:"button",className:"met-inline-link",onClick:(Yf)=>{Yf.stopPropagation(),E(e)}},e.projectPath),R("code",null,e.id)),R("td",null,R("span",null,`${Kf.currentEpoch??"--"} / ${Kf.epochTarget??e.epochTarget??"--"}`),R("div",{className:"met-progress"},R("span",{style:{width:D6(Kf.progressPercent)}}))),R("td",null,R("strong",null,b2(jK(e)))),R("td",null,e.status==="succeeded"||e.status==="failed"||e.status==="canceled"?rK(e):e.status==="running"?`ETA ${hj(RB(e))}`:"--"),R("td",null,e.gpuName||"--"),R("td",null,e.exitCode??"--"),R("td",null,Nf(e.updatedAt)),R("td",null,k?R("button",{type:"button",className:"ghost-btn mini",onClick:(Yf)=>{Yf.stopPropagation(),Z(e)},disabled:y.actionBusy},"取消"):null,R(T$,{title:`MET Job ${e.id}`,data:e,onOpen:u,testId:`raw-met-job-${e.id}`})))}))))}function s(){return R("div",{className:"met-queue-summary","data-testid":"met-current-summary"},R(T6,{status:"staged"},`待启动 ${B.staged??0}`),R(T6,{status:"queued"},`排队中 ${B.queued??0}`),R(T6,{status:"running"},`训练中 ${B.running??0}`),R("span",null,`最大并发 ${y.summary?.queue?.maxConcurrency??y.queue?.queue?.maxConcurrency??A.maxConcurrency}`),R("span",null,`目标 GPU ${y.summary?.queue?.targetGpuName??y.queue?.queue?.targetGpuName??A.targetGpuName}`))}function d(c,o){let e=A.expandedProjectDirs[c];return e===void 0?o<2:Boolean(e)}function a(c,o){let e=d(c,o);U({expandedProjectDirs:{...A.expandedProjectDirs,[c]:!e}})}function I(c){let o=8+Math.max(0,c.depth)*16;if(Boolean(c.project)){let k=c.project,Af=Boolean(A.selectedProjects[k.projectPath]),Yf=r.kind==="project"&&r.key===k.projectPath;return R("div",{key:c.path,className:`met-tree-row project ${Af?"selected":""} ${Yf?"active":""}`,style:{paddingLeft:o},onClick:()=>N(k),"data-testid":`met-project-node-${v2(k.projectPath)}`},R("div",{className:"met-tree-name"},R("input",{type:"checkbox",checked:Af,onClick:(Bf)=>Bf.stopPropagation(),onChange:(Bf)=>U({selectedProjects:{...A.selectedProjects,[k.projectPath]:Bf.target.checked}}),"data-testid":`met-project-checkbox-${v2(k.projectPath)}`}),R("button",{type:"button",className:"met-inline-link project-path",onClick:(Bf)=>{Bf.stopPropagation(),N(k)}},c.name)),R("span",null,k.useModel||"--"),R("span",null,k.epochTrain??"--"),R("span",null,D6(k.progress?.progressPercent)),R("span",null,b2(k.progress?.epochPerHour)))}let Kf=d(c.path,c.depth);return R(P6.default.Fragment,{key:c.path},R("div",{className:"met-tree-row folder",style:{paddingLeft:o},"data-testid":`met-project-folder-${v2(c.path)}`},R("button",{type:"button",className:"met-tree-toggle",onClick:()=>a(c.path,c.depth),"aria-label":Kf?`折叠 ${c.path}`:`展开 ${c.path}`},Kf?"-":"+"),R("strong",null,c.name),R("span",{className:"met-tree-count"},`${c.count} projects`)),Kf?c.children.map((k)=>I(k)):null)}function ff(c){return R("div",{className:"met-detail-kv"},c.map((o)=>R("div",{key:o.label,className:"met-detail-kv-item"},R("span",null,o.label),R("strong",null,M6(o.value)),o.hint?R("small",null,o.hint):null)))}function yf(c,o){return R("div",{className:"met-detail-section"},R("h3",null,c),ff(o))}function rf(c){if(!Array.isArray(c)||c.length===0)return R(P1,{title:"模型层未上报",text:"等待 data/model_info.json 或 compute_analysis.json 生成。"});return R("div",{className:"table-wrap met-layer-table"},R("table",null,R("thead",null,R("tr",null,R("th",null,"Layer"),R("th",null,"Type"),R("th",null,"Params"),R("th",null,"Trainable"),R("th",null,"Compute"))),R("tbody",null,c.slice(0,18).map((o,e)=>R("tr",{key:`${o.name||"layer"}-${e}`},R("td",null,o.name||`#${e+1}`),R("td",null,o.type||"--"),R("td",null,Ij(o.num_params)),R("td",null,o.trainable===void 0?"--":String(Boolean(o.trainable))),R("td",null,Ij(o.compute?.total??o.estimated_cost?.weighted_units?.total)))))))}function Wf(c){let o=Array.isArray(c)?c:[];if(o.length===0)return R(P1,{title:"data/ 暂无文件",text:"训练或评估完成后会生成 training_state、metrics、model_info 等文件。"});return R("div",{className:"met-file-chip-grid"},o.slice(0,48).map((e)=>R("span",{key:e},e)),o.length>48?R("span",null,`+${o.length-48}`):null)}function Ef(c){let o=String(c||"").replace(/\x1b\[[0-9;]*[A-Za-z]/g,"").split(/\r?\n/).map((e)=>e.trim()).filter(Boolean).slice(-12);if(o.length===0)return R(P1,{title:"暂无日志尾部",text:"该任务未上报 logTail 或日志已轮转。"});return R("div",{className:"met-log-lines"},o.map((e,Kf)=>R("div",{key:`${Kf}-${e.slice(0,16)}`},e)))}function Gf(){if(r.loading)return R("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},R("div",{className:"panel-head compact"},R("div",null,R("p",{className:"panel-eyebrow"},"Detail Loading"),R(j0,{title:"详情加载中",loading:!0}))),R(P1,{title:"详情加载中",text:r.title||"正在读取 D601 data/ 和 config.json"}));if(r.error)return R("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},R(A0,{error:r.error,wide:!0}));if(!r.data)return R("section",{className:"met-detail-panel muted","data-testid":"met-detail-panel"},R(P1,{title:"选择一个项目或任务查看详情",text:"项目库、当前队列、已完成和失败诊断中的行都可以点击;默认只展示结构化字段,原始 JSON 需显式点击按钮。"}));let c=bB(r),o=vB(r),e=Ru(c.config),Kf=Ru(c.progress||o.progress),k=Ru(c.data),Af=Ru(c.metrics||k.metrics||Kf.trainingInfo?.evaluation_metrics),Yf=Ru(k.trainingInfo||Kf.trainingInfo),Bf=Ru(k.trainingState),df=Ru(c.model||k.model),_0=Array.isArray(df.modelSummary)&&df.modelSummary.length>0?df.modelSummary:df.computeLayers,y0=Ru(Yf.evaluation_metrics),N0=r.kind==="job"?"训练任务详情":"Project 详情";return R("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},R("div",{className:"panel-head compact"},R("div",null,R("p",{className:"panel-eyebrow"},r.kind==="job"?"Job + Project Detail":"Project Library Detail"),R(j0,{title:N0}),R("code",null,c.projectPath||o.projectPath||r.title)),R("div",{className:"panel-actions"},R(T$,{title:`MET ${N0}`,data:r.data,onOpen:u,testId:"raw-met-detail"}))),r.kind==="job"?yf("任务状态",[{label:"Job ID",value:o.id},{label:"状态",value:AK(o.status)},{label:"GPU",value:o.gpuName},{label:"Exit Code",value:o.exitCode},{label:"耗时",value:rK(o)},{label:"训练速度",value:b2(jK({...o,progress:Kf}))}]):null,yf("config.json",[{label:"use_model",value:e.use_model},{label:"epoch_train",value:e.epoch_train},{label:"step_per_epoch",value:e.step_per_epoch},{label:"learning_rate",value:e.learning_rate},{label:"using_gpu",value:e.using_gpu},{label:"use_points",value:e.use_points},{label:"sample_rate",value:e.sample_rate},{label:"time_clipped_s",value:e.time_clipped_s},{label:"H_UNITS",value:e.H_UNITS},{label:"INNER_KAN_UNITS",value:e.INNER_KAN_UNITS},{label:"INNER_KAN_LAYERS",value:e.INNER_KAN_LAYERS},{label:"GRID_SIZE",value:e.GRID_SIZE},{label:"SPLINE_ORDER",value:e.SPLINE_ORDER},{label:"USE_FAST_MODEL",value:e.USE_FAST_MODEL},{label:"IIR_TRAINABLE",value:e.IIR_TRAINABLE}]),yf("data/ 训练状态",[{label:"Epoch",value:`${Kf.currentEpoch??Bf.current_epoch??Bf.completed_epoch??"--"} / ${Kf.epochTarget??e.epoch_train??"--"}`},{label:"Progress",value:D6(Kf.progressPercent)},{label:"Last Loss",value:Kf.lastLoss??Bf.loss},{label:"Last Val Loss",value:Kf.lastValLoss??Bf.val_loss},{label:"Min Loss",value:Yf.min_loss??Bf.min_loss},{label:"Min Val Loss",value:Yf.min_val_loss??Bf.min_val_loss},{label:"Log Lines",value:Kf.logLineCount},{label:"ETA",value:hj(Kf.etaSeconds??Bf.remaining_time)},{label:"训练速度",value:b2(Kf.epochPerHour??Bf.smoothed_speed)},{label:"Training Alive",value:Bf.training_alive}]),yf("模型参数",[{label:"Model Type",value:df.modelType??e.use_model},{label:"Total Params",value:df.totalParams,hint:df.totalParams===null||df.totalParams===void 0?"未上报":"data/model_info.json"},{label:"Trainable",value:df.trainableParams},{label:"Non-trainable",value:df.nonTrainableParams},{label:"Compute Cost",value:df.computeCost},{label:"Estimate Status",value:df.estimateStatus},{label:"Unsupported Layers",value:df.unsupportedLayerCount}]),yf("指标",[{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:`${M6(Yf.learning_rate_min)} / ${M6(Yf.learning_rate_mean)} / ${M6(Yf.learning_rate_max)}`}]),R("div",{className:"met-detail-section"},R("h3",null,"模型层"),rf(_0)),R("div",{className:"met-detail-section"},R("h3",null,"data/ 文件"),Wf(k.files)),r.kind==="job"?R("div",{className:"met-detail-section"},R("h3",null,"日志尾部"),Ef(Ru(r.data).logTail)):null)}return R("div",{className:"met-page","data-testid":"met-nonlinear-page"},R(vj,{title:"MET Nonlinear 训练编排",eyebrow:"D601 GPU 用户服务",loading:y.loading||y.actionBusy,actions:R("div",{className:"panel-actions"},R("button",{type:"button",className:"ghost-btn",onClick:Q,disabled:y.loading,"data-testid":"met-refresh-button"},y.loading?"刷新中":"刷新"),R(T$,{title:"MET Nonlinear 用户服务",data:_,onOpen:u,testId:"raw-met-service"}))},R("div",{className:"findjob-hero"},R("div",null,R("div",{className:"node-version-line"},R(T6,{status:q.providerStatus==="online"?"online":"warn"},q.providerStatus||"unknown"),R("span",null,_.providerId),R("span",null,w.public?"公网暴露":"仅 UniDesk frontend 代理访问")),R("p",{className:"muted paragraph"},_.description)),R("div",{className:"microservice-ref-card"},R("span",null,"Repo"),R("strong",null,Y.url||"--"),R("code",null,Y.commitId||"--")),R("div",{className:"microservice-ref-card"},R("span",null,"D601 Docker"),R("strong",null,`${w.nodeBindHost||"--"}:${w.nodePort||"--"}`),R("code",null,`${Y.composeFile||"--"} / ${Y.containerName||"--"}`))),R(A0,{error:y.error,wide:!0}),A.actionMessage?R("div",{className:"met-action-log","data-testid":"met-action-message"},A.actionMessage):null),R("div",{className:"met-grid"},R(vj,{title:"核心状态",eyebrow:y.refreshedAt?`Updated ${W0(y.refreshedAt)}`:"Queue + GPU",loading:y.loading},R("div",{className:"metric-grid"},R(N_,{label:"Staged",value:B.staged??0,hint:"加入队列未开始",tone:Number(B.staged||0)>0?"warn":""}),R(N_,{label:"Queued",value:B.queued??0,hint:"排队等待调度",tone:Number(B.queued||0)>0?"warn":""}),R(N_,{label:"Running",value:B.running??0,hint:`max ${y.summary?.queue?.maxConcurrency??y.queue?.queue?.maxConcurrency??"--"}`,tone:Number(B.running||0)>0?"ok":""}),R(N_,{label:"Succeeded",value:B.succeeded??0,hint:"已完成"}),R(N_,{label:"Failed",value:B.failed??0,hint:"需要诊断",tone:Number(B.failed||0)>0?"warn":""}),R(N_,{label:"2080Ti Free",value:h?D6(Number(h.freeRatio)*100):"--",hint:h?`${h.memoryFreeMiB}/${h.memoryTotalMiB} MiB`:"等待 GPU 上报"}),R(N_,{label:"ML Image",value:M.present?"READY":"MISSING",hint:M.image||"met-nonlinear-ml:tf26",tone:M.present?"ok":"warn"}),R(N_,{label:"Health",value:y.health?.ok?"OK":"--",hint:"D601 /health"}))),R(vj,{title:"队列控制",eyebrow:"Downloader-like staging",loading:y.actionBusy},R("div",{className:"met-control-strip"},R("label",null,"最大并发",R("input",{type:"number",min:1,max:16,value:A.maxConcurrency,"data-testid":"met-max-concurrency-input",onChange:(c)=>U({maxConcurrency:c.target.value})})),R("label",null,"目标 GPU",R("input",{value:A.targetGpuName,"data-testid":"met-target-gpu-input",onChange:(c)=>U({targetGpuName:c.target.value})})),R("button",{type:"button",className:"ghost-btn",onClick:G,disabled:y.actionBusy,"data-testid":"met-save-settings-button"},"保存设置"),R("button",{type:"button",className:"primary-btn",onClick:z,disabled:y.actionBusy||Number(B.staged||0)===0,"data-testid":"met-start-queue-button"},"启动队列")),R("p",{className:"muted paragraph"},"Project 先进入待启动队列,不会立即训练;点击启动队列后才切换为排队中,并由 D601 scheduler 按最大并发和 2080Ti 显存策略调度。")),R("section",{className:"panel met-workspace"},R("div",{className:"met-tabs",role:"tablist"},p.map((c)=>R("button",{key:c.id,type:"button",className:A.activeTab===c.id?"active":"",onClick:()=>U({activeTab:c.id}),"data-testid":`met-tab-${c.id}`},`${c.label} ${c.count}`))),R("div",{className:"panel-body"},A.activeTab==="projects"?R("div",{className:"met-form-grid","data-testid":"met-projects-pane"},R("div",{className:"met-fork-card"},R("h3",null,"Fork Project"),R("label",null,"源 Project",R("select",{value:i,"data-testid":"met-source-project-select",onChange:(c)=>U({sourceProject:c.target.value})},S.map((c)=>R("option",{key:c.projectPath,value:c.projectPath},`${c.projectPath} · ${c.useModel||"model?"}`)))),R("label",null,"Fork 数量",R("input",{type:"number",min:1,max:100,value:A.forkCount,"data-testid":"met-fork-count-input",onChange:(c)=>U({forkCount:c.target.value})})),R("label",null,"训练轮数",R("input",{type:"number",min:1,max:1e5,value:A.forkEpochs,"data-testid":"met-fork-epochs-input",onChange:(c)=>U({forkEpochs:c.target.value})})),R("label",null,"目标前缀",R("input",{value:A.forkPrefix,"data-testid":"met-fork-prefix-input",onChange:(c)=>U({forkPrefix:c.target.value})})),R("button",{type:"button",className:"primary-btn",onClick:O,disabled:y.actionBusy||!i,"data-testid":"met-fork-button"},"Fork Project"),R("p",{className:"muted paragraph"},"Fork 只创建新 Project 并自动勾选,不会直接训练;需要在右侧确认后加入待启动队列。")),R("div",{className:"met-project-list"},R("div",{className:"panel-head compact"},R("div",null,R("p",{className:"panel-eyebrow"},`Existing Projects · ${(y.projects?.roots||[]).map((c)=>`${c.root} ${c.count}`).join(" / ")}`),R(j0,{title:"选择已有 Project",loading:y.loading||y.actionBusy})),R("button",{type:"button",className:"ghost-btn",onClick:H,disabled:y.actionBusy||K().length===0,"data-testid":"met-stage-selected-button"},`加入待启动队列 (${K().length})`)),S.length===0?R(P1,{title:"暂无 project",text:"等待 D601 返回 /api/projects"}):R("div",{className:"met-project-table","data-testid":"met-project-tree"},R("div",{className:"met-tree-header"},R("span",null,"文件树 Project"),R("span",null,"Model"),R("span",null,"Epochs"),R("span",null,"Progress"),R("span",null,"速度")),T.children.map((c)=>I(c)))),Gf()):null,A.activeTab==="current"?R("div",{"data-testid":"met-current-pane"},s(),m(C,"current"),Gf(),R("div",{className:"panel-actions inline-actions"},R(T$,{title:"MET Queue",data:y.queue,onOpen:u,testId:"raw-met-queue"}))):null,A.activeTab==="completed"?R("div",{"data-testid":"met-completed-pane"},m(v.length>0?v:D.filter((c)=>c.status==="succeeded"),"completed"),Gf()):null,A.activeTab==="failed"?R("div",{"data-testid":"met-failed-pane"},m(X.length>0?X:D.filter((c)=>["failed","canceled"].includes(c.status)),"failed"),Gf(),R("div",{className:"panel-actions inline-actions"},R(T$,{title:"MET History",data:y.history,onOpen:u,testId:"raw-met-history"}))):null,A.activeTab==="gpu"?R("div",{className:"met-gpu-pane","data-testid":"met-gpu-pane"},P.length===0?R(P1,{title:"暂无 GPU 上报",text:"等待 D601 met-nonlinear-ts 或 ML image 提供 nvidia-smi 数据"}):R("div",{className:"table-wrap"},R("table",null,R("thead",null,R("tr",null,R("th",null,"Index"),R("th",null,"Name"),R("th",null,"Free"),R("th",null,"Policy"))),R("tbody",null,P.map((c)=>R("tr",{key:c.index},R("td",null,c.index),R("td",null,c.name),R("td",null,`${c.memoryFreeMiB} / ${c.memoryTotalMiB} MiB`,R("div",{className:"met-progress"},R("span",{style:{width:D6(Number(c.freeRatio)*100)}}))),R("td",null,String(c.name||"").includes("2080")?"target 2080Ti, <20% 限制并发":"non-target")))))),R("div",{className:"panel-actions inline-actions"},R(T$,{title:"MET Images",data:y.images,onOpen:u,testId:"raw-met-images"}))):null))))}var p2=[{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:"scheduled",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:"安全边界"}]}],n6=Object.fromEntries(p2.map((f)=>[f.id,f.tabs[0]?.id??""]));function hB(f){let u=String(f||"").trim();if(!u)return"";try{return decodeURIComponent(u)}catch{return u}}function I2(f){let u=String(f||"/"),[l]=u.split(/[?#]/u,1);if(l==="/")return"/";let y=`/${l.split("/").map(hB).filter(Boolean).join("/")}`;return y.endsWith("/")?y:`${y}/`}function IB(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 pj(f){return String(f||"").normalize("NFKD").replace(/[\u0300-\u036f]/gu,"").toLowerCase().replace(/[^a-z0-9]+/gu,"-").replace(/^-+|-+$/gu,"")}function UK(f){return String(f||"").trim().toLowerCase().replace(/[\s/\\?#%]+/gu,"-").replace(/-+/gu,"-").replace(/^-+|-+$/gu,"")}function QK(f){let u=pj(f.routeSegment||"")||UK(f.routeSegment||"");if(u)return u;let l=pj(f.id||"");if(l)return l;let _=pj(f.label||"")||UK(f.label||"");if(_)return _;return`route-${IB(JSON.stringify(f))}`}function mj(f,u){return`${f}:${u}`}function WK(f){let u=f.map((A)=>{let J=QK(A);return{...A,routeSegment:J,tabs:A.tabs.map((U)=>({...U,routeSegment:QK(U)}))}}),l={},_={},y={},$=u.map((A)=>{let J=A.tabs[0]?.id??"";y[A.id]=J;let U=A.tabs.map((G)=>{let K=`/${A.routeSegment}/${G.routeSegment}/`,H=[K],O={moduleId:A.id,tabId:G.id};for(let z of H)l[I2(z)]=O;return _[mj(A.id,G.id)]=K,{...G,canonicalPath:K,aliases:H}}),Q=`/${A.routeSegment}/`,W={moduleId:A.id,tabId:J};return l[I2(Q)]=W,{...A,routeSegment:A.routeSegment,canonicalPath:Q,tabs:U}}),r=$[0],j={moduleId:r?.id||"",tabId:r?.tabs[0]?.id||""};return l["/"]=j,{modules:$,moduleById:Object.fromEntries($.map((A)=>[A.id,A])),defaultActiveTabs:y,routeMap:l,canonicalPathByTarget:_,fallbackTarget:j}}function gj(f,u){return f.routeMap[I2(u)]||f.fallbackTarget}function m2(f,u,l){return f.canonicalPathByTarget[mj(u,l)]||f.canonicalPathByTarget[mj(f.fallbackTarget.moduleId,f.fallbackTarget.tabId)]||"/"}function zK(f,u){let l=f.routeMap[I2(u)];if(!l)return null;return m2(f,l.moduleId,l.tabId)}var M_=cf(O0(),1);var lf=cf(ZK(),1),_f=cf(O0(),1);function M0(f){if(typeof f==="string"||typeof f==="number")return""+f;let u="";if(Array.isArray(f)){for(let l=0,_;l{}};function HK(){for(var f=0,u=arguments.length,l={},_;f=0)_=l.slice(y+1),l=l.slice(0,y);if(l&&!u.hasOwnProperty(l))throw Error("unknown type: "+l);return{type:l,name:_}})}k2.prototype=HK.prototype={constructor:k2,on:function(f,u){var l=this._,_=aB(f+"",l),y,$=-1,r=_.length;if(arguments.length<2){while(++$0)for(var l=Array(y),_=0,y,$;_=0&&(u=f.slice(0,l))!=="xmlns")f=f.slice(l+1);return kj.hasOwnProperty(u)?{space:kj[u],local:f}:f}function tj(f){let u;while(u=f.sourceEvent)f=u;return f}function zu(f,u){if(f=tj(f),u===void 0)u=f.currentTarget;if(u){var l=u.ownerSVGElement||u;if(l.createSVGPoint){var _=l.createSVGPoint();return _.x=f.clientX,_.y=f.clientY,_=_.matrixTransform(u.getScreenCTM().inverse()),[_.x,_.y]}if(u.getBoundingClientRect){var y=u.getBoundingClientRect();return[f.clientX-y.left-u.clientLeft,f.clientY-y.top-u.clientTop]}}return[f.pageX,f.pageY]}function eB(){}function Z_(f){return f==null?eB:function(){return this.querySelector(f)}}function sj(f){if(typeof f!=="function")f=Z_(f);for(var u=this._groups,l=u.length,_=Array(l),y=0;y=N)N=Z+1;while(!(q=O[N])&&++N=0;)if(r=_[y]){if($&&r.compareDocumentPosition($)^4)$.parentNode.insertBefore(r,$);$=r}return this}function AA(f){if(!f)f=QY;function u(Q,W){return Q&&W?f(Q.__data__,W.__data__):!Q-!W}for(var l=this._groups,_=l.length,y=Array(_),$=0;$<_;++$){for(var r=l[$],j=r.length,A=y[$]=Array(j),J,U=0;Uu?1:f>=u?0:NaN}function FA(){var f=arguments[0];return arguments[0]=this,f.apply(null,arguments),this}function JA(){return Array.from(this)}function UA(){for(var f=this._groups,u=0,l=f.length;u1?this.each((u==null?EY:typeof u==="function"?OY:HY)(f,u,l==null?"":l)):E_(this.node(),f)}function E_(f,u){return f.style.getPropertyValue(u)||i6(f).getComputedStyle(f,null).getPropertyValue(u)}function VY(f){return function(){delete this[f]}}function qY(f,u){return function(){this[f]=u}}function LY(f,u){return function(){var l=u.apply(this,arguments);if(l==null)delete this[f];else this[f]=l}}function NA(f,u){return arguments.length>1?this.each((u==null?VY:typeof u==="function"?LY:qY)(f,u)):this.node()[f]}function OK(f){return f.trim().split(/^|\s+/)}function ZA(f){return f.classList||new VK(f)}function VK(f){this._node=f,this._names=OK(f.getAttribute("class")||"")}VK.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 qK(f,u){var l=ZA(f),_=-1,y=u.length;while(++_=0)l=u.slice(_+1),u=u.slice(0,_);return{type:u,name:l}})}function pY(f){return function(){var u=this.__on;if(!u)return;for(var l=0,_=-1,y=u.length,$;l()=>f;function b6(f,{sourceEvent:u,subject:l,target:_,identifier:y,active:$,x:r,y:j,dx:A,dy:J,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:_,enumerable:!0,configurable:!0},identifier:{value:y,enumerable:!0,configurable:!0},active:{value:$,enumerable:!0,configurable:!0},x:{value:r,enumerable:!0,configurable:!0},y:{value:j,enumerable:!0,configurable:!0},dx:{value:A,enumerable:!0,configurable:!0},dy:{value:J,enumerable:!0,configurable:!0},_:{value:U}})}b6.prototype.on=function(){var f=this._.on.apply(this._,arguments);return f===this._?this:f};function lw(f){return!f.ctrlKey&&!f.button}function _w(){return this.parentNode}function yw(f,u){return u==null?{x:f.x,y:f.y}:u}function $w(){return navigator.maxTouchPoints||"ontouchstart"in this}function v6(){var f=lw,u=_w,l=yw,_=$w,y={},$=Ky("start","drag","end"),r=0,j,A,J,U,Q=0;function W(E){E.on("mousedown.drag",G).filter(_).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(E,q){if(U||!f.call(this,E,q))return;var Y=N(this,u.call(this,E,q),E,q,"mouse");if(!Y)return;g0(E.view).on("mousemove.drag",K,Ny).on("mouseup.drag",H,Ny),n$(E.view),o2(E),J=!1,j=E.clientX,A=E.clientY,Y("start",E)}function K(E){if(S1(E),!J){var q=E.clientX-j,Y=E.clientY-A;J=q*q+Y*Y>Q}y.mouse("drag",E)}function H(E){g0(E.view).on("mousemove.drag mouseup.drag",null),R6(E.view,J),S1(E),y.mouse("end",E)}function O(E,q){if(!f.call(this,E,q))return;var Y=E.changedTouches,w=u.call(this,E,q),B=Y.length,P,h;for(P=0;P>8&15|u>>4&240,u>>4&15|u&240,(u&15)<<4|u&15,1):l===8?a2(u>>24&255,u>>16&255,u>>8&255,(u&255)/255):l===4?a2(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=jw.exec(f))?new xu(u[1],u[2],u[3],1):(u=Aw.exec(f))?new xu(u[1]*255/100,u[2]*255/100,u[3]*255/100,1):(u=Fw.exec(f))?a2(u[1],u[2],u[3],u[4]):(u=Jw.exec(f))?a2(u[1]*255/100,u[2]*255/100,u[3]*255/100,u[4]):(u=Uw.exec(f))?SK(u[1],u[2]/100,u[3]/100,1):(u=Qw.exec(f))?SK(u[1],u[2]/100,u[3]/100,u[4]):wK.hasOwnProperty(f)?MK(wK[f]):f==="transparent"?new xu(NaN,NaN,NaN,0):null}function MK(f){return new xu(f>>16&255,f>>8&255,f&255,1)}function a2(f,u,l,_){if(_<=0)f=u=l=NaN;return new xu(f,u,l,_)}function Gw(f){if(!(f instanceof m6))f=bl(f);if(!f)return new xu;return f=f.rgb(),new xu(f.r,f.g,f.b,f.opacity)}function C$(f,u,l,_){return arguments.length===1?Gw(f):new xu(f,u,l,_==null?1:_)}function xu(f,u,l,_){this.r=+f,this.g=+u,this.b=+l,this.opacity=+_}h6(xu,C$,nA(m6,{brighter(f){return f=f==null?e2:Math.pow(e2,f),new xu(this.r*f,this.g*f,this.b*f,this.opacity)},darker(f){return f=f==null?I6:Math.pow(I6,f),new xu(this.r*f,this.g*f,this.b*f,this.opacity)},rgb(){return this},clamp(){return new xu(Ey(this.r),Ey(this.g),Ey(this.b),f5(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:PK,formatHex:PK,formatHex8:Kw,formatRgb:nK,toString:nK}));function PK(){return`#${Zy(this.r)}${Zy(this.g)}${Zy(this.b)}`}function Kw(){return`#${Zy(this.r)}${Zy(this.g)}${Zy(this.b)}${Zy((isNaN(this.opacity)?1:this.opacity)*255)}`}function nK(){let f=f5(this.opacity);return`${f===1?"rgb(":"rgba("}${Ey(this.r)}, ${Ey(this.g)}, ${Ey(this.b)}${f===1?")":`, ${f})`}`}function f5(f){return isNaN(f)?1:Math.max(0,Math.min(1,f))}function Ey(f){return Math.max(0,Math.min(255,Math.round(f)||0))}function Zy(f){return f=Ey(f),(f<16?"0":"")+f.toString(16)}function SK(f,u,l,_){if(_<=0)f=u=l=NaN;else if(l<=0||l>=1)f=u=NaN;else if(u<=0)f=NaN;return new xl(f,u,l,_)}function iK(f){if(f instanceof xl)return new xl(f.h,f.s,f.l,f.opacity);if(!(f instanceof m6))f=bl(f);if(!f)return new xl;if(f instanceof xl)return f;f=f.rgb();var u=f.r/255,l=f.g/255,_=f.b/255,y=Math.min(u,l,_),$=Math.max(u,l,_),r=NaN,j=$-y,A=($+y)/2;if(j){if(u===$)r=(l-_)/j+(l<_)*6;else if(l===$)r=(_-u)/j+2;else r=(u-l)/j+4;j/=A<0.5?$+y:2-$-y,r*=60}else j=A>0&&A<1?0:r;return new xl(r,j,A,f.opacity)}function cK(f,u,l,_){return arguments.length===1?iK(f):new xl(f,u,l,_==null?1:_)}function xl(f,u,l,_){this.h=+f,this.s=+u,this.l=+l,this.opacity=+_}h6(xl,cK,nA(m6,{brighter(f){return f=f==null?e2:Math.pow(e2,f),new xl(this.h,this.s,this.l*f,this.opacity)},darker(f){return f=f==null?I6:Math.pow(I6,f),new xl(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,_=l+(l<0.5?l:1-l)*u,y=2*l-_;return new xu(SA(f>=240?f-240:f+120,y,_),SA(f,y,_),SA(f<120?f+240:f-120,y,_),this.opacity)},clamp(){return new xl(CK(this.h),d2(this.s),d2(this.l),f5(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=f5(this.opacity);return`${f===1?"hsl(":"hsla("}${CK(this.h)}, ${d2(this.s)*100}%, ${d2(this.l)*100}%${f===1?")":`, ${f})`}`}}));function CK(f){return f=(f||0)%360,f<0?f+360:f}function d2(f){return Math.max(0,Math.min(1,f||0))}function SA(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 CA(f,u,l,_,y){var $=f*f,r=$*f;return((1-3*f+3*$-r)*u+(4-6*$+3*r)*l+(1+3*f+3*$-3*r)*_+r*y)/6}function iA(f){var u=f.length-1;return function(l){var _=l<=0?l=0:l>=1?(l=1,u-1):Math.floor(l*u),y=f[_],$=f[_+1],r=_>0?f[_-1]:2*y-$,j=_()=>f;function Zw(f,u){return function(l){return f+l*u}}function Ew(f,u,l){return f=Math.pow(f,l),u=Math.pow(u,l)-f,l=1/l,function(_){return Math.pow(f+_*u,l)}}function RK(f){return(f=+f)===1?l5:function(u,l){return l-u?Ew(u,l,f):g6(isNaN(u)?l:u)}}function l5(f,u){var l=u-f;return l?Zw(f,l):g6(isNaN(f)?u:f)}var Hy=function f(u){var l=RK(u);function _(y,$){var r=l((y=C$(y)).r,($=C$($)).r),j=l(y.g,$.g),A=l(y.b,$.b),J=l5(y.opacity,$.opacity);return function(U){return y.r=r(U),y.g=j(U),y.b=A(U),y.opacity=J(U),y+""}}return _.gamma=f,_}(1);function xK(f){return function(u){var l=u.length,_=Array(l),y=Array(l),$=Array(l),r,j;for(r=0;rl)if($=u.slice(l,$),j[r])j[r]+=$;else j[++r]=$;if((_=_[0])===(y=y[0]))if(j[r])j[r]+=y;else j[++r]=y;else j[++r]=null,A.push({i:r,x:Gu(_,y)});l=vA.lastIndex}if(l180)U+=360;else if(U-J>180)J+=360;W.push({i:Q.push(y(Q)+"rotate(",null,_)-2,x:Gu(J,U)})}else if(U)Q.push(y(Q)+"rotate("+U+_)}function j(J,U,Q,W){if(J!==U)W.push({i:Q.push(y(Q)+"skewX(",null,_)-2,x:Gu(J,U)});else if(U)Q.push(y(Q)+"skewX("+U+_)}function A(J,U,Q,W,G,K){if(J!==Q||U!==W){var H=G.push(y(G)+"scale(",null,",",null,")");K.push({i:H-4,x:Gu(J,Q)},{i:H-2,x:Gu(U,W)})}else if(Q!==1||W!==1)G.push(y(G)+"scale("+Q+","+W+")")}return function(J,U){var Q=[],W=[];return J=f(J),U=f(U),$(J.translateX,J.translateY,U.translateX,U.translateY,Q,W),r(J.rotate,U.rotate,Q,W),j(J.skewX,U.skewX,Q,W),A(J.scaleX,J.scaleY,U.scaleX,U.scaleY,Q,W),J=U=null,function(G){var K=-1,H=W.length,O;while(++K=0)f._call.call(void 0,u);f=f._next}--c$}function oK(){Vy=(j5=o6.now())+A5,c$=t6=0;try{eK()}finally{c$=0,vw(),Vy=0}}function bw(){var f=o6.now(),u=f-j5;if(u>aK)A5-=u,j5=f}function vw(){var f,u=r5,l,_=1/0;while(u)if(u._call){if(_>u._time)_=u._time;f=u,u=u._next}else l=u._next,u._next=null,u=f?f._next=l:r5=l;s6=f,mA(_)}function mA(f){if(c$)return;if(t6)t6=clearTimeout(t6);var u=f-Vy;if(u>24){if(f<1/0)t6=setTimeout(oK,f-o6.now()-A5);if(k6)k6=clearInterval(k6)}else{if(!k6)j5=o6.now(),k6=setInterval(bw,aK);c$=1,dK(oK)}}function e6(f,u,l){var _=new a6;return u=u==null?0:+u,_.restart((y)=>{_.stop(),f(y+u)},u,l),_}var Iw=Ky("start","end","cancel","interrupt"),pw=[],lN=0,fN=1,U5=2,J5=3,uN=4,Q5=5,f4=6;function C1(f,u,l,_,y,$){var r=f.__transition;if(!r)f.__transition={};else if(l in r)return;mw(f,l,{name:u,index:_,group:y,on:Iw,tween:pw,time:$.time,delay:$.delay,duration:$.duration,ease:$.ease,timer:null,state:lN})}function u4(f,u){var l=k0(f,u);if(l.state>lN)throw Error("too late; already scheduled");return l}function $u(f,u){var l=k0(f,u);if(l.state>J5)throw Error("too late; already running");return l}function k0(f,u){var l=f.__transition;if(!l||!(l=l[u]))throw Error("transition not found");return l}function mw(f,u,l){var _=f.__transition,y;_[u]=l,l.timer=F5($,0,l.time);function $(J){if(l.state=fN,l.timer.restart(r,l.delay,l.time),l.delay<=J)r(J-l.delay)}function r(J){var U,Q,W,G;if(l.state!==fN)return A();for(U in _){if(G=_[U],G.name!==l.name)continue;if(G.state===J5)return e6(r);if(G.state===uN)G.state=f4,G.timer.stop(),G.on.call("interrupt",f,f.__data__,G.index,G.group),delete _[U];else if(+UU5&&_.state=0)u=u.slice(0,l);return!u||u==="start"})}function UD(f,u,l){var _,y,$=JD(u)?u4:$u;return function(){var r=$(this,f),j=r.on;if(j!==_)(y=(_=j).copy()).on(u,l);r.on=y}}function lF(f,u){var l=this._id;return arguments.length<2?k0(this.node(),l).on.on(f):this.each(UD(l,f,u))}function QD(f){return function(){var u=this.parentNode;for(var l in this.__transition)if(+l!==f)return;if(u)u.removeChild(this)}}function _F(){return this.on("end.remove",QD(this._id))}function yF(f){var u=this._name,l=this._id;if(typeof f!=="function")f=Z_(f);for(var _=this._groups,y=_.length,$=Array(y),r=0;r()=>f;function zF(f,{sourceEvent:u,target:l,transform:_,dispatch:y}){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:_,enumerable:!0,configurable:!0},_:{value:y}})}function vl(f,u,l){this.k=f,this.x=u,this.y=l}vl.prototype={constructor:vl,scale:function(f){return f===1?this:new vl(this.k*f,this.x,this.y)},translate:function(f,u){return f===0&u===0?this:new vl(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 qy=new vl(1,0,0);y4.prototype=vl.prototype;function y4(f){while(!f.__zoom)if(!(f=f.parentNode))return qy;return f.__zoom}function q5(f){f.stopImmediatePropagation()}function Ly(f){f.preventDefault(),f.stopImmediatePropagation()}function wD(f){return(!f.ctrlKey||f.type==="wheel")&&!f.button}function DD(){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 $N(){return this.__zoom||qy}function TD(f){return-f.deltaY*(f.deltaMode===1?0.05:f.deltaMode?1:0.002)*(f.ctrlKey?10:1)}function MD(){return navigator.maxTouchPoints||"ontouchstart"in this}function PD(f,u,l){var _=f.invertX(u[0][0])-l[0][0],y=f.invertX(u[1][0])-l[1][0],$=f.invertY(u[0][1])-l[0][1],r=f.invertY(u[1][1])-l[1][1];return f.translate(y>_?(_+y)/2:Math.min(0,_)||Math.max(0,y),r>$?($+r)/2:Math.min(0,$)||Math.max(0,r))}function $4(){var f=wD,u=DD,l=PD,_=TD,y=MD,$=[0,1/0],r=[[-1/0,-1/0],[1/0,1/0]],j=250,A=Oy,J=Ky("start","zoom","end"),U,Q,W,G=500,K=150,H=0,O=10;function z(T){T.property("__zoom",$N).on("wheel.zoom",B,{passive:!1}).on("mousedown.zoom",P).on("dblclick.zoom",h).filter(y).on("touchstart.zoom",M).on("touchmove.zoom",n).on("touchend.zoom touchcancel.zoom",S).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}z.transform=function(T,i,C,v){var X=T.selection?T.selection():T;if(X.property("__zoom",$N),T!==X)q(T,i,C,v);else X.interrupt().each(function(){Y(this,arguments).event(v).start().zoom(null,typeof i==="function"?i.apply(this,arguments):i).end()})},z.scaleBy=function(T,i,C,v){z.scaleTo(T,function(){var X=this.__zoom.k,D=typeof i==="function"?i.apply(this,arguments):i;return X*D},C,v)},z.scaleTo=function(T,i,C,v){z.transform(T,function(){var X=u.apply(this,arguments),D=this.__zoom,p=C==null?E(X):typeof C==="function"?C.apply(this,arguments):C,m=D.invert(p),s=typeof i==="function"?i.apply(this,arguments):i;return l(N(Z(D,s),p,m),X,r)},C,v)},z.translateBy=function(T,i,C,v){z.transform(T,function(){return l(this.__zoom.translate(typeof i==="function"?i.apply(this,arguments):i,typeof C==="function"?C.apply(this,arguments):C),u.apply(this,arguments),r)},null,v)},z.translateTo=function(T,i,C,v,X){z.transform(T,function(){var D=u.apply(this,arguments),p=this.__zoom,m=v==null?E(D):typeof v==="function"?v.apply(this,arguments):v;return l(qy.translate(m[0],m[1]).scale(p.k).translate(typeof i==="function"?-i.apply(this,arguments):-i,typeof C==="function"?-C.apply(this,arguments):-C),D,r)},v,X)};function Z(T,i){return i=Math.max($[0],Math.min($[1],i)),i===T.k?T:new vl(i,T.x,T.y)}function N(T,i,C){var v=i[0]-C[0]*T.k,X=i[1]-C[1]*T.k;return v===T.x&&X===T.y?T:new vl(T.k,v,X)}function E(T){return[(+T[0][0]+ +T[1][0])/2,(+T[0][1]+ +T[1][1])/2]}function q(T,i,C,v){T.on("start.zoom",function(){Y(this,arguments).event(v).start()}).on("interrupt.zoom end.zoom",function(){Y(this,arguments).event(v).end()}).tween("zoom",function(){var X=this,D=arguments,p=Y(X,D).event(v),m=u.apply(X,D),s=C==null?E(m):typeof C==="function"?C.apply(X,D):C,d=Math.max(m[1][0]-m[0][0],m[1][1]-m[0][1]),a=X.__zoom,I=typeof i==="function"?i.apply(X,D):i,ff=A(a.invert(s).concat(d/a.k),I.invert(s).concat(d/I.k));return function(yf){if(yf===1)yf=I;else{var rf=ff(yf),Wf=d/rf[2];yf=new vl(Wf,s[0]-rf[0]*Wf,s[1]-rf[1]*Wf)}p.zoom(null,yf)}})}function Y(T,i,C){return!C&&T.__zooming||new w(T,i)}function w(T,i){this.that=T,this.args=i,this.active=0,this.sourceEvent=null,this.extent=u.apply(T,i),this.taps=0}w.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,i){if(this.mouse&&T!=="mouse")this.mouse[1]=i.invert(this.mouse[0]);if(this.touch0&&T!=="touch")this.touch0[1]=i.invert(this.touch0[0]);if(this.touch1&&T!=="touch")this.touch1[1]=i.invert(this.touch1[0]);return this.that.__zoom=i,this.emit("zoom"),this},end:function(){if(--this.active===0)delete this.that.__zooming,this.emit("end");return this},emit:function(T){var i=g0(this.that).datum();J.call(T,this.that,new zF(T,{sourceEvent:this.sourceEvent,target:z,type:T,transform:this.that.__zoom,dispatch:J}),i)}};function B(T,...i){if(!f.apply(this,arguments))return;var C=Y(this,i).event(T),v=this.__zoom,X=Math.max($[0],Math.min($[1],v.k*Math.pow(2,_.apply(this,arguments)))),D=zu(T);if(C.wheel){if(C.mouse[0][0]!==D[0]||C.mouse[0][1]!==D[1])C.mouse[1]=v.invert(C.mouse[0]=D);clearTimeout(C.wheel)}else if(v.k===X)return;else C.mouse=[D,v.invert(D)],H_(this),C.start();Ly(T),C.wheel=setTimeout(p,K),C.zoom("mouse",l(N(Z(v,X),C.mouse[0],C.mouse[1]),C.extent,r));function p(){C.wheel=null,C.end()}}function P(T,...i){if(W||!f.apply(this,arguments))return;var C=T.currentTarget,v=Y(this,i,!0).event(T),X=g0(T.view).on("mousemove.zoom",s,!0).on("mouseup.zoom",d,!0),D=zu(T,C),p=T.clientX,m=T.clientY;n$(T.view),q5(T),v.mouse=[D,this.__zoom.invert(D)],H_(this),v.start();function s(a){if(Ly(a),!v.moved){var I=a.clientX-p,ff=a.clientY-m;v.moved=I*I+ff*ff>H}v.event(a).zoom("mouse",l(N(v.that.__zoom,v.mouse[0]=zu(a,C),v.mouse[1]),v.extent,r))}function d(a){X.on("mousemove.zoom mouseup.zoom",null),R6(a.view,v.moved),Ly(a),v.event(a).end()}}function h(T,...i){if(!f.apply(this,arguments))return;var C=this.__zoom,v=zu(T.changedTouches?T.changedTouches[0]:T,this),X=C.invert(v),D=C.k*(T.shiftKey?0.5:2),p=l(N(Z(C,D),v,X),u.apply(this,i),r);if(Ly(T),j>0)g0(this).transition().duration(j).call(q,p,v,T);else g0(this).call(z.transform,p,v,T)}function M(T,...i){if(!f.apply(this,arguments))return;var C=T.touches,v=C.length,X=Y(this,i,T.changedTouches.length===v).event(T),D,p,m,s;q5(T);for(p=0;p"[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:_})=>`Couldn't create edge for ${f} handle id: "${f==="source"?l:_}", 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."},I$=[[Number.NEGATIVE_INFINITY,Number.NEGATIVE_INFINITY],[Number.POSITIVE_INFINITY,Number.POSITIVE_INFINITY]],EF=["Enter"," ","Escape"],HF={"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"},q_;(function(f){f.Strict="strict",f.Loose="loose"})(q_||(q_={}));var c1;(function(f){f.Free="free",f.Vertical="vertical",f.Horizontal="horizontal"})(c1||(c1={}));var Xy;(function(f){f.Partial="partial",f.Full="full"})(Xy||(Xy={}));var OF={inProgress:!1,isValid:null,from:null,fromHandle:null,fromPosition:null,fromNode:null,to:null,toHandle:null,toPosition:null,toNode:null,pointer:null},$1;(function(f){f.Bezier="default",f.Straight="straight",f.Step="step",f.SmoothStep="smoothstep",f.SimpleBezier="simplebezier"})($1||($1={}));var L_;(function(f){f.Arrow="arrow",f.ArrowClosed="arrowclosed"})(L_||(L_={}));var Zf;(function(f){f.Left="left",f.Top="top",f.Right="right",f.Bottom="bottom"})(Zf||(Zf={}));var rN={[Zf.Left]:Zf.Right,[Zf.Right]:Zf.Left,[Zf.Top]:Zf.Bottom,[Zf.Bottom]:Zf.Top};function VF(f){return f===null?null:f?"valid":"invalid"}var qF=(f)=>("id"in f)&&("source"in f)&&("target"in f),EN=(f)=>("id"in f)&&("position"in f)&&!("source"in f)&&!("target"in f),LF=(f)=>("id"in f)&&("internals"in f)&&!("source"in f)&&!("target"in f);var A4=(f,u=[0,0])=>{let{width:l,height:_}=r1(f),y=f.origin??u,$=l*y[0],r=_*y[1];return{x:f.position.x-$,y:f.position.y-r}},XF=(f,u={nodeOrigin:[0,0]})=>{if(f.length===0)return{x:0,y:0,width:0,height:0};let l=f.reduce((_,y)=>{let $=typeof y==="string",r=!u.nodeLookup&&!$?y:void 0;if(u.nodeLookup)r=$?u.nodeLookup.get(y):!LF(y)?u.nodeLookup.get(y.id):y;let j=r?B5(r,u.nodeOrigin):{x:0,y:0,x2:0,y2:0};return w5(_,j)},{x:1/0,y:1/0,x2:-1/0,y2:-1/0});return D5(l)},p$=(f,u={})=>{let l={x:1/0,y:1/0,x2:-1/0,y2:-1/0},_=!1;return f.forEach((y)=>{if(u.filter===void 0||u.filter(y))l=w5(l,B5(y)),_=!0}),_?D5(l):{x:0,y:0,width:0,height:0}},Y5=(f,u,[l,_,y]=[0,0,1],$=!1,r=!1)=>{let j={...k$(u,[l,_,y]),width:u.width/y,height:u.height/y},A=[];for(let J of f.values()){let{measured:U,selectable:Q=!0,hidden:W=!1}=J;if(r&&!Q||W)continue;let G=U.width??J.width??J.initialWidth??null,K=U.height??J.height??J.initialHeight??null,H=m$(j,Yy(J)),O=(G??0)*(K??0),z=$&&H>0;if(!J.internals.handleBounds||z||H>=O||J.dragging)A.push(J)}return A},HN=(f,u)=>{let l=new Set;return f.forEach((_)=>{l.add(_.id)}),u.filter((_)=>l.has(_.source)||l.has(_.target))};function nD(f,u){let l=new Map,_=u?.nodes?new Set(u.nodes.map((y)=>y.id)):null;return f.forEach((y)=>{if(y.measured.width&&y.measured.height&&(u?.includeHiddenNodes||!y.hidden)&&(!_||_.has(y.id)))l.set(y.id,y)}),l}async function ON({nodes:f,width:u,height:l,panZoom:_,minZoom:y,maxZoom:$},r){if(f.size===0)return Promise.resolve(!0);let j=nD(f,r),A=p$(j),J=F4(A,u,l,r?.minZoom??y,r?.maxZoom??$,r?.padding??0.1);return await _.setViewport(J,{duration:r?.duration,ease:r?.ease,interpolate:r?.interpolate}),Promise.resolve(!0)}function BF({nodeId:f,nextPosition:u,nodeLookup:l,nodeOrigin:_=[0,0],nodeExtent:y,onError:$}){let r=l.get(f),j=r.parentId?l.get(r.parentId):void 0,{x:A,y:J}=j?j.internals.positionAbsolute:{x:0,y:0},U=r.origin??_,Q=r.extent||y;if(r.extent==="parent"&&!r.expandParent)if(!j)$?.("005",$l.error005());else{let G=j.measured.width,K=j.measured.height;if(G&&K)Q=[[A,J],[A+G,J+K]]}else if(j&&h$(r.extent))Q=[[r.extent[0][0]+A,r.extent[0][1]+J],[r.extent[1][0]+A,r.extent[1][1]+J]];let W=h$(Q)?By(u,Q,r.measured):u;if(r.measured.width===void 0||r.measured.height===void 0)$?.("015",$l.error015());return{position:{x:W.x-A+(r.measured.width??0)*U[0],y:W.y-J+(r.measured.height??0)*U[1]},positionAbsolute:W}}async function VN({nodesToRemove:f=[],edgesToRemove:u=[],nodes:l,edges:_,onBeforeDelete:y}){let $=new Set(f.map((W)=>W.id)),r=[];for(let W of l){if(W.deletable===!1)continue;let G=$.has(W.id),K=!G&&W.parentId&&r.find((H)=>H.id===W.parentId);if(G||K)r.push(W)}let j=new Set(u.map((W)=>W.id)),A=_.filter((W)=>W.deletable!==!1),U=HN(r,A);for(let W of A)if(j.has(W.id)&&!U.find((K)=>K.id===W.id))U.push(W);if(!y)return{edges:U,nodes:r};let Q=await y({nodes:r,edges:U});if(typeof Q==="boolean")return Q?{edges:U,nodes:r}:{edges:[],nodes:[]};return Q}var v$=(f,u=0,l=1)=>Math.min(Math.max(f,u),l),By=(f={x:0,y:0},u,l)=>({x:v$(f.x,u[0][0],u[1][0]-(l?.width??0)),y:v$(f.y,u[0][1],u[1][1]-(l?.height??0))});function qN(f,u,l){let{width:_,height:y}=r1(l),{x:$,y:r}=l.internals.positionAbsolute;return By(f,[[$,r],[$+_,r+y]],u)}var jN=(f,u,l)=>{if(fl)return-v$(Math.abs(f-l),1,u)/u;return 0},LN=(f,u,l=15,_=40)=>{let y=jN(f.x,_,u.width-_)*l,$=jN(f.y,_,u.height-_)*l;return[y,$]},w5=(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)}),ZF=({x:f,y:u,width:l,height:_})=>({x:f,y:u,x2:f+l,y2:u+_}),D5=({x:f,y:u,x2:l,y2:_})=>({x:f,y:u,width:l-f,height:_-u}),Yy=(f,u=[0,0])=>{let{x:l,y:_}=LF(f)?f.internals.positionAbsolute:A4(f,u);return{x:l,y:_,width:f.measured?.width??f.width??f.initialWidth??0,height:f.measured?.height??f.height??f.initialHeight??0}},B5=(f,u=[0,0])=>{let{x:l,y:_}=LF(f)?f.internals.positionAbsolute:A4(f,u);return{x:l,y:_,x2:l+(f.measured?.width??f.width??f.initialWidth??0),y2:_+(f.measured?.height??f.height??f.initialHeight??0)}},YF=(f,u)=>D5(w5(ZF(f),ZF(u))),m$=(f,u)=>{let l=Math.max(0,Math.min(f.x+f.width,u.x+u.width)-Math.max(f.x,u.x)),_=Math.max(0,Math.min(f.y+f.height,u.y+u.height)-Math.max(f.y,u.y));return Math.ceil(l*_)},wF=(f)=>Hl(f.width)&&Hl(f.height)&&Hl(f.x)&&Hl(f.y),Hl=(f)=>!isNaN(f)&&isFinite(f),DF=(f,u)=>{},g$=(f,u=[1,1])=>{return{x:u[0]*Math.round(f.x/u[0]),y:u[1]*Math.round(f.y/u[1])}},k$=({x:f,y:u},[l,_,y],$=!1,r=[1,1])=>{let j={x:(f-l)/y,y:(u-_)/y};return $?g$(j,r):j},j4=({x:f,y:u},[l,_,y])=>{return{x:f*y+l,y:u*y+_}};function x$(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 SD(f,u,l){if(typeof f==="string"||typeof f==="number"){let _=x$(f,l),y=x$(f,u);return{top:_,right:y,bottom:_,left:y,x:y*2,y:_*2}}if(typeof f==="object"){let _=x$(f.top??f.y??0,l),y=x$(f.bottom??f.y??0,l),$=x$(f.left??f.x??0,u),r=x$(f.right??f.x??0,u);return{top:_,right:r,bottom:y,left:$,x:$+r,y:_+y}}return{top:0,right:0,bottom:0,left:0,x:0,y:0}}function CD(f,u,l,_,y,$){let{x:r,y:j}=j4(f,[u,l,_]),{x:A,y:J}=j4({x:f.x+f.width,y:f.y+f.height},[u,l,_]),U=y-A,Q=$-J;return{left:Math.floor(r),top:Math.floor(j),right:Math.floor(U),bottom:Math.floor(Q)}}var F4=(f,u,l,_,y,$)=>{let r=SD($,u,l),j=(u-r.x)/f.width,A=(l-r.y)/f.height,J=Math.min(j,A),U=v$(J,_,y),Q=f.x+f.width/2,W=f.y+f.height/2,G=u/2-Q*U,K=l/2-W*U,H=CD(f,G,K,U,u,l),O={left:Math.min(H.left-r.left,0),top:Math.min(H.top-r.top,0),right:Math.min(H.right-r.right,0),bottom:Math.min(H.bottom-r.bottom,0)};return{x:G-O.left+O.right,y:K-O.top+O.bottom,zoom:U}},t$=()=>typeof navigator<"u"&&navigator?.userAgent?.indexOf("Mac")>=0;function h$(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 TF(f){return(f.measured?.width??f.width??f.initialWidth)!==void 0&&(f.measured?.height??f.height??f.initialHeight)!==void 0}function MF(f,u={width:0,height:0},l,_,y){let $={...f},r=_.get(l);if(r){let j=r.origin||y;$.x+=r.internals.positionAbsolute.x-(u.width??0)*j[0],$.y+=r.internals.positionAbsolute.y-(u.height??0)*j[1]}return $}function PF(f,u){if(f.size!==u.size)return!1;for(let l of f)if(!u.has(l))return!1;return!0}function XN(){let f,u;return{promise:new Promise((_,y)=>{f=_,u=y}),resolve:f,reject:u}}function BN(f){return{...HF,...f||{}}}function r4(f,{snapGrid:u=[0,0],snapToGrid:l=!1,transform:_,containerBounds:y}){let{x:$,y:r}=Ol(f),j=k$({x:$-(y?.left??0),y:r-(y?.top??0)},_),{x:A,y:J}=l?g$(j,u):j;return{xSnapped:A,ySnapped:J,...j}}var T5=(f)=>({width:f.offsetWidth,height:f.offsetHeight}),nF=(f)=>f?.getRootNode?.()||window?.document,iD=["INPUT","SELECT","TEXTAREA"];function SF(f){let u=f.composedPath?.()?.[0]||f.target;if(u?.nodeType!==1)return!1;return iD.includes(u.nodeName)||u.hasAttribute("contenteditable")||!!u.closest(".nokey")}var CF=(f)=>("clientX"in f),Ol=(f,u)=>{let l=CF(f),_=l?f.clientX:f.touches?.[0].clientX,y=l?f.clientY:f.touches?.[0].clientY;return{x:_-(u?.left??0),y:y-(u?.top??0)}},AN=(f,u,l,_,y)=>{let $=u.querySelectorAll(`.${f}`);if(!$||!$.length)return null;return Array.from($).map((r)=>{let j=r.getBoundingClientRect();return{id:r.getAttribute("data-handleid"),type:f,nodeId:y,position:r.getAttribute("data-handlepos"),x:(j.left-l.left)/_,y:(j.top-l.top)/_,...T5(r)}})};function M5({sourceX:f,sourceY:u,targetX:l,targetY:_,sourceControlX:y,sourceControlY:$,targetControlX:r,targetControlY:j}){let A=f*0.125+y*0.375+r*0.375+l*0.125,J=u*0.125+$*0.375+j*0.375+_*0.125,U=Math.abs(A-f),Q=Math.abs(J-u);return[A,J,U,Q]}function L5(f,u){if(f>=0)return 0.5*f;return u*25*Math.sqrt(-f)}function FN({pos:f,x1:u,y1:l,x2:_,y2:y,c:$}){switch(f){case Zf.Left:return[u-L5(u-_,$),l];case Zf.Right:return[u+L5(_-u,$),l];case Zf.Top:return[u,l-L5(l-y,$)];case Zf.Bottom:return[u,l+L5(y-l,$)]}}function P5({sourceX:f,sourceY:u,sourcePosition:l=Zf.Bottom,targetX:_,targetY:y,targetPosition:$=Zf.Top,curvature:r=0.25}){let[j,A]=FN({pos:l,x1:f,y1:u,x2:_,y2:y,c:r}),[J,U]=FN({pos:$,x1:_,y1:y,x2:f,y2:u,c:r}),[Q,W,G,K]=M5({sourceX:f,sourceY:u,targetX:_,targetY:y,sourceControlX:j,sourceControlY:A,targetControlX:J,targetControlY:U});return[`M${f},${u} C${j},${A} ${J},${U} ${_},${y}`,Q,W,G,K]}function iF({sourceX:f,sourceY:u,targetX:l,targetY:_}){let y=Math.abs(l-f)/2,$=l0}var cD=({source:f,sourceHandle:u,target:l,targetHandle:_})=>`xy-edge__${f}${u||""}-${l}${_||""}`,RD=(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))},cF=(f,u,l={})=>{if(!f.source||!f.target)return DF("006",$l.error006()),u;let _=l.getEdgeId||cD,y;if(qF(f))y={...f};else y={...f,id:_(f)};if(RD(y,u))return u;if(y.sourceHandle===null)delete y.sourceHandle;if(y.targetHandle===null)delete y.targetHandle;return u.concat(y)};function n5({sourceX:f,sourceY:u,targetX:l,targetY:_}){let[y,$,r,j]=iF({sourceX:f,sourceY:u,targetX:l,targetY:_});return[`M ${f},${u}L ${l},${_}`,y,$,r,j]}var JN={[Zf.Left]:{x:-1,y:0},[Zf.Right]:{x:1,y:0},[Zf.Top]:{x:0,y:-1},[Zf.Bottom]:{x:0,y:1}},xD=({source:f,sourcePosition:u=Zf.Bottom,target:l})=>{if(u===Zf.Left||u===Zf.Right)return f.xMath.sqrt(Math.pow(u.x-f.x,2)+Math.pow(u.y-f.y,2));function bD({source:f,sourcePosition:u=Zf.Bottom,target:l,targetPosition:_=Zf.Top,center:y,offset:$,stepPosition:r}){let j=JN[u],A=JN[_],J={x:f.x+j.x*$,y:f.y+j.y*$},U={x:l.x+A.x*$,y:l.y+A.y*$},Q=xD({source:J,sourcePosition:u,target:U}),W=Q.x!==0?"x":"y",G=Q[W],K=[],H,O,z={x:0,y:0},Z={x:0,y:0},[,,N,E]=iF({sourceX:f.x,sourceY:f.y,targetX:l.x,targetY:l.y});if(j[W]*A[W]===-1){if(W==="x")H=y.x??J.x+(U.x-J.x)*r,O=y.y??(J.y+U.y)/2;else H=y.x??(J.x+U.x)/2,O=y.y??J.y+(U.y-J.y)*r;let B=[{x:H,y:J.y},{x:H,y:U.y}],P=[{x:J.x,y:O},{x:U.x,y:O}];if(j[W]===G)K=W==="x"?B:P;else K=W==="x"?P:B}else{let B=[{x:J.x,y:U.y}],P=[{x:U.x,y:J.y}];if(W==="x")K=j.x===G?P:B;else K=j.y===G?B:P;if(u===_){let T=Math.abs(f[W]-l[W]);if(T<=$){let i=Math.min($-1,$-T);if(j[W]===G)z[W]=(J[W]>f[W]?-1:1)*i;else Z[W]=(U[W]>l[W]?-1:1)*i}}if(u!==_){let T=W==="x"?"y":"x",i=j[W]===A[T],C=J[T]>U[T],v=J[T]=S)H=(h.x+M.x)/2,O=K[0].y;else H=K[0].x,O=(h.y+M.y)/2}let q={x:J.x+z.x,y:J.y+z.y},Y={x:U.x+Z.x,y:U.y+Z.y};return[[f,...q.x!==K[0].x||q.y!==K[0].y?[q]:[],...K,...Y.x!==K[K.length-1].x||Y.y!==K[K.length-1].y?[Y]:[],l],H,O,N,E]}function vD(f,u,l,_){let y=Math.min(UN(f,u)/2,UN(u,l)/2,_),{x:$,y:r}=u;if(f.x===$&&$===l.x||f.y===r&&r===l.y)return`L${$} ${r}`;if(f.y===r){let J=f.xl.id===u))||null}function S5(f,u){if(!f)return"";if(typeof f==="string")return f;return`${u?`${u}__`:""}${Object.keys(f).sort().map((_)=>`${_}=${f[_]}`).join("&")}`}function TN(f,{id:u,defaultColor:l,defaultMarkerStart:_,defaultMarkerEnd:y}){let $=new Set;return f.reduce((r,j)=>{return[j.markerStart||_,j.markerEnd||y].forEach((A)=>{if(A&&typeof A==="object"){let J=S5(A,u);if(!$.has(J))r.push({id:J,color:A.color||l,...A}),$.add(J)}}),r},[]).sort((r,j)=>r.id.localeCompare(j.id))}var MN=1000,hD=10,RF={nodeOrigin:[0,0],nodeExtent:I$,elevateNodesOnSelect:!0,zIndexMode:"basic",defaults:{}},ID={...RF,checkEquality:!0};function xF(f,u){let l={...f};for(let _ in u)if(u[_]!==void 0)l[_]=u[_];return l}function PN(f,u,l){let _=xF(RF,l);for(let y of f.values())if(y.parentId)vF(y,f,u,_);else{let $=A4(y,_.nodeOrigin),r=h$(y.extent)?y.extent:_.nodeExtent,j=By($,r,r1(y));y.internals.positionAbsolute=j}}function pD(f,u){if(!f.handles)return!f.measured?void 0:u?.internals.handleBounds;let l=[],_=[];for(let y of f.handles){let $={id:y.id,width:y.width??1,height:y.height??1,nodeId:f.id,x:y.x,y:y.y,position:y.position,type:y.type};if(y.type==="source")l.push($);else if(y.type==="target")_.push($)}return{source:l,target:_}}function bF(f){return f==="manual"}function C5(f,u,l,_={}){let y=xF(ID,_),$={i:0},r=new Map(u),j=y?.elevateNodesOnSelect&&!bF(y.zIndexMode)?MN:0,A=f.length>0,J=!1;u.clear(),l.clear();for(let U of f){let Q=r.get(U.id);if(y.checkEquality&&U===Q?.internals.userNode)u.set(U.id,Q);else{let W=A4(U,y.nodeOrigin),G=h$(U.extent)?U.extent:y.nodeExtent,K=By(W,G,r1(U));Q={...y.defaults,...U,measured:{width:U.measured?.width,height:U.measured?.height},internals:{positionAbsolute:K,handleBounds:pD(U,Q),z:nN(U,j,y.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)vF(Q,u,l,_,$);J||=U.selected??!1}return{nodesInitialized:A,hasSelectedNodes:J}}function mD(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 vF(f,u,l,_,y){let{elevateNodesOnSelect:$,nodeOrigin:r,nodeExtent:j,zIndexMode:A}=xF(RF,_),J=f.parentId,U=u.get(J);if(!U){console.warn(`Parent node ${J} not found. Please make sure that parent nodes are in front of their child nodes in the nodes array.`);return}if(mD(f,l),y&&!U.parentId&&U.internals.rootParentIndex===void 0&&A==="auto")U.internals.rootParentIndex=++y.i,U.internals.z=U.internals.z+y.i*hD;if(y&&U.internals.rootParentIndex!==void 0)y.i=U.internals.rootParentIndex;let Q=$&&!bF(A)?MN:0,{x:W,y:G,z:K}=gD(f,U,r,j,Q,A),{positionAbsolute:H}=f.internals,O=W!==H.x||G!==H.y;if(O||K!==f.internals.z)u.set(f.id,{...f,internals:{...f.internals,positionAbsolute:O?{x:W,y:G}:H,z:K}})}function nN(f,u,l){let _=Hl(f.zIndex)?f.zIndex:0;if(bF(l))return _;return _+(f.selected?u:0)}function gD(f,u,l,_,y,$){let{x:r,y:j}=u.internals.positionAbsolute,A=r1(f),J=A4(f,l),U=h$(f.extent)?By(J,f.extent,A):J,Q=By({x:r+U.x,y:j+U.y},_,A);if(f.extent==="parent")Q=qN(Q,A,u);let W=nN(f,y,$),G=u.internals.z??0;return{x:Q.x,y:Q.y,z:G>=W?G+1:W}}function i5(f,u,l,_=[0,0]){let y=[],$=new Map;for(let r of f){let j=u.get(r.parentId);if(!j)continue;let A=$.get(r.parentId)?.expandedRect??Yy(j),J=YF(A,r.rect);$.set(r.parentId,{expandedRect:J,parent:j})}if($.size>0)$.forEach(({expandedRect:r,parent:j},A)=>{let J=j.internals.positionAbsolute,U=r1(j),Q=j.origin??_,W=r.x0||G>0||O||z)y.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))y.push({id:Z.id,type:"position",position:{x:Z.position.x+W,y:Z.position.y+G}})});if(U.width0){let G=i5(W,u,l,y);J.push(...G)}return{changes:J,updatedInternals:A}}async function CN({delta:f,panZoom:u,transform:l,translateExtent:_,width:y,height:$}){if(!u||!f.x&&!f.y)return Promise.resolve(!1);let r=await u.setViewportConstrained({x:l[0]+f.x,y:l[1]+f.y,zoom:l[2]},[[0,0],[y,$]],_),j=!!r&&(r.x!==l[0]||r.y!==l[1]||r.k!==l[2]);return Promise.resolve(j)}function GN(f,u,l,_,y,$){let r=y,j=_.get(r)||new Map;_.set(r,j.set(l,u)),r=`${y}-${f}`;let A=_.get(r)||new Map;if(_.set(r,A.set(l,u)),$){r=`${y}-${f}-${$}`;let J=_.get(r)||new Map;_.set(r,J.set(l,u))}}function hF(f,u,l){f.clear(),u.clear();for(let _ of l){let{source:y,target:$,sourceHandle:r=null,targetHandle:j=null}=_,A={edgeId:_.id,source:y,target:$,sourceHandle:r,targetHandle:j},J=`${y}-${r}--${$}-${j}`,U=`${$}-${j}--${y}-${r}`;GN("source",A,U,f,y,r),GN("target",A,J,f,$,j),u.set(_.id,_)}}function iN(f,u){if(!f.parentId)return!1;let l=u.get(f.parentId);if(!l)return!1;if(l.selected)return!0;return iN(l,u)}function KN(f,u,l){let _=f;do{if(_?.matches?.(u))return!0;if(_===l)return!1;_=_?.parentElement}while(_);return!1}function kD(f,u,l,_){let y=new Map;for(let[$,r]of f)if((r.selected||r.id===_)&&(!r.parentId||!iN(r,f))&&(r.draggable||u&&typeof r.draggable>"u")){let j=f.get($);if(j)y.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 y}function GF({nodeId:f,dragItems:u,nodeLookup:l,dragging:_=!0}){let y=[];for(let[r,j]of u){let A=l.get(r)?.internals.userNode;if(A)y.push({...A,position:j.position,dragging:_})}if(!f)return[y[0],y];let $=l.get(f)?.internals.userNode;return[!$?y[0]:{...$,position:u.get(f)?.position||$.position,dragging:_},y]}function tD({dragItems:f,snapGrid:u,x:l,y:_}){let y=f.values().next().value;if(!y)return null;let $={x:l-y.distance.x,y:_-y.distance.y},r=g$($,u);return{x:r.x-$.x,y:r.y-$.y}}function cN({onNodeMouseDown:f,getStoreItems:u,onDragStart:l,onDrag:_,onDragStop:y}){let $={x:null,y:null},r=0,j=new Map,A=!1,J={x:0,y:0},U=null,Q=!1,W=null,G=!1,K=!1,H=null;function O({noDragClassName:Z,handleSelector:N,domNode:E,isSelectable:q,nodeId:Y,nodeClickDistance:w=0}){W=g0(E);function B({x:n,y:S}){let{nodeLookup:T,nodeExtent:i,snapGrid:C,snapToGrid:v,nodeOrigin:X,onNodeDrag:D,onSelectionDrag:p,onError:m,updateNodePositions:s}=u();$={x:n,y:S};let d=!1,a=j.size>1,I=a&&i?ZF(p$(j)):null,ff=a&&v?tD({dragItems:j,snapGrid:C,x:n,y:S}):null;for(let[yf,rf]of j){if(!T.has(yf))continue;let Wf={x:n-rf.distance.x,y:S-rf.distance.y};if(v)Wf=ff?{x:Math.round(Wf.x+ff.x),y:Math.round(Wf.y+ff.y)}:g$(Wf,C);let Ef=null;if(a&&i&&!rf.extent&&I){let{positionAbsolute:o}=rf.internals,e=o.x-I.x+i[0][0],Kf=o.x+rf.measured.width-I.x2+i[1][0],k=o.y-I.y+i[0][1],Af=o.y+rf.measured.height-I.y2+i[1][1];Ef=[[e,k],[Kf,Af]]}let{position:Gf,positionAbsolute:c}=BF({nodeId:yf,nextPosition:Wf,nodeLookup:T,nodeExtent:Ef?Ef:i,nodeOrigin:X,onError:m});d=d||rf.position.x!==Gf.x||rf.position.y!==Gf.y,rf.position=Gf,rf.internals.positionAbsolute=c}if(K=K||d,!d)return;if(s(j,!0),H&&(_||D||!Y&&p)){let[yf,rf]=GF({nodeId:Y,dragItems:j,nodeLookup:T});if(_?.(H,j,yf,rf),D?.(H,yf,rf),!Y)p?.(H,rf)}}async function P(){if(!U)return;let{transform:n,panBy:S,autoPanSpeed:T,autoPanOnNodeDrag:i}=u();if(!i){A=!1,cancelAnimationFrame(r);return}let[C,v]=LN(J,U,T);if(C!==0||v!==0){if($.x=($.x??0)-C/n[2],$.y=($.y??0)-v/n[2],await S({x:C,y:v}))B($)}r=requestAnimationFrame(P)}function h(n){let{nodeLookup:S,multiSelectionActive:T,nodesDraggable:i,transform:C,snapGrid:v,snapToGrid:X,selectNodesOnDrag:D,onNodeDragStart:p,onSelectionDragStart:m,unselectNodesAndEdges:s}=u();if(Q=!0,(!D||!q)&&!T&&Y){if(!S.get(Y)?.selected)s()}if(q&&D&&Y)f?.(Y);let d=r4(n.sourceEvent,{transform:C,snapGrid:v,snapToGrid:X,containerBounds:U});if($=d,j=kD(S,i,d,Y),j.size>0&&(l||p||!Y&&m)){let[a,I]=GF({nodeId:Y,dragItems:j,nodeLookup:S});if(l?.(n.sourceEvent,j,a,I),p?.(n.sourceEvent,a,I),!Y)m?.(n.sourceEvent,I)}}let M=v6().clickDistance(w).on("start",(n)=>{let{domNode:S,nodeDragThreshold:T,transform:i,snapGrid:C,snapToGrid:v}=u();if(U=S?.getBoundingClientRect()||null,G=!1,K=!1,H=n.sourceEvent,T===0)h(n);$=r4(n.sourceEvent,{transform:i,snapGrid:C,snapToGrid:v,containerBounds:U}),J=Ol(n.sourceEvent,U)}).on("drag",(n)=>{let{autoPanOnNodeDrag:S,transform:T,snapGrid:i,snapToGrid:C,nodeDragThreshold:v,nodeLookup:X}=u(),D=r4(n.sourceEvent,{transform:T,snapGrid:i,snapToGrid:C,containerBounds:U});if(H=n.sourceEvent,n.sourceEvent.type==="touchmove"&&n.sourceEvent.touches.length>1||Y&&!X.has(Y))G=!0;if(G)return;if(!A&&S&&Q)A=!0,P();if(!Q){let p=Ol(n.sourceEvent,U),m=p.x-J.x,s=p.y-J.y;if(Math.sqrt(m*m+s*s)>v)h(n)}if(($.x!==D.xSnapped||$.y!==D.ySnapped)&&j&&Q)J=Ol(n.sourceEvent,U),B(D)}).on("end",(n)=>{if(!Q||G)return;if(A=!1,Q=!1,cancelAnimationFrame(r),j.size>0){let{nodeLookup:S,updateNodePositions:T,onNodeDragStop:i,onSelectionDragStop:C}=u();if(K)T(j,!1),K=!1;if(y||i||!Y&&C){let[v,X]=GF({nodeId:Y,dragItems:j,nodeLookup:S,dragging:!1});if(y?.(n.sourceEvent,j,v,X),i?.(n.sourceEvent,v,X),!Y)C?.(n.sourceEvent,X)}}}).filter((n)=>{let S=n.target;return!n.button&&(!Z||!KN(S,`.${Z}`,E))&&(!N||KN(S,N,E))});W.call(M)}function z(){W?.on(".drag",null)}return{update:O,destroy:z}}function sD(f,u,l){let _=[],y={x:f.x-l,y:f.y-l,width:l*2,height:l*2};for(let $ of u.values())if(m$(y,Yy($))>0)_.push($);return _}var oD=250;function aD(f,u,l,_){let y=[],$=1/0,r=sD(f,l,u+oD);for(let j of r){let A=[...j.internals.handleBounds?.source??[],...j.internals.handleBounds?.target??[]];for(let J of A){if(_.nodeId===J.nodeId&&_.type===J.type&&_.id===J.id)continue;let{x:U,y:Q}=X_(j,J,J.position,!0),W=Math.sqrt(Math.pow(U-f.x,2)+Math.pow(Q-f.y,2));if(W>u)continue;if(W<$)y=[{...J,x:U,y:Q}],$=W;else if(W===$)y.push({...J,x:U,y:Q})}}if(!y.length)return null;if(y.length>1){let j=_.type==="source"?"target":"source";return y.find((A)=>A.type===j)??y[0]}return y[0]}function RN(f,u,l,_,y,$=!1){let r=_.get(f);if(!r)return null;let j=y==="strict"?r.internals.handleBounds?.[u]:[...r.internals.handleBounds?.source??[],...r.internals.handleBounds?.target??[]],A=(l?j?.find((J)=>J.id===l):j?.[0])??null;return A&&$?{...A,...X_(r,A,A.position,!0)}:A}function xN(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 dD(f,u){let l=null;if(u)l=!0;else if(f&&!u)l=!1;return l}var bN=()=>!0;function eD(f,{connectionMode:u,connectionRadius:l,handleId:_,nodeId:y,edgeUpdaterType:$,isTarget:r,domNode:j,nodeLookup:A,lib:J,autoPanOnConnect:U,flowId:Q,panBy:W,cancelConnection:G,onConnectStart:K,onConnect:H,onConnectEnd:O,isValidConnection:z=bN,onReconnectEnd:Z,updateConnection:N,getTransform:E,getFromHandle:q,autoPanSpeed:Y,dragThreshold:w=1,handleDomNode:B}){let P=nF(f.target),h=0,M,{x:n,y:S}=Ol(f),T=xN($,B),i=j?.getBoundingClientRect(),C=!1;if(!i||!T)return;let v=RN(y,T,_,A,u);if(!v)return;let X=Ol(f,i),D=!1,p=null,m=!1,s=null;function d(){if(!U||!i)return;let[Gf,c]=LN(X,i,Y);W({x:Gf,y:c}),h=requestAnimationFrame(d)}let a={...v,nodeId:y,type:T,position:v.position},I=A.get(y),yf={inProgress:!0,isValid:null,from:X_(I,a,Zf.Left,!0),fromHandle:a,fromPosition:a.position,fromNode:I,to:X,toHandle:null,toPosition:rN[a.position],toNode:null,pointer:X};function rf(){C=!0,N(yf),K?.(f,{nodeId:y,handleId:_,handleType:T})}if(w===0)rf();function Wf(Gf){if(!C){let{x:Af,y:Yf}=Ol(Gf),Bf=Af-n,df=Yf-S;if(!(Bf*Bf+df*df>w*w))return;rf()}if(!q()||!a){Ef(Gf);return}let c=E();if(X=Ol(Gf,i),M=aD(k$(X,c,!1,[1,1]),l,A,a),!D)d(),D=!0;let o=vN(Gf,{handle:M,connectionMode:u,fromNodeId:y,fromHandleId:_,fromType:r?"target":"source",isValidConnection:z,doc:P,lib:J,flowId:Q,nodeLookup:A});s=o.handleDomNode,p=o.connection,m=dD(!!M,o.isValid);let e=A.get(y),Kf=e?X_(e,a,Zf.Left,!0):yf.from,k={...yf,from:Kf,isValid:m,to:o.toHandle&&m?j4({x:o.toHandle.x,y:o.toHandle.y},c):X,toHandle:o.toHandle,toPosition:m&&o.toHandle?o.toHandle.position:rN[a.position],toNode:o.toHandle?A.get(o.toHandle.nodeId):null,pointer:X};N(k),yf=k}function Ef(Gf){if("touches"in Gf&&Gf.touches.length>0)return;if(C){if((M||s)&&p&&m)H?.(p);let{inProgress:c,...o}=yf,e={...o,toPosition:yf.toHandle?yf.toPosition:null};if(O?.(Gf,e),$)Z?.(Gf,e)}G(),cancelAnimationFrame(h),D=!1,m=!1,p=null,s=null,P.removeEventListener("mousemove",Wf),P.removeEventListener("mouseup",Ef),P.removeEventListener("touchmove",Wf),P.removeEventListener("touchend",Ef)}P.addEventListener("mousemove",Wf),P.addEventListener("mouseup",Ef),P.addEventListener("touchmove",Wf),P.addEventListener("touchend",Ef)}function vN(f,{handle:u,connectionMode:l,fromNodeId:_,fromHandleId:y,fromType:$,doc:r,lib:j,flowId:A,isValidConnection:J=bN,nodeLookup:U}){let Q=$==="target",W=u?r.querySelector(`.${j}-flow__handle[data-id="${A}-${u?.nodeId}-${u?.id}-${u?.type}"]`):null,{x:G,y:K}=Ol(f),H=r.elementFromPoint(G,K),O=H?.classList.contains(`${j}-flow__handle`)?H:W,z={handleDomNode:O,isValid:!1,connection:null,toHandle:null};if(O){let Z=xN(void 0,O),N=O.getAttribute("data-nodeid"),E=O.getAttribute("data-handleid"),q=O.classList.contains("connectable"),Y=O.classList.contains("connectableend");if(!N||!Z)return z;let w={source:Q?N:_,sourceHandle:Q?E:y,target:Q?_:N,targetHandle:Q?y:E};z.connection=w;let P=q&&Y&&(l===q_.Strict?Q&&Z==="source"||!Q&&Z==="target":N!==_||E!==y);z.isValid=P&&J(w),z.toHandle=RN(N,Z,E,U,l,!0)}return z}var c5={onPointerDown:eD,isValid:vN};function hN({domNode:f,panZoom:u,getTransform:l,getViewScale:_}){let y=g0(f);function $({translateExtent:j,width:A,height:J,zoomStep:U=1,pannable:Q=!0,zoomable:W=!0,inversePan:G=!1}){let K=(N)=>{if(N.sourceEvent.type!=="wheel"||!u)return;let E=l(),q=N.sourceEvent.ctrlKey&&t$()?10:1,Y=-N.sourceEvent.deltaY*(N.sourceEvent.deltaMode===1?0.05:N.sourceEvent.deltaMode?1:0.002)*U,w=E[2]*Math.pow(2,Y*q);u.scaleTo(w)},H=[0,0],O=(N)=>{if(N.sourceEvent.type==="mousedown"||N.sourceEvent.type==="touchstart")H=[N.sourceEvent.clientX??N.sourceEvent.touches[0].clientX,N.sourceEvent.clientY??N.sourceEvent.touches[0].clientY]},z=(N)=>{let E=l();if(N.sourceEvent.type!=="mousemove"&&N.sourceEvent.type!=="touchmove"||!u)return;let q=[N.sourceEvent.clientX??N.sourceEvent.touches[0].clientX,N.sourceEvent.clientY??N.sourceEvent.touches[0].clientY],Y=[q[0]-H[0],q[1]-H[1]];H=q;let w=_()*Math.max(E[2],Math.log(E[2]))*(G?-1:1),B={x:E[0]-Y[0]*w,y:E[1]-Y[1]*w},P=[[0,0],[A,J]];u.setViewportConstrained({x:B.x,y:B.y,zoom:E[2]},P,j)},Z=$4().on("start",O).on("zoom",Q?z:null).on("zoom.wheel",W?K:null);y.call(Z,{})}function r(){y.on("zoom",null)}return{update:$,destroy:r,pointer:zu}}var R5=(f)=>({x:f.x,y:f.y,zoom:f.k}),KF=({x:f,y:u,zoom:l})=>qy.translate(f,u).scale(l),b$=(f,u)=>f.target.closest(`.${u}`),IN=(f,u)=>u===2&&Array.isArray(f)&&f.includes(2),fT=(f)=>((f*=2)<=1?f*f*f:(f-=2)*f*f+2)/2,NF=(f,u=0,l=fT,_=()=>{})=>{let y=typeof u==="number"&&u>0;if(!y)_();return y?f.transition().duration(u).ease(l).on("end",_):f},pN=(f)=>{let u=f.ctrlKey&&t$()?10:1;return-f.deltaY*(f.deltaMode===1?0.05:f.deltaMode?1:0.002)*u};function uT({zoomPanValues:f,noWheelClassName:u,d3Selection:l,d3Zoom:_,panOnScrollMode:y,panOnScrollSpeed:$,zoomOnPinch:r,onPanZoomStart:j,onPanZoom:A,onPanZoomEnd:J}){return(U)=>{if(b$(U,u)){if(U.ctrlKey)U.preventDefault();return!1}U.preventDefault(),U.stopImmediatePropagation();let Q=l.property("__zoom").k||1;if(U.ctrlKey&&r){let O=zu(U),z=pN(U),Z=Q*Math.pow(2,z);_.scaleTo(l,Z,O,U);return}let W=U.deltaMode===1?20:1,G=y===c1.Vertical?0:U.deltaX*W,K=y===c1.Horizontal?0:U.deltaY*W;if(!t$()&&U.shiftKey&&y!==c1.Vertical)G=U.deltaY*W,K=0;_.translateBy(l,-(G/Q)*$,-(K/Q)*$,{internal:!0});let H=R5(l.property("__zoom"));if(clearTimeout(f.panScrollTimeout),!f.isPanScrolling)f.isPanScrolling=!0,j?.(U,H);else A?.(U,H),f.panScrollTimeout=setTimeout(()=>{J?.(U,H),f.isPanScrolling=!1},150)}}function lT({noWheelClassName:f,preventScrolling:u,d3ZoomHandler:l}){return function(_,y){let $=_.type==="wheel",r=!u&&$&&!_.ctrlKey,j=b$(_,f);if(_.ctrlKey&&$&&j)_.preventDefault();if(r||j)return null;_.preventDefault(),l.call(this,_,y)}}function _T({zoomPanValues:f,onDraggingChange:u,onPanZoomStart:l}){return(_)=>{if(_.sourceEvent?.internal)return;let y=R5(_.transform);if(f.mouseButton=_.sourceEvent?.button||0,f.isZoomingOrPanning=!0,f.prevViewport=y,_.sourceEvent?.type==="mousedown")u(!0);if(l)l?.(_.sourceEvent,y)}}function yT({zoomPanValues:f,panOnDrag:u,onPaneContextMenu:l,onTransformChange:_,onPanZoom:y}){return($)=>{if(f.usedRightMouseButton=!!(l&&IN(u,f.mouseButton??0)),!$.sourceEvent?.sync)_([$.transform.x,$.transform.y,$.transform.k]);if(y&&!$.sourceEvent?.internal)y?.($.sourceEvent,R5($.transform))}}function $T({zoomPanValues:f,panOnDrag:u,panOnScroll:l,onDraggingChange:_,onPanZoomEnd:y,onPaneContextMenu:$}){return(r)=>{if(r.sourceEvent?.internal)return;if(f.isZoomingOrPanning=!1,$&&IN(u,f.mouseButton??0)&&!f.usedRightMouseButton&&r.sourceEvent)$(r.sourceEvent);if(f.usedRightMouseButton=!1,_(!1),y){let j=R5(r.transform);f.prevViewport=j,clearTimeout(f.timerId),f.timerId=setTimeout(()=>{y?.(r.sourceEvent,j)},l?150:0)}}}function rT({zoomActivationKeyPressed:f,zoomOnScroll:u,zoomOnPinch:l,panOnDrag:_,panOnScroll:y,zoomOnDoubleClick:$,userSelectionActive:r,noWheelClassName:j,noPanClassName:A,lib:J,connectionInProgress:U}){return(Q)=>{let W=f||u,G=l&&Q.ctrlKey,K=Q.type==="wheel";if(Q.button===1&&Q.type==="mousedown"&&(b$(Q,`${J}-flow__node`)||b$(Q,`${J}-flow__edge`)))return!0;if(!_&&!W&&!y&&!$&&!l)return!1;if(r)return!1;if(U&&!K)return!1;if(b$(Q,j)&&K)return!1;if(b$(Q,A)&&(!K||y&&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&&!y&&!G&&K)return!1;if(!_&&(Q.type==="mousedown"||Q.type==="touchstart"))return!1;if(Array.isArray(_)&&!_.includes(Q.button)&&Q.type==="mousedown")return!1;let H=Array.isArray(_)&&_.includes(Q.button)||!Q.button||Q.button<=1;return(!Q.ctrlKey||K)&&H}}function mN({domNode:f,minZoom:u,maxZoom:l,translateExtent:_,viewport:y,onPanZoom:$,onPanZoomStart:r,onPanZoomEnd:j,onDraggingChange:A}){let J={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=$4().scaleExtent([u,l]).translateExtent(_),W=g0(f).call(Q);Z({x:y.x,y:y.y,zoom:v$(y.zoom,u,l)},[[0,0],[U.width,U.height]],_);let G=W.on("wheel.zoom"),K=W.on("dblclick.zoom");Q.wheelDelta(pN);function H(M,n){if(W)return new Promise((S)=>{Q?.interpolate(n?.interpolate==="linear"?y1:Oy).transform(NF(W,n?.duration,n?.ease,()=>S(!0)),M)});return Promise.resolve(!1)}function O({noWheelClassName:M,noPanClassName:n,onPaneContextMenu:S,userSelectionActive:T,panOnScroll:i,panOnDrag:C,panOnScrollMode:v,panOnScrollSpeed:X,preventScrolling:D,zoomOnPinch:p,zoomOnScroll:m,zoomOnDoubleClick:s,zoomActivationKeyPressed:d,lib:a,onTransformChange:I,connectionInProgress:ff,paneClickDistance:yf,selectionOnDrag:rf}){if(T&&!J.isZoomingOrPanning)z();let Wf=i&&!d&&!T;Q.clickDistance(rf?1/0:!Hl(yf)||yf<0?0:yf);let Ef=Wf?uT({zoomPanValues:J,noWheelClassName:M,d3Selection:W,d3Zoom:Q,panOnScrollMode:v,panOnScrollSpeed:X,zoomOnPinch:p,onPanZoomStart:r,onPanZoom:$,onPanZoomEnd:j}):lT({noWheelClassName:M,preventScrolling:D,d3ZoomHandler:G});if(W.on("wheel.zoom",Ef,{passive:!1}),!T){let c=_T({zoomPanValues:J,onDraggingChange:A,onPanZoomStart:r});Q.on("start",c);let o=yT({zoomPanValues:J,panOnDrag:C,onPaneContextMenu:!!S,onPanZoom:$,onTransformChange:I});Q.on("zoom",o);let e=$T({zoomPanValues:J,panOnDrag:C,panOnScroll:i,onPaneContextMenu:S,onPanZoomEnd:j,onDraggingChange:A});Q.on("end",e)}let Gf=rT({zoomActivationKeyPressed:d,panOnDrag:C,zoomOnScroll:m,panOnScroll:i,zoomOnDoubleClick:s,zoomOnPinch:p,userSelectionActive:T,noPanClassName:n,noWheelClassName:M,lib:a,connectionInProgress:ff});if(Q.filter(Gf),s)W.on("dblclick.zoom",K);else W.on("dblclick.zoom",null)}function z(){Q.on("zoom",null)}async function Z(M,n,S){let T=KF(M),i=Q?.constrain()(T,n,S);if(i)await H(i);return new Promise((C)=>C(i))}async function N(M,n){let S=KF(M);return await H(S,n),new Promise((T)=>T(S))}function E(M){if(W){let n=KF(M),S=W.property("__zoom");if(S.k!==M.zoom||S.x!==M.x||S.y!==M.y)Q?.transform(W,n,null,{sync:!0})}}function q(){let M=W?y4(W.node()):{x:0,y:0,k:1};return{x:M.x,y:M.y,zoom:M.k}}function Y(M,n){if(W)return new Promise((S)=>{Q?.interpolate(n?.interpolate==="linear"?y1:Oy).scaleTo(NF(W,n?.duration,n?.ease,()=>S(!0)),M)});return Promise.resolve(!1)}function w(M,n){if(W)return new Promise((S)=>{Q?.interpolate(n?.interpolate==="linear"?y1:Oy).scaleBy(NF(W,n?.duration,n?.ease,()=>S(!0)),M)});return Promise.resolve(!1)}function B(M){Q?.scaleExtent(M)}function P(M){Q?.translateExtent(M)}function h(M){let n=!Hl(M)||M<0?0:M;Q?.clickDistance(n)}return{update:O,destroy:z,setViewport:N,setViewportConstrained:Z,getViewport:q,scaleTo:Y,scaleBy:w,setScaleExtent:B,setTranslateExtent:P,syncViewport:E,setClickDistance:h}}var B_;(function(f){f.Line="line",f.Handle="handle"})(B_||(B_={}));function jT({width:f,prevWidth:u,height:l,prevHeight:_,affectsX:y,affectsY:$}){let r=f-u,j=l-_,A=[r>0?1:r<0?-1:0,j>0?1:j<0?-1:0];if(r&&y)A[0]=A[0]*-1;if(j&&$)A[1]=A[1]*-1;return A}function NN(f){let u=f.includes("right")||f.includes("left"),l=f.includes("bottom")||f.includes("top"),_=f.includes("left"),y=f.includes("top");return{isHorizontal:u,isVertical:l,affectsX:_,affectsY:y}}function O_(f,u){return Math.max(0,u-f)}function V_(f,u){return Math.max(0,f-u)}function X5(f,u,l){return Math.max(0,u-f,f-l)}function ZN(f,u){return f?!u:u}function AT(f,u,l,_,y,$,r,j){let{affectsX:A,affectsY:J}=u,{isHorizontal:U,isVertical:Q}=u,W=U&&Q,{xSnapped:G,ySnapped:K}=l,{minWidth:H,maxWidth:O,minHeight:z,maxHeight:Z}=_,{x:N,y:E,width:q,height:Y,aspectRatio:w}=f,B=Math.floor(U?G-f.pointerX:0),P=Math.floor(Q?K-f.pointerY:0),h=q+(A?-B:B),M=Y+(J?-P:P),n=-$[0]*q,S=-$[1]*Y,T=X5(h,H,O),i=X5(M,z,Z);if(r){let X=0,D=0;if(A&&B<0)X=O_(N+B+n,r[0][0]);else if(!A&&B>0)X=V_(N+h+n,r[1][0]);if(J&&P<0)D=O_(E+P+S,r[0][1]);else if(!J&&P>0)D=V_(E+M+S,r[1][1]);T=Math.max(T,X),i=Math.max(i,D)}if(j){let X=0,D=0;if(A&&B>0)X=V_(N+B,j[0][0]);else if(!A&&B<0)X=O_(N+h,j[1][0]);if(J&&P>0)D=V_(E+P,j[0][1]);else if(!J&&P<0)D=O_(E+M,j[1][1]);T=Math.max(T,X),i=Math.max(i,D)}if(y){if(U){let X=X5(h/w,z,Z)*w;if(T=Math.max(T,X),r){let D=0;if(!A&&!J||A&&!J&&W)D=V_(E+S+h/w,r[1][1])*w;else D=O_(E+S+(A?B:-B)/w,r[0][1])*w;T=Math.max(T,D)}if(j){let D=0;if(!A&&!J||A&&!J&&W)D=O_(E+h/w,j[1][1])*w;else D=V_(E+(A?B:-B)/w,j[0][1])*w;T=Math.max(T,D)}}if(Q){let X=X5(M*w,H,O)/w;if(i=Math.max(i,X),r){let D=0;if(!A&&!J||J&&!A&&W)D=V_(N+M*w+n,r[1][0])/w;else D=O_(N+(J?P:-P)*w+n,r[0][0])/w;i=Math.max(i,D)}if(j){let D=0;if(!A&&!J||J&&!A&&W)D=O_(N+M*w,j[1][0])/w;else D=V_(N+(J?P:-P)*w,j[0][0])/w;i=Math.max(i,D)}}}if(P=P+(P<0?i:-i),B=B+(B<0?T:-T),y)if(W)if(h>M*w)P=(ZN(A,J)?-B:B)/w;else B=(ZN(A,J)?-P:P)*w;else if(U)P=B/w,J=A;else B=P*w,A=J;let C=A?N+B:N,v=J?E+P:E;return{width:q+(A?-B:B),height:Y+(J?-P:P),x:$[0]*B*(!A?1:-1)+C,y:$[1]*P*(!J?1:-1)+v}}var gN={width:0,height:0,x:0,y:0},FT={...gN,pointerX:0,pointerY:0,aspectRatio:1};function JT(f){return[[0,0],[f.measured.width,f.measured.height]]}function UT(f,u,l){let _=u.position.x+f.position.x,y=u.position.y+f.position.y,$=f.measured.width??0,r=f.measured.height??0,j=l[0]*$,A=l[1]*r;return[[_-j,y-A],[_+$-j,y+r-A]]}function kN({domNode:f,nodeId:u,getStoreItems:l,onChange:_,onEnd:y}){let $=g0(f),r={controlDirection:NN("bottom-right"),boundaries:{minWidth:0,minHeight:0,maxWidth:Number.MAX_VALUE,maxHeight:Number.MAX_VALUE},resizeDirection:void 0,keepAspectRatio:!1};function j({controlPosition:J,boundaries:U,keepAspectRatio:Q,resizeDirection:W,onResizeStart:G,onResize:K,onResizeEnd:H,shouldResize:O}){let z={...gN},Z={...FT};r={boundaries:U,resizeDirection:W,keepAspectRatio:Q,controlDirection:NN(J)};let N=void 0,E=null,q=[],Y=void 0,w=void 0,B=void 0,P=!1,h=v6().on("start",(M)=>{let{nodeLookup:n,transform:S,snapGrid:T,snapToGrid:i,nodeOrigin:C,paneDomNode:v}=l();if(N=n.get(u),!N)return;E=v?.getBoundingClientRect()??null;let{xSnapped:X,ySnapped:D}=r4(M.sourceEvent,{transform:S,snapGrid:T,snapToGrid:i,containerBounds:E});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:X,pointerY:D,aspectRatio:z.width/z.height},Y=void 0,N.parentId&&(N.extent==="parent"||N.expandParent))Y=n.get(N.parentId),w=Y&&N.extent==="parent"?JT(Y):void 0;q=[],B=void 0;for(let[p,m]of n)if(m.parentId===u){if(q.push({id:p,position:{...m.position},extent:m.extent}),m.extent==="parent"||m.expandParent){let s=UT(m,N,m.origin??C);if(B)B=[[Math.min(s[0][0],B[0][0]),Math.min(s[0][1],B[0][1])],[Math.max(s[1][0],B[1][0]),Math.max(s[1][1],B[1][1])]];else B=s}}G?.(M,{...z})}).on("drag",(M)=>{let{transform:n,snapGrid:S,snapToGrid:T,nodeOrigin:i}=l(),C=r4(M.sourceEvent,{transform:n,snapGrid:S,snapToGrid:T,containerBounds:E}),v=[];if(!N)return;let{x:X,y:D,width:p,height:m}=z,s={},d=N.origin??i,{width:a,height:I,x:ff,y:yf}=AT(Z,r.controlDirection,C,r.boundaries,r.keepAspectRatio,d,w,B),rf=a!==p,Wf=I!==m,Ef=ff!==X&&rf,Gf=yf!==D&&Wf;if(!Ef&&!Gf&&!rf&&!Wf)return;if(Ef||Gf||d[0]===1||d[1]===1){if(s.x=Ef?ff:z.x,s.y=Gf?yf:z.y,z.x=s.x,z.y=s.y,q.length>0){let Kf=ff-X,k=yf-D;for(let Af of q)Af.position={x:Af.position.x-Kf+d[0]*(a-p),y:Af.position.y-k+d[1]*(I-m)},v.push(Af)}}if(rf||Wf)s.width=rf&&(!r.resizeDirection||r.resizeDirection==="horizontal")?a:z.width,s.height=Wf&&(!r.resizeDirection||r.resizeDirection==="vertical")?I:z.height,z.width=s.width,z.height=s.height;if(Y&&N.expandParent){let Kf=d[0]*(s.width??0);if(s.x&&s.x{if(!P)return;H?.(M,{...z}),y?.({...z}),P=!1});$.call(h)}function A(){$.on(".drag",null)}return{update:j,destroy:A}}var $Z=cf(O0(),1),rZ=cf(uZ(),1);var lZ=(f)=>{let u,l=new Set,_=(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))}},y=()=>u,A={setState:_,getState:y,getInitialState:()=>J,subscribe:(U)=>{return l.add(U),()=>l.delete(U)},destroy:()=>{l.clear()}},J=u=f(_,y,A);return A},_Z=(f)=>f?lZ(f):lZ;var{useDebugValue:DT}=$Z.default,{useSyncExternalStoreWithSelector:TT}=rZ.default,MT=(f)=>f;function pF(f,u=MT,l){let _=TT(f.subscribe,f.getState,f.getServerState||f.getInitialState,u,l);return DT(_),_}var yZ=(f,u)=>{let l=_Z(f),_=(y,$=u)=>pF(l,y,$);return Object.assign(_,l),_},jZ=(f,u)=>f?yZ(f,u):yZ;function H0(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]of f)if(!Object.is(y,u.get(_)))return!1;return!0}if(f instanceof Set&&u instanceof Set){if(f.size!==u.size)return!1;for(let _ of f)if(!u.has(_))return!1;return!0}let l=Object.keys(f);if(l.length!==Object.keys(u).length)return!1;for(let _ of l)if(!Object.prototype.hasOwnProperty.call(u,_)||!Object.is(f[_],u[_]))return!1;return!0}var PT=cf(d7(),1),I5=_f.createContext(null),nT=I5.Provider,MZ=$l.error001();function f0(f,u){let l=_f.useContext(I5);if(l===null)throw Error(MZ);return pF(l,f,u)}function q0(){let f=_f.useContext(I5);if(f===null)throw Error(MZ);return _f.useMemo(()=>({getState:f.getState,setState:f.setState,subscribe:f.subscribe}),[f])}var AZ={display:"none"},ST={position:"absolute",width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0px, 0px, 0px, 0px)",clipPath:"inset(100%)"},PZ="react-flow__node-desc",nZ="react-flow__edge-desc",CT="react-flow__aria-live",iT=(f)=>f.ariaLiveMessage,cT=(f)=>f.ariaLabelConfig;function RT({rfId:f}){let u=f0(iT);return lf.jsx("div",{id:`${CT}-${f}`,"aria-live":"assertive","aria-atomic":"true",style:ST,children:u})}function xT({rfId:f,disableKeyboardA11y:u}){let l=f0(cT);return lf.jsxs(lf.Fragment,{children:[lf.jsx("div",{id:`${PZ}-${f}`,style:AZ,children:u?l["node.a11yDescription.default"]:l["node.a11yDescription.keyboardDisabled"]}),lf.jsx("div",{id:`${nZ}-${f}`,style:AZ,children:l["edge.a11yDescription.default"]}),!u&&lf.jsx(RT,{rfId:f})]})}var p5=_f.forwardRef(({position:f="top-left",children:u,className:l,style:_,...y},$)=>{let r=`${f}`.split("-");return lf.jsx("div",{className:M0(["react-flow__panel",l,...r]),style:_,ref:$,...y,children:u})});p5.displayName="Panel";function bT({proOptions:f,position:u="bottom-right"}){if(f?.hideAttribution)return null;return lf.jsx(p5,{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:lf.jsx("a",{href:"https://reactflow.dev",target:"_blank",rel:"noopener noreferrer","aria-label":"React Flow attribution",children:"React Flow"})})}var vT=(f)=>{let u=[],l=[];for(let[,_]of f.nodeLookup)if(_.selected)u.push(_.internals.userNode);for(let[,_]of f.edgeLookup)if(_.selected)l.push(_);return{selectedNodes:u,selectedEdges:l}},b5=(f)=>f.id;function hT(f,u){return H0(f.selectedNodes.map(b5),u.selectedNodes.map(b5))&&H0(f.selectedEdges.map(b5),u.selectedEdges.map(b5))}function IT({onSelectionChange:f}){let u=q0(),{selectedNodes:l,selectedEdges:_}=f0(vT,hT);return _f.useEffect(()=>{let y={nodes:l,edges:_};f?.(y),u.getState().onSelectionChangeHandlers.forEach(($)=>$(y))},[l,_,f]),null}var pT=(f)=>!!f.onSelectionChangeHandlers;function mT({onSelectionChange:f}){let u=f0(pT);if(f||u)return lf.jsx(IT,{onSelectionChange:f});return null}var kF=typeof window<"u"?_f.useLayoutEffect:_f.useEffect,SZ=[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"],FZ=[...kT,"rfId"],tT=(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}),JZ={translateExtent:I$,nodeOrigin:SZ,minZoom:0.5,maxZoom:2,elementsSelectable:!0,noPanClassName:"nopan",rfId:"1"};function sT(f){let{setNodes:u,setEdges:l,setMinZoom:_,setMaxZoom:y,setTranslateExtent:$,setNodeExtent:r,reset:j,setDefaultNodesAndEdges:A}=f0(tT,H0),J=q0();kF(()=>{return A(f.defaultNodes,f.defaultEdges),()=>{U.current=JZ,j()}},[]);let U=_f.useRef(JZ);return kF(()=>{for(let Q of FZ){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")_(W);else if(Q==="maxZoom")y(W);else if(Q==="translateExtent")$(W);else if(Q==="nodeExtent")r(W);else if(Q==="ariaLabelConfig")J.setState({ariaLabelConfig:BN(W)});else if(Q==="fitView")J.setState({fitViewQueued:W});else if(Q==="fitViewOptions")J.setState({fitViewOptions:W});else J.setState({[Q]:W})}U.current=f},FZ.map((Q)=>f[Q])),null}function UZ(){if(typeof window>"u"||!window.matchMedia)return null;return window.matchMedia("(prefers-color-scheme: dark)")}function oT(f){let[u,l]=_f.useState(f==="system"?null:f);return _f.useEffect(()=>{if(f!=="system"){l(f);return}let _=UZ(),y=()=>l(_?.matches?"dark":"light");return y(),_?.addEventListener("change",y),()=>{_?.removeEventListener("change",y)}},[f]),u!==null?u:UZ()?.matches?"dark":"light"}var QZ=typeof document<"u"?document:null;function U4(f=null,u={target:QZ,actInsideInputWithModifier:!0}){let[l,_]=_f.useState(!1),y=_f.useRef(!1),$=_f.useRef(new Set([])),[r,j]=_f.useMemo(()=>{if(f!==null){let J=(Array.isArray(f)?f:[f]).filter((Q)=>typeof Q==="string").map((Q)=>Q.replace("+",` `).replace(` `,` +`).split(` -`)),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(` +`)),U=J.reduce((Q,W)=>Q.concat(...W),[]);return[J,U]}return[[],[]]},[f]);return _f.useEffect(()=>{let A=u?.target??QZ,J=u?.actInsideInputWithModifier??!0;if(f!==null){let U=(G)=>{if(y.current=G.ctrlKey||G.metaKey||G.shiftKey||G.altKey,(!y.current||y.current&&!J)&&SF(G))return!1;let H=zZ(G.code,j);if($.current.add(G[H]),WZ(r,$.current,!1)){let O=G.composedPath?.()?.[0]||G.target,z=O?.nodeName==="BUTTON"||O?.nodeName==="A";if(u.preventDefault!==!1&&(y.current||!z))G.preventDefault();_(!0)}},Q=(G)=>{let K=zZ(G.code,j);if(WZ(r,$.current,!0))_(!1),$.current.clear();else $.current.delete(G[K]);if(G.key==="Meta")$.current.clear();y.current=!1},W=()=>{$.current.clear(),_(!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,_]),l}function WZ(f,u,l){return f.filter((_)=>l||_.length===u.size).some((_)=>_.every((y)=>u.has(y)))}function zZ(f,u){return u.includes(f)?"code":"key"}var aT=()=>{let f=q0();return _f.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:_}=f.getState();return _?_.scaleTo(u,l):Promise.resolve(!1)},getZoom:()=>f.getState().transform[2],setViewport:async(u,l)=>{let{transform:[_,y,$],panZoom:r}=f.getState();if(!r)return Promise.resolve(!1);return await r.setViewport({x:u.x??_,y:u.y??y,zoom:u.zoom??$},l),Promise.resolve(!0)},getViewport:()=>{let[u,l,_]=f.getState().transform;return{x:u,y:l,zoom:_}},setCenter:async(u,l,_)=>{return f.getState().setCenter(u,l,_)},fitBounds:async(u,l)=>{let{width:_,height:y,minZoom:$,maxZoom:r,panZoom:j}=f.getState(),A=F4(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:_,snapGrid:y,snapToGrid:$,domNode:r}=f.getState();if(!r)return u;let{x:j,y:A}=r.getBoundingClientRect(),J={x:u.x-j,y:u.y-A},U=l.snapGrid??y,Q=l.snapToGrid??$;return k$(J,_,Q,U)},flowToScreenPosition:(u)=>{let{transform:l,domNode:_}=f.getState();if(!_)return u;let{x:y,y:$}=_.getBoundingClientRect(),r=j4(u,l);return{x:r.x+y,y:r.y+$}}}},[])};function CZ(f,u){let l=[],_=new Map,y=[];for(let $ of f)if($.type==="add"){y.push($);continue}else if($.type==="remove"||$.type==="replace")_.set($.id,[$]);else{let r=_.get($.id);if(r)r.push($);else _.set($.id,[$])}for(let $ of u){let r=_.get($.id);if(!r){l.push($);continue}if(r[0].type==="remove")continue;if(r[0].type==="replace"){l.push({...r[0].item});continue}let j={...$};for(let A of r)dT(A,j);l.push(j)}if(y.length)y.forEach(($)=>{if($.index!==void 0)l.splice($.index,0,{...$.item});else l.push({...$.item})});return l}function dT(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 eT(f,u){return CZ(f,u)}function fM(f,u){return CZ(f,u)}function wy(f,u){return{id:f,type:"select",selected:u}}function o$(f,u=new Set,l=!1){let _=[];for(let[y,$]of f){let r=u.has(y);if(!($.selected===void 0&&!r)&&$.selected!==r){if(l)$.selected=r;_.push(wy($.id,r))}}return _}function GZ({items:f=[],lookup:u}){let l=[],_=new Map(f.map((y)=>[y.id,y]));for(let[y,$]of f.entries()){let r=u.get($.id),j=r?.internals?.userNode??r;if(j!==void 0&&j!==$)l.push({id:$.id,item:$,type:"replace"});if(j===void 0)l.push({item:$,type:"add",index:y})}for(let[y]of u)if(_.get(y)===void 0)l.push({id:y,type:"remove"});return l}function KZ(f){return{id:f.id,type:"remove"}}var NZ=(f)=>EN(f),uM=(f)=>qF(f);function iZ(f){return _f.forwardRef(f)}function ZZ(f){let[u,l]=_f.useState(BigInt(0)),[_]=_f.useState(()=>lM(()=>l((y)=>y+BigInt(1))));return kF(()=>{let y=_.get();if(y.length)f(y),_.reset()},[u]),_}function lM(f){let u=[];return{get:()=>u,reset:()=>{u=[]},push:(l)=>{u.push(l),f()}}}var cZ=_f.createContext(null);function _M({children:f}){let u=q0(),l=_f.useCallback((j)=>{let{nodes:A=[],setNodes:J,hasDefaultNodes:U,onNodesChange:Q,nodeLookup:W,fitViewQueued:G,onNodesChangeMiddlewareMap:K}=u.getState(),H=A;for(let z of j)H=typeof z==="function"?z(H):z;let O=GZ({items:H,lookup:W});for(let z of K.values())O=z(O);if(U)J(H);if(O.length>0)Q?.(O);else if(G)window.requestAnimationFrame(()=>{let{fitViewQueued:z,nodes:Z,setNodes:N}=u.getState();if(z)N(Z)})},[]),_=ZZ(l),y=_f.useCallback((j)=>{let{edges:A=[],setEdges:J,hasDefaultEdges:U,onEdgesChange:Q,edgeLookup:W}=u.getState(),G=A;for(let K of j)G=typeof K==="function"?K(G):K;if(U)J(G);else if(Q)Q(GZ({items:G,lookup:W}))},[]),$=ZZ(y),r=_f.useMemo(()=>({nodeQueue:_,edgeQueue:$}),[]);return lf.jsx(cZ.Provider,{value:r,children:f})}function yM(){let f=_f.useContext(cZ);if(!f)throw Error("useBatchContext must be used within a BatchProvider");return f}var $M=(f)=>!!f.panZoom;function sF(){let f=aT(),u=q0(),l=yM(),_=f0($M),y=_f.useMemo(()=>{let $=(Q)=>u.getState().nodeLookup.get(Q),r=(Q)=>{l.nodeQueue.push(Q)},j=(Q)=>{l.edgeQueue.push(Q)},A=(Q)=>{let{nodeLookup:W,nodeOrigin:G}=u.getState(),K=NZ(Q)?Q:W.get(Q.id),H=K.parentId?MF(K.position,K.measured,K.parentId,W,G):K.position,O={...K,position:H,width:K.measured?.width??K.width,height:K.measured?.height??K.height};return Yy(O)},J=(Q,W,G={replace:!1})=>{r((K)=>K.map((H)=>{if(H.id===Q){let O=typeof W==="function"?W(H):W;return G.replace&&NZ(O)?O:{...H,...O}}return H}))},U=(Q,W,G={replace:!1})=>{j((K)=>K.map((H)=>{if(H.id===Q){let O=typeof W==="function"?W(H):W;return G.replace&&uM(O)?O:{...H,...O}}return H}))};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:r,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,H,O]=G;return{nodes:Q.map((z)=>({...z})),edges:W.map((z)=>({...z})),viewport:{x:K,y:H,zoom:O}}},deleteElements:async({nodes:Q=[],edges:W=[]})=>{let{nodes:G,edges:K,onNodesDelete:H,onEdgesDelete:O,triggerNodeChanges:z,triggerEdgeChanges:Z,onDelete:N,onBeforeDelete:E}=u.getState(),{nodes:q,edges:Y}=await VN({nodesToRemove:Q,edgesToRemove:W,nodes:G,edges:K,onBeforeDelete:E}),w=Y.length>0,B=q.length>0;if(w){let P=Y.map(KZ);O?.(Y),Z(P)}if(B){let P=q.map(KZ);H?.(q),z(P)}if(B||w)N?.({nodes:q,edges:Y});return{deletedNodes:q,deletedEdges:Y}},getIntersectingNodes:(Q,W=!0,G)=>{let K=wF(Q),H=K?Q:A(Q),O=G!==void 0;if(!H)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=Yy(O?z:Z),E=m$(N,H);return W&&E>0||E>=N.width*N.height||E>=H.width*H.height})},isNodeIntersecting:(Q,W,G=!0)=>{let H=wF(Q)?Q:A(Q);if(!H)return!1;let O=m$(H,W);return G&&O>0||O>=W.width*W.height||O>=H.width*H.height},updateNode:J,updateNodeData:(Q,W,G={replace:!1})=>{J(Q,(K)=>{let H=typeof W==="function"?W(K):W;return G.replace?{...K,data:H}:{...K,data:{...K.data,...H}}},G)},updateEdge:U,updateEdgeData:(Q,W,G={replace:!1})=>{U(Q,(K)=>{let H=typeof W==="function"?W(K):W;return G.replace?{...K,data:H}:{...K,data:{...K.data,...H}}},G)},getNodesBounds:(Q)=>{let{nodeLookup:W,nodeOrigin:G}=u.getState();return XF(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??XN();return u.setState({fitViewQueued:!0,fitViewOptions:Q,fitViewResolver:W}),l.nodeQueue.push((G)=>[...G]),W.promise}}},[]);return _f.useMemo(()=>{return{...y,...f,viewportInitialized:_}},[_])}var EZ=(f)=>f.selected,rM=typeof window<"u"?window:void 0;function jM({deleteKeyCode:f,multiSelectionKeyCode:u}){let l=q0(),{deleteElements:_}=sF(),y=U4(f,{actInsideInputWithModifier:!1}),$=U4(u,{target:rM});_f.useEffect(()=>{if(y){let{edges:r,nodes:j}=l.getState();_({nodes:j.filter(EZ),edges:r.filter(EZ)}),l.setState({nodesSelectionActive:!1})}},[y]),_f.useEffect(()=>{l.setState({multiSelectionActive:$})},[$])}function AM(f){let u=q0();_f.useEffect(()=>{let l=()=>{if(!f.current||!(f.current.checkVisibility?.()??!0))return!1;let _=T5(f.current);if(_.height===0||_.width===0)u.getState().onError?.("004",$l.error004());u.setState({width:_.width||500,height:_.height||500})};if(f.current){l(),window.addEventListener("resize",l);let _=new ResizeObserver(()=>l());return _.observe(f.current),()=>{if(window.removeEventListener("resize",l),_&&f.current)_.unobserve(f.current)}}},[])}var m5={position:"absolute",width:"100%",height:"100%",top:0,left:0},FM=(f)=>({userSelectionActive:f.userSelectionActive,lib:f.lib,connectionInProgress:f.connection.inProgress});function JM({onPaneContextMenu:f,zoomOnScroll:u=!0,zoomOnPinch:l=!0,panOnScroll:_=!1,panOnScrollSpeed:y=0.5,panOnScrollMode:$=c1.Free,zoomOnDoubleClick:r=!0,panOnDrag:j=!0,defaultViewport:A,translateExtent:J,minZoom:U,maxZoom:Q,zoomActivationKeyCode:W,preventScrolling:G=!0,children:K,noWheelClassName:H,noPanClassName:O,onViewportChange:z,isControlledViewport:Z,paneClickDistance:N,selectionOnDrag:E}){let q=q0(),Y=_f.useRef(null),{userSelectionActive:w,lib:B,connectionInProgress:P}=f0(FM,H0),h=U4(W),M=_f.useRef();AM(Y);let n=_f.useCallback((S)=>{if(z?.({x:S[0],y:S[1],zoom:S[2]}),!Z)q.setState({transform:S})},[z,Z]);return _f.useEffect(()=>{if(Y.current){M.current=mN({domNode:Y.current,minZoom:U,maxZoom:Q,translateExtent:J,viewport:A,onDraggingChange:(C)=>q.setState((v)=>v.paneDragging===C?v:{paneDragging:C}),onPanZoomStart:(C,v)=>{let{onViewportChangeStart:X,onMoveStart:D}=q.getState();D?.(C,v),X?.(v)},onPanZoom:(C,v)=>{let{onViewportChange:X,onMove:D}=q.getState();D?.(C,v),X?.(v)},onPanZoomEnd:(C,v)=>{let{onViewportChangeEnd:X,onMoveEnd:D}=q.getState();D?.(C,v),X?.(v)}});let{x:S,y:T,zoom:i}=M.current.getViewport();return q.setState({panZoom:M.current,transform:[S,T,i],domNode:Y.current.closest(".react-flow")}),()=>{M.current?.destroy()}}},[]),_f.useEffect(()=>{M.current?.update({onPaneContextMenu:f,zoomOnScroll:u,zoomOnPinch:l,panOnScroll:_,panOnScrollSpeed:y,panOnScrollMode:$,zoomOnDoubleClick:r,panOnDrag:j,zoomActivationKeyPressed:h,preventScrolling:G,noPanClassName:O,userSelectionActive:w,noWheelClassName:H,lib:B,onTransformChange:n,connectionInProgress:P,selectionOnDrag:E,paneClickDistance:N})},[f,u,l,_,y,$,r,j,h,G,O,w,H,B,n,P,E,N]),lf.jsx("div",{className:"react-flow__renderer",ref:Y,style:m5,children:K})}var UM=(f)=>({userSelectionActive:f.userSelectionActive,userSelectionRect:f.userSelectionRect});function QM(){let{userSelectionActive:f,userSelectionRect:u}=f0(UM,H0);if(!(f&&u))return null;return lf.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 mF=(f,u)=>{return(l)=>{if(l.target!==u.current)return;f?.(l)}},WM=(f)=>({userSelectionActive:f.userSelectionActive,elementsSelectable:f.elementsSelectable,connectionInProgress:f.connection.inProgress,dragging:f.paneDragging});function zM({isSelecting:f,selectionKeyPressed:u,selectionMode:l=Xy.Full,panOnDrag:_,paneClickDistance:y,selectionOnDrag:$,onSelectionStart:r,onSelectionEnd:j,onPaneClick:A,onPaneContextMenu:J,onPaneScroll:U,onPaneMouseEnter:Q,onPaneMouseMove:W,onPaneMouseLeave:G,children:K}){let H=q0(),{userSelectionActive:O,elementsSelectable:z,dragging:Z,connectionInProgress:N}=f0(WM,H0),E=z&&(f||O),q=_f.useRef(null),Y=_f.useRef(),w=_f.useRef(new Set),B=_f.useRef(new Set),P=_f.useRef(!1),h=(X)=>{if(P.current||N){P.current=!1;return}A?.(X),H.getState().resetSelectedElements(),H.setState({nodesSelectionActive:!1})},M=(X)=>{if(Array.isArray(_)&&_?.includes(2)){X.preventDefault();return}J?.(X)},n=U?(X)=>U(X):void 0,S=(X)=>{if(P.current)X.stopPropagation(),P.current=!1},T=(X)=>{let{domNode:D}=H.getState();if(Y.current=D?.getBoundingClientRect(),!Y.current)return;let p=X.target===q.current;if(!p&&!!X.target.closest(".nokey")||!f||!($&&p||u)||X.button!==0||!X.isPrimary)return;X.target?.setPointerCapture?.(X.pointerId),P.current=!1;let{x:d,y:a}=Ol(X.nativeEvent,Y.current);if(H.setState({userSelectionRect:{width:0,height:0,startX:d,startY:a,x:d,y:a}}),!p)X.stopPropagation(),X.preventDefault()},i=(X)=>{let{userSelectionRect:D,transform:p,nodeLookup:m,edgeLookup:s,connectionLookup:d,triggerNodeChanges:a,triggerEdgeChanges:I,defaultEdgeOptions:ff,resetSelectedElements:yf}=H.getState();if(!Y.current||!D)return;let{x:rf,y:Wf}=Ol(X.nativeEvent,Y.current),{startX:Ef,startY:Gf}=D;if(!P.current){let k=u?0:y;if(Math.hypot(rf-Ef,Wf-Gf)<=k)return;yf(),r?.(X)}P.current=!0;let c={startX:Ef,startY:Gf,x:rfk.id)),B.current=new Set;let Kf=ff?.selectable??!0;for(let k of w.current){let Af=d.get(k);if(!Af)continue;for(let{edgeId:Yf}of Af.values()){let Bf=s.get(Yf);if(Bf&&(Bf.selectable??Kf))B.current.add(Yf)}}if(!PF(o,w.current)){let k=o$(m,w.current,!0);a(k)}if(!PF(e,B.current)){let k=o$(s,B.current);I(k)}H.setState({userSelectionRect:c,userSelectionActive:!0,nodesSelectionActive:!1})},C=(X)=>{if(X.button!==0)return;if(X.target?.releasePointerCapture?.(X.pointerId),!O&&X.target===q.current&&H.getState().userSelectionRect)h?.(X);if(H.setState({userSelectionActive:!1,userSelectionRect:null}),P.current)j?.(X),H.setState({nodesSelectionActive:w.current.size>0})},v=_===!0||Array.isArray(_)&&_.includes(0);return lf.jsxs("div",{className:M0(["react-flow__pane",{draggable:v,dragging:Z,selection:f}]),onClick:E?void 0:mF(h,q),onContextMenu:mF(M,q),onWheel:mF(n,q),onPointerEnter:E?void 0:Q,onPointerMove:E?i:W,onPointerUp:E?C:void 0,onPointerDownCapture:E?T:void 0,onClickCapture:E?S:void 0,onPointerLeave:G,ref:q,style:m5,children:[K,lf.jsx(QM,{})]})}function tF({id:f,store:u,unselect:l=!1,nodeRef:_}){let{addSelectedNodes:y,unselectNodesAndEdges:$,multiSelectionActive:r,nodeLookup:j,onError:A}=u.getState(),J=j.get(f);if(!J){A?.("012",$l.error012(f));return}if(u.setState({nodesSelectionActive:!1}),!J.selected)y([f]);else if(l||J.selected&&r)$({nodes:[J],edges:[]}),requestAnimationFrame(()=>_?.current?.blur())}function RZ({nodeRef:f,disabled:u=!1,noDragClassName:l,handleSelector:_,nodeId:y,isSelectable:$,nodeClickDistance:r}){let j=q0(),[A,J]=_f.useState(!1),U=_f.useRef();return _f.useEffect(()=>{U.current=cN({getStoreItems:()=>j.getState(),onNodeMouseDown:(Q)=>{tF({id:Q,store:j,nodeRef:f})},onDragStart:()=>{J(!0)},onDragStop:()=>{J(!1)}})},[]),_f.useEffect(()=>{if(u||!f.current||!U.current)return;return U.current.update({noDragClassName:l,handleSelector:_,domNode:f.current,isSelectable:$,nodeId:y,nodeClickDistance:r}),()=>{U.current?.destroy()}},[l,_,u,$,f,y,r]),A}var GM=(f)=>(u)=>u.selected&&(u.draggable||f&&typeof u.draggable>"u");function xZ(){let f=q0();return _f.useCallback((l)=>{let{nodeExtent:_,snapToGrid:y,snapGrid:$,nodesDraggable:r,onError:j,updateNodePositions:A,nodeLookup:J,nodeOrigin:U}=f.getState(),Q=new Map,W=GM(r),G=y?$[0]:5,K=y?$[1]:5,H=l.direction.x*G*l.factor,O=l.direction.y*K*l.factor;for(let[,z]of J){if(!W(z))continue;let Z={x:z.internals.positionAbsolute.x+H,y:z.internals.positionAbsolute.y+O};if(y)Z=g$(Z,$);let{position:N,positionAbsolute:E}=BF({nodeId:z.id,nextPosition:Z,nodeLookup:J,nodeExtent:_,nodeOrigin:U,onError:j});z.position=N,z.internals.positionAbsolute=E,Q.set(z.id,z)}A(Q)},[])}var oF=_f.createContext(null),KM=oF.Provider;oF.Consumer;var bZ=()=>{return _f.useContext(oF)},NM=(f)=>({connectOnClick:f.connectOnClick,noPanClassName:f.noPanClassName,rfId:f.rfId}),ZM=(f,u,l)=>(_)=>{let{connectionClickStartHandle:y,connectionMode:$,connection:r}=_,{fromHandle:j,toHandle:A,isValid:J}=r,U=A?.nodeId===f&&A?.id===u&&A?.type===l;return{connectingFrom:j?.nodeId===f&&j?.id===u&&j?.type===l,connectingTo:U,clickConnecting:y?.nodeId===f&&y?.id===u&&y?.type===l,isPossibleEndHandle:$===q_.Strict?j?.type!==l:f!==j?.nodeId||u!==j?.id,connectionInProcess:!!j,clickConnectionInProcess:!!y,valid:U&&J}};function EM({type:f="source",position:u=Zf.Top,isValidConnection:l,isConnectable:_=!0,isConnectableStart:y=!0,isConnectableEnd:$=!0,id:r,onConnect:j,children:A,className:J,onMouseDown:U,onTouchStart:Q,...W},G){let K=r||null,H=f==="target",O=q0(),z=bZ(),{connectOnClick:Z,noPanClassName:N,rfId:E}=f0(NM,H0),{connectingFrom:q,connectingTo:Y,clickConnecting:w,isPossibleEndHandle:B,connectionInProcess:P,clickConnectionInProcess:h,valid:M}=f0(ZM(z,K,f),H0);if(!z)O.getState().onError?.("010",$l.error010());let n=(i)=>{let{defaultEdgeOptions:C,onConnect:v,hasDefaultEdges:X}=O.getState(),D={...C,...i};if(X){let{edges:p,setEdges:m}=O.getState();m(cF(D,p))}v?.(D),j?.(D)},S=(i)=>{if(!z)return;let C=CF(i.nativeEvent);if(y&&(C&&i.button===0||!C)){let v=O.getState();c5.onPointerDown(i.nativeEvent,{handleDomNode:i.currentTarget,autoPanOnConnect:v.autoPanOnConnect,connectionMode:v.connectionMode,connectionRadius:v.connectionRadius,domNode:v.domNode,nodeLookup:v.nodeLookup,lib:v.lib,isTarget:H,handleId:K,nodeId:z,flowId:v.rfId,panBy:v.panBy,cancelConnection:v.cancelConnection,onConnectStart:v.onConnectStart,onConnectEnd:(...X)=>O.getState().onConnectEnd?.(...X),updateConnection:v.updateConnection,onConnect:n,isValidConnection:l||((...X)=>O.getState().isValidConnection?.(...X)??!0),getTransform:()=>O.getState().transform,getFromHandle:()=>O.getState().connection.fromHandle,autoPanSpeed:v.autoPanSpeed,dragThreshold:v.connectionDragThreshold})}if(C)U?.(i);else Q?.(i)},T=(i)=>{let{onClickConnectStart:C,onClickConnectEnd:v,connectionClickStartHandle:X,connectionMode:D,isValidConnection:p,lib:m,rfId:s,nodeLookup:d,connection:a}=O.getState();if(!z||!X&&!y)return;if(!X){C?.(i.nativeEvent,{nodeId:z,handleId:K,handleType:f}),O.setState({connectionClickStartHandle:{nodeId:z,type:f,id:K}});return}let I=nF(i.target),ff=l||p,{connection:yf,isValid:rf}=c5.isValid(i.nativeEvent,{handle:{nodeId:z,id:K,type:f},connectionMode:D,fromNodeId:X.nodeId,fromHandleId:X.id||null,fromType:X.type,isValidConnection:ff,flowId:s,doc:I,lib:m,nodeLookup:d});if(rf&&yf)n(yf);let Wf=structuredClone(a);delete Wf.inProgress,Wf.toPosition=Wf.toHandle?Wf.toHandle.position:null,v?.(i,Wf),O.setState({connectionClickStartHandle:null})};return lf.jsx("div",{"data-handleid":K,"data-nodeid":z,"data-handlepos":u,"data-id":`${E}-${z}-${K}-${f}`,className:M0(["react-flow__handle",`react-flow__handle-${u}`,"nodrag",N,J,{source:!H,target:H,connectable:_,connectablestart:y,connectableend:$,clickconnecting:w,connectingfrom:q,connectingto:Y,valid:M,connectionindicator:_&&(!P||B)&&(P||h?$:y)}]),onMouseDown:S,onTouchStart:S,onClick:Z?T:void 0,ref:G,...W,children:A})}var Dy=_f.memo(iZ(EM));function HM({data:f,isConnectable:u,sourcePosition:l=Zf.Bottom}){return lf.jsxs(lf.Fragment,{children:[f?.label,lf.jsx(Dy,{type:"source",position:l,isConnectable:u})]})}function OM({data:f,isConnectable:u,targetPosition:l=Zf.Top,sourcePosition:_=Zf.Bottom}){return lf.jsxs(lf.Fragment,{children:[lf.jsx(Dy,{type:"target",position:l,isConnectable:u}),f?.label,lf.jsx(Dy,{type:"source",position:_,isConnectable:u})]})}function VM(){return null}function qM({data:f,isConnectable:u,targetPosition:l=Zf.Top}){return lf.jsxs(lf.Fragment,{children:[lf.jsx(Dy,{type:"target",position:l,isConnectable:u}),f?.label]})}var h5={ArrowUp:{x:0,y:-1},ArrowDown:{x:0,y:1},ArrowLeft:{x:-1,y:0},ArrowRight:{x:1,y:0}},HZ={input:HM,default:OM,output:qM,group:VM};function LM(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 XM=(f)=>{let{width:u,height:l,x:_,y}=p$(f.nodeLookup,{filter:($)=>!!$.selected});return{width:Hl(u)?u:null,height:Hl(l)?l:null,userSelectionActive:f.userSelectionActive,transformString:`translate(${f.transform[0]}px,${f.transform[1]}px) scale(${f.transform[2]}) translate(${_}px,${y}px)`}};function BM({onSelectionContextMenu:f,noPanClassName:u,disableKeyboardA11y:l}){let _=q0(),{width:y,height:$,transformString:r,userSelectionActive:j}=f0(XM,H0),A=xZ(),J=_f.useRef(null);_f.useEffect(()=>{if(!l)J.current?.focus({preventScroll:!0})},[l]);let U=!j&&y!==null&&$!==null;if(RZ({nodeRef:J,disabled:!U}),!U)return null;let Q=f?(G)=>{let K=_.getState().nodes.filter((H)=>H.selected);f(G,K)}:void 0,W=(G)=>{if(Object.prototype.hasOwnProperty.call(h5,G.key))G.preventDefault(),A({direction:h5[G.key],factor:G.shiftKey?4:1})};return lf.jsx("div",{className:M0(["react-flow__nodesselection","react-flow__container",u]),style:{transform:r},children:lf.jsx("div",{ref:J,className:"react-flow__nodesselection-rect",onContextMenu:Q,tabIndex:l?void 0:-1,onKeyDown:l?void 0:W,style:{width:y,height:$}})})}var OZ=typeof window<"u"?window:void 0,YM=(f)=>{return{nodesSelectionActive:f.nodesSelectionActive,userSelectionActive:f.userSelectionActive}};function vZ({children:f,onPaneClick:u,onPaneMouseEnter:l,onPaneMouseMove:_,onPaneMouseLeave:y,onPaneContextMenu:$,onPaneScroll:r,paneClickDistance:j,deleteKeyCode:A,selectionKeyCode:J,selectionOnDrag:U,selectionMode:Q,onSelectionStart:W,onSelectionEnd:G,multiSelectionKeyCode:K,panActivationKeyCode:H,zoomActivationKeyCode:O,elementsSelectable:z,zoomOnScroll:Z,zoomOnPinch:N,panOnScroll:E,panOnScrollSpeed:q,panOnScrollMode:Y,zoomOnDoubleClick:w,panOnDrag:B,defaultViewport:P,translateExtent:h,minZoom:M,maxZoom:n,preventScrolling:S,onSelectionContextMenu:T,noWheelClassName:i,noPanClassName:C,disableKeyboardA11y:v,onViewportChange:X,isControlledViewport:D}){let{nodesSelectionActive:p,userSelectionActive:m}=f0(YM,H0),s=U4(J,{target:OZ}),d=U4(H,{target:OZ}),a=d||B,I=d||E,ff=U&&a!==!0,yf=s||m||ff;return jM({deleteKeyCode:A,multiSelectionKeyCode:K}),lf.jsx(JM,{onPaneContextMenu:$,elementsSelectable:z,zoomOnScroll:Z,zoomOnPinch:N,panOnScroll:I,panOnScrollSpeed:q,panOnScrollMode:Y,zoomOnDoubleClick:w,panOnDrag:!s&&a,defaultViewport:P,translateExtent:h,minZoom:M,maxZoom:n,zoomActivationKeyCode:O,preventScrolling:S,noWheelClassName:i,noPanClassName:C,onViewportChange:X,isControlledViewport:D,paneClickDistance:j,selectionOnDrag:ff,children:lf.jsxs(zM,{onSelectionStart:W,onSelectionEnd:G,onPaneClick:u,onPaneMouseEnter:l,onPaneMouseMove:_,onPaneMouseLeave:y,onPaneContextMenu:$,onPaneScroll:r,panOnDrag:a,isSelecting:!!yf,selectionMode:Q,selectionKeyPressed:s,paneClickDistance:j,selectionOnDrag:ff,children:[f,p&&lf.jsx(BM,{onSelectionContextMenu:T,noPanClassName:C,disableKeyboardA11y:v})]})})}vZ.displayName="FlowRenderer";var wM=_f.memo(vZ),DM=(f)=>(u)=>{return f?Y5(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 TM(f){return f0(_f.useCallback(DM(f),[f]),H0)}var MM=(f)=>f.updateNodeInternals;function PM(){let f=f0(MM),[u]=_f.useState(()=>{if(typeof ResizeObserver>"u")return null;return new ResizeObserver((l)=>{let _=new Map;l.forEach((y)=>{let $=y.target.getAttribute("data-id");_.set($,{id:$,nodeElement:y.target,force:!0})}),f(_)})});return _f.useEffect(()=>{return()=>{u?.disconnect()}},[u]),u}function nM({node:f,nodeType:u,hasDimensions:l,resizeObserver:_}){let y=q0(),$=_f.useRef(null),r=_f.useRef(null),j=_f.useRef(f.sourcePosition),A=_f.useRef(f.targetPosition),J=_f.useRef(u),U=l&&!!f.internals.handleBounds;return _f.useEffect(()=>{if($.current&&!f.hidden&&(!U||r.current!==$.current)){if(r.current)_?.unobserve(r.current);_?.observe($.current),r.current=$.current}},[U,f.hidden]),_f.useEffect(()=>{return()=>{if(r.current)_?.unobserve(r.current),r.current=null}},[]),_f.useEffect(()=>{if($.current){let Q=J.current!==u,W=j.current!==f.sourcePosition,G=A.current!==f.targetPosition;if(Q||W||G)J.current=u,j.current=f.sourcePosition,A.current=f.targetPosition,y.getState().updateNodeInternals(new Map([[f.id,{id:f.id,nodeElement:$.current,force:!0}]]))}},[f.id,u,f.sourcePosition,f.targetPosition]),$}function SM({id:f,onClick:u,onMouseEnter:l,onMouseMove:_,onMouseLeave:y,onContextMenu:$,onDoubleClick:r,nodesDraggable:j,elementsSelectable:A,nodesConnectable:J,nodesFocusable:U,resizeObserver:Q,noDragClassName:W,noPanClassName:G,disableKeyboardA11y:K,rfId:H,nodeTypes:O,nodeClickDistance:z,onError:Z}){let{node:N,internals:E,isParent:q}=f0((rf)=>{let Wf=rf.nodeLookup.get(f),Ef=rf.parentLookup.has(f);return{node:Wf,internals:Wf.internals,isParent:Ef}},H0),Y=N.type||"default",w=O?.[Y]||HZ[Y];if(w===void 0)Z?.("003",$l.error003(Y)),Y="default",w=O?.default||HZ.default;let B=!!(N.draggable||j&&typeof N.draggable>"u"),P=!!(N.selectable||A&&typeof N.selectable>"u"),h=!!(N.connectable||J&&typeof N.connectable>"u"),M=!!(N.focusable||U&&typeof N.focusable>"u"),n=q0(),S=TF(N),T=nM({node:N,nodeType:Y,hasDimensions:S,resizeObserver:Q}),i=RZ({nodeRef:T,disabled:N.hidden||!B,noDragClassName:W,handleSelector:N.dragHandle,nodeId:f,isSelectable:P,nodeClickDistance:z}),C=xZ();if(N.hidden)return null;let v=r1(N),X=LM(N),D=P||B||u||l||_||y,p=l?(rf)=>l(rf,{...E.userNode}):void 0,m=_?(rf)=>_(rf,{...E.userNode}):void 0,s=y?(rf)=>y(rf,{...E.userNode}):void 0,d=$?(rf)=>$(rf,{...E.userNode}):void 0,a=r?(rf)=>r(rf,{...E.userNode}):void 0,I=(rf)=>{let{selectNodesOnDrag:Wf,nodeDragThreshold:Ef}=n.getState();if(P&&(!Wf||!B||Ef>0))tF({id:f,store:n,nodeRef:T});if(u)u(rf,{...E.userNode})},ff=(rf)=>{if(SF(rf.nativeEvent)||K)return;if(EF.includes(rf.key)&&P){let Wf=rf.key==="Escape";tF({id:f,store:n,unselect:Wf,nodeRef:T})}else if(B&&N.selected&&Object.prototype.hasOwnProperty.call(h5,rf.key)){rf.preventDefault();let{ariaLabelConfig:Wf}=n.getState();n.setState({ariaLiveMessage:Wf["node.a11yDescription.ariaLiveMessage"]({direction:rf.key.replace("Arrow","").toLowerCase(),x:~~E.positionAbsolute.x,y:~~E.positionAbsolute.y})}),C({direction:h5[rf.key],factor:rf.shiftKey?4:1})}},yf=()=>{if(K||!T.current?.matches(":focus-visible"))return;let{transform:rf,width:Wf,height:Ef,autoPanOnNodeFocus:Gf,setCenter:c}=n.getState();if(!Gf)return;if(!(Y5(new Map([[f,N]]),{x:0,y:0,width:Wf,height:Ef},rf,!0).length>0))c(N.position.x+v.width/2,N.position.y+v.height/2,{zoom:rf[2]})};return lf.jsx("div",{className:M0(["react-flow__node",`react-flow__node-${Y}`,{[G]:B},N.className,{selected:N.selected,selectable:P,parent:q,draggable:B,dragging:i}]),ref:T,style:{zIndex:E.z,transform:`translate(${E.positionAbsolute.x}px,${E.positionAbsolute.y}px)`,pointerEvents:D?"all":"none",visibility:S?"visible":"hidden",...N.style,...X},"data-id":f,"data-testid":`rf__node-${f}`,onMouseEnter:p,onMouseMove:m,onMouseLeave:s,onContextMenu:d,onClick:I,onDoubleClick:a,onKeyDown:M?ff:void 0,tabIndex:M?0:void 0,onFocus:M?yf:void 0,role:N.ariaRole??(M?"group":void 0),"aria-roledescription":"node","aria-describedby":K?void 0:`${PZ}-${H}`,"aria-label":N.ariaLabel,...N.domAttributes,children:lf.jsx(KM,{value:f,children:lf.jsx(w,{id:f,data:N.data,type:Y,positionAbsoluteX:E.positionAbsolute.x,positionAbsoluteY:E.positionAbsolute.y,selected:N.selected??!1,selectable:P,draggable:B,deletable:N.deletable??!0,isConnectable:h,sourcePosition:N.sourcePosition,targetPosition:N.targetPosition,dragging:i,dragHandle:N.dragHandle,zIndex:E.z,parentId:N.parentId,...v})})})}var CM=_f.memo(SM),iM=(f)=>({nodesDraggable:f.nodesDraggable,nodesConnectable:f.nodesConnectable,nodesFocusable:f.nodesFocusable,elementsSelectable:f.elementsSelectable,onError:f.onError});function hZ(f){let{nodesDraggable:u,nodesConnectable:l,nodesFocusable:_,elementsSelectable:y,onError:$}=f0(iM,H0),r=TM(f.onlyRenderVisibleElements),j=PM();return lf.jsx("div",{className:"react-flow__nodes",style:m5,children:r.map((A)=>{return lf.jsx(CM,{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:_,elementsSelectable:y,nodeClickDistance:f.nodeClickDistance,onError:$},A)})})}hZ.displayName="NodeRenderer";var cM=_f.memo(hZ);function RM(f){return f0(_f.useCallback((l)=>{if(!f)return l.edges.map((y)=>y.id);let _=[];if(l.width&&l.height)for(let y of l.edges){let $=l.nodeLookup.get(y.source),r=l.nodeLookup.get(y.target);if($&&r&&wN({sourceNode:$,targetNode:r,width:l.width,height:l.height,transform:l.transform}))_.push(y.id)}return _},[f]),H0)}var xM=({color:f="none",strokeWidth:u=1})=>{let l={strokeWidth:u,...f&&{stroke:f}};return lf.jsx("polyline",{className:"arrow",style:l,strokeLinecap:"round",fill:"none",strokeLinejoin:"round",points:"-5,-4 0,0 -5,4"})},bM=({color:f="none",strokeWidth:u=1})=>{let l={strokeWidth:u,...f&&{stroke:f,fill:f}};return lf.jsx("polyline",{className:"arrowclosed",style:l,strokeLinecap:"round",strokeLinejoin:"round",points:"-5,-4 0,0 -5,4 -5,-4"})},VZ={[L_.Arrow]:xM,[L_.ArrowClosed]:bM};function vM(f){let u=q0();return _f.useMemo(()=>{if(!Object.prototype.hasOwnProperty.call(VZ,f))return u.getState().onError?.("009",$l.error009(f)),null;return VZ[f]},[f])}var hM=({id:f,type:u,color:l,width:_=12.5,height:y=12.5,markerUnits:$="strokeWidth",strokeWidth:r,orient:j="auto-start-reverse"})=>{let A=vM(u);if(!A)return null;return lf.jsx("marker",{className:"react-flow__arrowhead",id:f,markerWidth:`${_}`,markerHeight:`${y}`,viewBox:"-10 -10 20 20",markerUnits:$,orient:j,refX:"0",refY:"0",children:lf.jsx(A,{color:l,strokeWidth:r})})},IZ=({defaultColor:f,rfId:u})=>{let l=f0(($)=>$.edges),_=f0(($)=>$.defaultEdgeOptions),y=_f.useMemo(()=>{return TN(l,{id:u,defaultColor:f,defaultMarkerStart:_?.markerStart,defaultMarkerEnd:_?.markerEnd})},[l,_,u,f]);if(!y.length)return null;return lf.jsx("svg",{className:"react-flow__marker","aria-hidden":"true",children:lf.jsx("defs",{children:y.map(($)=>lf.jsx(hM,{id:$.id,type:$.type,color:$.color,width:$.width,height:$.height,markerUnits:$.markerUnits,strokeWidth:$.strokeWidth,orient:$.orient},$.id))})})};IZ.displayName="MarkerDefinitions";var IM=_f.memo(IZ);function pZ({x:f,y:u,label:l,labelStyle:_,labelShowBg:y=!0,labelBgStyle:$,labelBgPadding:r=[2,4],labelBgBorderRadius:j=2,children:A,className:J,...U}){let[Q,W]=_f.useState({x:1,y:0,width:0,height:0}),G=M0(["react-flow__edge-textwrapper",J]),K=_f.useRef(null);if(_f.useEffect(()=>{if(K.current){let H=K.current.getBBox();W({x:H.x,y:H.y,width:H.width,height:H.height})}},[l]),!l)return null;return lf.jsxs("g",{transform:`translate(${f-Q.width/2} ${u-Q.height/2})`,className:G,visibility:Q.width?"visible":"hidden",...U,children:[y&&lf.jsx("rect",{width:Q.width+2*r[0],x:-r[0],y:-r[1],height:Q.height+2*r[1],className:"react-flow__edge-textbg",style:$,rx:j,ry:j}),lf.jsx("text",{className:"react-flow__edge-text",y:Q.height/2,dy:"0.3em",ref:K,style:_,children:l}),A]})}pZ.displayName="EdgeText";var pM=_f.memo(pZ);function a$({path:f,labelX:u,labelY:l,label:_,labelStyle:y,labelShowBg:$,labelBgStyle:r,labelBgPadding:j,labelBgBorderRadius:A,interactionWidth:J=20,...U}){return lf.jsxs(lf.Fragment,{children:[lf.jsx("path",{...U,d:f,fill:"none",className:M0(["react-flow__edge-path",U.className])}),J?lf.jsx("path",{d:f,fill:"none",strokeOpacity:0,strokeWidth:J,className:"react-flow__edge-interaction"}):null,_&&Hl(u)&&Hl(l)?lf.jsx(pM,{x:u,y:l,label:_,labelStyle:y,labelShowBg:$,labelBgStyle:r,labelBgPadding:j,labelBgBorderRadius:A}):null]})}function qZ({pos:f,x1:u,y1:l,x2:_,y2:y}){if(f===Zf.Left||f===Zf.Right)return[0.5*(u+_),l];return[u,0.5*(l+y)]}function mZ({sourceX:f,sourceY:u,sourcePosition:l=Zf.Bottom,targetX:_,targetY:y,targetPosition:$=Zf.Top}){let[r,j]=qZ({pos:l,x1:f,y1:u,x2:_,y2:y}),[A,J]=qZ({pos:$,x1:_,y1:y,x2:f,y2:u}),[U,Q,W,G]=M5({sourceX:f,sourceY:u,targetX:_,targetY:y,sourceControlX:r,sourceControlY:j,targetControlX:A,targetControlY:J});return[`M${f},${u} C${r},${j} ${A},${J} ${_},${y}`,U,Q,W,G]}function gZ(f){return _f.memo(({id:u,sourceX:l,sourceY:_,targetX:y,targetY:$,sourcePosition:r,targetPosition:j,label:A,labelStyle:J,labelShowBg:U,labelBgStyle:Q,labelBgPadding:W,labelBgBorderRadius:G,style:K,markerEnd:H,markerStart:O,interactionWidth:z})=>{let[Z,N,E]=mZ({sourceX:l,sourceY:_,sourcePosition:r,targetX:y,targetY:$,targetPosition:j}),q=f.isInternal?void 0:u;return lf.jsx(a$,{id:q,path:Z,labelX:N,labelY:E,label:A,labelStyle:J,labelShowBg:U,labelBgStyle:Q,labelBgPadding:W,labelBgBorderRadius:G,style:K,markerEnd:H,markerStart:O,interactionWidth:z})})}var mM=gZ({isInternal:!1}),kZ=gZ({isInternal:!0});mM.displayName="SimpleBezierEdge";kZ.displayName="SimpleBezierEdgeInternal";function tZ(f){return _f.memo(({id:u,sourceX:l,sourceY:_,targetX:y,targetY:$,label:r,labelStyle:j,labelShowBg:A,labelBgStyle:J,labelBgPadding:U,labelBgBorderRadius:Q,style:W,sourcePosition:G=Zf.Bottom,targetPosition:K=Zf.Top,markerEnd:H,markerStart:O,pathOptions:z,interactionWidth:Z})=>{let[N,E,q]=J4({sourceX:l,sourceY:_,sourcePosition:G,targetX:y,targetY:$,targetPosition:K,borderRadius:z?.borderRadius,offset:z?.offset,stepPosition:z?.stepPosition}),Y=f.isInternal?void 0:u;return lf.jsx(a$,{id:Y,path:N,labelX:E,labelY:q,label:r,labelStyle:j,labelShowBg:A,labelBgStyle:J,labelBgPadding:U,labelBgBorderRadius:Q,style:W,markerEnd:H,markerStart:O,interactionWidth:Z})})}var sZ=tZ({isInternal:!1}),oZ=tZ({isInternal:!0});sZ.displayName="SmoothStepEdge";oZ.displayName="SmoothStepEdgeInternal";function aZ(f){return _f.memo(({id:u,...l})=>{let _=f.isInternal?void 0:u;return lf.jsx(sZ,{...l,id:_,pathOptions:_f.useMemo(()=>({borderRadius:0,offset:l.pathOptions?.offset}),[l.pathOptions?.offset])})})}var gM=aZ({isInternal:!1}),dZ=aZ({isInternal:!0});gM.displayName="StepEdge";dZ.displayName="StepEdgeInternal";function eZ(f){return _f.memo(({id:u,sourceX:l,sourceY:_,targetX:y,targetY:$,label:r,labelStyle:j,labelShowBg:A,labelBgStyle:J,labelBgPadding:U,labelBgBorderRadius:Q,style:W,markerEnd:G,markerStart:K,interactionWidth:H})=>{let[O,z,Z]=n5({sourceX:l,sourceY:_,targetX:y,targetY:$}),N=f.isInternal?void 0:u;return lf.jsx(a$,{id:N,path:O,labelX:z,labelY:Z,label:r,labelStyle:j,labelShowBg:A,labelBgStyle:J,labelBgPadding:U,labelBgBorderRadius:Q,style:W,markerEnd:G,markerStart:K,interactionWidth:H})})}var kM=eZ({isInternal:!1}),fE=eZ({isInternal:!0});kM.displayName="StraightEdge";fE.displayName="StraightEdgeInternal";function uE(f){return _f.memo(({id:u,sourceX:l,sourceY:_,targetX:y,targetY:$,sourcePosition:r=Zf.Bottom,targetPosition:j=Zf.Top,label:A,labelStyle:J,labelShowBg:U,labelBgStyle:Q,labelBgPadding:W,labelBgBorderRadius:G,style:K,markerEnd:H,markerStart:O,pathOptions:z,interactionWidth:Z})=>{let[N,E,q]=P5({sourceX:l,sourceY:_,sourcePosition:r,targetX:y,targetY:$,targetPosition:j,curvature:z?.curvature}),Y=f.isInternal?void 0:u;return lf.jsx(a$,{id:Y,path:N,labelX:E,labelY:q,label:A,labelStyle:J,labelShowBg:U,labelBgStyle:Q,labelBgPadding:W,labelBgBorderRadius:G,style:K,markerEnd:H,markerStart:O,interactionWidth:Z})})}var tM=uE({isInternal:!1}),lE=uE({isInternal:!0});tM.displayName="BezierEdge";lE.displayName="BezierEdgeInternal";var LZ={default:lE,straight:fE,step:dZ,smoothstep:oZ,simplebezier:kZ},XZ={sourceX:null,sourceY:null,targetX:null,targetY:null,sourcePosition:null,targetPosition:null},sM=(f,u,l)=>{if(l===Zf.Left)return f-u;if(l===Zf.Right)return f+u;return f},oM=(f,u,l)=>{if(l===Zf.Top)return f-u;if(l===Zf.Bottom)return f+u;return f},BZ="react-flow__edgeupdater";function YZ({position:f,centerX:u,centerY:l,radius:_=10,onMouseDown:y,onMouseEnter:$,onMouseOut:r,type:j}){return lf.jsx("circle",{onMouseDown:y,onMouseEnter:$,onMouseOut:r,className:M0([BZ,`${BZ}-${j}`]),cx:sM(u,_,f),cy:oM(l,_,f),r:_,stroke:"transparent",fill:"transparent"})}function aM({isReconnectable:f,reconnectRadius:u,edge:l,sourceX:_,sourceY:y,targetX:$,targetY:r,sourcePosition:j,targetPosition:A,onReconnect:J,onReconnectStart:U,onReconnectEnd:Q,setReconnecting:W,setUpdateHover:G}){let K=q0(),H=(E,q)=>{if(E.button!==0)return;let{autoPanOnConnect:Y,domNode:w,connectionMode:B,connectionRadius:P,lib:h,onConnectStart:M,cancelConnection:n,nodeLookup:S,rfId:T,panBy:i,updateConnection:C}=K.getState(),v=q.type==="target",X=(m,s)=>{W(!1),Q?.(m,l,q.type,s)},D=(m)=>J?.(l,m),p=(m,s)=>{W(!0),U?.(E,l,q.type),M?.(m,s)};c5.onPointerDown(E.nativeEvent,{autoPanOnConnect:Y,connectionMode:B,connectionRadius:P,domNode:w,handleId:q.id,nodeId:q.nodeId,nodeLookup:S,isTarget:v,edgeUpdaterType:q.type,lib:h,flowId:T,cancelConnection:n,panBy:i,isValidConnection:(...m)=>K.getState().isValidConnection?.(...m)??!0,onConnect:D,onConnectStart:p,onConnectEnd:(...m)=>K.getState().onConnectEnd?.(...m),onReconnectEnd:X,updateConnection:C,getTransform:()=>K.getState().transform,getFromHandle:()=>K.getState().connection.fromHandle,dragThreshold:K.getState().connectionDragThreshold,handleDomNode:E.currentTarget})},O=(E)=>H(E,{nodeId:l.target,id:l.targetHandle??null,type:"target"}),z=(E)=>H(E,{nodeId:l.source,id:l.sourceHandle??null,type:"source"}),Z=()=>G(!0),N=()=>G(!1);return lf.jsxs(lf.Fragment,{children:[(f===!0||f==="source")&&lf.jsx(YZ,{position:j,centerX:_,centerY:y,radius:u,onMouseDown:O,onMouseEnter:Z,onMouseOut:N,type:"source"}),(f===!0||f==="target")&&lf.jsx(YZ,{position:A,centerX:$,centerY:r,radius:u,onMouseDown:z,onMouseEnter:Z,onMouseOut:N,type:"target"})]})}function dM({id:f,edgesFocusable:u,edgesReconnectable:l,elementsSelectable:_,onClick:y,onDoubleClick:$,onContextMenu:r,onMouseEnter:j,onMouseMove:A,onMouseLeave:J,reconnectRadius:U,onReconnect:Q,onReconnectStart:W,onReconnectEnd:G,rfId:K,edgeTypes:H,noPanClassName:O,onError:z,disableKeyboardA11y:Z}){let N=f0((c)=>c.edgeLookup.get(f)),E=f0((c)=>c.defaultEdgeOptions);N=E?{...E,...N}:N;let q=N.type||"default",Y=H?.[q]||LZ[q];if(Y===void 0)z?.("011",$l.error011(q)),q="default",Y=H?.default||LZ.default;let w=!!(N.focusable||u&&typeof N.focusable>"u"),B=typeof Q<"u"&&(N.reconnectable||l&&typeof N.reconnectable>"u"),P=!!(N.selectable||_&&typeof N.selectable>"u"),h=_f.useRef(null),[M,n]=_f.useState(!1),[S,T]=_f.useState(!1),i=q0(),{zIndex:C,sourceX:v,sourceY:X,targetX:D,targetY:p,sourcePosition:m,targetPosition:s}=f0(_f.useCallback((c)=>{let o=c.nodeLookup.get(N.source),e=c.nodeLookup.get(N.target);if(!o||!e)return{zIndex:N.zIndex,...XZ};let Kf=DN({id:f,sourceNode:o,targetNode:e,sourceHandle:N.sourceHandle||null,targetHandle:N.targetHandle||null,connectionMode:c.connectionMode,onError:z});return{zIndex:YN({selected:N.selected,zIndex:N.zIndex,sourceNode:o,targetNode:e,elevateOnSelect:c.elevateEdgesOnSelect,zIndexMode:c.zIndexMode}),...Kf||XZ}},[N.source,N.target,N.sourceHandle,N.targetHandle,N.selected,N.zIndex]),H0),d=_f.useMemo(()=>N.markerStart?`url('#${S5(N.markerStart,K)}')`:void 0,[N.markerStart,K]),a=_f.useMemo(()=>N.markerEnd?`url('#${S5(N.markerEnd,K)}')`:void 0,[N.markerEnd,K]);if(N.hidden||v===null||X===null||D===null||p===null)return null;let I=(c)=>{let{addSelectedEdges:o,unselectNodesAndEdges:e,multiSelectionActive:Kf}=i.getState();if(P)if(i.setState({nodesSelectionActive:!1}),N.selected&&Kf)e({nodes:[],edges:[N]}),h.current?.blur();else o([f]);if(y)y(c,N)},ff=$?(c)=>{$(c,{...N})}:void 0,yf=r?(c)=>{r(c,{...N})}:void 0,rf=j?(c)=>{j(c,{...N})}:void 0,Wf=A?(c)=>{A(c,{...N})}:void 0,Ef=J?(c)=>{J(c,{...N})}:void 0,Gf=(c)=>{if(!Z&&EF.includes(c.key)&&P){let{unselectNodesAndEdges:o,addSelectedEdges:e}=i.getState();if(c.key==="Escape")h.current?.blur(),o({edges:[N]});else e([f])}};return lf.jsx("svg",{style:{zIndex:C},children:lf.jsxs("g",{className:M0(["react-flow__edge",`react-flow__edge-${q}`,N.className,O,{selected:N.selected,animated:N.animated,inactive:!P&&!y,updating:M,selectable:P}]),onClick:I,onDoubleClick:ff,onContextMenu:yf,onMouseEnter:rf,onMouseMove:Wf,onMouseLeave:Ef,onKeyDown:w?Gf:void 0,tabIndex:w?0:void 0,role:N.ariaRole??(w?"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":w?`${nZ}-${K}`:void 0,ref:h,...N.domAttributes,children:[!S&&lf.jsx(Y,{id:f,source:N.source,target:N.target,type:N.type,selected:N.selected,animated:N.animated,selectable:P,deletable:N.deletable??!0,label:N.label,labelStyle:N.labelStyle,labelShowBg:N.labelShowBg,labelBgStyle:N.labelBgStyle,labelBgPadding:N.labelBgPadding,labelBgBorderRadius:N.labelBgBorderRadius,sourceX:v,sourceY:X,targetX:D,targetY:p,sourcePosition:m,targetPosition:s,data:N.data,style:N.style,sourceHandleId:N.sourceHandle,targetHandleId:N.targetHandle,markerStart:d,markerEnd:a,pathOptions:"pathOptions"in N?N.pathOptions:void 0,interactionWidth:N.interactionWidth}),B&&lf.jsx(aM,{edge:N,isReconnectable:B,reconnectRadius:U,onReconnect:Q,onReconnectStart:W,onReconnectEnd:G,sourceX:v,sourceY:X,targetX:D,targetY:p,sourcePosition:m,targetPosition:s,setUpdateHover:n,setReconnecting:T})]})})}var eM=_f.memo(dM),fP=(f)=>({edgesFocusable:f.edgesFocusable,edgesReconnectable:f.edgesReconnectable,elementsSelectable:f.elementsSelectable,connectionMode:f.connectionMode,onError:f.onError});function _E({defaultMarkerColor:f,onlyRenderVisibleElements:u,rfId:l,edgeTypes:_,noPanClassName:y,onReconnect:$,onEdgeContextMenu:r,onEdgeMouseEnter:j,onEdgeMouseMove:A,onEdgeMouseLeave:J,onEdgeClick:U,reconnectRadius:Q,onEdgeDoubleClick:W,onReconnectStart:G,onReconnectEnd:K,disableKeyboardA11y:H}){let{edgesFocusable:O,edgesReconnectable:z,elementsSelectable:Z,onError:N}=f0(fP,H0),E=RM(u);return lf.jsxs("div",{className:"react-flow__edges",children:[lf.jsx(IM,{defaultColor:f,rfId:l}),E.map((q)=>{return lf.jsx(eM,{id:q,edgesFocusable:O,edgesReconnectable:z,elementsSelectable:Z,noPanClassName:y,onReconnect:$,onContextMenu:r,onMouseEnter:j,onMouseMove:A,onMouseLeave:J,onClick:U,reconnectRadius:Q,onDoubleClick:W,onReconnectStart:G,onReconnectEnd:K,rfId:l,onError:N,edgeTypes:_,disableKeyboardA11y:H},q)})]})}_E.displayName="EdgeRenderer";var uP=_f.memo(_E),lP=(f)=>`translate(${f.transform[0]}px,${f.transform[1]}px) scale(${f.transform[2]})`;function _P({children:f}){let u=f0(lP);return lf.jsx("div",{className:"react-flow__viewport xyflow__viewport react-flow__container",style:{transform:u},children:f})}function yP(f){let u=sF(),l=_f.useRef(!1);_f.useEffect(()=>{if(!l.current&&u.viewportInitialized&&f)setTimeout(()=>f(u),1),l.current=!0},[f,u.viewportInitialized])}var $P=(f)=>f.panZoom?.syncViewport;function rP(f){let u=f0($P),l=q0();return _f.useEffect(()=>{if(f)u?.(f),l.setState({transform:[f.x,f.y,f.zoom]})},[f,u]),null}function wZ(f){return f.connection.inProgress?{...f.connection,to:k$(f.connection.to,f.transform)}:{...f.connection}}function jP(f){if(f)return(l)=>{let _=wZ(l);return f(_)};return wZ}function AP(f){let u=jP(f);return f0(u,H0)}var FP=(f)=>({nodesConnectable:f.nodesConnectable,isValid:f.connection.isValid,inProgress:f.connection.inProgress,width:f.width,height:f.height});function JP({containerStyle:f,style:u,type:l,component:_}){let{nodesConnectable:y,width:$,height:r,isValid:j,inProgress:A}=f0(FP,H0);if(!($&&y&&A))return null;return lf.jsx("svg",{style:f,width:$,height:r,className:"react-flow__connectionline react-flow__container",children:lf.jsx("g",{className:M0(["react-flow__connection",VF(j)]),children:lf.jsx(yE,{style:u,type:l,CustomComponent:_,isValid:j})})})}var yE=({style:f,type:u=$1.Bezier,CustomComponent:l,isValid:_})=>{let{inProgress:y,from:$,fromNode:r,fromHandle:j,fromPosition:A,to:J,toNode:U,toHandle:Q,toPosition:W,pointer:G}=AP();if(!y)return;if(l)return lf.jsx(l,{connectionLineType:u,connectionLineStyle:f,fromNode:r,fromHandle:j,fromX:$.x,fromY:$.y,toX:J.x,toY:J.y,fromPosition:A,toPosition:W,connectionStatus:VF(_),toNode:U,toHandle:Q,pointer:G});let K="",H={sourceX:$.x,sourceY:$.y,sourcePosition:A,targetX:J.x,targetY:J.y,targetPosition:W};switch(u){case $1.Bezier:[K]=P5(H);break;case $1.SimpleBezier:[K]=mZ(H);break;case $1.Step:[K]=J4({...H,borderRadius:0});break;case $1.SmoothStep:[K]=J4(H);break;default:[K]=n5(H)}return lf.jsx("path",{d:K,fill:"none",className:"react-flow__connection-path",style:f})};yE.displayName="ConnectionLine";var UP={};function DZ(f=UP){let u=_f.useRef(f),l=q0();_f.useEffect(()=>{},[f])}function QP(){let f=q0(),u=_f.useRef(!1);_f.useEffect(()=>{},[])}function $E({nodeTypes:f,edgeTypes:u,onInit:l,onNodeClick:_,onEdgeClick:y,onNodeDoubleClick:$,onEdgeDoubleClick:r,onNodeMouseEnter:j,onNodeMouseMove:A,onNodeMouseLeave:J,onNodeContextMenu:U,onSelectionContextMenu:Q,onSelectionStart:W,onSelectionEnd:G,connectionLineType:K,connectionLineStyle:H,connectionLineComponent:O,connectionLineContainerStyle:z,selectionKeyCode:Z,selectionOnDrag:N,selectionMode:E,multiSelectionKeyCode:q,panActivationKeyCode:Y,zoomActivationKeyCode:w,deleteKeyCode:B,onlyRenderVisibleElements:P,elementsSelectable:h,defaultViewport:M,translateExtent:n,minZoom:S,maxZoom:T,preventScrolling:i,defaultMarkerColor:C,zoomOnScroll:v,zoomOnPinch:X,panOnScroll:D,panOnScrollSpeed:p,panOnScrollMode:m,zoomOnDoubleClick:s,panOnDrag:d,onPaneClick:a,onPaneMouseEnter:I,onPaneMouseMove:ff,onPaneMouseLeave:yf,onPaneScroll:rf,onPaneContextMenu:Wf,paneClickDistance:Ef,nodeClickDistance:Gf,onEdgeContextMenu:c,onEdgeMouseEnter:o,onEdgeMouseMove:e,onEdgeMouseLeave:Kf,reconnectRadius:k,onReconnect:Af,onReconnectStart:Yf,onReconnectEnd:Bf,noDragClassName:df,noWheelClassName:_0,noPanClassName:y0,disableKeyboardA11y:N0,nodeExtent:a0,rfId:pu,viewport:mu,onViewportChange:C0}){return DZ(f),DZ(u),QP(),yP(l),rP(mu),lf.jsx(wM,{onPaneClick:a,onPaneMouseEnter:I,onPaneMouseMove:ff,onPaneMouseLeave:yf,onPaneContextMenu:Wf,onPaneScroll:rf,paneClickDistance:Ef,deleteKeyCode:B,selectionKeyCode:Z,selectionOnDrag:N,selectionMode:E,onSelectionStart:W,onSelectionEnd:G,multiSelectionKeyCode:q,panActivationKeyCode:Y,zoomActivationKeyCode:w,elementsSelectable:h,zoomOnScroll:v,zoomOnPinch:X,zoomOnDoubleClick:s,panOnScroll:D,panOnScrollSpeed:p,panOnScrollMode:m,panOnDrag:d,defaultViewport:M,translateExtent:n,minZoom:S,maxZoom:T,onSelectionContextMenu:Q,preventScrolling:i,noDragClassName:df,noWheelClassName:_0,noPanClassName:y0,disableKeyboardA11y:N0,onViewportChange:C0,isControlledViewport:!!mu,children:lf.jsxs(_P,{children:[lf.jsx(uP,{edgeTypes:u,onEdgeClick:y,onEdgeDoubleClick:r,onReconnect:Af,onReconnectStart:Yf,onReconnectEnd:Bf,onlyRenderVisibleElements:P,onEdgeContextMenu:c,onEdgeMouseEnter:o,onEdgeMouseMove:e,onEdgeMouseLeave:Kf,reconnectRadius:k,defaultMarkerColor:C,noPanClassName:y0,disableKeyboardA11y:N0,rfId:pu}),lf.jsx(JP,{style:H,type:K,component:O,containerStyle:z}),lf.jsx("div",{className:"react-flow__edgelabel-renderer"}),lf.jsx(cM,{nodeTypes:f,onNodeClick:_,onNodeDoubleClick:$,onNodeMouseEnter:j,onNodeMouseMove:A,onNodeMouseLeave:J,onNodeContextMenu:U,nodeClickDistance:Gf,onlyRenderVisibleElements:P,noPanClassName:y0,noDragClassName:df,disableKeyboardA11y:N0,nodeExtent:a0,rfId:pu}),lf.jsx("div",{className:"react-flow__viewport-portal"})]})})}$E.displayName="GraphView";var WP=_f.memo($E),TZ=({nodes:f,edges:u,defaultNodes:l,defaultEdges:_,width:y,height:$,fitView:r,fitViewOptions:j,minZoom:A=0.5,maxZoom:J=2,nodeOrigin:U,nodeExtent:Q,zIndexMode:W="basic"}={})=>{let G=new Map,K=new Map,H=new Map,O=new Map,z=_??u??[],Z=l??f??[],N=U??[0,0],E=Q??I$;hF(H,O,z);let{nodesInitialized:q}=C5(Z,G,K,{nodeOrigin:N,nodeExtent:E,zIndexMode:W}),Y=[0,0,1];if(r&&y&&$){let w=p$(G,{filter:(M)=>!!((M.width||M.initialWidth)&&(M.height||M.initialHeight))}),{x:B,y:P,zoom:h}=F4(w,y,$,A,J,j?.padding??0.1);Y=[B,P,h]}return{rfId:"1",width:y??0,height:$??0,transform:Y,nodes:Z,nodesInitialized:q,nodeLookup:G,parentLookup:K,edges:z,edgeLookup:O,connectionLookup:H,onNodesChange:null,onEdgesChange:null,hasDefaultNodes:l!==void 0,hasDefaultEdges:_!==void 0,panZoom:null,minZoom:A,maxZoom:J,translateExtent:I$,nodeExtent:E,nodesSelectionActive:!1,userSelectionActive:!1,userSelectionRect:null,connectionMode:q_.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:r??!1,fitViewOptions:j,fitViewResolver:null,connection:{...OF},connectionClickStartHandle:null,connectOnClick:!0,ariaLiveMessage:"",autoPanOnConnect:!0,autoPanOnNodeDrag:!0,autoPanOnNodeFocus:!0,autoPanSpeed:15,connectionRadius:20,onError:DF,isValidConnection:void 0,onSelectionChangeHandlers:[],lib:"react",debug:!1,ariaLabelConfig:HF,zIndexMode:W,onNodesChangeMiddlewareMap:new Map,onEdgesChangeMiddlewareMap:new Map}},zP=({nodes:f,edges:u,defaultNodes:l,defaultEdges:_,width:y,height:$,fitView:r,fitViewOptions:j,minZoom:A,maxZoom:J,nodeOrigin:U,nodeExtent:Q,zIndexMode:W})=>jZ((G,K)=>{async function H(){let{nodeLookup:O,panZoom:z,fitViewOptions:Z,fitViewResolver:N,width:E,height:q,minZoom:Y,maxZoom:w}=K();if(!z)return;await ON({nodes:O,width:E,height:q,panZoom:z,minZoom:Y,maxZoom:w},Z),N?.resolve(!0),G({fitViewResolver:null})}return{...TZ({nodes:f,edges:u,width:y,height:$,fitView:r,fitViewOptions:j,minZoom:A,maxZoom:J,nodeOrigin:U,nodeExtent:Q,defaultNodes:l,defaultEdges:_,zIndexMode:W}),setNodes:(O)=>{let{nodeLookup:z,parentLookup:Z,nodeOrigin:N,elevateNodesOnSelect:E,fitViewQueued:q,zIndexMode:Y,nodesSelectionActive:w}=K(),{nodesInitialized:B,hasSelectedNodes:P}=C5(O,z,Z,{nodeOrigin:N,nodeExtent:Q,elevateNodesOnSelect:E,checkEquality:!0,zIndexMode:Y}),h=w&&P;if(q&&B)H(),G({nodes:O,nodesInitialized:B,fitViewQueued:!1,fitViewOptions:void 0,nodesSelectionActive:h});else G({nodes:O,nodesInitialized:B,nodesSelectionActive:h})},setEdges:(O)=>{let{connectionLookup:z,edgeLookup:Z}=K();hF(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:E,nodeOrigin:q,nodeExtent:Y,debug:w,fitViewQueued:B,zIndexMode:P}=K(),{changes:h,updatedInternals:M}=SN(O,Z,N,E,q,Y,P);if(!M)return;if(PN(Z,N,{nodeOrigin:q,nodeExtent:Y,zIndexMode:P}),B)H(),G({fitViewQueued:!1,fitViewOptions:void 0});else G({});if(h?.length>0){if(w)console.log("React Flow: trigger node changes",h);z?.(h)}},updateNodePositions:(O,z=!1)=>{let Z=[],N=[],{nodeLookup:E,triggerNodeChanges:q,connection:Y,updateConnection:w,onNodesChangeMiddlewareMap:B}=K();for(let[P,h]of O){let M=E.get(P),n=!!(M?.expandParent&&M?.parentId&&h?.position),S={id:P,type:"position",position:n?{x:Math.max(0,h.position.x),y:Math.max(0,h.position.y)}:h.position,dragging:z};if(M&&Y.inProgress&&Y.fromNode.id===M.id){let T=X_(M,Y.fromHandle,Zf.Left,!0);w({...Y,from:T})}if(n&&M.parentId)Z.push({id:P,parentId:M.parentId,rect:{...h.internals.positionAbsolute,width:h.measured.width??0,height:h.measured.height??0}});N.push(S)}if(Z.length>0){let{parentLookup:P,nodeOrigin:h}=K(),M=i5(Z,E,P,h);N.push(...M)}for(let P of B.values())N=P(N);q(N)},triggerNodeChanges:(O)=>{let{onNodesChange:z,setNodes:Z,nodes:N,hasDefaultNodes:E,debug:q}=K();if(O?.length){if(E){let Y=eT(O,N);Z(Y)}if(q)console.log("React Flow: trigger node changes",O);z?.(O)}},triggerEdgeChanges:(O)=>{let{onEdgesChange:z,setEdges:Z,edges:N,hasDefaultEdges:E,debug:q}=K();if(O?.length){if(E){let Y=fM(O,N);Z(Y)}if(q)console.log("React Flow: trigger edge changes",O);z?.(O)}},addSelectedNodes:(O)=>{let{multiSelectionActive:z,edgeLookup:Z,nodeLookup:N,triggerNodeChanges:E,triggerEdgeChanges:q}=K();if(z){let Y=O.map((w)=>wy(w,!0));E(Y);return}E(o$(N,new Set([...O]),!0)),q(o$(Z))},addSelectedEdges:(O)=>{let{multiSelectionActive:z,edgeLookup:Z,nodeLookup:N,triggerNodeChanges:E,triggerEdgeChanges:q}=K();if(z){let Y=O.map((w)=>wy(w,!0));q(Y);return}q(o$(Z,new Set([...O]))),E(o$(N,new Set,!0))},unselectNodesAndEdges:({nodes:O,edges:z}={})=>{let{edges:Z,nodes:N,nodeLookup:E,triggerNodeChanges:q,triggerEdgeChanges:Y}=K(),w=O?O:N,B=z?z:Z,P=[];for(let M of w){if(!M.selected)continue;let n=E.get(M.id);if(n)n.selected=!1;P.push(wy(M.id,!1))}let h=[];for(let M of B){if(!M.selected)continue;h.push(wy(M.id,!1))}q(P),Y(h)},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:E}=K();if(!E)return;let q=z.reduce((w,B)=>B.selected?[...w,wy(B.id,!1)]:w,[]),Y=O.reduce((w,B)=>B.selected?[...w,wy(B.id,!1)]:w,[]);Z(q),N(Y)},setNodeExtent:(O)=>{let{nodes:z,nodeLookup:Z,parentLookup:N,nodeOrigin:E,elevateNodesOnSelect:q,nodeExtent:Y,zIndexMode:w}=K();if(O[0][0]===Y[0][0]&&O[0][1]===Y[0][1]&&O[1][0]===Y[1][0]&&O[1][1]===Y[1][1])return;C5(z,Z,N,{nodeOrigin:E,nodeExtent:O,elevateNodesOnSelect:q,checkEquality:!1,zIndexMode:w}),G({nodeExtent:O})},panBy:(O)=>{let{transform:z,width:Z,height:N,panZoom:E,translateExtent:q}=K();return CN({delta:O,panZoom:E,transform:z,translateExtent:q,width:Z,height:N})},setCenter:async(O,z,Z)=>{let{width:N,height:E,maxZoom:q,panZoom:Y}=K();if(!Y)return Promise.resolve(!1);let w=typeof Z?.zoom<"u"?Z.zoom:q;return await Y.setViewport({x:N/2-O*w,y:E/2-z*w,zoom:w},{duration:Z?.duration,ease:Z?.ease,interpolate:Z?.interpolate}),Promise.resolve(!0)},cancelConnection:()=>{G({connection:{...OF}})},updateConnection:(O)=>{G({connection:O})},reset:()=>G({...TZ()})}},Object.is);function GP({initialNodes:f,initialEdges:u,defaultNodes:l,defaultEdges:_,initialWidth:y,initialHeight:$,initialMinZoom:r,initialMaxZoom:j,initialFitViewOptions:A,fitView:J,nodeOrigin:U,nodeExtent:Q,zIndexMode:W,children:G}){let[K]=_f.useState(()=>zP({nodes:f,edges:u,defaultNodes:l,defaultEdges:_,width:y,height:$,fitView:J,minZoom:r,maxZoom:j,fitViewOptions:A,nodeOrigin:U,nodeExtent:Q,zIndexMode:W}));return lf.jsx(nT,{value:K,children:lf.jsx(_M,{children:G})})}function KP({children:f,nodes:u,edges:l,defaultNodes:_,defaultEdges:y,width:$,height:r,fitView:j,fitViewOptions:A,minZoom:J,maxZoom:U,nodeOrigin:Q,nodeExtent:W,zIndexMode:G}){if(_f.useContext(I5))return lf.jsx(lf.Fragment,{children:f});return lf.jsx(GP,{initialNodes:u,initialEdges:l,defaultNodes:_,defaultEdges:y,initialWidth:$,initialHeight:r,fitView:j,initialFitViewOptions:A,initialMinZoom:J,initialMaxZoom:U,nodeOrigin:Q,nodeExtent:W,zIndexMode:G,children:f})}var NP={width:"100%",height:"100%",overflow:"hidden",position:"relative",zIndex:0};function ZP({nodes:f,edges:u,defaultNodes:l,defaultEdges:_,className:y,nodeTypes:$,edgeTypes:r,onNodeClick:j,onEdgeClick:A,onInit:J,onMove:U,onMoveStart:Q,onMoveEnd:W,onConnect:G,onConnectStart:K,onConnectEnd:H,onClickConnectStart:O,onClickConnectEnd:z,onNodeMouseEnter:Z,onNodeMouseMove:N,onNodeMouseLeave:E,onNodeContextMenu:q,onNodeDoubleClick:Y,onNodeDragStart:w,onNodeDrag:B,onNodeDragStop:P,onNodesDelete:h,onEdgesDelete:M,onDelete:n,onSelectionChange:S,onSelectionDragStart:T,onSelectionDrag:i,onSelectionDragStop:C,onSelectionContextMenu:v,onSelectionStart:X,onSelectionEnd:D,onBeforeDelete:p,connectionMode:m,connectionLineType:s=$1.Bezier,connectionLineStyle:d,connectionLineComponent:a,connectionLineContainerStyle:I,deleteKeyCode:ff="Backspace",selectionKeyCode:yf="Shift",selectionOnDrag:rf=!1,selectionMode:Wf=Xy.Full,panActivationKeyCode:Ef="Space",multiSelectionKeyCode:Gf=t$()?"Meta":"Control",zoomActivationKeyCode:c=t$()?"Meta":"Control",snapToGrid:o,snapGrid:e,onlyRenderVisibleElements:Kf=!1,selectNodesOnDrag:k,nodesDraggable:Af,autoPanOnNodeFocus:Yf,nodesConnectable:Bf,nodesFocusable:df,nodeOrigin:_0=SZ,edgesFocusable:y0,edgesReconnectable:N0,elementsSelectable:a0=!0,defaultViewport:pu=gT,minZoom:mu=0.5,maxZoom:C0=2,translateExtent:Q1=I$,preventScrolling:ru=!0,nodeExtent:Uf,defaultMarkerColor:nf="#b1b1b7",zoomOnScroll:Nu=!0,zoomOnPinch:Cf=!0,panOnScroll:d0=!1,panOnScrollSpeed:e0=0.5,panOnScrollMode:Zu=c1.Free,zoomOnDoubleClick:i0=!0,panOnDrag:Du=!0,onPaneClick:W1,onPaneMouseEnter:pl,onPaneMouseMove:R_,onPaneMouseLeave:x_,onPaneScroll:v0,onPaneContextMenu:uf,paneClickDistance:wf=1,nodeClickDistance:Hf=0,children:gf,onReconnect:pf,onReconnectStart:Q0,onReconnectEnd:u0,onEdgeContextMenu:fu,onEdgeDoubleClick:Bl,onEdgeMouseEnter:w4,onEdgeMouseMove:$3,onEdgeMouseLeave:D4,reconnectRadius:Tu=10,onNodesChange:ml,onEdgesChange:T4,noDragClassName:M4="nodrag",noWheelClassName:r3="nowheel",noPanClassName:j3="nopan",fitView:Yl,fitViewOptions:A3,connectOnClick:h0,attributionPosition:Nr,proOptions:P4,defaultEdgeOptions:z1,elevateNodesOnSelect:ny=!0,elevateEdgesOnSelect:b_=!1,disableKeyboardA11y:Sy=!1,autoPanOnConnect:cJ,autoPanOnNodeDrag:Cy,autoPanSpeed:RJ,connectionRadius:n4,isValidConnection:Zr,onError:Er,style:Hr,id:G1,nodeDragThreshold:iy,connectionDragThreshold:v_,viewport:Mu,onViewportChange:S4,width:F3,height:Fl,colorMode:C4="light",debug:Or,onScroll:i4,ariaLabelConfig:c4,zIndexMode:K1="basic",...R4},J3){let U3=G1||"1",Vr=oT(C4),Q3=_f.useCallback((Jl)=>{Jl.currentTarget.scrollTo({top:0,left:0,behavior:"instant"}),i4?.(Jl)},[i4]);return lf.jsx("div",{"data-testid":"rf__wrapper",...R4,onScroll:Q3,style:{...Hr,...NP},ref:J3,className:M0(["react-flow",y,Vr]),id:G1,role:"application",children:lf.jsxs(KP,{nodes:f,edges:u,width:F3,height:Fl,fitView:Yl,fitViewOptions:A3,minZoom:mu,maxZoom:C0,nodeOrigin:_0,nodeExtent:Uf,zIndexMode:K1,children:[lf.jsx(sT,{nodes:f,edges:u,defaultNodes:l,defaultEdges:_,onConnect:G,onConnectStart:K,onConnectEnd:H,onClickConnectStart:O,onClickConnectEnd:z,nodesDraggable:Af,autoPanOnNodeFocus:Yf,nodesConnectable:Bf,nodesFocusable:df,edgesFocusable:y0,edgesReconnectable:N0,elementsSelectable:a0,elevateNodesOnSelect:ny,elevateEdgesOnSelect:b_,minZoom:mu,maxZoom:C0,nodeExtent:Uf,onNodesChange:ml,onEdgesChange:T4,snapToGrid:o,snapGrid:e,connectionMode:m,translateExtent:Q1,connectOnClick:h0,defaultEdgeOptions:z1,fitView:Yl,fitViewOptions:A3,onNodesDelete:h,onEdgesDelete:M,onDelete:n,onNodeDragStart:w,onNodeDrag:B,onNodeDragStop:P,onSelectionDrag:i,onSelectionDragStart:T,onSelectionDragStop:C,onMove:U,onMoveStart:Q,onMoveEnd:W,noPanClassName:j3,nodeOrigin:_0,rfId:U3,autoPanOnConnect:cJ,autoPanOnNodeDrag:Cy,autoPanSpeed:RJ,onError:Er,connectionRadius:n4,isValidConnection:Zr,selectNodesOnDrag:k,nodeDragThreshold:iy,connectionDragThreshold:v_,onBeforeDelete:p,debug:Or,ariaLabelConfig:c4,zIndexMode:K1}),lf.jsx(WP,{onInit:J,onNodeClick:j,onEdgeClick:A,onNodeMouseEnter:Z,onNodeMouseMove:N,onNodeMouseLeave:E,onNodeContextMenu:q,onNodeDoubleClick:Y,nodeTypes:$,edgeTypes:r,connectionLineType:s,connectionLineStyle:d,connectionLineComponent:a,connectionLineContainerStyle:I,selectionKeyCode:yf,selectionOnDrag:rf,selectionMode:Wf,deleteKeyCode:ff,multiSelectionKeyCode:Gf,panActivationKeyCode:Ef,zoomActivationKeyCode:c,onlyRenderVisibleElements:Kf,defaultViewport:pu,translateExtent:Q1,minZoom:mu,maxZoom:C0,preventScrolling:ru,zoomOnScroll:Nu,zoomOnPinch:Cf,zoomOnDoubleClick:i0,panOnScroll:d0,panOnScrollSpeed:e0,panOnScrollMode:Zu,panOnDrag:Du,onPaneClick:W1,onPaneMouseEnter:pl,onPaneMouseMove:R_,onPaneMouseLeave:x_,onPaneScroll:v0,onPaneContextMenu:uf,paneClickDistance:wf,nodeClickDistance:Hf,onSelectionContextMenu:v,onSelectionStart:X,onSelectionEnd:D,onReconnect:pf,onReconnectStart:Q0,onReconnectEnd:u0,onEdgeContextMenu:fu,onEdgeDoubleClick:Bl,onEdgeMouseEnter:w4,onEdgeMouseMove:$3,onEdgeMouseLeave:D4,reconnectRadius:Tu,defaultMarkerColor:nf,noDragClassName:M4,noWheelClassName:r3,noPanClassName:j3,rfId:U3,disableKeyboardA11y:Sy,nodeExtent:Uf,viewport:Mu,onViewportChange:S4}),lf.jsx(mT,{onSelectionChange:S}),gf,lf.jsx(bT,{proOptions:P4,position:Nr}),lf.jsx(xT,{rfId:U3,disableKeyboardA11y:Sy})]})})}var rE=iZ(ZP);var Ih=$l.error014();function EP({dimensions:f,lineWidth:u,variant:l,className:_}){return lf.jsx("path",{strokeWidth:u,d:`M${f[0]/2} 0 V${f[1]} M0 ${f[1]/2} H${f[0]}`,className:M0(["react-flow__background-pattern",l,_])})}function HP({radius:f,className:u}){return lf.jsx("circle",{cx:f,cy:f,r:f,className:M0(["react-flow__background-pattern","dots",u])})}var Y_;(function(f){f.Lines="lines",f.Dots="dots",f.Cross="cross"})(Y_||(Y_={}));var OP={[Y_.Dots]:1,[Y_.Lines]:1,[Y_.Cross]:6},VP=(f)=>({transform:f.transform,patternId:`pattern-${f.rfId}`});function jE({id:f,variant:u=Y_.Dots,gap:l=20,size:_,lineWidth:y=1,offset:$=0,color:r,bgColor:j,style:A,className:J,patternClassName:U}){let Q=_f.useRef(null),{transform:W,patternId:G}=f0(VP,H0),K=_||OP[u],H=u===Y_.Dots,O=u===Y_.Cross,z=Array.isArray(l)?l:[l,l],Z=[z[0]*W[2]||1,z[1]*W[2]||1],N=K*W[2],E=Array.isArray($)?$:[$,$],q=O?[N,N]:Z,Y=[E[0]*W[2]||1+q[0]/2,E[1]*W[2]||1+q[1]/2],w=`${G}${f?f:""}`;return lf.jsxs("svg",{className:M0(["react-flow__background",J]),style:{...A,...m5,"--xy-background-color-props":j,"--xy-background-pattern-color-props":r},ref:Q,"data-testid":"rf__background",children:[lf.jsx("pattern",{id:w,x:W[0]%Z[0],y:W[1]%Z[1],width:Z[0],height:Z[1],patternUnits:"userSpaceOnUse",patternTransform:`translate(-${Y[0]},-${Y[1]})`,children:H?lf.jsx(HP,{radius:N/2,className:U}):lf.jsx(EP,{dimensions:q,lineWidth:y,variant:u,className:U})}),lf.jsx("rect",{x:"0",y:"0",width:"100%",height:"100%",fill:`url(#${w})`})]})}jE.displayName="Background";var AE=_f.memo(jE);function qP(){return lf.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32",children:lf.jsx("path",{d:"M32 18.133H18.133V32h-4.266V18.133H0v-4.266h13.867V0h4.266v13.867H32z"})})}function LP(){return lf.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 5",children:lf.jsx("path",{d:"M0 0h32v4.2H0z"})})}function XP(){return lf.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 30",children:lf.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 BP(){return lf.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 25 32",children:lf.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 YP(){return lf.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 25 32",children:lf.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 v5({children:f,className:u,...l}){return lf.jsx("button",{type:"button",className:M0(["react-flow__controls-button",u]),...l,children:f})}var wP=(f)=>({isInteractive:f.nodesDraggable||f.nodesConnectable||f.elementsSelectable,minZoomReached:f.transform[2]<=f.minZoom,maxZoomReached:f.transform[2]>=f.maxZoom,ariaLabelConfig:f.ariaLabelConfig});function FE({style:f,showZoom:u=!0,showFitView:l=!0,showInteractive:_=!0,fitViewOptions:y,onZoomIn:$,onZoomOut:r,onFitView:j,onInteractiveChange:A,className:J,children:U,position:Q="bottom-left",orientation:W="vertical","aria-label":G}){let K=q0(),{isInteractive:H,minZoomReached:O,maxZoomReached:z,ariaLabelConfig:Z}=f0(wP,H0),{zoomIn:N,zoomOut:E,fitView:q}=sF(),Y=()=>{N(),$?.()},w=()=>{E(),r?.()},B=()=>{q(y),j?.()},P=()=>{K.setState({nodesDraggable:!H,nodesConnectable:!H,elementsSelectable:!H}),A?.(!H)};return lf.jsxs(p5,{className:M0(["react-flow__controls",W==="horizontal"?"horizontal":"vertical",J]),position:Q,style:f,"data-testid":"rf__controls","aria-label":G??Z["controls.ariaLabel"],children:[u&&lf.jsxs(lf.Fragment,{children:[lf.jsx(v5,{onClick:Y,className:"react-flow__controls-zoomin",title:Z["controls.zoomIn.ariaLabel"],"aria-label":Z["controls.zoomIn.ariaLabel"],disabled:z,children:lf.jsx(qP,{})}),lf.jsx(v5,{onClick:w,className:"react-flow__controls-zoomout",title:Z["controls.zoomOut.ariaLabel"],"aria-label":Z["controls.zoomOut.ariaLabel"],disabled:O,children:lf.jsx(LP,{})})]}),l&&lf.jsx(v5,{className:"react-flow__controls-fitview",onClick:B,title:Z["controls.fitView.ariaLabel"],"aria-label":Z["controls.fitView.ariaLabel"],children:lf.jsx(XP,{})}),_&&lf.jsx(v5,{className:"react-flow__controls-interactive",onClick:P,title:Z["controls.interactive.ariaLabel"],"aria-label":Z["controls.interactive.ariaLabel"],children:H?lf.jsx(YP,{}):lf.jsx(BP,{})}),U]})}FE.displayName="Controls";var JE=_f.memo(FE);function DP({id:f,x:u,y:l,width:_,height:y,style:$,color:r,strokeColor:j,strokeWidth:A,className:J,borderRadius:U,shapeRendering:Q,selected:W,onClick:G}){let{background:K,backgroundColor:H}=$||{},O=r||K||H;return lf.jsx("rect",{className:M0(["react-flow__minimap-node",{selected:W},J]),x:u,y:l,rx:U,ry:U,width:_,height:y,style:{fill:O,stroke:j,strokeWidth:A},shapeRendering:Q,onClick:G?(z)=>G(z,f):void 0})}var TP=_f.memo(DP),MP=(f)=>f.nodes.map((u)=>u.id),gF=(f)=>f instanceof Function?f:()=>f;function PP({nodeStrokeColor:f,nodeColor:u,nodeClassName:l="",nodeBorderRadius:_=5,nodeStrokeWidth:y,nodeComponent:$=TP,onClick:r}){let j=f0(MP,H0),A=gF(u),J=gF(f),U=gF(l),Q=typeof window>"u"||!!window.chrome?"crispEdges":"geometricPrecision";return lf.jsx(lf.Fragment,{children:j.map((W)=>lf.jsx(SP,{id:W,nodeColorFunc:A,nodeStrokeColorFunc:J,nodeClassNameFunc:U,nodeBorderRadius:_,nodeStrokeWidth:y,NodeComponent:$,onClick:r,shapeRendering:Q},W))})}function nP({id:f,nodeColorFunc:u,nodeStrokeColorFunc:l,nodeClassNameFunc:_,nodeBorderRadius:y,nodeStrokeWidth:$,shapeRendering:r,NodeComponent:j,onClick:A}){let{node:J,x:U,y:Q,width:W,height:G}=f0((K)=>{let H=K.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:Z}=H.internals.positionAbsolute,{width:N,height:E}=r1(O);return{node:O,x:z,y:Z,width:N,height:E}},H0);if(!J||J.hidden||!TF(J))return null;return lf.jsx(j,{x:U,y:Q,width:W,height:G,style:J.style,selected:!!J.selected,className:_(J),color:u(J),borderRadius:y,strokeColor:l(J),strokeWidth:$,shapeRendering:r,onClick:A,id:J.id})}var SP=_f.memo(nP),CP=_f.memo(PP),iP=200,cP=150,RP=(f)=>!f.hidden,xP=(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?YF(p$(f.nodeLookup,{filter:RP}),u):u,rfId:f.rfId,panZoom:f.panZoom,translateExtent:f.translateExtent,flowWidth:f.width,flowHeight:f.height,ariaLabelConfig:f.ariaLabelConfig}},bP="react-flow__minimap-desc";function UE({style:f,className:u,nodeStrokeColor:l,nodeColor:_,nodeClassName:y="",nodeBorderRadius:$=5,nodeStrokeWidth:r,nodeComponent:j,bgColor:A,maskColor:J,maskStrokeColor:U,maskStrokeWidth:Q,position:W="bottom-right",onClick:G,onNodeClick:K,pannable:H=!1,zoomable:O=!1,ariaLabel:z,inversePan:Z,zoomStep:N=1,offsetScale:E=5}){let q=q0(),Y=_f.useRef(null),{boundingRect:w,viewBB:B,rfId:P,panZoom:h,translateExtent:M,flowWidth:n,flowHeight:S,ariaLabelConfig:T}=f0(xP,H0),i=f?.width??iP,C=f?.height??cP,v=w.width/i,X=w.height/C,D=Math.max(v,X),p=D*i,m=D*C,s=E*D,d=w.x-(p-w.width)/2-s,a=w.y-(m-w.height)/2-s,I=p+s*2,ff=m+s*2,yf=`${bP}-${P}`,rf=_f.useRef(0),Wf=_f.useRef();rf.current=D,_f.useEffect(()=>{if(Y.current&&h)return Wf.current=hN({domNode:Y.current,panZoom:h,getTransform:()=>q.getState().transform,getViewScale:()=>rf.current}),()=>{Wf.current?.destroy()}},[h]),_f.useEffect(()=>{Wf.current?.update({translateExtent:M,width:n,height:S,inversePan:Z,pannable:H,zoomStep:N,zoomable:O})},[H,O,Z,N,M,n,S]);let Ef=G?(o)=>{let[e,Kf]=Wf.current?.pointer(o)||[0,0];G(o,{x:e,y:Kf})}:void 0,Gf=K?_f.useCallback((o,e)=>{let Kf=q.getState().nodeLookup.get(e).internals.userNode;K(o,Kf)},[]):void 0,c=z??T["minimap.ariaLabel"];return lf.jsx(p5,{position:W,style:{...f,"--xy-minimap-background-color-props":typeof A==="string"?A:void 0,"--xy-minimap-mask-background-color-props":typeof J==="string"?J: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 _==="string"?_:void 0,"--xy-minimap-node-stroke-color-props":typeof l==="string"?l:void 0,"--xy-minimap-node-stroke-width-props":typeof r==="number"?r:void 0},className:M0(["react-flow__minimap",u]),"data-testid":"rf__minimap",children:lf.jsxs("svg",{width:i,height:C,viewBox:`${d} ${a} ${I} ${ff}`,className:"react-flow__minimap-svg",role:"img","aria-labelledby":yf,ref:Y,onClick:Ef,children:[c&&lf.jsx("title",{id:yf,children:c}),lf.jsx(CP,{onClick:Gf,nodeColor:_,nodeStrokeColor:l,nodeBorderRadius:$,nodeClassName:y,nodeStrokeWidth:r,nodeComponent:j}),lf.jsx("path",{className:"react-flow__minimap-mask",d:`M${d-s},${a-s}h${I+s*2}v${ff+s*2}h${-I-s*2}z + M${B.x},${B.y}h${B.width}v${B.height}h${-B.width}z`,fillRule:"evenodd",pointerEvents:"none"})]})})}UE.displayName="MiniMap";var ph=_f.memo(UE),vP=(f)=>(u)=>f?`${Math.max(1/u.transform[2],1)}`:void 0,hP={[B_.Line]:"right",[B_.Handle]:"bottom-right"};function IP({nodeId:f,position:u,variant:l=B_.Handle,className:_,style:y=void 0,children:$,color:r,minWidth:j=10,minHeight:A=10,maxWidth:J=Number.MAX_VALUE,maxHeight:U=Number.MAX_VALUE,keepAspectRatio:Q=!1,resizeDirection:W,autoScale:G=!0,shouldResize:K,onResizeStart:H,onResize:O,onResizeEnd:z}){let Z=bZ(),N=typeof f==="string"?f:Z,E=q0(),q=_f.useRef(null),Y=l===B_.Handle,w=f0(_f.useCallback(vP(Y&&G),[Y,G]),H0),B=_f.useRef(null),P=u??hP[l];_f.useEffect(()=>{if(!q.current||!N)return;if(!B.current)B.current=kN({domNode:q.current,nodeId:N,getStoreItems:()=>{let{nodeLookup:M,transform:n,snapGrid:S,snapToGrid:T,nodeOrigin:i,domNode:C}=E.getState();return{nodeLookup:M,transform:n,snapGrid:S,snapToGrid:T,nodeOrigin:i,paneDomNode:C}},onChange:(M,n)=>{let{triggerNodeChanges:S,nodeLookup:T,parentLookup:i,nodeOrigin:C}=E.getState(),v=[],X={x:M.x,y:M.y},D=T.get(N);if(D&&D.expandParent&&D.parentId){let p=D.origin??C,m=M.width??D.measured.width??0,s=M.height??D.measured.height??0,d={id:D.id,parentId:D.parentId,rect:{width:m,height:s,...MF({x:M.x??D.position.x,y:M.y??D.position.y},{width:m,height:s},D.parentId,T,p)}},a=i5([d],T,i,C);v.push(...a),X.x=M.x?Math.max(p[0]*m,M.x):void 0,X.y=M.y?Math.max(p[1]*s,M.y):void 0}if(X.x!==void 0&&X.y!==void 0){let p={id:N,type:"position",position:{...X}};v.push(p)}if(M.width!==void 0&&M.height!==void 0){let m={id:N,type:"dimensions",resizing:!0,setAttributes:!W?!0:W==="horizontal"?"width":"height",dimensions:{width:M.width,height:M.height}};v.push(m)}for(let p of n){let m={...p,type:"position"};v.push(m)}S(v)},onEnd:({width:M,height:n})=>{let S={id:N,type:"dimensions",resizing:!1,dimensions:{width:M,height:n}};E.getState().triggerNodeChanges([S])}});return B.current.update({controlPosition:P,boundaries:{minWidth:j,minHeight:A,maxWidth:J,maxHeight:U},keepAspectRatio:Q,resizeDirection:W,onResizeStart:H,onResize:O,onResizeEnd:z,shouldResize:K}),()=>{B.current?.destroy()}},[P,j,A,J,U,Q,H,O,z,K]);let h=P.split("-");return lf.jsx("div",{className:M0(["react-flow__resize-control","nodrag",...h,l,_]),ref:q,style:{...y,scale:w,...r&&{[Y?"backgroundColor":"borderColor"]:r}},children:$})}var mh=_f.memo(IP);var V=M_.default.createElement,{useEffect:F1}=M_.default,bu=M_.default.useState,T_=M_.default.useRef,Z4=[{id:"in-left",side:"left",position:Zf.Left,style:{top:"50%"}},{id:"in-top-left",side:"top",slot:"left",slotIndex:-1,position:Zf.Top,style:{left:"28%"}},{id:"in-top-mid",side:"top",slot:"mid",slotIndex:0,position:Zf.Top,style:{left:"50%"}},{id:"in-top-right",side:"top",slot:"right",slotIndex:1,position:Zf.Top,style:{left:"72%"}},{id:"in-bottom-left",side:"bottom",slot:"left",slotIndex:-1,position:Zf.Bottom,style:{left:"28%"}},{id:"in-bottom-mid",side:"bottom",slot:"mid",slotIndex:0,position:Zf.Bottom,style:{left:"50%"}},{id:"in-bottom-right",side:"bottom",slot:"right",slotIndex:1,position:Zf.Bottom,style:{left:"72%"}}],z4=[{id:"out-right",position:Zf.Right,style:{top:"50%"}}],QE=["#4eb7a8","#d7a13a","#69aee8","#e0835f","#b7d86b","#d98bd2","#5fc6bf"],d$=236,e$=88,WE=15000,pP=10,aF=96,j1=72,dF=64,zE=12;function g5(){return typeof document>"u"||document.visibilityState!=="hidden"}function GE(f,u){let l=Number.parseFloat(String(f||""));return Number.isFinite(l)?l/100:u}function mP(f,u,l){let _=String(f.side||"");if(_!=="top"&&_!=="bottom")return 0;let y=Number(f.slotIndex||0),$=_==="top"?"in-top-mid":"in-bottom-mid",r=u.get(f.id)||0,j=u.get($)||0;if(y===0)return j===0?-26:28+r*74;let A=l===0?Math.abs(y)*2:Math.sign(l)===Math.sign(y)?-3:3;if(j>0&&r===0)return-14+A;return 8+r*74+A}function k5(f){let u=f.filter(($,r)=>{let j=f[r-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}`,_=u[0];for(let $=1;$0.5||Math.abs(W.y-_.y)>0.5)l+=` L ${W.x},${W.y}`;l+=` Q ${j.x},${j.y} ${G.x},${G.y}`,_=G}let y=u[u.length-1];return`${l} L ${y.x},${y.y}`}function iE(f,u,l,_,y,$,r=""){let j=l>=f,A=Math.max(1,Math.abs(l-f)),J=Math.abs(_-u),U=Math.max(34,Math.min(118,A*0.26)),Q=Math.min(280,Math.abs($));if(j&&y===Zf.Left&&Q<4&&J<28&&A<420)return`M ${f},${u} C ${f+U},${u} ${l-U},${_} ${l},${_}`;if(j&&y===Zf.Left&&(r==="direct-forward-left"||A<=260&&J<=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},${_} ${l},${_}`}if(j){let z=f+U;if(y===Zf.Top||y===Zf.Bottom){let E=y===Zf.Top?-1:1,q=_+E*(54+Q*0.42);return k5([{x:f,y:u},{x:z,y:u},{x:z+Math.min(120,A*0.18),y:q},{x:l,y:q},{x:l,y:_+E*34},{x:l,y:_}])}let Z=l-U,N=(u+_)/2+$;return k5([{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=y===Zf.Bottom?1:y===Zf.Top?-1:$>=0?1:-1,H=Math.max(f,l)+92+Math.min(180,Q*0.52),O=K<0?Math.min(u,_)-84-Q*0.62:Math.max(u,_)+84+Q*0.62;if(y===Zf.Top||y===Zf.Bottom)return k5([{x:f,y:u},{x:f+U,y:u},{x:H,y:O},{x:l,y:O},{x:l,y:_+K*38},{x:l,y:_}]);return k5([{x:f,y:u},{x:f+U,y:u},{x:H,y:O},{x:l-U,y:O},{x:l-U,y:_},{x:l,y:_}])}function gP({data:f}){return V("div",{className:"pipeline-flow-node-body"},Z4.map((u)=>V(Dy,{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})),z4.map((u)=>V(Dy,{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 kP({id:f,sourceX:u,sourceY:l,targetX:_,targetY:y,targetPosition:$,markerEnd:r,markerStart:j,style:A,data:J}){let U=Number(J?.laneOffset||0),Q=iE(u,l,_,y,$,U,String(J?.routeMode||""));return V(a$,{id:f,path:Q,markerEnd:r,markerStart:j,style:A,interactionWidth:28})}var tP={pipelineCurve:kP},sP={pipelineNode:gP};function a5(f){if(!f)return"--";let u=new Date(f);if(Number.isNaN(u.getTime()))return"--";return W0(u)}function ql(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 eF(f){let u=Number(f);if(!Number.isFinite(u))return"--";return u.toLocaleString("zh-CN")}function KE(f){let u=Number(f);if(!Number.isFinite(u))return"--";return`${Math.round(Math.max(0,Math.min(1,u))*100)}%`}function Pf(f){return typeof f==="object"&&f!==null&&!Array.isArray(f)}function Xf(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 E4(f){return Number.isFinite(Number(f))?new Date(Number(f)).toISOString():""}function V4(...f){for(let u of f){let l=vf(u);if(l!==null)return new Date(l).toISOString()}return""}function WJ(...f){let u=f.map(vf).filter((l)=>l!==null);return u.length>0?new Date(Math.max(...u)).toISOString():""}function zJ(f){return["succeeded","failed","skipped","cancelled","canceled","completed"].includes(String(f||"").toLowerCase())}function cE(f){let u=bE(f).toLowerCase();return["running","active","in-progress","in_progress"].includes(u)}function NE(f,u="status"){return f.reduce((l,_)=>{let y=String(_?.[u]||"unknown").toLowerCase();return l[y]=(l[y]||0)+1,l},{})}function RE(f){if(!f||typeof f!=="string")return null;try{let u=JSON.parse(f);return Pf(u)?u:null}catch{return null}}function fJ(f){let u=f.map(RE).filter(($)=>Boolean($)),l=u.flatMap(($)=>[$.timestamp,$.createdAt,$.updatedAt]).filter(Boolean),_=WJ(...l),y=Array.from(new Set(u.map(($)=>String($.event||$.action||$.type||"")).filter(Boolean))).slice(0,3);return{total:f.length,parsed:u.length,lastAt:_,eventKinds:y}}function d5(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 xE(f,u=280){if(f===null||f===void 0)return"";let _=(typeof f==="string"?f:String(f)).replace(/\r\n/gu,` +`).trim();return _.length>u?`${_.slice(0,Math.max(0,u-1))}...`:_}function bE(f){if(typeof f==="string")return f;if(Pf(f))return String(f.status||f.state||f.phase||"unknown");return"unknown"}function oP(f){return f.filter((u)=>u&&u.value!==void 0&&u.value!==null&&String(u.value)!=="")}function rJ({items:f}){let u=oP(Xf(f));return V("div",{className:"pipeline-kv-grid"},u.map((l)=>V("span",{key:l.label},V("b",null,l.label),V("span",null,l.value))))}function GJ({items:f}){let u=Xf(f).map((l)=>String(l||"")).filter(Boolean);if(u.length===0)return null;return V("div",{className:"pipeline-chip-row"},u.map((l,_)=>V("span",{key:`${_}-${l}`},l)))}function jJ(f,u){let l=String(u?.procedureRunId||""),_=Xf(f?.procedureRuns);return _.find((y)=>String(Ll(y))===l)||_.at(-1)||null}function aP(f,u){let l=String(u||"");if(!l)return null;return Xf(f?.procedureRuns).find((_)=>Ll(_)===l)||null}function uJ(f){return Xf(f?.attempts).length}function ZE(f){return Xf(f?.attempts).reduce((u,l)=>u+$r(l).length,0)}function $r(f){return Xf(f?.opencodeMessages?.steps).filter(Pf)}function vE(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 dP(f,u){let l=AJ(f.map(($)=>$?.agent)).slice(0,3),_=AJ(f.map(($)=>$?.model)).slice(0,3),y=u.length<=2?u.map(($)=>`session ${$}`):[`sessions ${u.length}`,...u.slice(0,2).map(($)=>`session ${$}`)];return[...l.map(($)=>`agent ${$}`),..._.map(($)=>`model ${$}`),...y]}function G4(f,u=0){return String(f?.messageId||f?.index||"")||`step-${u}`}function eP({steps:f,sessionIds:u,sessionFacts:l,matchedStepKey:_}){let y=Xf(f),$=y.findIndex((O,z)=>G4(O,z)===_),r=$>=0?y[$]:null,j=y.flatMap((O)=>[vf(O?.createdAt),vf(O?.completedAt)]).filter((O)=>O!==null),A=j.length>0?Math.min(...j):null,J=j.length>0?Math.max(...j):null,U=A!==null&&J!==null?Math.max(0,J-A):null,Q=y.reduce((O,z)=>O+Xf(z?.parts).filter((Z)=>String(Z?.type||"").toLowerCase()==="tool").length,0),W=y.reduce((O,z)=>O+Xf(z?.parts).filter((Z)=>["text","reasoning"].includes(String(Z?.type||"").toLowerCase())).length,0),G=y.reduce((O,z)=>O+Xf(z?.parts).filter((Z)=>String(Z?.type||"").toLowerCase()==="tool"&&vE(Z)==="failed").length,0),K=[`${y.length} steps`,`${u.length} sessions`,`${W} messages`,`${Q} tools`,U!==null?`duration ${ql(U)}`:"",G>0?`${G} failed tools`:""].filter(Boolean),H=r?[`Step ${r?.index??$+1}`,String(r?.role||"role --"),r?.model?`model ${r.model}`:"",r?.finish?`finish ${r.finish}`:"",r?.durationMs!==void 0&&r?.durationMs!==null?`duration ${ql(r.durationMs)}`:""].filter(Boolean):[];return V("section",{className:"pipeline-trace-timeline","data-testid":"pipeline-step-timeline"},V("div",{className:"pipeline-trace-head"},V("div",null,V("b",null,"OpenCode Trace"),V("span",null,"Trace 使用 Code Queue 统一样式展示完整 agent loop;Pipeline 旧 step/message/tool 卡片样式已废弃。")),V("div",{className:"pipeline-trace-session-head","data-testid":"pipeline-step-timeline-session"},V("span",null,K.join(" / ")||"Trace"),l.length>0?V(GJ,{items:l}):null)),r?V("div",{className:"pipeline-trace-focus","data-testid":"pipeline-trace-matched-step"},V("span",{className:"codex-output-channel"},"Matched"),V("strong",null,`Gantt selection -> ${H.join(" / ")}`),V("time",null,`${a5(r?.createdAt)} -> ${a5(r?.completedAt)}`)):null,V(O2,{port:GG,input:y,className:"codex-transcript pipeline-trace",testId:"pipeline-opencode-step-trace",emptyText:"暂无 OpenCode Trace 输出",keepRecentToolCalls:3}))}function K4(f){return Xf(f).flatMap((u)=>{if(Pf(u))return[u];let l=RE(u);return l?[l]:[]})}function hl(f){return String(f?.event||f?.action||f?.requestedAction||f?.type||"").toLowerCase()}function Ty(f){return V4(f?.timestamp,f?.createdAt,f?.updatedAt,f?.startedAt,f?.finishedAt)}function fn(f){return vf(Ty(f))}function rr(f){return String(f?.attempt||f?.id||"")}function AJ(f){let u=new Set,l=[];for(let _ of f){let y=String(_||"");if(!y||u.has(y))continue;u.add(y),l.push(y)}return l}function EE(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 My(f){return String(f?.requestedAction||f?.action||"").toLowerCase()}function N4(f){switch(My(f)){case"guide":return"引导";case"modify":return"修改";case"approve":return"审核通过";case"restart":return"重启";case"redo":return"重做";default:return String(f?.requestedAction||f?.action||"控制")}}function HE(f){switch(hl(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`${N4(f)} 已发起`;case"control-command-applied":return`${N4(f)} 已生效`;case"control-command-ignored":return`${N4(f)} 已忽略`;default:return String(f?.event||f?.action||f?.requestedAction||"event")}}function OE(f){return xE(f?.promptPreview||f?.reasonPreview||f?.prompt||f?.reason||"",240)}function un(f){let u=String(f?.prompt||""),l=String(f?.reason||f?.restartReason||""),_=u?"":String(f?.promptPreview||""),y=l?"":String(f?.reasonPreview||"");return[u||_?{label:u?"prompt":"prompt preview",value:u||_}:null,l||y?{label:l?"reason":"reason preview",value:l||y}:null,Xf(f?.resetNodeIds).length>0?{label:"reset nodes",value:Xf(f.resetNodeIds).join(", ")}:null,Xf(f?.runningResetNodeIds).length>0?{label:"interrupted running nodes",value:Xf(f.runningResetNodeIds).join(", ")}:null,Xf(f?.interruptedProcedureRunIds).length>0?{label:"interrupted procedures",value:Xf(f.interruptedProcedureRunIds).join(", ")}:null,f?.interruptedProcedureRunId?{label:"interrupted procedure",value:String(f.interruptedProcedureRunId)}:null].filter(Boolean)}function lJ(f){let u=$r(f),l=u.map((A)=>vf(A?.createdAt)).filter((A)=>A!==null),_=u.map((A)=>vf(A?.completedAt)??vf(A?.createdAt)).filter((A)=>A!==null),y=K4(f?.controlEventRecords).map((A)=>fn(A)).filter((A)=>A!==null),$=Xf(f?.assistantOutputs).map((A)=>vf(A?.updatedAt)).filter((A)=>A!==null),r=l[0]??y[0]??$[0]??null,j=_.at(-1)??y.at(-1)??$.at(-1)??r;return{startMs:r,endMs:j}}function ln(f,u,l,_,y=""){let $=Xf(f?.procedureRuns).filter((j)=>jr(j,u)===l);if($.length===0)return null;if(y){let j=$.find((A)=>Ll(A)===y);if(j)return j}if(_===null)return $.at(-1)||null;let r=$.find((j)=>{let A=vf(t5(j,f)),J=vf(s5(j,f))??A;return A!==null&&J!==null&&_>=A-1000&&_<=J+1000});if(r)return r;return $.slice().sort((j,A)=>{let J=vf(t5(j,f))??_,U=vf(s5(j,f))??J,Q=vf(t5(A,f))??_,W=vf(s5(A,f))??Q,G=Math.min(Math.abs(J-_),Math.abs(U-_)),K=Math.min(Math.abs(Q-_),Math.abs(W-_));return G-K})[0]||null}function hE(f,u){let l=Xf(f?.attempts).filter(Pf);if(l.length===0)return null;let _=String(u?.attempt||"");if(_){let r=l.find((j)=>rr(j)===_);if(r)return r}let y=Number.isFinite(Number(u?.ms))?Number(u.ms):null;if(y===null)return l.at(-1)||null;let $=l.find((r)=>{let j=lJ(r);return Number.isFinite(j.startMs)&&Number.isFinite(j.endMs)&&y>=Number(j.startMs)-1000&&y<=Number(j.endMs)+1000});if($)return $;return l.slice().sort((r,j)=>{let A=lJ(r),J=lJ(j),U=Math.min(Math.abs(Number(A.startMs??y)-y),Math.abs(Number(A.endMs??y)-y)),Q=Math.min(Math.abs(Number(J.startMs??y)-y),Math.abs(Number(J.endMs??y)-y));return U-Q})[0]||l.at(-1)||null}function IE(f,u){let l=$r(f);if(l.length===0)return{step:null,stepIndex:-1,stepKey:""};if(u===null){let $=l[0];return{step:$,stepIndex:0,stepKey:G4($,0)}}for(let $=0;$=j-1000&&u<=A+1000)return{step:r,stepIndex:$,stepKey:G4(r,$)}}let _=l.findIndex(($)=>{let r=vf($?.createdAt)??vf($?.completedAt);return r!==null&&r>=u});if(_>=0){let $=l[_];return{step:$,stepIndex:_,stepKey:G4($,_)}}let y=Math.max(0,l.length-1);return{step:l[y],stepIndex:y,stepKey:G4(l[y],y)}}function _n(f,u){let l=String(u?.runId||f?.runId||"");if(String(u?.mode||"")==="interval"){let J=u?.interval||{},U=jJ(f,J)||J.raw||{};return{mode:"interval",runId:l,interval:J,marker:null,nodeId:String(J?.nodeId||jr(U,l)||""),procedure:U,attempt:null,matchedStep:null,matchedStepIndex:-1,matchedStepKey:""}}let _=Pf(u?.marker)?u.marker:{},y=Number.isFinite(Number(_?.ms))?Number(_.ms):null,$=String(_?.nodeId||""),r=$?ln(f,l,$,y,String(_?.procedureRunId||"")):null,j=r?hE(r,_):null,A=j?IE(j,y):{step:null,stepIndex:-1,stepKey:""};return{mode:"event",runId:l,interval:null,marker:_,nodeId:$,procedure:r,attempt:j,matchedStep:A.step,matchedStepIndex:A.stepIndex,matchedStepKey:A.stepKey}}function yn({procedure:f,matchedStepKey:u="",matchedAttemptId:l=""}){let _=Xf(f?.attempts);if(_.length===0)return V(jl,{title:"暂无 attempt 详情",text:"当前 procedure 还没有可展示的 attempt / OpenCode Trace;若刚点击甘特线,请等待 node 详情抓取完成。"});return _.map((y,$)=>{let r=y?.opencodeMessages||{},j=$r(y),A=Xf(r.sessionIds).map((W)=>String(W)).filter(Boolean),J=dP(j,A),U=rr(y)||`attempt-${$+1}`,Q=j.reduce((W,G)=>W+Xf(G?.parts).filter((K)=>String(K?.type||"").toLowerCase()==="tool"&&vE(K)==="failed").length,0);return V("article",{key:U,className:`pipeline-attempt-card ${l===U?"matched":""}`},V("div",{className:"pipeline-attempt-head"},V("div",null,V("strong",null,U),V("span",null,r.source||"opencode")),V("div",{className:"pipeline-attempt-badges"},V("span",null,`${j.length} steps`),V("span",null,`${r.toolCallCount??"--"} tools`),Q>0?V("span",{className:"danger"},`${Q} failed`):null)),V(rJ,{items:[{label:"messages",value:r.messageCount??"--"},{label:"steps",value:r.stepCount??j.length},{label:"tools",value:r.toolCallCount??"--"},{label:"updated",value:Nf(r.updatedAt)},{label:"sessions",value:A.join(", ")||"--"}]}),j.length===0?V("p",{className:"muted paragraph"},"当前 attempt 尚未返回 OpenCode Trace;请确认 D601 pipeline-control 已重建并重新抓取。"):V(eP,{steps:j,sessionIds:A,sessionFacts:J,matchedStepKey:u}))})}function _J(f,u){return`${f}::${u}`}function e5(f,u,l){if(!Pf(f))return null;return String(f.runId||"")===u&&String(f.nodeId||"")===l?f:null}function $n(f,u){let l=Pf(f)?f:{};if(!Pf(u))return l;let _=Xf(u.attempts),y=Xf(l.attempts);return{...l,...u,attempts:_.length>0?_:y}}function rn(f,u,l,_){if(!e5(u,l,_))return f;let y=Xf(u.procedureRuns),$=Pf(f)?f:{};return{...$,...u,controlCommands:Xf(u.controlCommands).length>0?u.controlCommands:$.controlCommands,controlEvents:Xf(u.controlEvents).length>0?u.controlEvents:$.controlEvents,procedureRuns:y.length>0?y:$.procedureRuns}}function jn({selection:f,runDetails:u,nodeDetails:l,nodeDetailsState:_,onRaw:y,onCollapse:$}){if(!f?.mode)return V("aside",{className:"pipeline-gantt-detail-panel empty","data-testid":"pipeline-gantt-detail-panel"},V("div",{className:"pipeline-gantt-detail-head"},V("div",null,V("span",{className:"panel-eyebrow"},"Gantt Detail"),V(j0,{title:"未选择元素",level:3})),V("button",{type:"button",className:"ghost-btn mini",onClick:$,"data-testid":"pipeline-gantt-sidebar-collapse"},"收起")),V(jl,{title:"选择一条执行线或一个控制点",text:"点击甘特图中的 node 执行线、prompt 点或控制点,在这里查看结构化过程和 OpenCode step。"}));let r=String(f?.runId||""),j=String(f?.interval?.nodeId||f?.marker?.nodeId||""),A=u?.runId===r?u.details:null,J=e5(l,r,j),U=String(_?.runId||"")===r&&String(_?.nodeId||"")===j,Q=rn(A,J,r,j),W=(String(u?.runId||"")!==r||Boolean(u?.loading))&&!Q,G=String(u?.runId||"")===r?String(u?.error||""):"",K=U?String(_?.error||""):"",H=Q?_n(Q,f):null,O=H?.interval||f?.interval||null,z=H?.marker||f?.marker||null,Z=String(O?.procedureRunId||z?.procedureRunId||""),N=J?aP(J,Z)||jJ(J,O||{procedureRunId:Z}):null,E=H?.procedure||(Q?jJ(Q,O||{procedureRunId:Z}):null)||O?.raw||{};if(N&&(uJ(E)===0||ZE(N)>=ZE(E)))E=$n(E,N);let q=H?.attempt||null,Y=String(H?.matchedStepKey||"");if(!q&&z&&uJ(E)>0)q=hE(E,z),Y=String(IE(q,Number.isFinite(Number(z?.ms))?Number(z.ms):null).stepKey||"");let w=rr(q),B=uJ(E)>0,P=U&&Boolean(_?.loading)&&!B,h=Boolean(W||P),M=[B?"":G,K].filter(Boolean).join(" / "),n=U&&_?.fetchedAt?_.fetchedAt:u?.fetchedAt,S=bE(E?.status||O?.status||z?.status||z?.event),T=f?.mode==="event"?z?.label||HE(z?.raw||z)||"event":H?.nodeId||O?.nodeId||"node",i=z?un(z?.raw||z):[],C=z?[hl(z?.raw||z)?`event ${hl(z?.raw||z)}`:"",z?.promptEvent?`prompt ${z.promptEvent}`:"",z?.action?`action ${z.action}`:"",z?.sourceKind?`source ${EE(z.sourceKind)}`:"",z?.sourceNodeId?`from ${z.sourceNodeId}`:"",z?.targetNodeId?`to ${z.targetNodeId}`:"",z?.snapReason?`draw ${z.snapReason}`:""].filter(Boolean):[];return V("aside",{className:"pipeline-gantt-detail-panel","data-testid":"pipeline-gantt-detail-panel"},V("div",{className:"pipeline-gantt-detail-head"},V("div",null,V("span",{className:"panel-eyebrow"},f?.mode==="event"?"Gantt Event Detail":"Gantt Line Detail"),V(j0,{title:T,level:3,loading:h})),V("div",{className:"pipeline-gantt-detail-head-actions"},V(P_,{status:S},S),V("button",{type:"button",className:"ghost-btn mini",onClick:$,"data-testid":"pipeline-gantt-sidebar-collapse"},"收起"))),z?V("article",{className:"pipeline-event-card"},V("div",{className:"pipeline-event-card-head"},V("strong",null,z?.label||HE(z?.raw||z)),V(GJ,{items:C})),V(rJ,{items:[{label:"event time",value:Nf(z?.timestampIso||z?.timestamp||"--")},z?.snapped?{label:"drawn time",value:Nf(z?.renderedTimestampIso||z?.ms)}:null,{label:"node",value:z?.nodeId||"--"},{label:"procedure",value:z?.procedureRunId||Ll(E)||"--"},{label:"attempt",value:z?.attempt||w||"--"},{label:"source kind",value:z?.sourceKind?EE(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]}),i.length>0?V("div",{className:"pipeline-event-blocks"},i.map((v,X)=>V("section",{key:`${v.label}-${X}`,className:"pipeline-event-text-block"},V("b",null,v.label),V("p",null,v.value)))):null,OE(z?.raw||z)?V("p",{className:"pipeline-text-preview"},OE(z?.raw||z)):null):null,V(rJ,{items:[{label:"epoch",value:r||O?.runId||"--"},{label:"node",value:H?.nodeId||O?.nodeId||z?.nodeId||"--"},{label:"procedure",value:O?.procedureRunId||z?.procedureRunId||Ll(E)||"--"},{label:"started",value:Nf(O?.startedAt||E?.startedAt)},{label:"finished",value:Nf(O?.finishedAt||E?.finishedAt)},{label:"duration",value:ql(O?.durationMs||E?.durationMs)},{label:"fetched",value:n?W0(n):"--"},H?.matchedStep?{label:"matched step",value:`Step ${H.matchedStep.index??H.matchedStepIndex+1}`}:null]}),V(A0,{error:M}),V("div",{className:"pipeline-gantt-detail-actions"},V(Il,{title:`Procedure ${O?.procedureRunId||z?.procedureRunId||H?.nodeId||"node"}`,data:E,onOpen:y,testId:"raw-pipeline-gantt-procedure"}),z?V(Il,{title:`Pipeline event ${z?.id||z?.commandId||z?.eventId||H?.nodeId||"event"}`,data:z?.raw||z,onOpen:y,testId:"raw-pipeline-gantt-event"}):null,Q?V(Il,{title:`Pipeline run ${r||"--"}`,data:Q,onOpen:y,testId:"raw-pipeline-gantt-node-details"}):null),!h&&!Ll(E)&&!z?V(jl,{title:"暂无过程详情",text:"当前选择还没有可匹配的 procedure 运行记录。"}):null,!h&&Ll(E)?V(yn,{procedure:E,matchedStepKey:Y,matchedAttemptId:w}):null)}function An({value:f}){let l=String(f||"--").split(/([_-])/u);return V(M_.default.Fragment,null,l.map((_,y)=>_==="-"||_==="_"?V(M_.default.Fragment,{key:y},_,V("wbr",null)):V(M_.default.Fragment,{key:y},_)))}async function w_(f,u={}){return Mf(f,{invalidJsonPrefix:"Pipeline 返回了无效 JSON",...u})}function P_({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return V("span",{className:`status-badge ${l}`},u||f||"unknown")}function wu({label:f,value:u,hint:l,tone:_}){return V("article",{className:`metric-card ${_||""}`},V("div",{className:"metric-label"},f),V("div",{className:"metric-value"},u),V("div",{className:"metric-hint"},l))}function A1({title:f,eyebrow:u,actions:l,children:_,className:y,loading:$}){return V("section",{className:`panel ${y||""}`},V("div",{className:"panel-head"},V("div",null,u?V("p",{className:"panel-eyebrow"},u):null,V(j0,{title:f,loading:$})),l?V("div",{className:"panel-actions"},l):null),V("div",{className:"panel-body"},_))}function Il({title:f,data:u,onOpen:l,testId:_}){return V("button",{type:"button",className:"ghost-btn","data-testid":_,onClick:()=>l(f,u)},"查看原始JSON")}function Vl({title:f,subtitle:u,facts:l,data:_,onRaw:y,testId:$}){let r=Xf(l).map((j)=>String(j||"")).filter(Boolean);return V("article",{className:"pipeline-evidence-row"},V("div",{className:"pipeline-evidence-main"},V("strong",null,f),u?V("span",null,u):null),V("div",{className:"pipeline-evidence-facts"},r.map((j,A)=>V("span",{key:`${A}-${j.slice(0,16)}`},j))),_!==void 0?V(Il,{title:f,data:_,onOpen:y,testId:$}):null)}function jl({title:f,text:u}){return V("div",{className:"empty-state"},V("strong",null,f),V("span",null,u))}function Fn(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function Jn(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function Un(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function Qn(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 VE(f,u,l){let _=f?._unidesk?.arrayLimits?.[u],y=Number(_?.originalLength);return Number.isFinite(y)?y:l}function pE(f){if(!f||typeof f!=="object"||Array.isArray(f))return"--";return`${f.componentClass||"--"}/${f.id||"--"}`}function fr(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 KJ(f){return f?.config&&typeof f.config==="object"&&!Array.isArray(f.config)?f.config:{}}function mE(f){let u=KJ(f),l=Array.isArray(u.nodes)?u.nodes:Array.isArray(f?.nodes)?f.nodes:[],_=new Map;for(let r of l){let j=String(r?.id||r?.nodeId||"");if(j)_.set(j,{...r,id:j})}let y=NJ(f),$=(r)=>{if(r&&!_.has(r))_.set(r,{id:r})};for(let r of ZJ(f))H4(r).forEach($);for(let r of y)$(String(r?.from||r?.source||"")),$(String(r?.to||r?.target||""));return Array.from(_.values())}function NJ(f){let u=KJ(f);return Array.isArray(u.edges)?u.edges:Array.isArray(f?.edges)?f.edges:[]}function ZJ(f){let u=KJ(f);return Array.isArray(u.topologicalBatches)?u.topologicalBatches:Array.isArray(f?.topologicalBatches)?f.topologicalBatches:[]}function Wn(f){let u=new Map;for(let l of f){let _=fr(l);if(_)u.set(_,l);let y=Array.isArray(l?.refs)?l.refs:[];for(let $ of y){let r=fr($);if(r)u.set(r,l)}}return u}function qE(f,u){let l=u.get(fr(f?.componentRef));if(l)return l;let _=fr({componentClass:f?.kind,id:f?.id});return _?u.get(_)||null:null}function LE(f,u){let l=gE(f,u);return String(l?.status||"pending")}function gE(f,u){return(Array.isArray(f?.nodes)?f.nodes:[]).find((_)=>_?.nodeId===u||_?.id===u)||null}function zn(f){return f.reduce((u,l)=>{let _=String(l?.status||"unknown").toLowerCase();return u[_]=(u[_]||0)+1,u},{})}function Gn(f){if(Array.isArray(f?.scorers))return f.scorers.filter(Pf);if(Array.isArray(f?.summary?.scorers))return f.summary.scorers.filter(Pf);if(Array.isArray(f?.artifact?.summary?.scorers))return f.artifact.summary.scorers.filter(Pf);return[]}function Kn(f){if(Pf(f?.run))return f.run;if(Pf(f?.runSummary))return f.runSummary;return null}function Nn(f,u){if(!Pf(f)&&!Pf(u))return null;if(!Pf(f))return u;if(!Pf(u))return f;return{...f,...u,request:Pf(f.request)||Pf(u.request)?{...Pf(f.request)?f.request:{},...Pf(u.request)?u.request:{}}:u.request??f.request,artifact:Pf(f.artifact)||Pf(u.artifact)?{...Pf(f.artifact)?f.artifact:{},...Pf(u.artifact)?u.artifact:{}}:u.artifact??f.artifact,summary:Pf(f.summary)||Pf(u.summary)?{...Pf(f.summary)?f.summary:{},...Pf(u.summary)?u.summary:{}}:u.summary??f.summary}}function ur(f){let u=Gn(f),l=u.find((U)=>Pf(U?.score))||u[0]||null,_=Pf(l?.score)?l.score:{},y=Number(_.passed),$=Number(_.total),r=Number(_.ratio),j=Number.isFinite(r)?r:Number.isFinite(y)&&Number.isFinite($)&&$>0?y/$:null,A=j===null?null:Math.round(Math.max(0,Math.min(100,j<=1?j*100:j))),J=String(_.text||(Number.isFinite(y)&&Number.isFinite($)?`${y}/${$}`:""));return{scorer:l,scorers:u,score:_,passed:Number.isFinite(y)?y:null,total:Number.isFinite($)?$:null,percent:A,text:J}}function FJ(f){let u=ur(f);return u.text||(u.scorers.length>0?String(u.scorer?.status||"pending"):"--")}function EJ(f){let u=ur(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 Zn(f){return Array.isArray(f?.items)?f.items.filter(Pf):[]}function En({run:f}){let u=FJ(f);return V("span",{className:`pipeline-score-badge ${EJ(f)}`},`score ${u}`)}function Hn({run:f,onRaw:u}){let _=ur(f).scorers;if(!f)return V(jl,{title:"暂无评分",text:"选择一个 epoch 后会显示 scorer 结果。"});if(_.length===0)return V("div",{className:"pipeline-score-empty"},V("strong",null,"评分器等待中"),V("span",null,"DAG 完成后,Pipeline control backend 会把 scorer summary 追加到 run artifact,并通过 UniDesk 显示。"));return V("div",{className:"pipeline-score-board","data-testid":"pipeline-score-board"},_.map((y,$)=>{let r=ur({scorers:[y]}),j=Zn(y),A=r.percent??0;return V("article",{key:`${y.scorerId||y.component||$}`,className:`pipeline-score-card ${EJ({scorers:[y]})}`},V("div",{className:"pipeline-score-head"},V("div",null,V("span",null,y.scorerId||y.component||"scorer"),V("strong",null,r.text||y.status||"--")),V(P_,{status:y.status||"unknown"},y.status||"unknown")),V("div",{className:"pipeline-score-meter","aria-label":`score ${A}%`},V("span",{style:{width:`${A}%`}})),V("div",{className:"pipeline-score-facts"},V("span",null,`${A}%`),V("span",null,y.component||"--"),V("span",null,y.applicationCheckoutRef||"--")),j.length>0?V("div",{className:"pipeline-score-items"},j.map((J)=>V("span",{key:`${J.id||J.filter}`,className:`pipeline-score-item ${String(J.status||"").toLowerCase()}`,title:`${J.filter||"--"} / ran=${J.ran??"?"}`},V("b",null,J.id||"--"),V("small",null,J.status||"--")))):V("p",{className:"muted paragraph"},"当前 scorer 尚未返回 item 级结果。"),y.error?V("p",{className:"pipeline-score-error"},xE(y.error,360)):null,V("div",{className:"panel-actions inline-actions"},V(Il,{title:`Scorer ${y.scorerId||$}`,data:y,onOpen:u,testId:"raw-pipeline-score"})))}))}function On(f){let u=f.reduce((l,_)=>{let y=String(_?.componentClass||"unknown");return l[y]=(l[y]||0)+1,l},{});return Object.entries(u).map(([l,_])=>({name:l,count:Number(_)})).sort((l,_)=>_.count-l.count||l.name.localeCompare(_.name))}function H4(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 H4(f.nodes);if(Array.isArray(f?.nodeIds))return H4(f.nodeIds);return[]}function Vn(f){return Pf(f?.instanceInputs?.monitor)?f.instanceInputs.monitor:{}}function kE(f,u){if(String(f?.kind||"").toLowerCase()!=="procedure")return!1;let l=Vn(f);if(f?.instanceInputs?.monitorMode===!0||l.enabled===!0)return!0;let _=pE(f?.componentRef);return String(u?.id||u?.config?.id||_||"").toLowerCase().includes("monitor")}function qn(f){return f.filter((u)=>kE(u)).map((u)=>String(u?.id||"")).filter(Boolean)}function Ln(f,u){if(u.length===0)return f;let l=new Set(u),_=u.filter((y)=>f.includes(y));if(_.length===0)return f;return[..._,...f.filter((y)=>!l.has(y))]}function Xn(f,u){if(u.length===0)return f;let l=new Set(u),_=u.filter(($)=>f.some((r)=>r.includes($)));if(_.length===0)return f;let y=f.map(($)=>$.filter((r)=>!l.has(r))).filter(($)=>$.length>0);return[_,...y]}function Bn(f,u,l){let y=ZJ(f).map(H4).filter((W)=>W.length>0);if(y.length>0)return y;let $=u.map((W)=>String(W?.id||"")).filter(Boolean),r=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(!r.has(G)||!r.has(K))continue;A.get(G)?.push(K),j.set(K,(j.get(K)||0)+1)}let J=new Map,U=$.filter((W)=>(j.get(W)||0)===0);for(let W of U)J.set(W,0);while(U.length>0){let W=U.shift(),G=(J.get(W)||0)+1;for(let K of A.get(W)||[])if(j.set(K,Math.max(0,(j.get(K)||0)-1)),J.set(K,Math.max(J.get(K)||0,G)),(j.get(K)||0)===0)U.push(K)}$.forEach((W)=>{if(!J.has(W))J.set(W,0)});let Q=Math.max(0,...Array.from(J.values()));return Array.from({length:Q+1},(W,G)=>$.filter((K)=>J.get(K)===G)).filter((W)=>W.length>0)}function Yn(f,u,l){let y=ZJ(f).map(H4).filter((j)=>j.length>0),$=y.length>0?y.flatMap((j)=>j):(()=>{let j=u.map((H)=>String(H?.id||"")).filter(Boolean),A=new Set(j),J=l.filter((H)=>String(H?.edgeType||"").toLowerCase()!=="rework"),U=new Map(j.map((H)=>[H,0])),Q=new Map(j.map((H)=>[H,[]]));for(let H of J){let O=String(H?.from||H?.source||""),z=String(H?.to||H?.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((H)=>(U.get(H)||0)===0);for(let H of G)W.set(H,0);while(G.length>0){let H=G.shift(),O=(W.get(H)||0)+1;for(let z of Q.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)G.push(z)}j.forEach((H)=>{if(!W.has(H))W.set(H,0)});let K=Math.max(0,...Array.from(W.values()));return Array.from({length:K+1},(H,O)=>j.filter((z)=>W.get(z)===O)).flatMap((H)=>H)})(),r=new Set($);for(let j of u){let A=String(j?.id||"");if(!A||r.has(A))continue;$.push(A),r.add(A)}return Ln($,qn(u))}function Q4(f){return`${f.source}->${f.target}-${f.index}`}function XE(f,u,l){let _=mE(f),y=NJ(f),$=Wn(l),r=new Map(_.map((S)=>[String(S?.id||""),S])),j=_.filter((S)=>kE(S,qE(S,$))).map((S)=>String(S?.id||"")).filter(Boolean),A=Xn(Bn(f,_,y),j),J=[],U=new Map,Q=330,W=122;A.forEach((S,T)=>{let i=S.length*122;S.forEach((C,v)=>{let X=r.get(C)||{id:C},D=qE(X,$),p=LE(u,C).toLowerCase(),m=String(X.kind||D?.componentClass||"node").toLowerCase(),s=pE(X.componentRef||D),d=String(D?.config?.version||D?.version||""),a=String(D?.config?.description||D?.description||""),I=v*122-Math.floor(i/2);U.set(C,{column:T,row:v,y:I}),J.push({id:C,type:"pipelineNode",position:{x:T*330,y:I},data:{exportLabel:{id:C,kind:m,componentRef:s,componentVersion:d,componentDescription:a,status:p},label:V("div",{className:"flow-node-label"},V("strong",null,C),V("span",null,m),V("code",{title:a||s},d?`${s}@${d}`:s),V(P_,{status:p},p))},className:`pipeline-flow-node ${m} ${p}`})})});let G=y.flatMap((S,T)=>{let i=String(S?.from||S?.source||""),C=String(S?.to||S?.target||"");if(!r.has(i)||!r.has(C))return[];return[{source:i,target:C,index:T,condition:S?.condition,edgeType:S?.edgeType}]}),K=G.reduce((S,T)=>S.set(T.source,(S.get(T.source)||0)+1),new Map),H=G.reduce((S,T)=>S.set(T.target,(S.get(T.target)||0)+1),new Map),O=G.reduce((S,T)=>{let i=`${T.source}->${T.target}`;return S.set(i,(S.get(i)||0)+1)},new Map),z=new Map,Z=new Map,N=new Map,E=new Map,q=new Map,Y=new Map,w=G.reduce((S,T)=>{let i=U.get(T.source),C=U.get(T.target),v=(C?.column||0)-(i?.column||0);if(v<=0||String(T.edgeType||"").toLowerCase()==="rework"||v!==1)return S;let D=`${T.source}->column:${C?.column??""}`,p=S.get(D)||[];return p.push(T),S.set(D,p),S},new Map);for(let S of w.values()){if(S.length<2)continue;S.slice().sort((T,i)=>{let C=U.get(T.target),v=U.get(i.target);return(C?.y||0)-(v?.y||0)||T.index-i.index}).forEach((T,i,C)=>{Y.set(Q4(T),{slot:i-(C.length-1)/2,count:C.length})})}[...G].sort((S,T)=>{let i=U.get(S.source),C=U.get(S.target),v=U.get(T.source),X=U.get(T.target),D=Math.abs((C?.column||0)-(i?.column||0))*330+Math.abs((C?.y||0)-(i?.y||0)),p=Math.abs((X?.column||0)-(v?.column||0))*330+Math.abs((X?.y||0)-(v?.y||0));return D-p||S.index-T.index}).forEach((S)=>{let T=U.get(S.source)||{column:0,row:0,y:0},i=U.get(S.target)||{column:0,row:0,y:0},C=i.column-T.column,v=Math.max(0,C),X=C<=0||String(S.edgeType||"").toLowerCase()==="rework",D=T.y-i.y,p=H.get(S.target)||1,m=Y.has(Q4(S)),s=!X&&v<=1&&(m||p===1),d=q.get(S.target)||new Map;q.set(S.target,d);let a=Z4.slice().sort((I,ff)=>{let yf=(Gf)=>{let c=String(Gf.side),o=0;if(X){if(c==="left")o+=86;if(c==="top")o+=i.y<=0?-22:12;if(c==="bottom")o+=i.y>=0?-22:12;if(Math.abs(i.y)<12&&c!=="left")o+=S.index%2===0?c==="top"?-6:6:c==="bottom"?-6:6;return o}if(s){if(c==="left")o-=m?72:44;if(c!=="left")o+=m?72:44;return o+Math.abs(D)*0.02}if(c==="left")o+=v<=1?0:24;if(c==="top")o+=D<-36?-18:42;if(c==="bottom")o+=D>36?-18:42;if(v<=1&&Math.abs(D)<=82&&c!=="left")o+=38;if(v>1&&c!=="left")o-=10;return o},rf=T.y-i.y,Wf=rf!==0?rf:S.index%2===0?-1:1,Ef=(Gf)=>{let c=d.get(Gf.id)||0;return yf(Gf)+c*64+mP(Gf,d,Wf)};return Ef(I)-Ef(ff)||String(I.id).localeCompare(String(ff.id))})[0];d.set(a.id,(d.get(a.id)||0)+1),E.set(Q4(S),a)});let P=G.map((S)=>{let T=LE(u,S.target).toLowerCase(),i=`${S.source}->${S.target}`,C=z.get(S.source)||0,v=Z.get(S.target)||0,X=N.get(i)||0;z.set(S.source,C+1),Z.set(S.target,v+1),N.set(i,X+1);let D=C-((K.get(S.source)||1)-1)/2,p=v-((H.get(S.target)||1)-1)/2,m=X-((O.get(i)||1)-1)/2,s=U.get(S.source),d=U.get(S.target),a=(d?.column||0)-(s?.column||0),I=Math.max(1,Math.abs(a)),ff=a<=0||String(S.edgeType||"").toLowerCase()==="rework",yf=Math.abs((d?.y||0)-(s?.y||0)),rf=Y.get(Q4(S)),Wf=!ff&&a===1&&(H.get(S.target)||0)>1,Ef=rf?rf.slot:m*2+D+p*0.45,Gf=Ef===0?S.index%2===0?-1:1:Math.sign(Ef),c=E.get(Q4(S))||Z4[1],o=c.side==="top"?-1:c.side==="bottom"?1:Gf,e=ff||I>1||yf>96||Math.abs(Ef)>0.2||c.side!=="left",Kf=ff?118+I*18:22+I*16,k=c.side==="left"?0:28,Af=e?Math.max(-280,Math.min(280,o*Math.min(180,Kf+k+yf*0.22)+Ef*28)):0,Yf=Math.max(0,Math.min(z4.length-1,Math.round(D+(z4.length-1)/2))),Bf=z4[Yf]||z4[1],df=T==="succeeded"?"var(--accent-2)":T==="running"?"var(--accent)":T==="failed"?"var(--danger)":"rgba(129, 147, 159, 0.78)",_0=s?.column||0,y0=d?.column||0,N0=Af===0?0:Math.sign(Af),a0=ff?`feedback:${_0}->${y0}:${N0}`:rf?`fanout:${_0}->${y0}:${S.source}`:Wf?`fanin:${_0}->${y0}:${S.target}`:c.side!=="left"||I>1?`corridor:${_0}->${y0}:${c.side}:${N0}:${Math.round(Math.abs(Af)/56)}`:"";return{id:`${S.source}->${S.target}-${S.index}`,source:S.source,target:S.target,sourceHandle:Bf.id,targetHandle:c.id,type:"pipelineCurve",zIndex:12,animated:T==="running",data:{baseEdgeColor:df,laneOffset:Af,routeMode:rf&&c.side==="left"?"direct-forward-left":"",targetSide:c.side,isFeedback:ff,overlapGroup:a0},targetStatus:T}}),h=P.reduce((S,T)=>{let i=String(T.data?.overlapGroup||"");return i?S.set(i,(S.get(i)||0)+1):S},new Map),M=new Map,n=P.map((S)=>{let T=String(S.targetStatus||"pending"),i={...S};delete i.targetStatus;let C=String(S.data?.overlapGroup||""),v=C?h.get(C)||0:0,X=v>1?M.get(C)||0:-1;if(v>1)M.set(C,X+1);let D=X>=0?QE[X%QE.length]:String(S.data.baseEdgeColor),p={stroke:D};if(S.data.isFeedback)p.strokeDasharray="9 7";return{...i,data:{...S.data,edgeColor:D,overlapSlot:X,overlapCount:v},style:p,markerEnd:{type:L_.ArrowClosed,color:D},className:`pipeline-flow-edge ${T} ${S.data.isFeedback?"feedback":""} ${X>=0?"overlap-colored":""}`}});return{nodes:J,edges:n}}function vu(f){return String(f??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}function BE(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 lr(f){return`arrow-${f.replace(/[^a-zA-Z0-9_-]+/g,"")}`}function tE(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,_=f.position.y,y=Z4.find(($)=>$.id===u);if(y?.side==="top")return{x:l+d$*GE(y.style?.left,0.5),y:_,position:Zf.Top};if(y?.side==="bottom")return{x:l+d$*GE(y.style?.left,0.5),y:_+e$,position:Zf.Bottom};return{x:l,y:_+e$/2,position:Zf.Left}}function wn(f){return{x:f.position.x+d$,y:f.position.y+e$/2}}function Dn(f,u){let l=Math.min(...f.nodes.map((H)=>H.position.x),0)-220,_=Math.min(...f.nodes.map((H)=>H.position.y),0)-220,y=Math.max(...f.nodes.map((H)=>H.position.x+d$),1)+220,$=Math.max(...f.nodes.map((H)=>H.position.y+e$),1)+220,r=Math.ceil(y-l),j=Math.ceil($-_),A=new Map(f.nodes.map((H)=>[H.id,H])),J=f.edges.map((H)=>BE(H.data?.edgeColor||H.style?.stroke)),Q=Array.from(new Set(["#4eb7a8","#d7a13a","#cf6a54","#81939f",...J])).map((H)=>``).join(""),W=f.edges.flatMap((H)=>{let O=A.get(H.source),z=A.get(H.target);if(!O||!z)return[];let Z=wn(O),N=YE(z,String(H.targetHandle||"in-left")),E=iE(Z.x,Z.y,N.x,N.y,N.position,Number(H.data?.laneOffset||0),String(H.data?.routeMode||"")),q=BE(H.data?.edgeColor||H.style?.stroke),Y=H.data?.isFeedback?' stroke-dasharray="9 7"':"";return``}).join(` +`),G=f.nodes.map((H)=>{let O=H.data?.exportLabel||{},z=String(O.status||"pending").toLowerCase(),Z=z==="succeeded"?"#4eb7a8":z==="running"?"#d7a13a":z==="failed"?"#cf6a54":"#81939f",N=H.position.x,E=H.position.y,q=Z4.map((Y)=>{let w=YE(H,Y.id);if(Y.side==="top"||Y.side==="bottom")return``;return``}).join(` `);return` - - ${Y} - - ${c0(O.id||E.id)} - ${c0(O.kind||"node")} - ${c0(O.componentRef||"--")} - ${c0(z)} + + ${q} + + ${vu(O.id||H.id)} + ${vu(O.kind||"node")} + ${vu(O.componentRef||"--")} + ${vu(z)} `}).join(` -`);return{svg:` +`);return{svg:` ${Q} - ${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` + ${vu(u)} + ${G}${W} + `,width:r,height:j}}function Tn(f){let u=String(f||"").toLowerCase();if(u==="succeeded"||u==="completed")return"#4eb7a8";if(u==="failed")return"#cf6a54";if(cE(u))return"#69aee8";return"#d7a13a"}function Mn(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 wE(f){let u=String(f?.sourceKind||"").toLowerCase(),l=String(f?.action||"").toLowerCase(),_=String(f?.status||"").toLowerCase();if(l==="observe"||_==="observation"||u==="monitor")return"#4eb7a8";if(u==="webui")return"#69aee8";if(u==="cli")return"#d7a13a";if(_.includes("ignored"))return"#81939f";return"#8aa0ad"}function Pn(f,u,l){let _=Mn(f),y=String(f?.kind||"");if(y==="control-source")return``;if(y==="control-target"){let r=String(f?.tone||"").toLowerCase()==="approve"?"rgba(78,183,168,0.22)":"#081118";return``}return``}function nn(f){let u=Xf(f.visibleNodeIds).map((X)=>String(X||"")).filter(Boolean),l=Xf(f.intervals).filter(Pf),_=Xf(f.markers).filter(Pf),y=Xf(f.arrows).filter(Pf),$=Xf(f.ticks).filter(Pf),r=Pf(f.bounds)?f.bounds:{},j=Pf(f.backendLayout)?f.backendLayout:null,A=Math.max(240,Math.round(Number(f.chartHeight||360))),J=Math.max(j1,108),U=128,Q=24,W=58,G=56,K=128+Math.max(1,u.length)*J,H=Math.max(760,K+48),O=114+A+24,z=24,Z=58,N=114,E=(X)=>152+X*J,q=(X)=>E(X)+J/2,Y=Xf(f.meta).map((X)=>String(X||"")).filter(Boolean).slice(0,4).join(" · "),w=new Map(_.map((X)=>[String(X.id||""),X])),P=Array.from(new Set(["#4eb7a8","#69aee8","#d7a13a","#cf6a54","#8aa0ad",...y.map(wE)])).map((X)=>``).join(""),h=$.map((X)=>{let D=114+lH(X,r,A,j);return` - ${c0(Kf(B.ms))} - +${c0(Nl(Number(B.offsetMs??Number(B.ms)-Number($.startMs))))} + ${vu(Nf(X.ms))} + +${vu(ql(Number(X.offsetMs??Number(X.ms)-Number(r.startMs))))} `}).join(` -`),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} +`),M=['','TIME',...u.map((X,D)=>{let p=E(D),m=X.length>18?`${X.slice(0,16)}…`:X;return` + + ${vu(m)} + node ${D+1} `})].join(` -`),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} +`),n=u.map((X,D)=>{return``}).join(` +`),S=l.map((X)=>{let D=u.indexOf(String(X.nodeId||""));if(D<0)return"";let p=114+yr(X,r,A,j),m=Math.max(2,uH(X,r,A,j)),s=Tn(X.status),d=q(D)-3.5,a=X.live?``:"",I=m>=28?`${vu(String(X.status||"working"))} + ${vu(ql(X.durationMs))}`:"";return` + + ${a} + ${I} `}).join(` -`),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} +`),T=_.map((X)=>{let D=u.indexOf(String(X.nodeId||""));if(D<0)return"";let p=114+rl(X,r,A,j);return Pn(X,q(D),p)}).join(` +`),i=y.map((X)=>{let D=w.get(String(X.targetMarkerId||""));if(!D)return"";let p=w.get(String(X.sourceMarkerId||"")),m=String(p?.nodeId||X.sourceNodeId||""),s=String(D.nodeId||X.targetNodeId||""),d=u.indexOf(m),a=u.indexOf(s);if(d<0||a<0)return"";let I=q(d)-24-128,ff=q(a)-24-128,yf=f3(j)?Y0(X.sourceY??X.y1)??(p?rl(p,r,A,j):rl(D,r,A,j)):p?rl(p,r,A,j):rl(D,r,A,j),rf=f3(j)?Y0(X.targetY??X.y2)??rl(D,r,A,j):rl(D,r,A,j),Wf=wE(X),Ef=String(X.action||"").toLowerCase()==="observe"?"3 4":"6 5",Gf=vu(_H(I,yf,ff,rf));return` + `}).join(` +`),C=u.length===0?'No visible Gantt nodes':"";return{svg:` + ${P} - ${c0(f.title||"Pipeline Epoch Gantt")} - ${c0(w)} + ${vu(f.title||"Pipeline Epoch Gantt")} + ${vu(Y)} ${M} - ${c} - ${m} - ${C} - ${R} + ${n} + ${h} + ${S} + ${i} ${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));})(); + ${C} + `,width:H,height:O}}function _r(f,u){let l=URL.createObjectURL(f),_=document.createElement("a");_.href=l,_.download=u,_.click(),setTimeout(()=>URL.revokeObjectURL(l),1000)}async function sE(f,u){let l=tE(u,"pipeline"),{svg:_,width:y,height:$}=Dn(f,u),r=new Blob([_],{type:"image/svg+xml;charset=utf-8"}),j=URL.createObjectURL(r);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 J=document.createElement("canvas");J.width=y,J.height=$;let U=J.getContext("2d");if(!U)throw Error("canvas unavailable");U.drawImage(A,0,0);let Q=await new Promise((W)=>J.toBlob(W,"image/png"));if(!Q)throw Error("png export failed");_r(Q,`${l}.png`)}catch{_r(r,`${l}.svg`)}finally{URL.revokeObjectURL(j)}}async function Sn(f){let u=tE(String(f?.title||"pipeline-gantt"),"pipeline-gantt"),{svg:l,width:_,height:y}=nn(f),$=new Blob([l],{type:"image/svg+xml;charset=utf-8"}),r=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=r});let A=document.createElement("canvas");A.width=_,A.height=y;let J=A.getContext("2d");if(!J)throw Error("canvas unavailable");J.drawImage(j,0,0);let U=await new Promise((Q)=>A.toBlob(Q,"image/png"));if(!U)throw Error("gantt png export failed");_r(U,`${u}.png`)}catch{_r($,`${u}.svg`)}finally{URL.revokeObjectURL(r)}}async function Cn(f){for(let u of f){if(u.flow.nodes.length===0)continue;await sE(u.flow,u.title),await new Promise((l)=>setTimeout(l,750))}}function DE(f,u){return f.find((l)=>String(l?.pipelineId||"")===u)||null}function TE(f){return vf(f?.startedAt)??vf(f?.artifact?.startedAt)??vf(f?.request?.createdAt)??vf(f?.updatedAt)??0}function cn(f,u){return f.filter((l)=>String(l?.pipelineId||"")===u).slice().sort((l,_)=>TE(l)-TE(_)||String(l?.runId||"").localeCompare(String(_?.runId||"")))}function JJ(f,u){let l=String(u?.runId||""),_=f.findIndex((r)=>String(r?.runId||"")===l),y=_>=0?_+1:f.length,$=String(u?.status||"--");return`Epoch ${y} / ${l||"--"} / ${$}`}function Ll(f){return String(f?.procedureRunId||f?.runId||"")}function jr(f,u){let l=String(f?.nodeId||f?.request?.nodeId||"");if(l)return l;let _=Ll(f),y=`${u}__`;if(_.startsWith(y))return _.slice(y.length).replace(/__\d+$/u,"");return""}function t5(f,u){let l=Pf(f?.artifact)?f.artifact:{},_=Pf(f?.request)?f.request:{};return V4(f?.startedAt,l.startedAt,_.createdAt,_.startedAt,f?.createdAt,f?.updatedAt,u?.startedAt,u?.request?.createdAt)}function s5(f,u){let l=String(f?.status?.status||f?.artifact?.status||f?.status||"").toLowerCase(),_=Pf(f?.artifact)?f.artifact:{},y=zJ(l);return V4(f?.finishedAt,_.finishedAt,f?.completedAt,y?f?.updatedAt:void 0,y?_.updatedAt:void 0,y?u?.updatedAt:void 0)}function oE(f,u,l=Date.now()){let _=String(f?.runId||""),y=new Set(u.map(($)=>String($?.id||"")).filter(Boolean));return Xf(f?.procedureRuns).flatMap(($)=>{let r=jr($,_);if(!r)return[];let j=String($?.status?.status||$?.artifact?.status||$?.status||"unknown").toLowerCase(),A=t5($,f),J=vf(A);if(J===null)return[];let U=s5($,f),Q=vf(U)??(zJ(j)?vf($?.updatedAt)??J+1000:l),W=Math.max(J+1000,Q);return[{nodeId:r,knownNode:y.has(r),procedureRunId:Ll($),status:j,startMs:J,endMs:W,startedAt:E4(J),finishedAt:E4(W),durationMs:W-J,runId:_,raw:$}]}).sort(($,r)=>$.startMs-r.startMs||$.endMs-r.endMs||$.nodeId.localeCompare(r.nodeId))}function Rn(f,u,l=[]){let _=u.map((U)=>Number(U.startMs)).filter(Number.isFinite),y=u.map((U)=>Number(U.endMs)).filter(Number.isFinite);for(let U of l){let Q=Y0(U?.eventMs??U?.ms);if(Q!==null)_.push(Q),y.push(Q)}let $=vf(f?.startedAt)??vf(f?.artifact?.startedAt)??vf(f?.request?.createdAt),r=vf(f?.finishedAt)??vf(f?.artifact?.finishedAt)??vf(f?.updatedAt);if($!==null)_.push($);if(r!==null)y.push(r);let j=Date.now(),A=_.length>0?Math.min(..._):j-60000,J=Math.max(A+60000,y.length>0?Math.max(...y):j);return{startMs:A,endMs:J,durationMs:J-A}}var o5=12,aE=20,UJ=100,xn=!1;function n_(f){let u=Number(f);if(!Number.isFinite(u))return 0;return Math.max(0,Math.min(100,Math.round(u*100)/100))}function bn(f){let u=Math.max(o5,Number(f||o5)),l=Math.log(u/o5)/Math.log(aE);return n_(l*100)}var O4=bn(UJ);function HJ(f){let u=n_(f)/100,l=o5*Math.pow(aE,u),_=u<0.24?"全局":u<0.64?"均衡":"细节";return{value:n_(u*100),pxPerMinute:l,label:_}}function yJ(f){let u=Math.round(Number(f));return Math.abs(u-UJ)<=1?UJ:u}function vn(f,u=O4){let l=Math.max(1,Number(f.durationMs||0)/60000),_=HJ(u);return Math.round(Math.max(360,Math.min(7200,l*Number(_.pxPerMinute||48))))}function hn(f,u=7){let l=Math.max(1,Number(f.endMs||0)-Number(f.startMs||0));return Array.from({length:u},(_,y)=>{let $=u===1?0:y/(u-1);return{ms:Number(f.startMs)+l*$,percent:$*100}})}function In(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 Y0(f){let u=Number(f);return Number.isFinite(u)?u:null}function OJ(f){return cE(f?.status)&&!zJ(f?.status)}function dE(f,u,l,_){let y=Math.max(1,l-u),$=Math.max(0,Math.min(1,(f-u)/y));return Number(($*_).toFixed(3))}function ME(f,u){if(!u)return null;let l=Y0(u?.startMs),_=Y0(u?.endMs),y=Y0(u?.chartHeight);if(l===null||_===null||y===null)return null;return dE(f,l,_,y)}function eE(f,u){let l=Y0(f?.rawStartMs??f?.startMs)??Y0(f?.startMs)??u,_=Y0(f?.endMs)??l+1000;if(!OJ(f))return Math.max(l+1000,_);return Math.max(l+1000,_,u)}function pn(f,u,l,_){let y=Y0(f?.startMs)??_-60000,$=Y0(f?.endMs)??_,r=l.reduce((K,H)=>Math.max(K,eE(H,_)),$),j=Math.max(y+60000,$,r),A=Math.max(1,j-y),J={startMs:y,endMs:j,durationMs:A},U=vn(J,u),Q=HJ(u),W=Math.max(5,Math.min(18,Math.round(U/150))),G=hn(J,W).map((K)=>{let H=Number(K.ms),O=dE(H,y,j,U);return{...K,y:O,timestamp:E4(H),offsetMs:H-y}});return{source:"frontend-y",startMs:y,endMs:j,durationMs:A,chartHeight:U,scale:n_(u),normalizedScale:Number((n_(u)/100).toFixed(3)),pxPerMinute:Number(Number(Q.pxPerMinute||0).toFixed(3)),ticks:G}}function mn(f,u,l){if(!OJ(f))return f;let _=Y0(f?.rawStartMs??f?.startMs)??Y0(f?.startMs)??l,y=eE(f,l),$=ME(_,u),r=ME(y,u),j=Y0($??f?.y1??f?.startY)??0,A=Y0(r??f?.y2??f?.endY)??j+10,J=Math.max(24,A-j);return{...f,live:!0,startMs:_,endMs:y,durationMs:Math.max(1000,y-_),finishedAt:E4(y),y1:j,y2:A,startY:j,endY:A,height:J}}function VJ(f,u,l){return In(f,u)/100*l}function f3(f){return Boolean(f&&String(f?.source||"")!=="frontend-y")}function fH(f,u,l,_,y){if(f3(_))for(let r of y){let j=Y0(f?.[r]);if(j!==null)return j}let $=Y0(f?.ms??f?.eventMs??f?.startMs);return VJ($??Number(u.startMs),u,l)}function yr(f,u,l,_){return fH(f,u,l,_,["y1","startY"])}function QJ(f,u,l,_){if(f3(_)){let $=Y0(f?.y2??f?.endY);if($!==null)return $}let y=Y0(f?.endMs)??Number(u.endMs);return VJ(y,u,l)}function uH(f,u,l,_){if(f3(_)){let $=Y0(f?.height);if($!==null)return Math.max(1,$)}let y=f?.live?24:10;return Math.max(y,QJ(f,u,l,_)-yr(f,u,l,_))}function rl(f,u,l,_){return fH(f,u,l,_,["y","timeAxisY"])}function lH(f,u,l,_){if(f3(_)||String(_?.source||"")==="frontend-y"){let r=Y0(f?.y);if(r!==null)return r}let y=Y0(f?.percent);if(y!==null)return y/100*l;let $=Y0(f?.ms)??Number(u.startMs);return VJ($,u,l)}function gn(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||""),_=String(f?.nodeId||f?.targetNodeId||"");return l&&l!==_?l:""}function kn(f,u){let l=new Set(u.map((y)=>[String(y.sourceNodeId||""),String(y.targetNodeId||""),String(y.targetMarkerId||""),String(y.action||"")].join(":"))),_=[...u];for(let y of f){let $=gn(y),r=String(y?.nodeId||""),j=String(y?.id||"");if(!$||!r||!j)continue;let A=[$,r,j,"observe"].join(":");if(l.has(A))continue;l.add(A),_.push({id:`observation-arrow:${j}:${$}:${r}`,commandId:String(y?.commandId||y?.eventId||j),sourceNodeId:$,targetNodeId:r,sourceMarkerId:"",targetMarkerId:j,sourceKind:"monitor",action:"observe",status:"observation"})}return{markers:f,arrows:_}}function PE(f,u=""){let l=hl(f)||u,_=String(f?.promptEvent||"");if(l==="initial-prompt-delivered")return"initial";if(_==="node-finished"||_==="node-long-running-observation"||_.startsWith("monitor-"))return"monitor";if(l==="monitor-prompt-delivered"||String(f?.sourceKind||"").toLowerCase()==="monitor"||u==="monitor-prompt-queued")return"monitor";return"append"}function tn(f){return Xf(f?.tags||f?.raw?.tags).map((u)=>String(u||"")).filter(Boolean)}function nE(f,u=""){let l=hl(f)||u,_=String(f?.promptEvent||"");if(l==="initial-prompt-delivered")return"初始 prompt";if(_==="node-long-running-observation")return"长任务观察";if(_==="node-finished")return tn(f).includes("monitor.audit")?"节点完成 / OA 审核":"节点完成";if(_==="monitor-interval")return"旧版轮询";if(_==="monitor-start")return"Monitor start";if(_==="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 SE(f){let u=hl(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 CE(f,u){let l=String(f?.commandId||"");if(l)return`command:${l}`;return["fallback",Ty(f)||V4(f?.createdAt,f?.timestamp)||`index-${u}`,String(f?.sourceKind||""),String(f?.sourceNodeId||""),String(f?.targetNodeId||""),My(f)].join(":")}function sn(f){return AJ([f?.targetNodeId,...Xf(f?.resetNodeIds)])}function on(f,u){let l=N4(f),_=hl(f),y=String(f?.targetNodeId||""),$=Boolean(y)&&u!==y;if(_==="control-command-applied")return $?`${l} 波及`:`${l} 生效`;if(_==="control-command-ignored")return`${l} 忽略`;if(_==="control-command-queued")return`${l} 已发起`;return $?`${l} 波及`:l}function an(f){if(hl(f)==="control-command-ignored")return"ignored";let l=My(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 dn(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 en(f,u,l,_){let y=f.filter((J)=>String(J.nodeId||"")===u).sort((J,U)=>Number(J.startMs)-Number(U.startMs)),$=y.find((J)=>l>=Number(J.startMs)-1000&&l<=Number(J.endMs)+1000);if($)return{ms:l,onInterval:!0,snapReason:"inside-interval",procedureRunId:String($.procedureRunId||"")};let r=My(_),j=y.slice().reverse().find((J)=>Number(J.endMs)<=l+1000);if(j&&r==="approve")return{ms:Number(j.endMs),onInterval:!0,snapReason:"previous-interval-end",procedureRunId:String(j.procedureRunId||"")};let A=y.find((J)=>Number(J.startMs)>=l-1000);if(A&&["guide","modify","restart","redo"].includes(r))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(_?.procedureRunId||"")}}function _H(f,u,l,_){let y=Math.hypot(l-f,_-u),$=y>zE?zE:0,r=$>0?l-(l-f)/y*$:l,j=$>0?_-(_-u)/y*$:_,A=r-f,J=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*J},${u} ${r-U*J},${j} ${r},${j}`}function fS(f,u){let l=String(f?.runId||u?.runId||""),_=oE({...Pf(u)?u:{},...Pf(f)?f:{},runId:l,procedureRuns:Xf(f?.procedureRuns).length>0?f.procedureRuns:u?.procedureRuns},[]),y=[],$=[],r=[],j=new Set,A=new Map,J=(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 Xf(f?.procedureRuns)){let G=jr(W,l),K=Ll(W);if(!G)continue;for(let H of Xf(W?.attempts)){let O=rr(H),z=new Set,Z=new Set;for(let E of K4(H?.controlEventRecords)){let q=hl(E);if(!["initial-prompt-delivered","append-prompt-delivered","monitor-prompt-delivered"].includes(q))continue;let Y=Ty(E),w=vf(Y);if(w===null)continue;let B=String(E?.eventId||"");if(B)z.add(B);Z.add(`${q}:${Y}:${String(E?.sourceKind||"")}:${String(E?.promptPreview||"")}`),J({id:`prompt:${B||`${K}:${O}:${q}:${w}`}`,runId:l,nodeId:G,procedureRunId:K,attempt:O,kind:"prompt",tone:PE(E,q),status:"delivered",label:nE(E,q),ms:w,timestampIso:Y,sourceKind:String(E?.sourceKind||""),sourceNodeId:String(E?.sourceNodeId||""),targetNodeId:G,action:"",eventId:B,commandId:String(E?.commandId||""),raw:E},y)}let N=[{records:K4(H?.controlPromptRecords),fallbackKind:"append-prompt-queued"},{records:K4(H?.monitorPromptRecords),fallbackKind:"monitor-prompt-queued"}];for(let E of N)for(let q of E.records){let Y=Ty(q),w=vf(Y);if(w===null)continue;let B=String(q?.eventId||"");if(B&&z.has(B))continue;let h=`${E.fallbackKind==="monitor-prompt-queued"?"monitor-prompt-delivered":"append-prompt-delivered"}:${Y}:${String(q?.sourceKind||"")}:${String(q?.promptPreview||"")}`;if(Z.has(h))continue;J({id:`prompt-fallback:${B||`${K}:${O}:${E.fallbackKind}:${w}`}`,runId:l,nodeId:G,procedureRunId:K,attempt:O,kind:"prompt",tone:PE(q,E.fallbackKind),status:"queued",label:nE(q,E.fallbackKind),ms:w,timestampIso:Y,sourceKind:String(q?.sourceKind||""),sourceNodeId:String(q?.sourceNodeId||""),targetNodeId:G,action:"",eventId:B,commandId:String(q?.commandId||""),raw:q},y)}}}let U=new Map;K4(f?.controlEvents).forEach((W,G)=>{let K=CE(W,G),H=U.get(K)||{key:K,events:[],commands:[]};H.events.push(W),U.set(K,H)}),Xf(f?.controlCommands).filter(Pf).forEach((W,G)=>{let K=CE(W,G),H=U.get(K)||{key:K,events:[],commands:[]};H.commands.push(W),U.set(K,H)});for(let W of U.values()){let G=Xf(W.events).slice().sort((n,S)=>SE(S)-SE(n)),K=Xf(W.commands),H=Xf(W.events).find((n)=>hl(n)==="control-command-queued")||K[0]||null,O=G[0]||K[0]||H;if(!H&&!O)continue;let z=String(H?.sourceNodeId||O?.sourceNodeId||""),Z=String(H?.sourceKind||O?.sourceKind||""),N=Ty(H)||Ty(O)||V4(H?.createdAt,O?.createdAt),E=vf(N),q=String(O?.commandId||H?.commandId||W.key),Y=(hl(O)||"control-command-queued").replace(/^control-command-/u,""),w="";if(z&&E!==null)w=`control-source:${q}:${z}`,A.set(q,w),J({id:w,runId:l,nodeId:z,procedureRunId:String(H?.procedureRunId||O?.procedureRunId||""),attempt:"",kind:"control-source",tone:dn(H||O),status:Y,label:`${N4(H||O)} 发起`,ms:E,timestampIso:N,action:My(H||O),sourceKind:Z,sourceNodeId:z,targetNodeId:String(O?.targetNodeId||H?.targetNodeId||""),commandId:q,raw:H||O},$);let B=O||H,P=Ty(B)||N,h=vf(P);if(h===null)continue;let M=sn(B);for(let n of M){let S=en(_,n,h,B),T=`control-target:${q}:${n}`;if(J({id:T,runId:l,nodeId:n,procedureRunId:S.procedureRunId,attempt:"",kind:"control-target",tone:an(B),status:Y,label:on(B,n),ms:S.ms,eventMs:h,onInterval:S.onInterval,snapReason:S.snapReason,snapped:Number(S.ms)!==h,timestampIso:P,renderedTimestampIso:E4(Number(S.ms)),action:My(B),sourceKind:Z,sourceNodeId:z,targetNodeId:n,commandId:q,raw:B},$),w&&z&&z!==n)r.push({id:`control-arrow:${q}:${z}:${n}`,commandId:q,sourceNodeId:z,targetNodeId:n,sourceMarkerId:w,targetMarkerId:T,sourceKind:Z,action:My(B),status:Y})}}let Q=[...y,...$].sort((W,G)=>Number(W.ms)-Number(G.ms)||String(W.nodeId).localeCompare(String(G.nodeId))||String(W.id).localeCompare(String(G.id)));return{...kn(Q,r),sourceMarkerByCommand:A}}function uS({details:f,selectedNodeId:u,selectedNodeRuntime:l,control:_,onRaw:y}){if(!f)return V("span",{className:"muted"},"点击“抓取过程”读取 node 运行材料;主界面只显示结构化摘要,完整内容需点开原始 JSON。");let $=Xf(f.procedureRuns),r=$.at(-1)||{},j=Xf(r.attempts),A=j.at(-1)||{},J=Xf(r.workerLogTail),U=Xf(A.controlEventsTail),Q=Xf(A.controlPromptsTail),W=Xf(A.monitorPromptsTail),G=fJ(U),K=fJ(Q),H=fJ(W),O=A.opencodeMessages||{};return V("div",{className:"pipeline-evidence-list compact"},V(Vl,{title:"Node runtime",subtitle:u||"--",facts:[`status ${l?.status||"pending"}`,`attempts ${l?.attempts??j.length}`,`procedure ${l?.currentProcedureRunId||Ll(r)||"--"}`,_.fetchedAt?`fetched ${W0(_.fetchedAt)}`:"not fetched"],data:f.node||f,onRaw:y,testId:"raw-pipeline-node-runtime"}),V(Vl,{title:"Procedure runs",subtitle:`${$.length} groups`,facts:[`latest ${r.status?.status||r.status||"--"}`,`steps ${Xf(r.recentSteps).length}`,`duration ${ql(vf(r.finishedAt)&&vf(r.startedAt)?Number(vf(r.finishedAt))-Number(vf(r.startedAt)):r.durationMs)}`],data:$,onRaw:y,testId:"raw-pipeline-node-procedures"}),V(Vl,{title:"OpenCode messages",subtitle:String(O.exists?"available":"not indexed"),facts:[`messages ${d5(O.messageCount)}`,`size ${d5(O.size)}`,`updated ${Nf(O.updatedAt)}`],data:O,onRaw:y,testId:"raw-pipeline-node-messages"}),V(Vl,{title:"Control prompts",subtitle:"manual / monitor append queues",facts:[`manual tail ${K.total}`,`monitor tail ${H.total}`,`last ${Nf(WJ(K.lastAt,H.lastAt))}`],data:{controlPromptsTail:Q,monitorPromptsTail:W},onRaw:y,testId:"raw-pipeline-node-prompts"}),V(Vl,{title:"Control events",subtitle:G.eventKinds.length>0?G.eventKinds.join(", "):"event tail",facts:[`tail ${G.total}`,`parsed ${G.parsed}`,`last ${Nf(G.lastAt)}`],data:U,onRaw:y,testId:"raw-pipeline-node-events"}),V(Vl,{title:"Worker log",subtitle:"tail is hidden on main canvas",facts:[`tail ${J.length} lines`,"raw only via button",`procedure ${Ll(r)||"--"}`],data:J,onRaw:y,testId:"raw-pipeline-node-worker-log"}))}function lS({activeRun:f,onRaw:u}){if(!f)return V(jl,{title:"暂无运行材料",text:"没有 Pipeline epoch 时不会展示运行材料索引。"});let l=Xf(f.nodes),_=Xf(f.procedureRuns),y=Xf(f.submissions),$=Xf(f.workerLogTail),r=NE(l),j=NE(_),A=_.filter((U)=>String(U?.status||"").toLowerCase()==="failed"),J=WJ(..._.flatMap((U)=>[U.updatedAt,U.finishedAt,U.startedAt]));return V("div",{className:"pipeline-evidence-list"},V(Vl,{title:"Epoch overview",subtitle:f.runId||"--",facts:[`pipeline ${f.pipelineId||"--"}`,`status ${f.status||"--"}`,`started ${Nf(f.startedAt)}`,`updated ${Nf(f.updatedAt)}`],data:f,onRaw:u,testId:"raw-pipeline-run"}),V(Vl,{title:"Node states",subtitle:`${l.length} nodes`,facts:[`running ${r.running||0}`,`succeeded ${r.succeeded||0}`,`failed ${r.failed||0}`,`pending ${r.pending||0}`],data:l,onRaw:u,testId:"raw-pipeline-run-nodes"}),V(Vl,{title:"Procedure run index",subtitle:`${_.length} procedure records`,facts:[`succeeded ${j.succeeded||0}`,`failed ${j.failed||0}`,`latest ${Nf(J)}`,`errors ${A.length}`],data:_,onRaw:u,testId:"raw-pipeline-run-procedures"}),V(Vl,{title:"OA submissions",subtitle:`${y.length} submission files`,facts:[`records ${y.length}`,`task ${d5(f.task)}`,"raw grouped by run"],data:y,onRaw:u,testId:"raw-pipeline-run-submissions"}),V(Vl,{title:"Worker log tail",subtitle:"hidden from main interface",facts:[`tail ${$.length} lines`,"display raw only after click",`updated ${Nf(f.updatedAt)}`],data:$,onRaw:u,testId:"raw-pipeline-run-worker-log"}))}function _S({diagnostics:f,onRaw:u}){let l=Xf(f?.runs).filter(Pf),_=Xf(f?.forbiddenResiduals),y=Pf(f?.guarantees)?f.guarantees:{},$=f?.hasNeutralNodeFinishedEvidence===!0&&f?.hasNoAuditPolicyEvidence===!0&&f?.hasAuditPolicyEvidence===!0,r=f?.ok===!0&&$&&_.length===0,j=l[0]||null,A=[{label:"中性完成事实",ok:y.neutralNodeFinished===!0,hint:"node-finished 不携带流程策略"},{label:"Config 策略判定",ok:y.auditPolicyFromConfig===!0,hint:"OA backend 读取当前 epoch 配置"},{label:"控制命令来自 OA",ok:y.runnerConsumesControlCommandsFromOaEvents===!0,hint:"runner 只消费 OA control.command"},{label:"无独立审核事件",ok:y.noIndependentAuditRequestEvent===!0,hint:"审核由 node-finished + policy 派生"},{label:"无批次门禁",ok:y.noBatchFinishedControlGate===!0,hint:"下游启动由每个 node 完成驱动"}];return V("div",{className:"pipeline-oa-panel","data-testid":"pipeline-oa-event-flow-panel"},V("div",{className:"metric-grid compact"},V(wu,{label:"OA Flow",value:r?"100%":"--",hint:String(f?.mode||"waiting diagnostics"),tone:r?"ok":"warn"}),V(wu,{label:"禁止残留",value:_.length,hint:_.length===0?"source scan clean":"needs cleanup",tone:_.length===0?"ok":"warn"}),V(wu,{label:"No-audit",value:f?.hasNoAuditPolicyEvidence?"OK":"--",hint:"OA 下游策略证据",tone:f?.hasNoAuditPolicyEvidence?"ok":"warn"}),V(wu,{label:"Monitor 审核",value:f?.hasAuditPolicyEvidence?"OK":"--",hint:"OA 控制事件闭环",tone:f?.hasAuditPolicyEvidence?"ok":"warn"})),V("div",{className:"pipeline-oa-guarantees"},A.map((J)=>V("article",{key:J.label,className:`pipeline-oa-guarantee ${J.ok?"ok":"warn"}`},V(P_,{status:J.ok?"online":"warn"},J.ok?"OK":"MISS"),V("div",null,V("strong",null,J.label),V("span",null,J.hint))))),V("div",{className:"pipeline-evidence-list compact"},l.slice(0,6).map((J)=>V(Vl,{key:J.runId,title:String(J.runId||"--"),subtitle:[Number(J.monitorAuditNodeFinishedCount||0)>0?"monitor audit":"",Number(J.noAuditPolicyCount||0)>0?"no-audit policy":""].filter(Boolean).join(" / ")||"event evidence",facts:[`events ${J.eventCount||0}`,`node-finished ${J.nodeFinishedCount||0}`,`policy-in-detail ${J.nodeFinishedWithPolicyCount||0}`,`queued ${J.controlQueuedCount||0}`,`applied ${J.controlAppliedCount||0}`],data:J,onRaw:u,testId:`raw-pipeline-oa-run-${String(J.runId||"run").replace(/[^a-zA-Z0-9_.-]+/g,"-")}`}))),j?V("p",{className:"muted paragraph"},`最新证据 ${j.runId}: ${j.nodeFinishedCount||0} 个 node-finished,${j.controlAppliedCount||0} 个控制结果。`):V(jl,{title:"暂无 OA 事件流证据",text:"等待 Pipeline backend 暴露 diagnostics。"}),f?V("div",{className:"panel-actions inline-actions"},V(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=Pf(f?.summary)?f.summary:{},_=Pf(f?.target)?f.target:{},y=Pf(f?.cache)?f.cache:{},$=f?.ok===!0,r=String(f?.modelId||l.modelName||_.modelName||"MiniMax-M2.7"),j=l.totalCount??_.currentIntervalTotalCount,A=l.usageCount??_.currentIntervalUsageCount,J=l.remainingCount??_.currentIntervalRemainingCount,U=l.remainingRatio??(Number.isFinite(Number(j))&&Number(j)>0&&Number.isFinite(Number(J))?Number(J)/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||_.endAt,G=l.remainsMs??_.remainsMs,K=Number(J),H=!$||Number.isFinite(K)&&K<=0?"warn":"ok",O=[$?`endpoint ${f?.endpoint||"--"}`:"quota unavailable",`fetched ${a5(f?.fetchedAt)}`,y.hit?`cache ${ql(y.ageMs)}`:"live quota"];return V("div",{className:"pipeline-minimax-quota-panel","data-testid":"pipeline-minimax-quota-panel"},V("div",{className:"metric-grid compact"},V(wu,{label:"MiniMax",value:$?r:"--",hint:f?.modelComponent||f?.error||"model/minimax-m27",tone:H}),V(wu,{label:"当前窗口",value:`${eF(A)}/${eF(j)}`,hint:`已用 ${KE(Q)}`,tone:H}),V(wu,{label:"剩余额度",value:eF(J),hint:`剩余 ${KE(U)}`,tone:H}),V(wu,{label:"重置时间",value:a5(W),hint:G!==void 0?`约 ${ql(G)}`:Nf(W),tone:H})),V(GJ,{items:O}),$?V("p",{className:"muted paragraph"},`MiniMax 限额来自 D601 Pipeline 后端实时查询;当前模型匹配 ${l.modelName||_.modelName||r}。`):V(A0,{error:f?.error||"MiniMax 限额查询失败"}),f?V("div",{className:"panel-actions inline-actions"},V(Il,{title:"Pipeline MiniMax Quota",data:f,onOpen:u,testId:"raw-pipeline-minimax-quota"})):null)}function $S({epochs:f,activeRun:u,activePipeline:l,pipelineNodes:_,pipelineEdges:y,runDetails:$,nodeDetails:r,nodeDetailsState:j,ganttScale:A=O4,onGanttScaleChange:J,onRunChange:U,onIntervalSelect:Q,onMarkerSelect:W,selection:G,detailOpen:K,onDetailOpenChange:H,onRaw:O}){let[z,Z]=bu(xn),[N,E]=bu({startY:0,endY:0,startMs:0,endMs:0}),[q,Y]=bu(Date.now()),w=T_(null),B=String(u?.runId||""),P=Boolean(K),h=(Uf)=>{if(typeof H==="function")H(Uf)},M=n_(A??O4),n=String($?.runId||"")===B?$?.details:null,S=n?{...Pf(u)?u:{},...Pf(n)?n:{},runId:B,procedureRuns:Xf(n?.procedureRuns).length>0?n.procedureRuns:u?.procedureRuns}:u,T=oE(S,_,q),i=n?fS(n,S):{markers:[],arrows:[]},C=Xf(i.markers),v=Rn(S,T,C),X=pn(v,M,T,q),D=String(X.source||"frontend-y"),p=T.map((Uf)=>mn(Uf,X,q)),m={startMs:Number(X.startMs),endMs:Number(X.endMs),durationMs:Math.max(1,Number(X.durationMs??Number(X.endMs)-Number(X.startMs)))},s=HJ(M),d={...s,pxPerMinute:Number(X.pxPerMinute??s.pxPerMinute)},a=Math.round(Number(X.chartHeight||360)),I=T.some(OJ);F1(()=>{if(!B||!I)return;let Uf=window.setInterval(()=>Y(Date.now()),1000);return()=>window.clearInterval(Uf)},[B,I]);let ff=Yn(l,_,Array.isArray(y)?y:[]),yf=_.map((Uf)=>String(Uf?.id||"")).filter(Boolean),rf=p.map((Uf)=>String(Uf.nodeId||"")).filter(Boolean),Wf=C.map((Uf)=>String(Uf.nodeId||"")).filter(Boolean),Ef=Array.from(new Set([...ff,...yf,...rf,...Wf])),Gf={startY:0,endY:a,startMs:Number(m.startMs),endMs:Number(m.endMs)},c=Number(N?.endY||0)>0?N:Gf,o=(Uf)=>{return yr(Uf,m,a,X)<=Number(c.endY)&&QJ(Uf,m,a,X)>=Number(c.startY)},e=(Uf)=>{let nf=rl(Uf,m,a,X);return nf>=Number(c.startY)&&nf<=Number(c.endY)},Kf=new Set(Ef.filter((Uf)=>p.some((nf)=>nf.nodeId===Uf&&o(nf))||C.some((nf)=>nf.nodeId===Uf&&e(nf)))),k=z?Ef.filter((Uf)=>Kf.has(Uf)):Ef,Af=`${aF}px ${k.length>0?k.map(()=>`${j1}px`).join(" "):"minmax(160px, 1fr)"}`,Yf=Xf(X.ticks).filter(Pf),Bf=String(G?.mode==="interval"?G?.interval?.procedureRunId||"":""),df=String(G?.mode==="event"?G?.marker?.id||"":""),_0=()=>{let Uf=w.current;if(!Uf){E(Gf);return}let nf=Math.max(0,Uf.scrollTop-dF),Nu=Math.max(120,Uf.clientHeight-dF),Cf=Math.min(a,nf+Nu),d0={startY:nf,endY:Cf,startMs:Number(m.startMs),endMs:Number(m.endMs)},e0=Math.max(0,Math.min(1,nf/a)),Zu=Math.max(e0,Math.min(1,Cf/a)),i0=Math.max(1,Number(m.endMs)-Number(m.startMs));d0.startMs=Number(m.startMs)+i0*e0,d0.endMs=Number(m.startMs)+i0*Zu,E(d0)};F1(()=>{let Uf=w.current,nf=window.setTimeout(_0,0);return Uf?.addEventListener("scroll",_0),window.addEventListener("resize",_0),()=>{window.clearTimeout(nf),Uf?.removeEventListener("scroll",_0),window.removeEventListener("resize",_0)}},[B,m.startMs,m.endMs,a]);let y0=Math.max(0,Ef.length-k.length),N0=new Set(C.filter((Uf)=>k.includes(String(Uf.nodeId||""))&&e(Uf)).map((Uf)=>String(Uf.id))),a0=new Map(C.map((Uf)=>[String(Uf.id),Uf])),pu=Xf(i.arrows).filter((Uf)=>{if(!N0.has(String(Uf.targetMarkerId||"")))return!1;if(String(Uf.action||"")==="observe")return k.includes(String(Uf.sourceNodeId||""));return N0.has(String(Uf.sourceMarkerId||""))}),mu=aF+Math.max(1,k.length)*j1,C0=(Uf)=>{let nf=n_(Uf.target.value);if(typeof J==="function")J(nf);window.setTimeout(_0,0)},Q1=()=>Sn({title:`${l?.id||"pipeline"}-${B||"epoch"}-gantt`,meta:[`run ${B||"--"}`,`${Nf(m.startMs)} -> ${Nf(m.endMs)}`,`duration ${ql(m.durationMs)}`,`${d.label} / ${yJ(d.pxPerMinute)} px/min`,`${k.length}/${Ef.length} nodes`,`${C.length} markers`],visibleNodeIds:k,intervals:p,markers:C.filter((Uf)=>k.includes(String(Uf.nodeId||""))),arrows:pu,ticks:Yf,bounds:m,chartHeight:a,backendLayout:X}),ru=Pf(n?.gantt?.diagnostics)?n.gantt.diagnostics:null;return V(A1,{title:"Epoch 甘特图",eyebrow:`${l?.id||"pipeline"} / ${f.length} epochs`,className:"pipeline-wide-panel",loading:$?.loading,actions:V("div",{className:"pipeline-gantt-actions"},V("select",{value:B,disabled:f.length===0,onChange:(Uf)=>U(Uf.target.value),"data-testid":"pipeline-epoch-select"},f.map((Uf)=>V("option",{key:Uf.runId,value:Uf.runId},JJ(f,Uf)))),V("label",{className:"pipeline-gantt-toggle"},V("input",{type:"checkbox","data-testid":"pipeline-gantt-auto-hide-idle",checked:z,onChange:(Uf)=>{Z(Boolean(Uf.target.checked)),window.setTimeout(_0,0)}}),V("span",null,"自动隐藏空闲列")),V("label",{className:"pipeline-gantt-scale"},V("span",null,V("b",null,"时间尺度"),V("em",{"data-testid":"pipeline-gantt-scale-label"},`${d.label} · ${yJ(d.pxPerMinute)} px/min`)),V("input",{type:"range",min:0,max:100,step:0.01,value:M,onChange:C0,"aria-label":"调整甘特图时间尺度","data-testid":"pipeline-gantt-time-scale"}),V("small",null,V("span",null,"全局"),V("span",null,"细节"))),u?V("button",{type:"button",className:"ghost-btn",onClick:Q1,disabled:k.length===0,"data-testid":"pipeline-export-gantt"},"导出甘特图"):null,u?V(Il,{title:`Pipeline Epoch ${u.runId}`,data:u,onOpen:O,testId:"raw-pipeline-epoch-gantt"}):null)},!u?V(jl,{title:"暂无 Epoch",text:"当前 pipeline 还没有完整运行记录。"}):p.length===0?V(jl,{title:"暂无时间区间",text:"等待 D601 Pipeline backend 在 procedure summary 中返回 startedAt / finishedAt。"}):V("div",{className:"pipeline-gantt-wrap"},V("div",{className:`pipeline-gantt-detail-layout ${P?"detail-open":"detail-collapsed"}`,"data-testid":"pipeline-gantt-detail-layout","data-sidebar-open":P?"true":"false"},V("div",{className:"pipeline-gantt-main"},V("div",{className:"pipeline-gantt-main-head"},V("div",{className:"pipeline-gantt-meta"},V("span",null,`time ${Nf(m.startMs)} -> ${Nf(m.endMs)}`),V("span",null,`duration ${ql(m.durationMs)}`),V("span",null,`scale ${d.label} / ${yJ(d.pxPerMinute)} px/min`),V("span",null,`layout ${D}`),ru?V("span",null,`align ${ru.timeAxisAlignmentOk===!1?"check":"ok"}`):null,V("span",null,`visible ${k.length}/${Ef.length} nodes`),n?V("span",null,`markers ${C.length}`):null,z&&y0>0?V("span",null,`hidden idle ${y0}`):null),!P?V("button",{type:"button",className:"pipeline-sidecar-tab right",disabled:!G?.mode,onClick:()=>h(!0),"data-testid":"pipeline-gantt-sidebar-toggle"},G?.mode?"展开详情":"点击甘特图元素展开详情"):null),V("div",{className:"pipeline-gantt-viewport",ref:w,"data-testid":"pipeline-epoch-gantt","data-pipeline-id":l?.id||"","data-run-id":B,"data-layout-source":D,"data-start-ms":String(m.startMs),"data-end-ms":String(m.endMs),"data-chart-height":String(a)},V("div",{className:"pipeline-gantt-board",style:{gridTemplateColumns:Af,minWidth:`${mu}px`}},V("div",{className:"pipeline-gantt-head time"},"Time"),k.length===0?V("div",{className:"pipeline-gantt-head empty"},"当前时间窗无工作节点"):k.map((Uf)=>V("div",{key:`head-${Uf}`,className:"pipeline-gantt-head node",title:Uf,"data-testid":"pipeline-gantt-head-node","data-node-id":Uf},V(An,{value:Uf}))),V("div",{className:"pipeline-gantt-time-axis",style:{height:`${a}px`}},Yf.map((Uf)=>{let nf=lH(Uf,m,a,X);return V("div",{key:`tick-${Uf.ms}-${nf}`,className:"pipeline-gantt-tick",style:{top:`${nf}px`},"data-testid":"pipeline-gantt-tick","data-ms":String(Uf.ms),"data-y":String(nf)},V("b",null,Nf(Uf.ms)),V("span",null,`+${ql(Number(Uf.offsetMs??Number(Uf.ms)-Number(m.startMs)))}`))})),k.length>0?V("svg",{className:"pipeline-gantt-arrow-layer",width:k.length*j1,height:a,viewBox:`0 0 ${k.length*j1} ${a}`,style:{left:`${aF}px`,top:`${dF}px`,width:`${k.length*j1}px`,height:`${a}px`},"aria-hidden":"true"},V("defs",null,V("marker",{id:"pipeline-gantt-arrowhead",viewBox:"0 0 10 10",refX:9,refY:5,markerWidth:6,markerHeight:6,orient:"auto-start-reverse"},V("path",{d:"M 0 0 L 10 5 L 0 10 z",fill:"context-stroke"}))),pu.map((Uf)=>{let nf=a0.get(String(Uf.targetMarkerId||""));if(!nf)return null;let Nu=a0.get(String(Uf.sourceMarkerId||"")),Cf=String(Nu?.nodeId||Uf.sourceNodeId||""),d0=k.indexOf(Cf),e0=k.indexOf(String(nf.nodeId||""));if(d0<0||e0<0)return null;let Zu=d0*j1+j1/2,i0=e0*j1+j1/2,Du=Nu?rl(Nu,m,a,X):rl(nf,m,a,X),W1=rl(nf,m,a,X);return V("path",{key:Uf.id,className:`pipeline-gantt-arrow ${String(Uf.sourceKind||"").toLowerCase()} ${String(Uf.status||"").toLowerCase()} ${String(Uf.action||"").toLowerCase()}`,d:_H(Zu,Du,i0,W1),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(Du),"data-target-y":String(W1)})})):null,k.length===0?V("div",{className:"pipeline-gantt-empty-col",style:{height:`${a}px`}},"滚动到有活动的时间段后,相关 node 列会自动出现。"):k.map((Uf)=>{let nf=p.filter((Cf)=>Cf.nodeId===Uf),Nu=C.filter((Cf)=>String(Cf.nodeId||"")===Uf);return V("div",{key:`col-${Uf}`,className:"pipeline-gantt-node-col",style:{height:`${a}px`}},nf.map((Cf)=>{let d0=yr(Cf,m,a,X),e0=QJ(Cf,m,a,X),Zu=uH(Cf,m,a,X),i0=String(Cf.procedureRunId||`${Uf}-${Cf.startMs}`);return V("button",{key:i0,type:"button",className:`pipeline-gantt-bar ${Cf.status} ${Cf.live?"live":""} ${Bf===i0?"selected":""}`,style:{top:`${d0}px`,height:`${Zu}px`},title:`${Uf} ${Cf.status} ${Nf(Cf.startedAt||Cf.startMs)} -> ${Nf(Cf.finishedAt||Cf.endMs)}`,onClick:()=>Q(Cf),"data-testid":"pipeline-gantt-line","data-node-id":Uf,"data-procedure-run-id":String(Cf.procedureRunId||""),"data-status":String(Cf.status||""),"data-live":Cf.live?"true":"false","data-start-ms":String(Cf.startMs||""),"data-end-ms":String(Cf.endMs||""),"data-y1":String(d0),"data-y2":String(e0),"data-natural-height":String(Math.max(0,e0-d0))},V("strong",null,Cf.status||"working"),V("span",null,ql(Cf.durationMs)))}),Nu.map((Cf)=>V("button",{key:Cf.id,type:"button",className:`pipeline-gantt-marker ${Cf.kind} ${Cf.tone||""} ${Cf.status||""} ${df===String(Cf.id)?"selected":""}`,style:{top:`${rl(Cf,m,a,X)}px`},title:`${Cf.label||"event"} / ${Nf(Cf.timestampIso||Cf.timestamp||Cf.ms)}`,onClick:()=>W(Cf),"data-testid":Cf.kind==="prompt"?"pipeline-gantt-prompt-marker":"pipeline-gantt-control-marker","data-marker-id":String(Cf.id||""),"data-ms":String(Cf.ms??Cf.eventMs??""),"data-y":String(rl(Cf,m,a,X))})))})))),P?V(jn,{selection:G,runDetails:$,nodeDetails:r,nodeDetailsState:j,onRaw:O,onCollapse:()=>h(!1)}):null)))}function R1(){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 $J(){return{runId:"",loading:!1,error:"",details:null,fetchedAt:null}}function W4(f,u){return`${f}/microservices/pipeline/proxy${u}`}function rS({activeRun:f,pipelineRuns:u,selectedRunId:l,onRunChange:_,selectedNodeId:y,selectedNodeConfig:$,selectedNodeRuntime:r,control:j,onControlChange:A,onFetch:J,onAction:U,onRaw:Q,onCollapse:W}){let G=String(f?.runId||""),K=String(r?.status||"pending"),H=!G||!y||j.loading||Boolean(j.actionLoading),O=(Z)=>(N)=>A({[Z]:N.target.value,error:"",message:""}),z=u.length>0?u:f?[f]:[];return V("aside",{className:"pipeline-node-control","data-testid":"pipeline-node-control"},V("div",{className:"pipeline-node-control-head"},V("div",null,V("p",{className:"panel-eyebrow"},"Manual Node Control"),V(j0,{title:y||"点击控制图中的 node",level:3,loading:j.loading||Boolean(j.actionLoading)})),V("div",{className:"pipeline-node-control-head-actions"},y?V(P_,{status:K},K):V(P_,{status:"pending"},"idle"),V("button",{type:"button",className:"ghost-btn mini",onClick:W,"data-testid":"pipeline-node-sidebar-collapse"},"收起"))),V("div",{className:"pipeline-control-runbar"},V("label",null,V("span",null,"目标 run"),V("select",{value:G||l,disabled:z.length===0,onChange:(Z)=>_(Z.target.value),"data-testid":"pipeline-node-run-select"},z.map((Z)=>V("option",{key:Z.runId,value:Z.runId},`${Z.runId||"--"} / ${Z.status||"--"}`)))),V("button",{type:"button",className:"ghost-btn",disabled:H,onClick:J,"data-testid":"pipeline-node-fetch"},j.loading?"抓取中":"抓取过程"),j.details?V(Il,{title:`Pipeline Node ${y}`,data:j.details,onOpen:Q,testId:"raw-pipeline-node-control"}):null),V("div",{className:"pipeline-control-meta"},V("span",null,V("b",null,"kind"),String($?.kind||"--")),V("span",null,V("b",null,"procedure"),String(r?.currentProcedureRunId||"--")),V("span",null,V("b",null,"attempts"),String(r?.attempts??"--")),V("span",null,V("b",null,"updated"),Nf(f?.updatedAt))),!y?V(jl,{title:"未选择 node",text:"点击 React Flow 控制图中的任意 node 后,可抓取执行过程、追加 prompt、下发引导、增量修改、审核通过或重做。"}):null,V(A0,{error:j.error,wide:!0}),V("div",{className:"pipeline-control-actions"},V("label",null,V("span",null,"实时追加 prompt(仅 running node)"),V("textarea",{value:j.appendPrompt,onChange:O("appendPrompt"),placeholder:"让当前执行中的 agent 继续、补充检查或调整当前步骤...",rows:4,disabled:!y,"data-testid":"pipeline-node-append-input"}),V("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")),V("label",null,V("span",null,"下次尝试引导 prompt"),V("textarea",{value:j.guidePrompt,onChange:O("guidePrompt"),placeholder:"给该 node 下一次 attempt 的执行提示;不会立即打断当前 session。",rows:4,disabled:!y,"data-testid":"pipeline-node-guide-input"}),V("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")),V("label",null,V("span",null,"完成后增量修改 prompt"),V("textarea",{value:j.modifyPrompt,onChange:O("modifyPrompt"),placeholder:"在该 node 已完成结果基础上追加修改要求;runner 会重跑目标 node,并保留同 node 既有 OA 输出作为上下文。",rows:4,disabled:!y,"data-testid":"pipeline-node-modify-input"}),V("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")),V("label",null,V("span",null,"Monitor 审核通过原因"),V("textarea",{value:j.approveReason,onChange:O("approveReason"),placeholder:"当流程配置开启 monitor 审核时,记录审核通过原因并释放后续 node。",rows:3,disabled:!y,"data-testid":"pipeline-node-approve-input"}),V("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"?"提交中":"审核通过")),V("label",null,V("span",null,"重做 / restart 原因"),V("textarea",{value:j.redoReason,onChange:O("redoReason"),placeholder:"说明为什么需要重做;runner 会重置目标 node 以及非 rework 下游 node。",rows:4,disabled:!y,"data-testid":"pipeline-node-redo-input"}),V("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"))),V("div",{className:"pipeline-control-evidence"},V("strong",null,"Node 过程索引"),V(uS,{details:j.details,selectedNodeId:y,selectedNodeRuntime:r,control:j,onRaw:Q})))}function yH({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let _=f.find((uf)=>uf.id==="pipeline")||null,[y,$]=bu({loading:!1,error:"",health:null,snapshot:null,oaDiagnostics:null,minimaxQuota:null,refreshedAt:null}),[r,j]=bu(""),[A,J]=bu(""),[U,Q]=bu(""),[W,G]=bu(R1()),[K,H]=bu({}),[O,z]=bu(D_()),[Z,N]=bu($J()),[E,q]=bu(O4),[Y,w]=bu(!1),[B,P]=bu(!1),h=T_(0),{addNotification:M}=Lu(),n=T_(!1),S=T_(0),T=T_(""),i=T_({}),C=T_(""),v=T_("");async function X(uf={}){let wf=uf.silent===!0;if(!_)return;if(n.current)return;n.current=!0;let Hf=h.current+1;if(h.current=Hf,!wf)$((gf)=>({...gf,loading:!0,error:""}));try{let gf=`__unideskArrayLimit=registry.components:80,runs:${pP}`,[pf,Q0,u0]=await Promise.all([w_(`${l}/microservices/pipeline/proxy/api/snapshot?${gf}`,{cache:"no-store"}),w_(`${l}/microservices/pipeline/proxy/api/oa-event-flow/diagnostics`,{cache:"no-store"}).catch((Bl)=>({ok:!1,error:Df(Bl,"OA event flow diagnostics failed")})),w_(`${l}/microservices/pipeline/proxy/api/model-quota/minimax`,{cache:"no-store"}).catch((Bl)=>({ok:!1,error:Df(Bl,"MiniMax quota failed")}))]);if(Hf!==h.current)return;let fu={ok:pf?.ok!==!1,service:"pipeline-v2-control snapshot"};$({loading:!1,error:"",health:fu,snapshot:pf,oaDiagnostics:Q0,minimaxQuota:u0,refreshedAt:new Date})}catch(gf){if(Hf!==h.current)return;$((pf)=>({...pf,loading:!1,error:Df(gf,"Pipeline 加载失败")}))}finally{n.current=!1}}F1(()=>{if(X(),!_)return;let uf=()=>{if(g5())X({silent:!0})},wf=window.setInterval(()=>{uf()},WE),Hf=()=>{if(g5())uf()};return document.addEventListener("visibilitychange",Hf),()=>{window.clearInterval(wf),document.removeEventListener("visibilitychange",Hf)}},[_?.id,_?.runtime?.providerStatus,l]);let D=Fn(_),p=Un(_),m=Jn(_),s=y.snapshot||{},d=y.oaDiagnostics||null,a=y.minimaxQuota||null,{components:I,pipelines:ff,runs:yf}=Qn(s),rf=String(yf[0]?.pipelineId||""),Wf=(rf?ff.find((uf)=>String(uf.id||"")===rf):null)||ff[0]||{},Ef=ff.find((uf)=>String(uf.id||"")===r)||Wf,Gf=String(Ef.id||""),c=mE(Ef),o=NJ(Ef),e=DE(yf,Gf),Kf=cn(yf,Gf),k=Kf.find((uf)=>String(uf?.runId||"")===A)||e,Af=String(Z.runId||"")===String(k?.runId||"")?Kn(Z.details):null,Yf=Nn(k,Af),Bf=String(Yf?.runId||""),df=c.find((uf)=>String(uf?.id||"")===U)||null,_0=U?gE(Yf,U):null,y0=zn(yf),N0=On(I),a0=Number(y.health?.components)||VE(s,"registry.components",I.length),pu=VE(s,"runs",yf.length),mu=XE(Ef,Yf,I),C0={nodes:mu.nodes.map((uf)=>uf.id===U?{...uf,selected:!0,className:`${uf.className||""} selected-control-node`}:uf),edges:mu.edges},Q1=ff.map((uf)=>{let wf=String(uf.id||"pipeline"),Hf=DE(yf,wf);return{title:`${wf}-${Hf?.runId||"snapshot"}`,flow:XE(uf,Hf,I)}}),ru=String(O?.runId||Bf||""),Uf=String(O?.interval?.nodeId||O?.marker?.nodeId||""),nf=ru&&Uf?K[_J(ru,Uf)]||null:null,Nu=e5(W.details,ru,Uf),Cf=e5(nf?.details,ru,Uf)||Nu,d0=ru&&Uf?{...Pf(nf)?nf:{},runId:ru,nodeId:Uf,details:Cf,loading:Boolean(nf?.loading)||!Cf&&Boolean(W.loading)&&U===Uf,error:String(nf?.error||""),fetchedAt:nf?.fetchedAt||(Nu?W.fetchedAt:null)}:null,e0=Kf.map((uf)=>String(uf?.runId||"")).filter(Boolean).join("|"),Zu=c.map((uf)=>String(uf?.id||"")).filter(Boolean).join("|");F1(()=>{C.current=U},[U]),F1(()=>{v.current=Bf},[Bf]),F1(()=>{if(!A||e0.split("|").includes(A))return;J("")},[A,e0]),F1(()=>{if(!U||Zu.split("|").includes(U))return;Q(""),G(R1()),z(D_()),w(!1),P(!1)},[U,Zu]),F1(()=>{if(!U)w(!1)},[U]),F1(()=>{if(!O.mode)P(!1)},[O.mode]);async function i0(uf=Bf,wf={}){if(!uf){N($J());return}let Hf=n_(wf.scale??E??O4),gf=`${uf}:timeline`;if(T.current===gf)return;T.current=gf;let pf=wf.silent===!0,Q0=S.current+1;S.current=Q0,N((u0)=>({runId:uf,scale:Hf,loading:!pf||String(u0.runId||"")!==uf||!u0.details,error:"",details:pf&&u0.runId===uf?u0.details:u0.runId===uf?u0.details:null,fetchedAt:u0.runId===uf?u0.fetchedAt:null}));try{let[u0,fu]=await Promise.all([w_(W4(l,`/api/node-control/runs/${encodeURIComponent(uf)}?tail=160&view=timeline`),{cache:"no-store",strictJson:!0}),w_(W4(l,`/api/runs/${encodeURIComponent(uf)}`),{cache:"no-store"}).catch((Bl)=>({ok:!1,runSummaryError:Df(Bl,"抓取评分失败")}))]);if(Q0!==S.current)return;N({runId:uf,scale:Hf,loading:!1,error:"",details:{...u0,run:Pf(fu?.run)?fu.run:void 0,runSummaryError:fu?.runSummaryError},fetchedAt:new Date})}catch(u0){if(Q0!==S.current)return;N((fu)=>({runId:uf,scale:Hf,loading:!1,error:Df(u0,"抓取 epoch 执行过程失败"),details:fu.runId===uf?fu.details:null,fetchedAt:fu.runId===uf?fu.fetchedAt:null}))}finally{if(T.current===gf)T.current=""}}function Du(uf,wf,Hf){let gf=_J(uf,wf);H((pf)=>{let Q0={...pf,[gf]:{...Pf(pf?.[gf])?pf[gf]:{},runId:uf,nodeId:wf,...Hf}},u0=Object.keys(Q0);if(u0.length>32)for(let fu of u0.slice(0,u0.length-32))delete Q0[fu];return Q0})}async function W1(uf,wf){if(!uf||!wf)return;let Hf=_J(uf,wf),gf=Number(i.current?.[Hf]||0)+1;i.current={...i.current,[Hf]:gf},Du(uf,wf,{loading:!0,error:""});try{let pf=await w_(W4(l,`/api/node-control/runs/${encodeURIComponent(uf)}/nodes/${encodeURIComponent(wf)}?tail=160`),{cache:"no-store",strictJson:!0});if(Number(i.current?.[Hf]||0)!==gf)return;let Q0=new Date;if(Du(uf,wf,{loading:!1,details:pf,fetchedAt:Q0,error:""}),C.current===wf&&v.current===uf)G((u0)=>({...u0,loading:!1,details:pf,fetchedAt:Q0,error:""}))}catch(pf){if(Number(i.current?.[Hf]||0)!==gf)return;Du(uf,wf,{loading:!1,error:Df(pf,"抓取 Gantt node 详情失败")})}}F1(()=>{if(!Bf){N($J());return}i0(Bf);let uf=()=>{if(g5())i0(Bf,{silent:!0})},wf=window.setInterval(()=>{uf()},WE),Hf=()=>{if(g5())uf()};return document.addEventListener("visibilitychange",Hf),()=>{window.clearInterval(wf),document.removeEventListener("visibilitychange",Hf)}},[Bf,l]);async function pl(uf=Bf,wf=U){if(!uf||!wf){G((Hf)=>({...Hf,error:"请先选择 run 和 node",message:""}));return}G((Hf)=>({...Hf,loading:!0,error:"",message:""}));try{let Hf=await w_(W4(l,`/api/node-control/runs/${encodeURIComponent(uf)}/nodes/${encodeURIComponent(wf)}?tail=160`),{cache:"no-store",strictJson:!0}),gf=new Date;G((pf)=>({...pf,loading:!1,details:Hf,fetchedAt:gf,error:""})),Du(uf,wf,{loading:!1,details:Hf,fetchedAt:gf,error:""})}catch(Hf){G((gf)=>({...gf,loading:!1,error:Df(Hf,"抓取 node 执行过程失败")}))}}async function R_(uf){let wf=String(uf?.runId||Bf||""),Hf=String(uf?.nodeId||"");if(z({mode:"interval",runId:wf,interval:uf,marker:null}),P(!0),!wf||!Hf)return;if(wf!==Bf)J(wf);Q(Hf),G(R1()),i0(wf,{silent:!0}),W1(wf,Hf)}async function x_(uf){let wf=String(uf?.runId||Bf||""),Hf=String(uf?.nodeId||"");if(z({mode:"event",runId:wf,interval:null,marker:uf}),P(!0),!wf)return;if(wf!==Bf)J(wf);if(i0(wf,{silent:!0}),!Hf)return;Q(Hf),G(R1()),W1(wf,Hf)}async function v0(uf){if(!Bf||!U){G((gf)=>({...gf,error:"请先选择 run 和 node",message:""}));return}let wf=uf==="append"?"prompts":uf,Hf=uf==="append"?W.appendPrompt:uf==="guide"?W.guidePrompt:uf==="modify"?W.modifyPrompt:uf==="approve"?W.approveReason:W.redoReason;if(!String(Hf||"").trim()){G((gf)=>({...gf,error:"操作内容不能为空",message:""}));return}G((gf)=>({...gf,actionLoading:uf,error:"",message:""}));try{let gf=uf==="redo"||uf==="approve"?{reason:Hf,source:"unidesk-frontend",sourceKind:"webui"}:{prompt:Hf,source:"unidesk-frontend",sourceKind:"webui"},pf=await w_(W4(l,`/api/node-control/runs/${encodeURIComponent(Bf)}/nodes/${encodeURIComponent(U)}/${wf}`),{method:"POST",body:JSON.stringify(gf)});if(G((u0)=>({...u0,actionLoading:"",details:pf,fetchedAt:new Date,appendPrompt:uf==="append"?"":u0.appendPrompt,guidePrompt:uf==="guide"?"":u0.guidePrompt,modifyPrompt:uf==="modify"?"":u0.modifyPrompt,approveReason:uf==="approve"?"":u0.approveReason,redoReason:uf==="redo"?"":u0.redoReason,message:uf==="append"?"已追加到运行中 node":uf==="guide"?"已下发 guide,等待 runner 处理":uf==="modify"?"已排队增量修改命令":uf==="approve"?"已提交审核通过决策":"已排队重做命令"})),M("success",uf==="append"?"已追加到运行中 node":uf==="guide"?"已下发 guide,等待 runner 处理":uf==="modify"?"已排队增量修改命令":uf==="approve"?"已提交审核通过决策":"已排队重做命令"),await pl(Bf,U),await i0(Bf,{silent:!0}),uf!=="append")await X()}catch(gf){G((pf)=>({...pf,actionLoading:"",error:Df(gf,"node 控制操作失败")}))}}if(!_)return V(jl,{title:"Pipeline 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=pipeline"});return V("div",{className:"pipeline-page","data-testid":"pipeline-page"},V(A1,{title:"Pipeline v2 工作台",eyebrow:"D601 Snapshot 用户服务",loading:y.loading,actions:V("div",{className:"panel-actions"},V("button",{type:"button",className:"ghost-btn",onClick:X,disabled:y.loading,"data-testid":"pipeline-refresh-button"},y.loading?"刷新中":"刷新"),V(Il,{title:"Pipeline 用户服务",data:_,onOpen:u,testId:"raw-pipeline-service"}))},V("div",{className:"pipeline-hero"},V("div",null,V("div",{className:"node-version-line"},V(P_,{status:D.providerStatus==="online"?"online":"warn"},D.providerStatus||"unknown"),V("span",null,_.providerId),V("span",null,m.public?"公网暴露":"仅 UniDesk frontend 代理访问")),V("p",{className:"muted paragraph"},_.description)),V("div",{className:"microservice-ref-card"},V("span",null,"Repo"),V("strong",null,p.url||"--"),V("code",null,p.commitId||"--")),V("div",{className:"microservice-ref-card"},V("span",null,"D601 Docker"),V("strong",null,`${m.nodeBindHost||"--"}:${m.nodePort||"--"}`),V("code",null,`${p.composeFile||"--"} / ${p.composeService||"--"}`))),V(A0,{error:y.error,wide:!0})),V("div",{className:"pipeline-grid"},V(A1,{title:"控制图",eyebrow:`${Ef.id||"pipeline"} / run ${Yf?.status||"--"}`,className:"pipeline-wide-panel",loading:y.loading,actions:V("div",{className:"pipeline-toolbar"},V("select",{value:Gf,disabled:ff.length===0,onChange:(uf)=>{j(uf.target.value),J(""),Q(""),G(R1()),z(D_()),w(!1),P(!1)},"data-testid":"pipeline-select"},ff.map((uf)=>V("option",{key:uf.id,value:uf.id},uf.id||uf.key))),V("select",{value:Bf,disabled:Kf.length===0,onChange:(uf)=>{if(J(uf.target.value),G(R1()),z(D_()),w(!1),P(!1),U)pl(uf.target.value,U)},"data-testid":"pipeline-run-select"},Kf.map((uf)=>V("option",{key:uf.runId,value:uf.runId},JJ(Kf,uf)))),V("button",{type:"button",className:"ghost-btn",disabled:C0.nodes.length===0,onClick:()=>sE(C0,`${Ef.id||"pipeline"}-${Yf?.runId||"snapshot"}`),"data-testid":"pipeline-export-graph"},"导出渲染图"),V("button",{type:"button",className:"ghost-btn",disabled:Q1.every((uf)=>uf.flow.nodes.length===0),onClick:()=>Cn(Q1),"data-testid":"pipeline-export-all-graphs"},"批量导出"))},c.length===0?V(jl,{title:"暂无控制图",text:"等待 D601 pipeline backend 返回 config.nodes / config.edges"}):V("div",{className:`pipeline-control-shell ${Y?"detail-open":"detail-collapsed"}`,"data-testid":"pipeline-control-shell","data-sidebar-open":Y?"true":"false"},V("div",{className:"pipeline-flow-frame","data-testid":"pipeline-react-flow"},V(rE,{nodes:C0.nodes,edges:C0.edges,nodeTypes:sP,edgeTypes:tP,fitView:!0,fitViewOptions:{padding:0.18},nodesDraggable:!1,nodesConnectable:!1,elementsSelectable:!0,minZoom:0.25,maxZoom:1.4,proOptions:{hideAttribution:!0},onNodeClick:(uf,wf)=>{let Hf=String(wf.id);if(Q(Hf),G(R1()),w(!0),Bf)pl(Bf,Hf)}},V(AE,{gap:22,size:1,color:"rgba(215, 161, 58, 0.24)"}),V(JE,{showInteractive:!1})),!Y?V("button",{type:"button",className:"pipeline-sidecar-tab right",disabled:!U,onClick:()=>w(!0),"data-testid":"pipeline-node-sidebar-toggle"},U?"展开 node 控制":"点击 node 展开控制"):null),Y?V(rS,{activeRun:Yf,pipelineRuns:Kf,selectedRunId:A,onRunChange:(uf)=>{if(J(uf),G(R1()),z(D_()),U)pl(uf,U)},selectedNodeId:U,selectedNodeConfig:df,selectedNodeRuntime:_0,control:W,onControlChange:(uf)=>G((wf)=>({...wf,...uf})),onFetch:()=>pl(),onAction:v0,onRaw:u,onCollapse:()=>w(!1)}):null),V("div",{className:"pipeline-flow-summary"},V("span",null,`${C0.nodes.length} nodes`),V("span",null,`${C0.edges.length} edges`),V("span",null,`${ff.length} pipelines`),V("span",null,`source config+components(${I.length})`),V("span",null,`run ${Yf?.runId||"--"}`),V("span",null,`score ${FJ(Yf)}`),V("span",null,U?`selected ${U}`:"click node to control"))),V($S,{epochs:Kf,activeRun:Yf,activePipeline:Ef,pipelineNodes:c,pipelineEdges:o,selection:O,detailOpen:B,onDetailOpenChange:P,runDetails:Z,nodeDetails:Cf,nodeDetailsState:d0,ganttScale:E,onGanttScaleChange:q,onIntervalSelect:R_,onMarkerSelect:x_,onRunChange:(uf)=>{if(J(uf),G(R1()),z(D_()),P(!1),U)pl(uf,U)},onRaw:u}),V(A1,{title:"观测指标",eyebrow:y.refreshedAt?`Updated ${W0(y.refreshedAt)}`:"Snapshot",loading:y.loading},V("div",{className:"metric-grid"},V(wu,{label:"Health",value:y.health?.ok?"OK":"--",hint:y.health?.service||"D601 /health",tone:y.health?.ok?"ok":"warn"}),V(wu,{label:"组件",value:a0,hint:"components registry",tone:s?.registry?.ok===!1?"warn":"ok"}),V(wu,{label:"Pipeline",value:ff.length,hint:`${c.length} nodes / ${o.length} edges`}),V(wu,{label:"运行记录",value:pu,hint:`${y0.succeeded||0} succeeded / ${y0.running||0} running`}),V(wu,{label:"OA 记录",value:Array.isArray(e?.submissions)?e.submissions.length:0,hint:e?.runId||"latest run"}),V(wu,{label:"Procedure",value:Array.isArray(e?.procedureRuns)?e.procedureRuns.length:0,hint:e?.status||"no run"}),V(wu,{label:"Score",value:FJ(Yf),hint:Yf?.runId||"selected epoch",tone:EJ(Yf)})),V("div",{className:"panel-actions inline-actions"},V(Il,{title:"Pipeline Snapshot",data:s,onOpen:u,testId:"raw-pipeline-snapshot"}))),V(A1,{title:"评分器",eyebrow:Yf?.runId||"selected epoch",loading:y.loading},V(Hn,{run:Yf,onRaw:u})),V(A1,{title:"MiniMax 限额",eyebrow:"model/minimax-m27 quota",loading:y.loading},V(yS,{quota:a,onRaw:u})),V(A1,{title:"OA 事件流",eyebrow:"100% event-driven diagnostics",className:"pipeline-wide-panel",loading:y.loading},V(_S,{diagnostics:d,onRaw:u})),V(A1,{title:"组件矩阵",eyebrow:`${N0.length} classes`,loading:y.loading},N0.length===0?V(jl,{title:"暂无组件",text:"等待 D601 pipeline backend 返回 registry.components"}):V("div",{className:"component-strata"},N0.map((uf)=>V("article",{key:uf.name,className:"component-stratum"},V("span",null,uf.name),V("strong",null,uf.count)))),V("div",{className:"pipeline-component-list"},I.slice(0,12).map((uf)=>V("span",{key:uf.key,className:"data-chip"},V("b",null,uf.componentClass||"--"),V("span",null,uf.id||uf.key||"--"))))),V(A1,{title:"Epoch 列表",eyebrow:`${Kf.length}/${pu} preview`,loading:y.loading},Kf.length===0?V(jl,{title:"暂无运行记录",text:"当前 pipeline 在 .state/pipeline-runs 中还没有 epoch。"}):V("div",{className:"pipeline-run-list"},Kf.map((uf)=>{let wf=String(uf?.runId||"")===Bf?Yf:uf;return V("article",{key:uf.runId,className:`pipeline-run-card ${String(uf.runId||"")===Bf?"active":""}`,role:"button",tabIndex:0,onClick:()=>{J(String(uf.runId||"")),z(D_())},onKeyDown:(Hf)=>{if(Hf.key==="Enter"||Hf.key===" ")J(String(uf.runId||"")),z(D_())}},V("div",{className:"node-card-head"},V("strong",null,JJ(Kf,uf)),V(P_,{status:uf.status},uf.status||"--")),V("div",{className:"docker-meta compact"},V("span",null,wf?.pipelineId||"--"),V("span",null,`nodes ${Array.isArray(wf?.nodes)?wf.nodes.length:0}`),V("span",null,`oa ${Array.isArray(wf?.submissions)?wf.submissions.length:0}`),V("span",null,`procedures ${Array.isArray(wf?.procedureRuns)?wf.procedureRuns.length:0}`),V(En,{run:wf})),V("p",{className:"muted paragraph"},d5(wf?.task)),V("span",{className:"pipeline-run-time"},Nf(wf?.updatedAt)))}))),V(A1,{title:"运行材料索引",eyebrow:Yf?.runId||"selected epoch",className:"pipeline-wide-panel",loading:y.loading},V(lS,{activeRun:Yf,onRaw:u}))))}var Jr=cf(O0(),1);var $f=Jr.default.createElement,{useEffect:jS}=Jr.default,Ar=Jr.default.useState,qJ={id:"",sequenceNo:"",contractNo:"",name:"",currentStatus:"",pending:"",paymentStatus:"",notes:""};function AS({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return $f("span",{className:`status-badge ${l}`},u||f||"unknown")}function Fr({label:f,value:u,hint:l,tone:_}){return $f("article",{className:`metric-card ${_||""}`},$f("div",{className:"metric-label"},f),$f("div",{className:"metric-value"},u),$f("div",{className:"metric-hint"},l))}function LJ({title:f,eyebrow:u,actions:l,children:_,className:y,loading:$}){return $f("section",{className:`panel ${y||""}`},$f("div",{className:"panel-head"},$f("div",null,u?$f("p",{className:"panel-eyebrow"},u):null,$f(j0,{title:f,loading:$})),l?$f("div",{className:"panel-actions"},l):null),$f("div",{className:"panel-body"},_))}function $H({title:f,data:u,onOpen:l,testId:_}){return $f("button",{type:"button",className:"ghost-btn","data-testid":_,onClick:()=>l(f,u)},"查看原始JSON")}function rH({title:f,text:u}){return $f("div",{className:"empty-state"},$f("strong",null,f),$f("span",null,u))}function FS(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function JS(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function US(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function u3(f,u){return`${f}/microservices/project-manager/proxy${u}`}function QS(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 WS(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 XJ(f){return String(f||"item").replace(/[^A-Za-z0-9_-]+/g,"-")}function zS(f){let u=new Uint8Array(f),l="",_=32768;for(let y=0;y$f("tr",{key:y.id,className:u===y.id?"active-row":"","data-testid":`project-manager-row-${XJ(y.id)}`},$f("td",null,y.sequenceNo??"--"),$f("td",null,$f("strong",null,y.contractNo||"--"),$f("code",null,y.id||"--")),$f("td",null,$f("strong",null,y.name||"--"),$f("span",{className:"muted block"},y.sourceFile||"--")),$f("td",null,y.currentStatus||"--"),$f("td",null,$f("span",{className:"preline"},y.pending||"--")),$f("td",null,$f(AS,{status:Number(y.paymentRatio||0)>=1?"online":"warn"},y.paymentStatus||"--")),$f("td",null,y.notes||"--"),$f("td",null,$f("div",{className:"inline-actions"},$f("button",{type:"button",className:"ghost-btn",onClick:()=>l(y),"data-testid":`project-manager-edit-${XJ(y.id)}`},"编辑"),$f($H,{title:`Project ${y.contractNo||y.id}`,data:y,onOpen:_,testId:`raw-project-${XJ(y.id)}`}))))))))}function jH({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let _=f.find((B)=>B.id==="project-manager")||null,[y,$]=Ar({loading:!1,saving:!1,importing:!1,exporting:!1,error:"",notice:"",health:null,list:null,refreshedAt:null}),[r,j]=Ar({...qJ}),[A,J]=Ar(""),[U,Q]=Ar("all"),{addNotification:W}=Lu();async function G(B=A,P=U){if(!_)return;$((h)=>({...h,loading:!0,error:""}));try{let h=new URLSearchParams({pageSize:"200",status:P});if(B.trim())h.set("q",B.trim());let[M,n]=await Promise.all([Mf(`${l}/microservices/project-manager/health`),Mf(u3(l,`/api/projects?${h.toString()}`))]);$((S)=>({...S,loading:!1,health:M,list:n,refreshedAt:new Date,error:""}))}catch(h){$((M)=>({...M,loading:!1,error:Df(h,"Project Manager 加载失败")}))}}jS(()=>{G()},[_?.id,_?.runtime?.providerStatus]);async function K(B){B.preventDefault(),$((P)=>({...P,saving:!0,error:"",notice:""}));try{let P=WS(r);if(r.id)await Mf(u3(l,`/api/projects/${encodeURIComponent(r.id)}`),{method:"PUT",body:JSON.stringify(P)});else await Mf(u3(l,"/api/projects"),{method:"POST",body:JSON.stringify(P)});let h=r.id?"项目已更新":"项目已创建";$((M)=>({...M,saving:!1,notice:h})),W("success",h),await G()}catch(P){$((h)=>({...h,saving:!1,error:Df(P,"保存项目失败")}))}}async function H(){if(!r.id)return;if(!window.confirm(`删除项目 ${r.contractNo||r.name||r.id} ?`))return;$((B)=>({...B,saving:!0,error:"",notice:""}));try{await Mf(u3(l,`/api/projects/${encodeURIComponent(r.id)}`),{method:"DELETE"}),j({...qJ});let B="项目已删除";$((P)=>({...P,saving:!1,notice:B})),W("success",B),await G()}catch(B){$((P)=>({...P,saving:!1,error:Df(B,"删除项目失败")}))}}async function O(B){let P=B.target.files?.[0];if(!P)return;$((h)=>({...h,importing:!0,error:"",notice:""}));try{let h=zS(await P.arrayBuffer()),n=`Excel 已导入 ${(await Mf(u3(l,"/api/import/excel"),{method:"POST",body:JSON.stringify({fileName:P.name,contentBase64:h,replace:!1})})).imported||0} 条项目`;$((S)=>({...S,importing:!1,notice:n})),W("success",n),B.target.value="",await G()}catch(h){$((M)=>({...M,importing:!1,error:Df(h,"Excel 导入失败")}))}}async function z(){$((B)=>({...B,exporting:!0,error:""}));try{let B=await nz(u3(l,"/api/projects/export.xlsx")),P=URL.createObjectURL(B),h=document.createElement("a");h.href=P,h.download=`project-manager-${UU()}.xlsx`,document.body.appendChild(h),h.click(),h.remove(),URL.revokeObjectURL(P),$((M)=>({...M,exporting:!1,notice:"Excel 已导出"}))}catch(B){$((P)=>({...P,exporting:!1,error:Df(B,"Excel 导出失败")}))}}if(!_)return $f(rH,{title:"Project Manager 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=project-manager"});let Z=FS(_),N=US(_),E=JS(_),q=Array.isArray(y.list?.projects)?y.list.projects:[],Y=y.list?.summary||{},w=y.health||{};return $f("div",{className:"project-manager-page","data-testid":"project-manager-page"},$f(LJ,{title:"项目管理工作台",eyebrow:"Main Server PostgreSQL 用户服务",loading:y.loading||y.exporting,actions:$f("div",{className:"panel-actions"},$f("button",{type:"button",className:"ghost-btn",disabled:y.loading,onClick:()=>G(),"data-testid":"project-manager-refresh-button"},y.loading?"刷新中":"刷新"),$f("button",{type:"button",className:"ghost-btn",disabled:y.exporting,onClick:z,"data-testid":"project-manager-export-button"},y.exporting?"导出中":"导出 Excel"),$f($H,{title:"Project Manager 用户服务",data:_,onOpen:u,testId:"raw-project-manager-service"}))},$f("div",{className:"project-manager-hero"},$f(Fr,{label:"项目总数",value:Y.total??q.length,hint:`PG 表 ${w.storage?.table||"project_manager_projects"}`,tone:"ok"}),$f(Fr,{label:"进行中",value:Y.active??"--",hint:"当前状态未完全完成"}),$f(Fr,{label:"已完成",value:Y.completed??"--",hint:"按 完成 关键字统计",tone:"ok"}),$f(Fr,{label:"未全款",value:Y.unpaid??"--",hint:"付款比例 < 1",tone:Number(Y.unpaid||0)>0?"warn":"ok"})),$f(A0,{error:y.error}),y.notice?$f("div",{className:"form-success"},y.notice):null),$f("div",{className:"project-manager-hero"},$f("div",{className:"microservice-ref-card"},$f("span",null,"Repo"),$f("strong",null,N.url||"--"),$f("code",null,N.commitId||"--")),$f("div",{className:"microservice-ref-card"},$f("span",null,"Main Server Docker"),$f("strong",null,`${E.nodeBindHost||"--"}:${E.nodePort||"--"}`),$f("code",null,`${N.composeService||"--"} / ${N.containerName||"--"}`)),$f("div",{className:"microservice-ref-card"},$f("span",null,"Runtime"),$f("strong",null,Z.providerName||_.providerId),$f("code",null,`Health ${w.ok?"OK":"--"} / ${y.refreshedAt?W0(y.refreshedAt):"--"}`)),$f("div",{className:"microservice-ref-card"},$f("span",null,"Import Source"),$f("strong",null,"D601 WeChat Excel"),$f("code",null,"合作项目列表_I_20260309.xlsx"))),$f("div",{className:"project-manager-layout"},$f(LJ,{title:"项目清单",eyebrow:"CRUD + Excel Export",loading:y.loading||y.importing||y.exporting,actions:$f("div",{className:"inline-actions project-manager-filters"},$f("input",{value:A,onChange:(B)=>J(B.target.value),placeholder:"搜索合同号 / 项目名称 / 状态","data-testid":"project-manager-search"}),$f("select",{value:U,onChange:(B)=>{Q(B.target.value),G(A,B.target.value)},"data-testid":"project-manager-status-filter"},$f("option",{value:"all"},"全部"),$f("option",{value:"active"},"进行中"),$f("option",{value:"completed"},"已完成"),$f("option",{value:"unpaid"},"未全款")),$f("button",{type:"button",className:"ghost-btn",onClick:()=>G(A,U)},"筛选"))},$f(GS,{projects:q,activeId:r.id,onSelect:(B)=>j(QS(B)),onRaw:u})),$f(LJ,{title:r.id?"编辑项目":"新建项目",eyebrow:"PostgreSQL Write Path",loading:y.saving||y.importing},$f("form",{className:"stack-form project-manager-form",onSubmit:K,"data-testid":"project-manager-form"},r.id?$f("label",null,"项目 ID",$f("input",{value:r.id,disabled:!0})):null,$f("label",null,"序号",$f("input",{type:"number",value:r.sequenceNo,onChange:(B)=>j((P)=>({...P,sequenceNo:B.target.value}))})),$f("label",null,"合同号",$f("input",{value:r.contractNo,onChange:(B)=>j((P)=>({...P,contractNo:B.target.value})),required:!0})),$f("label",null,"项目名称",$f("input",{value:r.name,onChange:(B)=>j((P)=>({...P,name:B.target.value})),required:!0})),$f("label",null,"当前状况",$f("textarea",{value:r.currentStatus,onChange:(B)=>j((P)=>({...P,currentStatus:B.target.value}))})),$f("label",null,"待完成",$f("textarea",{value:r.pending,onChange:(B)=>j((P)=>({...P,pending:B.target.value}))})),$f("label",null,"付款情况",$f("input",{value:r.paymentStatus,onChange:(B)=>j((P)=>({...P,paymentStatus:B.target.value})),placeholder:"例如 1 / 0.5 / 50%"})),$f("label",null,"其它",$f("input",{value:r.notes,onChange:(B)=>j((P)=>({...P,notes:B.target.value}))})),$f("div",{className:"inline-actions"},$f("button",{type:"submit",className:"primary-btn",disabled:y.saving,"data-testid":"project-manager-save-button"},y.saving?"保存中":r.id?"保存修改":"创建项目"),$f("button",{type:"button",className:"ghost-btn",onClick:()=>j({...qJ})},"清空"),r.id?$f("button",{type:"button",className:"danger-btn",disabled:y.saving,onClick:H,"data-testid":"project-manager-delete-button"},"删除"):null)),$f("div",{className:"project-manager-import"},$f("p",{className:"muted paragraph"},"浏览器只访问 UniDesk frontend;后端通过同源用户服务代理写入主 PostgreSQL,不暴露 4233 公网端口。"),$f("label",{className:"file-import"},y.importing?"导入中...":"导入 Excel",$f("input",{type:"file",accept:".xlsx",onChange:O,disabled:y.importing,"data-testid":"project-manager-import-input"}))))))}var Wr=cf(O0(),1);var Jf=Wr.default.createElement,{useEffect:KS}=Wr.default,hu=Wr.default.useState;function NS({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return Jf("span",{className:`status-badge ${l}`},u||f||"unknown")}function Ur({label:f,value:u,hint:l,tone:_}){return Jf("article",{className:`metric-card ${_||""}`},Jf("div",{className:"metric-label"},f),Jf("div",{className:"metric-value"},u),Jf("div",{className:"metric-hint"},l))}function BJ({title:f,eyebrow:u,actions:l,children:_,className:y,loading:$}){return Jf("section",{className:`panel ${y||""}`},Jf("div",{className:"panel-head"},Jf("div",null,u?Jf("p",{className:"panel-eyebrow"},u):null,Jf(j0,{title:f,loading:$})),l?Jf("div",{className:"panel-actions"},l):null),Jf("div",{className:"panel-body"},_))}function AH({title:f,data:u,onOpen:l,testId:_}){return Jf("button",{type:"button",className:"ghost-btn","data-testid":_,onClick:()=>l(f,u)},"查看原始JSON")}function Qr({title:f,text:u}){return Jf("div",{className:"empty-state"},Jf("strong",null,f),Jf("span",null,u))}function ZS(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function ES(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function HS(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function JH(f){return String(f).replace(/[^a-zA-Z0-9_-]/g,"_")}function OS(f){if(!Number.isFinite(f))return"--";return`${f.toFixed(1)}%`}function l3(f,u){return`${f}/microservices/todo-note/proxy${u}`}function UH(f){return f.reduce((u,l)=>{let _=UH(Array.isArray(l.children)?l.children:[]),y=Boolean(l.completed);return{total:u.total+1+_.total,completed:u.completed+(y?1:0)+_.completed,active:u.active+(y?0:1)+_.active}},{total:0,completed:0,active:0})}function wJ(f,u){let l=u==="all"||(u==="completed"?Boolean(f.completed):!f.completed),_=Array.isArray(f.children)?f.children:[];return l||_.some((y)=>wJ(y,u))}function FH(f){return Array.isArray(f?.instances)?f.instances:[]}function YJ(f,u){for(let l of f){if(l?.id===u)return Array.isArray(l.children)?l.children:[];let _=YJ(Array.isArray(l?.children)?l.children:[],u);if(_.length>0)return _}return[]}function QH({microservices:f,onRaw:u,apiBaseUrl:l="/api"}){let _=f.find((k)=>k.id==="todo-note")||null,[y,$]=hu(null),[r,j]=hu(null),[A,J]=hu(""),[U,Q]=hu(null),[W,G]=hu("all"),[K,H]=hu(13),[O,z]=hu(""),[Z,N]=hu(""),[E,q]=hu(""),[Y,w]=hu(""),[B,P]=hu(""),[h,M]=hu(!1),[n,S]=hu(""),[T,i]=hu(null),C=FH(r),v=UH(Array.isArray(U?.todos)?U.todos:[]),X=_?ZS(_):{},D=_?HS(_):{},p=_?ES(_):{};async function m(k=A){let[Af,Yf]=await Promise.all([Mf(`${l}/microservices/todo-note/health`),Mf(l3(l,"/api/instances"))]);$(Af),j(Yf);let Bf=FH(Yf),df=Bf.some((_0)=>_0.id===k)?k:Bf[0]?.id||"";return J(df),df}async function s(k=A){if(!k){Q(null);return}let Af=await Mf(l3(l,`/api/instances/${encodeURIComponent(k)}`));Q(Af)}async function d(k=A){if(!_)return;M(!0),S("");try{let Af=await m(k);await s(Af),i(new Date)}catch(Af){S(Df(Af,"Todo Note 加载失败"))}finally{M(!1)}}async function a(k){if(!A)return null;S("");try{let Af=await Mf(l3(l,`/api/instances/${encodeURIComponent(A)}/actions`),{method:"POST",body:JSON.stringify({action:k})});return Q(Af),await m(A),Af}catch(Af){return S(Df(Af,"Todo 操作失败")),null}}async function I(k){k.preventDefault();let Af=O.trim();if(!Af)return;M(!0),S("");try{let Yf=await Mf(l3(l,"/api/instances"),{method:"POST",body:JSON.stringify({name:Af})});z(""),await d(Yf.id)}catch(Yf){S(Df(Yf,"创建清单失败"))}finally{M(!1)}}async function ff(k){if(!window.confirm("确认删除这个 Todo Note 清单?"))return;M(!0),S("");try{await Mf(l3(l,`/api/instances/${encodeURIComponent(k)}`),{method:"DELETE"}),await d(A===k?"":A)}catch(Af){S(Df(Af,"删除清单失败"))}finally{M(!1)}}async function yf(k){k.preventDefault();let Af=Z.trim();if(!Af)return;N(""),await a({type:"addTodo",title:Af})}async function rf(k){if(!A)return;S("");try{let Af=await Mf(l3(l,`/api/instances/${encodeURIComponent(A)}/${k}`),{method:"POST",body:JSON.stringify({})});Q(Af),await m(A)}catch(Af){S(Df(Af,`${k} 失败`))}}function Wf(k){q(k.id),w(String(k.title||""))}async function Ef(k){let Af=Y.trim();if(q(""),w(""),Af)await a({type:"updateTodoTitle",todoId:k,title:Af})}async function Gf(k){let Yf=window.prompt("新增子任务标题")?.trim();if(!Yf)return;let Bf=YJ(Array.isArray(U?.todos)?U.todos:[],k),df=new Set(Bf.map((a0)=>a0.id)),_0=await a({type:"addTodo",title:Yf,parentId:k,targetIndex:0});if(!_0)return;let y0=YJ(Array.isArray(_0?.todos)?_0.todos:[],k),N0=y0.find((a0)=>!df.has(a0.id));if(N0&&y0[0]?.id!==N0.id)await a({type:"moveTodo",todoId:N0.id,targetParentId:k,targetIndex:0})}async function c(k,Af){if(!B)return;let Yf={type:"moveTodo",todoId:B,targetIndex:Af};if(k)Yf.targetParentId=k;P(""),await a(Yf)}if(KS(()=>{d()},[_?.id,_?.runtime?.providerStatus]),!_)return Jf(Qr,{title:"Todo Note 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=todo-note"});let o=C.find((k)=>k.id===A)||null,e=Array.isArray(U?.todos)?U.todos:[],Kf=e.map((k,Af)=>({todo:k,index:Af})).filter((k)=>wJ(k.todo,W));return Jf("div",{className:"todo-note-page","data-testid":"todo-note-page"},Jf(BJ,{title:"Todo Note 工作台",eyebrow:"Main Server 用户服务",loading:h,actions:Jf("div",{className:"panel-actions"},Jf("button",{type:"button",className:"ghost-btn",disabled:h,onClick:()=>d(A),"data-testid":"todo-note-refresh-button"},h?"刷新中":"刷新"),Jf(AH,{title:"Todo Note 用户服务",data:_,onOpen:u,testId:"raw-todo-note-service"}))},Jf("div",{className:"todo-note-hero"},Jf("div",null,Jf("div",{className:"node-version-line"},Jf(NS,{status:X.providerStatus==="online"?"online":"warn"},X.providerStatus||"unknown"),Jf("span",null,_.providerId),Jf("span",null,p.public?"公网暴露":"仅 UniDesk frontend 代理访问"),Jf("span",null,y?.ok?"Health OK":"Health --")),Jf("p",{className:"muted paragraph"},_.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,`${p.nodeBindHost||"--"}:${p.nodePort||"--"}`),Jf("code",null,`${D.composeService||"--"} / ${D.containerName||"--"}`))),Jf(A0,{error:n,wide:!0})),Jf("div",{className:"todo-note-layout"},Jf(BJ,{title:"清单",eyebrow:`${C.length} Instances`,className:"todo-list-panel",loading:h},Jf("form",{className:"todo-create-list",onSubmit:I},Jf("input",{placeholder:"新清单名称",value:O,onChange:(k)=>z(k.target.value),"aria-label":"新清单名称"}),Jf("button",{type:"submit",className:"ghost-btn",disabled:h||!O.trim()},"创建")),C.length===0?Jf(Qr,{title:"暂无清单",text:"迁移或创建清单后会出现在这里"}):Jf("div",{className:"todo-instance-list"},C.map((k)=>Jf("button",{key:k.id,type:"button",className:`todo-instance-row ${A===k.id?"active":""}`,onClick:()=>{J(k.id),s(k.id)},"data-testid":`todo-instance-${JH(k.id)}`},Jf("strong",null,k.name),Jf("span",null,`${k.completedCount??0}/${k.todoCount??0} 完成`),Jf("code",null,k.id))))),Jf("div",{className:"todo-main-stack"},Jf(BJ,{title:o?.name||"待选择清单",eyebrow:T?`Updated ${W0(T)}`:"Todo Tree",loading:h,actions:U?Jf("div",{className:"panel-actions"},Jf("button",{type:"button",className:"ghost-btn",onClick:()=>a({type:"renameInstance",name:window.prompt("清单新名称",U.name)||U.name})},"重命名"),Jf("button",{type:"button",className:"ghost-btn danger",onClick:()=>ff(A)},"删除清单"),Jf(AH,{title:`Todo Instance ${A}`,data:U,onOpen:u,testId:"raw-todo-instance"})):null},!U?Jf(Qr,{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:yf},Jf("input",{placeholder:"新增根任务",value:Z,onChange:(k)=>N(k.target.value),"aria-label":"新增根任务"}),Jf("button",{type:"submit",className:"ghost-btn",disabled:!Z.trim()},"新增")),Jf("div",{className:"todo-filter-strip"},["all","active","completed"].map((k)=>Jf("button",{key:k,type:"button",className:`todo-filter ${W===k?"active":""}`,onClick:()=>G(k)},k==="all"?"全部":k==="active"?"未完成":"已完成"))),Jf("div",{className:"todo-toolbar-actions"},Jf("button",{type:"button",className:"ghost-btn",onClick:()=>a({type:"setAllTodosExpanded",expanded:!0})},"全部展开"),Jf("button",{type:"button",className:"ghost-btn",onClick:()=>a({type:"setAllTodosExpanded",expanded:!1})},"全部收起"),Jf("button",{type:"button",className:"ghost-btn",onClick:()=>rf("undo")},"撤销"),Jf("button",{type:"button",className:"ghost-btn",onClick:()=>rf("redo")},"重做"),Jf("label",{className:"todo-font-control"},"字号",Jf("input",{type:"range",min:11,max:18,value:K,onChange:(k)=>H(Number(k.target.value))})))),Jf("div",{className:"todo-stats-grid"},Jf(Ur,{label:"总任务",value:v.total,hint:`${C.length} lists`}),Jf(Ur,{label:"已完成",value:v.completed,hint:`${OS(v.total?v.completed/v.total*100:0)}`,tone:"ok"}),Jf(Ur,{label:"未完成",value:v.active,hint:W==="active"?"当前筛选":"active tasks",tone:v.active>0?"warn":"ok"}),Jf(Ur,{label:"历史指针",value:U.historyPointer??0,hint:"undo / redo"})),Jf("div",{className:"todo-root-drop",onDragOver:(k)=>k.preventDefault(),onDrop:(k)=>{k.preventDefault(),c(null,e.length)}},"拖到这里可移为根任务末尾"),Jf("div",{className:"todo-tree","data-testid":"todo-note-tree"},Kf.length===0?Jf(Qr,{title:"没有匹配任务",text:"调整筛选或新增任务"}):Kf.map(({todo:k,index:Af})=>Jf(WH,{key:k.id,todo:k,depth:0,parentId:null,index:Af,siblingCount:e.length,filter:W,editingId:E,editingTitle:Y,setEditingTitle:w,beginEdit:Wf,saveEdit:Ef,applyTodoAction:a,addChild:Gf,dragTodoId:B,setDragTodoId:P,dropTodo:c}))))))))}function WH(f){let{todo:u,depth:l,parentId:_,index:y,siblingCount:$,filter:r,editingId:j,editingTitle:A,setEditingTitle:J,beginEdit:U,saveEdit:Q,applyTodoAction:W,addChild:G,dragTodoId:K,setDragTodoId:H,dropTodo:O}=f,z=Array.isArray(u.children)?u.children:[],Z=z.map((q,Y)=>({child:q,childIndex:Y})).filter((q)=>wJ(q.child,r)),N=j===u.id,E=_||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:(q)=>{H(u.id),q.dataTransfer.effectAllowed="move"},onDragOver:(q)=>q.preventDefault(),onDrop:(q)=>{q.preventDefault(),O(u.id,z.length)},"data-testid":`todo-row-${JH(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:(q)=>J(q.target.value),onKeyDown:(q)=>{if(q.key==="Enter")Q(u.id);if(q.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,`更新 ${Nf(u.updatedAt)}`),u.reminderAt?Jf("span",{className:"todo-reminder"},`提醒 ${Nf(u.reminderAt)}`):Jf("span",null,"无提醒"))),Jf("input",{className:"todo-reminder-input",type:"datetime-local",value:ir(u.reminderAt),onChange:(q)=>W({type:"setTodoReminder",todoId:u.id,reminderAt:QU(q.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:y<=0,onClick:()=>W({type:"moveTodo",todoId:u.id,...E?{targetParentId:E}:{},targetIndex:y-1})},"上移"),Jf("button",{type:"button",className:"ghost-btn",disabled:y<=0,onClick:()=>W({type:"moveTodo",todoId:u.id,...E?{targetParentId:E}:{},targetIndex:0})},"置顶"),Jf("button",{type:"button",className:"ghost-btn",disabled:y>=$-1,onClick:()=>W({type:"moveTodo",todoId:u.id,...E?{targetParentId:E}:{},targetIndex:y+1})},"下移"),Jf("button",{type:"button",className:"ghost-btn",disabled:!_,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:q,childIndex:Y})=>Jf(WH,{key:q.id,todo:q,depth:l+1,parentId:u.id,index:Y,siblingCount:z.length,filter:r,editingId:j,editingTitle:A,setEditingTitle:J,beginEdit:U,saveEdit:Q,applyTodoAction:W,addChild:G,dragTodoId:K,setDragTodoId:H,dropTodo:O}))):null)}var zH=cf(O0(),1),S_=zH.default.createElement;function GH({title:f,items:u,actions:l,className:_,testId:y}){let $=Array.isArray(u)?u:[];return S_("section",{className:`top-status-bar ${_||""}`,"data-testid":y},S_("div",{className:"top-status-main"},f?S_("strong",{className:"top-status-title"},f):null,S_("div",{className:"top-status-chips"},$.map((r,j)=>S_("span",{key:r?.key||`${r?.label||"status"}-${j}`,className:`top-status-chip ${r?.tone||""}`,"data-testid":r?.testId},r?.label?S_("b",null,r.label):null,S_("span",null,r?.value??"--"))))),l?S_("div",{className:"top-status-actions"},l):null)}var q4=cf(O0(),1);var x0=q4.default.createElement;function KH({onClose:f}){let{notifications:u,removeNotification:l,clearNotifications:_}=Lu(),y=q4.default.useRef(null);if(q4.default.useEffect(()=>{let $=(r)=>{if(y.current&&!y.current.contains(r.target))f()};return document.addEventListener("mousedown",$),()=>document.removeEventListener("mousedown",$)},[f]),u.length===0)return x0("div",{className:"notification-popup",ref:y},x0("div",{className:"notification-popup-header"},x0("span",null,"通知"),x0("button",{className:"notification-popup-close",onClick:f},"×")),x0("div",{className:"notification-popup-empty"},"暂无通知"));return x0("div",{className:"notification-popup",ref:y},x0("div",{className:"notification-popup-header"},x0("span",null,`通知 (${u.length})`),x0("div",{className:"notification-popup-actions"},x0("button",{className:"notification-popup-clear",onClick:_},"清空"),x0("button",{className:"notification-popup-close",onClick:f},"×"))),x0("div",{className:"notification-popup-list"},u.slice().reverse().map(($)=>x0("div",{key:$.id,className:`notification-item ${$.type}`},x0("span",{className:"notification-item-icon"},$.type==="success"?"✓":"×"),x0("span",{className:"notification-item-message"},$.message),x0("button",{className:"notification-item-dismiss",onClick:()=>l($.id)},"×")))))}function NH({notification:f}){let{removeNotification:u}=Lu();return q4.default.useEffect(()=>{let l=setTimeout(()=>{u(f.id)},3000);return()=>clearTimeout(l)},[f.id,u]),x0("div",{className:`notification-banner ${f.type}`,role:"alert"},x0("span",{className:"notification-banner-icon"},f.type==="success"?"✓":"×"),x0("span",{className:"notification-banner-message"},f.message),x0("button",{className:"notification-banner-dismiss",onClick:()=>u(f.id)},"×"))}function DH(f,u){let l=document.getElementById("root")?.getAttribute(f);if(!l)return u;try{let _=JSON.parse(l);return typeof _==="object"&&_!==null&&!Array.isArray(_)?_:u}catch{return u}}var mf=DH("data-config",{apiBaseUrl:"/api",authUsername:"admin"}),VS=DH("data-codex-overview",null),F=i_.default.createElement,{useEffect:J1,useMemo:B4}=i_.default,bf=i_.default.useState,PJ=i_.default.createContext(!1),Xl=WK(p2),qS={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 ZH(){return typeof document>"u"||document.visibilityState!=="hidden"}function LS(f,u){if(f==="ops"&&u==="status")return 5000;if(f==="nodes"&&u==="monitor")return 5000;if(f==="tasks"&&(u==="dispatch"||u==="scheduled"||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 XS(f){if(!f?._summaryOnly||!f?.id)return f;return(await Mf(`${mf.apiBaseUrl}/tasks/${encodeURIComponent(String(f.id))}`))?.task||f}function Y4(f){return f?._summaryOnly?{...f,_loadRaw:()=>XS(f)}:f}function x1(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 Al(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 x1(u/1000)}function Ku(f){let u=Number(f);if(!Number.isFinite(u)||u<=0)return"--";let l=["B","KB","MB","GB","TB"],_=u,y=0;while(_>=1024&&y0)return l[_]}return"任务失败但 provider 未返回明确原因"}function Py(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 YS(f,u){let l=f.replace(/[-_\s]/g,"").toLowerCase(),_=l==="ts"||l.endsWith("at")||l.endsWith("timestamp")||l.endsWith("heartbeat");if((typeof u==="string"||typeof u==="number")&&_){let y=Nf(u);if(y!=="--")return y}if(f==="bodyText"&&typeof u==="string")return`${/^\s*[{[]/.test(u)?"JSON":"HTTP"} body ${u.length} chars`;return Py(u)}function PH(f){if(!f||typeof f!=="object"||Array.isArray(f))return[];return Object.entries(f)}function Iu(f){return String(f).replace(/[^a-zA-Z0-9_-]/g,"_")}function SJ(f,u){return f&&typeof f==="object"&&!Array.isArray(f)?f[u]:void 0}function Gr(f,u,l="未知"){let _=SJ(f?.labels,u);return typeof _==="string"&&_.length>0?_:l}function nH(f){return Gr(f,"providerGatewayVersion")}function X4(f){return Gr(f,"providerGatewayUpgradePolicy")}function EH(f){return Gr(f,"providerGatewayStartedAt","")}function SH(f){let u=SJ(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 CH(f,u){return SH(f).includes(u)}function HH(f,u){let l=SJ(f?.labels,u);return l===!0||l==="true"||l==="1"}function wS(f){if(!CH(f,"host.ssh"))return{tone:"fail",label:"不可用",detail:"未声明 host.ssh"};if(!HH(f,"hostSshConfigured"))return{tone:"warn",label:"未配置",detail:"缺少 SSH 环境变量"};if(!HH(f,"hostSshKeyPresent"))return{tone:"warn",label:"缺 key",detail:"私钥未挂载"};return{tone:"ok",label:"可用",detail:Gr(f,"hostSshTarget","host.ssh ready")}}function DS(f){if(!CH(f,"provider.upgrade"))return{tone:"fail",label:"不可用",detail:"未声明 provider.upgrade"};let u=X4(f);if(u!=="always-enabled")return{tone:"warn",label:"待确认",detail:`策略 ${u}`};return{tone:"ok",label:"可用",detail:"always-enabled"}}function CJ(f){let u=typeof f==="string"&&f.length>0?f:"未知";if(u==="未知")return"版本未知";return u.startsWith("v")?u:`v${u}`}function iH(f){return f?.payload&&typeof f.payload==="object"&&!Array.isArray(f.payload)?f.payload:{}}function Kr(f){return f?.result&&typeof f.result==="object"&&!Array.isArray(f.result)?f.result:{}}function zr(f){let u=iH(f),l=Kr(f);return(u.mode??l.mode)==="schedule"?"schedule":"plan"}function TS(f){let u=iH(f).source;return typeof u==="string"&&u.length>0?u:"unknown"}function MS(f){let u=Kr(f),l=u.plan&&typeof u.plan==="object"&&!Array.isArray(u.plan)?u.plan:{},_=u.policy??l.policy;return typeof _==="string"&&_.length>0?_:"--"}function cH(f){let u=Kr(f),l=u.plan&&typeof u.plan==="object"&&!Array.isArray(u.plan)?u.plan:{},_=u.targetProviderGatewayVersion??u.providerGatewayVersion??l.targetProviderGatewayVersion??l.providerGatewayVersion;return typeof _==="string"&&_.length>0?CJ(_):"版本未知"}function RH(f){if(String(f?.status||"").toLowerCase()==="failed")return MH(f);if(y3(f))return"等待 provider 回传升级终态";let l=Kr(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 xH(f,u){return f.filter((l)=>l?.providerId===u&&l?.command==="provider.upgrade").sort((l,_)=>(U1(_.updatedAt)??0)-(U1(l.updatedAt)??0))}function PS(f){return f.find((u)=>zr(u)==="schedule")||f[0]||null}function bH(f){return f?.runtime&&typeof f.runtime==="object"&&!Array.isArray(f.runtime)?f.runtime:{}}function OH(f){return f?.backend&&typeof f.backend==="object"&&!Array.isArray(f.backend)?f.backend:{}}function nS(f){return f?.repository&&typeof f.repository==="object"&&!Array.isArray(f.repository)?f.repository:{}}function K0({status:f,children:u}){let l=String(f||"unknown").toLowerCase();return F("span",{className:`status-badge ${l}`},u||f||"unknown")}function F0({label:f,value:u,hint:l,tone:_,onClick:y,testId:$}){let r=typeof y==="function";return F("article",{className:`metric-card ${_||""} ${r?"clickable":""}`,role:r?"button":void 0,tabIndex:r?0:void 0,"data-testid":$,onClick:y,onKeyDown:r?(j)=>{if(j.key==="Enter"||j.key===" ")j.preventDefault(),y()}:void 0},F("div",{className:"metric-label"},f),F("div",{className:"metric-value"},u),F("div",{className:"metric-hint"},l))}function sf({title:f,eyebrow:u,actions:l,children:_,className:y,loading:$}){let r=i_.default.useContext(PJ),j=Boolean($)||r;return F("section",{className:`panel ${y||""}`},F("div",{className:"panel-head"},F("div",null,u?F("p",{className:"panel-eyebrow"},u):null,F(j0,{title:f,loading:j})),l?F("div",{className:"panel-actions"},l):null),F("div",{className:"panel-body"},_))}function b0({title:f,data:u,onOpen:l,testId:_}){let[y,$]=bf(!1),r=u&&typeof u==="object"&&typeof u._loadRaw==="function"?u._loadRaw:null;async function j(){if(!r){l(f,u);return}$(!0);try{l(f,await r())}catch(A){l(f,{ok:!1,error:Df(A,"读取原始 JSON 失败"),fallback:u})}finally{$(!1)}}return F("button",{type:"button",className:"ghost-btn","data-testid":_,disabled:y,onClick:()=>void j()},y?"读取中":"查看原始JSON")}function SS({raw:f,onClose:u}){if(!f)return null;return F("div",{className:"modal-backdrop",role:"presentation"},F("section",{className:"raw-dialog",role:"dialog","aria-modal":"true","aria-label":f.title},F("div",{className:"raw-dialog-head"},F("h2",null,f.title),F("button",{type:"button",className:"ghost-btn",onClick:u},"关闭")),F("pre",{className:"raw-json","data-testid":"raw-json"},JSON.stringify(f.data,null,2))))}function vH({labels:f,limit:u=8}){let l=PH(f).slice(0,u);if(l.length===0)return F("span",{className:"muted"},"无标签");return F("div",{className:"chip-row"},l.map(([_,y])=>F("span",{key:_,className:"data-chip"},F("b",null,_),F("span",null,Py(y)))))}function _3({node:f}){let u=nH(f);return F("span",{className:`version-chip ${u==="未知"?"unknown":""}`,"data-testid":`gateway-version-${Iu(f?.providerId||"unknown")}`},CJ(u))}function VH({title:f,state:u,testId:l}){return F("span",{className:`capability-badge ${u.tone}`,title:u.detail,"data-testid":l},F("b",null,f),F("strong",null,u.label),F("small",null,u.detail))}function iJ({node:f}){let u=Iu(f?.providerId||"unknown");return F("div",{className:"node-availability-strip"},F(VH,{title:"SSH 透传",state:wS(f),testId:`ssh-availability-${u}`}),F(VH,{title:"远程更新",state:DS(f),testId:`upgrade-availability-${u}`}))}function c_({data:f,empty:u="无数据"}){if(f===null||f===void 0)return F("span",{className:"muted"},u);if(typeof f!=="object")return F("span",{className:"summary-value"},Py(f));if(Array.isArray(f))return F("span",{className:"summary-value"},`${f.length} 项列表`);let l=Object.entries(f).slice(0,5);if(l.length===0)return F("span",{className:"muted"},u);return F("div",{className:"summary-grid"},l.map(([_,y])=>F("span",{key:_,className:"summary-item"},F("b",null,_),F("span",null,YS(_,y)))))}function J0({title:f,text:u}){return F("div",{className:"empty-state"},F("strong",null,f),F("span",null,u))}function CS({onLogin:f}){let[u,l]=bf(mf.authUsername||"admin"),[_,y]=bf(""),[$,r]=bf(""),[j,A]=bf(!1);async function J(U){U.preventDefault(),A(!0),r("");try{let Q=await Mf("/login",{method:"POST",body:JSON.stringify({username:u,password:_})});f(Q)}catch(Q){r(Df(Q,"登录失败"))}finally{A(!1)}}return F("main",{className:"login-screen","data-testid":"login-screen"},F("section",{className:"login-card"},F("div",{className:"login-brand"},F("span",{className:"brand-mark"},"UD"),F("div",null,F("h1",null,"UniDesk"),F("p",null,"Control Plane Login"))),F("form",{className:"login-form",onSubmit:J},F("label",null,"账号",F("input",{name:"username",autoComplete:"username",value:u,onChange:(U)=>l(U.target.value)})),F("label",null,"密码",F("input",{name:"password",type:"password",autoComplete:"current-password",value:_,onChange:(U)=>y(U.target.value)})),F(A0,{error:$}),F("button",{type:"submit",disabled:j},j?"登录中":"登录")),F("div",{className:"login-note"},"默认账号由 config.json 注入;公网入口只暴露前端登录面。")))}function iS({connection:f,lastRefresh:u,onRefresh:l,onLogout:_,session:y,clock:$,activeStatusItems:r=[],onNotificationToggle:j,unreadCount:A=0}){let J=[{key:"core",label:"核心",value:f.text,tone:f.ok?"ok":"fail",testId:"conn-text"},...Array.isArray(r)?r:[],{key:"refresh",label:"刷新",value:u?W0(u):"未刷新"},{key:"clock",label:g4,value:W0($)},{key:"user",label:"用户",value:y?.user?.username||"--",tone:"user"}];return F("header",{className:"topbar"},F("div",null,F("p",{className:"eyebrow"},"Distributed Work Platform"),F("h1",null,"UniDesk 控制平面")),F(GH,{className:"global-top-status",title:"状态",items:J,actions:[F("button",{key:"notification",type:"button",className:`notification-icon-btn ${A>0?"has-unread":""}`,onClick:j,"aria-label":"通知"},"\uD83D\uDD14",A>0?F("span",{key:"badge",className:"notification-badge"},A>99?"99+":A):null),F("button",{key:"refresh",type:"button",className:"ghost-btn",onClick:l},"刷新"),F("button",{key:"logout",type:"button",className:"ghost-btn danger",onClick:_},"退出")]}))}function cS(f){return!f.defaultPrevented&&f.button===0&&!f.metaKey&&!f.altKey&&!f.ctrlKey&&!f.shiftKey&&f.currentTarget.target!=="_blank"}function hH({moduleId:f,tabId:u,className:l,active:_=!1,title:y,testId:$,onNavigate:r,children:j}){let A=m2(Xl,f,u);return F("a",{href:A,role:"button",className:l,title:y,"aria-current":_?"page":void 0,"data-testid":$,"data-route":A,onClick:(J)=>{if(!cS(J))return;J.preventDefault(),r(f,u)}},j)}function RS({activeModule:f,activeTabs:u,onNavigate:l,collapsed:_,onToggle:y}){return F("aside",{className:`rail ${_?"collapsed":""}`,"aria-label":"主模块"},F("div",{className:"brand"},F("span",{className:"brand-mark"},"UD"),F("span",{className:"brand-text"},"UniDesk"),F("button",{type:"button",className:"rail-toggle",onClick:y,"aria-label":_?"展开左侧边栏":"收起左侧边栏","data-testid":"rail-toggle"},_?"»":"«")),p2.map(($)=>{let r=u[$.id]||n6[$.id]||$.tabs[0]?.id||"";return F(hH,{key:$.id,moduleId:$.id,tabId:r,className:`module ${f===$.id?"active":""}`,active:f===$.id,title:$.label,onNavigate:l},F("span",{className:"module-code"},$.code),F("span",null,$.label))}))}function xS({module:f,activeTab:u,onNavigate:l}){return F("nav",{className:"tabs","aria-label":`${f.label} 子功能`},f.tabs.map((_)=>F(hH,{key:_.id,moduleId:f.id,tabId:_.id,className:`tab ${u===_.id?"active":""}`,active:u===_.id,onNavigate:l},_.label)))}function bS({data:f,onRaw:u,onNavigate:l}){let _=f.overview||{},y=f.nodes.filter((J)=>J.status==="online"),$=f.pendingTasks||f.tasks.filter(y3),r=_.pendingTaskCount??$.length,j=f.tasks.slice(0,5),A=_.pgdata||{};return F("div",{className:"page-grid overview-grid","data-testid":"overview-page"},F(sf,{title:"核心指标",eyebrow:"Control"},F("div",{className:"metric-grid"},F(F0,{label:"数据库",value:_.dbReady?"READY":"WAIT",hint:"PostgreSQL internal network",tone:_.dbReady?"ok":"warn"}),F(F0,{label:"PGDATA",value:Ku(A.databaseBytes),hint:`${A.volumeName||"unidesk_pgdata_10gb"} / ${A.databasePretty||"--"}`,tone:"ok",testId:"pgdata-usage-card"}),F(F0,{label:"在线节点",value:_.onlineNodeCount??0,hint:`${_.nodeCount??0} registered`,tone:"ok"}),F(F0,{label:"WebSocket",value:_.activeSocketCount??0,hint:"Provider ingress sockets"}),F(F0,{label:"待处理任务",value:r,hint:r>0?"点击查看具体任务":`timeout ${x1(Math.floor((_.taskPendingTimeoutMs??0)/1000))}`,tone:r>0?"warn":"ok",onClick:()=>l("tasks","pending"),testId:"pending-task-card"}))),F(sf,{title:"本机 Provider",eyebrow:"Self Connected"},y.length===0?F(J0,{title:"暂无在线节点",text:"provider-gateway 未完成自接入"}):F("div",{className:"node-card-list"},y.slice(0,4).map((J)=>F(vS,{key:J.providerId,node:J,onRaw:u})))),F(sf,{title:"待处理任务明细",eyebrow:`${r} Pending`,actions:F("button",{type:"button",className:"ghost-btn",onClick:()=>l("tasks","pending"),"data-testid":"pending-task-detail-link"},"进入任务调度")},$.length===0?F(J0,{title:"当前无待处理",text:"queued / dispatched / running 超时后会自动转为 failed,避免总览长期卡住"}):F("div",{className:"compact-list"},$.slice(0,5).map((J)=>F(BH,{key:J.id,task:J,onRaw:u})))),F(sf,{title:"最近任务",eyebrow:"Dispatch"},j.length===0?F(J0,{title:"暂无任务",text:"可以在任务调度模块发起 docker.ps 或 echo"}):F("div",{className:"compact-list"},j.map((J)=>F(BH,{key:J.id,task:J,onRaw:u})))))}function vS({node:f,onRaw:u}){return F("article",{className:"node-card"},F("div",{className:"node-card-head"},F("div",null,F("strong",null,f.name),F("code",null,f.providerId)),F(K0,{status:f.status})),F("div",{className:"node-version-line"},F(_3,{node:f}),F("span",null,`升级策略 ${X4(f)}`)),F(iJ,{node:f}),F(vH,{labels:f.labels,limit:6}),F("div",{className:"node-card-foot"},F("span",null,`心跳 ${Nf(f.lastHeartbeat)}`),F(b0,{title:`Provider ${f.providerId}`,data:f,onOpen:u,testId:`raw-node-${Iu(f.providerId)}`})))}function hS({events:f,onRaw:u}){return F(sf,{title:"事件摘要",eyebrow:"Latest 100"},f.length===0?F(J0,{title:"暂无事件",text:"Provider 注册、心跳超时和任务状态会写入事件流"}):F("div",{className:"table-wrap"},F("table",null,F("thead",null,F("tr",null,F("th",null,"ID"),F("th",null,"类型"),F("th",null,"来源"),F("th",null,"摘要"),F("th",null,"时间"),F("th",null,"操作"))),F("tbody",null,f.map((l)=>F("tr",{key:l.id},F("td",null,F("code",null,l.id)),F("td",null,F(K0,{status:l.type},l.type)),F("td",null,F("code",null,l.source)),F("td",null,F(c_,{data:l.payload})),F("td",null,Nf(l.createdAt)),F("td",null,F(b0,{title:`Event ${l.id}`,data:l,onOpen:u}))))))))}function IS({logs:f,onRaw:u}){return F(sf,{title:"服务日志",eyebrow:"Core Recent"},f.length===0?F(J0,{title:"暂无日志",text:"backend-core 内存日志会在请求和 provider 事件后出现"}):F("div",{className:"log-list"},f.slice(-80).reverse().map((l,_)=>F("article",{key:_,className:`log-row ${l.level||"info"}`},F("span",null,Nf(l.ts)),F("b",null,l.level||"info"),F("strong",null,l.message||"log"),F(c_,{data:l.data,empty:"无附加字段"}),F(b0,{title:`Log ${l.message||_}`,data:l,onOpen:u})))))}function pS({nodes:f,onRaw:u}){return F(sf,{title:"节点清单",eyebrow:`${f.length} Providers`},f.length===0?F(J0,{title:"暂无 Provider 节点",text:"确认 provider-gateway 已连接 provider ingress"}):F("div",{className:"table-wrap"},F("table",{className:"node-list-table"},F("thead",null,F("tr",null,F("th",null,"状态"),F("th",null,"Provider"),F("th",null,"网关版本"),F("th",null,"运维可用性"),F("th",null,"资源标签"),F("th",null,"连接时间"),F("th",null,"最后心跳"),F("th",null,"操作"))),F("tbody",null,f.map((l)=>F("tr",{key:l.providerId},F("td",null,F(K0,{status:l.status})),F("td",null,F("strong",null,l.name),F("code",null,l.providerId)),F("td",null,F("div",{className:"gateway-cell"},F(_3,{node:l}),F("span",null,X4(l)))),F("td",null,F(iJ,{node:l})),F("td",null,F(vH,{labels:l.labels,limit:5})),F("td",null,Nf(l.connectedAt)),F("td",null,Nf(l.lastHeartbeat)),F("td",null,F(b0,{title:`Provider ${l.providerId}`,data:l,onOpen:u,testId:`raw-node-table-${Iu(l.providerId)}`}))))))))}function mS({nodes:f}){let u=B4(()=>{let l=[];for(let _ of f)for(let[y,$]of PH(_.labels))l.push({providerId:_.providerId,name:_.name,key:y,value:$});return l},[f]);return F(sf,{title:"资源标签",eyebrow:"Structured Labels"},u.length===0?F(J0,{title:"暂无标签",text:"provider-gateway 注册消息会同步资源标签"}):F("div",{className:"label-matrix"},u.map((l)=>F("article",{key:`${l.providerId}-${l.key}`,className:"label-card"},F("span",null,l.key),F("strong",null,Py(l.value)),F("code",null,l.providerId)))))}function gS({nodes:f}){return F(sf,{title:"心跳状态",eyebrow:"Provider Liveness"},f.length===0?F(J0,{title:"无心跳",text:"等待 provider 注册和 heartbeat"}):F("div",{className:"heartbeat-list"},f.map((u)=>F("article",{key:u.providerId,className:"heartbeat-row"},F("span",{className:`pulse ${u.status}`}),F("div",null,F("strong",null,u.name),F("code",null,u.providerId)),F("div",null,F("span",null,"connected"),F("b",null,Nf(u.connectedAt))),F("div",null,F("span",null,"last heartbeat"),F("b",null,Nf(u.lastHeartbeat)))))))}function kS({nodes:f,systemStatuses:u,tasks:l,onRaw:_,refresh:y}){let[$,r]=bf(""),j=B4(()=>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]),A=j.find((H)=>H.providerId===$)||j[0]||null;if(J1(()=>{if(!$&&j[0])r(j[0].providerId)},[j.length,$]),!A)return F(J0,{title:"暂无资源监控",text:"等待 provider 上报 CPU、内存和硬盘指标"});let J=A.systemCurrent,U=A.systemHistory||[],Q=J?.cpu||{},W=J?.memory||{},G=J?.disk||{},K=U.length>0?U:J?[{at:J.collectedAt,cpuPercent:Rf(Q.percent),memoryPercent:Rf(W.percent),diskPercent:Rf(G.percent)}]:[];return F("div",{className:"monitor-page","data-testid":"node-monitor-page"},F("div",{className:"docker-node-strip"},j.map((H)=>F("button",{key:H.providerId,type:"button",className:`docker-node-tile ${A.providerId===H.providerId?"active":""}`,onClick:()=>r(H.providerId)},F("span",{className:`pulse ${H.status}`}),F("strong",null,H.name),F("code",null,H.providerId),F("span",null,H.systemCurrent?`CPU ${C_(H.systemCurrent.cpu?.percent)} / MEM ${C_(H.systemCurrent.memory?.percent)}`:"等待指标")))),F("div",{className:"monitor-layout"},F(sf,{title:"任务管理器视图",eyebrow:A.name,className:"monitor-main-panel",actions:J?F(b0,{title:`System ${A.providerId}`,data:{current:J,history:U},onOpen:_}):null},!J?F(J0,{title:"系统指标未上报",text:"provider-gateway 会周期性采集 /proc 与 df,并保存历史曲线"}):F("div",null,F("div",{className:"monitor-hero"},F("div",null,F("p",{className:"panel-eyebrow"},"Node Performance"),F("h3",null,A.name),F("div",{className:"docker-meta"},F("span",null,`${Q.cores||0} CPU cores`),F("span",null,`load ${Rf(Q.load1).toFixed(2)} / ${Rf(Q.load5).toFixed(2)} / ${Rf(Q.load15).toFixed(2)}`),F("span",null,`memory actual ${Ku(W.usedBytes)} / ${Ku(W.totalBytes)}`),F("span",null,`disk ${Ku(G.usedBytes)} / ${Ku(G.totalBytes)}`))),F(K0,{status:J.ok?"online":"warn"},J.ok?"METRICS READY":"METRICS DEGRADED")),F("div",{className:"monitor-chart-grid"},F(TJ,{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"}),F(TJ,{title:"Memory",metricKey:"memoryPercent",current:W.percent,points:K,detail:`${Ku(W.usedBytes)} actual / ${Ku(W.cacheBytes)} cache excluded`,tone:"memory",testId:"metric-chart-memory"}),F(TJ,{title:"Disk",metricKey:"diskPercent",current:G.percent,points:K,detail:`${G.path||"/"} mounted ${G.mount||"--"}`,tone:"disk",testId:"metric-chart-disk"})),F("div",{className:"monitor-summary-grid"},F(F0,{label:"CPU 当前",value:C_(Q.percent),hint:`history ${K.length} samples`,tone:"ok"}),F(F0,{label:"实际内存",value:Ku(W.usedBytes),hint:`${C_(W.percent)} 不含缓存`}),F(F0,{label:"硬盘已用",value:Ku(G.usedBytes),hint:C_(G.percent)}),F(F0,{label:"更新时间",value:Nf(A.systemUpdatedAt||J.collectedAt),hint:A.providerId})),F(tS,{current:J,onRaw:_}))),F("div",{className:"monitor-side-stack"},F(lC,{provider:A,refresh:y,onRaw:_}),F(_C,{provider:A,tasks:l,onRaw:_,limit:5}),F(sf,{title:"采样说明",eyebrow:"Retention"},F("div",{className:"monitor-note-list"},F("article",null,F("b",null,"CPU"),F("span",null,"从 /proc/stat 计算相邻采样差值,首个采样用 load/cores 近似")),F("article",null,F("b",null,"Memory"),F("span",null,"实际内存 = MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem,不把 page cache / buffer 计入占用")),F("article",null,F("b",null,"Disk"),F("span",null,"使用 df -PB1 对配置路径采样,默认监控根文件系统")),F("article",null,F("b",null,"Process"),F("span",null,"从 /proc/[pid] 采集进程 CPU、实际内存 RSS、线程数和磁盘 I/O 速率;表格默认按内存占用降序")))))))}function qH(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 LH({value:f,label:u,tone:l}){let _=Math.max(1,Math.min(100,Rf(f)));return F("div",{className:`process-meter ${l||""}`},F("span",{style:{width:`${_}%`}}),F("b",null,u))}function tS({current:f,onRaw:u}){let[l,_]=bf({key:"memory",direction:"desc"}),y=i_.default.useContext(PJ),$=f?.processSummary&&typeof f.processSummary==="object"?f.processSummary:{},r=Array.isArray(f?.processes)?f.processes:[],j=B4(()=>{let J=l.direction==="asc"?1:-1;return[...r].sort((U,Q)=>{let W=qH(U,l.key),G=qH(Q,l.key);if(typeof W==="string"||typeof G==="string")return String(W).localeCompare(String(G),"zh-CN")*J;return(W-G)*J||Rf(U.pid)-Rf(Q.pid)})},[r,l.key,l.direction]),A=(J,U)=>{let Q=l.key===U,W=Q?l.direction==="asc"?"ascending":"descending":"none";return F("th",{"aria-sort":W},F("button",{type:"button",className:`process-sort-button ${Q?"active":""}`,"data-testid":`process-sort-${U}`,onClick:()=>_((G)=>({key:U,direction:G.key===U&&G.direction==="desc"?"asc":"desc"}))},J,F("span",null,Q?l.direction==="desc"?"↓":"↑":"↕")))};return F("section",{className:"process-resource-panel","data-testid":"process-resource-panel"},F("div",{className:"process-resource-head"},F("div",null,F("p",{className:"panel-eyebrow"},"Windows Resource Monitor Style"),F(j0,{title:"进程资源占用",level:3,loading:y})),F("div",{className:"process-resource-actions"},F("span",{className:"data-chip"},"默认按内存排序"),F("span",{className:"data-chip"},`${Rf($.visible,j.length)} / ${Rf($.total,j.length)} 进程`),F(b0,{title:"Process Resource Snapshot",data:{processSummary:$,processes:r},onOpen:u,testId:"raw-process-resources"}))),j.length===0?F(J0,{title:"暂无进程资源数据",text:"等待 provider-gateway 上报 /proc/[pid] 采样;旧版 provider 需要先升级到支持进程资源表的版本"}):F("div",{className:"process-table-wrap"},F("table",{className:"process-resource-table","data-testid":"process-resource-table"},F("thead",null,F("tr",null,A("进程","name"),A("PID","pid"),A("用户","user"),F("th",null,"状态"),A("CPU","cpu"),A("内存","memory"),F("th",null,"RSS"),A("磁盘 I/O","disk"),A("线程","threads"),A("运行时长","runtime"))),F("tbody",null,j.map((J)=>{let U=Rf(J.readBytesPerSecond)+Rf(J.writeBytesPerSecond);return F("tr",{key:`${J.pid}-${J.startedAt}`,"data-testid":`process-row-${Iu(J.pid)}`,"data-memory-bytes":String(Rf(J.rssBytes)),"data-cpu-percent":String(Rf(J.cpuPercent)),"data-disk-bps":String(U),"data-pid":String(Rf(J.pid))},F("td",null,F("div",{className:"process-name-cell"},F("strong",null,J.name||"--"),F("span",{className:"process-command"},J.command||"--"))),F("td",null,F("code",null,J.pid||"--")),F("td",null,J.user||`uid:${J.uid??"--"}`),F("td",null,F("span",{className:`process-state state-${Iu(J.state||"unknown")}`},J.state||"?")),F("td",null,F(LH,{value:J.cpuPercent,label:BS(J.cpuPercent),tone:"cpu"})),F("td",null,F(LH,{value:J.memoryPercent,label:C_(J.memoryPercent),tone:"memory"})),F("td",null,Ku(J.rssBytes)),F("td",null,F("div",{className:"process-io-cell"},F("strong",null,DJ(U)),F("span",null,`R ${DJ(J.readBytesPerSecond)} / W ${DJ(J.writeBytesPerSecond)}`))),F("td",null,J.threads||0),F("td",null,x1(Rf(J.elapsedSeconds))))})))))}function TJ({title:f,metricKey:u,current:l,points:_,detail:y,tone:$,testId:r}){let j=_.map((W)=>Math.max(0,Math.min(100,Rf(W[u])))),A=j.length>1?j:[j[0]||0,j[0]||0],J=A.length<=1?100:100/(A.length-1),U=A.map((W,G)=>`${(G*J).toFixed(2)},${(46-W*0.42).toFixed(2)}`).join(" "),Q=`0,48 ${U} 100,48`;return F("article",{className:`metric-chart ${$}`,"data-testid":r},F("div",{className:"metric-chart-head"},F("div",null,F("span",null,f),F("strong",null,C_(l))),F("code",null,`${_.length} pts`)),F("svg",{viewBox:"0 0 100 48",preserveAspectRatio:"none",role:"img","aria-label":`${f} usage curve`},F("polygon",{points:Q}),F("polyline",{points:U}),F("line",{x1:"0",x2:"100",y1:"24",y2:"24"})),F("div",{className:"metric-chart-foot"},F("span",null,"0%"),F("span",null,y),F("span",null,"100%")))}function b1(f){return Array.isArray(f)?f:[]}function sS(f){let u=b1(f?.core?.requests?.componentSummary);return[...b1(f?.frontend?.requests?.componentSummary),...u].sort((_,y)=>Rf(y.requestCount)-Rf(_.requestCount))}function oS(f){let u=b1(f?.core?.operations?.summary);return[...b1(f?.frontend?.operations?.summary),...u].sort((_,y)=>Rf(y.count)-Rf(_.count))}function aS(f){let u=b1(f?.core?.requests?.recentFailures).map((_)=>({source:"backend",..._}));return[...b1(f?.frontend?.requests?.recentFailures).map((_)=>({source:"frontend",..._})),...u].sort((_,y)=>(U1(y.at)??0)-(U1(_.at)??0)).slice(0,20)}function dS(f){let u=b1(f?.core?.operations?.recentSlowOperations);return[...b1(f?.frontend?.operations?.recentSlowOperations),...u].sort((_,y)=>Rf(y.durationMs)-Rf(_.durationMs)).slice(0,20)}function eS(f){let u=performance.memory,l=Number(u?.usedJSHeapSize);if(Number.isFinite(l)&&l>0)return l;let _=Number(f?.appBundleBytes);if(Number.isFinite(_)&&_>0)return _;return Rf(f?.process?.heapUsedBytes)}function fC({points:f}){let u=b1(f),l=u.map((W)=>Rf(W.mb)),_=Math.max(1,...l),y=Math.max(0,Math.min(...l,0)),$=Math.max(1,_-y),r=u.length>1?u:[...u,...u],j=r.length<=1?100:100/(r.length-1),A=r.map((W,G)=>{let K=Rf(W.mb);return`${(G*j).toFixed(2)},${(48-(K-y)/$*42).toFixed(2)}`}).join(" "),J=`0,50 ${A} 100,50`,U=u.at(-1),Q=u[0];return F("article",{className:"performance-memory-card","data-testid":"performance-memory-chart"},F("div",{className:"performance-memory-head"},F("strong",null,`Bwebui: ${U?`${Rf(U.mb).toFixed(1)}MB`:"--"}`),F("span",null,u.length>0?`${u.length} samples`:"等待采样")),F("svg",{viewBox:"0 0 100 50",preserveAspectRatio:"none",role:"img","aria-label":"Bwebui memory trend"},F("polygon",{points:J}),F("polyline",{points:A}),F("line",{x1:"0",x2:"100",y1:"25",y2:"25"})),F("div",{className:"performance-axis-row"},F("span",null,Q?W0(new Date(Q.at)):"--"),F("span",null,"时间"),F("span",null,U?W0(new Date(U.at)):"--")),F("div",{className:"performance-axis-row"},F("span",null,`${y.toFixed(1)}`),F("span",null,"(MB)"),F("span",null,`${_.toFixed(1)}`)))}function uC({onRaw:f}){let[u,l]=bf({core:null,frontend:null}),[_,y]=bf([]),[$,r]=bf(""),[j,A]=bf(!1),[J,U]=bf(null),[Q,W]=bf(!1);async function G(){A(!0),r("");try{let[n,S]=await Promise.all([Mf(`${mf.apiBaseUrl}/performance`,{cache:"no-store"}),Mf(`${mf.apiBaseUrl}/frontend-performance`,{cache:"no-store"})]);l({core:n,frontend:S});let T=eS(S);y((i)=>[...i,{at:new Date().toISOString(),mb:T/1048576}].slice(-80))}catch(n){r(Df(n,"性能指标加载失败"))}finally{A(!1)}}J1(()=>{G();let n=setInterval(()=>void G(),5000);return()=>clearInterval(n)},[]);async function K(){W(!0),r(""),U(null);try{let n=await Mf(`${mf.apiBaseUrl}/code-queue-load-test`,{method:"POST",body:JSON.stringify({targetMs:1000,timeoutMs:90000,url:mf.frontendPublicUrl||window.location.origin})});U(n),G()}catch(n){r(Df(n,"Code Queue Playwright 测量失败"))}finally{W(!1)}}let H=sS(u),O=aS(u),z=oS(u),Z=dS(u),N=u.core?.process||{},E=u.frontend?.process||{},q=u.core?.database?.codeQueueStorage||{},Y=Rf(q.total),w=J?.result||{},B=Rf(w.wallMs,NaN),P=Rf(w.networkIdleMs,NaN),h=w.withinTarget===!0,M=Q?"running":J===null?"idle":J.measurementOk===!0?h?"passed":"slow":"failed";return F("div",{className:"performance-page","data-testid":"performance-page"},F("div",{className:"performance-hero"},F("div",null,F("p",{className:"panel-eyebrow"},"Unified Performance"),F(j0,{title:"性能面板",loading:j||Q}),F("p",null,"按组件统计 HTTP 请求、失败率、P95 延迟,并汇总 backend/frontend 内部操作耗时。")),F("div",{className:"inline-actions"},F("button",{type:"button",className:"ghost-btn",onClick:()=>void K(),disabled:Q,"data-testid":"code-queue-load-test-button"},Q?"测试中...":"测试 Code Queue 加载"),F("button",{type:"button",className:"ghost-btn",onClick:()=>void G(),disabled:j,"data-testid":"performance-refresh-button"},j?"刷新中":"刷新"),F(b0,{title:"Performance Snapshot",data:u,onOpen:f,testId:"raw-performance"}))),F(A0,{error:$}),F("div",{className:"performance-top-grid"},F(fC,{points:_}),F("div",{className:"performance-metric-stack"},F(F0,{label:"backend RSS",value:Ku(N.rssBytes),hint:`heap ${Ku(N.heapUsedBytes)}`}),F(F0,{label:"frontend RSS",value:Ku(E.rssBytes),hint:`bundle ${Ku(u.frontend?.appBundleBytes)}`}),F(F0,{label:"Codex PG 任务",value:Y||"--",hint:q.ok?"unidesk_code_queue_tasks":"等待表初始化",tone:q.ok?"ok":"warn"}),F(F0,{label:"请求样本",value:Rf(u.core?.requests?.sampleCount)+Rf(u.frontend?.requests?.sampleCount),hint:"rolling window 3000"}))),F(sf,{title:"Code Queue 加载基准",eyebrow:"Playwright / target <1s",className:"codex-load-test-panel",loading:Q,actions:F("div",{className:"panel-actions"},F("button",{type:"button",className:"primary-btn",onClick:()=>void K(),disabled:Q,"data-testid":"code-queue-load-test-panel-button"},Q?"正在运行 Playwright...":"手动触发测试"),J?F(b0,{title:"Code Queue Load Test",data:J,onOpen:f,testId:"raw-code-queue-load-test"}):null)},F("div",{className:"codex-load-test-grid","data-testid":"code-queue-load-test-result"},F(F0,{label:"总耗时",value:Q?"运行中":Number.isFinite(B)?Al(B):"--",hint:J===null?"点击按钮启动远端 Playwright":`目标 ${Al(w.targetMs||1000)} / ${w.url||"Code Queue"}`,tone:M==="passed"?"ok":M==="failed"||M==="slow"?"warn":""}),F(F0,{label:"判定",value:Q?"RUNNING":M==="passed"?"PASS <1s":M==="slow"?"SLOW":M==="failed"?"FAILED":"--",hint:J?.measurementOk===!1?String(J.error||w.error||"measurement failed").slice(0,120):"导航开始 -> DOMContentLoaded -> data-load-state=complete",tone:M==="passed"?"ok":M==="idle"||M==="running"?"":"fail"}),F(F0,{label:"Network idle",value:Number.isFinite(P)?Al(P):"--",hint:`DOMContentLoaded ${Al(w.domContentLoadedMs)} / ${w.networkIdleReached===!1?"未在 5s 内空闲":"已空闲"}`,tone:Number.isFinite(P)&&P<=1000?"ok":"warn"}),F(F0,{label:"组件耗时",value:Number.isFinite(Rf(w.componentLoadMs,NaN))?Al(w.componentLoadMs):"--",hint:`queue ${Al(w.queueMs)} / detail ${Al(w.detailMs)}`,tone:Rf(w.componentLoadMs)>1000?"warn":"ok"}),F(F0,{label:"Trace 规模",value:Number.isFinite(Rf(w.transcriptRows,NaN))?String(w.transcriptRows):"--",hint:`${w.visibleTaskCount??0} visible tasks / ${w.partial?"preview":"complete"}`})),Q?F("div",{className:"performance-empty-line"},"正在通过 main-server Host SSH 启动 Playwright,完成后会显示 wall time、组件耗时和最慢 API。"):null,J&&Array.isArray(w.slowestApi)&&w.slowestApi.length>0?F("div",{className:"table-wrap performance-table-wrap compact codex-load-api-table"},F("table",{className:"performance-table"},F("thead",null,F("tr",null,["API","状态","耗时"].map((n)=>F("th",{key:n},n)))),F("tbody",null,w.slowestApi.slice(0,5).map((n,S)=>F("tr",{key:`${n.url}-${S}`},F("td",null,F("code",null,n.url)),F("td",null,n.status),F("td",null,Al(n.durationMs))))))):null),F("div",{className:"performance-grid"},F(sf,{title:"组件汇总",eyebrow:"Requests",loading:j},H.length===0?F(J0,{title:"暂无请求样本",text:"刷新几次或打开页面后会自动形成组件统计"}):F("div",{className:"table-wrap performance-table-wrap"},F("table",{className:"performance-table"},F("thead",null,F("tr",null,["组件","请求数","失败数","失败率","平均延迟","P95"].map((n)=>F("th",{key:n},n)))),F("tbody",null,H.map((n)=>F("tr",{key:n.component},F("td",null,F("code",null,n.component)),F("td",null,n.requestCount),F("td",null,n.failureCount),F("td",null,C_(Rf(n.failureRate)*100)),F("td",null,Al(n.averageLatencyMs)),F("td",null,Al(n.p95LatencyMs)))))))),F(sf,{title:"最近失败请求",eyebrow:"Failures",loading:j},O.length===0?F("div",{className:"performance-empty-line"},"最近没有失败请求"):F("div",{className:"table-wrap performance-table-wrap compact"},F("table",{className:"performance-table"},F("thead",null,F("tr",null,["时间","来源","组件","状态","路径"].map((n)=>F("th",{key:n},n)))),F("tbody",null,O.map((n,S)=>F("tr",{key:`${n.at}-${S}`},F("td",null,Nf(n.at)),F("td",null,n.source),F("td",null,F("code",null,n.component)),F("td",null,F(K0,{status:"failed"},n.status)),F("td",null,F("code",null,n.path)))))))),F(sf,{title:"内部操作汇总",eyebrow:"Operations",loading:j},z.length===0?F(J0,{title:"暂无内部操作样本",text:"API 查询和代理请求会自动记录内部操作耗时"}):F("div",{className:"table-wrap performance-table-wrap"},F("table",{className:"performance-table"},F("thead",null,F("tr",null,["服务","操作","次数","平均延迟","P95"].map((n)=>F("th",{key:n},n)))),F("tbody",null,z.map((n)=>F("tr",{key:`${n.service}-${n.operation}`},F("td",null,n.service),F("td",null,F("code",null,n.operation)),F("td",null,n.count),F("td",null,Al(n.averageLatencyMs)),F("td",null,Al(n.p95LatencyMs)))))))),F(sf,{title:"最近慢操作",eyebrow:"Slowest",loading:j},Z.length===0?F(J0,{title:"暂无慢操作",text:"后端会记录最近窗口内耗时最高的内部操作"}):F("div",{className:"table-wrap performance-table-wrap"},F("table",{className:"performance-table"},F("thead",null,F("tr",null,["时间","操作","耗时","结果","细节"].map((n)=>F("th",{key:n},n)))),F("tbody",null,Z.map((n,S)=>F("tr",{key:`${n.at}-${n.operation}-${S}`},F("td",null,Nf(n.at)),F("td",null,F("code",null,n.operation)),F("td",null,Al(n.durationMs)),F("td",null,n.ok?"成功":"失败"),F("td",null,n.detail||"-")))))))))}function lC({provider:f,refresh:u,onRaw:l}){let[_,y]=bf(""),[$,r]=bf(null),[j,A]=bf("");async function J(U){y(U),A("");try{let Q=await Mf(`${mf.apiBaseUrl}/dispatch`,{method:"POST",body:JSON.stringify({providerId:f.providerId,command:"provider.upgrade",payload:{mode:U,source:"frontend-resource-monitor",requestedAt:new Date().toISOString()}})});r({mode:U,...Q}),await u()}catch(Q){A(Df(Q,"升级命令下发失败"))}finally{y("")}}return F(sf,{title:"Provider Gateway 升级",eyebrow:"Remote Control",loading:Boolean(_)},F("div",{className:"upgrade-control","data-testid":"provider-upgrade-control"},F("p",null,"通过 UniDesk WebSocket 向当前计算节点下发 provider.upgrade;预检只生成升级计划,执行升级会调度节点本地 updater 容器。"),F("div",{className:"upgrade-target-line"},F("span",null,"指定 Provider"),F("code",null,f.providerId),F(_3,{node:f})),F("div",{className:"upgrade-actions"},F("button",{type:"button",className:"ghost-btn",disabled:Boolean(_),onClick:()=>J("plan"),"data-testid":"upgrade-plan-button"},_==="plan"?"预检中":"预检升级"),F("button",{type:"button",className:"ghost-btn danger",disabled:Boolean(_),onClick:()=>J("schedule"),"data-testid":"upgrade-schedule-button"},_==="schedule"?"调度中":"执行升级")),F(A0,{error:j}),$?F("div",{className:"upgrade-result"},F(K0,{status:$.status||"queued"},$.status||"queued"),F("span",null,`${$.mode==="schedule"?"执行升级":"预检升级"} 已下发`),F("span",null,`指定版本 ${CJ(nH(f))}`),F("code",null,$.taskId||"--"),F(b0,{title:"Provider Upgrade Dispatch",data:$,onOpen:l})):F("span",{className:"muted"},"升级任务结果会进入任务历史;执行升级可能导致 provider 短暂重连。")))}function IH({records:f,onRaw:u,compact:l=!1}){if(f.length===0)return F(J0,{title:"暂无远程更新记录",text:"该节点还没有 provider.upgrade 任务;执行预检或升级后会在这里形成结构化记录"});return F("div",{className:`upgrade-record-table-wrap table-wrap ${l?"compact":""}`},F("table",{className:"upgrade-record-table"},F("thead",null,F("tr",null,F("th",null,"状态"),F("th",null,"模式"),F("th",null,"任务"),F("th",null,"来源"),F("th",null,"耗时"),F("th",null,"策略"),F("th",null,"Gateway 版本"),F("th",null,"结果记录"),F("th",null,"更新时间"),F("th",null,"操作"))),F("tbody",null,f.map((_)=>F("tr",{key:_.id,"data-testid":`gateway-upgrade-record-${Iu(_.id)}`},F("td",null,F(K0,{status:_.status})),F("td",null,F("span",{className:`mode-chip ${zr(_)}`},zr(_)==="schedule"?"执行升级":"预检")),F("td",null,F("strong",null,"provider.upgrade"),F("code",null,_.id)),F("td",null,TS(_)),F("td",null,F(mH,{task:_})),F("td",null,MS(_)),F("td",null,F("span",{className:"version-chip"},cH(_))),F("td",null,F("span",{className:`upgrade-outcome ${String(_.status||"").toLowerCase()}`},RH(_))),F("td",null,Nf(_.updatedAt)),F("td",null,F(b0,{title:`Provider Upgrade Task ${_.id}`,data:Y4(_),onOpen:u})))))))}function _C({provider:f,tasks:u,onRaw:l,limit:_=5}){let y=xH(u,f.providerId).slice(0,_);return F(sf,{title:"远程更新记录",eyebrow:f.providerId,actions:F(_3,{node:f}),className:"provider-upgrade-records-panel"},F("div",{"data-testid":`provider-upgrade-records-${Iu(f.providerId)}`},F(IH,{records:y,onRaw:l,compact:!0})))}function yC({nodes:f,tasks:u,onRaw:l}){let _=B4(()=>f.map(($)=>{let r=xH(u,$.providerId);return{node:$,records:r,latest:PS(r),capabilities:SH($)}}),[f,u]),y=_.reduce(($,r)=>$+r.records.length,0);return F("div",{className:"gateway-page","data-testid":"gateway-version-page"},F(sf,{title:"Provider Gateway 版本",eyebrow:`${f.length} Providers / ${y} 更新记录`},f.length===0?F(J0,{title:"暂无 Provider 节点",text:"等待 provider-gateway 注册后显示版本号和升级记录"}):F("div",{className:"table-wrap gateway-version-table-wrap"},F("table",{className:"gateway-version-table"},F("thead",null,F("tr",null,F("th",null,"状态"),F("th",null,"Provider"),F("th",null,"Gateway 版本"),F("th",null,"升级策略"),F("th",null,"运维可用性"),F("th",null,"运行时间"),F("th",null,"能力"),F("th",null,"最近远程更新"),F("th",null,"操作"))),F("tbody",null,_.map(($)=>F("tr",{key:$.node.providerId},F("td",null,F(K0,{status:$.node.status})),F("td",null,F("strong",null,$.node.name),F("code",null,$.node.providerId)),F("td",null,F(_3,{node:$.node})),F("td",null,X4($.node)),F("td",null,F(iJ,{node:$.node})),F("td",null,EH($.node)?Nf(EH($.node)):"待新版上报"),F("td",null,F("div",{className:"capability-row"},$.capabilities.length===0?F("span",{className:"muted"},"未声明"):$.capabilities.slice(0,5).map((r)=>F("span",{key:r,className:"data-chip"},r)))),F("td",null,$.latest?F("div",{className:"latest-upgrade-cell"},F(K0,{status:$.latest.status}),F("span",null,`${zr($.latest)==="schedule"?"执行升级":"预检"} / ${Nf($.latest.updatedAt)}`),F("small",null,`Gateway ${cH($.latest)}`),F("small",null,RH($.latest))):F("span",{className:"muted"},"暂无记录")),F("td",null,F(b0,{title:`Provider ${$.node.providerId}`,data:$.node,onOpen:l})))))))),F(sf,{title:"远程更新记录",eyebrow:"Structured provider.upgrade records"},f.length===0?F(J0,{title:"暂无记录",text:"没有 provider 节点时不会生成远程更新记录"}):F("div",{className:"gateway-record-grid"},_.map(($)=>F("article",{key:$.node.providerId,className:"gateway-record-card","data-testid":`gateway-records-${Iu($.node.providerId)}`},F("div",{className:"gateway-record-head"},F("div",null,F("strong",null,$.node.name),F("code",null,$.node.providerId)),F(_3,{node:$.node})),F("div",{className:"gateway-record-meta"},F("span",null,`心跳 ${Nf($.node.lastHeartbeat)}`),F("span",null,`策略 ${X4($.node)}`),F("span",null,`${$.records.length} 条记录`)),F(IH,{records:$.records.slice(0,8),onRaw:l,compact:!0}))))))}function $C(f){if(f==="running")return"online";if(f==="paused"||f==="restarting")return"warn";if(f==="exited"||f==="dead")return"offline";return"internal"}function pH(f){return/^[a-f0-9]{48,64}$/i.test(f)}function L4(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 XH(f){let u=String(f?.name||""),l=String(f?.labels||"");if(L4(f))return 0;if(l.includes("com.docker.compose.project=unidesk"))return 1;if(!pH(u))return 2;return 3}function rC(f){return[...f].sort((u,l)=>{let _=XH(u)-XH(l);if(_!==0)return _;return String(u.name||"").localeCompare(String(l.name||""))})}function jC({nodes:f,dockerStatuses:u,onRaw:l}){let[_,y]=bf(""),$=B4(()=>f.map((Z)=>{let N=u.find((E)=>E.providerId===Z.providerId);return{...Z,dockerStatus:N?.dockerStatus||null,dockerUpdatedAt:N?.updatedAt||null}}),[f,u]),r=$.find((Z)=>Z.providerId===_)||$[0]||null;if(J1(()=>{if(!_&&$[0])y($[0].providerId)},[$.length,_]),!r)return F(J0,{title:"暂无 Docker 节点",text:"等待 provider 上报 Docker daemon 状态"});let j=r.dockerStatus,A=r.providerId==="main-server",J=j?.counts||{},U=j?.daemon||{},Q=j?.containers||[],W=j?.images||[],G=rC(j?.volumes||[]),K=A?G.find(L4):null,H=j?.networks||[],O=Q.filter((Z)=>Z.state==="running"),z=Q.filter((Z)=>Z.state!=="running");return F("div",{className:"docker-page","data-testid":"docker-status-page"},F("div",{className:"docker-node-strip"},$.map((Z)=>F("button",{key:Z.providerId,type:"button",className:`docker-node-tile ${r.providerId===Z.providerId?"active":""}`,onClick:()=>y(Z.providerId)},F("span",{className:`pulse ${Z.status}`}),F("strong",null,Z.name),F("code",null,Z.providerId),F("span",null,Z.dockerStatus?`Docker ${Z.dockerStatus.ok?"ready":"degraded"}`:"等待上报")))),F("div",{className:"docker-layout"},F(sf,{title:"Docker Desktop 视图",eyebrow:r.name,className:"docker-main-panel",actions:j?F(b0,{title:`Docker ${r.providerId}`,data:j,onOpen:l}):null},!j?F(J0,{title:"Docker 状态未上报",text:"provider-gateway 会在连接后周期性采集 docker info / ps / images / volume / network"}):F("div",null,F("div",{className:"docker-hero"},F("div",null,F("p",{className:"panel-eyebrow"},"Daemon"),F("h3",null,U.name||r.providerId),F("div",{className:"docker-meta"},F("span",null,U.serverVersion?`Engine ${U.serverVersion}`:"Engine --"),F("span",null,U.operatingSystem||"OS --"),F("span",null,U.architecture||"arch --"),F("span",null,`${U.cpus||0} CPU / ${Ku(U.memoryBytes)}`))),F(K0,{status:j.ok?"online":"warn"},j.ok?"Docker Ready":"Docker Degraded")),F("div",{className:"docker-metrics"},F(F0,{label:"Containers",value:J.containers??Q.length,hint:`${J.running??O.length} running / ${J.stopped??z.length} stopped`,tone:"ok"}),F(F0,{label:"Images",value:J.images??W.length,hint:`${J.daemonImages??J.images??W.length} daemon images`}),F(F0,{label:"Volumes",value:J.volumes??G.length,hint:A?K?"database volume visible":"database volume missing":"node local volumes",tone:K?"ok":""}),F(F0,{label:"Networks",value:J.networks??H.length,hint:U.driver?`driver ${U.driver}`:"docker networks"})),A?F(AC,{volume:K,volumeCount:G.length}):null,F("div",{className:"docker-section-head"},F("h3",null,"Containers"),F("span",null,`updated ${Nf(r.dockerUpdatedAt||j.collectedAt)}`)),F("div",{className:"docker-container-table table-wrap","data-testid":"docker-container-table"},F("table",null,F("thead",null,F("tr",null,F("th",null,"状态"),F("th",null,"容器"),F("th",null,"镜像"),F("th",null,"端口"),F("th",null,"运行时间"),F("th",null,"重启策略"),F("th",null,"PID"),F("th",null,"大小"))),F("tbody",null,Q.length===0?F("tr",null,F("td",{colSpan:8},"暂无容器")):Q.map((Z)=>F("tr",{key:`${Z.id}-${Z.name}`},F("td",null,F(K0,{status:$C(Z.state)},Z.state||"unknown")),F("td",null,F("strong",null,Z.name||"--"),F("code",null,Z.id||"--")),F("td",null,Z.image||"--"),F("td",null,Z.ports||F("span",{className:"muted"},"未发布")),F("td",null,Z.runningFor||Z.status||"--"),F("td",null,Z.restartPolicy?F(K0,{status:Z.restartPolicy==="always"?"online":"warn"},Z.restartPolicy):"--"),F("td",null,Z.pidMode?F("code",null,Z.pidMode):"--"),F("td",null,Z.size||"--")))))))),F("div",{className:"docker-side-stack"},F(MJ,{title:"Images",items:W,render:(Z)=>F("article",{key:`${Z.id}-${Z.repository}`,className:"docker-side-row"},F("strong",null,`${Z.repository}:${Z.tag}`),F("span",null,Z.size||"--"),F("code",null,Z.id||"--"))}),F(MJ,{title:"Volumes",items:G,limit:G.length,render:(Z)=>F("article",{key:Z.name,className:`docker-side-row volume-row ${A&&L4(Z)?"database-volume":""}`,"data-testid":A&&L4(Z)?"database-volume-row":void 0},F("strong",null,Z.name),F("span",null,A&&L4(Z)?"PostgreSQL":pH(String(Z.name||""))?"anonymous":"named"),F("code",null,Z.mountpoint||Z.driver||Z.scope||"--"))}),F(MJ,{title:"Networks",items:H,render:(Z)=>F("article",{key:Z.id||Z.name,className:"docker-side-row"},F("strong",null,Z.name),F("span",null,Z.driver||"--"),F("code",null,Z.id||"--"))}))))}function AC({volume:f,volumeCount:u}){return F("section",{className:`docker-volume-focus ${f?"ready":"missing"}`,"data-testid":"database-volume-card"},F("div",{className:"volume-focus-head"},F("span",{className:"panel-eyebrow"},"Database Named Volume"),F(K0,{status:f?"online":"warn"},f?"FOUND":"MISSING")),f?F("div",{className:"volume-focus-body"},F("strong",null,f.name),F("span",null,"PostgreSQL data volume for unidesk-database"),F("div",{className:"volume-route"},F("code",null,f.mountpoint||"/var/lib/docker/volumes/unidesk_pgdata_10gb/_data"),F("span",null,"->"),F("code",null,"unidesk-database:/var/lib/postgresql/data")),F("div",{className:"docker-meta compact"},F("span",null,`driver ${f.driver||"--"}`),F("span",null,`scope ${f.scope||"--"}`),F("span",null,`${u} volumes reported`))):F("div",{className:"volume-focus-body"},F("strong",null,"unidesk_pgdata_10gb"),F("span",null,"当前 Docker 快照没有发现数据库命名卷;请检查 provider-gateway 的 Docker volume 上报。")))}function MJ({title:f,items:u,render:l,limit:_}){let y=u.slice(0,_??12),$=Math.max(0,u.length-y.length);return F(sf,{title:f,eyebrow:`${u.length} items`,className:"docker-side-panel"},u.length===0?F(J0,{title:`暂无 ${f}`,text:"等待 Docker 状态采集"}):F("div",{className:"docker-side-list"},y.map(l),$>0?F("div",{className:"docker-side-more"},`+ ${$} more`):null))}function FC({microservices:f,onRaw:u,onNavigate:l}){let _=f.filter((y)=>OH(y).public===!1);return F("div",{className:"microservice-page","data-testid":"microservice-catalog-page"},F(sf,{title:"用户服务目录",eyebrow:"Provider Mounted User Services"},F("div",{className:"metric-grid"},F(F0,{label:"服务总数",value:f.length,hint:"config.json 用户服务登记"}),F(F0,{label:"私有后端",value:_.length,hint:"不直接暴露公网",tone:"ok"}),F(F0,{label:"D601 服务",value:f.filter((y)=>y.providerId==="D601").length,hint:"compute-node docker"}),F(F0,{label:"集成前端",value:f.filter((y)=>y.frontend?.integrated).length,hint:"UniDesk React 页面"}))),F(sf,{title:"服务映射",eyebrow:"Repo Reference + Runtime"},f.length===0?F(J0,{title:"暂无用户服务",text:"在 config.json 的 microservices 中登记用户服务的 provider、仓库引用和后端映射"}):F("div",{className:"table-wrap"},F("table",{className:"microservice-table"},F("thead",null,F("tr",null,F("th",null,"服务"),F("th",null,"Provider"),F("th",null,"代码引用"),F("th",null,"Docker 引用"),F("th",null,"后端映射"),F("th",null,"开发入口"),F("th",null,"运行态"),F("th",null,"操作"))),F("tbody",null,f.map((y)=>{let $=bH(y),r=nS(y),j=OH(y);return F("tr",{key:y.id,"data-testid":`microservice-row-${Iu(y.id)}`},F("td",null,F("strong",null,y.name),F("code",null,y.id)),F("td",null,F("strong",null,$.providerName||y.providerId),F("code",null,y.providerId)),F("td",null,F("span",null,r.url||"--"),F("code",null,r.commitId||"--")),F("td",null,F("span",null,r.composeFile||"--"),F("code",null,`${r.composeService||"--"} / ${r.containerName||"--"}`)),F("td",null,F(K0,{status:j.public?"warn":"online"},j.public?"public":"private"),F("code",null,`${j.nodeBindHost||"--"}:${j.nodePort||"--"} -> ${j.proxyMode||"--"}`)),F("td",null,F("span",null,y.development?.sshPassthrough?"SSH 透传":"未配置"),F("code",null,y.development?.worktreePath||"--")),F("td",null,F(K0,{status:$.providerStatus==="online"?"online":"warn"},$.providerStatus||"unknown"),F(c_,{data:$.container,empty:"容器快照未上报"})),F("td",null,F("div",{className:"microservice-actions"},y.id==="findjob"?F("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","findjob"),"data-testid":"open-findjob-button"},"打开"):null,y.id==="pipeline"?F("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","pipeline"),"data-testid":"open-pipeline-button"},"打开"):null,y.id==="todo-note"?F("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","todo-note"),"data-testid":"open-todo-note-button"},"打开"):null,y.id==="met-nonlinear"?F("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","met-nonlinear"),"data-testid":"open-met-nonlinear-button"},"打开"):null,y.id==="claudeqq"?F("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","claudeqq"),"data-testid":"open-claudeqq-button"},"打开"):null,y.id==="baidu-netdisk"?F("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","baidu-netdisk"),"data-testid":"open-baidu-netdisk-button"},"打开"):null,y.id==="code-queue"?F("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","code-queue"),"data-testid":"open-code-queue-button"},"打开"):null,y.id==="project-manager"?F("button",{type:"button",className:"ghost-btn",onClick:()=>l("apps","project-manager"),"data-testid":"open-project-manager-button"},"打开"):null,F(b0,{title:`用户服务 ${y.id}`,data:y,onOpen:u}))))}))))))}function JC({nodes:f,onDispatched:u,onRaw:l}){let _=f.filter((M)=>M.status==="online"),[y,$]=bf(_[0]?.providerId||f[0]?.providerId||""),[r,j]=bf("docker.ps"),[A,J]=bf("frontend"),[U,Q]=bf("operator-check"),[W,G]=bf("normal"),[K,H]=bf(!1),[O,z]=bf(""),[Z,N]=bf(!1),[E,q]=bf(null),[Y,w]=bf("");J1(()=>{if(!y&&(_[0]?.providerId||f[0]?.providerId))$(_[0]?.providerId||f[0].providerId)},[f.length,_.length,y]);function B(){return{source:A,note:U,priority:W}}function P(){z(JSON.stringify(B(),null,2)),H(!0)}async function h(M){M.preventDefault(),N(!0),w("");try{let n=K?JSON.parse(O||"{}"):B(),S=await Mf(`${mf.apiBaseUrl}/dispatch`,{method:"POST",body:JSON.stringify({providerId:y,command:r,payload:n})});q(S),await u()}catch(n){w(Df(n,"下发失败"))}finally{N(!1)}}return F("div",{className:"page-grid dispatch-grid"},F(sf,{title:"下发任务",eyebrow:"Real WebSocket Dispatch"},F("form",{className:"dispatch-form",onSubmit:h},F("label",null,"Provider",F("select",{value:y,onChange:(M)=>$(M.target.value)},f.map((M)=>F("option",{key:M.providerId,value:M.providerId},`${M.name} / ${M.providerId}`)))),F("label",null,"Command",F("select",{value:r,onChange:(M)=>j(M.target.value)},F("option",{value:"docker.ps"},"docker.ps"),F("option",{value:"host.ssh"},"host.ssh"),F("option",{value:"microservice.http"},"microservice.http"),F("option",{value:"echo"},"echo"))),F("label",null,"来源",F("input",{value:A,onChange:(M)=>J(M.target.value)})),F("label",null,"备注",F("input",{value:U,onChange:(M)=>Q(M.target.value)})),F("label",null,"优先级",F("select",{value:W,onChange:(M)=>G(M.target.value)},F("option",{value:"normal"},"normal"),F("option",{value:"low"},"low"),F("option",{value:"urgent"},"urgent"))),F("div",{className:"dispatch-actions"},F("button",{type:"button",className:"ghost-btn",onClick:P},"查看原始JSON"),F("button",{type:"submit",disabled:Z||!y},Z?"下发中":"下发任务")),K?F("label",{className:"raw-editor-label"},"高级 Payload",F("textarea",{className:"raw-editor",value:O,onChange:(M)=>z(M.target.value)})):null,F(A0,{error:Y,wide:!0}))),F(sf,{title:"下发结果",eyebrow:"Response"},E?F("div",{className:"result-card"},F(K0,{status:E.status||"queued"},E.status||"queued"),F("dl",null,F("dt",null,"Task ID"),F("dd",null,F("code",null,E.taskId||"--")),F("dt",null,"Provider 在线"),F("dd",null,Py(E.providerOnline))),F(b0,{title:"Dispatch Response",data:E,onOpen:l})):F(J0,{title:"等待操作",text:"任务响应会以结构化结果卡展示"})))}function BH({task:f,onRaw:u}){return F("article",{className:"compact-row"},F(K0,{status:f.status}),F("div",null,F("strong",null,f.command),F("code",null,f.id)),F("span",null,y3(f)?`已等待 ${nJ(f.updatedAt)}`:`耗时 ${x1(TH(f)??0)}`),F(b0,{title:`Task ${f.id}`,data:Y4(f),onOpen:u}))}function mH({task:f}){let u=TH(f),l=y3(f);return F("div",{className:"task-duration"},F("strong",null,u===null?"--":x1(u)),F("span",null,l?`已运行 / 创建 ${Nf(f.createdAt)}`:`创建 ${Nf(f.createdAt)}`))}function UC({task:f}){let u=String(f?.status||"").toLowerCase(),l=f?.result,_=l&&typeof l==="object"&&!Array.isArray(l)?l:{},$=["exitCode","code","signal","timeoutMs","previousStatus","mode"].filter((r)=>_[r]!==void 0&&_[r]!==null);if(u==="failed"){let r=MH(f);return F("div",{className:"task-diagnostic failed"},F("b",null,"失败原因"),F("span",{className:"diagnostic-reason"},Py(r)),$.length>0?F("div",{className:"diagnostic-meta"},$.map((j)=>F("span",{key:j,className:"data-chip"},F("b",null,j),F("span",null,Py(_[j]))))):null)}if(y3(f))return F("div",{className:"task-diagnostic warn"},F("b",null,"等待终态"),F("span",null,`最后更新 ${nJ(f.updatedAt)} 前`));return F("div",{className:"task-diagnostic ok"},F("b",null,"完成摘要"),F(c_,{data:l,empty:"无执行输出"}))}function QC({tasks:f,onRaw:u}){let l=f.filter(y3);return F("div",{"data-testid":"pending-task-page"},F(sf,{title:"待处理任务",eyebrow:`${l.length} Pending`},l.length===0?F(J0,{title:"当前无待处理任务",text:"queued / dispatched / running 会在超时后自动转为 failed;历史记录仍可在任务历史中查看"}):F("div",{className:"table-wrap","data-testid":"pending-task-table"},F("table",null,F("thead",null,F("tr",null,F("th",null,"状态"),F("th",null,"任务"),F("th",null,"Provider"),F("th",null,"已等待"),F("th",null,"载荷摘要"),F("th",null,"操作"))),F("tbody",null,l.map((_)=>F("tr",{key:_.id},F("td",null,F(K0,{status:_.status})),F("td",null,F("strong",null,_.command),F("code",null,_.id)),F("td",null,F("code",null,_.providerId)),F("td",null,nJ(_.updatedAt)),F("td",null,F(c_,{data:_.payload})),F("td",null,F(b0,{title:`Pending Task ${_.id}`,data:Y4(_),onOpen:u})))))))))}function WC({tasks:f,onRaw:u}){return F("div",{"data-testid":"task-history-page"},F(sf,{title:"任务历史",eyebrow:`${f.length} Tasks`},f.length===0?F(J0,{title:"暂无任务",text:"下发任务后会在这里看到生命周期"}):F("div",{className:"table-wrap"},F("table",{className:"task-history-table"},F("thead",null,F("tr",null,F("th",null,"状态"),F("th",null,"任务"),F("th",null,"Provider"),F("th",null,"任务耗时"),F("th",null,"载荷摘要"),F("th",null,"诊断信息"),F("th",null,"更新时间"),F("th",null,"操作"))),F("tbody",null,f.map((l)=>F("tr",{key:l.id,"data-testid":`task-row-${Iu(l.id)}`},F("td",null,F(K0,{status:l.status})),F("td",null,F("strong",null,l.command),F("code",null,l.id)),F("td",null,F("code",null,l.providerId)),F("td",null,F(mH,{task:l})),F("td",null,F(c_,{data:l.payload})),F("td",null,F(UC,{task:l})),F("td",null,Nf(l.updatedAt)),F("td",null,F(b0,{title:`Task ${l.id}`,data:Y4(l),onOpen:u})))))))))}function zC({tasks:f,onRaw:u}){let l=f.filter((_)=>["succeeded","failed"].includes(_.status));return F(sf,{title:"执行结果",eyebrow:"Finished Tasks"},l.length===0?F(J0,{title:"暂无结果",text:"任务完成后展示 provider 返回的结构化摘要"}):F("div",{className:"result-grid"},l.map((_)=>F("article",{key:_.id,className:"result-card"},F("div",{className:"node-card-head"},F("strong",null,_.command),F(K0,{status:_.status})),F("code",null,_.id),F(c_,{data:_.result,empty:"无执行输出"}),F(b0,{title:`Task Result ${_.id}`,data:Y4(_),onOpen:u})))))}function GC(f){if(!f||typeof f!=="object")return"--";if(f.type==="interval")return`每 ${x1(Number(f.everySeconds||0))}`;return`每天 ${f.timeOfDay||"03:00"} UTC`}function KC(f){if(!f||typeof f!=="object")return"--";if(f.type==="pgdata_backup")return`PGDATA -> ${f.remoteBaseDir||"/SERVER_DATA/UNIDESK_PG_DATA"}`;if(f.type==="dispatch")return`${f.providerId||"--"} / ${f.command||"--"}`;return String(f.type||"--")}function NC(f){let u=String(f||"").toLowerCase();if(u==="succeeded")return"online";if(u==="failed")return"failed";if(u==="running"||u==="queued")return"warn";return u}function ZC(f){let u=Number(f?.durationMs);if(Number.isFinite(u)&&u>=0)return x1(u/1000);let l=U1(f?.startedAt||f?.createdAt);if(l===null)return"--";let y=U1(f?.finishedAt)??Date.now();return x1(Math.max(0,(y-l)/1000))}function YH(f){return{id:"unidesk-pgdata-baidu-daily",name:"PGDATA daily Baidu Netdisk backup",description:"Daily PostgreSQL physical base backup uploaded to Baidu Netdisk /SERVER_DATA with monthly rotation.",enabled:!0,timeOfDay:"03:30",actionType:"pgdata_backup",providerId:f[0]?.providerId||"main-server",command:"echo",payloadJson:JSON.stringify({source:"scheduled-task",message:"hello from scheduler"},null,2),remoteBaseDir:"/SERVER_DATA/UNIDESK_PG_DATA",stagingSubdir:"server-data/unidesk-pg-data",timeoutMs:"3600000"}}function EC({schedules:f,scheduleRuns:u,nodes:l,refresh:_,onRaw:y}){let[$,r]=bf(YH(l||[])),[j,A]=bf(!1),[J,U]=bf(""),[Q,W]=bf(""),G=[...u||[]].sort((E,q)=>(U1(q.updatedAt)??0)-(U1(E.updatedAt)??0));function K(E,q){r((Y)=>({...Y,[E]:q}))}function H(E){let q=E?.action||{};r({id:E?.id||"",name:E?.name||"",description:E?.description||"",enabled:E?.enabled!==!1,timeOfDay:E?.schedule?.timeOfDay||"03:30",actionType:q.type||"dispatch",providerId:q.providerId||l[0]?.providerId||"main-server",command:q.command||"echo",payloadJson:JSON.stringify(q.payload||{source:"scheduled-task"},null,2),remoteBaseDir:q.remoteBaseDir||"/SERVER_DATA/UNIDESK_PG_DATA",stagingSubdir:q.stagingSubdir||"server-data/unidesk-pg-data",timeoutMs:String(q.timeoutMs||3600000)}),W(`正在编辑 ${E?.id||""}`)}function O(){let E={id:$.id,name:$.name,description:$.description,enabled:$.enabled,concurrencyPolicy:"skip",schedule:{type:"daily",timeOfDay:$.timeOfDay,timezone:"Etc/UTC"}};if($.actionType==="pgdata_backup")return{...E,action:{type:"pgdata_backup",volumeName:"unidesk_pgdata_10gb",remoteBaseDir:$.remoteBaseDir,stagingSubdir:$.stagingSubdir,timeoutMs:Number($.timeoutMs)||3600000,cleanupLocal:!0}};return{...E,action:{type:"dispatch",providerId:$.providerId,command:$.command,payload:JSON.parse($.payloadJson||"{}"),timeoutMs:Number($.timeoutMs)||600000}}}async function z(E){E.preventDefault(),A(!0),U(""),W("");try{let q=O(),Y=encodeURIComponent(String(q.id));await Mf(`${mf.apiBaseUrl}/schedules/${Y}`,{method:"PUT",body:JSON.stringify(q)}),W("定时任务已保存"),await _()}catch(q){U(Df(q,"保存定时任务失败"))}finally{A(!1)}}async function Z(E){if(!E?.id)return;A(!0),U(""),W("");try{await Mf(`${mf.apiBaseUrl}/schedules/${encodeURIComponent(E.id)}`,{method:"DELETE"}),W(`已删除 ${E.id}`),await _()}catch(q){U(Df(q,"删除定时任务失败"))}finally{A(!1)}}async function N(E){if(!E?.id)return;A(!0),U(""),W("");try{let q=await Mf(`${mf.apiBaseUrl}/schedules/${encodeURIComponent(E.id)}/run`,{method:"POST",body:"{}"});W(`已触发 ${E.id} / ${q?.run?.id||"run"}`),await _()}catch(q){U(Df(q,"触发定时任务失败"))}finally{A(!1)}}return F("div",{className:"page-grid scheduled-task-page","data-testid":"scheduled-task-page"},F(sf,{title:"定时任务",eyebrow:`${(f||[]).length} Schedules`},(f||[]).length===0?F(J0,{title:"暂无定时任务",text:"创建 daily / dispatch / PGDATA backup 任务后会在这里展示下一次执行时间和最近结果"}):F("div",{className:"schedule-card-grid"},(f||[]).map((E)=>F("article",{key:E.id,className:"schedule-card","data-testid":`schedule-row-${Iu(E.id)}`},F("div",{className:"node-card-head"},F("strong",null,E.name||E.id),F(K0,{status:E.enabled?"online":"warn"},E.enabled?"enabled":"disabled")),F("code",null,E.id),F("dl",null,F("dt",null,"计划"),F("dd",null,GC(E.schedule)),F("dt",null,"动作"),F("dd",null,KC(E.action)),F("dt",null,"下次执行"),F("dd",null,Nf(E.nextRunAt)),F("dt",null,"最近执行"),F("dd",null,E.lastRunAt?`${Nf(E.lastRunAt)} / ${E.lastRunId||"--"}`:"--")),F("div",{className:"dispatch-actions"},F("button",{type:"button",className:"ghost-btn",disabled:j,onClick:()=>H(E)},"编辑"),F("button",{type:"button",className:"ghost-btn",disabled:j,onClick:()=>N(E),"data-testid":`schedule-run-${Iu(E.id)}`},"手动触发"),F("button",{type:"button",className:"ghost-btn danger",disabled:j,onClick:()=>Z(E)},"删除"),F(b0,{title:`Schedule ${E.id}`,data:E,onOpen:y})))))),F(sf,{title:$.id?"配置定时任务":"新建定时任务",eyebrow:"CRUD"},F("form",{className:"dispatch-form schedule-form",onSubmit:z},F("label",null,"ID",F("input",{value:$.id,onChange:(E)=>K("id",E.target.value)})),F("label",null,"名称",F("input",{value:$.name,onChange:(E)=>K("name",E.target.value)})),F("label",null,"每日执行时间 UTC",F("input",{value:$.timeOfDay,placeholder:"03:30",onChange:(E)=>K("timeOfDay",E.target.value)})),F("label",null,"启用",F("select",{value:$.enabled?"true":"false",onChange:(E)=>K("enabled",E.target.value==="true")},F("option",{value:"true"},"enabled"),F("option",{value:"false"},"disabled"))),F("label",null,"动作类型",F("select",{value:$.actionType,onChange:(E)=>K("actionType",E.target.value)},F("option",{value:"pgdata_backup"},"PGDATA 备份到百度网盘"),F("option",{value:"dispatch"},"Provider Dispatch"))),$.actionType==="pgdata_backup"?[F("label",{key:"remote"},"网盘根目录",F("input",{value:$.remoteBaseDir,onChange:(E)=>K("remoteBaseDir",E.target.value)})),F("label",{key:"staging"},"本地 staging 子目录",F("input",{value:$.stagingSubdir,onChange:(E)=>K("stagingSubdir",E.target.value)}))]:[F("label",{key:"provider"},"Provider",F("select",{value:$.providerId,onChange:(E)=>K("providerId",E.target.value)},(l||[]).map((E)=>F("option",{key:E.providerId,value:E.providerId},`${E.name} / ${E.providerId}`)))),F("label",{key:"command"},"Command",F("select",{value:$.command,onChange:(E)=>K("command",E.target.value)},F("option",{value:"echo"},"echo"),F("option",{value:"docker.ps"},"docker.ps"),F("option",{value:"host.ssh"},"host.ssh"),F("option",{value:"microservice.http"},"microservice.http"))),F("label",{key:"payload",className:"raw-editor-label"},"Payload JSON",F("textarea",{className:"raw-editor",value:$.payloadJson,onChange:(E)=>K("payloadJson",E.target.value)}))],F("label",null,"超时 ms",F("input",{value:$.timeoutMs,onChange:(E)=>K("timeoutMs",E.target.value)})),F("label",{className:"raw-editor-label"},"描述",F("textarea",{className:"raw-editor compact",value:$.description,onChange:(E)=>K("description",E.target.value)})),F("div",{className:"dispatch-actions"},F("button",{type:"button",className:"ghost-btn",disabled:j,onClick:()=>r(YH(l||[]))},"重置"),F("button",{type:"submit",disabled:j||!$.id},j?"保存中":"保存任务")),Q?F("p",{className:"muted paragraph"},Q):null,F(A0,{error:J,wide:!0}))),F(sf,{title:"历史执行记录",eyebrow:`${G.length} Runs`},G.length===0?F(J0,{title:"暂无执行记录",text:"定时触发或手动触发后会生成 run history"}):F("div",{className:"table-wrap"},F("table",{className:"task-history-table schedule-run-table"},F("thead",null,F("tr",null,F("th",null,"状态"),F("th",null,"任务"),F("th",null,"触发"),F("th",null,"耗时"),F("th",null,"结果摘要"),F("th",null,"更新时间"),F("th",null,"操作"))),F("tbody",null,G.map((E)=>F("tr",{key:E.id,"data-testid":`schedule-run-row-${Iu(E.id)}`},F("td",null,F(K0,{status:NC(E.status)},E.status)),F("td",null,F("strong",null,E.scheduleId),F("code",null,E.id),E.taskId?F("code",null,E.taskId):null),F("td",null,E.trigger||"--"),F("td",null,ZC(E)),F("td",null,F(c_,{data:E.result||E.error,empty:"无结果"})),F("td",null,Nf(E.updatedAt)),F("td",null,F(b0,{title:`Schedule Run ${E.id}`,data:E,onOpen:y})))))))))}function HC({data:f}){let u=f.overview||{};return F("div",{className:"page-grid topology-grid"},F(sf,{title:"公开入口",eyebrow:"Public"},F("div",{className:"endpoint-list"},F("article",null,F("b",null,"Frontend"),F("span",null,mf.frontendPublicUrl||window.location.origin),F(K0,{status:"online"},"public")),F("article",null,F("b",null,"Provider Ingress"),F("span",null,mf.providerIngressPublicUrl||"ws://public/ws/provider"),F(K0,{status:"online"},"public")))),F(sf,{title:"内部服务",eyebrow:"Docker Network Only"},F("div",{className:"endpoint-list"},F("article",null,F("b",null,"backend-core API"),F("span",null,"http://backend-core:8080"),F(K0,{status:"internal"},"internal")),F("article",null,F("b",null,"database"),F("span",null,"postgres://database:5432/unidesk"),F(K0,{status:"internal"},"internal")))),F(sf,{title:"运行态",eyebrow:"Runtime"},F("div",{className:"metric-grid"},F(F0,{label:"DB Ready",value:u.dbReady?"YES":"NO",hint:"internal health"}),F(F0,{label:"Online Nodes",value:u.onlineNodeCount??0,hint:"provider-gateway self-link"}))))}function OC({session:f}){return F(sf,{title:"认证策略",eyebrow:"Frontend Login"},F("div",{className:"policy-grid"},F("article",null,F("span",null,"默认账号"),F("strong",null,mf.authUsername||"admin")),F("article",null,F("span",null,"当前会话"),F("strong",null,f?.user?.username||"--")),F("article",null,F("span",null,"Session TTL"),F("strong",null,`${mf.sessionTtlSeconds||0}s`)),F("article",null,F("span",null,"API 访问"),F("strong",null,"同源 Cookie 保护"))),F("p",{className:"muted paragraph"},"浏览器只访问 frontend 同源接口;frontend 容器使用 Docker 内网代理 backend-core API。"))}function VC(){return F(sf,{title:"安全边界",eyebrow:"Exposure Rule"},F("div",{className:"security-board"},F("article",{className:"allow"},F("b",null,"允许公网"),F("span",null,"frontend 登录入口"),F("span",null,"provider ingress WebSocket/health")),F("article",{className:"deny"},F("b",null,"禁止公网"),F("span",null,"backend-core REST API"),F("span",null,"PostgreSQL database")),F("article",null,F("b",null,"数据库卷"),F("span",null,"named volume unidesk_pgdata_10gb"),F("span",null,"CLI stop/start 不删除数据卷"))))}function qC({activeModule:f,activeTab:u,data:l,session:_,refresh:y,onRaw:$,onNavigate:r}){if(f==="ops"&&u==="status")return F(bS,{data:l,onRaw:$,onNavigate:r});if(f==="ops"&&u==="performance")return F(uC,{onRaw:$});if(f==="ops"&&u==="events")return F(hS,{events:l.events,onRaw:$});if(f==="ops"&&u==="logs")return F(IS,{logs:l.logs,onRaw:$});if(f==="nodes"&&u==="list")return F(pS,{nodes:l.nodes,onRaw:$});if(f==="nodes"&&u==="monitor")return F(kS,{nodes:l.nodes,systemStatuses:l.systemStatuses,tasks:l.tasks,onRaw:$,refresh:y});if(f==="nodes"&&u==="docker")return F(jC,{nodes:l.nodes,dockerStatuses:l.dockerStatuses,onRaw:$});if(f==="nodes"&&u==="gateway")return F(yC,{nodes:l.nodes,tasks:l.tasks,onRaw:$});if(f==="nodes"&&u==="labels")return F(mS,{nodes:l.nodes});if(f==="nodes"&&u==="heartbeats")return F(gS,{nodes:l.nodes});if(f==="tasks"&&u==="dispatch")return F(JC,{nodes:l.nodes,onDispatched:y,onRaw:$});if(f==="tasks"&&u==="scheduled")return F(EC,{schedules:l.schedules,scheduleRuns:l.scheduleRuns,nodes:l.nodes,refresh:y,onRaw:$});if(f==="tasks"&&u==="pending")return F(QC,{tasks:l.pendingTasks,onRaw:$});if(f==="tasks"&&u==="history")return F(WC,{tasks:l.tasks,onRaw:$});if(f==="tasks"&&u==="results")return F(zC,{tasks:l.tasks,onRaw:$});if(f==="apps"&&u==="catalog")return F(FC,{microservices:l.microservices,onRaw:$,onNavigate:r});if(f==="apps"&&u==="todo-note")return F(QH,{microservices:l.microservices,onRaw:$,apiBaseUrl:mf.apiBaseUrl});if(f==="apps"&&u==="findjob")return F($K,{microservices:l.microservices,onRaw:$,apiBaseUrl:mf.apiBaseUrl});if(f==="apps"&&u==="pipeline")return F(yH,{microservices:l.microservices,onRaw:$,apiBaseUrl:mf.apiBaseUrl});if(f==="apps"&&u==="met-nonlinear")return F(JK,{microservices:l.microservices,onRaw:$,apiBaseUrl:mf.apiBaseUrl});if(f==="apps"&&u==="claudeqq")return F(vz,{microservices:l.microservices,onRaw:$,apiBaseUrl:mf.apiBaseUrl});if(f==="apps"&&u==="baidu-netdisk")return F(Rz,{microservices:l.microservices,onRaw:$,apiBaseUrl:mf.apiBaseUrl});if(f==="apps"&&u==="filebrowser")return F(yK,{microservices:l.microservices,onRaw:$,apiBaseUrl:mf.apiBaseUrl});if(f==="apps"&&u==="code-queue")return F(dG,{microservices:l.microservices,onRaw:$,apiBaseUrl:mf.apiBaseUrl,initialTasksData:VS});if(f==="apps"&&u==="project-manager")return F(jH,{microservices:l.microservices,onRaw:$,apiBaseUrl:mf.apiBaseUrl});if(f==="config"&&u==="topology")return F(HC,{data:l});if(f==="config"&&u==="auth")return F(OC,{session:_});if(f==="config"&&u==="security")return F(VC);return F(J0,{title:"未找到页面",text:"请选择左侧主模块和顶部子功能标签"})}function LC({session:f,onLogout:u}){let l=gj(Xl,window.location.pathname),[_,y]=bf(l.moduleId),[$,r]=bf({...n6,[l.moduleId]:l.tabId}),[j,A]=bf({overview:null,nodes:[],systemStatuses:[],dockerStatuses:[],microservices:[],events:[],tasks:[],pendingTasks:[],schedules:[],scheduleRuns:[],logs:[]}),[J,U]=bf({ok:!1,text:"连接中"}),[Q,W]=bf(null),[G,K]=bf(new Date),[H,O]=bf(null),[z,Z]=bf(!1),[N,E]=bf(!1),q=i_.default.useRef(!1),Y=Xl.moduleById[_]||Xl.modules[0],w=$[_]||n6[_]||Y.tabs[0].id,B=Array.isArray(j.microservices)?j.microservices:[],P=B.length===0&&_==="apps"&&w==="code-queue"?[qS]:B,h=P===B?j:{...j,microservices:P},M=_==="apps"?P.find((d)=>String(d?.id||"")===w):null,n=M?bH(M):{},S=Y.tabs.find((d)=>d.id===w)?.label||w,T=M?[{key:"microservice",label:"用户服务",value:`${S} ${n.providerStatus==="online"?"在线":n.providerStatus||"未知"}`,tone:n.providerStatus==="online"?"ok":"warn",testId:"active-microservice-status"}]:[];async function i(){if(q.current)return;q.current=!0,E(!0);try{let d=[],a=(Wf,Ef)=>{d.push([Wf,Mf(Ef)])},I=_==="ops"&&w==="status",ff=I||_==="config"&&w==="topology",yf=I||_==="nodes"||_==="tasks"&&(w==="dispatch"||w==="scheduled"),rf=_==="apps"&&w!=="code-queue";if(ff)a("overview",`${mf.apiBaseUrl}/overview`);if(yf)a("nodes",`${mf.apiBaseUrl}/nodes`);if(_==="nodes"&&w==="monitor")a("systemStatuses",`${mf.apiBaseUrl}/nodes/system-status?limit=60`),a("tasks",`${mf.apiBaseUrl}/tasks?limit=120&summary=1`);else if(_==="nodes"&&w==="docker")a("dockerStatuses",`${mf.apiBaseUrl}/nodes/docker-status`);else if(_==="nodes"&&w==="gateway")a("tasks",`${mf.apiBaseUrl}/tasks?limit=300&summary=1`);else if(_==="tasks"&&w==="scheduled")a("schedules",`${mf.apiBaseUrl}/schedules?limit=100`),a("scheduleRuns",`${mf.apiBaseUrl}/schedules/runs?limit=100`);else if(_==="tasks"&&w==="pending")a("pendingTasks",`${mf.apiBaseUrl}/tasks?status=pending&limit=100&summary=1`);else if(_==="tasks"&&(w==="history"||w==="results"))a("tasks",`${mf.apiBaseUrl}/tasks?limit=300&summary=1`);else if(I)a("tasks",`${mf.apiBaseUrl}/tasks?limit=8&lite=1`),a("pendingTasks",`${mf.apiBaseUrl}/tasks?status=pending&limit=20&lite=1`);if(rf)a("microservices",`${mf.apiBaseUrl}/microservices`);if(_==="ops"&&w==="events")a("events",`${mf.apiBaseUrl}/events?limit=100`);if(_==="ops"&&w==="logs")a("logs","/logs?limit=100");await Promise.all(d.map(async([Wf,Ef])=>{let Gf=await Ef,c={};if(Wf==="overview")c.overview=Gf;if(Wf==="nodes")c.nodes=Gf.nodes||[];if(Wf==="systemStatuses")c.systemStatuses=Gf.systemStatuses||[];if(Wf==="dockerStatuses")c.dockerStatuses=Gf.dockerStatuses||[];if(Wf==="microservices")c.microservices=Gf.microservices||[];if(Wf==="events")c.events=Gf.events||[];if(Wf==="tasks")c.tasks=Gf.tasks||[];if(Wf==="pendingTasks")c.pendingTasks=Gf.tasks||[];if(Wf==="schedules")c.schedules=Gf.schedules||[];if(Wf==="scheduleRuns")c.scheduleRuns=Gf.runs||[];if(Wf==="logs")c.logs=Gf.logs||[];A((o)=>({...o,...c}))})),U({ok:!0,text:"核心在线"}),W(new Date)}catch(d){if(U({ok:!1,text:Df(d,"连接失败")}),d.status===401)u(!1)}finally{q.current=!1,E(!1)}}J1(()=>{let d=()=>{if(!ZH())return;i()};d();let a=setInterval(d,LS(_,w)),I=()=>{if(ZH())d()};return document.addEventListener("visibilitychange",I),()=>{clearInterval(a),document.removeEventListener("visibilitychange",I)}},[_,w]),J1(()=>{let d=setInterval(()=>K(new Date),1000);return()=>clearInterval(d)},[]),J1(()=>{let d=zK(Xl,window.location.pathname);if(d&&window.location.pathname!==d)window.history.replaceState(null,"",d)},[]),J1(()=>{let d=()=>{let a=gj(Xl,window.location.pathname);y(a.moduleId),r((I)=>({...I,[a.moduleId]:a.tabId})),O(null)};return window.addEventListener("popstate",d),()=>window.removeEventListener("popstate",d)},[]),J1(()=>{window.scrollTo({top:0,left:0,behavior:"auto"})},[_,w]);function C(d,a,I="push"){let ff=Xl.moduleById[d]?d:Xl.fallbackTarget.moduleId,yf=Xl.moduleById[ff]?.tabs.some((Wf)=>Wf.id===a)?a:n6[ff]||Xl.moduleById[ff]?.tabs[0]?.id||Xl.fallbackTarget.tabId;y(ff),r((Wf)=>({...Wf,[ff]:yf}));let rf=m2(Xl,ff,yf);if(window.location.pathname!==rf){let Wf=I==="replace"?"replaceState":"pushState";window.history[Wf](null,"",rf)}}function v(d,a){O({title:d,data:a})}let[X,D]=bf(!1),{unreadCount:p,notifications:m}=Lu(),s=m.length>0?m[m.length-1]:null;return F("div",{className:`shell ${z?"rail-collapsed":""}`,"data-testid":"app-shell"},F(RS,{activeModule:_,activeTabs:$,onNavigate:C,collapsed:z,onToggle:()=>Z((d)=>!d)}),F("main",{className:"workspace"},F(iS,{connection:J,lastRefresh:Q,onRefresh:i,onLogout:()=>u(!0),session:f,clock:G,activeStatusItems:T,onNotificationToggle:()=>D((d)=>!d),unreadCount:p}),F(xS,{module:Y,activeTab:w,onNavigate:C}),F(PJ.Provider,{value:N},F(qC,{activeModule:_,activeTab:w,data:h,session:f,refresh:i,onRaw:v,onNavigate:C}))),F(SS,{raw:H,onClose:()=>O(null)}),s&&F(NH,{key:s.id,notification:s}),X&&F(KH,{onClose:()=>D(!1)}))}function XC(){let[f,u]=bf(!0),[l,_]=bf(null);async function y(){u(!0);try{let r=await Mf("/api/session");_(r.authenticated?r:null)}catch{_(null)}finally{u(!1)}}async function $(r){if(r)try{await Mf("/logout",{method:"POST"})}catch{}_(null)}if(J1(()=>{y()},[]),f)return F("main",{className:"loading-screen"},F("div",{className:"brand-mark"},"UD"),F("span",null,"加载会话"));if(!l)return F(CS,{onLogin:_});return F(cz,null,F(LC,{session:l,onLogout:$}))}var gH=document.getElementById("root");if(gH===null)throw Error("root element not found");wH.createRoot(gH).render(F(XC));})(); diff --git a/src/components/frontend/public/style.css b/src/components/frontend/public/style.css index 97979465..6d2407d9 100644 --- a/src/components/frontend/public/style.css +++ b/src/components/frontend/public/style.css @@ -351,7 +351,7 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; } align-items: start; } -.overview-grid .panel:nth-child(n+3), .dispatch-grid .panel:first-child, .topology-grid .panel:nth-child(3) { grid-column: 1 / -1; } +.overview-grid .panel:nth-child(n+3), .dispatch-grid .panel:first-child, .scheduled-task-page .panel:first-child, .scheduled-task-page .panel:nth-child(3), .topology-grid .panel:nth-child(3) { grid-column: 1 / -1; } .panel { min-width: 0; @@ -412,17 +412,17 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; } } .metric-hint { margin-top: 3px; color: var(--muted); font-size: 11px; } -.node-card-list, .compact-list, .log-list, .heartbeat-list, .endpoint-list, .policy-grid, .security-board, .result-grid { +.node-card-list, .compact-list, .log-list, .heartbeat-list, .endpoint-list, .policy-grid, .security-board, .result-grid, .schedule-card-grid { display: grid; gap: 8px; } -.node-card, .compact-row, .log-row, .heartbeat-row, .endpoint-list article, .policy-grid article, .security-board article, .result-card, .label-card { +.node-card, .compact-row, .log-row, .heartbeat-row, .endpoint-list article, .policy-grid article, .security-board article, .result-card, .label-card, .schedule-card { border: 1px solid var(--line-soft); background: var(--panel-3); } -.node-card, .result-card { padding: 9px; } +.node-card, .result-card, .schedule-card { padding: 9px; } .node-card-head { display: flex; justify-content: space-between; @@ -1362,6 +1362,34 @@ td { color: var(--text); } letter-spacing: 0.12em; } .dispatch-actions { display: flex; gap: 6px; align-items: end; } +.schedule-card-grid { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); } +.schedule-card dl { + display: grid; + grid-template-columns: 92px minmax(0, 1fr); + gap: 5px 8px; + margin: 8px 0; +} +.schedule-card dt { color: var(--muted); } +.schedule-card dd { margin: 0; overflow-wrap: anywhere; } +.schedule-form { + grid-template-columns: inherit; +} +.dispatch-form, .schedule-form { + display: grid; + grid-template-columns: 1.2fr 180px 150px 1fr 140px auto; + gap: 8px; + align-items: end; +} +.dispatch-form label, .schedule-form label { + display: grid; + gap: 4px; + color: var(--muted); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.12em; +} +.raw-editor.compact { min-height: 72px; } +.schedule-run-table { min-width: 1180px; } .dispatch-actions button[type="submit"], .login-form button[type="submit"] { min-height: 32px; padding: 0 12px; @@ -5136,7 +5164,7 @@ 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, .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-form, .schedule-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, .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; } @@ -5151,7 +5179,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } .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, .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; } + .overview-grid .panel:nth-child(n+3), .dispatch-grid .panel:first-child, .scheduled-task-page .panel:first-child, .scheduled-task-page .panel:nth-child(3), .topology-grid .panel:nth-child(3) { grid-column: 1; } } @media (max-width: 760px) { @@ -5323,7 +5351,7 @@ 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, .code-queue-metrics, .codex-stats-summary-grid, .codex-form-grid, .baidu-doc-grid { grid-template-columns: 1fr; } + .metric-grid, .policy-grid, .security-board, .dispatch-form, .schedule-form, .schedule-card-grid, .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; } @@ -5870,3 +5898,188 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } min-height: 360px; } } + +.notification-icon-btn { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--text); + cursor: pointer; + font-size: 18px; + transition: background 0.15s; +} +.notification-icon-btn:hover { + background: var(--panel-2); +} +.notification-icon-btn.has-unread { + color: var(--accent); +} +.notification-icon-btn .notification-badge { + position: absolute; + top: 2px; + right: 2px; + min-width: 16px; + height: 16px; + padding: 0 4px; + border-radius: 8px; + background: var(--danger); + color: #fff; + font-size: 10px; + font-weight: 600; + line-height: 16px; + text-align: center; +} + +.notification-popup { + position: absolute; + top: calc(100% + 8px); + right: 0; + width: 360px; + max-height: 480px; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: var(--shadow); + z-index: 9999; + overflow: hidden; +} +.notification-popup-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--line); + font-weight: 600; + font-size: 14px; +} +.notification-popup-actions { + display: flex; + gap: 8px; +} +.notification-popup-clear, +.notification-popup-close { + padding: 4px 8px; + border: none; + border-radius: 4px; + background: transparent; + color: var(--muted); + cursor: pointer; + font-size: 12px; +} +.notification-popup-clear:hover, +.notification-popup-close:hover { + background: var(--panel-2); + color: var(--text); +} +.notification-popup-empty { + padding: 32px 16px; + text-align: center; + color: var(--muted); + font-size: 13px; +} +.notification-popup-list { + max-height: 400px; + overflow-y: auto; +} +.notification-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + border-bottom: 1px solid var(--line-soft); +} +.notification-item:last-child { + border-bottom: none; +} +.notification-item.success .notification-item-icon { + color: var(--ok); +} +.notification-item.error .notification-item-icon { + color: var(--danger); +} +.notification-item-icon { + flex-shrink: 0; + font-size: 14px; + font-weight: 700; +} +.notification-item-message { + flex: 1; + font-size: 13px; + color: var(--text); + word-break: break-word; +} +.notification-item-dismiss { + flex-shrink: 0; + padding: 2px 6px; + border: none; + border-radius: 4px; + background: transparent; + color: var(--muted); + cursor: pointer; + font-size: 14px; + line-height: 1; +} +.notification-item-dismiss:hover { + background: var(--panel-2); + color: var(--text); +} + +.notification-banner { + position: fixed; + top: 70px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 10px; + padding: 12px 20px; + border-radius: 8px; + box-shadow: var(--shadow); + z-index: 10000; + font-size: 14px; + animation: notification-slide-in 0.2s ease-out; +} +@keyframes notification-slide-in { + from { + opacity: 0; + transform: translateX(-50%) translateY(-10px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} +.notification-banner.success { + background: var(--ok); + color: #fff; +} +.notification-banner.error { + background: var(--danger); + color: #fff; +} +.notification-banner-icon { + font-size: 16px; + font-weight: 700; +} +.notification-banner-message { + flex: 1; +} +.notification-banner-dismiss { + padding: 2px 6px; + border: none; + border-radius: 4px; + background: rgba(255,255,255,0.2); + color: inherit; + cursor: pointer; + font-size: 14px; + line-height: 1; +} +.notification-banner-dismiss:hover { + background: rgba(255,255,255,0.3); +} diff --git a/src/components/frontend/src/app.tsx b/src/components/frontend/src/app.tsx index 69331400..ac4f49da 100644 --- a/src/components/frontend/src/app.tsx +++ b/src/components/frontend/src/app.tsx @@ -15,6 +15,8 @@ import { TopStatusBar } from "./top-status"; import { LoadingTitle } from "./loading-indicator"; import { errorMessage, requestJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; +import { NotificationProvider, useNotification } from "./notification-context"; +import { NotificationPopup, NotificationBanner } from "./notification-popup"; type AnyRecord = Record; @@ -61,7 +63,7 @@ function isDocumentVisible(): boolean { function shellRefreshIntervalMs(moduleId: string, tabId: string): number { if (moduleId === "ops" && tabId === "status") return 5_000; if (moduleId === "nodes" && tabId === "monitor") return 5_000; - if (moduleId === "tasks" && (tabId === "dispatch" || tabId === "pending")) return 5_000; + if (moduleId === "tasks" && (tabId === "dispatch" || tabId === "scheduled" || tabId === "pending")) return 5_000; if (moduleId === "nodes" || moduleId === "ops") return 10_000; if (moduleId === "apps") return 15_000; if (moduleId === "tasks") return 15_000; @@ -502,7 +504,7 @@ function LoginScreen({ onLogin }: AnyRecord) { ); } -function TopBar({ connection, lastRefresh, onRefresh, onLogout, session, clock, activeStatusItems = [] }: AnyRecord) { +function TopBar({ connection, lastRefresh, onRefresh, onLogout, session, clock, activeStatusItems = [], onNotificationToggle, unreadCount = 0 }: AnyRecord) { const statusItems = [ { key: "core", label: "核心", value: connection.text, tone: connection.ok ? "ok" : "fail", testId: "conn-text" }, ...(Array.isArray(activeStatusItems) ? activeStatusItems : []), @@ -517,8 +519,18 @@ function TopBar({ connection, lastRefresh, onRefresh, onLogout, session, clock, title: "状态", items: statusItems, actions: [ - h("button", { key: "refresh", type: "button", className: "ghost-btn", onClick: onRefresh }, "刷新"), - h("button", { key: "logout", type: "button", className: "ghost-btn danger", onClick: onLogout }, "退出"), + h("button", { + key: "notification", + type: "button", + className: `notification-icon-btn ${unreadCount > 0 ? "has-unread" : ""}`, + onClick: onNotificationToggle, + "aria-label": "通知", + }, + "🔔", + unreadCount > 0 ? h("span", { key: "badge", className: "notification-badge" }, unreadCount > 99 ? "99+" : unreadCount) : null + ), + h("button", { key: "refresh", type: "button", className: "ghost-btn", onClick: onRefresh }, "刷新"), + h("button", { key: "logout", type: "button", className: "ghost-btn danger", onClick: onLogout }, "退出"), ], }), ); @@ -1812,6 +1824,244 @@ function TaskResultsPage({ tasks, onRaw }: AnyRecord) { ); } +function scheduleSpecLabel(schedule: any): string { + if (!schedule || typeof schedule !== "object") return "--"; + if (schedule.type === "interval") return `每 ${fmtDuration(Number(schedule.everySeconds || 0))}`; + return `每天 ${schedule.timeOfDay || "03:00"} UTC`; +} + +function scheduleActionLabel(action: any): string { + if (!action || typeof action !== "object") return "--"; + if (action.type === "pgdata_backup") return `PGDATA -> ${action.remoteBaseDir || "/SERVER_DATA/UNIDESK_PG_DATA"}`; + if (action.type === "dispatch") return `${action.providerId || "--"} / ${action.command || "--"}`; + return String(action.type || "--"); +} + +function scheduleRunTone(status: any): string { + const value = String(status || "").toLowerCase(); + if (value === "succeeded") return "online"; + if (value === "failed") return "failed"; + if (value === "running" || value === "queued") return "warn"; + return value; +} + +function scheduleRunDuration(run: any): string { + const ms = Number(run?.durationMs); + if (Number.isFinite(ms) && ms >= 0) return fmtDuration(ms / 1000); + const started = timeMs(run?.startedAt || run?.createdAt); + if (started === null) return "--"; + const finished = timeMs(run?.finishedAt); + const ended = finished ?? Date.now(); + return fmtDuration(Math.max(0, (ended - started) / 1000)); +} + +function defaultScheduleForm(nodes: any[]): AnyRecord { + return { + id: "unidesk-pgdata-baidu-daily", + name: "PGDATA daily Baidu Netdisk backup", + description: "Daily PostgreSQL physical base backup uploaded to Baidu Netdisk /SERVER_DATA with monthly rotation.", + enabled: true, + timeOfDay: "03:30", + actionType: "pgdata_backup", + providerId: nodes[0]?.providerId || "main-server", + command: "echo", + payloadJson: JSON.stringify({ source: "scheduled-task", message: "hello from scheduler" }, null, 2), + remoteBaseDir: "/SERVER_DATA/UNIDESK_PG_DATA", + stagingSubdir: "server-data/unidesk-pg-data", + timeoutMs: "3600000", + }; +} + +function TaskScheduledPage({ schedules, scheduleRuns, nodes, refresh, onRaw }: AnyRecord) { + const [form, setForm] = useState(defaultScheduleForm(nodes || [])); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + const [notice, setNotice] = useState(""); + const sortedRuns = [...(scheduleRuns || [])].sort((left: any, right: any) => (timeMs(right.updatedAt) ?? 0) - (timeMs(left.updatedAt) ?? 0)); + + function update(key: string, value: any): void { + setForm((previous: AnyRecord) => ({ ...previous, [key]: value })); + } + + function editSchedule(schedule: any): void { + const action = schedule?.action || {}; + setForm({ + id: schedule?.id || "", + name: schedule?.name || "", + description: schedule?.description || "", + enabled: schedule?.enabled !== false, + timeOfDay: schedule?.schedule?.timeOfDay || "03:30", + actionType: action.type || "dispatch", + providerId: action.providerId || nodes[0]?.providerId || "main-server", + command: action.command || "echo", + payloadJson: JSON.stringify(action.payload || { source: "scheduled-task" }, null, 2), + remoteBaseDir: action.remoteBaseDir || "/SERVER_DATA/UNIDESK_PG_DATA", + stagingSubdir: action.stagingSubdir || "server-data/unidesk-pg-data", + timeoutMs: String(action.timeoutMs || 3600000), + }); + setNotice(`正在编辑 ${schedule?.id || ""}`); + } + + function scheduleBody(): AnyRecord { + const base = { + id: form.id, + name: form.name, + description: form.description, + enabled: form.enabled, + concurrencyPolicy: "skip", + schedule: { type: "daily", timeOfDay: form.timeOfDay, timezone: "Etc/UTC" }, + }; + if (form.actionType === "pgdata_backup") { + return { + ...base, + action: { + type: "pgdata_backup", + volumeName: "unidesk_pgdata_10gb", + remoteBaseDir: form.remoteBaseDir, + stagingSubdir: form.stagingSubdir, + timeoutMs: Number(form.timeoutMs) || 3600000, + cleanupLocal: true, + }, + }; + } + return { + ...base, + action: { + type: "dispatch", + providerId: form.providerId, + command: form.command, + payload: JSON.parse(form.payloadJson || "{}"), + timeoutMs: Number(form.timeoutMs) || 600000, + }, + }; + } + + async function save(event: any): Promise { + event.preventDefault(); + setBusy(true); + setError(""); + setNotice(""); + try { + const body = scheduleBody(); + const id = encodeURIComponent(String(body.id)); + await requestJson(`${cfg.apiBaseUrl}/schedules/${id}`, { method: "PUT", body: JSON.stringify(body) }); + setNotice("定时任务已保存"); + await refresh(); + } catch (err) { + setError(errorMessage(err, "保存定时任务失败")); + } finally { + setBusy(false); + } + } + + async function deleteSchedule(schedule: any): Promise { + if (!schedule?.id) return; + setBusy(true); + setError(""); + setNotice(""); + try { + await requestJson(`${cfg.apiBaseUrl}/schedules/${encodeURIComponent(schedule.id)}`, { method: "DELETE" }); + setNotice(`已删除 ${schedule.id}`); + await refresh(); + } catch (err) { + setError(errorMessage(err, "删除定时任务失败")); + } finally { + setBusy(false); + } + } + + async function runNow(schedule: any): Promise { + if (!schedule?.id) return; + setBusy(true); + setError(""); + setNotice(""); + try { + const response = await requestJson(`${cfg.apiBaseUrl}/schedules/${encodeURIComponent(schedule.id)}/run`, { method: "POST", body: "{}" }); + setNotice(`已触发 ${schedule.id} / ${response?.run?.id || "run"}`); + await refresh(); + } catch (err) { + setError(errorMessage(err, "触发定时任务失败")); + } finally { + setBusy(false); + } + } + + return h("div", { className: "page-grid scheduled-task-page", "data-testid": "scheduled-task-page" }, + h(Panel, { title: "定时任务", eyebrow: `${(schedules || []).length} Schedules` }, + (schedules || []).length === 0 ? h(EmptyState, { title: "暂无定时任务", text: "创建 daily / dispatch / PGDATA backup 任务后会在这里展示下一次执行时间和最近结果" }) : + h("div", { className: "schedule-card-grid" }, (schedules || []).map((schedule: any) => h("article", { key: schedule.id, className: "schedule-card", "data-testid": `schedule-row-${safeId(schedule.id)}` }, + h("div", { className: "node-card-head" }, h("strong", null, schedule.name || schedule.id), h(StatusBadge, { status: schedule.enabled ? "online" : "warn" }, schedule.enabled ? "enabled" : "disabled")), + h("code", null, schedule.id), + h("dl", null, + h("dt", null, "计划"), h("dd", null, scheduleSpecLabel(schedule.schedule)), + h("dt", null, "动作"), h("dd", null, scheduleActionLabel(schedule.action)), + h("dt", null, "下次执行"), h("dd", null, fmtDate(schedule.nextRunAt)), + h("dt", null, "最近执行"), h("dd", null, schedule.lastRunAt ? `${fmtDate(schedule.lastRunAt)} / ${schedule.lastRunId || "--"}` : "--"), + ), + h("div", { className: "dispatch-actions" }, + h("button", { type: "button", className: "ghost-btn", disabled: busy, onClick: () => editSchedule(schedule) }, "编辑"), + h("button", { type: "button", className: "ghost-btn", disabled: busy, onClick: () => runNow(schedule), "data-testid": `schedule-run-${safeId(schedule.id)}` }, "手动触发"), + h("button", { type: "button", className: "ghost-btn danger", disabled: busy, onClick: () => deleteSchedule(schedule) }, "删除"), + h(RawButton, { title: `Schedule ${schedule.id}`, data: schedule, onOpen: onRaw }), + ), + ))), + ), + h(Panel, { title: form.id ? "配置定时任务" : "新建定时任务", eyebrow: "CRUD" }, + h("form", { className: "dispatch-form schedule-form", onSubmit: save }, + h("label", null, "ID", h("input", { value: form.id, onChange: (event: any) => update("id", event.target.value) })), + h("label", null, "名称", h("input", { value: form.name, onChange: (event: any) => update("name", event.target.value) })), + h("label", null, "每日执行时间 UTC", h("input", { value: form.timeOfDay, placeholder: "03:30", onChange: (event: any) => update("timeOfDay", event.target.value) })), + h("label", null, "启用", h("select", { value: form.enabled ? "true" : "false", onChange: (event: any) => update("enabled", event.target.value === "true") }, + h("option", { value: "true" }, "enabled"), + h("option", { value: "false" }, "disabled"), + )), + h("label", null, "动作类型", h("select", { value: form.actionType, onChange: (event: any) => update("actionType", event.target.value) }, + h("option", { value: "pgdata_backup" }, "PGDATA 备份到百度网盘"), + h("option", { value: "dispatch" }, "Provider Dispatch"), + )), + form.actionType === "pgdata_backup" ? [ + h("label", { key: "remote" }, "网盘根目录", h("input", { value: form.remoteBaseDir, onChange: (event: any) => update("remoteBaseDir", event.target.value) })), + h("label", { key: "staging" }, "本地 staging 子目录", h("input", { value: form.stagingSubdir, onChange: (event: any) => update("stagingSubdir", event.target.value) })), + ] : [ + h("label", { key: "provider" }, "Provider", h("select", { value: form.providerId, onChange: (event: any) => update("providerId", event.target.value) }, + (nodes || []).map((node: any) => h("option", { key: node.providerId, value: node.providerId }, `${node.name} / ${node.providerId}`)), + )), + h("label", { key: "command" }, "Command", h("select", { value: form.command, onChange: (event: any) => update("command", event.target.value) }, + h("option", { value: "echo" }, "echo"), + h("option", { value: "docker.ps" }, "docker.ps"), + h("option", { value: "host.ssh" }, "host.ssh"), + h("option", { value: "microservice.http" }, "microservice.http"), + )), + h("label", { key: "payload", className: "raw-editor-label" }, "Payload JSON", h("textarea", { className: "raw-editor", value: form.payloadJson, onChange: (event: any) => update("payloadJson", event.target.value) })), + ], + h("label", null, "超时 ms", h("input", { value: form.timeoutMs, onChange: (event: any) => update("timeoutMs", event.target.value) })), + h("label", { className: "raw-editor-label" }, "描述", h("textarea", { className: "raw-editor compact", value: form.description, onChange: (event: any) => update("description", event.target.value) })), + h("div", { className: "dispatch-actions" }, + h("button", { type: "button", className: "ghost-btn", disabled: busy, onClick: () => setForm(defaultScheduleForm(nodes || [])) }, "重置"), + h("button", { type: "submit", disabled: busy || !form.id }, busy ? "保存中" : "保存任务"), + ), + notice ? h("p", { className: "muted paragraph" }, notice) : null, + h(UniDeskErrorBanner, { error, wide: true }), + ), + ), + h(Panel, { title: "历史执行记录", eyebrow: `${sortedRuns.length} Runs` }, + sortedRuns.length === 0 ? h(EmptyState, { title: "暂无执行记录", text: "定时触发或手动触发后会生成 run history" }) : + h("div", { className: "table-wrap" }, h("table", { className: "task-history-table schedule-run-table" }, + 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("tbody", null, sortedRuns.map((run: any) => h("tr", { key: run.id, "data-testid": `schedule-run-row-${safeId(run.id)}` }, + h("td", null, h(StatusBadge, { status: scheduleRunTone(run.status) }, run.status)), + h("td", null, h("strong", null, run.scheduleId), h("code", null, run.id), run.taskId ? h("code", null, run.taskId) : null), + h("td", null, run.trigger || "--"), + h("td", null, scheduleRunDuration(run)), + h("td", null, h(DataSummary, { data: run.result || run.error, empty: "无结果" })), + h("td", null, fmtDate(run.updatedAt)), + h("td", null, h(RawButton, { title: `Schedule Run ${run.id}`, data: run, onOpen: onRaw })), + ))), + )), + ), + ); +} + function TopologyPage({ data }: AnyRecord) { const overview = data.overview || {}; return h("div", { className: "page-grid topology-grid" }, @@ -1870,6 +2120,7 @@ function WorkArea({ activeModule, activeTab, data, session, refresh, onRaw, onNa if (activeModule === "nodes" && activeTab === "labels") return h(LabelsPage, { nodes: data.nodes }); if (activeModule === "nodes" && activeTab === "heartbeats") return h(HeartbeatPage, { nodes: data.nodes }); if (activeModule === "tasks" && activeTab === "dispatch") return h(DispatchPage, { nodes: data.nodes, onDispatched: refresh, onRaw }); + if (activeModule === "tasks" && activeTab === "scheduled") return h(TaskScheduledPage, { schedules: data.schedules, scheduleRuns: data.scheduleRuns, nodes: data.nodes, refresh, onRaw }); if (activeModule === "tasks" && activeTab === "pending") return h(TaskPendingPage, { tasks: data.pendingTasks, onRaw }); if (activeModule === "tasks" && activeTab === "history") return h(TaskHistoryPage, { tasks: data.tasks, onRaw }); if (activeModule === "tasks" && activeTab === "results") return h(TaskResultsPage, { tasks: data.tasks, onRaw }); @@ -1893,7 +2144,7 @@ function Shell({ session, onLogout }: AnyRecord) { const initialRouteTarget = resolveRouteTarget(ROUTE_REGISTRY, window.location.pathname); const [activeModule, setActiveModule] = useState(initialRouteTarget.moduleId); const [activeTabs, setActiveTabs] = useState({ ...DEFAULT_ACTIVE_TABS, [initialRouteTarget.moduleId]: initialRouteTarget.tabId }); - const [data, setData] = useState({ overview: null, nodes: [], systemStatuses: [], dockerStatuses: [], microservices: [], events: [], tasks: [], pendingTasks: [], logs: [] }); + const [data, setData] = useState({ overview: null, nodes: [], systemStatuses: [], dockerStatuses: [], microservices: [], events: [], tasks: [], pendingTasks: [], schedules: [], scheduleRuns: [], logs: [] }); const [connection, setConnection] = useState({ ok: false, text: "连接中" }); const [lastRefresh, setLastRefresh] = useState(null); const [clock, setClock] = useState(new Date()); @@ -1933,7 +2184,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 needsNodes = isOverview || activeModule === "nodes" || (activeModule === "tasks" && (activeTab === "dispatch" || activeTab === "scheduled")); const needsMicroservices = activeModule === "apps" && activeTab !== "code-queue"; if (needsOverviewSummary) add("overview", `${cfg.apiBaseUrl}/overview`); if (needsNodes) add("nodes", `${cfg.apiBaseUrl}/nodes`); @@ -1944,6 +2195,9 @@ function Shell({ session, onLogout }: AnyRecord) { add("dockerStatuses", `${cfg.apiBaseUrl}/nodes/docker-status`); } else if (activeModule === "nodes" && activeTab === "gateway") { add("tasks", `${cfg.apiBaseUrl}/tasks?limit=300&summary=1`); + } else if (activeModule === "tasks" && activeTab === "scheduled") { + add("schedules", `${cfg.apiBaseUrl}/schedules?limit=100`); + add("scheduleRuns", `${cfg.apiBaseUrl}/schedules/runs?limit=100`); } else if (activeModule === "tasks" && activeTab === "pending") { add("pendingTasks", `${cfg.apiBaseUrl}/tasks?status=pending&limit=100&summary=1`); } else if (activeModule === "tasks" && (activeTab === "history" || activeTab === "results")) { @@ -1967,6 +2221,8 @@ function Shell({ session, onLogout }: AnyRecord) { if (key === "events") patch.events = value.events || []; if (key === "tasks") patch.tasks = value.tasks || []; if (key === "pendingTasks") patch.pendingTasks = value.tasks || []; + if (key === "schedules") patch.schedules = value.schedules || []; + if (key === "scheduleRuns") patch.scheduleRuns = value.runs || []; if (key === "logs") patch.logs = value.logs || []; setData((previous: AnyRecord) => ({ ...previous, ...patch })); })); @@ -2043,16 +2299,22 @@ function Shell({ session, onLogout }: AnyRecord) { setRaw({ title, data: rawData }); } + const [notificationOpen, setNotificationOpen] = useState(false); + const { unreadCount, notifications } = useNotification(); + const latestNotification = notifications.length > 0 ? notifications[notifications.length - 1] : null; + return h("div", { className: `shell ${railCollapsed ? "rail-collapsed" : ""}`, "data-testid": "app-shell" }, h(Sidebar, { activeModule, activeTabs, onNavigate: navigate, collapsed: railCollapsed, onToggle: () => setRailCollapsed((value: boolean) => !value) }), h("main", { className: "workspace" }, - h(TopBar, { connection, lastRefresh, onRefresh: refresh, onLogout: () => onLogout(true), session, clock, activeStatusItems }), + h(TopBar, { connection, lastRefresh, onRefresh: refresh, onLogout: () => onLogout(true), session, clock, activeStatusItems, onNotificationToggle: () => setNotificationOpen((v: boolean) => !v), unreadCount }), h(TabBar, { module, activeTab, 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) }), + latestNotification && h(NotificationBanner, { key: latestNotification.id, notification: latestNotification }), + notificationOpen && h(NotificationPopup, { onClose: () => setNotificationOpen(false) }), ); } @@ -2083,7 +2345,7 @@ function App() { if (checking) return h("main", { className: "loading-screen" }, h("div", { className: "brand-mark" }, "UD"), h("span", null, "加载会话")); if (!session) return h(LoginScreen, { onLogin: setSession }); - return h(Shell, { session, onLogout: logout }); + return h(NotificationProvider, null, h(Shell, { session, onLogout: logout })); } const rootElement = document.getElementById("root"); diff --git a/src/components/frontend/src/baidu-netdisk.tsx b/src/components/frontend/src/baidu-netdisk.tsx index f4a8354d..0cf976b2 100644 --- a/src/components/frontend/src/baidu-netdisk.tsx +++ b/src/components/frontend/src/baidu-netdisk.tsx @@ -3,6 +3,7 @@ import { fmtClock, fmtDate } from "./time"; import { LoadingTitle } from "./loading-indicator"; import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; +import { useNotification } from "./notification-context"; type AnyRecord = Record; @@ -125,6 +126,10 @@ function pathJoin(base: string, name: string): string { return `${base.replace(/\/+$/u, "")}/${safeName}`; } +function rootDisplayName(root: string): string { + return root === "/" ? "/" : root.split("/").filter(Boolean).pop() || root; +} + 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)}%`)); @@ -133,13 +138,23 @@ function ProgressBar({ percent }: AnyRecord) { 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 [currentDir, setCurrentDir] = useState("/"); const [deviceSession, setDeviceSession] = useState(null); const [folderName, setFolderName] = useState(""); - const [uploadForm, setUploadForm] = useState({ localPath: "sample.txt", remotePath: "/apps/UniDeskBaiduNetdisk/sample.txt" }); + const [uploadForm, setUploadForm] = useState({ localPath: "sample.txt", remotePath: "/sample.txt" }); const [downloadForm, setDownloadForm] = useState({ fsId: "", localPath: "downloads/" }); + const { addNotification } = useNotification(); - const appRoot = state.health?.baidu?.appRoot || state.account?.rootPath || "/apps/UniDeskBaiduNetdisk"; + const appRoot = state.health?.baidu?.appRoot || state.account?.rootPath || "/"; + + useEffect(() => { + setUploadForm((prev: any) => { + const legacyDefaults = new Set(["/sample.txt", "/apps/UniDeskBaiduNetdisk/sample.txt"]); + if (prev.remotePath && !legacyDefaults.has(prev.remotePath)) return prev; + const remotePath = pathJoin(appRoot, "sample.txt"); + return prev.remotePath === remotePath ? prev : { ...prev, remotePath }; + }); + }, [appRoot]); async function loadFiles(dir = currentDir): Promise { const targetDir = dir || appRoot; @@ -354,13 +369,12 @@ export function BaiduNetdiskPage({ microservices, onRaw, apiBaseUrl = "/api" }: ), ), 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: "Work Root", value: rootDisplayName(appRoot), 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}` }), ), @@ -387,7 +401,7 @@ export function BaiduNetdiskPage({ microservices, onRaw, apiBaseUrl = "/api" }: }), h(DocLinkCard, { title: "服务方案与 API", - text: "说明 OAuth Device Code、应用目录、staging 上传下载任务和后端 API 设计。", + text: "说明 OAuth Device Code、根目录工作区、staging 上传下载任务和后端 API 设计。", href: "/docs/issue/baidu-netdisk-user-service.md", badge: "DESIGN", }), @@ -455,7 +469,7 @@ export function BaiduNetdiskPage({ microservices, onRaw, apiBaseUrl = "/api" }: 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("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、会员状态和容量" }), @@ -475,7 +489,7 @@ export function BaiduNetdiskPage({ microservices, onRaw, apiBaseUrl = "/api" }: 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 目录上传文件或新建文件夹" }) : + !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 }, @@ -502,7 +516,7 @@ export function BaiduNetdiskPage({ microservices, onRaw, apiBaseUrl = "/api" }: 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("label", null, "百度网盘目标路径", h("input", { value: uploadForm.remotePath, onChange: (event: any) => setUploadForm((prev: any) => ({ ...prev, remotePath: event.target.value })), placeholder: pathJoin(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" }, diff --git a/src/components/frontend/src/claudeqq.tsx b/src/components/frontend/src/claudeqq.tsx index 216a6783..282ac5c1 100644 --- a/src/components/frontend/src/claudeqq.tsx +++ b/src/components/frontend/src/claudeqq.tsx @@ -3,6 +3,7 @@ import { fmtClock, fmtDate } from "./time"; import { LoadingTitle } from "./loading-indicator"; import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; +import { useNotification } from "./notification-context"; type AnyRecord = Record; @@ -111,6 +112,7 @@ export function ClaudeQqPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR const [pushForm, setPushForm] = useState({ targetType: "private", targetId: String(PRIMARY_PRIVATE_CHAT.userId), message: "" }); const [subscriptionForm, setSubscriptionForm] = useState({ name: "unidesk-callback", targetUrl: "", eventTypes: "message", secret: "" }); const [actionMessage, setActionMessage] = useState(""); + const { addNotification } = useNotification(); async function load(): Promise { if (!service) return; @@ -173,8 +175,10 @@ export function ClaudeQqPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR message: pushForm.message, }), }); + const msg = "消息推送请求已提交"; setPushForm((prev: any) => ({ ...prev, targetType: "private", targetId: String(PRIMARY_PRIVATE_CHAT.userId), message: "" })); - setActionMessage("消息推送请求已提交"); + setActionMessage(msg); + addNotification("success", msg); await load(); } catch (err) { setState((prev: any) => ({ ...prev, error: errorMessage(err, "发送失败") })); @@ -199,7 +203,9 @@ export function ClaudeQqPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR enabled: true, }), }); - setActionMessage("事件订阅已创建"); + const msg = "事件订阅已创建"; + setActionMessage(msg); + addNotification("success", msg); await load(); } catch (err) { setState((prev: any) => ({ ...prev, error: errorMessage(err, "订阅失败") })); @@ -211,7 +217,9 @@ export function ClaudeQqPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR setActionMessage(""); try { await requestJson(claudeqqApi(apiBaseUrl, `/api/events/subscriptions/${encodeURIComponent(id)}`), { method: "DELETE" }); - setActionMessage("事件订阅已删除"); + const msg = "事件订阅已删除"; + setActionMessage(msg); + addNotification("success", msg); await load(); } catch (err) { setState((prev: any) => ({ ...prev, error: errorMessage(err, "删除订阅失败") })); diff --git a/src/components/frontend/src/code-queue.tsx b/src/components/frontend/src/code-queue.tsx index e982e8e0..826fc87e 100644 --- a/src/components/frontend/src/code-queue.tsx +++ b/src/components/frontend/src/code-queue.tsx @@ -5,6 +5,7 @@ import { MarkdownBody } from "./markdown"; import { TraceView, codexTracePort } from "./trace"; import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; +import { useNotification } from "./notification-context"; type AnyRecord = Record; @@ -75,6 +76,13 @@ function latestTimestampValue(...values: any[]): string { return best; } +function timestampIsAfter(left: any, right: any): boolean { + const leftMs = timestampMs(left); + if (leftMs === null) return false; + const rightMs = timestampMs(right); + return rightMs === null || leftMs > rightMs + 1; +} + function fmtPreciseMs(ms: any): string { const value = Number(ms); if (!Number.isFinite(value) || value < 0) return "--"; @@ -98,9 +106,9 @@ async function requestJson(path: string, options: AnyRecord = {}): Promise }); } -function StatusBadge({ status, children }: AnyRecord) { +function StatusBadge({ status, children, title }: AnyRecord) { const normalized = String(status || "unknown").toLowerCase(); - return h("span", { className: `status-badge ${normalized}` }, children || status || "unknown"); + return h("span", { className: `status-badge ${normalized}`, title }, children || status || "unknown"); } function Panel({ title, eyebrow, summary, actions, children, className, loading }: AnyRecord) { @@ -315,7 +323,7 @@ async function loadTaskOverview(apiBaseUrl: string, preferId: string, afterSeq = 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)}${taskListQuerySuffix(queueId, searchQuery)}`, + `/api/tasks/overview?limit=${encodeURIComponent(String(limit))}&transcriptLimit=1&compact=1&selected=0&includeActive=0&stats=0&beforeId=${encodeURIComponent(beforeId)}${taskListQuerySuffix(queueId, searchQuery)}`, )); } @@ -556,6 +564,35 @@ function taskExecutionSummary(task: any): AnyRecord { return execution && typeof execution === "object" && !Array.isArray(execution) ? execution : {}; } +function rawTaskStepCount(task: any): number { + const value = Number(task?.stepCount ?? task?.llmStepCount ?? 0); + return Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0; +} + +function traceSummaryStepCount(summary: AnyRecord | null): number { + const execution = objectRecord(summary?.execution) || {}; + const value = Number(summary?.stepCount ?? summary?.llmStepCount ?? execution.stepCount ?? execution.llmStepCount ?? execution.toolCallCount ?? 0); + return Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0; +} + +function traceSummaryIsCurrent(task: any): boolean { + if (!task || task?._traceSummaryLoaded !== true) return false; + const summary = taskTraceSummary(task); + const summaryAt = String(task?._traceSummaryUpdatedAt || summary?.updatedAt || ""); + const updatedAt = String(task?.updatedAt || ""); + if (updatedAt.length > 0) { + const summaryMs = timestampMs(summaryAt); + const updatedMs = timestampMs(updatedAt); + if (summaryMs !== null && updatedMs !== null) { + if (summaryMs + 1 < updatedMs) return false; + } else if (summaryAt !== updatedAt) { + return false; + } + } + const rawSteps = rawTaskStepCount(task); + return rawSteps <= 0 || traceSummaryStepCount(summary) >= rawSteps; +} + function taskBasePromptText(task: any): string { const summaryPrompt = taskPromptSummary(task); const basePrompt = String(summaryPrompt.basePrompt || ""); @@ -870,6 +907,36 @@ function taskQueueLabel(task: any): string { return String(task?.queueId || "default"); } +function queuedReasonRecord(task: any): AnyRecord | null { + const reason = objectRecord(task?.queuedReason); + return reason; +} + +function queuedReasonLabel(task: any): string { + const explicit = String(task?.queuedReasonLabel || "").trim(); + if (explicit.length > 0) return explicit.toUpperCase(); + const reason = queuedReasonRecord(task); + const label = String(reason?.label || "").trim(); + return label.length > 0 ? label.toUpperCase() : ""; +} + +function taskStatusBadgeText(task: any): string { + const status = String(task?.status || "unknown"); + if (status !== "queued") return status; + const reason = queuedReasonLabel(task); + return reason.length > 0 ? `QUEUED(${reason})` : "QUEUED"; +} + +function taskStatusBadgeTitle(task: any): string | undefined { + if (String(task?.status || "") !== "queued") return undefined; + const reason = queuedReasonRecord(task); + const message = String(reason?.message || "").trim(); + const label = queuedReasonLabel(task); + if (message.length > 0 && label.length > 0) return `${label}: ${message}`; + if (message.length > 0) return message; + return label.length > 0 ? label : undefined; +} + function channelLabel(channel: string): string { const labels: Record = { system: "SYS", @@ -1190,7 +1257,7 @@ function TaskCard({ task, selected, onSelect, onCopy, onReference, onMarkRead, c unread ? h("span", { className: "codex-unread-badge", title: "待读", "aria-label": "待读", "data-testid": `codex-unread-task-${taskId || "unknown"}` }) : null, h("div", { className: "codex-task-card-head" }, h("div", { className: "codex-task-status-line" }, - h(StatusBadge, { status: task?.status }, task?.status || "unknown"), + h(StatusBadge, { status: task?.status, title: taskStatusBadgeTitle(task) }, taskStatusBadgeText(task)), ), h("span", { className: "mono-text" }, `${task?.currentAttempt || 0}/${task?.maxAttempts || 0}`), ), @@ -1495,7 +1562,8 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL 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 summaryStepCountValue = Number(execution.stepCount ?? execution.llmStepCount ?? 0); + const summaryStepCount = Number.isFinite(summaryStepCountValue) && summaryStepCountValue >= 0 ? Math.floor(summaryStepCountValue) : 0; const editedFiles = Array.isArray(execution.editedFiles) ? execution.editedFiles : []; const commands = Array.isArray(execution.commands) ? execution.commands : []; const synthetic = isSyntheticAttemptSegment(attempt, attemptIndex); @@ -1503,6 +1571,11 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL const updatedAt = attemptExecutionUpdatedAt(task, attempt, attemptIndex, execution); const recentUpdateLabel = `最近更新: ${fmtRelativeAge(updatedAt)}`; const running = attemptExecutionIsRunning(task, attempt, attemptIndex); + const rawSteps = rawTaskStepCount(task); + const realAttemptCount = taskProgressiveAttempts(task).filter((item: any) => !isSyntheticAttemptSegment(item, item?.index)).length; + const canUseTaskStepFloor = !synthetic && steps.length === 0 && rawSteps > 0 && realAttemptCount <= 1; + const stepCount = canUseTaskStepFloor ? Math.max(summaryStepCount, rawSteps) : summaryStepCount; + const displayedToolCount = canUseTaskStepFloor ? Math.max(toolCount, stepCount) : toolCount; return h("details", { className: `codex-progressive-card codex-execution-summary ${running ? "running" : ""}`, "data-testid": testId, @@ -1518,7 +1591,7 @@ function ProgressiveExecutionSummary({ task, attempt, attemptIndex, loading, onL 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}`), + `${fmtDuration(execution.durationMs ?? execution.totalElapsedMs)} / ${displayedToolCount} tools / ${recentUpdateLabel}`), ), h("div", { className: "codex-execution-digest" }, h("span", null, `read ${Number(execution.readCount || 0)}`), @@ -1775,7 +1848,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi 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 traceSummaryInFlightRef = useRef; refreshAfter: boolean }>>(new Map()); const promptDetailInFlightRef = useRef>>(new Map()); const traceStepsInFlightRef = useRef>>(new Map()); const traceStepInFlightRef = useRef>>(new Map()); @@ -1814,6 +1887,7 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [notice, setNotice] = useState(""); + const { addNotification } = useNotification(); const [copiedTaskId, setCopiedTaskId] = useState(""); const [markingReadTaskId, setMarkingReadTaskId] = useState(""); const [markingAllRead, setMarkingAllRead] = useState(false); @@ -2056,36 +2130,48 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi if (!service || !taskId) return; const cached = sessionCacheRef.current.get(taskId); const cachedTask = cached?.task; - const cachedSummaryAt = String(cachedTask?._traceSummaryUpdatedAt || ""); - const cachedUpdatedAt = String(cachedTask?.updatedAt || ""); - if (!force && cachedTask?._traceSummaryLoaded === true && cachedSummaryAt === cachedUpdatedAt) return; + if (!force && traceSummaryIsCurrent(cachedTask)) return; const key = taskId; const existing = traceSummaryInFlightRef.current.get(key); - if (existing) return existing; + if (existing) { + if (force || !traceSummaryIsCurrent(cachedTask)) existing.refreshAfter = true; + return existing.promise; + } const token = detailLoadTokenRef.current; const startedAt = performance.now(); if (selectedIdRef.current === taskId) setSelectedDetailLoading(true); + const inFlight = { promise: Promise.resolve(), refreshAfter: false }; const promise = (async () => { try { const result = await loadTaskTraceSummary(apiBaseUrl, taskId); if (token !== detailLoadTokenRef.current || selectedIdRef.current !== taskId) return; const summary = result?.summary || {}; + const currentTask = sessionCacheRef.current.get(taskId)?.task || {}; + const summaryUpdatedAt = String(summary.updatedAt || ""); + const currentUpdatedAt = String(currentTask?.updatedAt || ""); + const summaryBehind = timestampIsAfter(currentUpdatedAt, summaryUpdatedAt) || rawTaskStepCount(currentTask) > traceSummaryStepCount(summary); + if (summaryBehind) inFlight.refreshAfter = true; + const effectiveUpdatedAt = summaryBehind + ? latestTimestampValue(currentUpdatedAt, summaryUpdatedAt) + : summaryUpdatedAt || currentUpdatedAt; publishCachedTask(taskId, { id: taskId, - status: summary.status, - updatedAt: summary.updatedAt, - startedAt: summary.startedAt, - finishedAt: summary.finishedAt, - currentAttempt: summary.currentAttempt, - maxAttempts: summary.maxAttempts, - finalResponse: summary.finalResponse, - lastJudge: summary.lastJudge, - lastError: summary.lastError, - attempts: Array.isArray(summary.attempts) ? summary.attempts : [], + status: summaryBehind ? (currentTask?.status || summary.status) : summary.status, + updatedAt: effectiveUpdatedAt, + startedAt: summary.startedAt || currentTask?.startedAt, + finishedAt: summaryBehind ? (currentTask?.finishedAt || summary.finishedAt) : summary.finishedAt, + currentAttempt: summary.currentAttempt ?? currentTask?.currentAttempt, + maxAttempts: summary.maxAttempts ?? currentTask?.maxAttempts, + finalResponse: summaryBehind ? preferLongerText(currentTask, summary, "finalResponse") : summary.finalResponse, + lastJudge: summaryBehind ? (currentTask?.lastJudge || summary.lastJudge) : summary.lastJudge, + lastError: summaryBehind ? (currentTask?.lastError || summary.lastError) : summary.lastError, + attempts: summaryBehind + ? preferRicherArray(currentTask, { attempts: Array.isArray(summary.attempts) ? summary.attempts : [] }, "attempts") + : Array.isArray(summary.attempts) ? summary.attempts : [], timing: summary.timing, _traceSummary: summary, _traceSummaryLoaded: true, - _traceSummaryUpdatedAt: String(summary.updatedAt || ""), + _traceSummaryUpdatedAt: summaryUpdatedAt, _detailLoaded: true, }, token); setLoadStats({ @@ -2100,11 +2186,18 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi completedAt: new Date(), }); } finally { + const shouldRefreshAfter = Boolean(inFlight.refreshAfter && selectedIdRef.current === taskId && !traceSummaryIsCurrent(sessionCacheRef.current.get(taskId)?.task)); traceSummaryInFlightRef.current.delete(key); if (token === detailLoadTokenRef.current && selectedIdRef.current === taskId) setSelectedDetailLoading(false); + if (shouldRefreshAfter) { + window.setTimeout(() => { + void ensureTraceSummary(taskId, true).catch((err) => setError(errorText(err, "自动刷新 Trace Summary 失败"))); + }, 0); + } } })(); - traceSummaryInFlightRef.current.set(key, promise); + inFlight.promise = promise; + traceSummaryInFlightRef.current.set(key, inFlight); await promise; } @@ -2543,7 +2636,9 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi } if (!copied) throw new Error("browser clipboard rejected the copy request"); setCopiedTaskId(taskId); - setNotice(`已复制任务 ID:${taskId}`); + const msg = `已复制任务 ID:${taskId}`; + setNotice(msg); + addNotification("success", msg); window.setTimeout(() => setCopiedTaskId((value: string) => value === taskId ? "" : value), 1600); } catch (err) { setError(`复制任务 ID 失败:${errorText(err)}`); @@ -2553,7 +2648,9 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi function referenceTask(taskId: string): void { if (!taskId) return; setReferenceTaskId(taskId); - setNotice(`已引用任务 ID:${taskId};提交时后端会读取并注入该任务上下文`); + const msg = `已引用任务 ID:${taskId};提交时后端会读取并注入该任务上下文`; + setNotice(msg); + addNotification("success", msg); } async function markTaskRead(taskId: string): Promise { @@ -2569,7 +2666,9 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi const readAt = String(task?.readAt || new Date().toISOString()); patchLoadedReadState([taskId], readAt, result?.queue || null, task); marked = true; - setNotice(`已将任务 ${taskId} 标为已读`); + const msg = `已将任务 ${taskId} 标为已读`; + setNotice(msg); + addNotification("success", msg); }, "标记 Codex task 已读失败"); if (!marked) { forgetLocalReadState([taskId]); @@ -2608,7 +2707,9 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi patchLoadedReadState(readIds, readAt, result?.queue || null); const markedCount = Number(result?.count || readIds.length); marked = true; - setNotice(`已将 ${markedCount} 个已结束未读任务标为已读`); + const msg = `已将 ${markedCount} 个已结束未读任务标为已读`; + setNotice(msg); + addNotification("success", msg); }, "全部标为已读失败"); if (!marked && optimisticReadIds.length > 0) { forgetLocalReadState(optimisticReadIds); @@ -2646,7 +2747,9 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi detailLoadTokenRef.current += 1; setSelectedId(""); setSelectedTask(null); - setNotice(`已创建并切换到 queue:${createdId}`); + const msg = `已创建并切换到 queue:${createdId}`; + setNotice(msg); + addNotification("success", msg); await load("", true, createdId); }, "创建 Codex queue 失败"); } @@ -2664,7 +2767,9 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi if (result?.summary) { setTasksData((previous: any) => previous ? { ...previous, queue: result.summary } : previous); } - setNotice(`已更新 queue 名称:${queueDisplayName(updatedQueue)}`); + const msg = `已更新 queue 名称:${queueDisplayName(updatedQueue)}`; + setNotice(msg); + addNotification("success", msg); await load(selectedIdRef.current, true, selectedQueueId); }, "修改 Codex queue 名称失败"); } @@ -2672,7 +2777,8 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi async function enqueue(event: any): Promise { event.preventDefault(); if (enqueueInFlightRef.current) { - setNotice("任务正在提交中,请等待当前请求完成,已阻止重复提交。"); + const msg = "任务正在提交中,请等待当前请求完成,已阻止重复提交。"; + setNotice(msg); return; } if (enqueueItems.length > 1 && !batchConfirmed) { @@ -2702,13 +2808,14 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi const result = await requestJson(codexApi(apiBaseUrl, submittingItems.length === 1 ? "/api/tasks" : "/api/tasks/batch"), { method: "POST", body }); const firstId = result?.tasks?.[0]?.id || ""; const ids = Array.isArray(result?.tasks) ? result.tasks.map((task: any) => String(task?.id || "")).filter(Boolean) : []; - setNotice(`已创建 ${ids.length || submittingItems.length} 个任务${ids.length > 0 ? `:${ids.join(" / ")}` : ""}`); + const msg = `已创建 ${ids.length || submittingItems.length} 个任务${ids.length > 0 ? `:${ids.join(" / ")}` : ""}`; + setNotice(msg); + addNotification("success", msg); setPrompt(""); setReferenceTaskId(""); setBatchConfirmed(false); selectedIdRef.current = firstId; if (selectedQueueId !== submitQueueId) setTasksData(null); - setSelectedQueueId(submitQueueId); setQueueId(submitQueueId); await load(firstId, true, submitQueueId); }, "Codex 任务入队失败"); @@ -2759,7 +2866,9 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi 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`); + const msg = result?.changed === false ? `任务 ${taskId} 的 prompt 未变化` : `已更新 queued 任务 ${taskId} 的用户 prompt`; + setNotice(msg); + addNotification("success", msg); await load(taskId, true, selectedQueueId); }, "编辑 queued 任务 prompt 失败"); } @@ -2801,7 +2910,9 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi setTasksData(null); setSelectedQueueId(nextQueueId); } - setNotice(`已将任务 ${taskId} 从 ${currentQueue} 移动到 ${nextQueueId}`); + const msg = `已将任务 ${taskId} 从 ${currentQueue} 移动到 ${nextQueueId}`; + setNotice(msg); + addNotification("success", msg); await load(taskId, true, isAllQueues(selectedQueueId) ? allQueuesId : nextQueueId); }, "移动任务 queue 失败"); } @@ -2877,13 +2988,12 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi const taskId = String(selectedTask.id || ""); if (!taskId) return; const updatedAt = String(selectedTask.updatedAt || ""); - const summaryAt = String(selectedTask._traceSummaryUpdatedAt || ""); - if (selectedTask._traceSummaryLoaded === true && summaryAt === updatedAt) return; - const key = `${taskId}:${updatedAt || "unknown"}`; + if (traceSummaryIsCurrent(selectedTask)) return; + const key = `${taskId}:${updatedAt || "unknown"}:${rawTaskStepCount(selectedTask)}:${traceSummaryStepCount(taskTraceSummary(selectedTask))}`; if (autoTraceLoadKeysRef.current.has(key)) return; autoTraceLoadKeysRef.current.add(key); void ensureTraceSummary(taskId, true).catch((err) => setError(errorText(err, "自动加载 Trace Summary 失败"))); - }, [service?.id, selectedTask?.id, selectedTask?.updatedAt, selectedTask?._traceSummaryUpdatedAt, selectedTask?._traceSummaryLoaded, selectedDetailLoading]); + }, [service?.id, selectedTask?.id, selectedTask?.updatedAt, selectedTask?.stepCount, selectedTask?.llmStepCount, selectedTask?._traceSummaryUpdatedAt, selectedTask?._traceSummaryLoaded, selectedDetailLoading]); const taskListContent = sidebarTasks.length === 0 ? h(EmptyState, { title: searchActive ? (searchLoading ? "搜索中" : "没有匹配任务") : "队列为空", @@ -3115,7 +3225,9 @@ export function CodeQueuePage({ microservices, onRaw, apiBaseUrl = "/api", initi setPrompt(""); setReferenceTaskId(""); setBatchConfirmed(false); - setNotice("已清空任务输入栏"); + const msg = "已清空任务输入栏"; + setNotice(msg); + addNotification("success", msg); }, "data-testid": "codex-clear-input-button", }, "清空输入"), diff --git a/src/components/frontend/src/index.ts b/src/components/frontend/src/index.ts index 29a9601c..1c3a605a 100644 --- a/src/components/frontend/src/index.ts +++ b/src/components/frontend/src/index.ts @@ -2,6 +2,7 @@ import { readFileSync } from "node:fs"; import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"; import { join } from "node:path"; import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../shared/src/rotating-jsonl"; +import { notificationStyles } from "./notification-styles"; interface RuntimeConfig { port: number; @@ -80,6 +81,10 @@ function cachedCodeQueueOverview(pathWithQuery: string, maxAgeMs = codeQueueOver return { payload: cached.payload, text: cached.text }; } +function invalidateCodeQueueOverviewCache(): void { + codeQueueOverviewCache.clear(); +} + async function refreshCodeQueueOverview(pathWithQuery: string, timeoutMs = 800): Promise { const existing = codeQueueOverviewRefreshes.get(pathWithQuery); if (existing !== undefined) return existing; @@ -117,7 +122,11 @@ function renderIndexHtml(extraRootAttributes = ""): string { const href = new URL(link.href, config.frontendPublicUrl).toString(); return `${escapeHtmlText(link.label)}`; }).join("")}`; - return indexHtmlTemplate.replace( + const notificationStyleTag = ``; + const htmlWithStyles = indexHtmlTemplate.includes("") + ? indexHtmlTemplate.replace("", `${notificationStyleTag}`) + : indexHtmlTemplate; + return htmlWithStyles.replace( indexHtmlRootMarker, `
${docsFallback}
`, ); @@ -698,6 +707,7 @@ async function proxyCodeQueueDirect(req: Request, url: URL): Promise { } const overviewCacheKey = `${suffix}${url.search}`; const canUseOverviewCache = req.method === "GET" && suffix === "/api/tasks/overview"; + if (req.method !== "GET" && req.method !== "HEAD") invalidateCodeQueueOverviewCache(); if (canUseOverviewCache) { const cached = cachedCodeQueueOverview(overviewCacheKey); if (cached !== null) { @@ -726,7 +736,7 @@ async function proxyCodeQueueDirect(req: Request, url: URL): Promise { const isJsonResponse = (upstreamContentType ?? "").toLowerCase().includes("json"); let parsedJson: unknown = null; let jsonText = ""; - if (isJsonResponse) { + if (canUseOverviewCache && upstream.ok && isJsonResponse) { jsonText = new TextDecoder().decode(upstreamBody); try { parsedJson = jsonText ? JSON.parse(jsonText) as unknown : null; @@ -753,7 +763,7 @@ async function proxyCodeQueueDirect(req: Request, url: URL): Promise { } } if (canUseOverviewCache && upstream.ok && isJsonResponse) { - const text = new TextDecoder().decode(upstreamBody); + const text = jsonText; if (typeof parsedJson === "object" && parsedJson !== null) { codeQueueOverviewCache.set(codeQueueOverviewCacheKey(overviewCacheKey), { at: Date.now(), payload: parsedJson as JsonValue, text }); } diff --git a/src/components/frontend/src/markdown.tsx b/src/components/frontend/src/markdown.tsx index d3c65245..75b55d6e 100644 --- a/src/components/frontend/src/markdown.tsx +++ b/src/components/frontend/src/markdown.tsx @@ -22,6 +22,10 @@ interface ListItemInfo { start?: number; } +interface InlineRenderOptions { + linkify?: boolean; +} + export function MarkdownBody({ markdown, className, testId }: MarkdownBodyProps) { const text = String(markdown ?? "").trimEnd(); const classes = ["markdown-body", className].filter(Boolean).join(" "); @@ -261,9 +265,10 @@ function renderTable(headers: string[], separators: string[], rows: string[][], ); } -function renderInline(text: string, keyPrefix: string): any[] { +function renderInline(text: string, keyPrefix: string, options: InlineRenderOptions = {}): any[] { const children: any[] = []; const pattern = /`([^`\n]+)`|\[([^\]\n]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|(https?:\/\/[^\s<>)]+)|\*\*([^*\n]+)\*\*|__([^_\n]+)__|~~([^~\n]+)~~|\*([^*\n]+)\*|_([^_\n]+)_/gu; + const linkify = options.linkify !== false; let cursor = 0; let tokenIndex = 0; for (const match of text.matchAll(pattern)) { @@ -279,10 +284,18 @@ function renderInline(text: string, keyPrefix: string): any[] { continue; } if (match[2] !== undefined && match[3] !== undefined) { + if (!linkify) { + appendText(children, token, `${key}-literal`); + continue; + } children.push(renderLink(match[2], match[3], key)); continue; } if (match[4] !== undefined) { + if (!linkify) { + appendText(children, token, `${key}-literal`); + continue; + } children.push(renderLink(match[4], match[4], key)); continue; } @@ -322,7 +335,7 @@ function renderLink(label: string, href: string, key: string): any { href: safeHref, target: external ? "_blank" : undefined, rel: external ? "noreferrer" : undefined, - }, renderInline(label, `${key}-label`)); + }, renderInline(label, `${key}-label`, { linkify: false })); } function safeMarkdownHref(raw: string): string | null { diff --git a/src/components/frontend/src/navigation.ts b/src/components/frontend/src/navigation.ts index c70decdd..078fd6d1 100644 --- a/src/components/frontend/src/navigation.ts +++ b/src/components/frontend/src/navigation.ts @@ -55,6 +55,7 @@ export const MODULES: UniDeskModuleDefinition[] = [ ] }, { id: "tasks", label: "任务调度", code: "TASK", tabs: [ { id: "dispatch", label: "下发任务" }, + { id: "scheduled", label: "定时任务" }, { id: "pending", label: "待处理任务" }, { id: "history", label: "任务历史" }, { id: "results", label: "执行结果" }, diff --git a/src/components/frontend/src/notification-context.tsx b/src/components/frontend/src/notification-context.tsx new file mode 100644 index 00000000..f24a4caa --- /dev/null +++ b/src/components/frontend/src/notification-context.tsx @@ -0,0 +1,74 @@ +import React from "react"; + +export type NotificationType = "success" | "error"; + +export interface NotificationItem { + id: string; + type: NotificationType; + message: string; + timestamp: number; +} + +export interface NotificationContextValue { + notifications: NotificationItem[]; + addNotification: (type: NotificationType, message: string) => void; + removeNotification: (id: string) => void; + clearNotifications: () => void; + unreadCount: number; + hasUnread: boolean; +} + +const NotificationContext = React.createContext(null); + +export function NotificationProvider({ children }: { children: React.ReactNode }) { + const [notifications, setNotifications] = React.useState([]); + const [lastReadTime, setLastReadTime] = React.useState(Date.now()); + + const addNotification = React.useCallback((type: NotificationType, message: string) => { + const id = `notif_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + const newNotification: NotificationItem = { id, type, message, timestamp: Date.now() }; + setNotifications(prev => { + const updated = [...prev, newNotification]; + if (updated.length > 50) { + return updated.slice(-50); + } + return updated; + }); + }, []); + + const removeNotification = React.useCallback((id: string) => { + setNotifications(prev => prev.filter(n => n.id !== id)); + }, []); + + const clearNotifications = React.useCallback(() => { + setNotifications([]); + setLastReadTime(Date.now()); + }, []); + + const unreadCount = React.useMemo(() => { + return notifications.filter(n => n.timestamp > lastReadTime).length; + }, [notifications, lastReadTime]); + + const hasUnread = unreadCount > 0; + + const value: NotificationContextValue = { + notifications, + addNotification, + removeNotification, + clearNotifications, + unreadCount, + hasUnread, + }; + + return h(NotificationContext.Provider, { value }, children); +} + +const h = React.createElement; + +export function useNotification(): NotificationContextValue { + const context = React.useContext(NotificationContext); + if (!context) { + throw new Error("useNotification must be used within NotificationProvider"); + } + return context; +} \ No newline at end of file diff --git a/src/components/frontend/src/notification-popup.tsx b/src/components/frontend/src/notification-popup.tsx new file mode 100644 index 00000000..97f06415 --- /dev/null +++ b/src/components/frontend/src/notification-popup.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { useNotification, NotificationItem } from "./notification-context"; + +const h = React.createElement; + +interface NotificationPopupProps { + onClose: () => void; +} + +export function NotificationPopup({ onClose }: NotificationPopupProps) { + const { notifications, removeNotification, clearNotifications } = useNotification(); + const listRef = React.useRef(null); + + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (listRef.current && !listRef.current.contains(event.target as Node)) { + onClose(); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [onClose]); + + if (notifications.length === 0) { + return h("div", { className: "notification-popup", ref: listRef }, + h("div", { className: "notification-popup-header" }, + h("span", null, "通知"), + h("button", { className: "notification-popup-close", onClick: onClose }, "×"), + ), + h("div", { className: "notification-popup-empty" }, "暂无通知"), + ); + } + + return h("div", { className: "notification-popup", ref: listRef }, + h("div", { className: "notification-popup-header" }, + h("span", null, `通知 (${notifications.length})`), + h("div", { className: "notification-popup-actions" }, + h("button", { className: "notification-popup-clear", onClick: clearNotifications }, "清空"), + h("button", { className: "notification-popup-close", onClick: onClose }, "×"), + ), + ), + h("div", { className: "notification-popup-list" }, + notifications.slice().reverse().map((item: NotificationItem) => + h("div", { + key: item.id, + className: `notification-item ${item.type}`, + }, + h("span", { className: "notification-item-icon" }, item.type === "success" ? "✓" : "×"), + h("span", { className: "notification-item-message" }, item.message), + h("button", { + className: "notification-item-dismiss", + onClick: () => removeNotification(item.id), + }, "×"), + ) + ), + ), + ); +} + +export function NotificationBanner({ notification }: { notification: NotificationItem }) { + const { removeNotification } = useNotification(); + + React.useEffect(() => { + const timer = setTimeout(() => { + removeNotification(notification.id); + }, 3000); + return () => clearTimeout(timer); + }, [notification.id, removeNotification]); + + return h("div", { + className: `notification-banner ${notification.type}`, + role: "alert", + }, + h("span", { className: "notification-banner-icon" }, notification.type === "success" ? "✓" : "×"), + h("span", { className: "notification-banner-message" }, notification.message), + h("button", { + className: "notification-banner-dismiss", + onClick: () => removeNotification(notification.id), + }, "×"), + ); +} \ No newline at end of file diff --git a/src/components/frontend/src/notification-styles.ts b/src/components/frontend/src/notification-styles.ts new file mode 100644 index 00000000..b2f5f07d --- /dev/null +++ b/src/components/frontend/src/notification-styles.ts @@ -0,0 +1,171 @@ +export const notificationStyles = ` +.notification-icon-btn { + position: relative; + background: none; + border: 1px solid #3a3a4a; + border-radius: 6px; + padding: 4px 8px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.2s; +} +.notification-icon-btn:hover { + background-color: #2a2a3a; +} +.notification-icon-btn.has-unread { + border-color: #4a9eff; +} +.notification-badge { + position: absolute; + top: -6px; + right: -6px; + background: #e53e3e; + color: white; + border-radius: 10px; + font-size: 10px; + min-width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; +} +.notification-popup { + position: fixed; + top: 48px; + right: 12px; + width: 340px; + max-height: 480px; + background: #1e1e2e; + border: 1px solid #3a3a4a; + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); + z-index: 99999; + display: flex; + flex-direction: column; + overflow: hidden; +} +.notification-popup-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid #3a3a4a; + font-weight: 600; + color: #e0e0e0; +} +.notification-popup-actions { + display: flex; + gap: 8px; +} +.notification-popup-clear, +.notification-popup-close { + background: none; + border: none; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + color: #888; + font-size: 16px; +} +.notification-popup-clear:hover, +.notification-popup-close:hover { + color: #e0e0e0; + background-color: #2a2a3a; +} +.notification-popup-empty { + padding: 32px 16px; + text-align: center; + color: #666; +} +.notification-popup-list { + overflow-y: auto; + flex: 1; +} +.notification-item { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + border-bottom: 1px solid #2a2a3a; + color: #e0e0e0; + font-size: 13px; +} +.notification-item.success { + border-left: 3px solid #38a169; +} +.notification-item.error { + border-left: 3px solid #e53e3e; +} +.notification-item-icon { + font-size: 14px; + width: 20px; + text-align: center; +} +.notification-item-message { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.notification-item-dismiss { + background: none; + border: none; + cursor: pointer; + color: #666; + padding: 2px 4px; + font-size: 14px; +} +.notification-item-dismiss:hover { + color: #e0e0e0; +} +.notification-banner { + position: fixed; + top: 52px; + left: 50%; + transform: translateX(-50%); + padding: 10px 20px; + border-radius: 8px; + display: flex; + align-items: center; + gap: 10px; + z-index: 999999; + animation: slideDown 0.3s ease; + box-shadow: 0 4px 20px rgba(0,0,0,0.4); + font-size: 14px; + color: white; +} +.notification-banner.success { + background: linear-gradient(135deg, #38a169, #2f855a); +} +.notification-banner.error { + background: linear-gradient(135deg, #e53e3e, #c53030); +} +.notification-banner-icon { + font-size: 16px; +} +.notification-banner-message { + flex: 1; +} +.notification-banner-dismiss { + background: none; + border: none; + cursor: pointer; + color: rgba(255,255,255,0.7); + padding: 2px 6px; + font-size: 16px; +} +.notification-banner-dismiss:hover { + color: white; +} +@keyframes slideDown { + from { + opacity: 0; + transform: translateX(-50%) translateY(-20px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} +`; \ No newline at end of file diff --git a/src/components/frontend/src/pipeline.tsx b/src/components/frontend/src/pipeline.tsx index ce9d9312..98c4956b 100644 --- a/src/components/frontend/src/pipeline.tsx +++ b/src/components/frontend/src/pipeline.tsx @@ -5,6 +5,7 @@ import { Background, BaseEdge, Controls, Handle, MarkerType, Position, ReactFlow import { TraceView, opencodeTracePort } from "./trace"; import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; +import { useNotification } from "./notification-context"; type AnyRecord = Record; @@ -995,7 +996,6 @@ function PipelineGanttDetailPanel({ selection, runDetails, nodeDetails, nodeDeta { label: "fetched", value: fetchedAt ? fmtClock(fetchedAt) : "--" }, context?.matchedStep ? { label: "matched step", value: `Step ${context.matchedStep.index ?? context.matchedStepIndex + 1}` } : null, ] }), - loading ? h("div", { className: "form-success" }, nodeLoading ? "正在抓取该 node 的 attempt / Trace..." : "正在抓取 epoch 执行过程..." ) : null, h(UniDeskErrorBanner, { error }), h("div", { className: "pipeline-gantt-detail-actions" }, h(RawButton, { title: `Procedure ${interval?.procedureRunId || marker?.procedureRunId || context?.nodeId || "node"}`, data: procedure, onOpen: onRaw, testId: "raw-pipeline-gantt-procedure" }), @@ -3344,7 +3344,6 @@ function PipelineNodeControlPanel({ activeRun, pipelineRuns, selectedRunId, onRu ), !selectedNodeId ? h(EmptyState, { title: "未选择 node", text: "点击 React Flow 控制图中的任意 node 后,可抓取执行过程、追加 prompt、下发引导、增量修改、审核通过或重做。" }) : null, h(UniDeskErrorBanner, { error: control.error, wide: true }), - control.message ? h("div", { className: "form-success wide" }, control.message) : null, h("div", { className: "pipeline-control-actions" }, h("label", null, h("span", null, "实时追加 prompt(仅 running node)"), @@ -3429,6 +3428,7 @@ export function PipelinePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR const [nodeControlOpen, setNodeControlOpen] = useState(false); const [ganttDetailOpen, setGanttDetailOpen] = useState(false); const loadRequestRef = useRef(0); + const { addNotification } = useNotification(); const loadInFlightRef = useRef(false); const runDetailsRequestRef = useRef(0); const runDetailsInFlightRef = useRef(""); @@ -3761,6 +3761,8 @@ export function PipelinePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR action === "approve" ? "已提交审核通过决策" : "已排队重做命令", })); + const msg = action === "append" ? "已追加到运行中 node" : action === "guide" ? "已下发 guide,等待 runner 处理" : action === "modify" ? "已排队增量修改命令" : action === "approve" ? "已提交审核通过决策" : "已排队重做命令"; + addNotification("success", msg); await fetchNodeDetails(activeRunId, selectedNodeId); await fetchRunDetails(activeRunId, { silent: true }); if (action !== "append") await load(); diff --git a/src/components/frontend/src/project-manager.tsx b/src/components/frontend/src/project-manager.tsx index acb2ad94..42823c8e 100644 --- a/src/components/frontend/src/project-manager.tsx +++ b/src/components/frontend/src/project-manager.tsx @@ -3,6 +3,7 @@ import { beijingDateStamp, fmtClock, fmtDate } from "./time"; import { LoadingTitle } from "./loading-indicator"; import { errorMessage, requestBlob, requestJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; +import { useNotification } from "./notification-context"; type AnyRecord = Record; @@ -145,6 +146,7 @@ export function ProjectManagerPage({ microservices, onRaw, apiBaseUrl = "/api" } const [form, setForm] = useState({ ...EMPTY_FORM }); const [query, setQuery] = useState(""); const [status, setStatus] = useState("all"); + const { addNotification } = useNotification(); async function load(nextQuery = query, nextStatus = status): Promise { if (!service) return; @@ -176,7 +178,9 @@ export function ProjectManagerPage({ microservices, onRaw, apiBaseUrl = "/api" } } else { await requestJson(projectApi(apiBaseUrl, "/api/projects"), { method: "POST", body: JSON.stringify(payload) }); } - setState((prev: any) => ({ ...prev, saving: false, notice: form.id ? "项目已更新" : "项目已创建" })); + const msg = form.id ? "项目已更新" : "项目已创建"; + setState((prev: any) => ({ ...prev, saving: false, notice: msg })); + addNotification("success", msg); await load(); } catch (err) { setState((prev: any) => ({ ...prev, saving: false, error: errorMessage(err, "保存项目失败") })); @@ -190,7 +194,9 @@ export function ProjectManagerPage({ microservices, onRaw, apiBaseUrl = "/api" } try { await requestJson(projectApi(apiBaseUrl, `/api/projects/${encodeURIComponent(form.id)}`), { method: "DELETE" }); setForm({ ...EMPTY_FORM }); - setState((prev: any) => ({ ...prev, saving: false, notice: "项目已删除" })); + const msg = "项目已删除"; + setState((prev: any) => ({ ...prev, saving: false, notice: msg })); + addNotification("success", msg); await load(); } catch (err) { setState((prev: any) => ({ ...prev, saving: false, error: errorMessage(err, "删除项目失败") })); @@ -207,7 +213,9 @@ export function ProjectManagerPage({ microservices, onRaw, apiBaseUrl = "/api" } method: "POST", body: JSON.stringify({ fileName: file.name, contentBase64, replace: false }), }); - setState((prev: any) => ({ ...prev, importing: false, notice: `Excel 已导入 ${result.imported || 0} 条项目` })); + const msg = `Excel 已导入 ${result.imported || 0} 条项目`; + setState((prev: any) => ({ ...prev, importing: false, notice: msg })); + addNotification("success", msg); event.target.value = ""; await load(); } catch (err) { diff --git a/src/components/frontend/src/trace.tsx b/src/components/frontend/src/trace.tsx index 9a965173..9bbc621f 100644 --- a/src/components/frontend/src/trace.tsx +++ b/src/components/frontend/src/trace.tsx @@ -224,7 +224,7 @@ function isEditedFileChangeItem(item: TraceItem): boolean { if ((status === "item/started" || status === "item/completed") && /file changes status=/u.test(body)) return true; if (/^Success\. Updated the following files:/mu.test(body)) return true; if (/^diff --git /mu.test(body)) return true; - return command.length === 0 && /^([AMDRCU?]{1,2})\s+\S+/mu.test(body); + return /^([AMDRCU?]{1,2})\s+\S+/mu.test(body) || (command.length > 0 && parseTraceDiffFiles(body).length > 0); } function cleanDiffPath(value: string): string { @@ -527,35 +527,6 @@ function opencodePartRawSeq(part: any, fallback: any): any { return part?.id || part?.messageId || fallback; } -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 || ""); - } - return ""; -} - -function opencodeToolKind(part: any): TraceItemKind { - const tool = String(part?.tool || part?.title || "").toLowerCase(); - 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 ""; @@ -566,32 +537,93 @@ function opencodePreviewValue(value: any, max = 1200): string { } } +function opencodeToolOutput(state: any, part: any, event: any): string { + if (typeof state?.metadata?.diff === "string" && state.metadata.diff.length > 0) return state.metadata.diff; + if (typeof state?.metadata?.filediff?.patch === "string" && state.metadata.filediff.patch.length > 0) return state.metadata.filediff.patch; + if (typeof state?.output === "string" && state.output.length > 0) return state.output; + if (typeof state?.result === "string" && state.result.length > 0) return state.result; + if (typeof part?.output === "string" && part.output.length > 0) return part.output; + if (typeof event?.output === "string" && event.output.length > 0) return event.output; + if (typeof state?.metadata?.output === "string" && state.metadata.output.length > 0) return state.metadata.output; + return ""; +} + +function opencodeRecordString(record: any, keys: string[]): string { + if (!record || typeof record !== "object" || Array.isArray(record)) return ""; + for (const key of keys) { + const value = record[key]; + if (typeof value === "string" && value.length > 0) return value; + if (value !== undefined && value !== null && typeof value !== "object") return String(value); + } + return ""; +} + +function opencodeRecordNumber(record: any, keys: string[]): number | null { + if (!record || typeof record !== "object" || Array.isArray(record)) return null; + for (const key of keys) { + const value = Number(record[key]); + if (Number.isFinite(value)) return value; + } + return null; +} + +function opencodeToolCommand(part: any, state: any): string { + const input = state?.input && typeof state.input === "object" && !Array.isArray(state.input) + ? state.input + : part?.input && typeof part.input === "object" && !Array.isArray(part.input) + ? part.input + : {}; + const inputCommand = opencodeRecordString(input, ["command", "cmd", "script"]); + if (inputCommand.length > 0) return inputCommand; + if (typeof part?.command === "string" && part.command.length > 0) return part.command; + if (typeof state?.command === "string" && state.command.length > 0) return state.command; + const tool = String(part?.tool || part?.title || "tool"); + const path = opencodeRecordString(input, ["filePath", "filepath", "path"]) || opencodeRecordString(part, ["filePath", "filepath", "path"]); + const pattern = opencodeRecordString(input, ["pattern", "query"]); + const offset = opencodeRecordNumber(input, ["offset"]); + const limit = opencodeRecordNumber(input, ["limit"]); + const args = [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}`); + return args.length > 1 ? args.join(" ") : tool; +} + 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 (type === "step_start" || type === "step-start" || partType === "step-start") return null; + if (type === "step_finish" || type === "step-finish" || partType === "step-finish") return null; 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); + const command = opencodeToolCommand(part, state); + const output = opencodeToolOutput(state, part, event); + const kind = opencodeToolKindFromCommand(command, String(part?.tool || part?.title || "")); + const observation = kind === "edited" ? { + status: String(state?.status || part?.status || event?.status || ""), + summary: shortText(output || command, 72), + files: parseTraceDiffFiles(output), + stages: [], + lines: traceDiffLines(output), + addedLines: 0, + removedLines: 0, + rawText: output, + } : undefined; return { seq, at: opencodePartCompletedAt(part, at), - kind: opencodeToolKind(part), + kind, 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), + bodyPreview: output, durationMs: opencodePartDurationMs(part), rawSeqs: [part?.id || part?.callID || event?.sessionID || seq], + editObservation: observation, }; } const text = opencodePreviewValue(part?.text ?? part?.content ?? part?.delta ?? event?.text ?? event?.content ?? event?.delta, 3000).trim(); @@ -610,25 +642,38 @@ function opencodeRawEventToTrace(event: any, fallbackSeq: number): TraceItem | n return null; } +function opencodeToolKindFromCommand(command: string, tool: string): TraceItemKind { + const normalized = `${tool} ${command}`.toLowerCase(); + if (/\b(read|grep|glob|list|ls|find|search|view|cat|sed|rg|head|tail|wc|file)\b/iu.test(normalized)) return "explored"; + if (/\b(edit|write|patch|apply|update|create|delete|apply_patch|git apply|cat >|tee .*<<|sed -i|python3? .*write_text|mkdir|rm |touch )\b/iu.test(normalized)) return "edited"; + return "ran"; +} + 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) { + const status = String(step?.status || ""); + if (status === "opencode/step-start" || status === "opencode/step-finish") continue; 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 : []; + if (parts.length === 0) { + 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); + } + } else if (step?.textPreview) { + rows.push({ seq: seq++, at, kind: "message", title: `${role || "assistant"} message`, status: role, bodyPreview: String(step.textPreview), rawSeqs: [step?.messageId || seq] }); + } + continue; + } for (const part of parts) { const type = String(part?.type || "").toLowerCase(); if (type === "step-start" || type === "step-finish") continue; @@ -648,27 +693,37 @@ export function opencodeStepsToTrace(steps: any[]): TraceItem[] { continue; } if (type === "tool") { - const command = partFieldValue(part, ["command", "cmd"]) || partFieldValue(part, ["filePath", "filepath", "path"]) || String(part?.title || part?.tool || "tool"); - const output = String(part?.outputPreview && part.outputPreview !== "--" ? part.outputPreview : part?.textPreview || ""); + const state = part?.state && typeof part.state === "object" && !Array.isArray(part.state) ? part.state : {}; + const command = opencodeToolCommand(part, state); + const output = opencodeToolOutput(state, part, {}); + const kind = opencodeToolKindFromCommand(command, String(part?.tool || part?.title || "")); + const observation = kind === "edited" ? { + status: String(state?.status || part?.status || ""), + summary: shortText(output || command, 72), + files: parseTraceDiffFiles(output), + stages: [], + lines: traceDiffLines(output), + addedLines: 0, + removedLines: 0, + rawText: output, + } : undefined; rows.push({ seq: seq++, at: opencodePartCompletedAt(part, at), - kind: opencodeToolKind(part), + kind, title: String(part?.title || part?.tool || "tool"), status: String(part?.status || ""), commandPreview: command, bodyPreview: output, durationMs: opencodePartDurationMs(part), rawSeqs: [opencodePartRawSeq(part, seq)], + editObservation: observation, }); continue; } const text = String(part?.textPreview || part?.title || type || "").trim(); if (text) rows.push({ seq: seq++, at: opencodePartCompletedAt(part, at), kind: "system", title: type || "part", bodyPreview: text, status: String(part?.status || ""), durationMs: opencodePartDurationMs(part), rawSeqs: [opencodePartRawSeq(part, seq)] }); } - if (parts.length === 0 && step?.textPreview) { - rows.push({ seq: seq++, at, kind: "message", title: `${role || "assistant"} message`, status: role, bodyPreview: String(step.textPreview), rawSeqs: [step?.messageId || seq] }); - } } return rows; } diff --git a/src/components/microservices/baidu-netdisk/src/index.ts b/src/components/microservices/baidu-netdisk/src/index.ts index 354e1dfb..fe377e86 100644 --- a/src/components/microservices/baidu-netdisk/src/index.ts +++ b/src/components/microservices/baidu-netdisk/src/index.ts @@ -99,9 +99,9 @@ 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"; + const raw = String(value || "/").trim() || "/"; + const normalized = pathPosix.normalize(raw.startsWith("/") ? raw : `/${raw}`); + return normalized === "/" ? "/" : normalized.replace(/\/+$/u, "") || "/"; } function configFromEnv(): RuntimeConfig { @@ -116,7 +116,7 @@ function configFromEnv(): RuntimeConfig { 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"), + appRoot: normalizedAppRoot(process.env.BAIDU_NETDISK_APP_ROOT || "/"), 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", @@ -448,6 +448,11 @@ async function createRemoteFolder(accessToken: string, path: string): Promise { + if (config.appRoot === "/") { + appRootEnsuredAt = Date.now(); + if (force) log("remote_root_ready", { path: config.appRoot }); + return; + } if (!force && appRootEnsuredAt > 0 && Date.now() - appRootEnsuredAt < 10 * 60 * 1000) return; if (appRootEnsurePromise) return appRootEnsurePromise; appRootEnsurePromise = (async () => { @@ -465,10 +470,11 @@ async function ensureAppRoot(accessToken: string, force = false): Promise function remotePathInsideRoot(input: string | undefined, fallback = config.appRoot): string { const raw = String(input || fallback).trim() || fallback; + if (raw.includes("\0")) throw new HttpError(400, "remote path must not contain null bytes"); 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 }); + const clean = normalized === "/" ? "/" : normalized.replace(/\/+$/u, "") || config.appRoot; + if (config.appRoot !== "/" && clean !== config.appRoot && !clean.startsWith(`${config.appRoot}/`)) { + throw new HttpError(400, "remote path must stay inside configured root", { rootPath: config.appRoot, path: clean }); } return clean; } diff --git a/src/components/microservices/code-queue/src/code-agent/codex.ts b/src/components/microservices/code-queue/src/code-agent/codex.ts new file mode 100644 index 00000000..50e7967e --- /dev/null +++ b/src/components/microservices/code-queue/src/code-agent/codex.ts @@ -0,0 +1,392 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import * as readline from "node:readline"; +import type { AppServerExit, CodexEventSummary, CodexRunResult, JsonValue, QueueTask, RuntimeConfig, SessionCommandOutput, TerminalStatus } from "../types"; +import type { ActiveRun, CodeAgentClient } from "./common"; +import { extractRecord, extractString, terminalStatus, textInput } from "./common"; + +export interface CodexPortContext { + config: Pick; + activeRuns: Map; + appendOutput: (task: QueueTask, channel: "system" | "assistant" | "reasoning" | "command" | "diff" | "tool" | "error", text: string, method?: string, itemId?: string, append?: boolean) => unknown; + addEvent: (task: QueueTask, event: CodexEventSummary) => void; + ensureTaskExecutionContainer: (task: QueueTask) => Promise; + formatCommandOutput: (output: SessionCommandOutput | null | undefined) => string; + logger: (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void; + persistTaskState: (task: QueueTask) => void; + providerIsMain: (providerId: string) => boolean; + queueIdOf: (task: QueueTask) => string; + recordNumberField: (record: Record | null, keys: string[]) => number | null; + recordStringField: (record: Record | null, keys: string[]) => string; + remoteAppServerCommand: (task: QueueTask) => string; + resolveReasoningEffort: (model: string, explicit?: string | null) => string | null; + safePreview: (value: string, max?: number) => string; + nowIso: () => string; +} + +let context: CodexPortContext | null = null; + +export function configureCodexPort(runtimeContext: CodexPortContext): void { + context = runtimeContext; +} + +function ctx(): CodexPortContext { + if (context === null) throw new Error("codex port is not configured"); + return context; +} + +class AppServerClient { + private child: ChildProcessWithoutNullStreams; + private nextId = 1; + private pending = new Map void; reject: (error: Error) => void }>(); + private stderrChunks: Buffer[] = []; + private closed = false; + private exitInfo: AppServerExit | null = null; + private closeResolve!: (value: AppServerExit) => void; + readonly closedPromise: Promise; + + constructor(private readonly task: QueueTask, private readonly onNotification: (message: Record) => void) { + this.closedPromise = new Promise((resolveClosed) => { this.closeResolve = resolveClosed; }); + this.child = ctx().providerIsMain(task.providerId) + ? spawn("codex", ["app-server", "--listen", "stdio://"], { + cwd: task.cwd, + env: { ...process.env, CODEX_HOME: ctx().config.codexHome, CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "unidesk_code_queue" }, + stdio: "pipe", + }) + : spawn("bun", ["scripts/cli.ts", "ssh", task.providerId, ctx().remoteAppServerCommand(task)], { + cwd: ctx().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(); + }); + 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)); + } + + async initialize(): Promise { + await this.request("initialize", { + clientInfo: { name: "unidesk_code_queue", title: "UniDesk Code Queue", version: "0.1.0" }, + capabilities: { experimentalApi: true }, + }); + this.notify("initialized", {}); + } + + async startOrResumeThread(): Promise { + if (this.task.codexThreadId !== null) { + ctx().appendOutput(this.task, "system", `thread resume requested ${this.task.codexThreadId}\n`, "thread/resume"); + const response = await this.request("thread/resume", { + threadId: this.task.codexThreadId, + model: this.task.model, + cwd: this.task.cwd, + approvalPolicy: ctx().config.approvalPolicy, + sandbox: ctx().config.sandbox, + }); + const threadId = extractString(extractRecord(response)?.thread, "id"); + if (threadId === null) throw new Error("thread/resume response did not include thread.id"); + ctx().appendOutput(this.task, "system", `thread resumed ${threadId}\n`, "thread/resume"); + return threadId; + } + const response = await this.request("thread/start", { + model: this.task.model, + cwd: this.task.cwd, + approvalPolicy: ctx().config.approvalPolicy, + sandbox: ctx().config.sandbox, + 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"); + return threadId; + } + + async startTurn(threadId: string, prompt: string): Promise { + const params: Record = { + threadId, + input: textInput(prompt), + cwd: this.task.cwd, + approvalPolicy: ctx().config.approvalPolicy, + model: this.task.model, + }; + const effort = ctx().resolveReasoningEffort(this.task.model, this.task.reasoningEffort); + if (effort !== null) params.effort = effort; + const response = await this.request("turn/start", params); + const turnId = extractString(extractRecord(response)?.turn, "id"); + if (turnId === null) throw new Error("turn/start response did not include turn.id"); + return turnId; + } + + async steer(threadId: string, turnId: string, prompt: string): Promise { + await this.request("turn/steer", { threadId, expectedTurnId: turnId, input: textInput(prompt) }); + } + + async interrupt(threadId: string, turnId: string): Promise { + await this.request("turn/interrupt", { threadId, turnId }); + } + + stop(): void { + if (this.closed) return; + this.child.kill("SIGTERM"); + setTimeout(() => { + if (!this.closed) this.child.kill("SIGKILL"); + }, 1500).unref?.(); + } + + private request(method: string, params: unknown): Promise { + if (this.closed) return Promise.reject(new Error("app-server is already closed")); + const id = this.nextId++; + const message = { method, id, params }; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + this.write(message); + }); + } + + private notify(method: string, params: unknown): void { + this.write({ method, params }); + } + + private write(message: unknown): void { + this.child.stdin.write(`${JSON.stringify(message)}\n`); + } + + private async readLines(rl: readline.Interface): Promise { + try { + for await (const line of rl) { + const trimmed = String(line).trim(); + if (trimmed.length === 0) continue; + const message = JSON.parse(trimmed) as Record; + this.handleMessage(message); + } + } catch (error) { + ctx().appendOutput(this.task, "error", `app-server stream error: ${error instanceof Error ? error.message : String(error)}\n`, "app-server"); + } + } + + private handleMessage(message: Record): void { + const id = typeof message.id === "number" ? message.id : null; + const method = typeof message.method === "string" ? message.method : null; + if (id !== null && method === null) { + const pending = this.pending.get(id); + if (pending === undefined) return; + this.pending.delete(id); + if ("error" in message) { + pending.reject(new Error(JSON.stringify(message.error))); + } else { + pending.resolve(message.result); + } + return; + } + if (id !== null && method !== null) { + this.handleServerRequest(id, method); + return; + } + if (method !== null) this.onNotification(message); + } + + private handleServerRequest(id: number, method: string): void { + if (method === "item/commandExecution/requestApproval") { + this.write({ id, result: { decision: "decline" } }); + return; + } + if (method === "item/fileChange/requestApproval") { + this.write({ id, result: { decision: "decline" } }); + return; + } + this.write({ id, error: { code: -32601, message: `Unsupported client-side request: ${method}` } }); + } + + private handleClose(code: number | null, signal: string | null): void { + if (this.closed) return; + this.closed = true; + this.exitInfo = { code, signal, stderrTail: Buffer.concat(this.stderrChunks).toString("utf8").slice(-8000) }; + for (const pending of this.pending.values()) pending.reject(new Error(`app-server closed with code=${code} signal=${signal}`)); + this.pending.clear(); + this.closeResolve(this.exitInfo); + } +} + +function eventSummary(message: Record): CodexEventSummary { + const params = extractRecord(message.params); + const item = extractRecord(params?.item); + const turn = extractRecord(params?.turn); + const error = extractRecord(turn?.error) ?? extractRecord(params?.error); + return { + at: ctx().nowIso(), + method: typeof message.method === "string" ? message.method : "unknown", + itemType: typeof item?.type === "string" ? item.type : undefined, + status: typeof item?.status === "string" ? item.status : typeof turn?.status === "string" ? turn.status : undefined, + message: typeof error?.message === "string" ? ctx().safePreview(error.message, 600) : undefined, + textPreview: typeof item?.text === "string" ? ctx().safePreview(item.text, 800) : undefined, + }; +} + +function commandCompletionStreams(item: Record | null): { stdout: string; stderr: string; output: string; exitCode: number | null } { + if (item === null) return { stdout: "", stderr: "", output: "", exitCode: null }; + const result = extractRecord(item.result); + const stdout = ctx().recordStringField(item, ["stdout", "stdoutText"]) || ctx().recordStringField(result, ["stdout", "stdoutText"]); + const stderr = ctx().recordStringField(item, ["stderr", "stderrText"]) || ctx().recordStringField(result, ["stderr", "stderrText"]); + const output = ctx().recordStringField(item, ["output", "outputText", "text"]) || ctx().recordStringField(result, ["output", "outputText", "text"]); + const exitCode = ctx().recordNumberField(item, ["exitCode", "code"]) ?? ctx().recordNumberField(result, ["exitCode", "code"]); + return { stdout: stdout || (stderr.length > 0 ? "" : output), stderr, output: output || [stdout, stderr].filter(Boolean).join("\n"), exitCode }; +} + +function hasRecentCommandOutputDelta(task: QueueTask, itemId: string | undefined): boolean { + if (itemId === undefined) return false; + for (let index = task.output.length - 1; index >= 0 && index >= task.output.length - 80; index -= 1) { + const output = task.output[index]; + if (output?.itemId !== itemId) continue; + if (output.method === "item/commandExecution/outputDelta") return true; + if (output.method === "item/started") return false; + } + return false; +} + +function handleNotification(task: QueueTask, message: Record, terminal: (status: TerminalStatus, error: string | null) => void): void { + const method = typeof message.method === "string" ? message.method : "unknown"; + const params = extractRecord(message.params); + ctx().addEvent(task, eventSummary(message)); + if (method === "thread/started") { + const threadId = extractString(extractRecord(params?.thread), "id"); + if (threadId !== null) task.codexThreadId = threadId; + ctx().appendOutput(task, "system", `thread started ${threadId ?? "unknown"}\n`, method); + return; + } + if (method === "turn/started") { + const turnId = extractString(extractRecord(params?.turn), "id"); + task.activeTurnId = turnId; + ctx().appendOutput(task, "system", `turn started ${turnId ?? "unknown"}\n`, method); + return; + } + if (method === "item/agentMessage/delta") { + ctx().appendOutput(task, "assistant", String(params?.delta ?? ""), method, extractString(params, "itemId") ?? undefined, true); + return; + } + if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") { + ctx().appendOutput(task, "reasoning", String(params?.delta ?? ""), method, extractString(params, "itemId") ?? undefined, true); + return; + } + if (method === "item/commandExecution/outputDelta") { + ctx().appendOutput(task, "command", String(params?.delta ?? ""), method, extractString(params, "itemId") ?? undefined, true); + return; + } + if (method === "item/fileChange/outputDelta") { + ctx().appendOutput(task, "diff", String(params?.delta ?? ""), method, extractString(params, "itemId") ?? undefined, true); + return; + } + if (method === "item/started" || method === "item/completed") { + const item = extractRecord(params?.item); + const type = String(item?.type ?? "item"); + if (type === "agentMessage" && typeof item?.text === "string") task.finalResponse = item.text; + if (type === "commandExecution") { + const itemId = extractString(item, "id") ?? undefined; + if (method === "item/completed") { + const streams = commandCompletionStreams(item); + const hasDelta = hasRecentCommandOutputDelta(task, itemId); + const completedOutput = hasDelta && streams.stderr.trim().length > 0 + ? `\n[stderr]\n${streams.stderr.trimEnd()}` + : hasDelta + ? "" + : ctx().formatCommandOutput({ callId: itemId ?? "", at: ctx().nowIso(), stdout: streams.stdout, stderr: streams.stderr, output: streams.output, exitCode: streams.exitCode }); + if (completedOutput.trim().length > 0) ctx().appendOutput(task, "command", `${completedOutput.trimEnd()}\n`, "item/commandExecution/outputDelta", itemId, true); + } + ctx().appendOutput(task, "command", `${method}: ${String(item?.command ?? "command")} status=${String(item?.status ?? "unknown")}\n`, method, itemId); + } + if (type === "fileChange") ctx().appendOutput(task, "diff", `${method}: file changes status=${String(item?.status ?? "unknown")}\n`, method, extractString(item, "id") ?? undefined); + if (type === "mcpToolCall" || type === "webSearch" || type === "dynamicToolCall") ctx().appendOutput(task, "tool", `${method}: ${type}\n`, method, extractString(item, "id") ?? undefined); + return; + } + if (method === "error") { + const error = extractRecord(params?.error); + ctx().appendOutput(task, "error", `${String(error?.message ?? "Codex error")}\n`, method); + return; + } + if (method === "turn/completed") { + const turn = extractRecord(params?.turn); + const status = terminalStatus(String(turn?.status ?? "failed")); + const error = extractRecord(turn?.error); + task.activeTurnId = null; + ctx().appendOutput(task, status === "completed" ? "system" : "error", `turn completed status=${status ?? "unknown"}\n`, method); + terminal(status, typeof error?.message === "string" ? error.message : null); + } +} + +export async function runCodexTurn(task: QueueTask, prompt: string): Promise { + const queueId = ctx().queueIdOf(task); + const events: CodexEventSummary[] = []; + let terminalSeen = false; + let lastAppActivityAt = Date.now(); + let terminalResult: { status: TerminalStatus; error: string | null } = { status: null, error: null }; + let terminalResolve!: () => void; + const terminalPromise = new Promise((resolveTerminal) => { terminalResolve = resolveTerminal; }); + await ctx().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(); + const before = task.events.length; + handleNotification(task, message, (status, error) => { + terminalSeen = true; + terminalResult = { status, error }; + terminalResolve(); + }); + events.push(...task.events.slice(before)); + }); + + try { + await app.initialize(); + const threadId = await app.startOrResumeThread(); + task.codexThreadId = threadId; + ctx().activeRuns.set(queueId, { taskId: task.id, queueId, app, port: "codex", threadId, turnId: null }); + const turnId = await app.startTurn(threadId, prompt); + task.activeTurnId = turnId; + const run = ctx().activeRuns.get(queueId); + if (run?.app === app) run.turnId = turnId; + ctx().persistTaskState(task); + const activityWatchdog = setInterval(() => { + if (terminalSeen) return; + const idleMs = Date.now() - lastAppActivityAt; + if (idleMs < ctx().config.turnNoActivityTimeoutMs) return; + const message = `No Codex app activity for ${Math.round(idleMs / 1000)}s; stopping app-server so the existing thread can retry.`; + ctx().appendOutput(task, "error", `${message}\n`, "turn/no-activity-watchdog"); + ctx().logger("warn", "turn_no_activity_watchdog", { taskId: task.id, turnId, idleMs, timeoutMs: ctx().config.turnNoActivityTimeoutMs }); + app.stop(); + }, 15_000); + const race = await Promise.race([terminalPromise.then(() => "terminal" as const), app.closedPromise.then(() => "closed" as const)]); + clearInterval(activityWatchdog); + const closedBeforeTerminal = race === "closed" && !terminalSeen; + if (terminalSeen) app.stop(); + const exit = await app.closedPromise; + return { + threadId, + turnId, + finalResponse: task.finalResponse, + terminalStatus: terminalResult.status, + terminalError: terminalResult.error, + transportClosedBeforeTerminal: closedBeforeTerminal, + appServerExit: exit, + events, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + ctx().appendOutput(task, "error", `${message}\n`, "app-server"); + app.stop(); + const exit = await app.closedPromise; + return { + threadId: task.codexThreadId, + turnId: task.activeTurnId, + finalResponse: task.finalResponse, + terminalStatus: "failed", + terminalError: message, + transportClosedBeforeTerminal: !terminalSeen, + appServerExit: exit, + events, + }; + } finally { + if (ctx().activeRuns.get(queueId)?.app === app) ctx().activeRuns.delete(queueId); + app.stop(); + } +} diff --git a/src/components/microservices/code-queue/src/code-agent/common.ts b/src/components/microservices/code-queue/src/code-agent/common.ts new file mode 100644 index 00000000..26c810a9 --- /dev/null +++ b/src/components/microservices/code-queue/src/code-agent/common.ts @@ -0,0 +1,84 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import type { JsonValue } from "../types"; + +export type CodeAgentPortKind = "codex" | "opencode"; + +export interface CodeAgentClient { + stop(): void; + steer?(threadId: string, turnId: string, prompt: string): Promise; + interrupt?(threadId: string, turnId: string): Promise; +} + +export interface ActiveRun { + taskId: string; + queueId: string; + app: CodeAgentClient; + port: CodeAgentPortKind; + threadId: string | null; + turnId: string | null; +} + +export interface ActiveRunSlotWaiter { + id: number; + taskId: string; + queueId: string; + enqueuedAt: string; +} + +export const minimaxM27Model = "minimax-m2.7"; +export const defaultCodeModels = ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4", minimaxM27Model]; +export const opencodeNpmPackage = "opencode-ai@1.14.48"; + +export 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; +} + +export function codeAgentPortForModel(model: string): CodeAgentPortKind { + return normalizeCodeModel(model) === minimaxM27Model ? "opencode" : "codex"; +} + +export function opencodeModels(models: string[]): string[] { + return models.filter((model) => codeAgentPortForModel(model) === "opencode"); +} + +export function codeModelPorts(models: string[]): Record { + return Object.fromEntries(models.map((model) => [model, codeAgentPortForModel(model)])); +} + +export 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", + }; +} + +export function textInput(text: string): JsonValue[] { + return [{ type: "text", text, text_elements: [] }]; +} + +export function extractRecord(value: unknown): Record | null { + return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : null; +} + +export function extractString(value: unknown, key: string): string | null { + const record = extractRecord(value); + const field = record?.[key]; + return typeof field === "string" ? field : null; +} + +export function terminalStatus(value: string): import("../types").TerminalStatus { + if (value === "completed" || value === "interrupted" || value === "failed") return value; + return null; +} + +export function stripAnsi(text: string): string { + return text.replace(/\u001b\[[0-9;]*m/gu, ""); +} diff --git a/src/components/microservices/code-queue/src/code-agent/opencode.ts b/src/components/microservices/code-queue/src/code-agent/opencode.ts new file mode 100644 index 00000000..cb0727e2 --- /dev/null +++ b/src/components/microservices/code-queue/src/code-agent/opencode.ts @@ -0,0 +1,400 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import * as readline from "node:readline"; +import type { AppServerExit, CodexEventSummary, CodexRunResult, JsonValue, QueueTask, RuntimeConfig, TerminalStatus } from "../types"; +import type { ActiveRun, CodeAgentClient } from "./common"; +import { extractRecord, minimaxM27Model, normalizeCodeModel, stripAnsi } from "./common"; + +export interface OpenCodePortContext { + config: Pick; + activeRuns: Map; + addEvent: (task: QueueTask, event: CodexEventSummary) => void; + appendOutput: (task: QueueTask, channel: "system" | "assistant" | "reasoning" | "command" | "diff" | "tool" | "error", text: string, method?: string, itemId?: string, append?: boolean) => unknown; + buildDevContainerPlan: (providerId: string, body: Record) => { containerName: string; remoteOpencodeXdgDir: string }; + compactRetryTaskContext: (task: QueueTask) => string; + ensureTaskExecutionContainer: (task: QueueTask) => Promise; + judgeReasonForPrompt: (reason: string) => string; + logger: (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void; + nowIso: () => string; + openCodeFreshRecoveryPrompt: (task: QueueTask, prompt: string, reason: string) => string; + openCodeXdgEnv: (root?: string) => Record; + persistTaskState: (task: QueueTask) => void; + providerIsMain: (providerId: string) => boolean; + queueIdOf: (task: QueueTask) => string; + recordStringField: (record: Record | null, keys: string[], max?: number) => string; + remoteHostWorkdirForTask: (task: QueueTask) => string; + safePreview: (value: string, max?: number) => string; + shellQuote: (value: string) => string; + shutdownRequested: () => boolean; +} + +let context: OpenCodePortContext | null = null; + +export function configureOpenCodePort(runtimeContext: OpenCodePortContext): void { + context = runtimeContext; +} + +function ctx(): OpenCodePortContext { + if (context === null) throw new Error("opencode port is not configured"); + return context; +} + +interface OpenCodeTextParts { + reasoning: string; + assistant: string; +} + +function stripThinkBlocks(text: string): string { + return String(text || "").replace(/<(?:think|thinking)\b[^>]*>[\s\S]*?<\/(?:think|thinking)>/giu, "").trim(); +} + +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 = ctx().config.minimaxModel.trim() || "MiniMax-M2.7"; + return `minimax/${providerModel}`; +} + +function openCodeConfigContent(): string { + const providerModel = ctx().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: ctx().config.minimaxApiBase, + apiKey: "{env:MINIMAX_API_KEY}", + }, + models: { + [providerModel]: { + name: minimaxM27Model, + limit: { context: 200000, output: 16384 }, + }, + }, + }, + }, + }); +} + +function openCodeEnv(): NodeJS.ProcessEnv { + const xdgEnv = ctx().openCodeXdgEnv(); + return { + ...process.env, + ...xdgEnv, + MINIMAX_API_KEY: ctx().config.minimaxApiKey, + MINIMAX_API_BASE: ctx().config.minimaxApiBase, + MINIMAX_MODEL: ctx().config.minimaxModel, + OPENCODE_CONFIG_CONTENT: openCodeConfigContent(), + }; +} + +function shellJoin(args: string[]): string { + return args.map(ctx().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 = ctx().buildDevContainerPlan(task.providerId, { workdir: ctx().remoteHostWorkdirForTask(task) }); + const envExports = [ + ...Object.entries(ctx().openCodeXdgEnv(plan.remoteOpencodeXdgDir)).map(([key, value]) => `export ${key}=${ctx().shellQuote(value)}`), + `export MINIMAX_API_BASE=${ctx().shellQuote(ctx().config.minimaxApiBase)}`, + `export MINIMAX_MODEL=${ctx().shellQuote(ctx().config.minimaxModel)}`, + `export OPENCODE_CONFIG_CONTENT=${ctx().shellQuote(openCodeConfigContent())}`, + ].join("; "); + const inner = [ + "set -euo pipefail", + `mkdir -p ${ctx().shellQuote(task.cwd)}`, + `cd ${ctx().shellQuote(task.cwd)}`, + envExports, + `exec ${shellJoin(openCodeRunArgs(task, prompt))}`, + ].join("; "); + return `docker exec -i ${ctx().shellQuote(plan.containerName)} bash -lc ${ctx().shellQuote(inner)}`; +} + +function openCodeSessionIdFromRecord(record: Record, part: Record | null): string | null { + const top = ctx().recordStringField(record, ["sessionID", "sessionId", "session_id"]); + if (top.length > 0) return top; + const nested = ctx().recordStringField(part, ["sessionID", "sessionId", "session_id"]); + return nested.length > 0 ? nested : null; +} + +function openCodeEventSummary(record: Record): CodexEventSummary { + const part = extractRecord(record.part); + const type = ctx().recordStringField(record, ["type", "event", "name"]) || ctx().recordStringField(part, ["type"]) || "unknown"; + const text = ctx().recordStringField(part, ["text", "content", "delta", "message"]) || ctx().recordStringField(record, ["text", "content", "delta", "message"]); + const error = ctx().recordStringField(part, ["error", "message"]) || ctx().recordStringField(record, ["error", "message"]); + return { + at: ctx().nowIso(), + method: `opencode/${type}`, + itemType: ctx().recordStringField(part, ["type"]) || type, + status: ctx().recordStringField(part, ["status", "reason"]) || ctx().recordStringField(record, ["status", "reason"]) || undefined, + message: error.length > 0 ? ctx().safePreview(error, 600) : undefined, + textPreview: text.length > 0 ? ctx().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 = ctx().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: ctx().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) { + ctx().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); + ctx().addEvent(this.task, event); + + const sessionId = openCodeSessionIdFromRecord(parsed, part); + if (sessionId !== null) this.setSessionId(sessionId); + + const type = ctx().recordStringField(parsed, ["type", "event", "name"]) || ctx().recordStringField(part, ["type"]) || "unknown"; + const partType = ctx().recordStringField(part, ["type"]); + const itemId = ctx().recordStringField(part, ["id"]) || undefined; + if (type === "step_finish" || type === "step-finish" || partType === "step-finish") { + this.stepFinished = true; + return; + } + if (type === "step_start" || type === "step-start" || partType === "step-start") { + return; + } + const text = ctx().recordStringField(part, ["text", "content", "delta"]) || ctx().recordStringField(parsed, ["text", "content", "delta"]); + if (text.length > 0 && (type === "text" || partType === "text" || partType === "reasoning")) { + if (partType === "reasoning") ctx().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}`)) { + ctx().appendOutput(this.task, type.includes("command") || partType.includes("command") ? "command" : "tool", `${JSON.stringify(parsed)}\n`, "opencode/tool", itemId); + return; + } + if (/error|failed/iu.test(`${type} ${partType}`)) { + ctx().appendOutput(this.task, "error", `${ctx().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 = ctx().activeRuns.get(ctx().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; + ctx().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) ctx().appendOutput(this.task, "reasoning", `${parts.reasoning.trimEnd()}\n`, "opencode/reasoning", itemId, true); + const visible = parts.assistant.length > 0 ? parts.assistant : parts.reasoning.length > 0 ? "" : rawText.trim(); + if (visible.length === 0) return; + this.assistantChunks.push(visible); + this.finalResponse = stripThinkBlocks(this.assistantChunks.join("\n\n").trim()); + this.task.finalResponse = this.finalResponse; + ctx().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 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}: ${ctx().safePreview(stderr, 800)}` : base; +} + +function openCodeSessionMissing(result: CodexRunResult): boolean { + return /Session not found/iu.test(stripAnsi(`${result.terminalError ?? ""}\n${result.appServerExit.stderrTail}`)); +} + +export async function runOpenCodeTurn(task: QueueTask, prompt: string): Promise { + const attemptedSessionId = task.codexThreadId; + const first = await runOpenCodeTurnOnce(task, prompt); + if (attemptedSessionId === null || task.cancelRequested || ctx().shutdownRequested() || !openCodeSessionMissing(first)) return first; + const reason = first.terminalError ?? first.appServerExit.stderrTail; + ctx().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"); + ctx().logger("warn", "opencode_session_missing_recover_fresh", { taskId: task.id, sessionId: attemptedSessionId, reason: ctx().safePreview(stripAnsi(reason), 500) }); + task.codexThreadId = null; + task.activeTurnId = null; + ctx().persistTaskState(task); + return runOpenCodeTurnOnce(task, ctx().openCodeFreshRecoveryPrompt(task, prompt, reason)); +} + +async function runOpenCodeTurnOnce(task: QueueTask, prompt: string): Promise { + const queueId = ctx().queueIdOf(task); + if (ctx().config.minimaxApiKey.length === 0) { + const message = "MINIMAX_API_KEY is required for opencode model minimax-m2.7."; + ctx().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 ctx().ensureTaskExecutionContainer(task); + const app = new OpenCodeRunClient(task, prompt); + ctx().activeRuns.set(queueId, { taskId: task.id, queueId, app, port: "opencode", threadId: task.codexThreadId, turnId: app.runId }); + task.activeTurnId = null; + ctx().persistTaskState(task); + const activityWatchdog = setInterval(() => { + const idleMs = Date.now() - app.lastActivityAt; + if (idleMs < ctx().config.turnNoActivityTimeoutMs) return; + const message = `No OpenCode activity for ${Math.round(idleMs / 1000)}s; stopping opencode run so the existing session can retry.`; + ctx().appendOutput(task, "error", `${message}\n`, "turn/no-activity-watchdog"); + ctx().logger("warn", "opencode_no_activity_watchdog", { taskId: task.id, runId: app.runId, idleMs, timeoutMs: ctx().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(); + ctx().appendOutput( + task, + status === "completed" ? "system" : "error", + `opencode completed status=${status} exit=${exit.code ?? "null"} signal=${exit.signal ?? "null"}${stderr.length > 0 ? ` stderr=${ctx().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); + ctx().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 (ctx().activeRuns.get(queueId)?.app === app) ctx().activeRuns.delete(queueId); + app.stop(); + } +} diff --git a/src/components/microservices/code-queue/src/dev-containers.ts b/src/components/microservices/code-queue/src/dev-containers.ts new file mode 100644 index 00000000..83fd1e0c --- /dev/null +++ b/src/components/microservices/code-queue/src/dev-containers.ts @@ -0,0 +1,193 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import type { DevContainerCommandLog, DevContainerPlan, JsonValue, QueueTask, RuntimeConfig } from "./types"; + +export interface DevContainerContext { + config: Pick; + appendOutput: (task: QueueTask, channel: "system" | "assistant" | "reasoning" | "command" | "diff" | "tool" | "error", text: string, method?: string, itemId?: string, append?: boolean) => unknown; + buildDevContainerPlan: (providerId: string, body: Record) => DevContainerPlan; + containerTunnelStartScript: (plan: DevContainerPlan) => string; + devContainerEnsurePromises: Map>; + devContainerPingScript: (plan: DevContainerPlan) => string; + errorToJson: (error: unknown) => JsonValue; + extractRecord: (value: unknown) => Record | null; + jsonResponse: (body: unknown, status?: number) => Response; + logger: (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void; + masterKeyReadScript: (plan: DevContainerPlan) => string; + masterKeySetupScript: (plan: DevContainerPlan) => string; + masterProxyEvidenceScript: (plan: DevContainerPlan) => string; + masterProxyFinishScript: (plan: DevContainerPlan) => string; + masterProxyPrepareScript: (plan: DevContainerPlan) => string; + normalizeProviderId: (value: unknown) => string | null; + providerIsMain: (providerId: string) => boolean; + readJson: (req: Request) => Promise; + remoteCodexConfigInstallScript: (plan: DevContainerPlan) => string; + remoteCodexRuntimePrepareScript: (plan: DevContainerPlan) => string; + remoteContainerStartScript: (plan: DevContainerPlan, forceRecreate: boolean) => string; + remoteHostWorkdirForTask: (task: QueueTask) => string; + remoteKeyInstallScript: (plan: DevContainerPlan, privateKeyBase64: string) => string; + runCodeQueueSsh: (providerId: string, script: string, timeoutMs: number, name: string) => DevContainerCommandLog; + safePreview: (value: string, max?: number) => string; + shellQuote: (value: string) => string; + throwIfCommandFailed: (command: DevContainerCommandLog) => void; +} + +let context: DevContainerContext | null = null; + +export function configureDevContainers(runtimeContext: DevContainerContext): void { + context = runtimeContext; +} + +function ctx(): DevContainerContext { + if (context === null) throw new Error("dev-containers module is not configured"); + return context; +} + +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 = ctx().runCodeQueueSsh(targetProviderId, script, timeoutMs, name); + commands.push(command); + ctx().throwIfCommandFailed(command); + return command; + }; + run("main-server", ctx().masterKeySetupScript(plan), 45_000, "master-key-setup"); + const keyRead = run("main-server", ctx().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, ctx().remoteKeyInstallScript(plan, keyBase64), 30_000, "remote-key-install"); + run(plan.providerId, ctx().remoteCodexConfigInstallScript(plan), 30_000, "remote-codex-config-install"); + run(plan.providerId, ctx().remoteContainerStartScript(plan, options.forceRecreate), 60_000, "remote-container-start"); + run("main-server", ctx().masterProxyPrepareScript(plan), 30_000, "master-proxy-prepare"); + run(plan.providerId, ctx().containerTunnelStartScript(plan), 45_000, "remote-tunnel-start"); + run("main-server", ctx().masterProxyFinishScript(plan), 30_000, "master-proxy-finish"); + if (options.prepareRuntime) run(plan.providerId, ctx().remoteCodexRuntimePrepareScript(plan), 180_000, "remote-codex-runtime-prepare"); + const verification: Record = {}; + if (options.verifyPing) { + const before = run("main-server", ctx().masterProxyEvidenceScript(plan), 15_000, "master-proxy-evidence-before-ping"); + const ping = run(plan.providerId, ctx().devContainerPingScript(plan), 20_000, "remote-container-ping-google"); + const after = run("main-server", ctx().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 = ctx().safePreview(before.stdout, 2000); + verification.pingGoogleLog = ping.stdout; + verification.masterProxyEvidenceAfter = ctx().safePreview(after.stdout, 2000); + } + return { ok: true, providerId: plan.providerId, plan, commands, verification }; +} + +export async function ensureTaskExecutionContainer(task: QueueTask): Promise { + if (ctx().providerIsMain(task.providerId)) return; + const plan = ctx().buildDevContainerPlan(task.providerId, { workdir: ctx().remoteHostWorkdirForTask(task) }); + const existing = ctx().devContainerEnsurePromises.get(plan.providerId); + if (existing !== undefined) return existing; + const promise = (async () => { + ctx().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 }); + ctx().appendOutput(task, "system", `provider container ready provider=${plan.providerId} container=${plan.containerName} commands=${result.commands.length}\n`, "provider/container"); + ctx().logger("info", "task_provider_container_ready", { + taskId: task.id, + providerId: plan.providerId, + containerName: plan.containerName, + workdir: task.cwd, + hostWorkdir: plan.workdir, + }); + })(); + ctx().devContainerEnsurePromises.set(plan.providerId, promise); + try { + await promise; + } finally { + if (ctx().devContainerEnsurePromises.get(plan.providerId) === promise) ctx().devContainerEnsurePromises.delete(plan.providerId); + } +} + +export async function startDevContainer(req: Request, providerFromPath: string | null): Promise { + const body = ctx().extractRecord(await ctx().readJson(req)) ?? {}; + const providerFromBody = ctx().normalizeProviderId(body.providerId); + const providerId = ctx().normalizeProviderId(providerFromPath ?? "") ?? providerFromBody ?? ctx().normalizeProviderId(ctx().config.devContainerDefaultProviderId) ?? "D601"; + const plan = ctx().buildDevContainerPlan(providerId, body); + try { + const result = await startDevContainerPlan(plan, { forceRecreate: true, verifyPing: true, prepareRuntime: false }); + ctx().logger("info", "dev_container_started", { + providerId, + containerName: plan.containerName, + masterHost: plan.masterHost, + tunName: plan.tunName, + natChain: plan.natChain, + pingPreview: ctx().safePreview(String(result.verification?.pingGoogleLog ?? ""), 600), + natAfterPreview: ctx().safePreview(String(result.verification?.masterProxyEvidenceAfter ?? ""), 600), + }); + return ctx().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) { + ctx().logger("error", "dev_container_start_failed", { providerId, containerName: plan.containerName, error: ctx().errorToJson(error) }); + return ctx().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); + } +} + +export async function devContainerStatus(providerFromPath: string | null): Promise { + const providerId = ctx().normalizeProviderId(providerFromPath ?? "") ?? ctx().normalizeProviderId(ctx().config.devContainerDefaultProviderId) ?? "D601"; + const plan = ctx().buildDevContainerPlan(providerId, {}); + const commands: DevContainerCommandLog[] = []; + const statusScript = `set -euo pipefail +CONTAINER=${ctx().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(ctx().runCodeQueueSsh(providerId, statusScript, 15_000, "remote-container-status")); + commands.push(ctx().runCodeQueueSsh("main-server", ctx().masterProxyEvidenceScript(plan), 15_000, "master-proxy-status")); + return ctx().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, + }); +} diff --git a/src/components/microservices/code-queue/src/index.ts b/src/components/microservices/code-queue/src/index.ts index 56e590c1..98b08596 100644 --- a/src/components/microservices/code-queue/src/index.ts +++ b/src/components/microservices/code-queue/src/index.ts @@ -1,425 +1,137 @@ -import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process"; -import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, type Dirent } from "node:fs"; +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 +import { spawnSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs"; 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"; -type RunMode = "initial" | "retry"; -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; - port: number; - dataDir: string; - 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; - modelReasoningEfforts: Record; - 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; - notifyClaudeQqEnabled: boolean; - notifyClaudeQqBaseUrl: string; - notifyClaudeQqTargetType: "private" | "group"; - notifyClaudeQqUserId: string; - notifyClaudeQqGroupId: string; - notifyClaudeQqMaxResponseChars: number; - notifyClaudeQqTimeoutMs: number; - notifyClaudeQqSendAttempts: number; - notifyClaudeQqRetryIntervalMs: number; - 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; - maxAttempts?: number; - referenceTaskIds?: string[]; - basePrompt?: string; - referenceInjection?: ReferenceInjectionRecord | null; -} - -interface LiveOutput { - seq: number; - at: string; - channel: OutputChannel; - text: string; - method?: string; - itemId?: string; -} - -interface ArchivedLiveOutput extends LiveOutput { - op?: "set" | "append"; -} - -interface TranscriptLine { - seq: number; - at: string; - durationMs?: number; - kind: TranscriptKind; - title: string; - status?: string; - commandPreview?: string; - commandOmittedLines?: number; - bodyPreview?: string; - bodyOmittedLines?: number; - stdoutPreview?: string; - stdoutOmittedLines?: number; - stderrPreview?: string; - stderrOmittedLines?: number; - rawSeqs: number[]; - fullPrompt?: string; - fullPromptLines?: number; - fullPromptChars?: number; - foldedReferencePrompt?: boolean; -} - -interface CodexEventSummary { - at: string; - method: string; - itemType?: string; - status?: string; - message?: string; - textPreview?: string; -} - -interface AttemptSummary { - index: number; - mode: RunMode; - startedAt: string; - finishedAt: string; - terminalStatus: TerminalStatus; - transportClosedBeforeTerminal: boolean; - appServerExitCode: number | null; - appServerSignal: string | null; - error: string | null; - inputPrompt?: string; - inputPromptPreview?: string; - inputPromptChars?: number; - inputPromptLines?: number; - finalResponse?: string; - finalResponsePreview: string; - finalResponseChars?: number; - judge?: JudgeResult | null; - judgeAt?: string | null; - judgeSeq?: number | null; - feedbackPrompt?: string; - feedbackPromptPreview?: string; - feedbackPromptChars?: number; - feedbackPromptLines?: number; - feedbackPromptSource?: string; - feedbackPromptForAttempt?: number | null; - stderrTail: string; - outputStartSeq?: number | null; - outputEndSeq?: number | null; - errorCount?: number; -} - -interface JudgeResult { - decision: JudgeDecision; - confidence: number; - reason: string; - continuePrompt?: string; - source: "minimax" | "fallback"; - raw?: JsonValue; -} - -interface FeedbackPromptRecord { - text: string; - preview: string; - chars: number; - lines: number; - source: string; - forAttempt: number | null; - truncated: boolean; -} - -interface ParsedJudgeJson { - value: Record; - source: string; -} - -interface MiniMaxJudgeResponse { - rawText: string; - content: string; -} - -interface ReferenceInjectionSummaryItem { - round: number; - roundIndex: number; - taskId: string; - viaTaskId: string | null; - status: TaskStatus; - providerId: string; - model: string; - cwd: string; - createdAt: string; - updatedAt: string; - promptChars: number; - finalResponseChars: number; - finalResponseAt: string | null; - finalResponseSource: string; - referenceTaskIds: string[]; - cliHint: string; -} - -interface ReferenceInjectionRecord { - version: 2; - injectedAt: string; - basePrompt: string; - directReferenceTaskIds: string[]; - maxRounds: number | null; - truncated: boolean; - itemCount: number; - items: ReferenceInjectionSummaryItem[]; -} - -interface PromptHistoryItem { - seq: number; - at: string; - method: "turn/steer"; - text: string; -} - -interface QueueTask { - id: string; - queueId: string; - queueEnteredAt: string; - prompt: string; - basePrompt: string; - referenceTaskIds: string[]; - referenceInjection: ReferenceInjectionRecord | null; - providerId: string; - cwd: string; - model: string; - reasoningEffort: string | null; - maxAttempts: number; - status: TaskStatus; - createdAt: string; - updatedAt: string; - startedAt: string | null; - finishedAt: string | null; - readAt: string | null; - currentAttempt: number; - currentMode: RunMode | null; - codexThreadId: string | null; - activeTurnId: string | null; - finalResponse: string; - lastError: string | null; - lastJudge: JudgeResult | null; - judgeFailCount: number; - promptHistory: PromptHistoryItem[]; - output: LiveOutput[]; - events: CodexEventSummary[]; - attempts: AttemptSummary[]; - cancelRequested: boolean; - nextPrompt: string | null; - nextMode: RunMode | null; -} - -interface PersistedState { - version: 1; - updatedAt: string; - nextSeq: number; - queues: QueueRecord[]; - tasks: QueueTask[]; -} - -interface ClaudeQqNotificationItem { - id: string; - kind: string; - dedupKey: string; - target: string; - message: string; - createdAt: string; - updatedAt: string; - attempts: number; - nextAttemptAt: string; - lastError: string | null; - sentAt: string | null; -} - -interface ClaudeQqNotificationOutboxState { - version: 1; - updatedAt: string; - items: ClaudeQqNotificationItem[]; -} - -interface ClaudeQqNotificationRow { - id: string; - kind: string; - dedup_key: string; - target: string; - message: string; - created_at: Date | string; - updated_at: Date | string; - attempts: number; - next_attempt_at: Date | string; - last_error: string | null; - sent_at: Date | string | null; -} - -interface QueueRecord { - id: string; - name: string; - createdAt: string; - updatedAt: string; -} - -interface AppServerExit { - code: number | null; - signal: string | null; - stderrTail: string; -} - -interface CodexRunResult { - threadId: string | null; - turnId: string | null; - finalResponse: string; - terminalStatus: TerminalStatus; - terminalError: string | null; - transportClosedBeforeTerminal: boolean; - appServerExit: AppServerExit; - events: CodexEventSummary[]; -} - -interface SessionFileChange { - callId: string; - at: string; - name: string; - input: string; - output: string; -} - -interface SessionCommandOutput { - callId: string; - at: string; - stdout: string; - stderr: string; - output: string; - exitCode: number | null; -} - -interface JudgeProbeCase { - id: string; - prompt: string; - finalResponse: string; - expected: JudgeDecision; - expectedContinuePromptIncludes?: string[]; - expectedContinuePromptExcludes?: string[]; - expectedContinuePromptMaxChars?: number; - expectedContinuePromptMaxLines?: number; - terminalStatus: TerminalStatus; - cancelRequested?: boolean; - transportClosedBeforeTerminal?: boolean; - terminalError?: string | null; - stderrTail?: string; - outputs?: Array<{ channel: OutputChannel; text: string; method?: string }>; - 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: 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; -} +import type { + AttemptSummary, + CgroupMemoryUsage, + CodexEventSummary, + CodexRunResult, + JudgeProbeCase, + JudgeResult, + JsonValue, + LiveOutput, + OutputChannel, + PersistedState, + PromptHistoryItem, + QueueRecord, + QueuedStatusReason, + QueueTask, + QueueTaskRequest, + RunMode, + RuntimeConfig, + TaskStatus, +} from "./types"; +import { + codeAgentPortForModel, + defaultCodeModels, + extractRecord, + extractString, + normalizeCodeModel, + terminalStatus, +} from "./code-agent/common"; +import type { ActiveRun, ActiveRunSlotWaiter } from "./code-agent/common"; +import { configureCodexPort, runCodexTurn } from "./code-agent/codex"; +import { configureOpenCodePort, runOpenCodeTurn } from "./code-agent/opencode"; +import { + compactRetryTaskContext, + compactContinuationPromptTargetChars, + configureJudge, + explicitUserInterrupt, + judgeFailContinuationPrompt, + judgeReasonForPrompt, + judgeTask, + queueRecoveryRetryPrompt, + retryPrompt, +} from "./judge"; +import { defaultJudgeProbeCases } from "./judge-probes"; +import { injectCodeQueueEnvironmentHint, promptWithCodeQueueEnvironmentHint, userPromptForDisplay } from "./prompts"; +import { configureDevContainers, devContainerStatus, ensureTaskExecutionContainer, startDevContainer } from "./dev-containers"; +import { appendOutput, appendOutputArchive, configureTaskOutput, taskFullOutput } from "./task-output"; +import { + buildDevContainerPlan, + configureProviderRuntime, + containerTunnelStartScript, + defaultWorkdirForProvider, + devContainerPingScript, + executionProviderOptions, + masterKeyReadScript, + masterKeySetupScript, + masterProxyEvidenceScript, + masterProxyFinishScript, + masterProxyPrepareScript, + normalizeProviderId, + normalizeTaskProviderId, + providerIsMain, + remoteAppServerCommand, + remoteCodexConfigInstallScript, + remoteCodexRuntimePrepareScript, + remoteContainerStartScript, + remoteHostWorkdirForTask, + remoteKeyInstallScript, + resolveTaskCwd, + runCodeQueueSsh, + shellQuote, + throwIfCommandFailed, +} from "./provider-runtime"; +import { + armIdleNotification, + backfillClaudeQqTaskNotifications, + claudeQqNotificationItems, + claudeQqNotificationOutboxItemCount, + claudeQqNotificationOutboxStats, + configureNotifications, + drainClaudeQqNotificationOutbox, + loadClaudeQqNotificationOutboxFromDatabase, + maybeNotifyQueueIdle, + notificationTargetConfigured, + notificationTargetLabel, + notifyTaskTerminal, + persistClaudeQqNotificationOutbox, + scheduleClaudeQqNotificationDrain, +} from "./notifications"; +import { + configureQueueApi, + loadAllTasksForRead, + outputChunkResponse, + perQueueSummaries, + queueSummary, + queueSummaryForHealth, + queueSummaryForResponse, + taskMatchesSearch, + taskPageRows, + taskSearchTerms, + tasksOverviewResponse, + taskForListResponse, + transcriptChunkResponse, +} from "./queue-api"; +import { configureReferences, injectReferencedTaskContext, taskReferenceIds } from "./references"; +import { configureSelfTests, runJudgeInfraSelfTest, runQueueOrderingSelfTest, runReferenceInjectionSelfTest, runTracePortSelfTest } from "./self-tests"; +import { + configureTaskView, + formatCommandOutput, + lastAssistantMessage, + promptLineCount, + recordNumberField, + recordStringField, + safePreview, + setAttemptFeedbackPrompt, + setAttemptInputPrompt, + statsDaysFromUrl, + taskForMetaResponse, + taskForResponse, + taskStatisticsSummary, + taskSummaryResponse, + timestampMs, + nonNegativeElapsed, + taskTraceStepDetailResponse, + taskTraceStepsResponse, + taskTraceSummaryResponse, + taskPromptDetailResponse, +} from "./task-view"; type SqlClient = postgres.Sql; type SqlExecutor = postgres.Sql | postgres.TransactionSql; @@ -435,9 +147,6 @@ 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("code-queue", config.logFile); const state = emptyState(); @@ -453,29 +162,17 @@ let persistTimer: ReturnType | null = null; let persistDirty = false; let shutdownRequested = false; let serviceReady = false; -const transcriptCache = new Map(); -const codexSessionPathCache = new Map(); -const codexSessionFileChangeCache = new Map }>(); -const codexSessionCommandOutputCache = new Map }>(); -const outputArchiveSeededTasks = new Set(); const sql: SqlClient = postgres(config.databaseUrl, { max: 4, idle_timeout: 20, connect_timeout: 10, }); -let claudeQqNotificationOutbox = emptyClaudeQqNotificationOutbox(); let databaseReady = false; let databaseLastError: string | null = null; let databaseFlushTimer: ReturnType | null = null; let databaseFlushInFlight = false; const dirtyDatabaseTaskIds = new Set(); const dirtyDatabaseQueueIds = new Set(); -const sentTaskNotificationKeys = new Set(); -const inFlightTaskNotificationKeys = new Set(); -let idleNotificationSent = true; -let idleNotificationInFlight = false; -let claudeQqNotificationDrainTimer: ReturnType | null = null; -let claudeQqNotificationDrainInFlight = false; function envString(name: string, fallback: string): string { const value = process.env[name]; @@ -541,35 +238,10 @@ 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; @@ -642,7 +314,7 @@ function readConfig(): RuntimeConfig { 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))), + maxActiveQueues: Math.max(0, Math.min(32, envNumber("CODE_QUEUE_MAX_ACTIVE_QUEUES", 0))), devContainerMasterHost: envString("CODE_QUEUE_DEV_CONTAINER_MASTER_HOST", "74.48.78.17"), devContainerDefaultProviderId, devContainerImage: envString("CODE_QUEUE_DEV_CONTAINER_IMAGE", ""), @@ -953,107 +625,6 @@ function emptyState(): PersistedState { return { version: 1, updatedAt: at, nextSeq: 1, queues: [{ id: defaultQueueId, name: defaultQueueId, createdAt: at, updatedAt: at }], tasks: [] }; } -function emptyClaudeQqNotificationOutbox(): ClaudeQqNotificationOutboxState { - return { version: 1, updatedAt: nowIso(), items: [] }; -} - -function notificationItemFromRow(row: ClaudeQqNotificationRow): ClaudeQqNotificationItem { - const createdAt = taskTimestamp(String(row.created_at)) ?? nowIso(); - const updatedAt = taskTimestamp(String(row.updated_at)) ?? createdAt; - return { - id: row.id, - kind: row.kind || "unknown", - dedupKey: row.dedup_key || row.id, - target: row.target || "-", - message: row.message || "", - createdAt, - updatedAt, - attempts: Number.isInteger(row.attempts) && row.attempts >= 0 ? row.attempts : 0, - nextAttemptAt: taskTimestamp(String(row.next_attempt_at)) ?? updatedAt, - lastError: typeof row.last_error === "string" && row.last_error.length > 0 ? row.last_error : null, - sentAt: row.sent_at === null ? null : taskTimestamp(String(row.sent_at)), - }; -} - -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_code_queue_notifications - ORDER BY created_at ASC, id ASC - `; - claudeQqNotificationOutbox = { - version: 1, - updatedAt: nowIso(), - items: rows.map(notificationItemFromRow).filter((item) => item.id.length > 0 && item.message.length > 0), - }; - pruneClaudeQqNotificationOutbox(); -} - -async function upsertClaudeQqNotificationToDatabase(client: SqlExecutor, item: ClaudeQqNotificationItem): Promise { - await client` - INSERT INTO unidesk_code_queue_notifications ( - id, - kind, - dedup_key, - target, - message, - created_at, - updated_at, - attempts, - next_attempt_at, - last_error, - sent_at - ) VALUES ( - ${item.id}, - ${item.kind}, - ${item.dedupKey}, - ${item.target}, - ${item.message}, - ${taskTimestamp(item.createdAt) ?? nowIso()}, - ${taskTimestamp(item.updatedAt) ?? nowIso()}, - ${item.attempts}, - ${taskTimestamp(item.nextAttemptAt) ?? nowIso()}, - ${item.lastError}, - ${taskTimestamp(item.sentAt)} - ) - ON CONFLICT (id) DO UPDATE SET - kind = EXCLUDED.kind, - dedup_key = EXCLUDED.dedup_key, - target = EXCLUDED.target, - message = EXCLUDED.message, - updated_at = EXCLUDED.updated_at, - attempts = EXCLUDED.attempts, - next_attempt_at = EXCLUDED.next_attempt_at, - last_error = EXCLUDED.last_error, - sent_at = EXCLUDED.sent_at - `; -} - -async function persistClaudeQqNotificationItem(item: ClaudeQqNotificationItem): Promise { - if (!databaseReady) throw new Error("PostgreSQL is not ready for ClaudeQQ notification outbox"); - claudeQqNotificationOutbox.updatedAt = nowIso(); - const deletedIds = pruneClaudeQqNotificationOutbox(); - 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_code_queue_notifications WHERE id = ${id}`; - }); - if (stillPresent && !claudeQqNotificationOutbox.items.some((candidate) => candidate.id === item.id)) { - claudeQqNotificationOutbox.items.push(item); - pruneClaudeQqNotificationOutbox(); - } -} - -async function persistClaudeQqNotificationOutbox(): Promise { - if (!databaseReady) throw new Error("PostgreSQL is not ready for ClaudeQQ notification outbox"); - claudeQqNotificationOutbox.updatedAt = nowIso(); - 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_code_queue_notifications WHERE id = ${id}`; - }); -} - function ensureQueue(queueId: string, queueName?: unknown): QueueRecord { const id = normalizeQueueId(queueId); const existing = state.queues.find((queue) => queue.id === id); @@ -1278,6 +849,7 @@ async function upsertTaskToDatabase(client: SqlExecutor, task: QueueTask): Promi updated_at, started_at, finished_at, + read_at, last_error, last_judge, output_count, @@ -1306,6 +878,7 @@ async function upsertTaskToDatabase(client: SqlExecutor, task: QueueTask): Promi ${taskTimestamp(task.updatedAt) ?? nowIso()}, ${taskTimestamp(task.startedAt)}, ${taskTimestamp(task.finishedAt)}, + ${taskTimestamp(task.readAt)}, ${task.lastError}, ${task.lastJudge === null ? null : client.json(task.lastJudge as unknown as postgres.JSONValue)}, ${task.output.length}, @@ -1334,6 +907,7 @@ async function upsertTaskToDatabase(client: SqlExecutor, task: QueueTask): Promi updated_at = EXCLUDED.updated_at, started_at = EXCLUDED.started_at, finished_at = EXCLUDED.finished_at, + read_at = EXCLUDED.read_at, last_error = EXCLUDED.last_error, last_judge = EXCLUDED.last_judge, output_count = EXCLUDED.output_count, @@ -1369,6 +943,10 @@ interface DatabaseTaskRow { task_json: unknown; } +interface DatabaseTaskIdRow { + id: string; +} + function normalizeDatabaseTaskRows(rows: DatabaseTaskRow[], source: string): QueueTask[] { const tasks: QueueTask[] = []; for (const row of rows) { @@ -1450,6 +1028,23 @@ async function loadTasksFromDatabase(where: "all" | "hot" = "all"): Promise { + const ids = Array.from(new Set(taskIds.map((id) => id.trim()).filter(Boolean))); + if (ids.length === 0) return []; + const rows = await sql` + SELECT id, updated_at, task_json + FROM ( + SELECT + id, + updated_at, + task_json - 'output' - 'events' - 'attempts' - 'promptHistory' AS task_json + FROM unidesk_code_queue_tasks + WHERE id IN ${sql(ids)} + ) AS lite_tasks + `; + return normalizeDatabaseTaskRows(rows, "by_ids"); +} + async function loadTaskFromDatabase(taskId: string): Promise { const rows = await sql` SELECT id, updated_at, task_json @@ -1514,6 +1109,47 @@ async function loadTaskFromDatabase(taskId: string): Promise { return normalizeDatabaseTaskRows(rows, "single")[0] ?? null; } +async function warmDatabaseOverviewQueries(): Promise { + if (!databaseReady) return; + const started = performance.now(); + try { + const [recentRows, unreadRows, activeRows] = await Promise.all([ + sql` + SELECT id + FROM unidesk_code_queue_tasks + ORDER BY created_at DESC, id DESC + LIMIT 48 + `, + sql` + SELECT id + FROM unidesk_code_queue_tasks + WHERE read_at IS NULL + AND status IN ('succeeded', 'failed', 'canceled') + ORDER BY updated_at DESC, id DESC + LIMIT 100 + `, + sql` + SELECT id + FROM unidesk_code_queue_tasks + WHERE status IN ('running', 'judging', 'retry_wait') + ORDER BY updated_at DESC, id DESC + LIMIT 48 + `, + ]); + const ids = Array.from(new Set([...unreadRows, ...activeRows, ...recentRows].map((row) => row.id))); + await Promise.all([ + queueSummaryForHealth(false), + loadTasksFromDatabaseByIds(ids), + ]); + logger("info", "database_overview_warm_complete", { + taskIdCount: ids.length, + durationMs: Math.round(performance.now() - started), + }); + } catch (error) { + logger("warn", "database_overview_warm_failed", { error: errorToJson(error) }); + } +} + function rememberHotTask(task: QueueTask): QueueTask { const existing = findTask(task.id); if (existing !== null) return existing; @@ -1599,6 +1235,7 @@ async function initDatabasePersistence(): Promise { updated_at TIMESTAMPTZ NOT NULL, started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ, + read_at TIMESTAMPTZ, last_error TEXT, last_judge JSONB, output_count INTEGER NOT NULL DEFAULT 0, @@ -1636,11 +1273,22 @@ async function initDatabasePersistence(): Promise { 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_tasks ADD COLUMN IF NOT EXISTS read_at TIMESTAMPTZ`; + await sql` + UPDATE unidesk_code_queue_tasks + SET read_at = NULLIF(task_json->>'readAt', '')::timestamptz + WHERE read_at IS NULL + AND status IN ('succeeded', 'failed', 'canceled') + AND COALESCE(task_json->>'readAt', '') <> '' + AND (task_json->>'readAt') ~ '^\\d{4}-\\d{2}-\\d{2}T' + `; 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_queue_created ON unidesk_code_queue_tasks(queue_id, created_at DESC, id DESC)`; + await sql`CREATE INDEX IF NOT EXISTS idx_unidesk_code_queue_tasks_unread_terminal ON unidesk_code_queue_tasks(queue_id, updated_at DESC) WHERE read_at IS NULL AND status IN ('succeeded', 'failed', 'canceled')`; 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)`; @@ -1689,11 +1337,12 @@ async function initDatabasePersistence(): Promise { await flushDirtyTasksToDatabase(true); runGarbageCollection(); await persistClaudeQqNotificationOutbox(); + await warmDatabaseOverviewQueries(); logger("info", "database_persistence_init_complete", { databaseTaskCount: Number(countRows[0]?.count ?? hotTasks.length), hotTaskCount: state.tasks.length, databaseQueueCount: queueRows.length, - databaseNotificationCount: claudeQqNotificationOutbox.items.length, + databaseNotificationCount: claudeQqNotificationOutboxItemCount(), taskCount: state.tasks.length, queueCount: state.queues.length, }); @@ -1743,599 +1392,6 @@ 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; -} - -function prefixPreview(value: string, max = 900): string { - const trimmed = value.trim(); - return trimmed.length > max ? `${trimmed.slice(0, max)}...` : trimmed; -} - -function linePreview(text: string, maxLines: number, maxChars: number): { text: string; omittedLines: number } { - const clean = text.replace(/\u001b\[[0-9;]*m/gu, "").trimEnd(); - if (clean.length === 0) return { text: "", omittedLines: 0 }; - const lines = clean.split(/\r?\n/u); - const kept: string[] = []; - let chars = 0; - for (const line of lines) { - if (kept.length >= maxLines || chars + line.length > maxChars) break; - kept.push(line); - chars += line.length + 1; - } - return { text: kept.join("\n"), omittedLines: Math.max(0, lines.length - kept.length) }; -} - -function completeTraceText(text: string): { text: string; omittedLines: number } { - return { text: text.replace(/\u001b\[[0-9;]*m/gu, "").trimEnd(), omittedLines: 0 }; -} - -function editedOutputPreview(text: string): { text: string; omittedLines: number } { - return linePreview(compactTranscriptBody(text), 120, 24_000); -} - -function codexSessionDateDir(value: string | null | undefined): string | null { - if (typeof value !== "string" || value.length === 0) return null; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return null; - return resolve( - config.codexHome, - "sessions", - String(date.getUTCFullYear()), - String(date.getUTCMonth() + 1).padStart(2, "0"), - String(date.getUTCDate()).padStart(2, "0"), - ); -} - -function codexSessionSignature(path: string): string | null { - try { - const stat = statSync(path); - return `${stat.size}:${Math.floor(stat.mtimeMs)}`; - } catch { - return null; - } -} - -function findCodexSessionFile(task: QueueTask): string | null { - const threadId = task.codexThreadId; - if (threadId === null || threadId.length === 0) return null; - const cached = codexSessionPathCache.get(threadId); - if (cached !== undefined && existsSync(cached)) return cached; - - const roots = Array.from(new Set([ - codexSessionDateDir(task.startedAt), - codexSessionDateDir(task.createdAt), - codexSessionDateDir(task.updatedAt), - resolve(config.codexHome, "sessions"), - ].filter((value): value is string => value !== null && existsSync(value)))); - const matches: string[] = []; - let scanned = 0; - const scan = (dir: string, depth: number): void => { - if (depth < 0 || scanned > 4000) return; - scanned += 1; - let entries: Dirent[]; - try { - entries = readdirSync(dir, { withFileTypes: true }); - } catch { - return; - } - for (const entry of entries) { - const path = resolve(dir, entry.name); - if (entry.isDirectory()) { - scan(path, depth - 1); - } else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(threadId)) { - matches.push(path); - } - } - }; - for (const root of roots) scan(root, root.endsWith("/sessions") ? 4 : 1); - matches.sort((left, right) => (statMtimeMs(right) - statMtimeMs(left)) || right.localeCompare(left)); - const match = matches[0] ?? null; - if (match !== null) codexSessionPathCache.set(threadId, match); - return match; -} - -function statMtimeMs(path: string): number { - try { - return statSync(path).mtimeMs; - } catch { - return 0; - } -} - -function parseCodexToolOutputText(raw: string): string { - const text = String(raw || ""); - try { - const parsed = JSON.parse(text) as Record; - if (typeof parsed.output === "string") return parsed.output; - } catch { - // Keep the raw tool output if it is not the JSON wrapper used by Codex. - } - return text; -} - -function recordStringField(record: Record | null, keys: string[]): string { - if (record === null) return ""; - for (const key of keys) { - const value = record[key]; - if (typeof value === "string") return value; - } - return ""; -} - -function recordNumberField(record: Record | null, keys: string[]): number | null { - if (record === null) return null; - for (const key of keys) { - const value = Number(record[key]); - if (Number.isFinite(value)) return value; - } - return null; -} - -function parseToolOutputStreams(raw: string): { stdout: string; stderr: string; output: string; exitCode: number | null } { - const text = String(raw || ""); - try { - const parsed = JSON.parse(text) as unknown; - const record = extractRecord(parsed); - const stdout = recordStringField(record, ["stdout", "stdoutText"]); - const stderr = recordStringField(record, ["stderr", "stderrText"]); - const output = recordStringField(record, ["output", "text"]); - const exitCode = recordNumberField(record, ["exitCode", "code", "status"]); - if (stdout.length > 0 || stderr.length > 0 || output.length > 0) { - return { stdout: stdout || output, stderr, output: output || [stdout, stderr].filter(Boolean).join("\n"), exitCode }; - } - } catch { - // Not a JSON wrapper; parse the Codex CLI tool-result envelope below. - } - - const outputMatch = /(?:^|\n)Output:\n([\s\S]*)$/u.exec(text); - const exitMatch = /Process exited with code\s+(-?\d+)/u.exec(text); - if (outputMatch !== null) { - const output = (outputMatch[1] ?? "").trimEnd(); - return { - stdout: output, - stderr: "", - output, - exitCode: exitMatch === null ? null : Number(exitMatch[1]), - }; - } - return { stdout: text, stderr: "", output: text, exitCode: exitMatch === null ? null : Number(exitMatch[1]) }; -} - -function formatCommandOutput(output: SessionCommandOutput | null | undefined): string { - if (output === null || output === undefined) return ""; - const parts: string[] = []; - const stdout = output.stdout.trimEnd(); - const stderr = output.stderr.trimEnd(); - if (stdout.length > 0) parts.push(stderr.length > 0 ? `[stdout]\n${stdout}` : stdout); - if (stderr.length > 0) parts.push(`[stderr]\n${stderr}`); - if (parts.length > 0) return parts.join("\n"); - return output.output.trimEnd(); -} - -function addCommandOutputStreams(line: TranscriptLine, output: SessionCommandOutput | null | undefined, fullText: boolean): TranscriptLine { - if (output === null || output === undefined) return line; - const stdout = output.stdout.trimEnd(); - const stderr = output.stderr.trimEnd(); - if (stdout.length > 0) { - const preview = fullText ? completeTraceText(stdout) : outputPreview(stdout); - if (preview.text.length > 0) { - line.stdoutPreview = preview.text; - line.stdoutOmittedLines = preview.omittedLines || undefined; - } - } - if (stderr.length > 0) { - const preview = fullText ? completeTraceText(stderr) : outputPreview(stderr); - if (preview.text.length > 0) { - line.stderrPreview = preview.text; - line.stderrOmittedLines = preview.omittedLines || undefined; - } - } - return line; -} - -function parseCodexSessionCommandOutputs(path: string): Map { - const signature = codexSessionSignature(path); - const cached = codexSessionCommandOutputCache.get(path); - if (signature !== null && cached?.signature === signature) return cached.outputs; - - const outputs = new Map(); - try { - const text = readFileSync(path, "utf8"); - for (const line of text.split(/\r?\n/u)) { - if (line.trim().length === 0) continue; - let record: Record; - try { - record = JSON.parse(line) as Record; - } catch { - continue; - } - const payload = extractRecord(record.payload); - if (payload === null) continue; - const type = String(payload.type || ""); - if (type !== "function_call_output" && type !== "custom_tool_call_output") continue; - const callId = typeof payload.call_id === "string" ? payload.call_id : ""; - if (callId.length === 0) continue; - const parsed = parseToolOutputStreams(String(payload.output || "")); - if (parsed.output.trim().length === 0 && parsed.stdout.trim().length === 0 && parsed.stderr.trim().length === 0) continue; - outputs.set(callId, { - callId, - at: typeof record.timestamp === "string" ? record.timestamp : "", - stdout: parsed.stdout, - stderr: parsed.stderr, - output: parsed.output, - exitCode: parsed.exitCode, - }); - } - } catch (error) { - logger("warn", "codex_session_command_output_parse_failed", { path, error: errorToJson(error) }); - } - if (signature !== null) codexSessionCommandOutputCache.set(path, { signature, outputs }); - if (codexSessionCommandOutputCache.size > 40) { - const firstKey = codexSessionCommandOutputCache.keys().next().value; - if (typeof firstKey === "string") codexSessionCommandOutputCache.delete(firstKey); - } - return outputs; -} - -function codexSessionCommandOutputsByCallId(task: QueueTask): Map { - const path = findCodexSessionFile(task); - return path === null ? new Map() : parseCodexSessionCommandOutputs(path); -} - -function isInlineFileChangeInput(name: string, input: string): boolean { - return name === "apply_patch" && /^\*\*\* Begin Patch/mu.test(input); -} - -function trimmedPatchForTrace(input: string): string { - const normalized = input.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n").trimEnd(); - const maxChars = 120_000; - if (normalized.length <= maxChars) return normalized; - return `${normalized.slice(0, maxChars)}\n...[patch content truncated: ${normalized.length - maxChars} chars omitted]`; -} - -function parseCodexSessionFileChanges(path: string): Map { - const signature = codexSessionSignature(path); - const cached = codexSessionFileChangeCache.get(path); - if (signature !== null && cached?.signature === signature) return cached.changes; - - const changes = new Map(); - try { - const text = readFileSync(path, "utf8"); - for (const line of text.split(/\r?\n/u)) { - if (line.trim().length === 0) continue; - let record: Record; - try { - record = JSON.parse(line) as Record; - } catch { - continue; - } - const payload = extractRecord(record.payload); - if (payload === null) continue; - const type = String(payload.type || ""); - const callId = typeof payload.call_id === "string" ? payload.call_id : ""; - if (callId.length === 0) continue; - if (type === "custom_tool_call") { - const name = String(payload.name || ""); - const input = typeof payload.input === "string" ? payload.input : ""; - if (!isInlineFileChangeInput(name, input)) continue; - changes.set(callId, { - callId, - at: typeof record.timestamp === "string" ? record.timestamp : "", - name, - input: trimmedPatchForTrace(input), - output: "", - }); - continue; - } - if (type === "custom_tool_call_output") { - const existing = changes.get(callId); - if (existing === undefined) continue; - existing.output = parseCodexToolOutputText(String(payload.output || "")); - } - } - } catch (error) { - logger("warn", "codex_session_file_change_parse_failed", { path, error: errorToJson(error) }); - } - if (signature !== null) codexSessionFileChangeCache.set(path, { signature, changes }); - if (codexSessionFileChangeCache.size > 40) { - const firstKey = codexSessionFileChangeCache.keys().next().value; - if (typeof firstKey === "string") codexSessionFileChangeCache.delete(firstKey); - } - return changes; -} - -function codexSessionFileChangesByCallId(task: QueueTask): Map { - const path = findCodexSessionFile(task); - return path === null ? new Map() : parseCodexSessionFileChanges(path); -} - -function fileChangeTextWithInlinePatch(item: LiveOutput, changes: Map): string { - if (item.method !== "item/fileChange/outputDelta" || typeof item.itemId !== "string") return item.text; - const change = changes.get(item.itemId); - if (change === undefined || change.input.length === 0 || item.text.includes(change.input)) return item.text; - return `${item.text.trimEnd()}\n\n${change.input}\n`; -} - -function compactNoisyLine(line: string): string { - const compact = line.replace(/\s+/gu, " ").trimEnd(); - const hasEncodedBlob = /[A-Za-z0-9+/=]{220,}/u.test(compact); - const hasSshWrapper = compact.includes("UNIDESK_SSH_TOOL_DIR") || compact.includes("apply_patch") || compact.includes("base64 -d"); - if (compact.length > 420 && (hasEncodedBlob || hasSshWrapper)) { - return `${compact.slice(0, 220)} ... [omitted noisy wrapper, ${compact.length - 220} chars]`; - } - return compact.length > 1200 ? `${compact.slice(0, 900)} ... [omitted ${compact.length - 900} chars]` : line; -} - -function compactTranscriptBody(text: string): string { - return text.split(/\r?\n/u).map(compactNoisyLine).join("\n"); -} - -function parseCommandLine(text: string): { command: string; status?: string } | null { - const match = text.match(/^item\/(?:started|completed):\s+([\s\S]*?)\s+status=([A-Za-z0-9_-]+)/u); - if (match === null) return null; - return { command: match[1]?.trim() ?? "", status: match[2] }; -} - -function extractOuterQuotedShellArg(text: string): { body: string; trailing: string } | null { - const quote = text[0]; - if (quote !== "\"" && quote !== "'") return null; - let escaped = false; - for (let index = 1; index < text.length; index += 1) { - const char = text[index] ?? ""; - if (quote === "\"" && escaped) { - escaped = false; - continue; - } - if (quote === "\"" && char === "\\") { - escaped = true; - continue; - } - if (char === quote) return { body: text.slice(1, index), trailing: text.slice(index + 1).trim() }; - } - return null; -} - -function decodeShellDoubleQuoted(text: string): string { - return text - .replace(/\\(["\\$`])/gu, "$1") - .replace(/\\\r?\n/gu, ""); -} - -function displayCommand(command: string): string { - const normalized = command.trim().replace(/\\n/gu, "\n"); - const match = normalized.match(/^(?:\/usr\/bin\/env\s+)?(?:\/(?:usr\/)?bin\/)?(?:bash|sh)\s+-lc\s+([\s\S]+)$/u); - if (match === null) return normalized; - const shellText = (match[1] ?? "").trim(); - const shellArg = extractOuterQuotedShellArg(shellText); - if (shellArg === null) return (match[1] ?? normalized).trim(); - const quote = shellText[0]; - const body = quote === "\"" ? decodeShellDoubleQuoted(shellArg.body) : shellArg.body; - return shellArg.trailing.length > 0 ? `${body} ${shellArg.trailing}` : body; -} - -function shortCommandTitle(command: string): string { - const firstLine = displayCommand(command).split(/\r?\n/u).find((line) => line.trim().length > 0)?.trim() ?? command.trim(); - return safePreview(firstLine, 180); -} - -function commandPreview(command: string): { text: string; omittedLines: number } { - return linePreview(compactTranscriptBody(displayCommand(command)), 10, 8000); -} - -function outputPreview(text: string): { text: string; omittedLines: number } { - return linePreview(compactTranscriptBody(text), 4, 1600); -} - -function fullMessageBody(text: string): { text: string; omittedLines: number } { - return linePreview(text.replace(/\u001b\[[0-9;]*m/gu, "").trimEnd(), 12, 4000); -} - -function timestampMs(value: string | null | undefined): number | null { - if (typeof value !== "string" || value.length === 0) return null; - const time = Date.parse(value); - return Number.isFinite(time) ? time : null; -} - -function nonNegativeElapsed(startMs: number | null, endMs: number | null): number | null { - if (startMs === null || endMs === null) return null; - return Math.max(0, endMs - startMs); -} - -function taskTiming(task: QueueTask): JsonValue { - const nowMs = Date.now(); - const createdMs = timestampMs(task.createdAt); - const startedMs = timestampMs(task.startedAt); - const finishedMs = timestampMs(task.finishedAt); - const updatedMs = timestampMs(task.updatedAt); - const terminal = task.status === "succeeded" || task.status === "failed" || task.status === "canceled"; - const effectiveEndMs = finishedMs ?? (terminal ? updatedMs : nowMs); - return { - queueWaitMs: nonNegativeElapsed(createdMs, startedMs ?? (terminal ? effectiveEndMs : nowMs)), - durationMs: nonNegativeElapsed(startedMs, effectiveEndMs), - totalElapsedMs: nonNegativeElapsed(createdMs, effectiveEndMs), - effectiveEndAt: finishedMs !== null ? task.finishedAt : terminal ? task.updatedAt : null, - running: !terminal && startedMs !== null, - }; -} - -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 }; - return { - seq, - at, - kind, - title, - status, - commandPreview: commandInfo.text || undefined, - commandOmittedLines: commandInfo.omittedLines || undefined, - bodyPreview: bodyInfo.text || undefined, - bodyOmittedLines: bodyInfo.omittedLines || undefined, - rawSeqs, - }; -} - function commandPath(command: string): string | null { const result = spawnSync("sh", ["-lc", `command -v ${shellQuote(command)}`], { encoding: "utf8", timeout: 2_000 }); if (result.status !== 0) return null; @@ -2531,134 +1587,6 @@ function createTask(request: QueueTaskRequest): QueueTask { }; } -function taskOutputArchivePath(taskId: string): string { - return resolve(config.outputArchiveDir, `${taskId}.jsonl`); -} - -function serializeArchivedOutput(output: LiveOutput, op: ArchivedLiveOutput["op"], text: string): string { - const record: ArchivedLiveOutput = { - seq: output.seq, - at: output.at, - channel: output.channel, - text, - ...(output.method === undefined ? {} : { method: output.method }), - ...(output.itemId === undefined ? {} : { itemId: output.itemId }), - op, - }; - return `${JSON.stringify(record)}\n`; -} - -function ensureTaskOutputArchiveSeeded(task: QueueTask): void { - if (outputArchiveSeededTasks.has(task.id)) return; - mkdirSync(config.outputArchiveDir, { recursive: true }); - const path = taskOutputArchivePath(task.id); - if (!existsSync(path) && task.output.length > 0) { - const seed = task.output.map((output) => serializeArchivedOutput(output, "set", output.text)).join(""); - appendFileSync(path, seed, "utf8"); - } - outputArchiveSeededTasks.add(task.id); -} - -function appendOutputArchive(task: QueueTask, output: LiveOutput, op: ArchivedLiveOutput["op"], text: string): void { - try { - mkdirSync(config.outputArchiveDir, { recursive: true }); - appendFileSync(taskOutputArchivePath(task.id), serializeArchivedOutput(output, op, text), "utf8"); - } catch (error) { - logger("error", "codex_output_archive_write_failed", { taskId: task.id, error: errorToJson(error) }); - } -} - -function archiveRecordToOutput(value: unknown): ArchivedLiveOutput | null { - if (typeof value !== "object" || value === null || Array.isArray(value)) return null; - const record = value as Record; - const seq = Number(record.seq); - if (!Number.isFinite(seq)) return null; - const channel = String(record.channel || "system") as OutputChannel; - if (!["system", "user", "assistant", "reasoning", "command", "diff", "tool", "error"].includes(channel)) return null; - return { - seq, - at: typeof record.at === "string" ? record.at : nowIso(), - channel, - text: typeof record.text === "string" ? record.text : "", - ...(typeof record.method === "string" ? { method: record.method } : {}), - ...(typeof record.itemId === "string" ? { itemId: record.itemId } : {}), - op: record.op === "append" ? "append" : "set", - }; -} - -function archivedTaskOutput(task: QueueTask): LiveOutput[] { - const path = taskOutputArchivePath(task.id); - if (!existsSync(path)) return []; - const bySeq = new Map(); - try { - const text = readFileSync(path, "utf8"); - for (const line of text.split(/\r?\n/u)) { - if (line.trim().length === 0) continue; - const record = archiveRecordToOutput(JSON.parse(line) as unknown); - if (record === null) continue; - const existing = bySeq.get(record.seq); - if (record.op === "append" && existing !== undefined) { - existing.text += record.text; - existing.at = record.at; - existing.channel = record.channel; - existing.method = record.method; - existing.itemId = record.itemId; - } else { - const { op: _op, ...output } = record; - bySeq.set(record.seq, output); - } - } - } catch (error) { - logger("warn", "codex_output_archive_read_failed", { taskId: task.id, error: errorToJson(error) }); - return []; - } - return Array.from(bySeq.values()).sort((left, right) => Number(left.seq) - Number(right.seq)); -} - -function taskFullOutput(task: QueueTask): LiveOutput[] { - const bySeq = new Map(); - for (const output of archivedTaskOutput(task)) bySeq.set(output.seq, output); - for (const output of task.output) bySeq.set(output.seq, output); - return Array.from(bySeq.values()).sort((left, right) => Number(left.seq) - Number(right.seq)); -} - -function outputArchiveSignature(task: QueueTask): string { - try { - const stat = statSync(taskOutputArchivePath(task.id)); - return `${stat.size}:${Math.floor(stat.mtimeMs)}`; - } catch { - return "none"; - } -} - -function appendOutput(task: QueueTask, channel: OutputChannel, text: string, method?: string, itemId?: string, append = false): LiveOutput | null { - if (text.length === 0) return null; - try { - ensureTaskOutputArchiveSeeded(task); - } catch (error) { - logger("error", "codex_output_archive_seed_failed", { taskId: task.id, error: errorToJson(error) }); - } - const last = task.output[task.output.length - 1]; - let output: LiveOutput; - let archiveOp: ArchivedLiveOutput["op"] = "set"; - let archiveText = text; - if (append && last !== undefined && last.channel === channel && last.itemId === itemId && last.method === method && last.text.length < 24_000) { - last.text += text; - last.at = nowIso(); - output = last; - archiveOp = "append"; - } else { - output = { seq: state.nextSeq++, at: nowIso(), channel, text, method, itemId }; - task.output.push(output); - } - appendOutputArchive(task, output, archiveOp, archiveText); - if (config.maxInMemoryOutputRecords > 0 && task.output.length > config.maxInMemoryOutputRecords) task.output.splice(0, task.output.length - config.maxInMemoryOutputRecords); - task.updatedAt = nowIso(); - markTaskDirty(task.id); - schedulePersistState(); - return output; -} - function appendPromptHistory(task: QueueTask, output: LiveOutput | null, method: PromptHistoryItem["method"], text: string): void { if (output === null) return; task.promptHistory = mergePromptHistory([...(Array.isArray(task.promptHistory) ? task.promptHistory : []), { @@ -2677,1350 +1605,6 @@ function addEvent(task: QueueTask, event: CodexEventSummary): void { markTaskDirty(task.id); } -function commandKind(command: string): TranscriptKind { - if (/\b(apply_patch|git apply|cat >|tee .*<<|sed -i|python3? .*write_text)\b/u.test(command)) return "edited"; - if (/\b(rg|grep|find|ls|cat|sed -n|tail|head|git status|git diff|ps)\b/u.test(command)) return "explored"; - return "ran"; -} - -function commandKindLabel(kind: TranscriptKind): string { - if (kind === "edited") return "Edited"; - if (kind === "explored") return "Explored"; - 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; -} - -function promptSnapshot(text: string, maxPreviewChars = 1200): { text: string; preview: string; chars: number; lines: number; truncated: boolean } { - const normalized = text.trimEnd(); - const preview = safePreview(normalized, maxPreviewChars); - return { - text: normalized, - preview, - chars: normalized.length, - lines: promptLineCount(normalized), - truncated: preview.length < normalized.length, - }; -} - -function setAttemptInputPrompt(attempt: AttemptSummary, prompt: string): void { - const snapshot = promptSnapshot(prompt, 1200); - if (snapshot.chars === 0) return; - attempt.inputPrompt = snapshot.text; - attempt.inputPromptPreview = snapshot.preview; - attempt.inputPromptChars = snapshot.chars; - attempt.inputPromptLines = snapshot.lines; -} - -function setAttemptFeedbackPrompt(attempt: AttemptSummary | undefined, prompt: string, source: string, forAttempt: number | null): void { - if (attempt === undefined) return; - const snapshot = promptSnapshot(prompt, 1600); - if (snapshot.chars === 0) return; - attempt.feedbackPrompt = snapshot.text; - attempt.feedbackPromptPreview = snapshot.preview; - attempt.feedbackPromptChars = snapshot.chars; - attempt.feedbackPromptLines = snapshot.lines; - attempt.feedbackPromptSource = source; - attempt.feedbackPromptForAttempt = forAttempt; -} - -function taskInitialPromptLine(task: QueueTask, fullText = false): TranscriptLine | null { - const prompt = (task.basePrompt || userPromptForDisplay(task.prompt)).trimEnd(); - if (prompt.length === 0) return null; - const line = transcriptLine("message", task.createdAt, 0.5, "Submitted prompt", [], prompt, "", "enqueue", fullText); - const fullPrompt = task.prompt.trimEnd(); - if (fullText && fullPrompt.length > 0 && fullPrompt !== prompt) { - line.fullPrompt = fullPrompt; - line.fullPromptLines = promptLineCount(fullPrompt); - line.fullPromptChars = fullPrompt.length; - line.foldedReferencePrompt = true; - } - return line; -} - -function promptHistoryTranscriptLines(task: QueueTask, fullText = false): TranscriptLine[] { - return mergePromptHistory([...(Array.isArray(task.promptHistory) ? task.promptHistory : []), ...outputPromptHistory(task)]) - .map((item) => transcriptLine("message", item.at, item.seq, "Steer prompt", [item.seq], item.text, "", item.method, fullText)); -} - -function sortTranscript(entries: TranscriptLine[]): TranscriptLine[] { - return entries.sort((left, right) => Number(left.seq) - Number(right.seq)); -} - -function boundedTranscript(entries: TranscriptLine[], limit: number): TranscriptLine[] { - sortTranscript(entries); - if (entries.length <= limit) return entries; - const first = entries[0]; - if (first?.title === "Submitted prompt" && first.rawSeqs.length === 0 && limit > 1) { - return [first, ...entries.slice(-(limit - 1))]; - } - return entries.slice(-limit); -} - -function buildTaskTranscript(task: QueueTask, limit = 180, rawOutputWindow = 0, fullText = false): TranscriptLine[] { - const entries: TranscriptLine[] = []; - const initialPrompt = taskInitialPromptLine(task, fullText); - if (initialPrompt !== null) entries.push(initialPrompt); - const promptHistoryLines = promptHistoryTranscriptLines(task, fullText); - const promptHistorySeqs = new Set(promptHistoryLines.map((line) => line.seq)); - entries.push(...promptHistoryLines); - let activeCommand: { seq: number; at: string; command: string; status?: string; body: string; rawSeqs: number[]; itemId?: string } | null = null; - - const flushCommand = (): void => { - if (activeCommand === null) return; - const kind = commandKind(activeCommand.command); - const output = activeCommand.itemId === undefined ? null : commandOutputs.get(activeCommand.itemId) ?? null; - const body = activeCommand.body.length > 0 ? activeCommand.body : formatCommandOutput(output); - entries.push(addCommandOutputStreams(transcriptLine( - kind, - activeCommand.at, - activeCommand.seq, - shortCommandTitle(activeCommand.command), - activeCommand.rawSeqs, - body, - activeCommand.command, - activeCommand.status, - fullText, - ), output, fullText)); - activeCommand = null; - }; - - const outputSource = rawOutputWindow > 0 ? task.output : taskFullOutput(task); - const outputItems = rawOutputWindow > 0 && outputSource.length > rawOutputWindow - ? outputSource.slice(-rawOutputWindow) - : outputSource; - const commandOutputs = outputItems.some((item) => item.channel === "command" && typeof item.itemId === "string") - ? codexSessionCommandOutputsByCallId(task) - : new Map(); - const fileChangeInputs = outputItems.some((item) => item.channel === "diff" && item.method === "item/fileChange/outputDelta" && typeof item.itemId === "string") - ? codexSessionFileChangesByCallId(task) - : new Map(); - for (const item of outputItems) { - if (initialPrompt !== null && item.channel === "user" && item.method === "enqueue") continue; - if (item.channel === "user" && item.method === "turn/steer" && promptHistorySeqs.has(item.seq)) continue; - if (item.channel === "command" && item.method === "item/started") { - flushCommand(); - const parsed = parseCommandLine(item.text); - activeCommand = { - seq: item.seq, - at: item.at, - command: parsed?.command || item.text, - status: parsed?.status, - body: "", - rawSeqs: [item.seq], - ...(typeof item.itemId === "string" ? { itemId: item.itemId } : {}), - }; - continue; - } - if (item.channel === "command" && item.method === "item/commandExecution/outputDelta") { - if (activeCommand !== null) { - activeCommand.body += item.text; - activeCommand.rawSeqs.push(item.seq); - } else { - const output = typeof item.itemId === "string" ? commandOutputs.get(item.itemId) ?? null : null; - entries.push(addCommandOutputStreams(transcriptLine("ran", item.at, item.seq, "Command output", [item.seq], item.text || formatCommandOutput(output), "", undefined, fullText), output, fullText)); - } - continue; - } - if (item.channel === "command" && item.method === "item/completed") { - const parsed = parseCommandLine(item.text); - if (activeCommand !== null) { - activeCommand.status = parsed?.status ?? activeCommand.status; - activeCommand.rawSeqs.push(item.seq); - flushCommand(); - } else { - const command = parsed?.command || item.text; - const kind = commandKind(command); - const output = typeof item.itemId === "string" ? commandOutputs.get(item.itemId) ?? null : null; - entries.push(addCommandOutputStreams(transcriptLine(kind, item.at, item.seq, shortCommandTitle(command), [item.seq], formatCommandOutput(output), command, parsed?.status, fullText), output, fullText)); - } - continue; - } - - flushCommand(); - if (item.channel === "diff") { - const text = fileChangeTextWithInlinePatch(item, fileChangeInputs); - entries.push(transcriptLine("edited", item.at, item.seq, "Edited files", [item.seq], text, "", item.method, fullText)); - } else if (item.channel === "error") { - entries.push(transcriptLine("error", item.at, item.seq, "Error", [item.seq], item.text, "", item.method, fullText)); - } else if (item.channel === "assistant") { - entries.push(transcriptLine("message", item.at, item.seq, "Assistant message", [item.seq], item.text, "", item.method, fullText)); - } else if (item.channel === "reasoning") { - 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" - ? "Recovered thread execution" - : item.method === "judge" - ? "Judge result" - : "System"; - entries.push(transcriptLine("system", item.at, item.seq, title, [item.seq], item.text, "", item.method, fullText)); - } - } - flushCommand(); - return boundedTranscript(entries, limit); -} - -function compactTaskTranscriptLine(item: LiveOutput, title: string, kind: TranscriptKind): TranscriptLine { - return { - seq: item.seq, - at: item.at, - kind, - title, - status: item.method, - bodyPreview: prefixPreview(item.text.replace(/\u001b\[[0-9;]*m/gu, ""), 900), - rawSeqs: [item.seq], - }; -} - -function buildCompactTaskTranscript(task: QueueTask, limit = 12, rawOutputWindow = 24): TranscriptLine[] { - const entries: TranscriptLine[] = []; - for (const item of task.promptHistory.slice(-2)) { - entries.push({ - seq: item.seq, - at: item.at, - kind: "message", - title: "Steer prompt", - status: item.method, - bodyPreview: prefixPreview(item.text, 900), - rawSeqs: [item.seq], - }); - } - const outputItems = task.output.slice(-rawOutputWindow); - const fileChangeInputs = outputItems.some((item) => item.channel === "diff" && item.method === "item/fileChange/outputDelta" && typeof item.itemId === "string") - ? codexSessionFileChangesByCallId(task) - : new Map(); - for (const item of outputItems) { - if (item.channel === "user" && item.method === "enqueue") continue; - if (item.channel === "command") { - const isOutput = item.method === "item/commandExecution/outputDelta"; - entries.push({ - seq: item.seq, - at: item.at, - kind: isOutput ? "ran" : item.method === "item/started" ? "ran" : "system", - title: isOutput ? "Command output" : item.method === "item/started" ? "Command started" : "Command completed", - status: item.method, - commandPreview: isOutput ? undefined : prefixPreview(item.text, 900), - bodyPreview: isOutput ? prefixPreview(item.text, 900) : undefined, - rawSeqs: [item.seq], - }); - } else if (item.channel === "diff") { - entries.push(compactTaskTranscriptLine({ ...item, text: fileChangeTextWithInlinePatch(item, fileChangeInputs) }, "Edited files", "edited")); - } else if (item.channel === "error") { - 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 { - 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); -} - -function transcriptSignature(task: QueueTask): string { - const last = task.output.at(-1); - return `${task.updatedAt}:${task.output.length}:${last?.seq ?? 0}:${last?.text.length ?? 0}:${outputArchiveSignature(task)}:${task.status}:${task.createdAt}:${task.basePrompt.length}:${task.prompt.length}:${task.promptHistory.length}:${task.promptHistory.at(-1)?.seq ?? 0}`; -} - -function cachedTranscript(task: QueueTask, fullText: boolean): TranscriptLine[] { - const signature = transcriptSignature(task); - const cached = transcriptCache.get(task.id); - if (cached?.signature === signature) { - const cachedTranscript = fullText ? cached.fullTranscript : cached.previewTranscript; - if (cachedTranscript !== undefined) return cachedTranscript; - } - const transcript = buildTaskTranscript(task, Number.MAX_SAFE_INTEGER, 0, fullText); - const next: { signature: string; previewTranscript?: TranscriptLine[]; fullTranscript?: TranscriptLine[] } = cached?.signature === signature ? cached : { signature }; - if (fullText) next.fullTranscript = transcript; - else next.previewTranscript = transcript; - transcriptCache.set(task.id, next); - if (transcriptCache.size > 80) { - const firstKey = transcriptCache.keys().next().value; - if (typeof firstKey === "string") transcriptCache.delete(firstKey); - } - return transcript; -} - -function cachedFullTranscript(task: QueueTask): TranscriptLine[] { - return cachedTranscript(task, true); -} - -function cachedPreviewTranscript(task: QueueTask): TranscriptLine[] { - return cachedTranscript(task, false); -} - -function outputForResponse(task: QueueTask, includeRaw: boolean): LiveOutput[] { - if (includeRaw) return taskFullOutput(task); - return task.output.slice(-80).map((item) => ({ ...item, text: safePreview(item.text, 4000) })); -} - -function attemptForResponse(attempt: AttemptSummary, full = false): JsonValue { - const finalResponse = String(attempt.finalResponse ?? ""); - const inputPrompt = String(attempt.inputPrompt ?? ""); - const feedbackPrompt = String(attempt.feedbackPrompt ?? ""); - return { - ...attempt, - inputPrompt: full ? inputPrompt : undefined, - inputPromptPreview: safePreview(String(attempt.inputPromptPreview || inputPrompt), full ? 3000 : 1200), - inputPromptChars: Number(attempt.inputPromptChars ?? inputPrompt.length), - inputPromptLines: Number(attempt.inputPromptLines ?? promptLineCount(inputPrompt)), - finalResponse: full ? finalResponse : safePreview(finalResponse, 1200), - finalResponsePreview: safePreview(String(attempt.finalResponsePreview || finalResponse), full ? 3000 : 1200), - finalResponseChars: Number(attempt.finalResponseChars ?? finalResponse.length), - feedbackPrompt: full ? feedbackPrompt : undefined, - feedbackPromptPreview: safePreview(String(attempt.feedbackPromptPreview || feedbackPrompt), full ? 3000 : 1200), - feedbackPromptChars: Number(attempt.feedbackPromptChars ?? feedbackPrompt.length), - feedbackPromptLines: Number(attempt.feedbackPromptLines ?? promptLineCount(feedbackPrompt)), - stderrTail: full ? attempt.stderrTail : safePreview(attempt.stderrTail, 1200), - } as unknown as 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), - terminalUnread: terminalTaskUnread(task), - attempts: task.attempts.map((attempt) => attemptForResponse(attempt, full)), - output: outputForResponse(task, includeRaw), - events: includeRaw ? task.events : task.events.slice(-120), - transcript: full ? fullTranscript(task) : buildTaskTranscript(task, 120, 0), - } as unknown as JsonValue; -} - -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), - queueEnteredAt: taskQueueEnteredAt(task), - prompt: task.prompt, - basePrompt: task.basePrompt, - displayPrompt, - 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, - createdAt: task.createdAt, - updatedAt: task.updatedAt, - startedAt: task.startedAt, - finishedAt: task.finishedAt, - readAt: task.readAt, - currentAttempt: task.currentAttempt, - currentMode: task.currentMode, - judgeFailCount: task.judgeFailCount, - judgeFailRetryLimit, - codexThreadId: task.codexThreadId, - activeTurnId: task.activeTurnId, - finalResponse: task.finalResponse, - lastError: task.lastError, - lastJudge: task.lastJudge, - promptHistory: task.promptHistory, - attempts: task.attempts, - cancelRequested: task.cancelRequested, - terminalUnread: terminalTaskUnread(task), - nextMode: task.nextMode, - outputCount: fullOutput.length, - retainedOutputCount: task.output.length, - eventCount: task.events.length, - transcriptCount: null, - transcriptMaxSeq: lastOutputSeq, - timing: taskTiming(task), - transcript: [], - output: [], - events: [], - } as unknown as 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), - queueEnteredAt: taskQueueEnteredAt(task), - prompt: prefixPreview(task.prompt, 900), - basePrompt: prefixPreview(task.basePrompt, 900), - displayPrompt: prefixPreview(displayPrompt, 900), - 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 : { - injectedAt: task.referenceInjection.injectedAt, - itemCount: task.referenceInjection.itemCount, - directReferenceTaskIds: task.referenceInjection.directReferenceTaskIds, - 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, - createdAt: task.createdAt, - updatedAt: task.updatedAt, - startedAt: task.startedAt, - finishedAt: task.finishedAt, - readAt: task.readAt, - currentAttempt: task.currentAttempt, - currentMode: task.currentMode, - judgeFailCount: task.judgeFailCount, - judgeFailRetryLimit, - codexThreadId: task.codexThreadId, - activeTurnId: task.activeTurnId, - finalResponse: prefixPreview(task.finalResponse, 1200), - lastError: task.lastError, - lastJudge: task.lastJudge, - promptHistory: task.promptHistory.slice(-8), - attempts: task.attempts.slice(-3).map((attempt) => attemptForResponse(attempt, false)), - cancelRequested: task.cancelRequested, - terminalUnread: terminalTaskUnread(task), - nextMode: task.nextMode, - outputCount: task.output.length, - eventCount: task.events.length, - transcriptCount: null, - transcriptMaxSeq: lastOutputSeq, - timing: taskTiming(task), - transcript: [], - output: [], - events: [], - } as unknown as JsonValue; -} - -function lastAssistantMessage(task: QueueTask, transcript = fullTranscript(task)): JsonValue { - const assistantLine = transcript.slice().reverse().find((line) => line.kind === "message" && line.title === "Assistant message"); - const text = task.finalResponse.trim().length > 0 ? task.finalResponse.trim() : String(assistantLine?.bodyPreview ?? "").trim(); - return { - text, - at: assistantLine?.at ?? task.finishedAt ?? task.updatedAt, - seq: assistantLine?.seq ?? null, - source: task.finalResponse.trim().length > 0 ? "finalResponse" : assistantLine !== undefined ? "transcript" : "none", - }; -} - -function parseToolLimit(url: URL): number { - const value = Number(url.searchParams.get("toolLimit") ?? 160); - return Number.isInteger(value) && value > 0 ? Math.min(500, value) : 160; -} - -function taskToolSummaryForLimit(task: QueueTask, limit: number, transcript = fullTranscript(task)): JsonValue { - const allTools = transcript.filter((line) => line.kind === "ran" || line.kind === "explored" || line.kind === "edited"); - const boundedLimit = Math.max(1, Math.min(500, Math.floor(limit))); - const start = Math.max(0, allTools.length - boundedLimit); - return { - count: allTools.length, - returned: allTools.length - start, - limit: boundedLimit, - truncated: start > 0, - items: allTools.slice(start).map((line) => ({ - seq: line.seq, - at: line.at, - kind: line.kind, - title: line.title, - status: line.status ?? null, - commandPreview: line.commandPreview ?? "", - commandOmittedLines: line.commandOmittedLines ?? 0, - outputPreview: line.bodyPreview ?? "", - outputOmittedLines: line.bodyOmittedLines ?? 0, - rawSeqs: line.rawSeqs, - })), - } as unknown as JsonValue; -} - -function taskToolSummary(task: QueueTask, url: URL, transcript = fullTranscript(task)): JsonValue { - return taskToolSummaryForLimit(task, parseToolLimit(url), transcript); -} - -function uniqueStrings(values: string[], limit = 20): string[] { - const result: string[] = []; - for (const value of values) { - const normalized = value.trim(); - if (normalized.length === 0 || result.includes(normalized)) continue; - result.push(normalized); - if (result.length >= limit) break; - } - return result; -} - -function cleanTracePath(value: string): string { - return value - .replace(/^['"`([{<]+/u, "") - .replace(/['"`)\]}>.,;:]+$/u, "") - .replace(/:\d+(?::\d+)?$/u, "") - .replace(/^[ab]\//u, "") - .trim(); -} - -function extractTracePaths(text: string): string[] { - const source = String(text || ""); - const matches = source.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) || []; - return uniqueStrings(matches.map(cleanTracePath).filter((path) => path.length >= 2 && !path.includes("...") && !/^(http|https|status|method)$/iu.test(path)), 40); -} - -function parseEditedFilesFromText(text: string): string[] { - const files: string[] = []; - const addFile = (path: string): void => { - const cleanPath = cleanTracePath(path); - if (cleanPath.length === 0 || cleanPath === "/dev/null" || files.includes(cleanPath)) return; - files.push(cleanPath); - }; - for (const line of text.split(/\r?\n/u)) { - const statusMatch = /^([AMDRCU?]{1,2})\s+(.+)$/u.exec(line); - if (statusMatch) { - addFile(statusMatch[2] || ""); - continue; - } - const patchMatch = /^\*\*\*\s+(?:Add|Update|Delete)\s+File:\s+(.+)$/u.exec(line); - if (patchMatch) { - addFile(patchMatch[1] || ""); - continue; - } - const moveMatch = /^\*\*\*\s+Move to:\s+(.+)$/u.exec(line); - if (moveMatch) { - addFile(moveMatch[1] || ""); - continue; - } - const diffMatch = /^diff --git a\/(.+?) b\/(.+)$/u.exec(line); - if (diffMatch) { - addFile(diffMatch[2] || diffMatch[1] || ""); - continue; - } - const plusMatch = /^\+\+\+ b\/(.+)$/u.exec(line); - if (plusMatch) addFile(plusMatch[1] || ""); - } - return files.length > 0 ? files : extractTracePaths(text); -} - -function transcriptPreviewLines(text: string, maxLines: number, maxChars: number): string[] { - const preview = linePreview(text, maxLines, maxChars).text; - return preview.length === 0 ? [] : preview.split(/\r?\n/u).slice(0, maxLines); -} - -function transcriptLineSummaryLines(line: TranscriptLine): string[] { - const lines: string[] = []; - const command = String(line.commandPreview || "").trim(); - if (command.length > 0) { - for (const item of transcriptPreviewLines(command, 2, 420)) lines.push(`$ ${item}`); - } - const body = String(line.bodyPreview || "").trim(); - const remaining = Math.max(0, 4 - lines.length); - if (body.length > 0 && remaining > 0) { - for (const item of transcriptPreviewLines(body, remaining, 700)) lines.push(item); - } - if (lines.length === 0) lines.push(line.status ? `${line.title} (${line.status})` : line.title); - 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(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 = toolStepCountsFromTranscript(transcript); - return { - durationMs: timing.durationMs ?? timing.totalElapsedMs ?? null, - totalElapsedMs: timing.totalElapsedMs ?? null, - queueWaitMs: timing.queueWaitMs ?? null, - toolCallCount: counts.toolCallCount, - readCount: counts.readCount, - editCount: counts.editCount, - runCount: counts.runCount, - editedFiles, - commands, - stepCount: counts.toolCallCount, - llmStepCount: counts.toolCallCount, - traceLineCount: transcript.filter((line) => line.title !== "Submitted prompt").length, - transcriptMaxSeq: transcript.at(-1)?.seq ?? 0, - outputCount, - retainedOutputCount, - } as unknown as JsonValue; -} - -function taskExecutionSummary(task: QueueTask, transcript = cachedPreviewTranscript(task)): JsonValue { - return executionSummaryFromTranscript(task, taskExecutionTranscript(task, transcript), taskTiming(task) as Record, undefined, undefined, task.currentAttempt || task.attempts.length); -} - -function parseAttemptIndex(text: string): number | null { - const match = /\battempt\s+(\d+)\s*\/\s*\d+/iu.exec(text); - if (match === null) return null; - const value = Number(match[1]); - return Number.isInteger(value) && value > 0 ? value : null; -} - -function traceAttemptIndexFromLine(line: TranscriptLine): number | null { - return parseAttemptIndex(`${line.bodyPreview ?? ""}\n${line.commandPreview ?? ""}\n${line.title}`); -} - -function parseJudgeLine(text: string): JudgeResult | null { - const match = /\bjudge=(complete|retry|fail)\s+confidence=([0-9.]+)\s+source=([A-Za-z0-9_-]+):\s*([\s\S]*)$/u.exec(text.trim()); - if (match === null) return null; - const confidence = Number(match[2]); - const source = match[3] === "minimax" ? "minimax" : "fallback"; - return { - decision: match[1] as JudgeDecision, - confidence: Number.isFinite(confidence) ? confidence : 0, - source, - reason: (match[4] ?? "").trim(), - }; -} - -function judgeFromAttemptLines(lines: TranscriptLine[]): { judge: JudgeResult; at: string | null; seq: number | null } | null { - for (const line of lines.slice().reverse()) { - if (line.title !== "Judge result" && line.status !== "judge") continue; - const judge = parseJudgeLine(String(line.bodyPreview || "")); - if (judge === null) continue; - return { judge, at: line.at || null, seq: Number.isFinite(Number(line.seq)) ? Number(line.seq) : null }; - } - return null; -} - -function attemptFeedbackPromptRecord(task: QueueTask, attemptIndex: number, attempt: AttemptSummary | null, judge: JudgeResult | null = attempt?.judge ?? null): FeedbackPromptRecord | null { - const directPrompt = String(attempt?.feedbackPrompt ?? "").trimEnd(); - if (directPrompt.length > 0) { - const snapshot = promptSnapshot(directPrompt, 1600); - return { - text: snapshot.text, - preview: String(attempt?.feedbackPromptPreview || snapshot.preview), - chars: Number(attempt?.feedbackPromptChars ?? snapshot.chars), - lines: Number(attempt?.feedbackPromptLines ?? snapshot.lines), - source: String(attempt?.feedbackPromptSource || "judge-feedback"), - forAttempt: Number.isFinite(Number(attempt?.feedbackPromptForAttempt)) ? Number(attempt?.feedbackPromptForAttempt) : attemptIndex + 1, - truncated: snapshot.truncated, - }; - } - - const hasPendingRetry = task.status === "retry_wait" || task.status === "queued"; - if (attemptIndex === task.attempts.length && hasPendingRetry && task.nextMode === "retry" && task.nextPrompt !== null && task.nextPrompt.trim().length > 0) { - const snapshot = promptSnapshot(task.nextPrompt, 1600); - return { ...snapshot, source: "pending-retry", forAttempt: attemptIndex + 1 }; - } - - const nextAttempt = task.attempts.find((item) => Number(item.index) === attemptIndex + 1) ?? task.attempts[attemptIndex] ?? null; - const nextInput = String(nextAttempt?.inputPrompt ?? "").trimEnd(); - if (nextInput.length > 0) { - const snapshot = promptSnapshot(nextInput, 1600); - return { ...snapshot, source: "attempt-input", forAttempt: attemptIndex + 1 }; - } - - if (judge?.decision === "retry") { - const generated = retryPrompt(task, judge); - const snapshot = promptSnapshot(generated, 1600); - return { ...snapshot, source: judge.continuePrompt?.trim() ? "judge-continue-prompt" : "judge-retry-generated", forAttempt: attemptIndex + 1 }; - } - - return null; -} - -function attemptTimingSummary(attempt: AttemptSummary | null, lines: TranscriptLine[]): Record { - const startedAt = attempt?.startedAt || lines[0]?.at || null; - const finishedAt = attempt?.finishedAt || lines.at(-1)?.at || null; - const startedMs = timestampMs(startedAt); - const finishedMs = timestampMs(finishedAt); - const durationMs = nonNegativeElapsed(startedMs, finishedMs); - return { - durationMs, - totalElapsedMs: durationMs, - queueWaitMs: null, - effectiveEndAt: finishedAt, - }; -} - -interface TraceAttemptWindow { - index: number; - attempt: AttemptSummary | null; - startSeq: number | null; - endSeq: number | null; - lines: TranscriptLine[]; - synthetic?: boolean; - 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 })) - .filter((item): item is { line: TranscriptLine; position: number; index: number } => item.index !== null) - .sort((left, right) => Number(left.line.seq) - Number(right.line.seq)); - const maxStartIndex = starts.reduce((max, item) => Math.max(max, item.index), 0); - const maxIndex = Math.max(task.attempts.length, task.currentAttempt || 0, maxStartIndex); - const windows: TraceAttemptWindow[] = []; - for (let index = 1; index <= maxIndex; index += 1) { - const attempt = task.attempts.find((item) => Number(item.index) === index) ?? task.attempts[index - 1] ?? null; - const start = starts.find((item) => item.index === index) ?? starts[index - 1] ?? null; - const nextStart = starts.find((item) => item.index > index) ?? null; - const explicitStartSeq = Number((attempt as AttemptSummary | null)?.outputStartSeq ?? NaN); - const explicitEndSeq = Number((attempt as AttemptSummary | null)?.outputEndSeq ?? NaN); - const startSeq = Number.isFinite(explicitStartSeq) ? explicitStartSeq : start?.line.seq ?? null; - const hasExplicitEndSeq = Number.isFinite(explicitEndSeq); - const endSeq = hasExplicitEndSeq ? explicitEndSeq : nextStart !== null ? nextStart.line.seq : null; - let lines = startSeq === null - ? [] - : transcript.filter((line) => Number(line.seq) >= startSeq && (endSeq === null || (hasExplicitEndSeq ? Number(line.seq) <= endSeq : Number(line.seq) < endSeq))); - if (lines.length === 0 && attempt !== null) { - const startedMs = timestampMs(attempt.startedAt); - const finishedMs = timestampMs(attempt.finishedAt); - lines = transcript.filter((line) => { - const lineMs = timestampMs(line.at); - return lineMs !== null - && (startedMs === null || lineMs >= startedMs) - && (finishedMs === null || lineMs <= finishedMs); - }); - } - windows.push({ index, attempt, startSeq, endSeq, lines }); - } - const coveredSeqs = new Set(); - for (const window of windows) { - for (const line of window.lines) coveredSeqs.add(Number(line.seq)); - } - const orphanedGroups: TranscriptLine[][] = []; - let group: TranscriptLine[] = []; - const flushGroup = (): void => { - if (group.length > 0 && executionLinesForAttempt(group).length > 0) orphanedGroups.push(group); - group = []; - }; - for (const line of transcript) { - const seq = Number(line.seq); - if (line.title === "Submitted prompt" || coveredSeqs.has(seq)) { - flushGroup(); - continue; - } - group.push(line); - } - flushGroup(); - - 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({ - index: syntheticIndex, - attempt: null, - startSeq: Number.isFinite(startSeq) ? startSeq : null, - endSeq: Number.isFinite(endSeq) ? endSeq : null, - lines, - synthetic: true, - label: hasRecovery - ? hasSteer ? "Recovered thread execution with steer prompt" : "Recovered thread execution" - : hasSteer ? "Recovered thread execution with steer prompt" : "System events", - }); - syntheticIndex -= 1; - } - - return windows.sort((left, right) => Number(left.lines[0]?.seq ?? left.startSeq ?? 0) - Number(right.lines[0]?.seq ?? right.startSeq ?? 0)); -} - -function executionLinesForAttempt(lines: TranscriptLine[]): TranscriptLine[] { - return lines.filter((line) => line.title !== "Submitted prompt" && line.title !== "Attempt started" && line.title !== "Judge result"); -} - -function taskTraceAttemptSummaries(task: QueueTask, transcript: TranscriptLine[]): JsonValue[] { - const windows = traceAttemptWindows(task, transcript); - return windows.map((window) => { - const attempt = window.attempt; - const parsedJudge = judgeFromAttemptLines(window.lines); - const storedJudge = attempt?.judge ?? null; - const synthetic = window.synthetic === true; - const judge = synthetic ? null : storedJudge ?? parsedJudge?.judge ?? (window.index === task.attempts.length && task.lastJudge !== null ? task.lastJudge : null); - 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 { - ...(attempt ?? {}), - index: attempt?.index ?? window.index, - synthetic, - label: window.label ?? null, - mode: attempt?.mode ?? (window.index <= 1 ? "initial" : "retry"), - startedAt: attempt?.startedAt ?? window.lines[0]?.at ?? task.startedAt, - finishedAt: attempt?.finishedAt ?? null, - terminalStatus: attempt?.terminalStatus ?? null, - transportClosedBeforeTerminal: attempt?.transportClosedBeforeTerminal ?? false, - appServerExitCode: attempt?.appServerExitCode ?? null, - appServerSignal: attempt?.appServerSignal ?? null, - error: attempt?.error ?? null, - stderrTail: attempt?.stderrTail ?? "", - startSeq: window.startSeq, - endSeq: window.endSeq, - inputPrompt: undefined, - inputPromptPreview: inputPrompt.preview, - inputPromptChars: Number(attempt?.inputPromptChars ?? inputPrompt.chars), - inputPromptLines: Number(attempt?.inputPromptLines ?? inputPrompt.lines), - finalResponse, - finalResponsePreview: attempt?.finalResponsePreview ?? safePreview(finalResponse, 3000), - finalResponseChars, - finalResponseTruncated: finalResponse.length < finalResponseChars, - judge, - judgeAt: attempt?.judgeAt ?? parsedJudge?.at ?? null, - judgeSeq: attempt?.judgeSeq ?? parsedJudge?.seq ?? null, - feedbackPrompt: undefined, - feedbackPromptPreview: feedbackPrompt?.preview ?? "", - feedbackPromptChars: feedbackPrompt?.chars ?? 0, - feedbackPromptLines: feedbackPrompt?.lines ?? 0, - feedbackPromptSource: feedbackPrompt?.source ?? null, - feedbackPromptForAttempt: feedbackPrompt?.forAttempt ?? null, - feedbackPromptTruncated: feedbackPrompt?.truncated ?? false, - 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 = stripCodeQueueEnvironmentHint(prompt); - const trimmed = withoutEnvironment.trimStart(); - if (!trimmed.startsWith(resolvedReferenceContextTitle)) { - return { reference: "", userPrompt: userPromptForDisplay(prompt) }; - } - const offset = withoutEnvironment.length - trimmed.length; - const index = withoutEnvironment.lastIndexOf(currentTaskPromptMarker); - if (index < offset) return { reference: "", userPrompt: userPromptForDisplay(prompt) }; - return { - reference: withoutEnvironment.slice(offset, index).trimEnd(), - userPrompt: withoutEnvironment.slice(index + currentTaskPromptMarker.length).trimStart(), - }; -} - -function taskTracePromptSummary(task: QueueTask): JsonValue { - const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt); - const parts = resolvedReferencePromptParts(task.prompt); - return { - basePrompt: displayPrompt, - basePromptChars: displayPrompt.length, - basePromptLines: promptLineCount(displayPrompt), - promptChars: task.prompt.length, - promptLines: promptLineCount(task.prompt), - referencePromptChars: parts.reference.length, - referencePromptLines: promptLineCount(parts.reference), - hasReferenceInjection: parts.reference.length > 0 || task.referenceInjection !== null, - referenceTaskIds: task.referenceTaskIds, - referenceInjectionSummary: task.referenceInjection === null ? null : { - injectedAt: task.referenceInjection.injectedAt, - itemCount: task.referenceInjection.itemCount, - directReferenceTaskIds: task.referenceInjection.directReferenceTaskIds, - maxRounds: task.referenceInjection.maxRounds, - truncated: task.referenceInjection.truncated, - }, - } as unknown as 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, - startedAt: task.startedAt, - finishedAt: task.finishedAt, - 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; -} - -function taskFeedbackPromptDetail(task: QueueTask, attemptIndex: number | null): FeedbackPromptRecord | null { - const index = attemptIndex ?? (task.attempts.length > 0 ? task.attempts.length : task.currentAttempt || 1); - if (!Number.isInteger(index) || index <= 0) return null; - const attempt = task.attempts.find((item) => Number(item.index) === index) ?? task.attempts[index - 1] ?? null; - let judge = attempt?.judge ?? null; - if (judge === null) { - const window = traceAttemptWindows(task, cachedPreviewTranscript(task)).find((item) => item.index === index) ?? null; - judge = judgeFromAttemptLines(window?.lines ?? [])?.judge ?? null; - } - return attemptFeedbackPromptRecord(task, index, attempt, judge); -} - -function taskPromptDetailResponse(task: QueueTask, url: URL): Response { - const part = String(url.searchParams.get("part") || "full"); - const parts = resolvedReferencePromptParts(task.prompt); - const basePrompt = task.basePrompt || userPromptForDisplay(task.prompt); - if (part === "feedback" || part === "judge-feedback") { - const attemptIndex = parseSeqParam(url, "attempt", null); - const detail = taskFeedbackPromptDetail(task, attemptIndex); - const text = detail?.text ?? ""; - return jsonResponse({ - ok: true, - taskId: task.id, - queueId: queueIdOf(task), - status: task.status, - updatedAt: task.updatedAt, - part: "feedback", - attempt: attemptIndex, - forAttempt: detail?.forAttempt ?? null, - source: detail?.source ?? null, - text, - preview: detail?.preview ?? "", - chars: detail?.chars ?? text.length, - lines: detail?.lines ?? promptLineCount(text), - truncated: detail?.truncated ?? false, - }); - } - const text = part === "base" - ? basePrompt - : part === "reference" - ? parts.reference - : task.prompt; - return jsonResponse({ - ok: true, - taskId: task.id, - queueId: queueIdOf(task), - status: task.status, - updatedAt: task.updatedAt, - part, - text, - chars: text.length, - lines: promptLineCount(text), - promptChars: task.prompt.length, - basePromptChars: basePrompt.length, - referencePromptChars: parts.reference.length, - }); -} - -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); - const transcript = sourceTranscript.filter((line) => line.title !== "Submitted prompt"); - const page = pageBySeq(transcript, url, limit); - return jsonResponse({ - ok: true, - taskId: task.id, - queueId: queueIdOf(task), - status: task.status, - updatedAt: task.updatedAt, - agentPort, - agentPortInfo: codeAgentPortInfo(agentPort), - attempt: attemptIndex, - steps: page.chunk.map((line) => ({ - seq: line.seq, - at: line.at, - kind: line.kind, - title: line.title, - status: line.status ?? null, - durationMs: line.durationMs ?? null, - rawSeqs: line.rawSeqs, - summaryLines: transcriptLineSummaryLines(line), - hasDetail: true, - })), - mode: page.mode, - afterSeq: page.afterSeq, - nextAfterSeq: page.nextAfterSeq, - beforeSeq: page.beforeSeq, - previousBeforeSeq: page.previousBeforeSeq, - hasMore: page.hasMore, - hasBefore: page.hasBefore, - total: transcript.length, - maxSeq: transcript.at(-1)?.seq ?? 0, - }); -} - -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); - return jsonResponse({ - ok: true, - taskId: task.id, - queueId: queueIdOf(task), - status: task.status, - updatedAt: task.updatedAt, - agentPort, - agentPortInfo: codeAgentPortInfo(agentPort), - seq, - line, - }); -} - -function taskSummaryResponse(task: QueueTask, url: URL): JsonValue { - const transcript = fullTranscript(task); - const fullOutput = taskFullOutput(task); - 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, - maxAttempts: task.maxAttempts, - currentAttempt: task.currentAttempt, - currentMode: task.currentMode, - judgeFailCount: task.judgeFailCount, - judgeFailRetryLimit, - codexThreadId: task.codexThreadId, - activeTurnId: task.activeTurnId, - createdAt: task.createdAt, - startedAt: task.startedAt, - finishedAt: task.finishedAt, - updatedAt: task.updatedAt, - timing: taskTiming(task), - initialPrompt: task.prompt, - basePrompt: task.basePrompt, - prompt: task.prompt, - promptEditable: queuedTaskPromptEditable(task), - referenceTaskIds: task.referenceTaskIds, - referenceInjection: task.referenceInjection, - lastAssistantMessage: lastAssistantMessage(task, transcript), - toolSummary: taskToolSummary(task, url, transcript), - attempts: task.attempts, - lastJudge: task.lastJudge, - lastError: task.lastError, - cancelRequested: task.cancelRequested, - transcriptCount: transcript.length, - transcriptMaxSeq: transcript.at(-1)?.seq ?? 0, - outputCount: fullOutput.length, - retainedOutputCount: task.output.length, - outputMaxSeq: fullOutput.at(-1)?.seq ?? 0, - eventCount: task.events.length, - cliHint: `bun scripts/cli.ts codex task ${task.id}`, - traceHint: `bun scripts/cli.ts codex task ${task.id} --trace --tail --limit 80`, - rawOutputHint: `bun scripts/cli.ts codex output ${task.id} --tail --limit 20`, - } as unknown as JsonValue; -} - -const resolvedReferenceContextTitle = "# Code Queue 已解析引用上下文"; -const currentTaskPromptMarker = "\n# 本次任务\n"; -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+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; - return trimmed.slice(index + marker.length).trimStart(); -} - -function stripResolvedReferenceContext(prompt: string): string { - const trimmed = prompt.trimStart(); - if (!trimmed.startsWith(resolvedReferenceContextTitle)) return prompt; - const offset = prompt.length - trimmed.length; - const index = prompt.lastIndexOf(currentTaskPromptMarker); - if (index < offset) return prompt; - return prompt.slice(index + currentTaskPromptMarker.length).trimStart(); -} - -function stripCodeQueueEnvironmentHint(prompt: string): string { - const trimmed = prompt.trimStart(); - if (!trimmed.startsWith(codeQueueEnvironmentHintTitle)) return prompt; - const offset = prompt.length - trimmed.length; - const index = prompt.indexOf(currentTaskPromptMarker, offset); - if (index < offset) return prompt; - return prompt.slice(index + currentTaskPromptMarker.length).trimStart(); -} - -function userPromptForDisplay(prompt: string): string { - return stripAutoReferenceHint(stripResolvedReferenceContext(stripCodeQueueEnvironmentHint(prompt))); -} - -function promptWithCodeQueueEnvironmentHint(prompt: string): string { - if (prompt.trimStart().startsWith(codeQueueEnvironmentHintTitle)) return prompt; - return [codeQueueEnvironmentHint, "", "# 本次任务", prompt.trim()].join("\n"); -} - -function injectCodeQueueEnvironmentHint(request: QueueTaskRequest): QueueTaskRequest { - if (request.prompt.trimStart().startsWith(codeQueueEnvironmentHintTitle)) return request; - const basePrompt = request.basePrompt ?? userPromptForDisplay(request.prompt); - 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]); @@ -4081,166 +1665,6 @@ function rewriteEnqueueOutput(task: QueueTask): void { appendOutputArchive(task, nextOutput, "set", text); } -function taskReferenceIds(task: QueueTask): string[] { - const ids: string[] = []; - for (const id of task.referenceTaskIds ?? []) addUniqueTaskId(ids, id); - for (const id of referenceTaskIdsFromPrompt(task.basePrompt || userPromptForDisplay(task.prompt))) addUniqueTaskId(ids, id); - return ids; -} - -function taskBasePrompt(task: QueueTask): string { - return (task.basePrompt || userPromptForDisplay(task.prompt)).trimEnd(); -} - -function referenceSummaryItem(task: QueueTask, round: number, roundIndex: number, viaTaskId: string | null): ReferenceInjectionSummaryItem { - const lastMessage = lastAssistantMessage(task) as Record; - const lastText = typeof lastMessage.text === "string" ? lastMessage.text : ""; - return { - round, - roundIndex, - taskId: task.id, - viaTaskId, - status: task.status, - providerId: task.providerId, - model: task.model, - cwd: task.cwd, - createdAt: task.createdAt, - updatedAt: task.updatedAt, - promptChars: taskBasePrompt(task).length, - finalResponseChars: lastText.trimEnd().length, - finalResponseAt: typeof lastMessage.at === "string" ? lastMessage.at : null, - finalResponseSource: typeof lastMessage.source === "string" ? lastMessage.source : "unknown", - referenceTaskIds: taskReferenceIds(task), - cliHint: `bun scripts/cli.ts codex task ${task.id}`, - }; -} - -function collectReferenceGraph(rootIds: string[], maxRounds: number | null, finder: (id: string) => QueueTask | null = findTask): { items: ReferenceInjectionSummaryItem[]; tasks: QueueTask[]; truncated: boolean } { - const seen = new Set(); - let frontier = rootIds.map((id) => ({ id, viaTaskId: null as string | null })); - const rawItems: Array<{ task: QueueTask; depth: number; viaTaskId: string | null; discoveryIndex: number }> = []; - let truncated = false; - let discoveryIndex = 0; - for (let depth = 1; frontier.length > 0; depth += 1) { - if (maxRounds !== null && depth > maxRounds) { - truncated = frontier.some((entry) => !seen.has(entry.id)); - break; - } - const next: Array<{ id: string; viaTaskId: string | null }> = []; - for (const entry of frontier) { - if (seen.has(entry.id)) continue; - const task = finder(entry.id); - if (task === null) continue; - seen.add(entry.id); - discoveryIndex += 1; - rawItems.push({ task, depth, viaTaskId: entry.viaTaskId, discoveryIndex }); - for (const childId of taskReferenceIds(task)) { - if (!seen.has(childId)) next.push({ id: childId, viaTaskId: task.id }); - } - } - frontier = next; - } - if (frontier.some((entry) => !seen.has(entry.id))) truncated = true; - const sorted = rawItems.sort((left, right) => { - if (left.depth !== right.depth) return right.depth - left.depth; - const createdDelta = Date.parse(left.task.createdAt) - Date.parse(right.task.createdAt); - if (Number.isFinite(createdDelta) && createdDelta !== 0) return createdDelta; - return left.discoveryIndex - right.discoveryIndex; - }); - const depthToRound = new Map(); - Array.from(new Set(sorted.map((item) => item.depth))).forEach((depth, index) => depthToRound.set(depth, index + 1)); - const roundCounts = new Map(); - const items = sorted.map((item) => { - const round = depthToRound.get(item.depth) ?? item.depth; - const roundIndex = (roundCounts.get(round) ?? 0) + 1; - roundCounts.set(round, roundIndex); - return referenceSummaryItem(item.task, round, roundIndex, item.viaTaskId); - }); - const tasks = sorted.map((item) => item.task); - return { items, tasks, truncated }; -} - -function referenceRoundSeparator(round: number, totalRounds: number, items: ReferenceInjectionSummaryItem[]): string { - const createdTimes = items.map((item) => item.createdAt).filter(Boolean).sort(); - const updatedTimes = items.map((item) => item.updatedAt).filter(Boolean).sort(); - return [ - `----- Reference Round ${round}/${totalRounds} -----`, - `order: upstream/oldest context first; direct references appear in the last round`, - `tasks: ${items.length}`, - `createdRange: ${createdTimes[0] ?? "--"} -> ${createdTimes.at(-1) ?? "--"}`, - `updatedRange: ${updatedTimes[0] ?? "--"} -> ${updatedTimes.at(-1) ?? "--"}`, - "--------------------------------", - ].join("\n"); -} - -function referencedTaskContext(task: QueueTask, summary: ReferenceInjectionSummaryItem): string { - const lastMessage = lastAssistantMessage(task) as Record; - const lastMessageText = typeof lastMessage.text === "string" ? lastMessage.text : ""; - return [ - `## Round ${summary.round}.${summary.roundIndex} referenced task ${task.id}`, - `- via: ${summary.viaTaskId ?? "direct"}`, - `- 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}`, - "", - "### Initial prompt", - taskBasePrompt(task) || "(empty)", - "", - "### Final/last response", - lastMessageText.trimEnd() || "(none yet)", - "", - "### Query more context", - `Run: bun scripts/cli.ts codex task ${task.id}`, - ].join("\n"); -} - -function injectReferencedTaskContext(request: QueueTaskRequest, finder: (id: string) => QueueTask | null = findTask, injectedAt = nowIso()): QueueTaskRequest { - const ids = request.referenceTaskIds ?? []; - if (ids.length === 0 || request.prompt.includes(resolvedReferenceContextTitle)) return request; - 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 Code Queue task not found: ${missing.join(", ")}`); - const userPrompt = request.basePrompt ?? userPromptForDisplay(request.prompt); - const graph = collectReferenceGraph(ids, referenceInjectionMaxRounds, finder); - const injection: ReferenceInjectionRecord = { - version: 2, - injectedAt, - basePrompt: userPrompt, - directReferenceTaskIds: ids, - maxRounds: referenceInjectionMaxRounds, - truncated: graph.truncated, - itemCount: graph.items.length, - items: graph.items, - }; - const taskById = new Map(graph.tasks.map((task) => [task.id, task])); - const groupedItems = Array.from(new Set(graph.items.map((item) => item.round))).map((round) => ({ - round, - items: graph.items.filter((item) => item.round === round), - })); - const context = [ - resolvedReferenceContextTitle, - `injectedAt: ${injectedAt}`, - `directReferences: ${ids.join(", ")}`, - `referenceGraphItems: ${graph.items.length}${graph.truncated ? " (truncated)" : ""}`, - "说明:Code Queue 后端只读取每个被引用任务的结构化 basePrompt(注入前 prompt)和 final/last response;不会把历史引用注入块继续套入。多轮引用按上游/最早上下文在前、直接引用在后的顺序注入;中间执行过程不注入,只保留 CLI 查询提示。", - "", - ...(groupedItems.flatMap((group) => [ - referenceRoundSeparator(group.round, groupedItems.length, group.items), - "", - ...group.items.map((item) => { - const task = taskById.get(item.taskId); - return task === undefined ? "" : referencedTaskContext(task, item); - }).filter((text) => text.length > 0), - "", - ])), - "", - "# 本次任务", - userPrompt.trim(), - ].join("\n"); - return { ...request, prompt: context, basePrompt: userPrompt, referenceTaskIds: ids, referenceInjection: injection }; -} - function truthyParam(url: URL, name: string): boolean { const value = url.searchParams.get(name); return value === "1" || value === "true" || value === "yes"; @@ -4291,451 +1715,6 @@ 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({ - ok: true, - taskId: task.id, - queueId: queueIdOf(task), - status: task.status, - updatedAt: task.updatedAt, - agentPort, - agentPortInfo: codeAgentPortInfo(agentPort), - mode: page.mode, - transcript: page.chunk, - afterSeq: page.afterSeq, - nextAfterSeq: page.nextAfterSeq, - beforeSeq: page.beforeSeq, - previousBeforeSeq: page.previousBeforeSeq, - hasMore: page.hasMore, - hasBefore: page.hasBefore, - total: transcript.length, - maxSeq: transcript.at(-1)?.seq ?? 0, - fullText, - }); -} - -function outputChunkResponse(task: QueueTask, url: URL): Response { - const limit = parseLimit(url); - const fullText = truthyParam(url, "fullText") || truthyParam(url, "raw"); - const maxTextChars = parseTextLimit(url); - const fullOutput = taskFullOutput(task); - const page = pageBySeq(fullOutput, url, limit); - const output = page.chunk.map((item) => { - const truncated = !fullText && item.text.length > maxTextChars; - return { - ...item, - text: truncated ? item.text.slice(0, maxTextChars) : item.text, - textChars: item.text.length, - textTruncated: truncated, - omittedChars: truncated ? item.text.length - maxTextChars : 0, - }; - }); - return jsonResponse({ - ok: true, - taskId: task.id, - queueId: queueIdOf(task), - status: task.status, - updatedAt: task.updatedAt, - mode: page.mode, - output, - afterSeq: page.afterSeq, - nextAfterSeq: page.nextAfterSeq, - beforeSeq: page.beforeSeq, - previousBeforeSeq: page.previousBeforeSeq, - hasMore: page.hasMore, - hasBefore: page.hasBefore, - total: fullOutput.length, - retainedTotal: task.output.length, - maxSeq: fullOutput.at(-1)?.seq ?? 0, - fullText, - maxTextChars, - }); -} - -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, - queueId: queueIdOf(task), - queueEnteredAt: taskQueueEnteredAt(task), - prompt: prefixPreview(displayPrompt, 360), - basePrompt: prefixPreview(task.basePrompt, 360), - displayPrompt: prefixPreview(displayPrompt, 360), - 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 : { - injectedAt: task.referenceInjection.injectedAt, - itemCount: task.referenceInjection.itemCount, - directReferenceTaskIds: task.referenceInjection.directReferenceTaskIds, - 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, - createdAt: task.createdAt, - updatedAt: task.updatedAt, - startedAt: task.startedAt, - finishedAt: task.finishedAt, - readAt: task.readAt, - currentAttempt: task.currentAttempt, - currentMode: task.currentMode, - judgeFailCount: task.judgeFailCount, - judgeFailRetryLimit, - codexThreadId: task.codexThreadId, - activeTurnId: task.activeTurnId, - lastError: task.lastError, - lastJudge: task.lastJudge === null ? null : { - decision: task.lastJudge.decision, - confidence: task.lastJudge.confidence, - source: task.lastJudge.source, - reason: safePreview(task.lastJudge.reason, 260), - }, - cancelRequested: task.cancelRequested, - terminalUnread: terminalTaskUnread(task), - outputCount: task.output.length, - eventCount: task.events.length, - attemptCount: task.attempts.length, - timing, - } as unknown as JsonValue; - } - return { - id: task.id, - queueId: queueIdOf(task), - queueEnteredAt: taskQueueEnteredAt(task), - prompt: safePreview(displayPrompt, 2000), - basePrompt: safePreview(task.basePrompt, 2000), - displayPrompt: safePreview(displayPrompt, 2000), - 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, - createdAt: task.createdAt, - updatedAt: task.updatedAt, - startedAt: task.startedAt, - finishedAt: task.finishedAt, - readAt: task.readAt, - currentAttempt: task.currentAttempt, - currentMode: task.currentMode, - judgeFailCount: task.judgeFailCount, - judgeFailRetryLimit, - codexThreadId: task.codexThreadId, - activeTurnId: task.activeTurnId, - lastError: task.lastError, - lastJudge: task.lastJudge, - cancelRequested: task.cancelRequested, - terminalUnread: terminalTaskUnread(task), - outputCount: task.output.length, - eventCount: task.events.length, - attemptCount: task.attempts.length, - attempts: task.attempts.slice(-3), - timing, - } as unknown as JsonValue; -} - -function perQueueSummaries(tasks: QueueTask[] = state.tasks): JsonValue[] { - const summaries = new Map; - unreadTerminal: number; - activeTaskId: string | null; - runnableTaskId: string | null; - createdAt: string | null; - 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, - activeTaskId: activeRuns.get(queue.id)?.taskId ?? null, - runnableTaskId: null, - createdAt: queue.createdAt, - updatedAt: queue.updatedAt, - }); - } - 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, - activeTaskId: activeRuns.get(queueId)?.taskId ?? null, - runnableTaskId: null, - createdAt: null, - updatedAt: null, - }; - summaries.set(queueId, summary); - } - summary.total += 1; - summary.counts[task.status] = (summary.counts[task.status] ?? 0) + 1; - if (terminalTaskUnread(task)) summary.unreadTerminal += 1; - } - for (const [queueId, summary] of summaries) { - const activeRun = activeRuns.get(queueId); - const head = queueHeadTask(queueId); - summary.activeTaskId = activeRun?.taskId ?? (head !== null && (head.status === "running" || head.status === "judging") ? head.id : null); - summary.runnableTaskId = head !== null && queueTaskIsRunnable(head) ? head.id : null; - } - 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, - activeTaskId: summary.activeTaskId, - runnableTaskId: summary.runnableTaskId, - processing: processingQueues.has(queueId), - createdAt: summary.createdAt, - updatedAt: summary.updatedAt, - } as unknown as JsonValue; - }); - return rows; -} - -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 = 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 tasks) { - if (task.status === "running" || task.status === "judging") activeTaskIdSet.add(task.id); - } - const activeTaskIds = Array.from(activeTaskIdSet).sort(); - const activeTaskId = activeTaskIds[0] ?? tasks.find((task) => task.status === "running" || task.status === "judging")?.id ?? null; - const queues = perQueueSummaries(tasks); - const summary: Record = { - total: tasks.length, - defaultQueueId, - queueCount: queues.length, - queues, - activeQueueIds: activeRunSlots, - processingQueueIds: Array.from(processingQueues).sort(), - activeRunSlotCount: activeRunSlots.length, - activeRunSlotWaiters: activeRunSlotWaiterSummaries(), - activeTaskIds, - activeTaskId, - processing, - counts, - unreadTerminal, - judgeConfigured: config.minimaxApiKey.length > 0, - 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, - configured: notificationTargetConfigured(), - targetType: config.notifyClaudeQqTargetType, - target: notificationTargetLabel(), - baseUrl: config.notifyClaudeQqBaseUrl, - maxResponseChars: config.notifyClaudeQqMaxResponseChars, - timeoutMs: config.notifyClaudeQqTimeoutMs, - sendAttempts: config.notifyClaudeQqSendAttempts, - retryIntervalMs: config.notifyClaudeQqRetryIntervalMs, - outbox: claudeQqNotificationOutboxStats(), - }, - }, - storage: { - primary: "postgres", - postgresConfigured: true, - postgresReady: databaseReady, - dirtyTaskCount: dirtyDatabaseTaskIds.size, - lastError: databaseLastError, - 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"; } @@ -4765,919 +1744,6 @@ function formatDurationMs(value: number | null): string { return parts.join(" "); } -function queueNotificationStats(): Record { - const counts = state.tasks.reduce>((memo, task) => { - memo[task.status] = (memo[task.status] ?? 0) + 1; - return memo; - }, {}); - const activeTaskIds = Array.from(new Set([ - ...Array.from(activeRuns.values()).map((run) => run.taskId), - ...state.tasks.filter((task) => task.status === "running" || task.status === "judging").map((task) => task.id), - ])).sort(); - const activeTask = activeTaskIds.length > 0 ? state.tasks.find((task) => task.id === activeTaskIds[0]) ?? null : null; - const queued = (counts.queued ?? 0) + (counts.retry_wait ?? 0); - const running = (counts.running ?? 0) + (counts.judging ?? 0); - return { - running, - queued, - retryWait: counts.retry_wait ?? 0, - judging: counts.judging ?? 0, - total: state.tasks.length, - queueCount: perQueueSummaries().length, - processingQueueCount: processingQueues.size, - activeRunCount: activeRuns.size, - activeRunSlotCount: activeRunSlotCount(), - activeTaskIds, - activeTaskElapsed: activeTask === null ? "-" : formatDurationMs(durationMsBetween(activeTask.startedAt ?? activeTask.createdAt, nowIso())), - }; -} - -function notificationTargetConfigured(): boolean { - if (!config.notifyClaudeQqEnabled) return false; - return config.notifyClaudeQqTargetType === "group" - ? config.notifyClaudeQqGroupId.length > 0 - : config.notifyClaudeQqUserId.length > 0; -} - -function claudeQqTargetPayload(message: string): Record { - if (config.notifyClaudeQqTargetType === "group") { - return { targetType: "group", groupId: config.notifyClaudeQqGroupId, message }; - } - return { targetType: "private", userId: config.notifyClaudeQqUserId, message }; -} - -function notificationTargetLabel(): string { - return config.notifyClaudeQqTargetType === "group" - ? `group:${config.notifyClaudeQqGroupId || "-"}` - : `private:${config.notifyClaudeQqUserId || "-"}`; -} - -function truncateNotificationText(value: string, maxChars: number): string { - if (value.length <= maxChars) return value; - 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 { - const last = lastAssistantMessage(task) as Record; - const text = typeof last.text === "string" ? last.text.trimEnd() : ""; - if (text.trim().length > 0) return text; - if (typeof task.lastError === "string" && task.lastError.trim().length > 0) return `(没有最终 assistant response;lastError: ${task.lastError.trim()})`; - return "(没有最终 assistant response)"; -} - -function taskNotificationKey(task: QueueTask): string { - return `${task.id}:${task.status}:${task.finishedAt ?? task.updatedAt}:${task.attempts.length}`; -} - -function rememberTaskNotificationKey(key: string): void { - sentTaskNotificationKeys.add(key); - while (sentTaskNotificationKeys.size > 1000) { - const oldest = sentTaskNotificationKeys.values().next().value as string | undefined; - if (oldest === undefined) break; - sentTaskNotificationKeys.delete(oldest); - } -} - -function claudeQqNotificationId(kind: string, dedupKey: string): string { - return `${kind}:${dedupKey}`; -} - -function claudeQqNotificationOutboxStats(): Record { - const pending = claudeQqNotificationOutbox.items.filter((item) => item.sentAt === null); - const failed = pending.filter((item) => item.lastError !== null); - const oldestPending = pending.reduce((oldest, item) => { - if (oldest === null) return item.createdAt; - return (timestampMs(item.createdAt) ?? 0) < (timestampMs(oldest) ?? 0) ? item.createdAt : oldest; - }, null); - return { - storage: "postgres", - total: claudeQqNotificationOutbox.items.length, - pending: pending.length, - failed: failed.length, - sent: claudeQqNotificationOutbox.items.length - pending.length, - inFlight: claudeQqNotificationDrainInFlight, - nextDueAt: pending - .map((item) => item.nextAttemptAt) - .sort((left, right) => (timestampMs(left) ?? 0) - (timestampMs(right) ?? 0))[0] ?? null, - oldestPendingAt: oldestPending, - }; -} - -function pruneClaudeQqNotificationOutbox(): string[] { - const beforeIds = new Set(claudeQqNotificationOutbox.items.map((item) => item.id)); - const pending = claudeQqNotificationOutbox.items.filter((item) => item.sentAt === null); - const sent = claudeQqNotificationOutbox.items - .filter((item) => item.sentAt !== null) - .sort((left, right) => (timestampMs(right.sentAt) ?? 0) - (timestampMs(left.sentAt) ?? 0)); - const sentBudget = Math.max(0, config.notifyClaudeQqMaxOutboxItems - pending.length); - claudeQqNotificationOutbox.items = [...pending, ...sent.slice(0, sentBudget)] - .sort((left, right) => (timestampMs(left.createdAt) ?? 0) - (timestampMs(right.createdAt) ?? 0)); - const afterIds = new Set(claudeQqNotificationOutbox.items.map((item) => item.id)); - return Array.from(beforeIds).filter((id) => !afterIds.has(id)); -} - -function claudeQqNotificationRetryDelayMs(attempts: number): number { - const exponent = Math.max(0, Math.min(8, attempts - 1)); - return Math.min(30 * 60_000, config.notifyClaudeQqRetryIntervalMs * (2 ** exponent)); -} - -function scheduleClaudeQqNotificationDrain(delayMs = config.notifyClaudeQqRetryIntervalMs): void { - if (!notificationTargetConfigured() || shutdownRequested) return; - if (claudeQqNotificationOutbox.items.every((item) => item.sentAt !== null)) return; - if (claudeQqNotificationDrainTimer !== null) return; - claudeQqNotificationDrainTimer = setTimeout(() => { - claudeQqNotificationDrainTimer = null; - void drainClaudeQqNotificationOutbox("timer").catch((error) => logger("warn", "claudeqq_notify_outbox_drain_failed", { trigger: "timer", error: errorToJson(error) })); - }, Math.max(1000, delayMs)); -} - -async function enqueueClaudeQqNotification(kind: string, dedupKey: string, message: string): Promise { - if (!notificationTargetConfigured()) return false; - const id = claudeQqNotificationId(kind, dedupKey); - const existing = claudeQqNotificationOutbox.items.find((item) => item.id === id); - if (existing !== undefined && existing.sentAt !== null) return false; - const at = nowIso(); - let item: ClaudeQqNotificationItem; - if (existing !== undefined) { - existing.message = message; - existing.target = notificationTargetLabel(); - existing.updatedAt = at; - existing.nextAttemptAt = at; - existing.lastError = null; - item = existing; - } else { - item = { - id, - kind, - dedupKey, - target: notificationTargetLabel(), - message, - createdAt: at, - updatedAt: at, - attempts: 0, - nextAttemptAt: at, - lastError: null, - sentAt: null, - }; - claudeQqNotificationOutbox.items.push(item); - } - await persistClaudeQqNotificationItem(item); - void drainClaudeQqNotificationOutbox(`enqueue:${kind}`).catch((error) => logger("warn", "claudeqq_notify_outbox_drain_failed", { trigger: `enqueue:${kind}`, error: errorToJson(error) })); - return true; -} - -function dueClaudeQqNotificationItems(limit = 3): ClaudeQqNotificationItem[] { - const nowMs = Date.now(); - return claudeQqNotificationOutbox.items - .filter((item) => item.sentAt === null && (timestampMs(item.nextAttemptAt) ?? 0) <= nowMs) - .sort((left, right) => (timestampMs(left.nextAttemptAt) ?? 0) - (timestampMs(right.nextAttemptAt) ?? 0)) - .slice(0, limit); -} - -async function drainClaudeQqNotificationOutbox(trigger = "manual"): Promise> { - if (!notificationTargetConfigured()) return { ok: true, trigger, skipped: "not_configured" }; - if (claudeQqNotificationDrainInFlight) return { ok: true, trigger, skipped: "in_flight" }; - claudeQqNotificationDrainInFlight = true; - let sent = 0; - let failed = 0; - try { - await loadClaudeQqNotificationOutboxFromDatabase(); - const due = dueClaudeQqNotificationItems(); - for (const item of due) { - item.attempts += 1; - item.updatedAt = nowIso(); - await persistClaudeQqNotificationItem(item); - try { - await postClaudeQqText(item.kind, item.message); - item.sentAt = nowIso(); - item.updatedAt = item.sentAt; - item.lastError = null; - sent += 1; - if (item.kind === "task_terminal") rememberTaskNotificationKey(item.dedupKey); - logger("info", "claudeqq_notify_outbox_sent", { id: item.id, kind: item.kind, target: item.target, attempts: item.attempts, trigger }); - } catch (error) { - failed += 1; - const message = error instanceof Error ? error.message : String(error); - item.lastError = safePreview(message, 1000); - item.updatedAt = nowIso(); - item.nextAttemptAt = new Date(Date.now() + claudeQqNotificationRetryDelayMs(item.attempts)).toISOString(); - logger("warn", "claudeqq_notify_outbox_retry_scheduled", { - id: item.id, - kind: item.kind, - target: item.target, - attempts: item.attempts, - nextAttemptAt: item.nextAttemptAt, - trigger, - error: errorToJson(error), - }); - } finally { - await persistClaudeQqNotificationItem(item); - } - } - } finally { - claudeQqNotificationDrainInFlight = false; - await persistClaudeQqNotificationOutbox(); - if (claudeQqNotificationOutbox.items.some((item) => item.sentAt === null)) scheduleClaudeQqNotificationDrain(); - } - return { ok: true, trigger, sent, failed, outbox: claudeQqNotificationOutboxStats() }; -} - -async function postClaudeQqText(kind: string, message: string): Promise { - if (!notificationTargetConfigured()) return; - const url = `${config.notifyClaudeQqBaseUrl}/api/push/text`; - let lastError: unknown = null; - for (let attempt = 1; attempt <= config.notifyClaudeQqSendAttempts; attempt += 1) { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), config.notifyClaudeQqTimeoutMs); - let responseText = ""; - try { - const response = await fetch(url, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(claudeQqTargetPayload(message)), - signal: controller.signal, - }); - responseText = await response.text(); - if (!response.ok) throw new Error(`ClaudeQQ proxy returned HTTP ${response.status}: ${safePreview(responseText, 500)}`); - try { - const parsed = JSON.parse(responseText) as Record; - if (parsed.ok === false || parsed.success === false || parsed.status === "napcat_offline") { - throw new Error(`ClaudeQQ push failed: ${safePreview(responseText, 500)}`); - } - } catch (error) { - if (error instanceof SyntaxError) { - // Some deployments return plain text; HTTP 2xx is still accepted. - } else { - throw error; - } - } - logger("info", "claudeqq_notify_sent", { kind, target: notificationTargetLabel(), attempt, chars: message.length, responsePreview: safePreview(responseText, 500) }); - return; - } catch (error) { - lastError = error; - if (attempt >= config.notifyClaudeQqSendAttempts) break; - const delayMs = Math.min(30_000, 1000 * (2 ** (attempt - 1))); - logger("warn", "claudeqq_notify_retry", { kind, target: notificationTargetLabel(), attempt, nextDelayMs: delayMs, error: errorToJson(error) }); - await Bun.sleep(delayMs); - } finally { - clearTimeout(timer); - } - } - if (lastError !== null) { - throw lastError instanceof Error ? lastError : new Error(String(lastError)); - } -} - -function taskTerminalNotificationMessage(task: QueueTask): string { - const stats = queueNotificationStats(); - const totalElapsed = formatDurationMs(durationMsBetween(task.createdAt, task.finishedAt ?? task.updatedAt)); - const runElapsed = formatDurationMs(durationMsBetween(task.startedAt ?? task.createdAt, task.finishedAt ?? task.updatedAt)); - const response = truncateNotificationText(taskFinalResponseForNotification(task), config.notifyClaudeQqMaxResponseChars); - return [ - "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}`, - `current queue: running=${stats.running}, queued=${stats.queued}, retry_wait=${stats.retryWait}`, - "", - "Final response:", - response, - ].join("\n"); -} - -async function notifyTaskTerminal(task: QueueTask): Promise { - if (!terminalTask(task) || !notificationTargetConfigured()) return; - const key = taskNotificationKey(task); - if (sentTaskNotificationKeys.has(key) || inFlightTaskNotificationKeys.has(key)) return; - inFlightTaskNotificationKeys.add(key); - try { - await enqueueClaudeQqNotification("task_terminal", key, taskTerminalNotificationMessage(task)); - } catch (error) { - logger("warn", "claudeqq_task_notify_failed", { taskId: task.id, status: task.status, target: notificationTargetLabel(), error: errorToJson(error) }); - } finally { - inFlightTaskNotificationKeys.delete(key); - } -} - -async function backfillClaudeQqTaskNotifications(since: string | null, limit: number, dryRun: boolean): Promise> { - if (!notificationTargetConfigured()) return { ok: true, skipped: "not_configured" }; - await loadClaudeQqNotificationOutboxFromDatabase(); - const sinceMs = timestampMs(since); - const candidates = state.tasks - .filter((task) => terminalTask(task)) - .filter((task) => sinceMs === null || (timestampMs(task.finishedAt ?? task.updatedAt) ?? 0) > sinceMs) - .sort((left, right) => (timestampMs(left.finishedAt ?? left.updatedAt) ?? 0) - (timestampMs(right.finishedAt ?? right.updatedAt) ?? 0)) - .slice(0, limit); - const items = candidates.map((task) => ({ - taskId: task.id, - status: task.status, - queueId: queueIdOf(task), - finishedAt: task.finishedAt ?? task.updatedAt, - dedupKey: taskNotificationKey(task), - alreadyInOutbox: claudeQqNotificationOutbox.items.some((item) => item.id === claudeQqNotificationId("task_terminal", taskNotificationKey(task))), - })); - if (dryRun) return { ok: true, dryRun, since: since ?? null, scanned: candidates.length, enqueued: 0, items }; - let enqueued = 0; - for (const task of candidates) { - if (await enqueueClaudeQqNotification("task_terminal", taskNotificationKey(task), taskTerminalNotificationMessage(task))) enqueued += 1; - } - return { ok: true, dryRun, since: since ?? null, scanned: candidates.length, enqueued, outbox: claudeQqNotificationOutboxStats(), items }; -} - -function armIdleNotification(): void { - if (notificationTargetConfigured()) idleNotificationSent = false; -} - -async function maybeNotifyQueueIdle(triggerTaskId: string | null = null): Promise { - if (!notificationTargetConfigured() || idleNotificationSent || idleNotificationInFlight) return; - const stats = queueNotificationStats(); - if (Number(stats.running) !== 0 || Number(stats.queued) !== 0 || processingQueues.size !== 0 || activeRuns.size !== 0) return; - idleNotificationInFlight = true; - try { - const message = [ - "Code Queue 已空闲", - "running=0, queued=0", - `total tasks=${stats.total}, queues=${stats.queueCount}`, - triggerTaskId === null ? "" : `last task=${triggerTaskId}`, - ].filter((line) => line.length > 0).join("\n"); - await enqueueClaudeQqNotification("queue_idle", `queue_idle:${triggerTaskId ?? "unknown"}:${stats.total}`, message); - idleNotificationSent = true; - } catch (error) { - logger("warn", "claudeqq_idle_notify_failed", { triggerTaskId: triggerTaskId ?? "", target: notificationTargetLabel(), error: errorToJson(error) }); - } finally { - idleNotificationInFlight = false; - } -} - -function textInput(text: string): JsonValue[] { - return [{ type: "text", text, text_elements: [] }]; -} - -function extractRecord(value: unknown): Record | null { - return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : null; -} - -function extractString(value: unknown, key: string): string | null { - const record = extractRecord(value); - const field = record?.[key]; - return typeof field === "string" ? field : null; -} - -class AppServerClient { - private child: ChildProcessWithoutNullStreams; - private nextId = 1; - private pending = new Map void; reject: (error: Error) => void }>(); - private stderrChunks: Buffer[] = []; - private closed = false; - private exitInfo: AppServerExit | null = null; - private closeResolve!: (value: AppServerExit) => void; - readonly closedPromise: Promise; - - constructor(private readonly task: QueueTask, private readonly onNotification: (message: Record) => void) { - this.closedPromise = new Promise((resolveClosed) => { this.closeResolve = resolveClosed; }); - 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(); - }); - 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)); - } - - async initialize(): Promise { - await this.request("initialize", { - clientInfo: { name: "unidesk_code_queue", title: "UniDesk Code Queue", version: "0.1.0" }, - capabilities: { experimentalApi: true }, - }); - this.notify("initialized", {}); - } - - async startOrResumeThread(): Promise { - if (this.task.codexThreadId !== null) { - appendOutput(this.task, "system", `thread resume requested ${this.task.codexThreadId}\n`, "thread/resume"); - const response = await this.request("thread/resume", { - threadId: this.task.codexThreadId, - model: this.task.model, - cwd: this.task.cwd, - approvalPolicy: config.approvalPolicy, - sandbox: config.sandbox, - }); - const threadId = extractString(extractRecord(response)?.thread, "id"); - if (threadId === null) throw new Error("thread/resume response did not include thread.id"); - appendOutput(this.task, "system", `thread resumed ${threadId}\n`, "thread/resume"); - return threadId; - } - const response = await this.request("thread/start", { - model: this.task.model, - cwd: this.task.cwd, - approvalPolicy: config.approvalPolicy, - sandbox: config.sandbox, - 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"); - return threadId; - } - - async startTurn(threadId: string, prompt: string): Promise { - const params: Record = { - threadId, - input: textInput(prompt), - cwd: this.task.cwd, - approvalPolicy: config.approvalPolicy, - model: this.task.model, - }; - const effort = resolveReasoningEffort(this.task.model, this.task.reasoningEffort); - if (effort !== null) params.effort = effort; - const response = await this.request("turn/start", params); - const turnId = extractString(extractRecord(response)?.turn, "id"); - if (turnId === null) throw new Error("turn/start response did not include turn.id"); - return turnId; - } - - async steer(threadId: string, turnId: string, prompt: string): Promise { - await this.request("turn/steer", { threadId, expectedTurnId: turnId, input: textInput(prompt) }); - } - - async interrupt(threadId: string, turnId: string): Promise { - await this.request("turn/interrupt", { threadId, turnId }); - } - - stop(): void { - if (this.closed) return; - this.child.kill("SIGTERM"); - setTimeout(() => { - if (!this.closed) this.child.kill("SIGKILL"); - }, 1500).unref?.(); - } - - private request(method: string, params: unknown): Promise { - if (this.closed) return Promise.reject(new Error("app-server is already closed")); - const id = this.nextId++; - const message = { method, id, params }; - return new Promise((resolve, reject) => { - this.pending.set(id, { resolve, reject }); - this.write(message); - }); - } - - private notify(method: string, params: unknown): void { - this.write({ method, params }); - } - - private write(message: unknown): void { - this.child.stdin.write(`${JSON.stringify(message)}\n`); - } - - private async readLines(rl: readline.Interface): Promise { - try { - for await (const line of rl) { - const trimmed = String(line).trim(); - if (trimmed.length === 0) continue; - const message = JSON.parse(trimmed) as Record; - this.handleMessage(message); - } - } catch (error) { - appendOutput(this.task, "error", `app-server stream error: ${error instanceof Error ? error.message : String(error)}\n`, "app-server"); - } - } - - private handleMessage(message: Record): void { - const id = typeof message.id === "number" ? message.id : null; - const method = typeof message.method === "string" ? message.method : null; - if (id !== null && method === null) { - const pending = this.pending.get(id); - if (pending === undefined) return; - this.pending.delete(id); - if ("error" in message) { - pending.reject(new Error(JSON.stringify(message.error))); - } else { - pending.resolve(message.result); - } - return; - } - if (id !== null && method !== null) { - this.handleServerRequest(id, method); - return; - } - if (method !== null) this.onNotification(message); - } - - private handleServerRequest(id: number, method: string): void { - if (method === "item/commandExecution/requestApproval") { - this.write({ id, result: { decision: "decline" } }); - return; - } - if (method === "item/fileChange/requestApproval") { - this.write({ id, result: { decision: "decline" } }); - return; - } - this.write({ id, error: { code: -32601, message: `Unsupported client-side request: ${method}` } }); - } - - private handleClose(code: number | null, signal: string | null): void { - if (this.closed) return; - this.closed = true; - this.exitInfo = { code, signal, stderrTail: Buffer.concat(this.stderrChunks).toString("utf8").slice(-8000) }; - for (const pending of this.pending.values()) pending.reject(new Error(`app-server closed with code=${code} signal=${signal}`)); - this.pending.clear(); - this.closeResolve(this.exitInfo); - } -} - -function eventSummary(message: Record): CodexEventSummary { - const params = extractRecord(message.params); - const item = extractRecord(params?.item); - const turn = extractRecord(params?.turn); - const error = extractRecord(turn?.error) ?? extractRecord(params?.error); - return { - at: nowIso(), - method: typeof message.method === "string" ? message.method : "unknown", - itemType: typeof item?.type === "string" ? item.type : undefined, - status: typeof item?.status === "string" ? item.status : typeof turn?.status === "string" ? turn.status : undefined, - message: typeof error?.message === "string" ? safePreview(error.message, 600) : undefined, - textPreview: typeof item?.text === "string" ? safePreview(item.text, 800) : undefined, - }; -} - -function commandCompletionStreams(item: Record | null): { stdout: string; stderr: string; output: string; exitCode: number | null } { - if (item === null) return { stdout: "", stderr: "", output: "", exitCode: null }; - const result = extractRecord(item.result); - const stdout = recordStringField(item, ["stdout", "stdoutText"]) || recordStringField(result, ["stdout", "stdoutText"]); - const stderr = recordStringField(item, ["stderr", "stderrText"]) || recordStringField(result, ["stderr", "stderrText"]); - const output = recordStringField(item, ["output", "outputText", "text"]) || recordStringField(result, ["output", "outputText", "text"]); - const exitCode = recordNumberField(item, ["exitCode", "code"]) ?? recordNumberField(result, ["exitCode", "code"]); - return { stdout: stdout || (stderr.length > 0 ? "" : output), stderr, output: output || [stdout, stderr].filter(Boolean).join("\n"), exitCode }; -} - -function hasRecentCommandOutputDelta(task: QueueTask, itemId: string | undefined): boolean { - if (itemId === undefined) return false; - for (let index = task.output.length - 1; index >= 0 && index >= task.output.length - 80; index -= 1) { - const output = task.output[index]; - if (output?.itemId !== itemId) continue; - if (output.method === "item/commandExecution/outputDelta") return true; - if (output.method === "item/started") return false; - } - return false; -} - -function handleNotification(task: QueueTask, message: Record, terminal: (status: TerminalStatus, error: string | null) => void): void { - const method = typeof message.method === "string" ? message.method : "unknown"; - const params = extractRecord(message.params); - addEvent(task, eventSummary(message)); - if (method === "thread/started") { - const threadId = extractString(extractRecord(params?.thread), "id"); - if (threadId !== null) task.codexThreadId = threadId; - appendOutput(task, "system", `thread started ${threadId ?? "unknown"}\n`, method); - return; - } - if (method === "turn/started") { - const turnId = extractString(extractRecord(params?.turn), "id"); - task.activeTurnId = turnId; - appendOutput(task, "system", `turn started ${turnId ?? "unknown"}\n`, method); - return; - } - if (method === "item/agentMessage/delta") { - appendOutput(task, "assistant", String(params?.delta ?? ""), method, extractString(params, "itemId") ?? undefined, true); - return; - } - if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") { - appendOutput(task, "reasoning", String(params?.delta ?? ""), method, extractString(params, "itemId") ?? undefined, true); - return; - } - if (method === "item/commandExecution/outputDelta") { - appendOutput(task, "command", String(params?.delta ?? ""), method, extractString(params, "itemId") ?? undefined, true); - return; - } - if (method === "item/fileChange/outputDelta") { - appendOutput(task, "diff", String(params?.delta ?? ""), method, extractString(params, "itemId") ?? undefined, true); - return; - } - if (method === "item/started" || method === "item/completed") { - const item = extractRecord(params?.item); - const type = String(item?.type ?? "item"); - if (type === "agentMessage" && typeof item?.text === "string") task.finalResponse = item.text; - if (type === "commandExecution") { - const itemId = extractString(item, "id") ?? undefined; - if (method === "item/completed") { - const streams = commandCompletionStreams(item); - const hasDelta = hasRecentCommandOutputDelta(task, itemId); - const completedOutput = hasDelta && streams.stderr.trim().length > 0 - ? `\n[stderr]\n${streams.stderr.trimEnd()}` - : hasDelta - ? "" - : formatCommandOutput({ callId: itemId ?? "", at: nowIso(), stdout: streams.stdout, stderr: streams.stderr, output: streams.output, exitCode: streams.exitCode }); - if (completedOutput.trim().length > 0) appendOutput(task, "command", `${completedOutput.trimEnd()}\n`, "item/commandExecution/outputDelta", itemId, true); - } - appendOutput(task, "command", `${method}: ${String(item?.command ?? "command")} status=${String(item?.status ?? "unknown")}\n`, method, itemId); - } - if (type === "fileChange") appendOutput(task, "diff", `${method}: file changes status=${String(item?.status ?? "unknown")}\n`, method, extractString(item, "id") ?? undefined); - if (type === "mcpToolCall" || type === "webSearch" || type === "dynamicToolCall") appendOutput(task, "tool", `${method}: ${type}\n`, method, extractString(item, "id") ?? undefined); - return; - } - if (method === "error") { - const error = extractRecord(params?.error); - appendOutput(task, "error", `${String(error?.message ?? "Codex error")}\n`, method); - return; - } - if (method === "turn/completed") { - const turn = extractRecord(params?.turn); - const status = terminalStatus(String(turn?.status ?? "failed")); - const error = extractRecord(turn?.error); - task.activeTurnId = null; - appendOutput(task, status === "completed" ? "system" : "error", `turn completed status=${status ?? "unknown"}\n`, method); - terminal(status, typeof error?.message === "string" ? error.message : null); - } -} - -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 [ @@ -5704,1072 +1770,218 @@ function codexFreshRecoveryPrompt(task: QueueTask, prompt: string, reason: strin ].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[] = []; - let terminalSeen = false; - let lastAppActivityAt = Date.now(); - let terminalResult: { status: TerminalStatus; error: string | null } = { status: null, error: null }; - let terminalResolve!: () => 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(); - const before = task.events.length; - handleNotification(task, message, (status, error) => { - terminalSeen = true; - terminalResult = { status, error }; - terminalResolve(); - }); - events.push(...task.events.slice(before)); - }); - try { - await app.initialize(); - const threadId = await app.startOrResumeThread(); - task.codexThreadId = threadId; - activeRuns.set(queueId, { taskId: task.id, queueId, app, port: "codex", threadId, turnId: null }); - const turnId = await app.startTurn(threadId, prompt); - task.activeTurnId = turnId; - const run = activeRuns.get(queueId); - if (run?.app === app) run.turnId = turnId; - persistTaskState(task); - const activityWatchdog = setInterval(() => { - if (terminalSeen) return; - const idleMs = Date.now() - lastAppActivityAt; - if (idleMs < config.turnNoActivityTimeoutMs) return; - const message = `No Codex app activity for ${Math.round(idleMs / 1000)}s; stopping app-server so the existing thread can retry.`; - appendOutput(task, "error", `${message}\n`, "turn/no-activity-watchdog"); - logger("warn", "turn_no_activity_watchdog", { taskId: task.id, turnId, idleMs, timeoutMs: config.turnNoActivityTimeoutMs }); - app.stop(); - }, 15_000); - const race = await Promise.race([terminalPromise.then(() => "terminal" as const), app.closedPromise.then(() => "closed" as const)]); - clearInterval(activityWatchdog); - const closedBeforeTerminal = race === "closed" && !terminalSeen; - if (terminalSeen) app.stop(); - const exit = await app.closedPromise; - return { - threadId, - turnId, - finalResponse: task.finalResponse, - terminalStatus: terminalResult.status, - terminalError: terminalResult.error, - transportClosedBeforeTerminal: closedBeforeTerminal, - appServerExit: exit, - events, - }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - appendOutput(task, "error", `${message}\n`, "app-server"); - app.stop(); - const exit = await app.closedPromise; - return { - threadId: task.codexThreadId, - turnId: task.activeTurnId, - finalResponse: task.finalResponse, - terminalStatus: "failed", - terminalError: message, - transportClosedBeforeTerminal: !terminalSeen, - appServerExit: exit, - events, - }; - } finally { - if (activeRuns.get(queueId)?.app === app) activeRuns.delete(queueId); - app.stop(); - } -} -function fallbackJudge(result: CodexRunResult, minimaxError?: string): JudgeResult { - if (result.transportClosedBeforeTerminal || result.terminalStatus === null) { - return { decision: "retry", confidence: 0.75, reason: "Codex app-server 在 turn/completed 之前关闭。", continuePrompt: retryInstruction, source: "fallback" }; - } - if (result.terminalStatus === "failed") { - return { decision: "retry", confidence: 0.7, reason: result.terminalError ? `Codex turn 失败:${result.terminalError}` : "Codex turn 失败。", continuePrompt: retryInstruction, source: "fallback" }; - } - 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 当作任务已完成。`, - source: "fallback", - }; - } - return { decision: "complete", confidence: 0.65, reason: "Codex 输出了 completed 状态的 turn/completed,且未配置 MiniMax judge。", source: "fallback" }; -} +configureProviderRuntime({ + config, + safePreview, +}); -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 查询验证。"; +configureTaskOutput({ + config, + allocateSeq: () => state.nextSeq++, + errorToJson, + logger, + markTaskDirty, + nowIso, + schedulePersistState, +}); -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"))); -} +configureTaskView({ + config, + errorToJson, + jsonResponse, + logger, + mergePromptHistory, + nowIso, + outputPromptHistory, + pageBySeq, + parseLimit, + parseSeqParam, + queueIdOf, + queuedStatusReason, + queuedTaskPromptEditable, + taskQueueEnteredAt, +}); -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); -} +configureNotifications({ + config, + activeRunCount: () => activeRuns.size, + activeRunSlotCount, + activeRunTaskIds: () => Array.from(activeRuns.values()).map((run) => run.taskId), + databaseReady: () => databaseReady, + errorToJson, + hasRunnableTask, + lastAssistantMessage: (task) => lastAssistantMessage(task), + loadAllTasksForRead, + logger, + nonNegativeElapsed, + nowIso, + processingQueueCount: () => processingQueues.size, + queueCount: () => perQueueSummaries().length, + queueIdOf, + safePreview, + shutdownRequested: () => shutdownRequested, + sql, + taskTimestamp, + tasks: () => state.tasks, + timestampMs, +}); -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"); -} +configureQueueApi({ + config, + activeRunSlotQueueIds, + activeRunSlotWaiterSummaries, + activeRuns, + codexSqliteLogExporter: () => codexSqliteLogExporter as unknown as Record, + collectDevReady, + compactJsonResponse, + databaseLastError: () => databaseLastError, + databaseReady: () => databaseReady, + defaultQueueId, + dirtyDatabaseTaskCount: () => dirtyDatabaseTaskIds.size, + jsonResponse, + judgeFailRetryLimit, + loadTaskFromDatabase, + loadTasksFromDatabase, + loadTasksFromDatabaseByIds, + pageBySeq, + parseLimit, + parseTextLimit, + processing: () => processing, + processingQueues, + queueHeadTask, + queueIdOf, + queues: () => state.queues, + queueTaskIsRunnable, + queuedStatusReason, + queuedTaskPromptEditable, + runGarbageCollection, + safeQueueId, + safeQueueName, + sql, + taskQueueEnteredAt, + tasks: () => state.tasks, + truthyParam, +}); -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); -} +configureReferences({ + addUniqueTaskId, + findTask, + nowIso, + referenceInjectionMaxRounds, + referenceTaskIdsFromPrompt, +}); -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 继续。不要把更早 attempt 的限流/中断证据自动当作当前 attempt 的完成门禁;如果当前最新 attempt 已经提供完整完成证据,可以判定 complete。", - schema: { decision: "complete|retry|fail", confidence: "0..1", reason: "中文短句", continuePrompt: "decision=retry 时必填,除非确实没有可用的继续提示;内容必须是中文,保持简洁,不要粘贴原始任务、引用上下文、transcript 或 JSON" }, - originalTask: originalUserTask, - resolvedPromptForCodex, - attempt: task.currentAttempt, - maxAttempts: task.maxAttempts, - executionRecord: { - terminalStatus: result.terminalStatus, - terminalError: result.terminalError, - transportClosedBeforeTerminal: result.transportClosedBeforeTerminal, - appServerExitCode: result.appServerExit.code, - appServerSignal: result.appServerExit.signal, - stderrTail: safePreview(result.appServerExit.stderrTail, 2000), - finalResponse: safePreview(result.finalResponse, 6000), - finalResponseChars: result.finalResponse.length, - finalResponseMissing: result.finalResponse.trim().length === 0, - latestAttempt, - judgeFailCount: task.judgeFailCount, - judgeFailRetryLimit, - 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 的反馈;只能基于 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,并在部署后验证运行中的真实行为。", - "如果受影响服务包含 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 恢复本任务后继续验证。", - ], - 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 任务尚未完成的证据。", - "判断 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。", - "对于 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 运行证据,证明请求分数和比较结果。", - "对于 hardware、firmware、provider、WSL、SSH passthrough、skill、compile、flash/download、serial 或 board-comm 任务,complete 需要请求的操作步骤证据;如果请求了 download/board/serial 验证但没有证据,只有部分文档或一次成功 build 应选择 retry。", - "如果最终回复主要处理后来的 steer prompt,而原始任务仍有部分未完成,应选择 retry,并给出同时协调 steer 与原始任务的 continuation prompt。", - ], - classicFailureExamples: [ - { - pattern: "原始任务要求:没有通用 PikaPython benchmark skill 时不能达到 4/4;有该 skill 时必须达到 4/4。", - incompleteEvidence: "执行代理只是把定向 prompt 内容移入 skill,运行了 type/component/unit 测试,并表示为节省 quota 没有启动 PikaBench-4 baseline/no-skill 实跑。", - requiredDecision: "retry", - reason: "请求的 with-skill/no-skill 经验 pipeline 对照没有运行,因此声明的验收条件没有被证明。", - }, - { - pattern: "原始任务要求学习 D601 ConStart/constar 固件工作区、使用 skill 完成 compile/download 等,并更新长期文档;后续 steer 要求把项目文档移动到 constar/docs。", - incompleteEvidence: "执行代理做了一些 skill discovery 和文档迁移,但最终回复主要围绕 steer 驱动的文档迁移,未证明所有请求的 compile/download/serial/board-comm 操作验收点已完成。", - requiredDecision: "retry", - reason: "这是原始任务未完成,不是不可重试失败;应继续同一个 session,补齐缺失的操作验证和文档。", - }, - { - pattern: "原始任务要求停止 ClaudeQQ 登录二维码自动刷新。代码已编辑且 frontend rebuild 成功,但 Codex turn 后来在 E2E 或依赖验证仍在运行时因 429 Too Many Requests / exceeded retry limit 失败结束。", - incompleteEvidence: "终止 turn 状态是 failed,最终回复为空或缺少完整总结,且验证因服务限流没有完成。", - 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 变更实际上未生效。", - requiredDecision: "retry", - reason: "WebUI 可见变更只有在 deployed/served frontend 已重建或刷新并验证后才算完成;源码编辑加 type check 可能让 live UI 保持不变。", - }, - { - 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: "原始任务要求 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 部署已修改的 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 继续。", - }, - ], - }); -} +configureSelfTests({ + config, + activeRunSlotCount, + activeRunSlotReservations, + activeRunSlotWaiters, + availableQueueStartSlotsFor, + defaultQueueId, + enqueueActiveRunSlotWaiter, + injectReferencedTaskContext, + nextRunnableTaskFrom, + normalizeTask, + nowIso, + processingQueues, + queueHeadTask, + queuedStatusReason, + removeActiveRunSlotWaiter, + resolveReasoningEffort, + updateProcessingFlag, +}); -function parseRecordJson(text: string, source: string): ParsedJudgeJson { - const parsed = JSON.parse(text) as unknown; - if (typeof parsed === "string") return parseJudgeJson(parsed); - if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${source} parsed to non-object JSON`); - return { value: parsed as Record, source }; -} +configureJudge({ + config, + logger, + safePreview, + userPromptForDisplay, + taskFullOutput, + taskReferenceIds, + extractRecord, + extractString, + promptLineCount, + judgeFailRetryLimit, +}); -function balancedJsonCandidates(text: string): string[] { - const candidates: string[] = []; - for (let start = 0; start < text.length; start += 1) { - if (text[start] !== "{") continue; - let depth = 0; - let inString = false; - let escaped = false; - for (let index = start; index < text.length; index += 1) { - const char = text[index] ?? ""; - if (inString) { - if (escaped) { - escaped = false; - } else if (char === "\\") { - escaped = true; - } else if (char === "\"") { - inString = false; - } - continue; - } - if (char === "\"") { - inString = true; - } else if (char === "{") { - depth += 1; - } else if (char === "}") { - depth -= 1; - if (depth === 0) { - candidates.push(text.slice(start, index + 1)); - break; - } - } - } - } - return candidates; -} +configureCodexPort({ + config, + activeRuns, + appendOutput, + addEvent, + ensureTaskExecutionContainer, + formatCommandOutput, + logger, + persistTaskState, + providerIsMain, + queueIdOf, + recordNumberField, + recordStringField, + remoteAppServerCommand, + resolveReasoningEffort, + safePreview, + nowIso, +}); -function judgeJsonCandidates(text: string): Array<{ source: string; text: string }> { - const normalized = text.replace(/^\uFEFF/u, "").trim(); - 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 seen = new Set(); - return candidates.filter((candidate) => { - const key = candidate.text; - if (key.length === 0 || seen.has(key)) return false; - seen.add(key); - return true; - }); -} +configureOpenCodePort({ + config, + activeRuns, + addEvent, + appendOutput, + buildDevContainerPlan, + compactRetryTaskContext, + ensureTaskExecutionContainer, + judgeReasonForPrompt, + logger, + nowIso, + openCodeFreshRecoveryPrompt, + openCodeXdgEnv, + persistTaskState, + providerIsMain, + queueIdOf, + recordStringField, + remoteHostWorkdirForTask, + safePreview, + shellQuote, + shutdownRequested: () => shutdownRequested, +}); -function parseJudgeJson(text: string): ParsedJudgeJson { - let lastError = "no candidate JSON was found"; - for (const candidate of judgeJsonCandidates(text)) { - try { - const parsed = parseRecordJson(candidate.text, candidate.source); - if (validJudgeDecisionValue(parsed.value.decision)) return parsed; - lastError = `${candidate.source} parsed JSON does not contain a valid decision`; - } catch (error) { - lastError = error instanceof Error ? error.message : String(error); - } - } - throw new Error(`MiniMax judge did not return parseable JSON after denoise: ${lastError}; preview=${safePreview(text, 500)}`); -} - -function normalizedDecision(value: unknown): JudgeDecision { - if (value === "complete" || value === "retry" || value === "fail") return value; - if (value === "continue") return "retry"; - return "retry"; -} - -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, - task.prompt, - result.finalResponse, - result.terminalError ?? "", - result.appServerExit.stderrTail, - ].join("\n"); -} - -function needsRuntimeDeploymentEvidence(text: string): boolean { - 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 { - const normalized = line.trim(); - if (normalized.length === 0) return false; - if (/(judge feedback|judge 未完成原因|上一次 judge|被判|风险|不是|并非|不再|非.*未|已(?:经)?(?:部署|上线|重建|验证)|succeeded|healthy|verified|deployed|rebuilt)/iu.test(normalized)) return false; - return /(我没有重建运行中的容器|没有重建运行中|(?:没有|未)(?:执行|进行|完成)?[^。\n]{0,50}(?:server rebuild|rebuild|重建|部署|上线|live verification|公网|UI 验证|验证)|(?:后续|如果|若|如需|下一步)[^。\n]{0,80}(?:上线|部署|重建|验证)|rebuild\/deploy as advisory|treats? rebuild\/deploy as advisory|\b(?:not|never|no)\s+(?:rebuilt?|deployed?|restarted?|verified?))/iu.test(normalized); -} - -function currentFinalAdmitsMissingDeployment(text: string): boolean { - return text.split(/\r?\n/u).some(lineAdmitsMissingDeployment); -} - -function asksToConfirmConcurrentFileInsteadOfDelivery(text: string): boolean { - const normalized = text.replace(/\s+/gu, " "); - const asksForConfirmation = /(请确认要我怎么处理|按安全规则[^。.!?]*先停下确认|需要先停下确认|please confirm (?:how|what|whether)|I need to stop[^.?!]*confirm)/iu.test(normalized); - const concurrentFileContext = /(我没有修改的文件[^。.!?]*(?:modified|变成了 modified)|没有编辑这个文件|unexpected[^.?!]*(?:modified|change)|concurrent[^.?!]*(?:file|task|work)|其他任务[^。.!?]*并发|并发开发|忽略这个文件,只继续交付|只继续交付我已改的[^。.!?]*变更)/iu.test(normalized); - return asksForConfirmation && concurrentFileContext; -} - -function concurrentFileConfirmationFeedbackPrompt(task: QueueTask, reason: string): string { - return [ - retryInstruction, - "上一次 judge 判定为 retry:上一轮没有自主完成交付,而是在中途把并发修改文件的问题抛给用户确认。", - `judge 未完成原因:${judgeReasonForPrompt(reason)}`, - "这是其他任务正在并发开发,忽略这个文件,只继续交付我已改的前端相关变更。", - "不要再要求用户选择 1/2/3,也不要检查、覆盖或回滚那个并发修改文件;只围绕自己已改的前端相关文件完成必要验证、总结交付,并在最终 response 中列出实际验证结果。", - "如果自己的前端改动需要构建、上线或运行中验证,按项目规则执行对应 rebuild/live verification 后再声明完成;如确有阻塞,请给出具体命令和错误。", - "原始任务摘要/按需查询:", - compactRetryTaskContext(task), - ].join("\n\n"); -} - -function deploymentFeedbackPrompt(task: QueueTask, reason: string): string { - return [ - retryInstruction, - "上一次 judge 判定为 retry:当前实现仍是未上线/未完成状态,不能只复述源码修改。", - `judge 未完成原因:${judgeReasonForPrompt(reason)}`, - "请先确认受影响范围;凡是 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 { - ...judge, - decision: "retry", - confidence: Math.max(judge.confidence, 0.94), - reason, - continuePrompt: concurrentFileConfirmationFeedbackPrompt(task, reason), - raw: { previous: judge.raw ?? null, _safetyOverride: "mid_task_user_confirmation_concurrent_file" }, - }; - } - if (judge.decision !== "complete") return judge; - const scopeText = judgeScopeText(task, result); - if (!needsRuntimeDeploymentEvidence(scopeText) || !currentFinalAdmitsMissingDeployment(currentFinalText)) return judge; - const reason = "最终回复承认 runtime/UI/service 变更尚未部署到运行中服务或已服务 UI;只有源码编辑和检查还不完整。"; - return { - ...judge, - decision: "retry", - confidence: Math.max(judge.confidence, 0.92), - reason, - continuePrompt: deploymentFeedbackPrompt(task, reason), - raw: { previous: judge.raw ?? null, _safetyOverride: "missing_runtime_deployment" }, - }; -} - -async function judgeTask(task: QueueTask, result: CodexRunResult): Promise { - if (config.minimaxApiKey.length === 0) return applyFallbackSafetyOverrides(task, result, fallbackJudge(result)); - const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [ - { role: "system", content: "你是严格的任务状态分类器。只能返回一个紧凑原始 JSON object。禁止输出 Markdown fence、、思考过程、解释性正文或注释。所有自然语言字段必须使用中文,尤其是 reason 和 continuePrompt。" }, - { role: "user", content: judgePrompt(task, result) }, - ]; - try { - let lastParseError: string | null = null; - for (let repairAttempt = 0; repairAttempt <= config.judgeRepairAttempts; repairAttempt += 1) { - const response = await requestMiniMaxJudge(messages); - const preDenoiseContent = response.content; - try { - const parsedResult = parseJudgeJson(preDenoiseContent); - const parsed = parsedResult.value; - if (parsedResult.source !== "direct") { - logger("info", "judge_json_denoised", { taskId: task.id, parseSource: parsedResult.source, repairAttempt }); - } - const decision = normalizedDecision(parsed.decision); - const continuePrompt = parsedContinuePromptForJudge(parsed, decision); - const confidenceRaw = Number(parsed.confidence ?? 0.5); - const judge: JudgeResult = { - decision, - confidence: Number.isFinite(confidenceRaw) ? Math.max(0, Math.min(1, confidenceRaw)) : 0.5, - reason: typeof parsed.reason === "string" ? parsed.reason : "MiniMax judge returned a decision.", - 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); - logger("warn", "judge_json_parse_retry", { - taskId: task.id, - repairAttempt: repairAttempt + 1, - maxRepairAttempts: config.judgeRepairAttempts, - error: safePreview(lastParseError, 800), - preDenoiseResponsePreview: safePreview(preDenoiseContent, 1200), - }); - messages.push({ role: "assistant", content: preDenoiseContent }); - messages.push({ - role: "user", - content: JSON.stringify({ - instruction: judgeRepairInstruction(lastParseError), - parseOrValidationError: lastParseError, - requiredSchema: { decision: "complete|retry|fail", confidence: "0..1", reason: "中文短句", continuePrompt: `decision=retry 时必填,除非确实没有可用的继续提示;内容必须是中文,目标不超过 ${compactContinuationPromptTargetChars} 字,绝对不超过 ${continuePromptSourceBudgetChars} 字` }, - previousAnswerRaw: preDenoiseContent, - }), - }); - } - } - throw new Error(lastParseError ?? "MiniMax judge exhausted JSON repair attempts"); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - logger("warn", "judge_failed_fallback", { taskId: task.id, error: message }); - return applyFallbackSafetyOverrides(task, result, fallbackJudge(result, message)); - } -} - -async function requestMiniMaxJudge(messages: Array<{ role: "system" | "user" | "assistant"; content: string }>): Promise { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), config.judgeTimeoutMs); - try { - const response = await fetch(`${config.minimaxApiBase}/chat/completions`, { - method: "POST", - headers: { authorization: `Bearer ${config.minimaxApiKey}`, "content-type": "application/json" }, - body: JSON.stringify({ - model: config.minimaxModel, - temperature: 0, - max_tokens: config.judgeMaxTokens, - messages, - }), - signal: controller.signal, - }); - const rawText = await response.text(); - if (!response.ok) throw new Error(`MiniMax HTTP ${response.status}: ${safePreview(rawText, 1000)}`); - let content = rawText; - try { - const payload = JSON.parse(rawText) as Record; - const first = Array.isArray(payload.choices) ? extractRecord(payload.choices[0]) : null; - const message = extractRecord(first?.message); - content = extractString(message, "content") - ?? extractString(payload, "content") - ?? extractString(payload, "reply") - ?? extractString(payload, "text") - ?? (Object.prototype.hasOwnProperty.call(payload, "decision") ? JSON.stringify(payload) : rawText); - } catch { - content = rawText; - } - return { rawText, content }; - } finally { - clearTimeout(timer); - } -} - -const defaultJudgeProbeCases: JudgeProbeCase[] = [ - { - id: "completed_exact_response", - prompt: "Reply exactly: code-queue-judge-complete.", - finalResponse: "code-queue-judge-complete.", - expected: "complete", - terminalStatus: "completed", - outputs: [ - { channel: "user", text: "Reply exactly: code-queue-judge-complete.\n", method: "enqueue" }, - { channel: "assistant", text: "code-queue-judge-complete.", method: "item/agentMessage/delta" }, - { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, - ], - events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], - }, - { - id: "completed_but_plan_only", - prompt: "Create /root/unidesk/tmp/judge_probe.txt containing exactly judge-probe-ok, then summarize the file path.", - finalResponse: "I can do that. Plan: create the file under /root/unidesk/tmp and then summarize the path.", - expected: "retry", - terminalStatus: "completed", - outputs: [ - { channel: "user", text: "Create /root/unidesk/tmp/judge_probe.txt containing exactly judge-probe-ok, then summarize the file path.\n", method: "enqueue" }, - { channel: "assistant", text: "I can do that. Plan: create the file under /root/unidesk/tmp and then summarize the path.", method: "item/agentMessage/delta" }, - { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, - ], - events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], - }, - { - id: "pikapython_skill_ablation_not_run", - prompt: "从这个结果看,是通过专门定向优化prompt解决了pikabench-4的可解性,并且使得其他组件都没有明显贡献,这个思路是错误的,输入任务prompt应当是通用的,需求导向而非精确控制内部过程的。改进方案是将专门定向的prompt改为泛化的经验型skill来注入,预期的结果是,不注入这个泛化的经验型skill,就不能跑出4/4,注入了就能跑出。将禁止针对任务定向优化prompt的原则也固化到 pipeline 长期文档。", - finalResponse: "已把这轮“定向 prompt 解题”的路径改成“通用需求 prompt + 可消融的经验型 skill”。D601 Pipeline prompt 已去定向化,泛化经验已移入 PikaPython skill,no-skill 边界已恢复,长期规则已固化,测试已补防回归。验证已通过:npm run typecheck、npm run components:validate、npm run test,50 tests passed。我没有启动新的 PikaBench-4 baseline/no-skill 实跑,避免继续消耗 MiniMax 限额;现在代码与测试层面已经把“skill 可消融”边界重新建立起来。下一步如果要验证预期,应只跑最小对照:pikabench-4-ablation-baseline vs pikabench-4-no-skill。", - expected: "retry", - terminalStatus: "completed", - outputs: [ - { - channel: "user", - text: "从这个结果看,是通过专门定向优化prompt解决了pikabench-4的可解性,并且使得其他组件都没有明显贡献,这个思路是错误的,输入任务prompt应当是通用的,需求导向而非精确控制内部过程的。改进方案是将专门定向的prompt改为泛化的经验型skill来注入,预期的结果是,不注入这个泛化的经验型skill,就不能跑出4/4,注入了就能跑出。将禁止针对任务定向优化prompt的原则也固化到 pipeline 长期文档。\n", - method: "enqueue", - }, - { - channel: "command", - text: "item/completed: bun scripts/cli.ts ssh D601 'cd /home/ubuntu/pipeline && npm run typecheck && npm run components:validate && npm run test' status=completed; 50 tests passed", - method: "item/completed", - }, - { - channel: "assistant", - text: "已把这轮“定向 prompt 解题”的路径改成“通用需求 prompt + 可消融的经验型 skill”。验证已通过:npm run typecheck、npm run components:validate、npm run test,50 tests passed。我没有启动新的 PikaBench-4 baseline/no-skill 实跑,避免继续消耗 MiniMax 限额;下一步如果要验证预期,应只跑最小对照:pikabench-4-ablation-baseline vs pikabench-4-no-skill。", - method: "item/agentMessage/delta", - }, - { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, - ], - events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], - }, - { - id: "constar_docs_steer_unfinished_should_retry", - prompt: "constar的工作目录在D601 /mnt/f/work/constar,你先摸清楚这个工程怎么用D601的skills做编译,下载等,摸熟悉之后更新长期参考文档,包括unidesk中简要引导,constar 里面补充在wsl中开发的注意事项等", - finalResponse: "已按你的更正处理:长期文档可以留在 `constar/docs/`,不再放在 `constar/projects/` 里维护。已迁移非 docx 项目文档,已更新长期规则与索引,并验证项目文档目录只剩 docx。注意:根仓库新增/修改 `docs/reference/...`,FREQ 子仓库中 `README.md` 修改、`项目文档/quick_start_debug.md` 删除。", - expected: "retry", - terminalStatus: "completed", - outputs: [ - { - channel: "user", - text: "constar的工作目录在D601 /mnt/f/work/constar,你先摸清楚这个工程怎么用D601的skills做编译,下载等,摸熟悉之后更新长期参考文档,包括unidesk中简要引导,constar 里面补充在wsl中开发的注意事项等\n", - method: "enqueue", - }, - { - channel: "command", - text: "item/completed: bun scripts/cli.ts ssh D601 skills --limit 120 status=completed; keil/serial-monitor/board-comm wrappers discovered", - method: "item/completed", - }, - { - channel: "command", - text: "item/completed: bun scripts/cli.ts ssh D601 -- 'cd /mnt/f/Work/ConStart && keil build --wait -p projects/71-00075-11/FirmWare/MDK-ARM/FREQ_Controller_FW.uvprojx -t FREQ_Controller_FW' status=completed; return_code=0 success=true", - method: "item/completed", - }, - { - channel: "user", - text: "[steer] freq项目里面的项目文档要迁移到constar/docs/ 里面(docx不迁移),并在长期文档中说明只创建和维护constar/docs里面的长期文档\n", - method: "turn/steer", - }, - { - channel: "assistant", - text: "已按你的更正处理:长期文档可以留在 `constar/docs/`,不再放在 `constar/projects/` 里维护。已迁移非 docx 项目文档,并验证相关 Markdown。", - method: "item/agentMessage/delta", - }, - { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, - ], - events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], - }, - { - id: "rate_limit_after_partial_work_should_retry", - prompt: "不要总是刷新登录二维码,这样容易过期,只在没有获取二维码的时候获取,之后不要自动刷新,只保留手动刷新", - finalResponse: "", - expected: "retry", - terminalStatus: "failed", - terminalError: "exceeded retry limit, last status: 429 Too Many Requests, request id: 21zqfw7apcg", - outputs: [ - { - channel: "user", - text: "不要总是刷新登录二维码,这样容易过期,只在没有获取二维码的时候获取,之后不要自动刷新,只保留手动刷新\n", - method: "enqueue", - }, - { channel: "command", text: "item/completed: git diff -- src/components/frontend/src/claudeqq.tsx status=completed", method: "item/completed" }, - { channel: "command", text: "item/completed: bun run check status=completed", method: "item/completed" }, - { channel: "command", text: "item/completed: bun scripts/cli.ts server rebuild frontend status=completed; job succeeded", method: "item/completed" }, - { channel: "command", text: "item/completed: bun scripts/cli.ts e2e run --only frontend:claudeqq status=failed; Playwright browser/dependency issue", method: "item/completed" }, - { channel: "command", text: "item/started: bunx playwright install-deps chromium status=inProgress", method: "item/started" }, - { channel: "error", text: "exceeded retry limit, last status: 429 Too Many Requests, request id: 21zqfw7apcg\n", method: "error" }, - { channel: "error", text: "turn completed status=failed\n", method: "turn/completed" }, - ], - events: [ - { at: nowIso(), method: "item/completed", itemType: "commandExecution", status: "failed", message: "Playwright dependency validation not complete" }, - { at: nowIso(), method: "turn/completed", status: "failed", message: "exceeded retry limit, last status: 429 Too Many Requests" }, - ], - }, - { - id: "codex_1778545138372_limit_error_no_final_response_should_retry", - prompt: "基于 `https://github.com/filebrowser/filebrowser` 开发和部署一个文件管理器的用户服务,支持浏览 main server host、provider host 和 windows 文件 (wsl的/mnt/c/ 等目录,如果 provider 是 WSL,例如 D518 和 D601)", - finalResponse: "", - expected: "retry", - terminalStatus: "failed", - terminalError: "exceeded retry limit, last status: 429 Too Many Requests, request id: 9h5h8iolxwr", - outputs: [ - { - channel: "user", - text: "基于 `https://github.com/filebrowser/filebrowser` 开发和部署一个文件管理器的用户服务,支持浏览 main server host、provider host 和 windows 文件 (wsl的/mnt/c/ 等目录,如果 provider 是 WSL,例如 D518 和 D601)\n", - method: "enqueue", - }, - { - channel: "command", - text: "item/completed: microservice list shows filebrowser main-server, D601 and D518 containers all Up/healthy", - method: "item/completed", - }, - { - channel: "command", - text: "item/completed: API checks returned 200 for /mnt/c/Users and /mnt/c/Windows on D601 and D518 with directory item counts", - method: "item/completed", - }, - { - channel: "diff", - text: "item/completed: file changes status=completed; M docker-compose.yml M docs/reference/microservices.md", - method: "item/completed", - }, - { - channel: "command", - text: "item/completed: docker compose up -d --no-deps --force-recreate filebrowser status=completed; filebrowser-main became healthy", - method: "item/completed", - }, - { channel: "error", text: "exceeded retry limit, last status: 429 Too Many Requests, request id: 9h5h8iolxwr\n", method: "error" }, - { channel: "error", text: "turn completed status=failed\n", method: "turn/completed" }, - ], - events: [ - { at: nowIso(), method: "item/completed", itemType: "commandExecution", status: "completed", message: "filebrowser runtime checks showed partial success before interruption" }, - { at: nowIso(), method: "turn/completed", status: "failed", message: "exceeded retry limit, last status: 429 Too Many Requests" }, - ], - }, - { - id: "codex_1778551922480_fb2cc7_rate_limit_feedback_should_not_add_no_arg_api_requirement", - prompt: "71-Freq 工程里面的 main.c:282 打印了 period_sum/mpu_read_num,我需要在282的后面新增两个打印,分别是打印滑动滤波前和滑动滤波后的值,你先分析要使得能够正常打印,需要哪些工作(例如初始化、配置等),并且在 period_sum/mpu_read_num 相同的条件下打印(这个打印应该是在 little_shell 触发的某个命令下进入打印状态),这个命令如果已经做好了频率测量的初始化和配置,那就不需要再配置", - finalResponse: "", - expected: "retry", - expectedContinuePromptIncludes: [ - "未完成", - "原始需求", - "继续按照原始需求", - ], - expectedContinuePromptExcludes: [ - "无参", - "无参数", - "mpu_sensor_get_frequency_millihz()", - "mpu_sensor_get_filtered_frequency_millihz()", - "把 `mpu_sensor_get_frequency_millihz`", - "return value", - "返回 `uint32_t`", - "FirmWare/Core/Src/main.c", - "第 276-292 行", - ], - expectedContinuePromptMaxChars: 420, - expectedContinuePromptMaxLines: 6, - terminalStatus: "failed", - terminalError: "exceeded retry limit, last status: 429 Too Many Requests, request id: 8qegpzommds", - outputs: [ - { - channel: "user", - text: "71-Freq 工程里面的 main.c:282 打印了 period_sum/mpu_read_num,我需要在282的后面新增两个打印,分别是打印滑动滤波前和滑动滤波后的值,你先分析要使得能够正常打印,需要哪些工作(例如初始化、配置等),并且在 period_sum/mpu_read_num 相同的条件下打印(这个打印应该是在 little_shell 触发的某个命令下进入打印状态),这个命令如果已经做好了频率测量的初始化和配置,那就不需要再配置\n", - method: "enqueue", - }, - { - channel: "system", - text: "attempt 2/99 queue=constar provider=main-server cwd=/root/unidesk mode=retry model=gpt-5.5\n", - method: "queue", - }, - { - channel: "system", - text: "上一次 judge feedback 曾指定了具体 printf 形态;其中无参读取不是原始用户要求,只是 judge 推测。\n", - method: "queue", - }, - { - channel: "command", - text: "item/started: bun scripts/cli.ts debug dispatch D601 host.ssh --ssh-command 'cd /mnt/f/Work/ConStart/projects/71-00075-11 && sed -n \"276,292p\" FirmWare/Core/Src/main.c' status=inProgress", - method: "item/started", - }, - { - channel: "error", - text: "exceeded retry limit, last status: 429 Too Many Requests, request id: 8qegpzommds\n", - method: "error", - }, - { channel: "error", text: "turn completed status=failed\n", method: "turn/completed" }, - ], - events: [ - { at: nowIso(), method: "item/started", itemType: "commandExecution", status: "inProgress", message: "reading D601 main.c context" }, - { at: nowIso(), method: "turn/completed", status: "failed", message: "exceeded retry limit, last status: 429 Too Many Requests" }, - ], - }, - { - id: "frontend_visible_change_without_rebuild_should_retry", - prompt: "unidesk 把侧边栏的 `微服务` 改成 `用户服务`,单纯进行改名,长期文档中也要同步修改命名,明确概念,用户服务指的是在 unidesk 核心服务上挂载的用户使用的服务,unidesk 核心缺少这些服务也依然可以运行", - finalResponse: "已完成,属于纯命名调整,没有改路由/API/CLI 的兼容命名。\n\n- 前端侧边栏主模块从 `微服务` 改为 `用户服务`:`src/components/frontend/src/navigation.ts`\n- 同步了前端可见文案,包括用户服务目录、顶部状态、各服务页 eyebrow/raw title/空状态。\n- 更新 E2E 选择器和断言,Playwright 现在点击/校验 `用户服务`。\n- 长期文档已同步概念。\n\n验证已跑:\n- `bun scripts/cli.ts check` 通过\n- `bun scripts/cli.ts help` 过滤确认 `microservice` 命令描述已改为 user services\n\n未跑完整公网 E2E;本次只做命名和文档同步。", - expected: "retry", - terminalStatus: "completed", - outputs: [ - { - channel: "user", - text: "unidesk 把侧边栏的 `微服务` 改成 `用户服务`,单纯进行改名,长期文档中也要同步修改命名,明确概念,用户服务指的是在 unidesk 核心服务上挂载的用户使用的服务,unidesk 核心缺少这些服务也依然可以运行\n", - method: "enqueue", - }, - { channel: "diff", text: "item/completed: file changes status=completed; M src/components/frontend/src/navigation.ts M src/components/frontend/src/app.tsx M docs/reference/microservices.md", method: "item/completed" }, - { channel: "command", text: "item/completed: bun scripts/cli.ts check status=completed; ok=true", method: "item/completed" }, - { channel: "command", text: "item/completed: bun scripts/cli.ts help | python3 ... status=completed; microservice descriptions mention user services", method: "item/completed" }, - { - channel: "assistant", - text: "已完成,属于纯命名调整。验证已跑:bun scripts/cli.ts check 通过;bun scripts/cli.ts help 过滤确认 microservice 命令描述已改为 user services。未跑完整公网 E2E;本次只做命名和文档同步。", - method: "item/agentMessage/delta", - }, - { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, - ], - events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], - }, - { - id: "codex_1778483956252_c65680_mid_task_confirmation_should_retry", - prompt: "code-queue的这个卡片已经没有意义了,因为已经能够在 TraceView 展开,所以应当取消这个卡片,只保留TraceView即可\n\n```\ncodex_1778483881863_a48b0a\n\nPrompt 全量\nqueued\nmodel=gpt-5.5\ncwd=/root/unidesk\ncreated=2026/5/11 15:18:01\n29 lines / 385 chars\n渐进式 Trace\n首屏使用后端 Summary;展开 prompt / 步骤时只按需拉取对应片段,不一次性拉取完整 transcript。\n...\n```", - finalResponse: "我这边已完成 Code Queue 前端改动并跑过检查,但刚刚发现一个我没有修改的文件也变成了 modified:\n\n- `src/components/microservices/code-queue/src/index.ts`\n\n开始时工作树是干净的,我没有编辑这个文件。按安全规则我需要先停下确认,避免覆盖你的/其他进程的变更。\n\n请确认要我怎么处理:\n\n1. 忽略这个文件,只继续交付我已改的前端相关变更\n2. 先查看这个文件的 diff,判断来源和影响\n3. 你会自行处理,我这边只汇报已完成内容", - expected: "retry", - terminalStatus: "completed", - outputs: [ - { - channel: "user", - text: "code-queue的这个卡片已经没有意义了,因为已经能够在 TraceView 展开,所以应当取消这个卡片,只保留TraceView即可\n\n```\ncodex_1778483881863_a48b0a\n\nPrompt 全量\nqueued\nmodel=gpt-5.5\ncwd=/root/unidesk\ncreated=2026/5/11 15:18:01\n29 lines / 385 chars\n渐进式 Trace\n首屏使用后端 Summary;展开 prompt / 步骤时只按需拉取对应片段,不一次性拉取完整 transcript。\n...\n```\n", - method: "enqueue", - }, - { channel: "diff", text: "item/completed: file changes status=completed; M docs/reference/frontend.md M scripts/src/e2e.ts M src/components/frontend/public/app.js M src/components/frontend/public/style.css M src/components/frontend/src/code-queue.tsx", method: "item/completed" }, - { channel: "command", text: "item/completed: bun run --cwd src/components/frontend check status=completed; ok=true", method: "item/completed" }, - { channel: "command", text: "item/completed: bun run --cwd src/components/frontend build status=completed; ok=true", method: "item/completed" }, - { channel: "command", text: "item/completed: bun scripts/cli.ts check status=completed; ok=true", method: "item/completed" }, - { channel: "command", text: "item/completed: git status --short status=completed; M src/components/microservices/code-queue/src/index.ts also present", method: "item/completed" }, - { - channel: "assistant", - text: "我这边已完成 Code Queue 前端改动并跑过检查,但刚刚发现一个我没有修改的文件也变成了 modified:\n\n- `src/components/microservices/code-queue/src/index.ts`\n\n开始时工作树是干净的,我没有编辑这个文件。按安全规则我需要先停下确认,避免覆盖你的/其他进程的变更。\n\n请确认要我怎么处理:\n\n1. 忽略这个文件,只继续交付我已改的前端相关变更\n2. 先查看这个文件的 diff,判断来源和影响\n3. 你会自行处理,我这边只汇报已完成内容", - method: "item/agentMessage/delta", - }, - { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, - ], - events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], - }, - { - id: "codex_1778476426776_a2bf19_not_deployed_should_retry", - prompt: "originprompt -> 执行过程 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/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: [ - { - channel: "user", - 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/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;我没有重建运行中的容器。若要上线验证可重建 code-queue 和 frontend。", - method: "item/agentMessage/delta", - }, - { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, - ], - events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], - }, - { - 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 浏览器时区下验证顶部北京时间时钟、任务历史、Code Queue card、Codex Trace 和 Pipeline run 时间都显示北京时间。本轮不是只改源码未上线,运行中 served UI 已验证通过。", - expected: "complete", - terminalStatus: "completed", - outputs: [ - { - channel: "user", - text: "unidesk 的所有 frontend 时间显示必须采用北京时间,要修改 unidesk 的前端,并在长期参考文档中要求这一点\n", - method: "enqueue", - }, - { - channel: "system", - text: "judge=retry confidence=0.95 source=minimax: Frontend bundle was not rebuilt/deployed; finalResponse explicitly states the frontend rebuild was skipped to avoid service restart.\n", - method: "judge", - }, - { - channel: "system", - text: "上一次 judge 判定为 retry:当前实现仍是未上线/未完成状态,不能只复述源码修改。\n", - method: "queue", - }, - { - channel: "command", - text: "item/completed: bun scripts/cli.ts server rebuild frontend status=completed; job succeeded; unidesk-frontend healthy", - method: "item/completed", - }, - { - channel: "command", - text: "item/completed: Playwright live UI verification under America/Los_Angeles timezone status=completed; topbar/task/Codex/Pipeline times use Asia/Shanghai", - method: "item/completed", - }, - { - channel: "assistant", - text: "已完成北京时间任务并已上线验证。长期参考文档已加入 Frontend Time Zone Policy。已执行 server rebuild frontend,job succeeded,unidesk-frontend healthy。Playwright live UI verification 通过。本轮不是只改源码未上线,运行中 served UI 已验证通过。", - method: "item/agentMessage/delta", - }, - { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, - ], - events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], - }, - { - id: "transport_closed_before_terminal", - prompt: "Refactor the queue worker and run the focused tests.", - finalResponse: "", - expected: "retry", - terminalStatus: null, - transportClosedBeforeTerminal: true, - stderrTail: "stream disconnected before completion: upstream overloaded; app-server closed before turn/completed", - outputs: [ - { channel: "user", text: "Refactor the queue worker and run the focused tests.\n", method: "enqueue" }, - { channel: "system", text: "attempt 1/3 mode=initial model=gpt-5.4-mini\n", method: "queue" }, - ], - events: [{ at: nowIso(), method: "thread/status/changed", status: "inProgress" }], - }, - { - id: "user_interrupted", - prompt: "Run a long shell command, then produce a report.", - finalResponse: "", - expected: "fail", - terminalStatus: "interrupted", - cancelRequested: true, - outputs: [ - { channel: "user", text: "Run a long shell command, then produce a report.\n", method: "enqueue" }, - { channel: "system", text: "interrupt requested\n", method: "turn/interrupt" }, - { channel: "error", text: "turn completed status=interrupted\n", method: "turn/completed" }, - ], - events: [{ at: nowIso(), method: "turn/completed", status: "interrupted" }], - }, -]; +configureDevContainers({ + config, + appendOutput, + buildDevContainerPlan, + containerTunnelStartScript, + devContainerEnsurePromises, + devContainerPingScript, + errorToJson, + extractRecord, + jsonResponse, + logger, + masterKeyReadScript, + masterKeySetupScript, + masterProxyEvidenceScript, + masterProxyFinishScript, + masterProxyPrepareScript, + normalizeProviderId, + providerIsMain, + readJson, + remoteCodexConfigInstallScript, + remoteCodexRuntimePrepareScript, + remoteContainerStartScript, + remoteHostWorkdirForTask, + remoteKeyInstallScript, + runCodeQueueSsh, + safePreview, + shellQuote, + throwIfCommandFailed, +}); function outputForProbe(item: { channel: OutputChannel; text: string; method?: string }, index: number): LiveOutput { return { @@ -6911,56 +2123,6 @@ function attemptFromResult(task: QueueTask, mode: RunMode, startedAt: string, fi return attempt; } -const retryTaskSummaryMaxChars = 1200; -const judgeReasonPromptMaxChars = 1200; -const compactContinuationPromptTargetChars = 1200; -const continuePromptSourceBudgetChars = 4000; - -function judgeReasonForPrompt(reason: string): string { - return safePreview(reason, judgeReasonPromptMaxChars) || "(empty)"; -} - -function continuationPromptForRetry(prompt: string): string { - return prompt.trim(); -} - -function compactRetryTaskContext(task: QueueTask): string { - const basePrompt = task.basePrompt || userPromptForDisplay(task.prompt); - const referenceTaskIds = taskReferenceIds(task); - return [ - "原始任务摘要(同一 thread 已有完整上文,不重新注入引用全文):", - safePreview(basePrompt, retryTaskSummaryMaxChars) || "(empty)", - referenceTaskIds.length > 0 ? `引用任务 ID:${referenceTaskIds.join(", ")}` : "", - `按需查询有界摘要:bun scripts/cli.ts codex task ${task.id}`, - ].filter((line) => line.length > 0).join("\n"); -} - -function queueRecoveryRetryPrompt(task: QueueTask, reason: string): string { - return [ - retryInstruction, - "Code Queue 服务在任务运行中重启/停止;这是自动恢复提示,不是新任务,也不需要重新粘贴原始任务或引用全文。", - "如果本轮任务正是修改 Code Queue 自身,不要等待当前 task 退出;服务重启已经发生,继续完成恢复后的验证和剩余交付。", - `恢复原因:${judgeReasonForPrompt(reason)}`, - "请基于当前 thread 上文继续,只做最小必要状态核查,恢复未完成的等待、验证、部署或命令;最终 response 必须给出真实结果证据。", - "原始任务摘要/按需查询:", - compactRetryTaskContext(task), - ].join("\n\n"); -} - -function retryPrompt(task: QueueTask, judge: JudgeResult): string { - if (judge.continuePrompt !== undefined && judge.continuePrompt.trim().length > 0) return continuationPromptForRetry(judge.continuePrompt); - return [ - retryInstruction, - "上一次 judge 判定为 retry,下面是必须传递给本轮 continuation 的 judge feedback。请优先补齐这些缺口,不要只做泛泛的状态核查。", - `judge 来源:${judge.source}`, - `judge 置信度:${judge.confidence.toFixed(2)}`, - `judge 未完成原因:${judgeReasonForPrompt(judge.reason)}`, - "请在本轮最终 response 中明确说明已如何解决上述 judge feedback;如果 judge 要求 benchmark/上线/运行中验证,就必须提供对应真实命令和结果证据。", - "原始任务摘要/按需查询:", - compactRetryTaskContext(task), - ].join("\n\n"); -} - function retryBackoffMs(completedAttempts: number): number { const retryIndex = Math.max(1, Math.floor(completedAttempts)); const exponent = Math.min(20, retryIndex - 1); @@ -6976,20 +2138,6 @@ async function sleepForRetryBackoff(task: QueueTask, delayMs: number): Promise 0) { - parts.push("judge 建议的继续提示:", continuationPromptForRetry(judge.continuePrompt)); - } - parts.push("原始任务摘要/按需查询:", compactRetryTaskContext(task)); - return parts.join("\n\n"); -} - function queueActiveTasksForRestartRetry(reason: string, method: string): number { let recovered = 0; for (const task of state.tasks) { @@ -7327,6 +2475,81 @@ function availableQueueStartSlots(): number { return availableQueueStartSlotsFor(activeRunSlotCount()); } +function queuedReason(code: string, label: string, message: string, extra: Omit = {}): QueuedStatusReason { + return { code, label, message, ...extra }; +} + +function memoryPressureReasonPayload(usage: CgroupMemoryUsage & { thresholdBytes: number }): JsonValue { + return { + currentBytes: usage.currentBytes, + inactiveFileBytes: usage.inactiveFileBytes, + workingSetBytes: usage.workingSetBytes, + thresholdBytes: usage.thresholdBytes, + swapCurrentBytes: usage.swapCurrentBytes, + swapMaxBytes: usage.swapMaxBytes, + }; +} + +function queuedStatusReason(task: QueueTask, tasks: QueueTask[] = state.tasks): QueuedStatusReason | null { + if (task.status !== "queued") return null; + const queueId = queueIdOf(task); + const sourceTasks = tasks.some((item) => item.id === task.id) ? tasks : [...tasks, task]; + const rows = queueTaskRows(queueId, sourceTasks); + const head = rows.find(queueTaskBlocksFollowing) ?? null; + if (head !== null && head.id !== task.id) { + return queuedReason( + "prev_task", + "PREV TASK", + `Waiting for previous task ${head.id} in queue ${queueId} to finish first.`, + { blockerTaskId: head.id, blockerQueueId: queueId }, + ); + } + if (shutdownRequested) { + return queuedReason("shutdown", "SHUTDOWN", "Code Queue is shutting down; queued work will resume after restart."); + } + if (!serviceReady) { + return queuedReason("service", "SERVICE", "Code Queue is still starting and has not enabled scheduling yet."); + } + + const memoryPressure = activeRunMemoryPressure(); + if (memoryPressure !== null) { + return queuedReason( + "mem_limit", + "MEM LIMIT", + "Waiting for cgroup memory working set to fall below the configured start threshold.", + { memory: memoryPressureReasonPayload(memoryPressure), activeRunSlotCount: activeRunSlotCount(), maxActiveQueues: config.maxActiveQueues }, + ); + } + + const waitPosition = activeRunSlotWaiters.findIndex((waiter) => waiter.taskId === task.id); + if (config.maxActiveQueues > 0 && availableQueueStartSlots() <= 0) { + return queuedReason( + "active_limit", + "ACTIVE LIMIT", + `Waiting for an active run slot (${activeRunSlotCount()}/${config.maxActiveQueues} in use).`, + { waitPosition: waitPosition >= 0 ? waitPosition + 1 : null, activeRunSlotCount: activeRunSlotCount(), maxActiveQueues: config.maxActiveQueues }, + ); + } + if (waitPosition > 0) { + const blocker = firstActiveRunSlotWaiter(); + return queuedReason( + "slot_fifo", + "SLOT FIFO", + "Waiting behind an older runnable queue for the next active run slot.", + { blockerTaskId: blocker?.taskId ?? null, blockerQueueId: blocker?.queueId ?? null, waitPosition: waitPosition + 1, activeRunSlotCount: activeRunSlotCount(), maxActiveQueues: config.maxActiveQueues }, + ); + } + if (processingQueues.has(queueId) || waitPosition === 0) { + return queuedReason( + "starting", + "STARTING", + "Queue processor has selected this task and is starting the agent run.", + { waitPosition: waitPosition >= 0 ? waitPosition + 1 : null, activeRunSlotCount: activeRunSlotCount(), maxActiveQueues: config.maxActiveQueues }, + ); + } + return queuedReason("ready", "READY", "Task is the head of its queue and ready to start."); +} + function nextRunnableTask(queueId: string): QueueTask | null { return nextRunnableTaskFrom(queueId); } @@ -7472,167 +2695,6 @@ function parseLimit(url: URL): number { return Number.isInteger(value) && value > 0 ? Math.min(500, value) : 100; } -function parseNamedLimit(url: URL, name: string, defaultValue: number): number { - const value = Number(url.searchParams.get(name) ?? defaultValue); - return Number.isInteger(value) && value > 0 ? Math.min(500, value) : defaultValue; -} - -function activePriority(task: QueueTask): number { - const statusRank: Record = { - running: 0, - judging: 1, - retry_wait: 2, - queued: 3, - succeeded: 9, - failed: 9, - canceled: 9, - }; - return statusRank[task.status] ?? 9; -} - -function taskUpdatedSortValue(task: QueueTask): number { - const time = Date.parse(task.updatedAt || task.createdAt); - 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; - returned: number; - hasMore: boolean; - nextBeforeId: string | null; - beforeId: string | null; - includeActive: boolean; -} { - const beforeId = url.searchParams.get("beforeId"); - const includeActive = url.searchParams.get("includeActive") !== "0"; - const beforeIndex = beforeId === null ? -1 : filteredTasks.findIndex((task) => task.id === beforeId); - const safeEndIndex = beforeId === null || beforeIndex < 0 ? filteredTasks.length : beforeIndex; - const pageSource = filteredTasks.slice(Math.max(0, safeEndIndex - limit), safeEndIndex).reverse(); - const unreadTerminalRows = includeActive - ? filteredTasks - .filter(terminalTaskUnread) - .sort((left, right) => taskUpdatedSortValue(right) - taskUpdatedSortValue(left)) - : []; - const activeRows = includeActive - ? filteredTasks - .filter((task) => activePriority(task) < 3) - .sort((left, right) => { - const rankDelta = activePriority(left) - activePriority(right); - if (rankDelta !== 0) return rankDelta; - return taskUpdatedSortValue(right) - taskUpdatedSortValue(left); - }) - : []; - const byId = new Map(); - for (const task of [...unreadTerminalRows, ...activeRows, ...pageSource]) { - if (!byId.has(task.id)) byId.set(task.id, task); - } - const rows = Array.from(byId.values()); - const pageOldest = pageSource.at(-1) ?? null; - return { - rows, - total: filteredTasks.length, - returned: rows.length, - hasMore: safeEndIndex - limit > 0, - nextBeforeId: safeEndIndex - limit > 0 ? pageOldest?.id ?? null : null, - beforeId, - includeActive, - }; -} - -async function tasksOverviewResponse(url: URL): Promise { - const limit = parseLimit(url); - const compact = truthyParam(url, "compact"); - const queueFilter = url.searchParams.get("queueId"); - 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, 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) - ?? filteredTasks.find((task) => task.id === activeTaskId) - ?? rowsSource[0] - ?? null; - let selected: JsonValue = null; - if (selectedTask !== null) { - const afterSeqRaw = Number(url.searchParams.get("afterSeq") ?? 0); - const afterSeq = Number.isFinite(afterSeqRaw) ? afterSeqRaw : 0; - const transcriptLimit = parseNamedLimit(url, "transcriptLimit", 500); - const transcript = compact - ? buildCompactTaskTranscript(selectedTask, transcriptLimit, Math.max(12, transcriptLimit * 3)) - : buildTaskTranscript(selectedTask, transcriptLimit, 0); - const chunk = transcript.filter((line) => Number(line.seq) > afterSeq).slice(0, transcriptLimit); - const nextAfterSeq = chunk.at(-1)?.seq ?? afterSeq; - const outputMaxSeq = selectedTask.output.at(-1)?.seq ?? 0; - const promptHistoryMaxSeq = selectedTask.promptHistory.at(-1)?.seq ?? 0; - const maxSeq = Math.max(outputMaxSeq, promptHistoryMaxSeq, transcript.at(-1)?.seq ?? 0); - selected = { - task: compact ? taskForCompactMetaResponse(selectedTask) : taskForMetaResponse(selectedTask), - transcript: chunk, - afterSeq, - nextAfterSeq, - hasMore: maxSeq > Number(nextAfterSeq), - preview: true, - total: transcript.length, - maxSeq, - } as unknown as JsonValue; - } - return compactJsonResponse({ - ok: true, - queue, - statistics: taskStatisticsSummary(filteredTasks, statsDaysFromUrl(url)), - tasks: rowsSource.map((task) => taskForListResponse(task, true)), - selected, - pagination: { - limit, - returned: page.returned, - total: page.total, - hasMore: page.hasMore, - nextBeforeId: page.nextBeforeId, - beforeId: page.beforeId, - includeActive: page.includeActive, - }, - }); -} - async function createTasks(req: Request): Promise { const body = await readJson(req); const batchRecord = typeof body === "object" && body !== null && !Array.isArray(body) ? body as Record : {}; @@ -7655,829 +2717,6 @@ async function createTasks(req: Request): Promise { 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 { - return normalizeTask({ - id, - queueId: defaultQueueId, - queueEnteredAt: createdAt, - prompt, - basePrompt: prompt, - referenceTaskIds, - referenceInjection: null, - providerId: config.mainProviderId, - cwd: config.defaultWorkdir, - model: config.defaultModel, - reasoningEffort: resolveReasoningEffort(config.defaultModel, config.defaultReasoningEffort), - maxAttempts: 1, - status: "succeeded", - createdAt, - updatedAt: createdAt, - startedAt: createdAt, - finishedAt: createdAt, - readAt: null, - currentAttempt: 1, - currentMode: "initial", - codexThreadId: null, - activeTurnId: null, - finalResponse, - lastError: null, - lastJudge: null, - judgeFailCount: 0, - promptHistory: [], - output: finalResponse.length > 0 ? [{ seq: 1, at: createdAt, channel: "assistant", text: finalResponse, method: "item/agentMessage" }] : [], - events: [], - attempts: [], - cancelRequested: false, - nextPrompt: null, - nextMode: null, - }); -} - -function assertReferenceTest(condition: boolean, message: string): void { - if (!condition) throw new Error(message); -} - -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: "引用 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"); - taskB.basePrompt = injectedB.basePrompt ?? ""; - taskB.referenceInjection = injectedB.referenceInjection ?? null; - const injectedC = injectReferencedTaskContext({ - prompt: "C user prompt", - 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 = 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(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(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"); - assertReferenceTest(indexB > indexA, "C should include direct B after upstream A"); - assertReferenceTest(promptC.includes("----- Reference Round 1/2 -----"), "C should include explicit round 1 separator"); - assertReferenceTest(promptC.includes("----- Reference Round 2/2 -----"), "C should include explicit round 2 separator"); - 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# 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[] = []; - for (let index = 0; index < 8; index += 1) { - const id = `codex_${2000 + index}_${"abcdef".slice(0, 6 - String(index).length)}${index}`; - const parent = deepTasks.at(-1); - deepTasks.push(testTask(id, `deep prompt ${index}`, `deep final ${index}`, parent === undefined ? [] : [parent.id], `2026-05-08T00:${String(10 + index).padStart(2, "0")}:00.000Z`)); - } - const deepById = new Map(deepTasks.map((task) => [task.id, task])); - const injectedDeep = injectReferencedTaskContext({ - prompt: "Deep user prompt", - referenceTaskIds: [deepTasks.at(-1)?.id ?? ""], - }, (id) => deepById.get(id) ?? null, "2026-05-08T00:30:00.000Z"); - assertReferenceTest(injectedDeep.referenceInjection?.itemCount === 8, "deep reference chain should not truncate at six rounds"); - assertReferenceTest(injectedDeep.referenceInjection?.truncated === false, "deep reference chain should be marked complete"); - assertReferenceTest(injectedDeep.prompt.includes("----- Reference Round 8/8 -----"), "deep reference chain should expose all eight rounds"); - const retryTask = testTask("codex_3000_retry", injectedDeep.prompt, "", injectedDeep.referenceTaskIds, "2026-05-08T00:31:00.000Z"); - retryTask.basePrompt = injectedDeep.basePrompt ?? ""; - 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: [ - { name: "strip_frontend_hint_to_basePrompt", ok: true }, - { name: "multi_round_reference_graph", ok: true, itemCount: injectedC.referenceInjection?.itemCount ?? 0 }, - { name: "no_nested_injection_block", ok: true }, - { name: "chronological_round_order", ok: true }, - { name: "timestamp_and_round_separators", ok: true }, - { name: "environment_hint_injected_and_stripped_from_display", ok: true }, - { 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), - }; -} - -function queueOrderTestTask(id: string, status: TaskStatus, createdAt: string, queueEnteredAt: string): QueueTask { - const task = testTask(id, `${id} prompt`, status === "succeeded" ? `${id} final` : "", [], createdAt); - task.queueId = "queue_order_test"; - task.queueEnteredAt = queueEnteredAt; - task.status = status; - task.updatedAt = queueEnteredAt; - task.startedAt = status === "running" || status === "judging" ? queueEnteredAt : null; - task.finishedAt = terminalTask(task) ? queueEnteredAt : null; - task.currentAttempt = status === "queued" ? 0 : 1; - task.currentMode = status === "queued" ? null : "retry"; - task.nextMode = status === "retry_wait" ? "retry" : null; - task.nextPrompt = status === "retry_wait" ? "continue" : null; - return normalizeTask(task); -} - -function runQueueOrderingSelfTest(): JsonValue { - const activeRetry = queueOrderTestTask("codex_4000_active", "retry_wait", "2026-05-11T09:00:00.000Z", "2026-05-11T09:00:00.000Z"); - const movedOlderCreated = queueOrderTestTask("codex_3999_moved", "queued", "2026-05-11T08:00:00.000Z", "2026-05-11T08:00:00.000Z") as QueueTask & { queueEnteredAt?: string }; - delete (movedOlderCreated as Partial).queueEnteredAt; - movedOlderCreated.output.push({ seq: 2, at: "2026-05-11T09:30:00.000Z", channel: "system", text: "moved from queue=default to queue=queue_order_test\n", method: "queue/move" }); - normalizeTask(movedOlderCreated as QueueTask); - const blockedByRetry = [movedOlderCreated, activeRetry]; - const runningHead = queueOrderTestTask("codex_4100_running", "running", "2026-05-11T10:00:00.000Z", "2026-05-11T10:00:00.000Z"); - 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, - cases: [ - { 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) : ""; @@ -8601,13 +2840,96 @@ async function markTaskRead(task: QueueTask): Promise { logger("info", "task_marked_read", { taskId: task.id, queueId: queueIdOf(task), status: task.status }); } await flushDirtyTasksToDatabase(true); - return jsonResponse({ ok: true, task: taskForResponse(task), queue: await queueSummaryForResponse(false) }); + return compactJsonResponse({ ok: true, task: taskForListResponse(task, true), queue: await queueSummaryForResponse(false) }); +} + +function timestampToIso(value: Date | string | null): string | null { + if (value === null) return null; + const date = value instanceof Date ? value : new Date(value); + return Number.isNaN(date.getTime()) ? String(value) : date.toISOString(); +} + +async function markTaskReadById(taskId: string): Promise { + if (!databaseReady) { + const task = await findTaskForMutation(taskId); + return task === null ? jsonResponse({ ok: false, error: "task not found" }, 404) : markTaskRead(task); + } + const rows = await sql>` + SELECT id, queue_id, status, read_at + FROM unidesk_code_queue_tasks + WHERE id = ${taskId} + LIMIT 1 + `; + const row = rows[0] ?? null; + if (row === null) { + const task = findTask(taskId); + return task === null ? jsonResponse({ ok: false, error: "task not found" }, 404) : markTaskRead(task); + } + if (!(row.status === "succeeded" || row.status === "failed" || row.status === "canceled")) { + return compactJsonResponse({ ok: false, error: `task is not terminal: ${row.status}`, task: { id: taskId, queueId: safeQueueId(row.queue_id), status: row.status } }, 409); + } + const readAt = timestampToIso(row.read_at) ?? nowIso(); + if (row.read_at === null) { + await sql` + UPDATE unidesk_code_queue_tasks + SET + read_at = ${readAt}, + task_json = jsonb_set(task_json, '{readAt}', to_jsonb(${readAt}::text), true) + WHERE id = ${taskId} + AND read_at IS NULL + `; + const hotTask = findTask(taskId); + if (hotTask !== null) hotTask.readAt = readAt; + logger("info", "task_marked_read", { taskId, queueId: safeQueueId(row.queue_id), status: row.status }); + } + return compactJsonResponse({ + ok: true, + task: { + id: taskId, + queueId: safeQueueId(row.queue_id), + status: row.status, + readAt, + terminalUnread: false, + }, + queue: await queueSummaryForResponse(false), + }); } 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(); + if (databaseReady) { + const rows = queueId === null + ? await sql>` + UPDATE unidesk_code_queue_tasks + SET + read_at = ${readAt}, + task_json = jsonb_set(task_json, '{readAt}', to_jsonb(${readAt}::text), true) + WHERE status IN ('succeeded', 'failed', 'canceled') + AND read_at IS NULL + RETURNING id + ` + : await sql>` + UPDATE unidesk_code_queue_tasks + SET + read_at = ${readAt}, + task_json = jsonb_set(task_json, '{readAt}', to_jsonb(${readAt}::text), true) + WHERE queue_id = ${queueId} + AND status IN ('succeeded', 'failed', 'canceled') + AND read_at IS NULL + RETURNING id + `; + const ids = new Set(rows.map((row) => row.id)); + for (const task of state.tasks) { + if (!ids.has(task.id)) continue; + task.readAt = readAt; + } + if (ids.size > 0) { + logger("info", "terminal_tasks_marked_read", { count: ids.size, queueId }); + } + return jsonResponse({ ok: true, count: ids.size, readAt, queue: await queueSummaryForResponse(false) }); + } let count = 0; for (const task of state.tasks) { if (queueId !== null && queueIdOf(task) !== queueId) continue; @@ -8715,27 +3037,14 @@ async function route(req: Request): Promise { 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/judge/self-test" && (req.method === "GET" || req.method === "POST")) return jsonResponse(runJudgeInfraSelfTest()); 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); - const items = claudeQqNotificationOutbox.items.slice(-limit).map((item) => ({ - id: item.id, - kind: item.kind, - dedupKey: item.dedupKey, - target: item.target, - createdAt: item.createdAt, - updatedAt: item.updatedAt, - attempts: item.attempts, - nextAttemptAt: item.nextAttemptAt, - lastError: item.lastError, - sentAt: item.sentAt, - messagePreview: safePreview(item.message, 500), - messageChars: item.message.length, - })); - return jsonResponse({ ok: true, stats: claudeQqNotificationOutboxStats(), items }); + return jsonResponse({ ok: true, stats: claudeQqNotificationOutboxStats(), items: claudeQqNotificationItems(limit) }); } if (url.pathname === "/api/notifications/claudeqq/drain" && req.method === "POST") { return jsonResponse(await drainClaudeQqNotificationOutbox("api")); @@ -8778,7 +3087,7 @@ async function route(req: Request): Promise { .filter((task) => taskMatchesSearch(task, searchTerms)); const limit = parseLimit(url); const page = taskPageRows(filteredTasks, url, limit); - const tasks = page.rows.map((task) => taskForListResponse(task, lite)); + const tasks = page.rows.map((task) => taskForListResponse(task, lite, allTasks)); return jsonResponse({ ok: true, queue: queueSummary(includeDevReady, allTasks), @@ -8846,15 +3155,16 @@ async function route(req: Request): Promise { const match = url.pathname.match(/^\/api\/tasks\/([^/]+)(?:\/(retry|steer|interrupt|move|read|edit))?$/u); if (match !== null) { const action = match[2]; + const taskId = decodeURIComponent(match[1] ?? ""); + if (action === "read" && req.method === "POST") return markTaskReadById(taskId); const task = action === undefined && req.method === "GET" - ? await findTaskForRead(decodeURIComponent(match[1] ?? "")) - : await findTaskForMutation(decodeURIComponent(match[1] ?? "")); + ? await findTaskForRead(taskId) + : await findTaskForMutation(taskId); 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") { diff --git a/src/components/microservices/code-queue/src/judge-probes.ts b/src/components/microservices/code-queue/src/judge-probes.ts new file mode 100644 index 00000000..90cc6f67 --- /dev/null +++ b/src/components/microservices/code-queue/src/judge-probes.ts @@ -0,0 +1,366 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import type { JudgeProbeCase } from "./types"; + +function nowIso(): string { + return new Date().toISOString(); +} + +export const defaultJudgeProbeCases: JudgeProbeCase[] = [ + { + id: "completed_exact_response", + prompt: "Reply exactly: code-queue-judge-complete.", + finalResponse: "code-queue-judge-complete.", + expected: "complete", + terminalStatus: "completed", + outputs: [ + { channel: "user", text: "Reply exactly: code-queue-judge-complete.\n", method: "enqueue" }, + { channel: "assistant", text: "code-queue-judge-complete.", method: "item/agentMessage/delta" }, + { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, + ], + events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], + }, + { + id: "completed_but_plan_only", + prompt: "Create /root/unidesk/tmp/judge_probe.txt containing exactly judge-probe-ok, then summarize the file path.", + finalResponse: "I can do that. Plan: create the file under /root/unidesk/tmp and then summarize the path.", + expected: "retry", + terminalStatus: "completed", + outputs: [ + { channel: "user", text: "Create /root/unidesk/tmp/judge_probe.txt containing exactly judge-probe-ok, then summarize the file path.\n", method: "enqueue" }, + { channel: "assistant", text: "I can do that. Plan: create the file under /root/unidesk/tmp and then summarize the path.", method: "item/agentMessage/delta" }, + { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, + ], + events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], + }, + { + id: "pikapython_skill_ablation_not_run", + prompt: "从这个结果看,是通过专门定向优化prompt解决了pikabench-4的可解性,并且使得其他组件都没有明显贡献,这个思路是错误的,输入任务prompt应当是通用的,需求导向而非精确控制内部过程的。改进方案是将专门定向的prompt改为泛化的经验型skill来注入,预期的结果是,不注入这个泛化的经验型skill,就不能跑出4/4,注入了就能跑出。将禁止针对任务定向优化prompt的原则也固化到 pipeline 长期文档。", + finalResponse: "已把这轮“定向 prompt 解题”的路径改成“通用需求 prompt + 可消融的经验型 skill”。D601 Pipeline prompt 已去定向化,泛化经验已移入 PikaPython skill,no-skill 边界已恢复,长期规则已固化,测试已补防回归。验证已通过:npm run typecheck、npm run components:validate、npm run test,50 tests passed。我没有启动新的 PikaBench-4 baseline/no-skill 实跑,避免继续消耗 MiniMax 限额;现在代码与测试层面已经把“skill 可消融”边界重新建立起来。下一步如果要验证预期,应只跑最小对照:pikabench-4-ablation-baseline vs pikabench-4-no-skill。", + expected: "retry", + terminalStatus: "completed", + outputs: [ + { + channel: "user", + text: "从这个结果看,是通过专门定向优化prompt解决了pikabench-4的可解性,并且使得其他组件都没有明显贡献,这个思路是错误的,输入任务prompt应当是通用的,需求导向而非精确控制内部过程的。改进方案是将专门定向的prompt改为泛化的经验型skill来注入,预期的结果是,不注入这个泛化的经验型skill,就不能跑出4/4,注入了就能跑出。将禁止针对任务定向优化prompt的原则也固化到 pipeline 长期文档。\n", + method: "enqueue", + }, + { + channel: "command", + text: "item/completed: bun scripts/cli.ts ssh D601 'cd /home/ubuntu/pipeline && npm run typecheck && npm run components:validate && npm run test' status=completed; 50 tests passed", + method: "item/completed", + }, + { + channel: "assistant", + text: "已把这轮“定向 prompt 解题”的路径改成“通用需求 prompt + 可消融的经验型 skill”。验证已通过:npm run typecheck、npm run components:validate、npm run test,50 tests passed。我没有启动新的 PikaBench-4 baseline/no-skill 实跑,避免继续消耗 MiniMax 限额;下一步如果要验证预期,应只跑最小对照:pikabench-4-ablation-baseline vs pikabench-4-no-skill。", + method: "item/agentMessage/delta", + }, + { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, + ], + events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], + }, + { + id: "constar_docs_steer_unfinished_should_retry", + prompt: "constar的工作目录在D601 /mnt/f/work/constar,你先摸清楚这个工程怎么用D601的skills做编译,下载等,摸熟悉之后更新长期参考文档,包括unidesk中简要引导,constar 里面补充在wsl中开发的注意事项等", + finalResponse: "已按你的更正处理:长期文档可以留在 `constar/docs/`,不再放在 `constar/projects/` 里维护。已迁移非 docx 项目文档,已更新长期规则与索引,并验证项目文档目录只剩 docx。注意:根仓库新增/修改 `docs/reference/...`,FREQ 子仓库中 `README.md` 修改、`项目文档/quick_start_debug.md` 删除。", + expected: "retry", + terminalStatus: "completed", + outputs: [ + { + channel: "user", + text: "constar的工作目录在D601 /mnt/f/work/constar,你先摸清楚这个工程怎么用D601的skills做编译,下载等,摸熟悉之后更新长期参考文档,包括unidesk中简要引导,constar 里面补充在wsl中开发的注意事项等\n", + method: "enqueue", + }, + { + channel: "command", + text: "item/completed: bun scripts/cli.ts ssh D601 skills --limit 120 status=completed; keil/serial-monitor/board-comm wrappers discovered", + method: "item/completed", + }, + { + channel: "command", + text: "item/completed: bun scripts/cli.ts ssh D601 -- 'cd /mnt/f/Work/ConStart && keil build --wait -p projects/71-00075-11/FirmWare/MDK-ARM/FREQ_Controller_FW.uvprojx -t FREQ_Controller_FW' status=completed; return_code=0 success=true", + method: "item/completed", + }, + { + channel: "user", + text: "[steer] freq项目里面的项目文档要迁移到constar/docs/ 里面(docx不迁移),并在长期文档中说明只创建和维护constar/docs里面的长期文档\n", + method: "turn/steer", + }, + { + channel: "assistant", + text: "已按你的更正处理:长期文档可以留在 `constar/docs/`,不再放在 `constar/projects/` 里维护。已迁移非 docx 项目文档,并验证相关 Markdown。", + method: "item/agentMessage/delta", + }, + { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, + ], + events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], + }, + { + id: "rate_limit_after_partial_work_should_retry", + prompt: "不要总是刷新登录二维码,这样容易过期,只在没有获取二维码的时候获取,之后不要自动刷新,只保留手动刷新", + finalResponse: "", + expected: "retry", + terminalStatus: "failed", + terminalError: "exceeded retry limit, last status: 429 Too Many Requests, request id: 21zqfw7apcg", + outputs: [ + { + channel: "user", + text: "不要总是刷新登录二维码,这样容易过期,只在没有获取二维码的时候获取,之后不要自动刷新,只保留手动刷新\n", + method: "enqueue", + }, + { channel: "command", text: "item/completed: git diff -- src/components/frontend/src/claudeqq.tsx status=completed", method: "item/completed" }, + { channel: "command", text: "item/completed: bun run check status=completed", method: "item/completed" }, + { channel: "command", text: "item/completed: bun scripts/cli.ts server rebuild frontend status=completed; job succeeded", method: "item/completed" }, + { channel: "command", text: "item/completed: bun scripts/cli.ts e2e run --only frontend:claudeqq status=failed; Playwright browser/dependency issue", method: "item/completed" }, + { channel: "command", text: "item/started: bunx playwright install-deps chromium status=inProgress", method: "item/started" }, + { channel: "error", text: "exceeded retry limit, last status: 429 Too Many Requests, request id: 21zqfw7apcg\n", method: "error" }, + { channel: "error", text: "turn completed status=failed\n", method: "turn/completed" }, + ], + events: [ + { at: nowIso(), method: "item/completed", itemType: "commandExecution", status: "failed", message: "Playwright dependency validation not complete" }, + { at: nowIso(), method: "turn/completed", status: "failed", message: "exceeded retry limit, last status: 429 Too Many Requests" }, + ], + }, + { + id: "codex_1778545138372_limit_error_no_final_response_should_retry", + prompt: "基于 `https://github.com/filebrowser/filebrowser` 开发和部署一个文件管理器的用户服务,支持浏览 main server host、provider host 和 windows 文件 (wsl的/mnt/c/ 等目录,如果 provider 是 WSL,例如 D518 和 D601)", + finalResponse: "", + expected: "retry", + terminalStatus: "failed", + terminalError: "exceeded retry limit, last status: 429 Too Many Requests, request id: 9h5h8iolxwr", + outputs: [ + { + channel: "user", + text: "基于 `https://github.com/filebrowser/filebrowser` 开发和部署一个文件管理器的用户服务,支持浏览 main server host、provider host 和 windows 文件 (wsl的/mnt/c/ 等目录,如果 provider 是 WSL,例如 D518 和 D601)\n", + method: "enqueue", + }, + { + channel: "command", + text: "item/completed: microservice list shows filebrowser main-server, D601 and D518 containers all Up/healthy", + method: "item/completed", + }, + { + channel: "command", + text: "item/completed: API checks returned 200 for /mnt/c/Users and /mnt/c/Windows on D601 and D518 with directory item counts", + method: "item/completed", + }, + { + channel: "diff", + text: "item/completed: file changes status=completed; M docker-compose.yml M docs/reference/microservices.md", + method: "item/completed", + }, + { + channel: "command", + text: "item/completed: docker compose up -d --no-deps --force-recreate filebrowser status=completed; filebrowser-main became healthy", + method: "item/completed", + }, + { channel: "error", text: "exceeded retry limit, last status: 429 Too Many Requests, request id: 9h5h8iolxwr\n", method: "error" }, + { channel: "error", text: "turn completed status=failed\n", method: "turn/completed" }, + ], + events: [ + { at: nowIso(), method: "item/completed", itemType: "commandExecution", status: "completed", message: "filebrowser runtime checks showed partial success before interruption" }, + { at: nowIso(), method: "turn/completed", status: "failed", message: "exceeded retry limit, last status: 429 Too Many Requests" }, + ], + }, + { + id: "codex_1778551922480_fb2cc7_rate_limit_feedback_should_not_add_no_arg_api_requirement", + prompt: "71-Freq 工程里面的 main.c:282 打印了 period_sum/mpu_read_num,我需要在282的后面新增两个打印,分别是打印滑动滤波前和滑动滤波后的值,你先分析要使得能够正常打印,需要哪些工作(例如初始化、配置等),并且在 period_sum/mpu_read_num 相同的条件下打印(这个打印应该是在 little_shell 触发的某个命令下进入打印状态),这个命令如果已经做好了频率测量的初始化和配置,那就不需要再配置", + finalResponse: "", + expected: "retry", + expectedContinuePromptIncludes: [ + "未完成", + "原始需求", + "继续按照原始需求", + ], + expectedContinuePromptExcludes: [ + "无参", + "无参数", + "mpu_sensor_get_frequency_millihz()", + "mpu_sensor_get_filtered_frequency_millihz()", + "把 `mpu_sensor_get_frequency_millihz`", + "return value", + "返回 `uint32_t`", + "FirmWare/Core/Src/main.c", + "第 276-292 行", + ], + expectedContinuePromptMaxChars: 420, + expectedContinuePromptMaxLines: 6, + terminalStatus: "failed", + terminalError: "exceeded retry limit, last status: 429 Too Many Requests, request id: 8qegpzommds", + outputs: [ + { + channel: "user", + text: "71-Freq 工程里面的 main.c:282 打印了 period_sum/mpu_read_num,我需要在282的后面新增两个打印,分别是打印滑动滤波前和滑动滤波后的值,你先分析要使得能够正常打印,需要哪些工作(例如初始化、配置等),并且在 period_sum/mpu_read_num 相同的条件下打印(这个打印应该是在 little_shell 触发的某个命令下进入打印状态),这个命令如果已经做好了频率测量的初始化和配置,那就不需要再配置\n", + method: "enqueue", + }, + { + channel: "system", + text: "attempt 2/99 queue=constar provider=main-server cwd=/root/unidesk mode=retry model=gpt-5.5\n", + method: "queue", + }, + { + channel: "system", + text: "上一次 judge feedback 曾指定了具体 printf 形态;其中无参读取不是原始用户要求,只是 judge 推测。\n", + method: "queue", + }, + { + channel: "command", + text: "item/started: bun scripts/cli.ts debug dispatch D601 host.ssh --ssh-command 'cd /mnt/f/Work/ConStart/projects/71-00075-11 && sed -n \"276,292p\" FirmWare/Core/Src/main.c' status=inProgress", + method: "item/started", + }, + { + channel: "error", + text: "exceeded retry limit, last status: 429 Too Many Requests, request id: 8qegpzommds\n", + method: "error", + }, + { channel: "error", text: "turn completed status=failed\n", method: "turn/completed" }, + ], + events: [ + { at: nowIso(), method: "item/started", itemType: "commandExecution", status: "inProgress", message: "reading D601 main.c context" }, + { at: nowIso(), method: "turn/completed", status: "failed", message: "exceeded retry limit, last status: 429 Too Many Requests" }, + ], + }, + { + id: "frontend_visible_change_without_rebuild_should_retry", + prompt: "unidesk 把侧边栏的 `微服务` 改成 `用户服务`,单纯进行改名,长期文档中也要同步修改命名,明确概念,用户服务指的是在 unidesk 核心服务上挂载的用户使用的服务,unidesk 核心缺少这些服务也依然可以运行", + finalResponse: "已完成,属于纯命名调整,没有改路由/API/CLI 的兼容命名。\n\n- 前端侧边栏主模块从 `微服务` 改为 `用户服务`:`src/components/frontend/src/navigation.ts`\n- 同步了前端可见文案,包括用户服务目录、顶部状态、各服务页 eyebrow/raw title/空状态。\n- 更新 E2E 选择器和断言,Playwright 现在点击/校验 `用户服务`。\n- 长期文档已同步概念。\n\n验证已跑:\n- `bun scripts/cli.ts check` 通过\n- `bun scripts/cli.ts help` 过滤确认 `microservice` 命令描述已改为 user services\n\n未跑完整公网 E2E;本次只做命名和文档同步。", + expected: "retry", + terminalStatus: "completed", + outputs: [ + { + channel: "user", + text: "unidesk 把侧边栏的 `微服务` 改成 `用户服务`,单纯进行改名,长期文档中也要同步修改命名,明确概念,用户服务指的是在 unidesk 核心服务上挂载的用户使用的服务,unidesk 核心缺少这些服务也依然可以运行\n", + method: "enqueue", + }, + { channel: "diff", text: "item/completed: file changes status=completed; M src/components/frontend/src/navigation.ts M src/components/frontend/src/app.tsx M docs/reference/microservices.md", method: "item/completed" }, + { channel: "command", text: "item/completed: bun scripts/cli.ts check status=completed; ok=true", method: "item/completed" }, + { channel: "command", text: "item/completed: bun scripts/cli.ts help | python3 ... status=completed; microservice descriptions mention user services", method: "item/completed" }, + { + channel: "assistant", + text: "已完成,属于纯命名调整。验证已跑:bun scripts/cli.ts check 通过;bun scripts/cli.ts help 过滤确认 microservice 命令描述已改为 user services。未跑完整公网 E2E;本次只做命名和文档同步。", + method: "item/agentMessage/delta", + }, + { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, + ], + events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], + }, + { + id: "codex_1778483956252_c65680_mid_task_confirmation_should_retry", + prompt: "code-queue的这个卡片已经没有意义了,因为已经能够在 TraceView 展开,所以应当取消这个卡片,只保留TraceView即可\n\n```\ncodex_1778483881863_a48b0a\n\nPrompt 全量\nqueued\nmodel=gpt-5.5\ncwd=/root/unidesk\ncreated=2026/5/11 15:18:01\n29 lines / 385 chars\n渐进式 Trace\n首屏使用后端 Summary;展开 prompt / 步骤时只按需拉取对应片段,不一次性拉取完整 transcript。\n...\n```", + finalResponse: "我这边已完成 Code Queue 前端改动并跑过检查,但刚刚发现一个我没有修改的文件也变成了 modified:\n\n- `src/components/microservices/code-queue/src/index.ts`\n\n开始时工作树是干净的,我没有编辑这个文件。按安全规则我需要先停下确认,避免覆盖你的/其他进程的变更。\n\n请确认要我怎么处理:\n\n1. 忽略这个文件,只继续交付我已改的前端相关变更\n2. 先查看这个文件的 diff,判断来源和影响\n3. 你会自行处理,我这边只汇报已完成内容", + expected: "retry", + terminalStatus: "completed", + outputs: [ + { + channel: "user", + text: "code-queue的这个卡片已经没有意义了,因为已经能够在 TraceView 展开,所以应当取消这个卡片,只保留TraceView即可\n\n```\ncodex_1778483881863_a48b0a\n\nPrompt 全量\nqueued\nmodel=gpt-5.5\ncwd=/root/unidesk\ncreated=2026/5/11 15:18:01\n29 lines / 385 chars\n渐进式 Trace\n首屏使用后端 Summary;展开 prompt / 步骤时只按需拉取对应片段,不一次性拉取完整 transcript。\n...\n```\n", + method: "enqueue", + }, + { channel: "diff", text: "item/completed: file changes status=completed; M docs/reference/frontend.md M scripts/src/e2e.ts M src/components/frontend/public/app.js M src/components/frontend/public/style.css M src/components/frontend/src/code-queue.tsx", method: "item/completed" }, + { channel: "command", text: "item/completed: bun run --cwd src/components/frontend check status=completed; ok=true", method: "item/completed" }, + { channel: "command", text: "item/completed: bun run --cwd src/components/frontend build status=completed; ok=true", method: "item/completed" }, + { channel: "command", text: "item/completed: bun scripts/cli.ts check status=completed; ok=true", method: "item/completed" }, + { channel: "command", text: "item/completed: git status --short status=completed; M src/components/microservices/code-queue/src/index.ts also present", method: "item/completed" }, + { + channel: "assistant", + text: "我这边已完成 Code Queue 前端改动并跑过检查,但刚刚发现一个我没有修改的文件也变成了 modified:\n\n- `src/components/microservices/code-queue/src/index.ts`\n\n开始时工作树是干净的,我没有编辑这个文件。按安全规则我需要先停下确认,避免覆盖你的/其他进程的变更。\n\n请确认要我怎么处理:\n\n1. 忽略这个文件,只继续交付我已改的前端相关变更\n2. 先查看这个文件的 diff,判断来源和影响\n3. 你会自行处理,我这边只汇报已完成内容", + method: "item/agentMessage/delta", + }, + { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, + ], + events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], + }, + { + id: "codex_1778476426776_a2bf19_not_deployed_should_retry", + prompt: "originprompt -> 执行过程 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/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: [ + { + channel: "user", + 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/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;我没有重建运行中的容器。若要上线验证可重建 code-queue 和 frontend。", + method: "item/agentMessage/delta", + }, + { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, + ], + events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], + }, + { + 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 浏览器时区下验证顶部北京时间时钟、任务历史、Code Queue card、Codex Trace 和 Pipeline run 时间都显示北京时间。本轮不是只改源码未上线,运行中 served UI 已验证通过。", + expected: "complete", + terminalStatus: "completed", + outputs: [ + { + channel: "user", + text: "unidesk 的所有 frontend 时间显示必须采用北京时间,要修改 unidesk 的前端,并在长期参考文档中要求这一点\n", + method: "enqueue", + }, + { + channel: "system", + text: "judge=retry confidence=0.95 source=minimax: Frontend bundle was not rebuilt/deployed; finalResponse explicitly states the frontend rebuild was skipped to avoid service restart.\n", + method: "judge", + }, + { + channel: "system", + text: "上一次 judge 判定为 retry:当前实现仍是未上线/未完成状态,不能只复述源码修改。\n", + method: "queue", + }, + { + channel: "command", + text: "item/completed: bun scripts/cli.ts server rebuild frontend status=completed; job succeeded; unidesk-frontend healthy", + method: "item/completed", + }, + { + channel: "command", + text: "item/completed: Playwright live UI verification under America/Los_Angeles timezone status=completed; topbar/task/Codex/Pipeline times use Asia/Shanghai", + method: "item/completed", + }, + { + channel: "assistant", + text: "已完成北京时间任务并已上线验证。长期参考文档已加入 Frontend Time Zone Policy。已执行 server rebuild frontend,job succeeded,unidesk-frontend healthy。Playwright live UI verification 通过。本轮不是只改源码未上线,运行中 served UI 已验证通过。", + method: "item/agentMessage/delta", + }, + { channel: "system", text: "turn completed status=completed\n", method: "turn/completed" }, + ], + events: [{ at: nowIso(), method: "turn/completed", status: "completed" }], + }, + { + id: "transport_closed_before_terminal", + prompt: "Refactor the queue worker and run the focused tests.", + finalResponse: "", + expected: "retry", + terminalStatus: null, + transportClosedBeforeTerminal: true, + stderrTail: "stream disconnected before completion: upstream overloaded; app-server closed before turn/completed", + outputs: [ + { channel: "user", text: "Refactor the queue worker and run the focused tests.\n", method: "enqueue" }, + { channel: "system", text: "attempt 1/3 mode=initial model=gpt-5.4-mini\n", method: "queue" }, + ], + events: [{ at: nowIso(), method: "thread/status/changed", status: "inProgress" }], + }, + { + id: "user_interrupted", + prompt: "Run a long shell command, then produce a report.", + finalResponse: "", + expected: "fail", + terminalStatus: "interrupted", + cancelRequested: true, + outputs: [ + { channel: "user", text: "Run a long shell command, then produce a report.\n", method: "enqueue" }, + { channel: "system", text: "interrupt requested\n", method: "turn/interrupt" }, + { channel: "error", text: "turn completed status=interrupted\n", method: "turn/completed" }, + ], + events: [{ at: nowIso(), method: "turn/completed", status: "interrupted" }], + }, +]; diff --git a/src/components/microservices/code-queue/src/judge.ts b/src/components/microservices/code-queue/src/judge.ts new file mode 100644 index 00000000..570f97c2 --- /dev/null +++ b/src/components/microservices/code-queue/src/judge.ts @@ -0,0 +1,715 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import type { + CodexRunResult, + FeedbackPromptRecord, + JudgeDecision, + JudgeProbeCase, + JudgeResult, + JsonValue, + LiveOutput, + MiniMaxJudgeResponse, + ParsedJudgeJson, + QueueTask, +} from "./types"; + +export interface JudgeRuntimeContext { + config: { + minimaxApiKey: string; + minimaxApiBase: string; + minimaxModel: string; + judgeTimeoutMs: number; + judgeRepairAttempts: number; + judgeMaxTokens: number; + }; + logger: (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void; + safePreview: (value: string, max?: number) => string; + userPromptForDisplay: (prompt: string) => string; + taskFullOutput: (task: QueueTask) => LiveOutput[]; + taskReferenceIds: (task: QueueTask) => string[]; + extractRecord: (value: unknown) => Record | null; + extractString: (value: unknown, key: string) => string | null; + promptLineCount: (text: string) => number; + judgeFailRetryLimit: number; +} + +let context: JudgeRuntimeContext | null = null; + +export function configureJudge(runtimeContext: JudgeRuntimeContext): void { + context = runtimeContext; +} + +function ctx(): JudgeRuntimeContext { + if (context === null) throw new Error("judge module is not configured"); + return context; +} + +function config(): JudgeRuntimeContext["config"] { + return ctx().config; +} + +function logger(level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue): void { + ctx().logger(level, message, data); +} + +function safePreview(value: string, max?: number): string { + return ctx().safePreview(value, max); +} + +function userPromptForDisplay(prompt: string): string { + return ctx().userPromptForDisplay(prompt); +} + +function taskFullOutput(task: QueueTask): LiveOutput[] { + return ctx().taskFullOutput(task); +} + +function taskReferenceIds(task: QueueTask): string[] { + return ctx().taskReferenceIds(task); +} + +function extractRecord(value: unknown): Record | null { + return ctx().extractRecord(value); +} + +function extractString(value: unknown, key: string): string | null { + return ctx().extractString(value, key); +} + +function promptLineCount(text: string): number { + return ctx().promptLineCount(text); +} + +function fallbackJudge(result: CodexRunResult, minimaxError?: string): JudgeResult { + if (result.transportClosedBeforeTerminal || result.terminalStatus === null) { + return { decision: "retry", confidence: 0.75, reason: "Codex app-server 在 turn/completed 之前关闭。", continuePrompt: retryInstruction, source: "fallback" }; + } + if (result.terminalStatus === "failed") { + return { decision: "retry", confidence: 0.7, reason: result.terminalError ? `Codex turn 失败:${result.terminalError}` : "Codex turn 失败。", continuePrompt: retryInstruction, source: "fallback" }; + } + 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 当作任务已完成。`, + source: "fallback", + }; + } + return { decision: "complete", confidence: 0.65, reason: "Codex 输出了 completed 状态的 turn/completed,且未配置 MiniMax judge。", source: "fallback" }; +} + +export 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 查询验证。"; + +export 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 继续。不要把更早 attempt 的限流/中断证据自动当作当前 attempt 的完成门禁;如果当前最新 attempt 已经提供完整完成证据,可以判定 complete。", + schema: { decision: "complete|retry|fail", confidence: "0..1", reason: "中文短句", continuePrompt: "decision=retry 时必填,除非确实没有可用的继续提示;内容必须是中文,保持简洁,不要粘贴原始任务、引用上下文、transcript 或 JSON" }, + originalTask: originalUserTask, + resolvedPromptForCodex, + attempt: task.currentAttempt, + maxAttempts: task.maxAttempts, + executionRecord: { + terminalStatus: result.terminalStatus, + terminalError: result.terminalError, + transportClosedBeforeTerminal: result.transportClosedBeforeTerminal, + appServerExitCode: result.appServerExit.code, + appServerSignal: result.appServerExit.signal, + stderrTail: safePreview(result.appServerExit.stderrTail, 2000), + finalResponse: safePreview(result.finalResponse, 6000), + finalResponseChars: result.finalResponse.length, + finalResponseMissing: result.finalResponse.trim().length === 0, + latestAttempt, + judgeFailCount: task.judgeFailCount, + judgeFailRetryLimit: ctx().judgeFailRetryLimit, + 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 的反馈;只能基于 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,并在部署后验证运行中的真实行为。", + "如果受影响服务包含 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 恢复本任务后继续验证。", + ], + 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 任务尚未完成的证据。", + "判断 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。", + "对于 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 运行证据,证明请求分数和比较结果。", + "对于 hardware、firmware、provider、WSL、SSH passthrough、skill、compile、flash/download、serial 或 board-comm 任务,complete 需要请求的操作步骤证据;如果请求了 download/board/serial 验证但没有证据,只有部分文档或一次成功 build 应选择 retry。", + "如果最终回复主要处理后来的 steer prompt,而原始任务仍有部分未完成,应选择 retry,并给出同时协调 steer 与原始任务的 continuation prompt。", + ], + classicFailureExamples: [ + { + pattern: "原始任务要求:没有通用 PikaPython benchmark skill 时不能达到 4/4;有该 skill 时必须达到 4/4。", + incompleteEvidence: "执行代理只是把定向 prompt 内容移入 skill,运行了 type/component/unit 测试,并表示为节省 quota 没有启动 PikaBench-4 baseline/no-skill 实跑。", + requiredDecision: "retry", + reason: "请求的 with-skill/no-skill 经验 pipeline 对照没有运行,因此声明的验收条件没有被证明。", + }, + { + pattern: "原始任务要求学习 D601 ConStart/constar 固件工作区、使用 skill 完成 compile/download 等,并更新长期文档;后续 steer 要求把项目文档移动到 constar/docs。", + incompleteEvidence: "执行代理做了一些 skill discovery 和文档迁移,但最终回复主要围绕 steer 驱动的文档迁移,未证明所有请求的 compile/download/serial/board-comm 操作验收点已完成。", + requiredDecision: "retry", + reason: "这是原始任务未完成,不是不可重试失败;应继续同一个 session,补齐缺失的操作验证和文档。", + }, + { + pattern: "原始任务要求停止 ClaudeQQ 登录二维码自动刷新。代码已编辑且 frontend rebuild 成功,但 Codex turn 后来在 E2E 或依赖验证仍在运行时因 429 Too Many Requests / exceeded retry limit 失败结束。", + incompleteEvidence: "终止 turn 状态是 failed,最终回复为空或缺少完整总结,且验证因服务限流没有完成。", + 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 变更实际上未生效。", + requiredDecision: "retry", + reason: "WebUI 可见变更只有在 deployed/served frontend 已重建或刷新并验证后才算完成;源码编辑加 type check 可能让 live UI 保持不变。", + }, + { + 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: "原始任务要求 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 部署已修改的 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 继续。", + }, + ], + }); +} + +function parseRecordJson(text: string, source: string): ParsedJudgeJson { + const parsed = JSON.parse(text) as unknown; + if (typeof parsed === "string") return parseJudgeJson(parsed); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${source} parsed to non-object JSON`); + return { value: parsed as Record, source }; +} + +function balancedJsonCandidates(text: string): string[] { + const candidates: string[] = []; + for (let start = 0; start < text.length; start += 1) { + if (text[start] !== "{") continue; + let depth = 0; + let inString = false; + let escaped = false; + for (let index = start; index < text.length; index += 1) { + const char = text[index] ?? ""; + if (inString) { + if (escaped) { + escaped = false; + } else if (char === "\\") { + escaped = true; + } else if (char === "\"") { + inString = false; + } + continue; + } + if (char === "\"") { + inString = true; + } else if (char === "{") { + depth += 1; + } else if (char === "}") { + depth -= 1; + if (depth === 0) { + candidates.push(text.slice(start, index + 1)); + break; + } + } + } + } + return candidates; +} + +function judgeJsonCandidates(text: string): Array<{ source: string; text: string }> { + const normalized = text.replace(/^\uFEFF/u, "").trim(); + 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 seen = new Set(); + return candidates.filter((candidate) => { + const key = candidate.text; + if (key.length === 0 || seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +export function parseJudgeJson(text: string): ParsedJudgeJson { + let lastError = "no candidate JSON was found"; + for (const candidate of judgeJsonCandidates(text)) { + try { + const parsed = parseRecordJson(candidate.text, candidate.source); + if (validJudgeDecisionValue(parsed.value.decision)) return parsed; + lastError = `${candidate.source} parsed JSON does not contain a valid decision`; + } catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + throw new Error(`MiniMax judge did not return parseable JSON after denoise: ${lastError}; preview=${safePreview(text, 500)}`); +} + +function normalizedDecision(value: unknown): JudgeDecision { + if (value === "complete" || value === "retry" || value === "fail") return value; + if (value === "continue") return "retry"; + return "retry"; +} + +function validJudgeDecisionValue(value: unknown): boolean { + return value === "complete" || value === "retry" || value === "fail" || value === "continue"; +} + +const retryTaskSummaryMaxChars = 1200; +const judgeReasonPromptMaxChars = 1200; +export const compactContinuationPromptTargetChars = 1200; +export const continuePromptSourceBudgetChars = 4000; + +export function judgeReasonForPrompt(reason: string): string { + return safePreview(reason, judgeReasonPromptMaxChars) || "(empty)"; +} + +function continuationPromptForRetry(prompt: string): string { + return prompt.trim(); +} + +export function compactRetryTaskContext(task: QueueTask): string { + const basePrompt = task.basePrompt || userPromptForDisplay(task.prompt); + const referenceTaskIds = taskReferenceIds(task); + return [ + "原始任务摘要(同一 thread 已有完整上文,不重新注入引用全文):", + safePreview(basePrompt, retryTaskSummaryMaxChars) || "(empty)", + referenceTaskIds.length > 0 ? `引用任务 ID:${referenceTaskIds.join(", ")}` : "", + `按需查询有界摘要:bun scripts/cli.ts codex task ${task.id}`, + ].filter((line) => line.length > 0).join("\n"); +} + +export function queueRecoveryRetryPrompt(task: QueueTask, reason: string): string { + return [ + retryInstruction, + "Code Queue 服务在任务运行中重启/停止;这是自动恢复提示,不是新任务,也不需要重新粘贴原始任务或引用全文。", + "如果本轮任务正是修改 Code Queue 自身,不要等待当前 task 退出;服务重启已经发生,继续完成恢复后的验证和剩余交付。", + `恢复原因:${judgeReasonForPrompt(reason)}`, + "请基于当前 thread 上文继续,只做最小必要状态核查,恢复未完成的等待、验证、部署或命令;最终 response 必须给出真实结果证据。", + "原始任务摘要/按需查询:", + compactRetryTaskContext(task), + ].join("\n\n"); +} + +export function retryPrompt(task: QueueTask, judge: JudgeResult): string { + if (judge.continuePrompt !== undefined && judge.continuePrompt.trim().length > 0) return continuationPromptForRetry(judge.continuePrompt); + return [ + retryInstruction, + "上一次 judge 判定为 retry,下面是必须传递给本轮 continuation 的 judge feedback。请优先补齐这些缺口,不要只做泛泛的状态核查。", + `judge 来源:${judge.source}`, + `judge 置信度:${judge.confidence.toFixed(2)}`, + `judge 未完成原因:${judgeReasonForPrompt(judge.reason)}`, + "请在本轮最终 response 中明确说明已如何解决上述 judge feedback;如果 judge 要求 benchmark/上线/运行中验证,就必须提供对应真实命令和结果证据。", + "原始任务摘要/按需查询:", + compactRetryTaskContext(task), + ].join("\n\n"); +} + +export function judgeFailContinuationPrompt(task: QueueTask, judge: JudgeResult, failCount: number): string { + const limit = ctx().judgeFailRetryLimit; + const parts = [ + `上一次 judge 判定为 fail (${failCount}/${limit}),但 Code Queue 策略要求:非用户取消、非确定不可恢复的情况必须当作“未完成”继续当前 session,直到 fail 累计 ${limit} 次才真正放弃。`, + "这是同一个 Codex thread 的 continuation,不是新任务;不要重新开局或从头摸索,只补齐缺失验收项。", + `judge 理由:${judgeReasonForPrompt(judge.reason)}`, + "请不要放弃或新开任务。继续完成原始任务中尚未完成/未验证的验收项,优先补齐 judge 指出的缺口,并给出真实命令、文件或运行结果证据。", + ]; + if (judge.continuePrompt !== undefined && judge.continuePrompt.trim().length > 0) { + parts.push("judge 建议的继续提示:", continuationPromptForRetry(judge.continuePrompt)); + } + parts.push("原始任务摘要/按需查询:", compactRetryTaskContext(task)); + return parts.join("\n\n"); +} + +export 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; +} + +const judgeSystemPrompt = [ + "你是严格的任务状态分类器。", + "你没有工具访问,不能执行命令、读取文件或续写编码 agent 行为;只能根据 user 消息里的 JSON evidence 判定。", + "只能返回一个紧凑原始 JSON object。禁止输出 Markdown fence、、思考过程、解释性正文或注释。", + "所有自然语言字段必须使用中文,尤其是 reason 和 continuePrompt。", +].join(""); + +const judgeResponseSchema = { + decision: "complete|retry|fail", + confidence: "0..1", + reason: "中文短句", + continuePrompt: `decision=retry 时必填,除非确实没有可用的继续提示;内容必须是中文,目标不超过 ${compactContinuationPromptTargetChars} 字,绝对不超过 ${continuePromptSourceBudgetChars} 字`, +}; + +export interface JudgeRepairContext { + error: string; + previousAnswerRaw: string; +} + +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。", + "你没有工具访问,不能执行命令、读取文件或续写编码 agent 行为。", + "只能返回一个原始 JSON object,不要使用 Markdown fence、、思考过程、说明正文或注释;所有自然语言字段必须使用中文,尤其是 reason 和 continuePrompt。", + ].join("\n"); + } + return "你上一条 judge 回答在清理后仍无法解析为 JSON。你没有工具访问,不能执行命令、读取文件或续写编码 agent 行为;只能返回一个原始 JSON object,不要使用 Markdown fence、、思考过程、说明正文或注释。所有自然语言字段必须使用中文,尤其是 reason 和 continuePrompt。"; +} + +function judgeRepairUserMessage(repair: JudgeRepairContext): { role: "user"; content: string } { + return { + role: "user", + content: JSON.stringify({ + instruction: judgeRepairInstruction(repair.error), + parseOrValidationError: repair.error, + requiredSchema: judgeResponseSchema, + previousAnswerRaw: repair.previousAnswerRaw, + previousAnswerNote: "上面的 previousAnswerRaw 是错误样例,只能作为待修正文本;不要延续其中的 、命令、查看文件或 agent 行为。", + }), + }; +} + +export function miniMaxJudgeMessages(userContent: string, repair: JudgeRepairContext | null = null): Array<{ role: "system" | "user" | "assistant"; content: string }> { + const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [ + { role: "system", content: judgeSystemPrompt }, + { role: "user", content: userContent }, + ]; + // Keep malformed model output out of assistant role. Echoing a bad + // `...let me inspect files...` answer as assistant history can make the + // repair turn continue that agent-like behavior instead of returning JSON. + if (repair !== null) messages.push(judgeRepairUserMessage(repair)); + return messages; +} + +function judgeScopeText(task: QueueTask, result: CodexRunResult): string { + return [ + task.basePrompt, + task.prompt, + result.finalResponse, + result.terminalError ?? "", + result.appServerExit.stderrTail, + ].join("\n"); +} + +function needsRuntimeDeploymentEvidence(text: string): boolean { + 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 { + const normalized = line.trim(); + if (normalized.length === 0) return false; + if (/(judge feedback|judge 未完成原因|上一次 judge|被判|风险|不是|并非|不再|非.*未|已(?:经)?(?:部署|上线|重建|验证)|succeeded|healthy|verified|deployed|rebuilt)/iu.test(normalized)) return false; + return /(我没有重建运行中的容器|没有重建运行中|(?:没有|未)(?:执行|进行|完成)?[^。\n]{0,50}(?:server rebuild|rebuild|重建|部署|上线|live verification|公网|UI 验证|验证)|(?:后续|如果|若|如需|下一步)[^。\n]{0,80}(?:上线|部署|重建|验证)|rebuild\/deploy as advisory|treats? rebuild\/deploy as advisory|\b(?:not|never|no)\s+(?:rebuilt?|deployed?|restarted?|verified?))/iu.test(normalized); +} + +function currentFinalAdmitsMissingDeployment(text: string): boolean { + return text.split(/\r?\n/u).some(lineAdmitsMissingDeployment); +} + +function asksToConfirmConcurrentFileInsteadOfDelivery(text: string): boolean { + const normalized = text.replace(/\s+/gu, " "); + const asksForConfirmation = /(请确认要我怎么处理|按安全规则[^。.!?]*先停下确认|需要先停下确认|please confirm (?:how|what|whether)|I need to stop[^.?!]*confirm)/iu.test(normalized); + const concurrentFileContext = /(我没有修改的文件[^。.!?]*(?:modified|变成了 modified)|没有编辑这个文件|unexpected[^.?!]*(?:modified|change)|concurrent[^.?!]*(?:file|task|work)|其他任务[^。.!?]*并发|并发开发|忽略这个文件,只继续交付|只继续交付我已改的[^。.!?]*变更)/iu.test(normalized); + return asksForConfirmation && concurrentFileContext; +} + +function concurrentFileConfirmationFeedbackPrompt(task: QueueTask, reason: string): string { + return [ + retryInstruction, + "上一次 judge 判定为 retry:上一轮没有自主完成交付,而是在中途把并发修改文件的问题抛给用户确认。", + `judge 未完成原因:${judgeReasonForPrompt(reason)}`, + "这是其他任务正在并发开发,忽略这个文件,只继续交付我已改的前端相关变更。", + "不要再要求用户选择 1/2/3,也不要检查、覆盖或回滚那个并发修改文件;只围绕自己已改的前端相关文件完成必要验证、总结交付,并在最终 response 中列出实际验证结果。", + "如果自己的前端改动需要构建、上线或运行中验证,按项目规则执行对应 rebuild/live verification 后再声明完成;如确有阻塞,请给出具体命令和错误。", + "原始任务摘要/按需查询:", + compactRetryTaskContext(task), + ].join("\n\n"); +} + +function deploymentFeedbackPrompt(task: QueueTask, reason: string): string { + return [ + retryInstruction, + "上一次 judge 判定为 retry:当前实现仍是未上线/未完成状态,不能只复述源码修改。", + `judge 未完成原因:${judgeReasonForPrompt(reason)}`, + "请先确认受影响范围;凡是 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 { + ...judge, + decision: "retry", + confidence: Math.max(judge.confidence, 0.94), + reason, + continuePrompt: concurrentFileConfirmationFeedbackPrompt(task, reason), + raw: { previous: judge.raw ?? null, _safetyOverride: "mid_task_user_confirmation_concurrent_file" }, + }; + } + if (judge.decision !== "complete") return judge; + const scopeText = judgeScopeText(task, result); + if (!needsRuntimeDeploymentEvidence(scopeText) || !currentFinalAdmitsMissingDeployment(currentFinalText)) return judge; + const reason = "最终回复承认 runtime/UI/service 变更尚未部署到运行中服务或已服务 UI;只有源码编辑和检查还不完整。"; + return { + ...judge, + decision: "retry", + confidence: Math.max(judge.confidence, 0.92), + reason, + continuePrompt: deploymentFeedbackPrompt(task, reason), + raw: { previous: judge.raw ?? null, _safetyOverride: "missing_runtime_deployment" }, + }; +} + +export async function judgeTask(task: QueueTask, result: CodexRunResult): Promise { + if (config().minimaxApiKey.length === 0) return applyFallbackSafetyOverrides(task, result, fallbackJudge(result)); + const judgePromptContent = judgePrompt(task, result); + try { + let lastParseError: string | null = null; + let repairContext: JudgeRepairContext | null = null; + for (let repairAttempt = 0; repairAttempt <= config().judgeRepairAttempts; repairAttempt += 1) { + const messages = miniMaxJudgeMessages(judgePromptContent, repairContext); + const response = await requestMiniMaxJudge(messages); + const preDenoiseContent = response.content; + try { + const parsedResult = parseJudgeJson(preDenoiseContent); + const parsed = parsedResult.value; + if (parsedResult.source !== "direct") { + logger("info", "judge_json_denoised", { taskId: task.id, parseSource: parsedResult.source, repairAttempt }); + } + const decision = normalizedDecision(parsed.decision); + const continuePrompt = parsedContinuePromptForJudge(parsed, decision); + const confidenceRaw = Number(parsed.confidence ?? 0.5); + const judge: JudgeResult = { + decision, + confidence: Number.isFinite(confidenceRaw) ? Math.max(0, Math.min(1, confidenceRaw)) : 0.5, + reason: typeof parsed.reason === "string" ? parsed.reason : "MiniMax judge returned a decision.", + 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); + logger("warn", "judge_json_parse_retry", { + taskId: task.id, + repairAttempt: repairAttempt + 1, + maxRepairAttempts: config().judgeRepairAttempts, + error: safePreview(lastParseError, 800), + preDenoiseResponsePreview: safePreview(preDenoiseContent, 1200), + }); + repairContext = { error: lastParseError, previousAnswerRaw: preDenoiseContent }; + } + } + throw new Error(lastParseError ?? "MiniMax judge exhausted JSON repair attempts"); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger("warn", "judge_failed_fallback", { taskId: task.id, error: message }); + return applyFallbackSafetyOverrides(task, result, fallbackJudge(result, message)); + } +} + +async function requestMiniMaxJudge(messages: Array<{ role: "system" | "user" | "assistant"; content: string }>): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), config().judgeTimeoutMs); + try { + const response = await fetch(`${config().minimaxApiBase}/chat/completions`, { + method: "POST", + headers: { authorization: `Bearer ${config().minimaxApiKey}`, "content-type": "application/json" }, + body: JSON.stringify({ + model: config().minimaxModel, + temperature: 0, + max_tokens: config().judgeMaxTokens, + messages, + }), + signal: controller.signal, + }); + const rawText = await response.text(); + if (!response.ok) throw new Error(`MiniMax HTTP ${response.status}: ${safePreview(rawText, 1000)}`); + let content = rawText; + try { + const payload = JSON.parse(rawText) as Record; + const first = Array.isArray(payload.choices) ? extractRecord(payload.choices[0]) : null; + const message = extractRecord(first?.message); + content = extractString(message, "content") + ?? extractString(payload, "content") + ?? extractString(payload, "reply") + ?? extractString(payload, "text") + ?? (Object.prototype.hasOwnProperty.call(payload, "decision") ? JSON.stringify(payload) : rawText); + } catch { + content = rawText; + } + return { rawText, content }; + } finally { + clearTimeout(timer); + } +} diff --git a/src/components/microservices/code-queue/src/notifications.ts b/src/components/microservices/code-queue/src/notifications.ts new file mode 100644 index 00000000..8f1dcfd5 --- /dev/null +++ b/src/components/microservices/code-queue/src/notifications.ts @@ -0,0 +1,567 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import postgres from "postgres"; +import type { ClaudeQqNotificationItem, ClaudeQqNotificationOutboxState, ClaudeQqNotificationRow, JsonValue, QueueTask, RuntimeConfig } from "./types"; + +type SqlExecutor = postgres.Sql | postgres.TransactionSql; + +export interface NotificationsContext { + config: Pick; + activeRunCount: () => number; + activeRunSlotCount: () => number; + activeRunTaskIds: () => string[]; + errorToJson: (error: unknown) => JsonValue; + hasRunnableTask: () => boolean; + lastAssistantMessage: (task: QueueTask) => JsonValue; + loadAllTasksForRead: () => Promise; + logger: (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void; + nonNegativeElapsed: (startMs: number | null, endMs: number | null) => number | null; + nowIso: () => string; + processingQueueCount: () => number; + queueCount: () => number; + queueIdOf: (task: QueueTask) => string; + safePreview: (value: string, max?: number) => string; + shutdownRequested: () => boolean; + sql: postgres.Sql; + taskTimestamp: (value: string | null) => string | null; + tasks: () => QueueTask[]; + timestampMs: (value: string | null | undefined) => number | null; + databaseReady: () => boolean; +} + +let context: NotificationsContext | null = null; +let claudeQqNotificationOutbox: ClaudeQqNotificationOutboxState = { version: 1, updatedAt: new Date().toISOString(), items: [] }; +const sentTaskNotificationKeys = new Set(); +const inFlightTaskNotificationKeys = new Set(); +let idleNotificationSent = true; +let idleNotificationInFlight = false; +let claudeQqNotificationDrainTimer: ReturnType | null = null; +let claudeQqNotificationDrainInFlight = false; + +export function configureNotifications(runtimeContext: NotificationsContext): void { + context = runtimeContext; +} + +function ctx(): NotificationsContext { + if (context === null) throw new Error("notifications module is not configured"); + return context; +} + +function terminalTask(task: QueueTask): boolean { + return task.status === "succeeded" || task.status === "failed" || task.status === "canceled"; +} + +function durationMsBetween(startAt: string | null | undefined, endAt: string | null | undefined): number | null { + return ctx().nonNegativeElapsed(ctx().timestampMs(startAt), ctx().timestampMs(endAt)); +} + +function formatDurationMs(value: number | null): string { + if (value === null) return "-"; + const totalSeconds = Math.max(0, Math.floor(value / 1000)); + const days = Math.floor(totalSeconds / 86_400); + const hours = Math.floor((totalSeconds % 86_400) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + const parts: string[] = []; + if (days > 0) parts.push(`${days}d`); + if (hours > 0 || parts.length > 0) parts.push(`${hours}h`); + if (minutes > 0 || parts.length > 0) parts.push(`${minutes}m`); + parts.push(`${seconds}s`); + return parts.join(" "); +} + +function emptyClaudeQqNotificationOutbox(): ClaudeQqNotificationOutboxState { + return { version: 1, updatedAt: ctx().nowIso(), items: [] }; +} + +function notificationItemFromRow(row: ClaudeQqNotificationRow): ClaudeQqNotificationItem { + const createdAt = ctx().taskTimestamp(String(row.created_at)) ?? ctx().nowIso(); + const updatedAt = ctx().taskTimestamp(String(row.updated_at)) ?? createdAt; + return { + id: row.id, + kind: row.kind || "unknown", + dedupKey: row.dedup_key || row.id, + target: row.target || "-", + message: row.message || "", + createdAt, + updatedAt, + attempts: Number.isInteger(row.attempts) && row.attempts >= 0 ? row.attempts : 0, + nextAttemptAt: ctx().taskTimestamp(String(row.next_attempt_at)) ?? updatedAt, + lastError: typeof row.last_error === "string" && row.last_error.length > 0 ? row.last_error : null, + sentAt: row.sent_at === null ? null : ctx().taskTimestamp(String(row.sent_at)), + }; +} + +async function loadClaudeQqNotificationOutboxFromDatabase(client: SqlExecutor = ctx().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_code_queue_notifications + ORDER BY created_at ASC, id ASC + `; + claudeQqNotificationOutbox = { + version: 1, + updatedAt: ctx().nowIso(), + items: rows.map(notificationItemFromRow).filter((item) => item.id.length > 0 && item.message.length > 0), + }; + pruneClaudeQqNotificationOutbox(); +} + +async function upsertClaudeQqNotificationToDatabase(client: SqlExecutor, item: ClaudeQqNotificationItem): Promise { + await client` + INSERT INTO unidesk_code_queue_notifications ( + id, + kind, + dedup_key, + target, + message, + created_at, + updated_at, + attempts, + next_attempt_at, + last_error, + sent_at + ) VALUES ( + ${item.id}, + ${item.kind}, + ${item.dedupKey}, + ${item.target}, + ${item.message}, + ${ctx().taskTimestamp(item.createdAt) ?? ctx().nowIso()}, + ${ctx().taskTimestamp(item.updatedAt) ?? ctx().nowIso()}, + ${item.attempts}, + ${ctx().taskTimestamp(item.nextAttemptAt) ?? ctx().nowIso()}, + ${item.lastError}, + ${ctx().taskTimestamp(item.sentAt)} + ) + ON CONFLICT (id) DO UPDATE SET + kind = EXCLUDED.kind, + dedup_key = EXCLUDED.dedup_key, + target = EXCLUDED.target, + message = EXCLUDED.message, + updated_at = EXCLUDED.updated_at, + attempts = EXCLUDED.attempts, + next_attempt_at = EXCLUDED.next_attempt_at, + last_error = EXCLUDED.last_error, + sent_at = EXCLUDED.sent_at + `; +} + +async function persistClaudeQqNotificationItem(item: ClaudeQqNotificationItem): Promise { + if (!ctx().databaseReady()) throw new Error("PostgreSQL is not ready for ClaudeQQ notification outbox"); + claudeQqNotificationOutbox.updatedAt = ctx().nowIso(); + const deletedIds = pruneClaudeQqNotificationOutbox(); + const stillPresent = claudeQqNotificationOutbox.items.some((candidate) => candidate.id === item.id); + await ctx().sql.begin(async (client) => { + if (stillPresent) await upsertClaudeQqNotificationToDatabase(client, item); + 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); + pruneClaudeQqNotificationOutbox(); + } +} + +async function persistClaudeQqNotificationOutbox(): Promise { + if (!ctx().databaseReady()) throw new Error("PostgreSQL is not ready for ClaudeQQ notification outbox"); + claudeQqNotificationOutbox.updatedAt = ctx().nowIso(); + const deletedIds = pruneClaudeQqNotificationOutbox(); + await ctx().sql.begin(async (client) => { + for (const item of claudeQqNotificationOutbox.items) await upsertClaudeQqNotificationToDatabase(client, item); + for (const id of deletedIds) await client`DELETE FROM unidesk_code_queue_notifications WHERE id = ${id}`; + }); +} + + +function queueNotificationStats(): Record { + const counts = ctx().tasks().reduce>((memo, task) => { + memo[task.status] = (memo[task.status] ?? 0) + 1; + return memo; + }, {}); + const activeTaskIds = Array.from(new Set([ + ...ctx().activeRunTaskIds(), + ...ctx().tasks().filter((task) => task.status === "running" || task.status === "judging").map((task) => task.id), + ])).sort(); + const activeTask = activeTaskIds.length > 0 ? ctx().tasks().find((task) => task.id === activeTaskIds[0]) ?? null : null; + const queued = (counts.queued ?? 0) + (counts.retry_wait ?? 0); + const running = (counts.running ?? 0) + (counts.judging ?? 0); + return { + running, + queued, + retryWait: counts.retry_wait ?? 0, + judging: counts.judging ?? 0, + total: ctx().tasks().length, + queueCount: ctx().queueCount(), + processingQueueCount: ctx().processingQueueCount(), + activeRunCount: ctx().activeRunCount(), + activeRunSlotCount: ctx().activeRunSlotCount(), + activeTaskIds, + activeTaskElapsed: activeTask === null ? "-" : formatDurationMs(durationMsBetween(activeTask.startedAt ?? activeTask.createdAt, ctx().nowIso())), + }; +} + +function notificationTargetConfigured(): boolean { + if (!ctx().config.notifyClaudeQqEnabled) return false; + return ctx().config.notifyClaudeQqTargetType === "group" + ? ctx().config.notifyClaudeQqGroupId.length > 0 + : ctx().config.notifyClaudeQqUserId.length > 0; +} + +function claudeQqTargetPayload(message: string): Record { + if (ctx().config.notifyClaudeQqTargetType === "group") { + return { targetType: "group", groupId: ctx().config.notifyClaudeQqGroupId, message }; + } + return { targetType: "private", userId: ctx().config.notifyClaudeQqUserId, message }; +} + +function notificationTargetLabel(): string { + return ctx().config.notifyClaudeQqTargetType === "group" + ? `group:${ctx().config.notifyClaudeQqGroupId || "-"}` + : `private:${ctx().config.notifyClaudeQqUserId || "-"}`; +} + +function truncateNotificationText(value: string, maxChars: number): string { + if (value.length <= maxChars) return value; + 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 { + const last = ctx().lastAssistantMessage(task) as Record; + const text = typeof last.text === "string" ? last.text.trimEnd() : ""; + if (text.trim().length > 0) return text; + if (typeof task.lastError === "string" && task.lastError.trim().length > 0) return `(没有最终 assistant response;lastError: ${task.lastError.trim()})`; + return "(没有最终 assistant response)"; +} + +function taskNotificationKey(task: QueueTask): string { + return `${task.id}:${task.status}:${task.finishedAt ?? task.updatedAt}:${task.attempts.length}`; +} + +function rememberTaskNotificationKey(key: string): void { + sentTaskNotificationKeys.add(key); + while (sentTaskNotificationKeys.size > 1000) { + const oldest = sentTaskNotificationKeys.values().next().value as string | undefined; + if (oldest === undefined) break; + sentTaskNotificationKeys.delete(oldest); + } +} + +function claudeQqNotificationId(kind: string, dedupKey: string): string { + return `${kind}:${dedupKey}`; +} + +function claudeQqNotificationOutboxStats(): Record { + const pending = claudeQqNotificationOutbox.items.filter((item) => item.sentAt === null); + const failed = pending.filter((item) => item.lastError !== null); + const oldestPending = pending.reduce((oldest, item) => { + if (oldest === null) return item.createdAt; + return (ctx().timestampMs(item.createdAt) ?? 0) < (ctx().timestampMs(oldest) ?? 0) ? item.createdAt : oldest; + }, null); + return { + storage: "postgres", + total: claudeQqNotificationOutbox.items.length, + pending: pending.length, + failed: failed.length, + sent: claudeQqNotificationOutbox.items.length - pending.length, + inFlight: claudeQqNotificationDrainInFlight, + nextDueAt: pending + .map((item) => item.nextAttemptAt) + .sort((left, right) => (ctx().timestampMs(left) ?? 0) - (ctx().timestampMs(right) ?? 0))[0] ?? null, + oldestPendingAt: oldestPending, + }; +} + +function pruneClaudeQqNotificationOutbox(): string[] { + const beforeIds = new Set(claudeQqNotificationOutbox.items.map((item) => item.id)); + const pending = claudeQqNotificationOutbox.items.filter((item) => item.sentAt === null); + const sent = claudeQqNotificationOutbox.items + .filter((item) => item.sentAt !== null) + .sort((left, right) => (ctx().timestampMs(right.sentAt) ?? 0) - (ctx().timestampMs(left.sentAt) ?? 0)); + const sentBudget = Math.max(0, ctx().config.notifyClaudeQqMaxOutboxItems - pending.length); + claudeQqNotificationOutbox.items = [...pending, ...sent.slice(0, sentBudget)] + .sort((left, right) => (ctx().timestampMs(left.createdAt) ?? 0) - (ctx().timestampMs(right.createdAt) ?? 0)); + const afterIds = new Set(claudeQqNotificationOutbox.items.map((item) => item.id)); + return Array.from(beforeIds).filter((id) => !afterIds.has(id)); +} + +function claudeQqNotificationRetryDelayMs(attempts: number): number { + const exponent = Math.max(0, Math.min(8, attempts - 1)); + return Math.min(30 * 60_000, ctx().config.notifyClaudeQqRetryIntervalMs * (2 ** exponent)); +} + +function scheduleClaudeQqNotificationDrain(delayMs = ctx().config.notifyClaudeQqRetryIntervalMs): void { + if (!notificationTargetConfigured() || ctx().shutdownRequested()) return; + if (claudeQqNotificationOutbox.items.every((item) => item.sentAt !== null)) return; + if (claudeQqNotificationDrainTimer !== null) return; + claudeQqNotificationDrainTimer = setTimeout(() => { + claudeQqNotificationDrainTimer = null; + void drainClaudeQqNotificationOutbox("timer").catch((error) => ctx().logger("warn", "claudeqq_notify_outbox_drain_failed", { trigger: "timer", error: ctx().errorToJson(error) })); + }, Math.max(1000, delayMs)); +} + +async function enqueueClaudeQqNotification(kind: string, dedupKey: string, message: string): Promise { + if (!notificationTargetConfigured()) return false; + const id = claudeQqNotificationId(kind, dedupKey); + const existing = claudeQqNotificationOutbox.items.find((item) => item.id === id); + if (existing !== undefined && existing.sentAt !== null) return false; + const at = ctx().nowIso(); + let item: ClaudeQqNotificationItem; + if (existing !== undefined) { + existing.message = message; + existing.target = notificationTargetLabel(); + existing.updatedAt = at; + existing.nextAttemptAt = at; + existing.lastError = null; + item = existing; + } else { + item = { + id, + kind, + dedupKey, + target: notificationTargetLabel(), + message, + createdAt: at, + updatedAt: at, + attempts: 0, + nextAttemptAt: at, + lastError: null, + sentAt: null, + }; + claudeQqNotificationOutbox.items.push(item); + } + await persistClaudeQqNotificationItem(item); + void drainClaudeQqNotificationOutbox(`enqueue:${kind}`).catch((error) => ctx().logger("warn", "claudeqq_notify_outbox_drain_failed", { trigger: `enqueue:${kind}`, error: ctx().errorToJson(error) })); + return true; +} + +function dueClaudeQqNotificationItems(limit = 3): ClaudeQqNotificationItem[] { + const nowMs = Date.now(); + return claudeQqNotificationOutbox.items + .filter((item) => item.sentAt === null && (ctx().timestampMs(item.nextAttemptAt) ?? 0) <= nowMs) + .sort((left, right) => (ctx().timestampMs(left.nextAttemptAt) ?? 0) - (ctx().timestampMs(right.nextAttemptAt) ?? 0)) + .slice(0, limit); +} + +async function drainClaudeQqNotificationOutbox(trigger = "manual"): Promise> { + if (!notificationTargetConfigured()) return { ok: true, trigger, skipped: "not_configured" }; + if (claudeQqNotificationDrainInFlight) return { ok: true, trigger, skipped: "in_flight" }; + claudeQqNotificationDrainInFlight = true; + let sent = 0; + let failed = 0; + try { + await loadClaudeQqNotificationOutboxFromDatabase(); + const due = dueClaudeQqNotificationItems(); + for (const item of due) { + item.attempts += 1; + item.updatedAt = ctx().nowIso(); + await persistClaudeQqNotificationItem(item); + try { + await postClaudeQqText(item.kind, item.message); + item.sentAt = ctx().nowIso(); + item.updatedAt = item.sentAt; + item.lastError = null; + sent += 1; + if (item.kind === "task_terminal") rememberTaskNotificationKey(item.dedupKey); + ctx().logger("info", "claudeqq_notify_outbox_sent", { id: item.id, kind: item.kind, target: item.target, attempts: item.attempts, trigger }); + } catch (error) { + failed += 1; + const message = error instanceof Error ? error.message : String(error); + item.lastError = ctx().safePreview(message, 1000); + item.updatedAt = ctx().nowIso(); + item.nextAttemptAt = new Date(Date.now() + claudeQqNotificationRetryDelayMs(item.attempts)).toISOString(); + ctx().logger("warn", "claudeqq_notify_outbox_retry_scheduled", { + id: item.id, + kind: item.kind, + target: item.target, + attempts: item.attempts, + nextAttemptAt: item.nextAttemptAt, + trigger, + error: ctx().errorToJson(error), + }); + } finally { + await persistClaudeQqNotificationItem(item); + } + } + } finally { + claudeQqNotificationDrainInFlight = false; + await persistClaudeQqNotificationOutbox(); + if (claudeQqNotificationOutbox.items.some((item) => item.sentAt === null)) scheduleClaudeQqNotificationDrain(); + } + return { ok: true, trigger, sent, failed, outbox: claudeQqNotificationOutboxStats() }; +} + +async function postClaudeQqText(kind: string, message: string): Promise { + if (!notificationTargetConfigured()) return; + const url = `${ctx().config.notifyClaudeQqBaseUrl}/api/push/text`; + let lastError: unknown = null; + for (let attempt = 1; attempt <= ctx().config.notifyClaudeQqSendAttempts; attempt += 1) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), ctx().config.notifyClaudeQqTimeoutMs); + let responseText = ""; + try { + const response = await fetch(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(claudeQqTargetPayload(message)), + signal: controller.signal, + }); + responseText = await response.text(); + if (!response.ok) throw new Error(`ClaudeQQ proxy returned HTTP ${response.status}: ${ctx().safePreview(responseText, 500)}`); + try { + const parsed = JSON.parse(responseText) as Record; + if (parsed.ok === false || parsed.success === false || parsed.status === "napcat_offline") { + throw new Error(`ClaudeQQ push failed: ${ctx().safePreview(responseText, 500)}`); + } + } catch (error) { + if (error instanceof SyntaxError) { + // Some deployments return plain text; HTTP 2xx is still accepted. + } else { + throw error; + } + } + ctx().logger("info", "claudeqq_notify_sent", { kind, target: notificationTargetLabel(), attempt, chars: message.length, responsePreview: ctx().safePreview(responseText, 500) }); + return; + } catch (error) { + lastError = error; + if (attempt >= ctx().config.notifyClaudeQqSendAttempts) break; + const delayMs = Math.min(30_000, 1000 * (2 ** (attempt - 1))); + ctx().logger("warn", "claudeqq_notify_retry", { kind, target: notificationTargetLabel(), attempt, nextDelayMs: delayMs, error: ctx().errorToJson(error) }); + await Bun.sleep(delayMs); + } finally { + clearTimeout(timer); + } + } + if (lastError !== null) { + throw lastError instanceof Error ? lastError : new Error(String(lastError)); + } +} + +function taskTerminalNotificationMessage(task: QueueTask): string { + const stats = queueNotificationStats(); + const totalElapsed = formatDurationMs(durationMsBetween(task.createdAt, task.finishedAt ?? task.updatedAt)); + const runElapsed = formatDurationMs(durationMsBetween(task.startedAt ?? task.createdAt, task.finishedAt ?? task.updatedAt)); + const response = truncateNotificationText(taskFinalResponseForNotification(task), ctx().config.notifyClaudeQqMaxResponseChars); + return [ + "Code Queue 任务结束", + `task: ${task.id}`, + `queue: ${ctx().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}`, + `current queue: running=${stats.running}, queued=${stats.queued}, retry_wait=${stats.retryWait}`, + "", + "Final response:", + response, + ].join("\n"); +} + +async function notifyTaskTerminal(task: QueueTask): Promise { + if (!terminalTask(task) || !notificationTargetConfigured()) return; + const key = taskNotificationKey(task); + if (sentTaskNotificationKeys.has(key) || inFlightTaskNotificationKeys.has(key)) return; + inFlightTaskNotificationKeys.add(key); + try { + await enqueueClaudeQqNotification("task_terminal", key, taskTerminalNotificationMessage(task)); + } catch (error) { + ctx().logger("warn", "claudeqq_task_notify_failed", { taskId: task.id, status: task.status, target: notificationTargetLabel(), error: ctx().errorToJson(error) }); + } finally { + inFlightTaskNotificationKeys.delete(key); + } +} + +async function backfillClaudeQqTaskNotifications(since: string | null, limit: number, dryRun: boolean): Promise> { + if (!notificationTargetConfigured()) return { ok: true, skipped: "not_configured" }; + await loadClaudeQqNotificationOutboxFromDatabase(); + const sinceMs = ctx().timestampMs(since); + const candidates = ctx().tasks() + .filter((task) => terminalTask(task)) + .filter((task) => sinceMs === null || (ctx().timestampMs(task.finishedAt ?? task.updatedAt) ?? 0) > sinceMs) + .sort((left, right) => (ctx().timestampMs(left.finishedAt ?? left.updatedAt) ?? 0) - (ctx().timestampMs(right.finishedAt ?? right.updatedAt) ?? 0)) + .slice(0, limit); + const items = candidates.map((task) => ({ + taskId: task.id, + status: task.status, + queueId: ctx().queueIdOf(task), + finishedAt: task.finishedAt ?? task.updatedAt, + dedupKey: taskNotificationKey(task), + alreadyInOutbox: claudeQqNotificationOutbox.items.some((item) => item.id === claudeQqNotificationId("task_terminal", taskNotificationKey(task))), + })); + if (dryRun) return { ok: true, dryRun, since: since ?? null, scanned: candidates.length, enqueued: 0, items }; + let enqueued = 0; + for (const task of candidates) { + if (await enqueueClaudeQqNotification("task_terminal", taskNotificationKey(task), taskTerminalNotificationMessage(task))) enqueued += 1; + } + return { ok: true, dryRun, since: since ?? null, scanned: candidates.length, enqueued, outbox: claudeQqNotificationOutboxStats(), items }; +} + +function armIdleNotification(): void { + if (notificationTargetConfigured()) idleNotificationSent = false; +} + +async function maybeNotifyQueueIdle(triggerTaskId: string | null = null): Promise { + if (!notificationTargetConfigured() || idleNotificationSent || idleNotificationInFlight) return; + const stats = queueNotificationStats(); + if (Number(stats.running) !== 0 || Number(stats.queued) !== 0 || ctx().processingQueueCount() !== 0 || ctx().activeRunCount() !== 0) return; + idleNotificationInFlight = true; + try { + const message = [ + "Code Queue 已空闲", + "running=0, queued=0", + `total tasks=${stats.total}, queues=${stats.queueCount}`, + triggerTaskId === null ? "" : `last task=${triggerTaskId}`, + ].filter((line) => line.length > 0).join("\n"); + await enqueueClaudeQqNotification("queue_idle", `queue_idle:${triggerTaskId ?? "unknown"}:${stats.total}`, message); + idleNotificationSent = true; + } catch (error) { + ctx().logger("warn", "claudeqq_idle_notify_failed", { triggerTaskId: triggerTaskId ?? "", target: notificationTargetLabel(), error: ctx().errorToJson(error) }); + } finally { + idleNotificationInFlight = false; + } +} + + + + + + + +function claudeQqNotificationOutboxItemCount(): number { + return claudeQqNotificationOutbox.items.length; +} + +function claudeQqNotificationItems(limit: number): JsonValue[] { + return claudeQqNotificationOutbox.items.slice(-limit).map((item) => ({ + id: item.id, + kind: item.kind, + dedupKey: item.dedupKey, + target: item.target, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + attempts: item.attempts, + nextAttemptAt: item.nextAttemptAt, + lastError: item.lastError, + sentAt: item.sentAt, + messagePreview: ctx().safePreview(item.message, 500), + messageChars: item.message.length, + })) as unknown as JsonValue[]; +} + +export { + armIdleNotification, + backfillClaudeQqTaskNotifications, + claudeQqNotificationItems, + claudeQqNotificationOutboxItemCount, + claudeQqNotificationOutboxStats, + drainClaudeQqNotificationOutbox, + loadClaudeQqNotificationOutboxFromDatabase, + maybeNotifyQueueIdle, + notificationTargetConfigured, + notificationTargetLabel, + notifyTaskTerminal, + persistClaudeQqNotificationOutbox, + scheduleClaudeQqNotificationDrain, +}; diff --git a/src/components/microservices/code-queue/src/prompts.ts b/src/components/microservices/code-queue/src/prompts.ts new file mode 100644 index 00000000..390c28d4 --- /dev/null +++ b/src/components/microservices/code-queue/src/prompts.ts @@ -0,0 +1,53 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import type { QueueTaskRequest } from "./types"; + +export const resolvedReferenceContextTitle = "# Code Queue 已解析引用上下文"; +export const currentTaskPromptMarker = "\n# 本次任务\n"; +export const codeQueueEnvironmentHintTitle = "# Code Queue 运行环境提示"; +export const codeQueueEnvironmentHint = [ + codeQueueEnvironmentHintTitle, + "如果当前 Code Queue Docker 容器缺少完成任务所需的环境、系统包或语言依赖,可以先在容器内临时安装以推进当前任务;同时必须把该依赖补到 `src/components/microservices/code-queue/Dockerfile`,让后续任务重建镜像后可直接使用。", +].join("\n"); + +export function stripAutoReferenceHint(prompt: string): string { + const trimmed = prompt.trimStart(); + 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; + return trimmed.slice(index + marker.length).trimStart(); +} + +export function stripResolvedReferenceContext(prompt: string): string { + const trimmed = prompt.trimStart(); + if (!trimmed.startsWith(resolvedReferenceContextTitle)) return prompt; + const offset = prompt.length - trimmed.length; + const index = prompt.lastIndexOf(currentTaskPromptMarker); + if (index < offset) return prompt; + return prompt.slice(index + currentTaskPromptMarker.length).trimStart(); +} + +export function stripCodeQueueEnvironmentHint(prompt: string): string { + const trimmed = prompt.trimStart(); + if (!trimmed.startsWith(codeQueueEnvironmentHintTitle)) return prompt; + const offset = prompt.length - trimmed.length; + const index = prompt.indexOf(currentTaskPromptMarker, offset); + if (index < offset) return prompt; + return prompt.slice(index + currentTaskPromptMarker.length).trimStart(); +} + +export function userPromptForDisplay(prompt: string): string { + return stripAutoReferenceHint(stripResolvedReferenceContext(stripCodeQueueEnvironmentHint(prompt))); +} + +export function promptWithCodeQueueEnvironmentHint(prompt: string): string { + if (prompt.trimStart().startsWith(codeQueueEnvironmentHintTitle)) return prompt; + return [codeQueueEnvironmentHint, "", "# 本次任务", prompt.trim()].join("\n"); +} + +export function injectCodeQueueEnvironmentHint(request: QueueTaskRequest): QueueTaskRequest { + if (request.prompt.trimStart().startsWith(codeQueueEnvironmentHintTitle)) return request; + const basePrompt = request.basePrompt ?? userPromptForDisplay(request.prompt); + return { ...request, prompt: promptWithCodeQueueEnvironmentHint(request.prompt), basePrompt }; +} diff --git a/src/components/microservices/code-queue/src/provider-runtime.ts b/src/components/microservices/code-queue/src/provider-runtime.ts new file mode 100644 index 00000000..fe1fb3ba --- /dev/null +++ b/src/components/microservices/code-queue/src/provider-runtime.ts @@ -0,0 +1,455 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import { spawnSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { opencodeNpmPackage } from "./code-agent/common"; +import type { DevContainerCommandLog, DevContainerPlan, JsonValue, QueueTask, RuntimeConfig } from "./types"; + +export interface ProviderRuntimeContext { + config: Pick; + safePreview: (value: string, max?: number) => string; +} + +let context: ProviderRuntimeContext | null = null; + +export function configureProviderRuntime(runtimeContext: ProviderRuntimeContext): void { + context = runtimeContext; +} + +function ctx(): ProviderRuntimeContext { + if (context === null) throw new Error("provider-runtime module is not configured"); + return context; +} + +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) ?? ctx().config.mainProviderId; +} + +function providerIsMain(providerId: string): boolean { + return normalizeTaskProviderId(providerId) === ctx().config.mainProviderId; +} + +function defaultWorkdirForProvider(providerId: string): string { + return providerIsMain(providerId) ? ctx().config.defaultWorkdir : ctx().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([ + ctx().config.mainProviderId, + ...ctx().config.executionProviderIds, + ctx().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 + : ctx().config.devContainerImage.length > 0 + ? ctx().config.devContainerImage + : "unidesk-code-queue:latest"; + const workdir = workdirFromBody.length > 0 ? workdirFromBody : ctx().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 : ctx().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(ctx().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: ctx().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=${ctx().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 ctx().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(ctx().config.codexHome, "config.toml"); + const path = existsSync(homeConfig) ? homeConfig : ctx().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=${ctx().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)}`; +} + + +export { + buildDevContainerPlan, + containerTunnelStartScript, + defaultWorkdirForProvider, + devContainerPingScript, + executionProviderOptions, + masterKeyReadScript, + masterKeySetupScript, + masterProxyEvidenceScript, + masterProxyFinishScript, + masterProxyPrepareScript, + normalizeProviderId, + normalizeTaskProviderId, + providerIsMain, + remoteAppServerCommand, + remoteCodexConfigInstallScript, + remoteCodexRuntimePrepareScript, + remoteContainerStartScript, + remoteHostWorkdirForTask, + remoteKeyInstallScript, + resolveTaskCwd, + runCodeQueueSsh, + shellQuote, + throwIfCommandFailed, +}; diff --git a/src/components/microservices/code-queue/src/queue-api.ts b/src/components/microservices/code-queue/src/queue-api.ts new file mode 100644 index 00000000..274f2c6a --- /dev/null +++ b/src/components/microservices/code-queue/src/queue-api.ts @@ -0,0 +1,1133 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import postgres from "postgres"; +import { codeAgentPortForModel, codeAgentPortInfo, codeModelPorts as codeModelPortsFor, opencodeModels as opencodeModelsFor } from "./code-agent/common"; +import { claudeQqNotificationOutboxStats, notificationTargetConfigured, notificationTargetLabel } from "./notifications"; +import { executionProviderOptions } from "./provider-runtime"; +import { taskFullOutput } from "./task-output"; +import { buildCompactTaskTranscript, buildTaskTranscript, cachedPreviewTranscript, fullTranscript, prefixPreview, safePreview, statsDaysFromUrl, taskForCompactMetaResponse, taskForMetaResponse, taskLlmStepCount, taskStatisticsSummary, taskTiming, timestampMs } from "./task-view"; +import { userPromptForDisplay } from "./prompts"; +import type { ActiveRun, ActiveRunSlotWaiter } from "./code-agent/common"; +import type { JsonValue, QueueRecord, QueuedStatusReason, QueueTask, RuntimeConfig, TaskStatus, TranscriptLine } from "./types"; + +export interface QueueApiContext { + config: Pick; + activeRunSlotQueueIds: () => string[]; + activeRunSlotWaiterSummaries: () => JsonValue[]; + activeRuns: Map; + codexSqliteLogExporter: () => Record; + collectDevReady: () => JsonValue; + compactJsonResponse: (body: unknown, status?: number) => Response; + databaseLastError: () => string | null; + databaseReady: () => boolean; + defaultQueueId: string; + dirtyDatabaseTaskCount: () => number; + jsonResponse: (body: unknown, status?: number) => Response; + judgeFailRetryLimit: number; + loadTaskFromDatabase: (taskId: string) => Promise; + loadTasksFromDatabase: (where?: "all" | "hot") => Promise; + loadTasksFromDatabaseByIds: (taskIds: string[]) => Promise; + pageBySeq: (items: T[], url: URL, limit: number) => { mode: "tail" | "after" | "before"; afterSeq: number; beforeSeq: number | null; nextAfterSeq: number; previousBeforeSeq: number | null; hasMore: boolean; hasBefore: boolean; chunk: T[] }; + parseLimit: (url: URL) => number; + parseTextLimit: (url: URL) => number; + processing: () => boolean; + processingQueues: Set; + queueHeadTask: (queueId: string, tasks?: QueueTask[]) => QueueTask | null; + queueIdOf: (task: QueueTask) => string; + queues: () => QueueRecord[]; + queueTaskIsRunnable: (task: QueueTask) => boolean; + queuedStatusReason: (task: QueueTask, tasks?: QueueTask[]) => QueuedStatusReason | null; + queuedTaskPromptEditable: (task: QueueTask) => boolean; + runGarbageCollection: () => void; + safeQueueId: (value: unknown) => string; + safeQueueName: (value: unknown, queueId: string) => string; + sql: postgres.Sql; + taskQueueEnteredAt: (task: QueueTask) => string; + tasks: () => QueueTask[]; + truthyParam: (url: URL, name: string) => boolean; +} + +let context: QueueApiContext | null = null; + +export function configureQueueApi(runtimeContext: QueueApiContext): void { + context = runtimeContext; +} + +function ctx(): QueueApiContext { + if (context === null) throw new Error("queue-api module is not configured"); + return context; +} + +function terminalTask(task: QueueTask): boolean { + return task.status === "succeeded" || task.status === "failed" || task.status === "canceled"; +} + +function terminalTaskUnread(task: QueueTask): boolean { + return terminalTask(task) && task.readAt === null; +} + +function queuedStatusPayload(task: QueueTask, tasks?: QueueTask[]): { queuedReason: QueuedStatusReason | null; queuedReasonLabel: string | null } { + const reason = ctx().queuedStatusReason(task, tasks); + return { + queuedReason: reason, + queuedReasonLabel: reason?.label ?? null, + }; +} + +function transcriptChunkResponse(task: QueueTask, url: URL): Response { + const limit = ctx().parseLimit(url); + const fullText = ctx().truthyParam(url, "fullText") || ctx().truthyParam(url, "raw"); + const agentPort = codeAgentPortForModel(task.model); + const transcript = fullText ? fullTranscript(task) : cachedPreviewTranscript(task); + const page = ctx().pageBySeq(transcript, url, limit); + return ctx().jsonResponse({ + ok: true, + taskId: task.id, + queueId: ctx().queueIdOf(task), + status: task.status, + updatedAt: task.updatedAt, + agentPort, + agentPortInfo: codeAgentPortInfo(agentPort), + mode: page.mode, + transcript: page.chunk, + afterSeq: page.afterSeq, + nextAfterSeq: page.nextAfterSeq, + beforeSeq: page.beforeSeq, + previousBeforeSeq: page.previousBeforeSeq, + hasMore: page.hasMore, + hasBefore: page.hasBefore, + total: transcript.length, + maxSeq: transcript.at(-1)?.seq ?? 0, + fullText, + }); +} + +function outputChunkResponse(task: QueueTask, url: URL): Response { + const limit = ctx().parseLimit(url); + const fullText = ctx().truthyParam(url, "fullText") || ctx().truthyParam(url, "raw"); + const maxTextChars = ctx().parseTextLimit(url); + const fullOutput = taskFullOutput(task); + const page = ctx().pageBySeq(fullOutput, url, limit); + const output = page.chunk.map((item) => { + const truncated = !fullText && item.text.length > maxTextChars; + return { + ...item, + text: truncated ? item.text.slice(0, maxTextChars) : item.text, + textChars: item.text.length, + textTruncated: truncated, + omittedChars: truncated ? item.text.length - maxTextChars : 0, + }; + }); + return ctx().jsonResponse({ + ok: true, + taskId: task.id, + queueId: ctx().queueIdOf(task), + status: task.status, + updatedAt: task.updatedAt, + mode: page.mode, + output, + afterSeq: page.afterSeq, + nextAfterSeq: page.nextAfterSeq, + beforeSeq: page.beforeSeq, + previousBeforeSeq: page.previousBeforeSeq, + hasMore: page.hasMore, + hasBefore: page.hasBefore, + total: fullOutput.length, + retainedTotal: task.output.length, + maxSeq: fullOutput.at(-1)?.seq ?? 0, + fullText, + maxTextChars, + }); +} + +function taskForListResponse(task: QueueTask, lite = false, queueTasks?: QueueTask[]): JsonValue { + const timing = taskTiming(task); + const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt); + const stepCount = taskLlmStepCount(task); + if (lite) { + return { + id: task.id, + queueId: ctx().queueIdOf(task), + queueEnteredAt: ctx().taskQueueEnteredAt(task), + prompt: prefixPreview(displayPrompt, 360), + basePrompt: prefixPreview(task.basePrompt, 360), + displayPrompt: prefixPreview(displayPrompt, 360), + promptChars: task.prompt.length, + basePromptChars: task.basePrompt.length, + displayPromptChars: displayPrompt.length, + promptEditable: ctx().queuedTaskPromptEditable(task), + finalResponseChars: task.finalResponse.length, + stepCount, + llmStepCount: stepCount, + summaryOnly: true, + referenceTaskIds: task.referenceTaskIds, + referenceInjectionSummary: task.referenceInjection === null ? null : { + injectedAt: task.referenceInjection.injectedAt, + itemCount: task.referenceInjection.itemCount, + directReferenceTaskIds: task.referenceInjection.directReferenceTaskIds, + 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, + ...queuedStatusPayload(task, queueTasks), + createdAt: task.createdAt, + updatedAt: task.updatedAt, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + readAt: task.readAt, + currentAttempt: task.currentAttempt, + currentMode: task.currentMode, + judgeFailCount: task.judgeFailCount, + judgeFailRetryLimit: ctx().judgeFailRetryLimit, + codexThreadId: task.codexThreadId, + activeTurnId: task.activeTurnId, + lastError: task.lastError, + lastJudge: task.lastJudge === null ? null : { + decision: task.lastJudge.decision, + confidence: task.lastJudge.confidence, + source: task.lastJudge.source, + reason: safePreview(task.lastJudge.reason, 260), + }, + cancelRequested: task.cancelRequested, + terminalUnread: terminalTaskUnread(task), + outputCount: task.output.length, + eventCount: task.events.length, + attemptCount: task.attempts.length, + timing, + } as unknown as JsonValue; + } + return { + id: task.id, + queueId: ctx().queueIdOf(task), + queueEnteredAt: ctx().taskQueueEnteredAt(task), + prompt: safePreview(displayPrompt, 2000), + basePrompt: safePreview(task.basePrompt, 2000), + displayPrompt: safePreview(displayPrompt, 2000), + promptChars: task.prompt.length, + basePromptChars: task.basePrompt.length, + displayPromptChars: displayPrompt.length, + promptEditable: ctx().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, + ...queuedStatusPayload(task, queueTasks), + createdAt: task.createdAt, + updatedAt: task.updatedAt, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + readAt: task.readAt, + currentAttempt: task.currentAttempt, + currentMode: task.currentMode, + judgeFailCount: task.judgeFailCount, + judgeFailRetryLimit: ctx().judgeFailRetryLimit, + codexThreadId: task.codexThreadId, + activeTurnId: task.activeTurnId, + lastError: task.lastError, + lastJudge: task.lastJudge, + cancelRequested: task.cancelRequested, + terminalUnread: terminalTaskUnread(task), + outputCount: task.output.length, + eventCount: task.events.length, + attemptCount: task.attempts.length, + attempts: task.attempts.slice(-3), + timing, + } as unknown as JsonValue; +} + +function perQueueSummaries(tasks: QueueTask[] = ctx().tasks()): JsonValue[] { + const summaries = new Map; + unreadTerminal: number; + activeTaskId: string | null; + runnableTaskId: string | null; + createdAt: string | null; + updatedAt: string | null; + }>(); + for (const queue of ctx().queues()) { + queue.name = ctx().safeQueueName(queue.name, queue.id); + summaries.set(queue.id, { + name: queue.name, + total: 0, + counts: {}, + unreadTerminal: 0, + activeTaskId: ctx().activeRuns.get(queue.id)?.taskId ?? null, + runnableTaskId: null, + createdAt: queue.createdAt, + updatedAt: queue.updatedAt, + }); + } + for (const task of tasks) { + const queueId = ctx().queueIdOf(task); + let summary = summaries.get(queueId); + if (summary === undefined) { + summary = { + name: queueId, + total: 0, + counts: {}, + unreadTerminal: 0, + activeTaskId: ctx().activeRuns.get(queueId)?.taskId ?? null, + runnableTaskId: null, + createdAt: null, + updatedAt: null, + }; + summaries.set(queueId, summary); + } + summary.total += 1; + summary.counts[task.status] = (summary.counts[task.status] ?? 0) + 1; + if (terminalTaskUnread(task)) summary.unreadTerminal += 1; + } + for (const [queueId, summary] of summaries) { + const activeRun = ctx().activeRuns.get(queueId); + const head = ctx().queueHeadTask(queueId); + summary.activeTaskId = activeRun?.taskId ?? (head !== null && (head.status === "running" || head.status === "judging") ? head.id : null); + summary.runnableTaskId = head !== null && ctx().queueTaskIsRunnable(head) ? head.id : null; + } + 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, + activeTaskId: summary.activeTaskId, + runnableTaskId: summary.runnableTaskId, + processing: ctx().processingQueues.has(queueId), + createdAt: summary.createdAt, + updatedAt: summary.updatedAt, + } as unknown as JsonValue; + }); + return rows; +} + +function queueSummary(includeDevReady = true, tasks: QueueTask[] = ctx().tasks()): JsonValue { + const counts = tasks.reduce>((memo, task) => { + memo[task.status] = (memo[task.status] ?? 0) + 1; + return memo; + }, {}); + const unreadTerminal = tasks.reduce((total, task) => total + (terminalTaskUnread(task) ? 1 : 0), 0); + const activeRunSlots = ctx().activeRunSlotQueueIds(); + const activeTaskIdSet = new Set(Array.from(ctx().activeRuns.values()).map((run) => run.taskId)); + 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] ?? tasks.find((task) => task.status === "running" || task.status === "judging")?.id ?? null; + const queues = perQueueSummaries(tasks); + const summary: Record = { + total: tasks.length, + defaultQueueId: ctx().defaultQueueId, + queueCount: queues.length, + queues, + activeQueueIds: activeRunSlots, + processingQueueIds: Array.from(ctx().processingQueues).sort(), + activeRunSlotCount: activeRunSlots.length, + activeRunSlotWaiters: ctx().activeRunSlotWaiterSummaries(), + activeTaskIds, + activeTaskId, + processing: ctx().processing(), + counts, + unreadTerminal, + judgeConfigured: ctx().config.minimaxApiKey.length > 0, + judgeFailRetryLimit: ctx().judgeFailRetryLimit, + minimaxModel: ctx().config.minimaxModel, + minimaxJudgeRepairAttempts: ctx().config.judgeRepairAttempts, + minimaxJudgeMaxTokens: ctx().config.judgeMaxTokens, + defaultModel: ctx().config.defaultModel, + codeModels: ctx().config.codeModels, + codexModels: ctx().config.codexModels, + opencodeModels: opencodeModelsFor(ctx().config.codeModels), + modelPorts: codeModelPortsFor(ctx().config.codeModels) as unknown as JsonValue, + agentPorts: { + codex: codeAgentPortInfo("codex"), + opencode: codeAgentPortInfo("opencode"), + } as unknown as JsonValue, + defaultReasoningEffort: ctx().config.defaultReasoningEffort, + modelReasoningEfforts: ctx().config.modelReasoningEfforts, + defaultProviderId: ctx().config.mainProviderId, + mainProviderId: ctx().config.mainProviderId, + defaultWorkdir: ctx().config.defaultWorkdir, + remoteDefaultWorkdir: ctx().config.remoteDefaultWorkdir, + maxActiveQueues: ctx().config.maxActiveQueues, + executionProviders: executionProviderOptions(), + defaultWorkdirByProvider: Object.fromEntries((executionProviderOptions() as Array>).map((provider) => [String(provider.id), provider.defaultWorkdir ?? ctx().config.defaultWorkdir])) as JsonValue, + notifications: { + claudeqq: { + enabled: ctx().config.notifyClaudeQqEnabled, + configured: notificationTargetConfigured(), + targetType: ctx().config.notifyClaudeQqTargetType, + target: notificationTargetLabel(), + baseUrl: ctx().config.notifyClaudeQqBaseUrl, + maxResponseChars: ctx().config.notifyClaudeQqMaxResponseChars, + timeoutMs: ctx().config.notifyClaudeQqTimeoutMs, + sendAttempts: ctx().config.notifyClaudeQqSendAttempts, + retryIntervalMs: ctx().config.notifyClaudeQqRetryIntervalMs, + outbox: claudeQqNotificationOutboxStats(), + }, + }, + storage: { + primary: "postgres", + postgresConfigured: true, + postgresReady: ctx().databaseReady(), + dirtyTaskCount: ctx().dirtyDatabaseTaskCount(), + lastError: ctx().databaseLastError(), + outputArchiveDir: ctx().config.outputArchiveDir, + inMemoryOutputRecords: ctx().config.maxInMemoryOutputRecords, + inMemoryEventRecords: ctx().config.maxInMemoryEventRecords, + codexSqliteLogExport: { + enabled: ctx().config.codexSqliteLogExportEnabled, + intervalMs: ctx().config.codexSqliteLogExportIntervalMs, + batchSize: ctx().config.codexSqliteLogExportBatchSize, + maxBytes: ctx().config.codexSqliteLogMaxBytes, + running: ctx().codexSqliteLogExporter().running, + lastRunAt: ctx().codexSqliteLogExporter().lastRunAt, + lastExportedRows: ctx().codexSqliteLogExporter().lastExportedRows, + totalExportedRows: ctx().codexSqliteLogExporter().totalExportedRows, + lastDeletedRows: ctx().codexSqliteLogExporter().lastDeletedRows, + lastVacuumAt: ctx().codexSqliteLogExporter().lastVacuumAt, + lastError: ctx().codexSqliteLogExporter().lastError, + }, + }, + }; + if (includeDevReady) summary.devReady = ctx().collectDevReady(); + return summary; +} + +async function loadAllTasksForRead(): Promise { + if (!ctx().databaseReady()) return ctx().tasks(); + const tasks = await ctx().loadTasksFromDatabase("all"); + const byId = new Map(tasks.map((task) => [task.id, task])); + for (const active of ctx().tasks()) { + byId.set(active.id, active); + } + ctx().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 { + if (tasks !== undefined) return queueSummary(includeDevReady, tasks); + return queueSummaryForHealth(includeDevReady); +} + +async function queueSummaryForHealth(includeDevReady = true): Promise { + const summary = queueSummary(includeDevReady, ctx().tasks()) as Record; + if (!ctx().databaseReady()) return summary; + const [totalRows, statusRows, queueStatusRows, unreadRows] = await Promise.all([ + ctx().sql>`SELECT COUNT(*) AS total FROM unidesk_code_queue_tasks`, + ctx().sql>` + SELECT status, COUNT(*) AS count + FROM unidesk_code_queue_tasks + GROUP BY status + `, + ctx().sql>` + SELECT queue_id, status, COUNT(*) AS count + FROM unidesk_code_queue_tasks + GROUP BY queue_id, status + `, + ctx().sql>` + SELECT queue_id, COUNT(*) AS count + FROM unidesk_code_queue_tasks + WHERE status IN ('succeeded', 'failed', 'canceled') + AND read_at IS NULL + 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) => [ctx().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 ctx().queues()) { + summaries.set(queue.id, { + name: ctx().safeQueueName(queue.name, queue.id), + total: 0, + counts: {}, + unreadTerminal: unreadByQueue.get(queue.id) ?? 0, + activeTaskId: ctx().activeRuns.get(queue.id)?.taskId ?? null, + runnableTaskId: null, + createdAt: queue.createdAt, + updatedAt: queue.updatedAt, + }); + } + for (const row of queueStatusRows) { + const queueId = ctx().safeQueueId(row.queue_id); + let queue = summaries.get(queueId); + if (queue === undefined) { + queue = { + name: queueId, + total: 0, + counts: {}, + unreadTerminal: unreadByQueue.get(queueId) ?? 0, + activeTaskId: ctx().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 = ctx().activeRuns.get(queueId); + const head = ctx().queueHeadTask(queueId); + queue.activeTaskId = activeRun?.taskId ?? (head !== null && (head.status === "running" || head.status === "judging") ? head.id : null); + queue.runnableTaskId = head !== null && ctx().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: ctx().processingQueues.has(id), + createdAt: queue.createdAt, + updatedAt: queue.updatedAt, + })) as unknown as JsonValue[]; + summary.total = Number(totalRows[0]?.total ?? ctx().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 parseNamedLimit(url: URL, name: string, defaultValue: number): number { + const value = Number(url.searchParams.get(name) ?? defaultValue); + return Number.isInteger(value) && value > 0 ? Math.min(500, value) : defaultValue; +} + +function activePriority(task: QueueTask): number { + const statusRank: Record = { + running: 0, + judging: 1, + retry_wait: 2, + queued: 3, + succeeded: 9, + failed: 9, + canceled: 9, + }; + return statusRank[task.status] ?? 9; +} + +function taskUpdatedSortValue(task: QueueTask): number { + const time = Date.parse(task.updatedAt || task.createdAt); + 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 queued = queuedStatusPayload(task); + const haystack = [ + task.id, + ctx().queueIdOf(task), + task.status, + queued.queuedReasonLabel ?? "", + queued.queuedReason?.code ?? "", + queued.queuedReason?.message ?? "", + 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; + returned: number; + hasMore: boolean; + nextBeforeId: string | null; + beforeId: string | null; + includeActive: boolean; +} { + const beforeId = url.searchParams.get("beforeId"); + const includeActive = url.searchParams.get("includeActive") !== "0"; + const beforeIndex = beforeId === null ? -1 : filteredTasks.findIndex((task) => task.id === beforeId); + const safeEndIndex = beforeId === null || beforeIndex < 0 ? filteredTasks.length : beforeIndex; + const pageSource = filteredTasks.slice(Math.max(0, safeEndIndex - limit), safeEndIndex).reverse(); + const unreadTerminalRows = includeActive + ? filteredTasks + .filter(terminalTaskUnread) + .sort((left, right) => taskUpdatedSortValue(right) - taskUpdatedSortValue(left)) + : []; + const activeRows = includeActive + ? filteredTasks + .filter((task) => activePriority(task) < 3) + .sort((left, right) => { + const rankDelta = activePriority(left) - activePriority(right); + if (rankDelta !== 0) return rankDelta; + return taskUpdatedSortValue(right) - taskUpdatedSortValue(left); + }) + : []; + const byId = new Map(); + for (const task of [...unreadTerminalRows, ...activeRows, ...pageSource]) { + if (!byId.has(task.id)) byId.set(task.id, task); + } + const rows = Array.from(byId.values()); + const pageOldest = pageSource.at(-1) ?? null; + return { + rows, + total: filteredTasks.length, + returned: rows.length, + hasMore: safeEndIndex - limit > 0, + nextBeforeId: safeEndIndex - limit > 0 ? pageOldest?.id ?? null : null, + beforeId, + includeActive, + }; +} + +type TaskIdRow = { id: string; created_at?: Date | string }; +type CountRow = { count: string | number }; +type DailyCountRow = { date: string; count: string | number }; +type DailyCompletedRow = { + date: string; + status: string; + count: string | number; + total_duration_ms: string | number | null; + duration_samples: string | number; +}; + +const statsTimeZone = "Asia/Shanghai"; +const statsDateFormatter = new Intl.DateTimeFormat("en-CA", { + timeZone: statsTimeZone, + year: "numeric", + month: "2-digit", + day: "2-digit", +}); + +function statsDateKey(value: Date | string | 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 = statsDateFormatter.formatToParts(date).reduce>((memo, part) => { + if (part.type !== "literal") memo[part.type] = part.value; + return memo; + }, {}); + return parts.year && parts.month && parts.day ? `${parts.year}-${parts.month}-${parts.day}` : null; +} + +function shiftStatsDateKey(dateKey: string, offsetDays: number): string { + const [year = "1970", month = "01", day = "01"] = dateKey.split("-"); + return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day) + offsetDays)).toISOString().slice(0, 10); +} + +function statsDateStartUtc(dateKey: string): Date { + const [year = "1970", month = "01", day = "01"] = dateKey.split("-"); + return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)) - 8 * 60 * 60 * 1000); +} + +function emptyDatabaseStatsBucket(date: string): { + date: string; + executedTasks: number; + completedTasks: number; + retryAttempts: number; + succeededTasks: number; + failedTasks: number; + canceledTasks: number; + totalDurationMs: number; + durationSamples: number; +} { + return { + date, + executedTasks: 0, + completedTasks: 0, + retryAttempts: 0, + succeededTasks: 0, + failedTasks: 0, + canceledTasks: 0, + totalDurationMs: 0, + durationSamples: 0, + }; +} + +function databaseStatsRange(url: URL): { days: number; startDate: string; endDate: string; startAt: string; endAt: string } { + const generatedAt = new Date(); + const endDate = statsDateKey(generatedAt) ?? generatedAt.toISOString().slice(0, 10); + const days = Math.max(1, Math.min(90, Math.floor(statsDaysFromUrl(url)))); + const startDate = shiftStatsDateKey(endDate, 1 - days); + const endExclusiveDate = shiftStatsDateKey(endDate, 1); + return { + days, + startDate, + endDate, + startAt: statsDateStartUtc(startDate).toISOString(), + endAt: statsDateStartUtc(endExclusiveDate).toISOString(), + }; +} + +async function databaseTaskStatisticsSummary(queueId: string | null, url: URL): Promise { + const generatedAt = new Date().toISOString(); + const range = databaseStatsRange(url); + const buckets = new Map>(); + for (let offset = 0; offset < range.days; offset += 1) { + const date = shiftStatsDateKey(range.startDate, offset); + buckets.set(date, emptyDatabaseStatsBucket(date)); + } + const [executedRows, completedRows, retryRows] = queueId === null + ? await Promise.all([ + ctx().sql` + SELECT to_char(started_at AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS date, COUNT(*) AS count + FROM unidesk_code_queue_tasks + WHERE started_at >= ${range.startAt} + AND started_at < ${range.endAt} + GROUP BY date + `, + ctx().sql` + SELECT + to_char(COALESCE(finished_at, updated_at) AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS date, + status, + COUNT(*) AS count, + SUM(GREATEST(EXTRACT(EPOCH FROM (COALESCE(finished_at, updated_at) - COALESCE(started_at, created_at))) * 1000, 0)) AS total_duration_ms, + COUNT(CASE WHEN COALESCE(started_at, created_at) IS NOT NULL THEN 1 END) AS duration_samples + FROM unidesk_code_queue_tasks + WHERE status IN ('succeeded', 'failed', 'canceled') + AND COALESCE(finished_at, updated_at) >= ${range.startAt} + AND COALESCE(finished_at, updated_at) < ${range.endAt} + GROUP BY date, status + `, + ctx().sql` + SELECT + to_char(updated_at AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS date, + SUM(GREATEST(current_attempt - 1, attempt_count - 1, 0)) AS count + FROM unidesk_code_queue_tasks + WHERE updated_at >= ${range.startAt} + AND updated_at < ${range.endAt} + AND GREATEST(current_attempt - 1, attempt_count - 1, 0) > 0 + GROUP BY date + `, + ]) + : await Promise.all([ + ctx().sql` + SELECT to_char(started_at AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS date, COUNT(*) AS count + FROM unidesk_code_queue_tasks + WHERE queue_id = ${queueId} + AND started_at >= ${range.startAt} + AND started_at < ${range.endAt} + GROUP BY date + `, + ctx().sql` + SELECT + to_char(COALESCE(finished_at, updated_at) AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS date, + status, + COUNT(*) AS count, + SUM(GREATEST(EXTRACT(EPOCH FROM (COALESCE(finished_at, updated_at) - COALESCE(started_at, created_at))) * 1000, 0)) AS total_duration_ms, + COUNT(CASE WHEN COALESCE(started_at, created_at) IS NOT NULL THEN 1 END) AS duration_samples + FROM unidesk_code_queue_tasks + WHERE queue_id = ${queueId} + AND status IN ('succeeded', 'failed', 'canceled') + AND COALESCE(finished_at, updated_at) >= ${range.startAt} + AND COALESCE(finished_at, updated_at) < ${range.endAt} + GROUP BY date, status + `, + ctx().sql` + SELECT + to_char(updated_at AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS date, + SUM(GREATEST(current_attempt - 1, attempt_count - 1, 0)) AS count + FROM unidesk_code_queue_tasks + WHERE queue_id = ${queueId} + AND updated_at >= ${range.startAt} + AND updated_at < ${range.endAt} + AND GREATEST(current_attempt - 1, attempt_count - 1, 0) > 0 + GROUP BY date + `, + ]); + + for (const row of executedRows) { + const bucket = buckets.get(row.date); + if (bucket !== undefined) bucket.executedTasks += Number(row.count); + } + for (const row of retryRows) { + const bucket = buckets.get(row.date); + if (bucket !== undefined) bucket.retryAttempts += Number(row.count); + } + for (const row of completedRows) { + const bucket = buckets.get(row.date); + if (bucket === undefined) continue; + const count = Number(row.count); + bucket.completedTasks += count; + if (row.status === "succeeded") bucket.succeededTasks += count; + if (row.status === "failed") bucket.failedTasks += count; + if (row.status === "canceled") bucket.canceledTasks += count; + bucket.totalDurationMs += Math.round(Number(row.total_duration_ms ?? 0)); + bucket.durationSamples += Number(row.duration_samples); + } + const daily = Array.from(buckets.values()).map((bucket) => ({ + ...bucket, + avgDurationMs: bucket.durationSamples > 0 ? Math.round(bucket.totalDurationMs / bucket.durationSamples) : null, + })); + 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: statsTimeZone, + days: range.days, + range: { startDate: range.startDate, endDate: range.endDate }, + totals: { + ...totals, + avgDurationMs: totals.durationSamples > 0 ? Math.round(totals.totalDurationMs / totals.durationSamples) : null, + }, + daily, + } as unknown as JsonValue; +} + +async function databaseTaskTotal(queueId: string | null): Promise { + const rows = queueId === null + ? await ctx().sql`SELECT COUNT(*) AS count FROM unidesk_code_queue_tasks` + : await ctx().sql`SELECT COUNT(*) AS count FROM unidesk_code_queue_tasks WHERE queue_id = ${queueId}`; + return Number(rows[0]?.count ?? 0); +} + +async function databaseCursor(beforeId: string | null): Promise { + if (beforeId === null || beforeId.length === 0) return null; + const rows = await ctx().sql` + SELECT id, created_at + FROM unidesk_code_queue_tasks + WHERE id = ${beforeId} + LIMIT 1 + `; + return rows[0] ?? null; +} + +async function databasePageTaskIds(queueId: string | null, beforeId: string | null, limit: number): Promise<{ + ids: string[]; + total: number; + hasMore: boolean; + nextBeforeId: string | null; +}> { + const [total, cursor] = await Promise.all([databaseTaskTotal(queueId), databaseCursor(beforeId)]); + const fetchLimit = limit + 1; + let rows: TaskIdRow[]; + if (cursor === null && queueId === null) { + rows = await ctx().sql` + SELECT id + FROM unidesk_code_queue_tasks + ORDER BY created_at DESC, id DESC + LIMIT ${fetchLimit} + `; + } else if (cursor === null) { + rows = await ctx().sql` + SELECT id + FROM unidesk_code_queue_tasks + WHERE queue_id = ${queueId} + ORDER BY created_at DESC, id DESC + LIMIT ${fetchLimit} + `; + } else if (queueId === null) { + rows = await ctx().sql` + SELECT id + FROM unidesk_code_queue_tasks + WHERE (created_at, id) < (${cursor.created_at ?? new Date(0)}, ${cursor.id}) + ORDER BY created_at DESC, id DESC + LIMIT ${fetchLimit} + `; + } else { + rows = await ctx().sql` + SELECT id + FROM unidesk_code_queue_tasks + WHERE queue_id = ${queueId} + AND (created_at, id) < (${cursor.created_at ?? new Date(0)}, ${cursor.id}) + ORDER BY created_at DESC, id DESC + LIMIT ${fetchLimit} + `; + } + const ids = rows.slice(0, limit).map((row) => row.id); + const hasMore = rows.length > limit; + return { + ids, + total, + hasMore, + nextBeforeId: hasMore ? ids.at(-1) ?? null : null, + }; +} + +async function databasePriorityTaskIds(queueId: string | null, limit: number): Promise { + const [unreadRows, activeRows] = queueId === null + ? await Promise.all([ + ctx().sql` + SELECT id + FROM unidesk_code_queue_tasks + WHERE status IN ('succeeded', 'failed', 'canceled') + AND read_at IS NULL + ORDER BY updated_at DESC, id DESC + LIMIT ${limit} + `, + ctx().sql` + SELECT id + FROM unidesk_code_queue_tasks + WHERE status IN ('running', 'judging', 'retry_wait') + ORDER BY CASE status WHEN 'running' THEN 0 WHEN 'judging' THEN 1 ELSE 2 END, updated_at DESC, id DESC + LIMIT ${limit} + `, + ]) + : await Promise.all([ + ctx().sql` + SELECT id + FROM unidesk_code_queue_tasks + WHERE queue_id = ${queueId} + AND status IN ('succeeded', 'failed', 'canceled') + AND read_at IS NULL + ORDER BY updated_at DESC, id DESC + LIMIT ${limit} + `, + ctx().sql` + SELECT id + FROM unidesk_code_queue_tasks + WHERE queue_id = ${queueId} + AND status IN ('running', 'judging', 'retry_wait') + ORDER BY CASE status WHEN 'running' THEN 0 WHEN 'judging' THEN 1 ELSE 2 END, updated_at DESC, id DESC + LIMIT ${limit} + `, + ]); + return [...unreadRows.map((row) => row.id), ...activeRows.map((row) => row.id)]; +} + +function taskMatchesQueueFilter(task: QueueTask, queueId: string | null): boolean { + return queueId === null || ctx().queueIdOf(task) === queueId; +} + +function pushUniqueId(ids: string[], seen: Set, id: string): void { + if (id.length === 0 || seen.has(id)) return; + seen.add(id); + ids.push(id); +} + +async function databaseTasksOverviewResponse(url: URL): Promise { + if (!ctx().databaseReady()) return null; + const searchTerms = taskSearchTerms(url); + if (searchTerms.length > 0) return null; + const limit = ctx().parseLimit(url); + const compact = ctx().truthyParam(url, "compact"); + const queueFilter = url.searchParams.get("queueId"); + const queueId = queueFilter === null ? null : ctx().safeQueueId(queueFilter); + const includeActive = url.searchParams.get("includeActive") !== "0"; + const includeSelected = url.searchParams.get("selected") !== "0"; + const includeStatistics = url.searchParams.get("stats") !== "0"; + const priorityLimit = parseNamedLimit(url, "priorityLimit", Math.max(100, limit)); + const beforeId = url.searchParams.get("beforeId"); + const [queue, statistics, page, priorityIds] = await Promise.all([ + queueSummaryForResponse(false), + includeStatistics ? databaseTaskStatisticsSummary(queueId, url) : Promise.resolve(null), + databasePageTaskIds(queueId, beforeId, limit), + includeActive ? databasePriorityTaskIds(queueId, priorityLimit) : Promise.resolve([]), + ]); + const orderedIds: string[] = []; + const seenIds = new Set(); + for (const id of priorityIds) pushUniqueId(orderedIds, seenIds, id); + for (const id of page.ids) pushUniqueId(orderedIds, seenIds, id); + for (const task of ctx().tasks()) { + if (!includeActive || !taskMatchesQueueFilter(task, queueId)) continue; + if (terminalTaskUnread(task) || activePriority(task) < 3) pushUniqueId(orderedIds, seenIds, task.id); + } + const loadedTasks = await ctx().loadTasksFromDatabaseByIds(orderedIds); + const byId = new Map(); + for (const task of loadedTasks) byId.set(task.id, task); + for (const task of ctx().tasks()) { + if (seenIds.has(task.id) || byId.has(task.id)) byId.set(task.id, task); + } + const rowsSource = orderedIds + .map((id) => byId.get(id) ?? null) + .filter((task): task is QueueTask => task !== null && taskMatchesQueueFilter(task, queueId)); + const queueRecord = queue as Record; + const preferId = url.searchParams.get("preferId") ?? ""; + const activeTaskId = typeof queueRecord.activeTaskId === "string" ? queueRecord.activeTaskId : ""; + const selectedCandidates = includeSelected ? [preferId, activeTaskId, rowsSource[0]?.id ?? ""] : []; + let selectedTask: QueueTask | null = null; + for (const id of selectedCandidates) { + if (id.length === 0) continue; + const candidate = ctx().tasks().find((task) => task.id === id) + ?? await ctx().loadTaskFromDatabase(id); + if (candidate !== null && taskMatchesQueueFilter(candidate, queueId)) { + selectedTask = candidate; + break; + } + } + let selected: JsonValue = null; + if (selectedTask !== null) { + const afterSeqRaw = Number(url.searchParams.get("afterSeq") ?? 0); + const afterSeq = Number.isFinite(afterSeqRaw) ? afterSeqRaw : 0; + const transcriptLimit = parseNamedLimit(url, "transcriptLimit", 500); + const transcript = compact + ? buildCompactTaskTranscript(selectedTask, transcriptLimit, Math.max(12, transcriptLimit * 3)) + : buildTaskTranscript(selectedTask, transcriptLimit, 0); + const chunk = transcript.filter((line) => Number(line.seq) > afterSeq).slice(0, transcriptLimit); + const nextAfterSeq = chunk.at(-1)?.seq ?? afterSeq; + const outputMaxSeq = selectedTask.output.at(-1)?.seq ?? 0; + const promptHistoryMaxSeq = selectedTask.promptHistory.at(-1)?.seq ?? 0; + const maxSeq = Math.max(outputMaxSeq, promptHistoryMaxSeq, transcript.at(-1)?.seq ?? 0); + selected = { + task: compact ? taskForCompactMetaResponse(selectedTask) : taskForMetaResponse(selectedTask), + transcript: chunk, + afterSeq, + nextAfterSeq, + hasMore: maxSeq > Number(nextAfterSeq), + preview: true, + total: transcript.length, + maxSeq, + } as unknown as JsonValue; + } + const queueContextTasks = [...ctx().tasks(), ...rowsSource]; + return ctx().compactJsonResponse({ + ok: true, + queue, + statistics, + tasks: rowsSource.map((task) => taskForListResponse(task, true, queueContextTasks)), + selected, + pagination: { + limit, + returned: rowsSource.length, + total: page.total, + hasMore: page.hasMore, + nextBeforeId: page.nextBeforeId, + beforeId, + includeActive, + priorityLimit, + }, + }); +} + +async function tasksOverviewResponse(url: URL): Promise { + const databaseResponse = await databaseTasksOverviewResponse(url); + if (databaseResponse !== null) return databaseResponse; + const limit = ctx().parseLimit(url); + const compact = ctx().truthyParam(url, "compact"); + const queueFilter = url.searchParams.get("queueId"); + const searchTerms = taskSearchTerms(url); + const allTasks = await loadAllTasksForRead(); + const filteredTasks = allTasks + .filter((task) => queueFilter === null || ctx().queueIdOf(task) === ctx().safeQueueId(queueFilter)) + .filter((task) => taskMatchesSearch(task, searchTerms)); + const page = taskPageRows(filteredTasks, url, limit); + const rowsSource = page.rows; + 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) + ?? filteredTasks.find((task) => task.id === activeTaskId) + ?? rowsSource[0] + ?? null; + let selected: JsonValue = null; + if (selectedTask !== null) { + const afterSeqRaw = Number(url.searchParams.get("afterSeq") ?? 0); + const afterSeq = Number.isFinite(afterSeqRaw) ? afterSeqRaw : 0; + const transcriptLimit = parseNamedLimit(url, "transcriptLimit", 500); + const transcript = compact + ? buildCompactTaskTranscript(selectedTask, transcriptLimit, Math.max(12, transcriptLimit * 3)) + : buildTaskTranscript(selectedTask, transcriptLimit, 0); + const chunk = transcript.filter((line) => Number(line.seq) > afterSeq).slice(0, transcriptLimit); + const nextAfterSeq = chunk.at(-1)?.seq ?? afterSeq; + const outputMaxSeq = selectedTask.output.at(-1)?.seq ?? 0; + const promptHistoryMaxSeq = selectedTask.promptHistory.at(-1)?.seq ?? 0; + const maxSeq = Math.max(outputMaxSeq, promptHistoryMaxSeq, transcript.at(-1)?.seq ?? 0); + selected = { + task: compact ? taskForCompactMetaResponse(selectedTask) : taskForMetaResponse(selectedTask), + transcript: chunk, + afterSeq, + nextAfterSeq, + hasMore: maxSeq > Number(nextAfterSeq), + preview: true, + total: transcript.length, + maxSeq, + } as unknown as JsonValue; + } + return ctx().compactJsonResponse({ + ok: true, + queue, + statistics: taskStatisticsSummary(filteredTasks, statsDaysFromUrl(url)), + tasks: rowsSource.map((task) => taskForListResponse(task, true, allTasks)), + selected, + pagination: { + limit, + returned: page.returned, + total: page.total, + hasMore: page.hasMore, + nextBeforeId: page.nextBeforeId, + beforeId: page.beforeId, + includeActive: page.includeActive, + }, + }); +} + + +export { + loadAllTasksForRead, + outputChunkResponse, + perQueueSummaries, + queueSummary, + queueSummaryForHealth, + queueSummaryForResponse, + taskMatchesSearch, + taskPageRows, + taskSearchTerms, + tasksOverviewResponse, + taskForListResponse, + transcriptChunkResponse, +}; diff --git a/src/components/microservices/code-queue/src/references.ts b/src/components/microservices/code-queue/src/references.ts new file mode 100644 index 00000000..89256439 --- /dev/null +++ b/src/components/microservices/code-queue/src/references.ts @@ -0,0 +1,187 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import { lastAssistantMessage } from "./task-view"; +import { resolvedReferenceContextTitle, userPromptForDisplay } from "./prompts"; +import type { QueueTask, QueueTaskRequest, ReferenceInjectionRecord, ReferenceInjectionSummaryItem } from "./types"; + +export interface ReferencesContext { + addUniqueTaskId: (ids: string[], value: string) => void; + findTask: (id: string) => QueueTask | null; + nowIso: () => string; + referenceInjectionMaxRounds: number | null; + referenceTaskIdsFromPrompt: (prompt: string) => string[]; +} + +let context: ReferencesContext | null = null; + +export function configureReferences(runtimeContext: ReferencesContext): void { + context = runtimeContext; +} + +function ctx(): ReferencesContext { + if (context === null) throw new Error("references module is not configured"); + return context; +} + +function taskReferenceIds(task: QueueTask): string[] { + const ids: string[] = []; + for (const id of task.referenceTaskIds ?? []) ctx().addUniqueTaskId(ids, id); + for (const id of ctx().referenceTaskIdsFromPrompt(task.basePrompt || userPromptForDisplay(task.prompt))) ctx().addUniqueTaskId(ids, id); + return ids; +} + +function taskBasePrompt(task: QueueTask): string { + return (task.basePrompt || userPromptForDisplay(task.prompt)).trimEnd(); +} + +function referenceSummaryItem(task: QueueTask, round: number, roundIndex: number, viaTaskId: string | null): ReferenceInjectionSummaryItem { + const lastMessage = lastAssistantMessage(task) as Record; + const lastText = typeof lastMessage.text === "string" ? lastMessage.text : ""; + return { + round, + roundIndex, + taskId: task.id, + viaTaskId, + status: task.status, + providerId: task.providerId, + model: task.model, + cwd: task.cwd, + createdAt: task.createdAt, + updatedAt: task.updatedAt, + promptChars: taskBasePrompt(task).length, + finalResponseChars: lastText.trimEnd().length, + finalResponseAt: typeof lastMessage.at === "string" ? lastMessage.at : null, + finalResponseSource: typeof lastMessage.source === "string" ? lastMessage.source : "unknown", + referenceTaskIds: taskReferenceIds(task), + cliHint: `bun scripts/cli.ts codex task ${task.id}`, + }; +} + +function collectReferenceGraph(rootIds: string[], maxRounds: number | null, finder: (id: string) => QueueTask | null = ctx().findTask): { items: ReferenceInjectionSummaryItem[]; tasks: QueueTask[]; truncated: boolean } { + const seen = new Set(); + let frontier = rootIds.map((id) => ({ id, viaTaskId: null as string | null })); + const rawItems: Array<{ task: QueueTask; depth: number; viaTaskId: string | null; discoveryIndex: number }> = []; + let truncated = false; + let discoveryIndex = 0; + for (let depth = 1; frontier.length > 0; depth += 1) { + if (maxRounds !== null && depth > maxRounds) { + truncated = frontier.some((entry) => !seen.has(entry.id)); + break; + } + const next: Array<{ id: string; viaTaskId: string | null }> = []; + for (const entry of frontier) { + if (seen.has(entry.id)) continue; + const task = finder(entry.id); + if (task === null) continue; + seen.add(entry.id); + discoveryIndex += 1; + rawItems.push({ task, depth, viaTaskId: entry.viaTaskId, discoveryIndex }); + for (const childId of taskReferenceIds(task)) { + if (!seen.has(childId)) next.push({ id: childId, viaTaskId: task.id }); + } + } + frontier = next; + } + if (frontier.some((entry) => !seen.has(entry.id))) truncated = true; + const sorted = rawItems.sort((left, right) => { + if (left.depth !== right.depth) return right.depth - left.depth; + const createdDelta = Date.parse(left.task.createdAt) - Date.parse(right.task.createdAt); + if (Number.isFinite(createdDelta) && createdDelta !== 0) return createdDelta; + return left.discoveryIndex - right.discoveryIndex; + }); + const depthToRound = new Map(); + Array.from(new Set(sorted.map((item) => item.depth))).forEach((depth, index) => depthToRound.set(depth, index + 1)); + const roundCounts = new Map(); + const items = sorted.map((item) => { + const round = depthToRound.get(item.depth) ?? item.depth; + const roundIndex = (roundCounts.get(round) ?? 0) + 1; + roundCounts.set(round, roundIndex); + return referenceSummaryItem(item.task, round, roundIndex, item.viaTaskId); + }); + const tasks = sorted.map((item) => item.task); + return { items, tasks, truncated }; +} + +function referenceRoundSeparator(round: number, totalRounds: number, items: ReferenceInjectionSummaryItem[]): string { + const createdTimes = items.map((item) => item.createdAt).filter(Boolean).sort(); + const updatedTimes = items.map((item) => item.updatedAt).filter(Boolean).sort(); + return [ + `----- Reference Round ${round}/${totalRounds} -----`, + `order: upstream/oldest context first; direct references appear in the last round`, + `tasks: ${items.length}`, + `createdRange: ${createdTimes[0] ?? "--"} -> ${createdTimes.at(-1) ?? "--"}`, + `updatedRange: ${updatedTimes[0] ?? "--"} -> ${updatedTimes.at(-1) ?? "--"}`, + "--------------------------------", + ].join("\n"); +} + +function referencedTaskContext(task: QueueTask, summary: ReferenceInjectionSummaryItem): string { + const lastMessage = lastAssistantMessage(task) as Record; + const lastMessageText = typeof lastMessage.text === "string" ? lastMessage.text : ""; + return [ + `## Round ${summary.round}.${summary.roundIndex} referenced task ${task.id}`, + `- via: ${summary.viaTaskId ?? "direct"}`, + `- 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}`, + "", + "### Initial prompt", + taskBasePrompt(task) || "(empty)", + "", + "### Final/last response", + lastMessageText.trimEnd() || "(none yet)", + "", + "### Query more context", + `Run: bun scripts/cli.ts codex task ${task.id}`, + ].join("\n"); +} + +function injectReferencedTaskContext(request: QueueTaskRequest, finder: (id: string) => QueueTask | null = ctx().findTask, injectedAt = ctx().nowIso()): QueueTaskRequest { + const ids = request.referenceTaskIds ?? []; + if (ids.length === 0 || request.prompt.includes(resolvedReferenceContextTitle)) return request; + 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 Code Queue task not found: ${missing.join(", ")}`); + const userPrompt = request.basePrompt ?? userPromptForDisplay(request.prompt); + const graph = collectReferenceGraph(ids, ctx().referenceInjectionMaxRounds, finder); + const injection: ReferenceInjectionRecord = { + version: 2, + injectedAt, + basePrompt: userPrompt, + directReferenceTaskIds: ids, + maxRounds: ctx().referenceInjectionMaxRounds, + truncated: graph.truncated, + itemCount: graph.items.length, + items: graph.items, + }; + const taskById = new Map(graph.tasks.map((task) => [task.id, task])); + const groupedItems = Array.from(new Set(graph.items.map((item) => item.round))).map((round) => ({ + round, + items: graph.items.filter((item) => item.round === round), + })); + const context = [ + resolvedReferenceContextTitle, + `injectedAt: ${injectedAt}`, + `directReferences: ${ids.join(", ")}`, + `referenceGraphItems: ${graph.items.length}${graph.truncated ? " (truncated)" : ""}`, + "说明:Code Queue 后端只读取每个被引用任务的结构化 basePrompt(注入前 prompt)和 final/last response;不会把历史引用注入块继续套入。多轮引用按上游/最早上下文在前、直接引用在后的顺序注入;中间执行过程不注入,只保留 CLI 查询提示。", + "", + ...(groupedItems.flatMap((group) => [ + referenceRoundSeparator(group.round, groupedItems.length, group.items), + "", + ...group.items.map((item) => { + const task = taskById.get(item.taskId); + return task === undefined ? "" : referencedTaskContext(task, item); + }).filter((text) => text.length > 0), + "", + ])), + "", + "# 本次任务", + userPrompt.trim(), + ].join("\n"); + return { ...request, prompt: context, basePrompt: userPrompt, referenceTaskIds: ids, referenceInjection: injection }; +} + + +export { injectReferencedTaskContext, taskReferenceIds }; diff --git a/src/components/microservices/code-queue/src/self-tests.ts b/src/components/microservices/code-queue/src/self-tests.ts new file mode 100644 index 00000000..0251e77c --- /dev/null +++ b/src/components/microservices/code-queue/src/self-tests.ts @@ -0,0 +1,402 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import { minimaxM27Model } from "./code-agent/common"; +import { continuePromptSourceBudgetChars, miniMaxJudgeMessages, parsedContinuePromptForJudge, parseJudgeJson, queueRecoveryRetryPrompt, retryPrompt } from "./judge"; +import { codeQueueEnvironmentHintTitle, injectCodeQueueEnvironmentHint, promptWithCodeQueueEnvironmentHint, userPromptForDisplay } from "./prompts"; +import { buildTaskTranscript, safePreview, transcriptLineSummaryLines } from "./task-view"; +import type { ActiveRunSlotWaiter } from "./code-agent/common"; +import type { JsonValue, LiveOutput, QueueTask, QueuedStatusReason, QueueTaskRequest, RuntimeConfig, TaskStatus } from "./types"; + +export interface SelfTestsContext { + config: Pick; + activeRunSlotCount: () => number; + activeRunSlotReservations: Set; + activeRunSlotWaiters: ActiveRunSlotWaiter[]; + availableQueueStartSlotsFor: (activeSlotCount: number, maxActiveQueues?: number) => number; + defaultQueueId: string; + enqueueActiveRunSlotWaiter: (task: QueueTask) => ActiveRunSlotWaiter; + injectReferencedTaskContext: (request: QueueTaskRequest, finder?: (id: string) => QueueTask | null, injectedAt?: string) => QueueTaskRequest; + nextRunnableTaskFrom: (queueId: string, tasks?: QueueTask[]) => QueueTask | null; + normalizeTask: (task: QueueTask) => QueueTask; + nowIso: () => string; + processingQueues: Set; + queueHeadTask: (queueId: string, tasks?: QueueTask[]) => QueueTask | null; + queuedStatusReason: (task: QueueTask, tasks?: QueueTask[]) => QueuedStatusReason | null; + removeActiveRunSlotWaiter: (waiter: ActiveRunSlotWaiter) => void; + resolveReasoningEffort: (model: string, explicit?: string | null) => string | null; + updateProcessingFlag: () => void; +} + +let context: SelfTestsContext | null = null; + +export function configureSelfTests(runtimeContext: SelfTestsContext): void { + context = runtimeContext; +} + +function ctx(): SelfTestsContext { + if (context === null) throw new Error("self-tests module is not configured"); + return context; +} + +function terminalTask(task: QueueTask): boolean { + return task.status === "succeeded" || task.status === "failed" || task.status === "canceled"; +} + +function testTask(id: string, prompt: string, finalResponse: string, referenceTaskIds: string[] = [], createdAt = ctx().nowIso()): QueueTask { + return ctx().normalizeTask({ + id, + queueId: ctx().defaultQueueId, + queueEnteredAt: createdAt, + prompt, + basePrompt: prompt, + referenceTaskIds, + referenceInjection: null, + providerId: ctx().config.mainProviderId, + cwd: ctx().config.defaultWorkdir, + model: ctx().config.defaultModel, + reasoningEffort: ctx().resolveReasoningEffort(ctx().config.defaultModel, ctx().config.defaultReasoningEffort), + maxAttempts: 1, + status: "succeeded", + createdAt, + updatedAt: createdAt, + startedAt: createdAt, + finishedAt: createdAt, + readAt: null, + currentAttempt: 1, + currentMode: "initial", + codexThreadId: null, + activeTurnId: null, + finalResponse, + lastError: null, + lastJudge: null, + judgeFailCount: 0, + promptHistory: [], + output: finalResponse.length > 0 ? [{ seq: 1, at: createdAt, channel: "assistant", text: finalResponse, method: "item/agentMessage" }] : [], + events: [], + attempts: [], + cancelRequested: false, + nextPrompt: null, + nextMode: null, + }); +} + +function assertReferenceTest(condition: boolean, message: string): void { + if (!condition) throw new Error(message); +} + +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 = ctx().injectReferencedTaskContext({ + 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"); + taskB.basePrompt = injectedB.basePrompt ?? ""; + taskB.referenceInjection = injectedB.referenceInjection ?? null; + const injectedC = ctx().injectReferencedTaskContext({ + prompt: "C user prompt", + 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 = 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(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(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"); + assertReferenceTest(indexB > indexA, "C should include direct B after upstream A"); + assertReferenceTest(promptC.includes("----- Reference Round 1/2 -----"), "C should include explicit round 1 separator"); + assertReferenceTest(promptC.includes("----- Reference Round 2/2 -----"), "C should include explicit round 2 separator"); + 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# 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[] = []; + for (let index = 0; index < 8; index += 1) { + const id = `codex_${2000 + index}_${"abcdef".slice(0, 6 - String(index).length)}${index}`; + const parent = deepTasks.at(-1); + deepTasks.push(testTask(id, `deep prompt ${index}`, `deep final ${index}`, parent === undefined ? [] : [parent.id], `2026-05-08T00:${String(10 + index).padStart(2, "0")}:00.000Z`)); + } + const deepById = new Map(deepTasks.map((task) => [task.id, task])); + const injectedDeep = ctx().injectReferencedTaskContext({ + prompt: "Deep user prompt", + referenceTaskIds: [deepTasks.at(-1)?.id ?? ""], + }, (id) => deepById.get(id) ?? null, "2026-05-08T00:30:00.000Z"); + assertReferenceTest(injectedDeep.referenceInjection?.itemCount === 8, "deep reference chain should not truncate at six rounds"); + assertReferenceTest(injectedDeep.referenceInjection?.truncated === false, "deep reference chain should be marked complete"); + assertReferenceTest(injectedDeep.prompt.includes("----- Reference Round 8/8 -----"), "deep reference chain should expose all eight rounds"); + const retryTask = testTask("codex_3000_retry", injectedDeep.prompt, "", injectedDeep.referenceTaskIds, "2026-05-08T00:31:00.000Z"); + retryTask.basePrompt = injectedDeep.basePrompt ?? ""; + 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: [ + { name: "strip_frontend_hint_to_basePrompt", ok: true }, + { name: "multi_round_reference_graph", ok: true, itemCount: injectedC.referenceInjection?.itemCount ?? 0 }, + { name: "no_nested_injection_block", ok: true }, + { name: "chronological_round_order", ok: true }, + { name: "timestamp_and_round_separators", ok: true }, + { name: "environment_hint_injected_and_stripped_from_display", ok: true }, + { 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), + }; +} + +function queueOrderTestTask(id: string, status: TaskStatus, createdAt: string, queueEnteredAt: string): QueueTask { + const task = testTask(id, `${id} prompt`, status === "succeeded" ? `${id} final` : "", [], createdAt); + task.queueId = "queue_order_test"; + task.queueEnteredAt = queueEnteredAt; + task.status = status; + task.updatedAt = queueEnteredAt; + task.startedAt = status === "running" || status === "judging" ? queueEnteredAt : null; + task.finishedAt = terminalTask(task) ? queueEnteredAt : null; + task.currentAttempt = status === "queued" ? 0 : 1; + task.currentMode = status === "queued" ? null : "retry"; + task.nextMode = status === "retry_wait" ? "retry" : null; + task.nextPrompt = status === "retry_wait" ? "continue" : null; + return ctx().normalizeTask(task); +} + +function runQueueOrderingSelfTest(): JsonValue { + const activeRetry = queueOrderTestTask("codex_4000_active", "retry_wait", "2026-05-11T09:00:00.000Z", "2026-05-11T09:00:00.000Z"); + const movedOlderCreated = queueOrderTestTask("codex_3999_moved", "queued", "2026-05-11T08:00:00.000Z", "2026-05-11T08:00:00.000Z") as QueueTask & { queueEnteredAt?: string }; + delete (movedOlderCreated as Partial).queueEnteredAt; + movedOlderCreated.output.push({ seq: 2, at: "2026-05-11T09:30:00.000Z", channel: "system", text: "moved from queue=default to queue=queue_order_test\n", method: "queue/move" }); + ctx().normalizeTask(movedOlderCreated as QueueTask); + const blockedByRetry = [movedOlderCreated, activeRetry]; + const runningHead = queueOrderTestTask("codex_4100_running", "running", "2026-05-11T10:00:00.000Z", "2026-05-11T10:00:00.000Z"); + 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 = ctx().config.maxActiveQueues; + + assertReferenceTest(ctx().queueHeadTask("queue_order_test", blockedByRetry)?.id === activeRetry.id, "retry_wait head must keep blocking a moved older-created task"); + assertReferenceTest(ctx().nextRunnableTaskFrom("queue_order_test", blockedByRetry)?.id === activeRetry.id, "next runnable should be the retry_wait head"); + assertReferenceTest(ctx().nextRunnableTaskFrom("queue_order_test", [queuedBehindRunning, runningHead]) === null, "running head must block queued tasks behind it"); + assertReferenceTest(ctx().nextRunnableTaskFrom("queue_order_test", [queuedAfterTerminal, terminalAhead])?.id === queuedAfterTerminal.id, "terminal head should not block later queued task"); + assertReferenceTest(ctx().queuedStatusReason(queuedBehindRunning, [runningHead, queuedBehindRunning])?.label === "PREV TASK", "queued task behind an active same-queue task should expose PREV TASK reason"); + assertReferenceTest(ctx().availableQueueStartSlotsFor(8, 0) === Number.POSITIVE_INFINITY, "maxActiveQueues=0 should leave queue-to-queue concurrency unbounded"); + assertReferenceTest(ctx().availableQueueStartSlotsFor(0, 1) === 1, "empty active run slots should leave one slot available"); + assertReferenceTest(ctx().availableQueueStartSlotsFor(1, 1) === 0, "one active run slot should exhaust maxActiveQueues=1"); + try { + const marker = "__queue_order_idle_processing_self_test__"; + const beforeSlotCount = ctx().activeRunSlotCount(); + const hadProcessingMarker = ctx().processingQueues.has(marker); + const hadReservationMarker = ctx().activeRunSlotReservations.has(marker); + ctx().processingQueues.add(marker); + assertReferenceTest(ctx().activeRunSlotCount() === beforeSlotCount, "processing idle queue must not consume an active run slot"); + ctx().activeRunSlotReservations.add(marker); + assertReferenceTest(ctx().activeRunSlotCount() === beforeSlotCount + (hadReservationMarker ? 0 : 1), "reserved running queue must consume an active run slot"); + if (!hadProcessingMarker) ctx().processingQueues.delete(marker); + if (!hadReservationMarker) ctx().activeRunSlotReservations.delete(marker); + } finally { + ctx().config.maxActiveQueues = originalMaxActiveQueues; + ctx().updateProcessingFlag(); + } + { + const waiterCount = ctx().activeRunSlotWaiters.length; + const firstWaiter = ctx().enqueueActiveRunSlotWaiter(activeRetry); + const secondWaiter = ctx().enqueueActiveRunSlotWaiter(queuedAfterTerminal); + try { + assertReferenceTest(ctx().activeRunSlotWaiters[waiterCount]?.id === firstWaiter.id, "first active run slot waiter should keep FIFO position"); + assertReferenceTest(ctx().activeRunSlotWaiters[waiterCount + 1]?.id === secondWaiter.id, "second active run slot waiter should not jump ahead"); + } finally { + ctx().removeActiveRunSlotWaiter(firstWaiter); + ctx().removeActiveRunSlotWaiter(secondWaiter); + } + } + + return { + ok: true, + cases: [ + { 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: "queued_reason_prev_task", ok: true }, + { name: "max_active_queues_zero_is_unbounded", 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", metadata: Record | null = null): 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, + ...(metadata === null ? {} : { metadata }), + time: { start: 1000, end: 1337 }, + }, + }, + })}\n`, + itemId: `prt_trace_${seq}`, + }; +} + +function partialOpencodeTraceOutput(seq: number): LiveOutput { + const raw = JSON.stringify({ + type: "tool_use", + sessionID: "ses_trace_self_test", + part: { + type: "tool", + tool: "bash", + id: `prt_trace_${seq}`, + state: { + status: "completed", + input: { command: "git diff -- src/components/frontend/src/trace.tsx" }, + output: `diff --git a/src/components/frontend/src/trace.tsx b/src/components/frontend/src/trace.tsx\n${"x".repeat(3000)}`, + }, + }, + }); + return { + seq, + at: "2026-05-12T00:00:00.000Z", + channel: "tool", + method: "opencode/tool", + text: `${raw.slice(0, 700)}...`, + 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" }, + { seq: 2, at: "2026-05-12T00:00:00.000Z", channel: "system", method: "opencode/step-start", text: "opencode run started session=ses_trace_self_test\n" }, + opencodeTraceOutput(3, "grep", { command: "rg -n trace src/components/microservices/code-queue/src/index.ts" }, "src/components/microservices/code-queue/src/index.ts:1:trace"), + { seq: 4, at: "2026-05-12T00:00:00.000Z", channel: "system", method: "opencode/step-finish", text: "opencode run finished reason=tool-calls\n" }, + opencodeTraceOutput(5, "edit", { filePath: "src/components/frontend/src/trace.tsx" }, "Edit applied successfully.", "completed", { + diff: "Index: src/components/frontend/src/trace.tsx\n===================================================================\n--- src/components/frontend/src/trace.tsx\n+++ src/components/frontend/src/trace.tsx\n@@ -1,1 +1,2 @@\n const before = true;\n+const after = true;\n", + }), + opencodeTraceOutput(6, "bash", { command: "bunx tsc -p scripts/tsconfig.json --noEmit" }, "ok"), + partialOpencodeTraceOutput(7), + { seq: 8, at: "2026-05-12T00:00:00.000Z", channel: "reasoning", method: "opencode/reasoning", text: "hidden reasoning\n" }, + { seq: 9, at: "2026-05-12T00:00:00.000Z", channel: "assistant", method: "opencode/text", text: "hidden reasoning\n" }, + ]; + const transcript = buildTaskTranscript(task, 20, 0); + const explored = transcript.find((line) => line.seq === 3); + const edited = transcript.find((line) => line.seq === 5); + const ran = transcript.find((line) => line.seq === 6); + const partial = transcript.find((line) => line.seq === 7); + if (explored === undefined || edited === undefined || ran === undefined || partial === 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(partial.kind === "explored", "truncated opencode tool JSON should still normalize from command preview"); + 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"); + assertReferenceTest(String(edited.bodyPreview || "").includes("+const after = true;"), "opencode edit should use metadata diff for line diff display"); + assertReferenceTest(!transcript.some((line) => line.status === "opencode/step-start" || line.status === "opencode/step-finish"), "opencode step boundaries should stay out of trace"); + assertReferenceTest(!transcript.some((line) => String(line.bodyPreview || "").includes("hidden reasoning")), "reasoning-only opencode assistant text should not duplicate reasoning"); + 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: "partial_opencode_tool_to_explored", ok: true, title: partial?.title ?? null }, + { name: "step_boundaries_filtered", ok: true }, + { name: "reasoning_duplicate_filtered", ok: true }, + { 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 runJudgeInfraSelfTest(): JsonValue { + const thinkThenJson = [ + "The tool command seems to be garbled. Let me inspect the file.", + "只能根据 evidence 判定,最终 JSON 如下:", + "{\"decision\":\"complete\",\"confidence\":0.91,\"reason\":\"证据完整\",\"continuePrompt\":\"\"}", + ].join("\n"); + const fencedJson = [ + "```json", + "{\"decision\":\"retry\",\"confidence\":0.8,\"reason\":\"缺少 live 验证\",\"continuePrompt\":\"继续补齐 live 验证证据。\"}", + "```", + ].join("\n"); + const labeledContinue = "json:\n{\"decision\":\"continue\",\"confidence\":0.7,\"reason\":\"还要继续\",\"continuePrompt\":\"继续执行剩余验收。\"}"; + const parsedThink = parseJudgeJson(thinkThenJson); + const parsedFenced = parseJudgeJson(fencedJson); + const parsedContinue = parseJudgeJson(labeledContinue); + assertReferenceTest(parsedThink.value.decision === "complete", "judge parser should extract JSON after think/tool-like prose"); + assertReferenceTest(parsedFenced.value.decision === "retry", "judge parser should extract fenced JSON"); + assertReferenceTest(parsedContinue.value.decision === "continue", "judge parser should accept continue alias"); + + const repairMessages = miniMaxJudgeMessages("{\"instruction\":\"fixture\"}", { + error: "MiniMax judge did not return parseable JSON after denoise", + previousAnswerRaw: "Let me just look at the trace.tsx file.\nLet me inspect files first.", + }); + assertReferenceTest(repairMessages.length === 3, "repair request should include system, original user, and repair user messages"); + assertReferenceTest(repairMessages.every((message) => message.role !== "assistant"), "repair request must not echo malformed output as assistant history"); + assertReferenceTest(repairMessages[0]?.content.includes("没有工具访问") === true, "system prompt should prohibit tool/file inspection"); + assertReferenceTest(repairMessages[2]?.content.includes("previousAnswerRaw") === true, "repair request should preserve the bad answer as inert data"); + assertReferenceTest(repairMessages[2]?.content.includes("不要延续") === true, "repair request should explicitly stop continuing bad agent-like output"); + + return { + ok: true, + cases: [ + { name: "parse_think_then_json", ok: true, source: parsedThink.source }, + { name: "parse_fenced_json", ok: true, source: parsedFenced.source }, + { name: "parse_continue_alias", ok: true, source: parsedContinue.source }, + { name: "repair_without_assistant_echo", ok: true, roles: repairMessages.map((message) => message.role) }, + { name: "repair_blocks_tool_like_continuation", ok: true }, + ], + }; +} + +export { runJudgeInfraSelfTest, runQueueOrderingSelfTest, runReferenceInjectionSelfTest, runTracePortSelfTest }; diff --git a/src/components/microservices/code-queue/src/task-output.ts b/src/components/microservices/code-queue/src/task-output.ts new file mode 100644 index 00000000..140d01bd --- /dev/null +++ b/src/components/microservices/code-queue/src/task-output.ts @@ -0,0 +1,158 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs"; +import { resolve } from "node:path"; +import type { ArchivedLiveOutput, JsonValue, LiveOutput, OutputChannel, QueueTask, RuntimeConfig } from "./types"; + +export interface TaskOutputContext { + config: Pick; + allocateSeq: () => number; + errorToJson: (error: unknown) => JsonValue; + logger: (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void; + markTaskDirty: (taskId: string) => void; + nowIso: () => string; + schedulePersistState: () => void; +} + +const outputArchiveSeededTasks = new Set(); +let context: TaskOutputContext | null = null; + +export function configureTaskOutput(runtimeContext: TaskOutputContext): void { + context = runtimeContext; +} + +function ctx(): TaskOutputContext { + if (context === null) throw new Error("task-output module is not configured"); + return context; +} + +function taskOutputArchivePath(taskId: string): string { + return resolve(ctx().config.outputArchiveDir, `${taskId}.jsonl`); +} + +function serializeArchivedOutput(output: LiveOutput, op: ArchivedLiveOutput["op"], text: string): string { + const record: ArchivedLiveOutput = { + seq: output.seq, + at: output.at, + channel: output.channel, + text, + ...(output.method === undefined ? {} : { method: output.method }), + ...(output.itemId === undefined ? {} : { itemId: output.itemId }), + op, + }; + return `${JSON.stringify(record)}\n`; +} + +function ensureTaskOutputArchiveSeeded(task: QueueTask): void { + if (outputArchiveSeededTasks.has(task.id)) return; + mkdirSync(ctx().config.outputArchiveDir, { recursive: true }); + const path = taskOutputArchivePath(task.id); + if (!existsSync(path) && task.output.length > 0) { + const seed = task.output.map((output) => serializeArchivedOutput(output, "set", output.text)).join(""); + appendFileSync(path, seed, "utf8"); + } + outputArchiveSeededTasks.add(task.id); +} + +function appendOutputArchive(task: QueueTask, output: LiveOutput, op: ArchivedLiveOutput["op"], text: string): void { + try { + mkdirSync(ctx().config.outputArchiveDir, { recursive: true }); + appendFileSync(taskOutputArchivePath(task.id), serializeArchivedOutput(output, op, text), "utf8"); + } catch (error) { + ctx().logger("error", "codex_output_archive_write_failed", { taskId: task.id, error: ctx().errorToJson(error) }); + } +} + +function archiveRecordToOutput(value: unknown): ArchivedLiveOutput | null { + if (typeof value !== "object" || value === null || Array.isArray(value)) return null; + const record = value as Record; + const seq = Number(record.seq); + if (!Number.isFinite(seq)) return null; + const channel = String(record.channel || "system") as OutputChannel; + if (!["system", "user", "assistant", "reasoning", "command", "diff", "tool", "error"].includes(channel)) return null; + return { + seq, + at: typeof record.at === "string" ? record.at : ctx().nowIso(), + channel, + text: typeof record.text === "string" ? record.text : "", + ...(typeof record.method === "string" ? { method: record.method } : {}), + ...(typeof record.itemId === "string" ? { itemId: record.itemId } : {}), + op: record.op === "append" ? "append" : "set", + }; +} + +function archivedTaskOutput(task: QueueTask): LiveOutput[] { + const path = taskOutputArchivePath(task.id); + if (!existsSync(path)) return []; + const bySeq = new Map(); + try { + const text = readFileSync(path, "utf8"); + for (const line of text.split(/\r?\n/u)) { + if (line.trim().length === 0) continue; + const record = archiveRecordToOutput(JSON.parse(line) as unknown); + if (record === null) continue; + const existing = bySeq.get(record.seq); + if (record.op === "append" && existing !== undefined) { + existing.text += record.text; + existing.at = record.at; + existing.channel = record.channel; + existing.method = record.method; + existing.itemId = record.itemId; + } else { + const { op: _op, ...output } = record; + bySeq.set(record.seq, output); + } + } + } catch (error) { + ctx().logger("warn", "codex_output_archive_read_failed", { taskId: task.id, error: ctx().errorToJson(error) }); + return []; + } + return Array.from(bySeq.values()).sort((left, right) => Number(left.seq) - Number(right.seq)); +} + +function taskFullOutput(task: QueueTask): LiveOutput[] { + const bySeq = new Map(); + for (const output of archivedTaskOutput(task)) bySeq.set(output.seq, output); + for (const output of task.output) bySeq.set(output.seq, output); + return Array.from(bySeq.values()).sort((left, right) => Number(left.seq) - Number(right.seq)); +} + +function outputArchiveSignature(task: QueueTask): string { + try { + const stat = statSync(taskOutputArchivePath(task.id)); + return `${stat.size}:${Math.floor(stat.mtimeMs)}`; + } catch { + return "none"; + } +} + +function appendOutput(task: QueueTask, channel: OutputChannel, text: string, method?: string, itemId?: string, append = false): LiveOutput | null { + if (text.length === 0) return null; + try { + ensureTaskOutputArchiveSeeded(task); + } catch (error) { + ctx().logger("error", "codex_output_archive_seed_failed", { taskId: task.id, error: ctx().errorToJson(error) }); + } + const last = task.output[task.output.length - 1]; + let output: LiveOutput; + let archiveOp: ArchivedLiveOutput["op"] = "set"; + let archiveText = text; + if (append && last !== undefined && last.channel === channel && last.itemId === itemId && last.method === method && last.text.length < 24_000) { + last.text += text; + last.at = ctx().nowIso(); + output = last; + archiveOp = "append"; + } else { + output = { seq: ctx().allocateSeq(), at: ctx().nowIso(), channel, text, method, itemId }; + task.output.push(output); + } + appendOutputArchive(task, output, archiveOp, archiveText); + if (ctx().config.maxInMemoryOutputRecords > 0 && task.output.length > ctx().config.maxInMemoryOutputRecords) task.output.splice(0, task.output.length - ctx().config.maxInMemoryOutputRecords); + task.updatedAt = ctx().nowIso(); + ctx().markTaskDirty(task.id); + ctx().schedulePersistState(); + return output; +} + + +export { appendOutput, appendOutputArchive, archivedTaskOutput, outputArchiveSignature, taskFullOutput }; diff --git a/src/components/microservices/code-queue/src/task-view.ts b/src/components/microservices/code-queue/src/task-view.ts new file mode 100644 index 00000000..35e55c02 --- /dev/null +++ b/src/components/microservices/code-queue/src/task-view.ts @@ -0,0 +1,2111 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +import { existsSync, readdirSync, readFileSync, statSync, type Dirent } from "node:fs"; +import { resolve } from "node:path"; +import type { + AttemptSummary, + FeedbackPromptRecord, + JudgeDecision, + JudgeResult, + JsonValue, + LiveOutput, + PromptHistoryItem, + QueueTask, + QueuedStatusReason, + RuntimeConfig, + SessionCommandOutput, + SessionFileChange, + TranscriptKind, + TranscriptLine, +} from "./types"; +import { codeAgentPortForModel, codeAgentPortInfo, extractRecord } from "./code-agent/common"; +import { currentTaskPromptMarker, resolvedReferenceContextTitle, stripCodeQueueEnvironmentHint, userPromptForDisplay } from "./prompts"; +import { outputArchiveSignature, taskFullOutput } from "./task-output"; +import { retryPrompt } from "./judge"; + +export interface TaskViewContext { + config: Pick; + errorToJson: (error: unknown) => JsonValue; + jsonResponse: (body: unknown, status?: number) => Response; + logger: (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void; + mergePromptHistory: (items: PromptHistoryItem[]) => PromptHistoryItem[]; + nowIso: () => string; + outputPromptHistory: (task: QueueTask) => PromptHistoryItem[]; + pageBySeq: (items: T[], url: URL, limit: number) => { + mode: "tail" | "after" | "before"; + afterSeq: number; + beforeSeq: number | null; + nextAfterSeq: number; + previousBeforeSeq: number | null; + hasMore: boolean; + hasBefore: boolean; + chunk: T[]; + }; + parseLimit: (url: URL) => number; + parseSeqParam: (url: URL, name: string, defaultValue: number | null) => number | null; + queueIdOf: (task: QueueTask) => string; + queuedStatusReason: (task: QueueTask) => QueuedStatusReason | null; + queuedTaskPromptEditable: (task: QueueTask) => boolean; + taskQueueEnteredAt: (task: QueueTask) => string; +} + +const judgeFailRetryLimit = 3; +const transcriptCache = new Map(); +const codexSessionPathCache = new Map(); +const codexSessionFileChangeCache = new Map }>(); +const codexSessionCommandOutputCache = new Map }>(); +let context: TaskViewContext | null = null; + +export function configureTaskView(runtimeContext: TaskViewContext): void { + context = runtimeContext; +} + +function ctx(): TaskViewContext { + if (context === null) throw new Error("task-view module is not configured"); + return context; +} + +function taskTimestamp(value: string | null): string | null { + if (value === null || value.length === 0) return null; + const time = Date.parse(value); + return Number.isFinite(time) ? new Date(time).toISOString() : null; +} + +function terminalTask(task: QueueTask): boolean { + return task.status === "succeeded" || task.status === "failed" || task.status === "canceled"; +} + +function terminalTaskUnread(task: QueueTask): boolean { + return terminalTask(task) && task.readAt === null; +} + +function queuedStatusPayload(task: QueueTask): { queuedReason: QueuedStatusReason | null; queuedReasonLabel: string | null } { + const reason = ctx().queuedStatusReason(task); + return { + queuedReason: reason, + queuedReasonLabel: reason?.label ?? null, + }; +} + +function durationMsBetween(startAt: string | null | undefined, endAt: string | null | undefined): number | null { + const start = typeof startAt === "string" ? Date.parse(startAt) : NaN; + const end = typeof endAt === "string" ? Date.parse(endAt) : NaN; + if (!Number.isFinite(start) || !Number.isFinite(end)) return null; + return Math.max(0, end - start); +} + +function safePreview(value: string, max = 900): string { + const compact = value.replace(/\s+/gu, " ").trim(); + return compact.length > max ? `${compact.slice(0, max)}...` : compact; +} + +function prefixPreview(value: string, max = 900): string { + const trimmed = value.trim(); + return trimmed.length > max ? `${trimmed.slice(0, max)}...` : trimmed; +} + +function linePreview(text: string, maxLines: number, maxChars: number): { text: string; omittedLines: number } { + const clean = text.replace(/\u001b\[[0-9;]*m/gu, "").trimEnd(); + if (clean.length === 0) return { text: "", omittedLines: 0 }; + const lines = clean.split(/\r?\n/u); + const kept: string[] = []; + let chars = 0; + for (const line of lines) { + if (kept.length >= maxLines || chars + line.length > maxChars) break; + kept.push(line); + chars += line.length + 1; + } + return { text: kept.join("\n"), omittedLines: Math.max(0, lines.length - kept.length) }; +} + +function completeTraceText(text: string): { text: string; omittedLines: number } { + return { text: text.replace(/\u001b\[[0-9;]*m/gu, "").trimEnd(), omittedLines: 0 }; +} + +function editedOutputPreview(text: string): { text: string; omittedLines: number } { + return linePreview(compactTranscriptBody(text), 120, 24_000); +} + +function codexSessionDateDir(value: string | null | undefined): string | null { + if (typeof value !== "string" || value.length === 0) return null; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return null; + return resolve( + ctx().config.codexHome, + "sessions", + String(date.getUTCFullYear()), + String(date.getUTCMonth() + 1).padStart(2, "0"), + String(date.getUTCDate()).padStart(2, "0"), + ); +} + +function codexSessionSignature(path: string): string | null { + try { + const stat = statSync(path); + return `${stat.size}:${Math.floor(stat.mtimeMs)}`; + } catch { + return null; + } +} + +function findCodexSessionFile(task: QueueTask): string | null { + const threadId = task.codexThreadId; + if (threadId === null || threadId.length === 0) return null; + const cached = codexSessionPathCache.get(threadId); + if (cached !== undefined && existsSync(cached)) return cached; + + const roots = Array.from(new Set([ + codexSessionDateDir(task.startedAt), + codexSessionDateDir(task.createdAt), + codexSessionDateDir(task.updatedAt), + resolve(ctx().config.codexHome, "sessions"), + ].filter((value): value is string => value !== null && existsSync(value)))); + const matches: string[] = []; + let scanned = 0; + const scan = (dir: string, depth: number): void => { + if (depth < 0 || scanned > 4000) return; + scanned += 1; + let entries: Dirent[]; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + const path = resolve(dir, entry.name); + if (entry.isDirectory()) { + scan(path, depth - 1); + } else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(threadId)) { + matches.push(path); + } + } + }; + for (const root of roots) scan(root, root.endsWith("/sessions") ? 4 : 1); + matches.sort((left, right) => (statMtimeMs(right) - statMtimeMs(left)) || right.localeCompare(left)); + const match = matches[0] ?? null; + if (match !== null) codexSessionPathCache.set(threadId, match); + return match; +} + +function statMtimeMs(path: string): number { + try { + return statSync(path).mtimeMs; + } catch { + return 0; + } +} + +function parseCodexToolOutputText(raw: string): string { + const text = String(raw || ""); + try { + const parsed = JSON.parse(text) as Record; + if (typeof parsed.output === "string") return parsed.output; + } catch { + // Keep the raw tool output if it is not the JSON wrapper used by Codex. + } + return text; +} + +function recordStringField(record: Record | null, keys: string[]): string { + if (record === null) return ""; + for (const key of keys) { + const value = record[key]; + if (typeof value === "string") return value; + } + return ""; +} + +function recordNumberField(record: Record | null, keys: string[]): number | null { + if (record === null) return null; + for (const key of keys) { + const value = Number(record[key]); + if (Number.isFinite(value)) return value; + } + return null; +} + +function parseToolOutputStreams(raw: string): { stdout: string; stderr: string; output: string; exitCode: number | null } { + const text = String(raw || ""); + try { + const parsed = JSON.parse(text) as unknown; + const record = extractRecord(parsed); + const stdout = recordStringField(record, ["stdout", "stdoutText"]); + const stderr = recordStringField(record, ["stderr", "stderrText"]); + const output = recordStringField(record, ["output", "text"]); + const exitCode = recordNumberField(record, ["exitCode", "code", "status"]); + if (stdout.length > 0 || stderr.length > 0 || output.length > 0) { + return { stdout: stdout || output, stderr, output: output || [stdout, stderr].filter(Boolean).join("\n"), exitCode }; + } + } catch { + // Not a JSON wrapper; parse the Codex CLI tool-result envelope below. + } + + const outputMatch = /(?:^|\n)Output:\n([\s\S]*)$/u.exec(text); + const exitMatch = /Process exited with code\s+(-?\d+)/u.exec(text); + if (outputMatch !== null) { + const output = (outputMatch[1] ?? "").trimEnd(); + return { + stdout: output, + stderr: "", + output, + exitCode: exitMatch === null ? null : Number(exitMatch[1]), + }; + } + return { stdout: text, stderr: "", output: text, exitCode: exitMatch === null ? null : Number(exitMatch[1]) }; +} + +function formatCommandOutput(output: SessionCommandOutput | null | undefined): string { + if (output === null || output === undefined) return ""; + const parts: string[] = []; + const stdout = output.stdout.trimEnd(); + const stderr = output.stderr.trimEnd(); + if (stdout.length > 0) parts.push(stderr.length > 0 ? `[stdout]\n${stdout}` : stdout); + if (stderr.length > 0) parts.push(`[stderr]\n${stderr}`); + if (parts.length > 0) return parts.join("\n"); + return output.output.trimEnd(); +} + +function addCommandOutputStreams(line: TranscriptLine, output: SessionCommandOutput | null | undefined, fullText: boolean): TranscriptLine { + if (output === null || output === undefined) return line; + const stdout = output.stdout.trimEnd(); + const stderr = output.stderr.trimEnd(); + if (stdout.length > 0) { + const preview = fullText ? completeTraceText(stdout) : outputPreview(stdout); + if (preview.text.length > 0) { + line.stdoutPreview = preview.text; + line.stdoutOmittedLines = preview.omittedLines || undefined; + } + } + if (stderr.length > 0) { + const preview = fullText ? completeTraceText(stderr) : outputPreview(stderr); + if (preview.text.length > 0) { + line.stderrPreview = preview.text; + line.stderrOmittedLines = preview.omittedLines || undefined; + } + } + return line; +} + +function parseCodexSessionCommandOutputs(path: string): Map { + const signature = codexSessionSignature(path); + const cached = codexSessionCommandOutputCache.get(path); + if (signature !== null && cached?.signature === signature) return cached.outputs; + + const outputs = new Map(); + try { + const text = readFileSync(path, "utf8"); + for (const line of text.split(/\r?\n/u)) { + if (line.trim().length === 0) continue; + let record: Record; + try { + record = JSON.parse(line) as Record; + } catch { + continue; + } + const payload = extractRecord(record.payload); + if (payload === null) continue; + const type = String(payload.type || ""); + if (type !== "function_call_output" && type !== "custom_tool_call_output") continue; + const callId = typeof payload.call_id === "string" ? payload.call_id : ""; + if (callId.length === 0) continue; + const parsed = parseToolOutputStreams(String(payload.output || "")); + if (parsed.output.trim().length === 0 && parsed.stdout.trim().length === 0 && parsed.stderr.trim().length === 0) continue; + outputs.set(callId, { + callId, + at: typeof record.timestamp === "string" ? record.timestamp : "", + stdout: parsed.stdout, + stderr: parsed.stderr, + output: parsed.output, + exitCode: parsed.exitCode, + }); + } + } catch (error) { + ctx().logger("warn", "codex_session_command_output_parse_failed", { path, error: ctx().errorToJson(error) }); + } + if (signature !== null) codexSessionCommandOutputCache.set(path, { signature, outputs }); + if (codexSessionCommandOutputCache.size > 40) { + const firstKey = codexSessionCommandOutputCache.keys().next().value; + if (typeof firstKey === "string") codexSessionCommandOutputCache.delete(firstKey); + } + return outputs; +} + +function codexSessionCommandOutputsByCallId(task: QueueTask): Map { + const path = findCodexSessionFile(task); + return path === null ? new Map() : parseCodexSessionCommandOutputs(path); +} + +function isInlineFileChangeInput(name: string, input: string): boolean { + return name === "apply_patch" && /^\*\*\* Begin Patch/mu.test(input); +} + +function trimmedPatchForTrace(input: string): string { + const normalized = input.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n").trimEnd(); + const maxChars = 120_000; + if (normalized.length <= maxChars) return normalized; + return `${normalized.slice(0, maxChars)}\n...[patch content truncated: ${normalized.length - maxChars} chars omitted]`; +} + +function parseCodexSessionFileChanges(path: string): Map { + const signature = codexSessionSignature(path); + const cached = codexSessionFileChangeCache.get(path); + if (signature !== null && cached?.signature === signature) return cached.changes; + + const changes = new Map(); + try { + const text = readFileSync(path, "utf8"); + for (const line of text.split(/\r?\n/u)) { + if (line.trim().length === 0) continue; + let record: Record; + try { + record = JSON.parse(line) as Record; + } catch { + continue; + } + const payload = extractRecord(record.payload); + if (payload === null) continue; + const type = String(payload.type || ""); + const callId = typeof payload.call_id === "string" ? payload.call_id : ""; + if (callId.length === 0) continue; + if (type === "custom_tool_call") { + const name = String(payload.name || ""); + const input = typeof payload.input === "string" ? payload.input : ""; + if (!isInlineFileChangeInput(name, input)) continue; + changes.set(callId, { + callId, + at: typeof record.timestamp === "string" ? record.timestamp : "", + name, + input: trimmedPatchForTrace(input), + output: "", + }); + continue; + } + if (type === "custom_tool_call_output") { + const existing = changes.get(callId); + if (existing === undefined) continue; + existing.output = parseCodexToolOutputText(String(payload.output || "")); + } + } + } catch (error) { + ctx().logger("warn", "codex_session_file_change_parse_failed", { path, error: ctx().errorToJson(error) }); + } + if (signature !== null) codexSessionFileChangeCache.set(path, { signature, changes }); + if (codexSessionFileChangeCache.size > 40) { + const firstKey = codexSessionFileChangeCache.keys().next().value; + if (typeof firstKey === "string") codexSessionFileChangeCache.delete(firstKey); + } + return changes; +} + +function codexSessionFileChangesByCallId(task: QueueTask): Map { + const path = findCodexSessionFile(task); + return path === null ? new Map() : parseCodexSessionFileChanges(path); +} + +function fileChangeTextWithInlinePatch(item: LiveOutput, changes: Map): string { + if (item.method !== "item/fileChange/outputDelta" || typeof item.itemId !== "string") return item.text; + const change = changes.get(item.itemId); + if (change === undefined || change.input.length === 0 || item.text.includes(change.input)) return item.text; + return `${item.text.trimEnd()}\n\n${change.input}\n`; +} + +function compactNoisyLine(line: string): string { + const compact = line.replace(/\s+/gu, " ").trimEnd(); + const hasEncodedBlob = /[A-Za-z0-9+/=]{220,}/u.test(compact); + const hasSshWrapper = compact.includes("UNIDESK_SSH_TOOL_DIR") || compact.includes("apply_patch") || compact.includes("base64 -d"); + if (compact.length > 420 && (hasEncodedBlob || hasSshWrapper)) { + return `${compact.slice(0, 220)} ... [omitted noisy wrapper, ${compact.length - 220} chars]`; + } + return compact.length > 1200 ? `${compact.slice(0, 900)} ... [omitted ${compact.length - 900} chars]` : line; +} + +function compactTranscriptBody(text: string): string { + return text.split(/\r?\n/u).map(compactNoisyLine).join("\n"); +} + +function parseCommandLine(text: string): { command: string; status?: string } | null { + const match = text.match(/^item\/(?:started|completed):\s+([\s\S]*?)\s+status=([A-Za-z0-9_-]+)/u); + if (match === null) return null; + return { command: match[1]?.trim() ?? "", status: match[2] }; +} + +function extractOuterQuotedShellArg(text: string): { body: string; trailing: string } | null { + const quote = text[0]; + if (quote !== "\"" && quote !== "'") return null; + let escaped = false; + for (let index = 1; index < text.length; index += 1) { + const char = text[index] ?? ""; + if (quote === "\"" && escaped) { + escaped = false; + continue; + } + if (quote === "\"" && char === "\\") { + escaped = true; + continue; + } + if (char === quote) return { body: text.slice(1, index), trailing: text.slice(index + 1).trim() }; + } + return null; +} + +function decodeShellDoubleQuoted(text: string): string { + return text + .replace(/\\(["\\$`])/gu, "$1") + .replace(/\\\r?\n/gu, ""); +} + +function displayCommand(command: string): string { + const normalized = command.trim().replace(/\\n/gu, "\n"); + const match = normalized.match(/^(?:\/usr\/bin\/env\s+)?(?:\/(?:usr\/)?bin\/)?(?:bash|sh)\s+-lc\s+([\s\S]+)$/u); + if (match === null) return normalized; + const shellText = (match[1] ?? "").trim(); + const shellArg = extractOuterQuotedShellArg(shellText); + if (shellArg === null) return (match[1] ?? normalized).trim(); + const quote = shellText[0]; + const body = quote === "\"" ? decodeShellDoubleQuoted(shellArg.body) : shellArg.body; + return shellArg.trailing.length > 0 ? `${body} ${shellArg.trailing}` : body; +} + +function shortCommandTitle(command: string): string { + const firstLine = displayCommand(command).split(/\r?\n/u).find((line) => line.trim().length > 0)?.trim() ?? command.trim(); + return safePreview(firstLine, 180); +} + +function commandPreview(command: string): { text: string; omittedLines: number } { + return linePreview(compactTranscriptBody(displayCommand(command)), 10, 8000); +} + +function outputPreview(text: string): { text: string; omittedLines: number } { + return linePreview(compactTranscriptBody(text), 4, 1600); +} + +function fullMessageBody(text: string): { text: string; omittedLines: number } { + return linePreview(text.replace(/\u001b\[[0-9;]*m/gu, "").trimEnd(), 12, 4000); +} + +function timestampMs(value: string | null | undefined): number | null { + if (typeof value !== "string" || value.length === 0) return null; + const time = Date.parse(value); + return Number.isFinite(time) ? time : null; +} + +function nonNegativeElapsed(startMs: number | null, endMs: number | null): number | null { + if (startMs === null || endMs === null) return null; + return Math.max(0, endMs - startMs); +} + +function taskTiming(task: QueueTask): JsonValue { + const nowMs = Date.now(); + const createdMs = timestampMs(task.createdAt); + const startedMs = timestampMs(task.startedAt); + const finishedMs = timestampMs(task.finishedAt); + const updatedMs = timestampMs(task.updatedAt); + const terminal = task.status === "succeeded" || task.status === "failed" || task.status === "canceled"; + const effectiveEndMs = finishedMs ?? (terminal ? updatedMs : nowMs); + return { + queueWaitMs: nonNegativeElapsed(createdMs, startedMs ?? (terminal ? effectiveEndMs : nowMs)), + durationMs: nonNegativeElapsed(startedMs, effectiveEndMs), + totalElapsedMs: nonNegativeElapsed(createdMs, effectiveEndMs), + effectiveEndAt: finishedMs !== null ? task.finishedAt : terminal ? task.updatedAt : null, + running: !terminal && startedMs !== null, + }; +} + +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 = ctx().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 }; + return { + seq, + at, + kind, + title, + status, + commandPreview: commandInfo.text || undefined, + commandOmittedLines: commandInfo.omittedLines || undefined, + bodyPreview: bodyInfo.text || undefined, + bodyOmittedLines: bodyInfo.omittedLines || undefined, + rawSeqs, + }; +} + + +function commandKind(command: string): TranscriptKind { + if (/\b(apply_patch|git apply|cat >|tee .*<<|sed -i|python3? .*write_text)\b/u.test(command)) return "edited"; + if (/\b(rg|grep|find|ls|cat|sed -n|tail|head|git status|git diff|ps)\b/u.test(command)) return "explored"; + return "ran"; +} + +function commandKindLabel(kind: TranscriptKind): string { + if (kind === "edited") return "Edited"; + if (kind === "explored") return "Explored"; + 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 decodeJsonStringFragment(value: string): string { + try { + return JSON.parse(`"${value}"`) as string; + } catch { + return value + .replace(/\\"/gu, "\"") + .replace(/\\\\/gu, "\\") + .replace(/\\n/gu, "\n") + .replace(/\\r/gu, "\r") + .replace(/\\t/gu, "\t"); + } +} + +function partialJsonStringField(text: string, key: string): string { + const match = new RegExp(`"${key}"\\s*:\\s*"`, "u").exec(text); + if (match === null) return ""; + const start = match.index + match[0].length; + let escaped = false; + for (let index = start; index < text.length; index += 1) { + const char = text[index] ?? ""; + if (escaped) { + escaped = false; + continue; + } + if (char === "\\") { + escaped = true; + continue; + } + if (char === "\"") return decodeJsonStringFragment(text.slice(start, index)); + } + return decodeJsonStringFragment(text.slice(start).replace(/\s*\.\.\.\s*$/u, "")); +} + +function partialJsonNumberField(text: string, key: string): number | null { + const match = new RegExp(`"${key}"\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`, "u").exec(text); + if (match === null) return null; + const value = Number(match[1]); + return Number.isFinite(value) ? value : null; +} + +function openCodePartialRecordFromOutput(item: LiveOutput): Record | null { + if (!String(item.method || "").startsWith("opencode/")) return null; + const text = item.text.trim(); + if (!text.includes("\"part\"") && !text.includes("\"tool\"")) return null; + const tool = partialJsonStringField(text, "tool"); + const command = partialJsonStringField(text, "command") || partialJsonStringField(text, "cmd") || partialJsonStringField(text, "script"); + const output = partialJsonStringField(text, "output") || partialJsonStringField(text, "result"); + if (tool.length === 0 && command.length === 0 && output.length === 0) return null; + const input: Record = {}; + for (const key of ["command", "cmd", "script", "filePath", "filepath", "path", "pattern", "query", "title", "description"]) { + const value = partialJsonStringField(text, key); + if (value.length > 0) input[key] = value; + } + for (const key of ["offset", "limit"]) { + const value = partialJsonNumberField(text, key); + if (value !== null) input[key] = value; + } + return { + type: partialJsonStringField(text, "type") || "tool_use", + part: { + type: "tool", + tool: tool || "tool", + state: { + status: partialJsonStringField(text, "status"), + input, + output, + }, + }, + }; +} + +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 openCodePartialRecordFromOutput(item); + } +} + +function openCodeToolInputCommand(tool: string, input: Record | null): string { + const command = recordStringField(input, ["command", "cmd", "script"]); + 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 isOpenCodeStepBoundaryMethod(method: string | undefined): boolean { + return method === "opencode/step-start" || method === "opencode/step-finish"; +} + +function openCodeVisibleAssistantText(text: string): string { + return String(text || "").replace(/<(?:think|thinking)\b[^>]*>[\s\S]*?<\/(?:think|thinking)>/giu, "").trim(); +} + +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 recordType = recordStringField(record, ["type", "event", "name"]).toLowerCase(); + const partType = recordStringField(part, ["type"]).toLowerCase(); + if (recordType === "step_start" || recordType === "step-start" || partType === "step-start") return null; + if (recordType === "step_finish" || recordType === "step-finish" || partType === "step-finish") 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 metadata = extractRecord(state?.metadata); + const filediff = extractRecord(metadata?.filediff); + const rawOutput = recordDisplayField(state, ["output", "result"]) || recordDisplayField(part, ["output", "text", "content"]) || recordDisplayField(record, ["output", "text", "content"]); + const diffOutput = recordDisplayField(metadata, ["diff"]) || recordDisplayField(filediff, ["patch"]); + const status = recordStringField(state, ["status"]) || recordStringField(part, ["status"]) || recordStringField(record, ["status"]); + const kind = openCodeToolKind(tool, command); + const body = kind === "edited" && diffOutput.length > 0 ? diffOutput : rawOutput.length > 0 ? rawOutput : 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; +} + +function promptSnapshot(text: string, maxPreviewChars = 1200): { text: string; preview: string; chars: number; lines: number; truncated: boolean } { + const normalized = text.trimEnd(); + const preview = safePreview(normalized, maxPreviewChars); + return { + text: normalized, + preview, + chars: normalized.length, + lines: promptLineCount(normalized), + truncated: preview.length < normalized.length, + }; +} + +function setAttemptInputPrompt(attempt: AttemptSummary, prompt: string): void { + const snapshot = promptSnapshot(prompt, 1200); + if (snapshot.chars === 0) return; + attempt.inputPrompt = snapshot.text; + attempt.inputPromptPreview = snapshot.preview; + attempt.inputPromptChars = snapshot.chars; + attempt.inputPromptLines = snapshot.lines; +} + +function setAttemptFeedbackPrompt(attempt: AttemptSummary | undefined, prompt: string, source: string, forAttempt: number | null): void { + if (attempt === undefined) return; + const snapshot = promptSnapshot(prompt, 1600); + if (snapshot.chars === 0) return; + attempt.feedbackPrompt = snapshot.text; + attempt.feedbackPromptPreview = snapshot.preview; + attempt.feedbackPromptChars = snapshot.chars; + attempt.feedbackPromptLines = snapshot.lines; + attempt.feedbackPromptSource = source; + attempt.feedbackPromptForAttempt = forAttempt; +} + +function taskInitialPromptLine(task: QueueTask, fullText = false): TranscriptLine | null { + const prompt = (task.basePrompt || userPromptForDisplay(task.prompt)).trimEnd(); + if (prompt.length === 0) return null; + const line = transcriptLine("message", task.createdAt, 0.5, "Submitted prompt", [], prompt, "", "enqueue", fullText); + const fullPrompt = task.prompt.trimEnd(); + if (fullText && fullPrompt.length > 0 && fullPrompt !== prompt) { + line.fullPrompt = fullPrompt; + line.fullPromptLines = promptLineCount(fullPrompt); + line.fullPromptChars = fullPrompt.length; + line.foldedReferencePrompt = true; + } + return line; +} + +function promptHistoryTranscriptLines(task: QueueTask, fullText = false): TranscriptLine[] { + return ctx().mergePromptHistory([...(Array.isArray(task.promptHistory) ? task.promptHistory : []), ...ctx().outputPromptHistory(task)]) + .map((item) => transcriptLine("message", item.at, item.seq, "Steer prompt", [item.seq], item.text, "", item.method, fullText)); +} + +function sortTranscript(entries: TranscriptLine[]): TranscriptLine[] { + return entries.sort((left, right) => Number(left.seq) - Number(right.seq)); +} + +function boundedTranscript(entries: TranscriptLine[], limit: number): TranscriptLine[] { + sortTranscript(entries); + if (entries.length <= limit) return entries; + const first = entries[0]; + if (first?.title === "Submitted prompt" && first.rawSeqs.length === 0 && limit > 1) { + return [first, ...entries.slice(-(limit - 1))]; + } + return entries.slice(-limit); +} + +function buildTaskTranscript(task: QueueTask, limit = 180, rawOutputWindow = 0, fullText = false): TranscriptLine[] { + const entries: TranscriptLine[] = []; + const initialPrompt = taskInitialPromptLine(task, fullText); + if (initialPrompt !== null) entries.push(initialPrompt); + const promptHistoryLines = promptHistoryTranscriptLines(task, fullText); + const promptHistorySeqs = new Set(promptHistoryLines.map((line) => line.seq)); + entries.push(...promptHistoryLines); + let activeCommand: { seq: number; at: string; command: string; status?: string; body: string; rawSeqs: number[]; itemId?: string } | null = null; + + const flushCommand = (): void => { + if (activeCommand === null) return; + const kind = commandKind(activeCommand.command); + const output = activeCommand.itemId === undefined ? null : commandOutputs.get(activeCommand.itemId) ?? null; + const body = activeCommand.body.length > 0 ? activeCommand.body : formatCommandOutput(output); + entries.push(addCommandOutputStreams(transcriptLine( + kind, + activeCommand.at, + activeCommand.seq, + shortCommandTitle(activeCommand.command), + activeCommand.rawSeqs, + body, + activeCommand.command, + activeCommand.status, + fullText, + ), output, fullText)); + activeCommand = null; + }; + + const outputSource = rawOutputWindow > 0 ? task.output : taskFullOutput(task); + const outputItems = rawOutputWindow > 0 && outputSource.length > rawOutputWindow + ? outputSource.slice(-rawOutputWindow) + : outputSource; + const commandOutputs = outputItems.some((item) => item.channel === "command" && typeof item.itemId === "string") + ? codexSessionCommandOutputsByCallId(task) + : new Map(); + const fileChangeInputs = outputItems.some((item) => item.channel === "diff" && item.method === "item/fileChange/outputDelta" && typeof item.itemId === "string") + ? codexSessionFileChangesByCallId(task) + : new Map(); + for (const item of outputItems) { + if (initialPrompt !== null && item.channel === "user" && item.method === "enqueue") continue; + if (item.channel === "user" && item.method === "turn/steer" && promptHistorySeqs.has(item.seq)) continue; + if (isOpenCodeStepBoundaryMethod(item.method)) continue; + if (item.channel === "command" && item.method === "item/started") { + flushCommand(); + const parsed = parseCommandLine(item.text); + activeCommand = { + seq: item.seq, + at: item.at, + command: parsed?.command || item.text, + status: parsed?.status, + body: "", + rawSeqs: [item.seq], + ...(typeof item.itemId === "string" ? { itemId: item.itemId } : {}), + }; + continue; + } + if (item.channel === "command" && item.method === "item/commandExecution/outputDelta") { + if (activeCommand !== null) { + activeCommand.body += item.text; + activeCommand.rawSeqs.push(item.seq); + } else { + const output = typeof item.itemId === "string" ? commandOutputs.get(item.itemId) ?? null : null; + entries.push(addCommandOutputStreams(transcriptLine("ran", item.at, item.seq, "Command output", [item.seq], item.text || formatCommandOutput(output), "", undefined, fullText), output, fullText)); + } + continue; + } + if (item.channel === "command" && item.method === "item/completed") { + const parsed = parseCommandLine(item.text); + if (activeCommand !== null) { + activeCommand.status = parsed?.status ?? activeCommand.status; + activeCommand.rawSeqs.push(item.seq); + flushCommand(); + } else { + const command = parsed?.command || item.text; + const kind = commandKind(command); + const output = typeof item.itemId === "string" ? commandOutputs.get(item.itemId) ?? null : null; + entries.push(addCommandOutputStreams(transcriptLine(kind, item.at, item.seq, shortCommandTitle(command), [item.seq], formatCommandOutput(output), command, parsed?.status, fullText), output, fullText)); + } + continue; + } + + flushCommand(); + if (item.channel === "diff") { + const text = fileChangeTextWithInlinePatch(item, fileChangeInputs); + entries.push(transcriptLine("edited", item.at, item.seq, "Edited files", [item.seq], text, "", item.method, fullText)); + } else if (item.channel === "error") { + entries.push(transcriptLine("error", item.at, item.seq, "Error", [item.seq], item.text, "", item.method, fullText)); + } else if (item.channel === "assistant") { + const body = String(item.method || "").startsWith("opencode/") ? openCodeVisibleAssistantText(item.text) : item.text; + if (body.length > 0) entries.push(transcriptLine("message", item.at, item.seq, "Assistant message", [item.seq], body, "", item.method, fullText)); + } else if (item.channel === "reasoning") { + 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" + ? "Recovered thread execution" + : item.method === "judge" + ? "Judge result" + : "System"; + entries.push(transcriptLine("system", item.at, item.seq, title, [item.seq], item.text, "", item.method, fullText)); + } + } + flushCommand(); + return boundedTranscript(entries, limit); +} + +function compactTaskTranscriptLine(item: LiveOutput, title: string, kind: TranscriptKind): TranscriptLine { + return { + seq: item.seq, + at: item.at, + kind, + title, + status: item.method, + bodyPreview: prefixPreview(item.text.replace(/\u001b\[[0-9;]*m/gu, ""), 900), + rawSeqs: [item.seq], + }; +} + +function buildCompactTaskTranscript(task: QueueTask, limit = 12, rawOutputWindow = 24): TranscriptLine[] { + const entries: TranscriptLine[] = []; + for (const item of task.promptHistory.slice(-2)) { + entries.push({ + seq: item.seq, + at: item.at, + kind: "message", + title: "Steer prompt", + status: item.method, + bodyPreview: prefixPreview(item.text, 900), + rawSeqs: [item.seq], + }); + } + const outputItems = task.output.slice(-rawOutputWindow); + const fileChangeInputs = outputItems.some((item) => item.channel === "diff" && item.method === "item/fileChange/outputDelta" && typeof item.itemId === "string") + ? codexSessionFileChangesByCallId(task) + : new Map(); + for (const item of outputItems) { + if (item.channel === "user" && item.method === "enqueue") continue; + if (isOpenCodeStepBoundaryMethod(item.method)) continue; + if (item.channel === "command") { + const isOutput = item.method === "item/commandExecution/outputDelta"; + entries.push({ + seq: item.seq, + at: item.at, + kind: isOutput ? "ran" : item.method === "item/started" ? "ran" : "system", + title: isOutput ? "Command output" : item.method === "item/started" ? "Command started" : "Command completed", + status: item.method, + commandPreview: isOutput ? undefined : prefixPreview(item.text, 900), + bodyPreview: isOutput ? prefixPreview(item.text, 900) : undefined, + rawSeqs: [item.seq], + }); + } else if (item.channel === "diff") { + entries.push(compactTaskTranscriptLine({ ...item, text: fileChangeTextWithInlinePatch(item, fileChangeInputs) }, "Edited files", "edited")); + } else if (item.channel === "error") { + entries.push(compactTaskTranscriptLine(item, "Error", "error")); + } else if (item.channel === "assistant" || item.channel === "reasoning" || item.channel === "user") { + const body = item.channel === "assistant" && String(item.method || "").startsWith("opencode/") ? openCodeVisibleAssistantText(item.text) : item.text; + if (body.length > 0) entries.push(compactTaskTranscriptLine({ ...item, text: body }, 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 { + 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); +} + +function transcriptSignature(task: QueueTask): string { + const last = task.output.at(-1); + return `${task.updatedAt}:${task.output.length}:${last?.seq ?? 0}:${last?.text.length ?? 0}:${outputArchiveSignature(task)}:${task.status}:${task.createdAt}:${task.basePrompt.length}:${task.prompt.length}:${task.promptHistory.length}:${task.promptHistory.at(-1)?.seq ?? 0}`; +} + +function cachedTranscript(task: QueueTask, fullText: boolean): TranscriptLine[] { + const signature = transcriptSignature(task); + const cached = transcriptCache.get(task.id); + if (cached?.signature === signature) { + const cachedTranscript = fullText ? cached.fullTranscript : cached.previewTranscript; + if (cachedTranscript !== undefined) return cachedTranscript; + } + const transcript = buildTaskTranscript(task, Number.MAX_SAFE_INTEGER, 0, fullText); + const next: { signature: string; previewTranscript?: TranscriptLine[]; fullTranscript?: TranscriptLine[] } = cached?.signature === signature ? cached : { signature }; + if (fullText) next.fullTranscript = transcript; + else next.previewTranscript = transcript; + transcriptCache.set(task.id, next); + if (transcriptCache.size > 80) { + const firstKey = transcriptCache.keys().next().value; + if (typeof firstKey === "string") transcriptCache.delete(firstKey); + } + return transcript; +} + +function cachedFullTranscript(task: QueueTask): TranscriptLine[] { + return cachedTranscript(task, true); +} + +function cachedPreviewTranscript(task: QueueTask): TranscriptLine[] { + return cachedTranscript(task, false); +} + +function outputForResponse(task: QueueTask, includeRaw: boolean): LiveOutput[] { + if (includeRaw) return taskFullOutput(task); + return task.output.slice(-80).map((item) => ({ ...item, text: safePreview(item.text, 4000) })); +} + +function attemptForResponse(attempt: AttemptSummary, full = false): JsonValue { + const finalResponse = String(attempt.finalResponse ?? ""); + const inputPrompt = String(attempt.inputPrompt ?? ""); + const feedbackPrompt = String(attempt.feedbackPrompt ?? ""); + return { + ...attempt, + inputPrompt: full ? inputPrompt : undefined, + inputPromptPreview: safePreview(String(attempt.inputPromptPreview || inputPrompt), full ? 3000 : 1200), + inputPromptChars: Number(attempt.inputPromptChars ?? inputPrompt.length), + inputPromptLines: Number(attempt.inputPromptLines ?? promptLineCount(inputPrompt)), + finalResponse: full ? finalResponse : safePreview(finalResponse, 1200), + finalResponsePreview: safePreview(String(attempt.finalResponsePreview || finalResponse), full ? 3000 : 1200), + finalResponseChars: Number(attempt.finalResponseChars ?? finalResponse.length), + feedbackPrompt: full ? feedbackPrompt : undefined, + feedbackPromptPreview: safePreview(String(attempt.feedbackPromptPreview || feedbackPrompt), full ? 3000 : 1200), + feedbackPromptChars: Number(attempt.feedbackPromptChars ?? feedbackPrompt.length), + feedbackPromptLines: Number(attempt.feedbackPromptLines ?? promptLineCount(feedbackPrompt)), + stderrTail: full ? attempt.stderrTail : safePreview(attempt.stderrTail, 1200), + } as unknown as 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: ctx().queuedTaskPromptEditable(task), + ...queuedStatusPayload(task), + referenceTaskIds: task.referenceTaskIds, + referenceInjection: task.referenceInjection, + finalResponse: full ? task.finalResponse : safePreview(task.finalResponse, 5000), + terminalUnread: terminalTaskUnread(task), + attempts: task.attempts.map((attempt) => attemptForResponse(attempt, full)), + output: outputForResponse(task, includeRaw), + events: includeRaw ? task.events : task.events.slice(-120), + transcript: full ? fullTranscript(task) : buildTaskTranscript(task, 120, 0), + } as unknown as JsonValue; +} + +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: ctx().queueIdOf(task), + queueEnteredAt: ctx().taskQueueEnteredAt(task), + prompt: task.prompt, + basePrompt: task.basePrompt, + displayPrompt, + promptChars: task.prompt.length, + basePromptChars: task.basePrompt.length, + displayPromptChars: displayPrompt.length, + promptEditable: ctx().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, + ...queuedStatusPayload(task), + createdAt: task.createdAt, + updatedAt: task.updatedAt, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + readAt: task.readAt, + currentAttempt: task.currentAttempt, + currentMode: task.currentMode, + judgeFailCount: task.judgeFailCount, + judgeFailRetryLimit, + codexThreadId: task.codexThreadId, + activeTurnId: task.activeTurnId, + finalResponse: task.finalResponse, + lastError: task.lastError, + lastJudge: task.lastJudge, + promptHistory: task.promptHistory, + attempts: task.attempts, + cancelRequested: task.cancelRequested, + terminalUnread: terminalTaskUnread(task), + nextMode: task.nextMode, + outputCount: fullOutput.length, + retainedOutputCount: task.output.length, + eventCount: task.events.length, + transcriptCount: null, + transcriptMaxSeq: lastOutputSeq, + timing: taskTiming(task), + transcript: [], + output: [], + events: [], + } as unknown as 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: ctx().queueIdOf(task), + queueEnteredAt: ctx().taskQueueEnteredAt(task), + prompt: prefixPreview(task.prompt, 900), + basePrompt: prefixPreview(task.basePrompt, 900), + displayPrompt: prefixPreview(displayPrompt, 900), + promptChars: task.prompt.length, + basePromptChars: task.basePrompt.length, + displayPromptChars: displayPrompt.length, + promptEditable: ctx().queuedTaskPromptEditable(task), + finalResponseChars: task.finalResponse.length, + stepCount, + llmStepCount: stepCount, + summaryOnly: true, + referenceTaskIds: task.referenceTaskIds, + referenceInjection: task.referenceInjection === null ? null : { + injectedAt: task.referenceInjection.injectedAt, + itemCount: task.referenceInjection.itemCount, + directReferenceTaskIds: task.referenceInjection.directReferenceTaskIds, + 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, + ...queuedStatusPayload(task), + createdAt: task.createdAt, + updatedAt: task.updatedAt, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + readAt: task.readAt, + currentAttempt: task.currentAttempt, + currentMode: task.currentMode, + judgeFailCount: task.judgeFailCount, + judgeFailRetryLimit, + codexThreadId: task.codexThreadId, + activeTurnId: task.activeTurnId, + finalResponse: prefixPreview(task.finalResponse, 1200), + lastError: task.lastError, + lastJudge: task.lastJudge, + promptHistory: task.promptHistory.slice(-8), + attempts: task.attempts.slice(-3).map((attempt) => attemptForResponse(attempt, false)), + cancelRequested: task.cancelRequested, + terminalUnread: terminalTaskUnread(task), + nextMode: task.nextMode, + outputCount: task.output.length, + eventCount: task.events.length, + transcriptCount: null, + transcriptMaxSeq: lastOutputSeq, + timing: taskTiming(task), + transcript: [], + output: [], + events: [], + } as unknown as JsonValue; +} + +function lastAssistantMessage(task: QueueTask, transcript = fullTranscript(task)): JsonValue { + const assistantLine = transcript.slice().reverse().find((line) => line.kind === "message" && line.title === "Assistant message"); + const text = task.finalResponse.trim().length > 0 ? task.finalResponse.trim() : String(assistantLine?.bodyPreview ?? "").trim(); + return { + text, + at: assistantLine?.at ?? task.finishedAt ?? task.updatedAt, + seq: assistantLine?.seq ?? null, + source: task.finalResponse.trim().length > 0 ? "finalResponse" : assistantLine !== undefined ? "transcript" : "none", + }; +} + +function parseToolLimit(url: URL): number { + const value = Number(url.searchParams.get("toolLimit") ?? 160); + return Number.isInteger(value) && value > 0 ? Math.min(500, value) : 160; +} + +function taskToolSummaryForLimit(task: QueueTask, limit: number, transcript = fullTranscript(task)): JsonValue { + const allTools = transcript.filter((line) => line.kind === "ran" || line.kind === "explored" || line.kind === "edited"); + const boundedLimit = Math.max(1, Math.min(500, Math.floor(limit))); + const start = Math.max(0, allTools.length - boundedLimit); + return { + count: allTools.length, + returned: allTools.length - start, + limit: boundedLimit, + truncated: start > 0, + items: allTools.slice(start).map((line) => ({ + seq: line.seq, + at: line.at, + kind: line.kind, + title: line.title, + status: line.status ?? null, + commandPreview: line.commandPreview ?? "", + commandOmittedLines: line.commandOmittedLines ?? 0, + outputPreview: line.bodyPreview ?? "", + outputOmittedLines: line.bodyOmittedLines ?? 0, + rawSeqs: line.rawSeqs, + })), + } as unknown as JsonValue; +} + +function taskToolSummary(task: QueueTask, url: URL, transcript = fullTranscript(task)): JsonValue { + return taskToolSummaryForLimit(task, parseToolLimit(url), transcript); +} + +function uniqueStrings(values: string[], limit = 20): string[] { + const result: string[] = []; + for (const value of values) { + const normalized = value.trim(); + if (normalized.length === 0 || result.includes(normalized)) continue; + result.push(normalized); + if (result.length >= limit) break; + } + return result; +} + +function cleanTracePath(value: string): string { + return value + .replace(/^['"`([{<]+/u, "") + .replace(/['"`)\]}>.,;:]+$/u, "") + .replace(/:\d+(?::\d+)?$/u, "") + .replace(/^[ab]\//u, "") + .trim(); +} + +function extractTracePaths(text: string): string[] { + const source = String(text || ""); + const matches = source.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) || []; + return uniqueStrings(matches.map(cleanTracePath).filter((path) => path.length >= 2 && !path.includes("...") && !/^(http|https|status|method)$/iu.test(path)), 40); +} + +function parseEditedFilesFromText(text: string): string[] { + const files: string[] = []; + const addFile = (path: string): void => { + const cleanPath = cleanTracePath(path); + if (cleanPath.length === 0 || cleanPath === "/dev/null" || files.includes(cleanPath)) return; + files.push(cleanPath); + }; + for (const line of text.split(/\r?\n/u)) { + const statusMatch = /^([AMDRCU?]{1,2})\s+(.+)$/u.exec(line); + if (statusMatch) { + addFile(statusMatch[2] || ""); + continue; + } + const patchMatch = /^\*\*\*\s+(?:Add|Update|Delete)\s+File:\s+(.+)$/u.exec(line); + if (patchMatch) { + addFile(patchMatch[1] || ""); + continue; + } + const moveMatch = /^\*\*\*\s+Move to:\s+(.+)$/u.exec(line); + if (moveMatch) { + addFile(moveMatch[1] || ""); + continue; + } + const diffMatch = /^diff --git a\/(.+?) b\/(.+)$/u.exec(line); + if (diffMatch) { + addFile(diffMatch[2] || diffMatch[1] || ""); + continue; + } + const plusMatch = /^\+\+\+ b\/(.+)$/u.exec(line); + if (plusMatch) addFile(plusMatch[1] || ""); + } + return files.length > 0 ? files : extractTracePaths(text); +} + +function transcriptPreviewLines(text: string, maxLines: number, maxChars: number): string[] { + const preview = linePreview(text, maxLines, maxChars).text; + return preview.length === 0 ? [] : preview.split(/\r?\n/u).slice(0, maxLines); +} + +function transcriptLineSummaryLines(line: TranscriptLine): string[] { + const lines: string[] = []; + const command = String(line.commandPreview || "").trim(); + if (command.length > 0) { + for (const item of transcriptPreviewLines(command, 2, 420)) lines.push(`$ ${item}`); + } + const body = String(line.bodyPreview || "").trim(); + const remaining = Math.max(0, 4 - lines.length); + if (body.length > 0 && remaining > 0) { + for (const item of transcriptPreviewLines(body, remaining, 700)) lines.push(item); + } + if (lines.length === 0) lines.push(line.status ? `${line.title} (${line.status})` : line.title); + 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(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 = toolStepCountsFromTranscript(transcript); + return { + durationMs: timing.durationMs ?? timing.totalElapsedMs ?? null, + totalElapsedMs: timing.totalElapsedMs ?? null, + queueWaitMs: timing.queueWaitMs ?? null, + toolCallCount: counts.toolCallCount, + readCount: counts.readCount, + editCount: counts.editCount, + runCount: counts.runCount, + editedFiles, + commands, + stepCount: counts.toolCallCount, + llmStepCount: counts.toolCallCount, + traceLineCount: transcript.filter((line) => line.title !== "Submitted prompt").length, + transcriptMaxSeq: transcript.at(-1)?.seq ?? 0, + outputCount, + retainedOutputCount, + } as unknown as JsonValue; +} + +function taskExecutionSummary(task: QueueTask, transcript = cachedPreviewTranscript(task)): JsonValue { + return executionSummaryFromTranscript(task, taskExecutionTranscript(task, transcript), taskTiming(task) as Record, undefined, undefined, task.currentAttempt || task.attempts.length); +} + +function parseAttemptIndex(text: string): number | null { + const match = /\battempt\s+(\d+)\s*\/\s*\d+/iu.exec(text); + if (match === null) return null; + const value = Number(match[1]); + return Number.isInteger(value) && value > 0 ? value : null; +} + +function traceAttemptIndexFromLine(line: TranscriptLine): number | null { + return parseAttemptIndex(`${line.bodyPreview ?? ""}\n${line.commandPreview ?? ""}\n${line.title}`); +} + +function parseJudgeLine(text: string): JudgeResult | null { + const match = /\bjudge=(complete|retry|fail)\s+confidence=([0-9.]+)\s+source=([A-Za-z0-9_-]+):\s*([\s\S]*)$/u.exec(text.trim()); + if (match === null) return null; + const confidence = Number(match[2]); + const source = match[3] === "minimax" ? "minimax" : "fallback"; + return { + decision: match[1] as JudgeDecision, + confidence: Number.isFinite(confidence) ? confidence : 0, + source, + reason: (match[4] ?? "").trim(), + }; +} + +function judgeFromAttemptLines(lines: TranscriptLine[]): { judge: JudgeResult; at: string | null; seq: number | null } | null { + for (const line of lines.slice().reverse()) { + if (line.title !== "Judge result" && line.status !== "judge") continue; + const judge = parseJudgeLine(String(line.bodyPreview || "")); + if (judge === null) continue; + return { judge, at: line.at || null, seq: Number.isFinite(Number(line.seq)) ? Number(line.seq) : null }; + } + return null; +} + +function attemptFeedbackPromptRecord(task: QueueTask, attemptIndex: number, attempt: AttemptSummary | null, judge: JudgeResult | null = attempt?.judge ?? null): FeedbackPromptRecord | null { + const directPrompt = String(attempt?.feedbackPrompt ?? "").trimEnd(); + if (directPrompt.length > 0) { + const snapshot = promptSnapshot(directPrompt, 1600); + return { + text: snapshot.text, + preview: String(attempt?.feedbackPromptPreview || snapshot.preview), + chars: Number(attempt?.feedbackPromptChars ?? snapshot.chars), + lines: Number(attempt?.feedbackPromptLines ?? snapshot.lines), + source: String(attempt?.feedbackPromptSource || "judge-feedback"), + forAttempt: Number.isFinite(Number(attempt?.feedbackPromptForAttempt)) ? Number(attempt?.feedbackPromptForAttempt) : attemptIndex + 1, + truncated: snapshot.truncated, + }; + } + + const hasPendingRetry = task.status === "retry_wait" || task.status === "queued"; + if (attemptIndex === task.attempts.length && hasPendingRetry && task.nextMode === "retry" && task.nextPrompt !== null && task.nextPrompt.trim().length > 0) { + const snapshot = promptSnapshot(task.nextPrompt, 1600); + return { ...snapshot, source: "pending-retry", forAttempt: attemptIndex + 1 }; + } + + const nextAttempt = task.attempts.find((item) => Number(item.index) === attemptIndex + 1) ?? task.attempts[attemptIndex] ?? null; + const nextInput = String(nextAttempt?.inputPrompt ?? "").trimEnd(); + if (nextInput.length > 0) { + const snapshot = promptSnapshot(nextInput, 1600); + return { ...snapshot, source: "attempt-input", forAttempt: attemptIndex + 1 }; + } + + if (judge?.decision === "retry") { + const generated = retryPrompt(task, judge); + const snapshot = promptSnapshot(generated, 1600); + return { ...snapshot, source: judge.continuePrompt?.trim() ? "judge-continue-prompt" : "judge-retry-generated", forAttempt: attemptIndex + 1 }; + } + + return null; +} + +function attemptTimingSummary(attempt: AttemptSummary | null, lines: TranscriptLine[]): Record { + const startedAt = attempt?.startedAt || lines[0]?.at || null; + const finishedAt = attempt?.finishedAt || lines.at(-1)?.at || null; + const startedMs = timestampMs(startedAt); + const finishedMs = timestampMs(finishedAt); + const durationMs = nonNegativeElapsed(startedMs, finishedMs); + return { + durationMs, + totalElapsedMs: durationMs, + queueWaitMs: null, + effectiveEndAt: finishedAt, + }; +} + +interface TraceAttemptWindow { + index: number; + attempt: AttemptSummary | null; + startSeq: number | null; + endSeq: number | null; + lines: TranscriptLine[]; + synthetic?: boolean; + 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 })) + .filter((item): item is { line: TranscriptLine; position: number; index: number } => item.index !== null) + .sort((left, right) => Number(left.line.seq) - Number(right.line.seq)); + const maxStartIndex = starts.reduce((max, item) => Math.max(max, item.index), 0); + const maxIndex = Math.max(task.attempts.length, task.currentAttempt || 0, maxStartIndex); + const windows: TraceAttemptWindow[] = []; + for (let index = 1; index <= maxIndex; index += 1) { + const attempt = task.attempts.find((item) => Number(item.index) === index) ?? task.attempts[index - 1] ?? null; + const start = starts.find((item) => item.index === index) ?? starts[index - 1] ?? null; + const nextStart = starts.find((item) => item.index > index) ?? null; + const explicitStartSeq = Number((attempt as AttemptSummary | null)?.outputStartSeq ?? NaN); + const explicitEndSeq = Number((attempt as AttemptSummary | null)?.outputEndSeq ?? NaN); + const startSeq = Number.isFinite(explicitStartSeq) ? explicitStartSeq : start?.line.seq ?? null; + const hasExplicitEndSeq = Number.isFinite(explicitEndSeq); + const endSeq = hasExplicitEndSeq ? explicitEndSeq : nextStart !== null ? nextStart.line.seq : null; + let lines = startSeq === null + ? [] + : transcript.filter((line) => Number(line.seq) >= startSeq && (endSeq === null || (hasExplicitEndSeq ? Number(line.seq) <= endSeq : Number(line.seq) < endSeq))); + if (lines.length === 0 && attempt !== null) { + const startedMs = timestampMs(attempt.startedAt); + const finishedMs = timestampMs(attempt.finishedAt); + lines = transcript.filter((line) => { + const lineMs = timestampMs(line.at); + return lineMs !== null + && (startedMs === null || lineMs >= startedMs) + && (finishedMs === null || lineMs <= finishedMs); + }); + } + windows.push({ index, attempt, startSeq, endSeq, lines }); + } + const coveredSeqs = new Set(); + for (const window of windows) { + for (const line of window.lines) coveredSeqs.add(Number(line.seq)); + } + const orphanedGroups: TranscriptLine[][] = []; + let group: TranscriptLine[] = []; + const flushGroup = (): void => { + if (group.length > 0 && executionLinesForAttempt(group).length > 0) orphanedGroups.push(group); + group = []; + }; + for (const line of transcript) { + const seq = Number(line.seq); + if (line.title === "Submitted prompt" || coveredSeqs.has(seq)) { + flushGroup(); + continue; + } + group.push(line); + } + flushGroup(); + + 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({ + index: syntheticIndex, + attempt: null, + startSeq: Number.isFinite(startSeq) ? startSeq : null, + endSeq: Number.isFinite(endSeq) ? endSeq : null, + lines, + synthetic: true, + label: hasRecovery + ? hasSteer ? "Recovered thread execution with steer prompt" : "Recovered thread execution" + : hasSteer ? "Recovered thread execution with steer prompt" : "System events", + }); + syntheticIndex -= 1; + } + + return windows.sort((left, right) => Number(left.lines[0]?.seq ?? left.startSeq ?? 0) - Number(right.lines[0]?.seq ?? right.startSeq ?? 0)); +} + +function executionLinesForAttempt(lines: TranscriptLine[]): TranscriptLine[] { + return lines.filter((line) => line.title !== "Submitted prompt" && line.title !== "Attempt started" && line.title !== "Judge result"); +} + +function taskTraceAttemptSummaries(task: QueueTask, transcript: TranscriptLine[]): JsonValue[] { + const windows = traceAttemptWindows(task, transcript); + return windows.map((window) => { + const attempt = window.attempt; + const parsedJudge = judgeFromAttemptLines(window.lines); + const storedJudge = attempt?.judge ?? null; + const synthetic = window.synthetic === true; + const judge = synthetic ? null : storedJudge ?? parsedJudge?.judge ?? (window.index === task.attempts.length && task.lastJudge !== null ? task.lastJudge : null); + 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 { + ...(attempt ?? {}), + index: attempt?.index ?? window.index, + synthetic, + label: window.label ?? null, + mode: attempt?.mode ?? (window.index <= 1 ? "initial" : "retry"), + startedAt: attempt?.startedAt ?? window.lines[0]?.at ?? task.startedAt, + finishedAt: attempt?.finishedAt ?? null, + terminalStatus: attempt?.terminalStatus ?? null, + transportClosedBeforeTerminal: attempt?.transportClosedBeforeTerminal ?? false, + appServerExitCode: attempt?.appServerExitCode ?? null, + appServerSignal: attempt?.appServerSignal ?? null, + error: attempt?.error ?? null, + stderrTail: attempt?.stderrTail ?? "", + startSeq: window.startSeq, + endSeq: window.endSeq, + inputPrompt: undefined, + inputPromptPreview: inputPrompt.preview, + inputPromptChars: Number(attempt?.inputPromptChars ?? inputPrompt.chars), + inputPromptLines: Number(attempt?.inputPromptLines ?? inputPrompt.lines), + finalResponse, + finalResponsePreview: attempt?.finalResponsePreview ?? safePreview(finalResponse, 3000), + finalResponseChars, + finalResponseTruncated: finalResponse.length < finalResponseChars, + judge, + judgeAt: attempt?.judgeAt ?? parsedJudge?.at ?? null, + judgeSeq: attempt?.judgeSeq ?? parsedJudge?.seq ?? null, + feedbackPrompt: undefined, + feedbackPromptPreview: feedbackPrompt?.preview ?? "", + feedbackPromptChars: feedbackPrompt?.chars ?? 0, + feedbackPromptLines: feedbackPrompt?.lines ?? 0, + feedbackPromptSource: feedbackPrompt?.source ?? null, + feedbackPromptForAttempt: feedbackPrompt?.forAttempt ?? null, + feedbackPromptTruncated: feedbackPrompt?.truncated ?? false, + 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 = stripCodeQueueEnvironmentHint(prompt); + const trimmed = withoutEnvironment.trimStart(); + if (!trimmed.startsWith(resolvedReferenceContextTitle)) { + return { reference: "", userPrompt: userPromptForDisplay(prompt) }; + } + const offset = withoutEnvironment.length - trimmed.length; + const index = withoutEnvironment.lastIndexOf(currentTaskPromptMarker); + if (index < offset) return { reference: "", userPrompt: userPromptForDisplay(prompt) }; + return { + reference: withoutEnvironment.slice(offset, index).trimEnd(), + userPrompt: withoutEnvironment.slice(index + currentTaskPromptMarker.length).trimStart(), + }; +} + +function taskTracePromptSummary(task: QueueTask): JsonValue { + const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt); + const parts = resolvedReferencePromptParts(task.prompt); + return { + basePrompt: displayPrompt, + basePromptChars: displayPrompt.length, + basePromptLines: promptLineCount(displayPrompt), + promptChars: task.prompt.length, + promptLines: promptLineCount(task.prompt), + referencePromptChars: parts.reference.length, + referencePromptLines: promptLineCount(parts.reference), + hasReferenceInjection: parts.reference.length > 0 || task.referenceInjection !== null, + referenceTaskIds: task.referenceTaskIds, + referenceInjectionSummary: task.referenceInjection === null ? null : { + injectedAt: task.referenceInjection.injectedAt, + itemCount: task.referenceInjection.itemCount, + directReferenceTaskIds: task.referenceInjection.directReferenceTaskIds, + maxRounds: task.referenceInjection.maxRounds, + truncated: task.referenceInjection.truncated, + }, + } as unknown as 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: ctx().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, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + updatedAt: task.updatedAt, + currentAttempt: task.currentAttempt, + maxAttempts: task.maxAttempts, + stepCount, + llmStepCount: stepCount, + promptEditable: ctx().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; +} + +function taskFeedbackPromptDetail(task: QueueTask, attemptIndex: number | null): FeedbackPromptRecord | null { + const index = attemptIndex ?? (task.attempts.length > 0 ? task.attempts.length : task.currentAttempt || 1); + if (!Number.isInteger(index) || index <= 0) return null; + const attempt = task.attempts.find((item) => Number(item.index) === index) ?? task.attempts[index - 1] ?? null; + let judge = attempt?.judge ?? null; + if (judge === null) { + const window = traceAttemptWindows(task, cachedPreviewTranscript(task)).find((item) => item.index === index) ?? null; + judge = judgeFromAttemptLines(window?.lines ?? [])?.judge ?? null; + } + return attemptFeedbackPromptRecord(task, index, attempt, judge); +} + +function taskPromptDetailResponse(task: QueueTask, url: URL): Response { + const part = String(url.searchParams.get("part") || "full"); + const parts = resolvedReferencePromptParts(task.prompt); + const basePrompt = task.basePrompt || userPromptForDisplay(task.prompt); + if (part === "feedback" || part === "judge-feedback") { + const attemptIndex = ctx().parseSeqParam(url, "attempt", null); + const detail = taskFeedbackPromptDetail(task, attemptIndex); + const text = detail?.text ?? ""; + return ctx().jsonResponse({ + ok: true, + taskId: task.id, + queueId: ctx().queueIdOf(task), + status: task.status, + updatedAt: task.updatedAt, + part: "feedback", + attempt: attemptIndex, + forAttempt: detail?.forAttempt ?? null, + source: detail?.source ?? null, + text, + preview: detail?.preview ?? "", + chars: detail?.chars ?? text.length, + lines: detail?.lines ?? promptLineCount(text), + truncated: detail?.truncated ?? false, + }); + } + const text = part === "base" + ? basePrompt + : part === "reference" + ? parts.reference + : task.prompt; + return ctx().jsonResponse({ + ok: true, + taskId: task.id, + queueId: ctx().queueIdOf(task), + status: task.status, + updatedAt: task.updatedAt, + part, + text, + chars: text.length, + lines: promptLineCount(text), + promptChars: task.prompt.length, + basePromptChars: basePrompt.length, + referencePromptChars: parts.reference.length, + }); +} + +function taskTraceStepsResponse(task: QueueTask, url: URL): Response { + const limit = ctx().parseLimit(url); + const attemptIndex = ctx().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); + const transcript = sourceTranscript.filter((line) => line.title !== "Submitted prompt"); + const page = ctx().pageBySeq(transcript, url, limit); + return ctx().jsonResponse({ + ok: true, + taskId: task.id, + queueId: ctx().queueIdOf(task), + status: task.status, + updatedAt: task.updatedAt, + agentPort, + agentPortInfo: codeAgentPortInfo(agentPort), + attempt: attemptIndex, + steps: page.chunk.map((line) => ({ + seq: line.seq, + at: line.at, + kind: line.kind, + title: line.title, + status: line.status ?? null, + durationMs: line.durationMs ?? null, + rawSeqs: line.rawSeqs, + summaryLines: transcriptLineSummaryLines(line), + hasDetail: true, + })), + mode: page.mode, + afterSeq: page.afterSeq, + nextAfterSeq: page.nextAfterSeq, + beforeSeq: page.beforeSeq, + previousBeforeSeq: page.previousBeforeSeq, + hasMore: page.hasMore, + hasBefore: page.hasBefore, + total: transcript.length, + maxSeq: transcript.at(-1)?.seq ?? 0, + }); +} + +function taskTraceStepDetailResponse(task: QueueTask, url: URL): Response { + const seq = Number(url.searchParams.get("seq")); + if (!Number.isFinite(seq)) return ctx().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 ctx().jsonResponse({ ok: false, error: "trace step not found", seq }, 404); + return ctx().jsonResponse({ + ok: true, + taskId: task.id, + queueId: ctx().queueIdOf(task), + status: task.status, + updatedAt: task.updatedAt, + agentPort, + agentPortInfo: codeAgentPortInfo(agentPort), + seq, + line, + }); +} + +function taskSummaryResponse(task: QueueTask, url: URL): JsonValue { + const transcript = fullTranscript(task); + const fullOutput = taskFullOutput(task); + return { + id: task.id, + queueId: ctx().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, + currentAttempt: task.currentAttempt, + currentMode: task.currentMode, + judgeFailCount: task.judgeFailCount, + judgeFailRetryLimit, + codexThreadId: task.codexThreadId, + activeTurnId: task.activeTurnId, + createdAt: task.createdAt, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + updatedAt: task.updatedAt, + timing: taskTiming(task), + initialPrompt: task.prompt, + basePrompt: task.basePrompt, + prompt: task.prompt, + promptEditable: ctx().queuedTaskPromptEditable(task), + referenceTaskIds: task.referenceTaskIds, + referenceInjection: task.referenceInjection, + lastAssistantMessage: lastAssistantMessage(task, transcript), + toolSummary: taskToolSummary(task, url, transcript), + attempts: task.attempts, + lastJudge: task.lastJudge, + lastError: task.lastError, + cancelRequested: task.cancelRequested, + transcriptCount: transcript.length, + transcriptMaxSeq: transcript.at(-1)?.seq ?? 0, + outputCount: fullOutput.length, + retainedOutputCount: task.output.length, + outputMaxSeq: fullOutput.at(-1)?.seq ?? 0, + eventCount: task.events.length, + cliHint: `bun scripts/cli.ts codex task ${task.id}`, + traceHint: `bun scripts/cli.ts codex task ${task.id} --trace --tail --limit 80`, + rawOutputHint: `bun scripts/cli.ts codex output ${task.id} --tail --limit 20`, + } as unknown as JsonValue; +} + + +export { + buildCompactTaskTranscript, + buildTaskTranscript, + cachedPreviewTranscript, + formatCommandOutput, + fullTranscript, + lastAssistantMessage, + promptLineCount, + promptSnapshot, + recordNumberField, + recordStringField, + safePreview, + prefixPreview, + setAttemptFeedbackPrompt, + setAttemptInputPrompt, + statsDaysFromUrl, + taskExecutionSummary, + taskLlmStepCount, + taskForCompactMetaResponse, + taskForMetaResponse, + taskForResponse, + taskStatisticsSummary, + taskSummaryResponse, + taskTiming, + timestampMs, + nonNegativeElapsed, + taskToolSummary, + taskTraceStepDetailResponse, + taskTraceStepsResponse, + taskTraceSummaryResponse, + taskPromptDetailResponse, + transcriptLineSummaryLines, +}; diff --git a/src/components/microservices/code-queue/src/types.ts b/src/components/microservices/code-queue/src/types.ts new file mode 100644 index 00000000..49f95e49 --- /dev/null +++ b/src/components/microservices/code-queue/src/types.ts @@ -0,0 +1,411 @@ +// 重构前 index.ts 只读参考:commit 6a04144d3f5103014f75b637d7e6bc2f45bf007f,blob 56e590c1a6b5ca7ad128bf2c992f60e46c355a58;可用 `git show 6a04144d3f5103014f75b637d7e6bc2f45bf007f:src/components/microservices/code-queue/src/index.ts` 查看。 + +export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; + +export type TaskStatus = "queued" | "running" | "judging" | "retry_wait" | "succeeded" | "failed" | "canceled"; + +export interface QueuedStatusReason { + code: string; + label: string; + message: string; + blockerTaskId?: string | null; + blockerQueueId?: string | null; + waitPosition?: number | null; + activeRunSlotCount?: number | null; + maxActiveQueues?: number | null; + memory?: JsonValue; +} + +export type RunMode = "initial" | "retry"; + +export type JudgeDecision = "complete" | "retry" | "fail"; + +export type OutputChannel = "system" | "user" | "assistant" | "reasoning" | "command" | "diff" | "tool" | "error"; + +export type TerminalStatus = "completed" | "interrupted" | "failed" | null; + +export type TranscriptKind = "ran" | "explored" | "edited" | "plan" | "message" | "system" | "error"; + +export interface RuntimeConfig { + host: string; + port: number; + dataDir: string; + 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; + modelReasoningEfforts: Record; + 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; + notifyClaudeQqEnabled: boolean; + notifyClaudeQqBaseUrl: string; + notifyClaudeQqTargetType: "private" | "group"; + notifyClaudeQqUserId: string; + notifyClaudeQqGroupId: string; + notifyClaudeQqMaxResponseChars: number; + notifyClaudeQqTimeoutMs: number; + notifyClaudeQqSendAttempts: number; + notifyClaudeQqRetryIntervalMs: number; + notifyClaudeQqMaxOutboxItems: number; + maxInMemoryOutputRecords: number; + maxInMemoryEventRecords: number; + maxActiveQueues: number; + devContainerMasterHost: string; + devContainerDefaultProviderId: string; + devContainerImage: string; + devContainerWorkdir: string; +} + +export interface QueueTaskRequest { + prompt: string; + queueId?: string; + providerId?: string; + cwd?: string; + model?: string; + reasoningEffort?: string; + maxAttempts?: number; + referenceTaskIds?: string[]; + basePrompt?: string; + referenceInjection?: ReferenceInjectionRecord | null; +} + +export interface LiveOutput { + seq: number; + at: string; + channel: OutputChannel; + text: string; + method?: string; + itemId?: string; +} + +export interface ArchivedLiveOutput extends LiveOutput { + op?: "set" | "append"; +} + +export interface TranscriptLine { + seq: number; + at: string; + durationMs?: number; + kind: TranscriptKind; + title: string; + status?: string; + commandPreview?: string; + commandOmittedLines?: number; + bodyPreview?: string; + bodyOmittedLines?: number; + stdoutPreview?: string; + stdoutOmittedLines?: number; + stderrPreview?: string; + stderrOmittedLines?: number; + rawSeqs: number[]; + fullPrompt?: string; + fullPromptLines?: number; + fullPromptChars?: number; + foldedReferencePrompt?: boolean; +} + +export interface CodexEventSummary { + at: string; + method: string; + itemType?: string; + status?: string; + message?: string; + textPreview?: string; +} + +export interface AttemptSummary { + index: number; + mode: RunMode; + startedAt: string; + finishedAt: string; + terminalStatus: TerminalStatus; + transportClosedBeforeTerminal: boolean; + appServerExitCode: number | null; + appServerSignal: string | null; + error: string | null; + inputPrompt?: string; + inputPromptPreview?: string; + inputPromptChars?: number; + inputPromptLines?: number; + finalResponse?: string; + finalResponsePreview: string; + finalResponseChars?: number; + judge?: JudgeResult | null; + judgeAt?: string | null; + judgeSeq?: number | null; + feedbackPrompt?: string; + feedbackPromptPreview?: string; + feedbackPromptChars?: number; + feedbackPromptLines?: number; + feedbackPromptSource?: string; + feedbackPromptForAttempt?: number | null; + stderrTail: string; + outputStartSeq?: number | null; + outputEndSeq?: number | null; + errorCount?: number; +} + +export interface JudgeResult { + decision: JudgeDecision; + confidence: number; + reason: string; + continuePrompt?: string; + source: "minimax" | "fallback"; + raw?: JsonValue; +} + +export interface FeedbackPromptRecord { + text: string; + preview: string; + chars: number; + lines: number; + source: string; + forAttempt: number | null; + truncated: boolean; +} + +export interface ParsedJudgeJson { + value: Record; + source: string; +} + +export interface MiniMaxJudgeResponse { + rawText: string; + content: string; +} + +export interface ReferenceInjectionSummaryItem { + round: number; + roundIndex: number; + taskId: string; + viaTaskId: string | null; + status: TaskStatus; + providerId: string; + model: string; + cwd: string; + createdAt: string; + updatedAt: string; + promptChars: number; + finalResponseChars: number; + finalResponseAt: string | null; + finalResponseSource: string; + referenceTaskIds: string[]; + cliHint: string; +} + +export interface ReferenceInjectionRecord { + version: 2; + injectedAt: string; + basePrompt: string; + directReferenceTaskIds: string[]; + maxRounds: number | null; + truncated: boolean; + itemCount: number; + items: ReferenceInjectionSummaryItem[]; +} + +export interface PromptHistoryItem { + seq: number; + at: string; + method: "turn/steer"; + text: string; +} + +export interface QueueTask { + id: string; + queueId: string; + queueEnteredAt: string; + prompt: string; + basePrompt: string; + referenceTaskIds: string[]; + referenceInjection: ReferenceInjectionRecord | null; + providerId: string; + cwd: string; + model: string; + reasoningEffort: string | null; + maxAttempts: number; + status: TaskStatus; + createdAt: string; + updatedAt: string; + startedAt: string | null; + finishedAt: string | null; + readAt: string | null; + currentAttempt: number; + currentMode: RunMode | null; + codexThreadId: string | null; + activeTurnId: string | null; + finalResponse: string; + lastError: string | null; + lastJudge: JudgeResult | null; + judgeFailCount: number; + promptHistory: PromptHistoryItem[]; + output: LiveOutput[]; + events: CodexEventSummary[]; + attempts: AttemptSummary[]; + cancelRequested: boolean; + nextPrompt: string | null; + nextMode: RunMode | null; +} + +export interface PersistedState { + version: 1; + updatedAt: string; + nextSeq: number; + queues: QueueRecord[]; + tasks: QueueTask[]; +} + +export interface ClaudeQqNotificationItem { + id: string; + kind: string; + dedupKey: string; + target: string; + message: string; + createdAt: string; + updatedAt: string; + attempts: number; + nextAttemptAt: string; + lastError: string | null; + sentAt: string | null; +} + +export interface ClaudeQqNotificationOutboxState { + version: 1; + updatedAt: string; + items: ClaudeQqNotificationItem[]; +} + +export interface ClaudeQqNotificationRow { + id: string; + kind: string; + dedup_key: string; + target: string; + message: string; + created_at: Date | string; + updated_at: Date | string; + attempts: number; + next_attempt_at: Date | string; + last_error: string | null; + sent_at: Date | string | null; +} + +export interface QueueRecord { + id: string; + name: string; + createdAt: string; + updatedAt: string; +} + +export interface AppServerExit { + code: number | null; + signal: string | null; + stderrTail: string; +} + +export interface CodexRunResult { + threadId: string | null; + turnId: string | null; + finalResponse: string; + terminalStatus: TerminalStatus; + terminalError: string | null; + transportClosedBeforeTerminal: boolean; + appServerExit: AppServerExit; + events: CodexEventSummary[]; +} + +export interface SessionFileChange { + callId: string; + at: string; + name: string; + input: string; + output: string; +} + +export interface SessionCommandOutput { + callId: string; + at: string; + stdout: string; + stderr: string; + output: string; + exitCode: number | null; +} + +export interface JudgeProbeCase { + id: string; + prompt: string; + finalResponse: string; + expected: JudgeDecision; + expectedContinuePromptIncludes?: string[]; + expectedContinuePromptExcludes?: string[]; + expectedContinuePromptMaxChars?: number; + expectedContinuePromptMaxLines?: number; + terminalStatus: TerminalStatus; + cancelRequested?: boolean; + transportClosedBeforeTerminal?: boolean; + terminalError?: string | null; + stderrTail?: string; + outputs?: Array<{ channel: OutputChannel; text: string; method?: string }>; + events?: CodexEventSummary[]; +} + +export 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; +} + +export interface DevContainerCommandLog { + name: string; + providerId: string | null; + exitCode: number | null; + signal: NodeJS.Signals | null; + durationMs: number; + stdout: string; + stderr: string; +} + +export interface CgroupMemoryUsage { + currentBytes: number; + inactiveFileBytes: number; + workingSetBytes: number; + swapCurrentBytes: number; + swapMaxBytes: number | null; +}