From 2c80aab62155bd4f04a080d7944048021d211126 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Tue, 15 Sep 2026 19:58:21 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E6=8C=89=E6=96=87=E4=BB=B6=E7=B1=BB=E5=9E=8B=E5=88=86=E6=B5=81?= =?UTF-8?q?=EF=BC=9A=E6=96=B0=E5=A2=9E=E7=B1=BB=E5=9E=8B=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E4=B8=8E=E4=BB=A3=E7=A0=81=E5=8D=A1=E5=85=8D=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - resourceCardPreviewModel:新增 Markdown / 代码 / 纯文本三支扩展名口径,`.json`/`.yaml`/`.toml`/`.xml`/`.html`/`.css`/`.sql` 按用户口径归入代码 - resourceCardPreviewModel:新增 projectResourceCardPreviewVariant 给出卡面分流结论,projectResourceCodeTypeLabel 给出代码卡类型标签 - resourceCardPreviewModel:纯文本(`.txt`/`.csv`/`.log` 等)在上游无类型结论时兜底为文档,改走文本预览而不是占位图标 - resourceCardPreviewModel:用按行截断的 projectResourceDocumentPreviewText 替换压成一行的 summarizeProjectResourceDocument - resourceCardPreviewModel:Markdown 轻量清理只去标记符号并保留换行,不引入完整渲染器 - useProjectResourceCardPreviews:代码卡在 requestPreview 入队门禁直接返回,不再读取文件内容(判据与卡面分流同源) - useProjectResourceCardPreviews:收窄 readPreview 的文档/代码双分支为文档单分支,去掉已不可达的代码读取路径 --- .../resourceCardPreviewModel.ts | 154 +++++++++++++++++- .../useProjectResourceCardPreviews.ts | 25 ++- 2 files changed, 166 insertions(+), 13 deletions(-) diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceCardPreviewModel.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceCardPreviewModel.ts index 006048f62..9603f4fd1 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceCardPreviewModel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceCardPreviewModel.ts @@ -198,6 +198,40 @@ const rasterImageExtension = /\.(png|jpe?g|webp)$/iu; const extendedImageExtension = /\.(gif|svg|avif|bmp)$/iu; const videoExtension = /\.(mp4|webm|mov)$/iu; +/** Markdown 文档:卡面显示前几行,并做轻量标记清理。 */ +const markdownExtension = /\.(md|markdown|mdx)$/iu; +/** + * 代码文件扩展名(**只看路径,不读文件内容**)。 + * + * 与 `resourceProjectionModel` 的 `gameCodeExtension` 是两份口径,刻意不复用: + * 那份用于**筛选与归属**,改动会波及画布栏目与计数;这份只决定**卡面怎么画**。 + * 这里按用户口径把 `.json` / `.yaml` / `.toml` / `.xml` / `.html` / `.css` / `.sql` + * 一并算代码 —— 它们在上游被登记成「文档」,但卡面当代码画更干净。 + */ +const cardCodeExtension = + /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|rs|py|go|java|kt|kts|cs|cpp|cc|cxx|c|h|hpp|swift|php|rb|lua|sh|bash|zsh|ps1|psm1|json|jsonc|ya?ml|toml|xml|html?|css|scss|less|sql|graphql|gql|vue|svelte)$/iu; +/** 纯文本:按纯文本处理,显示前几行但不做标记清理。 */ +const plainTextExtension = /\.(txt|text|csv|tsv|log|ini|conf|cfg|properties|env)$/iu; + +/** + * 取路径末段的扩展名(小写、不含点)。取不到(无扩展名)时返回 `null`。 + */ +export function projectResourcePathExtension(path: string): string | null { + const fileName = path.split(/[\\/]/u).filter(Boolean).pop() ?? ''; + const matched = /\.([a-z0-9]+)$/iu.exec(fileName.trim()); + return matched ? matched[1]!.toLowerCase() : null; +} + +/** 代码文件的类型标签(如 `.ts` → `TS`);不是代码文件时返回 `null`。 */ +export function projectResourceCodeTypeLabel(path: string): string | null { + const trimmed = path.trim(); + if (!cardCodeExtension.test(trimmed)) { + return null; + } + const extension = projectResourcePathExtension(trimmed); + return extension ? extension.toUpperCase() : null; +} + export function projectResourceCardPreviewKind( resource: ProjectResource, ): ProjectResourceCardPreviewKind { @@ -207,6 +241,14 @@ export function projectResourceCardPreviewKind( if (resource.subtype === 'agent-result') { return 'document'; } + // Markdown / 代码在扩展名这一层就分流,不再依赖上游登记类型: + // 上游把 `.json` / `.tsx` 这类文件登记成「文档」,卡面却该按代码画。 + if (markdownExtension.test(resource.path)) { + return 'document'; + } + if (cardCodeExtension.test(resource.path)) { + return 'code'; + } const kind = projectResourceDisplayKind(resource); if (kind === 'code') { return 'code'; @@ -235,9 +277,56 @@ export function projectResourceCardPreviewKind( ) { return 'media-image'; } + // 纯文本兜底:`.txt` / `.csv` / `.log` 这类文件在上游没有类型结论(判成 `null`), + // 卡面按纯文本预览比占位图标更有信息量。放在最后,不改动任何已有分支的结论。 + if (kind === null && plainTextExtension.test(resource.path)) { + return 'document'; + } return 'placeholder'; } +/** + * 文档类卡片(`document` / `code`)的卡面分流结论。 + * + * 三种落点: + * - `markdown`:显示前几行,预览前做轻量标记清理; + * - `code`:**完全不读内容**,卡面改画代码图标 + 类型标签; + * - `plain-text`:显示前几行,不做标记清理。 + */ +export type ProjectResourceCardPreviewVariant = + | 'markdown' + | 'code' + | 'plain-text' + | null; + +export function projectResourceCardPreviewVariant( + resource: ProjectResource, +): ProjectResourceCardPreviewVariant { + if (resource.subtype === 'agent-result') { + return 'markdown'; + } + if (markdownExtension.test(resource.path)) { + return 'markdown'; + } + if (cardCodeExtension.test(resource.path)) { + return 'code'; + } + return projectResourceCardPreviewKind(resource) === 'document' + ? 'plain-text' + : null; +} + +/** + * 代码卡**不做文件读取**的判据:卡面只用路径画图标,读内容既无用又占读取槽。 + * + * 与 `projectResourceCardPreviewVariant` 同源,避免"卡面不画内容、却仍在后台读内容"的分叉。 + */ +export function projectResourceCardPreviewReadsContent( + resource: ProjectResource, +): boolean { + return projectResourceCardPreviewVariant(resource) !== 'code'; +} + /** * `read_local_project_media_preview` 的线上 `category` 入参:只按文件类型分美术 / 音频两支。 * @@ -271,14 +360,63 @@ export function projectResourceCardPreviewIdentity(input: { ]); } -export function summarizeProjectResourceDocument(content: string) { +/** 卡面文档预览的最大行数(超出由 CSS 省略号截断)。 */ +export const PROJECT_RESOURCE_CARD_PREVIEW_LINE_LIMIT = 3; +/** 单行预览的最大字符数,超出截断成省略号。 */ +export const PROJECT_RESOURCE_CARD_PREVIEW_LINE_LENGTH = 72; + +/** + * Markdown 的**轻量**清理:只去掉标记符号,不是渲染器。 + * + * - 去掉围栏标记行(``` / ~~~),保留代码内容本身; + * - 去掉行首标题 `#`、引用 `>` 与列表符号(保留缩进层级); + * - 去掉强调 / 行内代码 / 图片 / 链接的标记符号,保留可见文字; + * - **不折叠换行** —— 卡面要的是"前几行",压成一行就看不出结构了。 + */ +function stripMarkdownMarkers(content: string): string { return content - .replace(/!\[[^\]]*\]\([^)]*\)/gu, ' ') + .replace(/^\s*(?:```|~~~).*$/gmu, '') + .replace(/^\s{0,3}#{1,6}\s*/gmu, '') + .replace(/^\s{0,3}>\s?/gmu, '') + .replace(/^\s*[-+*]\s+/gmu, '') + .replace(/!\[([^\]]*)\]\([^)]*\)/gu, '$1') .replace(/\[([^\]]+)\]\([^)]*\)/gu, '$1') - .replace(/<[^>]*>/gu, ' ') - .replace(/[`*_~>#|{}[\]]/gu, ' ') - .replace(/^\s*[-+]\s+/gmu, '') - .replace(/\s+/gu, ' ') - .trim() - .slice(0, 180); + .replace(/`{1,3}([^`]*)`{1,3}/gu, '$1') + .replace(/\*\*([^*]+)\*\*/gu, '$1') + .replace(/__([^_]+)__/gu, '$1') + .replace(/(^|[^*])\*([^*\n]+)\*/gu, '$1$2') + .replace(/(^|[^_])_([^_\n]+)_/gu, '$1$2'); +} + +/** + * 文档卡面的前几行预览文本。 + * + * `isMarkdown` 为 `true` 时先做轻量标记清理;纯文本原样输出。 + * 逐行去掉多余空白(缩进保留最多 2 个空格)并按 `PROJECT_RESOURCE_CARD_PREVIEW_LINE_LENGTH` + * 单行截断,最多取 `PROJECT_RESOURCE_CARD_PREVIEW_LINE_LIMIT` 行。 + */ +export function projectResourceDocumentPreviewText( + content: string, + isMarkdown: boolean, +): string { + const normalized = isMarkdown ? stripMarkdownMarkers(content) : content; + const lines: string[] = []; + for (const rawLine of normalized.split(/\r?\n/u)) { + const line = rawLine.replace(/\t/gu, ' ').replace(/\s+$/u, ''); + const trimmed = line.trimStart(); + if (!trimmed) { + continue; + } + const indent = line.slice(0, line.length - trimmed.length).slice(0, 2); + const text = `${indent}${trimmed}`; + lines.push( + text.length > PROJECT_RESOURCE_CARD_PREVIEW_LINE_LENGTH + ? `${text.slice(0, PROJECT_RESOURCE_CARD_PREVIEW_LINE_LENGTH)}…` + : text, + ); + if (lines.length >= PROJECT_RESOURCE_CARD_PREVIEW_LINE_LIMIT) { + break; + } + } + return lines.join('\n'); } diff --git a/apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts b/apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts index 94d17a065..ab4e899f7 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts @@ -26,6 +26,7 @@ import { projectResourceCardPreviewImageDimensions, type ProjectResourceCardPreviewKind, projectResourceCardPreviewKind, + projectResourceCardPreviewReadsContent, type ProjectResourceCardPreviewPayload, type ProjectResourceCardPreviewState, type ProjectResourceCardPreviewTransportPayload, @@ -488,10 +489,12 @@ export function useProjectResourceCardPreviews(input: { job: PreviewJob, ): Promise => { const kind = projectResourceCardPreviewKind(job.resource); - if ( - (kind === 'document' || kind === 'code') && - job.resource.content !== undefined - ) { + /** + * 这段之前的 `kind === 'document' || kind === 'code'` 双分支已收窄:代码卡在 + * `requestPreview` 的入队门禁就被挡下(**判据同源**,见 `projectResourceCardPreviewReadsContent`), + * 走到这里的只可能是需要读内容的文档卡(Markdown / 纯文本)。 + */ + if (job.resource.content !== undefined) { return { path: job.resource.path, mediaType: job.resource.mediaType, @@ -502,7 +505,7 @@ export function useProjectResourceCardPreviews(input: { const invoke = window.__TAURI__?.core?.invoke; if (!invoke) { throw new Error( - kind === 'document' || kind === 'code' + kind === 'document' ? '文档预览需要在客户端内打开' : '媒体预览需要在客户端内打开', ); @@ -630,6 +633,18 @@ export function useProjectResourceCardPreviews(input: { return; } const kind = projectResourceCardPreviewKind(resource); + /** + * 代码卡**不入队列、不读内容**:卡面只用路径画图标 + 类型标签,读回来的内容没有任何 + * 消费方。此前代码卡与文档卡走同一条读取分支,读回一大段源码再被折成一行摘要 —— + * 既是"丑预览"的来源,也白占全局 3 个读取槽。 + * + * 判据与卡面分流(`projectResourceCardPreviewVariant`)同源,避免"卡面不画内容、 + * 后台仍在读内容"的分叉。这里直接 `return` 而不是入队后失败:卡停在 `idle`, + * 卡面照常画代码图标,不产生误导性的失败态。 + */ + if (!projectResourceCardPreviewReadsContent(resource)) { + return; + } if ( kind === 'version' || kind === 'placeholder' || From 608c3e879016667aefa4b77717fc8fb2e7a47cdd Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Tue, 15 Sep 2026 19:58:25 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E8=B5=84=E6=BA=90=E5=8D=A1=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=88=86=E5=8C=BA=EF=BC=9AMarkdown/=E7=BA=AF=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E6=98=BE=E7=A4=BA=E5=89=8D=E5=87=A0=E8=A1=8C=EF=BC=8C?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=96=87=E4=BB=B6=E6=94=B9=E7=94=BB=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index.tsx:卡面据 projectResourceCardPreviewVariant 分流,代码卡分支只渲染 lucide 代码图标 + 扩展名标签,不再显示文件内容 - index.tsx:文档卡改用 projectResourceDocumentPreviewText 取前 3 行,并带上 data-document-preview-variant 便于排障与验收 - index.tsx:引入 FileCode2 图标 - ResourcePreviewMedia.tsx:弹窗宿主里代码文件同样改画代码图标,避免看起来像"读不出来的图片" - styles.css:文档卡预览保留换行并限制 3 行(white-space: pre-line + -webkit-line-clamp: 3) - styles.css:新增 .game-resource-card-code-visual / .game-resource-card-code-label,复用现有卡片色板与居中排布口径 --- apps/ai-game-creator-shell/src/styles.css | 33 +++++++++++- .../ResourcePreviewMedia.tsx | 13 ++++- .../src/view/project-development/index.tsx | 53 ++++++++++++++++--- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 5374a3cff..912935119 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -7209,9 +7209,38 @@ iframe.preview-frame { font-size: 11px; line-height: 1.55; text-align: left; - white-space: normal; + /* 文档卡预览改为"前几行"(最多 3 行),必须保留换行:折成一行就看不出结构了。 */ + white-space: pre-line; + word-break: break-word; -webkit-box-orient: vertical; - -webkit-line-clamp: 5; + -webkit-line-clamp: 3; +} + +/* + * 代码文件卡:不显示文件内容,只给代码图标 + 扩展名标签。 + * 与 `.game-resource-card-version-visual` 共用同一套居中排布口径,视觉上仍是同一族卡片。 + */ +.game-resource-card-code-visual { + display: grid; + gap: 8px; + width: 100%; + height: 100%; + background: linear-gradient(145deg, #fff8f1, #f2ded2); + color: #8b5b45; + /* 图标与标签作为一组整体垂直居中,与 `.game-resource-card-version-visual` 同口径。 */ + align-content: center; + place-items: center; +} + +.game-resource-card-code-label { + padding: 1px 7px; + border: 1px solid rgb(139 91 69 / 28%); + border-radius: 999px; + background: rgb(255 255 255 / 72%); + color: #8b5b45; + font-size: 9px; + font-weight: 700; + letter-spacing: 0.04em; } .game-resource-card-version-visual { diff --git a/apps/ai-game-creator-shell/src/view/project-development/ResourcePreviewMedia.tsx b/apps/ai-game-creator-shell/src/view/project-development/ResourcePreviewMedia.tsx index 1da6ca17c..637ab9103 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/ResourcePreviewMedia.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/ResourcePreviewMedia.tsx @@ -1,9 +1,10 @@ -import { Image as ImageIcon, Music2, Video } from 'lucide-react'; +import { FileCode2, Image as ImageIcon, Music2, Video } from 'lucide-react'; import { useEffect, useRef } from 'react'; import { projectResourceCardPreviewKind, type ProjectResourceCardPreviewState, + projectResourceCardPreviewVariant, } from './resourceCardPreviewModel'; import type { ProjectResource } from './resourceProjectionModel'; @@ -79,6 +80,13 @@ export function ResourcePreviewMedia({ }, [onObservePreview, previewIdentity, resource]); const kind = resource ? projectResourceCardPreviewKind(resource) : null; + /** + * 代码文件走与资源卡一致的分流:不画内容(本组件本来也只画缩略图 / 占位), + * 占位图标换成代码图标,免得弹窗里代码文件看起来像"读不出来的图片"。 + */ + const previewVariant = resource + ? projectResourceCardPreviewVariant(resource) + : null; const sourceUrl = preview.status === 'loaded' ? (preview.preview.sourceUrl ?? null) : null; const visual = (() => { @@ -111,6 +119,9 @@ export function ResourcePreviewMedia({ /> ); } + if (previewVariant === 'code') { + return