Harden remote43 acceptance script
This commit is contained in:
@@ -18,6 +18,19 @@
|
||||
- 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` 路径后再重试
|
||||
- 该修正现在不再依赖宿主长期保留补丁,宿主升级后只要下次 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 验证矩阵当前仍保留一条非阻塞事实:
|
||||
- `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 首版放行
|
||||
@@ -122,6 +135,11 @@
|
||||
- `21-summary.json` 已到 `batch_status=succeeded`、`provider_status=active`
|
||||
- `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 首版范围上线)
|
||||
|
||||
1. 运营前置
|
||||
|
||||
@@ -623,6 +623,98 @@
|
||||
- 如果 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. 新增供应链账号或模型后,哪些结果可以算“已确认”,哪些只能算“部分确认”?
|
||||
|
||||
#### 可算“已确认”
|
||||
|
||||
@@ -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`。
|
||||
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 闭环是否打通为主。
|
||||
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. 先看环境
|
||||
- CRM 是否是最新提交对应的在线进程
|
||||
- `PACK_PATH` 是否是 CRM 本机可读路径
|
||||
- `CRM_HOST_BASE` 是否与 CRM 到 host 的实际访问地址一致
|
||||
- CRM 是否是最新提交对应的在线进程
|
||||
- `PACK_PATH` 是否是 CRM 本机可读路径
|
||||
- `CRM_HOST_BASE` 是否与 CRM 到 host 的实际访问地址一致
|
||||
- 如果走 SSH 隧道,`CRM_HOST_BASE` 是否真的可在本机 `curl -I --max-time 5` 读到响应
|
||||
- 若脚本还要在 SSH 会话里执行 host probe,`REMOTE_HOST_BASE` 是否与远端主机看到的地址一致
|
||||
2. 再看宿主落库
|
||||
- account `credentials.model_mapping`
|
||||
- `GET /api/v1/admin/accounts/:id/models`
|
||||
- channel `model_mapping/model_pricing/restrict_models/billing_model_source`
|
||||
- account `credentials.model_mapping`
|
||||
- `GET /api/v1/admin/accounts/:id/models`
|
||||
- channel `model_mapping/model_pricing/restrict_models/billing_model_source`
|
||||
3. 最后看普通用户流量
|
||||
- `/v1/models`
|
||||
- `/v1/chat/completions`
|
||||
|
||||
@@ -15,10 +15,11 @@ REMOTE="${REMOTE:-ubuntu@43.155.133.187}"
|
||||
CRM_BASE="${CRM_BASE:-http://127.0.0.1:18088}"
|
||||
HOST_BASE="${HOST_BASE:-http://127.0.0.1:18087}"
|
||||
CRM_HOST_BASE="${CRM_HOST_BASE:-$HOST_BASE}"
|
||||
REMOTE_HOST_BASE="${REMOTE_HOST_BASE:-$CRM_HOST_BASE}"
|
||||
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_PG_CONTAINER="${REMOTE_PG_CONTAINER:-sub2api-relaymgr-pg}"
|
||||
REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-sub2api-relaymgr-redis}"
|
||||
REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-}"
|
||||
REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-}"
|
||||
PACK_PATH="${PACK_PATH:-$ROOT_DIR/packs/openai-cn-pack}"
|
||||
ROOT="${ROOT:-$ROOT_DIR/artifacts/real-host-acceptance}"
|
||||
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_NOTES="${SUBSCRIPTION_NOTES:-hermes remote subscription validation}"
|
||||
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
|
||||
upstream_key="$(tr -d '\r\n' < "$key_file")"
|
||||
@@ -60,6 +59,62 @@ ssh_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() {
|
||||
local selector="$1"
|
||||
local group_id="$2"
|
||||
@@ -130,7 +185,7 @@ from pathlib import Path
|
||||
import json, subprocess, sys
|
||||
|
||||
env_path = Path(${REMOTE_HOST_ENV_FILE@Q})
|
||||
host_base = ${HOST_BASE@Q}
|
||||
host_base = ${REMOTE_HOST_BASE@Q}
|
||||
vals = {}
|
||||
for line in env_path.read_text().splitlines():
|
||||
if '=' not in line:
|
||||
@@ -318,13 +373,14 @@ $(remote_pg_query "$create_user_sql")
|
||||
EOF
|
||||
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
|
||||
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({
|
||||
'crm_base': crm,
|
||||
'host_base': host,
|
||||
'crm_host_base': crm_host,
|
||||
'remote_host_base': remote_host,
|
||||
'provider_id': provider_id,
|
||||
'subscription_user_id': sub_uid,
|
||||
'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"
|
||||
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
|
||||
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({
|
||||
'crm_base': crm,
|
||||
'host_base': host,
|
||||
'crm_host_base': crm_host,
|
||||
'remote_host_base': remote_host,
|
||||
'provider_id': provider_id,
|
||||
'subscription_user_id': sub_uid,
|
||||
'subscription_user_key_prefix': sub_key[:12],
|
||||
@@ -460,11 +517,11 @@ print(json.dumps({
|
||||
}, ensure_ascii=False))
|
||||
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_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_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_body.txt" > "$ART/20-upstream-chat.body.txt"
|
||||
|
||||
crm_curl_json GET "/api/providers/$provider_id/status" > "$ART/13-provider-status.json"
|
||||
crm_curl_json GET "/api/providers/$provider_id/access/status" > "$ART/14-access-status.json"
|
||||
provider_query_suffix="?host_id=$(python3 - "$HOST_NAME" <<'PY'
|
||||
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'
|
||||
import json, sys
|
||||
print(json.dumps({'provider_id': sys.argv[1], 'mode': 'subscription'}, ensure_ascii=False))
|
||||
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"
|
||||
|
||||
python3 - "$ART" "$provider_id" "$batch_id" "$subscription_group_id" "$model_name" <<'PY'
|
||||
|
||||
@@ -16,6 +16,14 @@ assert_contains() {
|
||||
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() {
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT_DIR/scripts/host_access_prep_lib.sh"
|
||||
@@ -314,13 +322,13 @@ case "$url" in
|
||||
*/api/import-batches/123)
|
||||
write_body '{"managed_resources":[{"ResourceType":"group","HostResourceID":"7","ResourceName":"DeepSeek 默认分组"}]}'
|
||||
;;
|
||||
*/api/providers/deepseek/status)
|
||||
*/api/providers/deepseek/status*)
|
||||
write_body '{"status":"ready"}'
|
||||
;;
|
||||
*/api/providers/deepseek/access/status)
|
||||
*/api/providers/deepseek/access/status*)
|
||||
write_body '{"latest_access_status":"subscription_ready"}'
|
||||
;;
|
||||
*/api/providers/deepseek/access/preview)
|
||||
*/api/providers/deepseek/access/preview*)
|
||||
write_body '{"available":true}'
|
||||
;;
|
||||
*)
|
||||
@@ -342,6 +350,9 @@ if [[ "$cmd" == *'***'* ]]; then
|
||||
exit 1
|
||||
fi
|
||||
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"*)
|
||||
printf '%s\n' 'host-bearer-token'
|
||||
;;
|
||||
@@ -425,10 +436,10 @@ fi
|
||||
*"/api/providers/deepseek/reconcile"*)
|
||||
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}}'
|
||||
;;
|
||||
*"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'
|
||||
import base64, os, re, pathlib, sys
|
||||
cmd = os.environ['CMD']
|
||||
@@ -454,7 +465,7 @@ else:
|
||||
print('')
|
||||
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'
|
||||
;;
|
||||
*)
|
||||
@@ -472,11 +483,10 @@ EOF
|
||||
CRM_BASE="http://127.0.0.1:18088" \
|
||||
HOST_BASE="http://127.0.0.1:18087" \
|
||||
CRM_HOST_BASE="http://127.0.0.1:18093" \
|
||||
REMOTE_HOST_BASE="http://127.0.0.1:18093" \
|
||||
ROOT="$artifact_dir/root" \
|
||||
ART="$artifact_dir/run" \
|
||||
PACK_PATH="$pack_dir" \
|
||||
REMOTE_PG_CONTAINER="fresh-pg" \
|
||||
REMOTE_REDIS_CONTAINER="fresh-redis" \
|
||||
UPSTREAM_KEY="upstream-test-key" \
|
||||
SUBSCRIPTION_DAYS=30 \
|
||||
MIN_BALANCE=10 \
|
||||
@@ -493,6 +503,7 @@ EOF
|
||||
local runtime_context invalidation_log
|
||||
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" '"remote_host_base": "http://127.0.0.1:18093"'
|
||||
invalidation_log="$(cat "$artifact_dir/run/07-redis-targeted-invalidation.txt")"
|
||||
assert_contains "$invalidation_log" "auth_cache_key=apikey:auth:"
|
||||
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" '"completion_classification": "unknown"'
|
||||
[[ -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
|
||||
|
||||
Reference in New Issue
Block a user