Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
484 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "chenguojun06-star/fz66666", "files": {"cloudbaserc.json": {"92136382831779e22f23b72a08f4e137bfa17dd3": {"t": 1785745313.773528, "n": 3154, "h": "d7faedb3f54e9911", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-backend\",\n \"servicePath\": \"/api\",\n \"localPath\": \"./backend\",\n \"dockerfilePath\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"initialDelaySeconds\": 300,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"AGNES_MODEL\": \"{{AGNES_MODEL:agnes-2.5-flash}}\",\n \"AGNES2_MODEL\": \"{{AGNES2_MODEL:agnes-2.5-flash}}\",\n \"AI_MODEL_VISION\": \"{{AI_MODEL_VISION:agnes-2.5-flash}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-admin\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"dockerfilePath\": \"./frontend/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"envVariables\": {\n \"BACKEND_URL\": \"https://api.webyszl.cn\"\n }\n }\n },\n \"h5\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-h5\",\n \"servicePath\": \"/h5\",\n \"localPath\": \"./h5-web\",\n \"dockerfilePath\": \"./h5-web/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.25,\n \"mem\": 0.5\n }\n }\n }\n }\n}"}, "e3eca03c3b9734ba801c15ce6bafd09fa6b8584a": {"t": 1785749953.5917158, "n": 3154, "h": "d7faedb3f54e9911", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-backend\",\n \"servicePath\": \"/api\",\n \"localPath\": \"./backend\",\n \"dockerfilePath\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"initialDelaySeconds\": 300,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"AGNES_MODEL\": \"{{AGNES_MODEL:agnes-2.5-flash}}\",\n \"AGNES2_MODEL\": \"{{AGNES2_MODEL:agnes-2.5-flash}}\",\n \"AI_MODEL_VISION\": \"{{AI_MODEL_VISION:agnes-2.5-flash}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-admin\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"dockerfilePath\": \"./frontend/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"envVariables\": {\n \"BACKEND_URL\": \"https://api.webyszl.cn\"\n }\n }\n },\n \"h5\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-h5\",\n \"servicePath\": \"/h5\",\n \"localPath\": \"./h5-web\",\n \"dockerfilePath\": \"./h5-web/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.25,\n \"mem\": 0.5\n }\n }\n }\n }\n}"}, "260347e68db7ecd42554f892d362755a81e6df07": {"t": 1785749954.6667893, "n": 2958, "h": "7d5d9fff4061f6be", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-backend\",\n \"servicePath\": \"/api\",\n \"localPath\": \"./backend\",\n \"dockerfilePath\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"initialDelaySeconds\": 300,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-admin\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"dockerfilePath\": \"./frontend/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"envVariables\": {\n \"BACKEND_URL\": \"https://api.webyszl.cn\"\n }\n }\n },\n \"h5\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-h5\",\n \"servicePath\": \"/h5\",\n \"localPath\": \"./h5-web\",\n \"dockerfilePath\": \"./h5-web/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.25,\n \"mem\": 0.5\n }\n }\n }\n }\n}"}, "4d454c0ea46f46c64a4b5e6e9ead7cf0843b7890": {"t": 1785749955.7756293, "n": 2050, "h": "cef5193b6963e416", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"InitialDelaySeconds\": 300,\n \"PeriodSeconds\": 30,\n \"TimeoutSeconds\": 10,\n \"FailureThreshold\": 5,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "9016c9bd9aad954ab25b55fac14ed0295cda5550": {"t": 1785749957.903767, "n": 1966, "h": "6f9aba996428b19a", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"InitialDelaySeconds\": 150,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "49b31a363b2a1b237b97d53d638aad2dbcd3279f": {"t": 1785749959.2831678, "n": 1966, "h": "8b9aaf48e647f684", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"initialDelaySeconds\": 120,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "45a0163991a4d9c2e1add3c194206884f6ecd03a": {"t": 1785749960.358283, "n": 1932, "h": "d8be46df05863e91", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "fb7445b2abf78a0442d8fc132ec96513b2399050": {"t": 1785749961.8656561, "n": 1934, "h": "89ed0346fe79a2dd", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "956c9f0e06bd2fa961a921da326c30029b68938a": {"t": 1785749962.9523864, "n": 1734, "h": "e1f979c607b4c7bb", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "1ca8b8f7f9cdd4597b35367dcb6be1e89c77b1e4": {"t": 1785749964.0457792, "n": 1526, "h": "b89773a21c9bf00d", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "4893a2ef7839c1499308898109716c4f8ca8a213": {"t": 1785749969.022853, "n": 1457, "h": "52700e8c9a1d134d", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "c5ff9274b0de31b599715f7fc2aa76b4bf7f0c74": {"t": 1785749970.1310608, "n": 1355, "h": "327aacc25772a5d6", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "e701be03d01d12a792e286729420a3e2dadf973b": {"t": 1785749971.2254527, "n": 1119, "h": "f94db0707c47bd0e", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "8cf66d2861f0cd29ad6f5cca6aed03fa700d0901": {"t": 1785749972.2951632, "n": 1077, "h": "e8ee40f14345cbb3", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "8cac5f58a452b3c354e16fc82c389f4912fe2ed5": {"t": 1785749973.5188007, "n": 1077, "h": "b6a839446273c8a3", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8080,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "65e5b81123568f44fc9098a0c003378f12852fe2": {"t": 1785749974.6060681, "n": 972, "h": "59e63bd1ef0c6267", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8080,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n }\n ]\n}\n"}, "bf0d8e1358f851dea63091cdab4e485562dd8725": {"t": 1785749975.6847663, "n": 2554, "h": "794b87bf8666262b", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_ENABLED\": \"true\",\n \"QWEN_VL_API_KEY\": \"{{QWEN_VL_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"APP_QRCODE_HMAC_SECRET\": \"{{QRCODE_HMAC_SECRET}}\",\n \"APP_QRCODE_REQUIRE_SIGNATURE\": \"true\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "94f4aebfc1a4ac8252fd599f34112b1d8933c02a": {"t": 1785749976.7376614, "n": 2573, "h": "7937ea1a9bed18ae", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_ENABLED\": \"true\",\n \"QWEN_VL_API_KEY\": \"{{QWEN_VL_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"7a8b0840-0a70-4fc1-a765-bb10c67beb06\",\n \"APP_QRCODE_HMAC_SECRET\": \"{{QRCODE_HMAC_SECRET}}\",\n \"APP_QRCODE_REQUIRE_SIGNATURE\": \"true\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "d74935f72791358c9b608ef2939d28f89c2f7fbd": {"t": 1785749977.7932262, "n": 2457, "h": "0b4e89fe05118e48", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_ENABLED\": \"true\",\n \"QWEN_VL_API_KEY\": \"{{QWEN_VL_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"7a8b0840-0a70-4fc1-a765-bb10c67beb06\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "029844acda5c091b5a594ea030aef7895aa831c6": {"t": 1785749978.875513, "n": 2387, "h": "4b14515dd791616a", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_ENABLED\": \"true\",\n \"QWEN_VL_API_KEY\": \"{{QWEN_VL_API_KEY}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "fe45a6442ae73581b550b593691ff9808801d5a5": {"t": 1785749979.981822, "n": 2333, "h": "365d68f84ced12c2", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_ENABLED\": \"true\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "8bb2076c82e219ecf8dcaa787165a03823b89348": {"t": 1785749981.0676723, "n": 2379, "h": "127277d1ca56b543", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_KEY\": \"{{SERPAPI_KEY}}\",\n \"SERPAPI_ENABLED\": \"true\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "b5a72b5c7060b6722ec3683f93a29968358e4a4b": {"t": 1785749982.104251, "n": 2294, "h": "c4158c5a28591e68", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "7e3b905db3795692be63c5757355ad74956e44be": {"t": 1785749983.2247484, "n": 2353, "h": "c71db57996def376", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"SPRING_REDIS_PASSWORD\": \"{{REDIS_PASSWORD}}\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "2eb2c092df42834f13ba9748da605aacf77d750a": {"t": 1785749984.2849438, "n": 2269, "h": "bd39884af4741658", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"{{REDIS_HOST}}\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "ad51ee6c6a3f767ca4208e39dffd66fb54532203": {"t": 1785749985.343798, "n": 2211, "h": "67574c62768cce8f", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"{{REDIS_HOST}}\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "601715e5cbc4c33900ddcd2a3df14884eaf406b0": {"t": 1785749986.407472, "n": 2165, "h": "8cf96518164031d4", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"{{REDIS_HOST}}\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "40ac69d8bb188db9ff3a7e76bcbef748accb06a1": {"t": 1785749987.5192716, "n": 2156, "h": "2bfc268bdbb7fd0f", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"{{REDIS_HOST}}\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "270bc97bb3d0f62e5eded3e08752b3677c7c64af": {"t": 1785749988.746784, "n": 1966, "h": "c22de3d22a3029a3", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"{{REDIS_HOST}}\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}}, "backend/src/main/java/com/fashion/supplychain/intelligence/gateway/AiInferenceRouter.java": {"92136382831779e22f23b72a08f4e137bfa17dd3": {"t": 1785745687.3106582, "n": 20750, "h": "31e72f3762438f68", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY 非空。\n *\n * <p>原行为:key 为空时启动正常,但所有 AI 功能(对话/视觉/工具调用)都会失败,\n * 用户排查困难(错误散落在各 Service 中)。\n *\n * <p>现行为:\n * <ul>\n * <li>fail-fast-on-empty=true(默认 false):启动失败,明确提示缺失 key</li>\n * <li>fail-fast-on-empty=false(默认):启动成功,但打印 ERROR 级别告警 + 列出受影响功能</li>\n * </ul>\n *\n * <p>受影响功能:DeepSeek 对话、Spring AI 工具调用、AiAdvisor 日常建议、\n * SelfCritic 自评分、EvolutionPipeline 自进化、Qdrant 向量检索(embedding 降级)。\n */\n @Value(\"${ai.api-key.fail-fast-on-empty:false}\")\n private boolean failFastOnEmptyApiKey;\n\n @Value(\"${ai.deepseek.api-key:${DEEPSEEK_API_KEY:}}\")\n private String deepseekApiKey;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY。\n * 默认仅告警不阻止启动,开启 fail-fast-on-empty=true 可强制阻止启动。\n */\n @jakarta.annotation.PostConstruct\n public void validateApiKey() {\n boolean keyMissing = deepseekApiKey == null || deepseekApiKey.isBlank();\n if (!keyMissing) {\n log.info(\"[AiInferenceRouter] DEEPSEEK_API_KEY 已配置(长度={}),AI 功能正常可用\",\n deepseekApiKey.length());\n return;\n }\n\n String affectedFeatures = \"DeepSeek 对话 / Spring AI 工具调用 / AiAdvisor 日常建议 / \"\n + \"SelfCritic 自评分 / EvolutionPipeline 自进化 / Qdrant 向量检索(embedding 降级)\";\n\n if (failFastOnEmptyApiKey) {\n String msg = \"[AiInferenceRouter] DEEPSEEK_API_KEY 为空且 ai.api-key.fail-fast-on-empty=true,\"\n + \"应用启动被阻止。受影响功能:\" + affectedFeatures\n + \"。解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\";\n log.error(msg);\n throw new IllegalStateException(msg);\n }\n\n log.error(\"[AiInferenceRouter] ⚠️ DEEPSEEK_API_KEY 为空!AI 功能将全部失败。受影响功能:{}\", affectedFeatures);\n log.error(\"[AiInferenceRouter] 解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\");\n log.error(\"[AiInferenceRouter] 如需启动失败而非降级,设置 ai.api-key.fail-fast-on-empty=true\");\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public boolean isVisionAvailable() {\n if (legacyAdapter != null && legacyAdapter.isVisionAvailable()) {\n return true;\n }\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isVisionAvailable()) {\n return true;\n }\n return false;\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n if (prompt == null || prompt.isBlank()) {\n log.warn(\"[AiInferenceRouter] chatSimple 收到空 prompt,跳过 LLM 调用\");\n return \"\";\n }\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "92b7fd9578d448a1537b900a5ee7fb0da73abc84": {"t": 1785749954.3668191, "n": 20750, "h": "31e72f3762438f68", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY 非空。\n *\n * <p>原行为:key 为空时启动正常,但所有 AI 功能(对话/视觉/工具调用)都会失败,\n * 用户排查困难(错误散落在各 Service 中)。\n *\n * <p>现行为:\n * <ul>\n * <li>fail-fast-on-empty=true(默认 false):启动失败,明确提示缺失 key</li>\n * <li>fail-fast-on-empty=false(默认):启动成功,但打印 ERROR 级别告警 + 列出受影响功能</li>\n * </ul>\n *\n * <p>受影响功能:DeepSeek 对话、Spring AI 工具调用、AiAdvisor 日常建议、\n * SelfCritic 自评分、EvolutionPipeline 自进化、Qdrant 向量检索(embedding 降级)。\n */\n @Value(\"${ai.api-key.fail-fast-on-empty:false}\")\n private boolean failFastOnEmptyApiKey;\n\n @Value(\"${ai.deepseek.api-key:${DEEPSEEK_API_KEY:}}\")\n private String deepseekApiKey;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY。\n * 默认仅告警不阻止启动,开启 fail-fast-on-empty=true 可强制阻止启动。\n */\n @jakarta.annotation.PostConstruct\n public void validateApiKey() {\n boolean keyMissing = deepseekApiKey == null || deepseekApiKey.isBlank();\n if (!keyMissing) {\n log.info(\"[AiInferenceRouter] DEEPSEEK_API_KEY 已配置(长度={}),AI 功能正常可用\",\n deepseekApiKey.length());\n return;\n }\n\n String affectedFeatures = \"DeepSeek 对话 / Spring AI 工具调用 / AiAdvisor 日常建议 / \"\n + \"SelfCritic 自评分 / EvolutionPipeline 自进化 / Qdrant 向量检索(embedding 降级)\";\n\n if (failFastOnEmptyApiKey) {\n String msg = \"[AiInferenceRouter] DEEPSEEK_API_KEY 为空且 ai.api-key.fail-fast-on-empty=true,\"\n + \"应用启动被阻止。受影响功能:\" + affectedFeatures\n + \"。解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\";\n log.error(msg);\n throw new IllegalStateException(msg);\n }\n\n log.error(\"[AiInferenceRouter] ⚠️ DEEPSEEK_API_KEY 为空!AI 功能将全部失败。受影响功能:{}\", affectedFeatures);\n log.error(\"[AiInferenceRouter] 解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\");\n log.error(\"[AiInferenceRouter] 如需启动失败而非降级,设置 ai.api-key.fail-fast-on-empty=true\");\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public boolean isVisionAvailable() {\n if (legacyAdapter != null && legacyAdapter.isVisionAvailable()) {\n return true;\n }\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isVisionAvailable()) {\n return true;\n }\n return false;\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n if (prompt == null || prompt.isBlank()) {\n log.warn(\"[AiInferenceRouter] chatSimple 收到空 prompt,跳过 LLM 调用\");\n return \"\";\n }\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "6fe4a586649c80cfffa2ec60afd3daca815705ed": {"t": 1785749955.5275457, "n": 18783, "h": "d1c9ce85273937de", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public boolean isVisionAvailable() {\n if (legacyAdapter != null && legacyAdapter.isVisionAvailable()) {\n return true;\n }\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isVisionAvailable()) {\n return true;\n }\n return false;\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n if (prompt == null || prompt.isBlank()) {\n log.warn(\"[AiInferenceRouter] chatSimple 收到空 prompt,跳过 LLM 调用\");\n return \"\";\n }\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "0fc1de77cd3cb0bcbf5f816d748f22c7ffbf316f": {"t": 1785749956.6754286, "n": 18449, "h": "15ced3ba90c9e71c", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n if (prompt == null || prompt.isBlank()) {\n log.warn(\"[AiInferenceRouter] chatSimple 收到空 prompt,跳过 LLM 调用\");\n return \"\";\n }\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "dd96c9df2639c5b031479024e45d7cf36378fc96": {"t": 1785749957.8133483, "n": 18289, "h": "f37704f23d2abafb", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "5096c8449e8798402e701944701dd0cc7033e1b7": {"t": 1785749959.105322, "n": 13376, "h": "ea0aae905f5cd560", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "fc10d435e3d8429461ad7e59b9cdd2179a093756": {"t": 1785749960.1908967, "n": 12396, "h": "f2fd10483f74da71", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n}\n"}, "90c4cf9babe331e23a41125a6d92eb5c22f87e67": {"t": 1785749962.3397975, "n": 12338, "h": "29bcfb47b5e08689", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n}\n"}, "8984df30bfe77bcc304b28856cde70285a8310aa": {"t": 1785749963.4729555, "n": 11590, "h": "dfc25be17d3265ed", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n}\n"}, "01152878ccaac8d964b541b9cfaad7850ed2f39a": {"t": 1785749969.0422065, "n": 11400, "h": "545ebcd37a4241d1", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n}\n"}, "79b8c850437e336d9253403986bf1d477c67887b": {"t": 1785749970.164954, "n": 10968, "h": "41acbf9c7acd51d6", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n log.info(\"[AiInferenceRouter] Spring AI 熔断恢复({}秒冷却期已过)\", CIRCUIT_RESET_MS / 1000);\n return false;\n }\n return true;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures >= CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.compareAndSet(0, System.currentTimeMillis())) {\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后自动恢复尝试)\", failures, CIRCUIT_RESET_MS / 1000);\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-chat\"; // 默认\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n}\n"}, "ba4fdd20657f2c6fc9b5b95efce4d1123dd72f7a": {"t": 1785749971.2633975, "n": 9722, "h": "53ffd1286a57b186", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n log.info(\"[AiInferenceRouter] Spring AI 熔断恢复({}秒冷却期已过)\", CIRCUIT_RESET_MS / 1000);\n return false;\n }\n return true;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures >= CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.compareAndSet(0, System.currentTimeMillis())) {\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后自动恢复尝试)\", failures, CIRCUIT_RESET_MS / 1000);\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable(),\n \"springAiCircuitOpen\", isSpringAiCircuitOpen(),\n \"springAiConsecutiveFailures\", springAiConsecutiveFailures.get(),\n \"activeProvider\", resolveGateway(null).getProviderName()\n );\n }\n}\n"}, "c40d5ea3a741db9d441460401c55605395c9f249": {"t": 1785749972.3551986, "n": 8367, "h": "cbc60b275b5af521", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 5;\n private static final long CIRCUIT_RESET_MS = 60_000L;\n\n @Value(\"${ai.gateway.routing-strategy:legacy}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover();\n case \"legacy\" -> legacyAdapter;\n default -> legacyAdapter;\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover() {\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.warn(\"[AiInferenceRouter] legacy unavailable, using spring-ai as failover\");\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n log.info(\"[AiInferenceRouter] Spring AI 熔断恢复({}秒冷却期已过)\", CIRCUIT_RESET_MS / 1000);\n return false;\n }\n return true;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures >= CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.compareAndSet(0, System.currentTimeMillis())) {\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后自动恢复尝试)\", failures, CIRCUIT_RESET_MS / 1000);\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable(),\n \"springAiCircuitOpen\", isSpringAiCircuitOpen(),\n \"springAiConsecutiveFailures\", springAiConsecutiveFailures.get(),\n \"activeProvider\", resolveGateway(null).getProviderName()\n );\n }\n}\n"}, "224b8fcea85eeb056d0d0a23657208a2ead7c393": {"t": 1785749974.4750092, "n": 5472, "h": "d7ab29a3a989ef56", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n @Value(\"${ai.gateway.routing-strategy:legacy}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover();\n case \"legacy\" -> legacyAdapter;\n default -> legacyAdapter;\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover() {\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.warn(\"[AiInferenceRouter] legacy unavailable, using spring-ai as failover\");\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable(),\n \"activeProvider\", resolveGateway(null).getProviderName()\n );\n }\n}\n"}, "9ff702bfd86152d27bcb7ab8788c73ff521c091b": {"t": 1785749975.545124, "n": 4441, "h": "17bf3af9d40deb40", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n @Value(\"${ai.gateway.routing-strategy:legacy}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return legacyAdapter;\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", false,\n \"activeProvider\", legacyAdapter.getProviderName()\n );\n }\n}\n"}, "3e9c97d347fac7e1e28d906154ecb80603ffc273": {"t": 1785749976.6258533, "n": 5472, "h": "d7ab29a3a989ef56", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n @Value(\"${ai.gateway.routing-strategy:legacy}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover();\n case \"legacy\" -> legacyAdapter;\n default -> legacyAdapter;\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover() {\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.warn(\"[AiInferenceRouter] legacy unavailable, using spring-ai as failover\");\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable(),\n \"activeProvider\", resolveGateway(null).getProviderName()\n );\n }\n}\n"}, "1d8263154937b9e693cf726a990b9a264c4debef": {"t": 1785749977.6948547, "n": 5408, "h": "b7989e14d577f458", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n @Value(\"${ai.gateway.routing-strategy:legacy}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover();\n case \"legacy\" -> legacyAdapter;\n default -> legacyAdapter;\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover() {\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.warn(\"[AiInferenceRouter] legacy unavailable, using spring-ai as failover\");\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable(),\n \"activeProvider\", resolveGateway(null).getProviderName()\n );\n }\n}\n"}, "4de03f126cb66570d274e08d7146394318803d70": {"t": 1786067436.9854896, "n": 20750, "h": "31e72f3762438f68", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY 非空。\n *\n * <p>原行为:key 为空时启动正常,但所有 AI 功能(对话/视觉/工具调用)都会失败,\n * 用户排查困难(错误散落在各 Service 中)。\n *\n * <p>现行为:\n * <ul>\n * <li>fail-fast-on-empty=true(默认 false):启动失败,明确提示缺失 key</li>\n * <li>fail-fast-on-empty=false(默认):启动成功,但打印 ERROR 级别告警 + 列出受影响功能</li>\n * </ul>\n *\n * <p>受影响功能:DeepSeek 对话、Spring AI 工具调用、AiAdvisor 日常建议、\n * SelfCritic 自评分、EvolutionPipeline 自进化、Qdrant 向量检索(embedding 降级)。\n */\n @Value(\"${ai.api-key.fail-fast-on-empty:false}\")\n private boolean failFastOnEmptyApiKey;\n\n @Value(\"${ai.deepseek.api-key:${DEEPSEEK_API_KEY:}}\")\n private String deepseekApiKey;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY。\n * 默认仅告警不阻止启动,开启 fail-fast-on-empty=true 可强制阻止启动。\n */\n @jakarta.annotation.PostConstruct\n public void validateApiKey() {\n boolean keyMissing = deepseekApiKey == null || deepseekApiKey.isBlank();\n if (!keyMissing) {\n log.info(\"[AiInferenceRouter] DEEPSEEK_API_KEY 已配置(长度={}),AI 功能正常可用\",\n deepseekApiKey.length());\n return;\n }\n\n String affectedFeatures = \"DeepSeek 对话 / Spring AI 工具调用 / AiAdvisor 日常建议 / \"\n + \"SelfCritic 自评分 / EvolutionPipeline 自进化 / Qdrant 向量检索(embedding 降级)\";\n\n if (failFastOnEmptyApiKey) {\n String msg = \"[AiInferenceRouter] DEEPSEEK_API_KEY 为空且 ai.api-key.fail-fast-on-empty=true,\"\n + \"应用启动被阻止。受影响功能:\" + affectedFeatures\n + \"。解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\";\n log.error(msg);\n throw new IllegalStateException(msg);\n }\n\n log.error(\"[AiInferenceRouter] ⚠️ DEEPSEEK_API_KEY 为空!AI 功能将全部失败。受影响功能:{}\", affectedFeatures);\n log.error(\"[AiInferenceRouter] 解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\");\n log.error(\"[AiInferenceRouter] 如需启动失败而非降级,设置 ai.api-key.fail-fast-on-empty=true\");\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public boolean isVisionAvailable() {\n if (legacyAdapter != null && legacyAdapter.isVisionAvailable()) {\n return true;\n }\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isVisionAvailable()) {\n return true;\n }\n return false;\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n if (prompt == null || prompt.isBlank()) {\n log.warn(\"[AiInferenceRouter] chatSimple 收到空 prompt,跳过 LLM 调用\");\n return \"\";\n }\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "b9ae58d3147bb0308a7a5ead91018682cac43d9c": {"t": 1786068301.6568787, "n": 20750, "h": "31e72f3762438f68", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY 非空。\n *\n * <p>原行为:key 为空时启动正常,但所有 AI 功能(对话/视觉/工具调用)都会失败,\n * 用户排查困难(错误散落在各 Service 中)。\n *\n * <p>现行为:\n * <ul>\n * <li>fail-fast-on-empty=true(默认 false):启动失败,明确提示缺失 key</li>\n * <li>fail-fast-on-empty=false(默认):启动成功,但打印 ERROR 级别告警 + 列出受影响功能</li>\n * </ul>\n *\n * <p>受影响功能:DeepSeek 对话、Spring AI 工具调用、AiAdvisor 日常建议、\n * SelfCritic 自评分、EvolutionPipeline 自进化、Qdrant 向量检索(embedding 降级)。\n */\n @Value(\"${ai.api-key.fail-fast-on-empty:false}\")\n private boolean failFastOnEmptyApiKey;\n\n @Value(\"${ai.deepseek.api-key:${DEEPSEEK_API_KEY:}}\")\n private String deepseekApiKey;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY。\n * 默认仅告警不阻止启动,开启 fail-fast-on-empty=true 可强制阻止启动。\n */\n @jakarta.annotation.PostConstruct\n public void validateApiKey() {\n boolean keyMissing = deepseekApiKey == null || deepseekApiKey.isBlank();\n if (!keyMissing) {\n log.info(\"[AiInferenceRouter] DEEPSEEK_API_KEY 已配置(长度={}),AI 功能正常可用\",\n deepseekApiKey.length());\n return;\n }\n\n String affectedFeatures = \"DeepSeek 对话 / Spring AI 工具调用 / AiAdvisor 日常建议 / \"\n + \"SelfCritic 自评分 / EvolutionPipeline 自进化 / Qdrant 向量检索(embedding 降级)\";\n\n if (failFastOnEmptyApiKey) {\n String msg = \"[AiInferenceRouter] DEEPSEEK_API_KEY 为空且 ai.api-key.fail-fast-on-empty=true,\"\n + \"应用启动被阻止。受影响功能:\" + affectedFeatures\n + \"。解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\";\n log.error(msg);\n throw new IllegalStateException(msg);\n }\n\n log.error(\"[AiInferenceRouter] ⚠️ DEEPSEEK_API_KEY 为空!AI 功能将全部失败。受影响功能:{}\", affectedFeatures);\n log.error(\"[AiInferenceRouter] 解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\");\n log.error(\"[AiInferenceRouter] 如需启动失败而非降级,设置 ai.api-key.fail-fast-on-empty=true\");\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public boolean isVisionAvailable() {\n if (legacyAdapter != null && legacyAdapter.isVisionAvailable()) {\n return true;\n }\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isVisionAvailable()) {\n return true;\n }\n return false;\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n if (prompt == null || prompt.isBlank()) {\n log.warn(\"[AiInferenceRouter] chatSimple 收到空 prompt,跳过 LLM 调用\");\n return \"\";\n }\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}}, "backend/src/main/java/com/fashion/supplychain/intelligence/service/AiAdvisorService.java": {"92136382831779e22f23b72a08f4e137bfa17dd3": {"t": 1785745701.6783586, "n": 5838, "h": "0c462b9a466a0964", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Lazy\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n @Scheduled(fixedRate = 3600000)\n public void cleanupStaleCounters() {\n String todaySuffix = \"_\" + LocalDate.now();\n dailyCounters.keySet().removeIf(key -> !key.endsWith(todaySuffix));\n }\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + T(java.time.LocalDate).now()\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "fc10d435e3d8429461ad7e59b9cdd2179a093756": {"t": 1785749953.3053546, "n": 5838, "h": "0c462b9a466a0964", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Lazy\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n @Scheduled(fixedRate = 3600000)\n public void cleanupStaleCounters() {\n String todaySuffix = \"_\" + LocalDate.now();\n dailyCounters.keySet().removeIf(key -> !key.endsWith(todaySuffix));\n }\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + T(java.time.LocalDate).now()\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "cc55c6b31d802f926aac252da09310cf26df12ee": {"t": 1785749954.580515, "n": 5780, "h": "d524bdf2a0c2d6de", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n @Scheduled(fixedRate = 3600000)\n public void cleanupStaleCounters() {\n String todaySuffix = \"_\" + LocalDate.now();\n dailyCounters.keySet().removeIf(key -> !key.endsWith(todaySuffix));\n }\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + T(java.time.LocalDate).now()\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "5f3b3d1c5d9e1263d4af0f790474284a920c8a8f": {"t": 1785749957.7036502, "n": 5495, "h": "60fab315a6e32af6", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + #contextSummary\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "64c4685b5bf2577e8ed6f8aff55fa2b7fa6cf319": {"t": 1785749958.7763484, "n": 5543, "h": "15755135f48490ef", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + #contextSummary\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "34014ba5d2031264de5c960eaad707590ce3b729": {"t": 1785749959.8803394, "n": 5478, "h": "3cfa15a117d66e9f", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"#contextSummary\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "ee39636df0ebdc4a1f9923cc34d0684344a4bf4b": {"t": 1785749960.9941413, "n": 5184, "h": "f1a7f3b07fb8ce2d", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "6f4ed6d1a104aecf5e94fb4632c129e2a83d710e": {"t": 1785749963.079358, "n": 6578, "h": "164114db2d13a06e", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务 — 统一的 AI API 接入层\n *\n * <p>默认接 DeepSeek,配置方式(环境变量):\n * <pre>\n * AI_API_KEY = sk-xxxxxxxxxxxxxxxx (必填,有此值才会真实调用 AI)\n * AI_API_URL = https://api.deepseek.com/v1/chat/completions (可选,默认 DeepSeek)\n * AI_MODEL = deepseek-chat (可选,默认 deepseek-chat)\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n *\n * <p>切换 OpenAI / 其他兼容 API 只需改 AI_API_URL 和 AI_MODEL。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n @Value(\"${ai.deepseek.api-url:https://api.deepseek.com/v1/chat/completions}\")\n private String apiUrl;\n\n @Value(\"${ai.deepseek.model:deepseek-chat}\")\n private String model;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n private static final int TIMEOUT_SECONDS = 30;\n private static final ObjectMapper MAPPER = new ObjectMapper();\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return apiKey != null && !apiKey.isBlank();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n log.debug(\"[AiAdvisor] AI_API_KEY 未配置,跳过 AI 调用\");\n return null;\n }\n\n try {\n String body = buildRequestBody(systemPrompt, userMessage);\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(10))\n .build();\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(apiUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(body))\n .timeout(Duration.ofSeconds(TIMEOUT_SECONDS))\n .build();\n\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n\n if (response.statusCode() == 200) {\n return extractContent(response.body());\n } else {\n log.warn(\"[AiAdvisor] API 返回非200: status={}, body={}\", response.statusCode(),\n response.body().substring(0, Math.min(200, response.body().length())));\n return null;\n }\n } catch (Exception e) {\n log.warn(\"[AiAdvisor] AI 调用失败: {}\", e.getMessage());\n return null;\n }\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n return chat(systemPrompt, contextSummary);\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n return chat(systemPrompt, question);\n }\n\n // ────────────────── 私有方法 ──────────────────\n\n private String buildRequestBody(String systemPrompt, String userMessage) throws Exception {\n var root = MAPPER.createObjectNode();\n root.put(\"model\", model);\n root.put(\"temperature\", 0.3);\n root.put(\"max_tokens\", 512);\n var messages = root.putArray(\"messages\");\n messages.addObject().put(\"role\", \"system\").put(\"content\", systemPrompt);\n messages.addObject().put(\"role\", \"user\").put(\"content\", userMessage);\n return MAPPER.writeValueAsString(root);\n }\n\n private String extractContent(String responseBody) throws Exception {\n JsonNode root = MAPPER.readTree(responseBody);\n JsonNode choices = root.path(\"choices\");\n if (choices.isArray() && choices.size() > 0) {\n return choices.get(0).path(\"message\").path(\"content\").asText(null);\n }\n return null;\n }\n}\n"}, "fdd7d49b70ea84d9b24ec29bc480b05a726ff117": {"t": 1785749969.0282362, "n": 4998, "h": "61094422963d0eb0", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\n\n/**\n * AI 顾问服务 — 统一的 AI API 接入层\n *\n * <p>默认接 DeepSeek,配置方式(环境变量):\n * <pre>\n * AI_API_KEY = sk-xxxxxxxxxxxxxxxx (必填,有此值才会真实调用 AI)\n * AI_API_URL = https://api.deepseek.com/v1/chat/completions (可选,默认 DeepSeek)\n * AI_MODEL = deepseek-chat (可选,默认 deepseek-chat)\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n *\n * <p>切换 OpenAI / 其他兼容 API 只需改 AI_API_URL 和 AI_MODEL。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n @Value(\"${ai.deepseek.api-url:https://api.deepseek.com/v1/chat/completions}\")\n private String apiUrl;\n\n @Value(\"${ai.deepseek.model:deepseek-chat}\")\n private String model;\n\n private static final int TIMEOUT_SECONDS = 30;\n private static final ObjectMapper MAPPER = new ObjectMapper();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return apiKey != null && !apiKey.isBlank();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n log.debug(\"[AiAdvisor] AI_API_KEY 未配置,跳过 AI 调用\");\n return null;\n }\n\n try {\n String body = buildRequestBody(systemPrompt, userMessage);\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(10))\n .build();\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(apiUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(body))\n .timeout(Duration.ofSeconds(TIMEOUT_SECONDS))\n .build();\n\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n\n if (response.statusCode() == 200) {\n return extractContent(response.body());\n } else {\n log.warn(\"[AiAdvisor] API 返回非200: status={}, body={}\", response.statusCode(),\n response.body().substring(0, Math.min(200, response.body().length())));\n return null;\n }\n } catch (Exception e) {\n log.warn(\"[AiAdvisor] AI 调用失败: {}\", e.getMessage());\n return null;\n }\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n return chat(systemPrompt, contextSummary);\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n return chat(systemPrompt, question);\n }\n\n // ────────────────── 私有方法 ──────────────────\n\n private String buildRequestBody(String systemPrompt, String userMessage) throws Exception {\n var root = MAPPER.createObjectNode();\n root.put(\"model\", model);\n root.put(\"temperature\", 0.3);\n root.put(\"max_tokens\", 512);\n var messages = root.putArray(\"messages\");\n messages.addObject().put(\"role\", \"system\").put(\"content\", systemPrompt);\n messages.addObject().put(\"role\", \"user\").put(\"content\", userMessage);\n return MAPPER.writeValueAsString(root);\n }\n\n private String extractContent(String responseBody) throws Exception {\n JsonNode root = MAPPER.readTree(responseBody);\n JsonNode choices = root.path(\"choices\");\n if (choices.isArray() && choices.size() > 0) {\n return choices.get(0).path(\"message\").path(\"content\").asText(null);\n }\n return null;\n }\n}\n"}, "4de03f126cb66570d274e08d7146394318803d70": {"t": 1786067442.4516592, "n": 5838, "h": "0c462b9a466a0964", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Lazy\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n @Scheduled(fixedRate = 3600000)\n public void cleanupStaleCounters() {\n String todaySuffix = \"_\" + LocalDate.now();\n dailyCounters.keySet().removeIf(key -> !key.endsWith(todaySuffix));\n }\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + T(java.time.LocalDate).now()\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "b9ae58d3147bb0308a7a5ead91018682cac43d9c": {"t": 1786068303.962534, "n": 5838, "h": "0c462b9a466a0964", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Lazy\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n @Scheduled(fixedRate = 3600000)\n public void cleanupStaleCounters() {\n String todaySuffix = \"_\" + LocalDate.now();\n dailyCounters.keySet().removeIf(key -> !key.endsWith(todaySuffix));\n }\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + T(java.time.LocalDate).now()\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}}, "backend/src/main/java/com/fashion/supplychain/intelligence/service/QdrantService.java": {"b9ae58d3147bb0308a7a5ead91018682cac43d9c": {"t": 1786068304.6111438, "n": 63782, "h": "ceba53f5a02d69f1", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.security.MessageDigest;\nimport java.util.stream.Collectors;\nimport jakarta.annotation.PostConstruct;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.http.HttpEntity;\nimport org.springframework.http.HttpHeaders;\nimport org.springframework.http.HttpMethod;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.http.client.SimpleClientHttpRequestFactory;\nimport org.springframework.stereotype.Service;\nimport org.springframework.web.client.RestTemplate;\nimport org.springframework.context.annotation.Lazy;\n\n/**\n * Qdrant 向量库接入服务\n *\n * <p>通过 Qdrant REST API v1.x 实现向量存储与相似检索。\n * 每个租户使用同一个 collection,通过 tenant_id payload 过滤隔离。\n *\n * <p>向量生成优先级:① Agnes 视觉分析 + Agnes/DeepSeek Embedding(图片→文字描述→向量,推荐)\n * ② DeepSeek Embedding API(text-embedding-v2,用图片URL文本生成向量)\n * ③ 关键词哈希伪向量(pseudoEmbedding,128维,无需 API Key)\n *\n * <p>配置项(application.yml):\n * <pre>\n * intelligence:\n * qdrant:\n * url: http://localhost:6333\n * collection: fashion_memory\n * vector-size: 1024\n * ai:\n * agnes:\n * api-key: ${AGNES_API_KEY:} # Agnes AI 视觉+Embedding\n * model: agnes-2.5-flash\n * deepseek:\n * api-key: sk-xxx\n * embedding-model: text-embedding-v2\n * </pre>\n */\n@Service\n@Lazy\n@Slf4j\npublic class QdrantService {\n\n private static final String COLLECTION_DEFAULT = \"fashion_memory\";\n private static final int VECTOR_DIM_REAL = 1024;\n private static final int VECTOR_DIM_PSEUDO = 128;\n /** 款式图片单独集合,与文字记忆集合分离 */\n private static final String STYLE_IMAGE_COLLECTION = \"style_images\";\n /** 稀疏向量名称,与 Qdrant 集合的 sparse_vectors 配置对应 */\n private static final String SPARSE_VECTOR_NAME = \"text-sparse\";\n\n @Value(\"${intelligence.qdrant.url:http://localhost:6333}\")\n private String qdrantUrl;\n\n @Value(\"${intelligence.qdrant.collection:\" + COLLECTION_DEFAULT + \"}\")\n private String collectionName;\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String deepseekApiKey;\n\n @Value(\"${ai.deepseek.embedding-model:text-embedding-v2}\")\n private String embeddingModel;\n\n @Value(\"${ai.deepseek.base-url:https://api.deepseek.com}\")\n private String deepseekBaseUrl;\n\n @Value(\"${intelligence.qdrant.enabled:false}\")\n private boolean qdrantEnabled;\n\n @Value(\"${intelligence.qdrant.timeout-seconds:10}\")\n private int qdrantTimeoutSeconds;\n\n @Autowired\n private ObjectMapper objectMapper;\n\n @Value(\"${ai.agnes.api-key:}\")\n private String agnesApiKey;\n\n @Value(\"${ai.agnes.api-url:https://apihub.agnes-ai.com/v1/chat/completions}\")\n private String agnesApiUrl;\n\n @Value(\"${ai.agnes.model:agnes-2.5-flash}\")\n private String agnesModel;\n\n @Autowired\n private com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator inferenceOrchestrator;\n\n private RestTemplate restTemplate;\n\n private final ConcurrentHashMap<String, EmbeddingCacheEntry> embeddingCache = new ConcurrentHashMap<>();\n private static final long EMBEDDING_CACHE_TTL_MS = TimeUnit.MINUTES.toMillis(30);\n private static final int EMBEDDING_CACHE_MAX = 500;\n private static final String PROVIDER_DEEPSEEK = \"deepseek\";\n private static final String PROVIDER_AGNES = \"agnes\";\n\n private final AtomicBoolean collectionVerified = new AtomicBoolean(false);\n private final AtomicBoolean styleImageCollectionVerified = new AtomicBoolean(false);\n /** 混合检索降级标记:true 表示 Qdrant 不支持混合检索,后续直接走纯稠密检索 */\n private final AtomicBoolean hybridSearchDegraded = new AtomicBoolean(false);\n\n private static class EmbeddingCacheEntry {\n final float[] vector;\n final String provider;\n final long createdAt;\n EmbeddingCacheEntry(float[] vector, String provider) {\n this.vector = vector;\n this.provider = provider;\n this.createdAt = System.currentTimeMillis();\n }\n boolean isExpired() { return System.currentTimeMillis() - createdAt > EMBEDDING_CACHE_TTL_MS; }\n }\n\n private String sha256Hex(String text) {\n try {\n MessageDigest md = MessageDigest.getInstance(\"SHA-256\");\n byte[] hash = md.digest(text.getBytes(java.nio.charset.StandardCharsets.UTF_8));\n StringBuilder sb = new StringBuilder();\n for (byte b : hash) sb.append(String.format(\"%02x\", b));\n return sb.toString();\n } catch (Exception e) {\n return String.valueOf(text.hashCode());\n }\n }\n\n @PostConstruct\n void initRestTemplate() {\n SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();\n factory.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(Math.max(qdrantTimeoutSeconds, 5)));\n factory.setReadTimeout((int) TimeUnit.SECONDS.toMillis(qdrantTimeoutSeconds));\n this.restTemplate = new RestTemplate(factory);\n }\n\n /** F4: 启动时校验 Qdrant 集合向量维度是否与当前配置一致 */\n @PostConstruct\n void validateCollectionDimension() {\n if (!qdrantEnabled) {\n log.info(\"[Qdrant] 已禁用(intelligence.qdrant.enabled=false),跳过向量库初始化。如需要启用请配置 QDRANT_URL 并设 intelligence.qdrant.enabled=true\");\n return;\n }\n try {\n ResponseEntity<String> resp = restTemplate.getForEntity(\n qdrantUrl + \"/collections/\" + collectionName, String.class);\n if (resp.getStatusCode().is2xxSuccessful() && resp.getBody() != null) {\n JsonNode root = objectMapper.readTree(resp.getBody());\n long storedDim = root.path(\"result\").path(\"config\")\n .path(\"params\").path(\"vectors\").path(\"size\").asLong(-1);\n int expectedDim = getVectorDim();\n if (storedDim > 0 && storedDim != expectedDim) {\n // 【P1-7修复】原 log.error 在\"无 API Key 走 pseudoEmbedding(128维)\"场景下产生大量 ERROR 噪音\n // 改为 log.warn:仍保留问题可见性,但不污染 ERROR 级别日志(真正的服务错误才用 ERROR)\n // 同时补充降级提示:若使用 pseudoEmbedding 属于配置降级,非故障\n log.warn(\"[Qdrant] 向量维度不匹配(可能为 pseudoEmbedding 降级模式)集合={} 存储维度={} 当前配置维度={}\"\n + \" — 搜索结果可能不可靠,请重建集合或调整 API Key 配置\",\n collectionName, storedDim, expectedDim);\n } else if (storedDim > 0) {\n log.info(\"[Qdrant] 集合 {} 维度校验通过 dim={}\", collectionName, storedDim);\n }\n }\n } catch (Exception e) {\n log.debug(\"[Qdrant] 启动维度校验跳过(Qdrant不可用): {}\", e.getMessage());\n }\n }\n\n // ──────────────────────────────────────────────────────────────\n // 公开接口\n // ──────────────────────────────────────────────────────────────\n\n /**\n * 向量化存储一条记忆。\n *\n * @param pointId 唯一ID(使用 t_intelligence_memory.id 转字符串)\n * @param tenantId 租户ID(用于 payload 过滤)\n * @param content 记忆文本(用于生成伪向量)\n * @param payload 附加元数据(title/type/domain 等)\n * @return 是否成功\n */\n public boolean upsertVector(String pointId, Long tenantId, String content,\n java.util.Map<String, Object> payload) {\n if (!qdrantEnabled) return false;\n if (tenantId == null) {\n log.warn(\"[Qdrant] upsert拒绝:tenantId为null,禁止写入孤儿向量 pointId={}\", pointId);\n return false;\n }\n try {\n ensureCollectionExists();\n float[] vector = computeEmbedding(content);\n if (vector == null) {\n log.warn(\"[Qdrant] upsert跳过:内容为空无法生成向量 pointId={}\", pointId);\n return false;\n }\n\n ObjectNode body = objectMapper.createObjectNode();\n ArrayNode points = body.putArray(\"points\");\n ObjectNode point = points.addObject();\n point.put(\"id\", pointId);\n\n ArrayNode vec = point.putArray(\"vector\");\n for (float v : vector) {\n vec.add(v);\n }\n\n ObjectNode payloadNode = point.putObject(\"payload\");\n payloadNode.put(\"tenant_id\", tenantId);\n if (payload != null) {\n payload.forEach((k, v) -> payloadNode.put(k, String.valueOf(v)));\n }\n\n String url = qdrantUrl + \"/collections/\" + collectionName + \"/points\";\n HttpEntity<String> entity = jsonEntity(body.toString());\n ResponseEntity<String> resp = restTemplate.exchange(url,\n HttpMethod.PUT, entity, String.class);\n return resp.getStatusCode().is2xxSuccessful();\n } catch (Exception e) {\n log.warn(\"[Qdrant] upsert失败 pointId={}: {}\", pointId, e.getMessage());\n return false;\n }\n }\n\n /**\n * 按文本内容搜索相似记忆(限同租户)。\n *\n * @param tenantId 租户ID\n * @param queryText 查询文本\n * @param topK 返回条数\n * @return 匹配点的 pointId 列表(按相似度降序)\n */\n public List<ScoredPoint> search(Long tenantId, String queryText, int topK) {\n if (!qdrantEnabled) return Collections.emptyList();\n List<ScoredPoint> results = new ArrayList<>();\n try {\n float[] vector = computeEmbedding(queryText);\n if (vector == null) {\n log.warn(\"[Qdrant] search跳过:查询文本为空无法生成向量\");\n return results;\n }\n\n ObjectNode body = objectMapper.createObjectNode();\n ArrayNode vec = body.putArray(\"vector\");\n for (float v : vector) {\n vec.add(v);\n }\n body.put(\"limit\", topK);\n body.put(\"with_payload\", true);\n // F26: 过滤低质量匹配,余弦相似度 < 0.3 的结果直接丢弃\n body.put(\"score_threshold\", 0.3);\n\n // 租户隔离过滤(tenantId 为 null 时拒绝搜索,防止跨租户数据泄漏)\n if (tenantId == null) {\n log.warn(\"[Qdrant] search拒绝: tenantId为null,跳过搜索以防止跨租户数据泄漏\");\n return results;\n }\n ObjectNode filter = body.putObject(\"filter\");\n ArrayNode should = filter.putArray(\"should\");\n\n ObjectNode tenantCond = should.addObject();\n tenantCond.put(\"key\", \"tenant_id\");\n ObjectNode tenantMatchVal = tenantCond.putObject(\"match\");\n tenantMatchVal.put(\"integer\", tenantId);\n\n ObjectNode publicCond = should.addObject();\n publicCond.put(\"key\", \"tenant_id\");\n ObjectNode publicMatchVal = publicCond.putObject(\"match\");\n publicMatchVal.put(\"integer\", 0);\n\n String url = qdrantUrl + \"/collections/\" + collectionName + \"/points/search\";\n HttpEntity<String> entity = jsonEntity(body.toString());\n ResponseEntity<String> resp = restTemplate.postForEntity(url, entity, String.class);\n\n if (resp.getStatusCode().is2xxSuccessful() && resp.getBody() != null) {\n JsonNode root = objectMapper.readTree(resp.getBody());\n JsonNode resultNode = root.path(\"result\");\n if (resultNode.isArray()) {\n for (JsonNode item : resultNode) {\n ScoredPoint sp = new ScoredPoint();\n sp.setPointId(item.path(\"id\").asText());\n sp.setScore((float) item.path(\"score\").asDouble());\n sp.setPayload(readPayload(item.path(\"payload\")));\n results.add(sp);\n }\n }\n }\n } catch (Exception e) {\n log.warn(\"[Qdrant] search失败 tenantId={}: {}\", tenantId, e.getMessage());\n }\n return results;\n }\n\n private Map<String, String> readPayload(JsonNode payloadNode) {\n if (payloadNode == null || payloadNode.isMissingNode() || payloadNode.isNull()) {\n return Collections.emptyMap();\n }\n Map<String, String> payload = new LinkedHashMap<>();\n payloadNode.fields().forEachRemaining(entry -> payload.put(entry.getKey(), entry.getValue().asText(\"\")));\n return payload;\n }\n\n // ──────────────────────────────────────────────────────────────\n // 混合检索(稀疏+稠密)\n // ──────────────────────────────────────────────────────────────\n\n /**\n * 混合检索:同时执行稠密向量检索和稀疏关键词检索,合并结果按综合分数排序。\n *\n * <p>使用 Qdrant 的 /points/query 端点,支持稀疏+稠密混合检索。\n * 如果 Qdrant 不支持混合检索(旧版本),自动降级到纯稠密检索。\n *\n * @param tenantId 租户ID(必须非null,否则拒绝搜索)\n * @param queryText 查询文本\n * @param topK 返回条数\n * @return 匹配点列表(按综合分数降序)\n */\n public List<ScoredPoint> hybridSearch(Long tenantId, String queryText, int topK) {\n if (!qdrantEnabled) return new ArrayList<>();\n if (tenantId == null) {\n log.warn(\"[Qdrant] hybridSearch拒绝: tenantId为null,跳过搜索以防止跨租户数据泄漏\");\n return new ArrayList<>();\n }\n\n // 如果已确认不支持混合检索,直接降级\n if (hybridSearchDegraded.get()) {\n log.debug(\"[Qdrant] hybridSearch降级: Qdrant不支持混合检索,走纯稠密检索\");\n return search(tenantId, queryText, topK);\n }\n\n try {\n ensureCollectionExists();\n\n // 1. 生成稠密向量\n float[] denseVector = computeEmbedding(queryText);\n if (denseVector == null) {\n log.warn(\"[Qdrant] hybridSearch跳过:查询文本为空无法生成向量\");\n return new ArrayList<>();\n }\n\n // 2. 生成稀疏向量\n SparseVector sparseVector = computeSparseVector(queryText);\n\n // 3. 构造 /points/query 请求体\n ObjectNode body = objectMapper.createObjectNode();\n\n // 稠密向量\n ArrayNode queryArr = body.putArray(\"query\");\n for (float v : denseVector) {\n queryArr.add(v);\n }\n\n // 稀疏向量\n if (sparseVector != null && !sparseVector.indices.isEmpty()) {\n ObjectNode sparseNode = body.putObject(\"sparse_vector\");\n ArrayNode indicesArr = sparseNode.putArray(\"indices\");\n ArrayNode valuesArr = sparseNode.putArray(\"values\");\n for (int i = 0; i < sparseVector.indices.size(); i++) {\n indicesArr.add(sparseVector.indices.get(i));\n valuesArr.add(sparseVector.values.get(i));\n }\n }\n\n body.put(\"limit\", topK);\n body.put(\"with_payload\", true);\n body.put(\"score_threshold\", 0.3);\n\n // 租户隔离过滤\n ObjectNode filter = body.putObject(\"filter\");\n ArrayNode should = filter.putArray(\"should\");\n\n ObjectNode tenantCond = should.addObject();\n tenantCond.put(\"key\", \"tenant_id\");\n ObjectNode tenantMatchVal = tenantCond.putObject(\"match\");\n tenantMatchVal.put(\"integer\", tenantId);\n\n ObjectNode publicCond = should.addObject();\n publicCond.put(\"key\", \"tenant_id\");\n ObjectNode publicMatchVal = publicCond.putObject(\"match\");\n publicMatchVal.put(\"integer\", 0);\n\n // 4. 调用 /points/query 端点\n String url = qdrantUrl + \"/collections/\" + collectionName + \"/points/query\";\n HttpEntity<String> entity = jsonEntity(body.toString());\n ResponseEntity<String> resp = restTemplate.postForEntity(url, entity, String.class);\n\n if (resp.getStatusCode().is2xxSuccessful() && resp.getBody() != null) {\n List<ScoredPoint> results = new ArrayList<>();\n JsonNode root = objectMapper.readTree(resp.getBody());\n JsonNode resultNode = root.path(\"result\");\n if (resultNode.isArray()) {\n for (JsonNode item : resultNode) {\n ScoredPoint sp = new ScoredPoint();\n sp.setPointId(item.path(\"id\").asText());\n sp.setScore((float) item.path(\"score\").asDouble());\n sp.setPayload(readPayload(item.path(\"payload\")));\n results.add(sp);\n }\n }\n log.debug(\"[Qdrant] hybridSearch成功 tenantId={} 结果数={}\", tenantId, results.size());\n return results;\n }\n } catch (Exception e) {\n String msg = e.getMessage();\n // 判断是否为不支持混合检索的错误\n if (msg != null && (msg.contains(\"sparse\") || msg.contains(\"not found\")\n || msg.contains(\"not supported\") || msg.contains(\"404\")\n || msg.contains(\"No sparse vector\"))) {\n log.warn(\"[Qdrant] hybridSearch降级: Qdrant不支持混合检索,后续将走纯稠密检索 - {}\", msg);\n hybridSearchDegraded.set(true);\n return search(tenantId, queryText, topK);\n }\n log.warn(\"[Qdrant] hybridSearch失败 tenantId={}: {},降级到纯稠密检索\", tenantId, msg);\n }\n\n // 降级到纯稠密检索\n return search(tenantId, queryText, topK);\n }\n\n /**\n * 检查混合检索是否可用(供外部判断是否使用混合检索)。\n */\n public boolean isHybridSearchAvailable() {\n return !hybridSearchDegraded.get();\n }\n\n /**\n * 稀疏向量内部表示\n */\n private static class SparseVector {\n final List<Integer> indices = new ArrayList<>();\n final List<Float> values = new ArrayList<>();\n }\n\n /**\n * BM25风格稀疏向量生成:对查询文本分词,生成 token-id:tf 键值对。\n *\n * <p>分词策略:中文按双字符滑动窗口 + 单字符,英文按空格分词。\n * token-id 通过字符串哈希映射到正整数空间。\n * tf(词频)归一化到 [0, 1] 区间。\n */\n private SparseVector computeSparseVector(String text) {\n if (text == null || text.isBlank()) return null;\n try {\n // 分词:中文双字符窗口 + 单字符 + 英文单词\n Map<String, Integer> termFreq = new HashMap<>();\n String trimmed = text.trim();\n\n // 英文分词(按空格/标点分割)\n String[] words = trimmed.split(\"[\\\\s,,。.!!??;;::、/\\\\\\\\()()\\\\[\\\\]【】{}]+\");\n for (String word : words) {\n if (word.isEmpty()) continue;\n String lower = word.toLowerCase();\n termFreq.merge(lower, 1, Integer::sum);\n }\n\n // 中文双字符滑动窗口(bigram)\n for (int i = 0; i < trimmed.length() - 1; i++) {\n char c1 = trimmed.charAt(i);\n char c2 = trimmed.charAt(i + 1);\n if (isChineseChar(c1) && isChineseChar(c2)) {\n String bigram = \"\" + c1 + c2;\n termFreq.merge(bigram, 1, Integer::sum);\n }\n }\n\n // 中文单字符(仅对高频字单独建索引)\n for (int i = 0; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (isChineseChar(c)) {\n String single = String.valueOf(c);\n termFreq.merge(single, 1, Integer::sum);\n }\n }\n\n if (termFreq.isEmpty()) return null;\n\n // 找最大词频用于归一化\n int maxFreq = termFreq.values().stream().mapToInt(Integer::intValue).max().orElse(1);\n\n // 构建 SparseVector\n SparseVector sv = new SparseVector();\n for (Map.Entry<String, Integer> entry : termFreq.entrySet()) {\n int tokenId = Math.abs(entry.getKey().hashCode()) % 100000 + 1;\n float tf = (float) entry.getValue() / maxFreq;\n sv.indices.add(tokenId);\n sv.values.add(tf);\n }\n return sv;\n } catch (Exception e) {\n log.debug(\"[Qdrant] 稀疏向量生成失败: {}\", e.getMessage());\n return null;\n }\n }\n\n private boolean isChineseChar(char c) {\n return c >= '\\u4e00' && c <= '\\u9fff';\n }\n\n /** 删除指定向量点(需提供 tenantId 校验归属,tenantId 为 null 时拒绝删除) */\n public void deleteVector(String pointId, Long tenantId) {\n if (tenantId == null) {\n log.warn(\"[Qdrant] delete拒绝: tenantId为null,拒绝删除以防止跨租户数据操作 pointId={}\", pointId);\n return;\n }\n try {\n ObjectNode body = objectMapper.createObjectNode();\n ObjectNode filter = body.putObject(\"filter\");\n ArrayNode must = filter.putArray(\"must\");\n ObjectNode idCond = must.addObject();\n idCond.put(\"key\", \"id\");\n idCond.putObject(\"match\").put(\"value\", pointId);\n ObjectNode tenantCond = must.addObject();\n tenantCond.put(\"key\", \"tenant_id\");\n tenantCond.putObject(\"match\").put(\"integer\", tenantId);\n\n String url = qdrantUrl + \"/collections/\" + collectionName + \"/points/delete\";\n restTemplate.exchange(url, HttpMethod.POST, jsonEntity(body.toString()), String.class);\n } catch (Exception e) {\n log.warn(\"[Qdrant] delete失败 pointId={}: {}\", pointId, e.getMessage());\n }\n }\n\n private final java.util.concurrent.atomic.AtomicLong lastHealthCheckTime = new java.util.concurrent.atomic.AtomicLong(0);\n private volatile boolean lastHealthStatus = false;\n private static final long HEALTH_CHECK_CACHE_MS = 30_000L;\n\n public boolean isAvailable() {\n if (!qdrantEnabled) return false;\n long now = System.currentTimeMillis();\n long last = lastHealthCheckTime.get();\n if (now - last < HEALTH_CHECK_CACHE_MS) {\n return lastHealthStatus;\n }\n try {\n ResponseEntity<String> resp = restTemplate.getForEntity(\n qdrantUrl + \"/healthz\", String.class);\n boolean ok = resp.getStatusCode().is2xxSuccessful();\n lastHealthStatus = ok;\n lastHealthCheckTime.set(now);\n return ok;\n } catch (Exception e) {\n lastHealthStatus = false;\n lastHealthCheckTime.set(now);\n return false;\n }\n }\n\n /**\n * 确保集合存在(公开管理入口,供 QdrantAdminOrchestrator 在启动时调用)。\n * @return true=新建了集合;false=集合已存在或 Qdrant 不可用\n */\n public boolean ensureCollection() {\n if (!qdrantEnabled) return false;\n try {\n restTemplate.getForEntity(\n qdrantUrl + \"/collections/\" + collectionName, String.class);\n return false; // 已存在\n } catch (Exception e) {\n // 不存在,创建\n ensureCollectionExists();\n return true;\n }\n }\n\n /**\n * 查询集合中向量总数(Admin 统计用)。\n * @return 向量点数;-1 表示查询失败或 Qdrant 不可用\n */\n public long countVectors() {\n try {\n ResponseEntity<String> resp = restTemplate.getForEntity(\n qdrantUrl + \"/collections/\" + collectionName, String.class);\n if (!resp.getStatusCode().is2xxSuccessful() || resp.getBody() == null) return -1;\n JsonNode root = objectMapper.readTree(resp.getBody());\n return root.path(\"result\").path(\"vectors_count\").asLong(-1);\n } catch (Exception e) {\n return -1;\n }\n }\n\n /**\n * 按租户 ID 批量删除向量(清理离职租户或冷数据)。\n * 使用 Qdrant Payload Filter 删除;单次最多清理 10000 条。\n * @return 实际删除条数(Qdrant 返回 operation_id,无法精确计数时返回 0)\n */\n public int deleteVectorsByTenant(Long tenantId) {\n try {\n ObjectNode body = objectMapper.createObjectNode();\n ObjectNode filter = body.putObject(\"filter\");\n ArrayNode must = filter.putArray(\"must\");\n ObjectNode cond = must.addObject();\n cond.put(\"key\", \"tenant_id\");\n cond.putObject(\"match\").put(\"integer\", tenantId);\n\n String url = qdrantUrl + \"/collections/\" + collectionName + \"/points/delete\";\n restTemplate.exchange(url, HttpMethod.POST, jsonEntity(body.toString()), String.class);\n log.info(\"[Qdrant] 已触发租户向量删除 tenantId={}\", tenantId);\n return 0; // Qdrant filter-delete 不返回精确条数\n } catch (Exception e) {\n log.warn(\"[Qdrant] 租户向量删除失败 tenantId={}: {}\", tenantId, e.getMessage());\n return -1;\n }\n }\n\n // ──────────────────────────────────────────────────────────────\n // 内部工具\n // ──────────────────────────────────────────────────────────────\n\n private void ensureCollectionExists() {\n if (collectionVerified.get()) return;\n try {\n restTemplate.getForEntity(\n qdrantUrl + \"/collections/\" + collectionName, String.class);\n collectionVerified.set(true);\n } catch (Exception e) {\n try {\n ObjectNode body = objectMapper.createObjectNode();\n ObjectNode params = body.putObject(\"vectors\");\n params.put(\"size\", getVectorDim());\n params.put(\"distance\", \"Cosine\");\n // 添加稀疏向量支持\n ObjectNode sparseVectors = body.putObject(\"sparse_vectors\");\n sparseVectors.putObject(SPARSE_VECTOR_NAME);\n restTemplate.postForEntity(\n qdrantUrl + \"/collections/\" + collectionName,\n jsonEntity(body.toString()), String.class);\n log.info(\"[Qdrant] 集合 {} 已自动创建(含稀疏向量支持)\", collectionName);\n collectionVerified.set(true);\n } catch (Exception ex) {\n // 稀疏向量配置可能不被旧版 Qdrant 支持,尝试不带稀疏向量创建\n log.warn(\"[Qdrant] 集合创建(含稀疏向量)失败,尝试不带稀疏向量创建: {}\", ex.getMessage());\n try {\n ObjectNode body2 = objectMapper.createObjectNode();\n ObjectNode params2 = body2.putObject(\"vectors\");\n params2.put(\"size\", getVectorDim());\n params2.put(\"distance\", \"Cosine\");\n restTemplate.postForEntity(\n qdrantUrl + \"/collections/\" + collectionName,\n jsonEntity(body2.toString()), String.class);\n log.info(\"[Qdrant] 集合 {} 已自动创建(不含稀疏向量,旧版Qdrant)\", collectionName);\n collectionVerified.set(true);\n hybridSearchDegraded.set(true);\n } catch (Exception ex2) {\n log.warn(\"[Qdrant] 集合创建失败: {}\", ex2.getMessage());\n }\n }\n }\n }\n\n /**\n * 生成语义向量:优先 Voyage AI → DeepSeek → 伪向量(逐级降级)。\n * F5: 空文本返回 null(零向量的余弦相似度未定义,会产生无意义匹配结果)。\n * @return 向量数组;null 表示输入为空无法生成有效向量\n */\n private float[] computeEmbedding(String text) {\n if (text == null || text.isBlank()) {\n return null;\n }\n String activeProvider = resolveActiveProvider();\n String cacheKey = sha256Hex(text) + \":\" + activeProvider;\n EmbeddingCacheEntry cached = embeddingCache.get(cacheKey);\n if (cached != null && !cached.isExpired()) {\n return cached.vector;\n }\n if (PROVIDER_AGNES.equals(activeProvider)) {\n try {\n float[] vector = tryAgnesEmbedding(text);\n if (vector != null) {\n embeddingCache.put(cacheKey, new EmbeddingCacheEntry(vector, PROVIDER_AGNES));\n evictCacheIfNeeded();\n return vector;\n }\n } catch (Exception e) {\n log.warn(\"[Agnes] Embedding 调用失败,降级到 DeepSeek: {}\", e.getMessage());\n }\n }\n if (PROVIDER_DEEPSEEK.equals(activeProvider)) {\n try {\n float[] vector = callEmbeddingApi(text);\n embeddingCache.put(cacheKey, new EmbeddingCacheEntry(vector, PROVIDER_DEEPSEEK));\n evictCacheIfNeeded();\n return vector;\n } catch (Exception e) {\n log.warn(\"[Qdrant] DeepSeek Embedding API 调用失败,降级为伪向量: {}\", e.getMessage());\n }\n }\n return pseudoEmbedding(text);\n }\n\n private String resolveActiveProvider() {\n if (agnesApiKey != null && !agnesApiKey.isEmpty()) return PROVIDER_AGNES;\n if (deepseekApiKey != null && !deepseekApiKey.isEmpty()) return PROVIDER_DEEPSEEK;\n return \"pseudo\";\n }\n\n private void evictCacheIfNeeded() {\n if (embeddingCache.size() > EMBEDDING_CACHE_MAX) {\n embeddingCache.entrySet().removeIf(e -> e.getValue().isExpired());\n }\n }\n\n /**\n * 调用 DeepSeek Embedding API 获取真实语义向量。\n */\n private float[] callEmbeddingApi(String text) {\n String url = deepseekBaseUrl + \"/v1/embeddings\";\n ObjectNode body = objectMapper.createObjectNode();\n body.put(\"model\", embeddingModel);\n body.put(\"input\", text);\n body.put(\"encoding_format\", \"float\");\n\n HttpHeaders headers = new HttpHeaders();\n headers.setContentType(MediaType.APPLICATION_JSON);\n headers.setBearerAuth(deepseekApiKey);\n HttpEntity<String> entity = new HttpEntity<>(body.toString(), headers);\n\n ResponseEntity<String> resp = restTemplate.postForEntity(url, entity, String.class);\n if (resp.getStatusCode().is2xxSuccessful() && resp.getBody() != null) {\n JsonNode root;\n try {\n root = objectMapper.readTree(resp.getBody());\n } catch (Exception e) {\n throw new RuntimeException(\"Embedding response parse failed\", e);\n }\n JsonNode embedding = root.path(\"data\").path(0).path(\"embedding\");\n if (embedding.isArray()) {\n float[] vec = new float[embedding.size()];\n for (int i = 0; i < embedding.size(); i++) {\n vec[i] = (float) embedding.get(i).asDouble();\n }\n log.debug(\"[Qdrant] 真实语义向量生成成功,维度={}\", vec.length);\n return vec;\n }\n }\n throw new RuntimeException(\"Embedding API returned unexpected response\");\n }\n\n /** 获取当前使用的向量维度(agnes/deepseek 均为 1024 维,伪向量 128 维) */\n private int getVectorDim() {\n boolean hasRealKey = (agnesApiKey != null && !agnesApiKey.isEmpty())\n || (deepseekApiKey != null && !deepseekApiKey.isEmpty());\n return hasRealKey ? VECTOR_DIM_REAL : VECTOR_DIM_PSEUDO;\n }\n\n /**\n * 公开诊断方法 — 返回 Agnes/DeepSeek 配置状态,用于 /visual/diag 端点排查问题\n */\n public Map<String, Object> getVectorDimInfo() {\n Map<String, Object> info = new java.util.LinkedHashMap<>();\n boolean hasAgnes = agnesApiKey != null && !agnesApiKey.isEmpty();\n boolean hasDeepSeek = deepseekApiKey != null && !deepseekApiKey.isEmpty();\n boolean hasInferenceOrch = inferenceOrchestrator != null;\n info.put(\"hasAgnesKey\", hasAgnes);\n info.put(\"agnesModel\", agnesModel);\n info.put(\"agnesApiUrl\", agnesApiUrl);\n info.put(\"hasDeepSeekKey\", hasDeepSeek);\n info.put(\"deepseekBaseUrl\", deepseekBaseUrl);\n info.put(\"deepseekEmbeddingModel\", embeddingModel);\n info.put(\"hasInferenceOrchestrator\", hasInferenceOrch);\n info.put(\"currentVectorDim\", getVectorDim());\n info.put(\"realVectorDim\", VECTOR_DIM_REAL);\n info.put(\"pseudoVectorDim\", VECTOR_DIM_PSEUDO);\n if (hasAgnes && hasInferenceOrch) {\n info.put(\"recommendedMode\", \"agnes_vision_and_embedding (最佳质量)\");\n } else if (hasDeepSeek) {\n info.put(\"recommendedMode\", \"deepseek_text_embedding (基础质量)\");\n } else {\n info.put(\"recommendedMode\", \"pseudo_hashing (最低质量,不建议生产)\");\n }\n return info;\n }\n\n /**\n * 伪向量生成(基于字符哈希),仅在 Embedding API 不可用时降级使用。\n */\n private float[] pseudoEmbedding(String text) {\n float[] vec = new float[VECTOR_DIM_PSEUDO];\n if (text == null || text.isEmpty()) return vec;\n // 对字符串字符进行分组哈希,映射到 [-1, 1]\n for (int i = 0; i < text.length(); i++) {\n int idx = (text.charAt(i) * 31 + i) % VECTOR_DIM_PSEUDO;\n if (idx < 0) idx += VECTOR_DIM_PSEUDO;\n vec[idx] += (float) Math.sin(text.charAt(i) * 0.1);\n }\n // 归一化\n float norm = 0;\n for (float v : vec) norm += v * v;\n norm = (float) Math.sqrt(norm);\n if (norm > 0) {\n for (int i = 0; i < vec.length; i++) vec[i] /= norm;\n }\n return vec;\n }\n\n private HttpEntity<String> jsonEntity(String json) {\n HttpHeaders headers = new HttpHeaders();\n headers.setContentType(MediaType.APPLICATION_JSON);\n return new HttpEntity<>(json, headers);\n }\n\n // ──────────────────────────────────────────────────────────────\n // 款式图多模态接口\n // ──────────────────────────────────────────────────────────────\n\n /**\n * 对款式封面图生成语义向量,用于以图搜款和难度评估。\n *\n * <p>向量生成优先级(尽量只用 AGNES_API_KEY 一个 Key):\n * 1. Agnes 视觉分析 + Agnes Embedding(图片→描述→向量,只需 AGNES_API_KEY)\n * 2. Agnes 视觉分析 + DeepSeek Embedding(图片→描述→向量,需两个 Key)\n * 3. DeepSeek 纯文本 Embedding(用图片 URL 文本生成向量,质量一般)\n * 4. 伪向量(哈希)— 最低质量,仅兜底\n */\n public float[] computeMultimodalEmbedding(String imageUrl) {\n if (!qdrantEnabled) return null;\n if (imageUrl == null || imageUrl.isBlank()) {\n throw new IllegalArgumentException(\"imageUrl 不能为空\");\n }\n\n // ========== 运行时诊断:打印当前配置状态 ==========\n boolean hasAgnesKey = agnesApiKey != null && !agnesApiKey.isEmpty();\n boolean hasDeepSeekKey = deepseekApiKey != null && !deepseekApiKey.isEmpty();\n boolean hasInferenceOrch = inferenceOrchestrator != null;\n log.info(\"[Qdrant] 向量生成启动 imageUrlLen={} hasAgnesKey={} hasDeepSeekKey={} hasInferenceOrch={}\",\n imageUrl.length(), hasAgnesKey, hasDeepSeekKey, hasInferenceOrch);\n\n // ========== 第 1 级:Agnes 视觉分析 → 文字描述 → Embedding 向量 ==========\n if (hasAgnesKey && hasInferenceOrch) {\n try {\n log.info(\"[Qdrant] 尝试方案1: Agnes视觉描述 + Agnes Embedding (模型={})\", agnesModel);\n String visualDescription = describeImageWithAgnes(imageUrl);\n if (visualDescription != null && !visualDescription.isBlank()) {\n log.info(\"[Qdrant] Agnes视觉描述成功 descLen={}\", visualDescription.length());\n // 方案1a: 尝试用 Agnes Embedding\n float[] vec = tryAgnesEmbedding(visualDescription);\n if (vec != null) {\n log.info(\"[Qdrant] ✓ 方案1成功 Agnes视觉+Agnes Embedding 维度={}\", vec.length);\n return vec;\n }\n // 方案1b: Agnes Embedding 不支持,降级到 DeepSeek Embedding\n if (hasDeepSeekKey) {\n vec = callEmbeddingApi(visualDescription);\n log.info(\"[Qdrant] ✓ 方案2成功 Agnes视觉+DeepSeek Embedding 维度={}\", vec.length);\n return vec;\n }\n // 方案1c: 只有 Agnes Key,用 Agnes Embedding 重试一次(兜底特殊路径)\n log.warn(\"[Qdrant] 已获取视觉描述但无可用 Embedding endpoint,继续降级\");\n } else {\n log.warn(\"[Qdrant] Agnes视觉描述返回空\");\n }\n } catch (Exception e) {\n log.warn(\"[Qdrant] Agnes视觉+Embedding 失败: {}\", e.getMessage());\n }\n } else {\n log.warn(\"[Qdrant] 跳过 Agnes 链路: hasAgnesKey={} hasInferenceOrch={}\", hasAgnesKey, hasInferenceOrch);\n }\n\n // ========== 第 2 级:DeepSeek 纯文本 Embedding(用图片 URL 文本生成向量) ==========\n if (hasDeepSeekKey) {\n log.info(\"[Qdrant] 尝试方案3: DeepSeek 文本 Embedding(用 imageUrl 文本)\");\n try {\n float[] vec = callEmbeddingApi(imageUrl);\n log.info(\"[Qdrant] ✓ 方案3成功 DeepSeek 文本 Embedding 维度={}\", vec.length);\n return vec;\n } catch (Exception e) {\n log.warn(\"[Qdrant] DeepSeek Embedding 失败: {}\", e.getMessage());\n }\n }\n\n // ========== 第 3 级:伪向量兜底 ==========\n log.error(\"[Qdrant] ⚠ 所有 Embedding 方案不可用,降级为伪向量(搜索质量极低)!\" +\n \"建议:1)在微信云【环境变量】配置 AGNES_API_KEY 以启用视觉分析;\" +\n \"2)或配置 DEEPSEEK_API_KEY 启用文本 Embedding\");\n return pseudoEmbedding(imageUrl);\n }\n\n /**\n * 尝试调用 Agnes 的 /v1/embeddings 端点。\n * Agnes 声称 OpenAI 兼容,如果支持 embeddings 端点,则只需一个 API Key。\n * 如果不支持,返回 null,由调用方降级到 DeepSeek。\n */\n private float[] tryAgnesEmbedding(String text) {\n try {\n String agnesBaseUrl = agnesApiUrl.replace(\"/chat/completions\", \"\");\n String url = agnesBaseUrl + \"/embeddings\";\n ObjectNode body = objectMapper.createObjectNode();\n body.put(\"model\", agnesModel);\n body.put(\"input\", text);\n body.put(\"encoding_format\", \"float\");\n\n HttpHeaders headers = new HttpHeaders();\n headers.setContentType(MediaType.APPLICATION_JSON);\n headers.setBearerAuth(agnesApiKey);\n HttpEntity<String> entity = new HttpEntity<>(body.toString(), headers);\n\n ResponseEntity<String> resp = restTemplate.postForEntity(url, entity, String.class);\n if (resp.getStatusCode().is2xxSuccessful() && resp.getBody() != null) {\n JsonNode root = objectMapper.readTree(resp.getBody());\n JsonNode embedding = root.path(\"data\").path(0).path(\"embedding\");\n if (embedding.isArray() && embedding.size() > 0) {\n float[] vec = new float[embedding.size()];\n for (int i = 0; i < embedding.size(); i++) {\n vec[i] = (float) embedding.get(i).asDouble();\n }\n log.info(\"[Qdrant] Agnes Embedding 端点可用!维度={}\", vec.length);\n return vec;\n }\n }\n log.debug(\"[Qdrant] Agnes Embedding 端点不可用,降级到 DeepSeek\");\n } catch (Exception e) {\n log.debug(\"[Qdrant] Agnes Embedding 端点不可用: {}\", e.getMessage());\n }\n return null;\n }\n\n /**\n * 用 Agnes 视觉模型对图片生成文字描述,用于后续 Embedding。\n * 这比直接用图片 URL 做 Embedding 质量高得多,因为 Agnes 能理解图片内容。\n */\n private String describeImageWithAgnes(String imageUrl) {\n try {\n String desc = inferenceOrchestrator.chatWithVision(imageUrl,\n \"请用50字以内简洁描述这件服装的款式特征(领型、袖型、版型、面料质感、装饰工艺),\"\n + \"仅描述可见的工艺特征,不评价颜色和风格。\");\n if (desc != null && !desc.isBlank()) {\n return desc.length() > 200 ? desc.substring(0, 200) : desc;\n }\n } catch (Exception e) {\n log.debug(\"[Qdrant] Agnes 视觉描述失败: {}\", e.getMessage());\n }\n return null;\n }\n\n /**\n * 将款式图片向量存入 style_images 集合,供后续相似款式搜索。\n */\n public boolean upsertStyleImageVector(Long styleId, String styleNo, float[] embedding,\n String difficultyLevel, int difficultyScore,\n Long tenantId) {\n try {\n ensureStyleImageCollectionExists();\n ObjectNode body = objectMapper.createObjectNode();\n ArrayNode points = body.putArray(\"points\");\n ObjectNode point = points.addObject();\n point.put(\"id\", styleId);\n ArrayNode vec = point.putArray(\"vector\");\n for (float v : embedding) vec.add(v);\n ObjectNode payloadNode = point.putObject(\"payload\");\n payloadNode.put(\"style_no\", styleNo != null ? styleNo : \"\");\n payloadNode.put(\"difficulty_level\", difficultyLevel != null ? difficultyLevel : \"MEDIUM\");\n payloadNode.put(\"difficulty_score\", difficultyScore);\n payloadNode.put(\"tenant_id\", tenantId != null ? tenantId : 0L);\n String url = qdrantUrl + \"/collections/\" + STYLE_IMAGE_COLLECTION + \"/points\";\n ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.PUT,\n jsonEntity(body.toString()), String.class);\n return resp.getStatusCode().is2xxSuccessful();\n } catch (Exception e) {\n log.warn(\"[Qdrant] style_images upsert失败 styleId={}: {}\", styleId, e.getMessage());\n return false;\n }\n }\n\n /**\n * 搜索视觉相似的历史款式(仅在 style_images 集合中检索)。\n */\n public List<SimilarStyle> searchSimilarStyleImages(float[] embedding, int topK, Long tenantId) {\n if (!qdrantEnabled) return Collections.emptyList();\n List<SimilarStyle> results = new ArrayList<>();\n try {\n ObjectNode body = objectMapper.createObjectNode();\n ArrayNode vec = body.putArray(\"vector\");\n for (float v : embedding) vec.add(v);\n body.put(\"limit\", topK);\n body.put(\"with_payload\", true);\n // 租户隔离过滤(tenantId 为 null 时拒绝搜索,防止跨租户数据泄漏)\n if (tenantId == null) {\n log.warn(\"[Qdrant] style_images search拒绝: tenantId为null,跳过搜索以防止跨租户数据泄漏\");\n return results;\n }\n ObjectNode filter = body.putObject(\"filter\");\n ArrayNode should = filter.putArray(\"should\");\n ObjectNode tenantCond = should.addObject();\n tenantCond.put(\"key\", \"tenant_id\");\n tenantCond.putObject(\"match\").put(\"integer\", tenantId);\n ObjectNode publicCond = should.addObject();\n publicCond.put(\"key\", \"tenant_id\");\n publicCond.putObject(\"match\").put(\"integer\", 0);\n String url = qdrantUrl + \"/collections/\" + STYLE_IMAGE_COLLECTION + \"/points/search\";\n ResponseEntity<String> resp = restTemplate.postForEntity(url, jsonEntity(body.toString()), String.class);\n if (resp.getStatusCode().is2xxSuccessful() && resp.getBody() != null) {\n JsonNode root = objectMapper.readTree(resp.getBody());\n for (JsonNode item : root.path(\"result\")) {\n SimilarStyle ss = new SimilarStyle();\n ss.setStyleNo(item.path(\"payload\").path(\"style_no\").asText(\"\"));\n ss.setDifficultyLevel(item.path(\"payload\").path(\"difficulty_level\").asText(\"MEDIUM\"));\n ss.setDifficultyScore(item.path(\"payload\").path(\"difficulty_score\").asInt(5));\n ss.setSimilarity((float) item.path(\"score\").asDouble());\n if (ss.getSimilarity() > 0.1f) {\n results.add(ss);\n }\n }\n }\n } catch (Exception e) {\n log.warn(\"[Qdrant] style_images search失败: {}\", e.getMessage());\n }\n return results;\n }\n\n private void ensureStyleImageCollectionExists() {\n if (styleImageCollectionVerified.get()) return;\n try {\n ResponseEntity<String> r = restTemplate.getForEntity(\n qdrantUrl + \"/collections/\" + STYLE_IMAGE_COLLECTION, String.class);\n if (r.getStatusCode().is2xxSuccessful()) {\n styleImageCollectionVerified.set(true);\n return;\n }\n } catch (Exception e) { log.debug(\"Non-critical error: {}\", e.getMessage()); }\n try {\n ObjectNode body = objectMapper.createObjectNode();\n ObjectNode params = body.putObject(\"vectors\");\n params.put(\"size\", VECTOR_DIM_REAL);\n params.put(\"distance\", \"Cosine\");\n restTemplate.postForEntity(\n qdrantUrl + \"/collections/\" + STYLE_IMAGE_COLLECTION,\n jsonEntity(body.toString()), String.class);\n log.info(\"[Qdrant] 集合 {} 已自动创建\", STYLE_IMAGE_COLLECTION);\n styleImageCollectionVerified.set(true);\n } catch (Exception ex) {\n log.warn(\"[Qdrant] style_images集合创建失败: {}\", ex.getMessage());\n }\n }\n\n public int backfillStyleImageTenantIds(java.util.Map<Long, Long> styleIdToTenantId) {\n if (styleIdToTenantId == null || styleIdToTenantId.isEmpty()) return 0;\n int updated = 0;\n try {\n ensureStyleImageCollectionExists();\n ObjectNode body = objectMapper.createObjectNode();\n ArrayNode points = body.putArray(\"points\");\n for (Map.Entry<Long, Long> entry : styleIdToTenantId.entrySet()) {\n Long styleId = entry.getKey();\n Long tenantId = entry.getValue();\n ObjectNode point = points.addObject();\n point.put(\"id\", styleId);\n ObjectNode payload = point.putObject(\"payload\");\n payload.put(\"tenant_id\", tenantId != null ? tenantId : 0L);\n }\n String url = qdrantUrl + \"/collections/\" + STYLE_IMAGE_COLLECTION + \"/points/payload\";\n ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST,\n jsonEntity(body.toString()), String.class);\n if (resp.getStatusCode().is2xxSuccessful()) {\n updated = styleIdToTenantId.size();\n log.info(\"[Qdrant] style_images tenant_id补刷完成, 共{}条\", updated);\n }\n } catch (Exception e) {\n log.warn(\"[Qdrant] style_images tenant_id补刷失败: {}\", e.getMessage());\n }\n return updated;\n }\n\n // ──────────────────────────────────────────────────────────────\n // L5 Archival Memory — 每租户独立 collection,承载 6 个月+ 冷数据\n // 设计参考:five-layer-memory-design.md 第五章\n // 多租户安全:collection 名按 tenantId 隔离;payload 必含 tenant_id(双保险)\n // ──────────────────────────────────────────────────────────────\n\n /** 归档 collection 前缀,最终 collection 名为 archival_memory_{tenantId} */\n private static final String ARCHIVAL_COLLECTION_PREFIX = \"archival_memory_\";\n\n /** 已验证存在的归档 collection 缓存(避免每次查询都检查) */\n private final java.util.Set<Long> archivalCollectionsVerified =\n java.util.Collections.newSetFromMap(new ConcurrentHashMap<>());\n\n /**\n * 获取租户归档 collection 名。\n * @param tenantId 租户ID(必填,符合 P0 铁律 4)\n */\n private String archivalCollectionName(Long tenantId) {\n if (tenantId == null) {\n throw new IllegalArgumentException(\"[Archival] tenantId 不能为空(P0 铁律 4:多租户隔离)\");\n }\n return ARCHIVAL_COLLECTION_PREFIX + tenantId;\n }\n\n /**\n * 确保租户归档 collection 存在(幂等)。\n * 启动时或首次写入前调用。\n * @param tenantId 租户ID\n * @return true=新建了 collection;false=已存在或 Qdrant 不可用\n */\n public synchronized boolean ensureArchivalCollection(Long tenantId) {\n if (!qdrantEnabled) return false;\n if (tenantId == null) return false;\n if (archivalCollectionsVerified.contains(tenantId)) return false;\n\n String collection = archivalCollectionName(tenantId);\n try {\n restTemplate.getForEntity(qdrantUrl + \"/collections/\" + collection, String.class);\n archivalCollectionsVerified.add(tenantId);\n return false; // 已存在\n } catch (Exception e) {\n try {\n ObjectNode body = objectMapper.createObjectNode();\n ObjectNode params = body.putObject(\"vectors\");\n params.put(\"size\", getVectorDim());\n params.put(\"distance\", \"Cosine\");\n restTemplate.postForEntity(\n qdrantUrl + \"/collections/\" + collection,\n jsonEntity(body.toString()), String.class);\n log.info(\"[Archival] 租户 {} 归档 collection 已创建: {}\", tenantId, collection);\n archivalCollectionsVerified.add(tenantId);\n return true;\n } catch (Exception ex) {\n log.warn(\"[Archival] 租户 {} 归档 collection 创建失败: {}\", tenantId, ex.getMessage());\n return false;\n }\n }\n }\n\n /**\n * 写入一条归档记忆(L5)。\n * @param tenantId 租户ID(必填)\n * @param originalId 原表记录ID(t_ai_conversation_memory.id 或 t_ai_long_memory.id)\n * @param memoryType 记忆类型:conversation_summary / long_fact / long_episodic / long_reflective\n * @param summary 摘要文本(用于向量化)\n * @param keyEntities 关键实体 JSON(如 orderNo/styleNo/userId)\n * @param createTime 原记录创建时间(用于时间范围过滤)\n * @return true=成功;false=失败或 Qdrant 不可用\n */\n public boolean upsertArchival(Long tenantId, String originalId, String memoryType,\n String summary, String keyEntities, String createTime) {\n return upsertArchivalTiered(tenantId, originalId, memoryType, summary, keyEntities,\n createTime, null);\n }\n\n /**\n * 写入一条归档记忆(L5,P3-3 分级存储版本)。\n *\n * <p>根据 originalCreateTime 自动计算分级(HOT/WARM/COLD),写入 payload.tier 字段。\n * 召回时 {@link #searchArchivalTiered} 可按 tier 过滤,优先返回 HOT 数据。\n *\n * @param tier 分级(null 时根据 createTime 自动计算;createTime 也为空时默认 HOT)\n * @return true=成功;false=失败或 Qdrant 不可用\n */\n public boolean upsertArchivalTiered(Long tenantId, String originalId, String memoryType,\n String summary, String keyEntities, String createTime,\n com.fashion.supplychain.intelligence.entity.ArchivalTier tier) {\n if (!qdrantEnabled) return false;\n if (tenantId == null || originalId == null) return false;\n if (summary == null || summary.isBlank()) return false;\n\n ensureArchivalCollection(tenantId);\n\n // P3-3:分级计算(优先使用传入 tier,否则根据 createTime 推导)\n com.fashion.supplychain.intelligence.entity.ArchivalTier finalTier = tier;\n if (finalTier == null) {\n java.time.LocalDateTime originalTime = parseCreateTime(createTime);\n finalTier = com.fashion.supplychain.intelligence.entity.ArchivalTier.of(\n originalTime, java.time.LocalDateTime.now());\n }\n\n try {\n float[] vector = computeEmbedding(summary);\n if (vector == null) {\n log.debug(\"[Archival] 向量生成失败,跳过归档 tenantId={} originalId={}\", tenantId, originalId);\n return false;\n }\n\n String pointId = tenantId + \":\" + originalId;\n ObjectNode body = objectMapper.createObjectNode();\n ArrayNode points = body.putArray(\"points\");\n ObjectNode point = points.addObject();\n point.put(\"id\", pointId);\n point.put(\"vector\", toJsonArray(vector));\n\n ObjectNode payload = point.putObject(\"payload\");\n payload.put(\"tenant_id\", tenantId); // P0 铁律 4:payload 必含 tenant_id\n payload.put(\"original_id\", originalId);\n payload.put(\"memory_type\", memoryType != null ? memoryType : \"unknown\");\n payload.put(\"summary\", summary.length() > 1000 ? summary.substring(0, 1000) : summary);\n payload.put(\"key_entities\", keyEntities != null ? keyEntities : \"\");\n payload.put(\"create_time\", createTime != null ? createTime : \"\");\n payload.put(\"archived_at\", System.currentTimeMillis());\n payload.put(\"tier\", finalTier.name()); // P3-3:分级字段\n\n String url = qdrantUrl + \"/collections/\" + archivalCollectionName(tenantId) + \"/points?wait=true\";\n restTemplate.postForEntity(url, jsonEntity(body.toString()), String.class);\n return true;\n } catch (Exception e) {\n log.warn(\"[Archival] 写入归档失败 tenantId={} originalId={}: {}\", tenantId, originalId, e.getMessage());\n return false;\n }\n }\n\n /**\n * P3-3:解析 create_time 字符串为 LocalDateTime(容错处理)。\n */\n private java.time.LocalDateTime parseCreateTime(String createTime) {\n if (createTime == null || createTime.isBlank()) return null;\n try {\n return java.time.LocalDateTime.parse(createTime);\n } catch (Exception e) {\n // 兼容 ISO 末尾带 Z 的格式\n try {\n return java.time.LocalDateTime.parse(createTime.replace(\"Z\", \"\"));\n } catch (Exception ex) {\n return null;\n }\n }\n }\n\n /**\n * 检索归档记忆(L5 召回)。\n * @param tenantId 租户ID(必填,跨租户隔离)\n * @param queryText 查询文本\n * @param topK 返回条数(默认 5)\n * @param startTimeIso 可选起始时间(ISO 格式,如 \"2026-01-01T00:00:00\"),null 不过滤\n * @param endTimeIso 可选结束时间,null 不过滤\n * @return 命中结果列表(按相似度降序);空列表表示无结果或 Qdrant 不可用\n */\n public List<ScoredPoint> searchArchival(Long tenantId, String queryText, int topK,\n String startTimeIso, String endTimeIso) {\n if (!qdrantEnabled) return List.of();\n if (tenantId == null || queryText == null || queryText.isBlank()) return List.of();\n if (topK <= 0 || topK > 50) topK = 5;\n\n ensureArchivalCollection(tenantId);\n\n try {\n float[] vector = computeEmbedding(queryText);\n if (vector == null) return List.of();\n\n ObjectNode body = objectMapper.createObjectNode();\n body.put(\"vector\", toJsonArray(vector));\n body.put(\"limit\", topK);\n body.put(\"with_payload\", true);\n\n // 必带 tenant_id 过滤(双保险,即使 collection 名已含 tenantId)\n ObjectNode filter = body.putObject(\"filter\");\n ArrayNode must = filter.putArray(\"must\");\n ObjectNode tenantCond = must.addObject();\n tenantCond.put(\"key\", \"tenant_id\");\n tenantCond.putObject(\"match\").put(\"integer\", tenantId);\n\n // 时间范围过滤(可选)\n if (startTimeIso != null && !startTimeIso.isBlank()\n && endTimeIso != null && !endTimeIso.isBlank()) {\n ObjectNode rangeCond = must.addObject();\n rangeCond.put(\"key\", \"create_time\");\n ObjectNode range = rangeCond.putObject(\"range\");\n range.put(\"gte\", startTimeIso);\n range.put(\"lte\", endTimeIso);\n }\n\n String url = qdrantUrl + \"/collections/\" + archivalCollectionName(tenantId) + \"/points/search\";\n ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, jsonEntity(body.toString()), String.class);\n if (!resp.getStatusCode().is2xxSuccessful() || resp.getBody() == null) return List.of();\n\n JsonNode root = objectMapper.readTree(resp.getBody());\n JsonNode resultArr = root.path(\"result\");\n if (!resultArr.isArray()) return List.of();\n\n List<ScoredPoint> results = new ArrayList<>();\n for (JsonNode item : resultArr) {\n ScoredPoint sp = new ScoredPoint();\n sp.setPointId(item.path(\"id\").asText());\n sp.setScore((float) item.path(\"score\").asDouble());\n sp.setPayload(readPayload(item.path(\"payload\")));\n results.add(sp);\n }\n return results;\n } catch (Exception e) {\n log.debug(\"[Archival] 检索归档失败 tenantId={}: {}\", tenantId, e.getMessage());\n return List.of();\n }\n }\n\n /**\n * P3-3:分级召回策略 — 默认只搜 HOT 层;HOT 不足时扩展到 WARM;全量查询时搜全部。\n *\n * <p>策略说明:\n * <ul>\n * <li>{@code tierFilter=null}:全量搜索(HOT+WARM+COLD),用于明确历史查询</li>\n * <li>{@code tierFilter=[HOT]}:仅搜 HOT(默认场景,速度快)</li>\n * <li>{@code tierFilter=[HOT, WARM]}:HOT 不足时扩展(兜底场景)</li>\n * </ul>\n *\n * @param tierFilter 分级过滤(null 表示全量搜索)\n */\n public List<ScoredPoint> searchArchivalTiered(Long tenantId, String queryText, int topK,\n String startTimeIso, String endTimeIso,\n java.util.List<com.fashion.supplychain.intelligence.entity.ArchivalTier> tierFilter) {\n if (!qdrantEnabled) return List.of();\n if (tenantId == null || queryText == null || queryText.isBlank()) return List.of();\n if (topK <= 0 || topK > 50) topK = 5;\n\n ensureArchivalCollection(tenantId);\n\n try {\n float[] vector = computeEmbedding(queryText);\n if (vector == null) return List.of();\n\n ObjectNode body = objectMapper.createObjectNode();\n body.put(\"vector\", toJsonArray(vector));\n body.put(\"limit\", topK);\n body.put(\"with_payload\", true);\n\n // 必带 tenant_id 过滤(双保险)\n ObjectNode filter = body.putObject(\"filter\");\n ArrayNode must = filter.putArray(\"must\");\n ObjectNode tenantCond = must.addObject();\n tenantCond.put(\"key\", \"tenant_id\");\n tenantCond.putObject(\"match\").put(\"integer\", tenantId);\n\n // P3-3:分级过滤\n if (tierFilter != null && !tierFilter.isEmpty()) {\n ObjectNode tierCond = must.addObject();\n tierCond.put(\"key\", \"tier\");\n ObjectNode matchAny = tierCond.putObject(\"match\");\n ArrayNode any = matchAny.putArray(\"any\");\n for (com.fashion.supplychain.intelligence.entity.ArchivalTier t : tierFilter) {\n any.add(t.name());\n }\n }\n\n // 时间范围过滤(可选)\n if (startTimeIso != null && !startTimeIso.isBlank()\n && endTimeIso != null && !endTimeIso.isBlank()) {\n ObjectNode rangeCond = must.addObject();\n rangeCond.put(\"key\", \"create_time\");\n ObjectNode range = rangeCond.putObject(\"range\");\n range.put(\"gte\", startTimeIso);\n range.put(\"lte\", endTimeIso);\n }\n\n String url = qdrantUrl + \"/collections/\" + archivalCollectionName(tenantId) + \"/points/search\";\n ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST,\n jsonEntity(body.toString()), String.class);\n if (!resp.getStatusCode().is2xxSuccessful() || resp.getBody() == null) return List.of();\n\n JsonNode root = objectMapper.readTree(resp.getBody());\n JsonNode resultArr = root.path(\"result\");\n if (!resultArr.isArray()) return List.of();\n\n List<ScoredPoint> results = new ArrayList<>();\n for (JsonNode item : resultArr) {\n ScoredPoint sp = new ScoredPoint();\n sp.setPointId(item.path(\"id\").asText());\n sp.setScore((float) item.path(\"score\").asDouble());\n sp.setPayload(readPayload(item.path(\"payload\")));\n results.add(sp);\n }\n return results;\n } catch (Exception e) {\n log.debug(\"[Archival] 分级检索失败 tenantId={}: {}\", tenantId, e.getMessage());\n return List.of();\n }\n }\n\n /**\n * P3-3:分级召回 — 智能扩展策略。\n *\n * <p>调用流程:\n * <ol>\n * <li>先搜 HOT 层(topK)</li>\n * <li>若 HOT 结果不足 half(topK/2),扩展到 HOT+WARM</li>\n * <li>若仍不足且 includeCold=true,扩展到全部 tier</li>\n * </ol>\n *\n * @param includeCold 是否最终兜底到 COLD 层\n */\n public List<ScoredPoint> searchArchivalSmart(Long tenantId, String queryText, int topK,\n String startTimeIso, String endTimeIso,\n boolean includeCold) {\n if (!qdrantEnabled) return List.of();\n if (tenantId == null || queryText == null || queryText.isBlank()) return List.of();\n if (topK <= 0 || topK > 50) topK = 5;\n\n // 第1轮:仅 HOT\n List<ScoredPoint> hotResults = searchArchivalTiered(tenantId, queryText, topK,\n startTimeIso, endTimeIso, List.of(com.fashion.supplychain.intelligence.entity.ArchivalTier.HOT));\n if (hotResults.size() >= topK) {\n return hotResults;\n }\n\n // 第2轮:HOT + WARM\n int half = Math.max(1, topK / 2);\n if (hotResults.size() < half) {\n List<ScoredPoint> warmResults = searchArchivalTiered(tenantId, queryText, topK,\n startTimeIso, endTimeIso,\n List.of(com.fashion.supplychain.intelligence.entity.ArchivalTier.HOT,\n com.fashion.supplychain.intelligence.entity.ArchivalTier.WARM));\n if (warmResults.size() >= topK || !includeCold) {\n return warmResults;\n }\n\n // 第3轮:全量(HOT+WARM+COLD)\n if (warmResults.size() < half) {\n return searchArchivalTiered(tenantId, queryText, topK,\n startTimeIso, endTimeIso,\n List.of(com.fashion.supplychain.intelligence.entity.ArchivalTier.HOT,\n com.fashion.supplychain.intelligence.entity.ArchivalTier.WARM,\n com.fashion.supplychain.intelligence.entity.ArchivalTier.COLD));\n }\n }\n\n return hotResults;\n }\n\n /**\n * P3-3:统计租户归档 collection 的分级分布。\n *\n * @return Map:tier 名 → 计数;空 Map 表示 Qdrant 不可用或 collection 不存在\n */\n public java.util.Map<String, Long> countArchivalByTier(Long tenantId) {\n java.util.Map<String, Long> result = new java.util.LinkedHashMap<>();\n if (!qdrantEnabled) return result;\n if (tenantId == null) return result;\n\n ensureArchivalCollection(tenantId);\n\n try {\n for (com.fashion.supplychain.intelligence.entity.ArchivalTier tier :\n com.fashion.supplychain.intelligence.entity.ArchivalTier.values()) {\n ObjectNode body = objectMapper.createObjectNode();\n body.put(\"exact\", true);\n ObjectNode filter = body.putObject(\"filter\");\n ArrayNode must = filter.putArray(\"must\");\n\n ObjectNode tenantCond = must.addObject();\n tenantCond.put(\"key\", \"tenant_id\");\n tenantCond.putObject(\"match\").put(\"integer\", tenantId);\n\n ObjectNode tierCond = must.addObject();\n tierCond.put(\"key\", \"tier\");\n tierCond.putObject(\"match\").put(\"keyword\", tier.name());\n\n String url = qdrantUrl + \"/collections/\" + archivalCollectionName(tenantId)\n + \"/points/count\";\n ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST,\n jsonEntity(body.toString()), String.class);\n if (resp.getStatusCode().is2xxSuccessful() && resp.getBody() != null) {\n JsonNode root = objectMapper.readTree(resp.getBody());\n long count = root.path(\"result\").path(\"count\").asLong(0);\n result.put(tier.name(), count);\n } else {\n result.put(tier.name(), 0L);\n }\n }\n } catch (Exception e) {\n log.debug(\"[Archival] 分级统计失败 tenantId={}: {}\", tenantId, e.getMessage());\n }\n return result;\n }\n\n /**\n * 删除租户归档 collection(用于租户注销/数据清理)。\n * @return true=成功;false=失败或 Qdrant 不可用\n */\n public boolean deleteArchivalCollection(Long tenantId) {\n if (!qdrantEnabled) return false;\n if (tenantId == null) return false;\n try {\n restTemplate.delete(qdrantUrl + \"/collections/\" + archivalCollectionName(tenantId));\n archivalCollectionsVerified.remove(tenantId);\n log.info(\"[Archival] 租户 {} 归档 collection 已删除\", tenantId);\n return true;\n } catch (Exception e) {\n log.warn(\"[Archival] 删除租户 {} 归档 collection 失败: {}\", tenantId, e.getMessage());\n return false;\n }\n }\n\n /** 数组转换辅助 */\n private ArrayNode toJsonArray(float[] vector) {\n ArrayNode arr = objectMapper.createArrayNode();\n for (float v : vector) arr.add(v);\n return arr;\n }\n\n // ──────────────────────────────────────────────────────────────\n // 内嵌 DTO\n // ──────────────────────────────────────────────────────────────\n\n @lombok.Data\n public static class ScoredPoint {\n private String pointId;\n private float score;\n private Map<String, String> payload;\n }\n\n @lombok.Data\n public static class SimilarStyle {\n private String styleNo;\n private String difficultyLevel;\n private int difficultyScore;\n private float similarity;\n }\n}\n"}}}}