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
8010ba3c
Commit
8010ba3c
authored
Oct 10, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
ops: guard deployment mode and simplify production handoff
parent
7118f9bf
Pipeline
#3742
failed in 0 seconds
Changes
14
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
342 additions
and
83 deletions
+342
-83
deploy/README.md
+1
-1
deploy/deploy-ai.sh
+8
-0
deploy/deploy-ingress.sh
+6
-0
deploy/deploy-mode.sh
+57
-0
deploy/deploy-prod.sh
+11
-11
deploy/production/friday-ai-gateway.conf.example
+10
-11
deploy/tests/deploy-ai.test.mjs
+32
-1
deploy/tests/deploy-ingress.test.mjs
+37
-4
deploy/tests/deploy-mode.test.mjs
+71
-0
deploy/tests/deploy-pac.test.mjs
+22
-6
docs/friday-ai-production-deployment.md
+23
-18
docs/friday-ai-production-handoff.md
+62
-30
docs/friday-ai-test-deployment.md
+1
-0
docs/qa/2026-10-10-production-readiness.md
+1
-1
No files found.
deploy/README.md
View file @
8010ba3c
...
...
@@ -125,7 +125,7 @@ TLS 证书自备(Let's Encrypt / 云证书);域名要和 `.env` 的 `PAC_DOMAIN`
### 托管 DB/Redis(阿里云 RDS + Redis,可选)
默认自带 PG/Redis(compose 内置)
。要走托管:①gen-env 时传托管连接串,②部署时加
`COMPOSE_MANAGED=1`
。应用代码零改动。
基础 Compose 默认自带 PG/Redis。当前 main/production 必须显式设置
`COMPOSE_MANAGED=1`
,test/staging 则保持未设置或
`0`
;三个发布入口共用模式检查,错误时在构建/迁移/重启前退出。托管发布完成前还需通过应用机私网端点检查
。要走托管:①gen-env 时传托管连接串,②部署时加
`COMPOSE_MANAGED=1`
。应用代码零改动。
```
bash
# ① 生成 env 时指向托管端点(不启自带 PG/Redis)
...
...
deploy/deploy-ai.sh
View file @
8010ba3c
...
...
@@ -7,6 +7,8 @@ main() {
[[
"
$target
"
==
staging
||
"
$target
"
==
production
]]
||
{
echo
'用法:bash deploy/deploy-ai.sh staging|production [--check|--no-pull]'
;
return
1
;
}
[[
-z
"
$option
"
||
"
$option
"
==
--check
||
"
$option
"
==
--no-pull
]]
||
{
echo
'未知选项'
;
return
1
;
}
cd
"
$(
dirname
"
${
BASH_SOURCE
[0]
}
"
)
/.."
source
deploy/deploy-mode.sh
check_deploy_mode
"
$target
"
local
branch
=
test
[[
"
$target
"
==
production
]]
&&
branch
=
main
[[
"
$(
git branch
--show-current
)
"
==
"
$branch
"
]]
||
{
echo
"部署要求当前分支为
$branch
"
;
return
1
;
}
...
...
@@ -53,6 +55,12 @@ main() {
[[
"
$actual
"
==
"
$expected
"
]]
||
{
echo
"
$service
镜像校验失败"
;
return
1
;
}
done
"
${
compose
[@]
}
"
exec
-T
--interactive
=
false
ai-service node
-e
"fetch(process.env.AI_PAC_BASE_URL+'/health',{signal:AbortSignal.timeout(5000)}).then(async r=>{const v=await r.json();if(!r.ok||v.status!=='ok')process.exit(1)}).catch(()=>process.exit(1))"
if
[[
"
${
COMPOSE_MANAGED
:-
0
}
"
==
1
]]
;
then
local
private_ip
private_ip
=
$(
deploy_private_ip
)
verify_deploy_endpoint
"http://
$private_ip
:3104/health/ready"
ready
verify_deploy_endpoint
"http://
$private_ip
:3103/login"
web
fi
printf
'%s\n'
"
$AI_IMAGE_TAG
"
>
"
$state
/current-image-tag"
echo
"FridayAI 部署完成:
${
AI_IMAGE_TAG
}
;Web 3103 / API 3104;PAC 未重启"
}
...
...
deploy/deploy-ingress.sh
View file @
8010ba3c
...
...
@@ -6,6 +6,8 @@ option=${2:-}
[[
-z
"
$option
"
||
"
$option
"
==
--check
]]
||
{
echo
'未知选项'
;
exit
1
;
}
[[
"
$target
"
==
staging
||
"
$target
"
==
production
]]
||
{
echo
'用法:bash deploy/deploy-ingress.sh staging|production [--check]'
;
exit
1
;
}
cd
"
$(
dirname
"
${
BASH_SOURCE
[0]
}
"
)
/.."
source
deploy/deploy-mode.sh
check_deploy_mode
"
$target
"
branch
=
test
[[
"
$target
"
==
production
]]
&&
branch
=
main
[[
"
$(
git branch
--show-current
)
"
==
"
$branch
"
]]
||
{
echo
"要求当前分支为
$branch
"
;
exit
1
;
}
...
...
@@ -31,6 +33,10 @@ id=$("${compose[@]}" ps -q channel-ingress)
expected
=
$(
docker image inspect
"pac-channel-ingress:
$INGRESS_IMAGE_TAG
"
--format
'{{.Id}}'
)
actual
=
$(
docker inspect
"
$id
"
--format
'{{.Image}}'
)
[[
"
$expected
"
==
"
$actual
"
]]
||
{
echo
'中转镜像校验失败'
;
exit
1
;
}
if
[[
"
${
COMPOSE_MANAGED
:-
0
}
"
==
1
]]
;
then
private_ip
=
$(
deploy_private_ip
)
verify_deploy_endpoint
"http://
$private_ip
:3106/health"
health
fi
mkdir
-p
"
$(
git rev-parse
--git-common-dir
)
/ingress-deploy"
printf
'%s\n'
"
$INGRESS_IMAGE_TAG
"
>
"
$(
git rev-parse
--git-common-dir
)
/ingress-deploy/current-image-tag"
echo
"中转部署完成:
${
INGRESS_IMAGE_TAG
}
;3106;未修改 Nginx、未停止旧调试中转"
deploy/deploy-mode.sh
0 → 100644
View file @
8010ba3c
#!/usr/bin/env bash
# 三套发布入口共用;当前生产采用 RDS + 远端网关,测试采用本机代理。
# ⛔ 不自动开启托管模式:它会开放主机监听地址,必须由部署者明确选择。
check_deploy_mode
()
{
local
target
=
"
$1
"
mode
=
"
${
COMPOSE_MANAGED
:-
0
}
"
case
"
$mode
"
in
0|1
)
;;
*
)
echo
'FAIL: COMPOSE_MANAGED 只能为 0 或 1'
>
&2
;
return
1
;;
esac
case
"
$target
"
in
main|production
)
if
[[
"
$mode
"
!=
1
]]
;
then
echo
'FAIL: 当前生产使用远端网关,必须显式设置 COMPOSE_MANAGED=1;未执行构建、迁移或重启。'
>
&2
return
1
fi
;;
test
|
staging
)
if
[[
"
$mode
"
!=
0
]]
;
then
echo
'FAIL: 当前测试使用本机代理,请取消 COMPOSE_MANAGED 或设为 0,避免开放应用端口。'
>
&2
return
1
fi
;;
esac
}
# 在应用已通过容器健康检查后,另验主机私网路径。不能代替网关/安全组验收。
deploy_private_ip
()
{
local
addr
addr
=
$(
ip
-4
route get 1 2>/dev/null | awk
'{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1);exit}}'
)
||
true
if
[[
-z
"
$addr
"
||
"
$addr
"
==
127.
*
||
"
$addr
"
==
0.0.0.0
]]
;
then
echo
'FAIL: 无法确定应用机私网地址,不能验证网关上游'
>
&2
return
1
fi
printf
'%s'
"
$addr
"
}
# $1 URL,$2 web|health|ready;只接受 HTTP 200,健康接口必须核验 JSON 状态。
verify_deploy_endpoint
()
{
local
url
=
"
$1
"
kind
=
"
$2
"
out code body
out
=
$(
curl
--noproxy
'*'
-sS
--connect-timeout
3
--max-time
10
-w
'\n%{http_code}'
"
$url
"
)
||
{
echo
"FAIL: 私网端点不可达:
${
url
}
;请检查 managed 端口绑定"
>
&2
return
1
}
code
=
${
out
##*
$'
\n
'
}
body
=
${
out
%
$'
\n
'
*
}
if
[[
"
$code
"
!=
200
]]
;
then
echo
"FAIL: 私网端点
$url
返回 HTTP
$code
"
>
&2
return
1
fi
if
[[
"
$kind
"
!=
web
]]
&&
!
grep
-Eq
'"status"[[:space:]]*:[[:space:]]*"ok"'
<<<
"
$body
"
;
then
echo
"FAIL: 私网健康检查不通过:
$url
"
>
&2
return
1
fi
if
[[
"
$kind
"
==
ready
]]
&&
!
grep
-Eq
'"db"[[:space:]]*:[[:space:]]*"up"'
<<<
"
$body
"
;
then
echo
"FAIL: 私网数据库就绪检查不通过:
$url
"
>
&2
return
1
fi
echo
"私网端点通过:
$url
"
}
deploy/deploy-prod.sh
View file @
8010ba3c
...
...
@@ -15,9 +15,10 @@
# 故生产机停在 main、测试机停在 test 即可;临时切别的分支用 DEPLOY_BRANCH 覆盖。
#
# 用法(服务器上,~/pac):
# bash deploy/deploy-prod.sh # 部署当前分支(生产机=main / 测试机=test)
# COMPOSE_MANAGED=1 bash deploy/deploy-prod.sh # 生产(main,远端网关)
# bash deploy/deploy-prod.sh # 测试(test,本机代理)
# DEPLOY_BRANCH=test bash deploy/deploy-prod.sh # 显式指定分支(首次把某机切到该分支)
#
bash deploy/deploy-prod.sh --no-pull #
跳过 git 拉取(代码已就位时)
#
COMPOSE_MANAGED=1 bash deploy/deploy-prod.sh --no-pull # 生产
跳过 git 拉取(代码已就位时)
set
-euo
pipefail
log
()
{
printf
'\n\033[1;36m== %s ==\033[0m\n'
"
$*
"
;
}
...
...
@@ -43,6 +44,10 @@ probe_health() { # $1=base(如 http://127.0.0.1:3101) → stdout "<code> <body
main
()
{
cd
"
$(
dirname
"
$0
"
)
/.."
source
deploy/deploy-mode.sh
local
BRANCH
=
"
${
DEPLOY_BRANCH
:-$(
git rev-parse
--abbrev-ref
HEAD
)}
"
check_deploy_mode
"
$BRANCH
"
# 与 FridayAI 共用 checkout,发布期间禁止另一套流程修改源码或构建。
exec
9>
"
$(
git rev-parse
--git-common-dir
)
/deploy.lock"
flock
-n
9
||
die
"PAC 或 FridayAI 正在部署"
...
...
@@ -84,8 +89,6 @@ main() {
# 部署哪个分支:规约 main=生产 / test=测试。默认跟随本机当前 checkout 的分支(不写死),
# 故生产机(在 main)自然部署 main、测试机(在 test)自然部署 test;需临时切分支再用 DEPLOY_BRANCH 覆盖。
local
BRANCH
=
"
${
DEPLOY_BRANCH
:-$(
git rev-parse
--abbrev-ref
HEAD
)}
"
if
[[
"
${
1
:-}
"
!=
"--no-pull"
]]
;
then
log
"拉取分支
$BRANCH
(fetch + checkout + ff-only)"
git fetch origin
"
$BRANCH
"
...
...
@@ -182,13 +185,10 @@ main() {
# 这里额外验私网 IP 可达,把"loopback 通、网关不通"的漂移在部署当场拦下(2026-07-18 踩过)。
if
[[
"
${
COMPOSE_MANAGED
:-
0
}
"
==
"1"
]]
;
then
log
"验证 3b:私网 IP 可达(网关路径,防 loopback-only 绑定)"
local
priv phealth pweb
priv
=
$(
ip route get 1 2>/dev/null | awk
'{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1);exit}}'
)
[[
-n
"
$priv
"
]]
||
die
"拿不到私网 IP(ip route get 1 解析失败)"
phealth
=
$(
probe_health
"http://
$priv
:3101"
)
||
die
"私网
$priv
:3101 /health 不健康(
$phealth
)— 应用可能绑成 loopback,网关够不到(会 502)。托管环境需 web/service 绑 0.0.0.0(见 docker-compose.managed.yml)"
pweb
=
$(
curl
-s
-m
30
-o
/dev/null
-w
'%{http_code}'
"http://
$priv
:3100/plans"
||
true
)
[[
"
$pweb
"
==
"200"
]]
||
die
"私网
$priv
:3100 web=
$pweb
— 同上(loopback 绑定 → 网关 502)"
echo
" 私网
$priv
health 200 / web 200(网关路径 OK)"
local
priv
priv
=
$(
deploy_private_ip
)
verify_deploy_endpoint
"http://
$priv
:3101/health"
health
verify_deploy_endpoint
"http://
$priv
:3100/plans"
web
fi
log
"部署成功 ✅
$(
git log
--oneline
-1
)
"
...
...
deploy/production/friday-ai-gateway.conf.example
View file @
8010ba3c
# 在现有生产网关安装。
替换 AI_DOMAIN、RELAY_DOMAIN、
证书路径后 nginx -t。
# 在现有生产网关安装。
域名统一为 fai.friday.tech;填写实际
证书路径后 nginx -t。
# 前提:只有此网关可访问应用机 3103/3104/3106;真实来源仅一层代理。
server {
listen 80;
server_name
AI_DOMAIN RELAY_DOMAIN
;
server_name
fai.friday.tech
;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name
AI_DOMAIN
;
server_name
fai.friday.tech
;
ssl_certificate /REQUIRED/AI/fullchain.pem;
ssl_certificate_key /REQUIRED/AI/privkey.pem;
client_max_body_size 25m;
...
...
@@ -27,13 +27,7 @@ server {
}
location = /health { proxy_pass http://172.16.4.239:3104; access_log off; }
location = /health/ready { proxy_pass http://172.16.4.239:3104; access_log off; }
location / { proxy_pass http://172.16.4.239:3103; }
}
server {
listen 443 ssl;
server_name RELAY_DOMAIN;
ssl_certificate /REQUIRED/RELAY/fullchain.pem;
ssl_certificate_key /REQUIRED/RELAY/privkey.pem;
# 回调直接交给独立中转,AI 停服时仍可接收。
location = /hook/ecom {
limit_except POST { deny all; }
client_max_body_size 20m;
...
...
@@ -44,5 +38,10 @@ server {
proxy_connect_timeout 2s;
proxy_read_timeout 8s;
}
location / { return 404; }
# 不暴露中转的本地调试和管理入口。
location = /local { return 404; }
location ^~ /local/ { return 404; }
location = /status { return 404; }
location ^~ /hook/ { return 404; }
location / { proxy_pass http://172.16.4.239:3103; }
}
deploy/tests/deploy-ai.test.mjs
View file @
8010ba3c
...
...
@@ -15,12 +15,14 @@ function runDeploy({
piped = false,
managed = false,
target = 'staging',
privateFail = false,
} = {}) {
const root = mkdtempSync(join(tmpdir(), 'ai-deploy-flow-'));
const bin = join(root, 'bin'),
log = join(root, 'commands.log');
for (const dir of [bin, join(root, '.git'), join(root, 'deploy')]) mkdirSync(dir);
copyFileSync(source, join(root, 'deploy/deploy-ai.sh'));
copyFileSync(resolve('deploy/deploy-mode.sh'), join(root, 'deploy/deploy-mode.sh'));
writeFileSync(log, '');
const common = 'printf "%s\\n" "$(basename "$0") $*" >> "$DEPLOY_TEST_LOG"\n';
const stubs = {
...
...
@@ -30,6 +32,9 @@ function runDeploy({
'rev-parse --git-common-dir') echo .git ;;
esac`,
flock: 'exit 0',
ip: 'echo "1 via 172.16.4.1 dev eth0 src 172.16.4.239"',
curl: `[[ "$PRIVATE_FAIL" != 1 ]] || exit 7
printf '{"status":"ok","db":"up"}\\n200'`,
docker: String.raw`if [[ ( "$*" == *' run '* || "$*" == *' exec '* ) && "$*" != *'--interactive=false'* ]]; then cat >/dev/null; fi
if [[ -n "$DEPLOY_TEST_FAIL" && "$*" == *"$DEPLOY_TEST_FAIL"* ]]; then exit 43; fi
case "$*" in
...
...
@@ -62,7 +67,8 @@ function runDeploy({
...process.env,
PATH: `${bin}:${process.env.PATH}`,
DEPLOY_TEST_LOG: log,
COMPOSE_MANAGED: managed ? '1' : '0',
COMPOSE_MANAGED: typeof managed === 'string' ? managed : managed ? '1' : '0',
PRIVATE_FAIL: privateFail ? '1' : '0',
DEPLOY_TEST_FAIL: fail,
DEPLOY_TEST_BRANCH: branch,
DEPLOY_TEST_MISMATCH: mismatch ? '1' : '0',
...
...
@@ -141,3 +147,28 @@ test('生产托管模式每个 Compose 操作均加载生产端口覆盖', () =>
.every((c) => c.includes('-f docker-compose.ai.managed.yml')),
);
});
for (const managed of [false, '', 'true']) {
test(`生产未明确开启托管模式时提前拒绝: ${managed}`, () => {
const { result, commands } = runDeploy({ target: 'production', branch: 'main', managed });
assert.notEqual(result.status, 0);
assert.match(result.stderr, /COMPOSE_MANAGED/);
assert.ok(!commands.some((c) => /^(docker|flock) |git fetch|git merge/.test(c)));
});
}
test('测试误开托管模式时提前拒绝', () => {
const { result, commands } = runDeploy({ managed: true });
assert.notEqual(result.status, 0);
assert.ok(!commands.some((c) => c.startsWith('docker ')));
});
test('AI 容器健康但私网不可达时不能报告部署完成', () => {
const { result } = runDeploy({
target: 'production',
branch: 'main',
managed: true,
privateFail: true,
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /私网端点不可达/);
assert.doesNotMatch(result.stdout, /FridayAI 部署完成/);
});
deploy/tests/deploy-ingress.test.mjs
View file @
8010ba3c
...
...
@@ -4,30 +4,42 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, rmSy
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
function run({ check = false, fail = '' } = {}) {
function run({
check = false,
fail = '',
managed = '1',
privateFail = false,
target = 'production',
} = {}) {
const root = mkdtempSync(join(tmpdir(), 'ingress-deploy-'));
try {
for (const d of ['deploy', 'bin', '.git']) mkdirSync(join(root, d));
copyFileSync(resolve('deploy/deploy-ingress.sh'), join(root, 'deploy/deploy-ingress.sh'));
copyFileSync(resolve('deploy/deploy-mode.sh'), join(root, 'deploy/deploy-mode.sh'));
const log = join(root, 'log');
writeFileSync(log, '');
const scripts = {
git: `case "$*" in 'branch --show-current') echo
main
;; 'rev-parse --git-common-dir') echo .git;; 'rev-parse --short HEAD') echo abc123;; esac`,
git: `case "$*" in 'branch --show-current') echo
"$BRANCH"
;; 'rev-parse --git-common-dir') echo .git;; 'rev-parse --short HEAD') echo abc123;; esac`,
flock: 'exit 0',
ip: 'echo "1 via 172.16.4.1 dev eth0 src 172.16.4.239"',
curl: `[[ "$PRIVATE_FAIL" != 1 ]] || exit 7
printf '{"status":"ok","db":"up"}\\n200'`,
docker: `printf '%s\\n' "$*" >> "$LOG"\n[[ -z "$FAIL" || "$*" != *"$FAIL"* ]] || exit 43\ncase "$*" in *' ps -q channel-ingress') echo container;; 'image inspect '*|'inspect '*) echo sha256:expected;; esac`,
};
for (const [n, s] of Object.entries(scripts))
writeFileSync(join(root, 'bin', n), '#!/bin/bash\nset -eu\n' + s, { mode: 0o755 });
const r = spawnSync(
'bash',
[join(root, 'deploy/deploy-ingress.sh'),
'production'
, ...(check ? ['--check'] : [])],
[join(root, 'deploy/deploy-ingress.sh'),
target
, ...(check ? ['--check'] : [])],
{
encoding: 'utf8',
timeout: 10000,
env: {
...process.env,
PATH: join(root, 'bin') + ':' + process.env.PATH,
COMPOSE_MANAGED: '1',
COMPOSE_MANAGED: managed,
PRIVATE_FAIL: privateFail ? '1' : '0',
BRANCH: target === 'production' ? 'main' : 'test',
LOG: log,
FAIL: fail,
},
...
...
@@ -64,3 +76,24 @@ test('中转路由校验失败保持原容器,成功后才切换镜像', () =>
assert.ok(good.log.indexOf('verifyRouting') < good.log.indexOf('up -d'));
assert.doesNotMatch(good.log, /stop ai/);
});
for (const managed of ['', '0', 'true']) {
test(`生产中转漏配或错配托管模式时不调用 Docker: ${managed}`, () => {
const { r, log } = run({ managed });
assert.notEqual(r.status, 0);
assert.match(r.stderr, /COMPOSE_MANAGED/);
assert.equal(log, '');
});
}
test('测试中转本机模式正常,误开托管模式拒绝', () => {
assert.equal(run({ target: 'staging', managed: '0', check: true }).r.status, 0);
const { r, log } = run({ target: 'staging' });
assert.notEqual(r.status, 0);
assert.equal(log, '');
});
test('中转私网不通时不能报告部署完成', () => {
const { r } = run({ privateFail: true });
assert.notEqual(r.status, 0);
assert.match(r.stderr, /私网端点不可达/);
assert.doesNotMatch(r.stdout, /中转部署完成/);
});
deploy/tests/deploy-mode.test.mjs
0 → 100644
View file @
8010ba3c
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, copyFileSync, rmSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
const helper = resolve('deploy/deploy-mode.sh');
function probe(body, code = '200', kind = 'ready') {
const root = mkdtempSync(join(tmpdir(), 'deploy-probe-'));
try {
writeFileSync(join(root, 'curl'), '#!/bin/sh\nprintf "%s\\n%s" "$BODY" "$CODE"\n', {
mode: 0o755,
});
return spawnSync(
'bash',
[
'-c',
'source "$1"; verify_deploy_endpoint http://172.16.4.239:3104/health/ready "$2"',
'test',
helper,
kind,
],
{
encoding: 'utf8',
timeout: 5000,
env: { ...process.env, PATH: root + ':' + process.env.PATH, BODY: body, CODE: code },
},
);
} finally {
rmSync(root, { recursive: true, force: true });
}
}
test('健康检查验证业务状态,不能把 HTTP 200 错误页视为成功', () => {
assert.equal(probe('{"status": "ok", "db": "up"}').status, 0);
for (const body of ['{"code":10004}', '{"status":"ok","db":"down"}', '
<html>
error
</html>
'])
assert.notEqual(probe(body).status, 0);
assert.notEqual(probe('{"status":"ok","db":"up"}', '502').status, 0);
assert.equal(probe('{"status":"ok"}', '200', 'health').status, 0);
assert.equal(probe('
<html>
login
</html>
', '200', 'web').status, 0);
});
test('PAC main 分支漏配托管模式,即使没有 env,也在 Docker 和发布锁之前拒绝', () => {
const root = mkdtempSync(join(tmpdir(), 'pac-mode-'));
try {
for (const d of ['deploy', 'bin']) mkdirSync(join(root, d));
for (const f of ['deploy-prod.sh', 'deploy-mode.sh'])
copyFileSync(resolve('deploy', f), join(root, 'deploy', f));
writeFileSync(join(root, 'bin/git'), '#!/bin/sh\necho main\n', { mode: 0o755 });
for (const f of ['docker', 'flock'])
writeFileSync(join(root, 'bin', f), '#!/bin/sh\necho called >> "$LOG"\nexit 99\n', {
mode: 0o755,
});
const log = join(root, 'log');
writeFileSync(log, '');
const env = { ...process.env, PATH: join(root, 'bin') + ':' + process.env.PATH, LOG: log };
delete env.COMPOSE_MANAGED;
delete env.DEPLOY_BRANCH;
const r = spawnSync('bash', [join(root, 'deploy/deploy-prod.sh'), '--no-pull'], {
env,
encoding: 'utf8',
timeout: 5000,
});
assert.notEqual(r.status, 0);
assert.match(r.stderr, /COMPOSE_MANAGED=1/);
assert.equal(readFileSync(log, 'utf8'), '');
} finally {
rmSync(root, { recursive: true, force: true });
}
});
deploy/tests/deploy-pac.test.mjs
View file @
8010ba3c
...
...
@@ -8,22 +8,38 @@ import { spawnSync } from 'node:child_process';
test('PAC 迁移失败保留应用和已运行的基础设施', () => {
const root = mkdtempSync(join(tmpdir(), 'pac-migration-failure-'));
try {
for (const d of ['bin', '.git', 'deploy', 'apps/pac-service']) mkdirSync(join(root, d), { recursive: true });
for (const d of ['bin', '.git', 'deploy', 'apps/pac-service'])
mkdirSync(join(root, d), { recursive: true });
copyFileSync(resolve('deploy/deploy-prod.sh'), join(root, 'deploy/deploy-prod.sh'));
writeFileSync(join(root, 'apps/pac-service/.env'), 'DATABASE_URL=postgresql://dev:dev@localhost:5432/pac\n');
copyFileSync(resolve('deploy/deploy-mode.sh'), join(root, 'deploy/deploy-mode.sh'));
writeFileSync(
join(root, 'apps/pac-service/.env'),
'DATABASE_URL=postgresql://dev:dev@localhost:5432/pac\n',
);
const log = join(root, 'calls');
writeFileSync(log, '');
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',
flock: 'exit 0',
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 });
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 });
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);
const calls = readFileSync(log, 'utf8');
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)/);
} finally { rmSync(root, { recursive: true, force: true }); }
} finally {
rmSync(root, { recursive: true, force: true });
}
});
docs/friday-ai-production-deployment.md
View file @
8010ba3c
...
...
@@ -18,22 +18,22 @@
| RDS 规模 | 统计估算约 122 万患者、4,732 万事实、4,722 万事务;连接当时 42 / 820。未执行全表计数 |
| 基础工具 | Compose 2.32.4,满足 raw env / override 要求;主机未发现
`psql`
、
`nginx`
、
`jq`
。Node/pnpm 随镜像提供,不需要安装到主机 |
生产仍使用同一份
`/data/pac`
源码,三个 Compose 项目独立发布。生产命令统一加
`COMPOSE_MANAGED=1`
,加载仓库中的私网端口覆盖;测试环境不加。无需主机 Node/pnpm。
生产仍使用同一份
`/data/pac`
源码,三个 Compose 项目独立发布。生产命令统一加
`COMPOSE_MANAGED=1`
,加载仓库中的私网端口覆盖;测试环境不加。
三个部署入口均在构建/迁移/停服务前检查:生产必须显式设为
`1`
,测试拒绝设为
`1`
,其他非法值直接退出;不自动切换模式。部署后还校验应用机私网端点,失败不会报告部署成功,但仍需运维验证网关和安全组。
无需主机 Node/pnpm。
准备执行记录见
[
上线准备验收
](
qa/2026-10-10-production-readiness.md
)
。
本手册中的域名、账号和供应商值是待运维填写项,不能直接用测试值替代
。
准备执行记录见
[
上线准备验收
](
qa/2026-10-10-production-readiness.md
)
。
生产域名已确定为
`fai.friday.tech`
,DNS 与证书待运维配置。OSS 和模型凭据按已确认方案复用测试值,其余账号及供应商配置仍需分别核实
。
## 2. 发布前交付清单
| 提供方 | 必需资料 / 动作 |
| --- | --- |
| 运维:域名 |
确认 AI 生产域名与回调域名,例如
`fai.friday.tech`
、
`relay.friday.tech`
(仅建议,尚未确认);DNS、HTTPS 证书、网关配置权限
|
| 运维:域名 |
为
`fai.friday.tech`
配置 DNS 指向现有生产网关,签发 HTTPS 证书;AI 与 ECom 回调共用此域名
|
| 运维:网络 | 网关到应用机私网 IP 的 3103 / 3104 / 3106;仅允许网关来源,禁止公网直连。核对真实代理层数;确保应用机容器可访问 RDS、ECom、OSS、模型服务 |
| 运维:RDS | 在现有实例建独立
`friday_ai`
库和专用账号;当前
`pac`
用户没有 CREATEDB / CREATEROLE 权限。由有权限人员操作,不复用 PAC 数据库账号 |
| 运维:备份 | PAC 发布前可恢复的 RDS 备份;AI 上线后同样纳入自动备份和恢复演练。三个大表索引已经建好;仍需确认上线后的 RDS 磁盘、IOPS 余量与发布窗口(本次没有云控制台权限核查) |
| 业务 / ECom | 生产 API 地址、accessId、accessSecret、回调出口 IP、生产回调 URL 登记;确认凭据/回调能与测试隔离,不能把现有测试回调直接改成生产 |
| 业务 / ECom | 首个验收企微号、收费/设备额度及归属企业;不能与本地/测试同时托管同一号,不能在重试时重复申请设备 |
| 运维:OSS |
生产 region、私有 bucket、RAM 凭据;允许必要读写及签名下载,前缀
`friday-ai-prod`
。不复制测试附件和密钥
|
| 模型 |
生产模型 API Key、额度;代码默认阿里云兼容接口 /
`qwen3.8-max`
。需确认该账号有模型访问权限;换端点/型号时显式配置
|
| 运维:OSS |
复用测试 region、bucket 与 AccessKey;生产前缀
`friday-ai-prod`
,对象保持 private;确认授权覆盖新前缀、生命周期规则不误删生产附件
|
| 模型 |
复用测试百炼 API Key 与
`qwen3.8-max`
,两环境共享账号模型配额;上线前核实实际额度,测试避免批量压测
|
| 实施:客户 | 集团名称、首位客户管理员姓名/手机号、首批诊所及对应 PAC 节点、企微绑定员工、席位数量和期限 |
| 实施:平台 | 平台管理员姓名/手机号;生产使用正式资料,不默认复制测试的“小克”或本地账号 |
...
...
@@ -139,13 +139,13 @@ AI_ECOM_ACCESS_ID=<生产凭据>
AI_ECOM_ACCESS_SECRET=<生产凭据>
AI_INGRESS_TOKEN=<ingress.key内容>
AI_TRUST_PROXY_HOPS=1
AI_LLM_API_KEY=<
生产
模型密钥>
AI_LLM_API_KEY=<
与测试共用的
模型密钥>
AI_OUTBOX_WORKER=off
AI_SCHEDULER=off
AI_OSS_REGION=<
生产
region>
AI_OSS_BUCKET=<
生产
bucket>
AI_OSS_ACCESS_KEY_ID=<
生产
OSS凭据>
AI_OSS_ACCESS_KEY_SECRET=<
生产
OSS凭据>
AI_OSS_REGION=<
与测试相同的
region>
AI_OSS_BUCKET=<
与测试相同的
bucket>
AI_OSS_ACCESS_KEY_ID=<
与测试共用的
OSS凭据>
AI_OSS_ACCESS_KEY_SECRET=<
与测试共用的
OSS凭据>
AI_OSS_PREFIX=friday-ai-prod
```
...
...
@@ -243,13 +243,13 @@ ingress() { docker compose -p pac-ingress -f docker-compose.ingress.yml \
| 生产域名 | 路由 | 上游 |
| --- | --- | --- |
|
确认后的 AI 域名
|
`/api/`
、
`/health`
、
`/health/ready`
|
`http://172.16.4.239:3104`
|
|
`fai.friday.tech`
|
`/api/`
、
`/health`
、
`/health/ready`
|
`http://172.16.4.239:3104`
|
| 同上 |
`/`
|
`http://172.16.4.239:3103`
|
|
确认后的回调域名
| 仅
`POST /hook/ecom`
|
`http://172.16.4.239:3106`
|
|
`fai.friday.tech`
| 仅
`POST /hook/ecom`
|
`http://172.16.4.239:3106`
|
完整模板:
`deploy/production/friday-ai-gateway.conf.example`
;
替换域名、
证书路径并核对私网 IP。AI 行为与
`deploy/nginx/friday-ai-test.conf`
对齐,保留 API 原路径、SSE 关闭缓冲/缓存、HTTP/1.1、长连接读取超时、25m 上传上限;改域名/证书/上游 IP。不加阻止宿主 iframe 的 X-Frame-Options/CSP。AI 与 PAC 使用独立域名,保留 PAC 现有 server 不动。
完整模板:
`deploy/production/friday-ai-gateway.conf.example`
;
域名已固定,填写实际
证书路径并核对私网 IP。AI 行为与
`deploy/nginx/friday-ai-test.conf`
对齐,保留 API 原路径、SSE 关闭缓冲/缓存、HTTP/1.1、长连接读取超时、25m 上传上限;改域名/证书/上游 IP。不加阻止宿主 iframe 的 X-Frame-Options/CSP。AI 与 PAC 使用独立域名,保留 PAC 现有 server 不动。
回调 HTTPS server 中使用以下 location,其余路径默认 404。生产不开放
`/local/*`
、
`/status`
或
调试观察台:
在同一个
`fai.friday.tech`
HTTPS server 中,精确匹配回调路径并直接转发到中转,不经过 AI 服务。生产不开放
`/local/*`
、
`/status`
或中转
调试观察台:
```
nginx
location
=
/hook/ecom
{
...
...
@@ -262,12 +262,17 @@ location = /hook/ecom {
proxy_connect_timeout
2s
;
proxy_read_timeout
8s
;
}
location
/
{
return
404
;
}
location
=
/local
{
return
404
;
}
location
^~
/local/
{
return
404
;
}
location
=
/status
{
return
404
;
}
location
^~
/hook/
{
return
404
;
}
# 页面仍由 Web 服务承接,完整配置见模板。
location
/
{
proxy_pass
http://172.16.4.239:3103
;
}
```
网关覆盖客户端自填来源头;如网关前还有 LB/CDN,须先准确恢复可信来源 IP,再按真实代理层数配置 AI。不能把网关 IP 加入 ECom 供应商白名单来“解决”鉴权失败。
运维
`nginx -t`
成功后 reload;验证 HTTPS
`/login`
、
`/health/ready`
(JSON
`status=ok`
、
`db=up`
)及 PAC
`/health`
。ECom
回调
必须等新中转就绪后再登记,保留测试
`relay.jarvismedical.asia`
的用途与配置。
运维
`nginx -t`
成功后 reload;验证 HTTPS
`/login`
、
`/health/ready`
(JSON
`status=ok`
、
`db=up`
)及 PAC
`/health`
。ECom
生产回调登记为
`https://fai.friday.tech/hook/ecom`
,
必须等新中转就绪后再登记,保留测试
`relay.jarvismedical.asia`
的用途与配置。
## 10. 开户、组织、席位
...
...
@@ -346,7 +351,7 @@ install -m 644 deploy/production/friday-ai.logrotate /etc/logrotate.d/friday-ai-
**运维须配置通知,否则日志不等于告警:**
云日志采集
`/var/log/friday-ai-health.log`
,连续三次
`degraded`
或五分钟未收到记录告警;配置接收人。另用云拨测或独立探针主机每分钟请求生产 AI
`/health/ready`
,同时检查
`status=ok`
、
`db=up`
,连续三次失败告警。监控 RDS 存储/IOPS/连接、应用机内存和证书到期。
可复用
`deploy/health-watch.sh`
在另一台服务器运行:
`WATCH_URL=https://
<AI域名>`
、
`WATCH_PATH=/health/ready`
、
`WATCH_REQUIRE_DB=1`
、
`WATCH_NAME=friday-ai-prod`
,私密配置通知 webhook,沿用该脚本的失败/恢复和每日心跳机制。不要覆盖现有 PAC 探针配置;生产域名未确定前不
启用该探针。
可复用
`deploy/health-watch.sh`
在另一台服务器运行:
`WATCH_URL=https://
fai.friday.tech`
、
`WATCH_PATH=/health/ready`
、
`WATCH_REQUIRE_DB=1`
、
`WATCH_NAME=friday-ai-prod`
,私密配置通知 webhook,沿用该脚本的失败/恢复和每日心跳机制。不要覆盖现有 PAC 探针配置;DNS、证书和服务验收完成后再
启用该探针。
服务器巡检覆盖进程内故障和积压,外部拨测覆盖主机/代理不可达,两者均不依赖开发电脑。用受控验收演练验证通知实际到达,再开放业务。
...
...
@@ -358,6 +363,6 @@ install -m 644 deploy/production/friday-ai.logrotate /etc/logrotate.d/friday-ai-
-
中转独立更新:第 8 节中转段;仍有自身重启窗口,预先核对供应商重试策略。
-
回退先保存部署 SHA、运行镜像 ID、配置备份、迁移记录及队列状态,由开发确认旧镜像兼容已迁移 schema,再切旧镜像;不得直接 git reset / Prisma reset 或整库恢复。
-
AI 与中转共用
`friday_ai`
库。AI 停止时中转仍在写,整库恢复会覆盖备份后已向 ECom 确认的消息;必须先保全队列及 AI 回执,再制定恢复步骤。
-
本次生产初始化不复用测试数据库、用户、企微设备或
密钥
;不改既有 PAC 宿主授权,不部署自动回写服务。
-
本次生产初始化不复用测试数据库、用户、企微设备或
内部服务密钥;仅 OSS 和模型 API 凭据按已确认方案共用
;不改既有 PAC 宿主授权,不部署自动回写服务。
交付记录:发布 SHA、各镜像标签/ID、RDS 备份 ID、迁移结果、域名/证书、开户/席位编号、功能开关、真实读取/隔离/消息补发验收、已知限制。未使用的步骤汇总能力已撤掉,不执行回填。凭据单独交付。
docs/friday-ai-production-handoff.md
View file @
8010ba3c
# FridayAI
首次生产上线:运维交接
单
# FridayAI
生产上线:运维协助清
单
应用机
`ssh pac-friday`
,源码
`/data/pac`
,分支
`main`
。PAC 已运行;FridayAI 和中转尚未上线。详细命令见
[
生产部署步骤
](
friday-ai-production-deployment.md
)
,执行证据见
[
准备记录
](
qa/2026-10-10-production-readiness.md
)
。
## 项目与环境
## 已准备
仓库:
<https://code.jwsmed.com/ai-tools/pac>
(PAC / FridayAI 单仓库)。
-
生产发布脚本、远端网关端口覆盖、独立中转部署和配置预检。
-
服务器健康/队列巡检、cron 与日志轮转模板;不依赖开发电脑。
-
生产专用 JWT 私钥、读取密钥、中转密钥、数据库应用密码及 env 草稿,位于应用机
`/data/pac-ops/readiness-20261010/config/`
,尚未激活。
-
PAC 三个大表索引已于 2026-10-10 01:55 建成并验证有效,合计约 2.73 GiB;5i5ya 诊所名称字典已补齐 66 条。正式发布仍需运行七条迁移登记,已建索引会跳过重复创建。
-
未使用的治疗步骤汇总功能已移除,不需要回填或全量重算患者。
| 环境 | 应用服务器 | 代码目录 / 分支 | 域名 |
| --- | --- | --- | --- |
| 生产(本次操作) |
`47.99.62.30:3335`
;私网
`172.16.4.239`
|
`/data/pac`
/
`main`
| 新增
`fai.friday.tech`
;现有 PAC 为
`pac.friday.tech`
|
| 测试(仅供参考) |
`47.251.104.47:19822`
|
`/root/pac`
/
`test`
|
`fai.jarvismedical.asia`
;回调
`relay.jarvismedical.asia`
|
## 请运维处理
生产 Nginx 在现有独立网关,
**不在应用服务器上**
。沿用
`pac.friday.tech`
的网关(现有入口
`lb1.friday.tech`
,公网
`47.98.152.240`
);请运维核对网关实例后配置。
| 项目 | 运维执行 / 提供 | 验收 |
| --- | --- | --- |
| RDS | 备份 PAC 并登记恢复点;确认磁盘/IOPS;执行
`deploy/production/friday-ai-database.sql`
建独立库和账号,在新库安装两个扩展 | 新应用账号可迁移
`friday_ai`
;UTF8、UTC;不改 PAC 时区 |
| 数据库密码 | 安全交付专用应用账号密码,或使用已生成的
`database.key`
设置 | 两份 env 中 URL 一致;不需要交出 RDS 管理账号 |
| 域名/证书 | 确认 AI 和回调的生产域名,配置 DNS、证书 | HTTPS 正常;不覆盖 PAC 域名 |
| 网关/防火墙 | 按
`deploy/production/friday-ai-gateway.conf.example`
配置;仅网关访问应用机 3103/3104/3106 | API、SSE、上传可用;回调域名只开放 POST
`/hook/ecom`
|
| OSS | 生产私有 bucket、region、应用 RAM 凭据 | 生产附件上传及签名下载成功 |
| ECom | 生产 API 凭据、回调出口 IP;登记独立生产回调 | 测试回调保持原样;生产无本地分流 |
| 模型 | 生产 API Key 和模型访问额度 | 测试调用成功 |
| 告警 | 采集巡检日志;连续三次异常或五分钟缺日志告警;外部拨测;登记接收人 | 受控演练可收到告警和恢复通知 |
## 1. RDS:新建库和专用账号
业务还需提供:集团正式名称、首位管理员姓名/手机号、平台管理员资料、首批诊所/员工范围、批准的席位数量/期限、专用验收企微号。先接 5i5ya:
`partner=jvs-dw`
、
`partnerTenant=ruier-grp`
;无需提供其 PAC appSecret,不创建 Friday SaaS 生产租户
。
实例:
`fridaypac.pg.rds.aliyuncs.com:5432`
(现有 PAC 使用的 PostgreSQL 实例)
。
## 实施顺序
用 RDS 管理账号连接
`postgres`
库,执行以下 SQL;
`CREATE DATABASE`
不放在事务中:
1.
核对准备记录和索引状态;确认 RDS 备份、网关限制、所有生产配置已齐。
2.
备份现有配置和镜像记录;
`main`
快进到本次经过测试的 SHA,保护现有未跟踪文件。
3.
补全 env 草稿、安装配置和私钥;PAC/AI 使用同一服务读取密钥。
4.
`COMPOSE_MANAGED=1 bash deploy/deploy-prod.sh --no-pull`
升级 PAC,核验七条迁移及真实读取;不首灌、不回填、不全量重算。
5.
`COMPOSE_MANAGED=1 bash deploy/deploy-ai.sh production --no-pull`
。
6.
`COMPOSE_MANAGED=1 bash deploy/deploy-ingress.sh production`
;检查健康,启用网关和巡检。
7.
CLI 开户,配置组织/员工范围及席位;验收 5i5ya 患者数据和诊所隔离。
8.
先开 worker、后按业务确认开 scheduler;专用号验收收发/图片/文件及 AI 停服补投,再开放正式业务。
```
sql
CREATE
ROLE
friday_ai
LOGIN
;
CREATE
DATABASE
friday_ai
OWNER
friday_ai
TEMPLATE
template0
ENCODING
'UTF8'
LC_COLLATE
'C'
LC_CTYPE
'en_US.utf8'
;
ALTER
DATABASE
friday_ai
SET
timezone
TO
'UTC'
;
```
**生产发布仍有短暂停服;本次没有承诺零停机。**
中转保护 AI 停服期间已持久接收的回调,不覆盖中转、主机或数据库自身故障。首次上线不能照搬测试账号、设备或密钥。
随后
**切换连接到 `friday_ai` 库**
,执行:
```
sql
CREATE
EXTENSION
IF
NOT
EXISTS
pgcrypto
;
CREATE
EXTENSION
IF
NOT
EXISTS
btree_gist
;
```
-
用 RDS 控制台/DMS 设置
`friday_ai`
密码,或在 psql 中执行
`\password friday_ai`
;通过安全渠道交付密码。
-
账号需拥有该库及
`public`
schema 建表、索引、约束和读写权限;仅使用
`friday_ai`
,不复用
`pac`
账号。
-
确认应用机
`172.16.4.239`
可连接该实例。
-
不改现有
`pac`
库、账号及实例全局时区。若库或账号已存在,核对后补齐权限,不删除重建。
-
发布前确认 PAC 可恢复备份和恢复点、RDS 磁盘/IOPS 余量;新库纳入自动备份。
SQL 文件:
[
friday-ai-database.sql
](
../deploy/production/friday-ai-database.sql
)
(扩展语句须切换库后执行)。
## 2. 域名、证书与网关
1.
新增
`fai.friday.tech`
的 DNS,指向现有生产网关;申请 HTTPS 证书并配置自动续期。
2.
在网关新增独立站点,使用
[
friday-ai-gateway.conf.example
](
../deploy/production/friday-ai-gateway.conf.example
)
,替换两处证书路径。
3.
配置网关到应用机的网络放行:仅允许网关访问
`172.16.4.239`
的 TCP
`3103 / 3104 / 3106`
,不开放公网直连。
4.
执行
`nginx -t`
,通过后 reload;保留现有 PAC 和测试站点配置。
模板已包含以下转发规则,请完整保留:
| 路径 | 上游 |
| --- | --- |
|
`/`
|
`http://172.16.4.239:3103`
|
|
`/api/`
、
`/health`
、
`/health/ready`
|
`http://172.16.4.239:3104`
|
| 仅 POST
`/hook/ecom`
|
`http://172.16.4.239:3106`
|
模板已配置 SSE 禁用缓冲、长连接超时和上传限制,并禁止访问中转调试/管理路径。若网关前还有其他代理,请告知代理层数和真实来源 IP 的传递方式。
应用部署完成后共同验收:
`https://fai.friday.tech/login`
可打开;
`/health/ready`
返回 HTTP 200 且包含
`status=ok`
、
`db=up`
。应用未启动前上游暂不可用属预期,正式切换回调由我们协调。
## 3. 接入现有监控告警(应用上线后启用)
-
采集应用机
`/var/log/friday-ai-health.log`
:连续三条
`degraded`
,或五分钟无新日志时通知负责人。
-
外部拨测
`https://fai.friday.tech/health/ready`
:每分钟一次,连续三次失败告警,检查 HTTP 状态和上述 JSON 字段。
-
纳入主机内存/磁盘、RDS 存储/IOPS/连接数、证书到期监控,配置异常及恢复通知。
## 完成后反馈
-
新库/账号已就绪、密码已安全交付、备份恢复点。
-
DNS、证书、网关配置完成,网络规则已放行;有额外代理时说明。
-
告警接收人及通知渠道。
docs/friday-ai-test-deployment.md
View file @
8010ba3c
...
...
@@ -5,6 +5,7 @@
-
服务器:
`ssh -p 19822 root@47.251.104.47`
。
-
源码:
`/root/pac`
,分支
`test`
;feature 合入 test,验证通过后再由 feature 合入 main。
-
域名:
`https://fai.jarvismedical.asia`
;Web 3103、API 3104,仅发布到主机 loopback。
-
测试发布不设置
`COMPOSE_MANAGED`
(或设为
`0`
);误设为
`1`
会在构建、迁移和重启前被拒绝。
-
使用 Docker Compose;Node、pnpm 随镜像提供,不安装主机运行时,不使用 systemd。
-
PAC 已有
`pac_default`
网络及 PostgreSQL;AI 使用独立
`friday_ai`
库、独立
`pac-ai`
Compose 项目。
...
...
docs/qa/2026-10-10-production-readiness.md
View file @
8010ba3c
...
...
@@ -55,6 +55,6 @@
## 运维交付
按
[
生产部署
](
../friday-ai-production-deployment.md
)
实施。需要运维执行的 SQL、网关、cron、日志轮转模板在
`deploy/production/`
。
待提供:生产域名/证书,专用数据库账号,ECom/OSS/模型生产配置
,告警接收人,首个集团/管理员/诊所/席位信息。无需交付 RDS 管理账号。
按
[
生产部署
](
../friday-ai-production-deployment.md
)
实施。需要运维执行的 SQL、网关、cron、日志轮转模板在
`deploy/production/`
。
后续已确定 AI 与回调共用
`fai.friday.tech`
,OSS 和模型凭据复用测试配置(OSS 生产前缀
`friday-ai-prod`
)。待提供/落实:该域名 DNS/证书,专用数据库账号,ECom 生产配置及共用资源权限/额度核验
,告警接收人,首个集团/管理员/诊所/席位信息。无需交付 RDS 管理账号。
生产准备工具已验证后合入 main
`ad25aed`
、test
`938dff5`
;本次收尾仅更新完成记录及运维步骤,不更改应用代码。临时索引/字典任务已退出,收尾自动化在文档同步完成后停用。
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