1 line
352 KiB
JSON
1 line
352 KiB
JSON
{"repo": "chenguojun06-star/fz66666", "files": {"cloudbaserc.json": {"92136382831779e22f23b72a08f4e137bfa17dd3": {"t": 1785745313.773528, "n": 3154, "h": "d7faedb3f54e9911", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-backend\",\n \"servicePath\": \"/api\",\n \"localPath\": \"./backend\",\n \"dockerfilePath\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"initialDelaySeconds\": 300,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"AGNES_MODEL\": \"{{AGNES_MODEL:agnes-2.5-flash}}\",\n \"AGNES2_MODEL\": \"{{AGNES2_MODEL:agnes-2.5-flash}}\",\n \"AI_MODEL_VISION\": \"{{AI_MODEL_VISION:agnes-2.5-flash}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-admin\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"dockerfilePath\": \"./frontend/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"envVariables\": {\n \"BACKEND_URL\": \"https://api.webyszl.cn\"\n }\n }\n },\n \"h5\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-h5\",\n \"servicePath\": \"/h5\",\n \"localPath\": \"./h5-web\",\n \"dockerfilePath\": \"./h5-web/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.25,\n \"mem\": 0.5\n }\n }\n }\n }\n}"}, "e3eca03c3b9734ba801c15ce6bafd09fa6b8584a": {"t": 1785749953.5917158, "n": 3154, "h": "d7faedb3f54e9911", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-backend\",\n \"servicePath\": \"/api\",\n \"localPath\": \"./backend\",\n \"dockerfilePath\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"initialDelaySeconds\": 300,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"AGNES_MODEL\": \"{{AGNES_MODEL:agnes-2.5-flash}}\",\n \"AGNES2_MODEL\": \"{{AGNES2_MODEL:agnes-2.5-flash}}\",\n \"AI_MODEL_VISION\": \"{{AI_MODEL_VISION:agnes-2.5-flash}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-admin\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"dockerfilePath\": \"./frontend/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"envVariables\": {\n \"BACKEND_URL\": \"https://api.webyszl.cn\"\n }\n }\n },\n \"h5\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-h5\",\n \"servicePath\": \"/h5\",\n \"localPath\": \"./h5-web\",\n \"dockerfilePath\": \"./h5-web/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.25,\n \"mem\": 0.5\n }\n }\n }\n }\n}"}, "260347e68db7ecd42554f892d362755a81e6df07": {"t": 1785749954.6667893, "n": 2958, "h": "7d5d9fff4061f6be", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-backend\",\n \"servicePath\": \"/api\",\n \"localPath\": \"./backend\",\n \"dockerfilePath\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"initialDelaySeconds\": 300,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-admin\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"dockerfilePath\": \"./frontend/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"envVariables\": {\n \"BACKEND_URL\": \"https://api.webyszl.cn\"\n }\n }\n },\n \"h5\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"fashion-h5\",\n \"servicePath\": \"/h5\",\n \"localPath\": \"./h5-web\",\n \"dockerfilePath\": \"./h5-web/Dockerfile\",\n \"containerPort\": 80,\n \"initialDelaySeconds\": 30,\n \"minNum\": 1,\n \"maxNum\": 3,\n \"cpu\": 0.25,\n \"mem\": 0.5\n }\n }\n }\n }\n}"}, "4d454c0ea46f46c64a4b5e6e9ead7cf0843b7890": {"t": 1785749955.7756293, "n": 2050, "h": "cef5193b6963e416", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"InitialDelaySeconds\": 300,\n \"PeriodSeconds\": 30,\n \"TimeoutSeconds\": 10,\n \"FailureThreshold\": 5,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "9016c9bd9aad954ab25b55fac14ed0295cda5550": {"t": 1785749957.903767, "n": 1966, "h": "6f9aba996428b19a", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"InitialDelaySeconds\": 150,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "49b31a363b2a1b237b97d53d638aad2dbcd3279f": {"t": 1785749959.2831678, "n": 1966, "h": "8b9aaf48e647f684", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"initialDelaySeconds\": 120,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "45a0163991a4d9c2e1add3c194206884f6ecd03a": {"t": 1785749960.358283, "n": 1932, "h": "d8be46df05863e91", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"AGNES_API_KEY\": \"{{AGNES_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "fb7445b2abf78a0442d8fc132ec96513b2399050": {"t": 1785749961.8656561, "n": 1934, "h": "89ed0346fe79a2dd", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\",\n \"AI_IM_AI_FEISHU_ENABLED\": \"true\",\n \"AI_IM_AI_FEISHU_VERIFICATION_TOKEN\": \"dxZCfyFE816hLOrPbpE3pzdHJpmcXAMQ\",\n \"AI_IM_AI_FEISHU_ENCRYPT_KEY\": \"7qZik8wobG3FJVt8rrg97esIHfRRV6dc\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "956c9f0e06bd2fa961a921da326c30029b68938a": {"t": 1785749962.9523864, "n": 1734, "h": "e1f979c607b4c7bb", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\",\n \"AI_OPENAI_COMPAT_ENABLED\": \"{{AI_OPENAI_COMPAT_ENABLED}}\",\n \"AI_OPENAI_COMPAT_API_KEY\": \"{{AI_OPENAI_COMPAT_API_KEY}}\",\n \"AI_OPENAI_COMPAT_TENANT_ID\": \"{{AI_OPENAI_COMPAT_TENANT_ID}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "1ca8b8f7f9cdd4597b35367dcb6be1e89c77b1e4": {"t": 1785749964.0457792, "n": 1526, "h": "b89773a21c9bf00d", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\",\n \"APP_SECURITY_PII_ENCRYPTION_KEY\": \"{{PII_ENCRYPTION_KEY}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "4893a2ef7839c1499308898109716c4f8ca8a213": {"t": 1785749969.022853, "n": 1457, "h": "52700e8c9a1d134d", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_MP_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_MP_SECRET}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "c5ff9274b0de31b599715f7fc2aa76b4bf7f0c74": {"t": 1785749970.1310608, "n": 1355, "h": "327aacc25772a5d6", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\",\n \"DEEPSEEK_API_KEY\": \"{{DEEPSEEK_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"VOYAGE_API_KEY\": \"{{VOYAGE_API_KEY}}\",\n \"QDRANT_URL\": \"{{QDRANT_URL}}\",\n \"QDRANT_API_KEY\": \"{{QDRANT_API_KEY}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "e701be03d01d12a792e286729420a3e2dadf973b": {"t": 1785749971.2254527, "n": 1119, "h": "f94db0707c47bd0e", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"WECHAT_TRUST_ALL_CERTS\": \"true\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "8cf66d2861f0cd29ad6f5cca6aed03fa700d0901": {"t": 1785749972.2951632, "n": 1077, "h": "e8ee40f14345cbb3", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8088,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "8cac5f58a452b3c354e16fc82c389f4912fe2ed5": {"t": 1785749973.5188007, "n": 1077, "h": "b6a839446273c8a3", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8080,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n },\n {\n \"name\": \"fashion-admin\",\n \"localPath\": \"./frontend/dist\",\n \"cloudPath\": \"/\"\n }\n ]\n}\n"}, "65e5b81123568f44fc9098a0c003378f12852fe2": {"t": 1785749974.6060681, "n": 972, "h": "59e63bd1ef0c6267", "c": "{\n \"version\": \"2.0\",\n \"envId\": \"{{TCB_ENV_ID}}\",\n \"services\": [\n {\n \"name\": \"fashion-backend\",\n \"dockerfile\": \"./backend/Dockerfile\",\n \"containerPort\": 8080,\n \"minNum\": 1,\n \"maxNum\": 5,\n \"cpu\": 1,\n \"mem\": 2,\n \"docker\": {\n \"buildType\": \"Dockerfile\",\n \"dockerfilePath\": \"./backend/Dockerfile\"\n },\n \"envVariables\": {\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"APP_DB_HOST\": \"{{DB_HOST}}\",\n \"APP_DB_NAME\": \"{{DB_NAME}}\",\n \"APP_DB_USERNAME\": \"{{DB_USERNAME}}\",\n \"APP_DB_PASSWORD\": \"{{DB_PASSWORD}}\",\n \"APP_COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"APP_COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"APP_COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"APP_COS_REGION\": \"{{COS_REGION}}\"\n }\n }\n ],\n \"static\": [\n {\n \"name\": \"fashion-h5\",\n \"localPath\": \"./h5-web/dist\",\n \"cloudPath\": \"/h5\"\n }\n ]\n}\n"}, "bf0d8e1358f851dea63091cdab4e485562dd8725": {"t": 1785749975.6847663, "n": 2554, "h": "794b87bf8666262b", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_ENABLED\": \"true\",\n \"QWEN_VL_API_KEY\": \"{{QWEN_VL_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"{{DOUBAO_API_KEY}}\",\n \"APP_QRCODE_HMAC_SECRET\": \"{{QRCODE_HMAC_SECRET}}\",\n \"APP_QRCODE_REQUIRE_SIGNATURE\": \"true\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "94f4aebfc1a4ac8252fd599f34112b1d8933c02a": {"t": 1785749976.7376614, "n": 2573, "h": "7937ea1a9bed18ae", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_ENABLED\": \"true\",\n \"QWEN_VL_API_KEY\": \"{{QWEN_VL_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"7a8b0840-0a70-4fc1-a765-bb10c67beb06\",\n \"APP_QRCODE_HMAC_SECRET\": \"{{QRCODE_HMAC_SECRET}}\",\n \"APP_QRCODE_REQUIRE_SIGNATURE\": \"true\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "d74935f72791358c9b608ef2939d28f89c2f7fbd": {"t": 1785749977.7932262, "n": 2457, "h": "0b4e89fe05118e48", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_ENABLED\": \"true\",\n \"QWEN_VL_API_KEY\": \"{{QWEN_VL_API_KEY}}\",\n \"DOUBAO_API_KEY\": \"7a8b0840-0a70-4fc1-a765-bb10c67beb06\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "029844acda5c091b5a594ea030aef7895aa831c6": {"t": 1785749978.875513, "n": 2387, "h": "4b14515dd791616a", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_ENABLED\": \"true\",\n \"QWEN_VL_API_KEY\": \"{{QWEN_VL_API_KEY}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "fe45a6442ae73581b550b593691ff9808801d5a5": {"t": 1785749979.981822, "n": 2333, "h": "365d68f84ced12c2", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_ENABLED\": \"true\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "8bb2076c82e219ecf8dcaa787165a03823b89348": {"t": 1785749981.0676723, "n": 2379, "h": "127277d1ca56b543", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\",\n \"SERPAPI_KEY\": \"{{SERPAPI_KEY}}\",\n \"SERPAPI_ENABLED\": \"true\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "b5a72b5c7060b6722ec3683f93a29968358e4a4b": {"t": 1785749982.104251, "n": 2294, "h": "c4158c5a28591e68", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "7e3b905db3795692be63c5757355ad74956e44be": {"t": 1785749983.2247484, "n": 2353, "h": "c71db57996def376", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"dgfbkffb.my-redis.j6jq9qgw.o7t59w4f.com\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"SPRING_REDIS_PASSWORD\": \"{{REDIS_PASSWORD}}\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "2eb2c092df42834f13ba9748da605aacf77d750a": {"t": 1785749984.2849438, "n": 2269, "h": "bd39884af4741658", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000&tcpKeepAlive=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"{{REDIS_HOST}}\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "ad51ee6c6a3f767ca4208e39dffd66fb54532203": {"t": 1785749985.343798, "n": 2211, "h": "67574c62768cce8f", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"{{REDIS_HOST}}\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"SPRING_PROFILES_ACTIVE\": \"prod\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "601715e5cbc4c33900ddcd2a3df14884eaf406b0": {"t": 1785749986.407472, "n": 2165, "h": "8cf96518164031d4", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"{{REDIS_HOST}}\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health/liveness\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "40ac69d8bb188db9ff3a7e76bcbef748accb06a1": {"t": 1785749987.5192716, "n": 2156, "h": "2bfc268bdbb7fd0f", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"{{REDIS_HOST}}\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\",\n \"COS_SECRET_ID\": \"{{COS_SECRET_ID}}\",\n \"COS_SECRET_KEY\": \"{{COS_SECRET_KEY}}\",\n \"COS_REGION\": \"{{COS_REGION}}\",\n \"COS_BUCKET\": \"{{COS_BUCKET}}\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}, "270bc97bb3d0f62e5eded3e08752b3677c7c64af": {"t": 1785749988.746784, "n": 1966, "h": "c22de3d22a3029a3", "c": "{\n \"envId\": \"{{ENV_ID}}\",\n \"framework\": {\n \"name\": \"fashion-supplychain\",\n \"plugins\": {\n \"backend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"backend\",\n \"servicePath\": \"/backend\",\n \"localPath\": \"./backend\",\n \"containerPort\": 8088,\n \"cpu\": 2,\n \"mem\": 4,\n \"minNum\": 1,\n \"maxNum\": 4,\n \"policyType\": \"cpu\",\n \"policyThreshold\": 60,\n \"envVariables\": {\n \"PORT\": \"8088\",\n \"SPRING_DATASOURCE_URL\": \"jdbc:mysql://{{MYSQL_HOST}}:3306/fashion_supplychain?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true\",\n \"SPRING_DATASOURCE_USERNAME\": \"{{MYSQL_USER}}\",\n \"SPRING_DATASOURCE_PASSWORD\": \"{{MYSQL_PASSWORD}}\",\n \"SPRING_REDIS_HOST\": \"{{REDIS_HOST}}\",\n \"SPRING_REDIS_PORT\": \"6379\",\n \"APP_AUTH_JWT_SECRET\": \"{{JWT_SECRET}}\",\n \"WECHAT_MP_APPID\": \"{{WECHAT_APPID}}\",\n \"WECHAT_MP_SECRET\": \"{{WECHAT_SECRET}}\",\n \"WECHAT_MINI_PROGRAM_MOCK_ENABLED\": \"false\",\n \"FLYWAY_ENABLED\": \"true\",\n \"FASHION_DB_INITIALIZER_ENABLED\": \"false\",\n \"APP_CORS_ALLOWED_ORIGIN_PATTERNS\": \"https://*.tcloudbase.com,https://*.service.tcloudbase.com,https://*.ap-shanghai.service.tcloudbase.com\"\n },\n \"customLogs\": \"stdout\",\n \"healthPath\": \"/actuator/health\"\n }\n },\n \"frontend\": {\n \"use\": \"@cloudbase/framework-plugin-container\",\n \"inputs\": {\n \"serviceName\": \"frontend\",\n \"servicePath\": \"/\",\n \"localPath\": \"./frontend\",\n \"containerPort\": 80,\n \"cpu\": 0.5,\n \"mem\": 1,\n \"minNum\": 1,\n \"maxNum\": 2,\n \"envVariables\": {\n \"BACKEND_URL\": \"http://backend:8088\"\n }\n }\n }\n }\n }\n}\n"}}, "backend/src/main/java/com/fashion/supplychain/intelligence/gateway/AiInferenceRouter.java": {"92136382831779e22f23b72a08f4e137bfa17dd3": {"t": 1785745687.3106582, "n": 20750, "h": "31e72f3762438f68", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY 非空。\n *\n * <p>原行为:key 为空时启动正常,但所有 AI 功能(对话/视觉/工具调用)都会失败,\n * 用户排查困难(错误散落在各 Service 中)。\n *\n * <p>现行为:\n * <ul>\n * <li>fail-fast-on-empty=true(默认 false):启动失败,明确提示缺失 key</li>\n * <li>fail-fast-on-empty=false(默认):启动成功,但打印 ERROR 级别告警 + 列出受影响功能</li>\n * </ul>\n *\n * <p>受影响功能:DeepSeek 对话、Spring AI 工具调用、AiAdvisor 日常建议、\n * SelfCritic 自评分、EvolutionPipeline 自进化、Qdrant 向量检索(embedding 降级)。\n */\n @Value(\"${ai.api-key.fail-fast-on-empty:false}\")\n private boolean failFastOnEmptyApiKey;\n\n @Value(\"${ai.deepseek.api-key:${DEEPSEEK_API_KEY:}}\")\n private String deepseekApiKey;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY。\n * 默认仅告警不阻止启动,开启 fail-fast-on-empty=true 可强制阻止启动。\n */\n @jakarta.annotation.PostConstruct\n public void validateApiKey() {\n boolean keyMissing = deepseekApiKey == null || deepseekApiKey.isBlank();\n if (!keyMissing) {\n log.info(\"[AiInferenceRouter] DEEPSEEK_API_KEY 已配置(长度={}),AI 功能正常可用\",\n deepseekApiKey.length());\n return;\n }\n\n String affectedFeatures = \"DeepSeek 对话 / Spring AI 工具调用 / AiAdvisor 日常建议 / \"\n + \"SelfCritic 自评分 / EvolutionPipeline 自进化 / Qdrant 向量检索(embedding 降级)\";\n\n if (failFastOnEmptyApiKey) {\n String msg = \"[AiInferenceRouter] DEEPSEEK_API_KEY 为空且 ai.api-key.fail-fast-on-empty=true,\"\n + \"应用启动被阻止。受影响功能:\" + affectedFeatures\n + \"。解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\";\n log.error(msg);\n throw new IllegalStateException(msg);\n }\n\n log.error(\"[AiInferenceRouter] ⚠️ DEEPSEEK_API_KEY 为空!AI 功能将全部失败。受影响功能:{}\", affectedFeatures);\n log.error(\"[AiInferenceRouter] 解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\");\n log.error(\"[AiInferenceRouter] 如需启动失败而非降级,设置 ai.api-key.fail-fast-on-empty=true\");\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public boolean isVisionAvailable() {\n if (legacyAdapter != null && legacyAdapter.isVisionAvailable()) {\n return true;\n }\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isVisionAvailable()) {\n return true;\n }\n return false;\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n if (prompt == null || prompt.isBlank()) {\n log.warn(\"[AiInferenceRouter] chatSimple 收到空 prompt,跳过 LLM 调用\");\n return \"\";\n }\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "92b7fd9578d448a1537b900a5ee7fb0da73abc84": {"t": 1785749954.3668191, "n": 20750, "h": "31e72f3762438f68", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY 非空。\n *\n * <p>原行为:key 为空时启动正常,但所有 AI 功能(对话/视觉/工具调用)都会失败,\n * 用户排查困难(错误散落在各 Service 中)。\n *\n * <p>现行为:\n * <ul>\n * <li>fail-fast-on-empty=true(默认 false):启动失败,明确提示缺失 key</li>\n * <li>fail-fast-on-empty=false(默认):启动成功,但打印 ERROR 级别告警 + 列出受影响功能</li>\n * </ul>\n *\n * <p>受影响功能:DeepSeek 对话、Spring AI 工具调用、AiAdvisor 日常建议、\n * SelfCritic 自评分、EvolutionPipeline 自进化、Qdrant 向量检索(embedding 降级)。\n */\n @Value(\"${ai.api-key.fail-fast-on-empty:false}\")\n private boolean failFastOnEmptyApiKey;\n\n @Value(\"${ai.deepseek.api-key:${DEEPSEEK_API_KEY:}}\")\n private String deepseekApiKey;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n /**\n * 【P1-10修复】启动时校验 DEEPSEEK_API_KEY。\n * 默认仅告警不阻止启动,开启 fail-fast-on-empty=true 可强制阻止启动。\n */\n @jakarta.annotation.PostConstruct\n public void validateApiKey() {\n boolean keyMissing = deepseekApiKey == null || deepseekApiKey.isBlank();\n if (!keyMissing) {\n log.info(\"[AiInferenceRouter] DEEPSEEK_API_KEY 已配置(长度={}),AI 功能正常可用\",\n deepseekApiKey.length());\n return;\n }\n\n String affectedFeatures = \"DeepSeek 对话 / Spring AI 工具调用 / AiAdvisor 日常建议 / \"\n + \"SelfCritic 自评分 / EvolutionPipeline 自进化 / Qdrant 向量检索(embedding 降级)\";\n\n if (failFastOnEmptyApiKey) {\n String msg = \"[AiInferenceRouter] DEEPSEEK_API_KEY 为空且 ai.api-key.fail-fast-on-empty=true,\"\n + \"应用启动被阻止。受影响功能:\" + affectedFeatures\n + \"。解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\";\n log.error(msg);\n throw new IllegalStateException(msg);\n }\n\n log.error(\"[AiInferenceRouter] ⚠️ DEEPSEEK_API_KEY 为空!AI 功能将全部失败。受影响功能:{}\", affectedFeatures);\n log.error(\"[AiInferenceRouter] 解决:设置环境变量 DEEPSEEK_API_KEY 或在 yml 配置 ai.deepseek.api-key\");\n log.error(\"[AiInferenceRouter] 如需启动失败而非降级,设置 ai.api-key.fail-fast-on-empty=true\");\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public boolean isVisionAvailable() {\n if (legacyAdapter != null && legacyAdapter.isVisionAvailable()) {\n return true;\n }\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isVisionAvailable()) {\n return true;\n }\n return false;\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n if (prompt == null || prompt.isBlank()) {\n log.warn(\"[AiInferenceRouter] chatSimple 收到空 prompt,跳过 LLM 调用\");\n return \"\";\n }\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "6fe4a586649c80cfffa2ec60afd3daca815705ed": {"t": 1785749955.5275457, "n": 18783, "h": "d1c9ce85273937de", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public boolean isVisionAvailable() {\n if (legacyAdapter != null && legacyAdapter.isVisionAvailable()) {\n return true;\n }\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isVisionAvailable()) {\n return true;\n }\n return false;\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n if (prompt == null || prompt.isBlank()) {\n log.warn(\"[AiInferenceRouter] chatSimple 收到空 prompt,跳过 LLM 调用\");\n return \"\";\n }\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "0fc1de77cd3cb0bcbf5f816d748f22c7ffbf316f": {"t": 1785749956.6754286, "n": 18449, "h": "15ced3ba90c9e71c", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n if (prompt == null || prompt.isBlank()) {\n log.warn(\"[AiInferenceRouter] chatSimple 收到空 prompt,跳过 LLM 调用\");\n return \"\";\n }\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "dd96c9df2639c5b031479024e45d7cf36378fc96": {"t": 1785749957.8133483, "n": 18289, "h": "f37704f23d2abafb", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport com.fashion.supplychain.intelligence.service.CostExplosionGuard;\nimport com.fashion.supplychain.intelligence.service.ModelSelectionRouter;\nimport com.fashion.supplychain.common.UserContext;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n @Autowired(required = false)\n private ModelSelectionRouter modelSelectionRouter;\n\n @Autowired(required = false)\n private CostExplosionGuard costExplosionGuard;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 带模型选择的聊天接口实现(per-call model selection)。\n *\n * <p>路由到具体的底层 gateway(SpringAiInferenceAdapter / LegacyInferenceAdapter),\n * 由底层 adapter 真正实现 per-call model 覆盖。\n * 同时记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御)。\n */\n @Override\n public String chatWithModel(String prompt, Long tenantId, Long userId, String modelId) {\n long start = System.currentTimeMillis();\n AiInferenceGateway gateway = resolveGateway(\"model-selection\");\n String content;\n try {\n content = gateway.chatWithModel(prompt, tenantId, userId, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModel failed, fallback to legacy: {}\", e.getMessage());\n content = legacyAdapter.chatWithModel(prompt, tenantId, userId, modelId);\n }\n long elapsed = System.currentTimeMillis() - start;\n\n // 记录 LLM 调用成本到 CostExplosionGuard(用于成本爆炸防御 + 重复检测)\n if (costExplosionGuard != null && tenantId != null) {\n try {\n // 用 \"llm_call\" 作为 toolName,prompt 哈希作为 paramsHash\n String paramsHash = costExplosionGuard.hashParams(prompt);\n costExplosionGuard.recordToolCall(tenantId, \"llm_call\", paramsHash,\n content != null ? content : \"\");\n log.debug(\"[AiInferenceRouter] LLM 调用成本已记录 tenantId={} modelId={} elapsed={}ms\",\n tenantId, modelId, elapsed);\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] 记录 LLM 调用成本失败(不影响主流程): {}\", e.getMessage());\n }\n }\n\n log.info(\"[AiInferenceRouter] chatWithModel: gateway={} modelId={} elapsed={}ms contentLen={}\",\n gateway.getProviderName(), modelId != null ? modelId : \"default\",\n elapsed, content != null ? content.length() : 0);\n return content;\n }\n\n /**\n * Per-call 模型选择调用(借鉴 Claude Agent SDK per-call model selection)。\n *\n * <p>根据用户消息复杂度、预估工具调用数、多域标识自动选择模型分级:\n * <ul>\n * <li>ECONOMY — 简单查询 → 便宜模型</li>\n * <li>STANDARD — 普通对话 → 标准模型</li>\n * <li>PREMIUM — 复杂排产 → 强模型</li>\n * </ul>\n *\n * <p>★ 接入点3:真正调用 gateway.chatWithModel 传递 modelId,而非仅用 scene 路由。\n *\n * @param prompt 完整提示词\n * @param userMessage 用户原始消息(用于复杂度评估)\n * @param estimatedToolCalls 预估工具调用数\n * @param isMultiDomain 是否多域任务\n * @return LLM 回答文本\n */\n public String chatWithModelSelection(String prompt, String userMessage,\n int estimatedToolCalls, boolean isMultiDomain) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n ModelSelectionRouter.ModelTier tier = modelSelectionRouter.selectModel(\n userMessage, estimatedToolCalls, isMultiDomain);\n String modelId = modelSelectionRouter.resolveModelId(tier);\n log.info(\"[AiInferenceRouter] per-call model selection: tier={} modelId={}\", tier, modelId);\n\n // 从 UserContext 获取 tenantId/userId(多租户隔离 + 成本追踪)\n Long tenantId = UserContext.tenantId();\n String userIdStr = UserContext.userId();\n\n // ★ 真正调用 gateway.chatWithModel 传递 modelId(接入点3 核心)\n try {\n return chatWithModel(prompt, tenantId, userIdStr != null ? userIdStr.hashCode() : 0L, modelId);\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] chatWithModelSelection failed, fallback to chatSimple: {}\", e.getMessage());\n return chatSimple(prompt);\n }\n }\n\n /**\n * 强制使用 PREMIUM 模型调用(高风险场景,如复杂排产优化)。\n *\n * @param prompt 完整提示词\n * @return LLM 回答文本\n */\n public String chatPremium(String prompt) {\n if (modelSelectionRouter == null || !modelSelectionRouter.isEnabled()) {\n return chatSimple(prompt);\n }\n String modelId = modelSelectionRouter.resolveModelId(ModelSelectionRouter.ModelTier.PREMIUM);\n log.info(\"[AiInferenceRouter] forced premium model: modelId={}\", modelId);\n IntelligenceInferenceResult result = chat(\"premium-reasoning\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /** 根据模型分级映射到 scene(用于网关路由 hint) */\n private String resolveSceneByTier(ModelSelectionRouter.ModelTier tier) {\n return switch (tier) {\n case ECONOMY -> \"economy-query\";\n case STANDARD -> \"reflection\";\n case PREMIUM -> \"premium-reasoning\";\n };\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "5096c8449e8798402e701944701dd0cc7033e1b7": {"t": 1785749959.105322, "n": 13376, "h": "ea0aae905f5cd560", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n // 视觉调用优先走 legacy adapter — Agnes视觉模型支持真正的image_url格式,\n // 比Spring AI适配器的文本嵌入方式更可靠\n if (legacyAdapter != null && legacyAdapter.isAvailable()) {\n try {\n IntelligenceInferenceResult result =\n legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n if (result != null && result.isSuccess() && hasText(result.getContent())) {\n recordCostAndAudit(scene, result);\n return result;\n }\n log.warn(\"[AiInferenceRouter] legacy视觉调用未返回有效内容,fallback到Spring AI\");\n } catch (Exception e) {\n log.warn(\"[AiInferenceRouter] legacy视觉调用异常,fallback到Spring AI: {}\", e.getMessage());\n }\n }\n // Spring AI 作为第二路径\n if (springAiAdapter != null && !isSpringAiCircuitOpen() && springAiAdapter.isAvailable()) {\n IntelligenceInferenceResult result = springAiAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n recordCostAndAudit(scene, result);\n return result;\n }\n // 最后兜底返回空结果\n IntelligenceInferenceResult empty = new IntelligenceInferenceResult();\n empty.setSuccess(false);\n empty.setProvider(\"none\");\n empty.setErrorMessage(\"没有可用的视觉模型(请配置AGNES_API_KEY或DEEPSEEK_API_KEY)\");\n recordCostAndAudit(scene, empty);\n return empty;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n\n private boolean hasText(String value) {\n return value != null && !value.trim().isEmpty();\n }\n}\n"}, "fc10d435e3d8429461ad7e59b9cdd2179a093756": {"t": 1785749960.1908967, "n": 12396, "h": "f2fd10483f74da71", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\n@Slf4j\n@Service\n@Lazy\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n}\n"}, "90c4cf9babe331e23a41125a6d92eb5c22f87e67": {"t": 1785749962.3397975, "n": 12338, "h": "29bcfb47b5e08689", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatWithVision(String scene, String systemPrompt, String userMessage, String imageUrl) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatWithVision(scene, systemPrompt, userMessage, imageUrl);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n}\n"}, "8984df30bfe77bcc304b28856cde70285a8310aa": {"t": 1785749963.4729555, "n": 11590, "h": "dfc25be17d3265ed", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n public String chatSimple(String prompt) {\n IntelligenceInferenceResult result = chat(\"reflection\", null, prompt);\n return result != null ? result.getContent() : \"\";\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n}\n"}, "01152878ccaac8d964b541b9cfaad7850ed2f39a": {"t": 1785749969.0422065, "n": 11400, "h": "545ebcd37a4241d1", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断进入半开状态,允许探测请求\");\n return false;\n }\n return openSince > 0;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n if (springAiConsecutiveFailures.get() >= CIRCUIT_OPEN_THRESHOLD) {\n log.info(\"[AiInferenceRouter] Spring AI 半开探测成功,熔断恢复\");\n }\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures == CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.get() == 0) {\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后进入半开探测)\", failures, CIRCUIT_RESET_MS / 1000);\n } else if (springAiCircuitOpenSince.get() > 0\n && System.currentTimeMillis() - springAiCircuitOpenSince.get() > CIRCUIT_RESET_MS) {\n log.warn(\"[AiInferenceRouter] Spring AI 半开探测失败,重新熔断 {} 秒\", CIRCUIT_RESET_MS / 1000);\n springAiCircuitOpenSince.set(System.currentTimeMillis());\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-v4-flash\";\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n}\n"}, "79b8c850437e336d9253403986bf1d477c67887b": {"t": 1785749970.164954, "n": 10968, "h": "41acbf9c7acd51d6", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Autowired(required = false)\n private ModelConsortiumRouter modelConsortiumRouter;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n log.info(\"[AiInferenceRouter] Spring AI 熔断恢复({}秒冷却期已过)\", CIRCUIT_RESET_MS / 1000);\n return false;\n }\n return true;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures >= CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.compareAndSet(0, System.currentTimeMillis())) {\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后自动恢复尝试)\", failures, CIRCUIT_RESET_MS / 1000);\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n Map<String, Object> status = new java.util.LinkedHashMap<>();\n status.put(\"strategy\", routingStrategy);\n status.put(\"legacyAvailable\", legacyAdapter.isAvailable());\n status.put(\"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable());\n status.put(\"springAiCircuitOpen\", isSpringAiCircuitOpen());\n status.put(\"springAiConsecutiveFailures\", springAiConsecutiveFailures.get());\n status.put(\"activeProvider\", resolveGateway(null).getProviderName());\n if (modelConsortiumRouter != null) {\n status.put(\"consortium\", modelConsortiumRouter.getConfig());\n }\n return status;\n }\n\n /**\n * 模型智能路由 — 根据用户查询复杂度选择最优模型。\n * 可在上层(AiAgentOrchestrator)调用此方法获取推荐模型,传递给推理适配器。\n */\n public String recommendModel(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n return modelConsortiumRouter.selectModel(userMessage, hasImage, toolCount);\n }\n return \"deepseek-chat\"; // 默认\n }\n\n /**\n * 获取模型推荐参数(temperature、maxTokens、timeout)。\n */\n public ModelConsortiumRouter.ModelParams recommendModelParams(String userMessage, boolean hasImage, int toolCount) {\n if (modelConsortiumRouter != null) {\n ModelConsortiumRouter.Complexity complexity = modelConsortiumRouter.classifyComplexity(\n userMessage, hasImage, toolCount);\n return modelConsortiumRouter.getModelParams(complexity);\n }\n return new ModelConsortiumRouter.ModelParams(0.5, 1024, 30);\n }\n}\n"}, "ba4fdd20657f2c6fc9b5b95efce4d1123dd72f7a": {"t": 1785749971.2633975, "n": 9722, "h": "53ffd1286a57b186", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 3;\n private static final long CIRCUIT_RESET_MS = 30_000L;\n\n @Value(\"${ai.gateway.routing-strategy:failover}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover(scene);\n case \"legacy\" -> legacyAdapter;\n default -> resolveFailover(scene);\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover(String scene) {\n // 短查询/非Agent场景优先走 Spring AI(更快、工具调用更新)\n boolean preferSpringAi = isShortQueryScene(scene);\n if (preferSpringAi && !isSpringAiCircuitOpen()\n && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.debug(\"[AiInferenceRouter] 短查询场景={},优先使用 Spring AI\", scene);\n return springAiAdapter;\n }\n // Agent主循环优先Spring AI(流式+tool_calls更好),熔断时降级\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null) {\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isShortQueryScene(String scene) {\n return scene != null && (\n \"nl-intent\".equals(scene) || \"daily-brief\".equals(scene) ||\n \"critic_review\".equals(scene) || scene.startsWith(\"memory\"));\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n log.info(\"[AiInferenceRouter] Spring AI 熔断恢复({}秒冷却期已过)\", CIRCUIT_RESET_MS / 1000);\n return false;\n }\n return true;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures >= CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.compareAndSet(0, System.currentTimeMillis())) {\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后自动恢复尝试)\", failures, CIRCUIT_RESET_MS / 1000);\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n // 统一性能日志:方便排查双AI引擎表现\n log.info(\"[AiInference] engine={}, scene={}, success={}, latency={}ms, tokens={}/{}, fallback={}\",\n result.getProvider() != null ? result.getProvider() : \"unknown\",\n scene != null ? scene : \"default\",\n result.isSuccess(),\n result.getLatencyMs(),\n result.getPromptTokens(), result.getCompletionTokens(),\n Boolean.TRUE.equals(result.isFallbackUsed()));\n if (!result.isSuccess()) {\n log.warn(\"[AiInference] engine={}, scene={}, error={}\", result.getProvider(), scene, result.getErrorMessage());\n }\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable(),\n \"springAiCircuitOpen\", isSpringAiCircuitOpen(),\n \"springAiConsecutiveFailures\", springAiConsecutiveFailures.get(),\n \"activeProvider\", resolveGateway(null).getProviderName()\n );\n }\n}\n"}, "c40d5ea3a741db9d441460401c55605395c9f249": {"t": 1785749972.3551986, "n": 8367, "h": "cbc60b275b5af521", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n private static final int CIRCUIT_OPEN_THRESHOLD = 5;\n private static final long CIRCUIT_RESET_MS = 60_000L;\n\n @Value(\"${ai.gateway.routing-strategy:legacy}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n private final AtomicInteger springAiConsecutiveFailures = new AtomicInteger(0);\n private final AtomicLong springAiCircuitOpenSince = new AtomicLong(0);\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, systemPrompt, userMessage);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chat(scene, messages, tools);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n trackSpringAiHealth(result);\n if (isSpringAiCircuitOpen() && !\"legacy\".equals(gateway.getProviderName())) {\n log.info(\"[AiInferenceRouter] Spring AI 熔断触发,降级到 legacy 重试 scene={}\", scene);\n result = legacyAdapter.chatStream(scene, messages, tools, chunkConsumer);\n result.setFallbackUsed(true);\n }\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover();\n case \"legacy\" -> legacyAdapter;\n default -> legacyAdapter;\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (isSpringAiCircuitOpen()) {\n log.warn(\"[AiInferenceRouter] Spring AI 熔断中,降级到 legacy\");\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover() {\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (!isSpringAiCircuitOpen() && springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.warn(\"[AiInferenceRouter] legacy unavailable, using spring-ai as failover\");\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private boolean isSpringAiCircuitOpen() {\n if (springAiConsecutiveFailures.get() < CIRCUIT_OPEN_THRESHOLD) return false;\n long openSince = springAiCircuitOpenSince.get();\n if (openSince > 0 && System.currentTimeMillis() - openSince > CIRCUIT_RESET_MS) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n log.info(\"[AiInferenceRouter] Spring AI 熔断恢复({}秒冷却期已过)\", CIRCUIT_RESET_MS / 1000);\n return false;\n }\n return true;\n }\n\n private void trackSpringAiHealth(IntelligenceInferenceResult result) {\n if (result == null || !\"spring-ai\".equals(result.getProvider())) return;\n if (result.isSuccess()) {\n springAiConsecutiveFailures.set(0);\n springAiCircuitOpenSince.set(0);\n } else {\n int failures = springAiConsecutiveFailures.incrementAndGet();\n if (failures >= CIRCUIT_OPEN_THRESHOLD && springAiCircuitOpenSince.compareAndSet(0, System.currentTimeMillis())) {\n log.warn(\"[AiInferenceRouter] Spring AI 连续{}次失败,熔断开启({}秒后自动恢复尝试)\", failures, CIRCUIT_RESET_MS / 1000);\n }\n }\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable(),\n \"springAiCircuitOpen\", isSpringAiCircuitOpen(),\n \"springAiConsecutiveFailures\", springAiConsecutiveFailures.get(),\n \"activeProvider\", resolveGateway(null).getProviderName()\n );\n }\n}\n"}, "224b8fcea85eeb056d0d0a23657208a2ead7c393": {"t": 1785749974.4750092, "n": 5472, "h": "d7ab29a3a989ef56", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n @Value(\"${ai.gateway.routing-strategy:legacy}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover();\n case \"legacy\" -> legacyAdapter;\n default -> legacyAdapter;\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover() {\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.warn(\"[AiInferenceRouter] legacy unavailable, using spring-ai as failover\");\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable(),\n \"activeProvider\", resolveGateway(null).getProviderName()\n );\n }\n}\n"}, "9ff702bfd86152d27bcb7ab8788c73ff521c091b": {"t": 1785749975.545124, "n": 4441, "h": "17bf3af9d40deb40", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n @Value(\"${ai.gateway.routing-strategy:legacy}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return legacyAdapter;\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", false,\n \"activeProvider\", legacyAdapter.getProviderName()\n );\n }\n}\n"}, "3e9c97d347fac7e1e28d906154ecb80603ffc273": {"t": 1785749976.6258533, "n": 5472, "h": "d7ab29a3a989ef56", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\n@Primary\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n @Value(\"${ai.gateway.routing-strategy:legacy}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover();\n case \"legacy\" -> legacyAdapter;\n default -> legacyAdapter;\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover() {\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.warn(\"[AiInferenceRouter] legacy unavailable, using spring-ai as failover\");\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable(),\n \"activeProvider\", resolveGateway(null).getProviderName()\n );\n }\n}\n"}, "1d8263154937b9e693cf726a990b9a264c4debef": {"t": 1785749977.6948547, "n": 5408, "h": "b7989e14d577f458", "c": "package com.fashion.supplychain.intelligence.gateway;\n\nimport com.fashion.supplychain.intelligence.agent.AiMessage;\nimport com.fashion.supplychain.intelligence.agent.AiTool;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.AiCostTrackingOrchestrator;\nimport com.fashion.supplychain.intelligence.orchestration.AiOperationAuditOrchestrator;\nimport java.util.List;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\n@Slf4j\n@Service\npublic class AiInferenceRouter implements AiInferenceGateway {\n\n @Value(\"${ai.gateway.routing-strategy:legacy}\")\n private String routingStrategy;\n\n @Autowired\n private LegacyInferenceAdapter legacyAdapter;\n\n @Autowired(required = false)\n private SpringAiInferenceAdapter springAiAdapter;\n\n @Autowired(required = false)\n private AiCostTrackingOrchestrator aiCostTrackingOrchestrator;\n\n @Autowired(required = false)\n private AiOperationAuditOrchestrator aiOperationAuditOrchestrator;\n\n @Override\n public IntelligenceInferenceResult chat(String scene, String systemPrompt, String userMessage) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, systemPrompt, userMessage);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chat(String scene, List<AiMessage> messages, List<AiTool> tools) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chat(scene, messages, tools);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public IntelligenceInferenceResult chatStream(String scene, List<AiMessage> messages,\n List<AiTool> tools,\n StreamChunkConsumer chunkConsumer) {\n AiInferenceGateway gateway = resolveGateway(scene);\n IntelligenceInferenceResult result = gateway.chatStream(scene, messages, tools, chunkConsumer);\n recordCostAndAudit(scene, result);\n return result;\n }\n\n @Override\n public boolean isAvailable() {\n return resolveGateway(null).isAvailable();\n }\n\n @Override\n public String getProviderName() {\n return resolveGateway(null).getProviderName();\n }\n\n private AiInferenceGateway resolveGateway(String scene) {\n return switch (routingStrategy) {\n case \"spring-ai\" -> resolveSpringAi();\n case \"failover\" -> resolveFailover();\n case \"legacy\" -> legacyAdapter;\n default -> legacyAdapter;\n };\n }\n\n private AiInferenceGateway resolveSpringAi() {\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n return springAiAdapter;\n }\n log.warn(\"[AiInferenceRouter] spring-ai unavailable, falling back to legacy\");\n return legacyAdapter;\n }\n\n private AiInferenceGateway resolveFailover() {\n if (legacyAdapter.isAvailable()) {\n return legacyAdapter;\n }\n if (springAiAdapter != null && springAiAdapter.isAvailable()) {\n log.warn(\"[AiInferenceRouter] legacy unavailable, using spring-ai as failover\");\n return springAiAdapter;\n }\n return legacyAdapter;\n }\n\n private void recordCostAndAudit(String scene, IntelligenceInferenceResult result) {\n if (result == null) return;\n try {\n if (aiCostTrackingOrchestrator != null) {\n aiCostTrackingOrchestrator.recordAsync(\n result.getModel() != null ? result.getModel() : result.getProvider(),\n scene,\n result.getPromptTokens(),\n result.getCompletionTokens(),\n (int) result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] cost tracking failed: {}\", e.getMessage());\n }\n try {\n if (aiOperationAuditOrchestrator != null && result.getTraceId() != null) {\n aiOperationAuditOrchestrator.recordAudit(\n result.getTraceId(),\n \"inference:\" + scene,\n result.getPromptChars() + \" chars\",\n result.getResponseChars() + \" chars\",\n result.getLatencyMs(),\n result.isSuccess(),\n result.getErrorMessage()\n );\n }\n } catch (Exception e) {\n log.debug(\"[AiInferenceRouter] audit recording failed: {}\", e.getMessage());\n }\n }\n\n public Map<String, Object> getRoutingStatus() {\n return Map.of(\n \"strategy\", routingStrategy,\n \"legacyAvailable\", legacyAdapter.isAvailable(),\n \"springAiAvailable\", springAiAdapter != null && springAiAdapter.isAvailable(),\n \"activeProvider\", resolveGateway(null).getProviderName()\n );\n }\n}\n"}}, "backend/src/main/java/com/fashion/supplychain/intelligence/service/AiAdvisorService.java": {"92136382831779e22f23b72a08f4e137bfa17dd3": {"t": 1785745701.6783586, "n": 5838, "h": "0c462b9a466a0964", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Lazy\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n @Scheduled(fixedRate = 3600000)\n public void cleanupStaleCounters() {\n String todaySuffix = \"_\" + LocalDate.now();\n dailyCounters.keySet().removeIf(key -> !key.endsWith(todaySuffix));\n }\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + T(java.time.LocalDate).now()\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "fc10d435e3d8429461ad7e59b9cdd2179a093756": {"t": 1785749953.3053546, "n": 5838, "h": "0c462b9a466a0964", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\nimport org.springframework.context.annotation.Lazy;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Lazy\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n @Scheduled(fixedRate = 3600000)\n public void cleanupStaleCounters() {\n String todaySuffix = \"_\" + LocalDate.now();\n dailyCounters.keySet().removeIf(key -> !key.endsWith(todaySuffix));\n }\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + T(java.time.LocalDate).now()\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "cc55c6b31d802f926aac252da09310cf26df12ee": {"t": 1785749954.580515, "n": 5780, "h": "d524bdf2a0c2d6de", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n @Scheduled(fixedRate = 3600000)\n public void cleanupStaleCounters() {\n String todaySuffix = \"_\" + LocalDate.now();\n dailyCounters.keySet().removeIf(key -> !key.endsWith(todaySuffix));\n }\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + T(java.time.LocalDate).now()\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "5f3b3d1c5d9e1263d4af0f790474284a920c8a8f": {"t": 1785749957.7036502, "n": 5495, "h": "60fab315a6e32af6", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + #contextSummary\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "64c4685b5bf2577e8ed6f8aff55fa2b7fa6cf319": {"t": 1785749958.7763484, "n": 5543, "h": "15755135f48490ef", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"T(com.fashion.supplychain.common.UserContext).tenantId() + ':' + #contextSummary\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "34014ba5d2031264de5c960eaad707590ce3b729": {"t": 1785749959.8803394, "n": 5478, "h": "3cfa15a117d66e9f", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * <p>结果按 contextSummary 内容缓存 5 分钟,相同摘要(同一租户当日数据未变化)\n * 直接返回缓存值,避免重复调用 DeepSeek API(每次约 2~3 秒延迟)。\n * Redis 不可用时自动降级为直接调用 AI(见 RedisConfig.errorHandler)。</p>\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n @Cacheable(value = \"daily-brief\", key = \"#contextSummary\")\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "ee39636df0ebdc4a1f9923cc34d0684344a4bf4b": {"t": 1785749960.9941413, "n": 5184, "h": "f1a7f3b07fb8ce2d", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fashion.supplychain.intelligence.dto.IntelligenceInferenceResult;\nimport com.fashion.supplychain.intelligence.orchestration.IntelligenceInferenceOrchestrator;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务。\n *\n * <p>职责:保留配额与启用判断,对外提供 AI 顾问语义能力。\n * 真实推理由独立的 {@link IntelligenceInferenceOrchestrator} 执行。</p>\n *\n * <p>默认直连 DeepSeek,也支持通过 LiteLLM 网关接入,配置方式(环境变量):\n * <pre>\n * DEEPSEEK_API_KEY = sk-xxxxxxxxxxxxxxxx\n * AI_LITELLM_ENABLED = true/false\n * AI_LITELLM_BASE_URL = http://localhost:4000\n * AI_LITELLM_API_KEY = sk-virtual-key\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n @Autowired\n private IntelligenceInferenceOrchestrator intelligenceInferenceOrchestrator;\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return intelligenceInferenceOrchestrator.isAnyModelEnabled();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n IntelligenceInferenceResult result = invoke(\"ai-advisor\", systemPrompt, userMessage);\n if (!result.isSuccess()) {\n log.warn(\"[AiAdvisor] AI 调用失败 provider={} error={}\", result.getProvider(), result.getErrorMessage());\n return null;\n }\n return result.getContent();\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n IntelligenceInferenceResult result = invoke(\"daily-brief\", systemPrompt, contextSummary);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n IntelligenceInferenceResult result = invoke(\"nl-intent\", systemPrompt, question);\n return result.isSuccess() ? result.getContent() : null;\n }\n\n private IntelligenceInferenceResult invoke(String scene, String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n IntelligenceInferenceResult result = new IntelligenceInferenceResult();\n result.setSuccess(false);\n result.setProvider(\"none\");\n result.setErrorMessage(\"ai-disabled\");\n return result;\n }\n return intelligenceInferenceOrchestrator.chat(scene, systemPrompt, userMessage);\n }\n}\n"}, "6f4ed6d1a104aecf5e94fb4632c129e2a83d710e": {"t": 1785749963.079358, "n": 6578, "h": "164114db2d13a06e", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.time.LocalDate;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * AI 顾问服务 — 统一的 AI API 接入层\n *\n * <p>默认接 DeepSeek,配置方式(环境变量):\n * <pre>\n * AI_API_KEY = sk-xxxxxxxxxxxxxxxx (必填,有此值才会真实调用 AI)\n * AI_API_URL = https://api.deepseek.com/v1/chat/completions (可选,默认 DeepSeek)\n * AI_MODEL = deepseek-chat (可选,默认 deepseek-chat)\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n *\n * <p>切换 OpenAI / 其他兼容 API 只需改 AI_API_URL 和 AI_MODEL。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n @Value(\"${ai.deepseek.api-url:https://api.deepseek.com/v1/chat/completions}\")\n private String apiUrl;\n\n @Value(\"${ai.deepseek.model:deepseek-chat}\")\n private String model;\n\n /** 每个租户每天最多允许调用 DeepSeek 的次数(0 = 不限制) */\n @Value(\"${ai.deepseek.daily-quota-per-tenant:50}\")\n private int dailyQuotaPerTenant;\n\n private static final int TIMEOUT_SECONDS = 30;\n private static final ObjectMapper MAPPER = new ObjectMapper();\n\n /**\n * 每租户每日调用计数器\n * key = \"tenantId_yyyyMMdd\",value = 当日已调用次数\n * 不同日期的 key 自动过期(不再被读写),内存占用可忽略不计\n */\n private final ConcurrentHashMap<String, AtomicInteger> dailyCounters = new ConcurrentHashMap<>();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return apiKey != null && !apiKey.isBlank();\n }\n\n /**\n * 检查当前租户今日配额是否已用完\n *\n * @param tenantId 租户ID(null 视为系统内部调用,不限制)\n * @return true = 配额充足可以调用;false = 今日配额已用完\n */\n public boolean checkAndConsumeQuota(Long tenantId) {\n if (dailyQuotaPerTenant <= 0 || tenantId == null) return true; // 0=不限\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.computeIfAbsent(key, k -> new AtomicInteger(0));\n int used = count.incrementAndGet();\n if (used > dailyQuotaPerTenant) {\n count.decrementAndGet(); // 回滚,不消耗\n log.warn(\"[AiAdvisor] 租户 {} 今日AI调用已达配额上限 {}\", tenantId, dailyQuotaPerTenant);\n return false;\n }\n log.debug(\"[AiAdvisor] 租户 {} 今日AI调用 {}/{}\", tenantId, used, dailyQuotaPerTenant);\n return true;\n }\n\n /**\n * 查询当前租户今日已用配额\n */\n public int getTodayUsage(Long tenantId) {\n if (tenantId == null) return 0;\n String key = tenantId + \"_\" + LocalDate.now();\n AtomicInteger count = dailyCounters.get(key);\n return count == null ? 0 : count.get();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n log.debug(\"[AiAdvisor] AI_API_KEY 未配置,跳过 AI 调用\");\n return null;\n }\n\n try {\n String body = buildRequestBody(systemPrompt, userMessage);\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(10))\n .build();\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(apiUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(body))\n .timeout(Duration.ofSeconds(TIMEOUT_SECONDS))\n .build();\n\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n\n if (response.statusCode() == 200) {\n return extractContent(response.body());\n } else {\n log.warn(\"[AiAdvisor] API 返回非200: status={}, body={}\", response.statusCode(),\n response.body().substring(0, Math.min(200, response.body().length())));\n return null;\n }\n } catch (Exception e) {\n log.warn(\"[AiAdvisor] AI 调用失败: {}\", e.getMessage());\n return null;\n }\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n return chat(systemPrompt, contextSummary);\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n return chat(systemPrompt, question);\n }\n\n // ────────────────── 私有方法 ──────────────────\n\n private String buildRequestBody(String systemPrompt, String userMessage) throws Exception {\n var root = MAPPER.createObjectNode();\n root.put(\"model\", model);\n root.put(\"temperature\", 0.3);\n root.put(\"max_tokens\", 512);\n var messages = root.putArray(\"messages\");\n messages.addObject().put(\"role\", \"system\").put(\"content\", systemPrompt);\n messages.addObject().put(\"role\", \"user\").put(\"content\", userMessage);\n return MAPPER.writeValueAsString(root);\n }\n\n private String extractContent(String responseBody) throws Exception {\n JsonNode root = MAPPER.readTree(responseBody);\n JsonNode choices = root.path(\"choices\");\n if (choices.isArray() && choices.size() > 0) {\n return choices.get(0).path(\"message\").path(\"content\").asText(null);\n }\n return null;\n }\n}\n"}, "fdd7d49b70ea84d9b24ec29bc480b05a726ff117": {"t": 1785749969.0282362, "n": 4998, "h": "61094422963d0eb0", "c": "package com.fashion.supplychain.intelligence.service;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\n\n/**\n * AI 顾问服务 — 统一的 AI API 接入层\n *\n * <p>默认接 DeepSeek,配置方式(环境变量):\n * <pre>\n * AI_API_KEY = sk-xxxxxxxxxxxxxxxx (必填,有此值才会真实调用 AI)\n * AI_API_URL = https://api.deepseek.com/v1/chat/completions (可选,默认 DeepSeek)\n * AI_MODEL = deepseek-chat (可选,默认 deepseek-chat)\n * </pre>\n *\n * <p>若未配置 AI_API_KEY,所有调用会立即返回 null,系统继续走本地规则引擎,\n * 不影响任何现有功能。\n *\n * <p>切换 OpenAI / 其他兼容 API 只需改 AI_API_URL 和 AI_MODEL。\n */\n@Service\n@Slf4j\npublic class AiAdvisorService {\n\n @Value(\"${ai.deepseek.api-key:}\")\n private String apiKey;\n\n @Value(\"${ai.deepseek.api-url:https://api.deepseek.com/v1/chat/completions}\")\n private String apiUrl;\n\n @Value(\"${ai.deepseek.model:deepseek-chat}\")\n private String model;\n\n private static final int TIMEOUT_SECONDS = 30;\n private static final ObjectMapper MAPPER = new ObjectMapper();\n\n /**\n * 是否已启用 AI(有 API Key)\n */\n public boolean isEnabled() {\n return apiKey != null && !apiKey.isBlank();\n }\n\n /**\n * 发送单轮对话请求\n *\n * @param systemPrompt 系统角色描述(如:你是服装供应链分析师)\n * @param userMessage 用户消息\n * @return AI 回复文本,未配置 Key 或调用失败时返回 null\n */\n public String chat(String systemPrompt, String userMessage) {\n if (!isEnabled()) {\n log.debug(\"[AiAdvisor] AI_API_KEY 未配置,跳过 AI 调用\");\n return null;\n }\n\n try {\n String body = buildRequestBody(systemPrompt, userMessage);\n HttpClient client = HttpClient.newBuilder()\n .connectTimeout(Duration.ofSeconds(10))\n .build();\n HttpRequest request = HttpRequest.newBuilder()\n .uri(URI.create(apiUrl))\n .header(\"Content-Type\", \"application/json\")\n .header(\"Authorization\", \"Bearer \" + apiKey)\n .POST(HttpRequest.BodyPublishers.ofString(body))\n .timeout(Duration.ofSeconds(TIMEOUT_SECONDS))\n .build();\n\n HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n\n if (response.statusCode() == 200) {\n return extractContent(response.body());\n } else {\n log.warn(\"[AiAdvisor] API 返回非200: status={}, body={}\", response.statusCode(),\n response.body().substring(0, Math.min(200, response.body().length())));\n return null;\n }\n } catch (Exception e) {\n log.warn(\"[AiAdvisor] AI 调用失败: {}\", e.getMessage());\n return null;\n }\n }\n\n /**\n * 智能建议:给定业务数据摘要,返回简短建议文案(用于运营日报)\n *\n * @param contextSummary 业务摘要(如:今日逾期3单,高风险2单,停滞1单)\n * @return 1~3句建议文案,未启用时返回 null\n */\n public String getDailyAdvice(String contextSummary) {\n String systemPrompt = \"你是一名服装供应链管理顾问,根据工厂今日生产数据,\" +\n \"用简洁中文给出1~3条可执行的管理建议。每条建议一行,不超过30字。不要废话。\";\n return chat(systemPrompt, contextSummary);\n }\n\n /**\n * 自然语言转结构化查询意图分析\n * 当 NlQueryOrchestrator 本地规则无法匹配时,调用此方法让 AI 理解意图\n *\n * @param question 用户问题\n * @return JSON 字符串,格式:{\"intent\":\"xxx\",\"params\":{\"key\":\"value\"}}\n * 失败返回 null\n */\n public String parseNlIntent(String question) {\n String systemPrompt = \"你是服装供应链系统的 NLP 解析器。\" +\n \"将用户问题解析为 JSON:{\\\"intent\\\":\\\"意图\\\",\\\"params\\\":{\\\"参数\\\":\\\"值\\\"}}。\" +\n \"可能的 intent: query_order_status, query_material_stock, query_factory_capacity, \" +\n \"query_finance_settlement, query_worker_efficiency, query_delivery_risk。\" +\n \"只输出 JSON,不要解释。\";\n return chat(systemPrompt, question);\n }\n\n // ────────────────── 私有方法 ──────────────────\n\n private String buildRequestBody(String systemPrompt, String userMessage) throws Exception {\n var root = MAPPER.createObjectNode();\n root.put(\"model\", model);\n root.put(\"temperature\", 0.3);\n root.put(\"max_tokens\", 512);\n var messages = root.putArray(\"messages\");\n messages.addObject().put(\"role\", \"system\").put(\"content\", systemPrompt);\n messages.addObject().put(\"role\", \"user\").put(\"content\", userMessage);\n return MAPPER.writeValueAsString(root);\n }\n\n private String extractContent(String responseBody) throws Exception {\n JsonNode root = MAPPER.readTree(responseBody);\n JsonNode choices = root.path(\"choices\");\n if (choices.isArray() && choices.size() > 0) {\n return choices.get(0).path(\"message\").path(\"content\").asText(null);\n }\n return null;\n }\n}\n"}}}} |