docs(code): 补齐第二阶段前后端中文注释
This commit is contained in:
@@ -222,6 +222,6 @@ function close() { disarm(); viewer.value?.close(); svgHtml.value = ''; opener?.
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Keep 10px axis labels readable on narrow screens; the existing container scrolls. */
|
||||
/* 窄屏仍保持 10px 坐标轴文字可读,溢出由现有图表容器滚动承接。 */
|
||||
.function-plot-preview > svg, .markdown-function-plot > svg { min-width: 640px; }
|
||||
</style>
|
||||
|
||||
@@ -81,8 +81,7 @@ const router = createRouter({
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
// A benchmark can run without the workspace UI being open. Its persisted Trace
|
||||
// and permission tickets must remain reachable from the report page.
|
||||
// Benchmark 不依赖工作区界面;报告中的持久化 Trace 和待决权限入口必须仍可访问。
|
||||
const existingAgentRun = to.name === 'agent' && Boolean(to.params.runId)
|
||||
if (to.meta.requiresVault && !workspaceStore.hasVault && !existingAgentRun) {
|
||||
return { path: '/' }
|
||||
|
||||
@@ -2,6 +2,7 @@ import { apiClient } from './apiClient'
|
||||
interface RunWire { run_id: string; kind: 'rag' | 'agent'; dataset_id: string; status: string; progress: number | null; config_snapshot: Record<string, unknown>; error_code: string | null }
|
||||
export interface BenchmarkRun { id: string; kind: 'rag' | 'agent'; datasetId: string; status: string; progress: number | null; agentId?: string; errorCode: string | null }
|
||||
const map = (r: RunWire): BenchmarkRun => ({ id: r.run_id, kind: r.kind, datasetId: r.dataset_id, status: r.status, progress: r.progress, agentId: r.config_snapshot.active_agent_run_id as string | undefined, errorCode: r.error_code })
|
||||
// 保留运行配置中的 Agent Run ID,使报告页可直接进入对应 Trace 和权限处理入口。
|
||||
export const benchmarkService = {
|
||||
async datasets(kind: 'rag' | 'agent') {
|
||||
const r = await apiClient.get<{ items: { dataset_id: string; description: string; case_count: number }[] }>('/api/benchmarks/datasets', { params: { kind } })
|
||||
|
||||
@@ -11,6 +11,7 @@ interface JobWire {
|
||||
export interface ExportJob { id: string; status: JobWire['status']; warnings: string[]; error: string | null; fileName?: string }
|
||||
const mapJob = (w: JobWire): ExportJob => ({ id: w.job_id, status: w.status, warnings: w.warnings, error: w.error, fileName: w.file?.file_name })
|
||||
export type ExportPalette = Record<'page' | 'surface' | 'text' | 'muted' | 'code' | 'border' | 'accent', string>
|
||||
// 冻结导出开始时的主题颜色,避免后台兼容渲染受到后续主题切换影响。
|
||||
export function captureExportPalette(): ExportPalette | undefined {
|
||||
const style = getComputedStyle(document.documentElement)
|
||||
const tokens = { page:'background-primary', surface:'surface-primary', text:'text-primary', muted:'text-secondary', code:'background-secondary', border:'border-default', accent:'accent-primary' }
|
||||
@@ -20,8 +21,7 @@ export function captureExportPalette(): ExportPalette | undefined {
|
||||
if (/^#[0-9a-f]{3}$/i.test(value)) return [key, '#' + [...value.slice(1)].map(c => c+c).join('')]
|
||||
const rgb = value.match(/^rgb\(\s*(\d+)[, ]+\s*(\d+)[, ]+\s*(\d+)\s*\)$/)
|
||||
if (rgb) return [key, '#' + rgb.slice(1,4).map(v => Number(v).toString(16).padStart(2,'0')).join('')]
|
||||
// Resolve named colors, color-mix/OKLCH and alpha through the browser's
|
||||
// color implementation before freezing a portable RGB palette.
|
||||
// 借助浏览器解析命名色、color-mix、OKLCH 和透明色,再冻结为可移植 RGB 色板。
|
||||
if (typeof CSS !== 'undefined' && CSS.supports('color', value)) {
|
||||
const canvas = document.createElement('canvas'); canvas.width = canvas.height = 1
|
||||
const context = canvas.getContext('2d')
|
||||
@@ -37,6 +37,7 @@ export function captureExportPalette(): ExportPalette | undefined {
|
||||
return entries.every(([,value]) => value) ? Object.fromEntries(entries) as ExportPalette : undefined
|
||||
}
|
||||
export async function rasterize(svg: string, signal?: AbortSignal, unlimited = false, background = '#ffffff'): Promise<string> {
|
||||
// 非 PDF 格式保留像素预算和解码超时;PDF 的自包含快照解除资源配额。
|
||||
const doc = new DOMParser().parseFromString(svg, 'image/svg+xml')
|
||||
const root = doc.documentElement
|
||||
const box = root.getAttribute('viewBox')?.split(/[ ,]+/).map(Number)
|
||||
@@ -86,8 +87,7 @@ export const exportService = {
|
||||
assets.push({ kind: 'mermaid', source_hash: await hashSource(source), png_base64: await rasterize(result.svg, signal, pdf, pdf ? options.palette?.surface ?? (['dark','midnight-purple'].includes(options.theme_id) ? '#161b22' : '#ffffff') : '#ffffff') })
|
||||
}
|
||||
signal?.throwIfAborted()
|
||||
// Keep the response handle when cancellation arrives during submission:
|
||||
// aborting HTTP alone could leave an undiscoverable running server job.
|
||||
// 提交期间收到取消时仍等待服务器返回任务句柄;只中断 HTTP 会遗留无法追踪的后台任务。
|
||||
const job = mapJob(await apiClient.post<JobWire>('/api/exports', { source: { type: 'markdown', markdown, file_path: filePath }, title, format, options, assets, ...(printHtml ? { print_html:printHtml } : {}) }))
|
||||
if (signal?.aborted) {
|
||||
await apiClient.post(`/api/exports/${encodeURIComponent(job.id)}/cancel`)
|
||||
|
||||
@@ -8,9 +8,11 @@ interface PlotWire {
|
||||
}
|
||||
const cache = new Map<string, Promise<{ svg: string; warnings: string[]; nodeCount: number }>>()
|
||||
export function renderFunctionPlot(source: string, themeId = 'light') {
|
||||
// 缓存 Promise 既合并并发的相同请求,也避免重复渲染;失败结果立即移除以允许重试。
|
||||
const key = JSON.stringify([source, themeId])
|
||||
if (cache.has(key)) return cache.get(key)!
|
||||
const result = apiClient.post<PlotWire>('/api/plots/function', { source, theme_id: themeId }, { timeoutMs: 30000 }).then(wire => ({
|
||||
// 后端只生成静态 SVG,前端仍在渲染边界执行净化,防止未来响应扩展引入可执行标记。
|
||||
svg: DOMPurify.sanitize(wire.result?.content ?? '', { USE_PROFILES: { svg: true } }),
|
||||
warnings: [...wire.diagnostics.map(d => `${d.message}${d.line ? ` (行 ${d.line})` : ''}`), ...(wire.result?.warnings ?? [])],
|
||||
nodeCount: wire.node_count,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { mermaidThemeVariables } from './mermaidService'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { useMarkdownPreferencesStore } from '@/stores/markdownPreferences'
|
||||
import { useHeadingAppearanceStore } from '@/stores/headingAppearance'
|
||||
// Load the same CSS, including Vue's scoped editor rules, without mounting an editor.
|
||||
// 仅加载编辑器及 Markdown 组件的样式,包括 Vue scoped 规则,不额外挂载编辑器实例。
|
||||
import MarkdownContent from '@/components/common/MarkdownContent.vue'
|
||||
import VisualMarkdownEditor from '@/features/editor/VisualMarkdownEditor.vue'
|
||||
void MarkdownContent; void VisualMarkdownEditor
|
||||
@@ -39,6 +39,7 @@ async function dataUrl(url: string, signal?:AbortSignal):Promise<string> {
|
||||
return await new Promise((resolve,reject)=>{const reader=new FileReader();reader.onload=()=>resolve(String(reader.result));reader.onerror=reject;reader.readAsDataURL(blob)})
|
||||
}
|
||||
async function embedCss(css: string, base: string, signal?:AbortSignal) {
|
||||
// 打印进程完全离线,主题资源必须来自应用同源地址并在此转换为 data URL。
|
||||
const matches=[...css.matchAll(/url\(\s*(['"]?)(.*?)\1\s*\)/g)]
|
||||
for(const match of matches) {
|
||||
const url=match[2]!
|
||||
@@ -62,6 +63,7 @@ function stylesheetSnapshot(): {css:string;base:string}[] {
|
||||
}
|
||||
|
||||
export async function preparePdfSnapshot(markdown:string,title:string,options:Options,signal?:AbortSignal,filePath?:string):Promise<string> {
|
||||
// 在任何异步资源请求前冻结主题、排版和编辑器内容,保证产物对应点击导出时的状态。
|
||||
signal?.throwIfAborted()
|
||||
const theme=useThemeStore(), preferences={...useMarkdownPreferencesStore().normalized}, heading=useHeadingAppearanceStore()
|
||||
if(theme.currentThemeId && theme.currentThemeId!==options.theme_id)throw Error('主题在导出准备期间发生变化,请重新导出。')
|
||||
@@ -76,8 +78,9 @@ export async function preparePdfSnapshot(markdown:string,title:string,options:Op
|
||||
const metadata=splitNoteMetadata(markdown)
|
||||
const body=metadata?.body ?? markdown
|
||||
const scope=scopeAttributes(VisualMarkdownEditor)
|
||||
// Match the editor DOM and scoped styles, with read-only metadata controls.
|
||||
// 复用编辑器 DOM 与 scoped 样式;元数据只输出展示内容,不携带编辑控件。
|
||||
const metadataHtml=metadata ? `<section class="note-metadata"${scope} aria-label="${escape(t('笔记属性','Note properties'))}"><span class="metadata-caption"${scope}>${escape(t('笔记属性','Note properties'))}</span>${metadata.title ? `<h1${scope}>${escape(metadata.title)}</h1>` : ''}<div class="metadata-tags"${scope}><span class="metadata-label"${scope}>${escape(t('标签','Tags'))}</span>${metadata.tags.map(tag=>`<span class="metadata-tag"${scope}><span${scope}>${escape(tag)}</span></span>`).join('')}</div></section>` : ''
|
||||
// 仅含元数据的笔记没有正文资源,跳过请求可避免空 Markdown 触发接口的 422 校验。
|
||||
const resources:Resources=body.trim() ? await apiClient.post<Resources>('/api/exports/preview-resources',{format:'pdf',source:{type:'markdown',markdown:body,file_path:filePath},options}) : {images:[],plots:[]}
|
||||
signal?.throwIfAborted()
|
||||
const rendered=await renderMarkdown(body,{themeId:options.theme_id,theme:dark?'dark':'light',preferences,pdf:{mermaidVariables:diagramVariables,plot:async source=>{
|
||||
@@ -91,10 +94,9 @@ export async function preparePdfSnapshot(markdown:string,title:string,options:Op
|
||||
if(!resource?.data)throw Error(resource?.warnings.join('; ')||`PDF 图片无法读取:${source}`)
|
||||
image.src=resource.data
|
||||
}
|
||||
// Print all callout content and remove only interactive tools, not decoration.
|
||||
// 打印全部警告框内容,只移除交互控件,保留主题装饰。
|
||||
fragment.querySelectorAll('details').forEach(d=>d.open=true)
|
||||
// The workspace uses blockquotes for callouts. Preserve that DOM contract so
|
||||
// editor-specific theme selectors apply, including spacing and decoration.
|
||||
// 工作区使用 blockquote 表示警告框;保持相同 DOM 契约,让间距和装饰选择器继续生效。
|
||||
fragment.querySelectorAll('.markdown-callout:not(blockquote)').forEach(details=>{
|
||||
const block=fragment.createElement('blockquote')
|
||||
for(const attribute of [...details.attributes])if(attribute.name!=='open')block.setAttribute(attribute.name,attribute.value)
|
||||
|
||||
@@ -376,7 +376,7 @@ ol {
|
||||
--color-border-disabled: #eadfc4;
|
||||
}
|
||||
|
||||
/* Function plot tokens inherit all installed themes, including custom packages. */
|
||||
/* 函数图颜色继承所有已安装主题,也允许自定义主题包覆盖这些 Token。 */
|
||||
:root {
|
||||
--color-plot-background: var(--color-surface-primary);
|
||||
--color-plot-text: var(--color-text-primary);
|
||||
|
||||
@@ -146,6 +146,7 @@ export async function renderMarkdown(source: string, options?: { themeId?: strin
|
||||
|
||||
for (const code of documentNode.querySelectorAll('pre > code')) {
|
||||
const requestedLanguage = [...code.classList].find((name) => name.startsWith('language-'))?.slice(9) || 'text'
|
||||
// Mermaid 与函数图共用静态图表管线;兼容旧的 function_plot 围栏写法。
|
||||
const diagramKind = requestedLanguage.toLowerCase().split(/\s+/)[0]!.replace('function_plot','function-plot')
|
||||
if (['mermaid', 'function-plot'].includes(diagramKind) && preferences.diagrams) {
|
||||
mermaidBlocks.push({ pre: code.parentElement!, source: code.textContent ?? '', kind: diagramKind })
|
||||
@@ -173,6 +174,7 @@ export async function renderMarkdown(source: string, options?: { themeId?: strin
|
||||
let plotCount = 0, plotNodes = 0
|
||||
for (const { pre, source, kind } of mermaidBlocks) {
|
||||
try {
|
||||
// 交互预览保持数量和 AST 复杂度预算;PDF 已在隔离渲染链路中按需求解除限制。
|
||||
if (!options?.pdf && kind === 'function-plot' && ++plotCount > 16) throw new Error('函数图像数量超过 16')
|
||||
const result = kind === 'function-plot' ? (options?.pdf ? await options.pdf.plot(source) : await renderFunctionPlot(source, options?.themeId)) : await renderMermaid(source, { theme: options?.theme, mode: 'static', ...(options?.pdf ? { unlimited:true, themeVariables:options.pdf.mermaidVariables } : {}) })
|
||||
if (!options?.pdf && 'nodeCount' in result && (plotNodes += Number(result.nodeCount)) > 8000) throw new Error('函数图像累计复杂度超过 8000')
|
||||
|
||||
Reference in New Issue
Block a user