Selaa lähdekoodia

基础UI设计

kagg886 3 kuukautta sitten
vanhempi
sitoutus
5437550e9e

+ 98 - 0
src/components/markdown/index.vue

@@ -0,0 +1,98 @@
+<template>
+	<div
+		ref="container"
+		class="markdown-content"
+		v-html="renderedContent"
+	/>
+</template>
+
+<script setup lang="ts">
+import { ref, computed} from 'vue'
+import MarkdownIt from 'markdown-it';
+import { MarkdownPlugin } from '/@/components/markdown/types'
+
+interface Props {
+	content: string
+	plugins?: Array<MarkdownPlugin<any>>
+}
+
+const props = withDefaults(defineProps<Props>(), {
+	plugins: undefined
+})
+
+const container = ref<HTMLElement>()
+
+const md = new MarkdownIt({
+	html: true,
+	linkify: true,
+	typographer: true
+})
+
+for (const plugin of props.plugins ?? []) {
+	md.use(plugin.impl, plugin.settings())
+}
+
+// 渲染markdown内容
+const renderedContent = computed(() => {
+	try {
+		return md.render(props.content)
+	} catch (error) {
+		console.error('Markdown渲染错误:', error)
+		return `<div style="color: red; padding: 20px; border: 1px solid red; border-radius: 4px;">
+      <strong>Markdown渲染错误:</strong><br>
+      ${error instanceof Error ? error.message : String(error)}
+    </div>`
+	}
+})
+</script>
+
+<style scoped>
+
+/* 基本的markdown样式 */
+:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
+	margin-top: 24px;
+	margin-bottom: 16px;
+	font-weight: 600;
+	line-height: 1.25;
+}
+
+:deep(h1) { font-size: 2em; }
+:deep(h2) { font-size: 1.5em; }
+:deep(h3) { font-size: 1.25em; }
+
+:deep(p) {
+	margin-bottom: 16px;
+	line-height: 1.6;
+}
+
+:deep(pre) {
+	background-color: #f6f8fa;
+	border-radius: 6px;
+	padding: 16px;
+	overflow: auto;
+	margin: 16px 0;
+}
+
+:deep(code) {
+	background-color: #f6f8fa;
+	padding: 2px 4px;
+	border-radius: 3px;
+	font-size: 85%;
+}
+
+:deep(blockquote) {
+	border-left: 4px solid #d1d5da;
+	padding-left: 16px;
+	margin: 16px 0;
+	color: #6a737d;
+}
+
+:deep(ul), :deep(ol) {
+	margin: 16px 0;
+	padding-left: 32px;
+}
+
+:deep(li) {
+	margin: 4px 0;
+}
+</style>

+ 108 - 0
src/components/markdown/plugins/markdown-it-echarts.ts

@@ -0,0 +1,108 @@
+import MarkdownIt from "markdown-it";
+import type { RenderRule } from "markdown-it/lib/renderer.mjs";
+import type Token from "markdown-it/lib/token.mjs";
+import type { Options } from "markdown-it/lib/index.mjs";
+import type Renderer from "markdown-it/lib/renderer.mjs";
+import * as echarts from 'echarts'
+
+class EChartsElement extends HTMLDivElement {
+  instance!: echarts.ECharts
+  private resizeHandler!: () => void
+
+  connectedCallback() {
+    const config = decodeURIComponent(this.getAttribute('config') ?? '') ?? '';
+    let data: echarts.EChartsOption
+    try {
+      data = JSON.parse(config)
+    } catch (e) {
+      console.error(e)
+      return
+    }
+    this.instance = echarts.init(this)
+    this.instance.setOption(data)
+
+    // 创建绑定了正确上下文的 resize 处理器
+    this.resizeHandler = () => {
+      this.instance?.resize()
+    }
+
+    window.addEventListener('resize', this.resizeHandler)
+  }
+
+  disconnectedCallback() {
+    // 清理 resize 事件监听器
+    if (this.resizeHandler) {
+      window.removeEventListener('resize', this.resizeHandler)
+    }
+
+    // 销毁 ECharts 实例
+    if (this.instance) {
+      this.instance.dispose()
+    }
+  }
+}
+
+
+export type EchartsPluginOptions = {
+}
+
+// 生成唯一ID
+function generateId(): string {
+  return 'echarts-' + Math.random().toString(36).substring(2, 9)
+}
+
+// 验证JSON格式
+function isValidJSON(str: string): boolean {
+  try {
+    JSON.parse(str)
+    return true
+  } catch {
+    return false
+  }
+}
+
+// 渲染echarts代码块
+const renderEcharts: RenderRule = (tokens: Token[], idx: number, _options: Options, env: any, _self: Renderer) => {
+  const token = tokens[idx]
+  const content = token.content.trim()
+
+  if (!content) {
+    return '<div style="padding: 16px;background-color: #fff5f5;border: 1px solid #fed7d7;border-radius: 6px;color: #c53030;margin: 16px 0;">ECharts配置不能为空</div>'
+  }
+
+  const id = generateId()
+  const className = env.echartsClassName || 'echarts-container'
+  // 生成完整HTML
+  return `<div is="echarts-container" style="width: 100%;height: 350px;margin: 16px 0; border-radius: 6px"  class="${className}" id="${id}" config="${encodeURIComponent(content)}"></div>`
+}
+
+// markdown-it插件
+//@ts-ignore
+// eslint-disable-next-line no-unused-vars
+function echartsPlugin(md: MarkdownIt, options: EchartsPluginOptions = {}) {
+  // 保存原始的fence渲染器
+  const defaultRender = md.renderer.rules.fence ?? function(tokens, idx, options, _env, renderer) {
+    return renderer.renderToken(tokens, idx, options)
+  }
+
+  if (customElements.get('echarts-container') === undefined) {
+    customElements.define('echarts-container', EChartsElement, { extends: 'div' })
+  }
+
+  // 重写fence渲染器
+  md.renderer.rules.fence = function(tokens, idx, options, env, renderer) {
+    const token = tokens[idx]
+    const info = token.info ? token.info.trim() : ''
+
+    // 检查是否是echarts代码块
+    if (info === 'echarts' && isValidJSON(token.content.trim())) {
+      console.log(`prepare renderer`,tokens,idx,options,env,renderer,tokens[idx])
+      return renderEcharts(tokens, idx, options, env, renderer)
+    }
+
+    // 其他代码块使用默认渲染器
+    return defaultRender(tokens, idx, options, env, renderer)
+  }
+}
+
+export default echartsPlugin

+ 14 - 0
src/components/markdown/types.ts

@@ -0,0 +1,14 @@
+import type {PluginWithOptions} from "markdown-it/index.mjs";
+
+export type MarkdownPlugin<Settings> = {
+	impl: PluginWithOptions<Settings>,
+	settings: () => Settings
+}
+
+
+export function defineMarkdownPlugin<Settings>(impl: PluginWithOptions<Settings>,settings: ()=>Settings):MarkdownPlugin<Settings> {
+	return {
+		impl: impl,
+		settings: settings
+	}
+}

+ 643 - 0
src/views/assistant/index.vue

@@ -0,0 +1,643 @@
+<script setup lang="ts">
+import { defineMarkdownPlugin, MarkdownPlugin } from '/@/components/markdown/types'
+import echartsPlugin, { EchartsPluginOptions } from '/@/components/markdown/plugins/markdown-it-echarts'
+import Markdown from '/@/components/markdown/index.vue'
+import { ref, nextTick, onMounted, computed } from 'vue'
+import { Local } from '/@/utils/storage'
+import { User, ChatDotRound } from '@element-plus/icons-vue'
+
+const plugins: Array<MarkdownPlugin<any>> = [
+	defineMarkdownPlugin<EchartsPluginOptions>(echartsPlugin, () => {
+		return {}
+	})
+]
+
+const getUserInfos = ref<{
+	avatar: string
+	userName: string
+}>(Local.get('userInfo') || {})
+
+// 消息类型定义
+interface Message {
+	id: number
+	type: 'user' | 'ai'
+	content: string
+	timestamp: Date
+}
+
+// 会话列表
+const conversations = ref([
+	{ id: 1, title: 'Summary 1', active: true },
+	{ id: 2, title: 'Summary 2', active: false }
+])
+
+// 消息列表
+const messages = ref<Message[]>([
+	{
+		id: 1,
+		type: 'user',
+		content: '请帮我分析这个数据',
+		timestamp: new Date()
+	},
+	{
+		id: 2,
+		type: 'ai',
+		content: `好的,我来为您查询相关的人员信息。首先请让我从数据库中获取所有相关百科:
+
+## Tool Calls 📊
+
+\`\`\`json
+{
+  "id": "XXX",
+  "params": "yyy",
+  "returns": "zzz"
+}
+\`\`\`
+
+...ListItem
+
+根据我查询到的信息,我找到您想要的相关人员信息:
+
+1. xxxxx
+2. yyyyy
+3. zzzzz
+
+\`\`\`echarts
+{
+  "title": {
+    "text": "charts 1"
+  },
+  "xAxis": {
+    "type": "category",
+    "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
+  },
+  "yAxis": {
+    "type": "value"
+  },
+  "series": [{
+    "data": [120, 200, 150, 80, 70, 110, 130],
+    "type": "bar"
+  }]
+}
+\`\`\``,
+		timestamp: new Date()
+	}
+])
+
+// 输入框内容
+const inputMessage = ref('')
+const messagesContainer = ref<HTMLElement>()
+
+// 工具选择数据
+const toolOptions = ref([
+	{
+		value: 'code',
+		label: '代码工具',
+		children: [
+			{
+				value: 'codebase-retrieval',
+				label: '代码库检索'
+			},
+			{
+				value: 'str-replace-editor',
+				label: '代码编辑器'
+			},
+			{
+				value: 'save-file',
+				label: '文件保存'
+			}
+		]
+	},
+	{
+		value: 'web',
+		label: '网络工具',
+		children: [
+			{
+				value: 'web-search',
+				label: '网络搜索'
+			},
+			{
+				value: 'web-fetch',
+				label: '网页获取'
+			}
+		]
+	},
+	{
+		value: 'system',
+		label: '系统工具',
+		children: [
+			{
+				value: 'execute-command',
+				label: '命令执行'
+			},
+			{
+				value: 'launch-process',
+				label: '进程启动'
+			}
+		]
+	}
+])
+
+// 模型选择数据
+const modelOptions = ref([
+	{ label: 'Claude Sonnet 4', value: 'claude-sonnet-4' },
+	{ label: 'GPT-4', value: 'gpt-4' },
+	{ label: 'GPT-3.5 Turbo', value: 'gpt-3.5-turbo' },
+	{ label: 'Gemini Pro', value: 'gemini-pro' }
+])
+
+// 选中的工具和模型
+const selectedTool = ref([])
+const selectedModel = ref('claude-sonnet-4')
+
+// 对话状态
+const isConversationActive = ref(false)
+
+// 发送消息
+const sendMessage = () => {
+	if (!inputMessage.value.trim()) return
+
+	// 设置对话状态为活跃
+	isConversationActive.value = true
+
+	// 添加用户消息
+	messages.value.push({
+		id: Date.now(),
+		type: 'user',
+		content: inputMessage.value,
+		timestamp: new Date()
+	})
+
+	const userMessage = inputMessage.value
+	inputMessage.value = ''
+
+	// 滚动到底部
+	scrollToBottom()
+
+	// 模拟AI回复
+	setTimeout(() => {
+		messages.value.push({
+			id: Date.now() + 1,
+			type: 'ai',
+			content: `我收到了您的消息:"${userMessage}"。这是一个示例回复,包含markdown格式的内容。
+
+## 回复内容
+
+- 列表项 1
+- 列表项 2
+- 列表项 3
+
+\`\`\`javascript
+console.log('这是代码示例');
+\`\`\``,
+			timestamp: new Date()
+		})
+		isConversationActive.value = false
+		scrollToBottom()
+	}, 1000)
+}
+
+// 终止对话
+const stopConversation = () => {
+	isConversationActive.value = false
+	// 这里可以添加实际的终止逻辑
+}
+
+// 滚动到底部
+const scrollToBottom = () => {
+	nextTick(() => {
+		if (messagesContainer.value) {
+			messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
+		}
+	})
+}
+// 选择会话
+const selectConversation = (id: number) => {
+	conversations.value.forEach(conv => {
+		conv.active = conv.id === id
+	})
+}
+
+onMounted(() => {
+	scrollToBottom()
+})
+
+</script>
+
+<template>
+	<el-container class="chat-container">
+		<!-- 左侧会话列表 -->
+		<el-aside width="300px" class="chat-sidebar">
+			<div class="sidebar-header">
+				<h3>对话历史</h3>
+			</div>
+			<div class="conversation-list">
+				<div
+					v-for="conv in conversations"
+					:key="conv.id"
+					:class="['conversation-item', { active: conv.active }]"
+					@click="selectConversation(conv.id)"
+				>
+					{{ conv.title }}
+				</div>
+			</div>
+		</el-aside>
+
+		<!-- 右侧聊天区域 -->
+		<el-main class="chat-main">
+			<!-- 消息展示区域 -->
+			<div class="messages-container" ref="messagesContainer">
+				<div
+					v-for="message in messages"
+					:key="message.id"
+					:class="['message-wrapper', message.type]"
+				>
+					<!-- AI消息 -->
+					<div v-if="message.type === 'ai'" class="ai-message-container">
+						<el-avatar class="message-avatar" :icon="ChatDotRound" />
+						<div class="message-bubble ai-bubble">
+							<Markdown
+								:content="message.content"
+								:plugins="plugins"
+							/>
+						</div>
+					</div>
+
+					<!-- 用户消息 -->
+					<div v-else class="user-message-container">
+						<div class="message-bubble user-bubble">
+							{{ message.content }}
+						</div>
+						<el-avatar
+							class="message-avatar"
+							:src="getUserInfos.avatar"
+							:icon="User"
+						/>
+					</div>
+				</div>
+
+				<div class="messages-spacer"></div>
+			</div>
+
+			<div class="input-container">
+				<!-- 工具和模型选择行 -->
+				<div class="selection-row">
+					<!-- 工具选择 -->
+					<div class="tool-selector">
+						<el-cascader
+							v-model="selectedTool"
+							:options="toolOptions"
+							:show-all-levels="false"
+							:props="{ multiple: true }"
+							collapse-tags
+							collapse-tags-tooltip
+							clearable
+							size="small"
+							style="width: 200px"
+						/>
+					</div>
+
+					<!-- 模型选择 -->
+					<div class="model-selector">
+						<el-select
+							v-model="selectedModel"
+							placeholder="选择模型"
+							size="small"
+							style="width: 200px"
+						>
+							<el-option
+								v-for="item in modelOptions"
+								:key="item.value"
+								:label="item.label"
+								:value="item.value"
+							/>
+						</el-select>
+					</div>
+				</div>
+
+				<!-- 输入框和按钮行 -->
+				<div class="input-row">
+					<!-- 输入框 -->
+					<div class="message-input-wrapper">
+						<el-input
+							v-model="inputMessage"
+							type="textarea"
+							placeholder="请输入您的问题..."
+							:rows="2"
+							resize="none"
+							@keydown.enter.ctrl="sendMessage"
+							@keydown.enter.meta="sendMessage"
+						/>
+					</div>
+
+					<!-- 按钮组 -->
+					<div class="button-group">
+						<el-button
+							v-if="!isConversationActive"
+							type="primary"
+							size="small"
+							@click="sendMessage"
+							:disabled="!inputMessage.trim()"
+						>
+							发送
+						</el-button>
+						<el-button
+							v-else
+							type="danger"
+							size="small"
+							@click="stopConversation"
+						>
+							终止
+						</el-button>
+					</div>
+				</div>
+			</div>
+		</el-main>
+	</el-container>
+</template>
+
+<style scoped lang="scss">
+.chat-container {
+	height: 100%;
+	background: var(--el-bg-color-page);
+}
+/* 左侧边栏样式 */
+.chat-sidebar {
+	background: var(--el-bg-color);
+	border-right: 1px solid var(--el-border-color-light);
+	display: flex;
+	flex-direction: column;
+}
+
+.sidebar-header {
+	padding: 20px;
+	border-bottom: 1px solid var(--el-border-color-light);
+
+	h3 {
+		margin: 0;
+		color: var(--el-text-color-primary);
+		font-size: 16px;
+		font-weight: 600;
+	}
+}
+
+.conversation-list {
+	flex: 1;
+	padding: 10px;
+}
+
+.conversation-item {
+	padding: 12px 16px;
+	margin-bottom: 8px;
+	border-radius: 8px;
+	cursor: pointer;
+	transition: all 0.2s;
+	color: var(--el-text-color-regular);
+	border: 1px solid transparent;
+
+	&:hover {
+		background: var(--el-fill-color-light);
+		color: var(--el-text-color-primary);
+	}
+
+	&.active {
+		background: var(--el-color-primary-light-9);
+		color: var(--el-color-primary);
+		border-color: var(--el-color-primary-light-7);
+	}
+}
+
+/* 右侧聊天区域样式 */
+.chat-main {
+	display: flex;
+	flex-direction: column;
+	padding: 0;
+	background: var(--el-bg-color-page);
+
+	position: relative;
+}
+
+.messages-container {
+	flex: 1;
+	padding: 20px;
+	overflow-y: auto;
+}
+
+.messages-spacer {
+	height: 160px;
+}
+
+.message-wrapper {
+	margin-bottom: 20px;
+
+	&:last-child {
+		margin-bottom: 0;
+	}
+}
+
+/* AI消息样式 */
+.ai-message-container {
+	display: flex;
+	align-items: flex-start;
+	gap: 12px;
+}
+
+.ai-bubble {
+	background: #f3f4f6;
+	color: var(--el-text-color-primary);
+	position: relative;
+	max-width: 70%;
+
+	&::before {
+		content: '';
+		position: absolute;
+		left: -8px;
+		top: 12px;
+		width: 0;
+		height: 0;
+		border-right: 8px solid #f3f4f6;
+		border-top: 8px solid transparent;
+		border-bottom: 8px solid transparent;
+	}
+}
+
+/* 用户消息样式 */
+.user-message-container {
+	display: flex;
+	align-items: flex-start;
+	gap: 12px;
+	justify-content: flex-end;
+}
+
+.user-bubble {
+	background: #4ade80;
+	color: white;
+	position: relative;
+	max-width: 70%;
+
+	&::after {
+		content: '';
+		position: absolute;
+		right: -8px;
+		top: 12px;
+		width: 0;
+		height: 0;
+		border-left: 8px solid #4ade80;
+		border-top: 8px solid transparent;
+		border-bottom: 8px solid transparent;
+	}
+}
+
+/* 消息气泡通用样式 */
+.message-bubble {
+	padding: 12px 16px;
+	border-radius: 12px;
+	line-height: 1.5;
+	word-wrap: break-word;
+	box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.message-avatar {
+	flex-shrink: 0;
+	margin-top: 2px;
+}
+
+.tools-button {
+	margin-bottom: 10px;
+	color: var(--el-text-color-regular);
+}
+
+.message-input {
+	margin-bottom: 12px;
+
+	:deep(.el-textarea__inner) {
+		border-radius: 8px;
+		resize: none;
+	}
+}
+
+.input-actions {
+	display: flex;
+	justify-content: flex-end;
+	gap: 12px;
+}
+
+/* Markdown 内容样式优化 */
+:deep(.markdown-content) {
+	h2 {
+		color: var(--el-text-color-primary);
+		margin: 16px 0 12px 0;
+		font-size: 18px;
+		font-weight: 600;
+	}
+
+	ul, ol {
+		margin: 12px 0;
+		padding-left: 20px;
+	}
+
+	li {
+		margin: 4px 0;
+		color: var(--el-text-color-regular);
+	}
+
+	pre {
+		background: rgba(0, 0, 0, 0.05);
+		border-radius: 6px;
+		margin: 12px 0;
+	}
+
+	code {
+		background: rgba(0, 0, 0, 0.05);
+		padding: 2px 6px;
+		border-radius: 4px;
+		font-size: 14px;
+	}
+
+	p {
+		margin: 8px 0;
+		color: var(--el-text-color-regular);
+	}
+}
+
+.input-container {
+	position: absolute;
+	width: 85%;
+	left: 50%;
+	bottom: 25px;
+	transform: translate(-50%, 0);
+	background: var(--el-bg-color);
+	border: 1px solid var(--el-border-color-light);
+	border-radius: 12px;
+	padding: 16px;
+	box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+	display: flex;
+	flex-direction: column;
+	gap: 12px;
+}
+
+.selection-row {
+	display: flex;
+	align-items: center;
+	gap: 12px;
+	flex-wrap: wrap;
+}
+
+.tool-selector,
+.model-selector {
+	flex-shrink: 0;
+}
+
+.input-row {
+	display: flex;
+	align-items: flex-end;
+	gap: 12px;
+}
+
+.message-input-wrapper {
+	flex: 1;
+	min-width: 0;
+}
+
+.message-input-wrapper :deep(.el-textarea__inner) {
+	border-radius: 8px;
+	resize: none;
+	font-size: 14px;
+	line-height: 1.4;
+}
+
+.button-group {
+	display: flex;
+	flex-direction: column;
+	gap: 6px;
+	flex-shrink: 0;
+}
+
+.button-group .el-button {
+	min-width: 60px;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+	.input-container {
+		width: 95%;
+		padding: 12px;
+	}
+
+	.selection-row {
+		flex-direction: column;
+		align-items: stretch;
+		gap: 8px;
+	}
+
+	.tool-selector,
+	.model-selector {
+		width: 100%;
+	}
+
+	.tool-selector :deep(.el-cascader),
+	.model-selector :deep(.el-select) {
+		width: 100% !important;
+	}
+}
+</style>

+ 53 - 1
yarn.lock

@@ -555,6 +555,11 @@
   resolved "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz"
   integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
 
+"@types/linkify-it@^5":
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
+  integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
+
 "@types/lodash-es@^4.17.6":
   version "4.17.12"
   resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz"
@@ -567,6 +572,19 @@
   resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.7.tgz"
   integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==
 
+"@types/markdown-it@^14.1.2":
+  version "14.1.2"
+  resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
+  integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
+  dependencies:
+    "@types/linkify-it" "^5"
+    "@types/mdurl" "^2"
+
+"@types/mdurl@^2":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
+  integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
+
 "@types/node@17.0.21":
   version "17.0.21"
   resolved "https://registry.npmmirror.com/@types/node/-/node-17.0.21.tgz"
@@ -1659,7 +1677,7 @@ element-resize-detector@^1.2.1:
   dependencies:
     batch-processor "1.0.0"
 
-entities@^4.5.0:
+entities@^4.4.0, entities@^4.5.0:
   version "4.5.0"
   resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz"
   integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
@@ -2629,6 +2647,13 @@ levn@^0.4.1:
     prelude-ls "^1.2.1"
     type-check "~0.4.0"
 
+linkify-it@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
+  integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
+  dependencies:
+    uc.micro "^2.0.0"
+
 loadsh@0.0.4:
   version "0.0.4"
   resolved "https://registry.npmmirror.com/loadsh/-/loadsh-0.0.4.tgz"
@@ -2673,11 +2698,28 @@ magic-string@^0.30.11, magic-string@^0.30.8:
   dependencies:
     "@jridgewell/sourcemap-codec" "^1.5.0"
 
+markdown-it@^14.1.0:
+  version "14.1.0"
+  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
+  integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
+  dependencies:
+    argparse "^2.0.1"
+    entities "^4.4.0"
+    linkify-it "^5.0.0"
+    mdurl "^2.0.0"
+    punycode.js "^2.3.1"
+    uc.micro "^2.1.0"
+
 mdn-data@2.0.30:
   version "2.0.30"
   resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.30.tgz"
   integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
 
+mdurl@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
+  integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
+
 memoize-one@^6.0.0:
   version "6.0.0"
   resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz"
@@ -2935,6 +2977,11 @@ province-city-china@8.5.7:
   dependencies:
     "@province-city-china/types" "8.5.7"
 
+punycode.js@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
+  integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
+
 punycode@^2.1.0:
   version "2.3.1"
   resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz"
@@ -3444,6 +3491,11 @@ ua-parser-js@^1.0.38:
   resolved "https://registry.npmmirror.com/ua-parser-js/-/ua-parser-js-1.0.38.tgz"
   integrity sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==
 
+uc.micro@^2.0.0, uc.micro@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
+  integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
+
 uglify-js@^2.6.2:
   version "2.8.29"
   resolved "https://registry.npmmirror.com/uglify-js/-/uglify-js-2.8.29.tgz"