Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pac
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tools
pac
Commits
681f3302
Commit
681f3302
authored
Oct 09, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(deploy): preserve caller input and record verified test release
parent
75233df0
Pipeline
#3724
failed in 0 seconds
Changes
7
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
113 additions
and
17 deletions
+113
-17
deploy/deploy-ai.sh
+3
-3
deploy/deploy-ingress.sh
+3
-3
deploy/deploy-prod.sh
+1
-1
deploy/tests/deploy-ai.test.mjs
+16
-7
deploy/tests/deploy-pac.test.mjs
+2
-2
docs/friday-ai-test-deployment.md
+7
-1
docs/qa/2026-10-09-friday-ai-test-release.md
+81
-0
No files found.
deploy/deploy-ai.sh
View file @
681f3302
...
@@ -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
...
...
deploy/deploy-ingress.sh
View file @
681f3302
...
@@ -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}}'
)
...
...
deploy/deploy-prod.sh
View file @
681f3302
...
@@ -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 的重建判定)"
...
...
deploy/tests/deploy-ai.test.mjs
View file @
681f3302
...
@@ -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/);
});
deploy/tests/deploy-pac.test.mjs
View file @
681f3302
...
@@ -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 }); }
});
});
docs/friday-ai-test-deployment.md
View file @
681f3302
...
@@ -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-0
8 实查:AI 未部署;域名已解析,证书尚未覆盖新域名。先将本次 Docker 部署改动合入 test
。
2026-10-0
9 已完成首次测试部署: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
...
...
docs/qa/2026-10-09-friday-ai-test-release.md
0 → 100644
View file @
681f3302
# 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;无需重建数据库或中转。
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment