diff --git a/package-lock.json b/package-lock.json
index 0031be2..30c59c6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,12 +11,15 @@
"@lucide/vue": "^1.34.0",
"axios": "^1.19.0",
"dingtalk-jsapi": "^3.2.9",
+ "dompurify": "^3.4.17",
+ "markdown-it": "^15.0.3",
"pinia": "^4.0.3",
"vue": "^3.5.41",
"vue-router": "^5.2.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
+ "@types/markdown-it": "^14.2.0",
"@types/node": "^26.2.0",
"@vitejs/plugin-vue": "^6.0.8",
"@vue/test-utils": "^2.4.11",
@@ -958,6 +961,31 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz",
+ "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "26.4.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz",
@@ -968,6 +996,13 @@
"undici-types": "~8.3.0"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.68.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz",
@@ -1669,6 +1704,22 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/argparse": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.4.tgz",
+ "integrity": "sha512-fwWK3Zq22l4XJJjx5mcBHGikIk+hJqO+32dPm0Jur7+rtG5x1scOjSfdT9ZKeWHPpsya/l0HHK4tDImBkTrtvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "PSF-2.0"
+ },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -2019,6 +2070,15 @@
"promise-polyfill": "^7.1.0"
}
},
+ "node_modules/dompurify": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz",
+ "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -3258,6 +3318,25 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/linkify-it": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz",
+ "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^3.0.0"
+ }
+ },
"node_modules/local-pkg": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz",
@@ -3325,6 +3404,45 @@
"url": "https://github.com/sponsors/sxzz"
}
},
+ "node_modules/markdown-it": {
+ "version": "15.0.3",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.3.tgz",
+ "integrity": "sha512-+Az7Ojz3iRGu8KYMvJ6xVhvrxP3eE+vEtJX75Ow88XBpcQNV/0lGX5ZynIIBftREo4DC2jsveAMDaeltmm8QvQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^3.0.3",
+ "entities": "^8.1.0",
+ "linkify-it": "^6.1.0",
+ "mdurl": "^2.1.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^3.0.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/markdown-it/node_modules/entities": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
+ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -3341,6 +3459,12 @@
"dev": true,
"license": "CC0-1.0"
},
+ "node_modules/mdurl": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
+ "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
+ "license": "MIT"
+ },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -3803,6 +3927,15 @@
"node": ">=6"
}
},
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/quansync": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
@@ -4238,6 +4371,12 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
+ "node_modules/uc.micro": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz",
+ "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==",
+ "license": "MIT"
+ },
"node_modules/ufo": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
diff --git a/package.json b/package.json
index 1a64c03..bc9d5a8 100644
--- a/package.json
+++ b/package.json
@@ -11,15 +11,18 @@
"format": "prettier --write \"src/**/*.{ts,vue,css}\""
},
"dependencies": {
- "dingtalk-jsapi": "^3.2.9",
- "axios": "^1.19.0",
"@lucide/vue": "^1.34.0",
+ "axios": "^1.19.0",
+ "dingtalk-jsapi": "^3.2.9",
+ "dompurify": "^3.4.17",
+ "markdown-it": "^15.0.3",
"pinia": "^4.0.3",
"vue": "^3.5.41",
"vue-router": "^5.2.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
+ "@types/markdown-it": "^14.2.0",
"@types/node": "^26.2.0",
"@vitejs/plugin-vue": "^6.0.8",
"@vue/test-utils": "^2.4.11",
diff --git a/src/pages/RiskAssistantPage.vue b/src/pages/RiskAssistantPage.vue
index 6eb0be7..f4b0b7e 100644
--- a/src/pages/RiskAssistantPage.vue
+++ b/src/pages/RiskAssistantPage.vue
@@ -13,6 +13,7 @@ import { directConversationQuery } from "@/api/conversations";
import { useConversationStore } from "@/stores/conversation.store";
import { useAuthStore } from "@/stores/auth.store";
import { storeToRefs } from "pinia";
+import { renderSafeMarkdown } from "@/utils/markdown";
import type { ReportCard, RiskAssessmentResult } from "@/types/risk";
import type { AssistantResponseCard as AssistantResponseCardData } from "@/types/conversation";
import {
@@ -28,6 +29,7 @@ const {
current: conversation,
sending: loading,
generating,
+ withdrawing,
error,
} = storeToRefs(conversationStore);
const withdrawingId = ref(null);
@@ -230,7 +232,10 @@ function closeTool() {
directResult.value = null;
}
-async function submitDirectTool(kind: "supplier" | "material" | "cases" | "solution", query: string) {
+async function submitDirectTool(
+ kind: "supplier" | "material" | "cases" | "solution",
+ query: string,
+) {
directLoading.value = true;
toolError.value = "";
try {
@@ -238,7 +243,8 @@ async function submitDirectTool(kind: "supplier" | "material" | "cases" | "solut
directResult.value = response.data.data ?? null;
if (response.data.data) {
directUserPrompt.value = query;
- directResponseCard.value = response.data.data as AssistantResponseCardData;
+ directResponseCard.value = response.data
+ .data as AssistantResponseCardData;
closeTool();
await nextTick();
await scrollToBottom();
@@ -246,7 +252,9 @@ async function submitDirectTool(kind: "supplier" | "material" | "cases" | "solut
} catch (reason) {
toolError.value = axios.isAxiosError(reason)
? String(reason.response?.data?.message || "查询失败")
- : reason instanceof Error ? reason.message : "查询失败";
+ : reason instanceof Error
+ ? reason.message
+ : "查询失败";
} finally {
directLoading.value = false;
}
@@ -298,12 +306,19 @@ async function submitTool() {
toolError.value = "请先填写必填内容";
return;
}
- if (["supplier", "material", "cases", "solution"].includes(selectedTool ?? "")) {
- await submitDirectTool(selectedTool as "supplier" | "material" | "cases" | "solution", finalValue);
+ if (
+ ["supplier", "material", "cases", "solution"].includes(selectedTool ?? "")
+ ) {
+ await submitDirectTool(
+ selectedTool as "supplier" | "material" | "cases" | "solution",
+ finalValue,
+ );
return;
}
closeTool();
- void usePrompt(finalValue || `${selectedTitle}:${toolFile.value?.name ?? "附件"}`);
+ void usePrompt(
+ finalValue || `${selectedTitle}:${toolFile.value?.name ?? "附件"}`,
+ );
}
function selectFile(event: Event) {
@@ -340,10 +355,6 @@ function handleScroll() {
if (scrollArea.value && scrollArea.value.scrollTop <= 80) loadOlder();
}
-function hasAssistantPair(index: number) {
- return conversation.value?.messages[index + 1]?.role === "assistant";
-}
-
function stopGeneration() {
conversationStore.stopGeneration();
}
@@ -354,6 +365,8 @@ async function withdrawMessage(messageId: number) {
withdrawingId.value = messageId;
try {
await conversationStore.withdraw(messageId);
+ } catch {
+ // The store restores the message and exposes the failure in the page.
} finally {
withdrawingId.value = null;
}
@@ -411,10 +424,10 @@ function isPrimaryRiskReport(
): report is RiskAssessmentResult {
return Boolean(
report &&
- (report.dimensionScores?.length ||
- report.factors?.length ||
- report.categoryWarning ||
- report.supplierFreq),
+ (report.dimensionScores?.length ||
+ report.factors?.length ||
+ report.categoryWarning ||
+ report.supplierFreq),
);
}
@@ -422,9 +435,7 @@ function refreshPrimaryReportState() {
const conversationId = conversation.value?.id;
primaryReportReady.value = Boolean(
conversationId &&
- isPrimaryRiskReport(
- readConversationReport(conversationId, auth.user?.id),
- ),
+ isPrimaryRiskReport(readConversationReport(conversationId, auth.user?.id)),
);
}
@@ -441,10 +452,12 @@ function openReport(result: RiskAssessmentResult) {
stored?.subjectId === result.subjectId
? {
...cloneReport(result),
- reportCards: storedCards.length ? storedCards : result.reportCards ?? [],
+ reportCards: storedCards.length
+ ? storedCards
+ : (result.reportCards ?? []),
aiSolutionCards: stored.aiSolutionCards?.length
? stored.aiSolutionCards
- : result.aiSolutionCards ?? [],
+ : (result.aiSolutionCards ?? []),
}
: cloneReport(result);
writeConversationReport(conversationId, merged, auth.user?.id);
@@ -485,7 +498,7 @@ async function submit(
messageType: "text" | "image" | "file" | "audio" = "text",
) {
const value = prompt.value.trim();
- if (!value || loading.value) return;
+ if (!value || loading.value || withdrawing.value) return;
prompt.value = "";
const sentConversation = await conversationStore.streamSend(
value,
@@ -551,18 +564,14 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
@@ -584,7 +593,9 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
>
-
{{ auth.user?.name?.slice(0, 1) ?? "我" }}
+
+ {{ auth.user?.name?.slice(0, 1) ?? "我" }}
+
{{ directUserPrompt }}
@@ -680,13 +701,21 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
{{ error }}
+
+ 正在撤回消息…
+
{{ toolError }}
- {{ directResult.tier || "新供应商" }}
+ {{
+ directResult.tier || "新供应商"
+ }}
-
近 3 年异常 N={{ directResult.anomalyCount ?? 0 }}(代理合作频次口径)
-
基础分 {{ directResult.supplierBaseScore ?? 70 }} · α ×{{ Number(directResult.frequencyCoefficient ?? 1).toFixed(2) }}
-
供应商得分 S供 = {{ directResult.supplierScore ?? 70 }} 分
+
+ 近 3 年异常 N={{ directResult.anomalyCount ?? 0 }}(代理合作频次口径)
+
+
+ 基础分 {{ directResult.supplierBaseScore ?? 70 }} · α
+ ×{{
+ Number(directResult.frequencyCoefficient ?? 1).toFixed(2)
+ }}
+
+
+ 供应商得分 S供 =
+ {{ directResult.supplierScore ?? 70 }} 分
+
-
- 高频异常:{{ (directResult.topProblems as Array<{name: string; count: number}>).map((item) => `${item.name}(${item.count} 条)`).join("、") }}
+
+ 高频异常:{{
+ (
+ directResult.topProblems as Array<{
+ name: string;
+ count: number;
+ }>
+ )
+ .map((item) => `${item.name}(${item.count} 条)`)
+ .join("、")
+ }}
+
+
+ 建议:{{ (directResult.suggestions as string[]).join(" + ") }}
- 建议:{{ (directResult.suggestions as string[]).join(" + ") }}
{{ directResult.title || "物料风险分析" }}
- 物料:{{ directResult.material || directResult.query }} · 历史异常 {{ directResult.anomalyCount ?? 0 }} 条
-
- 高频问题:{{ (directResult.frequentProblems as Array<{name: string; count: number}>).map((item) => `${item.name}(${item.count} 条)`).join("、") }}
+
+ 物料:{{ directResult.material || directResult.query }} ·
+ 历史异常 {{ directResult.anomalyCount ?? 0 }} 条
+
+
+ 高频问题:{{
+ (
+ directResult.frequentProblems as Array<{
+ name: string;
+ count: number;
+ }>
+ )
+ .map((item) => `${item.name}(${item.count} 条)`)
+ .join("、")
+ }}
+
+
+ 建议:{{ (directResult.suggestions as string[]).join(";") }}
- 建议:{{ (directResult.suggestions as string[]).join(";") }}
相似案例检索
- 关键词:{{ directResult.query }} · 匹配 {{ directResult.matchedCount ?? 0 }} 条
-
{{ directResult.title || "AI 方案生成" }}
- {{ directResult.analysisBase || "已结合历史案例与标准库生成分析方案。" }}
-
- - {{ item }}
+
+ {{
+ directResult.analysisBase ||
+ "已结合历史案例与标准库生成分析方案。"
+ }}
+
+
+ -
+ {{ item }}
+
@@ -797,7 +901,11 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
-