Merge pull request 'Route four models and send current-turn images' (#563) from feature/four-model-vision into main
Why: land four-model routing and current-turn image delivery.
Evidence: test, test-pg, and svc-realcombo success on f938b3ca97, run 3303.
Impact: main serves images on capable models; 8788 is not restarted.
Refs: #563
This commit was merged in pull request #563.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -810,7 +810,12 @@ scripts/svc.sh prod deploy --env .env.prod # 等价
|
||||
| `SVC_SRC_DIR` | 独立部署源仓路径(默认 `~/.cache/agent-system-svc/src`) |
|
||||
| `SVC_PG_BIN` | PostgreSQL 工具链 bindir |
|
||||
| `SVC_MODEL_BASE/KEY/NAME` | 主模型(播种 `settings.models.chat`) |
|
||||
| `SVC_MODEL_FALLBACKS` | 主模型回退链(逗号分隔,播种 `settings.models.chat.fallbacks`) |
|
||||
| `SVC_LIGHT_BASE/KEY/NAME` | 轻量模型 |
|
||||
| `SVC_VISION_MODEL` | 视觉兜底模型(播种 `settings.models.vision.model`) |
|
||||
| `SVC_VISION_CAPABLE` | 可看图模型清单(逗号分隔,播种 `settings.models.vision.capable`) |
|
||||
| `SVC_MINIMAX_BASE/KEY` | 原 MiniMax 端点(播种 `settings.models.reserve.minimax`,不替换当前对话模型) |
|
||||
| `SVC_MINIMAX_CHAT/FAST` | 原 MiniMax 对话模型与快速模型名 |
|
||||
| `SVC_TAVILY_KEYS` | Tavily 密钥池(逗号分隔) |
|
||||
|
||||
### 9.4 持久化语义
|
||||
|
||||
+1
-1
Submodule modules/backend updated: 80bb123c87...6532fad4d5
@@ -27,7 +27,9 @@
|
||||
"backend/app backend/tools",
|
||||
"backend/app backend/workflow",
|
||||
"backend/cardsvc backend/store",
|
||||
"backend/ctxasm backend/vision",
|
||||
"backend/hostapi backend/store",
|
||||
"backend/ingest backend/vision",
|
||||
"backend/pgbus backend/bus",
|
||||
"backend/pgbus backend/store",
|
||||
"backend/plugins backend/settings",
|
||||
@@ -58,6 +60,7 @@
|
||||
"backend/runtime backend/quality",
|
||||
"backend/runtime backend/squadadapter",
|
||||
"backend/runtime backend/wfcompile",
|
||||
"backend/runtime backend/vision",
|
||||
"backend/runtime backend/workflow",
|
||||
"backend/server backend/cardsvc",
|
||||
"backend/server backend/cmdbus",
|
||||
|
||||
+141
-31
@@ -572,7 +572,12 @@ dotenv 格式:KEY=VALUE,支持 # 注释、成对引号、export 前缀;只
|
||||
SVC_SRC_DIR 独立部署源仓路径(默认 ~/.cache/agent-system-svc/src)
|
||||
SVC_PG_BIN postgres 工具链 bindir(默认 pg_config --bindir 探测)
|
||||
SVC_MODEL_BASE/KEY/NAME 主模型(agent 启动健康后播种)
|
||||
SVC_MODEL_FALLBACKS 主模型回退链(逗号分隔,播种 models.chat.fallbacks)
|
||||
SVC_LIGHT_BASE/KEY/NAME 轻量模型
|
||||
SVC_VISION_MODEL 视觉兜底模型(播种 models.vision.model)
|
||||
SVC_VISION_CAPABLE 可看图模型清单(逗号分隔,播种 models.vision.capable)
|
||||
SVC_MINIMAX_BASE/KEY 原 MiniMax 端点(播种 models.reserve.minimax,不作为当前对话模型)
|
||||
SVC_MINIMAX_CHAT/FAST 原 MiniMax 对话模型与快速模型名
|
||||
SVC_TAVILY_KEYS Tavily 密钥池
|
||||
SVC_SEED_OWNER 播种目标账号(缺省取库内首个管理员)
|
||||
播种幂等:设置持久化在 agent 数据目录,播过一次后续启动无需再传。
|
||||
@@ -726,6 +731,39 @@ load_env_file() {
|
||||
done < "$f"
|
||||
}
|
||||
|
||||
# load_env_file_if_unset FILE:只填当前为空的变量,不覆盖调用方已经传入的环境。
|
||||
load_env_file_if_unset() {
|
||||
local f="$1" line k v
|
||||
[ -f "$f" ] || return 0
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
line="${line%$'\r'}"
|
||||
case "$line" in ''|'#'*) continue ;; esac
|
||||
[[ "$line" =~ ^[[:space:]]*(export[[:space:]]+)?[A-Za-z_][A-Za-z0-9_]*= ]] || continue
|
||||
k="${line%%=*}"
|
||||
k="${k#"${k%%[![:space:]]*}"}"
|
||||
k="${k#export}"
|
||||
k="${k#"${k%%[![:space:]]*}"}"
|
||||
[ -n "${!k:-}" ] && continue
|
||||
v="${line#*=}"
|
||||
v="${v#"${v%%[![:space:]]*}"}"
|
||||
v="${v%\"}"; v="${v%\'}"; v="${v#\"}"; v="${v#\'}"
|
||||
printf -v "$k" '%s' "$v"
|
||||
done < "$f"
|
||||
}
|
||||
|
||||
# load_saved_envfile:svc.env 记住的 envfile= 在播种前补进空变量。
|
||||
load_saved_envfile() {
|
||||
local f
|
||||
[ -n "${SVC_ENVFILE:-}" ] && return 0
|
||||
f="$(svc_get envfile)"
|
||||
[ -n "$f" ] || return 0
|
||||
case "$f" in
|
||||
/*) ;;
|
||||
*) f="$ROOT/$f" ;;
|
||||
esac
|
||||
load_env_file_if_unset "$f"
|
||||
}
|
||||
|
||||
# ---- 命令行参数(覆盖层)----
|
||||
CLI_ADDR=""; CLI_HOST=""; CLI_PORT=""; CLI_DATA=""; CLI_DSN=""
|
||||
CLI_AUTH=""; CLI_MOCK=0; CLI_PLUGINS=""; CLI_EXTRA=""; CLI_LOGS_N=""
|
||||
@@ -900,7 +938,7 @@ svc_merge_tags() {
|
||||
# agent 服务经统一解析启动后按生效态写 db=(pg/mock;显式 sqlite)与 dbref=/dsn=
|
||||
# (托管绑定不重复存 DSN,外部直连存受保护 dsn=);未走解析的调用保持旧值。
|
||||
record_state() {
|
||||
local pid prev_src prev_dbref prev_conffile prev_db prev_dsn prev_plugins plugins_out merged_tags
|
||||
local pid prev_src prev_dbref prev_conffile prev_db prev_dsn prev_envfile prev_plugins plugins_out merged_tags
|
||||
pid="$(read_pid)"
|
||||
prev_src=""
|
||||
[ -f "$ENV_DIR/svc.env" ] && prev_src="$(svc_get source)"
|
||||
@@ -908,6 +946,7 @@ record_state() {
|
||||
prev_conffile="$(svc_get conffile)"
|
||||
prev_db="$(svc_get db)"
|
||||
prev_dsn="$(svc_get dsn)"
|
||||
prev_envfile="$(svc_get envfile)"
|
||||
prev_plugins="$(svc_get plugins)"
|
||||
plugins_out="$prev_plugins"
|
||||
# 只有激活校验和 UI 探活都通过之后才提交新的 plugins=。中途 record_state
|
||||
@@ -952,6 +991,11 @@ record_state() {
|
||||
[ -n "$dbref_out" ] && printf 'dbref=%s\n' "$dbref_out"
|
||||
[ -n "$dsn_out" ] && printf 'dsn=%s\n' "$dsn_out"
|
||||
[ -n "$prev_conffile" ] && printf 'conffile=%s\n' "$prev_conffile"
|
||||
if [ -n "${SVC_ENVFILE:-}" ]; then
|
||||
printf 'envfile=%s\n' "$SVC_ENVFILE"
|
||||
elif [ -n "$prev_envfile" ]; then
|
||||
printf 'envfile=%s\n' "$prev_envfile"
|
||||
fi
|
||||
[ -n "$plugins_out" ] && printf 'plugins=%s\n' "$plugins_out"
|
||||
[ $# -gt 0 ] && printf '%s\n' "$@"
|
||||
} > "$tmp"
|
||||
@@ -3421,6 +3465,48 @@ print_agent_plan() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# model_seed_payload:把当前环境里的模型配置打成系统设置 JSON。没有可写项时输出空。
|
||||
model_seed_payload() {
|
||||
local cb="${CLI_MBASE:-${SVC_MODEL_BASE:-}}" ck="${CLI_MKEY:-${SVC_MODEL_KEY:-}}" cn="${CLI_MNAME:-${SVC_MODEL_NAME:-}}"
|
||||
local lb="${CLI_LBASE:-${SVC_LIGHT_BASE:-}}" lk="${CLI_LKEY:-${SVC_LIGHT_KEY:-}}" ln="${CLI_LNAME:-${SVC_LIGHT_NAME:-}}"
|
||||
local tk="${CLI_TAVILY:-${SVC_TAVILY_KEYS:-}}"
|
||||
local fb="${SVC_MODEL_FALLBACKS:-}" vm="${SVC_VISION_MODEL:-}" vc="${SVC_VISION_CAPABLE:-}"
|
||||
local mb="${SVC_MINIMAX_BASE:-}" mk="${SVC_MINIMAX_KEY:-}" mc="${SVC_MINIMAX_CHAT:-}" mf="${SVC_MINIMAX_FAST:-}"
|
||||
[ -z "$cb$ck$cn$lb$lk$ln$tk$fb$vm$vc$mb$mk$mc$mf" ] && return 0
|
||||
command -v python3 >/dev/null 2>&1 || return 1
|
||||
python3 - "$cb" "$ck" "$cn" "$lb" "$lk" "$ln" "$tk" "$fb" "$vm" "$vc" "$mb" "$mk" "$mc" "$mf" <<'PY'
|
||||
import json, sys
|
||||
cb, ck, cn, lb, lk, ln, tk, fb, vm, vc, mb, mk, mc, mf = sys.argv[1:15]
|
||||
def sub(base, key, name):
|
||||
d = {}
|
||||
if base: d["baseUrl"] = base
|
||||
if key: d["apiKey"] = key
|
||||
if name: d["model"] = name
|
||||
return d
|
||||
chat, light = sub(cb, ck, cn), sub(lb, lk, ln)
|
||||
if fb:
|
||||
chat["fallbacks"] = fb
|
||||
models = {}
|
||||
if chat: models["chat"] = chat
|
||||
if light: models["lightweight"] = light
|
||||
vision = {}
|
||||
if vm: vision["model"] = vm
|
||||
if vc: vision["capable"] = vc
|
||||
if vision: models["vision"] = vision
|
||||
reserve = {}
|
||||
if mb: reserve["baseUrl"] = mb
|
||||
if mk: reserve["apiKey"] = mk
|
||||
if mc: reserve["chat"] = mc
|
||||
if mf: reserve["fast"] = mf
|
||||
if reserve:
|
||||
models["reserve"] = {"minimax": reserve}
|
||||
out = {}
|
||||
if models: out["models"] = models
|
||||
if tk: out["tools"] = {"tavily": {"apiKeys": tk}}
|
||||
print(json.dumps(out))
|
||||
PY
|
||||
}
|
||||
|
||||
# seed_settings:把 CLI/环境变量给出的模型与搜索配置写入系统设置(幂等)。
|
||||
# P2 受管(提案 2026-10-13 rdm-settings-members-duties-perms-increment):models.*
|
||||
# 与 tools.tavily.* 是系统受管项,user 层写入整笔拒绝,播种改走系统管理面
|
||||
@@ -3430,31 +3516,10 @@ print_agent_plan() {
|
||||
# 铸 key 失败维持无头 PUT(仅无认证单机档可成)。
|
||||
seed_settings() {
|
||||
local host="$1" port="$2"
|
||||
local cb="${CLI_MBASE:-${SVC_MODEL_BASE:-}}" ck="${CLI_MKEY:-${SVC_MODEL_KEY:-}}" cn="${CLI_MNAME:-${SVC_MODEL_NAME:-}}"
|
||||
local lb="${CLI_LBASE:-${SVC_LIGHT_BASE:-}}" lk="${CLI_LKEY:-${SVC_LIGHT_KEY:-}}" ln="${CLI_LNAME:-${SVC_LIGHT_NAME:-}}"
|
||||
local tk="${CLI_TAVILY:-${SVC_TAVILY_KEYS:-}}"
|
||||
[ -z "$cb$ck$cn$lb$lk$ln$tk" ] && return 0
|
||||
command -v python3 >/dev/null 2>&1 || { c_warn "[$ENV_NAME] 跳过配置播种:需要 python3 构造请求"; return 0; }
|
||||
local payload
|
||||
payload="$(python3 - "$cb" "$ck" "$cn" "$lb" "$lk" "$ln" "$tk" <<'PY'
|
||||
import json, sys
|
||||
cb, ck, cn, lb, lk, ln, tk = sys.argv[1:8]
|
||||
out = {}
|
||||
def sub(base, key, name):
|
||||
d = {}
|
||||
if base: d["baseUrl"] = base
|
||||
if key: d["apiKey"] = key
|
||||
if name: d["model"] = name
|
||||
return d
|
||||
chat, light = sub(cb, ck, cn), sub(lb, lk, ln)
|
||||
models = {}
|
||||
if chat: models["chat"] = chat
|
||||
if light: models["lightweight"] = light
|
||||
if models: out["models"] = models
|
||||
if tk: out["tools"] = {"tavily": {"apiKeys": tk}}
|
||||
print(json.dumps(out))
|
||||
PY
|
||||
)"
|
||||
payload="$(model_seed_payload)" || { c_warn "[$ENV_NAME] 跳过配置播种:需要 python3 构造请求"; return 0; }
|
||||
[ -n "$payload" ] && [ "$payload" != "{}" ] || return 0
|
||||
command -v python3 >/dev/null 2>&1 || { c_warn "[$ENV_NAME] 跳过配置播种:需要 python3 构造请求"; return 0; }
|
||||
# 认证开启时的播种凭证:临时 key,明文只存在于本函数局部变量,不回显、不落盘。
|
||||
# apikey 子命令不读环境 DSN,只能经 CLI 传递(进程列表短暂可见;serve 走
|
||||
# AGENT_SYSTEM_DSN 环境注入,此路对 CLI 子命令不可用)。
|
||||
@@ -3492,15 +3557,59 @@ for u in users:
|
||||
fi
|
||||
if echo "$resp" | grep -q '"ok": *true'; then
|
||||
local did=""
|
||||
[ -n "$cb$ck$cn" ] && did="主模型"
|
||||
[ -n "$lb$lk$ln" ] && did="${did:+$did、}轻量模型"
|
||||
[ -n "$tk" ] && did="${did:+$did、}Tavily"
|
||||
[ -n "${CLI_MBASE:-${SVC_MODEL_BASE:-}}${CLI_MKEY:-${SVC_MODEL_KEY:-}}${CLI_MNAME:-${SVC_MODEL_NAME:-}}" ] && did="主模型"
|
||||
[ -n "${CLI_LBASE:-${SVC_LIGHT_BASE:-}}${CLI_LKEY:-${SVC_LIGHT_KEY:-}}${CLI_LNAME:-${SVC_LIGHT_NAME:-}}" ] && did="${did:+$did、}轻量模型"
|
||||
[ -n "${SVC_MODEL_FALLBACKS:-}" ] && did="${did:+$did、}回退链"
|
||||
[ -n "${SVC_VISION_MODEL:-}${SVC_VISION_CAPABLE:-}" ] && did="${did:+$did、}视觉模型"
|
||||
[ -n "${SVC_MINIMAX_BASE:-}${SVC_MINIMAX_KEY:-}${SVC_MINIMAX_CHAT:-}${SVC_MINIMAX_FAST:-}" ] && did="${did:+$did、}MiniMax 备用"
|
||||
[ -n "${CLI_TAVILY:-${SVC_TAVILY_KEYS:-}}" ] && did="${did:+$did、}Tavily"
|
||||
c_ok "[$ENV_NAME] 已播种设置:$did(持久化于 $ENV_DATA,后续启动无需再传)"
|
||||
else
|
||||
c_warn "[$ENV_NAME] 设置播种未成功:${resp:-<无响应>}(认证开启时需库内已有账号以铸临时密钥,或用 --seed-owner 指定;也可在设置页手动配置)"
|
||||
fi
|
||||
}
|
||||
|
||||
# seed_settings_db:rdm 产物不能铸 API key 时,把同一份配置合并进嵌入式系统设置。
|
||||
# 库还不存在时不创建,避免在空数据目录里写出半套表。
|
||||
seed_settings_db() {
|
||||
local payload db
|
||||
payload="$(model_seed_payload)" || { c_warn "[$ENV_NAME] 跳过配置播种:需要 python3 构造请求"; return 0; }
|
||||
[ -n "$payload" ] && [ "$payload" != "{}" ] || return 0
|
||||
db="${ENV_DATA:-}/agent-system.db"
|
||||
[ -f "$db" ] || return 0
|
||||
if python3 - "$db" "$payload" <<'PY'
|
||||
import json, sqlite3, sys
|
||||
db, payload = sys.argv[1], sys.argv[2]
|
||||
incoming = json.loads(payload)
|
||||
con = sqlite3.connect(db, timeout=10)
|
||||
tables = {row[0] for row in con.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
||||
if "settings" not in tables:
|
||||
raise SystemExit(2)
|
||||
row = con.execute('SELECT "values" FROM settings WHERE owner=? AND scope=?', ("", "system")).fetchone()
|
||||
current = json.loads(row[0]) if row else {}
|
||||
|
||||
def merge(dst, src):
|
||||
for key, value in src.items():
|
||||
if isinstance(value, dict) and isinstance(dst.get(key), dict):
|
||||
merge(dst[key], value)
|
||||
else:
|
||||
dst[key] = value
|
||||
return dst
|
||||
|
||||
blob = json.dumps(merge(current, incoming), ensure_ascii=False, separators=(",", ":"))
|
||||
if row:
|
||||
con.execute('UPDATE settings SET "values"=? WHERE owner=? AND scope=?', (blob, "", "system"))
|
||||
else:
|
||||
con.execute('INSERT INTO settings(owner, scope, "values") VALUES(?,?,?)', ("", "system", blob))
|
||||
con.commit()
|
||||
PY
|
||||
then
|
||||
c_ok "[$ENV_NAME] 已把模型配置写入数据目录(rdm 产物无播种命令,直接更新系统设置)"
|
||||
else
|
||||
c_warn "[$ENV_NAME] 数据目录中的模型配置未写上"
|
||||
fi
|
||||
}
|
||||
|
||||
# dsn_from_dbref DBREF:由 dbref(pg实例/库名)从该实例登记重建连接串。
|
||||
# 实例未登记 / 非 postgres / 缺 addr·user·password 时返回非零且不输出。
|
||||
dsn_from_dbref() {
|
||||
@@ -3919,10 +4028,11 @@ agent_do_start() {
|
||||
svc_mark_autostart
|
||||
c_ok "[$ENV_NAME] 已启动 pid=$(read_pid) → http://$phost:${probe##*:}(来源 ${SVC_SOURCE:-?},日志:$ENV_DIR/svc.log)"
|
||||
hist_event start ok "$(read_pid)" "http://$phost:${probe##*:}"
|
||||
# rdm 组合产物无 user/apikey CLI 子命令:设置播种(--model-*/--tavily-keys)仅
|
||||
# 基座产物支持;记录态 svc.env 的 source=rdm@* 跨裸 start/restart 稳定标识形态。
|
||||
load_saved_envfile
|
||||
# rdm 组合产物没有 user/apikey 子命令,不能铸临时管理员密钥走 HTTP。
|
||||
# 嵌入式库改为直接合并系统设置,避免重启后回到旧模型。
|
||||
case "${SVC_SOURCE:-}" in
|
||||
rdm@*) ;;
|
||||
rdm@*) seed_settings_db ;;
|
||||
*) seed_settings "$phost" "${probe##*:}" ;;
|
||||
esac
|
||||
return 0
|
||||
|
||||
@@ -713,6 +713,26 @@ class SvcDbRecoveryTests(unittest.TestCase):
|
||||
self.assertIn("apikey create --dsn %s --owner op --name svc-seed" % dsn, cli)
|
||||
self.assertIn("apikey revoke --dsn %s --owner op --name svc-seed" % dsn, cli)
|
||||
|
||||
def test_seed_writes_fallbacks_and_vision(self):
|
||||
self.register_pg("pg1", 18951, password="seed-pw-v")
|
||||
dsn = "postgresql://agent:seed-pw-v@127.0.0.1:18951/agent"
|
||||
d = self.runtime("dev")
|
||||
self.seed_agent("dev", 18952,
|
||||
"--addr 127.0.0.1:18952 --data %s --plugins %s --auth auto --dsn %s"
|
||||
% (d / "data", d / "plugins", dsn))
|
||||
result = self.svc("dev", "start", env=self.model_env(
|
||||
SVC_MODEL_FALLBACKS="ZHIPU/GLM-5.3-FlashX,ZHIPU/GLM-5.3-Flash",
|
||||
SVC_VISION_MODEL="qwen3.8-flash",
|
||||
SVC_VISION_CAPABLE="deepseek-v4.1-flash,qwen3.8-flash",
|
||||
))
|
||||
self.assertIn("已播种设置:主模型", result.stdout)
|
||||
self.assertIn("回退链", result.stdout)
|
||||
self.assertIn("视觉模型", result.stdout)
|
||||
body_part = self.put_record().split("BODY=", 1)[1]
|
||||
self.assertIn('"fallbacks": "ZHIPU/GLM-5.3-FlashX,ZHIPU/GLM-5.3-Flash"', body_part)
|
||||
self.assertIn('"model": "qwen3.8-flash"', body_part)
|
||||
self.assertIn('"capable": "deepseek-v4.1-flash,qwen3.8-flash"', body_part)
|
||||
|
||||
def test_seed_owner_override_skips_account_lookup(self):
|
||||
self.register_pg("pg1", 18833, password="seed-pw-2")
|
||||
dsn = "postgresql://agent:seed-pw-2@127.0.0.1:18833/agent"
|
||||
|
||||
Reference in New Issue
Block a user