2026-09-20 17:53:06 +08:00
|
|
|
|
"""⚠️ 已废弃,请勿重跑;保留仅作历史参考。
|
|
|
|
|
|
|
|
|
|
|
|
现役 embedding 端点是 `knowledge-embedding-qwen-v3` / `text-embedding-v3`(实库 `id=1`),
|
|
|
|
|
|
而本脚本写的是 `qwen-embedding` / `qwen3.7-text-embedding-flash` —— 两者不一致。
|
|
|
|
|
|
**重跑会在 active 端点里凭空多出一个声明 `embedding` 的端点**:索引向量与查询向量可能
|
|
|
|
|
|
来自不同模型,Milvus 的 `COSINE` 相似度会整体失真且**不报错**(`DatabaseModelEndpointResolver`
|
|
|
|
|
|
有"embedding 端点应恰好 1 个"的 WARNING 守卫,但那只是留痕,故障已经发生)。
|
|
|
|
|
|
|
|
|
|
|
|
配置并激活 embedding 模型端点(走管理 API,不直接写库)。
|
2026-09-10 20:15:09 +08:00
|
|
|
|
|
|
|
|
|
|
为什么走 API 而不是 INSERT:端点配置要经过 draft → approved → active 状态机并留下
|
|
|
|
|
|
`interaction_audit`。直接写库会绕过审核与审计,而且 `DatabaseModelGateway` 只认
|
|
|
|
|
|
`status='active'`,手工写错状态会表现为「模型端点未注册或未激活」这种与病因无关的报错。
|
|
|
|
|
|
|
|
|
|
|
|
幂等:脚本先查该 endpoint_code 是否已存在,已存在则跳过创建,只做后续状态推进。
|
|
|
|
|
|
|
|
|
|
|
|
用法:python tools/configure_embedding_endpoint.py
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import datetime as dt
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
import asyncmy
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
import jwt
|
|
|
|
|
|
|
|
|
|
|
|
from app.core.config import get_settings
|
|
|
|
|
|
from app.main import create_app
|
|
|
|
|
|
|
|
|
|
|
|
ADMIN = "9003"
|
|
|
|
|
|
ENDPOINT_CODE = "qwen-embedding"
|
|
|
|
|
|
MYSQL_DSN_HOST = "127.0.0.1"
|
|
|
|
|
|
MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB = "root", "123456", "jr"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def token(subject: str) -> str:
|
|
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
private_key = Path(settings.jwt_private_key_path).read_text(encoding="utf-8")
|
|
|
|
|
|
now = dt.datetime.now(dt.UTC)
|
|
|
|
|
|
return jwt.encode(
|
|
|
|
|
|
{
|
|
|
|
|
|
"sub": subject, "iss": settings.jwt_issuer, "aud": settings.jwt_audience,
|
|
|
|
|
|
"exp": now + dt.timedelta(minutes=30), "nbf": now - dt.timedelta(seconds=5),
|
|
|
|
|
|
"jti": str(uuid.uuid4()),
|
|
|
|
|
|
},
|
|
|
|
|
|
private_key,
|
|
|
|
|
|
algorithm="RS256",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def endpoint_id_if_exists() -> int | None:
|
|
|
|
|
|
connection = await asyncmy.connect(
|
|
|
|
|
|
host=MYSQL_DSN_HOST, port=3306, user=MYSQL_USER, password=MYSQL_PASSWORD, db=MYSQL_DB
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
cursor = connection.cursor()
|
|
|
|
|
|
await cursor.execute(
|
|
|
|
|
|
"SELECT id FROM model_endpoint_config WHERE endpoint_code=%s", (ENDPOINT_CODE,)
|
|
|
|
|
|
)
|
|
|
|
|
|
row = await cursor.fetchone()
|
|
|
|
|
|
return int(row[0]) if row else None
|
|
|
|
|
|
finally:
|
|
|
|
|
|
connection.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def post(
|
|
|
|
|
|
client: httpx.AsyncClient, path: str, *, auth: dict[str, str],
|
|
|
|
|
|
payload: dict[str, object] | None = None, if_match: str | None = None,
|
|
|
|
|
|
) -> httpx.Response:
|
|
|
|
|
|
headers = {**auth, "Idempotency-Key": uuid.uuid4().hex}
|
|
|
|
|
|
if if_match:
|
|
|
|
|
|
headers["If-Match"] = if_match
|
|
|
|
|
|
return await client.post(path, json=payload, headers=headers)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def etag_of(client: httpx.AsyncClient, path: str, auth: dict[str, str]) -> str | None:
|
|
|
|
|
|
response = await client.get(path, headers=auth)
|
|
|
|
|
|
return response.headers.get("ETag")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def main() -> int:
|
|
|
|
|
|
app = create_app()
|
|
|
|
|
|
auth = {"Authorization": f"Bearer {token(ADMIN)}"}
|
|
|
|
|
|
async with httpx.AsyncClient(
|
|
|
|
|
|
transport=httpx.ASGITransport(app=app), base_url="http://test", timeout=60
|
|
|
|
|
|
) as client:
|
|
|
|
|
|
endpoint_id = await endpoint_id_if_exists()
|
|
|
|
|
|
if endpoint_id is None:
|
|
|
|
|
|
created = await post(
|
|
|
|
|
|
client, "/api/v1/admin/model-endpoints", auth=auth,
|
|
|
|
|
|
payload={
|
|
|
|
|
|
"endpoint_code": ENDPOINT_CODE,
|
|
|
|
|
|
"provider": "dashscope",
|
|
|
|
|
|
"model_name": "qwen3.7-text-embedding-flash",
|
|
|
|
|
|
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
|
|
|
|
"secret_ref": "env:QWEN_EMBEDDING_API_KEY",
|
|
|
|
|
|
"capabilities": ["embedding"],
|
|
|
|
|
|
"allowed_data_levels": ["public", "internal"],
|
|
|
|
|
|
"context_window": 8192,
|
|
|
|
|
|
"timeout_ms": 30000,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"创建端点:{created.status_code} {created.text[:160]}")
|
|
|
|
|
|
if created.status_code != 201:
|
|
|
|
|
|
return 1
|
|
|
|
|
|
endpoint_id = int(created.json()["data"]["id"])
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"端点已存在,复用 id={endpoint_id}")
|
|
|
|
|
|
|
|
|
|
|
|
detail_path = f"/api/v1/admin/model-endpoints/{endpoint_id}"
|
|
|
|
|
|
current = (await client.get(detail_path, headers=auth)).json()["data"]
|
|
|
|
|
|
print(f"当前状态:{current['status']}")
|
|
|
|
|
|
|
|
|
|
|
|
if current["status"] == "draft":
|
|
|
|
|
|
reviewed = await post(
|
|
|
|
|
|
client, f"{detail_path}/reviews", auth=auth,
|
|
|
|
|
|
payload={"decision": "approved", "comment": "embedding 端点配置"},
|
|
|
|
|
|
if_match=await etag_of(client, detail_path, auth),
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"审核:{reviewed.status_code} {reviewed.text[:160]}")
|
|
|
|
|
|
if reviewed.status_code not in (200, 201):
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
current = (await client.get(detail_path, headers=auth)).json()["data"]
|
|
|
|
|
|
if current["status"] == "approved":
|
|
|
|
|
|
activated = await post(
|
|
|
|
|
|
client, f"{detail_path}/activations", auth=auth,
|
|
|
|
|
|
# activations 的 body 是必填的 EmptyPayload(extra=forbid),
|
|
|
|
|
|
# 不传 body 会得到 422 "Field required",因此显式给空对象。
|
|
|
|
|
|
payload={},
|
|
|
|
|
|
if_match=await etag_of(client, detail_path, auth),
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"激活:{activated.status_code} {activated.text[:200]}")
|
|
|
|
|
|
if activated.status_code not in (200, 201):
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
final = (await client.get(detail_path, headers=auth)).json()["data"]
|
|
|
|
|
|
print(f"最终状态:{final['status']} 模型={final['model_name']}")
|
|
|
|
|
|
return 0 if final["status"] == "active" else 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sys.exit(asyncio.run(main()))
|