import { normalizeResponseData, requestJson } from "./request" const OPEN_API_BASE = import.meta.env.VITE_OPEN_API_BASE || "/openApi" const MAX_DATA_CHARS = 120000 const MAX_ANSWER_CHARS = 8000 const MAX_DATASETS = 12 const MAX_DATASET_ROWS = 500 function joinBase(path) { if (/^https?:\/\//.test(path)) return path return `${OPEN_API_BASE}${path.startsWith("/") ? path : `/${path}`}` } function unwrapResponse(response) { const failed = response?.success === false || (response?.code !== undefined && ![0, 200, "0", "200"].includes(response.code)) if (failed) { const error = new Error(response?.message || response?.msg || "HTML界面生成失败") error.code = response?.code throw error } return normalizeResponseData(response) } export async function generateSmartArtifact({ question, answer = "", charts = [], datasets = [], context = null, pagePath = window.location.hash || window.location.pathname, pageName = "智慧管理", } = {}) { const dataJson = buildArtifactDataJson({ question, answer, charts, datasets, context }) const response = await requestJson(joinBase("/ai/artifact/html"), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ question, pagePath, pageName, dataJson, }), timeout: 110000, }).then(unwrapResponse) const provider = String(response?.provider || "") const model = String(response?.model || "") if (provider.toLowerCase() !== "deepseek" || !model.toLowerCase().startsWith("deepseek-")) { throw new Error("HTML界面不是由DeepSeek模型生成,已拒绝展示") } const html = extractHtml(response?.html || response?.content || response) if (!html) { throw new Error("代码模型没有返回完整HTML") } if (isDataDumpArtifactHtml(html)) { throw new Error("专题页面仍为原始数据表,已拒绝展示,请重新生成") } return { ...response, html, } } function buildArtifactDataJson({ question, answer, charts, datasets, context }) { const payload = { question: String(question || "").trim(), generatedAt: new Date().toISOString(), presentation: { format: "business-report", requirements: [ "输出应为业务报告页面,而不是原始数据表或字段清单。", "不要直接展示 key、rawValue、areaCode、areaRange 等技术字段名。", "页面需包含结论、关键指标、图表化对比、重点对象和工作建议。", ], }, answer: String(answer || "").slice(0, MAX_ANSWER_CHARS), context: sanitizeContext(context), datasets: [ ...normalizeChartDatasets(charts), ...normalizeDatasets(datasets), ], } const serialized = JSON.stringify(payload) if (serialized.length > MAX_DATA_CHARS) { const error = new Error("汇总数据超过120000字符,请先缩小查询范围") error.code = "ARTIFACT_DATA_TOO_LARGE" throw error } return serialized } function sanitizeContext(context) { if (!context || typeof context !== "object") return null const { question, pageName, pagePath, scope, intents, notes, warnings, insights, } = context return sanitizeSerializableValue({ question, pageName, pagePath, scope, intents, notes, warnings, insights, }) } function normalizeChartDatasets(charts = []) { return (Array.isArray(charts) ? charts : []).slice(0, 6).map((chart, index) => ({ id: chart?.id || `chart-${index + 1}`, title: chart?.title || `统计数据${index + 1}`, suggestedView: chart?.type || "bar", unit: chart?.unit || "", subtitle: chart?.subtitle || "", source: chart?.source || {}, rows: normalizeRows(chart?.rows), })) } function normalizeDatasets(datasets = []) { return (Array.isArray(datasets) ? datasets : []).slice(0, MAX_DATASETS).map((dataset, index) => ({ id: dataset?.id || `dataset-${index + 1}`, title: dataset?.title || dataset?.name || `汇总数据${index + 1}`, suggestedView: dataset?.suggestedView || dataset?.type || "auto", unit: dataset?.unit || "", source: dataset?.source || {}, rows: normalizeRows(dataset?.rows || dataset?.data || dataset), })) } function normalizeRows(rows) { return (Array.isArray(rows) ? rows : []) .slice(0, MAX_DATASET_ROWS) .map((row) => sanitizeSerializableValue(row)) } function sanitizeSerializableValue(value, depth = 0) { if (depth > 4 || value === undefined || typeof value === "function") return null if (value === null || ["string", "number", "boolean"].includes(typeof value)) return value if (Array.isArray(value)) return value.slice(0, 50).map((item) => sanitizeSerializableValue(item, depth + 1)) if (typeof value === "object") { return Object.entries(value).reduce((result, [key, item]) => { result[key] = sanitizeSerializableValue(item, depth + 1) return result }, {}) } return String(value) } function extractHtml(content) { let html = String(content || "").trim() if (html.startsWith("```")) { html = html.replace(/^```(?:html)?\s*/i, "").replace(/\s*```$/i, "").trim() } const startMatch = html.match(/") if (startMatch && endIndex >= startMatch.index) { html = html.slice(startMatch.index, endIndex + 7) } return /]+>/g, " ") .replace(/\s+/g, " ") .toLowerCase() const tableCount = (source.match(/ text.includes(term)).length return (tableCount >= 2 && hitCount >= 4) || (tableCount >= 1 && hitCount >= 6) }