Commit 681f3302 by luoqi

fix(deploy): preserve caller input and record verified test release

parent 75233df0
Pipeline #3724 failed in 0 seconds
...@@ -28,7 +28,7 @@ main() { ...@@ -28,7 +28,7 @@ main() {
# 先构建、预检;失败时旧容器继续运行。串行构建,限制对同机 PAC 的影响。 # 先构建、预检;失败时旧容器继续运行。串行构建,限制对同机 PAC 的影响。
"${compose[@]}" build ai-service "${compose[@]}" build ai-service
"${compose[@]}" build ai-web "${compose[@]}" build ai-web
"${compose[@]}" run --rm --no-deps ai-preflight "${compose[@]}" run --rm --no-deps -T --interactive=false ai-preflight
if [[ "$option" == --check ]]; then echo "构建与配置检查通过:${AI_IMAGE_TAG}(未迁移或重启)"; return; fi if [[ "$option" == --check ]]; then echo "构建与配置检查通过:${AI_IMAGE_TAG}(未迁移或重启)"; return; fi
# 记录可回退的旧镜像;数据库备份由部署手册要求在发布前完成。 # 记录可回退的旧镜像;数据库备份由部署手册要求在发布前完成。
...@@ -41,8 +41,8 @@ main() { ...@@ -41,8 +41,8 @@ main() {
done > "$state/previous-$AI_IMAGE_TAG.txt" done > "$state/previous-$AI_IMAGE_TAG.txt"
"${compose[@]}" stop ai-service ai-web "${compose[@]}" stop ai-service ai-web
# 迁移失败保持 AI 停止,禁止自动启动可能不兼容的旧代码。 # 迁移失败保持 AI 停止,禁止自动启动可能不兼容的旧代码。
"${compose[@]}" run --rm --no-deps ai-migrate "${compose[@]}" run --rm --no-deps -T --interactive=false ai-migrate
"${compose[@]}" run --rm --no-deps ai-migrate pnpm exec prisma migrate status "${compose[@]}" run --rm --no-deps -T --interactive=false ai-migrate pnpm exec prisma migrate status
"${compose[@]}" up -d --no-deps --force-recreate --wait --wait-timeout 120 ai-service ai-web "${compose[@]}" up -d --no-deps --force-recreate --wait --wait-timeout 120 ai-service ai-web
for service in ai-service ai-web; do for service in ai-service ai-web; do
local id expected actual local id expected actual
......
...@@ -17,11 +17,11 @@ export INGRESS_IMAGE_TAG="$(git rev-parse --short HEAD)-$(date -u +%Y%m%dT%H%M%S ...@@ -17,11 +17,11 @@ export INGRESS_IMAGE_TAG="$(git rev-parse --short HEAD)-$(date -u +%Y%m%dT%H%M%S
compose=(docker compose -p pac-ingress -f docker-compose.ingress.yml) compose=(docker compose -p pac-ingress -f docker-compose.ingress.yml)
"${compose[@]}" config --quiet "${compose[@]}" config --quiet
"${compose[@]}" build channel-ingress "${compose[@]}" build channel-ingress
"${compose[@]}" run --rm --no-deps channel-ingress node -e "try{require('./dist/config').readConfig()}catch{console.error('中转配置无效');process.exit(1)}" "${compose[@]}" run --rm --no-deps -T --interactive=false channel-ingress node -e "try{require('./dist/config').readConfig()}catch{console.error('中转配置无效');process.exit(1)}"
# 独立 schema 的幂等 DDL;不停止 AI,不执行 AI 的迁移。 # 独立 schema 的幂等 DDL;不停止 AI,不执行 AI 的迁移。
"${compose[@]}" run --rm --no-deps ingress-migrate "${compose[@]}" run --rm --no-deps -T --interactive=false ingress-migrate
# 切换配置前先检查积压归属;失败时旧中转保持运行。 # 切换配置前先检查积压归属;失败时旧中转保持运行。
"${compose[@]}" run --rm --no-deps channel-ingress node -e "const{readConfig}=require('./dist/config');const{createPool,PgStore}=require('./dist/store');const c=readConfig();const p=createPool(c.databaseUrl);new PgStore(p,c.localAccounts).verifyRouting().catch(()=>{console.error('路由检查失败:请检查数据库,并先排空归属变更账号的旧积压');process.exitCode=1}).finally(()=>p.end())" "${compose[@]}" run --rm --no-deps -T --interactive=false channel-ingress node -e "const{readConfig}=require('./dist/config');const{createPool,PgStore}=require('./dist/store');const c=readConfig();const p=createPool(c.databaseUrl);new PgStore(p,c.localAccounts).verifyRouting().catch(()=>{console.error('路由检查失败:请检查数据库,并先排空归属变更账号的旧积压');process.exitCode=1}).finally(()=>p.end())"
"${compose[@]}" up -d --no-deps --wait --wait-timeout 60 channel-ingress "${compose[@]}" up -d --no-deps --wait --wait-timeout 60 channel-ingress
id=$("${compose[@]}" ps -q channel-ingress) id=$("${compose[@]}" ps -q channel-ingress)
expected=$(docker image inspect "pac-channel-ingress:$INGRESS_IMAGE_TAG" --format '{{.Id}}') expected=$(docker image inspect "pac-channel-ingress:$INGRESS_IMAGE_TAG" --format '{{.Id}}')
......
...@@ -127,7 +127,7 @@ main() { ...@@ -127,7 +127,7 @@ main() {
fi fi
# 迁移明确先完成,再替换业务容器;--no-deps 下不依赖 Compose 隐式排序。 # 迁移明确先完成,再替换业务容器;--no-deps 下不依赖 Compose 隐式排序。
log "执行数据库迁移" log "执行数据库迁移"
"${COMPOSE[@]}" run --rm --no-deps pac-migrate "${COMPOSE[@]}" run --rm --no-deps -T --interactive=false pac-migrate
local RUNTIME_SERVICES=(pac-service pac-web) local RUNTIME_SERVICES=(pac-service pac-web)
[[ "${WITH_ASR:-0}" == "1" ]] && RUNTIME_SERVICES+=(pac-asr) [[ "${WITH_ASR:-0}" == "1" ]] && RUNTIME_SERVICES+=(pac-asr)
log "force-recreate(不信 compose 的重建判定)" log "force-recreate(不信 compose 的重建判定)"
......
...@@ -7,7 +7,7 @@ import { spawnSync } from 'node:child_process'; ...@@ -7,7 +7,7 @@ import { spawnSync } from 'node:child_process';
const source = resolve('deploy/deploy-ai.sh'); const source = resolve('deploy/deploy-ai.sh');
// 用假 Docker 注入失败,验证真实脚本不会迁移/切换不完整版本,不触碰本地或远端容器。 // 用假 Docker 注入失败,验证真实脚本不会迁移/切换不完整版本,不触碰本地或远端容器。
function runDeploy({ fail = '', check = false, branch = 'test', mismatch = false } = {}) { function runDeploy({ fail = '', check = false, branch = 'test', mismatch = false, piped = false } = {}) {
const root = mkdtempSync(join(tmpdir(), 'ai-deploy-flow-')); const root = mkdtempSync(join(tmpdir(), 'ai-deploy-flow-'));
const bin = join(root, 'bin'), log = join(root, 'commands.log'); const bin = join(root, 'bin'), log = join(root, 'commands.log');
for (const dir of [bin, join(root, '.git'), join(root, 'deploy')]) mkdirSync(dir); for (const dir of [bin, join(root, '.git'), join(root, 'deploy')]) mkdirSync(dir);
...@@ -21,7 +21,8 @@ function runDeploy({ fail = '', check = false, branch = 'test', mismatch = false ...@@ -21,7 +21,8 @@ function runDeploy({ fail = '', check = false, branch = 'test', mismatch = false
'rev-parse --git-common-dir') echo .git ;; 'rev-parse --git-common-dir') echo .git ;;
esac`, esac`,
flock: 'exit 0', flock: 'exit 0',
docker: String.raw`if [[ -n "$DEPLOY_TEST_FAIL" && "$*" == *"$DEPLOY_TEST_FAIL"* ]]; then exit 43; fi docker: String.raw`if [[ "$*" == *' run '* && "$*" != *'--interactive=false'* ]]; then cat >/dev/null; fi
if [[ -n "$DEPLOY_TEST_FAIL" && "$*" == *"$DEPLOY_TEST_FAIL"* ]]; then exit 43; fi
case "$*" in case "$*" in
*' ps -q ai-service') echo test-service ;; *' ps -q ai-service') echo test-service ;;
*' ps -q ai-web') echo test-web ;; *' ps -q ai-web') echo test-web ;;
...@@ -34,7 +35,9 @@ function runDeploy({ fail = '', check = false, branch = 'test', mismatch = false ...@@ -34,7 +35,9 @@ function runDeploy({ fail = '', check = false, branch = 'test', mismatch = false
try { try {
for (const [name, body] of Object.entries(stubs)) for (const [name, body] of Object.entries(stubs))
writeFileSync(join(bin, name), '#!/usr/bin/env bash\nset -eu\n' + common + body + '\n', { mode: 0o755 }); writeFileSync(join(bin, name), '#!/usr/bin/env bash\nset -eu\n' + common + body + '\n', { mode: 0o755 });
const result = spawnSync('bash', [join(root, 'deploy/deploy-ai.sh'), 'staging', ...(check ? ['--check'] : [])], { const script = join(root, 'deploy/deploy-ai.sh');
const result = spawnSync('bash', piped ? ['-s'] : [script, 'staging', ...(check ? ['--check'] : [])], {
...(piped ? { input: `bash '${script}' staging --check\nprintf 'FOLLOWUP_REACHED\\n'\n` } : {}),
encoding: 'utf8', timeout: 10000, encoding: 'utf8', timeout: 10000,
env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, DEPLOY_TEST_LOG: log, env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, DEPLOY_TEST_LOG: log,
DEPLOY_TEST_FAIL: fail, DEPLOY_TEST_BRANCH: branch, DEPLOY_TEST_MISMATCH: mismatch ? '1' : '0' }, DEPLOY_TEST_FAIL: fail, DEPLOY_TEST_BRANCH: branch, DEPLOY_TEST_MISMATCH: mismatch ? '1' : '0' },
...@@ -47,14 +50,14 @@ test('Docker 构建和预检成功后才停 AI、迁移、切换并校验镜像' ...@@ -47,14 +50,14 @@ test('Docker 构建和预检成功后才停 AI、迁移、切换并校验镜像'
const { result, commands } = runDeploy(); const { result, commands } = runDeploy();
assert.equal(result.status, 0, result.stderr); assert.equal(result.status, 0, result.stderr);
const at = (s) => commands.findIndex((c) => c.includes(s)); const at = (s) => commands.findIndex((c) => c.includes(s));
assert.ok(at('build ai-web') < at('run --rm --no-deps ai-preflight')); assert.ok(at('build ai-web') < at('run --rm --no-deps -T --interactive=false ai-preflight'));
assert.ok(at('ai-preflight') < at('stop ai-service ai-web')); assert.ok(at('ai-preflight') < at('stop ai-service ai-web'));
assert.ok(at('stop ai-service ai-web') < at('run --rm --no-deps ai-migrate')); assert.ok(at('stop ai-service ai-web') < at('run --rm --no-deps -T --interactive=false ai-migrate'));
assert.ok(at('prisma migrate status') < at('up -d --no-deps --force-recreate --wait')); assert.ok(at('prisma migrate status') < at('up -d --no-deps --force-recreate --wait'));
assert.ok(commands.filter((c) => c.startsWith('docker compose')).every((c) => c.includes('-p pac-ai -f docker-compose.ai.yml'))); assert.ok(commands.filter((c) => c.startsWith('docker compose')).every((c) => c.includes('-p pac-ai -f docker-compose.ai.yml')));
assert.ok(!commands.some((c) => /systemctl|pnpm install|stop pac-|up.*pac-service/.test(c))); assert.ok(!commands.some((c) => /systemctl|pnpm install|stop pac-|up.*pac-service/.test(c)));
}); });
for (const failure of ['build ai-web', 'run --rm --no-deps ai-preflight']) { for (const failure of ['build ai-web', 'run --rm --no-deps -T --interactive=false ai-preflight']) {
test(`${failure} 失败不停止旧服务、不执行迁移`, () => { test(`${failure} 失败不停止旧服务、不执行迁移`, () => {
const { result, commands } = runDeploy({ fail: failure }); const { result, commands } = runDeploy({ fail: failure });
assert.equal(result.status, 43); assert.equal(result.status, 43);
...@@ -62,7 +65,7 @@ for (const failure of ['build ai-web', 'run --rm --no-deps ai-preflight']) { ...@@ -62,7 +65,7 @@ for (const failure of ['build ai-web', 'run --rm --no-deps ai-preflight']) {
}); });
} }
test('迁移失败不启动新旧应用', () => { test('迁移失败不启动新旧应用', () => {
const { result, commands } = runDeploy({ fail: 'run --rm --no-deps ai-migrate' }); const { result, commands } = runDeploy({ fail: 'run --rm --no-deps -T --interactive=false ai-migrate' });
assert.equal(result.status, 43); assert.equal(result.status, 43);
assert.ok(!commands.some((c) => c.includes('up -d'))); assert.ok(!commands.some((c) => c.includes('up -d')));
}); });
...@@ -80,3 +83,9 @@ test('错误分支在 Docker 操作前停止', () => { ...@@ -80,3 +83,9 @@ test('错误分支在 Docker 操作前停止', () => {
test('运行容器镜像与构建镜像不一致时拒绝报告成功', () => { test('运行容器镜像与构建镜像不一致时拒绝报告成功', () => {
assert.notEqual(runDeploy({ mismatch: true }).result.status, 0); assert.notEqual(runDeploy({ mismatch: true }).result.status, 0);
}); });
test('SSH 管道执行预检后仍执行后续命令,不被 Compose 吞掉标准输入', () => {
const { result } = runDeploy({ piped: true });
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /FOLLOWUP_REACHED/);
});
...@@ -16,14 +16,14 @@ test('PAC 迁移失败保留应用和已运行的基础设施', () => { ...@@ -16,14 +16,14 @@ test('PAC 迁移失败保留应用和已运行的基础设施', () => {
for (const [name, body] of Object.entries({ for (const [name, body] of Object.entries({
git: 'case "$*" in "rev-parse --git-common-dir") echo .git;; "rev-parse --abbrev-ref HEAD") echo test;; esac', git: 'case "$*" in "rev-parse --git-common-dir") echo .git;; "rev-parse --abbrev-ref HEAD") echo test;; esac',
flock: 'exit 0', flock: 'exit 0',
docker: 'echo "$*" >> "$TEST_CALLS"\ncase "$*" in *"ps -q --status running"*) echo existing;; *"run --rm --no-deps pac-migrate"*) exit 42;; esac', docker: 'echo "$*" >> "$TEST_CALLS"\ncase "$*" in *"ps -q --status running"*) echo existing;; *"run --rm --no-deps -T --interactive=false pac-migrate"*) exit 42;; esac',
})) writeFileSync(join(root, 'bin', name), '#!/bin/sh\n' + body + '\n', { mode: 0o755 }); })) writeFileSync(join(root, 'bin', name), '#!/bin/sh\n' + body + '\n', { mode: 0o755 });
const r = spawnSync('bash', ['deploy/deploy-prod.sh', '--no-pull'], { const r = spawnSync('bash', ['deploy/deploy-prod.sh', '--no-pull'], {
cwd: root, encoding: 'utf8', env: { ...process.env, COMPOSE_MANAGED: '0', PATH: join(root, 'bin') + ':' + process.env.PATH, TEST_CALLS: log }, cwd: root, encoding: 'utf8', env: { ...process.env, COMPOSE_MANAGED: '0', PATH: join(root, 'bin') + ':' + process.env.PATH, TEST_CALLS: log },
}); });
assert.equal(r.status, 42, r.stdout + r.stderr); assert.equal(r.status, 42, r.stdout + r.stderr);
const calls = readFileSync(log, 'utf8'); const calls = readFileSync(log, 'utf8');
assert.match(calls, /run --rm --no-deps pac-migrate/); assert.match(calls, /run --rm --no-deps -T --interactive=false pac-migrate/);
assert.doesNotMatch(calls, /\b(up|stop|restart|rm)\b.*(pac-service|pac-web|postgres|redis)/); assert.doesNotMatch(calls, /\b(up|stop|restart|rm)\b.*(pac-service|pac-web|postgres|redis)/);
} finally { rmSync(root, { recursive: true, force: true }); } } finally { rmSync(root, { recursive: true, force: true }); }
}); });
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
**部署前置:先发布同版本 PAC 的只读接口。在 PAC `.env` 设置 `PAC_READ_SECRET`,FridayAI 的 `AI_PAC_READ_SECRET` 填同一值;部署后验证真实读取和权限隔离。无需逐宿主执行授权 CLI。** **部署前置:先发布同版本 PAC 的只读接口。在 PAC `.env` 设置 `PAC_READ_SECRET`,FridayAI 的 `AI_PAC_READ_SECRET` 填同一值;部署后验证真实读取和权限隔离。无需逐宿主执行授权 CLI。**
2026-10-08 实查:AI 未部署;域名已解析,证书尚未覆盖新域名。先将本次 Docker 部署改动合入 test。 2026-10-09 已完成首次测试部署:FridayAI 独立 HTTPS 证书、AI 数据库与持久中转已启用。日常升级走下表「日常升级 AI」,不要重复建库、生成密钥或开户。验收记录见 [首次测试发布](qa/2026-10-09-friday-ai-test-release.md)。
### 执行顺序 ### 执行顺序
...@@ -22,6 +22,12 @@ ...@@ -22,6 +22,12 @@
本地调试账号由测试中转配置,日常 AI 发布不修改账号归属、不重启中转。已有 `.env` 和私钥继续使用,不用示例覆盖,也不重新生成密钥。 本地调试账号由测试中转配置,日常 AI 发布不修改账号归属、不重启中转。已有 `.env` 和私钥继续使用,不用示例覆盖,也不重新生成密钥。
首次发布 PAC 步骤查询接口、或后续调整治疗步骤口径时,PAC 发布后还须执行一次步骤汇总表 `patient_step_latest` 的回填并等待完成;日常 AI 升级跳过:
```sh
docker exec pac-pac-service-1 node dist/cli/backfill-step-latest.cli.js
```
## 2. 更新代码、准备私钥 ## 2. 更新代码、准备私钥
```sh ```sh
......
# FridayAI 首次测试发布验收
日期:2026-10-09(上海);服务器操作时间为 UTC 2026-10-08。
## 发布范围
- 特性分支 `codex/host-writeback` 合入 `test` 后发布;PAC 运行代码 `97dfb04`,FridayAI 与中转运行代码 `c761952`(后续变更仅涉及 PAC 维护 CLI、统计信息、部署脚本和记录)。`main` 未改动,未操作生产环境。
- 共用 `/root/pac` 源码,PAC、FridayAI、通道中转分别部署。既有 PostgreSQL、Redis 容器未重建。
- PAC 前后端已更新,54 条迁移全部完成;新增 `PAC_READ_SECRET` 服务级读取鉴权,宿主既有接入密钥继续使用。
- FridayAI:`https://fai.jarvismedical.asia`;镜像版本 `c761952-20261008T160800Z`。
- 通道中转:镜像版本 `c761952-20261008T161032Z`;供应商地址仍为 `https://relay.jarvismedical.asia/hook/ecom`。
- AI 独立数据库 `friday_ai`,72 条迁移完成,UTF8 / en_US.utf8 / UTC。中转使用其独立 `ingress` schema。
- FridayAI 使用独立证书,未改 PAC 共享证书;3103、3104、3106 仅绑定主机 loopback。
## 发布前校验
- PAC 119 套 / 2087 项通过;FridayAI 182 套 / 3185 项通过,另 9 套 / 70 项按现有集成环境条件跳过。
- host-service 22 项、中转真实数据库集成 5 项、部署脚本 21 项通过。
- 各受影响包类型检查、前后端构建、包边界检查通过;源码 lint 无错误。
- PAC 迁移失败模拟已确认旧应用和既有基础设施不会被替换。
- Compose 预检与迁移统一使用非交互输入,增加 SSH 管道后续命令不会丢失的回归测试。
## 已验收
| 项目 | 实测结果 |
| --- | --- |
| 数据库迁移、健康、运行镜像 | PAC、AI、中转部署脚本均通过 |
| PAC 服务级读取 | Friday 与 jvs-dw 两宿主读取成功;错误密钥被拒,未知数据空间无数据 |
| 普尔开户 | 独立 AI 租户、首个管理员、集团节点和三家诊所已建立并关联 PAC |
| 患者读取 | 祥云小镇、现代城、达美中心三店的搜索与档案成功 |
| 权限 | 跨客户组织关联被拒,指定其他客户的真实患者 ID 读取卡片/档案均为空;无效诊所选择回落到本人有权访问的默认诊所 |
| 登录 | 首次改密、Bearer 登录、刷新、读取当前用户、退出通过 |
| 网页与实时连接 | 登录页与嵌入页 HTTP 200;Bearer SSE 成功;嵌入页无禁止 iframe 的响应头 |
| 外部依赖 | ECOM 只读 API、模型 API、OSS 上传读取及签名链接通过;匿名 OSS 访问被拒 |
| AI 停服 | 停 AI 后两条合成回调持久入库并返回 success;恢复后全部补投 |
| 投递幂等 | 三条探针各有一个回执和原始记录;重复投递不增加记录;同编号不同报文返回 409 |
| 步骤查询 | 步骤时间窗接口返回与 PAC 步骤汇总表抽样一致 |
| 公网入口 | 未授权回调、本地拉取及伪造来源头均被拒;旧观察台、历史、SSE 和新 /status 均不公开 |
合成回调使用未登记探针账号,只验证消息接管,不生成患者消息或企微发送任务。
## PAC 详情慢查询修复
部署后复查此前的召回详情接口,连续两次分别耗时 47,829 / 48,070ms。`patient_facts` 的宿主与租户高度相关,旧统计按独立选择率相乘,误选患者索引与宿主范围索引的 BitmapAnd。
新增 `20261008170000_fact_host_tenant_statistics`,声明两列 dependencies 并刷新统计;不改患者事实、不删索引、不移除权限条件。实际执行计划改为患者/状态索引,SQL 抽样 11.8ms。公网同一接口复测 2,172 / 280 / 132ms(首轮含冷连接),均成功。该迁移已通过 Prisma 正式记录,后续环境发布会自动执行。
## 存量步骤汇总
- 回填正常退出(0),扫描 323,859 位患者,更新 311,928 位,耗时 3,949.9 秒。
- 完成后实查 `patient_step_latest`:704,993 条、311,947 位患者、21 类步骤;与回填期间正常增量写入并存。
- 不修改原始事实、不触发全量画像或召回重算。
- 收尾日志发现 PAC 维护 CLI 会继承机器人配置而建立连接;已修复 CLI 启动隔离,清空子进程机器人凭据,不影响常驻服务。新增两项生命周期回归测试及源码/测试类型检查通过;新镜像实机执行 `--check` 确认没有机器人 SDK 活动。
## 旧中转与本地调试
- 旧本地接管水位为 6156;将 6157~6478 共 322 条记录按原文及顺序迁入新队列。
- 迁移 ID 由旧序号、时间和原文确定,切换后再次导入新增 0 条,避免重复迁入。
- Nginx 旧 worker 完成在途请求后停止并禁用 `ecom-relay.service`,旧程序和历史文件保留。
- 现有两个调试账号只路由到本地,其他账号交测试 AI;未启用 `*` 全量路由。
- 本地 AI 保持 3104,已改用主动拉取、事务接管和确认;不再订阅旧 SSE。322 条旧记录已全部在本地形成接管回执,中转本地与服务器队列均归零。
## 备份与运行证据
私密目录 `/var/backups/pac-release-20261008`:
- PAC 完整备份 `pac.dump`(约 18GB)及 `pg_restore --list` 校验结果;迁移前 schema、代码 SHA、镜像和配置。
- `friday-ai-before.dump`、AI 与中转配置、JWT 私钥、旧中转文件及 Nginx 配置。
- `pac-deploy.log`、`ai-deploy.log`、`ingress-deploy.log`、`ai-accept.log`、步骤回填日志、`statistics-migrate.log`、`statistics-query-plan.txt`、`pac-plan-check.log`。
以上备份含私密配置及业务数据,目录权限 700,不进入 Git。管理员密码另存本机 `deploy/secrets/friday-ai-test-admin.json`(600,已忽略)。
## 尚需人工业务验收
- 测试 AI 尚未绑定企微账号。绑定一个不属于本地调试路由的专用测试号,检查任务后再启用 `AI_OUTBOX_WORKER` / `AI_SCHEDULER`。
- 当前两个开关均为 off;本次完成的是接入、数据读取与可靠接管验证,未向真实患者发送消息。
- 宿主回写账号未配置,保留人工到宿主录入的原流程;host-service 未部署。
- 历史 EMR 重解析、全量召回重算均未执行。
- Nginx 原有 PAC 重复 server_name 等警告未在此次跨服务发布中改动;配置检查通过。
后续按 [测试部署步骤](../friday-ai-test-deployment.md) 升级 AI;无需重建数据库或中转。
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment