Skip to content

Commit 457ae75

Browse files
committed
chore(release): v1.0.60-pre.9
1 parent 86a29f5 commit 457ae75

9 files changed

Lines changed: 202 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to the "Acopilot" extension will be documented in this file.
44

5+
## [1.0.60-pre.9] - 2026-04-01
6+
7+
### Fixed
8+
- Chat: stop bundling KaTeX font assets into the VSIX while keeping the KaTeX layout rules needed for formula rendering.
9+
- Webview: resolve built frontend scripts and styles from `frontend/dist/index.html` so hashed asset names keep loading correctly.
10+
- Build: restore normal frontend asset naming instead of forcing every emitted asset to `index.css`.
11+
512
## [1.0.60-pre.8] - 2026-04-01
613

714
### Fixed

frontend/src/utils/katexCss.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const KATEX_FONT_FACE_RULE = /@font-face\s*\{[^{}]*\}/g
2+
3+
export function stripKatexFontFaceRules(css: string): string {
4+
return css.replace(KATEX_FONT_FACE_RULE, '')
5+
}

frontend/vite.config.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,29 @@ import { defineConfig } from 'vite';
22
import vue from '@vitejs/plugin-vue';
33
import path from 'path';
44
import fs from 'fs';
5+
import { stripKatexFontFaceRules } from './src/utils/katexCss';
56

67
// 读取根目录下的 package.json
78
const packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
89

910
export default defineConfig({
10-
plugins: [vue()],
11+
plugins: [
12+
vue(),
13+
{
14+
name: 'strip-katex-font-assets',
15+
enforce: 'pre',
16+
transform(code, id) {
17+
if (!id.includes('/katex/dist/katex.min.css')) {
18+
return null;
19+
}
20+
21+
return {
22+
code: stripKatexFontFaceRules(code),
23+
map: null
24+
};
25+
}
26+
}
27+
],
1128
define: {
1229
__APP_VERSION__: JSON.stringify(packageJson.version),
1330
__APP_NAME__: JSON.stringify(packageJson.displayName || packageJson.name),
@@ -19,7 +36,7 @@ export default defineConfig({
1936
rollupOptions: {
2037
output: {
2138
entryFileNames: 'index.js',
22-
assetFileNames: 'index.css'
39+
assetFileNames: 'assets/[name]-[hash][extname]'
2340
}
2441
}
2542
},
@@ -28,4 +45,4 @@ export default defineConfig({
2845
'@': path.resolve(__dirname, 'src')
2946
}
3047
}
31-
});
48+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "acopilot",
33
"displayName": "Acopilot",
44
"description": "Acopilot 是一个AI编程助手插件,支持多模态和复杂功能。",
5-
"version": "1.0.60-pre.8",
5+
"version": "1.0.60-pre.9",
66
"publisher": "Andy963",
77
"icon": "resources/icon.png",
88
"engines": {

test/katexCssBuild.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { stripKatexFontFaceRules } from '../frontend/src/utils/katexCss'
4+
5+
describe('KaTeX CSS build helper', () => {
6+
it('removes embedded font-face rules while keeping layout rules', () => {
7+
const css = [
8+
'@font-face{font-family:KaTeX_Main;src:url(font.woff2) format("woff2")}',
9+
'.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2}',
10+
'@font-face{font-family:KaTeX_Math;src:url(math.woff2) format("woff2")}',
11+
'.katex .base{display:inline-block}',
12+
].join('')
13+
14+
const stripped = stripKatexFontFaceRules(css)
15+
16+
expect(stripped).not.toContain('@font-face')
17+
expect(stripped).toContain('.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2}')
18+
expect(stripped).toContain('.katex .base{display:inline-block}')
19+
})
20+
})

test/webviewAssetManifest.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import path from 'path';
2+
import { describe, expect, it } from 'vitest';
3+
import {
4+
parseFrontendBuildAssets,
5+
resolveFrontendAssetFsPath,
6+
} from '../webview/utils/webviewAssets';
7+
8+
describe('parseFrontendBuildAssets', () => {
9+
it('extracts the built script and stylesheet paths from vite index.html', () => {
10+
const html = `<!DOCTYPE html>
11+
<html lang="zh-CN">
12+
<head>
13+
<link rel="stylesheet" crossorigin href="/index60.css">
14+
<link rel="icon" href="/favicon.ico">
15+
</head>
16+
<body>
17+
<div id="app"></div>
18+
<script type="module" crossorigin src="/index.js"></script>
19+
</body>
20+
</html>`;
21+
22+
expect(parseFrontendBuildAssets(html)).toEqual({
23+
scriptPaths: ['index.js'],
24+
stylePaths: ['index60.css'],
25+
});
26+
});
27+
28+
it('ignores external or unsafe asset paths', () => {
29+
const html = `<!DOCTYPE html>
30+
<html>
31+
<head>
32+
<link rel="stylesheet" href="https://example.com/remote.css">
33+
<link rel="stylesheet" href="/assets/main.css?hash=123">
34+
</head>
35+
<body>
36+
<script src="//cdn.example.com/remote.js"></script>
37+
<script src="../escape.js"></script>
38+
<script src="/assets/index.js"></script>
39+
</body>
40+
</html>`;
41+
42+
expect(parseFrontendBuildAssets(html)).toEqual({
43+
scriptPaths: ['assets/index.js'],
44+
stylePaths: ['assets/main.css'],
45+
});
46+
});
47+
});
48+
49+
describe('resolveFrontendAssetFsPath', () => {
50+
it('joins normalized asset paths under the dist directory', () => {
51+
expect(resolveFrontendAssetFsPath('/tmp/frontend/dist', 'assets/index.js')).toBe(
52+
path.join('/tmp/frontend/dist', 'assets/index.js'),
53+
);
54+
});
55+
});

webview/ChatViewProvider.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { MessageRouter } from './MessageRouter';
4040
import { initializeChatBackend } from './chatBackendInitializer';
4141
import type { HandlerContext, DiffPreviewContentProvider as IDiffPreviewContentProvider } from './types';
4242
import { isRecord, parseWebviewRequest } from './protocol';
43+
import { readFrontendBuildAssets, resolveFrontendAssetFsPath } from './utils';
4344

4445
/**
4546
* Diff 预览内容提供者
@@ -455,15 +456,38 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
455456
*/
456457
private getHtmlForWebview(webview: vscode.Webview): string {
457458
const nonce = getNonce();
458-
const scriptUri = webview.asWebviewUri(
459-
vscode.Uri.file(path.join(this.context.extensionPath, 'frontend', 'dist', 'index.js'))
460-
);
461-
const styleUri = webview.asWebviewUri(
462-
vscode.Uri.file(path.join(this.context.extensionPath, 'frontend', 'dist', 'index.css'))
463-
);
459+
const distDir = path.join(this.context.extensionPath, 'frontend', 'dist');
460+
const indexHtmlPath = path.join(distDir, 'index.html');
461+
const fallbackScriptPath = path.join(distDir, 'index.js');
462+
const fallbackStylePath = path.join(distDir, 'index.css');
463+
let scriptUris = [webview.asWebviewUri(vscode.Uri.file(fallbackScriptPath))];
464+
let styleUris = [webview.asWebviewUri(vscode.Uri.file(fallbackStylePath))];
465+
466+
try {
467+
const assets = readFrontendBuildAssets(indexHtmlPath);
468+
if (assets.scriptPaths.length > 0) {
469+
scriptUris = assets.scriptPaths.map(assetPath =>
470+
webview.asWebviewUri(vscode.Uri.file(resolveFrontendAssetFsPath(distDir, assetPath)))
471+
);
472+
}
473+
if (assets.stylePaths.length > 0) {
474+
styleUris = assets.stylePaths.map(assetPath =>
475+
webview.asWebviewUri(vscode.Uri.file(resolveFrontendAssetFsPath(distDir, assetPath)))
476+
);
477+
}
478+
} catch (error) {
479+
console.warn('Failed to resolve frontend build assets from dist/index.html, using fallback asset names.', error);
480+
}
481+
464482
const codiconsUri = webview.asWebviewUri(
465483
vscode.Uri.file(path.join(this.context.extensionPath, 'resources', 'codicons', 'codicon.css'))
466484
);
485+
const styleLinks = styleUris
486+
.map(styleUri => `<link href="${styleUri}" rel="stylesheet">`)
487+
.join('\n ');
488+
const scriptTags = scriptUris
489+
.map(scriptUri => `<script nonce="${nonce}" src="${scriptUri}"></script>`)
490+
.join('\n ');
467491

468492
return `<!DOCTYPE html>
469493
<html lang="zh-CN">
@@ -472,12 +496,12 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
472496
<meta name="viewport" content="width=device-width, initial-scale=1.0">
473497
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; font-src ${webview.cspSource}; script-src 'nonce-${nonce}'; img-src ${webview.cspSource} data: blob:; media-src ${webview.cspSource} data: blob:;">
474498
<link href="${codiconsUri}" rel="stylesheet">
475-
<link href="${styleUri}" rel="stylesheet">
499+
${styleLinks}
476500
<title>Acopilot Chat</title>
477501
</head>
478502
<body>
479503
<div id="app"></div>
480-
<script nonce="${nonce}" src="${scriptUri}"></script>
504+
${scriptTags}
481505
</body>
482506
</html>`;
483507
}

webview/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
*/
44

55
export * from './WorkspaceUtils';
6+
export * from './webviewAssets';

webview/utils/webviewAssets.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
4+
export interface FrontendBuildAssets {
5+
scriptPaths: string[];
6+
stylePaths: string[];
7+
}
8+
9+
const LINK_TAG_RE = /<link\b[^>]*>/gi;
10+
const SCRIPT_TAG_RE = /<script\b[^>]*><\/script>/gi;
11+
12+
function getAttribute(tag: string, attribute: string): string | null {
13+
const match = new RegExp(`\\b${attribute}\\s*=\\s*(['"])(.*?)\\1`, 'i').exec(tag);
14+
return match?.[2] ?? null;
15+
}
16+
17+
function normalizeAssetPath(rawPath: string | null): string | null {
18+
if (!rawPath) return null;
19+
20+
const withoutQuery = rawPath.split(/[?#]/, 1)[0]?.trim();
21+
if (!withoutQuery) return null;
22+
if (/^[a-z]+:/i.test(withoutQuery) || withoutQuery.startsWith('//')) return null;
23+
24+
const normalized = withoutQuery.replace(/^\/+/, '');
25+
if (!normalized) return null;
26+
if (normalized.startsWith('../') || normalized.includes('/../')) return null;
27+
28+
return normalized;
29+
}
30+
31+
export function parseFrontendBuildAssets(indexHtml: string): FrontendBuildAssets {
32+
const scriptPaths: string[] = [];
33+
const stylePaths: string[] = [];
34+
35+
for (const tag of indexHtml.match(LINK_TAG_RE) ?? []) {
36+
const rel = getAttribute(tag, 'rel');
37+
if (rel?.toLowerCase() !== 'stylesheet') continue;
38+
39+
const normalized = normalizeAssetPath(getAttribute(tag, 'href'));
40+
if (normalized) stylePaths.push(normalized);
41+
}
42+
43+
for (const tag of indexHtml.match(SCRIPT_TAG_RE) ?? []) {
44+
const normalized = normalizeAssetPath(getAttribute(tag, 'src'));
45+
if (normalized) scriptPaths.push(normalized);
46+
}
47+
48+
return {
49+
scriptPaths: [...new Set(scriptPaths)],
50+
stylePaths: [...new Set(stylePaths)],
51+
};
52+
}
53+
54+
export function readFrontendBuildAssets(indexHtmlPath: string): FrontendBuildAssets {
55+
const indexHtml = fs.readFileSync(indexHtmlPath, 'utf-8');
56+
return parseFrontendBuildAssets(indexHtml);
57+
}
58+
59+
export function resolveFrontendAssetFsPath(distDir: string, assetPath: string): string {
60+
return path.join(distDir, assetPath);
61+
}

0 commit comments

Comments
 (0)