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.
 
 
 
 
 
 
hy-screen-2.0/src/services/smartArtifactApi.js

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)
}