## 版本概述 发布 CloudSSH v1.3.2。 本版本建立完整的发布前质量门禁,增强服务器标签与分页、SFTP 多选批处理,并将终端选区询问 Agent 调整为可预览、可移除的上下文附件,避免未经用户补充问题就直接发送。 ## 主要变更 ### 质量门禁与可复现构建 - `test` 与 `main` 部署前执行冻结锁文件安装、Worker/前端类型检查、单元与集成测试、生产构建、Playwright 浏览器 E2E 和 axe 无障碍回归。 - 构建流程不再隐式安装依赖,并校验唯一 JS/CSS 入口,保证 Worker 内联产物可复现。 - 新增服务器分页、Agent 上下文及无障碍浏览器回归。 ### 服务器管理 - 服务器配置支持最多 10 个规范化标签,并持久化到 UserDBDO SQLite。 - 服务器列表支持标签筛选,搜索与标签变化时自动回到第一页。 - 每页固定展示 9 张服务器卡片。 ### SFTP 文件管理 - 支持普通单选、`Cmd/Ctrl` 切换选择、`Shift` 连选及全选。 - 支持批量文件下载和批量删除。 - 批量下载复用顺序下载队列,批量删除等待全部结果后统一刷新。 ### Agent 终端选区上下文 - “询问 AI 助手”不再自动发送终端选区。 - 选区作为当前标签独立的上下文附件加入输入区,支持来源和大小展示、展开预览、替换及移除。 - 用户必须补充问题后才能发送;选区被明确标记为非可信分析数据,不代表操作授权,也不能覆盖用户指令。 - 补充成功发送清理、失败保留、会话隔离和无障碍回归。 ### 文档与版本 - 根版本号升级至 `1.3.2`。 - 按 Keep a Changelog 规范更新 `CHANGELOG.md`。 - 同步更新中英文 README、测试说明和项目维护索引。 ## 验证结果 - `pnpm run typecheck`:通过。 - `pnpm test`:26 个测试文件、421 个测试用例全部通过。 - `pnpm run build:frontend`:生产前端构建及 Worker HTML 内联成功。 - `pnpm run test:e2e`:4 项 Chromium E2E 与 axe 无障碍回归全部通过。 - `git diff --check`:通过。 ## 备注 - 构建仍存在原有的 JavaScript 单包体积和动态/静态导入提示,不影响本次发布。 --------- Co-authored-by: TanXin <tanxin@example.com>
75 lines
3.1 KiB
JavaScript
75 lines
3.1 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
|
|
const rootDir = path.resolve(__dirname, '..');
|
|
const frontendDir = path.join(rootDir, 'frontend');
|
|
const distDir = path.join(frontendDir, 'dist');
|
|
const workerDir = path.join(rootDir, 'src', 'worker');
|
|
const htmlTsFile = path.join(workerDir, 'html.ts');
|
|
|
|
console.log('Building frontend...');
|
|
try {
|
|
// Dependencies are installed explicitly by developers/CI. Keeping installs out
|
|
// of the build makes production artifacts deterministic and offline-buildable.
|
|
execFileSync('pnpm', ['run', 'build'], { cwd: frontendDir, stdio: 'inherit' });
|
|
|
|
console.log('Inlining assets...');
|
|
// 2. Read dist/index.html
|
|
let html = fs.readFileSync(path.join(distDir, 'index.html'), 'utf8');
|
|
|
|
// 3. Find assets in dist/assets
|
|
const assetsDir = path.join(distDir, 'assets');
|
|
const files = fs.readdirSync(assetsDir).sort();
|
|
const jsFiles = files.filter((file) => file.endsWith('.js'));
|
|
const cssFiles = files.filter((file) => file.endsWith('.css'));
|
|
|
|
if (jsFiles.length !== 1 || cssFiles.length !== 1) {
|
|
throw new Error(
|
|
`Expected exactly one JS and one CSS bundle for Worker inlining, got ${jsFiles.length} JS and ${cssFiles.length} CSS.`
|
|
);
|
|
}
|
|
|
|
const jsContent = fs.readFileSync(path.join(assetsDir, jsFiles[0]), 'utf8');
|
|
const cssContent = fs.readFileSync(path.join(assetsDir, cssFiles[0]), 'utf8');
|
|
|
|
// 4. Inline favicon.svg as base64 data URL
|
|
const faviconPath = path.join(frontendDir, 'public', 'favicon.svg');
|
|
if (fs.existsSync(faviconPath)) {
|
|
const faviconContent = fs.readFileSync(faviconPath, 'utf8');
|
|
const faviconBase64 = Buffer.from(faviconContent).toString('base64');
|
|
html = html.replace(
|
|
/href=["']\/favicon\.svg["']/i,
|
|
`href="data:image/svg+xml;base64,${faviconBase64}"`
|
|
);
|
|
}
|
|
|
|
// 5. Inline CSS (replace <link rel="stylesheet" ...>)
|
|
html = html.replace(/<link[^>]*rel=["']stylesheet["'][^>]*href=["']\/assets\/[^"']+["'][^>]*>/i, () => `<style>${cssContent}</style>`);
|
|
html = html.replace(/<link[^>]*rel=["']stylesheet["'][^>]*href=["']\/src\/[^"']+["'][^>]*>/i, () => `<style>${cssContent}</style>`);
|
|
|
|
// 6. Inline JS (replace <script type="module" src="/src/main.ts"></script> or similar)
|
|
html = html.replace(/<script[^>]*type=["']module["'][^>]*src=["']\/assets\/[^"']+["'][^>]*><\/script>/i, () => `<script type="module">${jsContent}</script>`);
|
|
html = html.replace(/<script[^>]*type=["']module["'][^>]*src=["']\/src\/[^"']+["'][^>]*><\/script>/i, () => `<script type="module">${jsContent}</script>`);
|
|
|
|
// 7. Write to src/worker/html.ts
|
|
const escapedHtml = html
|
|
.replace(/\\/g, '\\\\')
|
|
.replace(/`/g, '\\`')
|
|
.replace(/\${/g, '\\${');
|
|
|
|
const htmlTsContent = `// Automatically generated by scripts/build-html.js. Do not edit directly.
|
|
export const HTML = \`${escapedHtml}\`;
|
|
`;
|
|
|
|
if (!fs.existsSync(workerDir)) {
|
|
fs.mkdirSync(workerDir, { recursive: true });
|
|
}
|
|
|
|
fs.writeFileSync(htmlTsFile, htmlTsContent, 'utf8');
|
|
console.log('Frontend built and inlined successfully to src/worker/html.ts!');
|
|
} catch (error) {
|
|
console.error('Failed to build and inline frontend:', error);
|
|
process.exit(1);
|
|
}
|