1 line
81 KiB
JSON
1 line
81 KiB
JSON
{"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"}}}} |