Harden remote43 acceptance script

This commit is contained in:
phamnazage-jpg
2026-05-23 15:03:59 +08:00
parent 8c364206c5
commit 728ed9a064
5 changed files with 236 additions and 28 deletions

View File

@@ -18,6 +18,19 @@
- latest-head relay-manager 已新增宿主 capability 自愈: - latest-head relay-manager 已新增宿主 capability 自愈:
- 当第三方 OpenAI-compatible upstream 因宿主把 `openai_responses_supported` 误判成 `true` 而导致 host `/v1/chat/completions` 返回 `502 upstream_error`access closure 与后台 reconcile 会自动把相关 account 修正到 raw `/chat/completions` 路径后再重试 - 当第三方 OpenAI-compatible upstream 因宿主把 `openai_responses_supported` 误判成 `true` 而导致 host `/v1/chat/completions` 返回 `502 upstream_error`access closure 与后台 reconcile 会自动把相关 account 修正到 raw `/chat/completions` 路径后再重试
- 该修正现在不再依赖宿主长期保留补丁,宿主升级后只要下次 import/access/reconcile 触发,就能重新收敛到正确 capability - 该修正现在不再依赖宿主长期保留补丁,宿主升级后只要下次 import/access/reconcile 触发,就能重新收敛到正确 capability
- 2026-05-23 remote43 线上验收脚本已继续收口:
- `scripts/import_remote43_provider.sh` 现已明确拆分 `CRM_HOST_BASE``REMOTE_HOST_BASE`
- 远端 Postgres / Redis 容器已改成按目标宿主端口自动解析,不再硬编码落到 `sub2api-relaymgr-pg/redis`
- 远端 managed probe `/v1/models``/v1/chat/completions` 已改成只走 `REMOTE_HOST_BASE`
- provider status / access status / access preview 末尾查询已补 `host_id`,避免本地 CRM 有多宿主历史时被 `provider exists on multiple hosts` 截断
- `artifacts/real-host-acceptance/20260523_144937_remote43_kimi-a7m_key_import` 已证明:
- 这轮线上 `kimi-a7m` 不再复现“错库取 key 导致统一 401”或“模型列表串成 GPT 默认集合”
- import 已返回 `gateway.status_code=200``models=["kimi-k2.6"]``has_expected_model=true`
- upstream `/models``/chat/completions` 都是 `200`
- 未改宿主的真实阻塞已收缩为 host `/v1/chat/completions` 仍返回 `503/502`,不再是插件脚本的数据面问题
- `artifacts/real-host-acceptance/20260523_145531_remote43_kimi-a7m_key_import` 说明另一类运行时噪音:
- 当本地 SSH 隧道端口存活但链路已失活时,`POST /api/hosts` 阶段会在 `get host version` 处超时
- 这类现象应优先解释为 tunnel/runtime 故障,而不是 provider 导入逻辑回退
- 官方 provider 验证矩阵当前仍保留一条非阻塞事实: - 官方 provider 验证矩阵当前仍保留一条非阻塞事实:
- `artifacts/real-host-acceptance/20260521_222212_remote43_minimax-m2-7-official_key_import/21-summary.json` 已证明 official MiniMax 模板链路是通的,但该验证 key 当前命中 upstream `429` - `artifacts/real-host-acceptance/20260521_222212_remote43_minimax-m2-7-official_key_import/21-summary.json` 已证明 official MiniMax 模板链路是通的,但该验证 key 当前命中 upstream `429`
- `reconcile=drifted` 仍可能在 shared fresh-host 上出现,但当前解释是“历史残留资源噪音”,不阻塞 PRD 首版放行 - `reconcile=drifted` 仍可能在 shared fresh-host 上出现,但当前解释是“历史残留资源噪音”,不阻塞 PRD 首版放行
@@ -122,6 +135,11 @@
- `21-summary.json` 已到 `batch_status=succeeded``provider_status=active` - `21-summary.json` 已到 `batch_status=succeeded``provider_status=active`
- `account_probe_summary` 明确记录 `probe_advisory=true``validation_status=warning`,证明 403 probe race 已被 relay-manager 正确降级 - `account_probe_summary` 明确记录 `probe_advisory=true``validation_status=warning`,证明 403 probe race 已被 relay-manager 正确降级
8. `artifacts/real-host-acceptance/20260523_144937_remote43_kimi-a7m_key_import`
- remote43 未改宿主 + 修正后的 latest-head 验收脚本样本
- 已证明脚本层的“错库取 key / 错地址 / 多 host 历史查询”问题被收掉
- 仍保留的真实阻塞是宿主 completion 路径 `502/503`
## 剩余项P2 / 运营前置,不阻塞按 PRD 首版范围上线) ## 剩余项P2 / 运营前置,不阻塞按 PRD 首版范围上线)
1. 运营前置 1. 运营前置

View File

@@ -623,6 +623,98 @@
- 如果 account `/models` 已对、channel 落库也对,但普通用户流量不对,优先怀疑运营前置或 harness 参数 - 如果 account `/models` 已对、channel 落库也对,但普通用户流量不对,优先怀疑运营前置或 harness 参数
- 不要立即重开“导入代码失效”的结论 - 不要立即重开“导入代码失效”的结论
### 13. remote43 如果同时有“本地 CRM + 远端宿主 + 远端 DB/Redis”最容易错哪三件事
2026-05-23 这一轮 remote43 `kimi-a7m` 复验把最容易反复出错的 3 个点彻底暴露出来了。
#### 1) 把 `CRM_HOST_BASE` 和 `REMOTE_HOST_BASE` 混成一个地址
- 本地运行的 CRM 访问宿主时,应该走本地 SSH 隧道,例如 `http://127.0.0.1:18089`
- 远端 SSH 内部执行 `curl``docker exec` 时,才应该走远端机器自己能看到的地址,例如 `http://127.0.0.1:18097`
- 如果把两者都写成 `18097`,本地 CRM 会尝试访问自己机器上的 `127.0.0.1:18097`,结果在 `POST /api/hosts` 阶段直接掉进 `500 internal_error`
这类错误的现象通常是:
- `01a-create-host.json` 为空
- `03-import.body.json` 直接是 `batch_id=0`
- message 落在 `get host version``probe host capabilities`
经验结论:
- **本地 CRM 到宿主的地址** 和 **远端 SSH 侧到宿主的地址** 必须分开记录
- 以后若脚本同时涉及 `curl CRM API``ssh remote curl host API`,必须显式区分 `CRM_HOST_BASE``REMOTE_HOST_BASE`
#### 2) 远端 DB/Redis 误指到 relaymgr 数据面
之前 remote43 统一 `401 INVALID_API_KEY` 的主因不是 provider key 坏,而是:
- 脚本错误地从 `sub2api-relaymgr-pg` 里找普通用户 key
- 但实际宿主是另一套 fresh-host app + postgres + redis
修正后脚本已经改为:
- 先按目标宿主端口解析远端 `app` 容器
- 再自动推导同栈的 `postgres/redis`
2026-05-23 的 `20260523_144937_remote43_kimi-a7m_key_import` 已证明这条修正生效:
- `subscription_user_key_prefix``managed_user_id``managed_probe_key_prefix` 都来自目标 fresh-host 数据面
- 不再复现统一 `401`
经验结论:
- 远端若同时存在 `relaymgr``fresh-host` 两套栈,**任何 subscription user / api key / group state / redis invalidation 都必须落到目标宿主自己的数据面**
- 不要再靠固定容器名假设
#### 3) provider status / access status 忘了带 `host_id`
当本地 CRM 状态库里同一个 provider 已经跑过多个 host 样本时:
- `GET /api/providers/{provider}/status`
- `GET /api/providers/{provider}/access/status`
- `POST /api/providers/{provider}/access/preview`
如果不显式带 `host_id`,很容易直接返回:
- `provider exists on multiple hosts; host_id is required`
- 外部看起来像验收在最后一步莫名其妙 `400`
经验结论:
- 这不是导入失败,也不是宿主坏了
- 这是 **状态查询维度不完整**
- 对带历史样本的 live CRM所有 provider 尾部查询都应该带 `host_id`
### 14. `20260523_144937_remote43_kimi-a7m_key_import` 到底证明了什么?
这份 artifact 很关键,因为它把“脚本问题”和“宿主问题”拆开了。
它证明了:
- `POST /api/hosts` 已成功
- import 已成功返回 `HTTP 200`
- `gateway.models=["kimi-k2.6"]`
- `has_expected_model=true`
- upstream `/models=200`
- upstream `/chat/completions=200`
同时它也证明:
- 未改宿主的 host `/v1/chat/completions` 仍然返回 `503 Service temporarily unavailable`
- account probe 仍是 `403 Forbidden`,但已经只是 advisory / warning不再阻断 import 主链
经验结论:
- 这份样本可以用来证明:**插件脚本的数据面/地址问题已经修掉**
- 它不能用来证明“宿主已经通过”
- 它应该被归类为:**插件侧修复完成,未改宿主 completion 路径仍异常**
### 15. 如果 create-host 阶段突然又回到 `500`,先查什么?
`20260523_145531_remote43_kimi-a7m_key_import` 提供了另一类重要样本:
- `01a-create-host.json` 仍成功
-`03-import.body.json` 直接写明:
- `get host version: perform GET /api/v1/admin/system/version request`
- `context deadline exceeded`
这说明当时不是 provider key 坏,不是脚本回退,而是:
- 本地 `18089` 隧道虽然监听着端口
- 但到远端宿主的链路已经不再返回字节
经验结论:
- 如果 `host tunnel` 端口还在监听,但 `curl -I --max-time 5 $CRM_HOST_BASE/healthz` 无法返回任何 header
- 那就先把它当成 **隧道失活 / 运行时链路问题**
- 不要先把结论写成“导入逻辑回退”或“provider 又坏了”
### 10. 新增供应链账号或模型后,哪些结果可以算“已确认”,哪些只能算“部分确认”? ### 10. 新增供应链账号或模型后,哪些结果可以算“已确认”,哪些只能算“部分确认”?
#### 可算“已确认” #### 可算“已确认”

View File

@@ -232,17 +232,33 @@ SKIP_ROLLBACK=1 scripts/real_host_acceptance.sh
18. self_service 场景里,普通用户 gateway key 访问宿主 `/v1/models` / `/v1/chat/completions` 时,真实语义是 `Authorization: Bearer <gateway-key>`;若 CRM 的 self_service closure 仍显示 `401/403 broken`,优先排查 gateway probe 是否错误复用了 `x-api-key` 18. self_service 场景里,普通用户 gateway key 访问宿主 `/v1/models` / `/v1/chat/completions` 时,真实语义是 `Authorization: Bearer <gateway-key>`;若 CRM 的 self_service closure 仍显示 `401/403 broken`,优先排查 gateway probe 是否错误复用了 `x-api-key`
19. fresh-host 管理员 bearer token 过期时,最前面的 `POST /api/hosts` / `probe-host` 可能直接表现成 CRM 侧 `502`。遇到这类现象,先刷新 host bearer token再继续验收不要先把它归因为最新代码故障。 19. fresh-host 管理员 bearer token 过期时,最前面的 `POST /api/hosts` / `probe-host` 可能直接表现成 CRM 侧 `502`。遇到这类现象,先刷新 host bearer token再继续验收不要先把它归因为最新代码故障。
20. shared fresh-host 上若 `05-import.json` / `07-access-status.json` 已经 ready`09-reconcile.json` 仍是 `status=drifted`优先把它解释为历史残留资源噪音PRD 首版放行判断应以 import/access 闭环是否打通为主。 20. shared fresh-host 上若 `05-import.json` / `07-access-status.json` 已经 ready`09-reconcile.json` 仍是 `status=drifted`优先把它解释为历史残留资源噪音PRD 首版放行判断应以 import/access 闭环是否打通为主。
21. 如果 CRM 本身运行在本机,而宿主运行在远端 SSH 隧道后面,必须同时明确 2 个地址:
- `CRM_HOST_BASE`:本地 CRM 实际访问宿主时使用的地址,例如 `http://127.0.0.1:18089`
- `REMOTE_HOST_BASE`:远端 SSH 会话内部访问宿主时使用的地址,例如 `http://127.0.0.1:18097`
- 两者不能混用;混用后 `POST /api/hosts` 往往会先在 `get host version` / `probe host capabilities` 处直接变成 `500`
22. 如果远端同时存在 `relaymgr``fresh-host` 两套容器栈,不要手填 `REMOTE_PG_CONTAINER` / `REMOTE_REDIS_CONTAINER` 到旧的 relaymgr 容器。优先按目标宿主端口自动解析同栈的 `postgres/redis`;否则最容易回到“错库取 key 导致统一 401”。
23. 若同一个 provider 已在本地 CRM 状态库里跑过多个宿主样本,尾部查询必须带 `host_id`
- `GET /api/providers/{provider}/status`
- `GET /api/providers/{provider}/access/status`
- `POST /api/providers/{provider}/access/preview`
- 否则很容易在最后一步收到 `provider exists on multiple hosts; host_id is required`
24. 不要把“隧道端口还在 LISTEN”误判成“链路可用”。
-`curl -I --max-time 5 $CRM_HOST_BASE/healthz` 完全收不到 header
- 就应先判定为 tunnel 失活或远端链路异常
- 这类现象会在 `03-import.body.json` 中表现为 `get host version ... context deadline exceeded`
## 建议固定执行的快速诊断顺序 ## 建议固定执行的快速诊断顺序
1. 先看环境 1. 先看环境
- CRM 是否是最新提交对应的在线进程 - CRM 是否是最新提交对应的在线进程
- `PACK_PATH` 是否是 CRM 本机可读路径 - `PACK_PATH` 是否是 CRM 本机可读路径
- `CRM_HOST_BASE` 是否与 CRM 到 host 的实际访问地址一致 - `CRM_HOST_BASE` 是否与 CRM 到 host 的实际访问地址一致
- 如果走 SSH 隧道,`CRM_HOST_BASE` 是否真的可在本机 `curl -I --max-time 5` 读到响应
- 若脚本还要在 SSH 会话里执行 host probe`REMOTE_HOST_BASE` 是否与远端主机看到的地址一致
2. 再看宿主落库 2. 再看宿主落库
- account `credentials.model_mapping` - account `credentials.model_mapping`
- `GET /api/v1/admin/accounts/:id/models` - `GET /api/v1/admin/accounts/:id/models`
- channel `model_mapping/model_pricing/restrict_models/billing_model_source` - channel `model_mapping/model_pricing/restrict_models/billing_model_source`
3. 最后看普通用户流量 3. 最后看普通用户流量
- `/v1/models` - `/v1/models`
- `/v1/chat/completions` - `/v1/chat/completions`

View File

@@ -15,10 +15,11 @@ REMOTE="${REMOTE:-ubuntu@43.155.133.187}"
CRM_BASE="${CRM_BASE:-http://127.0.0.1:18088}" CRM_BASE="${CRM_BASE:-http://127.0.0.1:18088}"
HOST_BASE="${HOST_BASE:-http://127.0.0.1:18087}" HOST_BASE="${HOST_BASE:-http://127.0.0.1:18087}"
CRM_HOST_BASE="${CRM_HOST_BASE:-$HOST_BASE}" CRM_HOST_BASE="${CRM_HOST_BASE:-$HOST_BASE}"
REMOTE_HOST_BASE="${REMOTE_HOST_BASE:-$CRM_HOST_BASE}"
HOST_NAME="${HOST_NAME:-remote43-current-host}" HOST_NAME="${HOST_NAME:-remote43-current-host}"
REMOTE_HOST_ENV_FILE="${REMOTE_HOST_ENV_FILE:-/home/ubuntu/sub2api-host-validation-fresh-deepseek-20260519_115244/.env}" REMOTE_HOST_ENV_FILE="${REMOTE_HOST_ENV_FILE:-/home/ubuntu/sub2api-host-validation-fresh-deepseek-20260519_115244/.env}"
REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-sub2api-relaymgr-pg}" REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-}"
REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-sub2api-relaymgr-redis}" REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-}"
PACK_PATH="${PACK_PATH:-$ROOT_DIR/packs/openai-cn-pack}" PACK_PATH="${PACK_PATH:-$ROOT_DIR/packs/openai-cn-pack}"
ROOT="${ROOT:-$ROOT_DIR/artifacts/real-host-acceptance}" ROOT="${ROOT:-$ROOT_DIR/artifacts/real-host-acceptance}"
ART="${ART:-$ROOT/$(date +%Y%m%d_%H%M%S)_remote43_${provider_id}_key_import}" ART="${ART:-$ROOT/$(date +%Y%m%d_%H%M%S)_remote43_${provider_id}_key_import}"
@@ -26,8 +27,6 @@ MIN_BALANCE="${MIN_BALANCE:-10}"
SUBSCRIPTION_DAYS="${SUBSCRIPTION_DAYS:-30}" SUBSCRIPTION_DAYS="${SUBSCRIPTION_DAYS:-30}"
SUBSCRIPTION_NOTES="${SUBSCRIPTION_NOTES:-hermes remote subscription validation}" SUBSCRIPTION_NOTES="${SUBSCRIPTION_NOTES:-hermes remote subscription validation}"
mkdir -p "$ART" mkdir -p "$ART"
REMOTE_PG_CONTAINER_Q="$(printf '%q' "$REMOTE_PG_CONTAINER")"
REMOTE_REDIS_CONTAINER_Q="$(printf '%q' "$REMOTE_REDIS_CONTAINER")"
if [[ -n "$key_file" ]]; then if [[ -n "$key_file" ]]; then
upstream_key="$(tr -d '\r\n' < "$key_file")" upstream_key="$(tr -d '\r\n' < "$key_file")"
@@ -60,6 +59,62 @@ ssh_cmd() {
ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$cmd" ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$cmd"
} }
resolve_remote_host_runtime() {
local remote_port host_containers
remote_port="$(python3 - "$REMOTE_HOST_BASE" <<'PY'
from urllib.parse import urlparse
import sys
target = urlparse(sys.argv[1])
if target.port is not None:
print(target.port)
elif target.scheme == 'https':
print(443)
else:
print(80)
PY
)"
host_containers="$(ssh_cmd "sudo -n docker ps --format '{{.Names}}\t{{.Ports}}'")"
HOST_CONTAINERS="$host_containers" python3 - "$remote_port" <<'PY'
import os
import sys
port = sys.argv[1]
rows = os.environ.get("HOST_CONTAINERS", "").splitlines()
for row in rows:
name, _, ports = row.partition('\t')
if f":{port}->" not in ports:
continue
app = name.strip()
if app.endswith("-app-1"):
prefix = app[:-len("-app-1")]
print(app)
print(f"{prefix}-postgres-1")
print(f"{prefix}-redis-1")
raise SystemExit(0)
if app.endswith("-app"):
prefix = app[:-len("-app")]
print(app)
print(f"{prefix}-pg")
print(f"{prefix}-redis")
raise SystemExit(0)
raise SystemExit(f"unable to derive target host containers from port {port}")
PY
}
if [[ -z "$REMOTE_PG_CONTAINER" || -z "$REMOTE_REDIS_CONTAINER" ]]; then
mapfile -t resolved_remote_runtime < <(resolve_remote_host_runtime)
if [[ ${#resolved_remote_runtime[@]} -lt 3 ]]; then
echo "unable to resolve remote host runtime containers for $REMOTE_HOST_BASE" >&2
exit 2
fi
REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-${resolved_remote_runtime[1]}}"
REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-${resolved_remote_runtime[2]}}"
fi
REMOTE_PG_CONTAINER_Q="$(printf '%q' "$REMOTE_PG_CONTAINER")"
REMOTE_REDIS_CONTAINER_Q="$(printf '%q' "$REMOTE_REDIS_CONTAINER")"
build_managed_subscription_identity_json() { build_managed_subscription_identity_json() {
local selector="$1" local selector="$1"
local group_id="$2" local group_id="$2"
@@ -130,7 +185,7 @@ from pathlib import Path
import json, subprocess, sys import json, subprocess, sys
env_path = Path(${REMOTE_HOST_ENV_FILE@Q}) env_path = Path(${REMOTE_HOST_ENV_FILE@Q})
host_base = ${HOST_BASE@Q} host_base = ${REMOTE_HOST_BASE@Q}
vals = {} vals = {}
for line in env_path.read_text().splitlines(): for line in env_path.read_text().splitlines():
if '=' not in line: if '=' not in line:
@@ -318,13 +373,14 @@ $(remote_pg_query "$create_user_sql")
EOF EOF
fi fi
python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" <<'PY' python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$REMOTE_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" <<'PY'
import json, sys, pathlib import json, sys, pathlib
path, crm, host, crm_host, provider_id, sub_uid, sub_key = sys.argv[1:8] path, crm, host, crm_host, remote_host, provider_id, sub_uid, sub_key = sys.argv[1:9]
pathlib.Path(path).write_text(json.dumps({ pathlib.Path(path).write_text(json.dumps({
'crm_base': crm, 'crm_base': crm,
'host_base': host, 'host_base': host,
'crm_host_base': crm_host, 'crm_host_base': crm_host,
'remote_host_base': remote_host,
'provider_id': provider_id, 'provider_id': provider_id,
'subscription_user_id': sub_uid, 'subscription_user_id': sub_uid,
'subscription_user_key_prefix': sub_key[:12], 'subscription_user_key_prefix': sub_key[:12],
@@ -432,13 +488,14 @@ else
remote_fetch_group_state "$subscription_group_id" "$sub_uid" "$sub_key" "$ART/08-subscription-group-state.json" remote_fetch_group_state "$subscription_group_id" "$sub_uid" "$sub_key" "$ART/08-subscription-group-state.json"
fi fi
python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" "$subscription_group_id" "$admin_uid" "$managed_user_email" "$managed_probe_key" "$managed_user_id" <<'PY' python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$REMOTE_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" "$subscription_group_id" "$admin_uid" "$managed_user_email" "$managed_probe_key" "$managed_user_id" <<'PY'
import json, sys, pathlib import json, sys, pathlib
path, crm, host, crm_host, provider_id, sub_uid, sub_key, group_id, admin_uid, managed_user_email, managed_probe_key, managed_user_id = sys.argv[1:13] path, crm, host, crm_host, remote_host, provider_id, sub_uid, sub_key, group_id, admin_uid, managed_user_email, managed_probe_key, managed_user_id = sys.argv[1:14]
pathlib.Path(path).write_text(json.dumps({ pathlib.Path(path).write_text(json.dumps({
'crm_base': crm, 'crm_base': crm,
'host_base': host, 'host_base': host,
'crm_host_base': crm_host, 'crm_host_base': crm_host,
'remote_host_base': remote_host,
'provider_id': provider_id, 'provider_id': provider_id,
'subscription_user_id': sub_uid, 'subscription_user_id': sub_uid,
'subscription_user_key_prefix': sub_key[:12], 'subscription_user_key_prefix': sub_key[:12],
@@ -460,11 +517,11 @@ print(json.dumps({
}, ensure_ascii=False)) }, ensure_ascii=False))
PY PY
)" )"
ssh_cmd "curl -sS -D /tmp/models_headers.txt -o /tmp/models_body.json -H 'Authorization: Bearer $managed_probe_key' $HOST_BASE/v1/models" ssh_cmd "curl -sS -D /tmp/models_headers.txt -o /tmp/models_body.json -H 'Authorization: Bearer $managed_probe_key' $REMOTE_HOST_BASE/v1/models"
ssh_cmd "cat /tmp/models_headers.txt" > "$ART/09-models.headers.txt" ssh_cmd "cat /tmp/models_headers.txt" > "$ART/09-models.headers.txt"
ssh_cmd "cat /tmp/models_body.json" > "$ART/10-models.body.json" ssh_cmd "cat /tmp/models_body.json" > "$ART/10-models.body.json"
ssh_cmd "curl -sS -D /tmp/chat_headers.txt -o /tmp/chat_body.json -H 'Authorization: Bearer $managed_probe_key' -H 'Content-Type: application/json' $HOST_BASE/v1/chat/completions -d $(printf %q "$probe_payload")" ssh_cmd "curl -sS -D /tmp/chat_headers.txt -o /tmp/chat_body.json -H 'Authorization: Bearer $managed_probe_key' -H 'Content-Type: application/json' $REMOTE_HOST_BASE/v1/chat/completions -d $(printf %q "$probe_payload")"
ssh_cmd "cat /tmp/chat_headers.txt" > "$ART/11-chat.headers.txt" ssh_cmd "cat /tmp/chat_headers.txt" > "$ART/11-chat.headers.txt"
ssh_cmd "cat /tmp/chat_body.json" > "$ART/12-chat.body.json" ssh_cmd "cat /tmp/chat_body.json" > "$ART/12-chat.body.json"
@@ -476,14 +533,21 @@ ssh_cmd "curl -sS -D /tmp/upstream_chat_headers.txt -o /tmp/upstream_chat_body.t
ssh_cmd "cat /tmp/upstream_chat_headers.txt" > "$ART/19-upstream-chat.headers.txt" ssh_cmd "cat /tmp/upstream_chat_headers.txt" > "$ART/19-upstream-chat.headers.txt"
ssh_cmd "cat /tmp/upstream_chat_body.txt" > "$ART/20-upstream-chat.body.txt" ssh_cmd "cat /tmp/upstream_chat_body.txt" > "$ART/20-upstream-chat.body.txt"
crm_curl_json GET "/api/providers/$provider_id/status" > "$ART/13-provider-status.json" provider_query_suffix="?host_id=$(python3 - "$HOST_NAME" <<'PY'
crm_curl_json GET "/api/providers/$provider_id/access/status" > "$ART/14-access-status.json" import sys
from urllib.parse import quote
print(quote(sys.argv[1], safe=''))
PY
)"
crm_curl_json GET "/api/providers/$provider_id/status${provider_query_suffix}" > "$ART/13-provider-status.json"
crm_curl_json GET "/api/providers/$provider_id/access/status${provider_query_suffix}" > "$ART/14-access-status.json"
preview_payload="$(python3 - "$provider_id" <<'PY' preview_payload="$(python3 - "$provider_id" <<'PY'
import json, sys import json, sys
print(json.dumps({'provider_id': sys.argv[1], 'mode': 'subscription'}, ensure_ascii=False)) print(json.dumps({'provider_id': sys.argv[1], 'mode': 'subscription'}, ensure_ascii=False))
PY PY
)" )"
crm_curl_json POST "/api/providers/$provider_id/access/preview" "$preview_payload" > "$ART/15-access-preview.json" crm_curl_json POST "/api/providers/$provider_id/access/preview${provider_query_suffix}" "$preview_payload" > "$ART/15-access-preview.json"
crm_curl_json GET "/api/import-batches/$batch_id" > "$ART/16-batch-detail-final.json" crm_curl_json GET "/api/import-batches/$batch_id" > "$ART/16-batch-detail-final.json"
python3 - "$ART" "$provider_id" "$batch_id" "$subscription_group_id" "$model_name" <<'PY' python3 - "$ART" "$provider_id" "$batch_id" "$subscription_group_id" "$model_name" <<'PY'

View File

@@ -16,6 +16,14 @@ assert_contains() {
fi fi
} }
assert_not_contains() {
local haystack="$1"
local needle="$2"
if [[ "$haystack" == *"$needle"* ]]; then
fail "expected to avoid [$needle] in [$haystack]"
fi
}
run_test_build_subscription_access_prep_sql() { run_test_build_subscription_access_prep_sql() {
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$ROOT_DIR/scripts/host_access_prep_lib.sh" source "$ROOT_DIR/scripts/host_access_prep_lib.sh"
@@ -314,13 +322,13 @@ case "$url" in
*/api/import-batches/123) */api/import-batches/123)
write_body '{"managed_resources":[{"ResourceType":"group","HostResourceID":"7","ResourceName":"DeepSeek 默认分组"}]}' write_body '{"managed_resources":[{"ResourceType":"group","HostResourceID":"7","ResourceName":"DeepSeek 默认分组"}]}'
;; ;;
*/api/providers/deepseek/status) */api/providers/deepseek/status*)
write_body '{"status":"ready"}' write_body '{"status":"ready"}'
;; ;;
*/api/providers/deepseek/access/status) */api/providers/deepseek/access/status*)
write_body '{"latest_access_status":"subscription_ready"}' write_body '{"latest_access_status":"subscription_ready"}'
;; ;;
*/api/providers/deepseek/access/preview) */api/providers/deepseek/access/preview*)
write_body '{"available":true}' write_body '{"available":true}'
;; ;;
*) *)
@@ -342,6 +350,9 @@ if [[ "$cmd" == *'***'* ]]; then
exit 1 exit 1
fi fi
case "$cmd" in case "$cmd" in
"sudo -n docker ps --format '{{.Names}}\t{{.Ports}}'"*)
printf '%s\n' 'sub2api-fresh-deepseek-20260519_115244-app-1 127.0.0.1:18093->8080/tcp'
;;
*"/api/v1/auth/login"*) *"/api/v1/auth/login"*)
printf '%s\n' 'host-bearer-token' printf '%s\n' 'host-bearer-token'
;; ;;
@@ -425,10 +436,10 @@ fi
*"/api/providers/deepseek/reconcile"*) *"/api/providers/deepseek/reconcile"*)
printf '%s\n' '{"status":"in_sync"}' printf '%s\n' '{"status":"in_sync"}'
;; ;;
*"sudo -n docker exec -i fresh-pg psql -U sub2api -d sub2api -At -F ''"*) *"sudo -n docker exec -i sub2api-fresh-deepseek-20260519_115244-postgres-1 psql -U sub2api -d sub2api -At -F ''"*)
printf '%s\n' '{"group_id":7,"subscription":{"status":"active"},"key":{"group_id":7}}' printf '%s\n' '{"group_id":7,"subscription":{"status":"active"},"key":{"group_id":7}}'
;; ;;
*"sudo -n docker exec -i fresh-pg psql -U sub2api -d sub2api"*) *"sudo -n docker exec -i sub2api-fresh-deepseek-20260519_115244-postgres-1 psql -U sub2api -d sub2api"*)
CMD="$cmd" LOG_DIR="$log_dir" python3 - <<'PY' CMD="$cmd" LOG_DIR="$log_dir" python3 - <<'PY'
import base64, os, re, pathlib, sys import base64, os, re, pathlib, sys
cmd = os.environ['CMD'] cmd = os.environ['CMD']
@@ -454,7 +465,7 @@ else:
print('') print('')
PY PY
;; ;;
*"sudo -n docker exec fresh-redis redis-cli DEL apikey:auth:"*" billing:balance:"*" billing:sub:"*":7"*) *"sudo -n docker exec sub2api-fresh-deepseek-20260519_115244-redis-1 redis-cli DEL apikey:auth:"*" billing:balance:"*" billing:sub:"*":7"*)
printf '%s\n' '3' printf '%s\n' '3'
;; ;;
*) *)
@@ -472,11 +483,10 @@ EOF
CRM_BASE="http://127.0.0.1:18088" \ CRM_BASE="http://127.0.0.1:18088" \
HOST_BASE="http://127.0.0.1:18087" \ HOST_BASE="http://127.0.0.1:18087" \
CRM_HOST_BASE="http://127.0.0.1:18093" \ CRM_HOST_BASE="http://127.0.0.1:18093" \
REMOTE_HOST_BASE="http://127.0.0.1:18093" \
ROOT="$artifact_dir/root" \ ROOT="$artifact_dir/root" \
ART="$artifact_dir/run" \ ART="$artifact_dir/run" \
PACK_PATH="$pack_dir" \ PACK_PATH="$pack_dir" \
REMOTE_PG_CONTAINER="fresh-pg" \
REMOTE_REDIS_CONTAINER="fresh-redis" \
UPSTREAM_KEY="upstream-test-key" \ UPSTREAM_KEY="upstream-test-key" \
SUBSCRIPTION_DAYS=30 \ SUBSCRIPTION_DAYS=30 \
MIN_BALANCE=10 \ MIN_BALANCE=10 \
@@ -493,6 +503,7 @@ EOF
local runtime_context invalidation_log local runtime_context invalidation_log
runtime_context="$(cat "$artifact_dir/run/01-runtime-context.json")" runtime_context="$(cat "$artifact_dir/run/01-runtime-context.json")"
assert_contains "$runtime_context" '"crm_host_base": "http://127.0.0.1:18093"' assert_contains "$runtime_context" '"crm_host_base": "http://127.0.0.1:18093"'
assert_contains "$runtime_context" '"remote_host_base": "http://127.0.0.1:18093"'
invalidation_log="$(cat "$artifact_dir/run/07-redis-targeted-invalidation.txt")" invalidation_log="$(cat "$artifact_dir/run/07-redis-targeted-invalidation.txt")"
assert_contains "$invalidation_log" "auth_cache_key=apikey:auth:" assert_contains "$invalidation_log" "auth_cache_key=apikey:auth:"
assert_contains "$invalidation_log" "balance_cache_key=billing:balance:84" assert_contains "$invalidation_log" "balance_cache_key=billing:balance:84"
@@ -513,6 +524,13 @@ EOF
assert_contains "$summary_json" '"upstream_models_has_expected_model": true' assert_contains "$summary_json" '"upstream_models_has_expected_model": true'
assert_contains "$summary_json" '"completion_classification": "unknown"' assert_contains "$summary_json" '"completion_classification": "unknown"'
[[ -s "$ssh_log" ]] || fail "ssh log was empty" [[ -s "$ssh_log" ]] || fail "ssh log was empty"
local ssh_contents
ssh_contents="$(cat "$ssh_log")"
assert_contains "$ssh_contents" "sudo -n docker ps --format"
assert_contains "$ssh_contents" "http://127.0.0.1:18093/v1/models"
assert_contains "$ssh_contents" "http://127.0.0.1:18093/v1/chat/completions"
assert_not_contains "$ssh_contents" "http://127.0.0.1:18087/v1/models"
assert_not_contains "$ssh_contents" "http://127.0.0.1:18087/v1/chat/completions"
} }
run_test_build_subscription_access_prep_sql run_test_build_subscription_access_prep_sql