diff --git a/digital-avatar-app/backend/embeddings.py b/digital-avatar-app/backend/embeddings.py index 0e20019..259e061 100644 --- a/digital-avatar-app/backend/embeddings.py +++ b/digital-avatar-app/backend/embeddings.py @@ -18,6 +18,14 @@ EMBED_DIM = 256 MODEL = os.getenv("EMBEDDING_MODEL", "mock-hash-embed-v1") +def _embedding_endpoint(api_url): + """Accept either an OpenAI-compatible base URL or its full endpoint.""" + api_url = (api_url or "").strip().rstrip("/") + if not api_url or api_url.endswith("/embeddings"): + return api_url + return f"{api_url}/embeddings" + + def _tokenize(text): text = (text or "").lower() # 英文/数字按词,CJK 逐字(中文无空格,需拆到字级才能命中子词) @@ -47,7 +55,7 @@ def embed(texts): """返回 list[list[float]],与输入顺序一致。""" if not texts: return [] - api_url = os.getenv("EMBEDDING_API_URL") + api_url = _embedding_endpoint(os.getenv("EMBEDDING_API_URL")) if api_url: api_key = os.getenv("EMBEDDING_API_KEY", "") model = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small") diff --git a/digital-avatar-app/backend/tests/test_embeddings.py b/digital-avatar-app/backend/tests/test_embeddings.py index eafb1d8..55098f5 100644 --- a/digital-avatar-app/backend/tests/test_embeddings.py +++ b/digital-avatar-app/backend/tests/test_embeddings.py @@ -48,9 +48,11 @@ class RemoteEmbeddingTests(unittest.TestCase): def test_large_input_is_split_into_provider_safe_batches(self): texts = [f"chunk-{index}" for index in range(14)] batch_sizes = [] + requested_urls = [] def fake_urlopen(request, timeout): self.assertEqual(timeout, 30) + requested_urls.append(request.full_url) payload = json.loads(request.data.decode("utf-8")) batch_sizes.append(len(payload["input"])) return FakeResponse({ @@ -61,7 +63,7 @@ class RemoteEmbeddingTests(unittest.TestCase): }) with patch.dict(os.environ, { - "EMBEDDING_API_URL": "https://embedding.example/v1/embeddings", + "EMBEDDING_API_URL": "https://embedding.example/v1", "EMBEDDING_API_KEY": "test-key", "EMBEDDING_MODEL": "text-embedding-v4", "EMBEDDING_BATCH_SIZE": "10", @@ -69,8 +71,18 @@ class RemoteEmbeddingTests(unittest.TestCase): result = embeddings.embed(texts) self.assertEqual(batch_sizes, [10, 4]) + self.assertEqual(requested_urls, [ + "https://embedding.example/v1/embeddings", + "https://embedding.example/v1/embeddings", + ]) self.assertEqual(result, [[float(index)] for index in range(14)]) + def test_full_embedding_endpoint_is_not_modified(self): + self.assertEqual( + embeddings._embedding_endpoint("https://embedding.example/v1/embeddings/"), + "https://embedding.example/v1/embeddings", + ) + if __name__ == "__main__": unittest.main() diff --git a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md index 3139594..cce121a 100644 --- a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md +++ b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md @@ -47,10 +47,16 @@ HUIHUI_PAYMENT_TIMEOUT_SECONDS=30 DATABASE_URL=sqlite:////data/avatar.db UPLOAD_DIR=/data/uploads CHAT_MODEL_CONFIG_URL=http:///api/ai-models/runtime/digital-avatar +EMBEDDING_API_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 +EMBEDDING_API_KEY= +EMBEDDING_MODEL=text-embedding-v3 +EMBEDDING_BATCH_SIZE=10 ``` 如生产 AI 配置中心不可用,还应提供当前项目支持的 `OPENAI_API_KEY`、`OPENAI_BASE_URL`、`CHAT_MODEL` 等兜底配置。`/data` 必须挂载持久卷,数据库与知识库文件不可存放在容器临时层。 +`EMBEDDING_API_URL` 同时支持 OpenAI 兼容基础地址(如上面的 `/v1`)和完整的 `/v1/embeddings` 地址,后端会统一请求 `/embeddings`。发布后必须在后端容器内执行一次最小向量探针,确认返回向量数量和维度,而不能只检查 `/api/health`。 + 积分充值使用会会支付体系的 `payment-v3/payment/pay`,渠道值为 `WECHAT` / `ALIPAY`,端内支付场景为 `APP`,微信内 H5 使用 `JSAPI`。`HUIHUI_PAYMENT_CALLBACK_SECRET` 只用于为每笔订单生成 HMAC 回调签名,不会发送到前端或直接出现在回调地址中。支付回调确认状态成功且金额与套餐价格完全一致后才增加积分,重复回调不会重复到账。 ## 3. 构建与发布 @@ -74,6 +80,7 @@ docker compose build --pull avatar-backend avatar-frontend docker compose up -d avatar-backend avatar-frontend docker compose ps curl -fsS http://127.0.0.1:8099/api/health +docker compose exec avatar-backend python -c 'import embeddings; v=embeddings.embed(["部署向量探针"]); print(len(v), len(v[0]))' ``` 生产编排应把示例中的测试端口改为内网暴露,由统一 HTTPS 网关接入。后端暂时使用 SQLite,必须保持单实例写入;若扩展为多后端实例,应先迁移到 PostgreSQL,并把延迟接管任务改为共享队列。