AML/.claude/skills/aml-local-refresh/SKILL.md

243 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
name: aml-local-refresh
description: Refresh the AML local dev stack - git pull AML_Backend (and AML_Frontend), restore the newest AML-local-dev-*.bak into the mssql container as AbpAML, rebuild the httpapi-host image, and restart the API bypassing db-migrator. Also covers running AML_Frontend against that local backend (npm run start:local, login admin, on the Mac or on FG-W11-ICON-DEV via a reverse SSH tunnel). Use when asked to 更新/还原/重建/重启本地后端环境, "拉最新代码并还原数据库", "restore the latest bak", 前端连本地后端/本地登录, 在 Windows 那台跑 AML 前端, or when local portal endpoints 500 after the backend moved on.
---
# AML 本地环境刷新(拉代码 + 还原库 + 重启服务)
一条龙刷新 `/Users/dev/projects/AML/docker-compose-local-dev/` 这套本地栈。栈本身的架构、配置来源(绕过 Nacos)、ABP 授权等背景见同目录 `readme.md`;本 skill 只讲**刷新流程**和**必须踩对的顺序**。
所有命令的工作目录都是:
```bash
cd /Users/dev/projects/AML/docker-compose-local-dev
```
## 铁律(顺序错了必炸)
1. **还原库前先停 host**,否则 `RESTORE` 拿不到独占锁。
2. **永远不要对还原库跑 `db-migrator`**。`.bak` 里**没有** `__EFMigrationsHistory` 表,migrator 会从初始迁移重建表 → `SqlException 2714` → exit 133;而 compose 里 `httpapi-host depends_on db-migrator: service_completed_successfully`,migrator 一挂 host 就起不来。**起 host 必须带 `--no-deps`**。
3. **代码更新了就必须 `build` 重建 host 镜像**。旧二进制配新库 = 列类型对不上(历史事故:`InvalidCastException: Decimal→Int32`,所有门户端点 500)。
4. 还原后 **`seed-root-ou.sql` 必跑**(缺 root OU → 支付后建租户静默失败),`local-schema-catchup.sql` 也跑一遍(幂等)。
5. `seed-agent-plans.sql` **默认不要跑**——先查活跃推薦人关联,有就跳过(见下)。
## 步骤
### 0. 前置检查
```bash
docker info >/dev/null 2>&1 || open -a Docker # 没起就拉起 Docker Desktop,等 docker info 通
```
Docker 内存需 ≥16 GB(mssql 在 Apple Silicon 上是 amd64 模拟,不够会 exit 139)。
### 1. 拉最新后端代码
```bash
cd /Users/dev/projects/AML/AML_Backend && git status -sb | head -5 && git pull
```
- 分支通常是 `dev`。工作区有改动先问用户,别自作主张 stash。
- `failed to get/store: -25308` 是 macOS keychain 的噪音,不影响 pull。
- 拉完看一眼新迁移:`ls src/iCON.Abp.FX.EntityFrameworkCore.DbMigrations/Migrations/*.cs | grep -v Designer | tail -8`,留着第 4 步核对。
### 1b. 同步拉前端代码(AML_Frontend)
后端功能(批量操作、电话区号之类)通常前后端成对提交,只更后端会让前端对不上。前端不在 docker 栈里,这一步**只拉代码**:
```bash
cd /Users/dev/projects/AML/AML_Frontend && git status -sb | head -5
OLD=$(git rev-parse HEAD) && git pull --ff-only
git log -1 --format='%h %s'
git diff --name-only $OLD HEAD -- package.json # 有输出 = 依赖变了
```
- 分支是 **`dev`**,和后端一样。团队在 dev 上提交,再通过 PR 合进 `main`,所以 dev 上的代码永远不比 main 旧。本地如果还停在 main,先确认 `git rev-list --count origin/dev..dev` 是 0(本地 dev 没有未推的 commit),再 `git fetch . origin/dev:dev` 快进,然后**单独跑一条** `git switch dev`,确认切换成功后再 pull。工作区有改动同样先问用户。
- 往前端提交时也落在 dev 上,**不要直接推 main**。切分支、commit、push 要分开跑,每一步都确认分支对了再继续。之前就因为把它们串在一条命令里,switch 失败后 commit 被推到了 main。
- `package-lock.json` 被 gitignore 了,所以只能看 `package.json` 有没有变。
- 依赖变了就装一次。系统 Node 是 v24,Angular 11 不支持,所以前端一律通过 **fnm 用 Node 14(x64,跑在 Rosetta 上)**:`AML_Frontend/.node-version` 写的是 `14`(只在本地,已加进 `.git/info/exclude`)。非交互 shell 里不会自动切版本,要显式写:
```bash
fnm exec --using 14 -- npm install --legacy-peer-deps
fnm exec --using 14 -- npm run build:dev # 可选验证,约 2 分钟
```
`watchpack-chokidar2/fsevents` 的 `gyp ERR!` 是可选依赖编译失败,npm 会跳过,exit 0 就算成功。
### 2. 起基础设施 + 停 host
```bash
cd /Users/dev/projects/AML/docker-compose-local-dev
docker stop aml-httpapi-host
docker compose -f docker-compose.local.yml up -d mssql rabbitmq
# 等健康
until [ "$(docker inspect -f '{{.State.Health.Status}}' aml-mssql 2>/dev/null)" = healthy ]; do /bin/sleep 10; done
```
### 3. 还原最新 .bak
**别写死文件名**,用户会不定期换新的(文件名带日期戳,旧的会删):
```bash
BAK=$(ls -t *.bak | head -1); echo "using: $BAK"
docker cp "$BAK" aml-mssql:/var/opt/mssql/restore.bak
# 坑:docker cp 进去是 root:root 0600,sqlservr 读不到 → "Operating system error 5(Access is denied)"
docker exec -u root aml-mssql bash -lc 'chown mssql:root /var/opt/mssql/restore.bak && chmod 644 /var/opt/mssql/restore.bak'
docker exec aml-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Aml@Local2026' -C -b -Q "
IF DB_ID('AbpAML') IS NOT NULL ALTER DATABASE [AbpAML] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
RESTORE DATABASE [AbpAML] FROM DISK='/var/opt/mssql/restore.bak'
WITH REPLACE, RECOVERY,
MOVE 'AML-local-dev' TO '/var/opt/mssql/data/AbpAML.mdf',
MOVE 'AML-local-dev_log' TO '/var/opt/mssql/data/AbpAML_log.ldf';
ALTER DATABASE [AbpAML] SET MULTI_USER;"
```
逻辑名恒为 `AML-local-dev` / `AML-local-dev_log`(不确定就先 `RESTORE FILELISTONLY`)。sqlcmd 在容器内路径是 `/opt/mssql-tools18/bin/sqlcmd`,**必须带 `-C`**。
### 4. 补数据 + 对齐 schema
```bash
docker exec -i aml-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Aml@Local2026' -C -d AbpAML -b -i /dev/stdin < seed-root-ou.sql
docker exec -i aml-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Aml@Local2026' -C -d AbpAML -b -i /dev/stdin < local-schema-catchup.sql
```
`local-schema-catchup.sql` 输出可能是乱码(中文编码),看 exit code 即可。
然后拿第 1 步列出的最新几条迁移,逐个查库里缺不缺(缺了就往 `local-schema-catchup.sql` 追加幂等 DDL —— 注意 `ALTER TABLE ADD` 与引用该新列的语句不能同批次,回填要包在 `EXEC(N'...')` 里):
```bash
docker exec aml-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Aml@Local2026' -C -d AbpAML -h -1 -W -Q "
SELECT 'SomeTable='+ISNULL(CAST(OBJECT_ID('SomeTable') AS varchar),'MISSING')
+' | SomeCol='+ISNULL(CAST(COL_LENGTH('SomeTable','SomeCol') AS varchar),'MISSING');"
```
> 查表存在性别用 `SELECT COUNT(*) FROM 该表`——表不存在会在编译期报 208。一律 `OBJECT_ID` / `COL_LENGTH` + `ISNULL(CAST(... AS varchar),'MISSING')`。
> Edition 表叫 **`SaasEditions`**(不是 `AbpEditions`)。
推薦人脚本判断:
```bash
docker exec aml-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Aml@Local2026' -C -d AbpAML -h -1 -W \
-Q "SELECT COUNT(*) FROM AMLPortal_AgentUserPlans WHERE IsDeleted=0;"
```
> 0 才考虑 `seed-agent-plans.sql`;非 0(新库自带真实关联)**跳过**——该脚本会先软删 agent_A 的现有关联,且它预期的 agentTest2/3 已不存在(RAISERROR 中止)。
### 5. 重建 host 镜像
```bash
docker compose -f docker-compose.local.yml build httpapi-host
```
- 全量重建约 10–20 分钟(restore 大量 ABP 商业包 + 编译 ~40 个项目)。建议 `run_in_background` 跑,**别用 `| tail` 管道**——那会把失败的 exit code 吃掉,看起来像成功。
- 常见失败 `error NU1301: Unable to load the service index for source https://www.myget.org/...`:sdk 镜像 CA 过期。`Dockerfile.local` 的 `build` 阶段已加 `ca-certificates` 刷新修好;若再复现,同样手法排查(`docker run --rm mcr.microsoft.com/dotnet/sdk:6.0 bash -lc 'curl -sS https://www.myget.org/F/blazorise/api/v3/index.json'`)。
- 编译 OOM(exit 137):确认 Docker 内存 ≥16 GB。
### 6. 起 host(绕过 migrator)
```bash
docker compose -f docker-compose.local.yml up -d --no-build --no-deps httpapi-host
```
### 7. 冒烟验证
```bash
docker compose -f docker-compose.local.yml ps
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:44331/swagger/index.html
# 门户端点是 POST,GET 返 405
curl -s -X POST http://localhost:44331/api/amlPortal/Order/portal/GetEditionList -H 'Content-Type: application/json' -d '{}' | head -c 300
curl -s -X POST http://localhost:44331/api/amlPortal/plan/portal/GetPlanList -H 'Content-Type: application/json' -d '{}' | head -c 300
```
期望 `{"code":0,...}`。
- **一律 `http://localhost:44331`**,`https` 会报 `tlsv1 alert protocol version` / `000`,看着像挂了其实只是协议不对。
- host 起不来先看日志:Release 构建只写文件,`docker logs` 是空的 → `docker cp aml-httpapi-host:/app/Logs ./Logs`。
- exit 214 / `ABP-LIC-0008` = ABP 授权令牌缺失,见 `readme.md` 第 3 节。
## 前端联调(AML_Frontend 连本地后端)
**登录账号**:宿主层管理员 `admin`,租户**留空(Host)**。
- 还原 `.bak` 的库(本 skill 的正常流程):密码 **`1qaz@WSX`**。
- 用 db-migrator 种子新建的库:密码 `1q2w3E*`(ABP 默认,readme 里也记了这个)。
库里还有几十个租户下也叫 `admin` 的账号,那些是真实客户的,别拿来试。
### 关键约束:issuer 必须是 `http://localhost:44331`
- 后端的 `AuthServer__Authority` / `App__SelfUrl` 固定是 `http://localhost:44331`。IdentityServer 的 issuer 跟着**请求的主机名**走:用 IP 访问时,发现文档里的 issuer 会变成 `http://192.168.x.x:44331`,签出来的 token 会被 API 拒掉(401)。所以**无论前端跑在哪台机器上,浏览器都必须通过 `localhost:44331` 访问后端**。
- 前端默认的 `environment.ts` 写的是 `https://localhost:44331`,而本地 host 只提供 http,所以用默认配置连本地后端是登录不了的。要用 **`npm run start:local`**:angular.json 里的 `local` 配置会把它换成 `src/environments/environment.local.ts`,全部指向 http。
- 后端这边已经就绪,不用改:CORS 放行了 `http://localhost:4200`,`FX_App` client 允许 `password` 授权、不需要 secret、回调地址里有 `http://localhost:4200`。前端的 `loginMode = 'password'` 用的就是密码模式。
- 排查顺序:先看 `http://localhost:44331/.well-known/openid-configuration` 里的 issuer,再用 `Origin: http://localhost:4200` 对 `/connect/token` 发一个 OPTIONS 预检(期望 204),最后看浏览器 Network 里 `/connect/token` 的响应。
### 在 Mac 上跑
```bash
cd /Users/dev/projects/AML/AML_Frontend
fnm exec --using 14 -- npm run start:local # 编译约 2 分钟,浏览器打开 http://localhost:4200
```
验证配置是否生效:`curl -s http://localhost:4200/main.js | grep -o "issuer: *'[^']*'"`,应该输出 `http://localhost:44331`。
### 在 FG-W11-ICON-DEV(Windows)上跑
用户有些测试要在那台机器上做。操作远程机器走 `remote-dev-box` skill(先 `boxes.sh locate -H FG-W11-ICON-DEV`,IP 会变)。现状:
| 项 | 值 |
|---|---|
| 代码 | `C:/Projects/AML_Frontend`,**不是 git 仓库**,是从 Mac 推过去的快照 |
| Node | 便携版 `C:/Tools/node-v14.21.3-win-x64`(系统里的 Node 22 不动) |
| 启动脚本 | `C:/Tools/start-aml-frontend.cmd`(设好了 PATH 和 4 GB 堆上限,用户双击就能起) |
| 日志 | `C:/Tools/aml-frontend-serve.log` |
**1. 后端:由 Mac 起反向隧道**,让 Windows 上的 `localhost:44331` 通到 Mac 的 docker host:
```bash
ssh -fNT -o ControlPath=none -o ExitOnForwardFailure=yes -R 44331:127.0.0.1:44331 FG-W11-ICON-DEV
```
- 这条 auto mode 不让 Claude 跑,**要请用户自己在提示符里用 `!` 运行**,并提醒他们**整行粘贴**:之前在 `-R` 后面断行,结果报了 `option requires an argument -- R`。
- `ControlPath=none` 不能省:`~/.ssh/config` 里给这台机器开了 `ControlMaster auto` + `ControlPersist 10m`,不关的话,转发会挂在 run.sh 的复用连接上,10 分钟就断。
- Mac 睡眠、断网或重启后隧道会断,重跑即可。关闭:`pkill -f "R 44331"`。
- 验证(在 Windows 上):`Invoke-WebRequest -UseBasicParsing http://localhost:44331/.well-known/openid-configuration`,issuer 应该是 `http://localhost:44331`。
**2. 更新代码**(Mac 上的前端有新提交,或 start:local 相关文件改了时):把 git 跟踪的文件和未跟踪的新文件一起打包推过去,不带 node_modules:
```bash
cd /Users/dev/projects/AML/AML_Frontend
git ls-files -co --exclude-standard -z | COPYFILE_DISABLE=1 tar --null -T - -czf "$SCRATCH/aml-frontend-src.tgz"
$S/cp.sh push -H FG-W11-ICON-DEV "$SCRATCH/aml-frontend-src.tgz" 'C:/Tools/aml-frontend-src.tgz'
# 远端:tar -xzf C:/Tools/aml-frontend-src.tgz -C C:/Projects/AML_Frontend ,然后删掉 tgz
```
覆盖解压不会删除文件。如果上游删了文件,要先清掉旧目录(node_modules 可以留着)。
**3. 装依赖**(首次安装,或 package.json 变了):
```powershell
$env:Path = 'C:/Tools/node-v14.21.3-win-x64;' + $env:Path
Set-Location C:/Projects/AML_Frontend
npm install --legacy-peer-deps --no-audit --no-fund --prefer-offline --maxsockets=6 --fetch-retries=5 *> C:/Tools/aml-npm-install.log
```
- 要用 `run_in_background` 跑,首次安装约 17 分钟。
- npm 6 在这台机器上**会假死**:进程还在、和 registry 的连接也还在,但 npm 缓存大小、CPU 时间、日志行数几分钟都不动。所以跑的过程中要隔几分钟查一次这三样,卡住就 `Stop-Process` 掉重跑。已经下载的包都在缓存里,重跑会接着用。看到 `node_modules/.staging` 里的目录数在涨,说明正在解压。
**4. 启动**:SSH 起的进程在 session 0,用户看不到,所以要用一次性计划任务在桌面会话里起:
```powershell
schtasks /Create /TN TmpAmlFrontend /TR '"C:\Tools\start-aml-frontend.cmd"' /SC ONCE /ST 23:59 /IT /F
schtasks /Run /TN TmpAmlFrontend
Start-Sleep 8; schtasks /Delete /TN TmpAmlFrontend /F
```
- **`NODE_OPTIONS=--max_old_space_size=4096` 必须设**(已经写在启动脚本里)。Node 14 默认堆上限约 2 GB,首次编译完会紧接着再构建一轮,这时会 `JavaScript heap out of memory`,ng serve 直接退出。
- 首次编译约 4–5 分钟。dev server **构建期间会挂住请求**,`Invoke-WebRequest` 看起来像超时,不是端口问题。日志最后一行如果还是 `Generating browser application bundles...`,就继续等。
- 验证:日志里有 `Compiled successfully`,且之后没有 `FATAL` / `npm ERR`;`http://127.0.0.1:4200/` 返回 200;`main.js` 里的 issuer 是 `http://localhost:44331`。起来一分钟后再查一次,前面那次 OOM 就是在第二轮构建时才崩的。
- 用户在那台机器的浏览器里打开 `http://localhost:4200` 登录。关掉桌面上那个「AML Frontend (start:local)」窗口就停了。
## 报告给用户
刷新完说清楚五件事:后端拉到的 commit、前端拉到的 commit(及 package.json 是否变了)、用的 .bak 文件名、库里的数量(表 / Plans / Editions / Tenants)、以及冒烟结果。