fix: route claudeqq notifications through k3s

This commit is contained in:
Codex
2026-05-16 18:06:06 +00:00
parent 919bdb6b4b
commit 611f287c12
15 changed files with 414 additions and 65 deletions
+2 -2
View File
@@ -103,7 +103,7 @@
## T23 D601 Code Queue User Service
阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md``scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts microservice list`,确认 `code-queue` 显示为 `providerId=D601``public=false``frontendOnly=true`、仓库 URL `https://github.com/pikasTech/unidesk`、k3s/k8s `k3s://unidesk/code-queue:4222` 逻辑服务映射、`deployment.mode=k3sctl-managed``runtime.orchestrator=k3sctl` 且无业务直连容器摘要;使用 `bun scripts/cli.ts codex deploy <已push的commitId>` 重建/启动 D601 Code Queue,确认命令立即返回异步 job id,`bun scripts/cli.ts job status <jobId> --tail-bytes 30000` 能看到 fetch/export、rsync、Docker build、native k3s provider egress proxy、有效 `rancher/mirrored-pause:3.6` sandbox 镜像导入、k3s image import、kubectl apply、部署 commit 戳记、rollout、legacy direct cleanup 和 health commit 验证进度,并确认 job 最终校验真实 Code Queue `/health` 返回的 `deploy.commit` 精确匹配本次 remote commit,不能由旧服务或旧 Pod 充数;同时确认主 server 根目录 `docker-compose.yml` 中不再存在 `code-queue` service,并通过 `bun scripts/cli.ts ssh D601 argv bash -lc 'systemctl is-active k3s && KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get nodes -o wide && sudo ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F docker.io/rancher/mirrored-pause:3.6 && ! docker ps --format "{{.Names}} {{.Image}}" | grep -E "[[:space:]]rancher/k3s:" && ! docker ps --format "{{.Names}}" | grep -Fx code-queue-backend'` 或等价检查证明 D601 k3s 是 WSL 原生 systemd 服务、native containerd 已有正确 pause sandbox 镜像、没有 active `rancher/k3s` 控制面容器且旧 direct Docker `code-queue-backend` 没有并行运行。运行 `bun scripts/cli.ts microservice health code-queue``bun scripts/cli.ts microservice proxy code-queue /api/dev-ready --raw``bun scripts/cli.ts microservice proxy code-queue '/api/tasks/overview?limit=5&transcriptLimit=1&compact=1&afterSeq=0&preferId='``bun scripts/cli.ts codex task <已有taskId>`,确认链路通过 backend-core、k3sctl-adapter、Kubernetes API service proxy 和 D601 active Code Queue Service,且 task id 查询返回初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,`queue.storage.primary=postgres``queue.storage.postgresReady=true``queue.devReady.missingTools=[]``queue.devReady.docker.versionOk=true``queue.devReady.docker.composeOk=true``queue.devReady.ssh.ready` 只在需要跨 Provider SSH/Windows-native 任务时作为强制项。在 D601 active Code Queue Pod 内验证主 PostgreSQL 端口映射可执行 `select 1`,主 OA Event Flow 端口映射 `/health` 可访问,本机 ClaudeQQ `http://host.docker.internal:3290/health` 可访问;这些映射不得成为任意公网入口。
阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md``scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts microservice list`,确认 `code-queue` 显示为 `providerId=D601``public=false``frontendOnly=true`、仓库 URL `https://github.com/pikasTech/unidesk`、k3s/k8s `k3s://unidesk/code-queue:4222` 逻辑服务映射、`deployment.mode=k3sctl-managed``runtime.orchestrator=k3sctl` 且无业务直连容器摘要;使用 `bun scripts/cli.ts codex deploy <已push的commitId>` 重建/启动 D601 Code Queue,确认命令立即返回异步 job id,`bun scripts/cli.ts job status <jobId> --tail-bytes 30000` 能看到 fetch/export、rsync、Docker build、native k3s provider egress proxy、有效 `rancher/mirrored-pause:3.6` sandbox 镜像导入、k3s image import、kubectl apply、部署 commit 戳记、rollout、legacy direct cleanup 和 health commit 验证进度,并确认 job 最终校验真实 Code Queue `/health` 返回的 `deploy.commit` 精确匹配本次 remote commit,不能由旧服务或旧 Pod 充数;同时确认主 server 根目录 `docker-compose.yml` 中不再存在 `code-queue` service,并通过 `bun scripts/cli.ts ssh D601 argv bash -lc 'systemctl is-active k3s && KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get nodes -o wide && sudo ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F docker.io/rancher/mirrored-pause:3.6 && ! docker ps --format "{{.Names}} {{.Image}}" | grep -E "[[:space:]]rancher/k3s:" && ! docker ps --format "{{.Names}}" | grep -Fx code-queue-backend'` 或等价检查证明 D601 k3s 是 WSL 原生 systemd 服务、native containerd 已有正确 pause sandbox 镜像、没有 active `rancher/k3s` 控制面容器且旧 direct Docker `code-queue-backend` 没有并行运行。运行 `bun scripts/cli.ts microservice health code-queue``bun scripts/cli.ts microservice proxy code-queue /api/dev-ready --raw``bun scripts/cli.ts microservice proxy code-queue '/api/tasks/overview?limit=5&transcriptLimit=1&compact=1&afterSeq=0&preferId='``bun scripts/cli.ts codex task <已有taskId>`,确认链路通过 backend-core、k3sctl-adapter、Kubernetes API service proxy 和 D601 active Code Queue Service,且 task id 查询返回初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,`queue.storage.primary=postgres``queue.storage.postgresReady=true``queue.devReady.missingTools=[]``queue.devReady.docker.versionOk=true``queue.devReady.docker.composeOk=true``queue.devReady.ssh.ready` 只在需要跨 Provider SSH/Windows-native 任务时作为强制项。在 D601 active Code Queue Pod 内验证主 PostgreSQL 端口映射可执行 `select 1`,主 OA Event Flow 端口映射 `/health` 可访问,集群内 ClaudeQQ Service `http://claudeqq.unidesk.svc.cluster.local:3290/health` 可访问;这些映射不得成为任意公网入口。
随后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / Code Queue`,确认页面显示默认模型 `gpt-5.5`、默认执行 Provider `D601`、默认工作目录 `/workspace`、模型下拉菜单包含 `gpt-5.4-mini`/`gpt-5.4`/`gpt-5.5`、入队份数、队列指标、任务 ID、复制任务 ID、引用按钮、任务耗时、引用任务 ID、清空输入、创建成功提示、任务提交表单、Trace 输出、attempt 表、MiniMax/fallback judge 状态、追加 prompt、打断和重试控件;通过页面提交一个小任务,确认任务进入 queued/running/succeeded 或可解释的 failed 状态,并且输出区能看到运行中的 Codex 消息。批量验收时设置 `入队份数=5` 或用 `---` 分隔 5 段 prompt,一次性入队 5 条任务,确认 5 条任务按顺序运行并全部进入 succeeded 或可解释的非成功终态,不能只运行第一条后停止;其中任一任务被 judge 判定 `fail` 时只能把当前任务标为 failed,后续 queued 任务仍必须继续推进。测试异常中断时可以提交长任务后点击 `打断`,确认任务变为 canceled 或被 judge 标记为非成功终态;自动重试只应在服务端/传输异常、任务正常结束但 execution record 显示未完成、或 judge 判定 retry 时发生;retry 必须复用已有 Codex thread 并 append 继续执行 prompt,只有当前任务 complete 后才推进队列中的下一个任务。MiniMax judge 必须能处理 Markdown fence/夹杂文本等 JSON 去噪;若去噪后仍失败,必须把解析错误和上一轮去噪前原始回答反馈给 MiniMax 修复后重试,日志中应出现 `judge_json_parse_retry`,且 repair 成功时仍以 `source=minimax` 返回。Codex provider key 只能通过 `OPENAI_API_KEY``CRS_OAI_KEY` 这类运行时环境透传,MiniMax API key 只能通过 D601 env-file 运行时环境传入,禁止写入 `config.json`、Dockerfile、源码或测试文档。
@@ -113,4 +113,4 @@
## T25 ClaudeQQ D601 QQ Gateway User Service
阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md``scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:确认 D601 `/home/ubuntu/.agents/skills/claudeqq` 中存在 `Dockerfile``docker-compose.unidesk.yml``napcat/config/onebot11.json``scripts/src/server_ts/src/server/event_bus.ts`;运行 `bun scripts/cli.ts microservice list`,确认 `claudeqq` 显示为 `providerId=D601``public=false``frontendOnly=true`、仓库 URL `https://gitee.com/lyon1998/agent_skills``127.0.0.1:3290` 后端映射和 `claudeqq-backend` 容器摘要,并在 D601 上确认 `claudeqq-napcat` 容器运行且只绑定 `127.0.0.1:3000/3001/6099`运行 `bun scripts/cli.ts microservice health claudeqq``bun scripts/cli.ts microservice proxy claudeqq /api/napcat/login``bun scripts/cli.ts microservice proxy claudeqq /api/events/recent``bun scripts/cli.ts microservice proxy claudeqq /api/events/subscriptions`,确认链路通过 backend-core、D601 provider-gateway 和 D601 本机 ClaudeQQ 后端health 返回 `service=claudeqq``pureBackend=true``napcat.containerized=true`、NapCat HTTP/WS 状态、登录状态/二维码、`/api/push/text``/api/events/subscriptions` 端点;通过 `POST /api/events/subscriptions` 创建临时 webhook 订阅再删除,确认 main server 和其他用户服务可订阅 QQ 消息事件;通过 `POST /api/push/text` 发送消息时若 NapCat 未登录必须返回可解释错误,NapCat 在线时必须返回消息 ID,人工推送验收只允许发给主用户私聊账号 `645275593`。最后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / ClaudeQQ`,确认页面以 React 控件显示 D601、仓库引用、私有后端映射、NapCat 容器登录二维码、NapCat 状态、QQ 事件订阅、消息推送、主用户私聊账号 `645275593`、最近 QQ 事件和已发送记录,默认没有裸 JSON,只有点击 `查看原始JSON` 才显示原始数据;不得把 D601 `3290/3000/3001/6099` 暴露到公网,也不得 iframe ClaudeQQ 旧 WebUI。
阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md``scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:确认 D601 `/home/ubuntu/.agents/skills/claudeqq` 中存在 `Dockerfile``docker-compose.unidesk.yml``napcat/config/onebot11.json``scripts/src/server_ts/src/server/event_bus.ts`;运行 `bun scripts/cli.ts microservice list`,确认 `claudeqq` 显示为 `providerId=D601``public=false``frontendOnly=true`、仓库 URL `https://gitee.com/lyon1998/agent_skills`k3s/k8s `k3s://unidesk/claudeqq:3290` 逻辑服务映射、`deployment.mode=k3sctl-managed``runtime.orchestrator=k3sctl` 且无业务直连容器摘要;使用 `bun scripts/cli.ts deploy apply --service claudeqq``deploy.json` 期望状态部署,确认 job 能在 D601 target-side fetch/export 外部 repo、构建 `unidesk-claudeqq:d601`、导入原生 k3s/containerd、apply `src/components/microservices/k3sctl-adapter/k3s/claudeqq.k8s.yaml`、stamp deployment commit、rollout 并通过 UniDesk microservice proxy 验证 live commit。运行 `bun scripts/cli.ts microservice health claudeqq``bun scripts/cli.ts microservice proxy claudeqq /api/napcat/login``bun scripts/cli.ts microservice proxy claudeqq /api/events/recent``bun scripts/cli.ts microservice proxy claudeqq /api/events/subscriptions`,确认链路通过 backend-core、k3sctl-adapter、Kubernetes API service proxy 和 D601 ClaudeQQ Servicehealth 返回 `service=claudeqq``pureBackend=true``napcat.containerized=true`、NapCat HTTP/WS 状态、登录状态/二维码、`/api/push/text``/api/events/subscriptions` 端点;通过 `POST /api/events/subscriptions` 创建临时 webhook 订阅再删除,确认 main server 和其他用户服务可订阅 QQ 消息事件;通过 `POST /api/push/text` 发送消息时若 NapCat 未登录必须返回可解释错误,NapCat 在线时必须返回消息 ID,人工推送验收只允许发给主用户私聊账号 `645275593`。最后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / ClaudeQQ`,确认页面以 React 控件显示 D601、仓库引用、私有后端映射、NapCat 容器登录二维码、NapCat 状态、QQ 事件订阅、消息推送、主用户私聊账号 `645275593`、最近 QQ 事件和已发送记录,默认没有裸 JSON,只有点击 `查看原始JSON` 才显示原始数据;不得把 D601 `3290/3000/3001/6099` 暴露到公网,也不得 iframe ClaudeQQ 旧 WebUI。
+14 -7
View File
@@ -210,20 +210,20 @@
"id": "claudeqq",
"name": "ClaudeQQ",
"providerId": "D601",
"description": "ClaudeQQ 纯后端 QQ 消息网关,容器化部署在 D601 Docker 中,为 UniDesk、主 server 和其他用户服务提供 QQ 消息事件订阅与消息推送 API。",
"description": "ClaudeQQ 是由 D601 k3s 控制平面代管的 QQ 消息网关用户服务,为 UniDesk、主 server 和其他用户服务提供 QQ 消息事件订阅与消息推送 API。",
"repository": {
"url": "https://gitee.com/lyon1998/agent_skills",
"commitId": "203b1f46684c91340ecbbd8a74502bd55e4f2011",
"dockerfile": "claudeqq/Dockerfile",
"composeFile": "claudeqq/docker-compose.unidesk.yml",
"composeFile": "src/components/microservices/k3sctl-adapter/k3s/claudeqq.k3s.json",
"composeService": "claudeqq",
"containerName": "claudeqq-backend"
"containerName": "k3s:claudeqq"
},
"backend": {
"nodeBaseUrl": "http://host.docker.internal:3290",
"nodeBindHost": "127.0.0.1",
"nodeBaseUrl": "k3s://claudeqq",
"nodeBindHost": "k3s://unidesk/claudeqq",
"nodePort": 3290,
"proxyMode": "provider-gateway-http",
"proxyMode": "k3sctl-adapter-http",
"frontendOnly": true,
"public": false,
"allowedMethods": [
@@ -250,7 +250,14 @@
"integrated": true
},
"deployment": {
"mode": "unidesk-direct"
"mode": "k3sctl-managed",
"adapterServiceId": "k3sctl-adapter",
"k3sServiceId": "claudeqq",
"namespace": "unidesk",
"expectedNodeIds": [
"D601"
],
"activeNodeId": "D601"
}
},
{
+1 -1
View File
@@ -69,7 +69,7 @@ The PostgreSQL data volume is the named Docker volume `unidesk_pgdata_10gb`. CLI
Any new user service, service migration, or change to a service's Compose/docker run/k8s configuration must prove it can recover after container restart and Docker daemon restart. The delivery evidence must include the service's `config.json` id/provider/container or Kubernetes Service mapping, restart policy or Deployment replica policy, private port or ClusterIP Service, persistent mounts or PostgreSQL tables, health readiness fields, and at least one post-restart `bun scripts/cli.ts microservice health <id>` plus a representative `microservice proxy` check through the real UniDesk path. `k3sctl-managed` services must prove the proxy path through `k3sctl-adapter` and Kubernetes API service proxy, not the provider-gateway direct business path.
D601 services have an extra gate because Windows, WSL and Docker Desktop are separate supervisors: record the Windows scheduled task or equivalent keepalive, run `docker inspect` to confirm `met-nonlinear-ts`, `claudeqq-backend`, `claudeqq-napcat` and any changed service have non-empty restart policies and host bind mounts for durable state, then verify MET Nonlinear queue/image health and ClaudeQQ logged-in NapCat HTTP/WebSocket state after the restart. A service that only becomes `running` but loses login, queue, token, subscription, data directory or pending work is not restart-recovery complete.
D601 services have an extra gate because Windows, WSL, Docker Desktop and native k3s are separate supervisors: record the Windows scheduled task or equivalent keepalive, run `docker inspect` to confirm Docker-managed services such as `met-nonlinear-ts` have non-empty restart policies and host bind mounts for durable state, and run `kubectl -n unidesk get deploy,pod,svc,endpoints claudeqq -o wide` for k3s-managed ClaudeQQ. Then verify MET Nonlinear queue/image health and ClaudeQQ logged-in NapCat HTTP/WebSocket state through the real UniDesk proxy after the restart. A service that only becomes `running` but loses login, queue, token, subscription, data directory or pending work is not restart-recovery complete.
## Delivery Gate
+28 -26
View File
@@ -151,7 +151,7 @@ Baidu Netdisk 在 UniDesk 语境中按纯后端服务管理:不得暴露百度
- 实例语义:D601 是默认 active/single-writer 执行节点,当前 `code-queue-scheduler` 仍以一个 scheduler Pod 承载长生命周期 Codex/OpenCode 子进程;D518 是 standby/read-only 节点,必须设置 `CODE_QUEUE_SERVICE_ROLE=read``CODE_QUEUE_SCHEDULER_ENABLED=false``CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED=false`,避免两个实例同时消费同一 PostgreSQL 队列或重复回放 OA 统计。D601 scheduler 也默认关闭 `CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED`;历史 OA Trace/STEP 回填必须通过显式 `/api/oa/backfill` 运维动作触发,不能在每次 Pod 重启时自动批量发布旧事件。
- 滚动更新边界:read/write/scheduler 三服务拆分可以保证滚动更新期间 Code Queue 的只读 API 与大部分控制面入口可用,但当前 scheduler Pod 内仍直接承载正在运行的 agent 子进程,scheduler Pod 被替换时 active task 仍会进入 restart-recovery/retry 语义,不能宣称 running task 零中断。真正的长期目标是继续把调度器和执行器拆开:scheduler 只负责 claim task 并创建 Kubernetes Job/Pod 或独立 workerrunner 把输出、状态、attempt、事件和通知写回 PostgreSQL/OA Event Flow/归档;只有这样 controller/scheduler 滚动更新才不会影响正在执行的任务。
- 部署引用:Code Queue 镜像仍复用 `src/components/microservices/code-queue/Dockerfile`Kubernetes 运行清单为 `src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml``config.json` 对外记录 k3s manifest `src/components/microservices/k3sctl-adapter/k3s/code-queue.k3s.json`;主 server 根目录 `docker-compose.yml` 不包含 `code-queue` service,旧 D601 direct Compose 文件只作为迁移/本地诊断参考,不是正式运行入口。
- 主服务依赖映射:Code Queue 仍以主 PostgreSQL 为权威数据库,但 D601 k3s Pod 不能依赖公网直连 `74.48.78.17:15432/4255`。Pod 内 `DATABASE_URL``OA_EVENT_FLOW_BASE_URL` 必须指向集群内 `d601-tcp-egress-gateway` Service,再由该 gateway 通过 D601 provider-gateway egress proxy 的 HTTP CONNECT 转发到主 PostgreSQL 和 OA Event Flow;新增 TCP 依赖时扩展 `TCP_EGRESS_ROUTES`,不得在业务容器里新增一次性公网直连或 ad hoc 隧道。D601 active 实例的 `CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL` 直接使用本机 ClaudeQQ 映射 `http://host.docker.internal:3290`。这些端口映射只服务受控节点运行时,必须用防火墙或等价策略限制来源,不得成为浏览器或任意公网客户端入口。
- 主服务依赖映射:Code Queue 仍以主 PostgreSQL 为权威数据库,但 D601 k3s Pod 不能依赖公网直连 `74.48.78.17:15432/4255`。Pod 内 `DATABASE_URL``OA_EVENT_FLOW_BASE_URL` 必须指向集群内 `d601-tcp-egress-gateway` Service,再由该 gateway 通过 D601 provider-gateway egress proxy 的 HTTP CONNECT 转发到主 PostgreSQL 和 OA Event Flow;新增 TCP 依赖时扩展 `TCP_EGRESS_ROUTES`,不得在业务容器里新增一次性公网直连或 ad hoc 隧道。D601 active 实例的 `CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL` 必须使用集群内 ClaudeQQ Service `http://claudeqq.unidesk.svc.cluster.local:3290`,并把 `claudeqq`/`claudeqq.unidesk.svc.cluster.local` 加入 `NO_PROXY`,避免任务完成通知被默认出网代理错误转发。旧 `http://host.docker.internal:3290` 只允许作为迁移期诊断,不得作为 Code Queue k3s Pod 的正式通知路径。这些端口映射只服务受控节点运行时,必须用防火墙或等价策略限制来源,不得成为浏览器或任意公网客户端入口。
- K8s 探针与启动维护:Kubernetes liveness/startup probe 必须使用轻量 `/live`,readiness 和用户服务健康使用 `/health``/health` 不得执行全量任务聚合、历史回填或长事务索引维护,历史任务总览应由 `/api/tasks/overview` 读取 PostgreSQL。启动时允许后台执行队列元数据 flush、通知 outbox 读取、任务表索引维护和 overview warmup,但这些维护不得阻塞 Bun server、readiness endpoint 或 frontend overview;通知表索引和大批量 OA backfill 不得作为默认启动副作用。
- MiniMax/OpenCode 并发:`minimax-m2.7` 通过 OpenCode JSON 事件端口运行;每个 Code Queue task 必须使用独立的 OpenCode XDG data/config/cache/state 目录,禁止多队列并发任务共享同一个 OpenCode SQLite/WAL 状态目录,否则并发 smoke 会触发 `PRAGMA journal_mode = WAL` 之类的数据库锁或初始化错误。用于验证 k3s/k8s 链路的 MiniMax smoke 以“至少 4 个任务、分布到 2 个 queue、至少 2 个终态成功”为链路验收线;剩余失败如果是 OpenCode 最终回复捕获、业务任务判定或模型限流,应作为 Code Queue 执行可靠性问题单独排查,不能反推 k3s 代理链路失败。
- 默认出网代理:D601 active Code Queue Pod 必须默认把 `HTTP_PROXY``HTTPS_PROXY``ALL_PROXY` 注入给 Codex/OpenCode、`git``curl``npm` 等任务子进程;当前唯一上游是 D601 provider-gateway egress HTTP CONNECT 代理,并通过 Kubernetes `Service d601-provider-egress-proxy` 暴露给 `unidesk` namespace 内的 Pod。该 Service 通过 selector 指向 D601 上的 hostNetwork 桥接 Pod,桥接 Pod 在集群端监听 service port `18789`、在宿主侧只连接 `127.0.0.1:18789` 的 provider-gateway egress endpoint;不得再用手工 EndpointSlice、provider-gateway Docker bridge IP 或固定 `172.*` 地址作为长期拓扑。Pod 内代理 URL 使用 `http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789`provider-gateway 宿主端口仍只允许绑定 `127.0.0.1`,不得开放公网;桥接 Pod 或 provider-gateway 重建后必须用 Code Queue `/health.egressProxy.connected=true` 验证。这里的 provider-gateway 只承担出网代理,不承担 Code Queue 业务 HTTP 代理;业务访问仍只能走 Kubernetes API service proxy。k3s/k8s 原生 egress gateway、service mesh 或 CNI egress policy 只作为后续网络层增强方向,当前交付态不引入第二套出网控制面。远程开发/执行容器不得只依赖这些环境变量,必须在容器网络层用 TUN 默认路由和 OUTPUT 防火墙强制外网流量只能经 master TUN 出口。
@@ -180,10 +180,18 @@ Baidu Netdisk 在 UniDesk 语境中按纯后端服务管理:不得暴露百度
- Judge 探针与复现:`GET|POST /api/judge/probe` 使用同一套 judge 逻辑跑内置 synthetic execution records,覆盖正常完成、正常结束但只给计划、未上线/未部署的服务或 WebUI 改动、传输中断和用户打断等样本,返回 `hits``total``hitRate`、每例 `expected``decision`;该接口不得回显 MiniMax API key。真实任务排障必须优先使用 `codex judge <taskId> --attempt N``/api/tasks/{id}/judge?attempt=N`,响应要包含 attempt 窗口、promptChars/payloadBytes、stored judge 对比、MiniMax 失败阶段和是否因历史 per-attempt events 缺失而降级为 retained events 重建。
- 模型选择:默认 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 结果。
- 状态与日志:`D601` 默认工作目录为容器内 `/workspace`,该路径映射 D601 WSL host 的 `/home/ubuntu`;容器内 `/home/ubuntu` 也必须映射同一个 hostPath,保证从 `/workspace` 看到的绝对 symlink 可以继续解析到真实文件。服务自身仓库路径 `/root/unidesk``/app` 单独映射 D601 WSL host 的 `/home/ubuntu/cq-deploy`。其他 Provider 的任务默认工作目录为 `/home/ubuntu`,任务 JSON、列表、Trace 摘要和 CLI 查询都必须显示 `providerId``executionMode` 与最终 `cwd``executionMode=default` 在 D601 本机 Code Queue 容器中运行 Codex/OpenCode`executionMode=windows-native` 只允许非本机 WSL Provider、Codex 模型和 `/mnt/<drive>` 工作目录,Code Queue 仍会启动远程执行容器,但容器只运行 stdio relay,经 WSL bridge 调用 Windows 宿主原生 `codex app-server --listen stdio://`。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。D601 资源比主 server 宽裕,但 Code Queue 仍必须把“内存是稀缺资源”作为核心设计约束:历史任务列表、详情、统计和只读 Trace 查询优先从 PostgreSQL 直读,进程内只保留当前 running/judging、queued、retry_wait 等调度必需热任务;需要短期热缓存时必须有严格上限、可裁剪、可从 PostgreSQL 和 append-only 输出归档重建。WebUI 不得用 browser `localStorage``sessionStorage` 或 IndexedDB 持久化 task/queue/readAt/unread 等业务状态;浏览器只能保留临时 UI 内存缓存,刷新后必须重新从后端读取 PostgreSQL 权威数据。Codex CLI-like output/Trace 的完整记录可以使用 append-only 文件作为日志型归档,但任务状态、未读状态和列表摘要不得依赖这些文件作为权威来源;`/api/tasks/<id>/transcript``/api/tasks/<id>/output` 必须能分页重建完整历史,不得因为热状态裁剪而丢失早期 trace。WebUI 必须支持多 queue 查看、显式创建 queue、提交时下拉选择 queue、提交时下拉选择执行 Provider 和执行模式,并支持把已创建且非 active 的任务移动到其他 queuequeue 内串行,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 回归且主进程内存可控。日志写入 D601 `.state/code-queue/logs` 挂载的 UniDesk JSONL 日志;Codex app-server 上游产生的 `logs_*.sqlite` 只能作为短暂缓冲,必须由 Code Queue 周期性导出为 JSONL,避免重新形成大 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 状态;D601 本机默认执行必须能通过 `/root/.ssh` 复用 WSL host GitHub SSH key 执行 Git over SSH,跨 Provider SSH 或 `windows-native` 任务同样需要 `queue.devReady.ssh.ready=true`。Codex CLI-like 输出可能很大,服务必须节流状态持久化,禁止对每个 output delta 同步重写完整 state 导致 `/health` 和控制 API 卡死;容器 healthcheck 必须使用带超时的 HTTP 探针,不能留下堆积的无超时探针进程.
- ClaudeQQ 通知:Code Queue 在 D601 上通过 `CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL=http://host.docker.internal:3290` 直接调用本机 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 超时或离线不会让任务完成通知永久丢失。
- OA 接入:Code Queue 后端通过 D601 env-file 中的 `OA_EVENT_FLOW_BASE_URL` 指向主 server OA Event Flow 受限端口映射,发布每个 TraceView 可见执行行的 `trace-step-created`、幂等种子/乱序校正用 `trace-stats-snapshot``task-updated` 和 queue 事件;服务启动或手动 backfill 时必须用相同 `eventId` 幂等回放历史 TraceView 可见执行行,避免历史任务停留在旧 STEP 统计口径。前端通过 `oa-event-flow``service:code-queue` tag stream 更新 STEP 和 Trace SummaryCode Queue 私有 SSE 不再作为刷新权威。`STEP` 表示 TraceView 可见且非 system 的执行行数;system 行可保留在任务原始输出/数据库中,但默认不展示、不计入 STEP,工具调用数必须由 `readCount+editCount+runCount` 展示,不能复用 `stepCount`
- 代理路径:只允许 `/health``/logs``/api/` 前缀;允许方法为 `GET``HEAD``POST``DELETE``PATCH`。Code Queue 只在 Compose 内网暴露 `4222/tcp`,不得映射或开放到公网。
- UniDesk 前端:`用户服务 / Code Queue` React 页面负责展示队列卡片、任务 ID、复制任务 ID、引用按钮、任务耗时、默认模型、模型下拉、执行 Provider 下拉、执行模式下拉、Provider/模式对应默认工作目录、工作目录下拉菜单、新建工作目录选项、删除已保存工作目录选项、显式入队份数、引用任务 ID、清空输入、创建成功提示、MiniMax judge 状态、Codex CLI-like 输出流、attempt 终态、追加 prompt、打断和手动重试控件;工作目录选项由 Code Queue 后端持久化到 PostgreSQL,前端不得用 localStorage/sessionStorage/IndexedDB 保存,删除只移除下拉菜单选项,不递归删除磁盘项目目录;选择 `windows-native` 时应优先切到支持 Windows 原生 Codex 的非主 server Provider,并把工作目录提示切到 `/mnt/<drive>` 默认路径;整个 agent loop 消息流统一命名为专有名词 `Trace``Trace` 包含 assistant message、user prompt、system event 和 tool callCode 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 <taskId>` 的提示,让 Codex 读取初始 prompt、最后消息和工具摘要后继续;连续执行同一 prompt 应使用 `入队份数` 一次性生成多条队列任务,而不是依赖快速连点按钮;左侧 queue/session 卡片的 `QUEUED` 状态必须显示原因,例如 `QUEUED(PREV TASK)``QUEUED(MEM LIMIT)``QUEUED(ACTIVE LIMIT)`;原始任务 JSON 只能通过显式 `查看原始JSON` 打开。
- ClaudeQQ 通知:Code Queue 在 D601 k3s 上通过 `CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL=http://claudeqq.unidesk.svc.cluster.local:3290` 调用 k3s 代管 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 超时或离线不会让任务完成通知永久丢失。
### ClaudeQQ k3s-Managed
当前 ClaudeQQ 作为 `id=claudeqq``k3sctl-managed` 用户服务登记在 `config.json`,业务实例由 D601 k3s 控制面代管:
- Orchestrator`deployment.mode=k3sctl-managed``deployment.adapterServiceId=k3sctl-adapter``deployment.k3sServiceId=claudeqq``backend.proxyMode=k3sctl-adapter-http``backend.nodeBaseUrl=k3s://claudeqq`backend-core 对 ClaudeQQ 的正式链路只能是 `frontend -> backend-core -> k3sctl-adapter -> Kubernetes API service proxy -> Kubernetes Service claudeqq:3290`
- 部署引用:ClaudeQQ 源码来自 `https://gitee.com/lyon1998/agent_skills``claudeqq` 子目录,镜像构建使用该目录内 `Dockerfile`Kubernetes 运行清单为 `src/components/microservices/k3sctl-adapter/k3s/claudeqq.k8s.yaml``config.json` 对外记录 k3s manifest `src/components/microservices/k3sctl-adapter/k3s/claudeqq.k3s.json`。旧 `docker-compose.unidesk.yml` 只作为迁移/本地诊断参考,不是正式运行入口。
- 运行对象:ClaudeQQ Pod 在 D601 上以同 Pod 双容器运行 `claudeqq``napcat`,仅通过 ClusterIP Service 暴露 `claudeqq:3290` 给 UniDesk 代理和 Code Queue 通知链路;NapCat 的 `3000/3001/6099` 只在 Pod 内可达,不得作为 NodePort、hostPort 或 provider-gateway 业务直连目标。
- 持久化路径:NapCat 登录态保存在 D601 hostPath `/home/ubuntu/.agents/skills/claudeqq/napcat/qq`,NapCat 配置和二维码缓存分别保存在 `napcat/config``napcat/cache`;ClaudeQQ 后端挂载同一业务目录中的 `config.json``bot_workspace``logs``.state` 和只读 `napcat`。这些路径不得改成匿名 volume,避免重建 Pod 后 QQ 登录态和事件/订阅状态丢失。
- 代理/API:只允许 `/health``/logs``/api/` 前缀;允许方法为 `GET``HEAD``POST``DELETE``POST /api/push/text` 接受 `userId``groupId``message`,由 ClaudeQQ 通过同 Pod NapCat HTTP API 发送 QQ 消息;NapCat 不可用时必须快速返回 `status=napcat_offline` 或可解释错误。
- UniDesk 前端:`用户服务 / ClaudeQQ` React 页面负责展示 D601、仓库引用、私有 k3s 后端映射、NapCat 登录二维码、NapCat HTTP/WS 状态、事件缓存、订阅表、订阅创建表单、消息推送表单、主用户私聊账号 `645275593`、最近 QQ 事件和已发送记录;完整原始 JSON 只能通过显式 `查看原始JSON` 打开。浏览器只能通过 UniDesk frontend 同源代理访问 ClaudeQQ,不得直接访问 D601 `3290/3000/3001/6099`,也不得 iframe ClaudeQQ 旧 WebUI。
### MDTODO k3s-Managed
@@ -205,15 +213,15 @@ Baidu Netdisk 在 UniDesk 语境中按纯后端服务管理:不得暴露百度
- `met-nonlinear`MET Nonlinear 训练编排服务,UniDesk frontend 渲染 GPU/镜像、训练队列、Project config 预览、训练进度、ETA 和历史记录。
- `claudeqq`ClaudeQQ 纯后端 QQ 消息网关,UniDesk frontend 渲染 NapCat 连接、事件订阅、消息推送、最近 QQ 事件和发送记录。
### D601 Docker Restart Recovery
### D601 Docker/k3s Restart Recovery
D601 是 Windows + WSL Ubuntu + Docker Desktop 节点,Docker Desktop 当前 `LiveRestore=false` 时,机器或 Docker daemon 重启会停止容器,恢复链路必须同时覆盖 Windows 登录、WSL keepalive、Docker daemon ready、provider-gateway 和业务用户服务:
- Windows 登录任务:计划任务 `UniDesk-D601-Autostart` 在用户 `DESKTOP-1MHOD9I\liang` 登录时运行 `C:\WINDOWS\System32\cmd.exe /c ""C:\Users\liang\AppData\Local\UniDesk\d601-autostart.cmd""`,工作目录为 `C:\Users\liang\AppData\Local\UniDesk`
- Windows launcher`C:\Users\liang\AppData\Local\UniDesk\d601-autostart.cmd` 先启动 `%ProgramFiles%\Docker\Docker\Docker Desktop.exe`,再执行 `C:\Windows\System32\wsl.exe -d Ubuntu -u ubuntu -- /bin/bash -lc "/home/ubuntu/.local/bin/unidesk-d601-autostart task"`D601 的 WSL distro 名必须写 `Ubuntu`,不能写成未验证的 `Ubuntu-22.04`
- WSL keepalive`/home/ubuntu/.local/bin/unidesk-d601-autostart` 使用 `~/.state/unidesk/d601-autostart.lock` 防重复,启动 WSL `sshd`,等待 `docker info`,把 `unidesk-provider-gateway-D601` 修正为 `restart always` 并启动,然后调用 `/home/ubuntu/.local/bin/unidesk-microservice-autorecover boot`;进入常驻 watchdog 后每 300 秒重复检查 provider-gateway 和用户服务。
- 用户服务 autorecover`/home/ubuntu/.local/bin/unidesk-microservice-autorecover` 只在 Docker ready 后运行;用容器 `State.Running``State.ExitCode` 和轻量 HTTP 探针判断是否需要恢复,MET Nonlinear 失败时在 `/home/ubuntu/met_nonlinear` 执行 `docker compose -f docker-compose.unidesk.yml up -d --force-recreate met-nonlinear-ts`ClaudeQQ/NapCat 失败时在 `/home/ubuntu/.agents/skills/claudeqq` 执行 `docker compose -f docker-compose.unidesk.yml up -d --force-recreate napcat claudeqq`
- 验收命令:Docker 恢复后必须同时验证 `docker inspect --format '{{.HostConfig.RestartPolicy.Name}} {{.State.Status}}' met-nonlinear-ts claudeqq-backend claudeqq-napcat``bun scripts/cli.ts microservice health met-nonlinear``bun scripts/cli.ts microservice health claudeqq` 和公网 frontend 页面;ClaudeQQ 还必须验证 `/api/napcat/login``state=logged_in`、HTTP connected 和 WebSocket connected。
- WSL keepalive`/home/ubuntu/.local/bin/unidesk-d601-autostart` 使用 `~/.state/unidesk/d601-autostart.lock` 防重复,启动 WSL `sshd`,等待 Docker Desktop daemon 和原生 k3s 就绪,把 `unidesk-provider-gateway-D601` 修正为 `restart always` 且 running,然后调用 `/home/ubuntu/.local/bin/unidesk-microservice-autorecover boot`;进入常驻 watchdog 后每 300 秒重复检查 provider-gateway、Docker 直管服务和 k3s 代管服务。
- 用户服务 autorecover`/home/ubuntu/.local/bin/unidesk-microservice-autorecover` 只在 Docker 和 k3s ready 后运行;Docker 直管服务仍用容器 `State.Running``State.ExitCode` 和轻量 HTTP 探针判断是否需要恢复,MET Nonlinear 失败时在 `/home/ubuntu/met_nonlinear` 执行 `docker compose -f docker-compose.unidesk.yml up -d --force-recreate met-nonlinear-ts`ClaudeQQ 必须通过 k3s Deployment/Service 恢复和验证,不再用 `docker compose` 作为正式恢复入口
- 验收命令:Docker/k3s 恢复后必须同时验证 `docker inspect --format '{{.HostConfig.RestartPolicy.Name}} {{.State.Status}}' met-nonlinear-ts``kubectl -n unidesk get deploy/pod/svc/endpoints claudeqq -o wide``bun scripts/cli.ts microservice health met-nonlinear``bun scripts/cli.ts microservice health claudeqq` 和公网 frontend 页面;ClaudeQQ 还必须经 UniDesk 代理验证 `/api/napcat/login``state=logged_in`、HTTP connected 和 WebSocket connected。
### FindJob On D601
@@ -264,25 +272,19 @@ MET Nonlinear 的长期服务边界写在业务仓库 `~/met_nonlinear/docs/refe
MET Nonlinear 验收必须通过公网 UniDesk frontend 的交互式 UI 完成:选择已有 source Project,设置训练轮数和最大并发,使用 `Fork Project` 创建新的 `projects/unidesk_forks/` Project,确认新 Project 只是被选中而不会直接训练,再加入待启动队列并点击 `启动队列`。验收时必须确认项目库的 `projects/``ex_projects/` 按文件树层级展开、文件夹 Project 计数与后端返回数量一致;点击项目行后详情显示 `config.json``data/` 训练状态、模型参数量和指标;待启动、排队中、训练中、已完成和失败诊断分标签可见;训练队列和已完成行显示 `epoch/h` 训练速度且可点击打开任务详情。最大并发必须按 UI 设置生效,运行中行显示训练进度和 ETA,目标 GPU 为 2080Ti2080Ti 显存余量低于 20% 时自动限制并发,并确认训练容器结束后不残留。批量规模由 UI 输入框决定,完整验收可以通过输入 `Fork 数量=10``训练轮数=200``最大并发=3` 执行,但不得把该规模做成专用硬编码按钮。CLI `/api/queue/server-test` 仅保留为后端兼容入口,不作为 frontend 操作入口。
### ClaudeQQ On D601
### ClaudeQQ Development On D601
当前 ClaudeQQ 作为 `id=claudeqq` 的用户服务登记在 `config.json`
ClaudeQQ 的业务源码和持久化数据仍在 D601,但正式运行由 k3s 代管
- Provider`D601`
- 开发工作树:`/home/ubuntu/.agents/skills/claudeqq`,后端、Dockerfile、订阅分发和 NapCat 连接调试必须通过 UniDesk SSH 透传在 D601 完成;主 server 本地只允许开发 UniDesk frontend代理登记。
- 开发工作树:`/home/ubuntu/.agents/skills/claudeqq`,后端、Dockerfile、订阅分发和 NapCat 连接调试必须通过 UniDesk SSH 透传在 D601 完成;主 server 本地只允许开发 UniDesk frontend代理登记和 k3s manifest
- 代码引用:`https://gitee.com/lyon1998/agent_skills` 与配置中的 `repository.commitId`,实际服务目录为仓库内 `claudeqq/`
- 部署引用:业务目录 `Dockerfile``docker-compose.unidesk.yml`Compose service 为 `claudeqq` `napcat`,容器名分别为 `claudeqq-backend``claudeqq-napcat`
- 运行配置:`docker-compose.unidesk.yml` 必须同时定义 `napcat``claudeqq` 两个 service,均使用 `restart: unless-stopped``napcat` 固定 `ACCOUNT=${CLAUDEQQ_NAPCAT_ACCOUNT:-763382329}``WEBUI_PREFIX=/webui` 和本机端口 `127.0.0.1:3000/3001/6099``claudeqq` 固定 `127.0.0.1:3290:3290``CLAUDEQQ_AUTO_REPLY=false``CLAUDEQQ_NAPCAT_HTTP_HOST=napcat``CLAUDEQQ_NAPCAT_WS_HOST=napcat``CLAUDEQQ_ONLINE_NOTICE_USER_ID=645275593``CLAUDEQQ_LOGIN_MONITOR_INTERVAL_MS`
- 持久化路径:NapCat 登录态必须保存在业务目录下的 `./napcat/qq:/app/.config/QQ`,NapCat 配置和二维码缓存分别保存在 `./napcat/config:/app/napcat/config``./napcat/cache:/app/napcat/cache`ClaudeQQ 后端必须挂载 `./config.json:/app/config.json:ro``./bot_workspace:/bot_workspace``./logs:/app/logs``./.state:/app/.state``./napcat:/napcat:ro`。如果这些 host 目录丢失或改成匿名 volume,Docker 重启后 QQ 登录态和事件/订阅状态会丢失,不得判定为已具备自动登录
- 节点后端:D601 上 `127.0.0.1:3290`provider-gateway 容器内通过 `http://host.docker.internal:3290` 访问。
- 代理路径:只允许 `/health``/logs``/api/` 前缀;允许方法为 `GET``HEAD``POST``DELETE`
- 服务模式:ClaudeQQ 在 UniDesk 中按纯后端运行,默认 `CLAUDEQQ_AUTO_REPLY=false`,只负责 NapCat HTTP/WS 连接、QQ 事件入站记录、HTTP webhook 订阅投递和 `/api/push/text` 消息推送,不把 ClaudeQQ 自身旧 WebUI 作为用户入口。NapCat 必须随同 `docker-compose.unidesk.yml` 容器化部署,D601 只绑定 `127.0.0.1:3000``127.0.0.1:3001``127.0.0.1:6099`ClaudeQQ 容器通过 Compose 内网 `napcat:3000/3001` 访问。
- 部署引用:标准 desired state 为根目录 `deploy.json` 中的 `claudeqq` 服务;运行清单为 UniDesk 仓库内 `src/components/microservices/k3sctl-adapter/k3s/claudeqq.k3s.json` `claudeqq.k8s.yaml`,镜像名固定 `unidesk-claudeqq:d601`。业务仓库内 `docker-compose.unidesk.yml` 只作为迁移/本地诊断参考,不是正式部署入口
- 运行配置:ClaudeQQ Pod 同时包含 `napcat``claudeqq` 两个容器;`claudeqq` 固定 `CLAUDEQQ_AUTO_REPLY=false``CLAUDEQQ_NAPCAT_HTTP_HOST=127.0.0.1``CLAUDEQQ_NAPCAT_WS_HOST=127.0.0.1``CLAUDEQQ_ONLINE_NOTICE_USER_ID=645275593``CLAUDEQQ_LOGIN_MONITOR_INTERVAL_MS`NapCat 的 `3000/3001/6099` 仅 Pod 内访问。
- 节点后端:UniDesk 逻辑后端为 `k3s://unidesk/claudeqq:3290`,由 backend-core 经过 k3sctl-adapter 和 Kubernetes API service proxy 访问 Kubernetes Service `claudeqq:3290`;不得再通过 provider-gateway 业务代理或 D601 本机端口作为正式链路
- NapCat 登录 API`GET /api/napcat/login``GET /api/napcat/status` 返回容器化状态、HTTP/WS 连通性、登录状态和二维码 data URL;`GET /api/napcat/qrcode` 只返回二维码。二维码来源为共享挂载中的 `/napcat/cache/qrcode.png`,由 ClaudeQQ 后端转为 JSON data URL 后经 UniDesk 同源代理给前端展示。`/health` 的 healthy 条件必须包含 `ready=true`、NapCat HTTP connected、NapCat WebSocket connected 和 `loginState=logged_in`;仅有二维码、NapCat 容器 running 或后端进程 running 不能算健康。
- 订阅 API`GET /api/events/recent` 返回最近 QQ 事件,`GET|POST /api/events/subscriptions` 管理 webhook 订阅,`DELETE /api/events/subscriptions/{id}` 删除订阅;订阅回调使用 HTTP POST JSON,并在配置 secret 时携带 `x-claudeqq-signature` HMAC-SHA256。
- 推送 API`POST /api/push/text` 接受 `userId``groupId``message`,由 ClaudeQQ 通过 NapCat HTTP API 发送 QQ 消息;NapCat 不可用时必须快速返回 `status=napcat_offline` 和具体连接错误;当前人工推送验收只允许发给主用户私聊账号 `645275593`,其他用户服务和 main server 应通过 UniDesk 用户服务代理调用,不得直连 D601 公网端口
- UniDesk 前端:`用户服务 / ClaudeQQ` React 页面负责展示 D601 仓库引用、私有后端映射、NapCat 容器登录二维码、NapCat HTTP/WS 状态、事件缓存、订阅表、订阅创建表单、消息推送表单、主用户私聊账号 `645275593` 标记、最近 QQ 事件和已发送记录;完整原始 JSON 只能通过显式 `查看原始JSON` 打开。
ClaudeQQ 在 UniDesk 语境中按消息网关后端服务管理:不得直接暴露 D601 的 `3290``3000``3001``6099` 到公网,不得 iframe ClaudeQQ 旧 WebUI。浏览器只能通过 UniDesk frontend 的 `/api/microservices/claudeqq/health``/api/microservices/claudeqq/proxy/...` 同源代理访问。
- 推送 API`POST /api/push/text` 接受 `userId``groupId``message`,由 ClaudeQQ 通过 NapCat HTTP API 发送 QQ 消息;NapCat 不可用时必须快速返回 `status=napcat_offline` 和具体连接错误;当前人工推送验收只允许发给主用户私聊账号 `645275593`,其他用户服务和 main server 应通过 UniDesk 用户服务代理调用。
## CLI
@@ -319,21 +321,21 @@ ClaudeQQ 在 UniDesk 语境中按消息网关后端服务管理:不得直接
- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `findjob``providerId=D601``public=false``frontendOnly=true`、仓库 URL、commit id、`127.0.0.1:3254` 映射和 `findjob-server` 容器摘要可见。
- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `pipeline``providerId=D601``public=false``frontendOnly=true`、仓库 URL、commit id、`127.0.0.1:18082` 映射和 `pipeline-v2-control` 容器摘要可见。
- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `met-nonlinear``providerId=D601``public=false``frontendOnly=true`、仓库 URL、commit id、`127.0.0.1:3288` 映射和 `met-nonlinear-ts` 容器摘要可见。
- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `claudeqq``providerId=D601``public=false``frontendOnly=true`、仓库 URL、commit id、`127.0.0.1:3290` 映射和 `claudeqq-backend` 容器摘要可见
- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `claudeqq``providerId=D601``public=false``frontendOnly=true`、仓库 URL、commit id、`deployment.mode=k3sctl-managed``runtime.orchestrator=k3sctl``backend.proxyMode=k3sctl-adapter-http``backend.nodeBaseUrl=k3s://claudeqq``k3s://unidesk/claudeqq:3290` 逻辑 Service 映射可见,且不显示业务容器直连摘要
- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `k3sctl-adapter``providerId=D601``deployment.mode=unidesk-direct`、后端私有端口 `127.0.0.1:4266`,并确认 `code-queue``deployment.mode=k3sctl-managed``runtime.orchestrator=k3sctl``backend.proxyMode=k3sctl-adapter-http``backend.nodeBaseUrl=k3s://code-queue`,且不再显示业务容器直连摘要。
- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `filebrowser``filebrowser-d601` 分别显示为 `providerId=D518``providerId=D601`,均为 `public=false``frontendOnly=true`,仓库 URL 为 `https://github.com/filebrowser/filebrowser`,后端映射为 `host.docker.internal:4251`,容器摘要分别为 `unidesk-filebrowser-d518``unidesk-filebrowser-d601`;列表中不得再出现主 server `filebrowser-main` 容器。
- 运行 `bun scripts/cli.ts microservice health findjob``bun scripts/cli.ts microservice proxy findjob /api/summary`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 FindJob 后端。
- 运行 `bun scripts/cli.ts microservice health pipeline``bun scripts/cli.ts microservice proxy pipeline '/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3'`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 Pipeline 后端,且 run/procedure 摘要包含甘特图所需时间字段。
- 运行 `bun scripts/cli.ts microservice health met-nonlinear``bun scripts/cli.ts microservice proxy met-nonlinear /api/queue``bun scripts/cli.ts microservice proxy met-nonlinear '/api/projects?root=projects&limit=20'``bun scripts/cli.ts microservice proxy met-nonlinear /api/images`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 MET Nonlinear TS 后端。
- 运行 `bun scripts/cli.ts microservice health claudeqq``bun scripts/cli.ts microservice proxy claudeqq /api/napcat/login``bun scripts/cli.ts microservice proxy claudeqq /api/events/recent``bun scripts/cli.ts microservice proxy claudeqq /api/events/subscriptions`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 ClaudeQQ 后端;在 D601 上 `curl http://127.0.0.1:3290/health` 应显示 `service=claudeqq``pureBackend=true``napcat.containerized=true`、NapCat HTTP/WS 状态、二维码状态和订阅计数。
- 运行 `bun scripts/cli.ts microservice health claudeqq``bun scripts/cli.ts microservice proxy claudeqq /api/napcat/login``bun scripts/cli.ts microservice proxy claudeqq /api/events/recent``bun scripts/cli.ts microservice proxy claudeqq /api/events/subscriptions`,确认真实链路经过 backend-core、k3sctl-adapter、Kubernetes API service proxy 和 D601 Kubernetes Service `claudeqq:3290`health 应显示 `service=claudeqq``pureBackend=true``napcat.containerized=true`、NapCat HTTP/WS 状态、二维码状态和订阅计数。
- 运行 `bun scripts/cli.ts microservice health todo-note``bun scripts/cli.ts microservice proxy todo-note /api/instances`,确认真实链路经过 backend-core、WebSocket、main-server provider-gateway 和主 server `todo-note-backend` 后端;输出中必须包含五个迁移清单和 PostgreSQL 存储健康状态。
- 运行 `bun scripts/cli.ts microservice health k3sctl-adapter``bun scripts/cli.ts microservice proxy k3sctl-adapter /api/control-plane --raw``bun scripts/cli.ts microservice health code-queue``bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview`,确认真实链路经过 backend-core -> k3sctl-adapter -> k3s Service proxy,且 read/write/scheduler 三个内部 Service 都有 ready endpointadapter 验收还必须证明其作为 UniDesk 直管服务运行在 k3s 外部,Docker 形态下挂载宿主 `/etc/rancher/k3s/k3s.yaml``/run/host-ssh/id_ed25519`,通过容器内 SSH local tunnel 连接 WSL 原生 k3s API,且没有 active `rancher/k3s` 控制面容器。Code Queue `/health` 必须仍返回业务后端自己的 `role=scheduler``queue.storage.primary=postgres``queue.storage.postgresReady=true``queue.notifications.claudeqq.outbox.storage=postgres``egressProxy.connected=true`,不得被 adapter 聚合健康 JSON 替代。还必须在 active Code Queue Pod 内验证主 PostgreSQL 端口映射、主 OA Event Flow 端口映射、本机 ClaudeQQ `http://host.docker.internal:3290``d601-provider-egress-proxy` 均可访问,并确认 `/workspace``/home/ubuntu` 指向同一 WSL home hostPath`/workspace/cq-deploy` 这类绝对 symlink 可以进入真实目录。再在 adapter 控制页确认 D601 scheduler serving healthy、D601 read/write Service healthy、D518 standby pod ready、`missingNodeIds=[]` 且整体不退化为 hidden fallback。再通过公网 frontend 提交一个 `gpt-5.5` 小任务,确认任务由 write 入库、scheduler 轮询执行、read 返回输出实时更新、结束后有 judge 判定,且运行中可追加 prompt 或打断。Code Queue 的重启恢复必须作为验收项:运行中任务存在时重启或重建 scheduler 实例后,任务必须从 PostgreSQL 恢复到可继续执行状态,不能丢失 active task、`promptHistory`、后续 queued 任务、readAt/未读状态或已入 outbox 的 ClaudeQQ 通知。Code Queue 服务名、表名前缀或持久化目录发生迁移后,还必须运行 `bun scripts/cli.ts e2e run --only microservice:catalog-code-queue,microservice:code-queue-status,microservice:code-queue-health,microservice:code-queue-tasks`,证明 backend-core catalog、k3s adapter 私有代理、PostgreSQL 队列和任务列表都指向 `code-queue`。批量验收必须通过公网 frontend 设置 `入队份数=5` 或使用多段 prompt 分隔,一次性入队 5 条任务,并确认 5 条任务按顺序进入 running/judging/succeeded,而不是只运行第一条。
- 运行 `bun scripts/cli.ts microservice health k3sctl-adapter``bun scripts/cli.ts microservice proxy k3sctl-adapter /api/control-plane --raw``bun scripts/cli.ts microservice health code-queue``bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview`,确认真实链路经过 backend-core -> k3sctl-adapter -> k3s Service proxy,且 read/write/scheduler 三个内部 Service 都有 ready endpointadapter 验收还必须证明其作为 UniDesk 直管服务运行在 k3s 外部,Docker 形态下挂载宿主 `/etc/rancher/k3s/k3s.yaml``/run/host-ssh/id_ed25519`,通过容器内 SSH local tunnel 连接 WSL 原生 k3s API,且没有 active `rancher/k3s` 控制面容器。Code Queue `/health` 必须仍返回业务后端自己的 `role=scheduler``queue.storage.primary=postgres``queue.storage.postgresReady=true``queue.notifications.claudeqq.outbox.storage=postgres``egressProxy.connected=true`,不得被 adapter 聚合健康 JSON 替代。还必须在 active Code Queue Pod 内验证主 PostgreSQL 端口映射、主 OA Event Flow 端口映射、集群内 ClaudeQQ `http://claudeqq.unidesk.svc.cluster.local:3290/health``d601-provider-egress-proxy` 均可访问,并确认 `/workspace``/home/ubuntu` 指向同一 WSL home hostPath`/workspace/cq-deploy` 这类绝对 symlink 可以进入真实目录。再在 adapter 控制页确认 D601 scheduler serving healthy、D601 read/write Service healthy、D518 standby pod ready、`missingNodeIds=[]` 且整体不退化为 hidden fallback。再通过公网 frontend 提交一个 `gpt-5.5` 小任务,确认任务由 write 入库、scheduler 轮询执行、read 返回输出实时更新、结束后有 judge 判定,且运行中可追加 prompt 或打断。Code Queue 的重启恢复必须作为验收项:运行中任务存在时重启或重建 scheduler 实例后,任务必须从 PostgreSQL 恢复到可继续执行状态,不能丢失 active task、`promptHistory`、后续 queued 任务、readAt/未读状态或已入 outbox 的 ClaudeQQ 通知。Code Queue 服务名、表名前缀或持久化目录发生迁移后,还必须运行 `bun scripts/cli.ts e2e run --only microservice:catalog-code-queue,microservice:code-queue-status,microservice:code-queue-health,microservice:code-queue-tasks`,证明 backend-core catalog、k3s adapter 私有代理、PostgreSQL 队列和任务列表都指向 `code-queue`。批量验收必须通过公网 frontend 设置 `入队份数=5` 或使用多段 prompt 分隔,一次性入队 5 条任务,并确认 5 条任务按顺序进入 running/judging/succeeded,而不是只运行第一条。
- Code Queue 内存防回归验收:凡是改动 Code Queue 的持久化、scheduler、输出/Trace、health、列表/详情查询、日志导出或容器运行参数,交付前必须在 D601 用 `kubectl -n unidesk get deploy,pod,svc,endpoints -o wide``kubectl -n unidesk describe deploy/code-queue` 或等价 Docker inspect 确认 memory/swap 硬上限符合预算,运行 `kubectl -n unidesk top pod` 或 Docker stats 确认常驻内存、`OOMKilled=false``RestartCount` 未异常增长,再运行 `bun scripts/cli.ts microservice health code-queue` 确认 `/health` 是轻量 readiness 且暴露 PostgreSQL/notification/outbox 状态。验收还必须覆盖有历史任务存在时的 `/api/tasks/overview`、单任务详情和 output/transcript 查询,证明热状态裁剪不会丢历史输出、也不会重新把全部历史 `task_json` 缓存在进程内;涉及 TypeScript/frontend 验证的任务应能在 D601 Code Queue memory/swap 预算中完成 `bun run --cwd src/components/frontend check` 这类短时高内存命令,而不是被 memory watchdog 反复 SIGTERM。
- Code Queue 延迟防回归验收:凡是改动 Code Queue 列表、overview、readAt、Trace/summary 懒加载、实时 output/SSE 事件发布、frontend 请求策略、backend-core 用户服务代理或 frontend Code Queue 请求路径,交付前必须在有历史任务数据且有 active output 流动的 live 环境验证 `GET /api/tasks/overview``POST /api/tasks/<id>/read`、选定 task 的 `trace-step` 和前端 `/app/code-queue/` 首屏均低于 1s 目标;可运行 `bun scripts/src/code-queue-perf.ts --json --target-ms 1000` 采集公网 frontend 下的首屏耗时、最慢 API 和 DOM 完成指标,并用 `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview --raw`、D601 Pod `/health``/api/tasks/overview` curl、性能面板 `/api/performance``/api/frontend-performance` 失败/慢操作记录、`kubectl -n unidesk top pod` 或 Docker stats 补充后端耗时、代理 502 和内存/CPU 证据。验收结论必须同时说明是否使用了短 TTL cache、cache 如何被 mutation 或 archive append 失效、数据库索引/聚合是否命中、输出热路径是否只读增量指标,以及分页加载是否跳过 selected/active/stats;不能只展示 cache 命中后的单次快照。
- 运行 `bun scripts/cli.ts microservice health filebrowser``bun scripts/cli.ts microservice health filebrowser-d601``bun scripts/cli.ts microservice proxy filebrowser / --max-body-bytes 2000`,确认 File Browser health 返回 `status=OK`WebUI HTML 包含 `File Browser`D518/D601 通过 provider-gateway 访问节点本机 `4251`;随后在公网 frontend 的 `用户服务 / File Browser` 中确认 D518 为默认目标、可导出截图、iframe 紧凑布局不再有巨大 `folder` 标记遮挡文件名,并可浏览 `/mnt/c`
- 在 D601 上用 `bun scripts/cli.ts ssh D601 ...` 调试业务仓库和容器,确认 `curl http://127.0.0.1:3254/api/health` 可用;不要把调试服务部署到主 server。
- 在 D601 上用 `bun scripts/cli.ts ssh D601 ...` 调试业务仓库和容器,确认 `curl http://127.0.0.1:18082/health``curl http://127.0.0.1:18082/api/snapshot` 可用;不要把 Pipeline 调试服务部署到主 server。
- 在 D601 上用 `bun scripts/cli.ts ssh D601 ...` 调试 `~/met_nonlinear`,确认 `curl http://127.0.0.1:3288/health` 可用;最终验收必须回到公网 UniDesk frontend,通过项目库选择、Fork、加入待启动队列和启动队列完成,不要把 MET Nonlinear 后端、Docker build 或训练任务部署到主 server。
- 在 D601 上用 `bun scripts/cli.ts ssh D601 ...` 调试 `~/.agents/skills/claudeqq`确认 `docker compose -f docker-compose.unidesk.yml up -d --build claudeqq``claudeqq-backend``claudeqq-napcat` 都运行,`curl http://127.0.0.1:3290/health``curl http://127.0.0.1:3290/api/napcat/login` 可用;不要把 ClaudeQQ 后端或 NapCat 调试服务部署到主 server。
- 在 D601 上用 `bun scripts/cli.ts ssh D601 ...` 调试 `~/.agents/skills/claudeqq`可使用业务仓库 `docker-compose.unidesk.yml` 做本地诊断,但正式部署必须回到 `bun scripts/cli.ts deploy apply --service claudeqq`、k3s Deployment `claudeqq` 和 UniDesk `microservice health/proxy` 验证;不要把 ClaudeQQ 后端或 NapCat 调试服务部署到主 server,也不要把诊断 Compose 当作正式运行态
- 运行 `bun scripts/cli.ts e2e run`,确认用户服务相关检查 passed,并确认 Playwright 访问的是公网 `http://74.48.78.17:18081/`
- 登录公网 frontend,进入 `用户服务 / 服务目录``用户服务 / Todo Note``用户服务 / FindJob``用户服务 / Pipeline``用户服务 / MET Nonlinear``用户服务 / ClaudeQQ`,确认能看到主 server 与 D601 provider、仓库引用、后端私有映射、Todo Note 迁移清单与树形任务、FindJob 指标和岗位预览、Pipeline 组件矩阵、React Flow 控制图、epoch 列表、epoch 甘特图和运行材料索引、MET Nonlinear 队列/GPU/镜像/Project config/训练历史、ClaudeQQ NapCat 容器登录二维码/NapCat 状态/事件订阅/消息推送/最近 QQ 事件;Todo Note 页面必须能创建临时清单、添加任务并删除临时清单,删除前必须按唯一临时清单名称重新选中对应行,禁止用未确认的当前 active 清单执行删除,FindJob 页面必须显示真实数字指标、`HEALTH OK` 和非空岗位预览,Pipeline 页面必须显示 `Pipeline v2 工作台``Health OK`、组件数、epoch 甘特图和结构化运行材料索引,MET Nonlinear 页面必须显示 `Health OK``Fork Project``启动队列``当前队列`、最大并发设置和 GPU/镜像面板,ClaudeQQ 页面必须显示 `Health OK``NapCat 容器登录``QQ 事件订阅``消息推送``事件缓存` 和私有代理说明,不能只停留在 loading 骨架;页面默认不得出现裸 JSON、JSONL 或逐行日志。
+88 -13
View File
@@ -94,6 +94,7 @@ const providerGatewayWsEgressProxyUrl = "http://127.0.0.1:18789";
const nativeK3sInstallVersion = "v1.34.1+k3s1";
const nativeK3sImage = "rancher/k3s:v1.34.1-k3s1";
const nativeK3sCtrAddress = "/run/k3s/containerd/containerd.sock";
const unideskRepoUrl = "https://github.com/pikasTech/unidesk";
function isHelpArg(value: string | undefined): boolean {
return value === "help" || value === "--help" || value === "-h";
@@ -180,6 +181,16 @@ function repoSlug(repo: string): string | null {
return null;
}
function isFullGitSha(value: string): boolean {
return /^[0-9a-f]{40}$/iu.test(value);
}
function isUnideskRepo(repo: string): boolean {
const desiredSlug = repoSlug(repo);
const unideskSlug = repoSlug(unideskRepoUrl);
return desiredSlug !== null && desiredSlug === unideskSlug;
}
function sshUrlForSlug(slug: string | null): string | null {
if (slug === null) return null;
const [host = "", ...pathParts] = slug.split("/");
@@ -208,7 +219,7 @@ function providerSourceRepoUrl(repo: string): string {
function localResolvedCommitForDesired(desired: DeployManifestService): string | null {
const desiredSlug = repoSlug(desired.repo);
if (desiredSlug === null || desired.commitId.length !== 40) return null;
if (desiredSlug === null || !isFullGitSha(desired.commitId)) return null;
const localOrigin = runCommand(["git", "remote", "get-url", "origin"], repoRoot);
const localOriginUrl = localOrigin.exitCode === 0 ? localOrigin.stdout.trim() : "";
if (localOriginUrl.length === 0 || repoSlug(localOriginUrl) !== desiredSlug) return null;
@@ -235,6 +246,7 @@ function runResolveGit(args: string[], cwd: string): ReturnType<typeof runComman
function resolveDesiredCommit(desired: DeployManifestService): DeployManifestService {
const localCommit = localResolvedCommitForDesired(desired);
if (localCommit !== null) return { ...desired, commitId: localCommit };
if (isFullGitSha(desired.commitId) && !isUnideskRepo(desired.repo)) return { ...desired, commitId: desired.commitId.toLowerCase() };
const cacheDir = repoResolveCacheDir(desired.repo);
mkdirSync(cacheDir, { recursive: true });
@@ -412,12 +424,32 @@ function targetExportDir(service: UniDeskMicroserviceConfig, runId: string): str
function targetWorkDir(service: UniDeskMicroserviceConfig): string {
if (service.deployment.mode === "k3sctl-managed") return k3sDeployDir;
if (targetIsMain(service) && service.repository.url === "https://github.com/pikasTech/unidesk") {
if (targetIsMain(service) && service.repository.url === unideskRepoUrl) {
return rootPath(".state", "deploy", "work", safeId(service.id));
}
return service.development.worktreePath;
}
function sourceWorkDir(service: UniDeskMicroserviceConfig): string {
if (service.deployment.mode === "k3sctl-managed" && service.repository.url !== unideskRepoUrl) {
return `${remoteDeployRoot}/work/${safeId(service.id)}`;
}
return targetWorkDir(service);
}
function sourceRootSubdir(service: UniDeskMicroserviceConfig): string {
const claudeqqPrefix = "claudeqq/";
if (service.id === "claudeqq" && service.repository.url !== unideskRepoUrl && service.repository.dockerfile.startsWith(claudeqqPrefix)) {
return "claudeqq";
}
return "";
}
function sourceBuildContext(service: UniDeskMicroserviceConfig): string {
const subdir = sourceRootSubdir(service);
return subdir.length > 0 ? `${sourceWorkDir(service)}/${subdir}` : sourceWorkDir(service);
}
function buildImageTag(service: UniDeskMicroserviceConfig): string {
if (service.deployment.mode === "k3sctl-managed") return `unidesk-${service.id}:d601`;
if (targetIsMain(service)) {
@@ -438,12 +470,12 @@ function directComposeEnvFile(service: UniDeskMicroserviceConfig): string {
}
function directBuildContextOverride(service: UniDeskMicroserviceConfig): string {
if (targetIsMain(service) && service.repository.url === "https://github.com/pikasTech/unidesk") return targetWorkDir(service);
if (targetIsMain(service) && service.repository.url === unideskRepoUrl) return targetWorkDir(service);
return "";
}
function directDockerfileOverride(service: UniDeskMicroserviceConfig): string {
if (targetIsMain(service) && service.repository.url === "https://github.com/pikasTech/unidesk") return service.repository.dockerfile;
if (targetIsMain(service) && service.repository.url === unideskRepoUrl) return service.repository.dockerfile;
return "";
}
@@ -453,8 +485,17 @@ function k8sManifestPath(service: UniDeskMicroserviceConfig): string {
return composeFile.replace(/\.k3s\.json$/u, ".k8s.yaml");
}
function sourceDockerfilePath(service: UniDeskMicroserviceConfig): string {
const claudeqqPrefix = "claudeqq/";
if (service.id === "claudeqq" && service.repository.url !== unideskRepoUrl && service.repository.dockerfile.startsWith(claudeqqPrefix)) {
return service.repository.dockerfile.slice(claudeqqPrefix.length);
}
return service.repository.dockerfile;
}
function sourceProxyPrelude(service: UniDeskMicroserviceConfig): string {
if (targetIsMain(service)) return "";
const strictHostKeyChecking = isUnideskRepo(service.repository.url) ? "yes" : "accept-new";
return [
`build_proxy=${shellQuote(providerGatewayWsEgressProxyUrl)}`,
"export HTTP_PROXY=\"$build_proxy\" HTTPS_PROXY=\"$build_proxy\" ALL_PROXY=\"$build_proxy\"",
@@ -521,7 +562,7 @@ while True:
"UNIDESK_GIT_SSH_PROXY",
"chmod 700 \"$git_ssh_proxy\"",
"export UNIDESK_GIT_SSH_HTTP_PROXY=\"$build_proxy\"",
"export GIT_SSH_COMMAND=\"ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts -i $HOME/.ssh/id_ed25519 -o 'ProxyCommand=$git_ssh_proxy %h %p'\"",
`export GIT_SSH_COMMAND="ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=${strictHostKeyChecking} -o UserKnownHostsFile=$HOME/.ssh/known_hosts -i $HOME/.ssh/id_ed25519 -o 'ProxyCommand=$git_ssh_proxy %h %p'"`,
"curl -fsSI --max-time 20 -x \"$build_proxy\" https://github.com >/dev/null",
"echo target_source_proxy=provider-gateway-ws-egress:$build_proxy",
"echo target_build_proxy=provider-gateway-ws-egress:$build_proxy",
@@ -541,7 +582,7 @@ function buildCachePrelude(dockerfileVariable: string): string[] {
}
function prepareSourceScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, exportDir: string): string {
if (targetIsMain(service) && desired.repo === "https://github.com/pikasTech/unidesk") {
if (targetIsMain(service) && desired.repo === unideskRepoUrl) {
return [
"set -euo pipefail",
`repo=${shellQuote(repoRoot)}`,
@@ -577,11 +618,15 @@ function prepareSourceScript(service: UniDeskMicroserviceConfig, desired: Deploy
}
function syncSourceScript(service: UniDeskMicroserviceConfig, exportDir: string): string {
const workDir = targetWorkDir(service);
const workDir = sourceWorkDir(service);
const buildContext = sourceBuildContext(service);
const dockerfilePath = sourceDockerfilePath(service);
return [
"set -euo pipefail",
`export_dir=${shellQuote(exportDir)}`,
`work_dir=${shellQuote(workDir)}`,
`build_context=${shellQuote(buildContext)}`,
`dockerfile_path=${shellQuote(dockerfilePath)}`,
"mkdir -p \"$work_dir\"",
[
"rsync -a --delete",
@@ -593,27 +638,51 @@ function syncSourceScript(service: UniDeskMicroserviceConfig, exportDir: string)
"\"$export_dir/\"",
"\"$work_dir/\"",
].join(" "),
`test -f "$work_dir/${service.repository.dockerfile}"`,
"printf 'synced deploy worktree to %s\\n' \"$work_dir\"",
"test -f \"$build_context/$dockerfile_path\"",
"printf 'synced deploy worktree to %s\\nbuild_context=%s\\n' \"$work_dir\" \"$build_context\"",
].join("\n");
}
function syncK8sControlManifestsScript(service: UniDeskMicroserviceConfig): string {
if (service.deployment.mode !== "k3sctl-managed" || isUnideskRepo(service.repository.url)) return "";
const manifests = [service.repository.composeFile, k8sManifestPath(service)];
const commands = [
"set -euo pipefail",
`target_root=${shellQuote(targetWorkDir(service))}`,
"mkdir -p \"$target_root\"",
];
for (const relativePath of manifests) {
const absolutePath = rootPath(relativePath);
if (!existsSync(absolutePath)) throw new Error(`${service.id} k3s control manifest missing: ${relativePath}`);
const encoded = Buffer.from(readFileSync(absolutePath, "utf8"), "utf8").toString("base64");
commands.push(
`relative_path=${shellQuote(relativePath)}`,
`target_file="$target_root/$relative_path"`,
"mkdir -p \"$(dirname \"$target_file\")\"",
`printf %s ${shellQuote(encoded)} | base64 -d > "$target_file"`,
"printf 'synced_k3s_control_manifest=%s\\n' \"$target_file\"",
);
}
return commands.join("\n");
}
function buildImageScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): string {
const image = buildImageTag(service);
const workDir = targetWorkDir(service);
const dockerfile = `${workDir}/${service.repository.dockerfile}`;
const buildContext = sourceBuildContext(service);
const dockerfilePath = sourceDockerfilePath(service);
const dockerfile = `${buildContext}/${dockerfilePath}`;
const labelArgs = [
"--label", `unidesk.ai/service-id=${service.id}`,
"--label", `unidesk.ai/source-repo=${desired.repo}`,
"--label", `unidesk.ai/source-commit=${resolvedCommit}`,
"--label", `unidesk.ai/dockerfile=${service.repository.dockerfile}`,
"--label", `unidesk.ai/dockerfile=${dockerfilePath}`,
];
const commonArgs = [
"--progress=plain",
...labelArgs,
"-t", image,
"-f", dockerfile,
workDir,
buildContext,
];
const proxyBuildArgs = targetIsMain(service)
? []
@@ -1399,6 +1468,12 @@ async function applyOneService(config: UniDeskConfig, service: UniDeskMicroservi
const sync = await step(config, service, "sync-source", syncSourceScript(service, exportDir), targetIsMain(service) ? repoRoot : "/home/ubuntu", 90_000, false);
if (!pushStep(steps, sync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
const controlManifestSyncScript = syncK8sControlManifestsScript(service);
if (controlManifestSyncScript.length > 0) {
const controlManifestSync = await step(config, service, "sync-k3s-control-manifests", controlManifestSyncScript, targetIsMain(service) ? repoRoot : "/home/ubuntu", 90_000, !targetIsMain(service));
if (!pushStep(steps, controlManifestSync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
}
const buildScript = service.deployment.mode === "unidesk-direct"
? buildDirectImageScript(service, desired, resolvedCommit)
: buildImageScript(service, desired, resolvedCommit);
+1 -1
View File
@@ -152,7 +152,7 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
UNIDESK_OA_EVENT_FLOW_PORT: runtimeSecret("UNIDESK_OA_EVENT_FLOW_PORT") || "4255",
UNIDESK_OA_EVENT_FLOW_BIND_HOST: runtimeSecretWithDefault("UNIDESK_OA_EVENT_FLOW_BIND_HOST", restrictedHostBind, "127.0.0.1"),
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED") || "true",
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL") || "http://backend-core:8080/api/microservices/claudeqq/proxy",
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL") || "http://claudeqq.unidesk.svc.cluster.local:3290",
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE") || "private",
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID") || "645275593",
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID"),
+17 -2
View File
@@ -1124,6 +1124,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
}>;
} }).body;
const k3sctlCodeQueueService = k3sctlControlPlaneBody?.services?.find((service) => service.id === "code-queue");
const k3sctlClaudeqqService = k3sctlControlPlaneBody?.services?.find((service) => service.id === "claudeqq");
const k3sctlD518Instance = k3sctlCodeQueueService?.instances?.find((instance) => instance.id === "D518");
const filebrowserHealthBody = (filebrowserHealth as { body?: { status?: string } }).body;
const filebrowserD601HealthBody = (filebrowserD601Health as { body?: { status?: string } }).body;
@@ -1152,7 +1153,15 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "microservice:catalog-findjob", (microservices as { ok?: boolean }).ok === true && findjob?.providerId === "D601" && findjob.backend?.public === false, { microservices });
addSelectedCheck(checks, options, "microservice:catalog-pipeline", (microservices as { ok?: boolean }).ok === true && pipeline?.providerId === "D601" && pipeline.backend?.public === false && pipeline.runtime?.container?.name === "pipeline-v2-control", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-met-nonlinear", (microservices as { ok?: boolean }).ok === true && metNonlinear?.providerId === "D601" && metNonlinear.backend?.public === false && metNonlinear.runtime?.container?.name === "met-nonlinear-ts", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-claudeqq", (microservices as { ok?: boolean }).ok === true && claudeqq?.providerId === "D601" && claudeqq.backend?.public === false && claudeqq.runtime?.container?.name === "claudeqq-backend", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-claudeqq",
(microservices as { ok?: boolean }).ok === true
&& claudeqq?.providerId === "D601"
&& claudeqq.backend?.public === false
&& claudeqq.backend?.proxyMode === "k3sctl-adapter-http"
&& claudeqq.deployment?.mode === "k3sctl-managed"
&& claudeqq.runtime?.orchestrator === "k3sctl"
&& claudeqq.runtime?.container === null,
{ microservices });
addSelectedCheck(checks, options, "microservice:catalog-todo-note", (microservices as { ok?: boolean }).ok === true && todoNote?.providerId === config.providerGateway.id && todoNote.backend?.public === false && todoNote.runtime?.container?.name === "todo-note-backend", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-oa-event-flow", (microservices as { ok?: boolean }).ok === true && oaEventFlow?.providerId === config.providerGateway.id && oaEventFlow.backend?.public === false && oaEventFlow.runtime?.container?.name === "oa-event-flow-backend", { microservices });
addSelectedCheck(checks, options, "microservice:catalog-code-queue",
@@ -1181,6 +1190,11 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
&& k3sctlCodeQueueService?.servingHealthy === true
&& k3sctlCodeQueueService?.active?.id === "D601"
&& k3sctlCodeQueueService?.active?.healthy === true
&& k3sctlClaudeqqService?.status === "healthy"
&& k3sctlClaudeqqService?.topologyComplete === true
&& k3sctlClaudeqqService?.servingHealthy === true
&& k3sctlClaudeqqService?.active?.id === "D601"
&& k3sctlClaudeqqService?.active?.healthy === true
&& (k3sctlCodeQueueService?.presentNodeIds ?? []).includes("D601")
&& (k3sctlCodeQueueService?.presentNodeIds ?? []).includes("D518")
&& (k3sctlCodeQueueService?.missingNodeIds ?? []).length === 0
@@ -1192,6 +1206,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
noFallback: k3sctlControlPlaneBody?.noFallback,
kubeApiProxy: k3sctlControlPlaneBody?.kubeApiProxy,
service: k3sctlCodeQueueService,
claudeqq: k3sctlClaudeqqService,
});
addSelectedCheck(checks, options, "microservice:catalog-filebrowser", (microservices as { ok?: boolean }).ok === true
&& filebrowser?.providerId === "D518"
@@ -2831,7 +2846,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "frontend:code-queue-judge-wrap",
codexJudgeWrapMetrics.checked === true && codexJudgeWrapMetrics.ok === true,
{ codexJudgeWrapMetrics });
addSelectedCheck(checks, options, "frontend:claudeqq-integrated-visible", claudeqqTextLower.includes("claudeqq 工作台") && claudeqqText.includes("D601") && claudeqqText.includes("QQ 事件订阅") && claudeqqText.includes("消息推送") && claudeqqText.includes("事件缓存") && claudeqqText.includes("主用户私聊账号") && claudeqqText.includes("645275593") && claudeqqTextLower.includes("napcat 容器登录") && claudeqqText.includes("已登录") && /health\s+ok/i.test(claudeqqText) && claudeqqText.includes("仅 UniDesk frontend 代理访问") && !claudeqqText.includes("{\n"), { claudeqqTextPreview: claudeqqText.slice(0, 1400) });
addSelectedCheck(checks, options, "frontend:claudeqq-integrated-visible", claudeqqTextLower.includes("claudeqq 工作台") && claudeqqText.includes("D601") && claudeqqText.includes("D601 k3s Service") && claudeqqText.includes("k3s://unidesk/claudeqq") && claudeqqText.includes("QQ 事件订阅") && claudeqqText.includes("消息推送") && claudeqqText.includes("事件缓存") && claudeqqText.includes("主用户私聊账号") && claudeqqText.includes("645275593") && claudeqqTextLower.includes("napcat 容器登录") && claudeqqText.includes("已登录") && /health\s+ok/i.test(claudeqqText) && claudeqqText.includes("仅 UniDesk frontend 代理访问") && !claudeqqText.includes("{\n"), { claudeqqTextPreview: claudeqqText.slice(0, 1400) });
addSelectedCheck(checks, options, "frontend:url-route-deeplink", routeInitialPath === "/app/pipeline/" && routeDockerPath === "/nodes/docker/" && routeBackPath === "/app/pipeline/" && routeOverviewPath === "/ops/status/" && routeCodexPath === "/app/code-queue/" && routeDeepLinkText.toLowerCase().includes("pipeline v2 工作台".toLowerCase()) && routeOverviewText.includes("核心指标") && routeCodexShellMetrics.appShell === true && routeCodexShellMetrics.standalone === false && routeCodexShellMetrics.topbar === true && routeCodexShellMetrics.codexPage === true && String(routeCodexShellMetrics.railText || "").includes("用户服务") && String(routeCodexShellMetrics.tabsText || "").includes("Code Queue"), { routeInitialPath, routeDockerPath, routeBackIntermediatePath, routeBackPath, routeOverviewPath, routeCodexPath, routeCodexShellMetrics, routeDeepLinkPreview: routeDeepLinkText.slice(0, 1200), routeOverviewPreview: routeOverviewText.slice(0, 800) });
addSelectedCheck(checks, options, "frontend:pipeline-integrated-visible",
pipelineTextLower.includes("pipeline v2 工作台".toLowerCase())
+2 -2
View File
@@ -286,9 +286,9 @@ export function ClaudeQqPage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyR
h("code", null, repository.commitId || "--"),
),
h("div", { className: "microservice-ref-card" },
h("span", null, "D601 Docker"),
h("span", null, "D601 k3s Service"),
h("strong", null, `${backend.nodeBindHost || "--"}:${backend.nodePort || "--"}`),
h("code", null, `${repository.composeFile || "--"} / ${repository.composeService || "--"}`),
h("code", null, `${backend.proxyMode || "--"} / ${repository.composeFile || "--"}`),
),
),
h(UniDeskErrorBanner, { error: state.error, wide: true }),
@@ -50,15 +50,15 @@ services:
CODE_QUEUE_DEV_CONTAINER_WORKDIR: "${CODE_QUEUE_DEV_CONTAINER_WORKDIR:-/home/ubuntu}"
CODE_QUEUE_EGRESS_PROXY_ENABLED: "${CODE_QUEUE_EGRESS_PROXY_ENABLED:-true}"
CODE_QUEUE_EGRESS_PROXY_URL: "${CODE_QUEUE_EGRESS_PROXY_URL:-http://unidesk-provider-gateway-D601:18789}"
CODE_QUEUE_EGRESS_PROXY_NO_PROXY: "${CODE_QUEUE_EGRESS_PROXY_NO_PROXY:-localhost,127.0.0.1,::1,host.docker.internal,unidesk-provider-gateway-D601,74.48.78.17,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com}"
CODE_QUEUE_EGRESS_PROXY_NO_PROXY: "${CODE_QUEUE_EGRESS_PROXY_NO_PROXY:-localhost,127.0.0.1,::1,host.docker.internal,claudeqq,claudeqq.unidesk,claudeqq.unidesk.svc,claudeqq.unidesk.svc.cluster.local,unidesk-provider-gateway-D601,74.48.78.17,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com}"
HTTP_PROXY: "${CODE_QUEUE_EGRESS_PROXY_URL:-http://unidesk-provider-gateway-D601:18789}"
HTTPS_PROXY: "${CODE_QUEUE_EGRESS_PROXY_URL:-http://unidesk-provider-gateway-D601:18789}"
ALL_PROXY: "${CODE_QUEUE_EGRESS_PROXY_URL:-http://unidesk-provider-gateway-D601:18789}"
http_proxy: "${CODE_QUEUE_EGRESS_PROXY_URL:-http://unidesk-provider-gateway-D601:18789}"
https_proxy: "${CODE_QUEUE_EGRESS_PROXY_URL:-http://unidesk-provider-gateway-D601:18789}"
all_proxy: "${CODE_QUEUE_EGRESS_PROXY_URL:-http://unidesk-provider-gateway-D601:18789}"
NO_PROXY: "${CODE_QUEUE_EGRESS_PROXY_NO_PROXY:-localhost,127.0.0.1,::1,host.docker.internal,unidesk-provider-gateway-D601,74.48.78.17,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com}"
no_proxy: "${CODE_QUEUE_EGRESS_PROXY_NO_PROXY:-localhost,127.0.0.1,::1,host.docker.internal,unidesk-provider-gateway-D601,74.48.78.17,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com}"
NO_PROXY: "${CODE_QUEUE_EGRESS_PROXY_NO_PROXY:-localhost,127.0.0.1,::1,host.docker.internal,claudeqq,claudeqq.unidesk,claudeqq.unidesk.svc,claudeqq.unidesk.svc.cluster.local,unidesk-provider-gateway-D601,74.48.78.17,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com}"
no_proxy: "${CODE_QUEUE_EGRESS_PROXY_NO_PROXY:-localhost,127.0.0.1,::1,host.docker.internal,claudeqq,claudeqq.unidesk,claudeqq.unidesk.svc,claudeqq.unidesk.svc.cluster.local,unidesk-provider-gateway-D601,74.48.78.17,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com}"
CODE_QUEUE_WINDOWS_NATIVE_CODEX_DEFAULT_WORKDIR: "${CODE_QUEUE_WINDOWS_NATIVE_CODEX_DEFAULT_WORKDIR:-/mnt/f/Work/ConStart}"
CODE_QUEUE_WINDOWS_NATIVE_CODEX_BRIDGE_DIR: "${CODE_QUEUE_WINDOWS_NATIVE_CODEX_BRIDGE_DIR:-/home/ubuntu/.unidesk/code-queue/windows-native-codex}"
CODE_QUEUE_WINDOWS_NATIVE_CODEX_COMMAND: "${CODE_QUEUE_WINDOWS_NATIVE_CODEX_COMMAND:-codex app-server --listen stdio://}"
@@ -66,7 +66,7 @@ services:
CODE_QUEUE_WINDOWS_NATIVE_CODEX_IDLE_TIMEOUT_MS: "${CODE_QUEUE_WINDOWS_NATIVE_CODEX_IDLE_TIMEOUT_MS:-600000}"
OA_EVENT_FLOW_BASE_URL: "${OA_EVENT_FLOW_BASE_URL:-http://74.48.78.17:4255}"
CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: "${CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED:-true}"
CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: "${CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL:-http://host.docker.internal:3290}"
CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: "${CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL:-http://claudeqq.unidesk.svc.cluster.local:3290}"
CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE: "${CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE:-private}"
CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID: "${CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID:-645275593}"
CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID: "${CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID:-}"
@@ -384,7 +384,7 @@ function readConfig(): RuntimeConfig {
databaseFlushIntervalMs: Math.max(100, Math.min(10_000, envNumber("CODE_QUEUE_DATABASE_FLUSH_INTERVAL_MS", 1000))),
oaEventFlowBaseUrl: envString("OA_EVENT_FLOW_BASE_URL", "http://oa-event-flow:4255").replace(/\/+$/u, ""),
notifyClaudeQqEnabled: envBool("CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED", false),
notifyClaudeQqBaseUrl: envString("CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL", "http://backend-core:8080/api/microservices/claudeqq/proxy").replace(/\/+$/u, ""),
notifyClaudeQqBaseUrl: envString("CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL", "http://claudeqq.unidesk.svc.cluster.local:3290").replace(/\/+$/u, ""),
notifyClaudeQqTargetType: notifyTargetTypeRaw === "group" ? "group" : "private",
notifyClaudeQqUserId: envString("CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID", "645275593").trim(),
notifyClaudeQqGroupId: envString("CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID", "").trim(),
@@ -39,7 +39,7 @@ services:
K3SCTL_NATIVE_SERVICE_TUNNEL_CONNECT_TIMEOUT_MS: "${K3SCTL_NATIVE_SERVICE_TUNNEL_CONNECT_TIMEOUT_MS:-3000}"
K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE: "${K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE:-}"
K3SCTL_NATIVE_SERVICE_URL_MDTODO: "${K3SCTL_NATIVE_SERVICE_URL_MDTODO:-}"
K3SCTL_MANIFEST_PATHS: "${K3SCTL_MANIFEST_PATHS:-k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json}"
K3SCTL_MANIFEST_PATHS: "${K3SCTL_MANIFEST_PATHS:-k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json,k3s/claudeqq.k3s.json}"
K3SCTL_SERVICES_JSON: "${K3SCTL_SERVICES_JSON:-[]}"
UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}"
volumes:
@@ -0,0 +1,37 @@
{
"apiVersion": "unidesk.ai/k3s/v1",
"kind": "ManagedKubernetesService",
"metadata": {
"name": "claudeqq",
"namespace": "unidesk"
},
"spec": {
"adapterServiceId": "k3sctl-adapter",
"controlPlane": {
"type": "kubernetes",
"cluster": "unidesk-k3s",
"context": "unidesk-k3s"
},
"route": {
"kind": "kubernetes-service",
"serviceName": "claudeqq",
"servicePort": 3290
},
"activeInstanceId": "D601",
"singleWriter": true,
"expectedNodeIds": [
"D601"
],
"instances": [
{
"id": "D601",
"nodeId": "D601",
"role": "primary",
"baseUrl": "kubernetes://unidesk/services/claudeqq:3290",
"healthPath": "/health",
"healthMode": "service-proxy"
}
],
"requireAllInstancesHealthy": true
}
}
@@ -0,0 +1,213 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: claudeqq
namespace: unidesk
labels:
app.kubernetes.io/name: claudeqq
app.kubernetes.io/part-of: unidesk
unidesk.ai/deployment-mode: k3sctl-managed
unidesk.ai/instance-id: D601
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: claudeqq
unidesk.ai/instance-id: D601
template:
metadata:
labels:
app.kubernetes.io/name: claudeqq
app.kubernetes.io/part-of: unidesk
unidesk.ai/deployment-mode: k3sctl-managed
unidesk.ai/instance-id: D601
unidesk.ai/node-id: D601
spec:
nodeSelector:
unidesk.ai/node-id: D601
terminationGracePeriodSeconds: 30
containers:
- name: napcat
image: mlikiowa/napcat-docker:latest
imagePullPolicy: IfNotPresent
ports:
- name: napcat-http
containerPort: 3000
- name: napcat-ws
containerPort: 3001
- name: napcat-webui
containerPort: 6099
env:
- name: WEBUI_PREFIX
value: "/webui"
- name: WEBUI_TOKEN
value: "unidesk-napcat"
- name: NAPCAT_UID
value: "1000"
- name: NAPCAT_GID
value: "1000"
- name: ACCOUNT
value: "763382329"
volumeMounts:
- name: napcat-qq
mountPath: /app/.config/QQ
- name: napcat-config
mountPath: /app/napcat/config
- name: napcat-cache
mountPath: /app/napcat/cache
readinessProbe:
tcpSocket:
port: napcat-http
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 12
livenessProbe:
tcpSocket:
port: napcat-http
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 12
- name: claudeqq
image: unidesk-claudeqq:d601
imagePullPolicy: IfNotPresent
workingDir: /app/scripts/src/server_ts
command:
- node
- dist/index.js
- --max-restarts
- "0"
ports:
- name: http
containerPort: 3290
env:
- name: CLAUDEQQ_HOST
value: "0.0.0.0"
- name: CLAUDEQQ_PORT
value: "3290"
- name: CLAUDEQQ_WORKSPACE_DIR
value: "/bot_workspace"
- name: CLAUDEQQ_AUTO_REPLY
value: "false"
- name: CLAUDEQQ_NAPCAT_HTTP_HOST
value: "127.0.0.1"
- name: CLAUDEQQ_NAPCAT_HTTP_PORT
value: "3000"
- name: CLAUDEQQ_NAPCAT_WS_HOST
value: "127.0.0.1"
- name: CLAUDEQQ_NAPCAT_WS_PORT
value: "3001"
- name: CLAUDEQQ_NAPCAT_ACCOUNT
value: "763382329"
- name: CLAUDEQQ_NAPCAT_DATA_DIR
value: "/napcat"
- name: CLAUDEQQ_NAPCAT_WEBUI_URL
value: "http://127.0.0.1:6099/webui"
- name: CLAUDEQQ_ONLINE_NOTICE_ENABLED
value: "true"
- name: CLAUDEQQ_ONLINE_NOTICE_USER_ID
value: "645275593"
- name: CLAUDEQQ_LOGIN_MONITOR_INTERVAL_MS
value: "10000"
- name: UNIDESK_DEPLOY_SERVICE_ID
value: "claudeqq"
volumeMounts:
- name: config-json
mountPath: /app/config.json
readOnly: true
- name: bot-workspace
mountPath: /bot_workspace
- name: logs
mountPath: /app/logs
- name: state
mountPath: /app/.state
- name: napcat-root
mountPath: /napcat
readOnly: true
- name: napcat-shell
mountPath: /app/NapCat.Shell.Windows.OneKey
readOnly: true
readinessProbe:
httpGet:
path: /health
port: http
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 20
livenessProbe:
httpGet:
path: /health
port: http
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 12
startupProbe:
httpGet:
path: /health
port: http
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 60
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
volumes:
- name: config-json
hostPath:
path: /home/ubuntu/.agents/skills/claudeqq/config.json
type: File
- name: bot-workspace
hostPath:
path: /home/ubuntu/.agents/skills/claudeqq/bot_workspace
type: DirectoryOrCreate
- name: logs
hostPath:
path: /home/ubuntu/.agents/skills/claudeqq/logs
type: DirectoryOrCreate
- name: state
hostPath:
path: /home/ubuntu/.agents/skills/claudeqq/.state
type: DirectoryOrCreate
- name: napcat-root
hostPath:
path: /home/ubuntu/.agents/skills/claudeqq/napcat
type: DirectoryOrCreate
- name: napcat-qq
hostPath:
path: /home/ubuntu/.agents/skills/claudeqq/napcat/qq
type: DirectoryOrCreate
- name: napcat-config
hostPath:
path: /home/ubuntu/.agents/skills/claudeqq/napcat/config
type: DirectoryOrCreate
- name: napcat-cache
hostPath:
path: /home/ubuntu/.agents/skills/claudeqq/napcat/cache
type: DirectoryOrCreate
- name: napcat-shell
hostPath:
path: /home/ubuntu/.agents/skills/claudeqq/NapCat.Shell.Windows.OneKey
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: claudeqq
namespace: unidesk
labels:
app.kubernetes.io/name: claudeqq
app.kubernetes.io/part-of: unidesk
unidesk.ai/deployment-mode: k3sctl-managed
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: claudeqq
unidesk.ai/instance-id: D601
ports:
- name: http
port: 3290
targetPort: http
@@ -988,7 +988,7 @@ spec:
- name: CODE_QUEUE_EGRESS_PROXY_URL
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
- name: CODE_QUEUE_EGRESS_PROXY_NO_PROXY
value: "localhost,127.0.0.1,::1,host.docker.internal,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,172.25.0.3,unidesk-provider-gateway-D601,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com"
value: "localhost,127.0.0.1,::1,host.docker.internal,claudeqq,claudeqq.unidesk,claudeqq.unidesk.svc,claudeqq.unidesk.svc.cluster.local,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,172.25.0.3,unidesk-provider-gateway-D601,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com"
- name: HTTP_PROXY
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
- name: HTTPS_PROXY
@@ -1002,15 +1002,15 @@ spec:
- name: all_proxy
value: "http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789"
- name: NO_PROXY
value: "localhost,127.0.0.1,::1,host.docker.internal,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,172.25.0.3,unidesk-provider-gateway-D601,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com"
value: "localhost,127.0.0.1,::1,host.docker.internal,claudeqq,claudeqq.unidesk,claudeqq.unidesk.svc,claudeqq.unidesk.svc.cluster.local,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,172.25.0.3,unidesk-provider-gateway-D601,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com"
- name: no_proxy
value: "localhost,127.0.0.1,::1,host.docker.internal,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,172.25.0.3,unidesk-provider-gateway-D601,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com"
value: "localhost,127.0.0.1,::1,host.docker.internal,claudeqq,claudeqq.unidesk,claudeqq.unidesk.svc,claudeqq.unidesk.svc.cluster.local,d601-provider-egress-proxy,d601-provider-egress-proxy.unidesk,d601-provider-egress-proxy.unidesk.svc,d601-provider-egress-proxy.unidesk.svc.cluster.local,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,172.25.0.3,unidesk-provider-gateway-D601,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com"
- name: OA_EVENT_FLOW_BASE_URL
value: "http://d601-tcp-egress-gateway.unidesk.svc.cluster.local:4255"
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED
value: "true"
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL
value: "http://host.docker.internal:3290"
value: "http://claudeqq.unidesk.svc.cluster.local:3290"
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE
value: "private"
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID
@@ -274,7 +274,7 @@ function mergeServices(services: ManagedService[]): ManagedService[] {
}
function readConfig(): RuntimeConfig {
const paths = manifestPaths(envString("K3SCTL_MANIFEST_PATHS", "k3s/code-queue.k3s.json"));
const paths = manifestPaths(envString("K3SCTL_MANIFEST_PATHS", "k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json,k3s/claudeqq.k3s.json"));
const inlineServices = parseServices(envString("K3SCTL_SERVICES_JSON", "[]"));
const manifestServices = readManifestServices(paths);
return {