feat: 内置阅读器、批注笔记与 AI 助手,发布 1.3.0
新增 PDF/EPUB/MOBI/AZW 内置阅读器,PDF 分段读取支持超大文件, 批注、读书与画布笔记、封面生成与本地导入。AI 助手支持三种协议、 图像上下文与安全 Markdown 渲染,上下文范围改为 选中/当前页/全文, 页面与全文无需选中文本即可发送,全文会提示可能超出模型限制。 便携版输出目录固定为 PeopleLib-windows-x64,不再随版本号变化, 避免升级后 data/ 被遗留在旧目录。 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
b8c8d24107
commit
3ccd044527
+151
-26
@@ -1,15 +1,86 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const ROOT = __dirname;
|
||||
const pkg = require('./package.json');
|
||||
const OUT = process.env.PEOPLELIB_OUT_DIR
|
||||
const PRODUCT = pkg.productName || 'PeopleLib';
|
||||
const TARGET = `${PRODUCT}-windows-x64`;
|
||||
const REQUESTED_OUT = process.env.PEOPLELIB_OUT_DIR
|
||||
? path.resolve(process.env.PEOPLELIB_OUT_DIR)
|
||||
: path.join(ROOT, 'dist', `PeopleLib-${pkg.version}`);
|
||||
: path.join(ROOT, 'dist', TARGET);
|
||||
const OUT = REQUESTED_OUT;
|
||||
const APP = path.join(OUT, 'resources', 'app');
|
||||
const PRODUCT = 'PeopleLib';
|
||||
|
||||
function rimraf(p) { if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true }); }
|
||||
function clearOutput(dir) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
const executable = path.join(dir, `${PRODUCT}.exe`);
|
||||
const probe = executable + '.build-lock-check';
|
||||
if (fs.existsSync(executable)) {
|
||||
try {
|
||||
fs.renameSync(executable, probe);
|
||||
fs.renameSync(probe, executable);
|
||||
} catch (error) {
|
||||
if (!fs.existsSync(executable) && fs.existsSync(probe)) {
|
||||
try { fs.renameSync(probe, executable); } catch (restoreError) { /* report the original lock error */ }
|
||||
}
|
||||
if (error && (error.code === 'EPERM' || error.code === 'EBUSY' || error.code === 'EACCES')) {
|
||||
throw new Error(`无法清理固定输出目录 ${dir},请先关闭其中正在运行的 ${PRODUCT}.exe 后重试`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === 'data') continue;
|
||||
const target = path.join(dir, entry.name);
|
||||
try {
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
if (error && (error.code === 'EPERM' || error.code === 'EBUSY')) {
|
||||
throw new Error(`无法清理固定输出目录 ${dir},请先关闭其中正在运行的 ${PRODUCT}.exe 后重试`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureElectronRuntime() {
|
||||
const electronDir = path.join(ROOT, 'node_modules', 'electron');
|
||||
const distDir = path.join(electronDir, 'dist');
|
||||
const executable = path.join(distDir, 'electron.exe');
|
||||
const versionFile = path.join(distDir, 'version');
|
||||
const expected = String(pkg.devDependencies && pkg.devDependencies.electron || '').replace(/^v/, '');
|
||||
const installed = fs.existsSync(versionFile)
|
||||
? fs.readFileSync(versionFile, 'utf8').trim().replace(/^v/, '')
|
||||
: '';
|
||||
if (installed === expected && fs.existsSync(executable)) return distDir;
|
||||
|
||||
const installer = path.join(electronDir, 'install.js');
|
||||
if (!fs.existsSync(installer)) throw new Error('缺少 Electron 安装脚本,请先运行 npm ci');
|
||||
console.log(`准备 Electron ${expected} 运行时...`);
|
||||
const result = spawnSync(process.execPath, [installer], {
|
||||
cwd: electronDir,
|
||||
env: {
|
||||
...process.env,
|
||||
ELECTRON_MIRROR: process.env.ELECTRON_MIRROR
|
||||
|| process.env.npm_config_electron_mirror
|
||||
|| 'https://npmmirror.com/mirrors/electron/'
|
||||
},
|
||||
stdio: 'inherit'
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) throw new Error(`Electron 运行时安装失败(退出码 ${result.status})`);
|
||||
|
||||
const prepared = fs.existsSync(versionFile)
|
||||
? fs.readFileSync(versionFile, 'utf8').trim().replace(/^v/, '')
|
||||
: '';
|
||||
if (prepared !== expected || !fs.existsSync(executable)) {
|
||||
throw new Error(`Electron 运行时版本无效(期望 ${expected || '未知'},实际 ${prepared || '缺失'})`);
|
||||
}
|
||||
return distDir;
|
||||
}
|
||||
|
||||
function copyDir(src, dst, skip) {
|
||||
fs.mkdirSync(dst, { recursive: true });
|
||||
for (const e of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
@@ -47,32 +118,86 @@ function copyUndici(dst) {
|
||||
copyDir(path.join(src, 'lib'), path.join(dst, 'lib'), skipDevFiles);
|
||||
}
|
||||
|
||||
console.log('清理输出目录...');
|
||||
rimraf(OUT);
|
||||
function copyFoliate(dst) {
|
||||
const src = path.join(ROOT, 'node_modules', 'foliate-js');
|
||||
fs.mkdirSync(path.join(dst, 'vendor'), { recursive: true });
|
||||
for (const name of ['package.json', 'LICENSE', 'mobi.js']) {
|
||||
fs.copyFileSync(path.join(src, name), path.join(dst, name));
|
||||
}
|
||||
fs.copyFileSync(
|
||||
path.join(src, 'vendor', 'fflate.js'),
|
||||
path.join(dst, 'vendor', 'fflate.js')
|
||||
);
|
||||
}
|
||||
|
||||
console.log('复制 Electron 运行时...');
|
||||
copyDir(path.join(ROOT, 'node_modules', 'electron', 'dist'), OUT);
|
||||
async function build() {
|
||||
const electronDist = ensureElectronRuntime();
|
||||
|
||||
console.log('精简语言包...');
|
||||
pruneLocales(path.join(OUT, 'locales'));
|
||||
console.log('清理固定输出目录(保留 data)...');
|
||||
clearOutput(OUT);
|
||||
|
||||
console.log('重命名可执行文件...');
|
||||
fs.renameSync(path.join(OUT, 'electron.exe'), path.join(OUT, PRODUCT + '.exe'));
|
||||
rimraf(path.join(OUT, 'resources', 'default_app.asar'));
|
||||
console.log('复制 Electron 运行时...');
|
||||
copyDir(electronDist, OUT);
|
||||
|
||||
console.log('组装 app 源码...');
|
||||
fs.mkdirSync(APP, { recursive: true });
|
||||
fs.copyFileSync(path.join(ROOT, 'main.js'), path.join(APP, 'main.js'));
|
||||
fs.copyFileSync(path.join(ROOT, 'preload.js'), path.join(APP, 'preload.js'));
|
||||
copyDir(path.join(ROOT, 'src'), path.join(APP, 'src'), (e) => e.name.startsWith('_test'));
|
||||
copyUndici(path.join(APP, 'node_modules', 'undici'));
|
||||
console.log('精简语言包...');
|
||||
pruneLocales(path.join(OUT, 'locales'));
|
||||
|
||||
fs.writeFileSync(path.join(APP, 'package.json'), JSON.stringify({
|
||||
name: pkg.name, version: pkg.version, description: pkg.description,
|
||||
main: 'main.js', author: pkg.author, license: pkg.license,
|
||||
dependencies: { undici: pkg.dependencies.undici }
|
||||
}, null, 2));
|
||||
console.log('重命名可执行文件...');
|
||||
const executable = path.join(OUT, PRODUCT + '.exe');
|
||||
fs.renameSync(path.join(OUT, 'electron.exe'), executable);
|
||||
rimraf(path.join(OUT, 'resources', 'default_app.asar'));
|
||||
|
||||
console.log('\n构建完成:');
|
||||
console.log(' 目录:', OUT);
|
||||
console.log(' 可执行文件:', path.join(OUT, PRODUCT + '.exe'));
|
||||
console.log(`应用 ${PRODUCT} 图标...`);
|
||||
const { rcedit } = await import('rcedit');
|
||||
await rcedit(executable, {
|
||||
icon: path.join(ROOT, 'icons', 'dist', 'book-ai-dark.ico'),
|
||||
'file-version': pkg.version,
|
||||
'product-version': pkg.version,
|
||||
'version-string': {
|
||||
ProductName: PRODUCT,
|
||||
FileDescription: PRODUCT,
|
||||
InternalName: PRODUCT,
|
||||
OriginalFilename: `${PRODUCT}.exe`
|
||||
}
|
||||
});
|
||||
|
||||
console.log('组装 app 源码...');
|
||||
fs.mkdirSync(APP, { recursive: true });
|
||||
fs.copyFileSync(path.join(ROOT, 'main.js'), path.join(APP, 'main.js'));
|
||||
fs.copyFileSync(path.join(ROOT, 'preload.js'), path.join(APP, 'preload.js'));
|
||||
copyDir(path.join(ROOT, 'src'), path.join(APP, 'src'), (e) => e.name.startsWith('_test'));
|
||||
const iconDir = path.join(APP, 'icons', 'dist');
|
||||
fs.mkdirSync(iconDir, { recursive: true });
|
||||
for (const name of ['book-ai-dark.ico', 'book-ai-light.ico']) {
|
||||
fs.copyFileSync(path.join(ROOT, 'icons', 'dist', name), path.join(iconDir, name));
|
||||
}
|
||||
for (const theme of ['dark', 'light']) {
|
||||
const themeDir = path.join(iconDir, theme);
|
||||
fs.mkdirSync(themeDir, { recursive: true });
|
||||
fs.copyFileSync(
|
||||
path.join(ROOT, 'icons', 'dist', theme, 'icon-32.png'),
|
||||
path.join(themeDir, 'icon-32.png')
|
||||
);
|
||||
}
|
||||
copyUndici(path.join(APP, 'node_modules', 'undici'));
|
||||
copyFoliate(path.join(APP, 'node_modules', 'foliate-js'));
|
||||
|
||||
fs.writeFileSync(path.join(APP, 'package.json'), JSON.stringify({
|
||||
name: pkg.name, version: pkg.version, description: pkg.description,
|
||||
productName: PRODUCT,
|
||||
main: 'main.js', author: pkg.author, license: pkg.license,
|
||||
dependencies: {
|
||||
undici: pkg.dependencies.undici,
|
||||
'foliate-js': pkg.dependencies['foliate-js']
|
||||
}
|
||||
}, null, 2));
|
||||
|
||||
console.log('\n构建完成:');
|
||||
console.log(' 目录:', OUT);
|
||||
console.log(' 可执行文件:', executable);
|
||||
}
|
||||
|
||||
build().catch((error) => {
|
||||
console.error('构建失败:', error && error.message ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user