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
7e877f50
Commit
7e877f50
authored
Oct 09, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Plain Diff
Merge deployment stdin fix and test release report into test
parents
97dfb046
681f3302
Pipeline
#3725
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 @
7e877f50
...
@@ -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 @
7e877f50
...
@@ -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 @
7e877f50
...
@@ -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 @
7e877f50
...
@@ -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 @
7e877f50
...
@@ -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 @
7e877f50
...
@@ -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 @
7e877f50
# 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