You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
199 lines
6.0 KiB
199 lines
6.0 KiB
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(/<!doctype\s+html|<html\b/i)
|
|
const endIndex = html.toLowerCase().lastIndexOf("</html>")
|
|
if (startMatch && endIndex >= startMatch.index) {
|
|
html = html.slice(startMatch.index, endIndex + 7)
|
|
}
|
|
return /<html\b/i.test(html) && /<body\b/i.test(html) ? html : ""
|
|
}
|
|
|
|
function isDataDumpArtifactHtml(html) {
|
|
const source = String(html || "")
|
|
const text = source
|
|
.replace(/<[^>]+>/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.toLowerCase()
|
|
const tableCount = (source.match(/<table\b/gi) || []).length
|
|
const technicalTerms = [
|
|
"rawvalue",
|
|
"areacode",
|
|
"arearange",
|
|
"livestockcapacity",
|
|
"bearingcapacity",
|
|
"overloadrate",
|
|
"dataset",
|
|
" key ",
|
|
" type ",
|
|
]
|
|
const hitCount = technicalTerms.filter((term) => text.includes(term)).length
|
|
return (tableCount >= 2 && hitCount >= 4) || (tableCount >= 1 && hitCount >= 6)
|
|
}
|
|
|