文档卡片按文件类型分流:新增类型识别与代码卡免读取门禁
- 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 的文档/代码双分支为文档单分支,去掉已不可达的代码读取路径
This commit is contained in:
+146
-8
@@ -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');
|
||||
}
|
||||
|
||||
+20
-5
@@ -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<ProjectResourceCardPreviewTransportPayload> => {
|
||||
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' ||
|
||||
|
||||
Reference in New Issue
Block a user