huiwings 的目录、域名,以及 Agent 怎么推代码看 CI

最近把 huiwings 的目录又对了一遍,在这里记录一下。开发、部署、测试都在 Agent 里做。人要看清的是代码在哪一个 git 根,push 到哪,以及哪一条 CI 真的会跑。

角色从写代码转到定边界,之前记过12。认知上的六条见《AI 辅助编程中沉淀的六个核心洞见》3。下面是现在磁盘上的布局和流水线。

一、这台机器上的目录

工作区叫 huiwings。以前的笔记里同一个目录写作 huiwing-migration。当前布局如下:

huiwings/
├── monorepo/                  # 唯一的私有 git 根,分支 main
│   ├── AGENTS.md -> plan_skills/AGENTS.md
│   ├── AuthCore -> ../AuthCore
│   ├── AuthCoreJS -> ../AuthCoreJS
│   ├── ai-api/
│   ├── etl-tasks/
│   ├── htysso-web/
│   ├── htyteacher/
│   ├── htyts-web/
│   ├── huike-back/
│   ├── huike-e2e-moicen/
│   ├── huike-front/
│   ├── official-website/
│   ├── plan_skills/
│   └── resty_functions/
├── AuthCore/                  # GitHub alchemy-studio/AuthCore
├── AuthCoreJS/
├── authcoreadmin/             # GitHub alchemy-studio/AuthCoreAdmin
├── task_server/               # 旧 Java,不进每日循环
├── proc_server/
├── task_commons/
└── midi2musicxml、transcript、tsv2

可以看到,私有业务只有一个 git 根:monorepo。远端是 ci.moicen.com 上的 weli/monorepo。plan_skills 是给 Agent 读的手册,根上的 AGENTS.md 是指过去的符号链接。

并列的三个目录各自有 .git,远端在 GitHub:

monorepo/AuthCore 和 monorepo/AuthCoreJS 是指向上级目录的符号链接,不是第二份历史。huike-back 用 path patch 指到这条链接上:

# huike-back/Cargo.toml:14-19
# Path must stay *outside* this workspace so patched crates keep AuthCore's own [workspace.dependencies].
# Clone/symlink: monorepo `AuthCore` as sibling `../AuthCore` (see `.github/workflows/ci.yml`).
[patch."https://github.com/alchemy-studio/AuthCore.git"]
htycommons = { path = "../AuthCore/htycommons" }
htyuc_models = { path = "../AuthCore/htyuc_models" }
htyuc_remote = { path = "../AuthCore/htyuc_remote" }

htysso-web 的 package.json 里,@authcore/commons 是 file:../AuthCoreJS,同样靠这根链接。

洞见三说,一个仓库里 Agent 才能同时看见接口、调用方和测试,一次提交改完3。私有代码收进 monorepo 就是这件事:网关、后端、前端、E2E 在同一个 main 上。AuthCore 三仓留在 GitHub,因为它们是公开协议仓,历史不并进私有仓库。这是那篇「多仓库难协调」的例外,例外的原因是协议要公开。

task_server、proc_server、task_commons 还在磁盘上,远端仍是 GitHub 上的同名仓库。现网任务栈已经是 Rust 的 htyts / htyproc,迁移过程见 3 月那篇4。midi2musicxml、transcript、tsv2 也不进每日业务循环。

flowchart TB
  subgraph ac [AuthCore]
    UcBin[htyuc]
    AcCrates["htycommons htyuc_models htyuc_remote"]
    UcBin --> AcCrates
  end
  subgraph acjs [AuthCoreJS]
    Sdk["@authcore/commons"]
  end
  subgraph hb [huike-back]
    Ws[htyws]
    WsModels[htyws_models]
    Kc[htykc]
    Ts[htyts]
    TsModels[htyts_models]
    Proc[htyproc]
    Ws --> WsModels
    WsModels --> AcCrates
    Kc --> WsModels
    Kc --> AcCrates
    Ts --> TsModels
    TsModels --> AcCrates
    Proc --> TsModels
    Proc --> AcCrates
  end
  Front[huike-front] --> Sdk
  TsWeb[htyts-web] --> Sdk
  Sso[htysso-web] --> Sdk

可以看到,箭头是 Cargo 和 package.json 里的依赖,不是目录的并列。htyws、htykc 用到 AuthCore 的 htyuc_models 和 htyuc_remote,htykc 再依赖 htyws_models。htyts、htyproc 用 htycommons,htyproc 再依赖 htyts_models。huike-front、htyts-web、htysso-web 的 @authcore/commons 是 file:../AuthCoreJS。htyteacher 和 authcoreadmin 没有这层编译依赖,它们只在运行时打 HTTP。resty_functions 是 OpenResty 配置,也不在这张依赖图里。

二、两套域名

测试机对外是 moicen.com,正式机对外是 alchemy-studio.cn。两边的网关都是 OpenResty,配置在 resty_functions。浏览器先到网关,再进静态目录或后面的进程。

站点 测试 正式 代码
音乐教室 music-room.moicen.com music-room.huiwings.cn huike-front
管理端 admin.moicen.com admin.huiwings.cn authcoreadmin
教师 teacher.moicen.com teacher.huiwings.cn htyteacher
任务 ts.moicen.com ts.huiwings.cn htyts-web
登录 sso.moicen.com sso.huiwings.cn htysso-web
AI ai.moicen.com ai.alchemy-studio.cn ai-api
处理 proc.moicen.com proc.huiwings.cn htyproc

另外两个名字只在测试机上:ci.moicen.com 是 Gitea,ticket.moicen.com 是工单。ai.moicen.com 的 /api/v1/ai/ 反代到本机的 ai-api。它不是本站那些 Spring AI、LangChain 的 Demo。

进程来自上面那张依赖图:htyuc 是 AuthCore 的二进制,htyws、htykc、htyts、htyproc 是 huike-back 的二进制。两台机器的监听端口一样。htyproc 的 env 把本机地址写死了:

# huike-back/envs/moicen/htyproc.env 与 envs/huiwings/htyproc.env
TS_URL=http://127.0.0.1:3003
HTYUC_URL=http://127.0.0.1:3000
HTYWS_URL=http://127.0.0.1:3001
AI_URL=https://ai.moicen.com
PROC_PORT=3004

OpenResty 的 admin.conf 在测试服和正式服都把 /api/v1/uc、/api/v1/ws、/api/v1/kc 转到 3000、3001、3002,把 htyts、htyproc 转到 127.0.0.1:3003、127.0.0.1:3004。ai.conf 把 /api/v1/ai/ 转到 127.0.0.1:5000。

音乐教室的 vite.config.ts 不把 UC、WS、KC 打到自己的域名上,两种构建都指向 admin.*。任务和 AI 的地址两边不一样:

// huike-front/vite.config.ts
// moicen
kc_server: 'https://admin.moicen.com',
ts_server: '',
ai_server: 'https://ai.alchemy-studio.cn',
assets_base: "/"
// huiwing
kc_server: 'https://admin.huiwings.cn',
ts_server: 'https://ts.huiwings.cn',
ai_server: 'https://ai.alchemy-studio.cn',
assets_base: "https://static.huiwings.cn"

ts_server 为空时,测试服的任务轮询走 music-room.moicen.com 同域的 /api/v2/ts/,由这份站点自己的 nginx 转到 3003。htyteacher、htysso-web 的接口也指向 admin.*。htyts-web 在测试服把 TS、UC、WS 都指到 admin.moicen.com,在正式服把 TS 指到 ts.huiwings.cn,UC 和 WS 仍指到 admin.huiwings.cn。

flowchart TB
  subgraph testBox [测试服 moicen.com]
    direction TB
    Tspa["huike-front htyteacher htysso-web htyts-web authcoreadmin"]
    Tgw[OpenResty]
    Tuc["htyuc :3000"]
    Tws["htyws :3001"]
    Tkc["htykc :3002"]
    Tts["htyts :3003"]
    Tproc["htyproc :3004"]
    Tai["ai-api :5000"]
    Tdata["PostgreSQL Redis"]
    Tspa --> Tgw
    Tgw -->|/api/v1/uc| Tuc
    Tgw -->|/api/v1/ws| Tws
    Tgw -->|/api/v1/kc| Tkc
    Tgw -->|/api/v2/ts| Tts
    Tgw -->|/api/v2/proc| Tproc
    Tgw -->|ai.moicen.com| Tai
    Tts --> Tuc
    Tts --> Tkc
    Tproc --> Tts
    Tproc --> Tuc
    Tproc --> Tws
    Tuc --> Tdata
    Tws --> Tdata
    Tkc --> Tdata
    Tts --> Tdata
  end

  subgraph prodBox [正式服 alchemy-studio.cn]
    direction TB
    Pspa["同五个前端 index 在本机"]
    Pcdn["static.huiwings.cn"]
    Pgw[OpenResty]
    Puc["htyuc :3000"]
    Pws["htyws :3001"]
    Pkc["htykc :3002"]
    Pts["htyts :3003"]
    Pproc["htyproc :3004"]
    Pai["ai-api :5000"]
    Pdata["PostgreSQL Redis"]
    Pspa --> Pcdn
    Pspa --> Pgw
    Pgw -->|admin.huiwings.cn| Puc
    Pgw -->|admin.huiwings.cn| Pws
    Pgw -->|admin.huiwings.cn| Pkc
    Pgw -->|ts.huiwings.cn| Pts
    Pgw -->|/api/v2/proc| Pproc
    Pgw -->|ai.alchemy-studio.cn| Pai
    Pts --> Puc
    Pts --> Pkc
    Pproc --> Pts
    Pproc --> Puc
    Pproc --> Pws
    Puc --> Pdata
    Pws --> Pdata
    Pkc --> Pdata
    Pts --> Pdata
  end

  Tspa -->|ai_server| Pai
  Pproc -->|AI_URL| Tai

如上所示,每台机器自己跑一套进程,不是两台共用一组。页面先到本机 OpenResty。UC、WS、KC 汇合到 admin.*。测试服的任务轮询留在 music-room 同域,正式服改走 ts.huiwings.cn。正式服音乐教室的 JS/CSS 在 static.huiwings.cn,测试服的 assets_base 是 /,文件跟页面在同一台 OpenResty。

跨机的只有两条。音乐教室两种构建的 ai_server 都写成 ai.alchemy-studio.cn,所以测试服页面打的是正式服的 ai-api。htyproc 两份 env 的 AI_URL 都是 ai.moicen.com,正式服的处理进程反过来打测试服的 AI。Gitea 和工单只在测试服,不在这条业务链上。

构建模式跟域名绑在一起:测试 build:moicen,正式 build:huiwing。静态目录对照写在 plan_skills/guide/project-work-handbook.md。上线是那台机器上 git pull --ff-only 再构建:私有仓从 ci.moicen.com 拉,AuthCore 从 GitHub 拉。

测试服相对正式服,就是洞见四里说的那份可复刻环境3。页面是否真的出来,用 Playwright 看,也是那一节写的观测。

三、Agent 一天怎么走

Agent 先读 plan_skills/AGENTS.md 和 plan_skills/guide/huiwing-agent-harness.md。3 月那篇把计划写成边界4,现在这份边界就在 plan_skills 里。联网之前先挂代理,git push 和 gh 都走它:

$ eval "$(huiwing-proxy-export)"

改完按仓推。私有子目录:

$ git push origin main

远端是 weli/monorepo。开源仓以 AuthCore 为例:

$ git push origin main
$ gh run list --repo alchemy-studio/AuthCore --limit 5
$ gh run watch --repo alchemy-studio/AuthCore

AuthCore 的 Rust workflow 在 push 或 PR 到 main 时跑 cargo build,并在 Postgres、Redis 上跑 htyuc 的 e2e。gh 看的是这条。AuthCoreJS 的 workflow 文件写的触发分支是 master,当前检出的分支是 main,两边对不上,push main 不会匹配那份 on。authcoreadmin 仓库里没有 Actions 文件,管理端在服务器上构建。

sequenceDiagram
  participant Agent as Agent
  participant Git as 本地 git
  participant Gitea as Gitea
  participant Hub as GitHub
  participant Gh as gh

  Agent->>Git: 提交
  alt 私有 monorepo
    Git->>Gitea: push main
    Note over Gitea: 仓库根没有 workflow 目录
  else AuthCore
    Git->>Hub: push main
    Hub-->>Gh: Rust workflow
    Agent->>Gh: gh run watch
  end

私有仓里其实放着 Playwright。huike-e2e-moicen/.gitea/workflows/playwright-music-room.yml 写的是 push 到 main 默认跑 --project=smoke,会改数据的 project 用 workflow_dispatch 指定。这样是为了避免每次 push 都去创建打卡、发微信通知。同目录的 .github/workflows/playwright-production.yml 文件头写着:正式服冒烟只手动触发,加每天一次的 cron,不跟随 push。

这两份文件现在不在 git 根上。Gitea 找 workflow 的函数在 modules/actions/workflows.go 的 listWorkflowsInDirs:

// modules/actions/workflows.go:83-86
for _, workflowDir = range dirs {
    tree, err = commit.SubTree(ctx, gitRepo, workflowDir)
    if err == nil {
        break
    }

如上所示,循环在第一个存在的目录上停。默认目录顺序写在 modules/setting/actions.go 里,是 .gitea/workflows,然后才是 .github/workflows。维护者在 issue 里也说过:先看 .gitea,没有再退回 .github,不把两个目录合并跑5。

weli/monorepo 的根上这两个目录都没有。Playwright 的 yml 在 huike-e2e-moicen/ 下面。official-website、htyteacher、etl-tasks 子目录里的 .github/workflows 同样不在查找路径上。所以 push monorepo 的 main,Gitea 不会把这些文件排队。测试机上的 act_runner 只跑它真正领到的任务。

flowchart TD
  Root[仓库根]
  Root --> D1[".gitea/workflows"]
  D1 -->|目录存在| Run1[采用该目录并停止]
  D1 -->|不存在| D2[".github/workflows"]
  D2 -->|目录存在| Run2[采用该目录并停止]
  D2 -->|不存在| None[这次 push 没有 workflow]
  Nested["子目录里的 yml"] -.-> None

站点更新走另一条。在对应的机器上 git pull --ff-only,再用那台机器的构建模式产出静态文件或重启进程。2 月那张「合并后自动部署」的图是抽象版2。这里的门禁是 AuthCore 的 cargo test,以及 Playwright 文件里写的 smoke。现网没有 SonarQube,也没有 push 之后自动合并。正式服冒烟的 cron 写在子目录那份 yml 里,按上面的查找规则,它现在也不会被 monorepo 的 push 或仓库根的定时带起来。

工单在 Gitea 的 weli/tickets。带 approved 的单子由 Agent 领走,改完推送,再关单。git push 用人的账号,改工单用另一个机器人账号。循环的运行时是 tmux,定时脚本往会话里送按键。命令和原因见 5 月那篇第二节6。

对照洞见四:Observation 是 push 之后看得到的结果3。AuthCore 这条结果是 gh run watch。私有仓这条,在 workflow 还停在子目录里的时候,push 本身不会给出 Playwright 的结果。洞见六的两种模式同时在用3。Cursor 里对着一处改,是辅助者。从领单到推送、看结果、关单,是执行者。

CI 日志按公开处理。会进 GitHub Actions 的输出里不放连接串和密钥。这条写在 plan_skills/guide/huiwing-agent-harness.md。

四、对照

改动 远端 push 之后
monorepo 里的业务目录 ci.moicen.com 的 weli/monorepo 服务器上 pull,按环境构建。仓库根没有 workflow,子目录里的 Playwright 不会被这次 push 排队
AuthCore GitHub main Rust workflow,用 gh run watch 看
AuthCoreJS GitHub workflow 的 on 写的是 master,当前分支是 main
authcoreadmin GitHub 没有 Actions 文件,在服务器上构建

后续把 Playwright 的 yml 放到 monorepo 根的 .gitea/workflows/,push 才会重新变成私有仓的那次 Observation。

References

  1. 参见《One Year of AI-Assisted Programming: Insights, Practices, and Reflections》,2026-01-31。https://weinan.tech/2026/01/31/one-year-with-ai.html。人握全局,AI 做局部。 ↩

  2. 参见《AI Reshaping Software Development Workflow: From Code Writer to AI Conductor》,2026-02-10。https://weinan.tech/2026/02/10/ai-reshaping-software-development-workflow.html。文末那张合并前 / 合并后的图是抽象门禁。该文已 noindex。本文的门禁是 cargo test 和 Playwright smoke,部署是服务器上的 git pull 再构建。 ↩ ↩2

  3. 参见《AI 辅助编程中沉淀的六个核心洞见》,2026-06-09。https://weinan.tech/2026/06/09/ai-coding-six-core-insights.html。洞见三写 monorepo 里的一次跨项目提交;洞见四写 Observation、可复刻的测试环境和 Playwright;洞见六写执行者与辅助者。本文把这三节落到 huiwings 现在的目录和 CI 上。 ↩ ↩2 ↩3 ↩4 ↩5

  4. 参见《从 Java task_server 到 Rust(htyts / htyproc):用 AI 推进迁移,用 GitHub CI 与基础设施兜住 E2E》,2026-03-22。https://weinan.tech/2026/03/22/rust-task-server-migration-ai-ci-e2e.html。进程边界、计划即边界,以及轻量 E2E 和手动重联调的分法。本文不重讲 crate,只补上后来的仓库根和 Gitea 查找规则。 ↩ ↩2

  5. Gitea issue Query both .gitea and .github for workflows。维护者说明先查 .gitea/workflows,没有再退回 .github/workflows,不合并两个目录。实现见 listWorkflowsInDirs,默认目录见 WorkflowDirs。 ↩

  6. 参见《大模型工程化的自举迭代框架:从 Demo 到可持续生产系统的五层实践》,2026-05-04。https://weinan.tech/2026/05/04/llm-engineering-bootstrapping-iteration-framework.html。第二节把 tmux 当作长期 Agent 的运行时。工单循环用的就是这种 send-keys。 ↩