新增 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>
97 lines
3.5 KiB
JavaScript
97 lines
3.5 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
function fresh() {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'peoplelib-note-assets-'));
|
|
delete require.cache[require.resolve('../reader/note-assets')];
|
|
const assets = require('../reader/note-assets');
|
|
assets.init(root);
|
|
return { root, assets };
|
|
}
|
|
|
|
function pdf(file, suffix = '') {
|
|
fs.writeFileSync(file, `%PDF-1.4\n${suffix}\n%%EOF\n`);
|
|
}
|
|
|
|
test('PDF 底版选择令牌绑定渲染进程且只能由保存流程解析', () => {
|
|
const { root, assets } = fresh();
|
|
const file = path.join(root, 'paper.pdf');
|
|
pdf(file, 'page one');
|
|
const staged = assets.stagePdf(file, 101);
|
|
assert.match(staged.token, /^[0-9a-f-]{36}$/);
|
|
assert.strictEqual(staged.name, 'paper.pdf');
|
|
assert.throws(() => assets.readDraft(staged.token, 202), /选择已失效/);
|
|
assert.match(assets.readDraft(staged.token, 101).subarray(0, 5).toString(), /^%PDF-/);
|
|
|
|
const canvasContent = {
|
|
version: 1,
|
|
pages: [{
|
|
id: 'pg_one',
|
|
width: 612,
|
|
height: 792,
|
|
background: { type: 'pdf', page: 1, draftToken: staged.token },
|
|
objects: []
|
|
}]
|
|
};
|
|
const resolved = assets.resolveDrafts(canvasContent, 101);
|
|
assert.match(resolved.content.pages[0].background.assetId, /^pdf_[a-f0-9]{64}$/);
|
|
assert.ok(!Object.prototype.hasOwnProperty.call(
|
|
resolved.content.pages[0].background,
|
|
'draftToken'
|
|
));
|
|
assert.deepStrictEqual(resolved.tokens, [staged.token]);
|
|
assets.commitTokens(resolved.tokens);
|
|
assert.throws(() => assets.readDraft(staged.token, 101), /选择已失效/);
|
|
assert.match(assets.readAsset(resolved.content.pages[0].background.assetId).toString(), /page one/);
|
|
});
|
|
|
|
test('相同 PDF 复用内容资源并按引用集合清理孤儿', () => {
|
|
const { root, assets } = fresh();
|
|
const first = path.join(root, 'first.pdf');
|
|
const copy = path.join(root, 'copy.pdf');
|
|
const other = path.join(root, 'other.pdf');
|
|
pdf(first, 'same bytes');
|
|
fs.copyFileSync(first, copy);
|
|
pdf(other, 'different');
|
|
const a = assets.stagePdf(first, 1);
|
|
const b = assets.stagePdf(copy, 1);
|
|
const c = assets.stagePdf(other, 1);
|
|
const resolve = (token) => assets.resolveDrafts({
|
|
version: 1,
|
|
pages: [{
|
|
id: 'pg_one',
|
|
width: 612,
|
|
height: 792,
|
|
background: { type: 'pdf', page: 1, draftToken: token },
|
|
objects: []
|
|
}]
|
|
}, 1);
|
|
const ar = resolve(a.token);
|
|
const br = resolve(b.token);
|
|
const cr = resolve(c.token);
|
|
assert.strictEqual(
|
|
ar.content.pages[0].background.assetId,
|
|
br.content.pages[0].background.assetId
|
|
);
|
|
assert.notStrictEqual(
|
|
ar.content.pages[0].background.assetId,
|
|
cr.content.pages[0].background.assetId
|
|
);
|
|
assets.commitTokens([...ar.tokens, ...br.tokens, ...cr.tokens]);
|
|
assert.strictEqual(assets.cleanup([ar.content.pages[0].background.assetId]), 1);
|
|
assert.doesNotThrow(() => assets.readAsset(ar.content.pages[0].background.assetId));
|
|
assert.throws(() => assets.readAsset(cr.content.pages[0].background.assetId));
|
|
});
|
|
|
|
test('PDF 底版拒绝伪造文件、非法资源 ID 和不存在资源', () => {
|
|
const { root, assets } = fresh();
|
|
const fake = path.join(root, 'fake.pdf');
|
|
fs.writeFileSync(fake, 'not a pdf');
|
|
assert.throws(() => assets.stagePdf(fake, 1), /不是有效 PDF/);
|
|
assert.throws(() => assets.readAsset('../escape'), /资源标识无效/);
|
|
assert.throws(() => assets.readAsset(`pdf_${'f'.repeat(64)}`));
|
|
});
|