Files

1 line
19 KiB
JSON

{"repo": "amine-ge/backend-repo", "files": {"src/main/java/com/tap/backend/academic/controller/DeepSeekChatController.java": {"4a17c2d7db0c9f0a757bab649aec4fe93eb987e7": {"t": 1785746126.494974, "n": 7709, "h": "0d8ea6b3614835dc", "c": "package com.tap.backend.academic.controller;\n\nimport com.tap.backend.academic.security.StudentSessionResolver;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport jakarta.servlet.http.HttpServletRequest;\nimport java.io.BufferedReader;\nimport java.io.InputStreamReader;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport okhttp3.ResponseBody;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;\n\n@RestController\n@RequestMapping(\"/api\")\npublic class DeepSeekChatController {\n\n private static final String SYSTEM_PROMPT =\n \"你是一个专业的数据结构与算法学习助手,专注于帮助大学生解决数据结构课程中的学习问题。\"\n + \"你擅长解释数据结构概念、分析算法复杂度、帮助调试 C/C++ 代码,并提供解题思路。\"\n + \"回答要简洁、准确、有针对性,并使用中文。\";\n\n @Value(\"${tap.ai.openai.api-key:}\")\n private String apiKey;\n\n @Value(\"${tap.ai.openai.base-url:https://api.deepseek.com/v1}\")\n private String baseUrl;\n\n @Value(\"${tap.ai.openai.model:deepseek-chat}\")\n private String model;\n\n private final StudentSessionResolver studentSessionResolver;\n\n private final OkHttpClient httpClient = new OkHttpClient.Builder()\n .connectTimeout(30, TimeUnit.SECONDS)\n .readTimeout(120, TimeUnit.SECONDS)\n .writeTimeout(30, TimeUnit.SECONDS)\n .build();\n\n public DeepSeekChatController(StudentSessionResolver studentSessionResolver) {\n this.studentSessionResolver = studentSessionResolver;\n }\n\n @PostMapping(value = \"/chat\", produces = MediaType.TEXT_EVENT_STREAM_VALUE)\n public ResponseEntity<StreamingResponseBody> chat(\n @RequestBody Map<String, String> request,\n HttpServletRequest httpRequest\n ) {\n studentSessionResolver.requireStudentId(httpRequest);\n String userInput = request.get(\"userInput\");\n System.out.println(\"[DeepSeek] request: \" + userInput);\n\n if (userInput == null || userInput.trim().isEmpty()) {\n return ResponseEntity.ok().body(out -> {\n out.write(\"请输入有效的问题\".getBytes(StandardCharsets.UTF_8));\n out.flush();\n });\n }\n\n StreamingResponseBody body = outputStream -> {\n try {\n if (apiKey == null || apiKey.isBlank()) {\n outputStream.write(\n \"AI service is not configured. Set OPENAI_API_KEY in local.env.ps1 or the process environment.\"\n .getBytes(StandardCharsets.UTF_8)\n );\n outputStream.flush();\n return;\n }\n\n JsonObject reqBody = new JsonObject();\n reqBody.addProperty(\"model\", model);\n reqBody.addProperty(\"stream\", true);\n\n JsonArray messages = new JsonArray();\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", SYSTEM_PROMPT);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userInput);\n messages.add(userMessage);\n\n reqBody.add(\"messages\", messages);\n\n okhttp3.RequestBody okBody = okhttp3.RequestBody.create(\n reqBody.toString(),\n okhttp3.MediaType.parse(\"application/json; charset=utf-8\")\n );\n\n Request okReq = new Request.Builder()\n .url(baseUrl + \"/chat/completions\")\n .addHeader(\"Authorization\", \"Bearer \" + apiKey)\n .addHeader(\"Content-Type\", \"application/json\")\n .post(okBody)\n .build();\n\n try (Response response = httpClient.newCall(okReq).execute()) {\n if (!response.isSuccessful()) {\n String errBody = response.body() != null ? response.body().string() : \"unknown\";\n System.err.println(\"[DeepSeek] API error: \" + response.code() + \" \" + errBody);\n outputStream.write((\"AI 服务暂时不可用,错误码: \" + response.code()).getBytes(StandardCharsets.UTF_8));\n outputStream.flush();\n return;\n }\n\n ResponseBody respBody = response.body();\n if (respBody == null) {\n return;\n }\n\n try (BufferedReader reader = new BufferedReader(\n new InputStreamReader(respBody.byteStream(), StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n if (!line.startsWith(\"data: \")) {\n continue;\n }\n String data = line.substring(6).trim();\n if (\"[DONE]\".equals(data)) {\n break;\n }\n\n try {\n JsonObject chunk = JsonParser.parseString(data).getAsJsonObject();\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n continue;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta == null) {\n continue;\n }\n\n if (delta.has(\"reasoning_content\") && !delta.get(\"reasoning_content\").isJsonNull()) {\n continue;\n }\n\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n String content = delta.get(\"content\").getAsString();\n outputStream.write(content.getBytes(StandardCharsets.UTF_8));\n outputStream.flush();\n }\n } catch (Exception ignored) {\n }\n }\n }\n }\n } catch (Exception e) {\n System.err.println(\"[DeepSeek] exception: \" + e.getMessage());\n try {\n outputStream.write(\"抱歉,AI 助手暂时无法回答,请稍后再试。\".getBytes(StandardCharsets.UTF_8));\n outputStream.flush();\n } catch (Exception ignored) {\n }\n }\n };\n\n return ResponseEntity.ok()\n .contentType(MediaType.TEXT_EVENT_STREAM)\n .header(\"Cache-Control\", \"no-cache\")\n .header(\"X-Accel-Buffering\", \"no\")\n .body(body);\n }\n}\n"}}, "src/main/resources/application.yml": {"4a17c2d7db0c9f0a757bab649aec4fe93eb987e7": {"t": 1785746948.3260157, "n": 10390, "h": "0c218040248cb077", "c": "spring:\n config:\n import: optional:classpath:application-local.yml\n application:\n name: teaching-assistant-backend\n datasource:\n driver-class-name: com.mysql.cj.jdbc.Driver\n url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3307}/${DB_NAME:ptadatabase}?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048&useServerPrepStmts=true&rewriteBatchedStatements=true\n username: ${DB_USERNAME:root}\n password: ${DB_PASSWORD:${DB_PASS:}}\n hikari:\n connection-timeout: 20000\n maximum-pool-size: ${DB_POOL_MAX:30}\n minimum-idle: ${DB_POOL_MIN:10}\n idle-timeout: 300000\n max-lifetime: 1200000\n leak-detection-threshold: 60000\n pool-name: HikariPool-Main\n jpa:\n database-platform: org.hibernate.dialect.MySQL8Dialect\n show-sql: false\n hibernate:\n ddl-auto: validate\n open-in-view: false\n properties:\n hibernate:\n format_sql: false\n jdbc:\n batch_size: 25\n fetch_size: 50\n order_inserts: true\n order_updates: true\n generate_statistics: false\n flyway:\n enabled: true\n locations: classpath:db/migration\n baseline-on-migrate: true\n baseline-version: 0\n data:\n redis:\n host: ${REDIS_HOST:localhost}\n port: ${REDIS_PORT:6379}\n lettuce:\n pool:\n max-active: ${REDIS_POOL_MAX:16}\n max-idle: 8\n min-idle: 4\n max-wait: 3000ms\n # 临时禁用Redis以便测试\n timeout: 1000ms\n connect-timeout: 1000ms\n servlet:\n session:\n timeout: 3600\n multipart:\n max-file-size: 512MB\n max-request-size: 520MB\n task:\n execution:\n pool:\n core-size: 8\n max-size: 32\n queue-capacity: 200\n keep-alive: 60s\n thread-name-prefix: async-\n scheduling:\n pool:\n size: 4\n thread-name-prefix: sched-\n\nmybatis:\n configuration:\n log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl\n map-underscore-to-camel-case: true\n local-cache-scope: STATEMENT\n mapper-locations: classpath:/mappers/*.xml\n type-aliases-package: com.tap.backend.academic.entity\n\npagehelper:\n helper-dialect: mysql\n\nlogging:\n level:\n root: info\n com.tap.backend: info\n org:\n springframework:\n jdbc: warn\n transaction: warn\n web: warn\n\nserver:\n port: 8081\n servlet:\n encoding:\n charset: UTF-8\n enabled: true\n force: true\n tomcat:\n threads:\n max: ${TOMCAT_MAX_THREADS:200}\n min-spare: ${TOMCAT_MIN_SPARE:20}\n max-connections: ${TOMCAT_MAX_CONNECTIONS:8192}\n accept-count: ${TOMCAT_ACCEPT_COUNT:100}\n connection-timeout: 20000\n keep-alive-timeout: 30000\n max-keep-alive-requests: 100\n compression:\n enabled: true\n mime-types: application/json,application/xml,text/html,text/plain,text/css,application/javascript\n min-response-size: 1024\n\nmanagement:\n endpoints:\n web:\n exposure:\n include: health,info\n endpoint:\n health:\n show-details: always\n\nark:\n api:\n key: ${ARK_API_KEY:}\n\nvolcano:\n api:\n key: ${VOLCANO_API_KEY:}\n model: deepseek-r1-250120\n\n# ========== LeetCode 推荐系统配置 ==========\nleetcode:\n recommendation:\n # 推荐算法权重配置\n weights:\n need-match: 0.45\n difficulty-fit: 0.20\n success-prob: 0.15\n novelty: 0.10\n quality: 0.10\n repeat-penalty: 0.15\n # 召回配置\n recall:\n weak-skill-ratio: 0.60\n difficulty-progression-ratio: 0.25\n exploration-ratio: 0.15\n # 多样性配置\n diversity:\n min-easy-count: 2\n min-medium-count: 4\n max-hard-count: 2\n # 缓存配置\n cache:\n recommendation-ttl-minutes: 30\n skill-profile-ttl-minutes: 60\ntap:\n security:\n jwt:\n issuer: ${JWT_ISSUER:tap}\n secret: ${JWT_SECRET:local_dev_only_jwt_secret_change_me_1234567890}\n access-token-ttl-seconds: ${JWT_TTL_SECONDS:3600}\n pta-credentials:\n secret: ${PTA_CREDENTIALS_SECRET:${JWT_SECRET:local_dev_only_jwt_secret_change_me_1234567890}}\n quota:\n translation-chars-per-day: ${QUOTA_TRANSLATION_CHARS_PER_DAY:200000}\n ai-requests-per-day: ${QUOTA_AI_REQUESTS_PER_DAY:200}\n admin-unlimited: ${QUOTA_ADMIN_UNLIMITED:true}\n ai:\n provider: ${AI_PROVIDER:mock}\n openai:\n base-url: ${OPENAI_BASE_URL:https://api.deepseek.com/v1}\n api-key: ${OPENAI_API_KEY:}\n model: ${OPENAI_MODEL:deepseek-chat}\n dashscope:\n base-url: ${DASHSCOPE_BASE_URL:https://dashscope.aliyuncs.com/compatible-mode/v1}\n api-key: ${DASHSCOPE_API_KEY:}\n model: ${DASHSCOPE_MODEL:qwen-vl-max-latest}\n arxiv:\n # Shared paper-search endpoint for all teacher users for now.\n # Later this should move to admin-managed configuration instead of static app config.\n enabled: ${ARXIV_API_ENABLED:true}\n search-base-url: ${ARXIV_SEARCH_BASE_URL:https://export.arxiv.org/api/query}\n api-key: ${ARXIV_API_KEY:}\n api-key-header: ${ARXIV_API_KEY_HEADER:}\n max-results: ${ARXIV_MAX_RESULTS:5}\n timeout-seconds: ${ARXIV_TIMEOUT_SECONDS:8}\n teacher:\n read-path:\n unified-experiment-queries-enabled: ${TAP_TEACHER_UNIFIED_EXPERIMENT_QUERIES_ENABLED:true}\n unified-submission-detail-enabled: ${TAP_TEACHER_UNIFIED_SUBMISSION_DETAIL_ENABLED:true}\n submission-detail-legacy-code-fallback-enabled: ${TAP_TEACHER_SUBMISSION_DETAIL_LEGACY_CODE_FALLBACK_ENABLED:true}\n submission-detail-legacy-report-fallback-enabled: ${TAP_TEACHER_SUBMISSION_DETAIL_LEGACY_REPORT_FALLBACK_ENABLED:true}\n submission-detail-legacy-ai-remarks-fallback-enabled: ${TAP_TEACHER_SUBMISSION_DETAIL_LEGACY_AI_REMARKS_FALLBACK_ENABLED:true}\n documents:\n extracted-text-max-chars: ${DOC_EXTRACT_MAX_CHARS:20000}\n store-full-extracted-text-to-minio: ${DOC_STORE_FULL_TEXT_TO_MINIO:true}\n ocr-enabled: ${DOC_OCR_ENABLED:true}\n ocr-command: ${DOC_OCR_COMMAND:tesseract}\n ocr-language: ${DOC_OCR_LANGUAGE:chi_sim+eng}\n ocr-max-pages: ${DOC_OCR_MAX_PAGES:24}\n vlm-fallback-enabled: ${DOC_VLM_FALLBACK_ENABLED:true}\n vlm-max-pages: ${DOC_VLM_MAX_PAGES:8}\n agent:\n job-max-concurrency: ${AGENT_JOB_MAX_CONCURRENCY:4}\n doc-max-concurrency: ${AGENT_DOC_MAX_CONCURRENCY:4}\n poll-interval-ms: ${AGENT_POLL_INTERVAL_MS:2000}\n doc-task-max-retries: ${AGENT_DOC_TASK_MAX_RETRIES:3}\n zip-organize:\n job-max-concurrency: ${ZIP_ORGANIZE_JOB_MAX_CONCURRENCY:1}\n item-max-concurrency: ${ZIP_ORGANIZE_ITEM_MAX_CONCURRENCY:3}\n poll-interval-ms: ${ZIP_ORGANIZE_POLL_INTERVAL_MS:2000}\n max-files: ${ZIP_ORGANIZE_MAX_FILES:120}\n max-zip-bytes: ${ZIP_ORGANIZE_MAX_ZIP_BYTES:52428800}\n extract-text-max-chars: ${ZIP_ORGANIZE_EXTRACT_TEXT_MAX_CHARS:8000}\n ai-text-max-chars: ${ZIP_ORGANIZE_AI_TEXT_MAX_CHARS:5000}\n retry-limit: ${ZIP_ORGANIZE_RETRY_LIMIT:2}\n review-threshold: ${ZIP_ORGANIZE_REVIEW_THRESHOLD:0.6}\n translation:\n cache-ttl-seconds: ${TRANS_CACHE_TTL_SECONDS:604800}\n provider: ${TRANS_PROVIDER:deepl}\n deepl:\n api-key: ${DEEPL_API_KEY:}\n base-url: ${DEEPL_BASE_URL:https://api-free.deepl.com}\n max-batch-size: ${DEEPL_MAX_BATCH_SIZE:40}\n min-interval-ms: ${DEEPL_MIN_INTERVAL_MS:0}\n storage:\n s3:\n endpoint: ${MINIO_ENDPOINT:http://localhost:9000}\n access-key: ${MINIO_ACCESS_KEY:minioadmin}\n secret-key: ${MINIO_SECRET_KEY:minioadmin}\n bucket: ${MINIO_BUCKET:tap-files}\n rag:\n maintenance-mode: ${RAG_MAINTENANCE_MODE:false}\n langchain4j:\n enabled: ${RAG_LANGCHAIN4J_ENABLED:true}\n use-streaming: ${RAG_LANGCHAIN4J_USE_STREAMING:true}\n use-embedding-model: ${RAG_LANGCHAIN4J_USE_EMBEDDING_MODEL:true}\n chat:\n base-url: ${RAG_LANGCHAIN4J_CHAT_BASE_URL:}\n api-key: ${RAG_LANGCHAIN4J_CHAT_API_KEY:}\n model-name: ${RAG_LANGCHAIN4J_CHAT_MODEL_NAME:}\n timeout-seconds: ${RAG_LANGCHAIN4J_CHAT_TIMEOUT_SECONDS:120}\n embedding:\n base-url: ${RAG_LANGCHAIN4J_EMBEDDING_BASE_URL:${RAG_DASHSCOPE_BASE_URL:${DASHSCOPE_BASE_URL:https://dashscope.aliyuncs.com/compatible-mode/v1}}}\n api-key: ${RAG_LANGCHAIN4J_EMBEDDING_API_KEY:${DASHSCOPE_API_KEY:}}\n model-name: ${RAG_LANGCHAIN4J_EMBEDDING_MODEL_NAME:text-embedding-v3}\n dimensions: ${RAG_LANGCHAIN4J_EMBEDDING_DIMENSIONS:1024}\n timeout-seconds: ${RAG_LANGCHAIN4J_EMBEDDING_TIMEOUT_SECONDS:30}\n dashscope:\n api-key: ${DASHSCOPE_API_KEY:}\n base-url: ${RAG_DASHSCOPE_BASE_URL:${DASHSCOPE_BASE_URL:https://dashscope.aliyuncs.com/compatible-mode/v1}}\n embedding-model: text-embedding-v3\n embedding-dimensions: 1024\n milvus:\n host: ${MILVUS_HOST:127.0.0.1}\n port: ${MILVUS_PORT:19530}\n collection: course_chunks\n retrieval:\n top-k: 20\n top-parent: 5\n score-threshold: 0.5\n fusion:\n alpha: 0.5\n beta: 0.3\n gamma: 0.1\n delta: 0.1\n rerank:\n enabled: true\n provider: heuristic\n endpoint: ${RAG_RERANK_ENDPOINT:http://127.0.0.1:8101/rerank}\n timeout-ms: 2500\n cross-encoder-weight: 0.7\n top-n: 5\n base-weight: 0.45\n overlap-weight: 0.4\n phrase-weight: 0.1\n annotation-weight: 0.05\n mmr:\n lambda: 0.7\n coverage:\n threshold: 0.30\n evidence:\n max-sentences: 8\n token-ratio-min: 0.25\n token-ratio-max: 0.40\n web:\n tavily-api-key: ${TAVILY_API_KEY:}\n enabled: false\n lucene:\n index-path: ${LUCENE_INDEX_PATH:./data/lucene-index}\n grading:\n export:\n max-concurrency: ${GRADING_EXPORT_MAX_CONCURRENCY:6}\n submit-interval-ms: ${GRADING_EXPORT_SUBMIT_INTERVAL_MS:30}\n\n# PTA spider service\npta:\n spider-url: ${PTA_SPIDER_URL:http://127.0.0.1:8100}\n connect-timeout-ms: ${PTA_SPIDER_CONNECT_TIMEOUT_MS:5000}\n read-timeout-ms: ${PTA_SPIDER_READ_TIMEOUT_MS:20000}\n scheduler:\n max-concurrency: ${PTA_SYNC_MAX_CONCURRENCY:3}\n submit-interval-ms: ${PTA_SYNC_SUBMIT_INTERVAL_MS:200}\n auto-start: ${PTA_SPIDER_AUTO_START:false}\n start-script: ${PTA_SPIDER_START_SCRIPT:../services/pta_spider/start_spider_api.ps1}\n start-timeout-seconds: ${PTA_SPIDER_START_TIMEOUT_SECONDS:25}\n auto-stop: ${PTA_SPIDER_AUTO_STOP:true}\n stop-script: ${PTA_SPIDER_STOP_SCRIPT:../services/pta_spider/stop_spider_api.ps1}\n stop-timeout-seconds: ${PTA_SPIDER_STOP_TIMEOUT_SECONDS:12}\n"}}}}