{"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 *

原行为:key 为空时启动正常,但所有 AI 功能(对话/视觉/工具调用)都会失败,\n * 用户排查困难(错误散落在各 Service 中)。\n *\n *

现行为:\n *

\n *\n *

受影响功能: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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 *

路由到具体的底层 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 *

根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n *

\n *\n *

★ 接入点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 *

原行为:key 为空时启动正常,但所有 AI 功能(对话/视觉/工具调用)都会失败,\n * 用户排查困难(错误散落在各 Service 中)。\n *\n *

现行为:\n *

\n *\n *

受影响功能: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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 *

路由到具体的底层 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 *

根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n *

\n *\n *

★ 接入点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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 *

路由到具体的底层 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 *

根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n *

\n *\n *

★ 接入点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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 *

路由到具体的底层 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 *

根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n *

\n *\n *

★ 接入点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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 *

路由到具体的底层 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 *

根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n *

\n *\n *

★ 接入点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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 messages, List 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 messages,\n List 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 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 messages, List 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 messages,\n List 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 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 messages, List 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 messages,\n List 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 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 messages, List 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 messages,\n List 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 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 messages, List 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 messages,\n List 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 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 messages, List 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 messages,\n List 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 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 *

原行为:key 为空时启动正常,但所有 AI 功能(对话/视觉/工具调用)都会失败,\n * 用户排查困难(错误散落在各 Service 中)。\n *\n *

现行为:\n *

\n *\n *

受影响功能: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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 *

路由到具体的底层 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 *

根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n *

\n *\n *

★ 接入点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 *

原行为:key 为空时启动正常,但所有 AI 功能(对话/视觉/工具调用)都会失败,\n * 用户排查困难(错误散落在各 Service 中)。\n *\n *

现行为:\n *

\n *\n *

受影响功能: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 messages, List 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 messages,\n List 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 getRoutingStatus() {\n Map 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 *

路由到具体的底层 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 *

根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n *

\n *\n *

★ 接入点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 *

职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。

\n *\n *

默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n *

\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 * 
\n *\n *

若未配置 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 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 *

结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。

\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 *

职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。

\n *\n *

默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n *

\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 * 
\n *\n *

若未配置 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 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 *

结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。

\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 *

职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。

\n *\n *

默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n *

\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 * 
\n *\n *

若未配置 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 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 *

结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。

\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 *

职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。

\n *\n *

默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n *

\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 * 
\n *\n *

若未配置 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 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 *

结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。

\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 *

职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。

\n *\n *

默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n *

\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 * 
\n *\n *

若未配置 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 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 *

结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。

\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 *

职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。

\n *\n *

默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n *

\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 * 
\n *\n *

若未配置 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 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 *

结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。

\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 *

职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。

\n *\n *

默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n *

\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 * 
\n *\n *

若未配置 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 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 *

默认接 DeepSeek,配置方式(环境变量):\n *

\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 * 
\n *\n *

若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n *\n *

切换 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 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 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 *

默认接 DeepSeek,配置方式(环境变量):\n *

\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 * 
\n *\n *

若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n *\n *

切换 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 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 *

职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。

\n *\n *

默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n *

\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 * 
\n *\n *

若未配置 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 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 *

结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。

\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 *

职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。

\n *\n *

默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n *

\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 * 
\n *\n *

若未配置 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 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 *

结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。

\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 *

通过 Qdrant REST API v1.x 实现向量存储与相似检索。\n * 每个租户使用同一个 collection,通过 tenant_id payload 过滤隔离。\n *\n *

向量生成优先级:① Agnes 视觉分析 + Agnes/DeepSeek Embedding(图片→文字描述→向量,推荐)\n * ② DeepSeek Embedding API(text-embedding-v2,用图片URL文本生成向量)\n * ③ 关键词哈希伪向量(pseudoEmbedding,128维,无需 API Key)\n *\n *

配置项(application.yml):\n *

\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 * 
\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 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 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 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 entity = jsonEntity(body.toString());\n ResponseEntity 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 search(Long tenantId, String queryText, int topK) {\n if (!qdrantEnabled) return Collections.emptyList();\n List 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 entity = jsonEntity(body.toString());\n ResponseEntity 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 readPayload(JsonNode payloadNode) {\n if (payloadNode == null || payloadNode.isMissingNode() || payloadNode.isNull()) {\n return Collections.emptyMap();\n }\n Map 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 *

使用 Qdrant 的 /points/query 端点,支持稀疏+稠密混合检索。\n * 如果 Qdrant 不支持混合检索(旧版本),自动降级到纯稠密检索。\n *\n * @param tenantId 租户ID(必须非null,否则拒绝搜索)\n * @param queryText 查询文本\n * @param topK 返回条数\n * @return 匹配点列表(按综合分数降序)\n */\n public List 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 entity = jsonEntity(body.toString());\n ResponseEntity resp = restTemplate.postForEntity(url, entity, String.class);\n\n if (resp.getStatusCode().is2xxSuccessful() && resp.getBody() != null) {\n List 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 indices = new ArrayList<>();\n final List values = new ArrayList<>();\n }\n\n /**\n * BM25风格稀疏向量生成:对查询文本分词,生成 token-id:tf 键值对。\n *\n *

分词策略:中文按双字符滑动窗口 + 单字符,英文按空格分词。\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 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 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 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 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 entity = new HttpEntity<>(body.toString(), headers);\n\n ResponseEntity 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 getVectorDimInfo() {\n Map 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 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 *

向量生成优先级(尽量只用 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 entity = new HttpEntity<>(body.toString(), headers);\n\n ResponseEntity 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 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 searchSimilarStyleImages(float[] embedding, int topK, Long tenantId) {\n if (!qdrantEnabled) return Collections.emptyList();\n List 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 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 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 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 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 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 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 *

根据 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 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 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 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 *

策略说明:\n *

    \n *
  • {@code tierFilter=null}:全量搜索(HOT+WARM+COLD),用于明确历史查询
  • \n *
  • {@code tierFilter=[HOT]}:仅搜 HOT(默认场景,速度快)
  • \n *
  • {@code tierFilter=[HOT, WARM]}:HOT 不足时扩展(兜底场景)
  • \n *
\n *\n * @param tierFilter 分级过滤(null 表示全量搜索)\n */\n public List searchArchivalTiered(Long tenantId, String queryText, int topK,\n String startTimeIso, String endTimeIso,\n java.util.List 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 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 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 *

调用流程:\n *

    \n *
  1. 先搜 HOT 层(topK)
  2. \n *
  3. 若 HOT 结果不足 half(topK/2),扩展到 HOT+WARM
  4. \n *
  5. 若仍不足且 includeCold=true,扩展到全部 tier
  6. \n *
\n *\n * @param includeCold 是否最终兜底到 COLD 层\n */\n public List 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 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 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 countArchivalByTier(Long tenantId) {\n java.util.Map 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 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 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"}}}}