Files

1 line
81 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "1807676448/waterweb", "files": {"src/services/deepseekService.js": {"11d10e70b890dda170299d298f7eebb2b0e24dee": {"t": 1785745533.480059, "n": 12784, "h": "31abacc4d386c898", "c": "const { execFile } = require('child_process');\nconst { promisify } = require('util');\nconst config = require('../config');\nconst { latestWaterQuality } = require('./deviceService');\nconst { run, get } = require('../db');\n\nconst execFileAsync = promisify(execFile);\n\n// Retry transient upstream/network issues to reduce random ECONNRESET failures.\nconst RETRYABLE_ERROR_CODES = new Set([\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'ECONNABORTED',\n 'EPIPE',\n 'ENETRESET',\n 'EAI_AGAIN'\n]);\nconst REQUEST_TIMEOUT_MS = 120000;\nconst MAX_RETRIES = 2;\nconst RETRY_BASE_DELAY_MS = 800;\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction statusFromError(error) {\n return Number(error?.status || error?.response?.status || 0);\n}\n\nfunction shouldRetryDeepseekRequest(error) {\n const code = String(error?.code || '').toUpperCase();\n const status = statusFromError(error);\n const message = String(error?.message || '').toLowerCase();\n\n if (status === 429 || status >= 500) {\n return true;\n }\n if (RETRYABLE_ERROR_CODES.has(code)) {\n return true;\n }\n return (\n message.includes('aborted')\n || message.includes('socket hang up')\n || message.includes('terminated')\n || message.includes('network')\n );\n}\n\nfunction buildRequestBody(prompt) {\n const now = new Date();\n // 东八区北京时间\n const beijingOffset = 8 * 60 * 60 * 1000;\n const beijingTime = new Date(now.getTime() + beijingOffset);\n const currentDateStr = beijingTime.toISOString().replace('T', ' ').slice(0, 19);\n\n return {\n model: config.deepseek.model || 'deepseek-v4-flash',\n messages: [\n {\n role: 'system',\n content: `你是一个专业的水质分析专家。当前北京时间是 ${currentDateStr} (UTC+8)。请根据提供的传感器数据(TDS、COD、TOC、pH、温度、浊度等)输出中文 Markdown 报告,包含趋势、异常、原因和建议。报告生成时间使用北京时间,数据采集时间以数据中的实际时间戳换算为北京时间,不要编造日期。`\n },\n {\n role: 'user',\n content: `以下是最近的水质检测数据,请分析健康状况并给出建议:\\n${JSON.stringify(prompt, null, 2)}`\n }\n ],\n temperature: 0.3,\n max_tokens: 2048,\n thinking: { type: 'disabled' }\n };\n}\n\nasync function requestWithFetch(url, body) {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${config.deepseek.apiKey}`.trim(),\n 'Content-Type': 'application/json',\n // Keep response simple on Node 18 to reduce stream abort risk.\n 'Accept-Encoding': 'identity'\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)\n });\n\n const text = await response.text();\n if (!response.ok) {\n const error = new Error(`HTTP ${response.status}: ${text.slice(0, 400)}`);\n error.status = response.status;\n throw error;\n }\n\n let parsed;\n try {\n parsed = JSON.parse(text);\n } catch (error) {\n const parseError = new Error(`DeepSeek 响应不是合法 JSON: ${text.slice(0, 160)}`);\n parseError.code = 'INVALID_JSON';\n throw parseError;\n }\n\n return { data: parsed };\n}\n\nasync function requestWithCurl(url, body) {\n const timeoutSec = Math.ceil(REQUEST_TIMEOUT_MS / 1000);\n const args = [\n '-sS',\n '--connect-timeout',\n '10',\n '--max-time',\n String(timeoutSec),\n '-X',\n 'POST',\n '-H',\n 'Content-Type: application/json',\n '-H',\n `Authorization: Bearer ${String(config.deepseek.apiKey || '').trim()}`,\n '--data',\n JSON.stringify(body),\n '-w',\n '\\n%{http_code}',\n url\n ];\n\n const { stdout, stderr } = await execFileAsync('curl', args, {\n timeout: REQUEST_TIMEOUT_MS + 5000,\n maxBuffer: 1024 * 1024 * 5\n });\n\n if (stderr && stderr.trim()) {\n console.warn(`[deepseek] curl stderr: ${stderr.trim()}`);\n }\n\n const normalized = String(stdout || '');\n const cutIndex = normalized.lastIndexOf('\\n');\n const bodyText = cutIndex >= 0 ? normalized.slice(0, cutIndex) : normalized;\n const statusText = cutIndex >= 0 ? normalized.slice(cutIndex + 1).trim() : '';\n const status = Number(statusText || 0);\n\n if (!status || status >= 400) {\n const error = new Error(`curl HTTP ${status || 'N/A'}: ${bodyText.slice(0, 400)}`);\n error.status = status || 0;\n throw error;\n }\n\n let parsed;\n try {\n parsed = JSON.parse(bodyText);\n } catch (error) {\n const parseError = new Error(`curl 返回非 JSON: ${bodyText.slice(0, 160)}`);\n parseError.code = 'INVALID_JSON';\n throw parseError;\n }\n\n return { data: parsed };\n}\n\nasync function requestDeepseekWithRetry(prompt) {\n const totalAttempts = MAX_RETRIES + 1;\n const url = `${config.deepseek.baseUrl.replace(/\\/$/, '')}/chat/completions`;\n const requestBody = buildRequestBody(prompt);\n\n for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {\n try {\n console.log(`[deepseek] request attempt=${attempt}/${totalAttempts} via fetch`);\n return await requestWithFetch(url, requestBody);\n } catch (fetchError) {\n const code = String(fetchError?.code || 'FETCH_ERROR');\n const status = statusFromError(fetchError) || 'N/A';\n const message = String(fetchError?.message || 'unknown');\n console.warn(`[deepseek] fetch failed attempt=${attempt}/${totalAttempts} code=${code} status=${status} message=${message}`);\n\n try {\n console.log(`[deepseek] request attempt=${attempt}/${totalAttempts} fallback=curl`);\n return await requestWithCurl(url, requestBody);\n } catch (curlError) {\n const curlCode = String(curlError?.code || 'CURL_ERROR');\n const curlStatus = statusFromError(curlError) || 'N/A';\n const curlMessage = String(curlError?.message || 'unknown');\n const hasNextAttempt = attempt < totalAttempts;\n const retryable = shouldRetryDeepseekRequest(curlError);\n\n console.warn(\n `[deepseek] curl failed attempt=${attempt}/${totalAttempts} code=${curlCode} status=${curlStatus} message=${curlMessage}`\n );\n\n if (!retryable || !hasNextAttempt) {\n throw curlError;\n }\n\n const delayMs = RETRY_BASE_DELAY_MS * (2 ** (attempt - 1));\n await sleep(delayMs);\n }\n }\n }\n\n throw new Error('DeepSeek request failed without a concrete error');\n}\n\nasync function saveAnalysisReport({ deviceId, model, sampleCount, message, rawData }) {\n const result = await run(\n `INSERT INTO analysis_reports (\n device_id, model, sample_count, message_markdown, raw_data_json, created_at\n ) VALUES (?, ?, ?, ?, ?, datetime('now'))`,\n [\n deviceId || null,\n model,\n sampleCount,\n message,\n rawData ? JSON.stringify(rawData) : null\n ]\n );\n\n return result.id;\n}\n\nasync function latestAnalysisReport(deviceId) {\n const where = deviceId ? 'WHERE device_id = ?' : '';\n const params = deviceId ? [deviceId] : [];\n\n const row = await get(\n `SELECT id, device_id, model, sample_count, message_markdown, created_at\n FROM analysis_reports\n ${where}\n ORDER BY created_at DESC, id DESC\n LIMIT 1`,\n params\n );\n\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n device_id: row.device_id,\n model: row.model,\n sampleCount: row.sample_count,\n message: row.message_markdown,\n created_at: row.created_at\n };\n}\n\n// ──────────────────────────────────────────────\n// 嵌入式设备 dshelp 专用:构建请求体\n// ──────────────────────────────────────────────\nfunction buildEmbeddedRequestBody(dataSummary) {\n const now = new Date();\n const beijingOffset = 8 * 60 * 60 * 1000;\n const beijingTime = new Date(now.getTime() + beijingOffset);\n const currentDateStr = beijingTime.toISOString().replace('T', ' ').slice(0, 19);\n\n return {\n model: config.deepseek.model || 'deepseek-v4-flash',\n messages: [\n {\n role: 'system',\n content: [\n '你是水质分析助手。严格按以下格式输出,总字数不超过80个汉字。',\n '',\n '格式要求:',\n '【水质概况】一句话概括整体水质,约15字。',\n '【关键指标】列出2-3个核心指标及数值,约25字。',\n '【建议】给出1条最核心建议,约15字。',\n '其余字数用于补充。',\n '',\n '硬性约束:',\n '1. 只能使用常用汉字、阿拉伯数字、中文标点(,。!?)、℃和%。',\n '2. 禁止使用英文、拼音、Markdown、HTML、特殊符号(如*#`~)。',\n '3. 数字后不带单位汉字时使用中文缩写(如\"毫克每升\"写作\"mg/L\"是禁止的,应写\"123\"即可)。',\n '4. 如果数据不足或无明显异常,如实简短说明,不要编造。',\n '5. 当前北京时间: ' + currentDateStr + '(仅用于判断数据时效,不要输出到分析中)。'\n ].join('\\n')\n },\n {\n role: 'user',\n content: '以下为最近5次水质监测数据,请按要求输出分析:\\n' + dataSummary\n }\n ],\n temperature: 0.2,\n max_tokens: 256,\n thinking: { type: 'disabled' }\n };\n}\n\n// ──────────────────────────────────────────────\n// 清洗 AI 返回文本,适配嵌入式显示\n// ──────────────────────────────────────────────\nfunction sanitizeEmbeddedAnalysis(rawText) {\n if (!rawText) return '';\n\n let text = String(rawText);\n\n // 去掉 Markdown 标记\n text = text.replace(/[*#`~_>|]/g, '');\n // 去掉 HTML 标签\n text = text.replace(/<[^>]*>/g, '');\n // 去掉多余空白\n text = text.replace(/\\s+/g, ' ').trim();\n // 去掉英文单词(保留数字和中文标点)\n // 去掉可能残留的英文及拼音(保留中文、数字、常用标点、℃%)\n text = text.replace(/[^\\u4e00-\\u9fff\\u3000-\\u303f\\uff00-\\uffefa-zA-Z0-9,。!?、:;℃%%.\\-+ ]/g, '');\n // 压缩多余空格\n text = text.replace(/ {2,}/g, ' ').trim();\n\n // 按汉字计数截断到约80字\n let charCount = 0;\n let result = '';\n for (const ch of text) {\n if (/[\\u4e00-\\u9fff]/.test(ch)) {\n charCount += 1;\n }\n result += ch;\n if (charCount >= 80) break;\n }\n\n return result;\n}\n\n// ──────────────────────────────────────────────\n// 嵌入式设备分析:取最近5条数据 + DeepSeek 精简输出\n// ──────────────────────────────────────────────\nasync function analyzeForEmbedded(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 5);\n\n if (!latestData.length) {\n return {\n ok: false,\n analysis: '暂无水质数据,请先上报数据。',\n sample_count: 0\n };\n }\n\n if (!config.deepseek.apiKey) {\n return {\n ok: false,\n analysis: '未配置AI密钥,请联系管理员。',\n sample_count: latestData.length\n };\n }\n\n // 构建精简的数据摘要供 AI 分析\n const dataSummary = latestData\n .map((row, idx) => {\n const time = (row.created_at || '').slice(0, 19);\n return [\n `#${idx + 1} ${time}`,\n `TDS=${row.tds ?? '-'} COD=${row.cod ?? '-'} TOC=${row.toc ?? '-'}`,\n `pH=${row.ph ?? '-'} 水温=${row.tem ?? '-'} 浊度=${row.tur ?? '-'}`,\n `气温=${row.air_temp ?? '-'} 湿度=${row.air_hum ?? '-'}`\n ].join(' | ');\n })\n .join('\\n');\n\n const url = `${config.deepseek.baseUrl.replace(/\\/$/, '')}/chat/completions`;\n const requestBody = buildEmbeddedRequestBody(dataSummary);\n\n // 简化的两路重试(fetch → curl)\n let response;\n try {\n console.log('[deepseek:dshelp] request via fetch');\n response = await requestWithFetch(url, requestBody);\n } catch (fetchError) {\n console.warn('[deepseek:dshelp] fetch failed, fallback curl:', fetchError.message);\n try {\n response = await requestWithCurl(url, requestBody);\n } catch (curlError) {\n console.error('[deepseek:dshelp] curl also failed:', curlError.message);\n return {\n ok: false,\n analysis: 'AI服务暂时不可用,请稍后重试。',\n sample_count: latestData.length\n };\n }\n }\n\n const rawMessage = response.data?.choices?.[0]?.message?.content || '';\n const analysis = sanitizeEmbeddedAnalysis(rawMessage);\n\n // 入库记录\n await saveAnalysisReport({\n deviceId,\n model: config.deepseek.model,\n sampleCount: latestData.length,\n message: analysis,\n rawData: latestData\n });\n\n return {\n ok: true,\n analysis,\n sample_count: latestData.length,\n device_id: deviceId || ''\n };\n}\n\nasync function analyzeLatestWaterQuality(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 10);\n\n if (!latestData.length) {\n return {\n message: '暂无可分析的数据,请先接入设备上报。',\n sampleCount: 0,\n model: config.deepseek.model\n };\n }\n\n if (!config.deepseek.apiKey) {\n const localResult = {\n message: '未配置 DEEPSEEK_API_KEY,以下为本地提示:最近数据已接收,建议重点关注 pH、COD、TDS 的连续波动趋势。',\n sampleCount: latestData.length,\n model: 'local-fallback',\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: localResult.model,\n sampleCount: localResult.sampleCount,\n message: localResult.message,\n rawData: latestData\n });\n\n return {\n ...localResult,\n reportId\n };\n }\n\n const prompt = {\n instruction: '请基于以下最近10次水质监测数据,给出简明专业分析,包括整体趋势、异常指标、可能原因和处理建议。',\n data: latestData\n };\n\n const response = await requestDeepseekWithRetry(prompt);\n\n const message = response.data?.choices?.[0]?.message?.content || 'DeepSeek 未返回有效分析结果。';\n const result = {\n message,\n sampleCount: latestData.length,\n model: config.deepseek.model,\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: result.model,\n sampleCount: result.sampleCount,\n message: result.message,\n rawData: latestData\n });\n\n return {\n ...result,\n reportId\n };\n}\n\nmodule.exports = {\n analyzeLatestWaterQuality,\n latestAnalysisReport,\n analyzeForEmbedded\n};\n"}, "6058d9b8bbff007402e08e22cca54327b99deefd": {"t": 1785753963.7934322, "n": 8562, "h": "9b0b33581d5ebc9c", "c": "const { execFile } = require('child_process');\nconst { promisify } = require('util');\nconst config = require('../config');\nconst { latestWaterQuality } = require('./deviceService');\nconst { run, get } = require('../db');\n\nconst execFileAsync = promisify(execFile);\n\n// Retry transient upstream/network issues to reduce random ECONNRESET failures.\nconst RETRYABLE_ERROR_CODES = new Set([\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'ECONNABORTED',\n 'EPIPE',\n 'ENETRESET',\n 'EAI_AGAIN'\n]);\nconst REQUEST_TIMEOUT_MS = 120000;\nconst MAX_RETRIES = 2;\nconst RETRY_BASE_DELAY_MS = 800;\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction statusFromError(error) {\n return Number(error?.status || error?.response?.status || 0);\n}\n\nfunction shouldRetryDeepseekRequest(error) {\n const code = String(error?.code || '').toUpperCase();\n const status = statusFromError(error);\n const message = String(error?.message || '').toLowerCase();\n\n if (status === 429 || status >= 500) {\n return true;\n }\n if (RETRYABLE_ERROR_CODES.has(code)) {\n return true;\n }\n return (\n message.includes('aborted')\n || message.includes('socket hang up')\n || message.includes('terminated')\n || message.includes('network')\n );\n}\n\nfunction buildRequestBody(prompt) {\n const now = new Date();\n // 东八区北京时间\n const beijingOffset = 8 * 60 * 60 * 1000;\n const beijingTime = new Date(now.getTime() + beijingOffset);\n const currentDateStr = beijingTime.toISOString().replace('T', ' ').slice(0, 19);\n\n return {\n model: config.deepseek.model || 'deepseek-v4-flash',\n messages: [\n {\n role: 'system',\n content: `你是一个专业的水质分析专家。当前北京时间是 ${currentDateStr} (UTC+8)。请根据提供的传感器数据(TDS、COD、TOC、pH、温度、浊度等)输出中文 Markdown 报告,包含趋势、异常、原因和建议。报告生成时间使用北京时间,数据采集时间以数据中的实际时间戳换算为北京时间,不要编造日期。`\n },\n {\n role: 'user',\n content: `以下是最近的水质检测数据,请分析健康状况并给出建议:\\n${JSON.stringify(prompt, null, 2)}`\n }\n ],\n temperature: 0.3,\n max_tokens: 2048,\n thinking: { type: 'disabled' }\n };\n}\n\nasync function requestWithFetch(url, body) {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${config.deepseek.apiKey}`.trim(),\n 'Content-Type': 'application/json',\n // Keep response simple on Node 18 to reduce stream abort risk.\n 'Accept-Encoding': 'identity'\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)\n });\n\n const text = await response.text();\n if (!response.ok) {\n const error = new Error(`HTTP ${response.status}: ${text.slice(0, 400)}`);\n error.status = response.status;\n throw error;\n }\n\n let parsed;\n try {\n parsed = JSON.parse(text);\n } catch (error) {\n const parseError = new Error(`DeepSeek 响应不是合法 JSON: ${text.slice(0, 160)}`);\n parseError.code = 'INVALID_JSON';\n throw parseError;\n }\n\n return { data: parsed };\n}\n\nasync function requestWithCurl(url, body) {\n const timeoutSec = Math.ceil(REQUEST_TIMEOUT_MS / 1000);\n const args = [\n '-sS',\n '--connect-timeout',\n '10',\n '--max-time',\n String(timeoutSec),\n '-X',\n 'POST',\n '-H',\n 'Content-Type: application/json',\n '-H',\n `Authorization: Bearer ${String(config.deepseek.apiKey || '').trim()}`,\n '--data',\n JSON.stringify(body),\n '-w',\n '\\n%{http_code}',\n url\n ];\n\n const { stdout, stderr } = await execFileAsync('curl', args, {\n timeout: REQUEST_TIMEOUT_MS + 5000,\n maxBuffer: 1024 * 1024 * 5\n });\n\n if (stderr && stderr.trim()) {\n console.warn(`[deepseek] curl stderr: ${stderr.trim()}`);\n }\n\n const normalized = String(stdout || '');\n const cutIndex = normalized.lastIndexOf('\\n');\n const bodyText = cutIndex >= 0 ? normalized.slice(0, cutIndex) : normalized;\n const statusText = cutIndex >= 0 ? normalized.slice(cutIndex + 1).trim() : '';\n const status = Number(statusText || 0);\n\n if (!status || status >= 400) {\n const error = new Error(`curl HTTP ${status || 'N/A'}: ${bodyText.slice(0, 400)}`);\n error.status = status || 0;\n throw error;\n }\n\n let parsed;\n try {\n parsed = JSON.parse(bodyText);\n } catch (error) {\n const parseError = new Error(`curl 返回非 JSON: ${bodyText.slice(0, 160)}`);\n parseError.code = 'INVALID_JSON';\n throw parseError;\n }\n\n return { data: parsed };\n}\n\nasync function requestDeepseekWithRetry(prompt) {\n const totalAttempts = MAX_RETRIES + 1;\n const url = `${config.deepseek.baseUrl.replace(/\\/$/, '')}/chat/completions`;\n const requestBody = buildRequestBody(prompt);\n\n for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {\n try {\n console.log(`[deepseek] request attempt=${attempt}/${totalAttempts} via fetch`);\n return await requestWithFetch(url, requestBody);\n } catch (fetchError) {\n const code = String(fetchError?.code || 'FETCH_ERROR');\n const status = statusFromError(fetchError) || 'N/A';\n const message = String(fetchError?.message || 'unknown');\n console.warn(`[deepseek] fetch failed attempt=${attempt}/${totalAttempts} code=${code} status=${status} message=${message}`);\n\n try {\n console.log(`[deepseek] request attempt=${attempt}/${totalAttempts} fallback=curl`);\n return await requestWithCurl(url, requestBody);\n } catch (curlError) {\n const curlCode = String(curlError?.code || 'CURL_ERROR');\n const curlStatus = statusFromError(curlError) || 'N/A';\n const curlMessage = String(curlError?.message || 'unknown');\n const hasNextAttempt = attempt < totalAttempts;\n const retryable = shouldRetryDeepseekRequest(curlError);\n\n console.warn(\n `[deepseek] curl failed attempt=${attempt}/${totalAttempts} code=${curlCode} status=${curlStatus} message=${curlMessage}`\n );\n\n if (!retryable || !hasNextAttempt) {\n throw curlError;\n }\n\n const delayMs = RETRY_BASE_DELAY_MS * (2 ** (attempt - 1));\n await sleep(delayMs);\n }\n }\n }\n\n throw new Error('DeepSeek request failed without a concrete error');\n}\n\nasync function saveAnalysisReport({ deviceId, model, sampleCount, message, rawData }) {\n const result = await run(\n `INSERT INTO analysis_reports (\n device_id, model, sample_count, message_markdown, raw_data_json, created_at\n ) VALUES (?, ?, ?, ?, ?, datetime('now'))`,\n [\n deviceId || null,\n model,\n sampleCount,\n message,\n rawData ? JSON.stringify(rawData) : null\n ]\n );\n\n return result.id;\n}\n\nasync function latestAnalysisReport(deviceId) {\n const where = deviceId ? 'WHERE device_id = ?' : '';\n const params = deviceId ? [deviceId] : [];\n\n const row = await get(\n `SELECT id, device_id, model, sample_count, message_markdown, created_at\n FROM analysis_reports\n ${where}\n ORDER BY created_at DESC, id DESC\n LIMIT 1`,\n params\n );\n\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n device_id: row.device_id,\n model: row.model,\n sampleCount: row.sample_count,\n message: row.message_markdown,\n created_at: row.created_at\n };\n}\n\nasync function analyzeLatestWaterQuality(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 10);\n\n if (!latestData.length) {\n return {\n message: '暂无可分析的数据,请先接入设备上报。',\n sampleCount: 0,\n model: config.deepseek.model\n };\n }\n\n if (!config.deepseek.apiKey) {\n const localResult = {\n message: '未配置 DEEPSEEK_API_KEY,以下为本地提示:最近数据已接收,建议重点关注 pH、COD、TDS 的连续波动趋势。',\n sampleCount: latestData.length,\n model: 'local-fallback',\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: localResult.model,\n sampleCount: localResult.sampleCount,\n message: localResult.message,\n rawData: latestData\n });\n\n return {\n ...localResult,\n reportId\n };\n }\n\n const prompt = {\n instruction: '请基于以下最近10次水质监测数据,给出简明专业分析,包括整体趋势、异常指标、可能原因和处理建议。',\n data: latestData\n };\n\n const response = await requestDeepseekWithRetry(prompt);\n\n const message = response.data?.choices?.[0]?.message?.content || 'DeepSeek 未返回有效分析结果。';\n const result = {\n message,\n sampleCount: latestData.length,\n model: config.deepseek.model,\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: result.model,\n sampleCount: result.sampleCount,\n message: result.message,\n rawData: latestData\n });\n\n return {\n ...result,\n reportId\n };\n}\n\nmodule.exports = {\n analyzeLatestWaterQuality,\n latestAnalysisReport\n};\n"}, "506534f820d3edfd05e4fc19b32ca0eabd7f8e44": {"t": 1785753965.0641656, "n": 8420, "h": "453adc15f6a74a98", "c": "const { execFile } = require('child_process');\nconst { promisify } = require('util');\nconst config = require('../config');\nconst { latestWaterQuality } = require('./deviceService');\nconst { run, get } = require('../db');\n\nconst execFileAsync = promisify(execFile);\n\n// Retry transient upstream/network issues to reduce random ECONNRESET failures.\nconst RETRYABLE_ERROR_CODES = new Set([\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'ECONNABORTED',\n 'EPIPE',\n 'ENETRESET',\n 'EAI_AGAIN'\n]);\nconst REQUEST_TIMEOUT_MS = 120000;\nconst MAX_RETRIES = 2;\nconst RETRY_BASE_DELAY_MS = 800;\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction statusFromError(error) {\n return Number(error?.status || error?.response?.status || 0);\n}\n\nfunction shouldRetryDeepseekRequest(error) {\n const code = String(error?.code || '').toUpperCase();\n const status = statusFromError(error);\n const message = String(error?.message || '').toLowerCase();\n\n if (status === 429 || status >= 500) {\n return true;\n }\n if (RETRYABLE_ERROR_CODES.has(code)) {\n return true;\n }\n return (\n message.includes('aborted')\n || message.includes('socket hang up')\n || message.includes('terminated')\n || message.includes('network')\n );\n}\n\nfunction buildRequestBody(prompt) {\n const now = new Date();\n const currentDateStr = now.toISOString().replace('T', ' ').slice(0, 19);\n\n return {\n model: config.deepseek.model || 'deepseek-v4-flash',\n messages: [\n {\n role: 'system',\n content: `你是一个专业的水质分析专家。当前日期时间是 ${currentDateStr} UTC。请根据提供的传感器数据(TDS、COD、TOC、pH、温度、浊度等)输出中文 Markdown 报告,包含趋势、异常、原因和建议。报告生成时间和数据采集时间请以数据中的实际时间戳为准,不要编造日期。`\n },\n {\n role: 'user',\n content: `以下是最近的水质检测数据,请分析健康状况并给出建议:\\n${JSON.stringify(prompt, null, 2)}`\n }\n ],\n temperature: 0.3,\n max_tokens: 2048,\n thinking: { type: 'disabled' }\n };\n}\n\nasync function requestWithFetch(url, body) {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${config.deepseek.apiKey}`.trim(),\n 'Content-Type': 'application/json',\n // Keep response simple on Node 18 to reduce stream abort risk.\n 'Accept-Encoding': 'identity'\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)\n });\n\n const text = await response.text();\n if (!response.ok) {\n const error = new Error(`HTTP ${response.status}: ${text.slice(0, 400)}`);\n error.status = response.status;\n throw error;\n }\n\n let parsed;\n try {\n parsed = JSON.parse(text);\n } catch (error) {\n const parseError = new Error(`DeepSeek 响应不是合法 JSON: ${text.slice(0, 160)}`);\n parseError.code = 'INVALID_JSON';\n throw parseError;\n }\n\n return { data: parsed };\n}\n\nasync function requestWithCurl(url, body) {\n const timeoutSec = Math.ceil(REQUEST_TIMEOUT_MS / 1000);\n const args = [\n '-sS',\n '--connect-timeout',\n '10',\n '--max-time',\n String(timeoutSec),\n '-X',\n 'POST',\n '-H',\n 'Content-Type: application/json',\n '-H',\n `Authorization: Bearer ${String(config.deepseek.apiKey || '').trim()}`,\n '--data',\n JSON.stringify(body),\n '-w',\n '\\n%{http_code}',\n url\n ];\n\n const { stdout, stderr } = await execFileAsync('curl', args, {\n timeout: REQUEST_TIMEOUT_MS + 5000,\n maxBuffer: 1024 * 1024 * 5\n });\n\n if (stderr && stderr.trim()) {\n console.warn(`[deepseek] curl stderr: ${stderr.trim()}`);\n }\n\n const normalized = String(stdout || '');\n const cutIndex = normalized.lastIndexOf('\\n');\n const bodyText = cutIndex >= 0 ? normalized.slice(0, cutIndex) : normalized;\n const statusText = cutIndex >= 0 ? normalized.slice(cutIndex + 1).trim() : '';\n const status = Number(statusText || 0);\n\n if (!status || status >= 400) {\n const error = new Error(`curl HTTP ${status || 'N/A'}: ${bodyText.slice(0, 400)}`);\n error.status = status || 0;\n throw error;\n }\n\n let parsed;\n try {\n parsed = JSON.parse(bodyText);\n } catch (error) {\n const parseError = new Error(`curl 返回非 JSON: ${bodyText.slice(0, 160)}`);\n parseError.code = 'INVALID_JSON';\n throw parseError;\n }\n\n return { data: parsed };\n}\n\nasync function requestDeepseekWithRetry(prompt) {\n const totalAttempts = MAX_RETRIES + 1;\n const url = `${config.deepseek.baseUrl.replace(/\\/$/, '')}/chat/completions`;\n const requestBody = buildRequestBody(prompt);\n\n for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {\n try {\n console.log(`[deepseek] request attempt=${attempt}/${totalAttempts} via fetch`);\n return await requestWithFetch(url, requestBody);\n } catch (fetchError) {\n const code = String(fetchError?.code || 'FETCH_ERROR');\n const status = statusFromError(fetchError) || 'N/A';\n const message = String(fetchError?.message || 'unknown');\n console.warn(`[deepseek] fetch failed attempt=${attempt}/${totalAttempts} code=${code} status=${status} message=${message}`);\n\n try {\n console.log(`[deepseek] request attempt=${attempt}/${totalAttempts} fallback=curl`);\n return await requestWithCurl(url, requestBody);\n } catch (curlError) {\n const curlCode = String(curlError?.code || 'CURL_ERROR');\n const curlStatus = statusFromError(curlError) || 'N/A';\n const curlMessage = String(curlError?.message || 'unknown');\n const hasNextAttempt = attempt < totalAttempts;\n const retryable = shouldRetryDeepseekRequest(curlError);\n\n console.warn(\n `[deepseek] curl failed attempt=${attempt}/${totalAttempts} code=${curlCode} status=${curlStatus} message=${curlMessage}`\n );\n\n if (!retryable || !hasNextAttempt) {\n throw curlError;\n }\n\n const delayMs = RETRY_BASE_DELAY_MS * (2 ** (attempt - 1));\n await sleep(delayMs);\n }\n }\n }\n\n throw new Error('DeepSeek request failed without a concrete error');\n}\n\nasync function saveAnalysisReport({ deviceId, model, sampleCount, message, rawData }) {\n const result = await run(\n `INSERT INTO analysis_reports (\n device_id, model, sample_count, message_markdown, raw_data_json, created_at\n ) VALUES (?, ?, ?, ?, ?, datetime('now'))`,\n [\n deviceId || null,\n model,\n sampleCount,\n message,\n rawData ? JSON.stringify(rawData) : null\n ]\n );\n\n return result.id;\n}\n\nasync function latestAnalysisReport(deviceId) {\n const where = deviceId ? 'WHERE device_id = ?' : '';\n const params = deviceId ? [deviceId] : [];\n\n const row = await get(\n `SELECT id, device_id, model, sample_count, message_markdown, created_at\n FROM analysis_reports\n ${where}\n ORDER BY created_at DESC, id DESC\n LIMIT 1`,\n params\n );\n\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n device_id: row.device_id,\n model: row.model,\n sampleCount: row.sample_count,\n message: row.message_markdown,\n created_at: row.created_at\n };\n}\n\nasync function analyzeLatestWaterQuality(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 10);\n\n if (!latestData.length) {\n return {\n message: '暂无可分析的数据,请先接入设备上报。',\n sampleCount: 0,\n model: config.deepseek.model\n };\n }\n\n if (!config.deepseek.apiKey) {\n const localResult = {\n message: '未配置 DEEPSEEK_API_KEY,以下为本地提示:最近数据已接收,建议重点关注 pH、COD、TDS 的连续波动趋势。',\n sampleCount: latestData.length,\n model: 'local-fallback',\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: localResult.model,\n sampleCount: localResult.sampleCount,\n message: localResult.message,\n rawData: latestData\n });\n\n return {\n ...localResult,\n reportId\n };\n }\n\n const prompt = {\n instruction: '请基于以下最近10次水质监测数据,给出简明专业分析,包括整体趋势、异常指标、可能原因和处理建议。',\n data: latestData\n };\n\n const response = await requestDeepseekWithRetry(prompt);\n\n const message = response.data?.choices?.[0]?.message?.content || 'DeepSeek 未返回有效分析结果。';\n const result = {\n message,\n sampleCount: latestData.length,\n model: config.deepseek.model,\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: result.model,\n sampleCount: result.sampleCount,\n message: result.message,\n rawData: latestData\n });\n\n return {\n ...result,\n reportId\n };\n}\n\nmodule.exports = {\n analyzeLatestWaterQuality,\n latestAnalysisReport\n};\n"}, "2ef90696cd91683bfac0dc59573d3fd4a5261e5b": {"t": 1785753966.7576222, "n": 8254, "h": "1aac5d39b793a2ec", "c": "const { execFile } = require('child_process');\nconst { promisify } = require('util');\nconst config = require('../config');\nconst { latestWaterQuality } = require('./deviceService');\nconst { run, get } = require('../db');\n\nconst execFileAsync = promisify(execFile);\n\n// Retry transient upstream/network issues to reduce random ECONNRESET failures.\nconst RETRYABLE_ERROR_CODES = new Set([\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'ECONNABORTED',\n 'EPIPE',\n 'ENETRESET',\n 'EAI_AGAIN'\n]);\nconst REQUEST_TIMEOUT_MS = 120000;\nconst MAX_RETRIES = 2;\nconst RETRY_BASE_DELAY_MS = 800;\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction statusFromError(error) {\n return Number(error?.status || error?.response?.status || 0);\n}\n\nfunction shouldRetryDeepseekRequest(error) {\n const code = String(error?.code || '').toUpperCase();\n const status = statusFromError(error);\n const message = String(error?.message || '').toLowerCase();\n\n if (status === 429 || status >= 500) {\n return true;\n }\n if (RETRYABLE_ERROR_CODES.has(code)) {\n return true;\n }\n return (\n message.includes('aborted')\n || message.includes('socket hang up')\n || message.includes('terminated')\n || message.includes('network')\n );\n}\n\nfunction buildRequestBody(prompt) {\n return {\n model: config.deepseek.model || 'deepseek-v4-flash',\n messages: [\n {\n role: 'system',\n content: '你是一个专业的水质分析专家。请根据提供的传感器数据(TDS、COD、TOC、pH、温度、浊度等)输出中文 Markdown 报告,包含趋势、异常、原因和建议。'\n },\n {\n role: 'user',\n content: `以下是最近的水质检测数据,请分析健康状况并给出建议:\\n${JSON.stringify(prompt, null, 2)}`\n }\n ],\n temperature: 0.3,\n max_tokens: 2048,\n thinking: { type: 'disabled' }\n };\n}\n\nasync function requestWithFetch(url, body) {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${config.deepseek.apiKey}`.trim(),\n 'Content-Type': 'application/json',\n // Keep response simple on Node 18 to reduce stream abort risk.\n 'Accept-Encoding': 'identity'\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)\n });\n\n const text = await response.text();\n if (!response.ok) {\n const error = new Error(`HTTP ${response.status}: ${text.slice(0, 400)}`);\n error.status = response.status;\n throw error;\n }\n\n let parsed;\n try {\n parsed = JSON.parse(text);\n } catch (error) {\n const parseError = new Error(`DeepSeek 响应不是合法 JSON: ${text.slice(0, 160)}`);\n parseError.code = 'INVALID_JSON';\n throw parseError;\n }\n\n return { data: parsed };\n}\n\nasync function requestWithCurl(url, body) {\n const timeoutSec = Math.ceil(REQUEST_TIMEOUT_MS / 1000);\n const args = [\n '-sS',\n '--connect-timeout',\n '10',\n '--max-time',\n String(timeoutSec),\n '-X',\n 'POST',\n '-H',\n 'Content-Type: application/json',\n '-H',\n `Authorization: Bearer ${String(config.deepseek.apiKey || '').trim()}`,\n '--data',\n JSON.stringify(body),\n '-w',\n '\\n%{http_code}',\n url\n ];\n\n const { stdout, stderr } = await execFileAsync('curl', args, {\n timeout: REQUEST_TIMEOUT_MS + 5000,\n maxBuffer: 1024 * 1024 * 5\n });\n\n if (stderr && stderr.trim()) {\n console.warn(`[deepseek] curl stderr: ${stderr.trim()}`);\n }\n\n const normalized = String(stdout || '');\n const cutIndex = normalized.lastIndexOf('\\n');\n const bodyText = cutIndex >= 0 ? normalized.slice(0, cutIndex) : normalized;\n const statusText = cutIndex >= 0 ? normalized.slice(cutIndex + 1).trim() : '';\n const status = Number(statusText || 0);\n\n if (!status || status >= 400) {\n const error = new Error(`curl HTTP ${status || 'N/A'}: ${bodyText.slice(0, 400)}`);\n error.status = status || 0;\n throw error;\n }\n\n let parsed;\n try {\n parsed = JSON.parse(bodyText);\n } catch (error) {\n const parseError = new Error(`curl 返回非 JSON: ${bodyText.slice(0, 160)}`);\n parseError.code = 'INVALID_JSON';\n throw parseError;\n }\n\n return { data: parsed };\n}\n\nasync function requestDeepseekWithRetry(prompt) {\n const totalAttempts = MAX_RETRIES + 1;\n const url = `${config.deepseek.baseUrl.replace(/\\/$/, '')}/chat/completions`;\n const requestBody = buildRequestBody(prompt);\n\n for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {\n try {\n console.log(`[deepseek] request attempt=${attempt}/${totalAttempts} via fetch`);\n return await requestWithFetch(url, requestBody);\n } catch (fetchError) {\n const code = String(fetchError?.code || 'FETCH_ERROR');\n const status = statusFromError(fetchError) || 'N/A';\n const message = String(fetchError?.message || 'unknown');\n console.warn(`[deepseek] fetch failed attempt=${attempt}/${totalAttempts} code=${code} status=${status} message=${message}`);\n\n try {\n console.log(`[deepseek] request attempt=${attempt}/${totalAttempts} fallback=curl`);\n return await requestWithCurl(url, requestBody);\n } catch (curlError) {\n const curlCode = String(curlError?.code || 'CURL_ERROR');\n const curlStatus = statusFromError(curlError) || 'N/A';\n const curlMessage = String(curlError?.message || 'unknown');\n const hasNextAttempt = attempt < totalAttempts;\n const retryable = shouldRetryDeepseekRequest(curlError);\n\n console.warn(\n `[deepseek] curl failed attempt=${attempt}/${totalAttempts} code=${curlCode} status=${curlStatus} message=${curlMessage}`\n );\n\n if (!retryable || !hasNextAttempt) {\n throw curlError;\n }\n\n const delayMs = RETRY_BASE_DELAY_MS * (2 ** (attempt - 1));\n await sleep(delayMs);\n }\n }\n }\n\n throw new Error('DeepSeek request failed without a concrete error');\n}\n\nasync function saveAnalysisReport({ deviceId, model, sampleCount, message, rawData }) {\n const result = await run(\n `INSERT INTO analysis_reports (\n device_id, model, sample_count, message_markdown, raw_data_json, created_at\n ) VALUES (?, ?, ?, ?, ?, datetime('now'))`,\n [\n deviceId || null,\n model,\n sampleCount,\n message,\n rawData ? JSON.stringify(rawData) : null\n ]\n );\n\n return result.id;\n}\n\nasync function latestAnalysisReport(deviceId) {\n const where = deviceId ? 'WHERE device_id = ?' : '';\n const params = deviceId ? [deviceId] : [];\n\n const row = await get(\n `SELECT id, device_id, model, sample_count, message_markdown, created_at\n FROM analysis_reports\n ${where}\n ORDER BY created_at DESC, id DESC\n LIMIT 1`,\n params\n );\n\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n device_id: row.device_id,\n model: row.model,\n sampleCount: row.sample_count,\n message: row.message_markdown,\n created_at: row.created_at\n };\n}\n\nasync function analyzeLatestWaterQuality(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 10);\n\n if (!latestData.length) {\n return {\n message: '暂无可分析的数据,请先接入设备上报。',\n sampleCount: 0,\n model: config.deepseek.model\n };\n }\n\n if (!config.deepseek.apiKey) {\n const localResult = {\n message: '未配置 DEEPSEEK_API_KEY,以下为本地提示:最近数据已接收,建议重点关注 pH、COD、TDS 的连续波动趋势。',\n sampleCount: latestData.length,\n model: 'local-fallback',\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: localResult.model,\n sampleCount: localResult.sampleCount,\n message: localResult.message,\n rawData: latestData\n });\n\n return {\n ...localResult,\n reportId\n };\n }\n\n const prompt = {\n instruction: '请基于以下最近10次水质监测数据,给出简明专业分析,包括整体趋势、异常指标、可能原因和处理建议。',\n data: latestData\n };\n\n const response = await requestDeepseekWithRetry(prompt);\n\n const message = response.data?.choices?.[0]?.message?.content || 'DeepSeek 未返回有效分析结果。';\n const result = {\n message,\n sampleCount: latestData.length,\n model: config.deepseek.model,\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: result.model,\n sampleCount: result.sampleCount,\n message: result.message,\n rawData: latestData\n });\n\n return {\n ...result,\n reportId\n };\n}\n\nmodule.exports = {\n analyzeLatestWaterQuality,\n latestAnalysisReport\n};\n"}, "0919e97d409d0e6ae8f10c8bf3ea825b33544823": {"t": 1785753968.13207, "n": 8214, "h": "9aa296000cae7f6e", "c": "const { execFile } = require('child_process');\nconst { promisify } = require('util');\nconst config = require('../config');\nconst { latestWaterQuality } = require('./deviceService');\nconst { run, get } = require('../db');\n\nconst execFileAsync = promisify(execFile);\n\n// Retry transient upstream/network issues to reduce random ECONNRESET failures.\nconst RETRYABLE_ERROR_CODES = new Set([\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'ECONNABORTED',\n 'EPIPE',\n 'ENETRESET',\n 'EAI_AGAIN'\n]);\nconst REQUEST_TIMEOUT_MS = 120000;\nconst MAX_RETRIES = 2;\nconst RETRY_BASE_DELAY_MS = 800;\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction statusFromError(error) {\n return Number(error?.status || error?.response?.status || 0);\n}\n\nfunction shouldRetryDeepseekRequest(error) {\n const code = String(error?.code || '').toUpperCase();\n const status = statusFromError(error);\n const message = String(error?.message || '').toLowerCase();\n\n if (status === 429 || status >= 500) {\n return true;\n }\n if (RETRYABLE_ERROR_CODES.has(code)) {\n return true;\n }\n return (\n message.includes('aborted')\n || message.includes('socket hang up')\n || message.includes('terminated')\n || message.includes('network')\n );\n}\n\nfunction buildRequestBody(prompt) {\n return {\n model: config.deepseek.model || 'deepseek-chat',\n messages: [\n {\n role: 'system',\n content: '你是一个专业的水质分析专家。请根据提供的传感器数据(TDS、COD、TOC、pH、温度、浊度等)输出中文 Markdown 报告,包含趋势、异常、原因和建议。'\n },\n {\n role: 'user',\n content: `以下是最近的水质检测数据,请分析健康状况并给出建议:\\n${JSON.stringify(prompt, null, 2)}`\n }\n ],\n temperature: 0.3,\n max_tokens: 2048\n };\n}\n\nasync function requestWithFetch(url, body) {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${config.deepseek.apiKey}`.trim(),\n 'Content-Type': 'application/json',\n // Keep response simple on Node 18 to reduce stream abort risk.\n 'Accept-Encoding': 'identity'\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)\n });\n\n const text = await response.text();\n if (!response.ok) {\n const error = new Error(`HTTP ${response.status}: ${text.slice(0, 400)}`);\n error.status = response.status;\n throw error;\n }\n\n let parsed;\n try {\n parsed = JSON.parse(text);\n } catch (error) {\n const parseError = new Error(`DeepSeek 响应不是合法 JSON: ${text.slice(0, 160)}`);\n parseError.code = 'INVALID_JSON';\n throw parseError;\n }\n\n return { data: parsed };\n}\n\nasync function requestWithCurl(url, body) {\n const timeoutSec = Math.ceil(REQUEST_TIMEOUT_MS / 1000);\n const args = [\n '-sS',\n '--connect-timeout',\n '10',\n '--max-time',\n String(timeoutSec),\n '-X',\n 'POST',\n '-H',\n 'Content-Type: application/json',\n '-H',\n `Authorization: Bearer ${String(config.deepseek.apiKey || '').trim()}`,\n '--data',\n JSON.stringify(body),\n '-w',\n '\\n%{http_code}',\n url\n ];\n\n const { stdout, stderr } = await execFileAsync('curl', args, {\n timeout: REQUEST_TIMEOUT_MS + 5000,\n maxBuffer: 1024 * 1024 * 5\n });\n\n if (stderr && stderr.trim()) {\n console.warn(`[deepseek] curl stderr: ${stderr.trim()}`);\n }\n\n const normalized = String(stdout || '');\n const cutIndex = normalized.lastIndexOf('\\n');\n const bodyText = cutIndex >= 0 ? normalized.slice(0, cutIndex) : normalized;\n const statusText = cutIndex >= 0 ? normalized.slice(cutIndex + 1).trim() : '';\n const status = Number(statusText || 0);\n\n if (!status || status >= 400) {\n const error = new Error(`curl HTTP ${status || 'N/A'}: ${bodyText.slice(0, 400)}`);\n error.status = status || 0;\n throw error;\n }\n\n let parsed;\n try {\n parsed = JSON.parse(bodyText);\n } catch (error) {\n const parseError = new Error(`curl 返回非 JSON: ${bodyText.slice(0, 160)}`);\n parseError.code = 'INVALID_JSON';\n throw parseError;\n }\n\n return { data: parsed };\n}\n\nasync function requestDeepseekWithRetry(prompt) {\n const totalAttempts = MAX_RETRIES + 1;\n const url = `${config.deepseek.baseUrl.replace(/\\/$/, '')}/chat/completions`;\n const requestBody = buildRequestBody(prompt);\n\n for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {\n try {\n console.log(`[deepseek] request attempt=${attempt}/${totalAttempts} via fetch`);\n return await requestWithFetch(url, requestBody);\n } catch (fetchError) {\n const code = String(fetchError?.code || 'FETCH_ERROR');\n const status = statusFromError(fetchError) || 'N/A';\n const message = String(fetchError?.message || 'unknown');\n console.warn(`[deepseek] fetch failed attempt=${attempt}/${totalAttempts} code=${code} status=${status} message=${message}`);\n\n try {\n console.log(`[deepseek] request attempt=${attempt}/${totalAttempts} fallback=curl`);\n return await requestWithCurl(url, requestBody);\n } catch (curlError) {\n const curlCode = String(curlError?.code || 'CURL_ERROR');\n const curlStatus = statusFromError(curlError) || 'N/A';\n const curlMessage = String(curlError?.message || 'unknown');\n const hasNextAttempt = attempt < totalAttempts;\n const retryable = shouldRetryDeepseekRequest(curlError);\n\n console.warn(\n `[deepseek] curl failed attempt=${attempt}/${totalAttempts} code=${curlCode} status=${curlStatus} message=${curlMessage}`\n );\n\n if (!retryable || !hasNextAttempt) {\n throw curlError;\n }\n\n const delayMs = RETRY_BASE_DELAY_MS * (2 ** (attempt - 1));\n await sleep(delayMs);\n }\n }\n }\n\n throw new Error('DeepSeek request failed without a concrete error');\n}\n\nasync function saveAnalysisReport({ deviceId, model, sampleCount, message, rawData }) {\n const result = await run(\n `INSERT INTO analysis_reports (\n device_id, model, sample_count, message_markdown, raw_data_json, created_at\n ) VALUES (?, ?, ?, ?, ?, datetime('now'))`,\n [\n deviceId || null,\n model,\n sampleCount,\n message,\n rawData ? JSON.stringify(rawData) : null\n ]\n );\n\n return result.id;\n}\n\nasync function latestAnalysisReport(deviceId) {\n const where = deviceId ? 'WHERE device_id = ?' : '';\n const params = deviceId ? [deviceId] : [];\n\n const row = await get(\n `SELECT id, device_id, model, sample_count, message_markdown, created_at\n FROM analysis_reports\n ${where}\n ORDER BY created_at DESC, id DESC\n LIMIT 1`,\n params\n );\n\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n device_id: row.device_id,\n model: row.model,\n sampleCount: row.sample_count,\n message: row.message_markdown,\n created_at: row.created_at\n };\n}\n\nasync function analyzeLatestWaterQuality(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 10);\n\n if (!latestData.length) {\n return {\n message: '暂无可分析的数据,请先接入设备上报。',\n sampleCount: 0,\n model: config.deepseek.model\n };\n }\n\n if (!config.deepseek.apiKey) {\n const localResult = {\n message: '未配置 DEEPSEEK_API_KEY,以下为本地提示:最近数据已接收,建议重点关注 pH、COD、TDS 的连续波动趋势。',\n sampleCount: latestData.length,\n model: 'local-fallback',\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: localResult.model,\n sampleCount: localResult.sampleCount,\n message: localResult.message,\n rawData: latestData\n });\n\n return {\n ...localResult,\n reportId\n };\n }\n\n const prompt = {\n instruction: '请基于以下最近10次水质监测数据,给出简明专业分析,包括整体趋势、异常指标、可能原因和处理建议。',\n data: latestData\n };\n\n const response = await requestDeepseekWithRetry(prompt);\n\n const message = response.data?.choices?.[0]?.message?.content || 'DeepSeek 未返回有效分析结果。';\n const result = {\n message,\n sampleCount: latestData.length,\n model: config.deepseek.model,\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: result.model,\n sampleCount: result.sampleCount,\n message: result.message,\n rawData: latestData\n });\n\n return {\n ...result,\n reportId\n };\n}\n\nmodule.exports = {\n analyzeLatestWaterQuality,\n latestAnalysisReport\n};\n"}, "274d4288dce05be0cf4aeb55eaaedc6eca826ab7": {"t": 1785753970.5292802, "n": 5651, "h": "4f50f4b0b865f51d", "c": "const axios = require('axios');\nconst config = require('../config');\nconst { latestWaterQuality } = require('./deviceService');\nconst { run, get } = require('../db');\n\n// Retry transient upstream/network issues to reduce random ECONNRESET failures.\nconst RETRYABLE_ERROR_CODES = new Set([\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'ECONNABORTED',\n 'EPIPE',\n 'ENETRESET',\n 'EAI_AGAIN'\n]);\nconst REQUEST_TIMEOUT_MS = 120000;\nconst MAX_RETRIES = 2;\nconst RETRY_BASE_DELAY_MS = 800;\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction shouldRetryDeepseekRequest(error) {\n const code = String(error?.code || '').toUpperCase();\n const status = Number(error?.response?.status || 0);\n const message = String(error?.message || '').toLowerCase();\n\n if (status === 429 || (status >= 500 && status !== 504)) {\n return true;\n }\n if (RETRYABLE_ERROR_CODES.has(code)) {\n return true;\n }\n return message.includes('aborted') || message.includes('socket hang up');\n}\n\nasync function requestDeepseekWithRetry(prompt) {\n const totalAttempts = MAX_RETRIES + 1;\n\n for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {\n try {\n const payload = {\n model: config.deepseek.model || 'deepseek-chat',\n messages: [\n {\n role: 'system',\n content: '你是一个专业的水质分析专家。请根据提供的传感器数据(TDS, COD, TOC, pH, 温度, 浊度等),生成一份简明扼要的分析报告。要求:1. 使用中文;2. 结构清晰(可以使用 Markdown 标题和列表);3. 给出评价和建议。'\n },\n {\n role: 'user',\n content: `以下是最近的水质检测数据,请分析其健康状况并给出建议:\\n${JSON.stringify(prompt, null, 2)}`\n }\n ],\n temperature: 0.3,\n max_tokens: 2048\n };\n\n console.log(`[deepseek] sending request to ${config.deepseek.baseUrl}/chat/completions (attempt ${attempt})`);\n\n return await axios.post(\n `${config.deepseek.baseUrl}/chat/completions`,\n payload,\n {\n headers: {\n Authorization: `Bearer ${config.deepseek.apiKey}`.trim(),\n 'Content-Type': 'application/json',\n 'Accept-Encoding': 'identity',\n 'Connection': 'keep-alive',\n 'Cache-Control': 'no-cache',\n 'Pragma': 'no-cache'\n },\n timeout: REQUEST_TIMEOUT_MS,\n decompress: false,\n maxContentLength: Infinity,\n maxBodyLength: Infinity,\n // 强制使用 http 代理或禁用默认代理,防止内部库劫持\n proxy: false\n }\n );\n } catch (error) {\n const code = String(error?.code || 'UNKNOWN');\n const status = error?.response?.status ?? 'N/A';\n const message = String(error?.message || 'unknown');\n const retryable = shouldRetryDeepseekRequest(error);\n const hasNextAttempt = attempt < totalAttempts;\n\n console.warn(\n `[deepseek] request failed attempt=${attempt}/${totalAttempts} code=${code} status=${status} message=${message}`\n );\n\n if (!retryable || !hasNextAttempt) {\n throw error;\n }\n\n const delayMs = RETRY_BASE_DELAY_MS * (2 ** (attempt - 1));\n await sleep(delayMs);\n }\n }\n\n throw new Error('DeepSeek request failed without a concrete error');\n}\n\nasync function saveAnalysisReport({ deviceId, model, sampleCount, message, rawData }) {\n const result = await run(\n `INSERT INTO analysis_reports (\n device_id, model, sample_count, message_markdown, raw_data_json, created_at\n ) VALUES (?, ?, ?, ?, ?, datetime('now'))`,\n [\n deviceId || null,\n model,\n sampleCount,\n message,\n rawData ? JSON.stringify(rawData) : null\n ]\n );\n\n return result.id;\n}\n\nasync function latestAnalysisReport(deviceId) {\n const where = deviceId ? 'WHERE device_id = ?' : '';\n const params = deviceId ? [deviceId] : [];\n\n const row = await get(\n `SELECT id, device_id, model, sample_count, message_markdown, created_at\n FROM analysis_reports\n ${where}\n ORDER BY created_at DESC, id DESC\n LIMIT 1`,\n params\n );\n\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n device_id: row.device_id,\n model: row.model,\n sampleCount: row.sample_count,\n message: row.message_markdown,\n created_at: row.created_at\n };\n}\n\nasync function analyzeLatestWaterQuality(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 10);\n\n if (!latestData.length) {\n return {\n message: '暂无可分析的数据,请先接入设备上报。',\n sampleCount: 0,\n model: config.deepseek.model\n };\n }\n\n if (!config.deepseek.apiKey) {\n const localResult = {\n message: '未配置 DEEPSEEK_API_KEY,以下为本地提示:最近数据已接收,建议重点关注 pH、COD、TDS 的连续波动趋势。',\n sampleCount: latestData.length,\n model: 'local-fallback',\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: localResult.model,\n sampleCount: localResult.sampleCount,\n message: localResult.message,\n rawData: latestData\n });\n\n return {\n ...localResult,\n reportId\n };\n }\n\n const prompt = {\n instruction: '请基于以下最近10次水质监测数据,给出简明专业分析,包括整体趋势、异常指标、可能原因和处理建议。',\n data: latestData\n };\n\n const response = await requestDeepseekWithRetry(prompt);\n\n const message = response.data?.choices?.[0]?.message?.content || 'DeepSeek 未返回有效分析结果。';\n const result = {\n message,\n sampleCount: latestData.length,\n model: config.deepseek.model,\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: result.model,\n sampleCount: result.sampleCount,\n message: result.message,\n rawData: latestData\n });\n\n return {\n ...result,\n reportId\n };\n}\n\nmodule.exports = {\n analyzeLatestWaterQuality,\n latestAnalysisReport\n};\n"}, "459a384b9ec0633c2c4d98df9b59a272178b91d5": {"t": 1785753972.512322, "n": 5592, "h": "ab410e5897681bbf", "c": "const axios = require('axios');\nconst config = require('../config');\nconst { latestWaterQuality } = require('./deviceService');\nconst { run, get } = require('../db');\n\n// Retry transient upstream/network issues to reduce random ECONNRESET failures.\nconst RETRYABLE_ERROR_CODES = new Set([\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'ECONNABORTED',\n 'EPIPE',\n 'ENETRESET',\n 'EAI_AGAIN'\n]);\nconst REQUEST_TIMEOUT_MS = 120000;\nconst MAX_RETRIES = 2;\nconst RETRY_BASE_DELAY_MS = 800;\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction shouldRetryDeepseekRequest(error) {\n const code = String(error?.code || '').toUpperCase();\n const status = Number(error?.response?.status || 0);\n const message = String(error?.message || '').toLowerCase();\n\n if (status === 429 || (status >= 500 && status !== 504)) {\n return true;\n }\n if (RETRYABLE_ERROR_CODES.has(code)) {\n return true;\n }\n return message.includes('aborted') || message.includes('socket hang up');\n}\n\nasync function requestDeepseekWithRetry(prompt) {\n const totalAttempts = MAX_RETRIES + 1;\n\n for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {\n try {\n const payload = {\n model: config.deepseek.model || 'deepseek-chat',\n messages: [\n {\n role: 'system',\n content: '你是一个专业的水质分析专家。请根据提供的传感器数据(TDS, COD, TOC, pH, 温度, 浊度等),生成一份简明扼要的分析报告。要求:1. 使用中文;2. 结构清晰(可以使用 Markdown 标题和列表);3. 给出评价和建议。'\n },\n {\n role: 'user',\n content: `以下是最近的水质检测数据,请分析其健康状况并给出建议:\\n${JSON.stringify(prompt, null, 2)}`\n }\n ],\n temperature: 0.3,\n max_tokens: 2048\n };\n\n console.log(`[deepseek] sending request to ${config.deepseek.baseUrl}/chat/completions (attempt ${attempt})`);\n\n return await axios.post(\n `${config.deepseek.baseUrl}/chat/completions`,\n payload,\n {\n headers: {\n Authorization: `Bearer ${config.deepseek.apiKey}`.trim(),\n 'Content-Type': 'application/json',\n // 必须强制禁用压缩,Node 18 处理长响应压缩极不稳定\n 'Accept-Encoding': 'identity',\n 'Connection': 'keep-alive'\n },\n timeout: REQUEST_TIMEOUT_MS,\n // 增加以下配置防止 stream aborted\n decompress: false,\n maxContentLength: Infinity,\n maxBodyLength: Infinity\n }\n );\n } catch (error) {\n const code = String(error?.code || 'UNKNOWN');\n const status = error?.response?.status ?? 'N/A';\n const message = String(error?.message || 'unknown');\n const retryable = shouldRetryDeepseekRequest(error);\n const hasNextAttempt = attempt < totalAttempts;\n\n console.warn(\n `[deepseek] request failed attempt=${attempt}/${totalAttempts} code=${code} status=${status} message=${message}`\n );\n\n if (!retryable || !hasNextAttempt) {\n throw error;\n }\n\n const delayMs = RETRY_BASE_DELAY_MS * (2 ** (attempt - 1));\n await sleep(delayMs);\n }\n }\n\n throw new Error('DeepSeek request failed without a concrete error');\n}\n\nasync function saveAnalysisReport({ deviceId, model, sampleCount, message, rawData }) {\n const result = await run(\n `INSERT INTO analysis_reports (\n device_id, model, sample_count, message_markdown, raw_data_json, created_at\n ) VALUES (?, ?, ?, ?, ?, datetime('now'))`,\n [\n deviceId || null,\n model,\n sampleCount,\n message,\n rawData ? JSON.stringify(rawData) : null\n ]\n );\n\n return result.id;\n}\n\nasync function latestAnalysisReport(deviceId) {\n const where = deviceId ? 'WHERE device_id = ?' : '';\n const params = deviceId ? [deviceId] : [];\n\n const row = await get(\n `SELECT id, device_id, model, sample_count, message_markdown, created_at\n FROM analysis_reports\n ${where}\n ORDER BY created_at DESC, id DESC\n LIMIT 1`,\n params\n );\n\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n device_id: row.device_id,\n model: row.model,\n sampleCount: row.sample_count,\n message: row.message_markdown,\n created_at: row.created_at\n };\n}\n\nasync function analyzeLatestWaterQuality(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 10);\n\n if (!latestData.length) {\n return {\n message: '暂无可分析的数据,请先接入设备上报。',\n sampleCount: 0,\n model: config.deepseek.model\n };\n }\n\n if (!config.deepseek.apiKey) {\n const localResult = {\n message: '未配置 DEEPSEEK_API_KEY,以下为本地提示:最近数据已接收,建议重点关注 pH、COD、TDS 的连续波动趋势。',\n sampleCount: latestData.length,\n model: 'local-fallback',\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: localResult.model,\n sampleCount: localResult.sampleCount,\n message: localResult.message,\n rawData: latestData\n });\n\n return {\n ...localResult,\n reportId\n };\n }\n\n const prompt = {\n instruction: '请基于以下最近10次水质监测数据,给出简明专业分析,包括整体趋势、异常指标、可能原因和处理建议。',\n data: latestData\n };\n\n const response = await requestDeepseekWithRetry(prompt);\n\n const message = response.data?.choices?.[0]?.message?.content || 'DeepSeek 未返回有效分析结果。';\n const result = {\n message,\n sampleCount: latestData.length,\n model: config.deepseek.model,\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: result.model,\n sampleCount: result.sampleCount,\n message: result.message,\n rawData: latestData\n });\n\n return {\n ...result,\n reportId\n };\n}\n\nmodule.exports = {\n analyzeLatestWaterQuality,\n latestAnalysisReport\n};\n"}, "1e0907601b87a0dac47060eff82d4df68430e439": {"t": 1785753973.697274, "n": 5362, "h": "4c0702cdc57d9d25", "c": "const axios = require('axios');\nconst config = require('../config');\nconst { latestWaterQuality } = require('./deviceService');\nconst { run, get } = require('../db');\n\n// Retry transient upstream/network issues to reduce random ECONNRESET failures.\nconst RETRYABLE_ERROR_CODES = new Set([\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'ECONNABORTED',\n 'EPIPE',\n 'ENETRESET',\n 'EAI_AGAIN'\n]);\nconst REQUEST_TIMEOUT_MS = 120000;\nconst MAX_RETRIES = 2;\nconst RETRY_BASE_DELAY_MS = 800;\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction shouldRetryDeepseekRequest(error) {\n const code = String(error?.code || '').toUpperCase();\n const status = Number(error?.response?.status || 0);\n const message = String(error?.message || '').toLowerCase();\n\n if (status === 429 || (status >= 500 && status !== 504)) {\n return true;\n }\n if (RETRYABLE_ERROR_CODES.has(code)) {\n return true;\n }\n return message.includes('aborted') || message.includes('socket hang up');\n}\n\nasync function requestDeepseekWithRetry(prompt) {\n const totalAttempts = MAX_RETRIES + 1;\n\n for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {\n try {\n const payload = {\n model: config.deepseek.model || 'deepseek-chat',\n messages: [\n {\n role: 'system',\n content: '你是一个专业的水质分析专家。请根据提供的传感器数据(TDS, COD, TOC, pH, 温度, 浊度等),生成一份简明扼要的分析报告。要求:1. 使用中文;2. 结构清晰(可以使用 Markdown 标题和列表);3. 给出评价和建议。'\n },\n {\n role: 'user',\n content: `以下是最近的水质检测数据,请分析其健康状况并给出建议:\\n${JSON.stringify(prompt, null, 2)}`\n }\n ],\n temperature: 0.3,\n max_tokens: 2048\n };\n\n console.log(`[deepseek] sending request to ${config.deepseek.baseUrl}/chat/completions (attempt ${attempt})`);\n\n return await axios.post(\n `${config.deepseek.baseUrl}/chat/completions`,\n payload,\n {\n headers: {\n Authorization: `Bearer ${config.deepseek.apiKey}`,\n 'Content-Type': 'application/json',\n 'Accept-Encoding': 'identity'\n },\n timeout: REQUEST_TIMEOUT_MS\n }\n );\n } catch (error) {\n const code = String(error?.code || 'UNKNOWN');\n const status = error?.response?.status ?? 'N/A';\n const message = String(error?.message || 'unknown');\n const retryable = shouldRetryDeepseekRequest(error);\n const hasNextAttempt = attempt < totalAttempts;\n\n console.warn(\n `[deepseek] request failed attempt=${attempt}/${totalAttempts} code=${code} status=${status} message=${message}`\n );\n\n if (!retryable || !hasNextAttempt) {\n throw error;\n }\n\n const delayMs = RETRY_BASE_DELAY_MS * (2 ** (attempt - 1));\n await sleep(delayMs);\n }\n }\n\n throw new Error('DeepSeek request failed without a concrete error');\n}\n\nasync function saveAnalysisReport({ deviceId, model, sampleCount, message, rawData }) {\n const result = await run(\n `INSERT INTO analysis_reports (\n device_id, model, sample_count, message_markdown, raw_data_json, created_at\n ) VALUES (?, ?, ?, ?, ?, datetime('now'))`,\n [\n deviceId || null,\n model,\n sampleCount,\n message,\n rawData ? JSON.stringify(rawData) : null\n ]\n );\n\n return result.id;\n}\n\nasync function latestAnalysisReport(deviceId) {\n const where = deviceId ? 'WHERE device_id = ?' : '';\n const params = deviceId ? [deviceId] : [];\n\n const row = await get(\n `SELECT id, device_id, model, sample_count, message_markdown, created_at\n FROM analysis_reports\n ${where}\n ORDER BY created_at DESC, id DESC\n LIMIT 1`,\n params\n );\n\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n device_id: row.device_id,\n model: row.model,\n sampleCount: row.sample_count,\n message: row.message_markdown,\n created_at: row.created_at\n };\n}\n\nasync function analyzeLatestWaterQuality(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 10);\n\n if (!latestData.length) {\n return {\n message: '暂无可分析的数据,请先接入设备上报。',\n sampleCount: 0,\n model: config.deepseek.model\n };\n }\n\n if (!config.deepseek.apiKey) {\n const localResult = {\n message: '未配置 DEEPSEEK_API_KEY,以下为本地提示:最近数据已接收,建议重点关注 pH、COD、TDS 的连续波动趋势。',\n sampleCount: latestData.length,\n model: 'local-fallback',\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: localResult.model,\n sampleCount: localResult.sampleCount,\n message: localResult.message,\n rawData: latestData\n });\n\n return {\n ...localResult,\n reportId\n };\n }\n\n const prompt = {\n instruction: '请基于以下最近10次水质监测数据,给出简明专业分析,包括整体趋势、异常指标、可能原因和处理建议。',\n data: latestData\n };\n\n const response = await requestDeepseekWithRetry(prompt);\n\n const message = response.data?.choices?.[0]?.message?.content || 'DeepSeek 未返回有效分析结果。';\n const result = {\n message,\n sampleCount: latestData.length,\n model: config.deepseek.model,\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: result.model,\n sampleCount: result.sampleCount,\n message: result.message,\n rawData: latestData\n });\n\n return {\n ...result,\n reportId\n };\n}\n\nmodule.exports = {\n analyzeLatestWaterQuality,\n latestAnalysisReport\n};\n"}, "d6226d2e62b032f2b36a9265a3efd4c34fe51ae2": {"t": 1785753975.0940838, "n": 5135, "h": "6217cb7fff927c7e", "c": "const axios = require('axios');\nconst config = require('../config');\nconst { latestWaterQuality } = require('./deviceService');\nconst { run, get } = require('../db');\n\n// Retry transient upstream/network issues to reduce random ECONNRESET failures.\nconst RETRYABLE_ERROR_CODES = new Set([\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'ECONNABORTED',\n 'EPIPE',\n 'ENETRESET',\n 'EAI_AGAIN'\n]);\nconst REQUEST_TIMEOUT_MS = 30000;\nconst MAX_RETRIES = 2;\nconst RETRY_BASE_DELAY_MS = 800;\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction shouldRetryDeepseekRequest(error) {\n const code = String(error?.code || '').toUpperCase();\n const status = Number(error?.response?.status || 0);\n const message = String(error?.message || '').toLowerCase();\n\n if (status === 429 || status >= 500) {\n return true;\n }\n if (RETRYABLE_ERROR_CODES.has(code)) {\n return true;\n }\n return message.includes('aborted') || message.includes('socket hang up');\n}\n\nasync function requestDeepseekWithRetry(prompt) {\n const totalAttempts = MAX_RETRIES + 1;\n\n for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {\n try {\n return await axios.post(\n `${config.deepseek.baseUrl}/chat/completions`,\n {\n model: config.deepseek.model,\n messages: [\n {\n role: 'system',\n content: '你是水质分析专家,请使用中文输出,结构清晰,结论可执行。'\n },\n {\n role: 'user',\n content: JSON.stringify(prompt)\n }\n ],\n temperature: 0.2\n },\n {\n headers: {\n Authorization: `Bearer ${config.deepseek.apiKey}`,\n 'Content-Type': 'application/json',\n // Disable compressed response to avoid unzip-stream abort errors on unstable links.\n 'Accept-Encoding': 'identity'\n },\n timeout: REQUEST_TIMEOUT_MS\n }\n );\n } catch (error) {\n const code = String(error?.code || 'UNKNOWN');\n const status = error?.response?.status ?? 'N/A';\n const message = String(error?.message || 'unknown');\n const retryable = shouldRetryDeepseekRequest(error);\n const hasNextAttempt = attempt < totalAttempts;\n\n console.warn(\n `[deepseek] request failed attempt=${attempt}/${totalAttempts} code=${code} status=${status} message=${message}`\n );\n\n if (!retryable || !hasNextAttempt) {\n throw error;\n }\n\n const delayMs = RETRY_BASE_DELAY_MS * (2 ** (attempt - 1));\n await sleep(delayMs);\n }\n }\n\n throw new Error('DeepSeek request failed without a concrete error');\n}\n\nasync function saveAnalysisReport({ deviceId, model, sampleCount, message, rawData }) {\n const result = await run(\n `INSERT INTO analysis_reports (\n device_id, model, sample_count, message_markdown, raw_data_json, created_at\n ) VALUES (?, ?, ?, ?, ?, datetime('now'))`,\n [\n deviceId || null,\n model,\n sampleCount,\n message,\n rawData ? JSON.stringify(rawData) : null\n ]\n );\n\n return result.id;\n}\n\nasync function latestAnalysisReport(deviceId) {\n const where = deviceId ? 'WHERE device_id = ?' : '';\n const params = deviceId ? [deviceId] : [];\n\n const row = await get(\n `SELECT id, device_id, model, sample_count, message_markdown, created_at\n FROM analysis_reports\n ${where}\n ORDER BY created_at DESC, id DESC\n LIMIT 1`,\n params\n );\n\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n device_id: row.device_id,\n model: row.model,\n sampleCount: row.sample_count,\n message: row.message_markdown,\n created_at: row.created_at\n };\n}\n\nasync function analyzeLatestWaterQuality(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 10);\n\n if (!latestData.length) {\n return {\n message: '暂无可分析的数据,请先接入设备上报。',\n sampleCount: 0,\n model: config.deepseek.model\n };\n }\n\n if (!config.deepseek.apiKey) {\n const localResult = {\n message: '未配置 DEEPSEEK_API_KEY,以下为本地提示:最近数据已接收,建议重点关注 pH、COD、TDS 的连续波动趋势。',\n sampleCount: latestData.length,\n model: 'local-fallback',\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: localResult.model,\n sampleCount: localResult.sampleCount,\n message: localResult.message,\n rawData: latestData\n });\n\n return {\n ...localResult,\n reportId\n };\n }\n\n const prompt = {\n instruction: '请基于以下最近10次水质监测数据,给出简明专业分析,包括整体趋势、异常指标、可能原因和处理建议。',\n data: latestData\n };\n\n const response = await requestDeepseekWithRetry(prompt);\n\n const message = response.data?.choices?.[0]?.message?.content || 'DeepSeek 未返回有效分析结果。';\n const result = {\n message,\n sampleCount: latestData.length,\n model: config.deepseek.model,\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: result.model,\n sampleCount: result.sampleCount,\n message: result.message,\n rawData: latestData\n });\n\n return {\n ...result,\n reportId\n };\n}\n\nmodule.exports = {\n analyzeLatestWaterQuality,\n latestAnalysisReport\n};\n"}, "3dfb3a13b02b5cafe6020d85affc1bbdacbf1d98": {"t": 1785753976.3799593, "n": 3152, "h": "969ee9f0ec0d7c0c", "c": "const axios = require('axios');\nconst config = require('../config');\nconst { latestWaterQuality } = require('./deviceService');\nconst { run, get } = require('../db');\n\nasync function saveAnalysisReport({ deviceId, model, sampleCount, message, rawData }) {\n const result = await run(\n `INSERT INTO analysis_reports (\n device_id, model, sample_count, message_markdown, raw_data_json, created_at\n ) VALUES (?, ?, ?, ?, ?, datetime('now'))`,\n [\n deviceId || null,\n model,\n sampleCount,\n message,\n rawData ? JSON.stringify(rawData) : null\n ]\n );\n\n return result.id;\n}\n\nasync function latestAnalysisReport(deviceId) {\n const where = deviceId ? 'WHERE device_id = ?' : '';\n const params = deviceId ? [deviceId] : [];\n\n const row = await get(\n `SELECT id, device_id, model, sample_count, message_markdown, created_at\n FROM analysis_reports\n ${where}\n ORDER BY created_at DESC, id DESC\n LIMIT 1`,\n params\n );\n\n if (!row) {\n return null;\n }\n\n return {\n id: row.id,\n device_id: row.device_id,\n model: row.model,\n sampleCount: row.sample_count,\n message: row.message_markdown,\n created_at: row.created_at\n };\n}\n\nasync function analyzeLatestWaterQuality(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 10);\n\n if (!latestData.length) {\n return {\n message: '暂无可分析的数据,请先接入设备上报。',\n sampleCount: 0,\n model: config.deepseek.model\n };\n }\n\n if (!config.deepseek.apiKey) {\n const localResult = {\n message: '未配置 DEEPSEEK_API_KEY,以下为本地提示:最近数据已接收,建议重点关注 pH、COD、TDS 的连续波动趋势。',\n sampleCount: latestData.length,\n model: 'local-fallback',\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: localResult.model,\n sampleCount: localResult.sampleCount,\n message: localResult.message,\n rawData: latestData\n });\n\n return {\n ...localResult,\n reportId\n };\n }\n\n const prompt = {\n instruction: '请基于以下最近10次水质监测数据,给出简明专业分析,包括整体趋势、异常指标、可能原因和处理建议。',\n data: latestData\n };\n\n const response = await axios.post(\n `${config.deepseek.baseUrl}/chat/completions`,\n {\n model: config.deepseek.model,\n messages: [\n {\n role: 'system',\n content: '你是水质分析专家,请使用中文输出,结构清晰,结论可执行。'\n },\n {\n role: 'user',\n content: JSON.stringify(prompt)\n }\n ],\n temperature: 0.2\n },\n {\n headers: {\n Authorization: `Bearer ${config.deepseek.apiKey}`,\n 'Content-Type': 'application/json'\n },\n timeout: 30000\n }\n );\n\n const message = response.data?.choices?.[0]?.message?.content || 'DeepSeek 未返回有效分析结果。';\n const result = {\n message,\n sampleCount: latestData.length,\n model: config.deepseek.model,\n rawData: latestData\n };\n\n const reportId = await saveAnalysisReport({\n deviceId,\n model: result.model,\n sampleCount: result.sampleCount,\n message: result.message,\n rawData: latestData\n });\n\n return {\n ...result,\n reportId\n };\n}\n\nmodule.exports = {\n analyzeLatestWaterQuality,\n latestAnalysisReport\n};\n"}, "e947642709083501a6289e7d6438dd8cb65e04ba": {"t": 1785753977.6430883, "n": 1526, "h": "f252e8f6794f9db7", "c": "const axios = require('axios');\nconst config = require('../config');\nconst { latestWaterQuality } = require('./deviceService');\n\nasync function analyzeLatestWaterQuality(deviceId) {\n const latestData = await latestWaterQuality(deviceId, 10);\n\n if (!latestData.length) {\n return {\n message: '暂无可分析的数据,请先接入设备上报。',\n sampleCount: 0,\n model: config.deepseek.model\n };\n }\n\n if (!config.deepseek.apiKey) {\n return {\n message: '未配置 DEEPSEEK_API_KEY,以下为本地提示:最近数据已接收,建议重点关注 pH、COD、TDS 的连续波动趋势。',\n sampleCount: latestData.length,\n model: 'local-fallback',\n rawData: latestData\n };\n }\n\n const prompt = {\n instruction: '请基于以下最近10次水质监测数据,给出简明专业分析,包括整体趋势、异常指标、可能原因和处理建议。',\n data: latestData\n };\n\n const response = await axios.post(\n `${config.deepseek.baseUrl}/chat/completions`,\n {\n model: config.deepseek.model,\n messages: [\n {\n role: 'system',\n content: '你是水质分析专家,请使用中文输出,结构清晰,结论可执行。'\n },\n {\n role: 'user',\n content: JSON.stringify(prompt)\n }\n ],\n temperature: 0.2\n },\n {\n headers: {\n Authorization: `Bearer ${config.deepseek.apiKey}`,\n 'Content-Type': 'application/json'\n },\n timeout: 30000\n }\n );\n\n return {\n message: response.data?.choices?.[0]?.message?.content || 'DeepSeek 未返回有效分析结果。',\n sampleCount: latestData.length,\n model: config.deepseek.model,\n rawData: latestData\n };\n}\n\nmodule.exports = {\n analyzeLatestWaterQuality\n};\n"}}}}